Skip to content
This repository was archived by the owner on Feb 25, 2025. It is now read-only.

Commit 20250bc

Browse files
author
Emmanuel Garcia
committed
Add lock
1 parent a4e4e78 commit 20250bc

File tree

2 files changed

+73
-38
lines changed

2 files changed

+73
-38
lines changed

shell/platform/android/io/flutter/embedding/engine/FlutterJNI.java

Lines changed: 39 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@
4242
import java.util.Locale;
4343
import java.util.Set;
4444
import java.util.concurrent.CopyOnWriteArraySet;
45+
import java.util.concurrent.atomic.AtomicReference;
4546
import java.util.concurrent.locks.ReentrantReadWriteLock;
4647

4748
/**
@@ -73,7 +74,7 @@
7374
*
7475
* <pre>{@code
7576
* // Instantiate FlutterJNI and attach to the native side.
76-
* FlutterJNI flutterJNI = new FlutterJNI();
77+
* FlutterJNI flutterJNI = new FlutterJNI(Looper.getMainLooper());
7778
* flutterJNI.attachToNative();
7879
*
7980
* // Use FlutterJNI as desired. flutterJNI.dispatchPointerDataPacket(...);
@@ -107,11 +108,13 @@ public class FlutterJNI {
107108
// platform thread and doesn't require locking.
108109
private ReentrantReadWriteLock shellHolderLock = new ReentrantReadWriteLock();
109110

110-
// Prefer using the FlutterJNI.Factory so it's easier to test.
111+
/** @param looper The main looper. Typically, Looper.getMainLooper(). */
112+
public FlutterJNI(@NonNull Looper looper) {
113+
mainLooper = looper;
114+
}
115+
111116
public FlutterJNI() {
112-
// We cache the main looper so that we can ensure calls are made on the main thread
113-
// without consistently paying the synchronization cost of getMainLooper().
114-
mainLooper = Looper.getMainLooper();
117+
this(Looper.getMainLooper());
115118
}
116119

117120
/**
@@ -1170,8 +1173,7 @@ public FlutterOverlaySurface createOverlaySurface() {
11701173
}
11711174

11721175
@SuppressWarnings("unused")
1173-
@UiThread
1174-
public void destroyOverlaySurfaces() {
1176+
public void destroyOverlaySurfaces() throws Exception {
11751177
// Normally, this method is called on the Android main thread.
11761178
//
11771179
// However, it may be called from a different thread after the thread merger lease expired
@@ -1185,14 +1187,15 @@ public void destroyOverlaySurfaces() {
11851187
// method hops to the Android main thread if necessary.
11861188
//
11871189
// See Rasterizer::Teardown in C++, and FlutterEngineCache in Java.
1188-
runOnMainThread(
1190+
runOnLooper(
11891191
() -> {
11901192
if (platformViewsController == null) {
11911193
throw new RuntimeException(
11921194
"platformViewsController must be set before attempting to destroy an overlay surface");
11931195
}
11941196
platformViewsController.destroyOverlaySurfaces();
1195-
});
1197+
},
1198+
mainLooper);
11961199
}
11971200
// ----- End Engine Lifecycle Support ----
11981201

@@ -1423,18 +1426,39 @@ private void ensureRunningOnMainThread() {
14231426
}
14241427

14251428
/**
1426-
* Causes the runnable r to be run on the Android main thread. If the current thread is different,
1427-
* then it adds the runnable to the message queue of the main thread.
1429+
* Causes the runnable r to be run on the thread associated with the given looper l. Then, it
1430+
* waits for the runnable to run.
1431+
*
1432+
* <p>If the runnable throws, then the exception is captured and rethrown in the current thread.
14281433
*
14291434
* @param r The runnable that will be executed.
1435+
* @param l The looper where the runnable is added.
14301436
*/
1431-
private void runOnMainThread(Runnable r) {
1432-
if (Looper.myLooper() == mainLooper) {
1437+
private void runOnLooper(Runnable r, Looper l) throws Exception {
1438+
if (Looper.myLooper() == l) {
14331439
r.run();
14341440
return;
14351441
}
1436-
final Handler handler = new Handler(mainLooper);
1437-
handler.post(r);
1442+
final AtomicReference<Exception> exception = new AtomicReference<>();
1443+
final Handler handler = new Handler(l);
1444+
handler.post(
1445+
() -> {
1446+
try {
1447+
r.run();
1448+
} catch (Exception e) {
1449+
exception.set(e);
1450+
}
1451+
synchronized (handler) {
1452+
handler.notify();
1453+
}
1454+
});
1455+
synchronized (handler) {
1456+
handler.wait();
1457+
}
1458+
if (exception.get() != null) {
1459+
// Rethrow the exception on the current thread.
1460+
throw exception.get();
1461+
}
14381462
}
14391463

14401464
/**

shell/platform/android/test/io/flutter/embedding/engine/FlutterJNITest.java

Lines changed: 34 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,19 @@
11
package io.flutter.embedding.engine;
22

3-
import static android.os.Looper.getMainLooper;
43
import static org.junit.Assert.assertEquals;
5-
import static org.junit.Assert.fail;
64
import static org.mockito.Mockito.mock;
75
import static org.mockito.Mockito.times;
86
import static org.mockito.Mockito.verify;
97
import static org.mockito.Mockito.when;
10-
import static org.robolectric.Shadows.shadowOf;
118

129
import android.annotation.TargetApi;
1310
import android.content.Context;
1411
import android.content.res.Configuration;
1512
import android.content.res.Resources;
13+
import android.os.Handler;
1614
import android.os.LocaleList;
15+
import android.os.Looper;
16+
import android.os.Message;
1717
import io.flutter.embedding.engine.dart.DartExecutor;
1818
import io.flutter.embedding.engine.mutatorsstack.FlutterMutatorsStack;
1919
import io.flutter.embedding.engine.renderer.FlutterUiDisplayListener;
@@ -22,7 +22,6 @@
2222
import io.flutter.plugin.platform.PlatformViewsController;
2323
import java.nio.ByteBuffer;
2424
import java.util.Locale;
25-
import java.util.concurrent.CountDownLatch;
2625
import java.util.concurrent.atomic.AtomicInteger;
2726
import java.util.concurrent.atomic.AtomicReference;
2827
import org.junit.Test;
@@ -253,33 +252,45 @@ public void invokePlatformMessageResponseCallback__wantsDirectBuffer() {
253252

254253
@Test
255254
public void destroyOverlaySurfaces__calledFromDifferentThread() throws Exception {
255+
AtomicReference<FlutterJNI> flutterJNI = new AtomicReference<>();
256256
PlatformViewsController platformViewsController = mock(PlatformViewsController.class);
257257

258-
FlutterJNI flutterJNI = new FlutterJNI();
259-
flutterJNI.setPlatformViewsController(platformViewsController);
260-
261-
AtomicReference<Exception> exception = new AtomicReference<>();
262-
263-
// --- Execute Test ---
264-
CountDownLatch latch = new CountDownLatch(1);
265-
Thread thread =
258+
Object lock = new Object();
259+
// This thread acts as the Android main thread.
260+
// This is required because the test itself runs on the main thread, and without this
261+
// workaround,
262+
// a deadlock is produced.
263+
Thread mainThreadImpostor =
266264
new Thread(
267265
() -> {
268-
try {
269-
flutterJNI.destroyOverlaySurfaces();
270-
} catch (Exception e) {
271-
exception.set(e);
266+
Looper.prepare();
267+
268+
flutterJNI.set(new FlutterJNI(Looper.myLooper()));
269+
flutterJNI.get().setPlatformViewsController(platformViewsController);
270+
271+
synchronized (lock) {
272+
lock.notify();
272273
}
273-
latch.countDown();
274+
275+
new Handler(Looper.myLooper()) {
276+
public void handleMessage(Message msg) {
277+
msg.getCallback().run();
278+
}
279+
};
280+
Looper.loop();
274281
});
275282

276-
thread.start();
277-
latch.await();
278-
shadowOf(getMainLooper()).idle();
283+
mainThreadImpostor.start();
284+
synchronized (lock) {
285+
lock.wait();
286+
}
287+
288+
// --- Execute Test ---
289+
flutterJNI.get().destroyOverlaySurfaces();
279290

280291
// --- Verify Results ---
281-
if (exception.get() != null) {
282-
fail("Unexpected exception: " + exception.get().toString());
283-
}
292+
verify(platformViewsController, times(1)).destroyOverlaySurfaces();
293+
294+
mainThreadImpostor.interrupt();
284295
}
285296
}

0 commit comments

Comments
 (0)