diff --git a/docs/engine/Flutter-Android-Engine-Flags.md b/docs/engine/Flutter-Android-Engine-Flags.md new file mode 100644 index 00000000000..b039c5ded53 --- /dev/null +++ b/docs/engine/Flutter-Android-Engine-Flags.md @@ -0,0 +1,190 @@ +# Setting Flutter Android engine flags + +You can set flags for the Flutter engine on Android in two different ways: + +- From the command line when launching an app with the Flutter tool +- Via `AndroidManifest.xml` metadata (static, per-build configuration) + +All flags available on Android can be set via the command line **and** via +manifest metadata. See `src/flutter/shell/common/switches.cc` for +the list of all supported flags, and see +`src/flutter/shell/platform/android/io/flutter/embedding/engine/` +`FlutterEngineFlags.java` for the list of flags that can be set for the +Android shell. + +## When to use manifest metadata versus the command line + +Use the manifest when: + +- You want a fixed, reproducible baseline of engine flags + for your app across all launches. This is ideal for CI and for enforcing a + consistent configuration for your app. +- You want to vary flags by build mode or product flavor + via manifest merging. For example, place metadata in + `src/debug/AndroidManifest.xml`, `src/profile/AndroidManifest.xml`, and + `src/release/AndroidManifest.xml` (or per-flavor manifests) to tailor flags + per variant. + +Use the command line when: + +- You want to quickly experiment with a flag for a single run of your app. +- You need to override a flag that is already set in the manifest temporarily for debugging + or testing purposes. + +**Note: If a flag is specified both on the command line and in the manifest, +the command-line value takes precedence at runtime.** + +See below for details on using each method. + +## How to set engine flags from the command line + +When you run a standalone Flutter app with the Flutter tool, engine flags +can be passed directly and are forwarded to the Android engine. Examples: + +```bash +flutter run --trace-startup \ + --enable-software-rendering \ + --dart-flags="--enable-asserts" +``` + +Notes: + +- Flags that take values use the `--flag=value` form (with `=`). The Flutter + tool forwards them in that form to the Android embedding. + +## How to set engine flags in the manifest + +All manifest metadata keys must be prefixed with the package name +`io.flutter.embedding.android` and are suffixed with the metadata name for the +related command line flag as determined in +`src/flutter/shell/platform/android/io/flutter/embedding/engine/` +`FlutterEngineFlags.java`. For example, the `--impeller-lazy-shader-mode=` +command line flag corresponds to the metadata key +`io.flutter.embedding.android.ImpellerLazyShaderInitialization`. + +For flags that take values, set the numeric, string, or boolean value (without +the leading `--flag=` prefix). + +### Examples + +Set the `--trace-to-file=` flag to `some_file.txt`: + +```xml + + + + ... + + +``` + +Set the `--enable-flutter-gpu` flag: + +```xml + +``` + +## Release-mode restrictions + +- Some flags are not allowed in release mode. The Android embedding enforces + this policy (see `src/flutter/shell/platform/android/io/flutter/ + embedding/engine/FlutterEngineFlags`, which marks allowed flags + with `allowedInRelease`). If a disallowed flag is set in release, it will + be ignored. +- If you need different behavior in release vs debug/profile mode, configure it + via variant-specific manifests or product flavors. + +## How to set engine flags dynamically + +As of the writing of this document, setting Flutter shell arguments via an +Android `Intent` is no longer supported. If you need per-launch or +runtime-controlled flags in an add-to-app integration, you may do so +programatically before engine initialization. + +To do that, supply engine arguments directly to a `FlutterEngine` with the +desired flags from the earliest point you can control in your +application. For example, if you are writing an add-to-app app that launches +a `FlutterActivity` or `FlutterFragment`, then you can cache a +`FlutterEngine` that is initialized with your desired +engine flags: + +```kotlin +// Your native Android application +class MyApp : Application() { + override fun onCreate() { + super.onCreate() + // Initialize the Flutter engine with desired flags + val args = arrayOf( + "--trace-startup", + "--trace-to-file=some_file.txt", + "--enable-software-rendering" + ) + val flutterEngine = FlutterEngine(this, args) + + // Start executing Dart code in the FlutterEngine + flutterEngine.dartExecutor.executeDartEntrypoint( + DartEntrypoint.createDefault() + ) + + // Store the engine in the cache for later use + FlutterEngineCache.getInstance().put("my_engine_id", flutterEngine) + } +} +``` + +Then, your `Activity` can launch a `FlutterActivity` or `FlutterFragment` +with that cached `FlutterEngine`: + +```kotlin +// Start a FlutterActivity using the cached engine... +val intent = FlutterActivity.withCachedEngine("my_engine_id").build(this) +startActivity(intent) + +// Or launch a FlutterFragment using the cached engine +val flutterFragment = FlutterFragment.withCachedEngine("my_engine_id").build() +supportFragmentManager + .beginTransaction() + .add(R.id.fragment_container, flutterFragment, TAG_FLUTTER_FRAGMENT) + .commit() +``` + +For a normal Flutter Android app, you can create and initialize a `FlutterEngine` +with your desired flags the same as in the example above, then override +`provideFlutterEngine` in your app's `FlutterActivity` to provide the +configured `FlutterEngine`. For example: + +```kotlin +// Your Flutter Android application +class MyApplication : FlutterApplication() { + override fun onCreate() { + super.onCreate() + + val args = arrayOf( + "--trace-startup", + "--trace-to-file=some_file.txt", + "--enable-software-rendering" + ) + val flutterEngine = FlutterEngine(this, args) + flutterEngine.dartExecutor.executeDartEntrypoint( + DartExecutor.DartEntrypoint.createDefault() + ) + FlutterEngineCache + .getInstance() + .put(MY_ENGINE_ID, flutterEngine) + } +} + +// Your Flutter Android Activity +class MainActivity: FlutterActivity() { + override fun provideFlutterEngine(context: Context): FlutterEngine? { + return FlutterEngineCache + .getInstance() + .get(MyApplication.MY_ENGINE_ID) + } +} +``` diff --git a/engine/src/flutter/shell/platform/android/BUILD.gn b/engine/src/flutter/shell/platform/android/BUILD.gn index 2b188414f04..c15ac362d7d 100644 --- a/engine/src/flutter/shell/platform/android/BUILD.gn +++ b/engine/src/flutter/shell/platform/android/BUILD.gn @@ -250,6 +250,7 @@ android_java_sources = [ "io/flutter/embedding/engine/FlutterEngine.java", "io/flutter/embedding/engine/FlutterEngineCache.java", "io/flutter/embedding/engine/FlutterEngineConnectionRegistry.java", + "io/flutter/embedding/engine/FlutterEngineFlags.java", "io/flutter/embedding/engine/FlutterEngineGroup.java", "io/flutter/embedding/engine/FlutterEngineGroupCache.java", "io/flutter/embedding/engine/FlutterJNI.java", diff --git a/engine/src/flutter/shell/platform/android/io/flutter/embedding/android/FlutterActivityAndFragmentDelegate.java b/engine/src/flutter/shell/platform/android/io/flutter/embedding/android/FlutterActivityAndFragmentDelegate.java index 7eadd5c9065..70d3130e7b5 100644 --- a/engine/src/flutter/shell/platform/android/io/flutter/embedding/android/FlutterActivityAndFragmentDelegate.java +++ b/engine/src/flutter/shell/platform/android/io/flutter/embedding/android/FlutterActivityAndFragmentDelegate.java @@ -29,6 +29,7 @@ import io.flutter.FlutterInjector; import io.flutter.Log; import io.flutter.embedding.engine.FlutterEngine; import io.flutter.embedding.engine.FlutterEngineCache; +import io.flutter.embedding.engine.FlutterEngineFlags; import io.flutter.embedding.engine.FlutterEngineGroup; import io.flutter.embedding.engine.FlutterEngineGroupCache; import io.flutter.embedding.engine.FlutterShellArgs; @@ -38,6 +39,7 @@ import io.flutter.plugin.platform.PlatformPlugin; import io.flutter.plugin.view.SensitiveContentPlugin; import java.util.Arrays; import java.util.List; +import java.util.Set; /** * Delegate that implements all Flutter logic that is the same between a {@link FlutterActivity} and @@ -331,6 +333,7 @@ import java.util.List; "No preferred FlutterEngine was provided. Creating a new FlutterEngine for" + " this FlutterFragment."); + warnIfEngineFlagsSetViaIntent(host.getActivity().getIntent()); FlutterEngineGroup group = engineGroup == null ? new FlutterEngineGroup(host.getContext(), host.getFlutterShellArgs().toArray()) @@ -344,6 +347,30 @@ import java.util.List; isFlutterEngineFromHost = false; } + // As part of https://github.com/flutter/flutter/issues/180686, the ability + // to set engine flags via Intent extras is planned to be removed, so warn + // developers that engine shell arguments set that way will be ignored. + private void warnIfEngineFlagsSetViaIntent(@NonNull Intent intent) { + if (intent.getExtras() == null) { + return; + } + + Bundle extras = intent.getExtras(); + Set extrasKeys = extras.keySet(); + + for (String extrasKey : extrasKeys) { + FlutterEngineFlags.Flag flag = FlutterEngineFlags.getFlagFromIntentKey(extrasKey); + if (flag != null) { + Log.w( + TAG, + "Support for setting engine flags on Android via Intent will soon be dropped; see https://github.com/flutter/flutter/issues/180686 for more information on this breaking change. To migrate, set " + + flag.commandLineArgument + + " on the command line or see https://github.com/flutter/flutter/blob/main/docs/engine/Flutter-Android-Engine-Flags.md for alternative methods."); + break; + } + } + } + /** * Invoke this method from {@code Activity#onCreate(Bundle)} to create the content {@code View}, * or from {@code Fragment#onCreateView(LayoutInflater, ViewGroup, Bundle)}. @@ -1090,7 +1117,6 @@ import java.util.List; @NonNull Lifecycle getLifecycle(); - /** Returns the {@link FlutterShellArgs} that should be used when initializing Flutter. */ @NonNull FlutterShellArgs getFlutterShellArgs(); diff --git a/engine/src/flutter/shell/platform/android/io/flutter/embedding/engine/FlutterEngine.java b/engine/src/flutter/shell/platform/android/io/flutter/embedding/engine/FlutterEngine.java index cf57c07eccd..c8a66a02985 100644 --- a/engine/src/flutter/shell/platform/android/io/flutter/embedding/engine/FlutterEngine.java +++ b/engine/src/flutter/shell/platform/android/io/flutter/embedding/engine/FlutterEngine.java @@ -177,8 +177,8 @@ public class FlutterEngine implements ViewUtils.DisplayUpdater { * 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 + * io.flutter.embedding.engine.FlutterEngineFlags} for all available flags) when creating the VM, + * manually set the initialization arguments by calling {@link * io.flutter.embedding.engine.loader.FlutterLoader#startInitialization(Context)} and {@link * io.flutter.embedding.engine.loader.FlutterLoader#ensureInitializationComplete(Context, * String[])} before constructing the engine. diff --git a/engine/src/flutter/shell/platform/android/io/flutter/embedding/engine/FlutterEngineConnectionRegistry.java b/engine/src/flutter/shell/platform/android/io/flutter/embedding/engine/FlutterEngineConnectionRegistry.java index bcb5addfff6..458c73f3a5c 100644 --- a/engine/src/flutter/shell/platform/android/io/flutter/embedding/engine/FlutterEngineConnectionRegistry.java +++ b/engine/src/flutter/shell/platform/android/io/flutter/embedding/engine/FlutterEngineConnectionRegistry.java @@ -59,6 +59,7 @@ import java.util.Set; // Standard FlutterPlugin @NonNull private final FlutterEngine flutterEngine; + @NonNull private final FlutterLoader flutterLoader; @NonNull private final FlutterPlugin.FlutterPluginBinding pluginBinding; // ActivityAware @@ -100,6 +101,7 @@ import java.util.Set; @NonNull FlutterLoader flutterLoader, @Nullable FlutterEngineGroup group) { this.flutterEngine = flutterEngine; + this.flutterLoader = flutterLoader; pluginBinding = new FlutterPlugin.FlutterPluginBinding( appContext, @@ -326,13 +328,31 @@ import java.util.Set; private void attachToActivityInternal(@NonNull Activity activity, @NonNull Lifecycle lifecycle) { this.activityPluginBinding = new FlutterEngineActivityPluginBinding(activity, lifecycle); + final Intent intent = activity.getIntent(); - final boolean useSoftwareRendering = - activity.getIntent() != null - ? activity - .getIntent() - .getBooleanExtra(FlutterShellArgs.ARG_KEY_ENABLE_SOFTWARE_RENDERING, false) + // TODO(camsim99): Remove ability to set this flag via Intents. See + // https://github.com/flutter/flutter/issues/180686. + boolean useSoftwareRendering = + intent != null + ? intent.getBooleanExtra(FlutterShellArgs.ARG_KEY_ENABLE_SOFTWARE_RENDERING, false) : false; + + // As part of https://github.com/flutter/flutter/issues/172553, the ability to set + // --enable-software-rendering via Intent is planned to be removed. Warn + // developers about the new method for doing so if this was attempted. + // TODO(camsim99): Remove this warning after a stable release has passed: + // https://github.com/flutter/flutter/issues/179274. + if (useSoftwareRendering) { + Log.w( + TAG, + "Support for setting engine flags on Android via Intent will soon be dropped; see https://github.com/flutter/flutter/issues/172553 for more information on this breaking change. To migrate, set the " + + FlutterEngineFlags.ENABLE_SOFTWARE_RENDERING.metadataKey + + " metadata in the application manifest. See https://github.com/flutter/flutter/blob/main/docs/engine/Flutter-Android-Engine-Flags.md for more info."); + } else { + // Check manifest for software rendering configuration. + useSoftwareRendering = flutterLoader.getSofwareRenderingEnabledViaManifest(); + } + flutterEngine.getPlatformViewsController().setSoftwareRendering(useSoftwareRendering); // Activate the PlatformViewsController. This must happen before any plugins attempt diff --git a/engine/src/flutter/shell/platform/android/io/flutter/embedding/engine/FlutterEngineFlags.java b/engine/src/flutter/shell/platform/android/io/flutter/embedding/engine/FlutterEngineFlags.java new file mode 100644 index 00000000000..a78735efc59 --- /dev/null +++ b/engine/src/flutter/shell/platform/android/io/flutter/embedding/engine/FlutterEngineFlags.java @@ -0,0 +1,421 @@ +// 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; + +import androidx.annotation.VisibleForTesting; +import java.util.*; + +/** + * Arguments that can be delivered to the Flutter shell on Android. + * + *

The term "shell" refers to the native code that adapts Flutter to different platforms. + * Flutter's Android Java code initializes a native "shell" and passes these arguments to that + * native shell when it is initialized. See {@link + * io.flutter.embedding.engine.loader.FlutterLoader#ensureInitializationComplete(Context, String[])} + * for more information. + * + *

All of these flags map to a flag listed in shell/common/switches.cc, which contains the full + * list of flags that can be set across all platforms. + * + *

These flags can either be set via the manifest metadata in a Flutter component's + * AndroidManifest.xml or via the command line. See the inner {@code Flag} class for the + * specification of how to set each flag via the command line and manifest metadata. + * + *

If the same flag is provided both via command line arguments and via AndroidManifest.xml + * metadata, the command line value will take precedence at runtime. + */ +public final class FlutterEngineFlags { + + private FlutterEngineFlags() {} + + /** Represents a Flutter shell flag that can be set via manifest metadata or command line. */ + public static class Flag { + /** The command line argument used to specify the flag. */ + public final String commandLineArgument; + + /** + * The metadata key name used to specify the flag in AndroidManifest.xml. + * + *

To specify a flag in a manifest, it should be prefixed with {@code + * io.flutter.embedding.android.}. This is enforced to avoid potential naming collisions with + * other metadata keys. The only exception are flags that have already been deprecated. + */ + public final String metadataKey; + + /** Whether this flag is allowed to be set in release mode. */ + public final boolean allowedInRelease; + + /** + * Creates a new Flutter shell flag that is not allowed in release mode with the default flag + * prefix. + */ + private Flag(String commandLineArgument, String metaDataName) { + this(commandLineArgument, metaDataName, "io.flutter.embedding.android.", false); + } + + /** Creates a new Flutter shell flag with the default flag prefix. */ + private Flag(String commandLineArgument, String metaDataName, boolean allowedInRelease) { + this(commandLineArgument, metaDataName, "io.flutter.embedding.android.", allowedInRelease); + } + + /** + * Creates a new Flutter shell flag. + * + *

{@param allowedInRelease} determines whether or not this flag is allowed in release mode. + * Whenever possible, it is recommended to NOT allow this flag in release mode. Many flags are + * designed for debugging purposes and if enabled in production, could expose sensitive + * application data or make the app vulnerable to malicious actors. + * + *

If creating a flag that will be allowed in release, please leave a comment in the Javadoc + * explaining why it should be allowed in release. + */ + private Flag( + String commandLineArgument, + String metaDataName, + String flagPrefix, + boolean allowedInRelease) { + this.commandLineArgument = commandLineArgument; + this.metadataKey = flagPrefix + metaDataName; + this.allowedInRelease = allowedInRelease; + } + + /** Returns true if this flag requires a value to be specified. */ + public boolean hasValue() { + return commandLineArgument.endsWith("="); + } + } + + // Manifest flags allowed in release mode: + + /** + * Specifies the path to the AOT shared library containing compiled Dart code. + * + *

The AOT shared library that the engine uses will default to the library set by this flag, + * but will fall back to the libraries set internally by the embedding if the path specified by + * this argument is invalid. + * + *

Allowed in release to support the same AOT configuration regardless of build mode. + */ + public static final Flag AOT_SHARED_LIBRARY_NAME = + new Flag("--aot-shared-library-name=", "AOTSharedLibraryName", true); + + /** + * Deprecated flag that specifies the path to the AOT shared library containing compiled Dart + * code. + * + *

Please use {@link AOT_SHARED_LIBRARY_NAME} instead. + */ + @Deprecated + public static final Flag DEPRECATED_AOT_SHARED_LIBRARY_NAME = + new Flag( + "--aot-shared-library-name=", + "aot-shared-library-name", + "io.flutter.embedding.engine.loader.FlutterLoader.", + true); + + /** + * Sets the directory containing Flutter assets. + * + *

Allowed in release to specify custom asset locations in production. + */ + public static final Flag FLUTTER_ASSETS_DIR = + new Flag("--flutter-assets-dir=", "FlutterAssetsDir", true); + + /** + * The deprecated flag that sets the directory containing Flutter assets. + * + *

Please use {@link FLUTTER_ASSETS_DIR} infstead. + */ + @Deprecated + public static final Flag DEPRECATED_FLUTTER_ASSETS_DIR = + new Flag( + "--flutter-assets-dir=", + "flutter-assets-dir", + "io.flutter.embedding.engine.loader.FlutterLoader.", + true); + + /** + * Sets the old generation heap size for the Dart VM in megabytes. + * + *

Allowed in release for performance tuning. + */ + public static final Flag OLD_GEN_HEAP_SIZE = + new Flag("--old-gen-heap-size=", "OldGenHeapSize", true); + + /** + * Enables or disables the Impeller renderer. + * + *

Allowed in release to control which rendering backend is used in production. + */ + private static final Flag TOGGLE_IMPELLER = + new Flag("--enable-impeller=", "ToggleImpeller", true); + + /** + * Enables Impeller. + * + *

Allowed in release to control which rendering backend is used in production. + */ + private static final Flag ENABLE_IMPELLER = new Flag("--enable-impeller", "EnableImpeller", true); + /** + * Specifies the backend to use for Impeller rendering. + * + *

Allowed in release to select a specific graphics backend for Impeller in production. + */ + private static final Flag IMPELLER_BACKEND = + new Flag("--impeller-backend=", "ImpellerBackend", true); + + /** + * Enables Dart profiling for use with DevTools. + * + *

Allowed in release mode for testing purposes. + */ + private static final Flag ENABLE_DART_PROFILING = + new Flag("--enable-dart-profiling", "EnableDartProfiling", true); + + /** + * Discards new profiler samples once the buffer is full. Only meaningful when set in conjunction + * with {@link ENABLE_DART_PROFILING}. + * + *

Allowed in release mode to allow the startup performance to be profiled by DevTools. + */ + private static final Flag PROFILE_STARTUP = new Flag("--profile-startup", "ProfileStartup", true); + + /** + * Measures startup time and switches to an endless trace buffer. + * + *

Allowed in release mode to allow the startup performance to be profiled by DevTools. + */ + private static final Flag TRACE_STARTUP = new Flag("--trace-startup", "TraceStartup", true); + + /** + * Sets whether the UI thread and platform thread should be merged. + * + *

Allowed in release mode for performance purposes. + */ + private static final Flag MERGED_PLATFORM_UI_THREAD = + new Flag("--merged-platform-ui-thread", "MergedPlatformUIThread", true); + + /** + * Specifies the path to the VM snapshot data file. + * + *

Allowed in release to support different snapshot configurations. + */ + public static final Flag VM_SNAPSHOT_DATA = + new Flag("--vm-snapshot-data=", "VmSnapshotData", true); + + /** + * Specifies the path to the isolate snapshot data file. + * + *

Allowed in release to support different snapshot configurations. + */ + public static final Flag ISOLATE_SNAPSHOT_DATA = + new Flag("--isolate-snapshot-data=", "IsolateSnapshotData", true); + + // Manifest flags NOT allowed in release mode: + + /** Ensures deterministic Skia rendering by skipping CPU feature swaps. */ + private static final Flag SKIA_DETERMINISTIC_RENDERING = + new Flag("--skia-deterministic-rendering", "SkiaDeterministicRendering"); + + /** Use Skia software backend for rendering. */ + public static final Flag ENABLE_SOFTWARE_RENDERING = + new Flag("--enable-software-rendering", "EnableSoftwareRendering"); + + /** Use the Ahem test font for font resolution. */ + private static final Flag USE_TEST_FONTS = new Flag("--use-test-fonts", "UseTestFonts"); + + /** Sets the port for the Dart VM Service. */ + private static final Flag VM_SERVICE_PORT = new Flag("--vm-service-port=", "VMServicePort"); + + /** Enables Vulkan validation layers if available. */ + private static final Flag ENABLE_VULKAN_VALIDATION = + new Flag("--enable-vulkan-validation", "EnableVulkanValidation"); + + /** Fake flag used for integration testing of the Android embedding processing engine flags. */ + @VisibleForTesting public static final Flag TEST_FLAG = new Flag("--test-flag", "TestFlag"); + + /** + * Set whether leave or clean up the VM after the last shell shuts down. It can be set from app's + * metadata in the application block in AndroidManifest.xml. Set it to true in to leave the Dart + * VM, set it to false to destroy VM. + * + *

If your want to let your app destroy the last shell and re-create shells more quickly, set + * it to true, otherwise if you want to clean up the memory of the leak VM, set it to false. + * + *

TODO(eggfly): Should it be set to false by default? + * https://github.com/flutter/flutter/issues/96843 + */ + public static final Flag LEAK_VM = new Flag("--leak-vm=", "LeakVM"); + + /** Pauses Dart code execution at launch until a debugger is attached. */ + private static final Flag START_PAUSED = new Flag("--start-paused", "StartPaused"); + + /** Disables authentication codes for VM service communication. */ + private static final Flag DISABLE_SERVICE_AUTH_CODES = + new Flag("--disable-service-auth-codes", "DisableServiceAuthCodes"); + + /** Enables an endless trace buffer for timeline events. */ + private static final Flag ENDLESS_TRACE_BUFFER = + new Flag("--endless-trace-buffer", "EndlessTraceBuffer"); + + /** Enables tracing of Skia GPU calls. */ + private static final Flag TRACE_SKIA = new Flag("--trace-skia", "TraceSkia"); + + /** Only traces specified Skia event categories. */ + private static final Flag TRACE_SKIA_ALLOWLIST = + new Flag("--trace-skia-allowlist=", "TraceSkiaAllowList"); + + /** Traces to the system tracer on supported platforms. */ + private static final Flag TRACE_SYSTRACE = new Flag("--trace-systrace", "TraceSystrace"); + + /** Writes timeline trace to a file in Perfetto format. */ + private static final Flag TRACE_TO_FILE = new Flag("--trace-to-file=", "TraceToFile"); + + /** Collects and logs information about microtasks. */ + private static final Flag PROFILE_MICROTASKS = + new Flag("--profile-microtasks", "ProfileMicrotasks"); + + /** Dumps SKP files that trigger shader compilations. */ + private static final Flag DUMP_SKP_ON_SHADER_COMPILATION = + new Flag("--dump-skp-on-shader-compilation", "DumpSkpOnShaderCompilation"); + + /** Removes all persistent cache files for debugging. */ + private static final Flag PURGE_PERSISTENT_CACHE = + new Flag("--purge-persistent-cache", "PurgePersistentCache"); + + /** Enables logging at all severity levels. */ + private static final Flag VERBOSE_LOGGING = new Flag("--verbose-logging", "VerboseLogging"); + + /** Only cache the shader in SkSL instead of binary or GLSL. */ + private static final Flag CACHE_SKSL = new Flag("--cache-sksl", "CacheSksl"); + + /** + * Passes additional flags to the Dart VM. + * + *

All flags provided with this argument are subject to filtering based on a list of allowed + * flags in shell/common/switches.cc. If any flag provided is not allowed, the process will + * immediately terminate. + * + *

Flags should be separated by a space, e.g. "--dart-flags=--flag-1 --flag-2=2". + */ + private static final Flag DART_FLAGS = new Flag("--dart-flags=", "DartFlags"); + + // Deprecated flags: + + /** Disables the merging of the UI and platform threads. */ + @VisibleForTesting + public static final Flag DISABLE_MERGED_PLATFORM_UI_THREAD = + new Flag("--no-enable-merged-platform-ui-thread", "DisableMergedPlatformUIThread"); + + @VisibleForTesting + public static final List ALL_FLAGS = + Collections.unmodifiableList( + Arrays.asList( + VM_SERVICE_PORT, + USE_TEST_FONTS, + ENABLE_SOFTWARE_RENDERING, + SKIA_DETERMINISTIC_RENDERING, + AOT_SHARED_LIBRARY_NAME, + FLUTTER_ASSETS_DIR, + ENABLE_IMPELLER, + IMPELLER_BACKEND, + ENABLE_VULKAN_VALIDATION, + START_PAUSED, + DISABLE_SERVICE_AUTH_CODES, + ENDLESS_TRACE_BUFFER, + ENABLE_DART_PROFILING, + PROFILE_STARTUP, + TRACE_SKIA, + TRACE_SKIA_ALLOWLIST, + TRACE_SYSTRACE, + TRACE_TO_FILE, + PROFILE_MICROTASKS, + DUMP_SKP_ON_SHADER_COMPILATION, + VERBOSE_LOGGING, + DART_FLAGS, + MERGED_PLATFORM_UI_THREAD, + DISABLE_MERGED_PLATFORM_UI_THREAD, + DEPRECATED_AOT_SHARED_LIBRARY_NAME, + DEPRECATED_FLUTTER_ASSETS_DIR, + TOGGLE_IMPELLER, + OLD_GEN_HEAP_SIZE, + VM_SNAPSHOT_DATA, + ISOLATE_SNAPSHOT_DATA, + CACHE_SKSL, + PURGE_PERSISTENT_CACHE, + TRACE_STARTUP, + LEAK_VM, + TEST_FLAG)); + + // Flags that have been turned off. + private static final List DISABLED_FLAGS = + Collections.unmodifiableList(Arrays.asList(DISABLE_MERGED_PLATFORM_UI_THREAD)); + + // Lookup map for current flags that replace deprecated ones. + private static final Map DEPRECATED_FLAGS_BY_REPLACEMENT = + new HashMap() { + { + put(DEPRECATED_AOT_SHARED_LIBRARY_NAME, AOT_SHARED_LIBRARY_NAME); + put(DEPRECATED_FLUTTER_ASSETS_DIR, FLUTTER_ASSETS_DIR); + } + }; + + // Lookup map for retrieving the Flag corresponding to a specific command line argument. + private static final Map FLAG_BY_COMMAND_LINE_ARG; + + // Lookup map for retrieving the Flag corresponding to a specific metadata key. + private static final Map FLAG_BY_META_DATA_KEY; + + static { + Map map = new HashMap(ALL_FLAGS.size()); + Map metaMap = new HashMap(ALL_FLAGS.size()); + for (Flag flag : ALL_FLAGS) { + map.put(flag.commandLineArgument, flag); + metaMap.put(flag.metadataKey, flag); + } + FLAG_BY_COMMAND_LINE_ARG = Collections.unmodifiableMap(map); + FLAG_BY_META_DATA_KEY = Collections.unmodifiableMap(metaMap); + } + + /** Looks up a {@link Flag} by its commandLineArgument. */ + public static Flag getFlagByCommandLineArgument(String arg) { + int equalsIndex = arg.indexOf('='); + Flag flag = + FLAG_BY_COMMAND_LINE_ARG.get(equalsIndex == -1 ? arg : arg.substring(0, equalsIndex + 1)); + Flag replacementFlag = getReplacementFlagIfDeprecated(flag); + return replacementFlag != null ? replacementFlag : flag; + } + + /** + * Looks up a {@link Flag} by its Intent key. + * + *

Previously, the Intent keys were used to set Flutter shell arguments via Intent. The Intent + * keys match the command line argument without the "--" prefix and "=" suffix if the argument + * takes a value. + */ + public static Flag getFlagFromIntentKey(String intentKey) { + for (Flag flag : ALL_FLAGS) { + String commandLineArg = flag.commandLineArgument; + String key = commandLineArg.startsWith("--") ? commandLineArg.substring(2) : commandLineArg; + if (key.endsWith("=")) { + key = key.substring(0, key.length() - 1); + } + if (key.equals(intentKey)) { + return flag; + } + } + return null; + } + + /** Returns whether or not a flag is disabled and should raise an exception if used. */ + public static boolean isDisabled(Flag flag) { + return DISABLED_FLAGS.contains(flag); + } + + /** Returns the replacement flag of that given if it is deprecated. */ + public static Flag getReplacementFlagIfDeprecated(Flag flag) { + return DEPRECATED_FLAGS_BY_REPLACEMENT.get(flag); + } +} diff --git a/engine/src/flutter/shell/platform/android/io/flutter/embedding/engine/FlutterShellArgs.java b/engine/src/flutter/shell/platform/android/io/flutter/embedding/engine/FlutterShellArgs.java index 697ecc742d7..26058798bfb 100644 --- a/engine/src/flutter/shell/platform/android/io/flutter/embedding/engine/FlutterShellArgs.java +++ b/engine/src/flutter/shell/platform/android/io/flutter/embedding/engine/FlutterShellArgs.java @@ -10,7 +10,10 @@ import androidx.annotation.NonNull; import java.util.*; /** - * Arguments that can be delivered to the Flutter shell when it is created. + * DEPRECATED. Please see {@link FlutterEngineFlags} for the list of arguments to use or update if + * you are adding a new flag. + * + *

Arguments that can be delivered to the Flutter shell when it is created. * *

The term "shell" refers to the native code that adapts Flutter to different platforms. * Flutter's Android Java code initializes a native "shell" and passes these arguments to that @@ -18,7 +21,10 @@ import java.util.*; * io.flutter.embedding.engine.loader.FlutterLoader#ensureInitializationComplete(Context, String[])} * for more information. */ +// TODO(camsim99): Delete this class when support for setting engine shell arguments via Intent +// is no longer supported. See https://github.com/flutter/flutter/issues/180686. @SuppressWarnings({"WeakerAccess", "unused"}) +@Deprecated public class FlutterShellArgs { public static final String ARG_KEY_TRACE_STARTUP = "trace-startup"; public static final String ARG_TRACE_STARTUP = "--trace-startup"; diff --git a/engine/src/flutter/shell/platform/android/io/flutter/embedding/engine/loader/ApplicationInfoLoader.java b/engine/src/flutter/shell/platform/android/io/flutter/embedding/engine/loader/ApplicationInfoLoader.java index 24c50298358..412af23b691 100644 --- a/engine/src/flutter/shell/platform/android/io/flutter/embedding/engine/loader/ApplicationInfoLoader.java +++ b/engine/src/flutter/shell/platform/android/io/flutter/embedding/engine/loader/ApplicationInfoLoader.java @@ -10,24 +10,19 @@ import android.content.pm.PackageManager; import android.content.res.XmlResourceParser; import android.os.Bundle; import androidx.annotation.NonNull; +import io.flutter.embedding.engine.FlutterEngineFlags; import java.io.IOException; import org.json.JSONArray; import org.xmlpull.v1.XmlPullParserException; /** Loads application information given a Context. */ public final class ApplicationInfoLoader { - // XML Attribute keys supported in AndroidManifest.xml - public static final String PUBLIC_AOT_SHARED_LIBRARY_NAME = - FlutterLoader.class.getName() + '.' + FlutterLoader.AOT_SHARED_LIBRARY_NAME; - public static final String PUBLIC_VM_SNAPSHOT_DATA_KEY = - FlutterLoader.class.getName() + '.' + FlutterLoader.VM_SNAPSHOT_DATA_KEY; - public static final String PUBLIC_ISOLATE_SNAPSHOT_DATA_KEY = - FlutterLoader.class.getName() + '.' + FlutterLoader.ISOLATE_SNAPSHOT_DATA_KEY; - public static final String PUBLIC_FLUTTER_ASSETS_DIR_KEY = - FlutterLoader.class.getName() + '.' + FlutterLoader.FLUTTER_ASSETS_DIR_KEY; + // TODO(camsim99): Remove support for these flags: + // https://github.com/flutter/flutter/issues/179276. + // AndroidManifest.xml metadata keys for setting internal respective Flutter configuration values. public static final String NETWORK_POLICY_METADATA_KEY = "io.flutter.network-policy"; public static final String PUBLIC_AUTOMATICALLY_REGISTER_PLUGINS_METADATA_KEY = - "io.flutter." + FlutterLoader.AUTOMATICALLY_REGISTER_PLUGINS_KEY; + "io.flutter.automatically-register-plugins"; @NonNull private static ApplicationInfo getApplicationInfo(@NonNull Context applicationContext) { @@ -47,6 +42,20 @@ public final class ApplicationInfoLoader { return metadata.getString(key, null); } + private static String getStringWithFallback(Bundle metadata, String key, String fallbackKey) { + if (metadata == null) { + return null; + } + + String metadataString = metadata.getString(key, null); + + if (metadataString == null) { + metadataString = metadata.getString(fallbackKey); + } + + return metadataString; + } + private static boolean getBoolean(Bundle metadata, String key, boolean defaultValue) { if (metadata == null) { return defaultValue; @@ -146,11 +155,21 @@ public final class ApplicationInfoLoader { @NonNull public static FlutterApplicationInfo load(@NonNull Context applicationContext) { ApplicationInfo appInfo = getApplicationInfo(applicationContext); + + // TODO(camsim99): Remove support for DEPRECATED_AOT_SHARED_LIBRARY_NAME and + // DEPRECATED_FLUTTER_ASSETS_DIR + // when all usage of the deprecated names has been removed. return new FlutterApplicationInfo( - getString(appInfo.metaData, PUBLIC_AOT_SHARED_LIBRARY_NAME), - getString(appInfo.metaData, PUBLIC_VM_SNAPSHOT_DATA_KEY), - getString(appInfo.metaData, PUBLIC_ISOLATE_SNAPSHOT_DATA_KEY), - getString(appInfo.metaData, PUBLIC_FLUTTER_ASSETS_DIR_KEY), + getStringWithFallback( + appInfo.metaData, + FlutterEngineFlags.DEPRECATED_AOT_SHARED_LIBRARY_NAME.metadataKey, + FlutterEngineFlags.AOT_SHARED_LIBRARY_NAME.metadataKey), + getString(appInfo.metaData, FlutterEngineFlags.VM_SNAPSHOT_DATA.metadataKey), + getString(appInfo.metaData, FlutterEngineFlags.ISOLATE_SNAPSHOT_DATA.metadataKey), + getStringWithFallback( + appInfo.metaData, + FlutterEngineFlags.DEPRECATED_FLUTTER_ASSETS_DIR.metadataKey, + FlutterEngineFlags.FLUTTER_ASSETS_DIR.metadataKey), getNetworkPolicy(appInfo, applicationContext), appInfo.nativeLibraryDir, getBoolean(appInfo.metaData, PUBLIC_AUTOMATICALLY_REGISTER_PLUGINS_METADATA_KEY, true)); diff --git a/engine/src/flutter/shell/platform/android/io/flutter/embedding/engine/loader/FlutterLoader.java b/engine/src/flutter/shell/platform/android/io/flutter/embedding/engine/loader/FlutterLoader.java index 07df17b5d85..6ea5875ce67 100644 --- a/engine/src/flutter/shell/platform/android/io/flutter/embedding/engine/loader/FlutterLoader.java +++ b/engine/src/flutter/shell/platform/android/io/flutter/embedding/engine/loader/FlutterLoader.java @@ -22,6 +22,7 @@ import androidx.annotation.VisibleForTesting; import io.flutter.BuildConfig; import io.flutter.FlutterInjector; import io.flutter.Log; +import io.flutter.embedding.engine.FlutterEngineFlags; import io.flutter.embedding.engine.FlutterJNI; import io.flutter.util.HandlerCompat; import io.flutter.util.PathUtils; @@ -38,49 +39,10 @@ import java.util.concurrent.Future; public class FlutterLoader { private static final String TAG = "FlutterLoader"; - private static final String OLD_GEN_HEAP_SIZE_META_DATA_KEY = - "io.flutter.embedding.android.OldGenHeapSize"; - private static final String ENABLE_IMPELLER_META_DATA_KEY = - "io.flutter.embedding.android.EnableImpeller"; - private static final String ENABLE_VULKAN_VALIDATION_META_DATA_KEY = - "io.flutter.embedding.android.EnableVulkanValidation"; - private static final String IMPELLER_BACKEND_META_DATA_KEY = - "io.flutter.embedding.android.ImpellerBackend"; - private static final String IMPELLER_OPENGL_GPU_TRACING_DATA_KEY = - "io.flutter.embedding.android.EnableOpenGLGPUTracing"; - private static final String IMPELLER_VULKAN_GPU_TRACING_DATA_KEY = - "io.flutter.embedding.android.EnableVulkanGPUTracing"; - private static final String DISABLE_MERGED_PLATFORM_UI_THREAD_KEY = - "io.flutter.embedding.android.DisableMergedPlatformUIThread"; - private static final String ENABLE_SURFACE_CONTROL = - "io.flutter.embedding.android.EnableSurfaceControl"; - private static final String ENABLE_FLUTTER_GPU = "io.flutter.embedding.android.EnableFlutterGPU"; - private static final String IMPELLER_LAZY_SHADER_MODE = - "io.flutter.embedding.android.ImpellerLazyShaderInitialization"; - private static final String IMPELLER_ANTIALIAS_LINES = - "io.flutter.embedding.android.ImpellerAntialiasLines"; - - /** - * Set whether leave or clean up the VM after the last shell shuts down. It can be set from app's - * meta-data in in AndroidManifest.xml. Set it to true in to leave the Dart VM, - * set it to false to destroy VM. - * - *

If your want to let your app destroy the last shell and re-create shells more quickly, set - * it to true, otherwise if you want to clean up the memory of the leak VM, set it to false. - * - *

TODO(eggfly): Should it be set to false by default? - * https://github.com/flutter/flutter/issues/96843 - */ - private static final String LEAK_VM_META_DATA_KEY = "io.flutter.embedding.android.LeakVM"; - - // Must match values in flutter::switches - static final String AOT_SHARED_LIBRARY_NAME = "aot-shared-library-name"; - static final String AOT_VMSERVICE_SHARED_LIBRARY_NAME = "aot-vmservice-shared-library-name"; - static final String SNAPSHOT_ASSET_PATH_KEY = "snapshot-asset-path"; - static final String VM_SNAPSHOT_DATA_KEY = "vm-snapshot-data"; - static final String ISOLATE_SNAPSHOT_DATA_KEY = "isolate-snapshot-data"; - static final String FLUTTER_ASSETS_DIR_KEY = "flutter-assets-dir"; - static final String AUTOMATICALLY_REGISTER_PLUGINS_KEY = "automatically-register-plugins"; + // Flags to only be set internally by default. Match values in flutter::switches. + private static final String SNAPSHOT_ASSET_PATH_KEY = "snapshot-asset-path"; + private static final String AOT_VMSERVICE_SHARED_LIBRARY_NAME = + "aot-vmservice-shared-library-name"; // Resource names used for components of the precompiled snapshot. private static final String DEFAULT_LIBRARY = "libflutter.so"; @@ -89,8 +51,7 @@ public class FlutterLoader { private static FlutterLoader instance; - @VisibleForTesting - static final String aotSharedLibraryNameFlag = "--" + AOT_SHARED_LIBRARY_NAME + "="; + private boolean enableSoftwareRendering = false; /** * Creates a {@code FlutterLoader} that uses a default constructed {@link FlutterJNI} and {@link @@ -295,6 +256,21 @@ public class FlutterLoader { */ public void ensureInitializationComplete( @NonNull Context applicationContext, @Nullable String[] args) { + ensureInitializationComplete(applicationContext, args, BuildConfig.RELEASE); + } + + /** + * 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. + * @param isRelease Whether or not the Flutter component is running in release mode. + */ + @VisibleForTesting + void ensureInitializationComplete( + @NonNull Context applicationContext, @Nullable String[] args, @NonNull boolean isRelease) { if (initialized) { return; } @@ -311,59 +287,174 @@ public class FlutterLoader { InitResult result = initResultFuture.get(); List shellArgs = new ArrayList<>(); - shellArgs.add("--icu-symbol-prefix=_binary_icudtl_dat"); + // Add engine flags for which defaults set internally take precedent. + shellArgs.add("--icu-symbol-prefix=_binary_icudtl_dat"); shellArgs.add( "--icu-native-lib-path=" + flutterApplicationInfo.nativeLibraryDir + File.separator + DEFAULT_LIBRARY); - if (args != null) { - for (String arg : args) { - // Perform security check for path containing application's compiled Dart code and - // potentially user-provided compiled native code. - if (arg.startsWith(aotSharedLibraryNameFlag)) { - String safeAotSharedLibraryNameFlag = - getSafeAotSharedLibraryNameFlag(applicationContext, arg); - if (safeAotSharedLibraryNameFlag != null) { - arg = safeAotSharedLibraryNameFlag; - } else { - // If the library path is not safe, we will skip adding this argument. - Log.w( - TAG, - "Skipping unsafe AOT shared library name flag: " - + arg - + ". Please ensure that the library is vetted and placed in your application's internal storage."); - continue; - } + // Add engine flags provided by metadata in the application manifest. These settings will take + // precedent over any defaults set below, but will be overridden if additionally set by the + // command line. + ApplicationInfo applicationInfo = + applicationContext + .getPackageManager() + .getApplicationInfo( + applicationContext.getPackageName(), PackageManager.GET_META_DATA); + Bundle applicationMetaData = applicationInfo.metaData; + boolean oldGenHeapSizeSet = false; + boolean isLeakVMSet = false; + + if (applicationMetaData != null) { + for (FlutterEngineFlags.Flag flag : FlutterEngineFlags.ALL_FLAGS) { + String metadataKey = flag.metadataKey; + if (!applicationMetaData.containsKey(metadataKey)) { + continue; + } + + // Check if flag is valid: + + if (flag == FlutterEngineFlags.TEST_FLAG) { + Log.w( + TAG, + "For testing purposes only: test flag specified in the manifest was loaded by the FlutterLoader."); + continue; + } else if (FlutterEngineFlags.isDisabled(flag)) { + // Do not allow disabled flags. + throw new IllegalArgumentException( + metadataKey + + " is disabled and no longer allowed. Please remove this flag from your application manifest."); + } else if (FlutterEngineFlags.getReplacementFlagIfDeprecated(flag) != null) { + Log.w( + TAG, + "If you are trying to specify " + + metadataKey + + " in your application manifest, please make sure to use the new metadata key name: " + + FlutterEngineFlags.getReplacementFlagIfDeprecated(flag)); + } else if (!flag.allowedInRelease && isRelease) { + // Manifest flag is not allowed in release builds. + Log.w( + TAG, + "Flag with metadata key " + + metadataKey + + " is not allowed in release builds and will be ignored if specified in the application manifest or via the command line."); + continue; + } + + // Handle special cases for specific flags: + + if (flag == FlutterEngineFlags.OLD_GEN_HEAP_SIZE) { + // Mark if old gen heap size is set to track whether or not to set default + // internally. + oldGenHeapSizeSet = true; + } else if (flag == FlutterEngineFlags.LEAK_VM) { + // Mark if leak VM is set to track whether or not to set default internally. + isLeakVMSet = true; + } else if (flag == FlutterEngineFlags.ENABLE_SOFTWARE_RENDERING) { + // Enabling software rendering impacts platform views, so save this value + // so that the PlatformViewsController can be properly configured. + enableSoftwareRendering = + applicationMetaData.getBoolean( + FlutterEngineFlags.ENABLE_SOFTWARE_RENDERING.metadataKey, false); + } else if (flag == FlutterEngineFlags.AOT_SHARED_LIBRARY_NAME + || flag == FlutterEngineFlags.DEPRECATED_AOT_SHARED_LIBRARY_NAME) { + // Perform security check for path containing application's compiled Dart + // code and potentially user-provided compiled native code. + String aotSharedLibraryPath = applicationMetaData.getString(metadataKey); + maybeAddAotSharedLibraryNameArg(applicationContext, aotSharedLibraryPath, shellArgs); + continue; + } + + // Add flag to shell args. + String arg = flag.commandLineArgument; + if (flag.hasValue()) { + Object valueObj = applicationMetaData.get(metadataKey); + String value = valueObj != null ? valueObj.toString() : null; + if (value == null) { + Log.e( + TAG, + "Flag with metadata key " + + metadataKey + + " requires a value, but no value was found. Please ensure that the value is a string."); + continue; + } + arg += value; } - // TODO(camsim99): This is a dangerous pattern that blindly allows potentially malicious - // arguments to be used for engine initialization and should be fixed. See - // https://github.com/flutter/flutter/issues/172553. shellArgs.add(arg); } } + // Add any remaining engine flags provided by the command line. These settings will take + // precedent over any flag settings specified by application manifest + // metadata and any defaults set below. + if (args != null) { + for (String arg : args) { + FlutterEngineFlags.Flag flag = FlutterEngineFlags.getFlagByCommandLineArgument(arg); + if (flag == null) { + // TODO(camsim99): Reject unknown flags specified on the command line: + // https://github.com/flutter/flutter/issues/182557. + shellArgs.add(arg); + continue; + } else if (flag.equals(FlutterEngineFlags.TEST_FLAG)) { + Log.w( + TAG, + "For testing purposes only: test flag specified on the command line was loaded by the FlutterLoader."); + continue; + } else if (flag.equals(FlutterEngineFlags.AOT_SHARED_LIBRARY_NAME) + || flag.equals(FlutterEngineFlags.DEPRECATED_AOT_SHARED_LIBRARY_NAME)) { + // Perform security check for path containing application's compiled Dart + // code and potentially user-provided compiled native code. + String aotSharedLibraryPath = + arg.substring( + FlutterEngineFlags.AOT_SHARED_LIBRARY_NAME.commandLineArgument.length()); + maybeAddAotSharedLibraryNameArg(applicationContext, aotSharedLibraryPath, shellArgs); + continue; + } else if (!flag.allowedInRelease && isRelease) { + // Flag is not allowed in release builds. + Log.w( + TAG, + "Command line argument " + + arg + + " is not allowed in release builds and will be ignored if specified in the application manifest or via the command line."); + continue; + } + + shellArgs.add(arg); + } + } + + // Add engine flags set by default internally. Some of these settings can be overridden + // by command line args or application manifest metadata. + String kernelPath = null; if (BuildConfig.DEBUG || BuildConfig.JIT_RELEASE) { String snapshotAssetPath = result.dataDirPath + File.separator + flutterApplicationInfo.flutterAssetsDir; kernelPath = snapshotAssetPath + File.separator + DEFAULT_KERNEL_BLOB; shellArgs.add("--" + SNAPSHOT_ASSET_PATH_KEY + "=" + snapshotAssetPath); - shellArgs.add("--" + VM_SNAPSHOT_DATA_KEY + "=" + flutterApplicationInfo.vmSnapshotData); shellArgs.add( - "--" + ISOLATE_SNAPSHOT_DATA_KEY + "=" + flutterApplicationInfo.isolateSnapshotData); + FlutterEngineFlags.VM_SNAPSHOT_DATA.commandLineArgument + + flutterApplicationInfo.vmSnapshotData); + shellArgs.add( + FlutterEngineFlags.ISOLATE_SNAPSHOT_DATA.commandLineArgument + + flutterApplicationInfo.isolateSnapshotData); } else { - // Add default AOT shared library name arg. - shellArgs.add(aotSharedLibraryNameFlag + flutterApplicationInfo.aotSharedLibraryName); + // Add default AOT shared library name arg. Note that if a different library + // is set in the manifest, that value will take precendence and the default + // libraries will be used as fallbacks in the order that they are added. + shellArgs.add( + FlutterEngineFlags.AOT_SHARED_LIBRARY_NAME.commandLineArgument + + flutterApplicationInfo.aotSharedLibraryName); // Some devices cannot load the an AOT shared library based on the library name // with no directory path. So, we provide a fully qualified path to the default library // as a workaround for devices where that fails. shellArgs.add( - aotSharedLibraryNameFlag + FlutterEngineFlags.AOT_SHARED_LIBRARY_NAME.commandLineArgument + flutterApplicationInfo.nativeLibraryDir + File.separator + flutterApplicationInfo.aotSharedLibraryName); @@ -384,23 +475,17 @@ public class FlutterLoader { shellArgs.add("--log-tag=" + settings.getLogTag()); } - ApplicationInfo applicationInfo = - applicationContext - .getPackageManager() - .getApplicationInfo( - applicationContext.getPackageName(), PackageManager.GET_META_DATA); - Bundle metaData = applicationInfo.metaData; - int oldGenHeapSizeMegaBytes = - metaData != null ? metaData.getInt(OLD_GEN_HEAP_SIZE_META_DATA_KEY) : 0; - if (oldGenHeapSizeMegaBytes == 0) { - // default to half of total memory. + if (!oldGenHeapSizeSet) { + // Default to half of total memory. ActivityManager activityManager = (ActivityManager) applicationContext.getSystemService(Context.ACTIVITY_SERVICE); ActivityManager.MemoryInfo memInfo = new ActivityManager.MemoryInfo(); activityManager.getMemoryInfo(memInfo); - oldGenHeapSizeMegaBytes = (int) (memInfo.totalMem / 1e6 / 2); + int oldGenHeapSizeMegaBytes = (int) (memInfo.totalMem / 1e6 / 2); + shellArgs.add( + FlutterEngineFlags.OLD_GEN_HEAP_SIZE.commandLineArgument + + String.valueOf(oldGenHeapSizeMegaBytes)); } - shellArgs.add("--old-gen-heap-size=" + oldGenHeapSizeMegaBytes); DisplayMetrics displayMetrics = applicationContext.getResources().getDisplayMetrics(); int screenWidth = displayMetrics.widthPixels; @@ -412,49 +497,10 @@ public class FlutterLoader { shellArgs.add("--prefetched-default-font-manager"); - if (metaData != null) { - if (metaData.containsKey(ENABLE_IMPELLER_META_DATA_KEY)) { - if (metaData.getBoolean(ENABLE_IMPELLER_META_DATA_KEY)) { - shellArgs.add("--enable-impeller=true"); - } else { - shellArgs.add("--enable-impeller=false"); - } - } - if (metaData.getBoolean(ENABLE_VULKAN_VALIDATION_META_DATA_KEY, false)) { - shellArgs.add("--enable-vulkan-validation"); - } - if (metaData.getBoolean(IMPELLER_OPENGL_GPU_TRACING_DATA_KEY, false)) { - shellArgs.add("--enable-opengl-gpu-tracing"); - } - if (metaData.getBoolean(IMPELLER_VULKAN_GPU_TRACING_DATA_KEY, false)) { - shellArgs.add("--enable-vulkan-gpu-tracing"); - } - if (metaData.getBoolean(DISABLE_MERGED_PLATFORM_UI_THREAD_KEY, false)) { - throw new IllegalArgumentException( - DISABLE_MERGED_PLATFORM_UI_THREAD_KEY + " is no longer allowed."); - } - if (metaData.getBoolean(ENABLE_FLUTTER_GPU, false)) { - shellArgs.add("--enable-flutter-gpu"); - } - if (metaData.getBoolean(ENABLE_SURFACE_CONTROL, false)) { - shellArgs.add("--enable-surface-control"); - } - - String backend = metaData.getString(IMPELLER_BACKEND_META_DATA_KEY); - if (backend != null) { - shellArgs.add("--impeller-backend=" + backend); - } - if (metaData.getBoolean(IMPELLER_LAZY_SHADER_MODE)) { - shellArgs.add("--impeller-lazy-shader-mode"); - } - if (metaData.getBoolean(IMPELLER_ANTIALIAS_LINES)) { - shellArgs.add("--impeller-antialias-lines"); - } + if (!isLeakVMSet) { + shellArgs.add(FlutterEngineFlags.LEAK_VM.commandLineArgument + "true"); } - final String leakVM = isLeakVM(metaData) ? "true" : "false"; - shellArgs.add("--leak-vm=" + leakVM); - long initTimeMillis = SystemClock.uptimeMillis() - initStartTimestampMillis; flutterJNI.init( @@ -474,9 +520,56 @@ public class FlutterLoader { } /** - * Returns the AOT shared library name flag with the canonical path to the library that the engine - * will use to load application's Dart code if it lives within a path we consider safe, which is a - * path within the application's internal storage. Otherwise, returns null. + * Adds the AOT shared library name argument to the shell args if the provided path is safe. + * + *

If the path is safe, it will be added to the beginning of the arguments list of arguments. + * The earlier specified path takes precedence over any later specified paths for the AOT shared + * library name argument. + */ + private void maybeAddAotSharedLibraryNameArg( + @NonNull Context applicationContext, + @NonNull String aotSharedLibraryPath, + @NonNull List shellArgs) { + String safeAotSharedLibraryName = null; + try { + safeAotSharedLibraryName = + getSafeAotSharedLibraryName(applicationContext, aotSharedLibraryPath); + } catch (IOException exception) { + Log.w( + TAG, + "Error while validating AOT shared library name flag: " + aotSharedLibraryPath, + exception); + } + + if (safeAotSharedLibraryName != null) { + shellArgs.add( + 0, + FlutterEngineFlags.AOT_SHARED_LIBRARY_NAME.commandLineArgument + + safeAotSharedLibraryName); + } else { + // If the library path is not safe, we will skip adding this argument. + Log.w( + TAG, + "Skipping unsafe AOT shared library name flag: " + + aotSharedLibraryPath + + ". Please ensure that the library is vetted and placed in your application's internal storage."); + } + } + + /** + * Returns whether software rendering is enabled. + * + *

{@link #ensureInitializationComplete} must be called first in order to retrieve this value. + * Otherwise, this will return false. + */ + public boolean getSofwareRenderingEnabledViaManifest() { + return enableSoftwareRendering; + } + + /** + * Returns the canonical path to the AOT shared library that the engine will use to load the + * application's Dart code if it lives within a path we consider safe, which is a path within the + * application's internal storage. Otherwise, returns null. * *

If the library lives within the application's internal storage, this means that the * application developer either explicitly placed the library there or set the Android Gradle @@ -484,17 +577,9 @@ public class FlutterLoader { * https://developer.android.com/build/releases/past-releases/agp-4-2-0-release-notes#compress-native-libs-dsl * for more information. */ - private String getSafeAotSharedLibraryNameFlag( - @NonNull Context applicationContext, @NonNull String aotSharedLibraryNameArg) + private String getSafeAotSharedLibraryName( + @NonNull Context applicationContext, @NonNull String aotSharedLibraryPath) throws IOException { - // Isolate AOT shared library path. - if (!aotSharedLibraryNameArg.startsWith(aotSharedLibraryNameFlag)) { - throw new IllegalArgumentException( - "AOT shared library name flag was not specified correctly; please use --aot-shared-library-name=."); - } - String aotSharedLibraryPath = - aotSharedLibraryNameArg.substring(aotSharedLibraryNameFlag.length()); - // Canocalize path for safety analysis. File aotSharedLibraryFile = getFileFromPath(aotSharedLibraryPath); @@ -519,7 +604,7 @@ public class FlutterLoader { boolean isSoFile = aotSharedLibraryPathCanonicalPath.endsWith(".so"); if (livesWithinInternalStorage && isSoFile) { - return aotSharedLibraryNameFlag + aotSharedLibraryPathCanonicalPath; + return aotSharedLibraryPathCanonicalPath; } // If the library does not live within the application's internal storage, we will not use it. Log.e( @@ -535,14 +620,6 @@ public class FlutterLoader { return new File(path); } - private static boolean isLeakVM(@Nullable Bundle metaData) { - final boolean leakVMDefaultValue = true; - if (metaData == null) { - return leakVMDefaultValue; - } - return metaData.getBoolean(LEAK_VM_META_DATA_KEY, leakVMDefaultValue); - } - /** * Same as {@link #ensureInitializationComplete(Context, String[])} but waiting on a background * thread, then invoking {@code callback} on the {@code callbackHandler}. diff --git a/engine/src/flutter/shell/platform/android/test/io/flutter/embedding/android/FlutterActivityAndFragmentDelegateTest.java b/engine/src/flutter/shell/platform/android/test/io/flutter/embedding/android/FlutterActivityAndFragmentDelegateTest.java index ca2e40d1b96..253ecf5005f 100644 --- a/engine/src/flutter/shell/platform/android/test/io/flutter/embedding/android/FlutterActivityAndFragmentDelegateTest.java +++ b/engine/src/flutter/shell/platform/android/test/io/flutter/embedding/android/FlutterActivityAndFragmentDelegateTest.java @@ -40,7 +40,6 @@ import io.flutter.embedding.engine.FlutterEngine; import io.flutter.embedding.engine.FlutterEngineCache; import io.flutter.embedding.engine.FlutterEngineGroup; import io.flutter.embedding.engine.FlutterEngineGroupCache; -import io.flutter.embedding.engine.FlutterShellArgs; import io.flutter.embedding.engine.dart.DartExecutor; import io.flutter.embedding.engine.loader.FlutterLoader; import io.flutter.embedding.engine.plugins.activity.ActivityControlSurface; @@ -89,7 +88,6 @@ public class FlutterActivityAndFragmentDelegateTest { mockHost = mock(FlutterActivityAndFragmentDelegate.Host.class); when(mockHost.getContext()).thenReturn(ctx); when(mockHost.getLifecycle()).thenReturn(mock(Lifecycle.class)); - when(mockHost.getFlutterShellArgs()).thenReturn(new FlutterShellArgs(new String[] {})); when(mockHost.getDartEntrypointFunctionName()).thenReturn("main"); when(mockHost.getDartEntrypointArgs()).thenReturn(null); when(mockHost.getAppBundlePath()).thenReturn("/fake/path"); @@ -106,7 +104,6 @@ public class FlutterActivityAndFragmentDelegateTest { mockHost2 = mock(FlutterActivityAndFragmentDelegate.Host.class); when(mockHost2.getContext()).thenReturn(ctx); when(mockHost2.getLifecycle()).thenReturn(mock(Lifecycle.class)); - when(mockHost2.getFlutterShellArgs()).thenReturn(new FlutterShellArgs(new String[] {})); when(mockHost2.getDartEntrypointFunctionName()).thenReturn("main"); when(mockHost2.getDartEntrypointArgs()).thenReturn(null); when(mockHost2.getAppBundlePath()).thenReturn("/fake/path"); @@ -471,8 +468,6 @@ public class FlutterActivityAndFragmentDelegateTest { activity -> { when(customMockHost.getActivity()).thenReturn(activity); when(customMockHost.getLifecycle()).thenReturn(mock(Lifecycle.class)); - when(customMockHost.getFlutterShellArgs()) - .thenReturn(new FlutterShellArgs(new String[] {})); when(customMockHost.getDartEntrypointFunctionName()).thenReturn("main"); when(customMockHost.getAppBundlePath()).thenReturn("/fake/path"); when(customMockHost.getInitialRoute()).thenReturn("/"); diff --git a/engine/src/flutter/shell/platform/android/test/io/flutter/embedding/android/FlutterAndroidComponentTest.java b/engine/src/flutter/shell/platform/android/test/io/flutter/embedding/android/FlutterAndroidComponentTest.java index a9ac78f4fec..001a4a17aae 100644 --- a/engine/src/flutter/shell/platform/android/test/io/flutter/embedding/android/FlutterAndroidComponentTest.java +++ b/engine/src/flutter/shell/platform/android/test/io/flutter/embedding/android/FlutterAndroidComponentTest.java @@ -302,6 +302,9 @@ public class FlutterAndroidComponentTest { @NonNull @Override + // Annotation required because support for setting engine shell arguments via Intent will be + // removed; see https://github.com/flutter/flutter/issues/180686. + @SuppressWarnings("deprecation") public FlutterShellArgs getFlutterShellArgs() { return new FlutterShellArgs(new String[] {}); } diff --git a/engine/src/flutter/shell/platform/android/test/io/flutter/embedding/engine/FlutterEngineConnectionRegistryTest.java b/engine/src/flutter/shell/platform/android/test/io/flutter/embedding/engine/FlutterEngineConnectionRegistryTest.java index 091ca42931f..311d66beba1 100644 --- a/engine/src/flutter/shell/platform/android/test/io/flutter/embedding/engine/FlutterEngineConnectionRegistryTest.java +++ b/engine/src/flutter/shell/platform/android/test/io/flutter/embedding/engine/FlutterEngineConnectionRegistryTest.java @@ -11,14 +11,19 @@ import static org.mockito.Mockito.*; import android.app.Activity; import android.content.Context; import android.content.Intent; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageManager; +import android.os.Bundle; import androidx.annotation.NonNull; import androidx.lifecycle.Lifecycle; import androidx.test.ext.junit.runners.AndroidJUnit4; import io.flutter.embedding.android.ExclusiveAppComponent; +import io.flutter.embedding.engine.dart.DartExecutor; import io.flutter.embedding.engine.loader.FlutterLoader; import io.flutter.embedding.engine.plugins.FlutterPlugin; import io.flutter.embedding.engine.plugins.activity.ActivityAware; import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding; +import io.flutter.embedding.engine.renderer.FlutterRenderer; import io.flutter.plugin.common.PluginRegistry; import io.flutter.plugin.platform.PlatformViewsController; import io.flutter.plugin.platform.PlatformViewsController2; @@ -85,6 +90,19 @@ public class FlutterEngineConnectionRegistryTest { when(flutterEngine.getPlatformViewsControllerDelegator()) .thenReturn(platformViewsControllerDelegator); + PackageManager packageManager = mock(PackageManager.class); + String packageName = "io.flutter.test"; + ApplicationInfo applicationInfo = new ApplicationInfo(); + applicationInfo.metaData = new Bundle(); + when(context.getPackageName()).thenReturn(packageName); + when(context.getPackageManager()).thenReturn(packageManager); + try { + when(packageManager.getApplicationInfo(packageName, PackageManager.GET_META_DATA)) + .thenReturn(applicationInfo); + } catch (PackageManager.NameNotFoundException e) { + fail("Mocking application info threw an exception"); + } + FlutterLoader flutterLoader = mock(FlutterLoader.class); ExclusiveAppComponent appComponent = mock(ExclusiveAppComponent.class); @@ -127,41 +145,31 @@ public class FlutterEngineConnectionRegistryTest { } @Test - public void softwareRendering() { + public void attachToActivityConfiguresSoftwareRendering() { Context context = mock(Context.class); - FlutterEngine flutterEngine = mock(FlutterEngine.class); PlatformViewsController platformViewsController = mock(PlatformViewsController.class); - PlatformViewsController2 platformViewsController2 = mock(PlatformViewsController2.class); + FlutterLoader flutterLoader = mock(FlutterLoader.class); + ExclusiveAppComponent appComponent = mock(ExclusiveAppComponent.class); + Activity activity = mock(Activity.class); + Lifecycle lifecycle = mock(Lifecycle.class); + + when(flutterEngine.getPlatformViewsController()).thenReturn(platformViewsController); PlatformViewsControllerDelegator platformViewsControllerDelegator = mock(PlatformViewsControllerDelegator.class); when(flutterEngine.getPlatformViewsControllerDelegator()) .thenReturn(platformViewsControllerDelegator); - when(flutterEngine.getPlatformViewsController()).thenReturn(platformViewsController); - when(flutterEngine.getPlatformViewsController2()).thenReturn(platformViewsController2); - - FlutterLoader flutterLoader = mock(FlutterLoader.class); - - ExclusiveAppComponent appComponent = mock(ExclusiveAppComponent.class); - Activity activity = mock(Activity.class); - when(appComponent.getAppComponent()).thenReturn(activity); - - // Test attachToActivity with an Activity that has no Intent. + when(flutterEngine.getDartExecutor()).thenReturn(mock(DartExecutor.class)); + when(flutterEngine.getRenderer()).thenReturn(mock(FlutterRenderer.class)); FlutterEngineConnectionRegistry registry = new FlutterEngineConnectionRegistry(context, flutterEngine, flutterLoader, null); - registry.attachToActivity(appComponent, mock(Lifecycle.class)); - verify(platformViewsController).setSoftwareRendering(false); - Intent intent = mock(Intent.class); - when(intent.getBooleanExtra("enable-software-rendering", false)).thenReturn(false); - when(activity.getIntent()).thenReturn(intent); + when(flutterLoader.getSofwareRenderingEnabledViaManifest()).thenReturn(true); + when(appComponent.getAppComponent()).thenReturn(activity); + when(activity.getIntent()).thenReturn(mock(Intent.class)); - registry.attachToActivity(appComponent, mock(Lifecycle.class)); - verify(platformViewsController, times(2)).setSoftwareRendering(false); + registry.attachToActivity(appComponent, lifecycle); - when(intent.getBooleanExtra("enable-software-rendering", false)).thenReturn(true); - - registry.attachToActivity(appComponent, mock(Lifecycle.class)); verify(platformViewsController).setSoftwareRendering(true); } diff --git a/engine/src/flutter/shell/platform/android/test/io/flutter/embedding/engine/FlutterEngineFlagsTest.java b/engine/src/flutter/shell/platform/android/test/io/flutter/embedding/engine/FlutterEngineFlagsTest.java new file mode 100644 index 00000000000..f5e111165ef --- /dev/null +++ b/engine/src/flutter/shell/platform/android/test/io/flutter/embedding/engine/FlutterEngineFlagsTest.java @@ -0,0 +1,111 @@ +// 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; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; + +import java.lang.reflect.Field; +import java.lang.reflect.Modifier; +import org.junit.Test; + +public class FlutterEngineFlagsTest { + + @Test + public void allFlags_containsAllFlags() { + // Count the number of declared flags in FlutterEngineFlags. + int declaredFlagsCount = 0; + for (Field field : FlutterEngineFlags.class.getDeclaredFields()) { + if (FlutterEngineFlags.Flag.class.isAssignableFrom(field.getType()) + && Modifier.isStatic(field.getModifiers()) + && Modifier.isFinal(field.getModifiers())) { + declaredFlagsCount++; + } + } + + // Check that the number of declared flags matches the size of ALL_FLAGS. + assertEquals( + "If you are adding a new Flag to FlutterEngineFlags, please make sure it is added to ALL_FLAGS as well. Otherwise, the flag will be silently ignored when specified.", + declaredFlagsCount, + FlutterEngineFlags.ALL_FLAGS.size()); + } + + // Annotation required because support for setting engine shell arguments via Intent will be + // removed; see https://github.com/flutter/flutter/issues/180686. + @SuppressWarnings("deprecation") + @Test + public void allFlags_haveExpectedMetaDataNamePrefix() { + String defaultPrefix = "io.flutter.embedding.android."; + for (FlutterEngineFlags.Flag flag : FlutterEngineFlags.ALL_FLAGS) { + // Test all non-deprecated flags that should have the default prefix. + if (!flag.equals(FlutterEngineFlags.DEPRECATED_AOT_SHARED_LIBRARY_NAME) + && !flag.equals(FlutterEngineFlags.DEPRECATED_FLUTTER_ASSETS_DIR)) { + assertTrue( + "Flag " + flag.commandLineArgument + " does not have the correct metadata key prefix.", + flag.metadataKey.startsWith(defaultPrefix)); + } + } + } + + @Test + public void getFlagByCommandLineArgument_returnsExpectedFlagWhenValidArgumentSpecified() { + FlutterEngineFlags.Flag flag = + FlutterEngineFlags.getFlagByCommandLineArgument("--flutter-assets-dir="); + assertEquals(FlutterEngineFlags.FLUTTER_ASSETS_DIR, flag); + } + + @Test + public void getFlagByCommandLineArgument_returnsNullWhenInvalidArgumentSpecified() { + assertNull(FlutterEngineFlags.getFlagFromIntentKey("--non-existent-flag")); + } + + @Test + public void getFlagFromIntentKey_returnsExpectedFlagWhenValidKeySpecified() { + // Test flag without value. + FlutterEngineFlags.Flag flag = FlutterEngineFlags.getFlagFromIntentKey("old-gen-heap-size"); + assertEquals(FlutterEngineFlags.OLD_GEN_HEAP_SIZE, flag); + + // Test with flag. + flag = FlutterEngineFlags.getFlagFromIntentKey("vm-snapshot-data"); + assertEquals(FlutterEngineFlags.VM_SNAPSHOT_DATA, flag); + } + + @Test + public void getFlagFromIntentKey_returnsNullWhenInvalidKeySpecified() { + assertNull(FlutterEngineFlags.getFlagFromIntentKey("non-existent-flag")); + } + + @Test + public void isDisabled_returnsTrueWhenFlagIsDisabled() { + assertTrue(FlutterEngineFlags.isDisabled(FlutterEngineFlags.DISABLE_MERGED_PLATFORM_UI_THREAD)); + } + + @Test + public void isDisabled_returnsFalseWhenFlagIsNotDisabled() { + assertFalse(FlutterEngineFlags.isDisabled(FlutterEngineFlags.VM_SNAPSHOT_DATA)); + } + + // Deprecated flags are tested in this test. + @SuppressWarnings("deprecation") + @Test + public void getReplacementFlagIfDeprecated_returnsExpectedFlag() { + assertEquals( + FlutterEngineFlags.AOT_SHARED_LIBRARY_NAME, + FlutterEngineFlags.getReplacementFlagIfDeprecated( + FlutterEngineFlags.DEPRECATED_AOT_SHARED_LIBRARY_NAME)); + assertEquals( + FlutterEngineFlags.FLUTTER_ASSETS_DIR, + FlutterEngineFlags.getReplacementFlagIfDeprecated( + FlutterEngineFlags.DEPRECATED_FLUTTER_ASSETS_DIR)); + } + + @Test + public void getReplacementFlagIfDeprecated_returnsNullWhenFlagIsNotDeprecated() { + assertNull( + FlutterEngineFlags.getReplacementFlagIfDeprecated(FlutterEngineFlags.VM_SNAPSHOT_DATA)); + } +} diff --git a/engine/src/flutter/shell/platform/android/test/io/flutter/embedding/engine/FlutterShellArgsTest.java b/engine/src/flutter/shell/platform/android/test/io/flutter/embedding/engine/FlutterShellArgsTest.java index 0f05dc67f67..9d1be0e26d7 100644 --- a/engine/src/flutter/shell/platform/android/test/io/flutter/embedding/engine/FlutterShellArgsTest.java +++ b/engine/src/flutter/shell/platform/android/test/io/flutter/embedding/engine/FlutterShellArgsTest.java @@ -18,6 +18,7 @@ import org.junit.runner.RunWith; @RunWith(AndroidJUnit4.class) public class FlutterShellArgsTest { @Test + @SuppressWarnings("deprecation") public void itProcessesShellFlags() { // Setup the test. Intent intent = new Intent(); diff --git a/engine/src/flutter/shell/platform/android/test/io/flutter/embedding/engine/deferredcomponents/PlayStoreDeferredComponentManagerTest.java b/engine/src/flutter/shell/platform/android/test/io/flutter/embedding/engine/deferredcomponents/PlayStoreDeferredComponentManagerTest.java index e594ce80057..20c64ea281d 100644 --- a/engine/src/flutter/shell/platform/android/test/io/flutter/embedding/engine/deferredcomponents/PlayStoreDeferredComponentManagerTest.java +++ b/engine/src/flutter/shell/platform/android/test/io/flutter/embedding/engine/deferredcomponents/PlayStoreDeferredComponentManagerTest.java @@ -23,8 +23,8 @@ import android.os.Bundle; import androidx.annotation.NonNull; import androidx.test.core.app.ApplicationProvider; import androidx.test.ext.junit.runners.AndroidJUnit4; +import io.flutter.embedding.engine.FlutterEngineFlags; import io.flutter.embedding.engine.FlutterJNI; -import io.flutter.embedding.engine.loader.ApplicationInfoLoader; import java.io.File; import org.junit.Test; import org.junit.runner.RunWith; @@ -129,8 +129,8 @@ public class PlayStoreDeferredComponentManagerTest { TestFlutterJNI jni = new TestFlutterJNI(); Bundle bundle = new Bundle(); - bundle.putString(ApplicationInfoLoader.PUBLIC_AOT_SHARED_LIBRARY_NAME, "custom_name.so"); - bundle.putString(ApplicationInfoLoader.PUBLIC_FLUTTER_ASSETS_DIR_KEY, "custom_assets"); + bundle.putString(FlutterEngineFlags.AOT_SHARED_LIBRARY_NAME.metadataKey, "custom_name.so"); + bundle.putString(FlutterEngineFlags.FLUTTER_ASSETS_DIR.metadataKey, "custom_assets"); Context spyContext = createSpyContext(bundle); doReturn(null).when(spyContext).getAssets(); @@ -162,7 +162,7 @@ public class PlayStoreDeferredComponentManagerTest { Bundle bundle = new Bundle(); bundle.putString(PlayStoreDeferredComponentManager.MAPPING_KEY, "123:module:custom_name.so"); - bundle.putString(ApplicationInfoLoader.PUBLIC_FLUTTER_ASSETS_DIR_KEY, "custom_assets"); + bundle.putString(FlutterEngineFlags.FLUTTER_ASSETS_DIR.metadataKey, "custom_assets"); Context spyContext = createSpyContext(bundle); doReturn(null).when(spyContext).getAssets(); @@ -194,7 +194,7 @@ public class PlayStoreDeferredComponentManagerTest { Bundle bundle = new Bundle(); bundle.putString( PlayStoreDeferredComponentManager.MAPPING_KEY, "123:module:custom_name.so,3:,4:"); - bundle.putString(ApplicationInfoLoader.PUBLIC_FLUTTER_ASSETS_DIR_KEY, "custom_assets"); + bundle.putString(FlutterEngineFlags.FLUTTER_ASSETS_DIR.metadataKey, "custom_assets"); Context spyContext = createSpyContext(bundle); doReturn(null).when(spyContext).getAssets(); diff --git a/engine/src/flutter/shell/platform/android/test/io/flutter/embedding/engine/loader/ApplicationInfoLoaderTest.java b/engine/src/flutter/shell/platform/android/test/io/flutter/embedding/engine/loader/ApplicationInfoLoaderTest.java index 4a5f6183305..387781718ba 100644 --- a/engine/src/flutter/shell/platform/android/test/io/flutter/embedding/engine/loader/ApplicationInfoLoaderTest.java +++ b/engine/src/flutter/shell/platform/android/test/io/flutter/embedding/engine/loader/ApplicationInfoLoaderTest.java @@ -23,6 +23,7 @@ import android.content.res.XmlResourceParser; import android.os.Bundle; import androidx.test.core.app.ApplicationProvider; import androidx.test.ext.junit.runners.AndroidJUnit4; +import io.flutter.embedding.engine.FlutterEngineFlags; import java.io.StringReader; import org.junit.Test; import org.junit.runner.RunWith; @@ -70,10 +71,10 @@ public class ApplicationInfoLoaderTest { @Test public void itGeneratesCorrectApplicationInfoWithCustomValues() throws Exception { Bundle bundle = new Bundle(); - bundle.putString(ApplicationInfoLoader.PUBLIC_AOT_SHARED_LIBRARY_NAME, "testaot"); - bundle.putString(ApplicationInfoLoader.PUBLIC_VM_SNAPSHOT_DATA_KEY, "testvmsnapshot"); - bundle.putString(ApplicationInfoLoader.PUBLIC_ISOLATE_SNAPSHOT_DATA_KEY, "testisolatesnapshot"); - bundle.putString(ApplicationInfoLoader.PUBLIC_FLUTTER_ASSETS_DIR_KEY, "testassets"); + bundle.putString(FlutterEngineFlags.AOT_SHARED_LIBRARY_NAME.metadataKey, "testaot"); + bundle.putString(FlutterEngineFlags.VM_SNAPSHOT_DATA.metadataKey, "testvmsnapshot"); + bundle.putString(FlutterEngineFlags.ISOLATE_SNAPSHOT_DATA.metadataKey, "testisolatesnapshot"); + bundle.putString(FlutterEngineFlags.FLUTTER_ASSETS_DIR.metadataKey, "testassets"); Context context = generateMockContext(bundle, null); FlutterApplicationInfo info = ApplicationInfoLoader.load(context); assertNotNull(info); diff --git a/engine/src/flutter/shell/platform/android/test/io/flutter/embedding/engine/loader/FlutterLoaderTest.java b/engine/src/flutter/shell/platform/android/test/io/flutter/embedding/engine/loader/FlutterLoaderTest.java index eff98068a1e..66dad8c83ff 100644 --- a/engine/src/flutter/shell/platform/android/test/io/flutter/embedding/engine/loader/FlutterLoaderTest.java +++ b/engine/src/flutter/shell/platform/android/test/io/flutter/embedding/engine/loader/FlutterLoaderTest.java @@ -8,6 +8,7 @@ import static android.os.Looper.getMainLooper; import static junit.framework.TestCase.assertFalse; import static junit.framework.TestCase.assertTrue; import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertThrows; import static org.junit.Assert.fail; import static org.mockito.Mockito.any; import static org.mockito.Mockito.anyInt; @@ -203,35 +204,6 @@ public class FlutterLoaderTest { assertTrue(arguments.contains(leakVMArg)); } - @Test - public void itSetsTheLeakVMFromMetaData() { - FlutterJNI mockFlutterJNI = mock(FlutterJNI.class); - FlutterLoader flutterLoader = new FlutterLoader(mockFlutterJNI); - Bundle metaData = new Bundle(); - metaData.putBoolean("io.flutter.embedding.android.LeakVM", false); - ctx.getApplicationInfo().metaData = metaData; - - FlutterLoader.Settings settings = new FlutterLoader.Settings(); - assertFalse(flutterLoader.initialized()); - flutterLoader.startInitialization(ctx, settings); - flutterLoader.ensureInitializationComplete(ctx, null); - shadowOf(getMainLooper()).idle(); - - final String leakVMArg = "--leak-vm=false"; - ArgumentCaptor shellArgsCaptor = ArgumentCaptor.forClass(String[].class); - verify(mockFlutterJNI, times(1)) - .init( - eq(ctx), - shellArgsCaptor.capture(), - anyString(), - anyString(), - anyString(), - anyLong(), - anyInt()); - List arguments = Arrays.asList(shellArgsCaptor.getValue()); - assertTrue(arguments.contains(leakVMArg)); - } - @Test public void itUsesCorrectExecutorService() { FlutterJNI mockFlutterJNI = mock(FlutterJNI.class); @@ -294,22 +266,32 @@ public class FlutterLoaderTest { } @Test - public void itSetsEnableImpellerFromMetaData() { + public void itSetsDeprecatedAotSharedLibraryNameIfPathIsInInternalStorage() throws IOException { FlutterJNI mockFlutterJNI = mock(FlutterJNI.class); - FlutterLoader flutterLoader = new FlutterLoader(mockFlutterJNI); - Bundle metaData = new Bundle(); - metaData.putBoolean("io.flutter.embedding.android.EnableImpeller", true); - ctx.getApplicationInfo().metaData = metaData; + FlutterLoader flutterLoader = spy(new FlutterLoader(mockFlutterJNI)); + Context mockApplicationContext = mock(Context.class); + File internalStorageDir = ctx.getFilesDir(); + Path internalStorageDirAsPathObj = internalStorageDir.toPath(); - FlutterLoader.Settings settings = new FlutterLoader.Settings(); + ctx.getApplicationInfo().nativeLibraryDir = + Paths.get("some", "path", "doesnt", "matter").toString(); assertFalse(flutterLoader.initialized()); - flutterLoader.startInitialization(ctx, settings); - flutterLoader.ensureInitializationComplete(ctx, null); - shadowOf(getMainLooper()).idle(); + flutterLoader.startInitialization(ctx); + + // Test paths for library living within internal storage. + String librarySoFileName = "library.so"; + Path testPath = internalStorageDirAsPathObj.resolve(librarySoFileName); + + String path = testPath.toString(); + Bundle metadata = new Bundle(); + metadata.putString( + "io.flutter.embedding.engine.loader.FlutterLoader.aot-shared-library-name", path); + ctx.getApplicationInfo().metaData = metadata; + + flutterLoader.ensureInitializationComplete(ctx, null); - final String enableImpellerArg = "--enable-impeller=true"; ArgumentCaptor shellArgsCaptor = ArgumentCaptor.forClass(String[].class); - verify(mockFlutterJNI, times(1)) + verify(mockFlutterJNI) .init( eq(ctx), shellArgsCaptor.capture(), @@ -318,27 +300,51 @@ public class FlutterLoaderTest { anyString(), anyLong(), anyInt()); - List arguments = Arrays.asList(shellArgsCaptor.getValue()); - assertTrue(arguments.contains(enableImpellerArg)); + + List actualArgs = Arrays.asList(shellArgsCaptor.getValue()); + + // This check works because the tests run in debug mode. If run in release (or JIT release) + // mode, actualArgs would contain the default arguments for AOT shared library name on top + // of aotSharedLibraryNameArg. + String canonicalTestPath = testPath.toFile().getCanonicalPath(); + String canonicalAotSharedLibraryNameArg = "--aot-shared-library-name=" + canonicalTestPath; + assertTrue( + "Args sent to FlutterJni.init incorrectly did not include path " + path, + actualArgs.contains(canonicalAotSharedLibraryNameArg)); + + // Reset FlutterLoader and mockFlutterJNI to make more calls to + // FlutterLoader.ensureInitialized and mockFlutterJNI.init for testing. + flutterLoader.initialized = false; + clearInvocations(mockFlutterJNI); } @Test - public void itSetsEnableFlutterGPUFromMetaData() { + public void itSetsAotSharedLibraryNameIfPathIsInInternalStorageInReleaseMode() + throws IOException { FlutterJNI mockFlutterJNI = mock(FlutterJNI.class); - FlutterLoader flutterLoader = new FlutterLoader(mockFlutterJNI); - Bundle metaData = new Bundle(); - metaData.putBoolean("io.flutter.embedding.android.EnableFlutterGPU", true); - ctx.getApplicationInfo().metaData = metaData; + FlutterLoader flutterLoader = spy(new FlutterLoader(mockFlutterJNI)); + Context mockApplicationContext = mock(Context.class); + File internalStorageDir = ctx.getFilesDir(); + Path internalStorageDirAsPathObj = internalStorageDir.toPath(); - FlutterLoader.Settings settings = new FlutterLoader.Settings(); + ctx.getApplicationInfo().nativeLibraryDir = + Paths.get("some", "path", "doesnt", "matter").toString(); assertFalse(flutterLoader.initialized()); - flutterLoader.startInitialization(ctx, settings); - flutterLoader.ensureInitializationComplete(ctx, null); - shadowOf(getMainLooper()).idle(); + flutterLoader.startInitialization(ctx); + + // Test paths for library living within internal storage. + String librarySoFileName = "library.so"; + Path testPath = internalStorageDirAsPathObj.resolve(librarySoFileName); + + String path = testPath.toString(); + Bundle metadata = new Bundle(); + metadata.putString("io.flutter.embedding.android.AOTSharedLibraryName", path); + ctx.getApplicationInfo().metaData = metadata; + + flutterLoader.ensureInitializationComplete(ctx, null, true); - final String enableImpellerArg = "--enable-flutter-gpu"; ArgumentCaptor shellArgsCaptor = ArgumentCaptor.forClass(String[].class); - verify(mockFlutterJNI, times(1)) + verify(mockFlutterJNI) .init( eq(ctx), shellArgsCaptor.capture(), @@ -347,66 +353,22 @@ public class FlutterLoaderTest { anyString(), anyLong(), anyInt()); - List arguments = Arrays.asList(shellArgsCaptor.getValue()); - assertTrue(arguments.contains(enableImpellerArg)); - } - @Test - public void itSetsEnableSurfaceControlFromMetaData() { - FlutterJNI mockFlutterJNI = mock(FlutterJNI.class); - FlutterLoader flutterLoader = new FlutterLoader(mockFlutterJNI); - Bundle metaData = new Bundle(); - metaData.putBoolean("io.flutter.embedding.android.EnableSurfaceControl", true); - ctx.getApplicationInfo().metaData = metaData; + List actualArgs = Arrays.asList(shellArgsCaptor.getValue()); - FlutterLoader.Settings settings = new FlutterLoader.Settings(); - assertFalse(flutterLoader.initialized()); - flutterLoader.startInitialization(ctx, settings); - flutterLoader.ensureInitializationComplete(ctx, null); - shadowOf(getMainLooper()).idle(); + // This check works because the tests run in debug mode. If run in release (or JIT release) + // mode, actualArgs would contain the default arguments for AOT shared library name on top + // of aotSharedLibraryNameArg. + String canonicalTestPath = testPath.toFile().getCanonicalPath(); + String canonicalAotSharedLibraryNameArg = "--aot-shared-library-name=" + canonicalTestPath; + assertTrue( + "Args sent to FlutterJni.init incorrectly did not include path " + path, + actualArgs.contains(canonicalAotSharedLibraryNameArg)); - final String disabledControlArg = "--enable-surface-control"; - ArgumentCaptor shellArgsCaptor = ArgumentCaptor.forClass(String[].class); - verify(mockFlutterJNI, times(1)) - .init( - eq(ctx), - shellArgsCaptor.capture(), - anyString(), - anyString(), - anyString(), - anyLong(), - anyInt()); - List arguments = Arrays.asList(shellArgsCaptor.getValue()); - assertTrue(arguments.contains(disabledControlArg)); - } - - @Test - public void itSetsShaderInitModeFromMetaData() { - FlutterJNI mockFlutterJNI = mock(FlutterJNI.class); - FlutterLoader flutterLoader = new FlutterLoader(mockFlutterJNI); - Bundle metaData = new Bundle(); - metaData.putBoolean("io.flutter.embedding.android.ImpellerLazyShaderInitialization", true); - ctx.getApplicationInfo().metaData = metaData; - - FlutterLoader.Settings settings = new FlutterLoader.Settings(); - assertFalse(flutterLoader.initialized()); - flutterLoader.startInitialization(ctx, settings); - flutterLoader.ensureInitializationComplete(ctx, null); - shadowOf(getMainLooper()).idle(); - - final String shaderModeArg = "--impeller-lazy-shader-mode"; - ArgumentCaptor shellArgsCaptor = ArgumentCaptor.forClass(String[].class); - verify(mockFlutterJNI, times(1)) - .init( - eq(ctx), - shellArgsCaptor.capture(), - anyString(), - anyString(), - anyString(), - anyLong(), - anyInt()); - List arguments = Arrays.asList(shellArgsCaptor.getValue()); - assertTrue(arguments.contains(shaderModeArg)); + // Reset FlutterLoader and mockFlutterJNI to make more calls to + // FlutterLoader.ensureInitialized and mockFlutterJNI.init for testing. + flutterLoader.initialized = false; + clearInvocations(mockFlutterJNI); } @Test @@ -446,9 +408,11 @@ public class FlutterLoaderTest { for (Path testPath : pathsToTest) { String path = testPath.toString(); - String aotSharedLibraryNameArg = FlutterLoader.aotSharedLibraryNameFlag + path; - String[] args = {aotSharedLibraryNameArg}; - flutterLoader.ensureInitializationComplete(ctx, args); + Bundle metadata = new Bundle(); + metadata.putString("io.flutter.embedding.android.AOTSharedLibraryName", path); + ctx.getApplicationInfo().metaData = metadata; + + flutterLoader.ensureInitializationComplete(ctx, null); ArgumentCaptor shellArgsCaptor = ArgumentCaptor.forClass(String[].class); verify(mockFlutterJNI) @@ -467,8 +431,7 @@ public class FlutterLoaderTest { // mode, actualArgs would contain the default arguments for AOT shared library name on top // of aotSharedLibraryNameArg. String canonicalTestPath = testPath.toFile().getCanonicalPath(); - String canonicalAotSharedLibraryNameArg = - FlutterLoader.aotSharedLibraryNameFlag + canonicalTestPath; + String canonicalAotSharedLibraryNameArg = "--aot-shared-library-name=" + canonicalTestPath; assertTrue( "Args sent to FlutterJni.init incorrectly did not include path " + path, actualArgs.contains(canonicalAotSharedLibraryNameArg)); @@ -523,9 +486,11 @@ public class FlutterLoaderTest { for (Path testPath : pathsToTest) { String path = testPath.toString(); - String aotSharedLibraryNameArg = FlutterLoader.aotSharedLibraryNameFlag + path; - String[] args = {aotSharedLibraryNameArg}; - flutterLoader.ensureInitializationComplete(ctx, args); + Bundle metadata = new Bundle(); + metadata.putString("io.flutter.embedding.android.AOTSharedLibraryName", path); + ctx.getApplicationInfo().metaData = metadata; + + flutterLoader.ensureInitializationComplete(ctx, null); ArgumentCaptor shellArgsCaptor = ArgumentCaptor.forClass(String[].class); verify(mockFlutterJNI) @@ -544,8 +509,7 @@ public class FlutterLoaderTest { // mode, actualArgs would contain the default arguments for AOT shared library name on top // of aotSharedLibraryNameArg. String canonicalTestPath = testPath.toFile().getCanonicalPath(); - String canonicalAotSharedLibraryNameArg = - FlutterLoader.aotSharedLibraryNameFlag + canonicalTestPath; + String canonicalAotSharedLibraryNameArg = "--aot-shared-library-name=" + canonicalTestPath; assertFalse( "Args sent to FlutterJni.init incorrectly included canonical path " + canonicalTestPath, actualArgs.contains(canonicalAotSharedLibraryNameArg)); @@ -572,8 +536,11 @@ public class FlutterLoaderTest { String invalidFilePath = "my\0file.so"; - String[] args = {FlutterLoader.aotSharedLibraryNameFlag + invalidFilePath}; - flutterLoader.ensureInitializationComplete(ctx, args); + Bundle metadata = new Bundle(); + metadata.putString("io.flutter.embedding.android.AOTSharedLibraryName", invalidFilePath); + ctx.getApplicationInfo().metaData = metadata; + + flutterLoader.ensureInitializationComplete(ctx, null); ArgumentCaptor shellArgsCaptor = ArgumentCaptor.forClass(String[].class); verify(mockFlutterJNI) @@ -592,7 +559,7 @@ public class FlutterLoaderTest { // mode, actualArgs would contain the default arguments for AOT shared library name on top // of aotSharedLibraryNameArg. for (String arg : actualArgs) { - if (arg.startsWith(FlutterLoader.aotSharedLibraryNameFlag)) { + if (arg.startsWith("--aot-shared-library-name=")) { fail(); } } @@ -620,9 +587,11 @@ public class FlutterLoaderTest { when(flutterLoader.getFileFromPath(spySymlinkFile.getPath())).thenReturn(spySymlinkFile); doReturn(realSoFile.getCanonicalPath()).when(spySymlinkFile).getCanonicalPath(); - String symlinkArg = FlutterLoader.aotSharedLibraryNameFlag + spySymlinkFile.getPath(); - String[] args = {symlinkArg}; - flutterLoader.ensureInitializationComplete(ctx, args); + Bundle metadata = new Bundle(); + metadata.putString( + "io.flutter.embedding.android.AOTSharedLibraryName", spySymlinkFile.getPath()); + ctx.getApplicationInfo().metaData = metadata; + flutterLoader.ensureInitializationComplete(ctx, null); ArgumentCaptor shellArgsCaptor = ArgumentCaptor.forClass(String[].class); verify(mockFlutterJNI) @@ -638,12 +607,14 @@ public class FlutterLoaderTest { List actualArgs = Arrays.asList(shellArgsCaptor.getValue()); String canonicalSymlinkCanonicalizedPath = realSoFile.getCanonicalPath(); + String aotSharedLibraryNameFlag = "--aot-shared-library-name="; + String symlinkAotSharedLibraryNameArg = aotSharedLibraryNameFlag + spySymlinkFile.getPath(); String canonicalAotSharedLibraryNameArg = - FlutterLoader.aotSharedLibraryNameFlag + canonicalSymlinkCanonicalizedPath; + aotSharedLibraryNameFlag + canonicalSymlinkCanonicalizedPath; assertFalse( "Args sent to FlutterJni.init incorrectly included absolute symlink path: " + spySymlinkFile.getAbsolutePath(), - actualArgs.contains(symlinkArg)); + actualArgs.contains(symlinkAotSharedLibraryNameArg)); assertTrue( "Args sent to FlutterJni.init incorrectly did not include canonicalized path of symlink: " + canonicalSymlinkCanonicalizedPath, @@ -674,15 +645,17 @@ public class FlutterLoaderTest { List unsafeFiles = Arrays.asList(nonSoFile, fileJustOutsideInternalStorage); Files.deleteIfExists(spySymlinkFile.toPath()); - String symlinkArg = FlutterLoader.aotSharedLibraryNameFlag + spySymlinkFile.getAbsolutePath(); - String[] args = {symlinkArg}; + Bundle metadata = new Bundle(); + metadata.putString( + "io.flutter.embedding.android.AOTSharedLibraryName", spySymlinkFile.getAbsolutePath()); + ctx.getApplicationInfo().metaData = metadata; for (File unsafeFile : unsafeFiles) { // Simulate a symlink since some filesystems do not support symlinks. when(flutterLoader.getFileFromPath(spySymlinkFile.getPath())).thenReturn(spySymlinkFile); doReturn(unsafeFile.getCanonicalPath()).when(spySymlinkFile).getCanonicalPath(); - flutterLoader.ensureInitializationComplete(ctx, args); + flutterLoader.ensureInitializationComplete(ctx, null); ArgumentCaptor shellArgsCaptor = ArgumentCaptor.forClass(String[].class); verify(mockFlutterJNI) @@ -698,8 +671,11 @@ public class FlutterLoaderTest { List actualArgs = Arrays.asList(shellArgsCaptor.getValue()); String canonicalSymlinkCanonicalizedPath = unsafeFile.getCanonicalPath(); + String aotSharedLibraryNameFlag = "--aot-shared-library-name="; + String symlinkAotSharedLibraryNameArg = + aotSharedLibraryNameFlag + spySymlinkFile.getAbsolutePath(); String canonicalAotSharedLibraryNameArg = - FlutterLoader.aotSharedLibraryNameFlag + canonicalSymlinkCanonicalizedPath; + aotSharedLibraryNameFlag + canonicalSymlinkCanonicalizedPath; assertFalse( "Args sent to FlutterJni.init incorrectly included canonicalized path of symlink: " + canonicalSymlinkCanonicalizedPath, @@ -707,7 +683,7 @@ public class FlutterLoaderTest { assertFalse( "Args sent to FlutterJni.init incorrectly included absolute path of symlink: " + spySymlinkFile.getAbsolutePath(), - actualArgs.contains(symlinkArg)); + actualArgs.contains(symlinkAotSharedLibraryNameArg)); // Clean up created files. spySymlinkFile.delete(); @@ -719,4 +695,569 @@ public class FlutterLoaderTest { clearInvocations(mockFlutterJNI); } } -} \ No newline at end of file + + @Test + public void itSetsEnableSoftwareRenderingFromMetadata() { + testFlagFromMetadataPresent( + "io.flutter.embedding.android.EnableSoftwareRendering", + null, + "--enable-software-rendering"); + } + + @Test + public void getSofwareRenderingEnabledViaManifest_returnsExpectedValueWhenSetViaManifest() { + FlutterJNI mockFlutterJNI = mock(FlutterJNI.class); + FlutterLoader flutterLoader = new FlutterLoader(mockFlutterJNI); + Bundle metadata = new Bundle(); + + metadata.putBoolean("io.flutter.embedding.android.EnableSoftwareRendering", true); + + ctx.getApplicationInfo().metaData = metadata; + + FlutterLoader.Settings settings = new FlutterLoader.Settings(); + assertFalse(flutterLoader.initialized()); + flutterLoader.startInitialization(ctx, settings); + flutterLoader.ensureInitializationComplete(ctx, null); + shadowOf(getMainLooper()).idle(); + + assertTrue(flutterLoader.getSofwareRenderingEnabledViaManifest()); + } + + @Test + public void itSetsSkiaDeterministicRenderingFromMetadata() { + testFlagFromMetadataPresent( + "io.flutter.embedding.android.SkiaDeterministicRendering", + null, + "--skia-deterministic-rendering"); + } + + @Test + public void itSetsFlutterAssetsDirFromMetadata() { + String expectedAssetsDir = "flutter_assets_dir"; + // Test debug mode + testFlagFromMetadataPresent( + "io.flutter.embedding.android.FlutterAssetsDir", + expectedAssetsDir, + "--flutter-assets-dir=" + expectedAssetsDir); + + // Test release mode. + testFlagFromMetadataPresentInReleaseMode( + "io.flutter.embedding.android.FlutterAssetsDir", + expectedAssetsDir, + "--flutter-assets-dir=" + expectedAssetsDir); + } + + @Test + public void itSetsDeprecatedFlutterAssetsDirFromMetadata() { + String expectedAssetsDir = "flutter_assets_dir"; + + // Test debug mode. + testFlagFromMetadataPresent( + "io.flutter.embedding.engine.loader.FlutterLoader.flutter-assets-dir", + expectedAssetsDir, + "--flutter-assets-dir=" + expectedAssetsDir); + + // Test release mode. + testFlagFromMetadataPresentInReleaseMode( + "io.flutter.embedding.engine.loader.FlutterLoader.flutter-assets-dir", + expectedAssetsDir, + "--flutter-assets-dir=" + expectedAssetsDir); + } + + @Test + public void itSetsOldGenHeapSizeFromMetadata() { + // Test old gen heap size can be set from metadata in debug mode. + int expectedOldGenHeapSize = 256; + testFlagFromMetadataPresent( + "io.flutter.embedding.android.OldGenHeapSize", + expectedOldGenHeapSize, + "--old-gen-heap-size=" + expectedOldGenHeapSize); + + // Test old gen heap size can be set from metadta in release mode. + testFlagFromMetadataPresentInReleaseMode( + "io.flutter.embedding.android.OldGenHeapSize", + expectedOldGenHeapSize, + "--old-gen-heap-size=" + expectedOldGenHeapSize); + + // Test that default old gen heap size will not be included if it + // is configured via the manifest. + ActivityManager activityManager = + (ActivityManager) ctx.getSystemService(Context.ACTIVITY_SERVICE); + ActivityManager.MemoryInfo memInfo = new ActivityManager.MemoryInfo(); + activityManager.getMemoryInfo(memInfo); + int oldGenHeapSizeMegaBytes = (int) (memInfo.totalMem / 1e6 / 2); + testFlagFromMetadataNotPresent( + "io.flutter.embedding.android.OldGenHeapSize", + expectedOldGenHeapSize, + "--old-gen-heap-size=" + oldGenHeapSizeMegaBytes); + } + + @Test + public void itSetsToggleImpellerFromMetadata() { + // Test debug mode. + testFlagFromMetadataPresent( + "io.flutter.embedding.android.ToggleImpeller", true, "--enable-impeller=true"); + + // Test release mode. + testFlagFromMetadataPresentInReleaseMode( + "io.flutter.embedding.android.ToggleImpeller", true, "--enable-impeller=true"); + } + + @Test + public void itSetsEnableImpellerFromMetadata() { + // Test debug mode. + testFlagFromMetadataPresent( + "io.flutter.embedding.android.EnableImpeller", null, "--enable-impeller"); + + // Test release mode. + testFlagFromMetadataPresentInReleaseMode( + "io.flutter.embedding.android.EnableImpeller", null, "--enable-impeller"); + } + + @Test + public void itSetsImpellerBackendFromMetadata() { + String expectedImpellerBackend = "Vulkan"; + + // Test debug mode. + testFlagFromMetadataPresent( + "io.flutter.embedding.android.ImpellerBackend", + expectedImpellerBackend, + "--impeller-backend=" + expectedImpellerBackend); + + // Test release mode. + testFlagFromMetadataPresentInReleaseMode( + "io.flutter.embedding.android.ImpellerBackend", + expectedImpellerBackend, + "--impeller-backend=" + expectedImpellerBackend); + } + + @Test + public void itSetsVmSnapshotDataFromMetadata() { + String expectedVmSnapshotData = "vm_snapshot_data"; + + // Test debug mode. + testFlagFromMetadataPresent( + "io.flutter.embedding.android.VmSnapshotData", + expectedVmSnapshotData, + "--vm-snapshot-data=" + expectedVmSnapshotData); + + // Test release mode. + testFlagFromMetadataPresentInReleaseMode( + "io.flutter.embedding.android.VmSnapshotData", + expectedVmSnapshotData, + "--vm-snapshot-data=" + expectedVmSnapshotData); + } + + @Test + public void itSetsIsolateSnapshotDataFromMetadata() { + String expectedIsolateSnapshotData = "isolate_snapshot_data"; + + // Test debug mode. + testFlagFromMetadataPresent( + "io.flutter.embedding.android.IsolateSnapshotData", + expectedIsolateSnapshotData, + "--isolate-snapshot-data=" + expectedIsolateSnapshotData); + + // Test release mode. + testFlagFromMetadataPresentInReleaseMode( + "io.flutter.embedding.android.IsolateSnapshotData", + expectedIsolateSnapshotData, + "--isolate-snapshot-data=" + expectedIsolateSnapshotData); + } + + @Test + public void itSetsUseTestFontsFromMetadata() { + testFlagFromMetadataPresent( + "io.flutter.embedding.android.UseTestFonts", null, "--use-test-fonts"); + } + + @Test + public void itSetsVmServicePortFromMetadata() { + int expectedVmServicePort = 12345; + testFlagFromMetadataPresent( + "io.flutter.embedding.android.VMServicePort", + expectedVmServicePort, + "--vm-service-port=" + expectedVmServicePort); + } + + @Test + public void itSetsEnableVulkanValidationFromMetadata() { + testFlagFromMetadataPresent( + "io.flutter.embedding.android.EnableVulkanValidation", null, "--enable-vulkan-validation"); + } + + @Test + public void itSetsLeakVMFromMetadata() { + // Test that LeakVM can be set via manifest. + testFlagFromMetadataPresent("io.flutter.embedding.android.LeakVM", false, "--leak-vm=false"); + + // Test that default LeakVM will not be included if it is configured via the manifest. + testFlagFromMetadataNotPresent("io.flutter.embedding.android.LeakVM", false, "--leak-vm=true"); + } + + @Test + public void itSetsTraceStartupFromMetadata() { + testFlagFromMetadataPresent( + "io.flutter.embedding.android.TraceStartup", null, "--trace-startup"); + } + + @Test + public void itSetsStartPausedFromMetadata() { + testFlagFromMetadataPresent("io.flutter.embedding.android.StartPaused", null, "--start-paused"); + } + + @Test + public void itSetsDisableServiceAuthCodesFromMetadata() { + testFlagFromMetadataPresent( + "io.flutter.embedding.android.DisableServiceAuthCodes", + null, + "--disable-service-auth-codes"); + } + + @Test + public void itSetsEndlessTraceBufferFromMetadata() { + testFlagFromMetadataPresent( + "io.flutter.embedding.android.EndlessTraceBuffer", null, "--endless-trace-buffer"); + } + + @Test + public void itSetsEnableDartProfilingFromMetadata() { + // Test debug mode. + testFlagFromMetadataPresent( + "io.flutter.embedding.android.EnableDartProfiling", null, "--enable-dart-profiling"); + + // Test release mode. + testFlagFromMetadataPresentInReleaseMode( + "io.flutter.embedding.android.EnableDartProfiling", null, "--enable-dart-profiling"); + } + + @Test + public void itSetsProfileStartupFromMetadata() { + // Test debug mode. + testFlagFromMetadataPresent( + "io.flutter.embedding.android.ProfileStartup", null, "--profile-startup"); + + // Test release mode. + testFlagFromMetadataPresentInReleaseMode( + "io.flutter.embedding.android.ProfileStartup", null, "--profile-startup"); + } + + @Test + public void itSetsMergedPlatformUiThread() { + // Test debug mode. + testFlagFromMetadataPresent( + "io.flutter.embedding.android.MergedPlatformUIThread", null, "--merged-platform-ui-thread"); + + // Test release mode. + testFlagFromMetadataPresentInReleaseMode( + "io.flutter.embedding.android.MergedPlatformUIThread", null, "--merged-platform-ui-thread"); + } + + @Test + public void itSetsTraceSkiaFromMetadata() { + testFlagFromMetadataPresent("io.flutter.embedding.android.TraceSkia", null, "--trace-skia"); + } + + @Test + public void itSetsTraceSkiaAllowlistFromMetadata() { + String expectedTraceSkiaAllowList = "allowed1,allowed2,allowed3"; + testFlagFromMetadataPresent( + "io.flutter.embedding.android.TraceSkiaAllowList", + expectedTraceSkiaAllowList, + "--trace-skia-allowlist=" + expectedTraceSkiaAllowList); + } + + @Test + public void itSetsTraceSystraceFromMetadata() { + testFlagFromMetadataPresent( + "io.flutter.embedding.android.TraceSystrace", null, "--trace-systrace"); + } + + @Test + public void itSetsTraceToFileFromMetadata() { + String expectedTraceToFilePath = "/path/to/trace/file"; + testFlagFromMetadataPresent( + "io.flutter.embedding.android.TraceToFile", + expectedTraceToFilePath, + "--trace-to-file=" + expectedTraceToFilePath); + } + + @Test + public void itSetsProfileMicrotasksFromMetadata() { + testFlagFromMetadataPresent( + "io.flutter.embedding.android.ProfileMicrotasks", null, "--profile-microtasks"); + } + + @Test + public void itSetsDumpSkpOnShaderCompilationFromMetadata() { + testFlagFromMetadataPresent( + "io.flutter.embedding.android.DumpSkpOnShaderCompilation", + null, + "--dump-skp-on-shader-compilation"); + } + + @Test + public void itSetsPurgePersistentCacheFromMetadata() { + testFlagFromMetadataPresent( + "io.flutter.embedding.android.PurgePersistentCache", null, "--purge-persistent-cache"); + } + + @Test + public void itSetsVerboseLoggingFromMetadata() { + testFlagFromMetadataPresent( + "io.flutter.embedding.android.VerboseLogging", null, "--verbose-logging"); + } + + @Test + public void itSetsDartFlagsFromMetadata() { + String expectedDartFlags = "--enable-asserts --enable-vm-service"; + testFlagFromMetadataPresent( + "io.flutter.embedding.android.DartFlags", + expectedDartFlags, + "--dart-flags=" + expectedDartFlags); + } + + @Test + public void itDoesNotSetTestFlagFromMetadata() { + testFlagFromMetadataNotPresent("io.flutter.embedding.android.TestFlag", null, "--test-flag"); + } + + @Test + public void itDoesNotSetDisableMergedPlatformUIThreadFromMetadata() { + FlutterJNI mockFlutterJNI = mock(FlutterJNI.class); + FlutterLoader flutterLoader = new FlutterLoader(mockFlutterJNI); + Bundle metadata = new Bundle(); + + metadata.putBoolean("io.flutter.embedding.android.DisableMergedPlatformUIThread", true); + ctx.getApplicationInfo().metaData = metadata; + + FlutterLoader.Settings settings = new FlutterLoader.Settings(); + assertFalse(flutterLoader.initialized()); + flutterLoader.startInitialization(ctx, settings); + + // Verify that an IllegalArgumentException is thrown when DisableMergedPlatformUIThread is set, + // as it is no longer supported. + Exception exception = + assertThrows( + RuntimeException.class, () -> flutterLoader.ensureInitializationComplete(ctx, null)); + Throwable cause = exception.getCause(); + + assertNotNull(cause); + assertTrue( + "Expected cause to be IllegalArgumentException", cause instanceof IllegalArgumentException); + assertTrue( + cause + .getMessage() + .contains( + "io.flutter.embedding.android.DisableMergedPlatformUIThread is disabled and no longer allowed.")); + } + + @Test + public void itDoesSetRecognizedCommandLineArgument() { + FlutterJNI mockFlutterJNI = mock(FlutterJNI.class); + FlutterLoader flutterLoader = new FlutterLoader(mockFlutterJNI); + Bundle metadata = new Bundle(); + + String[] recognizedArg = {"--enable-impeller=true"}; + + FlutterLoader.Settings settings = new FlutterLoader.Settings(); + assertFalse(flutterLoader.initialized()); + flutterLoader.startInitialization(ctx, settings); + flutterLoader.ensureInitializationComplete(ctx, recognizedArg); + shadowOf(getMainLooper()).idle(); + + ArgumentCaptor shellArgsCaptor = ArgumentCaptor.forClass(String[].class); + verify(mockFlutterJNI, times(1)) + .init( + eq(ctx), + shellArgsCaptor.capture(), + anyString(), + anyString(), + anyString(), + anyLong(), + anyInt()); + List arguments = Arrays.asList(shellArgsCaptor.getValue()); + + assertTrue( + "Recognized argument '" + + recognizedArg[0] + + "' was not found in the arguments passed to FlutterJNI.init", + arguments.contains(recognizedArg[0])); + } + + @Test + public void ifFlagSetViaManifestAndCommandLineThenCommandLineTakesPrecedence() { + String expectedImpellerArgFromMetadata = "--enable-impeller=true"; + String expectedImpellerArgFromCommandLine = "--enable-impeller=false"; + + FlutterJNI mockFlutterJNI = mock(FlutterJNI.class); + FlutterLoader flutterLoader = new FlutterLoader(mockFlutterJNI); + Bundle metadata = new Bundle(); + + // Place metadata key and value into the metadata bundle used to mock the manifest. + metadata.putBoolean("io.flutter.embedding.android.EnableImpeller", true); + ctx.getApplicationInfo().metaData = metadata; + + FlutterLoader.Settings settings = new FlutterLoader.Settings(); + assertFalse(flutterLoader.initialized()); + flutterLoader.startInitialization(ctx, settings); + flutterLoader.ensureInitializationComplete( + ctx, new String[] {expectedImpellerArgFromCommandLine}); + shadowOf(getMainLooper()).idle(); + + ArgumentCaptor shellArgsCaptor = ArgumentCaptor.forClass(String[].class); + verify(mockFlutterJNI, times(1)) + .init( + eq(ctx), + shellArgsCaptor.capture(), + anyString(), + anyString(), + anyString(), + anyLong(), + anyInt()); + List arguments = Arrays.asList(shellArgsCaptor.getValue()); + + // Verify that the command line argument takes precedence over the manifest metadata. + assertTrue( + arguments.indexOf(expectedImpellerArgFromMetadata) + < arguments.indexOf(expectedImpellerArgFromCommandLine)); + } + + @Test + public void ifAOTSharedLibraryNameSetViaManifestAndCommandLineThenCommandLineTakesPrecedence() + throws IOException { + FlutterJNI mockFlutterJNI = mock(FlutterJNI.class); + FlutterLoader flutterLoader = spy(new FlutterLoader(mockFlutterJNI)); + File internalStorageDir = ctx.getFilesDir(); + Path internalStorageDirAsPathObj = internalStorageDir.toPath(); + + ctx.getApplicationInfo().nativeLibraryDir = + Paths.get("some", "path", "doesnt", "matter").toString(); + assertFalse(flutterLoader.initialized()); + flutterLoader.startInitialization(ctx); + + // Test paths for library living within internal storage. + Path pathWithDirectInternalStoragePath1 = internalStorageDirAsPathObj.resolve("library1.so"); + Path pathWithDirectInternalStoragePath2 = internalStorageDirAsPathObj.resolve("library2.so"); + + String expectedAotSharedLibraryNameFromMetadata = + "--aot-shared-library-name=" + + pathWithDirectInternalStoragePath1.toFile().getCanonicalPath(); + String expectedAotSharedLibraryNameFromCommandLine = + "--aot-shared-library-name=" + + pathWithDirectInternalStoragePath2.toFile().getCanonicalPath(); + + Bundle metadata = new Bundle(); + + // Place metadata key and value into the metadata bundle used to mock the manifest. + metadata.putString( + "io.flutter.embedding.android.AOTSharedLibraryName", + pathWithDirectInternalStoragePath1.toFile().getCanonicalPath()); + ctx.getApplicationInfo().metaData = metadata; + + FlutterLoader.Settings settings = new FlutterLoader.Settings(); + assertFalse(flutterLoader.initialized()); + flutterLoader.startInitialization(ctx, settings); + flutterLoader.ensureInitializationComplete( + ctx, + new String[] {expectedAotSharedLibraryNameFromCommandLine, "--enable-opengl-gpu-tracing"}); + shadowOf(getMainLooper()).idle(); + + ArgumentCaptor shellArgsCaptor = ArgumentCaptor.forClass(String[].class); + verify(mockFlutterJNI, times(1)) + .init( + eq(ctx), + shellArgsCaptor.capture(), + anyString(), + anyString(), + anyString(), + anyLong(), + anyInt()); + List arguments = Arrays.asList(shellArgsCaptor.getValue()); + + // Verify that the command line argument takes precedence over the manifest metadata. + assertTrue( + arguments.indexOf(expectedAotSharedLibraryNameFromCommandLine) + < arguments.indexOf(expectedAotSharedLibraryNameFromMetadata)); + + // Verify other command line arguments are still passed through. + assertTrue( + "Expected argument --enable-opengl-gpu-tracing was not found in the arguments passed to FlutterJNI.init", + arguments.contains("--enable-opengl-gpu-tracing")); + } + + private void testFlagFromMetadataPresentInReleaseMode( + String metadataKey, Object metadataValue, String expectedArg) { + testFlagFromMetadata(metadataKey, metadataValue, expectedArg, true, true); + } + + private void testFlagFromMetadataNotPresent( + String metadataKey, Object metadataValue, String expectedArg) { + testFlagFromMetadata(metadataKey, metadataValue, expectedArg, false, false); + } + + private void testFlagFromMetadataPresent( + String metadataKey, Object metadataValue, String expectedArg) { + testFlagFromMetadata(metadataKey, metadataValue, expectedArg, true, false); + } + + // Test that specified shell argument can be set via manifest metadata as expected. + private void testFlagFromMetadata( + String metadataKey, + Object metadataValue, + String expectedArg, + boolean shouldBeSet, + boolean isReleaseMode) { + FlutterJNI mockFlutterJNI = mock(FlutterJNI.class); + FlutterLoader flutterLoader = new FlutterLoader(mockFlutterJNI); + Bundle metadata = new Bundle(); + + // Place metadata key and value into the metadata bundle used to mock the manifest. + if (metadataValue == null) { + metadata.putString(metadataKey, null); + } else if (metadataValue instanceof Boolean) { + metadata.putBoolean(metadataKey, (Boolean) metadataValue); + } else if (metadataValue instanceof Integer) { + metadata.putInt(metadataKey, (Integer) metadataValue); + } else if (metadataValue instanceof String) { + metadata.putString(metadataKey, (String) metadataValue); + } else { + throw new IllegalArgumentException( + "Unsupported metadataValue type: " + metadataValue.getClass()); + } + + ctx.getApplicationInfo().metaData = metadata; + + FlutterLoader.Settings settings = new FlutterLoader.Settings(); + assertFalse(flutterLoader.initialized()); + flutterLoader.startInitialization(ctx, settings); + flutterLoader.ensureInitializationComplete(ctx, null, isReleaseMode); + shadowOf(getMainLooper()).idle(); + + ArgumentCaptor shellArgsCaptor = ArgumentCaptor.forClass(String[].class); + verify(mockFlutterJNI, times(1)) + .init( + eq(ctx), + shellArgsCaptor.capture(), + anyString(), + anyString(), + anyString(), + anyLong(), + anyInt()); + List arguments = Arrays.asList(shellArgsCaptor.getValue()); + + if (shouldBeSet) { + assertTrue( + "Expected argument '" + + expectedArg + + "' was not found in the arguments passed to FlutterJNI.init", + arguments.contains(expectedArg)); + } else { + assertFalse( + "Unexpected argument '" + + expectedArg + + "' was found in the arguments passed to FlutterJNI.init", + arguments.contains(expectedArg)); + } + } +} diff --git a/examples/flutter_view/android/app/src/main/java/com/example/view/MainActivity.java b/examples/flutter_view/android/app/src/main/java/com/example/view/MainActivity.java index 3c53005f354..73fd7e33fa0 100644 --- a/examples/flutter_view/android/app/src/main/java/com/example/view/MainActivity.java +++ b/examples/flutter_view/android/app/src/main/java/com/example/view/MainActivity.java @@ -15,11 +15,14 @@ import io.flutter.embedding.android.FlutterView; import io.flutter.embedding.engine.FlutterEngine; import io.flutter.embedding.engine.dart.DartExecutor; import io.flutter.embedding.engine.dart.DartExecutor.DartEntrypoint; +import io.flutter.Log; import io.flutter.plugin.common.BasicMessageChannel; import io.flutter.plugin.common.BasicMessageChannel.MessageHandler; import io.flutter.plugin.common.BasicMessageChannel.Reply; import io.flutter.plugin.common.StringCodec; +import java.util.Arrays; import java.util.ArrayList; +import java.util.List; public class MainActivity extends AppCompatActivity { private static FlutterEngine flutterEngine; @@ -31,34 +34,28 @@ public class MainActivity extends AppCompatActivity { private static final String PING = "ping"; private BasicMessageChannel messageChannel; - private String[] getArgsFromIntent(Intent intent) { - // Before adding more entries to this list, consider that arbitrary - // Android applications can generate intents with extra data and that - // there are many security-sensitive args in the binary. - ArrayList args = new ArrayList<>(); - if (intent.getBooleanExtra("trace-startup", false)) { - args.add("--trace-startup"); + // Previously, this example checked for certain flags set via Intent. Engine + // flags can no longer be set via Intent, so warn developers that Intent extras + // will be ignored and point to alternative methods for setting engine flags. + private void warnIfEngineFlagsSetViaIntent(Intent intent) { + List previouslySupportedFlagsViaIntent = Arrays.asList( + "trace-startup", "start-paused", "enable-dart-profiling"); + for (String flag : previouslySupportedFlagsViaIntent) { + if (intent.hasExtra(flag)) { + Log.w("MainActivity", "Engine flags can no longer be set via Intent on Android. If you wish to set " + flag + ", see https://github.com/flutter/flutter/blob/main/docs/engine/Flutter-Android-Engine-Flags.md for alternative methods."); + break; + } } - if (intent.getBooleanExtra("start-paused", false)) { - args.add("--start-paused"); - } - if (intent.getBooleanExtra("enable-dart-profiling", false)) { - args.add("--enable-dart-profiling"); - } - if (!args.isEmpty()) { - String[] argsArray = new String[args.size()]; - return args.toArray(argsArray); - } - return null; } @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - String[] args = getArgsFromIntent(getIntent()); + warnIfEngineFlagsSetViaIntent(getIntent()); + if (flutterEngine == null) { - flutterEngine = new FlutterEngine(this, args); + flutterEngine = new FlutterEngine(this); flutterEngine.getDartExecutor().executeDartEntrypoint( DartEntrypoint.createDefault() ); diff --git a/packages/flutter_tools/lib/src/android/android_device.dart b/packages/flutter_tools/lib/src/android/android_device.dart index 612388310ff..90d012a3c3c 100644 --- a/packages/flutter_tools/lib/src/android/android_device.dart +++ b/packages/flutter_tools/lib/src/android/android_device.dart @@ -674,7 +674,11 @@ class AndroidDevice extends Device { if (debuggingOptions.debuggingEnabled) ...[ if (debuggingOptions.buildInfo.isDebug) ...[ ...['--ez', 'enable-checked-mode', 'true'], - ...['--ez', 'verify-entry-points', 'true'], + ...[ + '--ez', + 'verify-entry-points', + 'true', + ], // TODO(camsim99): check if this is even used ], if (debuggingOptions.startPaused) ...['--ez', 'start-paused', 'true'], if (debuggingOptions.disableServiceAuthCodes) ...[