diff --git a/ci/licenses_golden/licenses_flutter b/ci/licenses_golden/licenses_flutter index bfc4c4245fb9e..12d3b72cb894b 100644 --- a/ci/licenses_golden/licenses_flutter +++ b/ci/licenses_golden/licenses_flutter @@ -597,6 +597,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 +670,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..c2f3d1f41e32f 100644 --- a/shell/platform/android/BUILD.gn +++ b/shell/platform/android/BUILD.gn @@ -149,6 +149,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 +222,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", ] diff --git a/shell/platform/android/io/flutter/embedding/engine/FlutterEngine.java b/shell/platform/android/io/flutter/embedding/engine/FlutterEngine.java index 618dec5a1f99e..42b0129d7298f 100644 --- a/shell/platform/android/io/flutter/embedding/engine/FlutterEngine.java +++ b/shell/platform/android/io/flutter/embedding/engine/FlutterEngine.java @@ -14,6 +14,7 @@ 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 +35,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 +121,35 @@ 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 FlutterLoader#startInitialization(Context)} + * and {@link FlutterLoader#ensureInitializationComplete(Context, String[])}. */ public FlutterEngine(@NonNull Context context) { + this(context, FlutterLoader.getInstance()); + } + + /* package */ FlutterEngine(@NonNull Context context, @NonNull FlutterLoader flutterLoader) { + flutterLoader.startInitialization(context); + flutterLoader.ensureInitializationComplete(context, null); + this.flutterJNI = new FlutterJNI(); flutterJNI.addEngineLifecycleListener(engineLifecycleListener); attachToJni(); @@ -174,9 +196,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 +228,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 +242,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..35f4098597842 --- /dev/null +++ b/shell/platform/android/io/flutter/embedding/engine/loader/FlutterLoader.java @@ -0,0 +1,349 @@ +// 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.util.Log; +import android.view.WindowManager; + +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.*; + +/** + * 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 + private static final String PUBLIC_AOT_SHARED_LIBRARY_NAME = + FlutterLoader.class.getName() + '.' + AOT_SHARED_LIBRARY_NAME; + private static final String PUBLIC_VM_SNAPSHOT_DATA_KEY = + FlutterLoader.class.getName() + '.' + VM_SNAPSHOT_DATA_KEY; + private static final String PUBLIC_ISOLATE_SNAPSHOT_DATA_KEY = + FlutterLoader.class.getName() + '.' + ISOLATE_SNAPSHOT_DATA_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. + 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"; + + // Mutable because default values can be overridden via config properties + 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; + + 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) { + 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 void startInitialization(@NonNull Context applicationContext) { + 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 void startInitialization(@NonNull Context applicationContext, @NonNull Settings settings) { + // 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; + + 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 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"); + } + try { + if (resourceExtractor != null) { + resourceExtractor.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 + flutterAssetsDir; + kernelPath = snapshotAssetPath + File.separator + DEFAULT_KERNEL_BLOB; + shellArgs.add("--" + SNAPSHOT_ASSET_PATH_KEY + "=" + snapshotAssetPath); + shellArgs.add("--" + VM_SNAPSHOT_DATA_KEY + "=" + vmSnapshotData); + shellArgs.add("--" + ISOLATE_SNAPSHOT_DATA_KEY + "=" + isolateSnapshotData); + } else { + 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 + aotSharedLibraryName); + } + + shellArgs.add("--cache-dir-path=" + PathUtils.getCacheDirectory(applicationContext)); + if (settings.getLogTag() != null) { + shellArgs.add("--log-tag=" + settings.getLogTag()); + } + + String appStoragePath = PathUtils.getFilesDir(applicationContext); + String engineCachesPath = PathUtils.getCacheDirectory(applicationContext); + FlutterJNI.nativeInit(applicationContext, shellArgs.toArray(new String[0]), + kernelPath, appStoragePath, engineCachesPath); + + initialized = 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 void ensureInitializationCompleteAsync( + @NonNull Context applicationContext, + @Nullable String[] args, + @NonNull Handler callbackHandler, + @NonNull Runnable callback + ) { + 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; + } + new Thread(new Runnable() { + @Override + public void run() { + if (resourceExtractor != null) { + resourceExtractor.waitForCompletion(); + } + new Handler(Looper.getMainLooper()).post(new Runnable() { + @Override + public void run() { + ensureInitializationComplete(applicationContext.getApplicationContext(), args); + callbackHandler.post(callback); + } + }); + } + }).start(); + } + + @NonNull + private 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 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; + } + + 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); + + 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 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(); + 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. + resourceExtractor + .addResource(fullAssetPathFrom(vmSnapshotData)) + .addResource(fullAssetPathFrom(isolateSnapshotData)) + .addResource(fullAssetPathFrom(DEFAULT_KERNEL_BLOB)); + + resourceExtractor.start(); + } + } + + @NonNull + public String findAppBundlePath() { + return flutterAssetsDir; + } + + /** + * 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 String getLookupKeyForAsset(@NonNull String asset) { + return fullAssetPathFrom(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 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/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..accd854eb65e6 100644 --- a/shell/platform/android/io/flutter/view/FlutterMain.java +++ b/shell/platform/android/io/flutter/view/FlutterMain.java @@ -5,81 +5,16 @@ 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.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 +38,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()); + FlutterLoader.getInstance().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()); + FlutterLoader.getInstance().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); - } + FlutterLoader.getInstance().ensureInitializationComplete(applicationContext, args); } /** @@ -228,102 +80,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(); - } + FlutterLoader.getInstance().ensureInitializationCompleteAsync( + applicationContext, args, callbackHandler, callback); } @NonNull public static String findAppBundlePath() { - return sFlutterAssetsDir; + return FlutterLoader.getInstance().findAppBundlePath(); } @Deprecated @Nullable public static String findAppBundlePath(@NonNull Context applicationContext) { - return sFlutterAssetsDir; + return FlutterLoader.getInstance().findAppBundlePath(); } /** @@ -336,7 +105,7 @@ public static String findAppBundlePath(@NonNull Context applicationContext) { */ @NonNull public static String getLookupKeyForAsset(@NonNull String asset) { - return fromFlutterAssets(asset); + return FlutterLoader.getInstance().getLookupKeyForAsset(asset); } /** @@ -350,7 +119,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 FlutterLoader.getInstance().getLookupKeyForAsset(asset, packageName); } } 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..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,7 +6,6 @@ import android.content.Intent; import android.support.annotation.NonNull; -import org.junit.After; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -28,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,10 +47,6 @@ public class FlutterActivityAndFragmentDelegateTest { @Before public void setup() { - // FlutterMain is utilized statically, therefore we need to inform it to behave differently - // for testing purposes. - FlutterMain.setIsRunningInRobolectricTest(true); - // Create a mocked FlutterEngine for the various interactions required by the delegate // being tested. mockFlutterEngine = mockFlutterEngine(); @@ -73,12 +67,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