Flutter Engine
The Flutter Engine
Loading...
Searching...
No Matches
FlutterAndroidComponentTest.java
Go to the documentation of this file.
1package io.flutter.embedding.android;
2
3import static org.junit.Assert.assertNotNull;
4import static org.mockito.ArgumentMatchers.any;
5import static org.mockito.ArgumentMatchers.isNull;
6import static org.mockito.Mockito.doAnswer;
7import static org.mockito.Mockito.inOrder;
8import static org.mockito.Mockito.mock;
9import static org.mockito.Mockito.never;
10import static org.mockito.Mockito.spy;
11import static org.mockito.Mockito.times;
12import static org.mockito.Mockito.verify;
13import static org.mockito.Mockito.verifyNoMoreInteractions;
14import static org.mockito.Mockito.when;
15import static org.mockito.Mockito.withSettings;
16
17import android.app.Activity;
18import android.content.Context;
19import android.content.Intent;
20import android.os.Bundle;
21import androidx.annotation.NonNull;
22import androidx.annotation.Nullable;
23import androidx.lifecycle.Lifecycle;
24import androidx.test.core.app.ApplicationProvider;
25import androidx.test.ext.junit.runners.AndroidJUnit4;
26import io.flutter.embedding.engine.FlutterEngine;
27import io.flutter.embedding.engine.FlutterEngineCache;
28import io.flutter.embedding.engine.FlutterJNI;
29import io.flutter.embedding.engine.FlutterShellArgs;
30import io.flutter.embedding.engine.loader.FlutterLoader;
31import io.flutter.embedding.engine.plugins.FlutterPlugin;
32import io.flutter.embedding.engine.plugins.activity.ActivityAware;
33import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding;
34import io.flutter.embedding.engine.systemchannels.LifecycleChannel;
35import io.flutter.plugin.platform.PlatformPlugin;
36import java.util.List;
37import org.junit.Test;
38import org.junit.runner.RunWith;
39import org.mockito.ArgumentCaptor;
40import org.mockito.InOrder;
41import org.mockito.invocation.InvocationOnMock;
42import org.mockito.stubbing.Answer;
43import org.robolectric.Robolectric;
44import org.robolectric.android.controller.ActivityController;
45import org.robolectric.annotation.Config;
46
47@Config(manifest = Config.NONE)
48@RunWith(AndroidJUnit4.class)
50 private final Context ctx = ApplicationProvider.getApplicationContext();
51
52 @Test
54 // ---- Test setup ----
55 // Place a FlutterEngine in the static cache.
56 FlutterLoader mockFlutterLoader = mock(FlutterLoader.class);
57 FlutterJNI mockFlutterJni = mock(FlutterJNI.class);
58 when(mockFlutterJni.isAttached()).thenReturn(true);
59 FlutterEngine cachedEngine = spy(new FlutterEngine(ctx, mockFlutterLoader, mockFlutterJni));
60 FlutterEngineCache.getInstance().put("my_flutter_engine", cachedEngine);
61
62 // Add mock plugin.
63 FlutterPlugin mockPlugin = mock(FlutterPlugin.class);
64 cachedEngine.getPlugins().add(mockPlugin);
65
66 // Create a fake Host, which is required by the delegate.
67 FakeHost fakeHost = new FakeHost(cachedEngine);
68 fakeHost.shouldDestroyEngineWithHost = true;
69
70 // Create the real object that we're testing.
72
73 // --- Execute the behavior under test ---
74 // Push the delegate through all lifecycle methods all the way to destruction.
75 delegate.onAttach(ctx);
76
77 // Verify that the plugin is attached to the FlutterEngine.
78 ArgumentCaptor<FlutterPlugin.FlutterPluginBinding> pluginBindingCaptor =
79 ArgumentCaptor.forClass(FlutterPlugin.FlutterPluginBinding.class);
80 verify(mockPlugin, times(1)).onAttachedToEngine(pluginBindingCaptor.capture());
81 FlutterPlugin.FlutterPluginBinding binding = pluginBindingCaptor.getValue();
82 assertNotNull(binding.getApplicationContext());
83 assertNotNull(binding.getBinaryMessenger());
84 assertNotNull(binding.getTextureRegistry());
85 assertNotNull(binding.getPlatformViewRegistry());
86
87 delegate.onRestoreInstanceState(null);
88 delegate.onCreateView(null, null, null, 0, true);
89 delegate.onStart();
90 delegate.onResume();
91 delegate.onPause();
92 delegate.onStop();
93 delegate.onDestroyView();
94 delegate.onDetach();
95
96 // Verify the plugin was detached from the FlutterEngine.
97 pluginBindingCaptor = ArgumentCaptor.forClass(FlutterPlugin.FlutterPluginBinding.class);
98 verify(mockPlugin, times(1)).onDetachedFromEngine(pluginBindingCaptor.capture());
99 binding = pluginBindingCaptor.getValue();
100 assertNotNull(binding.getApplicationContext());
101 assertNotNull(binding.getBinaryMessenger());
102 assertNotNull(binding.getTextureRegistry());
103 assertNotNull(binding.getPlatformViewRegistry());
104 }
105
106 @Test
108 // ---- Test setup ----
109 // Place a FlutterEngine in the static cache.
110 FlutterLoader mockFlutterLoader = mock(FlutterLoader.class);
111 FlutterJNI mockFlutterJni = mock(FlutterJNI.class);
112 when(mockFlutterJni.isAttached()).thenReturn(true);
113 FlutterEngine cachedEngine = spy(new FlutterEngine(ctx, mockFlutterLoader, mockFlutterJni));
114 FlutterEngineCache.getInstance().put("my_flutter_engine", cachedEngine);
115
116 // Add mock plugin.
117 FlutterPlugin mockPlugin =
118 mock(FlutterPlugin.class, withSettings().extraInterfaces(ActivityAware.class));
119 ActivityAware activityAwarePlugin = (ActivityAware) mockPlugin;
120 ActivityPluginBinding.OnSaveInstanceStateListener mockSaveStateListener =
121 mock(ActivityPluginBinding.OnSaveInstanceStateListener.class);
122
123 // Add a OnSaveStateListener when the Activity plugin binding is made available.
124 doAnswer(
125 new Answer() {
126 @Override
127 public Object answer(InvocationOnMock invocation) throws Throwable {
128 ActivityPluginBinding binding =
129 (ActivityPluginBinding) invocation.getArguments()[0];
130 binding.addOnSaveStateListener(mockSaveStateListener);
131 return null;
132 }
133 })
134 .when(activityAwarePlugin)
135 .onAttachedToActivity(any(ActivityPluginBinding.class));
136
137 cachedEngine.getPlugins().add(mockPlugin);
138
139 // Create a fake Host, which is required by the delegate.
140 FlutterActivityAndFragmentDelegate.Host fakeHost = new FakeHost(cachedEngine);
141
143
144 // --- Execute the behavior under test ---
145 // Push the delegate through all lifecycle methods all the way to destruction.
146 delegate.onAttach(ctx);
147
148 // Verify plugin was given an ActivityPluginBinding.
149 ArgumentCaptor<ActivityPluginBinding> pluginBindingCaptor =
150 ArgumentCaptor.forClass(ActivityPluginBinding.class);
151 verify(activityAwarePlugin, times(1)).onAttachedToActivity(pluginBindingCaptor.capture());
152 ActivityPluginBinding binding = pluginBindingCaptor.getValue();
153 assertNotNull(binding.getActivity());
154 assertNotNull(binding.getLifecycle());
155
156 delegate.onRestoreInstanceState(null);
157
158 // Verify that after Activity creation, the plugin was allowed to restore state.
159 verify(mockSaveStateListener, times(1)).onRestoreInstanceState(isNull());
160
161 delegate.onCreateView(null, null, null, 0, true);
162 delegate.onStart();
163 delegate.onResume();
164 delegate.onPause();
165 delegate.onStop();
166 delegate.onSaveInstanceState(mock(Bundle.class));
167
168 // Verify that the plugin was allowed to save state.
169 verify(mockSaveStateListener, times(1)).onSaveInstanceState(any(Bundle.class));
170
171 delegate.onDestroyView();
172 delegate.onDetach();
173
174 // Verify that the plugin was detached from the Activity.
175 verify(activityAwarePlugin, times(1)).onDetachedFromActivity();
176 }
177
178 @Test
180 // ---- Test setup ----
181 // Place a FlutterEngine in the static cache.
182 FlutterLoader mockFlutterLoader = mock(FlutterLoader.class);
183 FlutterJNI mockFlutterJni = mock(FlutterJNI.class);
184 when(mockFlutterJni.isAttached()).thenReturn(true);
185 FlutterEngine cachedEngine = spy(new FlutterEngine(ctx, mockFlutterLoader, mockFlutterJni));
186 FlutterEngineCache.getInstance().put("my_flutter_engine", cachedEngine);
187
188 // Create a fake Host, which is required by the delegate.
189 FakeHost fakeHost = new FakeHost(cachedEngine);
190
191 // Create the real object that we're testing.
193 spy(new FlutterActivityAndFragmentDelegate(fakeHost));
194
195 // --- Execute the behavior under test ---
196 // Push the delegate through all lifecycle methods all the way to destruction.
197 delegate.onAttach(ctx);
198 delegate.onRestoreInstanceState(null);
199 delegate.onCreateView(null, null, null, 0, true);
200 delegate.onStart();
201 delegate.onResume();
202 delegate.onPause();
203 delegate.onStop();
204 delegate.onDestroyView();
205 delegate.onDetach();
206
207 verify(delegate, never()).detachFromFlutterEngine();
208 }
209
210 @Test
212 // ---- Test setup ----
213 // Place a FlutterEngine in the static cache.
214 FlutterLoader mockFlutterLoader = mock(FlutterLoader.class);
215 FlutterJNI mockFlutterJni = mock(FlutterJNI.class);
216 when(mockFlutterJni.isAttached()).thenReturn(true);
217 FlutterEngine cachedEngine = spy(new FlutterEngine(ctx, mockFlutterLoader, mockFlutterJni));
218 FlutterEngineCache.getInstance().put("my_flutter_engine", cachedEngine);
219 LifecycleChannel mockLifecycleChannel = mock(LifecycleChannel.class);
220 when(cachedEngine.getLifecycleChannel()).thenReturn(mockLifecycleChannel);
221
222 Intent intent = FlutterActivity.withCachedEngine("my_flutter_engine").build(ctx);
223 ActivityController<FlutterActivity> activityController1 =
224 Robolectric.buildActivity(FlutterActivity.class, intent);
225 activityController1.create().start().resume();
226
227 InOrder inOrder = inOrder(mockLifecycleChannel);
228 inOrder.verify(mockLifecycleChannel, times(1)).appIsResumed();
229 verifyNoMoreInteractions(mockLifecycleChannel);
230
231 activityController1.pause();
232 // Create a second instance on the same engine and start running it as well.
233 ActivityController<FlutterActivity> activityController2 =
234 Robolectric.buildActivity(FlutterActivity.class, intent);
235 activityController2.create().start().resume();
236
237 // From the onPause of the first activity.
238 inOrder.verify(mockLifecycleChannel, times(1)).appIsInactive();
239 // By creating the second activity, we should automatically detach the first activity.
240 inOrder.verify(mockLifecycleChannel, times(1)).appIsDetached();
241 // In order, the second activity then is resumed.
242 inOrder.verify(mockLifecycleChannel, times(1)).appIsResumed();
243 verifyNoMoreInteractions(mockLifecycleChannel);
244
245 // The first activity goes through the normal lifecycles of destruction, but since we
246 // detached the first activity during the second activity's creation, we should ignore the
247 // first activity's destruction events to avoid crosstalk.
248 activityController1.stop().destroy();
249 verifyNoMoreInteractions(mockLifecycleChannel);
250 }
251
252 private static class FakeHost implements FlutterActivityAndFragmentDelegate.Host {
253 final FlutterEngine cachedEngine;
254 Activity activity;
255 boolean shouldDestroyEngineWithHost = false;
256 Lifecycle lifecycle = mock(Lifecycle.class);
257
258 private FakeHost(@NonNull FlutterEngine flutterEngine) {
259 cachedEngine = flutterEngine;
260 }
261
262 @NonNull
263 @Override
264 public Context getContext() {
265 return ApplicationProvider.getApplicationContext();
266 }
267
268 @SuppressWarnings("deprecation")
269 // Robolectric.setupActivity
270 // TODO(reidbaker): https://github.com/flutter/flutter/issues/133151
271 @Nullable
272 @Override
273 public Activity getActivity() {
274 if (activity == null) {
275 activity = Robolectric.setupActivity(Activity.class);
276 }
277 return activity;
278 }
279
280 @NonNull
281 @Override
282 public Lifecycle getLifecycle() {
283 return lifecycle;
284 }
285
286 @NonNull
287 @Override
288 public FlutterShellArgs getFlutterShellArgs() {
289 return new FlutterShellArgs(new String[] {});
290 }
291
292 @Nullable
293 @Override
294 public String getCachedEngineId() {
295 return "my_flutter_engine";
296 }
297
298 @Nullable
299 @Override
300 public String getCachedEngineGroupId() {
301 return "my_flutter_engine_group";
302 }
303
304 @Override
305 public boolean shouldDestroyEngineWithHost() {
306 return shouldDestroyEngineWithHost;
307 }
308
309 @NonNull
310 @Override
311 public String getDartEntrypointFunctionName() {
312 return "main";
313 }
314
315 @Nullable
316 @Override
317 public String getDartEntrypointLibraryUri() {
318 return null;
319 }
320
321 @Nullable
322 @Override
323 public List<String> getDartEntrypointArgs() {
324 return null;
325 }
326
327 @NonNull
328 @Override
329 public String getAppBundlePath() {
330 return "/fake/path";
331 }
332
333 @Nullable
334 @Override
335 public String getInitialRoute() {
336 return "/";
337 }
338
339 @NonNull
340 @Override
341 public RenderMode getRenderMode() {
342 return RenderMode.surface;
343 }
344
345 @NonNull
346 @Override
347 public TransparencyMode getTransparencyMode() {
348 return TransparencyMode.transparent;
349 }
350
351 @Override
352 public ExclusiveAppComponent<Activity> getExclusiveAppComponent() {
353 return null;
354 }
355
356 @Nullable
357 @Override
358 public FlutterEngine provideFlutterEngine(@NonNull Context context) {
359 return cachedEngine;
360 }
361
362 @Nullable
363 @Override
364 public PlatformPlugin providePlatformPlugin(
365 @Nullable Activity activity, @NonNull FlutterEngine flutterEngine) {
366 return null;
367 }
368
369 @Override
370 public void configureFlutterEngine(@NonNull FlutterEngine flutterEngine) {}
371
372 @Override
373 public void cleanUpFlutterEngine(@NonNull FlutterEngine flutterEngine) {}
374
375 @Override
376 public boolean shouldAttachEngineToActivity() {
377 return true;
378 }
379
380 @Override
381 public boolean shouldHandleDeeplinking() {
382 return false;
383 }
384
385 @Override
386 public boolean shouldRestoreAndSaveState() {
387 return true;
388 }
389
390 @Override
391 public boolean shouldDispatchAppLifecycleState() {
392 return true;
393 }
394
395 @Override
396 public boolean attachToEngineAutomatically() {
397 return true;
398 }
399
400 @Override
401 public void onFlutterSurfaceViewCreated(@NonNull FlutterSurfaceView flutterSurfaceView) {}
402
403 @Override
404 public void onFlutterTextureViewCreated(@NonNull FlutterTextureView flutterTextureView) {}
405
406 @Override
407 public void onFlutterUiDisplayed() {}
408
409 @Override
410 public void onFlutterUiNoLongerDisplayed() {}
411
412 @Override
413 public void detachFromFlutterEngine() {}
414
415 @Override
416 public void updateSystemUiOverlays() {}
417
418 @Override
419 public boolean popSystemNavigator() {
420 return false;
421 }
422
423 @Override
424 public void setFrameworkHandlesBack(boolean frameworkHandlesBack) {}
425 }
426}
static SkISize times(const SkISize &size, float factor)
View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState, int flutterViewId, boolean shouldDelayFirstAndroidViewDraw)
static CachedEngineIntentBuilder withCachedEngine(@NonNull String cachedEngineId)