From 0804369d39d3363efd04ee8f5c9106731282d328 Mon Sep 17 00:00:00 2001 From: Matt Carroll Date: Fri, 6 Sep 2019 20:31:29 -0700 Subject: [PATCH 1/3] xster's original work on the PR --- ci/licenses_golden/licenses_flutter | 8 +- shell/platform/android/BUILD.gn | 9 +- .../android/io/flutter/FlutterInjector.java | 114 ++++++ .../embedding/engine/FlutterEngine.java | 79 ++-- .../engine/loader/FlutterLoader.java | 360 ++++++++++++++++++ .../engine/loader}/ResourceCleaner.java | 2 +- .../engine/loader}/ResourceExtractor.java | 2 +- .../engine/loader}/ResourcePaths.java | 2 +- .../android/io/flutter/view/FlutterMain.java | 275 ++----------- .../test/io/flutter/FlutterInjectorTest.java | 57 +++ .../test/io/flutter/FlutterTestSuite.java | 2 + ...lutterActivityAndFragmentDelegateTest.java | 20 +- testing/scenario_app/README.md | 5 + testing/scenario_app/android/app/build.gradle | 3 +- .../scenarios/EngineLaunchE2ETest.java | 69 ++++ .../android/app/src/main/AndroidManifest.xml | 27 +- .../dev/flutter/scenarios/BlankActivity.java | 11 + ...ity.java => TextPlatformViewActivity.java} | 4 +- testing/scenario_app/android/build.gradle | 2 +- testing/scenario_app/run_android_tests.sh | 24 ++ 20 files changed, 760 insertions(+), 315 deletions(-) create mode 100644 shell/platform/android/io/flutter/FlutterInjector.java create mode 100644 shell/platform/android/io/flutter/embedding/engine/loader/FlutterLoader.java rename shell/platform/android/io/flutter/{view => embedding/engine/loader}/ResourceCleaner.java (98%) rename shell/platform/android/io/flutter/{view => embedding/engine/loader}/ResourceExtractor.java (99%) rename shell/platform/android/io/flutter/{view => embedding/engine/loader}/ResourcePaths.java (94%) create mode 100644 shell/platform/android/test/io/flutter/FlutterInjectorTest.java create mode 100644 testing/scenario_app/android/app/src/androidTest/java/dev/flutter/scenarios/EngineLaunchE2ETest.java create mode 100644 testing/scenario_app/android/app/src/main/java/dev/flutter/scenarios/BlankActivity.java rename testing/scenario_app/android/app/src/main/java/dev/flutter/scenarios/{MainActivity.java => TextPlatformViewActivity.java} (94%) create mode 100755 testing/scenario_app/run_android_tests.sh diff --git a/ci/licenses_golden/licenses_flutter b/ci/licenses_golden/licenses_flutter index bfc4c4245fb9e..1bf46053bfc99 100644 --- a/ci/licenses_golden/licenses_flutter +++ b/ci/licenses_golden/licenses_flutter @@ -565,6 +565,7 @@ FILE: ../../../flutter/shell/platform/android/apk_asset_provider.cc FILE: ../../../flutter/shell/platform/android/apk_asset_provider.h FILE: ../../../flutter/shell/platform/android/flutter_main.cc FILE: ../../../flutter/shell/platform/android/flutter_main.h +FILE: ../../../flutter/shell/platform/android/io/flutter/FlutterInjector.java FILE: ../../../flutter/shell/platform/android/io/flutter/Log.java FILE: ../../../flutter/shell/platform/android/io/flutter/app/FlutterActivity.java FILE: ../../../flutter/shell/platform/android/io/flutter/app/FlutterActivityDelegate.java @@ -597,6 +598,10 @@ FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/Flutte FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/dart/DartExecutor.java FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/dart/DartMessenger.java FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/dart/PlatformMessageHandler.java +FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/loader/FlutterLoader.java +FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/loader/ResourceCleaner.java +FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/loader/ResourceExtractor.java +FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/loader/ResourcePaths.java FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/plugins/FlutterPlugin.java FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/plugins/PluginRegistry.java FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/plugins/activity/ActivityAware.java @@ -666,9 +671,6 @@ FILE: ../../../flutter/shell/platform/android/io/flutter/view/FlutterMain.java FILE: ../../../flutter/shell/platform/android/io/flutter/view/FlutterNativeView.java FILE: ../../../flutter/shell/platform/android/io/flutter/view/FlutterRunArguments.java FILE: ../../../flutter/shell/platform/android/io/flutter/view/FlutterView.java -FILE: ../../../flutter/shell/platform/android/io/flutter/view/ResourceCleaner.java -FILE: ../../../flutter/shell/platform/android/io/flutter/view/ResourceExtractor.java -FILE: ../../../flutter/shell/platform/android/io/flutter/view/ResourcePaths.java FILE: ../../../flutter/shell/platform/android/io/flutter/view/TextureRegistry.java FILE: ../../../flutter/shell/platform/android/io/flutter/view/VsyncWaiter.java FILE: ../../../flutter/shell/platform/android/library_loader.cc diff --git a/shell/platform/android/BUILD.gn b/shell/platform/android/BUILD.gn index cd57506e623b6..d999c0ea5f22c 100644 --- a/shell/platform/android/BUILD.gn +++ b/shell/platform/android/BUILD.gn @@ -117,6 +117,7 @@ embedding_sources_jar_filename = "$embedding_artifact_id-sources.jar" embedding_source_jar_path = "$root_out_dir/$embedding_sources_jar_filename" android_java_sources = [ + "io/flutter/FlutterInjector.java", "io/flutter/Log.java", "io/flutter/app/FlutterActivity.java", "io/flutter/app/FlutterActivityDelegate.java", @@ -149,6 +150,10 @@ android_java_sources = [ "io/flutter/embedding/engine/dart/DartExecutor.java", "io/flutter/embedding/engine/dart/DartMessenger.java", "io/flutter/embedding/engine/dart/PlatformMessageHandler.java", + "io/flutter/embedding/engine/loader/FlutterLoader.java", + "io/flutter/embedding/engine/loader/ResourceCleaner.java", + "io/flutter/embedding/engine/loader/ResourceExtractor.java", + "io/flutter/embedding/engine/loader/ResourcePaths.java", "io/flutter/embedding/engine/plugins/FlutterPlugin.java", "io/flutter/embedding/engine/plugins/PluginRegistry.java", "io/flutter/embedding/engine/plugins/activity/ActivityAware.java", @@ -218,9 +223,6 @@ android_java_sources = [ "io/flutter/view/FlutterNativeView.java", "io/flutter/view/FlutterRunArguments.java", "io/flutter/view/FlutterView.java", - "io/flutter/view/ResourceCleaner.java", - "io/flutter/view/ResourceExtractor.java", - "io/flutter/view/ResourcePaths.java", "io/flutter/view/TextureRegistry.java", "io/flutter/view/VsyncWaiter.java", ] @@ -411,6 +413,7 @@ action("robolectric_tests") { jar_path = "$root_out_dir/robolectric_tests.jar" sources = [ + "test/io/flutter/FlutterInjectorTest.java", "test/io/flutter/FlutterTestSuite.java", "test/io/flutter/SmokeTest.java", "test/io/flutter/embedding/android/FlutterActivityAndFragmentDelegateTest.java", diff --git a/shell/platform/android/io/flutter/FlutterInjector.java b/shell/platform/android/io/flutter/FlutterInjector.java new file mode 100644 index 0000000000000..542c01b08545d --- /dev/null +++ b/shell/platform/android/io/flutter/FlutterInjector.java @@ -0,0 +1,114 @@ +// 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; + +import android.support.annotation.NonNull; +import android.support.annotation.VisibleForTesting; + +import io.flutter.embedding.engine.loader.FlutterLoader; + +import java.lang.IllegalStateException; + +/* + * This class is a simple dependency injector for the Android part of the Flutter engine. + * + * This simple solution is used facilitate testability without bringing in heavier app-development + * centric dependency injection frameworks such as Guice or Dagger2. + */ +public final class FlutterInjector { + + private static FlutterInjector instance; + private static boolean accessed; + + /* + * Use {@link FlutterInjector.Builder} to specify members to be injected via the static + * {@code FlutterInjector}. + * + * This can only be called at the beginning of the program before the {@link #instance()} is + * accessed. + */ + public static void setInstance(@NonNull FlutterInjector injector) { + if (accessed) { + throw new IllegalStateException("Cannot change the FlutterInjector instance once it's been " + + "read. If you're trying to dependency inject, be sure to do so at the beginning of " + + "the program"); + } + instance = injector; + } + + /* + * Retrieve the static instance of the {@code FlutterInjector} to use in your program. + * + * Once you access it, you can no longer change the values injected. + * + * If no override is provided for the injector, reasonable defaults are provided. + */ + public static FlutterInjector instance() { + accessed = true; + if (instance == null) { + instance = new Builder().build(); + } + return instance; + } + + // This whole class is here to enable testing so to test the thing that lets you test, some degree + // of hack is needed. + @VisibleForTesting + /* Package default */ static void reset() { + accessed = false; + instance = null; + } + + private FlutterInjector( + boolean isRunningInRobolectricTest, + @NonNull FlutterLoader flutterLoader + ) { + this.isRunningInRobolectricTest = isRunningInRobolectricTest; + this.flutterLoader = flutterLoader; + } + + private boolean isRunningInRobolectricTest; + private FlutterLoader flutterLoader; + + public boolean isRunningInRobolectricTest() { + return isRunningInRobolectricTest; + } + + @NonNull + public FlutterLoader flutterLoader() { + return flutterLoader; + } + + /* + * Builder used to supply a custom FlutterInjector instance to + * {@link FlutterInjector#setInstance(FlutterInjector)}. + * + * Non-overriden values have reasonable defaults. + */ + public static final class Builder { + + private boolean isRunningInRobolectricTest = false; + public Builder setIsRunningInRobolectricTest(boolean isRunningInRobolectricTest) { + this.isRunningInRobolectricTest = isRunningInRobolectricTest; + return this; + } + + private FlutterLoader flutterLoader; + public Builder setFlutterLoader(@NonNull FlutterLoader flutterLoader) { + this.flutterLoader = flutterLoader; + return this; + } + + public FlutterInjector build() { + if (flutterLoader == null) { + flutterLoader = new FlutterLoader(); + } + + return new FlutterInjector(isRunningInRobolectricTest, flutterLoader); + } + + } + +} \ No newline at end of file diff --git a/shell/platform/android/io/flutter/embedding/engine/FlutterEngine.java b/shell/platform/android/io/flutter/embedding/engine/FlutterEngine.java index 618dec5a1f99e..8c327d8b35010 100644 --- a/shell/platform/android/io/flutter/embedding/engine/FlutterEngine.java +++ b/shell/platform/android/io/flutter/embedding/engine/FlutterEngine.java @@ -12,8 +12,10 @@ import java.util.HashSet; import java.util.Set; +import io.flutter.FlutterInjector; import io.flutter.Log; import io.flutter.embedding.engine.dart.DartExecutor; +import io.flutter.embedding.engine.loader.FlutterLoader; import io.flutter.embedding.engine.plugins.PluginRegistry; import io.flutter.embedding.engine.plugins.activity.ActivityControlSurface; import io.flutter.embedding.engine.plugins.broadcastreceiver.BroadcastReceiverControlSurface; @@ -34,28 +36,38 @@ /** * A single Flutter execution environment. - * - * WARNING: THIS CLASS IS EXPERIMENTAL. DO NOT SHIP A DEPENDENCY ON THIS CODE. - * IF YOU USE IT, WE WILL BREAK YOU. - * - * A {@code FlutterEngine} can execute in the background, or it can be rendered to the screen by - * using the accompanying {@link FlutterRenderer}. Rendering can be started and stopped, thus - * allowing a {@code FlutterEngine} to move from UI interaction to data-only processing and then - * back to UI interaction. - * + *

+ * WARNING: THIS CLASS IS CURRENTLY EXPERIMENTAL. USE AT YOUR OWN RISK. + *

+ * The {@code FlutterEngine} is the container through which Dart code can be run in an Android + * application. + *

+ * Dart code in a {@code FlutterEngine} can execute in the background, or it can be render to the + * screen by using the accompanying {@link FlutterRenderer} and Dart code using the Flutter + * framework on the Dart side. Rendering can be started and stopped, thus allowing a + * {@code FlutterEngine} to move from UI interaction to data-only processing and then back to UI + * interaction. + *

* Multiple {@code FlutterEngine}s may exist, execute Dart code, and render UIs within a single * Android app. - * - * To start running Flutter within this {@code FlutterEngine}, get a reference to this engine's - * {@link DartExecutor} and then use {@link DartExecutor#executeDartEntrypoint(DartExecutor.DartEntrypoint)}. - * The {@link DartExecutor#executeDartEntrypoint(DartExecutor.DartEntrypoint)} method must not be + *

+ * To start running Dart and/or Flutter within this {@code FlutterEngine}, get a reference to this + * engine's {@link DartExecutor} and then use + * {@link DartExecutor#executeDartEntrypoint(DartExecutor.DartEntrypoint)}. The + * {@link DartExecutor#executeDartEntrypoint(DartExecutor.DartEntrypoint)} method must not be * invoked twice on the same {@code FlutterEngine}. - * + *

* To start rendering Flutter content to the screen, use {@link #getRenderer()} to obtain a - * {@link FlutterRenderer} and then attach a {@link RenderSurface}. Consider using - * a {@link io.flutter.embedding.android.FlutterView} as a {@link RenderSurface}. + * {@link FlutterRenderer} and then attach a {@link RenderSurface}. Consider using a + * {@link io.flutter.embedding.android.FlutterView} as a {@link RenderSurface}. + *

+ * Instatiating the first {@code FlutterEngine} per process will also load the Flutter engine's + * native library and start the Dart VM. Subsequent {@code FlutterEngine}s will run on the same VM + * instance but will have their own Dart Isolate when the + * {@link DartExecutor} is run. Each Isolate is a self-contained Dart environment and cannot + * communicate with each other except via Isolate ports. */ -// TODO(mattcarroll): re-evaluate system channel APIs - some are not well named or differentiated public class FlutterEngine implements LifecycleOwner { private static final String TAG = "FlutterEngine"; @@ -110,24 +122,31 @@ public void onPreEngineRestart() { /** * Constructs a new {@code FlutterEngine}. - * - * {@code FlutterMain.startInitialization} must be called before constructing a {@code FlutterEngine} - * to load the native libraries needed to attach to JNI. - * + *

* A new {@code FlutterEngine} does not execute any Dart code automatically. See * {@link #getDartExecutor()} and {@link DartExecutor#executeDartEntrypoint(DartExecutor.DartEntrypoint)} * to begin executing Dart code within this {@code FlutterEngine}. - * + *

* A new {@code FlutterEngine} will not display any UI until a * {@link RenderSurface} is registered. See * {@link #getRenderer()} and {@link FlutterRenderer#startRenderingToSurface(RenderSurface)}. - * + *

* A new {@code FlutterEngine} does not come with any Flutter plugins attached. To attach plugins, * see {@link #getPlugins()}. - * + *

* A new {@code FlutterEngine} does come with all default system channels attached. + *

+ * The first {@code FlutterEngine} instance constructed per process will also load the Flutter + * native library and start a Dart VM. + *

+ * In order to pass Dart VM initialization arguments (see {@link io.flutter.embedding.engine.FlutterShellArgs}) + * when creating the VM, manually set the initialization arguments by calling {@link FlutterMain#startInitialization(io.flutter.view.Context)} + * and {@link FlutterMain#ensureInitializationComplete(io.flutter.view.Context, String[])}. */ public FlutterEngine(@NonNull Context context) { + FlutterLoader flutterLoader = FlutterInjector.instance().flutterLoader(); + flutterLoader.startInitialization(context); + FlutterLoader.ensureInitializationComplete(context, null); this.flutterJNI = new FlutterJNI(); flutterJNI.addEngineLifecycleListener(engineLifecycleListener); attachToJni(); @@ -174,9 +193,9 @@ private boolean isAttachedToJni() { } /** - * Cleans up all components within this {@code FlutterEngine} and then detaches from Flutter's - * native implementation. - * + * Cleans up all components within this {@code FlutterEngine} and destroys the associated Dart + * Isolate. All state held by the Dart Isolate, such as the Flutter Elements tree, is lost. + *

* This {@code FlutterEngine} instance should be discarded after invoking this method. */ public void destroy() { @@ -206,10 +225,10 @@ public void removeEngineLifecycleListener(@NonNull EngineLifecycleListener liste /** * The Dart execution context associated with this {@code FlutterEngine}. - * + *

* The {@link DartExecutor} can be used to start executing Dart code from a given entrypoint. * See {@link DartExecutor#executeDartEntrypoint(DartExecutor.DartEntrypoint)}. - * + *

* Use the {@link DartExecutor} to connect any desired message channels and method channels * to facilitate communication between Android and Dart/Flutter. */ @@ -220,7 +239,7 @@ public DartExecutor getDartExecutor() { /** * The rendering system associated with this {@code FlutterEngine}. - * + *

* To render a Flutter UI that is produced by this {@code FlutterEngine}'s Dart code, attach * a {@link RenderSurface} to this * {@link FlutterRenderer}. diff --git a/shell/platform/android/io/flutter/embedding/engine/loader/FlutterLoader.java b/shell/platform/android/io/flutter/embedding/engine/loader/FlutterLoader.java new file mode 100644 index 0000000000000..849f1fb55eda3 --- /dev/null +++ b/shell/platform/android/io/flutter/embedding/engine/loader/FlutterLoader.java @@ -0,0 +1,360 @@ +// 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.engine.loader; + +import android.content.Context; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageManager; +import android.content.res.AssetManager; +import android.os.Bundle; +import android.os.Handler; +import android.os.Looper; +import android.os.SystemClock; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.support.annotation.VisibleForTesting; +import android.util.Log; +import android.view.WindowManager; + +import io.flutter.FlutterInjector; +import io.flutter.BuildConfig; +import io.flutter.embedding.engine.FlutterJNI; +import io.flutter.util.PathUtils; +import io.flutter.view.VsyncWaiter; + +import java.io.File; +import java.util.*; + +/** + * This class finds Flutter resources in an application APK and also loads Flutter's native library. + */ +public class FlutterLoader { + private static final String TAG = "FlutterLoader"; + + // Must match values in flutter::switches + private static final String AOT_SHARED_LIBRARY_NAME = "aot-shared-library-name"; + private static final String SNAPSHOT_ASSET_PATH_KEY = "snapshot-asset-path"; + private static final String VM_SNAPSHOT_DATA_KEY = "vm-snapshot-data"; + private static final String ISOLATE_SNAPSHOT_DATA_KEY = "isolate-snapshot-data"; + private static final String FLUTTER_ASSETS_DIR_KEY = "flutter-assets-dir"; + + // XML Attribute keys supported in AndroidManifest.xml + public static final String PUBLIC_AOT_SHARED_LIBRARY_NAME = + FlutterLoader.class.getName() + '.' + AOT_SHARED_LIBRARY_NAME; + public static final String PUBLIC_VM_SNAPSHOT_DATA_KEY = + FlutterLoader.class.getName() + '.' + VM_SNAPSHOT_DATA_KEY; + public static final String PUBLIC_ISOLATE_SNAPSHOT_DATA_KEY = + FlutterLoader.class.getName() + '.' + ISOLATE_SNAPSHOT_DATA_KEY; + public static final String PUBLIC_FLUTTER_ASSETS_DIR_KEY = + FlutterLoader.class.getName() + '.' + FLUTTER_ASSETS_DIR_KEY; + + // Resource names used for components of the precompiled snapshot. + private static final String DEFAULT_AOT_SHARED_LIBRARY_NAME = "libapp.so"; + private static final String DEFAULT_VM_SNAPSHOT_DATA = "vm_snapshot_data"; + private static final String DEFAULT_ISOLATE_SNAPSHOT_DATA = "isolate_snapshot_data"; + private static final String DEFAULT_LIBRARY = "libflutter.so"; + private static final String DEFAULT_KERNEL_BLOB = "kernel_blob.bin"; + private static final String DEFAULT_FLUTTER_ASSETS_DIR = "flutter_assets"; + + @NonNull + private static String fromFlutterAssets(@NonNull String filePath) { + return sFlutterAssetsDir + File.separator + filePath; + } + + // Mutable because default values can be overridden via config properties + private static String sAotSharedLibraryName = DEFAULT_AOT_SHARED_LIBRARY_NAME; + private static String sVmSnapshotData = DEFAULT_VM_SNAPSHOT_DATA; + private static String sIsolateSnapshotData = DEFAULT_ISOLATE_SNAPSHOT_DATA; + private static String sFlutterAssetsDir = DEFAULT_FLUTTER_ASSETS_DIR; + + private static boolean sInitialized = false; + + @Nullable + private static ResourceExtractor sResourceExtractor; + @Nullable + private static Settings sSettings; + + public static class Settings { + private String logTag; + + @Nullable + public String getLogTag() { + return logTag; + } + + /** + * Set the tag associated with Flutter app log messages. + * @param tag Log tag. + */ + public void setLogTag(String tag) { + logTag = tag; + } + } + + /** + * Starts initialization of the native system. + * @param applicationContext The Android application context. + */ + public static void startInitialization(@NonNull Context applicationContext) { + // Do nothing if we're running this in a Robolectric test. + if (FlutterInjector.instance().isRunningInRobolectricTest()) { + return; + } + startInitialization(applicationContext, new Settings()); + } + + /** + * Starts initialization of the native system. + *

+ * This loads the Flutter engine's native library to enable subsequent JNI calls. This also + * starts locating and unpacking Dart resources packaged in the app's APK. + *

+ * Calling this method multiple times has no effect. + * + * @param applicationContext The Android application context. + * @param settings Configuration settings. + */ + public static void startInitialization(@NonNull Context applicationContext, @NonNull Settings settings) { + // Do nothing if we're running this in a Robolectric test. + if (FlutterInjector.instance().isRunningInRobolectricTest()) { + return; + } + + if (Looper.myLooper() != Looper.getMainLooper()) { + throw new IllegalStateException("startInitialization must be called on the main thread"); + } + // Do not run startInitialization more than once. + if (sSettings != null) { + return; + } + + sSettings = settings; + + long initStartTimestampMillis = SystemClock.uptimeMillis(); + initConfig(applicationContext); + initResources(applicationContext); + + System.loadLibrary("flutter"); + + VsyncWaiter + .getInstance((WindowManager) applicationContext.getSystemService(Context.WINDOW_SERVICE)) + .init(); + + // We record the initialization time using SystemClock because at the start of the + // initialization we have not yet loaded the native library to call into dart_tools_api.h. + // To get Timeline timestamp of the start of initialization we simply subtract the delta + // from the Timeline timestamp at the current moment (the assumption is that the overhead + // of the JNI call is negligible). + long initTimeMillis = SystemClock.uptimeMillis() - initStartTimestampMillis; + FlutterJNI.nativeRecordStartTimestamp(initTimeMillis); + } + + /** + * Blocks until initialization of the native system has completed. + *

+ * Calling this method multiple times has no effect. + * + * @param applicationContext The Android application context. + * @param args Flags sent to the Flutter runtime. + */ + public static void ensureInitializationComplete(@NonNull Context applicationContext, @Nullable String[] args) { + // Do nothing if we're running this in a Robolectric test. + if (FlutterInjector.instance().isRunningInRobolectricTest()) { + return; + } + + if (Looper.myLooper() != Looper.getMainLooper()) { + throw new IllegalStateException("ensureInitializationComplete must be called on the main thread"); + } + if (sSettings == null) { + throw new IllegalStateException("ensureInitializationComplete must be called after startInitialization"); + } + if (sInitialized) { + return; + } + try { + if (sResourceExtractor != null) { + sResourceExtractor.waitForCompletion(); + } + + List shellArgs = new ArrayList<>(); + shellArgs.add("--icu-symbol-prefix=_binary_icudtl_dat"); + + ApplicationInfo applicationInfo = getApplicationInfo(applicationContext); + shellArgs.add("--icu-native-lib-path=" + applicationInfo.nativeLibraryDir + File.separator + DEFAULT_LIBRARY); + + if (args != null) { + Collections.addAll(shellArgs, args); + } + + String kernelPath = null; + if (BuildConfig.DEBUG || BuildConfig.JIT_RELEASE) { + String snapshotAssetPath = PathUtils.getDataDirectory(applicationContext) + File.separator + sFlutterAssetsDir; + kernelPath = snapshotAssetPath + File.separator + DEFAULT_KERNEL_BLOB; + shellArgs.add("--" + SNAPSHOT_ASSET_PATH_KEY + "=" + snapshotAssetPath); + shellArgs.add("--" + VM_SNAPSHOT_DATA_KEY + "=" + sVmSnapshotData); + shellArgs.add("--" + ISOLATE_SNAPSHOT_DATA_KEY + "=" + sIsolateSnapshotData); + } else { + shellArgs.add("--" + AOT_SHARED_LIBRARY_NAME + "=" + sAotSharedLibraryName); + + // Most devices can load the AOT shared library based on the library name + // with no directory path. Provide a fully qualified path to the library + // as a workaround for devices where that fails. + shellArgs.add("--" + AOT_SHARED_LIBRARY_NAME + "=" + applicationInfo.nativeLibraryDir + File.separator + sAotSharedLibraryName); + } + + shellArgs.add("--cache-dir-path=" + PathUtils.getCacheDirectory(applicationContext)); + if (sSettings.getLogTag() != null) { + shellArgs.add("--log-tag=" + sSettings.getLogTag()); + } + + String appStoragePath = PathUtils.getFilesDir(applicationContext); + String engineCachesPath = PathUtils.getCacheDirectory(applicationContext); + FlutterJNI.nativeInit(applicationContext, shellArgs.toArray(new String[0]), + kernelPath, appStoragePath, engineCachesPath); + + sInitialized = true; + } catch (Exception e) { + Log.e(TAG, "Flutter initialization failed.", e); + throw new RuntimeException(e); + } + } + + /** + * Same as {@link #ensureInitializationComplete(Context, String[])} but waiting on a background + * thread, then invoking {@code callback} on the {@code callbackHandler}. + */ + public static void ensureInitializationCompleteAsync( + @NonNull Context applicationContext, + @Nullable String[] args, + @NonNull Handler callbackHandler, + @NonNull Runnable callback + ) { + // Do nothing if we're running this in a Robolectric test. + if (FlutterInjector.instance().isRunningInRobolectricTest()) { + return; + } + + if (Looper.myLooper() != Looper.getMainLooper()) { + throw new IllegalStateException("ensureInitializationComplete must be called on the main thread"); + } + if (sSettings == null) { + throw new IllegalStateException("ensureInitializationComplete must be called after startInitialization"); + } + if (sInitialized) { + return; + } + new Thread(new Runnable() { + @Override + public void run() { + if (sResourceExtractor != null) { + sResourceExtractor.waitForCompletion(); + } + new Handler(Looper.getMainLooper()).post(new Runnable() { + @Override + public void run() { + ensureInitializationComplete(applicationContext.getApplicationContext(), args); + callbackHandler.post(callback); + } + }); + } + }).start(); + } + + @NonNull + private static ApplicationInfo getApplicationInfo(@NonNull Context applicationContext) { + try { + return applicationContext + .getPackageManager() + .getApplicationInfo(applicationContext.getPackageName(), PackageManager.GET_META_DATA); + } catch (PackageManager.NameNotFoundException e) { + throw new RuntimeException(e); + } + } + + /** + * Initialize our Flutter config values by obtaining them from the + * manifest XML file, falling back to default values. + */ + private static void initConfig(@NonNull Context applicationContext) { + Bundle metadata = getApplicationInfo(applicationContext).metaData; + + // There isn't a `` tag as a direct child of `` in + // `AndroidManifest.xml`. + if (metadata == null) { + return; + } + + sAotSharedLibraryName = metadata.getString(PUBLIC_AOT_SHARED_LIBRARY_NAME, DEFAULT_AOT_SHARED_LIBRARY_NAME); + sFlutterAssetsDir = metadata.getString(PUBLIC_FLUTTER_ASSETS_DIR_KEY, DEFAULT_FLUTTER_ASSETS_DIR); + + sVmSnapshotData = metadata.getString(PUBLIC_VM_SNAPSHOT_DATA_KEY, DEFAULT_VM_SNAPSHOT_DATA); + sIsolateSnapshotData = metadata.getString(PUBLIC_ISOLATE_SNAPSHOT_DATA_KEY, DEFAULT_ISOLATE_SNAPSHOT_DATA); + } + + /** + * Extract assets out of the APK that need to be cached as uncompressed + * files on disk. + */ + private static void initResources(@NonNull Context applicationContext) { + new ResourceCleaner(applicationContext).start(); + + if (BuildConfig.DEBUG || BuildConfig.JIT_RELEASE) { + final String dataDirPath = PathUtils.getDataDirectory(applicationContext); + final String packageName = applicationContext.getPackageName(); + final PackageManager packageManager = applicationContext.getPackageManager(); + final AssetManager assetManager = applicationContext.getResources().getAssets(); + sResourceExtractor = new ResourceExtractor(dataDirPath, packageName, packageManager, assetManager); + + // In debug/JIT mode these assets will be written to disk and then + // mapped into memory so they can be provided to the Dart VM. + sResourceExtractor + .addResource(fromFlutterAssets(sVmSnapshotData)) + .addResource(fromFlutterAssets(sIsolateSnapshotData)) + .addResource(fromFlutterAssets(DEFAULT_KERNEL_BLOB)); + + sResourceExtractor.start(); + } + } + + @NonNull + public static String findAppBundlePath() { + return sFlutterAssetsDir; + } + + @Deprecated + @Nullable + public static String findAppBundlePath(@NonNull Context applicationContext) { + return sFlutterAssetsDir; + } + + /** + * Returns the file name for the given asset. + * The returned file name can be used to access the asset in the APK + * through the {@link android.content.res.AssetManager} API. + * + * @param asset the name of the asset. The name can be hierarchical + * @return the filename to be used with {@link android.content.res.AssetManager} + */ + @NonNull + public static String getLookupKeyForAsset(@NonNull String asset) { + return fromFlutterAssets(asset); + } + + /** + * Returns the file name for the given asset which originates from the + * specified packageName. The returned file name can be used to access + * the asset in the APK through the {@link android.content.res.AssetManager} API. + * + * @param asset the name of the asset. The name can be hierarchical + * @param packageName the name of the package from which the asset originates + * @return the file name to be used with {@link android.content.res.AssetManager} + */ + @NonNull + public static String getLookupKeyForAsset(@NonNull String asset, @NonNull String packageName) { + return getLookupKeyForAsset( + "packages" + File.separator + packageName + File.separator + asset); + } +} diff --git a/shell/platform/android/io/flutter/view/ResourceCleaner.java b/shell/platform/android/io/flutter/embedding/engine/loader/ResourceCleaner.java similarity index 98% rename from shell/platform/android/io/flutter/view/ResourceCleaner.java rename to shell/platform/android/io/flutter/embedding/engine/loader/ResourceCleaner.java index c3ac6a325b598..f038726dc8e87 100644 --- a/shell/platform/android/io/flutter/view/ResourceCleaner.java +++ b/shell/platform/android/io/flutter/embedding/engine/loader/ResourceCleaner.java @@ -2,7 +2,7 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -package io.flutter.view; +package io.flutter.embedding.engine.loader; import android.content.Context; import android.os.AsyncTask; diff --git a/shell/platform/android/io/flutter/view/ResourceExtractor.java b/shell/platform/android/io/flutter/embedding/engine/loader/ResourceExtractor.java similarity index 99% rename from shell/platform/android/io/flutter/view/ResourceExtractor.java rename to shell/platform/android/io/flutter/embedding/engine/loader/ResourceExtractor.java index dfba572ee835c..974d0034da1e6 100644 --- a/shell/platform/android/io/flutter/view/ResourceExtractor.java +++ b/shell/platform/android/io/flutter/embedding/engine/loader/ResourceExtractor.java @@ -2,7 +2,7 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -package io.flutter.view; +package io.flutter.embedding.engine.loader; import static java.util.Arrays.asList; diff --git a/shell/platform/android/io/flutter/view/ResourcePaths.java b/shell/platform/android/io/flutter/embedding/engine/loader/ResourcePaths.java similarity index 94% rename from shell/platform/android/io/flutter/view/ResourcePaths.java rename to shell/platform/android/io/flutter/embedding/engine/loader/ResourcePaths.java index 2e2305e0945a6..b2d4ca45c0f9e 100644 --- a/shell/platform/android/io/flutter/view/ResourcePaths.java +++ b/shell/platform/android/io/flutter/embedding/engine/loader/ResourcePaths.java @@ -2,7 +2,7 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -package io.flutter.view; +package io.flutter.embedding.engine.loader; import android.content.Context; diff --git a/shell/platform/android/io/flutter/view/FlutterMain.java b/shell/platform/android/io/flutter/view/FlutterMain.java index 5e1d00b132ba1..80cd42f249476 100644 --- a/shell/platform/android/io/flutter/view/FlutterMain.java +++ b/shell/platform/android/io/flutter/view/FlutterMain.java @@ -5,81 +5,17 @@ package io.flutter.view; import android.content.Context; -import android.content.pm.ApplicationInfo; -import android.content.pm.PackageManager; -import android.content.res.AssetManager; -import android.os.Bundle; import android.os.Handler; -import android.os.Looper; -import android.os.SystemClock; import android.support.annotation.NonNull; import android.support.annotation.Nullable; -import android.support.annotation.VisibleForTesting; -import android.util.Log; -import android.view.WindowManager; -import io.flutter.BuildConfig; -import io.flutter.embedding.engine.FlutterJNI; -import io.flutter.util.PathUtils; - -import java.io.File; -import java.util.*; +import io.flutter.FlutterInjector; +import io.flutter.embedding.engine.loader.FlutterLoader; /** * A class to intialize the Flutter engine. */ public class FlutterMain { - private static final String TAG = "FlutterMain"; - - // Must match values in sky::switches - private static final String AOT_SHARED_LIBRARY_NAME = "aot-shared-library-name"; - private static final String SNAPSHOT_ASSET_PATH_KEY = "snapshot-asset-path"; - private static final String VM_SNAPSHOT_DATA_KEY = "vm-snapshot-data"; - private static final String ISOLATE_SNAPSHOT_DATA_KEY = "isolate-snapshot-data"; - private static final String FLUTTER_ASSETS_DIR_KEY = "flutter-assets-dir"; - - // XML Attribute keys supported in AndroidManifest.xml - public static final String PUBLIC_AOT_SHARED_LIBRARY_NAME = - FlutterMain.class.getName() + '.' + AOT_SHARED_LIBRARY_NAME; - public static final String PUBLIC_VM_SNAPSHOT_DATA_KEY = - FlutterMain.class.getName() + '.' + VM_SNAPSHOT_DATA_KEY; - public static final String PUBLIC_ISOLATE_SNAPSHOT_DATA_KEY = - FlutterMain.class.getName() + '.' + ISOLATE_SNAPSHOT_DATA_KEY; - public static final String PUBLIC_FLUTTER_ASSETS_DIR_KEY = - FlutterMain.class.getName() + '.' + FLUTTER_ASSETS_DIR_KEY; - - // Resource names used for components of the precompiled snapshot. - private static final String DEFAULT_AOT_SHARED_LIBRARY_NAME = "libapp.so"; - private static final String DEFAULT_VM_SNAPSHOT_DATA = "vm_snapshot_data"; - private static final String DEFAULT_ISOLATE_SNAPSHOT_DATA = "isolate_snapshot_data"; - private static final String DEFAULT_LIBRARY = "libflutter.so"; - private static final String DEFAULT_KERNEL_BLOB = "kernel_blob.bin"; - private static final String DEFAULT_FLUTTER_ASSETS_DIR = "flutter_assets"; - - private static boolean isRunningInRobolectricTest = false; - - @VisibleForTesting - public static void setIsRunningInRobolectricTest(boolean isRunningInRobolectricTest) { - FlutterMain.isRunningInRobolectricTest = isRunningInRobolectricTest; - } - - @NonNull - private static String fromFlutterAssets(@NonNull String filePath) { - return sFlutterAssetsDir + File.separator + filePath; - } - - // Mutable because default values can be overridden via config properties - private static String sAotSharedLibraryName = DEFAULT_AOT_SHARED_LIBRARY_NAME; - private static String sVmSnapshotData = DEFAULT_VM_SNAPSHOT_DATA; - private static String sIsolateSnapshotData = DEFAULT_ISOLATE_SNAPSHOT_DATA; - private static String sFlutterAssetsDir = DEFAULT_FLUTTER_ASSETS_DIR; - - private static boolean sInitialized = false; - - @Nullable - private static ResourceExtractor sResourceExtractor; - @Nullable - private static Settings sSettings; public static class Settings { private String logTag; @@ -103,119 +39,36 @@ public void setLogTag(String tag) { * @param applicationContext The Android application context. */ public static void startInitialization(@NonNull Context applicationContext) { - // Do nothing if we're running this in a Robolectric test. - if (isRunningInRobolectricTest) { - return; - } - startInitialization(applicationContext, new Settings()); + FlutterInjector.instance().flutterLoader().startInitialization(applicationContext); } /** * Starts initialization of the native system. + *

+ * This loads the Flutter engine's native library to enable subsequent JNI calls. This also + * starts locating and unpacking Dart resources packaged in the app's APK. + *

+ * Calling this method multiple times has no effect. + * * @param applicationContext The Android application context. * @param settings Configuration settings. */ public static void startInitialization(@NonNull Context applicationContext, @NonNull Settings settings) { - // Do nothing if we're running this in a Robolectric test. - if (isRunningInRobolectricTest) { - return; - } - - if (Looper.myLooper() != Looper.getMainLooper()) { - throw new IllegalStateException("startInitialization must be called on the main thread"); - } - // Do not run startInitialization more than once. - if (sSettings != null) { - return; - } - - sSettings = settings; - - long initStartTimestampMillis = SystemClock.uptimeMillis(); - initConfig(applicationContext); - initResources(applicationContext); - - System.loadLibrary("flutter"); - - VsyncWaiter - .getInstance((WindowManager) applicationContext.getSystemService(Context.WINDOW_SERVICE)) - .init(); - - // We record the initialization time using SystemClock because at the start of the - // initialization we have not yet loaded the native library to call into dart_tools_api.h. - // To get Timeline timestamp of the start of initialization we simply subtract the delta - // from the Timeline timestamp at the current moment (the assumption is that the overhead - // of the JNI call is negligible). - long initTimeMillis = SystemClock.uptimeMillis() - initStartTimestampMillis; - FlutterJNI.nativeRecordStartTimestamp(initTimeMillis); + FlutterLoader.Settings newSettings = new FlutterLoader.Settings(); + newSettings.setLogTag(settings.getLogTag()); + FlutterInjector.instance().flutterLoader().startInitialization(applicationContext, newSettings); } /** * Blocks until initialization of the native system has completed. + *

+ * Calling this method multiple times has no effect. + * * @param applicationContext The Android application context. * @param args Flags sent to the Flutter runtime. */ public static void ensureInitializationComplete(@NonNull Context applicationContext, @Nullable String[] args) { - // Do nothing if we're running this in a Robolectric test. - if (isRunningInRobolectricTest) { - return; - } - - if (Looper.myLooper() != Looper.getMainLooper()) { - throw new IllegalStateException("ensureInitializationComplete must be called on the main thread"); - } - if (sSettings == null) { - throw new IllegalStateException("ensureInitializationComplete must be called after startInitialization"); - } - if (sInitialized) { - return; - } - try { - if (sResourceExtractor != null) { - sResourceExtractor.waitForCompletion(); - } - - List shellArgs = new ArrayList<>(); - shellArgs.add("--icu-symbol-prefix=_binary_icudtl_dat"); - - ApplicationInfo applicationInfo = getApplicationInfo(applicationContext); - shellArgs.add("--icu-native-lib-path=" + applicationInfo.nativeLibraryDir + File.separator + DEFAULT_LIBRARY); - - if (args != null) { - Collections.addAll(shellArgs, args); - } - - String kernelPath = null; - if (BuildConfig.DEBUG || BuildConfig.JIT_RELEASE) { - String snapshotAssetPath = PathUtils.getDataDirectory(applicationContext) + File.separator + sFlutterAssetsDir; - kernelPath = snapshotAssetPath + File.separator + DEFAULT_KERNEL_BLOB; - shellArgs.add("--" + SNAPSHOT_ASSET_PATH_KEY + "=" + snapshotAssetPath); - shellArgs.add("--" + VM_SNAPSHOT_DATA_KEY + "=" + sVmSnapshotData); - shellArgs.add("--" + ISOLATE_SNAPSHOT_DATA_KEY + "=" + sIsolateSnapshotData); - } else { - shellArgs.add("--" + AOT_SHARED_LIBRARY_NAME + "=" + sAotSharedLibraryName); - - // Most devices can load the AOT shared library based on the library name - // with no directory path. Provide a fully qualified path to the library - // as a workaround for devices where that fails. - shellArgs.add("--" + AOT_SHARED_LIBRARY_NAME + "=" + applicationInfo.nativeLibraryDir + File.separator + sAotSharedLibraryName); - } - - shellArgs.add("--cache-dir-path=" + PathUtils.getCacheDirectory(applicationContext)); - if (sSettings.getLogTag() != null) { - shellArgs.add("--log-tag=" + sSettings.getLogTag()); - } - - String appStoragePath = PathUtils.getFilesDir(applicationContext); - String engineCachesPath = PathUtils.getCacheDirectory(applicationContext); - FlutterJNI.nativeInit(applicationContext, shellArgs.toArray(new String[0]), - kernelPath, appStoragePath, engineCachesPath); - - sInitialized = true; - } catch (Exception e) { - Log.e(TAG, "Flutter initialization failed.", e); - throw new RuntimeException(e); - } + FlutterInjector.instance().flutterLoader().ensureInitializationComplete(applicationContext, args); } /** @@ -228,102 +81,19 @@ public static void ensureInitializationCompleteAsync( @NonNull Handler callbackHandler, @NonNull Runnable callback ) { - // Do nothing if we're running this in a Robolectric test. - if (isRunningInRobolectricTest) { - return; - } - - if (Looper.myLooper() != Looper.getMainLooper()) { - throw new IllegalStateException("ensureInitializationComplete must be called on the main thread"); - } - if (sSettings == null) { - throw new IllegalStateException("ensureInitializationComplete must be called after startInitialization"); - } - if (sInitialized) { - return; - } - new Thread(new Runnable() { - @Override - public void run() { - if (sResourceExtractor != null) { - sResourceExtractor.waitForCompletion(); - } - new Handler(Looper.getMainLooper()).post(new Runnable() { - @Override - public void run() { - ensureInitializationComplete(applicationContext.getApplicationContext(), args); - callbackHandler.post(callback); - } - }); - } - }).start(); - } - - @NonNull - private static ApplicationInfo getApplicationInfo(@NonNull Context applicationContext) { - try { - return applicationContext - .getPackageManager() - .getApplicationInfo(applicationContext.getPackageName(), PackageManager.GET_META_DATA); - } catch (PackageManager.NameNotFoundException e) { - throw new RuntimeException(e); - } - } - - /** - * Initialize our Flutter config values by obtaining them from the - * manifest XML file, falling back to default values. - */ - private static void initConfig(@NonNull Context applicationContext) { - Bundle metadata = getApplicationInfo(applicationContext).metaData; - - // There isn't a `` tag as a direct child of `` in - // `AndroidManifest.xml`. - if (metadata == null) { - return; - } - - sAotSharedLibraryName = metadata.getString(PUBLIC_AOT_SHARED_LIBRARY_NAME, DEFAULT_AOT_SHARED_LIBRARY_NAME); - sFlutterAssetsDir = metadata.getString(PUBLIC_FLUTTER_ASSETS_DIR_KEY, DEFAULT_FLUTTER_ASSETS_DIR); - - sVmSnapshotData = metadata.getString(PUBLIC_VM_SNAPSHOT_DATA_KEY, DEFAULT_VM_SNAPSHOT_DATA); - sIsolateSnapshotData = metadata.getString(PUBLIC_ISOLATE_SNAPSHOT_DATA_KEY, DEFAULT_ISOLATE_SNAPSHOT_DATA); - } - - /** - * Extract assets out of the APK that need to be cached as uncompressed - * files on disk. - */ - private static void initResources(@NonNull Context applicationContext) { - new ResourceCleaner(applicationContext).start(); - - if (BuildConfig.DEBUG || BuildConfig.JIT_RELEASE) { - final String dataDirPath = PathUtils.getDataDirectory(applicationContext); - final String packageName = applicationContext.getPackageName(); - final PackageManager packageManager = applicationContext.getPackageManager(); - final AssetManager assetManager = applicationContext.getResources().getAssets(); - sResourceExtractor = new ResourceExtractor(dataDirPath, packageName, packageManager, assetManager); - - // In debug/JIT mode these assets will be written to disk and then - // mapped into memory so they can be provided to the Dart VM. - sResourceExtractor - .addResource(fromFlutterAssets(sVmSnapshotData)) - .addResource(fromFlutterAssets(sIsolateSnapshotData)) - .addResource(fromFlutterAssets(DEFAULT_KERNEL_BLOB)); - - sResourceExtractor.start(); - } + FlutterInjector.instance().flutterLoader().ensureInitializationCompleteAsync( + applicationContext, args, callbackHandler, callback); } @NonNull public static String findAppBundlePath() { - return sFlutterAssetsDir; + return FlutterInjector.instance().flutterLoader().findAppBundlePath(); } @Deprecated @Nullable public static String findAppBundlePath(@NonNull Context applicationContext) { - return sFlutterAssetsDir; + return FlutterInjector.instance().flutterLoader().findAppBundlePath(applicationContext); } /** @@ -336,7 +106,7 @@ public static String findAppBundlePath(@NonNull Context applicationContext) { */ @NonNull public static String getLookupKeyForAsset(@NonNull String asset) { - return fromFlutterAssets(asset); + return FlutterInjector.instance().flutterLoader().getLookupKeyForAsset(asset); } /** @@ -350,7 +120,6 @@ public static String getLookupKeyForAsset(@NonNull String asset) { */ @NonNull public static String getLookupKeyForAsset(@NonNull String asset, @NonNull String packageName) { - return getLookupKeyForAsset( - "packages" + File.separator + packageName + File.separator + asset); + return FlutterInjector.instance().flutterLoader().getLookupKeyForAsset(asset, packageName); } } diff --git a/shell/platform/android/test/io/flutter/FlutterInjectorTest.java b/shell/platform/android/test/io/flutter/FlutterInjectorTest.java new file mode 100644 index 0000000000000..568d87fa38429 --- /dev/null +++ b/shell/platform/android/test/io/flutter/FlutterInjectorTest.java @@ -0,0 +1,57 @@ +// 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; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.Robolectric; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.RuntimeEnvironment; +import org.robolectric.android.controller.ActivityController; +import org.robolectric.annotation.Config; + +import io.flutter.FlutterInjector; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; + +import java.lang.IllegalStateException; + +@Config(manifest=Config.NONE) +@RunWith(RobolectricTestRunner.class) +public class FlutterInjectorTest { + @Before + public void setUp() { + FlutterInjector.reset(); + } + + @Test + public void itHasSomeReasonableDefaults() { + FlutterInjector injector = FlutterInjector.instance(); + assertFalse(injector.isRunningInRobolectricTest()); + assertNotNull(injector.flutterLoader()); + } + + @Test + public void canPartiallyOverride() { + FlutterInjector.setInstance( + new FlutterInjector.Builder().setIsRunningInRobolectricTest(true).build()); + FlutterInjector injector = FlutterInjector.instance(); + assertTrue(injector.isRunningInRobolectricTest()); + assertNotNull(injector.flutterLoader()); + } + + @Test(expected = IllegalStateException.class) + public void cannotBeChangedOnceRead() { + FlutterInjector.instance(); + FlutterInjector.setInstance( + new FlutterInjector.Builder().setIsRunningInRobolectricTest(true).build()); + } +} diff --git a/shell/platform/android/test/io/flutter/FlutterTestSuite.java b/shell/platform/android/test/io/flutter/FlutterTestSuite.java index 59980291db489..7899ec60af673 100644 --- a/shell/platform/android/test/io/flutter/FlutterTestSuite.java +++ b/shell/platform/android/test/io/flutter/FlutterTestSuite.java @@ -8,6 +8,7 @@ import org.junit.runners.Suite; import org.junit.runners.Suite.SuiteClasses; +import io.flutter.FlutterInjectorTest; import io.flutter.embedding.android.FlutterActivityTest; import io.flutter.embedding.android.FlutterFragmentTest; import io.flutter.embedding.engine.FlutterEngineCacheTest; @@ -26,6 +27,7 @@ FlutterActivityTest.class, FlutterEngineCacheTest.class, FlutterFragmentTest.class, + FlutterInjectorTest.class, FlutterJNITest.class, FlutterRendererTest.class, PlatformChannelTest.class, diff --git a/shell/platform/android/test/io/flutter/embedding/android/FlutterActivityAndFragmentDelegateTest.java b/shell/platform/android/test/io/flutter/embedding/android/FlutterActivityAndFragmentDelegateTest.java index 953abb5a053e9..77abc73db523c 100644 --- a/shell/platform/android/test/io/flutter/embedding/android/FlutterActivityAndFragmentDelegateTest.java +++ b/shell/platform/android/test/io/flutter/embedding/android/FlutterActivityAndFragmentDelegateTest.java @@ -8,6 +8,7 @@ import org.junit.After; import org.junit.Before; +import org.junit.BeforeClass; import org.junit.Test; import org.junit.runner.RunWith; import org.robolectric.Robolectric; @@ -15,6 +16,7 @@ import org.robolectric.RuntimeEnvironment; import org.robolectric.annotation.Config; +import io.flutter.FlutterInjector; import io.flutter.embedding.engine.FlutterEngine; import io.flutter.embedding.engine.FlutterEngineCache; import io.flutter.embedding.engine.FlutterShellArgs; @@ -47,12 +49,16 @@ public class FlutterActivityAndFragmentDelegateTest { private FlutterEngine mockFlutterEngine; private FlutterActivityAndFragmentDelegate.Host mockHost; - @Before - public void setup() { - // FlutterMain is utilized statically, therefore we need to inform it to behave differently + @BeforeClass + public static void setupInjector() { + // FlutterLoader is utilized statically, therefore we need to inform it to behave differently // for testing purposes. - FlutterMain.setIsRunningInRobolectricTest(true); + FlutterInjector.setInstance( + new FlutterInjector.Builder().setIsRunningInRobolectricTest(true).build()); + } + @Before + public void setup() { // Create a mocked FlutterEngine for the various interactions required by the delegate // being tested. mockFlutterEngine = mockFlutterEngine(); @@ -73,12 +79,6 @@ public void setup() { when(mockHost.shouldDestroyEngineWithHost()).thenReturn(true); } - @After - public void teardown() { - // Return FlutterMain to normal. - FlutterMain.setIsRunningInRobolectricTest(false); - } - @Test public void itSendsLifecycleEventsToFlutter() { // ---- Test setup ---- diff --git a/testing/scenario_app/README.md b/testing/scenario_app/README.md index 8ecbf6c0312e0..082794fb8e0da 100644 --- a/testing/scenario_app/README.md +++ b/testing/scenario_app/README.md @@ -45,3 +45,8 @@ the app in the `android/` folder. The app can be run by opening it in Android Studio and running it, or by running `./gradlew assemble` in the `android/` folder and installing the APK from the correct folder in `android/app/build/outputs/apk`. + +## Changing dart:ui code + +If you change the dart:ui interface, remember to point the sky_engine and +sky_services clauses to your local engine's output path before compiling. \ No newline at end of file diff --git a/testing/scenario_app/android/app/build.gradle b/testing/scenario_app/android/app/build.gradle index 473a71e1f008f..0fbd03be46952 100644 --- a/testing/scenario_app/android/app/build.gradle +++ b/testing/scenario_app/android/app/build.gradle @@ -27,8 +27,9 @@ dependencies { implementation 'com.android.support:appcompat-v7:28.0.0' implementation 'com.android.support.constraint:constraint-layout:1.1.3' implementation 'com.android.support:design:28.0.0' - implementation 'android.arch.lifecycle:common-java8:1.1.0' + implementation 'android.arch.lifecycle:common-java8:1.1.1' testImplementation 'junit:junit:4.12' androidTestImplementation 'com.android.support.test:runner:1.0.2' + androidTestImplementation 'com.android.support.test:rules:1.0.2' androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2' } diff --git a/testing/scenario_app/android/app/src/androidTest/java/dev/flutter/scenarios/EngineLaunchE2ETest.java b/testing/scenario_app/android/app/src/androidTest/java/dev/flutter/scenarios/EngineLaunchE2ETest.java new file mode 100644 index 0000000000000..2f86d55fedfa5 --- /dev/null +++ b/testing/scenario_app/android/app/src/androidTest/java/dev/flutter/scenarios/EngineLaunchE2ETest.java @@ -0,0 +1,69 @@ +// 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 dev.flutter.scenarios; + +import android.content.Context; +import android.support.test.InstrumentationRegistry; +import android.support.test.internal.runner.junit4.statement.UiThreadStatement; +import android.support.test.runner.AndroidJUnit4; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.concurrent.atomic.AtomicReference; + +import io.flutter.embedding.engine.FlutterEngine; +import io.flutter.embedding.engine.dart.DartExecutor; + +import static org.junit.Assert.fail; + +@RunWith(AndroidJUnit4.class) +public class EngineLaunchE2ETest { + @Test + public void smokeTestEngineLaunch() throws Throwable { + Context applicationContext = InstrumentationRegistry.getTargetContext(); + // Specifically, create the engine without running FlutterMain first. + final AtomicReference engine = new AtomicReference<>(); + + // Run the production under test on the UI thread instead of annotating the whole test + // as @UiThreadTest because having the message handler and the CompletableFuture both being + // on the same thread will create deadlocks. + UiThreadStatement.runOnUiThread( + () -> engine.set(new FlutterEngine(applicationContext)) + ); + CompletableFuture statusReceived = new CompletableFuture<>(); + + // The default Dart main entrypoint sends back a platform message on the "scenario_status" + // channel. That will be our launch success assertion condition. + engine.get().getDartExecutor().setMessageHandler( + "scenario_status", + (byteBuffer, binaryReply) -> statusReceived.complete(Boolean.TRUE) + ); + + // Launching the entrypoint will run the Dart code that sends the "scenario_status" platform + // message. + UiThreadStatement.runOnUiThread( + () -> engine.get().getDartExecutor().executeDartEntrypoint(DartExecutor.DartEntrypoint.createDefault()) + ); + + try { + Boolean result = statusReceived.get(10, TimeUnit.SECONDS); + if (!result) { + fail("expected message on scenario_status not received"); + } + } catch (ExecutionException e) { + fail(e.getMessage()); + } catch (InterruptedException e) { + fail(e.getMessage()); + } catch (TimeoutException e) { + fail("timed out waiting for engine started signal"); + } + // If it gets to here, statusReceived is true. + } +} diff --git a/testing/scenario_app/android/app/src/main/AndroidManifest.xml b/testing/scenario_app/android/app/src/main/AndroidManifest.xml index 039498df0e9e0..5b9a040276c88 100644 --- a/testing/scenario_app/android/app/src/main/AndroidManifest.xml +++ b/testing/scenario_app/android/app/src/main/AndroidManifest.xml @@ -1,7 +1,7 @@ - + - - - + + + + + + + - - + + + + + + \ No newline at end of file diff --git a/testing/scenario_app/android/app/src/main/java/dev/flutter/scenarios/BlankActivity.java b/testing/scenario_app/android/app/src/main/java/dev/flutter/scenarios/BlankActivity.java new file mode 100644 index 0000000000000..01560e71902cb --- /dev/null +++ b/testing/scenario_app/android/app/src/main/java/dev/flutter/scenarios/BlankActivity.java @@ -0,0 +1,11 @@ +package dev.flutter.scenarios; + +import android.support.v7.app.AppCompatActivity; +import android.os.Bundle; + +public class BlankActivity extends AppCompatActivity { + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + } +} diff --git a/testing/scenario_app/android/app/src/main/java/dev/flutter/scenarios/MainActivity.java b/testing/scenario_app/android/app/src/main/java/dev/flutter/scenarios/TextPlatformViewActivity.java similarity index 94% rename from testing/scenario_app/android/app/src/main/java/dev/flutter/scenarios/MainActivity.java rename to testing/scenario_app/android/app/src/main/java/dev/flutter/scenarios/TextPlatformViewActivity.java index 2d237bdbb10cc..cf98dabc8b12d 100644 --- a/testing/scenario_app/android/app/src/main/java/dev/flutter/scenarios/MainActivity.java +++ b/testing/scenario_app/android/app/src/main/java/dev/flutter/scenarios/TextPlatformViewActivity.java @@ -15,14 +15,12 @@ import io.flutter.Log; import io.flutter.embedding.android.FlutterActivity; -import io.flutter.embedding.android.FlutterFragment; -import io.flutter.embedding.android.FlutterView; import io.flutter.embedding.engine.FlutterEngine; import io.flutter.embedding.engine.FlutterShellArgs; import io.flutter.plugin.common.BasicMessageChannel; import io.flutter.plugin.common.BinaryCodec; -public class MainActivity extends FlutterActivity { +public class TextPlatformViewActivity extends FlutterActivity { final static String TAG = "Scenarios"; @Override diff --git a/testing/scenario_app/android/build.gradle b/testing/scenario_app/android/build.gradle index e11a5b354c4d4..f5fb2ccce69da 100644 --- a/testing/scenario_app/android/build.gradle +++ b/testing/scenario_app/android/build.gradle @@ -7,7 +7,7 @@ buildscript { } dependencies { - classpath 'com.android.tools.build:gradle:3.3.2' + classpath 'com.android.tools.build:gradle:3.5.0' // NOTE: Do not place your application dependencies here; they belong // in the individual module build.gradle files diff --git a/testing/scenario_app/run_android_tests.sh b/testing/scenario_app/run_android_tests.sh new file mode 100755 index 0000000000000..fc173e11af796 --- /dev/null +++ b/testing/scenario_app/run_android_tests.sh @@ -0,0 +1,24 @@ +#!/bin/sh +# 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. + +# Runs the Android scenario tests on a connected device. + +set -e + +FLUTTER_ENGINE=android_profile_unopt_arm64 + +if [ $# -eq 1 ]; then + FLUTTER_ENGINE=$1 +fi + +cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd + +./compile_android_aot.sh ../../../out/host_profile_unopt_arm64 ../../../out/$FLUTTER_ENGINE/clang_x64 + +pushd android + +set -o pipefail && ./gradlew assembleAndroidTest && ./gradlew connectedAndroidTest + +popd From fe4e9348fd93c915456c87e6af1e814f0ea314aa Mon Sep 17 00:00:00 2001 From: Matt Carroll Date: Fri, 4 Oct 2019 13:51:23 -0700 Subject: [PATCH 2/3] Remove FlutterInjector and use constructor injection instead. --- ci/licenses_golden/licenses_flutter | 1 - shell/platform/android/BUILD.gn | 2 - .../android/io/flutter/FlutterInjector.java | 114 ------------ .../embedding/engine/FlutterEngine.java | 13 +- .../engine/loader/FlutterLoader.java | 174 ++++++++---------- .../android/io/flutter/view/FlutterMain.java | 17 +- .../test/io/flutter/FlutterInjectorTest.java | 57 ------ .../test/io/flutter/FlutterTestSuite.java | 2 - ...lutterActivityAndFragmentDelegateTest.java | 12 -- 9 files changed, 94 insertions(+), 298 deletions(-) delete mode 100644 shell/platform/android/io/flutter/FlutterInjector.java delete mode 100644 shell/platform/android/test/io/flutter/FlutterInjectorTest.java diff --git a/ci/licenses_golden/licenses_flutter b/ci/licenses_golden/licenses_flutter index 1bf46053bfc99..12d3b72cb894b 100644 --- a/ci/licenses_golden/licenses_flutter +++ b/ci/licenses_golden/licenses_flutter @@ -565,7 +565,6 @@ FILE: ../../../flutter/shell/platform/android/apk_asset_provider.cc FILE: ../../../flutter/shell/platform/android/apk_asset_provider.h FILE: ../../../flutter/shell/platform/android/flutter_main.cc FILE: ../../../flutter/shell/platform/android/flutter_main.h -FILE: ../../../flutter/shell/platform/android/io/flutter/FlutterInjector.java FILE: ../../../flutter/shell/platform/android/io/flutter/Log.java FILE: ../../../flutter/shell/platform/android/io/flutter/app/FlutterActivity.java FILE: ../../../flutter/shell/platform/android/io/flutter/app/FlutterActivityDelegate.java diff --git a/shell/platform/android/BUILD.gn b/shell/platform/android/BUILD.gn index d999c0ea5f22c..c2f3d1f41e32f 100644 --- a/shell/platform/android/BUILD.gn +++ b/shell/platform/android/BUILD.gn @@ -117,7 +117,6 @@ embedding_sources_jar_filename = "$embedding_artifact_id-sources.jar" embedding_source_jar_path = "$root_out_dir/$embedding_sources_jar_filename" android_java_sources = [ - "io/flutter/FlutterInjector.java", "io/flutter/Log.java", "io/flutter/app/FlutterActivity.java", "io/flutter/app/FlutterActivityDelegate.java", @@ -413,7 +412,6 @@ action("robolectric_tests") { jar_path = "$root_out_dir/robolectric_tests.jar" sources = [ - "test/io/flutter/FlutterInjectorTest.java", "test/io/flutter/FlutterTestSuite.java", "test/io/flutter/SmokeTest.java", "test/io/flutter/embedding/android/FlutterActivityAndFragmentDelegateTest.java", diff --git a/shell/platform/android/io/flutter/FlutterInjector.java b/shell/platform/android/io/flutter/FlutterInjector.java deleted file mode 100644 index 542c01b08545d..0000000000000 --- a/shell/platform/android/io/flutter/FlutterInjector.java +++ /dev/null @@ -1,114 +0,0 @@ -// 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; - -import android.support.annotation.NonNull; -import android.support.annotation.VisibleForTesting; - -import io.flutter.embedding.engine.loader.FlutterLoader; - -import java.lang.IllegalStateException; - -/* - * This class is a simple dependency injector for the Android part of the Flutter engine. - * - * This simple solution is used facilitate testability without bringing in heavier app-development - * centric dependency injection frameworks such as Guice or Dagger2. - */ -public final class FlutterInjector { - - private static FlutterInjector instance; - private static boolean accessed; - - /* - * Use {@link FlutterInjector.Builder} to specify members to be injected via the static - * {@code FlutterInjector}. - * - * This can only be called at the beginning of the program before the {@link #instance()} is - * accessed. - */ - public static void setInstance(@NonNull FlutterInjector injector) { - if (accessed) { - throw new IllegalStateException("Cannot change the FlutterInjector instance once it's been " + - "read. If you're trying to dependency inject, be sure to do so at the beginning of " + - "the program"); - } - instance = injector; - } - - /* - * Retrieve the static instance of the {@code FlutterInjector} to use in your program. - * - * Once you access it, you can no longer change the values injected. - * - * If no override is provided for the injector, reasonable defaults are provided. - */ - public static FlutterInjector instance() { - accessed = true; - if (instance == null) { - instance = new Builder().build(); - } - return instance; - } - - // This whole class is here to enable testing so to test the thing that lets you test, some degree - // of hack is needed. - @VisibleForTesting - /* Package default */ static void reset() { - accessed = false; - instance = null; - } - - private FlutterInjector( - boolean isRunningInRobolectricTest, - @NonNull FlutterLoader flutterLoader - ) { - this.isRunningInRobolectricTest = isRunningInRobolectricTest; - this.flutterLoader = flutterLoader; - } - - private boolean isRunningInRobolectricTest; - private FlutterLoader flutterLoader; - - public boolean isRunningInRobolectricTest() { - return isRunningInRobolectricTest; - } - - @NonNull - public FlutterLoader flutterLoader() { - return flutterLoader; - } - - /* - * Builder used to supply a custom FlutterInjector instance to - * {@link FlutterInjector#setInstance(FlutterInjector)}. - * - * Non-overriden values have reasonable defaults. - */ - public static final class Builder { - - private boolean isRunningInRobolectricTest = false; - public Builder setIsRunningInRobolectricTest(boolean isRunningInRobolectricTest) { - this.isRunningInRobolectricTest = isRunningInRobolectricTest; - return this; - } - - private FlutterLoader flutterLoader; - public Builder setFlutterLoader(@NonNull FlutterLoader flutterLoader) { - this.flutterLoader = flutterLoader; - return this; - } - - public FlutterInjector build() { - if (flutterLoader == null) { - flutterLoader = new FlutterLoader(); - } - - return new FlutterInjector(isRunningInRobolectricTest, flutterLoader); - } - - } - -} \ No newline at end of file diff --git a/shell/platform/android/io/flutter/embedding/engine/FlutterEngine.java b/shell/platform/android/io/flutter/embedding/engine/FlutterEngine.java index 8c327d8b35010..42b0129d7298f 100644 --- a/shell/platform/android/io/flutter/embedding/engine/FlutterEngine.java +++ b/shell/platform/android/io/flutter/embedding/engine/FlutterEngine.java @@ -12,7 +12,6 @@ import java.util.HashSet; import java.util.Set; -import io.flutter.FlutterInjector; import io.flutter.Log; import io.flutter.embedding.engine.dart.DartExecutor; import io.flutter.embedding.engine.loader.FlutterLoader; @@ -140,13 +139,17 @@ public void onPreEngineRestart() { * native library and start a Dart VM. *

* In order to pass Dart VM initialization arguments (see {@link io.flutter.embedding.engine.FlutterShellArgs}) - * when creating the VM, manually set the initialization arguments by calling {@link FlutterMain#startInitialization(io.flutter.view.Context)} - * and {@link FlutterMain#ensureInitializationComplete(io.flutter.view.Context, String[])}. + * when creating the VM, manually set the initialization arguments by calling {@link FlutterLoader#startInitialization(Context)} + * and {@link FlutterLoader#ensureInitializationComplete(Context, String[])}. */ public FlutterEngine(@NonNull Context context) { - FlutterLoader flutterLoader = FlutterInjector.instance().flutterLoader(); + this(context, FlutterLoader.getInstance()); + } + + /* package */ FlutterEngine(@NonNull Context context, @NonNull FlutterLoader flutterLoader) { flutterLoader.startInitialization(context); - FlutterLoader.ensureInitializationComplete(context, null); + flutterLoader.ensureInitializationComplete(context, null); + this.flutterJNI = new FlutterJNI(); flutterJNI.addEngineLifecycleListener(engineLifecycleListener); attachToJni(); diff --git a/shell/platform/android/io/flutter/embedding/engine/loader/FlutterLoader.java b/shell/platform/android/io/flutter/embedding/engine/loader/FlutterLoader.java index 849f1fb55eda3..153306019874d 100644 --- a/shell/platform/android/io/flutter/embedding/engine/loader/FlutterLoader.java +++ b/shell/platform/android/io/flutter/embedding/engine/loader/FlutterLoader.java @@ -14,11 +14,9 @@ import android.os.SystemClock; import android.support.annotation.NonNull; import android.support.annotation.Nullable; -import android.support.annotation.VisibleForTesting; import android.util.Log; import android.view.WindowManager; -import io.flutter.FlutterInjector; import io.flutter.BuildConfig; import io.flutter.embedding.engine.FlutterJNI; import io.flutter.util.PathUtils; @@ -58,50 +56,33 @@ public class FlutterLoader { private static final String DEFAULT_KERNEL_BLOB = "kernel_blob.bin"; private static final String DEFAULT_FLUTTER_ASSETS_DIR = "flutter_assets"; - @NonNull - private static String fromFlutterAssets(@NonNull String filePath) { - return sFlutterAssetsDir + File.separator + filePath; - } - // Mutable because default values can be overridden via config properties - private static String sAotSharedLibraryName = DEFAULT_AOT_SHARED_LIBRARY_NAME; - private static String sVmSnapshotData = DEFAULT_VM_SNAPSHOT_DATA; - private static String sIsolateSnapshotData = DEFAULT_ISOLATE_SNAPSHOT_DATA; - private static String sFlutterAssetsDir = DEFAULT_FLUTTER_ASSETS_DIR; - - private static boolean sInitialized = false; + private String aotSharedLibraryName = DEFAULT_AOT_SHARED_LIBRARY_NAME; + private String vmSnapshotData = DEFAULT_VM_SNAPSHOT_DATA; + private String isolateSnapshotData = DEFAULT_ISOLATE_SNAPSHOT_DATA; + private String flutterAssetsDir = DEFAULT_FLUTTER_ASSETS_DIR; - @Nullable - private static ResourceExtractor sResourceExtractor; - @Nullable - private static Settings sSettings; + private static FlutterLoader instance; - public static class Settings { - private String logTag; - - @Nullable - public String getLogTag() { - return logTag; - } - - /** - * Set the tag associated with Flutter app log messages. - * @param tag Log tag. - */ - public void setLogTag(String tag) { - logTag = tag; + @NonNull + public static FlutterLoader getInstance() { + if (instance == null) { + instance = new FlutterLoader(); } + return instance; } + private boolean initialized = false; + @Nullable + private ResourceExtractor resourceExtractor; + @Nullable + private Settings settings; + /** * Starts initialization of the native system. * @param applicationContext The Android application context. */ - public static void startInitialization(@NonNull Context applicationContext) { - // Do nothing if we're running this in a Robolectric test. - if (FlutterInjector.instance().isRunningInRobolectricTest()) { - return; - } + public void startInitialization(@NonNull Context applicationContext) { startInitialization(applicationContext, new Settings()); } @@ -116,21 +97,16 @@ public static void startInitialization(@NonNull Context applicationContext) { * @param applicationContext The Android application context. * @param settings Configuration settings. */ - public static void startInitialization(@NonNull Context applicationContext, @NonNull Settings settings) { - // Do nothing if we're running this in a Robolectric test. - if (FlutterInjector.instance().isRunningInRobolectricTest()) { - return; - } - + public void startInitialization(@NonNull Context applicationContext, @NonNull Settings settings) { if (Looper.myLooper() != Looper.getMainLooper()) { throw new IllegalStateException("startInitialization must be called on the main thread"); } // Do not run startInitialization more than once. - if (sSettings != null) { + if (this.settings != null) { return; } - sSettings = settings; + this.settings = settings; long initStartTimestampMillis = SystemClock.uptimeMillis(); initConfig(applicationContext); @@ -159,24 +135,19 @@ public static void startInitialization(@NonNull Context applicationContext, @Non * @param applicationContext The Android application context. * @param args Flags sent to the Flutter runtime. */ - public static void ensureInitializationComplete(@NonNull Context applicationContext, @Nullable String[] args) { - // Do nothing if we're running this in a Robolectric test. - if (FlutterInjector.instance().isRunningInRobolectricTest()) { - return; - } - + public void ensureInitializationComplete(@NonNull Context applicationContext, @Nullable String[] args) { if (Looper.myLooper() != Looper.getMainLooper()) { throw new IllegalStateException("ensureInitializationComplete must be called on the main thread"); } - if (sSettings == null) { + if (settings == null) { throw new IllegalStateException("ensureInitializationComplete must be called after startInitialization"); } - if (sInitialized) { + if (initialized) { return; } try { - if (sResourceExtractor != null) { - sResourceExtractor.waitForCompletion(); + if (resourceExtractor != null) { + resourceExtractor.waitForCompletion(); } List shellArgs = new ArrayList<>(); @@ -191,23 +162,23 @@ public static void ensureInitializationComplete(@NonNull Context applicationCont String kernelPath = null; if (BuildConfig.DEBUG || BuildConfig.JIT_RELEASE) { - String snapshotAssetPath = PathUtils.getDataDirectory(applicationContext) + File.separator + sFlutterAssetsDir; + String snapshotAssetPath = PathUtils.getDataDirectory(applicationContext) + File.separator + flutterAssetsDir; kernelPath = snapshotAssetPath + File.separator + DEFAULT_KERNEL_BLOB; shellArgs.add("--" + SNAPSHOT_ASSET_PATH_KEY + "=" + snapshotAssetPath); - shellArgs.add("--" + VM_SNAPSHOT_DATA_KEY + "=" + sVmSnapshotData); - shellArgs.add("--" + ISOLATE_SNAPSHOT_DATA_KEY + "=" + sIsolateSnapshotData); + shellArgs.add("--" + VM_SNAPSHOT_DATA_KEY + "=" + vmSnapshotData); + shellArgs.add("--" + ISOLATE_SNAPSHOT_DATA_KEY + "=" + isolateSnapshotData); } else { - shellArgs.add("--" + AOT_SHARED_LIBRARY_NAME + "=" + sAotSharedLibraryName); + shellArgs.add("--" + AOT_SHARED_LIBRARY_NAME + "=" + aotSharedLibraryName); // Most devices can load the AOT shared library based on the library name // with no directory path. Provide a fully qualified path to the library // as a workaround for devices where that fails. - shellArgs.add("--" + AOT_SHARED_LIBRARY_NAME + "=" + applicationInfo.nativeLibraryDir + File.separator + sAotSharedLibraryName); + shellArgs.add("--" + AOT_SHARED_LIBRARY_NAME + "=" + applicationInfo.nativeLibraryDir + File.separator + aotSharedLibraryName); } shellArgs.add("--cache-dir-path=" + PathUtils.getCacheDirectory(applicationContext)); - if (sSettings.getLogTag() != null) { - shellArgs.add("--log-tag=" + sSettings.getLogTag()); + if (settings.getLogTag() != null) { + shellArgs.add("--log-tag=" + settings.getLogTag()); } String appStoragePath = PathUtils.getFilesDir(applicationContext); @@ -215,7 +186,7 @@ public static void ensureInitializationComplete(@NonNull Context applicationCont FlutterJNI.nativeInit(applicationContext, shellArgs.toArray(new String[0]), kernelPath, appStoragePath, engineCachesPath); - sInitialized = true; + initialized = true; } catch (Exception e) { Log.e(TAG, "Flutter initialization failed.", e); throw new RuntimeException(e); @@ -226,31 +197,26 @@ public static void ensureInitializationComplete(@NonNull Context applicationCont * Same as {@link #ensureInitializationComplete(Context, String[])} but waiting on a background * thread, then invoking {@code callback} on the {@code callbackHandler}. */ - public static void ensureInitializationCompleteAsync( + public void ensureInitializationCompleteAsync( @NonNull Context applicationContext, @Nullable String[] args, @NonNull Handler callbackHandler, @NonNull Runnable callback ) { - // Do nothing if we're running this in a Robolectric test. - if (FlutterInjector.instance().isRunningInRobolectricTest()) { - return; - } - if (Looper.myLooper() != Looper.getMainLooper()) { throw new IllegalStateException("ensureInitializationComplete must be called on the main thread"); } - if (sSettings == null) { + if (settings == null) { throw new IllegalStateException("ensureInitializationComplete must be called after startInitialization"); } - if (sInitialized) { + if (initialized) { return; } new Thread(new Runnable() { @Override public void run() { - if (sResourceExtractor != null) { - sResourceExtractor.waitForCompletion(); + if (resourceExtractor != null) { + resourceExtractor.waitForCompletion(); } new Handler(Looper.getMainLooper()).post(new Runnable() { @Override @@ -264,7 +230,7 @@ public void run() { } @NonNull - private static ApplicationInfo getApplicationInfo(@NonNull Context applicationContext) { + private ApplicationInfo getApplicationInfo(@NonNull Context applicationContext) { try { return applicationContext .getPackageManager() @@ -278,7 +244,7 @@ private static ApplicationInfo getApplicationInfo(@NonNull Context applicationCo * Initialize our Flutter config values by obtaining them from the * manifest XML file, falling back to default values. */ - private static void initConfig(@NonNull Context applicationContext) { + private void initConfig(@NonNull Context applicationContext) { Bundle metadata = getApplicationInfo(applicationContext).metaData; // There isn't a `` tag as a direct child of `` in @@ -287,18 +253,18 @@ private static void initConfig(@NonNull Context applicationContext) { return; } - sAotSharedLibraryName = metadata.getString(PUBLIC_AOT_SHARED_LIBRARY_NAME, DEFAULT_AOT_SHARED_LIBRARY_NAME); - sFlutterAssetsDir = metadata.getString(PUBLIC_FLUTTER_ASSETS_DIR_KEY, DEFAULT_FLUTTER_ASSETS_DIR); + aotSharedLibraryName = metadata.getString(PUBLIC_AOT_SHARED_LIBRARY_NAME, DEFAULT_AOT_SHARED_LIBRARY_NAME); + flutterAssetsDir = metadata.getString(PUBLIC_FLUTTER_ASSETS_DIR_KEY, DEFAULT_FLUTTER_ASSETS_DIR); - sVmSnapshotData = metadata.getString(PUBLIC_VM_SNAPSHOT_DATA_KEY, DEFAULT_VM_SNAPSHOT_DATA); - sIsolateSnapshotData = metadata.getString(PUBLIC_ISOLATE_SNAPSHOT_DATA_KEY, DEFAULT_ISOLATE_SNAPSHOT_DATA); + vmSnapshotData = metadata.getString(PUBLIC_VM_SNAPSHOT_DATA_KEY, DEFAULT_VM_SNAPSHOT_DATA); + isolateSnapshotData = metadata.getString(PUBLIC_ISOLATE_SNAPSHOT_DATA_KEY, DEFAULT_ISOLATE_SNAPSHOT_DATA); } /** * Extract assets out of the APK that need to be cached as uncompressed * files on disk. */ - private static void initResources(@NonNull Context applicationContext) { + private void initResources(@NonNull Context applicationContext) { new ResourceCleaner(applicationContext).start(); if (BuildConfig.DEBUG || BuildConfig.JIT_RELEASE) { @@ -306,28 +272,22 @@ private static void initResources(@NonNull Context applicationContext) { final String packageName = applicationContext.getPackageName(); final PackageManager packageManager = applicationContext.getPackageManager(); final AssetManager assetManager = applicationContext.getResources().getAssets(); - sResourceExtractor = new ResourceExtractor(dataDirPath, packageName, packageManager, assetManager); + resourceExtractor = new ResourceExtractor(dataDirPath, packageName, packageManager, assetManager); // In debug/JIT mode these assets will be written to disk and then // mapped into memory so they can be provided to the Dart VM. - sResourceExtractor - .addResource(fromFlutterAssets(sVmSnapshotData)) - .addResource(fromFlutterAssets(sIsolateSnapshotData)) - .addResource(fromFlutterAssets(DEFAULT_KERNEL_BLOB)); + resourceExtractor + .addResource(fullAssetPathFrom(vmSnapshotData)) + .addResource(fullAssetPathFrom(isolateSnapshotData)) + .addResource(fullAssetPathFrom(DEFAULT_KERNEL_BLOB)); - sResourceExtractor.start(); + resourceExtractor.start(); } } @NonNull - public static String findAppBundlePath() { - return sFlutterAssetsDir; - } - - @Deprecated - @Nullable - public static String findAppBundlePath(@NonNull Context applicationContext) { - return sFlutterAssetsDir; + public String findAppBundlePath() { + return flutterAssetsDir; } /** @@ -339,8 +299,8 @@ public static String findAppBundlePath(@NonNull Context applicationContext) { * @return the filename to be used with {@link android.content.res.AssetManager} */ @NonNull - public static String getLookupKeyForAsset(@NonNull String asset) { - return fromFlutterAssets(asset); + public String getLookupKeyForAsset(@NonNull String asset) { + return fullAssetPathFrom(asset); } /** @@ -353,8 +313,30 @@ public static String getLookupKeyForAsset(@NonNull String asset) { * @return the file name to be used with {@link android.content.res.AssetManager} */ @NonNull - public static String getLookupKeyForAsset(@NonNull String asset, @NonNull String packageName) { + public String getLookupKeyForAsset(@NonNull String asset, @NonNull String packageName) { return getLookupKeyForAsset( "packages" + File.separator + packageName + File.separator + asset); } + + @NonNull + private String fullAssetPathFrom(@NonNull String filePath) { + return flutterAssetsDir + File.separator + filePath; + } + + public static class Settings { + private String logTag; + + @Nullable + public String getLogTag() { + return logTag; + } + + /** + * Set the tag associated with Flutter app log messages. + * @param tag Log tag. + */ + public void setLogTag(String tag) { + logTag = tag; + } + } } diff --git a/shell/platform/android/io/flutter/view/FlutterMain.java b/shell/platform/android/io/flutter/view/FlutterMain.java index 80cd42f249476..accd854eb65e6 100644 --- a/shell/platform/android/io/flutter/view/FlutterMain.java +++ b/shell/platform/android/io/flutter/view/FlutterMain.java @@ -9,7 +9,6 @@ import android.support.annotation.NonNull; import android.support.annotation.Nullable; -import io.flutter.FlutterInjector; import io.flutter.embedding.engine.loader.FlutterLoader; /** @@ -39,7 +38,7 @@ public void setLogTag(String tag) { * @param applicationContext The Android application context. */ public static void startInitialization(@NonNull Context applicationContext) { - FlutterInjector.instance().flutterLoader().startInitialization(applicationContext); + FlutterLoader.getInstance().startInitialization(applicationContext); } /** @@ -56,7 +55,7 @@ public static void startInitialization(@NonNull Context applicationContext) { public static void startInitialization(@NonNull Context applicationContext, @NonNull Settings settings) { FlutterLoader.Settings newSettings = new FlutterLoader.Settings(); newSettings.setLogTag(settings.getLogTag()); - FlutterInjector.instance().flutterLoader().startInitialization(applicationContext, newSettings); + FlutterLoader.getInstance().startInitialization(applicationContext, newSettings); } /** @@ -68,7 +67,7 @@ public static void startInitialization(@NonNull Context applicationContext, @Non * @param args Flags sent to the Flutter runtime. */ public static void ensureInitializationComplete(@NonNull Context applicationContext, @Nullable String[] args) { - FlutterInjector.instance().flutterLoader().ensureInitializationComplete(applicationContext, args); + FlutterLoader.getInstance().ensureInitializationComplete(applicationContext, args); } /** @@ -81,19 +80,19 @@ public static void ensureInitializationCompleteAsync( @NonNull Handler callbackHandler, @NonNull Runnable callback ) { - FlutterInjector.instance().flutterLoader().ensureInitializationCompleteAsync( + FlutterLoader.getInstance().ensureInitializationCompleteAsync( applicationContext, args, callbackHandler, callback); } @NonNull public static String findAppBundlePath() { - return FlutterInjector.instance().flutterLoader().findAppBundlePath(); + return FlutterLoader.getInstance().findAppBundlePath(); } @Deprecated @Nullable public static String findAppBundlePath(@NonNull Context applicationContext) { - return FlutterInjector.instance().flutterLoader().findAppBundlePath(applicationContext); + return FlutterLoader.getInstance().findAppBundlePath(); } /** @@ -106,7 +105,7 @@ public static String findAppBundlePath(@NonNull Context applicationContext) { */ @NonNull public static String getLookupKeyForAsset(@NonNull String asset) { - return FlutterInjector.instance().flutterLoader().getLookupKeyForAsset(asset); + return FlutterLoader.getInstance().getLookupKeyForAsset(asset); } /** @@ -120,6 +119,6 @@ public static String getLookupKeyForAsset(@NonNull String asset) { */ @NonNull public static String getLookupKeyForAsset(@NonNull String asset, @NonNull String packageName) { - return FlutterInjector.instance().flutterLoader().getLookupKeyForAsset(asset, packageName); + return FlutterLoader.getInstance().getLookupKeyForAsset(asset, packageName); } } diff --git a/shell/platform/android/test/io/flutter/FlutterInjectorTest.java b/shell/platform/android/test/io/flutter/FlutterInjectorTest.java deleted file mode 100644 index 568d87fa38429..0000000000000 --- a/shell/platform/android/test/io/flutter/FlutterInjectorTest.java +++ /dev/null @@ -1,57 +0,0 @@ -// 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; - -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.robolectric.Robolectric; -import org.robolectric.RobolectricTestRunner; -import org.robolectric.RuntimeEnvironment; -import org.robolectric.android.controller.ActivityController; -import org.robolectric.annotation.Config; - -import io.flutter.FlutterInjector; - -import static org.junit.Assert.assertArrayEquals; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertNull; -import static org.junit.Assert.assertTrue; - -import java.lang.IllegalStateException; - -@Config(manifest=Config.NONE) -@RunWith(RobolectricTestRunner.class) -public class FlutterInjectorTest { - @Before - public void setUp() { - FlutterInjector.reset(); - } - - @Test - public void itHasSomeReasonableDefaults() { - FlutterInjector injector = FlutterInjector.instance(); - assertFalse(injector.isRunningInRobolectricTest()); - assertNotNull(injector.flutterLoader()); - } - - @Test - public void canPartiallyOverride() { - FlutterInjector.setInstance( - new FlutterInjector.Builder().setIsRunningInRobolectricTest(true).build()); - FlutterInjector injector = FlutterInjector.instance(); - assertTrue(injector.isRunningInRobolectricTest()); - assertNotNull(injector.flutterLoader()); - } - - @Test(expected = IllegalStateException.class) - public void cannotBeChangedOnceRead() { - FlutterInjector.instance(); - FlutterInjector.setInstance( - new FlutterInjector.Builder().setIsRunningInRobolectricTest(true).build()); - } -} diff --git a/shell/platform/android/test/io/flutter/FlutterTestSuite.java b/shell/platform/android/test/io/flutter/FlutterTestSuite.java index 7899ec60af673..59980291db489 100644 --- a/shell/platform/android/test/io/flutter/FlutterTestSuite.java +++ b/shell/platform/android/test/io/flutter/FlutterTestSuite.java @@ -8,7 +8,6 @@ import org.junit.runners.Suite; import org.junit.runners.Suite.SuiteClasses; -import io.flutter.FlutterInjectorTest; import io.flutter.embedding.android.FlutterActivityTest; import io.flutter.embedding.android.FlutterFragmentTest; import io.flutter.embedding.engine.FlutterEngineCacheTest; @@ -27,7 +26,6 @@ FlutterActivityTest.class, FlutterEngineCacheTest.class, FlutterFragmentTest.class, - FlutterInjectorTest.class, FlutterJNITest.class, FlutterRendererTest.class, PlatformChannelTest.class, diff --git a/shell/platform/android/test/io/flutter/embedding/android/FlutterActivityAndFragmentDelegateTest.java b/shell/platform/android/test/io/flutter/embedding/android/FlutterActivityAndFragmentDelegateTest.java index 77abc73db523c..154c59f173191 100644 --- a/shell/platform/android/test/io/flutter/embedding/android/FlutterActivityAndFragmentDelegateTest.java +++ b/shell/platform/android/test/io/flutter/embedding/android/FlutterActivityAndFragmentDelegateTest.java @@ -6,9 +6,7 @@ import android.content.Intent; import android.support.annotation.NonNull; -import org.junit.After; import org.junit.Before; -import org.junit.BeforeClass; import org.junit.Test; import org.junit.runner.RunWith; import org.robolectric.Robolectric; @@ -16,7 +14,6 @@ import org.robolectric.RuntimeEnvironment; import org.robolectric.annotation.Config; -import io.flutter.FlutterInjector; import io.flutter.embedding.engine.FlutterEngine; import io.flutter.embedding.engine.FlutterEngineCache; import io.flutter.embedding.engine.FlutterShellArgs; @@ -30,7 +27,6 @@ import io.flutter.embedding.engine.systemchannels.SettingsChannel; import io.flutter.embedding.engine.systemchannels.SystemChannel; import io.flutter.plugin.platform.PlatformViewsController; -import io.flutter.view.FlutterMain; import static android.content.ComponentCallbacks2.TRIM_MEMORY_RUNNING_LOW; import static org.junit.Assert.assertEquals; @@ -49,14 +45,6 @@ public class FlutterActivityAndFragmentDelegateTest { private FlutterEngine mockFlutterEngine; private FlutterActivityAndFragmentDelegate.Host mockHost; - @BeforeClass - public static void setupInjector() { - // FlutterLoader is utilized statically, therefore we need to inform it to behave differently - // for testing purposes. - FlutterInjector.setInstance( - new FlutterInjector.Builder().setIsRunningInRobolectricTest(true).build()); - } - @Before public void setup() { // Create a mocked FlutterEngine for the various interactions required by the delegate From 7c2fec12181cf8ff461498afe55298d088c322b0 Mon Sep 17 00:00:00 2001 From: Matt Carroll Date: Fri, 4 Oct 2019 15:45:27 -0700 Subject: [PATCH 3/3] PR Updates. --- .../engine/loader/FlutterLoader.java | 29 ++++++++++++------- 1 file changed, 18 insertions(+), 11 deletions(-) diff --git a/shell/platform/android/io/flutter/embedding/engine/loader/FlutterLoader.java b/shell/platform/android/io/flutter/embedding/engine/loader/FlutterLoader.java index 153306019874d..35f4098597842 100644 --- a/shell/platform/android/io/flutter/embedding/engine/loader/FlutterLoader.java +++ b/shell/platform/android/io/flutter/embedding/engine/loader/FlutterLoader.java @@ -26,7 +26,7 @@ import java.util.*; /** - * This class finds Flutter resources in an application APK and also loads Flutter's native library. + * Finds Flutter resources in an application APK and also loads Flutter's native library. */ public class FlutterLoader { private static final String TAG = "FlutterLoader"; @@ -39,13 +39,13 @@ public class FlutterLoader { private static final String FLUTTER_ASSETS_DIR_KEY = "flutter-assets-dir"; // XML Attribute keys supported in AndroidManifest.xml - public static final String PUBLIC_AOT_SHARED_LIBRARY_NAME = + private static final String PUBLIC_AOT_SHARED_LIBRARY_NAME = FlutterLoader.class.getName() + '.' + AOT_SHARED_LIBRARY_NAME; - public static final String PUBLIC_VM_SNAPSHOT_DATA_KEY = + private static final String PUBLIC_VM_SNAPSHOT_DATA_KEY = FlutterLoader.class.getName() + '.' + VM_SNAPSHOT_DATA_KEY; - public static final String PUBLIC_ISOLATE_SNAPSHOT_DATA_KEY = + private static final String PUBLIC_ISOLATE_SNAPSHOT_DATA_KEY = FlutterLoader.class.getName() + '.' + ISOLATE_SNAPSHOT_DATA_KEY; - public static final String PUBLIC_FLUTTER_ASSETS_DIR_KEY = + private static final String PUBLIC_FLUTTER_ASSETS_DIR_KEY = FlutterLoader.class.getName() + '.' + FLUTTER_ASSETS_DIR_KEY; // Resource names used for components of the precompiled snapshot. @@ -64,6 +64,13 @@ public class FlutterLoader { private static FlutterLoader instance; + /** + * Returns a singleton {@code FlutterLoader} instance. + *

+ * The returned instance loads Flutter native libraries in the standard way. A singleton object + * is used instead of static methods to facilitate testing without actually running native + * library linking. + */ @NonNull public static FlutterLoader getInstance() { if (instance == null) { @@ -98,13 +105,13 @@ public void startInitialization(@NonNull Context applicationContext) { * @param settings Configuration settings. */ public void startInitialization(@NonNull Context applicationContext, @NonNull Settings settings) { - if (Looper.myLooper() != Looper.getMainLooper()) { - throw new IllegalStateException("startInitialization must be called on the main thread"); - } // Do not run startInitialization more than once. if (this.settings != null) { return; } + if (Looper.myLooper() != Looper.getMainLooper()) { + throw new IllegalStateException("startInitialization must be called on the main thread"); + } this.settings = settings; @@ -136,15 +143,15 @@ public void startInitialization(@NonNull Context applicationContext, @NonNull Se * @param args Flags sent to the Flutter runtime. */ public void ensureInitializationComplete(@NonNull Context applicationContext, @Nullable String[] args) { + if (initialized) { + return; + } if (Looper.myLooper() != Looper.getMainLooper()) { throw new IllegalStateException("ensureInitializationComplete must be called on the main thread"); } if (settings == null) { throw new IllegalStateException("ensureInitializationComplete must be called after startInitialization"); } - if (initialized) { - return; - } try { if (resourceExtractor != null) { resourceExtractor.waitForCompletion();