forked from flutter/engine
-
Notifications
You must be signed in to change notification settings - Fork 2
/
FlutterActivity.java
1459 lines (1359 loc) · 58.5 KB
/
FlutterActivity.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
// Copyright 2013 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package io.flutter.embedding.android;
import static io.flutter.embedding.android.FlutterActivityLaunchConfigs.DART_ENTRYPOINT_META_DATA_KEY;
import static io.flutter.embedding.android.FlutterActivityLaunchConfigs.DART_ENTRYPOINT_URI_META_DATA_KEY;
import static io.flutter.embedding.android.FlutterActivityLaunchConfigs.DEFAULT_BACKGROUND_MODE;
import static io.flutter.embedding.android.FlutterActivityLaunchConfigs.DEFAULT_DART_ENTRYPOINT;
import static io.flutter.embedding.android.FlutterActivityLaunchConfigs.DEFAULT_INITIAL_ROUTE;
import static io.flutter.embedding.android.FlutterActivityLaunchConfigs.EXTRA_BACKGROUND_MODE;
import static io.flutter.embedding.android.FlutterActivityLaunchConfigs.EXTRA_CACHED_ENGINE_GROUP_ID;
import static io.flutter.embedding.android.FlutterActivityLaunchConfigs.EXTRA_CACHED_ENGINE_ID;
import static io.flutter.embedding.android.FlutterActivityLaunchConfigs.EXTRA_DART_ENTRYPOINT;
import static io.flutter.embedding.android.FlutterActivityLaunchConfigs.EXTRA_DART_ENTRYPOINT_ARGS;
import static io.flutter.embedding.android.FlutterActivityLaunchConfigs.EXTRA_DESTROY_ENGINE_WITH_ACTIVITY;
import static io.flutter.embedding.android.FlutterActivityLaunchConfigs.EXTRA_ENABLE_STATE_RESTORATION;
import static io.flutter.embedding.android.FlutterActivityLaunchConfigs.EXTRA_INITIAL_ROUTE;
import static io.flutter.embedding.android.FlutterActivityLaunchConfigs.HANDLE_DEEPLINKING_META_DATA_KEY;
import static io.flutter.embedding.android.FlutterActivityLaunchConfigs.INITIAL_ROUTE_META_DATA_KEY;
import static io.flutter.embedding.android.FlutterActivityLaunchConfigs.NORMAL_THEME_META_DATA_KEY;
import static io.flutter.embedding.android.FlutterActivityLaunchConfigs.SPLASH_SCREEN_META_DATA_KEY;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.content.pm.ActivityInfo;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.content.res.Resources;
import android.graphics.Color;
import android.graphics.drawable.ColorDrawable;
import android.graphics.drawable.Drawable;
import android.os.Build;
import android.os.Bundle;
import android.view.View;
import android.view.Window;
import android.view.WindowManager;
import android.window.OnBackInvokedCallback;
import android.window.OnBackInvokedDispatcher;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import androidx.core.content.res.ResourcesCompat;
import androidx.lifecycle.Lifecycle;
import androidx.lifecycle.LifecycleOwner;
import androidx.lifecycle.LifecycleRegistry;
import io.flutter.Log;
import io.flutter.embedding.android.FlutterActivityLaunchConfigs.BackgroundMode;
import io.flutter.embedding.engine.FlutterEngine;
import io.flutter.embedding.engine.FlutterShellArgs;
import io.flutter.embedding.engine.plugins.activity.ActivityControlSurface;
import io.flutter.embedding.engine.plugins.util.GeneratedPluginRegister;
import io.flutter.plugin.platform.PlatformPlugin;
import io.flutter.util.ViewUtils;
import java.util.ArrayList;
import java.util.List;
/**
* {@code Activity} which displays a fullscreen Flutter UI.
*
* <p>{@code FlutterActivity} is the simplest and most direct way to integrate Flutter within an
* Android app.
*
* <p><strong>FlutterActivity responsibilities</strong>
*
* <p>{@code FlutterActivity} maintains the following responsibilities:
*
* <ul>
* <li>Displays an Android launch screen.
* <li>Displays a Flutter splash screen.
* <li>Configures the status bar appearance.
* <li>Chooses the Dart execution app bundle path, entrypoint and entrypoint arguments.
* <li>Chooses Flutter's initial route.
* <li>Renders {@code Activity} transparently, if desired.
* <li>Offers hooks for subclasses to provide and configure a {@link
* io.flutter.embedding.engine.FlutterEngine}.
* <li>Save and restore instance state, see {@code #shouldRestoreAndSaveState()};
* </ul>
*
* <p><strong>Dart entrypoint, entrypoint arguments, initial route, and app bundle path</strong>
*
* <p>The Dart entrypoint executed within this {@code Activity} is "main()" by default. To change
* the entrypoint that a {@code FlutterActivity} executes, subclass {@code FlutterActivity} and
* override {@link #getDartEntrypointFunctionName()}. For non-main Dart entrypoints to not be
* tree-shaken away, you need to annotate those functions with {@code @pragma('vm:entry-point')} in
* Dart.
*
* <p>The Dart entrypoint arguments will be passed as a list of string to Dart's entrypoint
* function. It can be passed using a {@link NewEngineIntentBuilder} via {@link
* NewEngineIntentBuilder#dartEntrypointArgs}.
*
* <p>The Flutter route that is initially loaded within this {@code Activity} is "/". The initial
* route may be specified explicitly by passing the name of the route as a {@code String} in {@link
* FlutterActivityLaunchConfigs#EXTRA_INITIAL_ROUTE}, e.g., "my/deep/link".
*
* <p>The initial route can each be controlled using a {@link NewEngineIntentBuilder} via {@link
* NewEngineIntentBuilder#initialRoute}.
*
* <p>The app bundle path, Dart entrypoint, Dart entrypoint arguments, and initial route can also be
* controlled in a subclass of {@code FlutterActivity} by overriding their respective methods:
*
* <ul>
* <li>{@link #getAppBundlePath()}
* <li>{@link #getDartEntrypointFunctionName()}
* <li>{@link #getDartEntrypointArgs()}
* <li>{@link #getInitialRoute()}
* </ul>
*
* <p>The Dart entrypoint and app bundle path are not supported as {@code Intent} parameters since
* your Dart library entrypoints are your private APIs and Intents are invocable by other processes.
*
* <p><strong>Using a cached FlutterEngine</strong>
*
* <p>{@code FlutterActivity} can be used with a cached {@link
* io.flutter.embedding.engine.FlutterEngine} instead of creating a new one. Use {@link
* #withCachedEngine(String)} to build a {@code FlutterActivity} {@code Intent} that is configured
* to use an existing, cached {@link io.flutter.embedding.engine.FlutterEngine}. {@link
* io.flutter.embedding.engine.FlutterEngineCache} is the cache that is used to obtain a given
* cached {@link io.flutter.embedding.engine.FlutterEngine}. You must create and put a {@link
* io.flutter.embedding.engine.FlutterEngine} into the {@link
* io.flutter.embedding.engine.FlutterEngineCache} yourself before using the {@link
* #withCachedEngine(String)} builder. An {@code IllegalStateException} will be thrown if a cached
* engine is requested but does not exist in the cache.
*
* <p>When using a cached {@link io.flutter.embedding.engine.FlutterEngine}, that {@link
* io.flutter.embedding.engine.FlutterEngine} should already be executing Dart code, which means
* that the Dart entrypoint and initial route have already been defined. Therefore, {@link
* CachedEngineIntentBuilder} does not offer configuration of these properties.
*
* <p>It is generally recommended to use a cached {@link io.flutter.embedding.engine.FlutterEngine}
* to avoid a momentary delay when initializing a new {@link
* io.flutter.embedding.engine.FlutterEngine}. The two exceptions to using a cached {@link
* FlutterEngine} are:
*
* <ul>
* <li>When {@code FlutterActivity} is the first {@code Activity} displayed by the app, because
* pre-warming a {@link io.flutter.embedding.engine.FlutterEngine} would have no impact in
* this situation.
* <li>When you are unsure when/if you will need to display a Flutter experience.
* </ul>
*
* <p>See https://flutter.dev/docs/development/add-to-app/performance for additional performance
* explorations on engine loading.
*
* <p>The following illustrates how to pre-warm and cache a {@link
* io.flutter.embedding.engine.FlutterEngine}:
*
* <pre>{@code
* // Create and pre-warm a FlutterEngine.
* FlutterEngineGroup group = new FlutterEngineGroup(context);
* FlutterEngine flutterEngine = group.createAndRunDefaultEngine(context);
* flutterEngine.getDartExecutor().executeDartEntrypoint(DartEntrypoint.createDefault());
*
* // Cache the pre-warmed FlutterEngine in the FlutterEngineCache.
* FlutterEngineCache.getInstance().put("my_engine", flutterEngine);
* }</pre>
*
* <p><strong>Alternatives to FlutterActivity</strong>
*
* <p>If Flutter is needed in a location that cannot use an {@code Activity}, consider using a
* {@link FlutterFragment}. Using a {@link FlutterFragment} requires forwarding some calls from an
* {@code Activity} to the {@link FlutterFragment}.
*
* <p>If Flutter is needed in a location that can only use a {@code View}, consider using a {@link
* FlutterView}. Using a {@link FlutterView} requires forwarding some calls from an {@code
* Activity}, as well as forwarding lifecycle calls from an {@code Activity} or a {@code Fragment}.
*
* <p><strong>Launch Screen and Splash Screen</strong>
*
* <p>{@code FlutterActivity} supports the display of an Android "launch screen", which is displayed
* while the Android application loads. It is only applicable if {@code FlutterActivity} is the
* first {@code Activity} displayed upon loading the app.
*
* <p>Prior to Flutter 2.5, {@code FlutterActivity} supported the display of a Flutter-specific
* "splash screen" that would be displayed after the launch screen passes. This has since been
* deprecated. If a launch screen is specified, it will automatically persist for as long as it
* takes Flutter to initialize and render its first frame.
*
* <p>Use Android themes to display a launch screen. Create two themes: a launch theme and a normal
* theme. In the launch theme, set {@code windowBackground} to the desired {@code Drawable} for the
* launch screen. In the normal theme, set {@code windowBackground} to any desired background color
* that should normally appear behind your Flutter content. In most cases this background color will
* never be seen, but for possible transition edge cases it is a good idea to explicitly replace the
* launch screen window background with a neutral color.
*
* <p>Do not change aspects of system chrome between a launch theme and normal theme. Either define
* both themes to be fullscreen or not, and define both themes to display the same status bar and
* navigation bar settings. To adjust system chrome once the Flutter app renders, use platform
* channels to instruct Android to do so at the appropriate time. This will avoid any jarring visual
* changes during app startup.
*
* <p>In the AndroidManifest.xml, set the theme of {@code FlutterActivity} to the defined launch
* theme. In the metadata section for {@code FlutterActivity}, defined the following reference to
* your normal theme:
*
* <p>{@code <meta-data android:name="io.flutter.embedding.android.NormalTheme"
* android:resource="@style/YourNormalTheme" /> }
*
* <p>With themes defined, and AndroidManifest.xml updated, Flutter displays the specified launch
* screen until the Android application is initialized.
*
* <p>Flutter also requires initialization time. To specify a splash screen for Flutter
* initialization, subclass {@code FlutterActivity} and override {@link #provideSplashScreen()}. See
* {@link SplashScreen} for details on implementing a splash screen.
*
* <p><strong>Alternative Activity</strong> {@link FlutterFragmentActivity} is also available, which
* is similar to {@code FlutterActivity} but it extends {@code FragmentActivity}. You should use
* {@code FlutterActivity}, if possible, but if you need a {@code FragmentActivity} then you should
* use {@link FlutterFragmentActivity}.
*/
// A number of methods in this class have the same implementation as FlutterFragmentActivity. These
// methods are duplicated for readability purposes. Be sure to replicate any change in this class in
// FlutterFragmentActivity, too.
public class FlutterActivity extends Activity
implements FlutterActivityAndFragmentDelegate.Host, LifecycleOwner {
private static final String TAG = "FlutterActivity";
/**
* The ID of the {@code FlutterView} created by this activity.
*
* <p>This ID can be used to lookup {@code FlutterView} in the Android view hierarchy. For more,
* see {@link android.view.View#findViewById}.
*/
public static final int FLUTTER_VIEW_ID = ViewUtils.generateViewId(0xF1F2);
/**
* Creates an {@link Intent} that launches a {@code FlutterActivity}, which creates a {@link
* FlutterEngine} that executes a {@code main()} Dart entrypoint, and displays the "/" route as
* Flutter's initial route.
*
* <p>Consider using the {@link #withCachedEngine(String)} {@link Intent} builder to control when
* the {@link io.flutter.embedding.engine.FlutterEngine} should be created in your application.
*
* @param launchContext The launch context. e.g. An Activity.
* @return The default intent.
*/
@NonNull
public static Intent createDefaultIntent(@NonNull Context launchContext) {
return withNewEngine().build(launchContext);
}
/**
* Creates an {@link NewEngineIntentBuilder}, which can be used to configure an {@link Intent} to
* launch a {@code FlutterActivity} that internally creates a new {@link
* io.flutter.embedding.engine.FlutterEngine} using the desired Dart entrypoint, initial route,
* etc.
*
* @return The engine intent builder.
*/
@NonNull
public static NewEngineIntentBuilder withNewEngine() {
return new NewEngineIntentBuilder(FlutterActivity.class);
}
/**
* Builder to create an {@code Intent} that launches a {@code FlutterActivity} with a new {@link
* FlutterEngine} and the desired configuration.
*/
public static class NewEngineIntentBuilder {
private final Class<? extends FlutterActivity> activityClass;
private String initialRoute = DEFAULT_INITIAL_ROUTE;
private String backgroundMode = DEFAULT_BACKGROUND_MODE;
@Nullable private List<String> dartEntrypointArgs;
/**
* Constructor that allows this {@code NewEngineIntentBuilder} to be used by subclasses of
* {@code FlutterActivity}.
*
* <p>Subclasses of {@code FlutterActivity} should provide their own static version of {@link
* #withNewEngine()}, which returns an instance of {@code NewEngineIntentBuilder} constructed
* with a {@code Class} reference to the {@code FlutterActivity} subclass, e.g.:
*
* <p>{@code return new NewEngineIntentBuilder(MyFlutterActivity.class); }
*/
public NewEngineIntentBuilder(@NonNull Class<? extends FlutterActivity> activityClass) {
this.activityClass = activityClass;
}
/**
* The initial route that a Flutter app will render in this {@link FlutterActivity}, defaults to
* "/".
*
* @param initialRoute The route.
* @return The engine intent builder.
*/
@NonNull
public NewEngineIntentBuilder initialRoute(@NonNull String initialRoute) {
this.initialRoute = initialRoute;
return this;
}
/**
* The mode of {@code FlutterActivity}'s background, either {@link BackgroundMode#opaque} or
* {@link BackgroundMode#transparent}.
*
* <p>The default background mode is {@link BackgroundMode#opaque}.
*
* <p>Choosing a background mode of {@link BackgroundMode#transparent} will configure the inner
* {@link FlutterView} of this {@code FlutterActivity} to be configured with a {@link
* FlutterTextureView} to support transparency. This choice has a non-trivial performance
* impact. A transparent background should only be used if it is necessary for the app design
* being implemented.
*
* <p>A {@code FlutterActivity} that is configured with a background mode of {@link
* BackgroundMode#transparent} must have a theme applied to it that includes the following
* property: {@code <item name="android:windowIsTranslucent">true</item>}.
*
* @param backgroundMode The background mode.
* @return The engine intent builder.
*/
@NonNull
public NewEngineIntentBuilder backgroundMode(@NonNull BackgroundMode backgroundMode) {
this.backgroundMode = backgroundMode.name();
return this;
}
/**
* The Dart entrypoint arguments will be passed as a list of string to Dart's entrypoint
* function.
*
* <p>A value of null means do not pass any arguments to Dart's entrypoint function.
*
* @param dartEntrypointArgs The Dart entrypoint arguments.
* @return The engine intent builder.
*/
@NonNull
public NewEngineIntentBuilder dartEntrypointArgs(@Nullable List<String> dartEntrypointArgs) {
this.dartEntrypointArgs = dartEntrypointArgs;
return this;
}
/**
* Creates and returns an {@link Intent} that will launch a {@code FlutterActivity} with the
* desired configuration.
*
* @param context The context. e.g. An Activity.
* @return The intent.
*/
@NonNull
public Intent build(@NonNull Context context) {
Intent intent =
new Intent(context, activityClass)
.putExtra(EXTRA_INITIAL_ROUTE, initialRoute)
.putExtra(EXTRA_BACKGROUND_MODE, backgroundMode)
.putExtra(EXTRA_DESTROY_ENGINE_WITH_ACTIVITY, true);
if (dartEntrypointArgs != null) {
intent.putExtra(EXTRA_DART_ENTRYPOINT_ARGS, new ArrayList(dartEntrypointArgs));
}
return intent;
}
}
/**
* Creates a {@link CachedEngineIntentBuilder}, which can be used to configure an {@link Intent}
* to launch a {@code FlutterActivity} that internally uses an existing {@link
* io.flutter.embedding.engine.FlutterEngine} that is cached in {@link
* io.flutter.embedding.engine.FlutterEngineCache}.
*
* @param cachedEngineId A cached engine ID.
* @return The builder.
*/
public static CachedEngineIntentBuilder withCachedEngine(@NonNull String cachedEngineId) {
return new CachedEngineIntentBuilder(FlutterActivity.class, cachedEngineId);
}
/**
* Builder to create an {@code Intent} that launches a {@code FlutterActivity} with an existing
* {@link io.flutter.embedding.engine.FlutterEngine} that is cached in {@link
* io.flutter.embedding.engine.FlutterEngineCache}.
*/
public static class CachedEngineIntentBuilder {
private final Class<? extends FlutterActivity> activityClass;
private final String cachedEngineId;
private boolean destroyEngineWithActivity = false;
private String backgroundMode = DEFAULT_BACKGROUND_MODE;
/**
* Constructor that allows this {@code CachedEngineIntentBuilder} to be used by subclasses of
* {@code FlutterActivity}.
*
* <p>Subclasses of {@code FlutterActivity} should provide their own static version of {@link
* FlutterActivity#withCachedEngine(String)}, which returns an instance of {@code
* CachedEngineIntentBuilder} constructed with a {@code Class} reference to the {@code
* FlutterActivity} subclass, e.g.:
*
* <p>{@code return new CachedEngineIntentBuilder(MyFlutterActivity.class, engineId); }
*
* @param activityClass A subclass of {@code FlutterActivity}.
* @param engineId The engine id.
*/
public CachedEngineIntentBuilder(
@NonNull Class<? extends FlutterActivity> activityClass, @NonNull String engineId) {
this.activityClass = activityClass;
this.cachedEngineId = engineId;
}
/**
* Whether the cached {@link io.flutter.embedding.engine.FlutterEngine} should be destroyed and
* removed from the cache when this {@code FlutterActivity} is destroyed.
*
* <p>The default value is {@code false}.
*
* @param destroyEngineWithActivity Whether to destroy the engine.
* @return The builder.
*/
public CachedEngineIntentBuilder destroyEngineWithActivity(boolean destroyEngineWithActivity) {
this.destroyEngineWithActivity = destroyEngineWithActivity;
return this;
}
/**
* The mode of {@code FlutterActivity}'s background, either {@link BackgroundMode#opaque} or
* {@link BackgroundMode#transparent}.
*
* <p>The default background mode is {@link BackgroundMode#opaque}.
*
* <p>Choosing a background mode of {@link BackgroundMode#transparent} will configure the inner
* {@link FlutterView} of this {@code FlutterActivity} to be configured with a {@link
* FlutterTextureView} to support transparency. This choice has a non-trivial performance
* impact. A transparent background should only be used if it is necessary for the app design
* being implemented.
*
* <p>A {@code FlutterActivity} that is configured with a background mode of {@link
* BackgroundMode#transparent} must have a theme applied to it that includes the following
* property: {@code <item name="android:windowIsTranslucent">true</item>}.
*
* @param backgroundMode The background mode
* @return The builder.
*/
@NonNull
public CachedEngineIntentBuilder backgroundMode(@NonNull BackgroundMode backgroundMode) {
this.backgroundMode = backgroundMode.name();
return this;
}
/**
* Creates and returns an {@link Intent} that will launch a {@code FlutterActivity} with the
* desired configuration.
*
* @param context The context. e.g. An Activity.
* @return The intent.
*/
@NonNull
public Intent build(@NonNull Context context) {
return new Intent(context, activityClass)
.putExtra(EXTRA_CACHED_ENGINE_ID, cachedEngineId)
.putExtra(EXTRA_DESTROY_ENGINE_WITH_ACTIVITY, destroyEngineWithActivity)
.putExtra(EXTRA_BACKGROUND_MODE, backgroundMode);
}
}
/**
* Creates a {@link NewEngineInGroupIntentBuilder}, which can be used to configure an {@link
* Intent} to launch a {@code FlutterActivity} by internally creating a FlutterEngine from an
* existing {@link io.flutter.embedding.engine.FlutterEngineGroup} cached in a specified {@link
* io.flutter.embedding.engine.FlutterEngineGroupCache}.
*
* <pre>{@code
* // Create a FlutterEngineGroup, such as in the onCreate method of the Application.
* FlutterEngineGroup engineGroup = new FlutterEngineGroup(this);
* FlutterEngineGroupCache.getInstance().put("my_cached_engine_group_id", engineGroup);
*
* // Start a FlutterActivity with the FlutterEngineGroup by creating an intent with withNewEngineInGroup
* Intent intent = FlutterActivity.withNewEngineInGroup("my_cached_engine_group_id")
* .dartEntrypoint("custom_entrypoint")
* .initialRoute("/custom/route")
* .backgroundMode(BackgroundMode.transparent)
* .build(context);
* startActivity(intent);
* }</pre>
*
* @param engineGroupId A cached engine group ID.
* @return The builder.
*/
public static NewEngineInGroupIntentBuilder withNewEngineInGroup(@NonNull String engineGroupId) {
return new NewEngineInGroupIntentBuilder(FlutterActivity.class, engineGroupId);
}
/**
* Builder to create an {@code Intent} that launches a {@code FlutterActivity} with a new {@link
* FlutterEngine} created by FlutterEngineGroup#createAndRunEngine.
*/
public static class NewEngineInGroupIntentBuilder {
private final Class<? extends FlutterActivity> activityClass;
private final String cachedEngineGroupId;
private String dartEntrypoint = DEFAULT_DART_ENTRYPOINT;
private String initialRoute = DEFAULT_INITIAL_ROUTE;
private String backgroundMode = DEFAULT_BACKGROUND_MODE;
/**
* Constructor that allows this {@code NewEngineInGroupIntentBuilder} to be used by subclasses
* of {@code FlutterActivity}.
*
* <p>Subclasses of {@code FlutterActivity} should provide their own static version of {@link
* #withNewEngineInGroup}, which returns an instance of {@code NewEngineInGroupIntentBuilder}
* constructed with a {@code Class} reference to the {@code FlutterActivity} subclass, e.g.:
*
* <p>{@code return new NewEngineInGroupIntentBuilder(MyFlutterActivity.class,
* cacheedEngineGroupId); }
*
* <pre>{@code
* // Create a FlutterEngineGroup, such as in the onCreate method of the Application.
* FlutterEngineGroup engineGroup = new FlutterEngineGroup(this);
* FlutterEngineGroupCache.getInstance().put("my_cached_engine_group_id", engineGroup);
*
* // Create a NewEngineInGroupIntentBuilder that would build an intent to start my custom FlutterActivity subclass.
* FlutterActivity.NewEngineInGroupIntentBuilder intentBuilder =
* new FlutterActivity.NewEngineInGroupIntentBuilder(
* MyFlutterActivity.class,
* app.engineGroupId);
* intentBuilder.dartEntrypoint("main")
* .initialRoute("/custom/route")
* .backgroundMode(BackgroundMode.transparent);
* startActivity(intentBuilder.build(context));
* }</pre>
*
* @param activityClass A subclass of {@code FlutterActivity}.
* @param engineGroupId The engine group id.
*/
public NewEngineInGroupIntentBuilder(
@NonNull Class<? extends FlutterActivity> activityClass, @NonNull String engineGroupId) {
this.activityClass = activityClass;
this.cachedEngineGroupId = engineGroupId;
}
/**
* The Dart entrypoint that will be executed in the newly created FlutterEngine as soon as the
* Dart snapshot is loaded. Default to "main".
*
* @param dartEntrypoint The dart entrypoint's name
* @return The engine group intent builder
*/
@NonNull
public NewEngineInGroupIntentBuilder dartEntrypoint(@NonNull String dartEntrypoint) {
this.dartEntrypoint = dartEntrypoint;
return this;
}
/**
* The initial route that a Flutter app will render in this {@link FlutterActivity}, defaults to
* "/".
*
* @param initialRoute The route.
* @return The engine group intent builder.
*/
@NonNull
public NewEngineInGroupIntentBuilder initialRoute(@NonNull String initialRoute) {
this.initialRoute = initialRoute;
return this;
}
/**
* The mode of {@code FlutterActivity}'s background, either {@link BackgroundMode#opaque} or
* {@link BackgroundMode#transparent}.
*
* <p>The default background mode is {@link BackgroundMode#opaque}.
*
* <p>Choosing a background mode of {@link BackgroundMode#transparent} will configure the inner
* {@link FlutterView} of this {@code FlutterActivity} to be configured with a {@link
* FlutterTextureView} to support transparency. This choice has a non-trivial performance
* impact. A transparent background should only be used if it is necessary for the app design
* being implemented.
*
* <p>A {@code FlutterActivity} that is configured with a background mode of {@link
* BackgroundMode#transparent} must have a theme applied to it that includes the following
* property: {@code <item name="android:windowIsTranslucent">true</item>}.
*
* @param backgroundMode The background mode.
* @return The engine group intent builder.
*/
@NonNull
public NewEngineInGroupIntentBuilder backgroundMode(@NonNull BackgroundMode backgroundMode) {
this.backgroundMode = backgroundMode.name();
return this;
}
/**
* Creates and returns an {@link Intent} that will launch a {@code FlutterActivity} with the
* desired configuration.
*
* @param context The context. e.g. An Activity.
* @return The intent.
*/
@NonNull
public Intent build(@NonNull Context context) {
return new Intent(context, activityClass)
.putExtra(EXTRA_DART_ENTRYPOINT, dartEntrypoint)
.putExtra(EXTRA_INITIAL_ROUTE, initialRoute)
.putExtra(EXTRA_CACHED_ENGINE_GROUP_ID, cachedEngineGroupId)
.putExtra(EXTRA_BACKGROUND_MODE, backgroundMode)
.putExtra(EXTRA_DESTROY_ENGINE_WITH_ACTIVITY, true);
}
}
// Delegate that runs all lifecycle and OS hook logic that is common between
// FlutterActivity and FlutterFragment. See the FlutterActivityAndFragmentDelegate
// implementation for details about why it exists.
@VisibleForTesting protected FlutterActivityAndFragmentDelegate delegate;
@NonNull private LifecycleRegistry lifecycle;
public FlutterActivity() {
lifecycle = new LifecycleRegistry(this);
}
/**
* This method exists so that JVM tests can ensure that a delegate exists without putting this
* Activity through any lifecycle events, because JVM tests cannot handle executing any lifecycle
* methods, at the time of writing this.
*
* <p>The testing infrastructure should be upgraded to make FlutterActivity tests easy to write
* while exercising real lifecycle methods. At such a time, this method should be removed.
*
* @param delegate The delegate.
*/
// TODO(mattcarroll): remove this when tests allow for it
// (https://github.com/flutter/flutter/issues/43798)
@VisibleForTesting
/* package */ void setDelegate(@NonNull FlutterActivityAndFragmentDelegate delegate) {
this.delegate = delegate;
}
/**
* Returns the Android App Component exclusively attached to {@link
* io.flutter.embedding.engine.FlutterEngine}.
*/
@Override
public ExclusiveAppComponent<Activity> getExclusiveAppComponent() {
return delegate;
}
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
switchLaunchThemeForNormalTheme();
super.onCreate(savedInstanceState);
delegate = new FlutterActivityAndFragmentDelegate(this);
delegate.onAttach(this);
delegate.onRestoreInstanceState(savedInstanceState);
lifecycle.handleLifecycleEvent(Lifecycle.Event.ON_CREATE);
registerOnBackInvokedCallback();
configureWindowForTransparency();
setContentView(createFlutterView());
configureStatusBarForFullscreenFlutterExperience();
}
/**
* Registers the callback with OnBackInvokedDispatcher to capture back navigation gestures and
* pass them to the framework.
*
* <p>This replaces the deprecated onBackPressed method override in order to support API 33's
* predictive back navigation feature.
*
* <p>The callback must be unregistered in order to prevent unpredictable behavior once outside
* the Flutter app.
*/
@VisibleForTesting
public void registerOnBackInvokedCallback() {
if (Build.VERSION.SDK_INT >= 33) {
getOnBackInvokedDispatcher()
.registerOnBackInvokedCallback(
OnBackInvokedDispatcher.PRIORITY_DEFAULT, onBackInvokedCallback);
}
}
/**
* Unregisters the callback from OnBackInvokedDispatcher.
*
* <p>This should be called when the activity is no longer in use to prevent unpredictable
* behavior such as being stuck and unable to press back.
*/
@VisibleForTesting
public void unregisterOnBackInvokedCallback() {
if (Build.VERSION.SDK_INT >= 33) {
getOnBackInvokedDispatcher().unregisterOnBackInvokedCallback(onBackInvokedCallback);
}
}
private final OnBackInvokedCallback onBackInvokedCallback =
Build.VERSION.SDK_INT >= 33
? new OnBackInvokedCallback() {
// TODO(garyq): Remove SuppressWarnings annotation. This was added to workaround
// a google3 bug where the linter is not properly running against API 33, causing
// a failure here. See b/243609613 and https://github.com/flutter/flutter/issues/111295
@SuppressWarnings("Override")
@Override
public void onBackInvoked() {
onBackPressed();
}
}
: null;
/**
* Switches themes for this {@code Activity} from the theme used to launch this {@code Activity}
* to a "normal theme" that is intended for regular {@code Activity} operation.
*
* <p>This behavior is offered so that a "launch screen" can be displayed while the application
* initially loads. To utilize this behavior in an app, do the following:
*
* <ol>
* <li>Create 2 different themes in style.xml: one theme for the launch screen and one theme for
* normal display.
* <li>In the launch screen theme, set the "windowBackground" property to a {@code Drawable} of
* your choice.
* <li>In the normal theme, customize however you'd like.
* <li>In the AndroidManifest.xml, set the theme of your {@code FlutterActivity} to your launch
* theme.
* <li>Add a {@code <meta-data>} property to your {@code FlutterActivity} with a name of
* "io.flutter.embedding.android.NormalTheme" and set the resource to your normal theme,
* e.g., {@code android:resource="@style/MyNormalTheme}.
* </ol>
*
* With the above settings, your launch theme will be used when loading the app, and then the
* theme will be switched to your normal theme once the app has initialized.
*
* <p>Do not change aspects of system chrome between a launch theme and normal theme. Either
* define both themes to be fullscreen or not, and define both themes to display the same status
* bar and navigation bar settings. If you wish to adjust system chrome once your Flutter app
* renders, use platform channels to instruct Android to do so at the appropriate time. This will
* avoid any jarring visual changes during app startup.
*/
private void switchLaunchThemeForNormalTheme() {
try {
Bundle metaData = getMetaData();
if (metaData != null) {
int normalThemeRID = metaData.getInt(NORMAL_THEME_META_DATA_KEY, -1);
if (normalThemeRID != -1) {
setTheme(normalThemeRID);
}
} else {
Log.v(TAG, "Using the launch theme as normal theme.");
}
} catch (PackageManager.NameNotFoundException exception) {
Log.e(
TAG,
"Could not read meta-data for FlutterActivity. Using the launch theme as normal theme.");
}
}
@Nullable
@Override
public SplashScreen provideSplashScreen() {
Drawable manifestSplashDrawable = getSplashScreenFromManifest();
if (manifestSplashDrawable != null) {
return new DrawableSplashScreen(manifestSplashDrawable);
} else {
return null;
}
}
/**
* Returns a {@link Drawable} to be used as a splash screen as requested by meta-data in the
* {@code AndroidManifest.xml} file, or null if no such splash screen is requested.
*
* <p>See {@link FlutterActivityLaunchConfigs#SPLASH_SCREEN_META_DATA_KEY} for the meta-data key
* to be used in a manifest file.
*/
@Nullable
private Drawable getSplashScreenFromManifest() {
try {
Bundle metaData = getMetaData();
int splashScreenId = metaData != null ? metaData.getInt(SPLASH_SCREEN_META_DATA_KEY) : 0;
return splashScreenId != 0
? ResourcesCompat.getDrawable(getResources(), splashScreenId, getTheme())
: null;
} catch (Resources.NotFoundException e) {
Log.e(TAG, "Splash screen not found. Ensure the drawable exists and that it's valid.");
throw e;
} catch (PackageManager.NameNotFoundException e) {
// This is never expected to happen.
return null;
}
}
/**
* Sets this {@code Activity}'s {@code Window} background to be transparent, and hides the status
* bar, if this {@code Activity}'s desired {@link BackgroundMode} is {@link
* BackgroundMode#transparent}.
*
* <p>For {@code Activity} transparency to work as expected, the theme applied to this {@code
* Activity} must include {@code <item name="android:windowIsTranslucent">true</item>}.
*/
private void configureWindowForTransparency() {
BackgroundMode backgroundMode = getBackgroundMode();
if (backgroundMode == BackgroundMode.transparent) {
getWindow().setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT));
}
}
@NonNull
private View createFlutterView() {
return delegate.onCreateView(
/* inflater=*/ null,
/* container=*/ null,
/* savedInstanceState=*/ null,
/*flutterViewId=*/ FLUTTER_VIEW_ID,
/*shouldDelayFirstAndroidViewDraw=*/ getRenderMode() == RenderMode.surface);
}
private void configureStatusBarForFullscreenFlutterExperience() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
Window window = getWindow();
window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS);
window.setStatusBarColor(0x40000000);
window.getDecorView().setSystemUiVisibility(PlatformPlugin.DEFAULT_SYSTEM_UI);
}
}
@Override
protected void onStart() {
super.onStart();
lifecycle.handleLifecycleEvent(Lifecycle.Event.ON_START);
if (stillAttachedForEvent("onStart")) {
delegate.onStart();
}
}
@Override
protected void onResume() {
super.onResume();
lifecycle.handleLifecycleEvent(Lifecycle.Event.ON_RESUME);
if (stillAttachedForEvent("onResume")) {
delegate.onResume();
}
}
@Override
public void onPostResume() {
super.onPostResume();
if (stillAttachedForEvent("onPostResume")) {
delegate.onPostResume();
}
}
@Override
protected void onPause() {
super.onPause();
if (stillAttachedForEvent("onPause")) {
delegate.onPause();
}
lifecycle.handleLifecycleEvent(Lifecycle.Event.ON_PAUSE);
}
@Override
protected void onStop() {
super.onStop();
if (stillAttachedForEvent("onStop")) {
delegate.onStop();
}
lifecycle.handleLifecycleEvent(Lifecycle.Event.ON_STOP);
}
@Override
protected void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
if (stillAttachedForEvent("onSaveInstanceState")) {
delegate.onSaveInstanceState(outState);
}
}
/**
* Irreversibly release this activity's control of the {@link
* io.flutter.embedding.engine.FlutterEngine} and its subcomponents.
*
* <p>Calling will disconnect this activity's view from the Flutter renderer, disconnect this
* activity from plugins' {@link ActivityControlSurface}, and stop system channel messages from
* this activity.
*
* <p>After calling, this activity should be disposed immediately and not be re-used.
*/
@VisibleForTesting
public void release() {
unregisterOnBackInvokedCallback();
if (delegate != null) {
delegate.release();
delegate = null;
}
}
@Override
public void detachFromFlutterEngine() {
Log.w(
TAG,
"FlutterActivity "
+ this
+ " connection to the engine "
+ getFlutterEngine()
+ " evicted by another attaching activity");
if (delegate != null) {
delegate.onDestroyView();
delegate.onDetach();
}
}
@Override
protected void onDestroy() {
super.onDestroy();
if (stillAttachedForEvent("onDestroy")) {
delegate.onDestroyView();
delegate.onDetach();
}
release();
lifecycle.handleLifecycleEvent(Lifecycle.Event.ON_DESTROY);
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
if (stillAttachedForEvent("onActivityResult")) {
delegate.onActivityResult(requestCode, resultCode, data);
}
}
@Override
protected void onNewIntent(@NonNull Intent intent) {
// TODO(mattcarroll): change G3 lint rule that forces us to call super
super.onNewIntent(intent);
if (stillAttachedForEvent("onNewIntent")) {
delegate.onNewIntent(intent);
}
}
@Override
public void onBackPressed() {
if (stillAttachedForEvent("onBackPressed")) {
delegate.onBackPressed();
}
}
@Override
public void onRequestPermissionsResult(
int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
if (stillAttachedForEvent("onRequestPermissionsResult")) {
delegate.onRequestPermissionsResult(requestCode, permissions, grantResults);
}
}
@Override
public void onUserLeaveHint() {
if (stillAttachedForEvent("onUserLeaveHint")) {
delegate.onUserLeaveHint();
}
}
@Override
public void onTrimMemory(int level) {
super.onTrimMemory(level);
if (stillAttachedForEvent("onTrimMemory")) {
delegate.onTrimMemory(level);
}
}
/**
* {@link FlutterActivityAndFragmentDelegate.Host} method that is used by {@link
* FlutterActivityAndFragmentDelegate} to obtain a {@code Context} reference as needed.
*/
@Override
@NonNull
public Context getContext() {
return this;
}
/**
* {@link FlutterActivityAndFragmentDelegate.Host} method that is used by {@link
* FlutterActivityAndFragmentDelegate} to obtain an {@code Activity} reference as needed. This
* reference is used by the delegate to instantiate a {@link FlutterView}, a {@link
* PlatformPlugin}, and to determine if the {@code Activity} is changing configurations.
*/
@Override
@NonNull
public Activity getActivity() {
return this;
}
/**
* {@link FlutterActivityAndFragmentDelegate.Host} method that is used by {@link
* FlutterActivityAndFragmentDelegate} to obtain a {@code Lifecycle} reference as needed. This
* reference is used by the delegate to provide Flutter plugins with access to lifecycle events.
*/
@Override
@NonNull
public Lifecycle getLifecycle() {
return lifecycle;
}
/**
* {@link FlutterActivityAndFragmentDelegate.Host} method that is used by {@link
* FlutterActivityAndFragmentDelegate} to obtain Flutter shell arguments when initializing
* Flutter.
*/
@NonNull
@Override
public FlutterShellArgs getFlutterShellArgs() {