Add exception callbacks to EglThread

This allows EglRenderer to preserve existing behavior of
not sending any more tasks to the render thread after an
GL exception has been thrown.

Bug: b/225229697
Change-Id: I09e7cc48bf139aab4c9e147c2b24972ccd401672
Reviewed-on: https://webrtc-review.googlesource.com/c/src/+/311548
Reviewed-by: Sergey Silkin <ssilkin@webrtc.org>
Commit-Queue: Linus Nilsson <lnilsson@webrtc.org>
Cr-Commit-Position: refs/heads/main@{#40419}
diff --git a/sdk/android/api/org/webrtc/EglThread.java b/sdk/android/api/org/webrtc/EglThread.java
index 0227f2a..47f7810 100644
--- a/sdk/android/api/org/webrtc/EglThread.java
+++ b/sdk/android/api/org/webrtc/EglThread.java
@@ -12,8 +12,12 @@
 
 import android.os.Handler;
 import android.os.HandlerThread;
+import android.os.Looper;
+import android.os.Message;
+import androidx.annotation.GuardedBy;
 import androidx.annotation.Nullable;
-import androidx.annotation.VisibleForTesting;
+import java.util.ArrayList;
+import java.util.List;
 import org.webrtc.EglBase.EglConnection;
 
 /** EGL graphics thread that allows multiple clients to share the same underlying EGLContext. */
@@ -31,7 +35,8 @@
       @Nullable final EglBase.Context sharedContext, final int[] configAttributes) {
     final HandlerThread renderThread = new HandlerThread("EglThread");
     renderThread.start();
-    Handler handler = new Handler(renderThread.getLooper());
+    HandlerWithExceptionCallbacks handler =
+        new HandlerWithExceptionCallbacks(renderThread.getLooper());
 
     // Not creating the EGLContext on the thread it will be used on seems to cause issues with
     // creating window surfaces on certain devices. So keep the same legacy behavior as EglRenderer
@@ -51,12 +56,51 @@
         releaseMonitor != null ? releaseMonitor : eglThread -> true, handler, eglConnection);
   }
 
+  /**
+   * Handler that triggers callbacks when an uncaught exception happens when handling a message.
+   */
+  private static class HandlerWithExceptionCallbacks extends Handler {
+    private final Object callbackLock = new Object();
+    @GuardedBy("callbackLock") private final List<Runnable> exceptionCallbacks = new ArrayList<>();
+
+    public HandlerWithExceptionCallbacks(Looper looper) {
+      super(looper);
+    }
+
+    @Override
+    public void dispatchMessage(Message msg) {
+      try {
+        super.dispatchMessage(msg);
+      } catch (Exception e) {
+        Logging.e("EglThread", "Exception on EglThread", e);
+        synchronized (callbackLock) {
+          for (Runnable callback : exceptionCallbacks) {
+            callback.run();
+          }
+        }
+        throw e;
+      }
+    }
+
+    public void addExceptionCallback(Runnable callback) {
+      synchronized (callbackLock) {
+        exceptionCallbacks.add(callback);
+      }
+    }
+
+    public void removeExceptionCallback(Runnable callback) {
+      synchronized (callbackLock) {
+        exceptionCallbacks.remove(callback);
+      }
+    }
+  }
+
   private final ReleaseMonitor releaseMonitor;
-  private final Handler handler;
+  private final HandlerWithExceptionCallbacks handler;
   private final EglConnection eglConnection;
 
-  @VisibleForTesting
-  EglThread(ReleaseMonitor releaseMonitor, Handler handler, EglConnection eglConnection) {
+  private EglThread(ReleaseMonitor releaseMonitor, HandlerWithExceptionCallbacks handler,
+      EglConnection eglConnection) {
     this.releaseMonitor = releaseMonitor;
     this.handler = handler;
     this.eglConnection = eglConnection;
@@ -88,4 +132,18 @@
   public Handler getHandler() {
     return handler;
   }
+
+  /**
+   * Adds a callback that will be called on the EGL thread if there is an exception on the thread.
+   */
+  public void addExceptionCallback(Runnable callback) {
+    handler.addExceptionCallback(callback);
+  }
+
+  /**
+   * Removes a previously added exception callback.
+   */
+  public void removeExceptionCallback(Runnable callback) {
+    handler.removeExceptionCallback(callback);
+  }
 }