package com.example.jnicpp; import androidx.appcompat.app.AppCompatActivity; import android.content.Intent; import android.opengl.GLSurfaceView; import android.os.Bundle; import android.util.Log; import android.view.MotionEvent; import android.view.View; import android.widget.Toast; import com.example.jnicpp.bowling.BowlingCameraActivity; import com.example.jnicpp.databinding.ActivityMainBinding; import javax.microedition.khronos.egl.EGLConfig; import javax.microedition.khronos.opengles.GL10; public class MainActivity extends AppCompatActivity implements GLSurfaceView.Renderer { private static final String TAG = "MainActivity"; // Used to load the 'jnicpp' library on application startup. static { try { System.loadLibrary("jnicpp"); Log.d(TAG, "Native library loaded successfully"); } catch (UnsatisfiedLinkError e) { Log.e(TAG, "Failed to load native library", e); } } private ActivityMainBinding binding; private GLSurfaceView glSurfaceView; private boolean isInitialized = false; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); Log.d(TAG, "onCreate called"); try { binding = ActivityMainBinding.inflate(getLayoutInflater()); setContentView(binding.getRoot()); // Setup GLSurfaceView glSurfaceView = binding.glSurfaceView; glSurfaceView.setEGLContextClientVersion(3); // OpenGL ES 3.0 // Without this, launching an opaque Activity on top (e.g. // BowlingCameraActivity) destroys the GLSurfaceView's Surface, which tears // down the EGL context too. On return, a new context gets created and // initGL() runs again, but UIRenderer's one-time GL object setup // (UI::Init()'s s_initialized guard) doesn't know the old context - and its // GL handles - are gone, so it skips recreating them and the menu UI stops // drawing (leaving only the background demo triangle visible). Preserving // the context avoids that teardown/recreate cycle entirely. glSurfaceView.setPreserveEGLContextOnPause(true); glSurfaceView.setRenderer(this); glSurfaceView.setRenderMode(GLSurfaceView.RENDERMODE_CONTINUOUSLY); // Touch listener attached directly to the GLSurfaceView (rather than // overriding Activity.onTouchEvent) so MotionEvent.getX()/getY() are // guaranteed relative to this view's own bounds - the same pixel space // our GL rendering and UI hit-testing use. Activity-level touch // coordinates aren't guaranteed to line up with a child view's // coordinates in every layout, which was causing clicks to land on the // wrong button. glSurfaceView.setOnTouchListener(new View.OnTouchListener() { @Override public boolean onTouch(View v, MotionEvent event) { int action = event.getActionMasked(); boolean isDown = (action == MotionEvent.ACTION_DOWN || action == MotionEvent.ACTION_MOVE); nativeOnTouch(event.getX(), event.getY(), isDown); return true; } }); // Example of a call to a native method String message = stringFromJNI(); Log.d(TAG, "Native message: " + message); } catch (Exception e) { Log.e(TAG, "Error in onCreate", e); showToast("Error initializing app: " + e.getMessage()); } } @Override protected void onPause() { super.onPause(); Log.d(TAG, "onPause called"); if (glSurfaceView != null) { glSurfaceView.onPause(); } } @Override protected void onResume() { super.onResume(); Log.d(TAG, "onResume called"); if (glSurfaceView != null) { glSurfaceView.onResume(); } } @Override protected void onDestroy() { super.onDestroy(); Log.d(TAG, "onDestroy called"); if (isInitialized) { try { cleanupGL(); } catch (Exception e) { Log.e(TAG, "Error during cleanup", e); } } } // Helper method to show toast from any thread private void showToast(final String message) { runOnUiThread(new Runnable() { @Override public void run() { Toast.makeText(MainActivity.this, message, Toast.LENGTH_SHORT).show(); } }); } /** * Called from native code (see PlatformBridge::LaunchBowlingCamera() in * my_gl_app/PlatformBridge.cpp) when the game state machine switches * into StateID::Menu3. Invoked via JNI from the GL render thread, so * this hops to the UI thread before touching Activity APIs, same as * showToast() above. Front/back camera switching and backing out happen * on BowlingCameraActivity itself (its SWITCH CAMERA and BACK buttons). */ public void launchBowlingCamera() { runOnUiThread(new Runnable() { @Override public void run() { startActivity(new Intent(MainActivity.this, BowlingCameraActivity.class)); } }); } // GLSurfaceView.Renderer implementation @Override public void onSurfaceCreated(GL10 gl, EGLConfig config) { Log.d(TAG, "Surface created"); try { // Initialize OpenGL in native code (no longer needs surface parameter) boolean success = initGL(); if (success) { isInitialized = true; Log.d(TAG, "OpenGL ES 3.0 initialized successfully"); } else { Log.e(TAG, "Failed to initialize OpenGL ES 3.0"); showToast("Failed to initialize OpenGL ES 3.0"); } } catch (Exception e) { Log.e(TAG, "Error in onSurfaceCreated", e); showToast("Error creating surface: " + e.getMessage()); } } @Override public void onSurfaceChanged(GL10 gl, int width, int height) { Log.d(TAG, "Surface changed: " + width + "x" + height); onSurfaceResized(width, height); } @Override public void onDrawFrame(GL10 gl) { if (isInitialized) { try { renderFrame(); } catch (Exception e) { Log.e(TAG, "Error in onDrawFrame", e); } } } /** * A native method that is implemented by the 'jnicpp' native library, * which is packaged with this application. */ public native String stringFromJNI(); /** * Initialize OpenGL ES 3.0 renderer */ public native boolean initGL(); /** * Render a frame */ public native void renderFrame(); /** * Cleanup OpenGL resources */ public native void cleanupGL(); /** * Notify native code of the surface size, for UI layout/hit-testing */ public native void onSurfaceResized(int width, int height); /** * Forward a touch event to the native UI system */ public native void nativeOnTouch(float x, float y, boolean isDown); }