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