• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 // Copyright 2013 The Flutter Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4 
5 package io.flutter.app;
6 
7 import android.animation.Animator;
8 import android.animation.AnimatorListenerAdapter;
9 import android.app.Activity;
10 import android.app.Application;
11 import android.content.Context;
12 import android.content.Intent;
13 import android.content.pm.ActivityInfo;
14 import android.content.pm.ApplicationInfo;
15 import android.content.pm.PackageManager;
16 import android.content.pm.PackageManager.NameNotFoundException;
17 import android.content.res.Configuration;
18 import android.content.res.Resources.NotFoundException;
19 import android.graphics.drawable.Drawable;
20 import android.os.Build;
21 import android.os.Bundle;
22 import android.util.Log;
23 import android.util.TypedValue;
24 import android.view.View;
25 import android.view.ViewGroup;
26 import android.view.Window;
27 import android.view.WindowManager.LayoutParams;
28 import io.flutter.plugin.common.PluginRegistry;
29 import io.flutter.plugin.platform.PlatformPlugin;
30 import io.flutter.util.Preconditions;
31 import io.flutter.view.FlutterMain;
32 import io.flutter.view.FlutterNativeView;
33 import io.flutter.view.FlutterRunArguments;
34 import io.flutter.view.FlutterView;
35 import org.json.JSONObject;
36 
37 import java.io.File;
38 import java.util.ArrayList;
39 
40 /**
41  * Class that performs the actual work of tying Android {@link Activity}
42  * instances to Flutter.
43  *
44  * <p>This exists as a dedicated class (as opposed to being integrated directly
45  * into {@link FlutterActivity}) to facilitate applications that don't wish
46  * to subclass {@code FlutterActivity}. The most obvious example of when this
47  * may come in handy is if an application wishes to subclass the Android v4
48  * support library's {@code FragmentActivity}.</p>
49  *
50  * <h3>Usage:</h3>
51  * <p>To wire this class up to your activity, simply forward the events defined
52  * in {@link FlutterActivityEvents} from your activity to an instance of this
53  * class. Optionally, you can make your activity implement
54  * {@link PluginRegistry} and/or {@link io.flutter.view.FlutterView.Provider}
55  * and forward those methods to this class as well.</p>
56  */
57 public final class FlutterActivityDelegate
58         implements FlutterActivityEvents,
59                    FlutterView.Provider,
60                    PluginRegistry {
61     private static final String SPLASH_SCREEN_META_DATA_KEY = "io.flutter.app.android.SplashScreenUntilFirstFrame";
62     private static final String TAG = "FlutterActivityDelegate";
63     private static final LayoutParams matchParent =
64         new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
65 
66     /**
67      * Specifies the mechanism by which Flutter views are created during the
68      * operation of a {@code FlutterActivityDelegate}.
69      *
70      * <p>A delegate's view factory will be consulted during
71      * {@link #onCreate(Bundle)}. If it returns {@code null}, then the delegate
72      * will fall back to instantiating a new full-screen {@code FlutterView}.</p>
73      *
74      * <p>A delegate's native view factory will be consulted during
75      * {@link #onCreate(Bundle)}. If it returns {@code null}, then the delegate
76      * will fall back to instantiating a new {@code FlutterNativeView}. This is
77      * useful for applications to override to reuse the FlutterNativeView held
78      * e.g. by a pre-existing background service.</p>
79      */
80     public interface ViewFactory {
createFlutterView(Context context)81         FlutterView createFlutterView(Context context);
createFlutterNativeView()82         FlutterNativeView createFlutterNativeView();
83 
84         /**
85          * Hook for subclasses to indicate that the {@code FlutterNativeView}
86          * returned by {@link #createFlutterNativeView()} should not be destroyed
87          * when this activity is destroyed.
88          */
retainFlutterNativeView()89         boolean retainFlutterNativeView();
90     }
91 
92     private final Activity activity;
93     private final ViewFactory viewFactory;
94     private FlutterView flutterView;
95     private View launchView;
96 
FlutterActivityDelegate(Activity activity, ViewFactory viewFactory)97     public FlutterActivityDelegate(Activity activity, ViewFactory viewFactory) {
98         this.activity = Preconditions.checkNotNull(activity);
99         this.viewFactory = Preconditions.checkNotNull(viewFactory);
100     }
101 
102     @Override
getFlutterView()103     public FlutterView getFlutterView() {
104         return flutterView;
105     }
106 
107     // The implementation of PluginRegistry forwards to flutterView.
108     @Override
hasPlugin(String key)109     public boolean hasPlugin(String key) {
110         return flutterView.getPluginRegistry().hasPlugin(key);
111     }
112 
113     @Override
114     @SuppressWarnings("unchecked")
valuePublishedByPlugin(String pluginKey)115     public <T> T valuePublishedByPlugin(String pluginKey) {
116         return (T) flutterView.getPluginRegistry().valuePublishedByPlugin(pluginKey);
117     }
118 
119     @Override
registrarFor(String pluginKey)120     public Registrar registrarFor(String pluginKey) {
121         return flutterView.getPluginRegistry().registrarFor(pluginKey);
122     }
123 
124     @Override
onRequestPermissionsResult( int requestCode, String[] permissions, int[] grantResults)125     public boolean onRequestPermissionsResult(
126             int requestCode, String[] permissions, int[] grantResults) {
127         return flutterView.getPluginRegistry().onRequestPermissionsResult(requestCode, permissions, grantResults);
128     }
129 
130     @Override
onActivityResult(int requestCode, int resultCode, Intent data)131     public boolean onActivityResult(int requestCode, int resultCode, Intent data) {
132         return flutterView.getPluginRegistry().onActivityResult(requestCode, resultCode, data);
133     }
134 
135     @Override
onCreate(Bundle savedInstanceState)136     public void onCreate(Bundle savedInstanceState) {
137         if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
138             Window window = activity.getWindow();
139             window.addFlags(LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS);
140             window.setStatusBarColor(0x40000000);
141             window.getDecorView().setSystemUiVisibility(PlatformPlugin.DEFAULT_SYSTEM_UI);
142         }
143 
144         String[] args = getArgsFromIntent(activity.getIntent());
145         FlutterMain.ensureInitializationComplete(activity.getApplicationContext(), args);
146 
147         flutterView = viewFactory.createFlutterView(activity);
148         if (flutterView == null) {
149             FlutterNativeView nativeView = viewFactory.createFlutterNativeView();
150             flutterView = new FlutterView(activity, null, nativeView);
151             flutterView.setLayoutParams(matchParent);
152             activity.setContentView(flutterView);
153             launchView = createLaunchView();
154             if (launchView != null) {
155                 addLaunchView();
156             }
157         }
158 
159         if (loadIntent(activity.getIntent())) {
160             return;
161         }
162 
163         String appBundlePath = FlutterMain.findAppBundlePath();
164         if (appBundlePath != null) {
165             runBundle(appBundlePath);
166         }
167     }
168 
169     @Override
onNewIntent(Intent intent)170     public void onNewIntent(Intent intent) {
171         // Only attempt to reload the Flutter Dart code during development. Use
172         // the debuggable flag as an indicator that we are in development mode.
173         if (!isDebuggable() || !loadIntent(intent)) {
174             flutterView.getPluginRegistry().onNewIntent(intent);
175         }
176     }
177 
isDebuggable()178     private boolean isDebuggable() {
179         return (activity.getApplicationInfo().flags & ApplicationInfo.FLAG_DEBUGGABLE) != 0;
180     }
181 
182     @Override
onPause()183     public void onPause() {
184         Application app = (Application) activity.getApplicationContext();
185         if (app instanceof FlutterApplication) {
186             FlutterApplication flutterApp = (FlutterApplication) app;
187             if (activity.equals(flutterApp.getCurrentActivity())) {
188                 flutterApp.setCurrentActivity(null);
189             }
190         }
191         if (flutterView != null) {
192             flutterView.onPause();
193         }
194     }
195 
196     @Override
onStart()197     public void onStart() {
198         if (flutterView != null) {
199             flutterView.onStart();
200         }
201     }
202 
203     @Override
onResume()204     public void onResume() {
205         Application app = (Application) activity.getApplicationContext();
206         if (app instanceof FlutterApplication) {
207             FlutterApplication flutterApp = (FlutterApplication) app;
208             flutterApp.setCurrentActivity(activity);
209         }
210     }
211 
212     @Override
onStop()213     public void onStop() {
214         flutterView.onStop();
215     }
216 
217     @Override
onPostResume()218     public void onPostResume() {
219         if (flutterView != null) {
220             flutterView.onPostResume();
221         }
222     }
223 
224     @Override
onDestroy()225     public void onDestroy() {
226         Application app = (Application) activity.getApplicationContext();
227         if (app instanceof FlutterApplication) {
228             FlutterApplication flutterApp = (FlutterApplication) app;
229             if (activity.equals(flutterApp.getCurrentActivity())) {
230                 flutterApp.setCurrentActivity(null);
231             }
232         }
233         if (flutterView != null) {
234             final boolean detach =
235                 flutterView.getPluginRegistry().onViewDestroy(flutterView.getFlutterNativeView());
236             if (detach || viewFactory.retainFlutterNativeView()) {
237                 // Detach, but do not destroy the FlutterView if a plugin
238                 // expressed interest in its FlutterNativeView.
239                 flutterView.detach();
240             } else {
241                 flutterView.destroy();
242             }
243         }
244     }
245 
246     @Override
onBackPressed()247     public boolean onBackPressed() {
248         if (flutterView != null) {
249             flutterView.popRoute();
250             return true;
251         }
252         return false;
253     }
254 
255     @Override
onUserLeaveHint()256     public void onUserLeaveHint() {
257         flutterView.getPluginRegistry().onUserLeaveHint();
258     }
259 
260     @Override
onTrimMemory(int level)261     public void onTrimMemory(int level) {
262         // Use a trim level delivered while the application is running so the
263         // framework has a chance to react to the notification.
264         if (level == TRIM_MEMORY_RUNNING_LOW) {
265             flutterView.onMemoryPressure();
266         }
267     }
268 
269     @Override
onLowMemory()270     public void onLowMemory() {
271         flutterView.onMemoryPressure();
272     }
273 
274     @Override
onConfigurationChanged(Configuration newConfig)275     public void onConfigurationChanged(Configuration newConfig) {}
276 
getArgsFromIntent(Intent intent)277     private static String[] getArgsFromIntent(Intent intent) {
278         // Before adding more entries to this list, consider that arbitrary
279         // Android applications can generate intents with extra data and that
280         // there are many security-sensitive args in the binary.
281         ArrayList<String> args = new ArrayList<>();
282         if (intent.getBooleanExtra("trace-startup", false)) {
283             args.add("--trace-startup");
284         }
285         if (intent.getBooleanExtra("start-paused", false)) {
286             args.add("--start-paused");
287         }
288         if (intent.getBooleanExtra("disable-service-auth-codes", false)) {
289             args.add("--disable-service-auth-codes");
290         }
291         if (intent.getBooleanExtra("use-test-fonts", false)) {
292             args.add("--use-test-fonts");
293         }
294         if (intent.getBooleanExtra("enable-dart-profiling", false)) {
295             args.add("--enable-dart-profiling");
296         }
297         if (intent.getBooleanExtra("enable-software-rendering", false)) {
298             args.add("--enable-software-rendering");
299         }
300         if (intent.getBooleanExtra("skia-deterministic-rendering", false)) {
301             args.add("--skia-deterministic-rendering");
302         }
303         if (intent.getBooleanExtra("trace-skia", false)) {
304             args.add("--trace-skia");
305         }
306         if (intent.getBooleanExtra("trace-systrace", false)) {
307             args.add("--trace-systrace");
308         }
309         if (intent.getBooleanExtra("dump-skp-on-shader-compilation", false)) {
310             args.add("--dump-skp-on-shader-compilation");
311         }
312         if (intent.getBooleanExtra("verbose-logging", false)) {
313             args.add("--verbose-logging");
314         }
315         final int observatoryPort = intent.getIntExtra("observatory-port", 0);
316         if (observatoryPort > 0) {
317             args.add("--observatory-port=" + Integer.toString(observatoryPort));
318         }
319         if (intent.getBooleanExtra("disable-service-auth-codes", false)) {
320             args.add("--disable-service-auth-codes");
321         }
322         // NOTE: all flags provided with this argument are subject to filtering
323         // based on a whitelist in shell/common/switches.cc. If any flag provided
324         // is not present in the whitelist, the process will immediately
325         // terminate.
326         if (intent.hasExtra("dart-flags")) {
327             args.add("--dart-flags=" + intent.getStringExtra("dart-flags"));
328         }
329         if (!args.isEmpty()) {
330             String[] argsArray = new String[args.size()];
331             return args.toArray(argsArray);
332         }
333         return null;
334     }
335 
loadIntent(Intent intent)336     private boolean loadIntent(Intent intent) {
337         String action = intent.getAction();
338         if (Intent.ACTION_RUN.equals(action)) {
339             String route = intent.getStringExtra("route");
340             String appBundlePath = intent.getDataString();
341             if (appBundlePath == null) {
342                 // Fall back to the installation path if no bundle path was specified.
343                 appBundlePath = FlutterMain.findAppBundlePath();
344             }
345             if (route != null) {
346                 flutterView.setInitialRoute(route);
347             }
348 
349             runBundle(appBundlePath);
350             return true;
351         }
352 
353         return false;
354     }
355 
runBundle(String appBundlePath)356     private void runBundle(String appBundlePath) {
357         if (!flutterView.getFlutterNativeView().isApplicationRunning()) {
358             FlutterRunArguments args = new FlutterRunArguments();
359             args.bundlePath = appBundlePath;
360             args.entrypoint = "main";
361             flutterView.runFromBundle(args);
362         }
363     }
364 
365     /**
366      * Creates a {@link View} containing the same {@link Drawable} as the one set as the
367      * {@code windowBackground} of the parent activity for use as a launch splash view.
368      *
369      * Returns null if no {@code windowBackground} is set for the activity.
370      */
createLaunchView()371     private View createLaunchView() {
372         if (!showSplashScreenUntilFirstFrame()) {
373             return null;
374         }
375         final Drawable launchScreenDrawable = getLaunchScreenDrawableFromActivityTheme();
376         if (launchScreenDrawable == null) {
377             return null;
378         }
379         final View view = new View(activity);
380         view.setLayoutParams(matchParent);
381         view.setBackground(launchScreenDrawable);
382         return view;
383     }
384 
385     /**
386      * Extracts a {@link Drawable} from the parent activity's {@code windowBackground}.
387      *
388      * {@code android:windowBackground} is specifically reused instead of a other attributes
389      * because the Android framework can display it fast enough when launching the app as opposed
390      * to anything defined in the Activity subclass.
391      *
392      * Returns null if no {@code windowBackground} is set for the activity.
393      */
394     @SuppressWarnings("deprecation")
getLaunchScreenDrawableFromActivityTheme()395     private Drawable getLaunchScreenDrawableFromActivityTheme() {
396         TypedValue typedValue = new TypedValue();
397         if (!activity.getTheme().resolveAttribute(
398             android.R.attr.windowBackground,
399             typedValue,
400             true)) {
401             return null;
402         }
403         if (typedValue.resourceId == 0) {
404             return null;
405         }
406         try {
407             return activity.getResources().getDrawable(typedValue.resourceId);
408         } catch (NotFoundException e) {
409             Log.e(TAG, "Referenced launch screen windowBackground resource does not exist");
410             return null;
411         }
412     }
413 
414     /**
415      * Let the user specify whether the activity's {@code windowBackground} is a launch screen
416      * and should be shown until the first frame via a <meta-data> tag in the activity.
417      */
showSplashScreenUntilFirstFrame()418     private Boolean showSplashScreenUntilFirstFrame() {
419         try {
420             ActivityInfo activityInfo = activity.getPackageManager().getActivityInfo(
421                 activity.getComponentName(),
422                 PackageManager.GET_META_DATA|PackageManager.GET_ACTIVITIES);
423             Bundle metadata = activityInfo.metaData;
424             return metadata != null && metadata.getBoolean(SPLASH_SCREEN_META_DATA_KEY);
425         } catch (NameNotFoundException e) {
426             return false;
427         }
428     }
429 
430     /**
431      * Show and then automatically animate out the launch view.
432      *
433      * If a launch screen is defined in the user application's AndroidManifest.xml as the
434      * activity's {@code windowBackground}, display it on top of the {@link FlutterView} and
435      * remove the activity's {@code windowBackground}.
436      *
437      * Fade it out and remove it when the {@link FlutterView} renders its first frame.
438      */
addLaunchView()439     private void addLaunchView() {
440         if (launchView == null) {
441             return;
442         }
443 
444         activity.addContentView(launchView, matchParent);
445         flutterView.addFirstFrameListener(new FlutterView.FirstFrameListener() {
446             @Override
447             public void onFirstFrame() {
448                 FlutterActivityDelegate.this.launchView.animate()
449                     .alpha(0f)
450                     // Use Android's default animation duration.
451                     .setListener(new AnimatorListenerAdapter() {
452                         @Override
453                         public void onAnimationEnd(Animator animation) {
454                             // Views added to an Activity's addContentView is always added to its
455                             // root FrameLayout.
456                             ((ViewGroup) FlutterActivityDelegate.this.launchView.getParent())
457                                 .removeView(FlutterActivityDelegate.this.launchView);
458                             FlutterActivityDelegate.this.launchView = null;
459                         }
460                     });
461 
462                 FlutterActivityDelegate.this.flutterView.removeFirstFrameListener(this);
463             }
464         });
465 
466         // Resets the activity theme from the one containing the launch screen in the window
467         // background to a blank one since the launch screen is now in a view in front of the
468         // FlutterView.
469         //
470         // We can make this configurable if users want it.
471         activity.setTheme(android.R.style.Theme_Black_NoTitleBar);
472     }
473 }
474