From 2ef523e47ee2166513728913a7d901e122ba4601 Mon Sep 17 00:00:00 2001 From: "auto-submit[bot]" Date: Tue, 9 Apr 2024 19:12:17 +0000 Subject: [PATCH] Revert "Delete engine v1 android embedding (#51229)" This reverts commit b735b76bf376ae60bc6cd16ffe214dd66c48454f. --- ci/licenses_golden/licenses_flutter | 24 + shell/platform/android/AndroidManifest.xml | 17 +- shell/platform/android/BUILD.gn | 12 + .../io/flutter/app/FlutterActivity.java | 181 ++++ .../flutter/app/FlutterActivityDelegate.java | 495 +++++++++ .../io/flutter/app/FlutterActivityEvents.java | 73 ++ .../io/flutter/app/FlutterApplication.java | 36 + .../flutter/app/FlutterFragmentActivity.java | 181 ++++ .../app/FlutterPlayStoreSplitApplication.java | 53 + .../io/flutter/app/FlutterPluginRegistry.java | 259 +++++ .../flutter/embedding/engine/FlutterJNI.java | 3 +- .../plugins/shim/ShimPluginRegistry.java | 153 +++ .../engine/plugins/shim/ShimRegistrar.java | 234 +++++ .../flutter/plugin/common/PluginRegistry.java | 367 ++++++- .../editing/ListenableEditingState.java | 2 +- .../platform/PlatformViewsController.java | 5 +- .../android/io/flutter/view/FlutterMain.java | 132 +++ .../io/flutter/view/FlutterNativeView.java | 204 ++++ .../android/io/flutter/view/FlutterView.java | 991 ++++++++++++++++++ .../io/flutter/view/TextureRegistry.java | 4 +- .../plugins/shim/ShimPluginRegistryTest.java | 153 +++ .../android/app/src/main/AndroidManifest.xml | 1 + tools/javadoc/gen_javadoc.py | 2 + 23 files changed, 3574 insertions(+), 8 deletions(-) create mode 100644 shell/platform/android/io/flutter/app/FlutterActivity.java create mode 100644 shell/platform/android/io/flutter/app/FlutterActivityDelegate.java create mode 100644 shell/platform/android/io/flutter/app/FlutterActivityEvents.java create mode 100644 shell/platform/android/io/flutter/app/FlutterApplication.java create mode 100644 shell/platform/android/io/flutter/app/FlutterFragmentActivity.java create mode 100644 shell/platform/android/io/flutter/app/FlutterPlayStoreSplitApplication.java create mode 100644 shell/platform/android/io/flutter/app/FlutterPluginRegistry.java create mode 100644 shell/platform/android/io/flutter/embedding/engine/plugins/shim/ShimPluginRegistry.java create mode 100644 shell/platform/android/io/flutter/embedding/engine/plugins/shim/ShimRegistrar.java create mode 100644 shell/platform/android/io/flutter/view/FlutterMain.java create mode 100644 shell/platform/android/io/flutter/view/FlutterNativeView.java create mode 100644 shell/platform/android/io/flutter/view/FlutterView.java create mode 100644 shell/platform/android/test/io/flutter/embedding/engine/plugins/shim/ShimPluginRegistryTest.java diff --git a/ci/licenses_golden/licenses_flutter b/ci/licenses_golden/licenses_flutter index d45c3e2e016f5..5ee86025aa619 100644 --- a/ci/licenses_golden/licenses_flutter +++ b/ci/licenses_golden/licenses_flutter @@ -41186,6 +41186,13 @@ ORIGIN: ../../../flutter/shell/platform/android/io/flutter/Build.java + ../../.. ORIGIN: ../../../flutter/shell/platform/android/io/flutter/BuildConfig.java + ../../../flutter/LICENSE ORIGIN: ../../../flutter/shell/platform/android/io/flutter/FlutterInjector.java + ../../../flutter/LICENSE ORIGIN: ../../../flutter/shell/platform/android/io/flutter/Log.java + ../../../flutter/LICENSE +ORIGIN: ../../../flutter/shell/platform/android/io/flutter/app/FlutterActivity.java + ../../../flutter/LICENSE +ORIGIN: ../../../flutter/shell/platform/android/io/flutter/app/FlutterActivityDelegate.java + ../../../flutter/LICENSE +ORIGIN: ../../../flutter/shell/platform/android/io/flutter/app/FlutterActivityEvents.java + ../../../flutter/LICENSE +ORIGIN: ../../../flutter/shell/platform/android/io/flutter/app/FlutterApplication.java + ../../../flutter/LICENSE +ORIGIN: ../../../flutter/shell/platform/android/io/flutter/app/FlutterFragmentActivity.java + ../../../flutter/LICENSE +ORIGIN: ../../../flutter/shell/platform/android/io/flutter/app/FlutterPlayStoreSplitApplication.java + ../../../flutter/LICENSE +ORIGIN: ../../../flutter/shell/platform/android/io/flutter/app/FlutterPluginRegistry.java + ../../../flutter/LICENSE ORIGIN: ../../../flutter/shell/platform/android/io/flutter/embedding/android/ExclusiveAppComponent.java + ../../../flutter/LICENSE ORIGIN: ../../../flutter/shell/platform/android/io/flutter/embedding/android/FlutterActivity.java + ../../../flutter/LICENSE ORIGIN: ../../../flutter/shell/platform/android/io/flutter/embedding/android/FlutterActivityAndFragmentDelegate.java + ../../../flutter/LICENSE @@ -41241,6 +41248,8 @@ ORIGIN: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/plug ORIGIN: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/plugins/service/ServiceAware.java + ../../../flutter/LICENSE ORIGIN: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/plugins/service/ServiceControlSurface.java + ../../../flutter/LICENSE ORIGIN: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/plugins/service/ServicePluginBinding.java + ../../../flutter/LICENSE +ORIGIN: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/plugins/shim/ShimPluginRegistry.java + ../../../flutter/LICENSE +ORIGIN: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/plugins/shim/ShimRegistrar.java + ../../../flutter/LICENSE ORIGIN: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/plugins/util/GeneratedPluginRegister.java + ../../../flutter/LICENSE ORIGIN: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/renderer/FlutterRenderer.java + ../../../flutter/LICENSE ORIGIN: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/renderer/FlutterUiDisplayListener.java + ../../../flutter/LICENSE @@ -41312,7 +41321,10 @@ ORIGIN: ../../../flutter/shell/platform/android/io/flutter/util/ViewUtils.java + ORIGIN: ../../../flutter/shell/platform/android/io/flutter/view/AccessibilityBridge.java + ../../../flutter/LICENSE ORIGIN: ../../../flutter/shell/platform/android/io/flutter/view/AccessibilityViewEmbedder.java + ../../../flutter/LICENSE ORIGIN: ../../../flutter/shell/platform/android/io/flutter/view/FlutterCallbackInformation.java + ../../../flutter/LICENSE +ORIGIN: ../../../flutter/shell/platform/android/io/flutter/view/FlutterMain.java + ../../../flutter/LICENSE +ORIGIN: ../../../flutter/shell/platform/android/io/flutter/view/FlutterNativeView.java + ../../../flutter/LICENSE ORIGIN: ../../../flutter/shell/platform/android/io/flutter/view/FlutterRunArguments.java + ../../../flutter/LICENSE +ORIGIN: ../../../flutter/shell/platform/android/io/flutter/view/FlutterView.java + ../../../flutter/LICENSE ORIGIN: ../../../flutter/shell/platform/android/io/flutter/view/TextureRegistry.java + ../../../flutter/LICENSE ORIGIN: ../../../flutter/shell/platform/android/io/flutter/view/VsyncWaiter.java + ../../../flutter/LICENSE ORIGIN: ../../../flutter/shell/platform/android/jni/jni_mock.h + ../../../flutter/LICENSE @@ -44056,6 +44068,13 @@ FILE: ../../../flutter/shell/platform/android/io/flutter/Build.java FILE: ../../../flutter/shell/platform/android/io/flutter/BuildConfig.java FILE: ../../../flutter/shell/platform/android/io/flutter/FlutterInjector.java FILE: ../../../flutter/shell/platform/android/io/flutter/Log.java +FILE: ../../../flutter/shell/platform/android/io/flutter/app/FlutterActivity.java +FILE: ../../../flutter/shell/platform/android/io/flutter/app/FlutterActivityDelegate.java +FILE: ../../../flutter/shell/platform/android/io/flutter/app/FlutterActivityEvents.java +FILE: ../../../flutter/shell/platform/android/io/flutter/app/FlutterApplication.java +FILE: ../../../flutter/shell/platform/android/io/flutter/app/FlutterFragmentActivity.java +FILE: ../../../flutter/shell/platform/android/io/flutter/app/FlutterPlayStoreSplitApplication.java +FILE: ../../../flutter/shell/platform/android/io/flutter/app/FlutterPluginRegistry.java FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/android/AndroidTouchProcessor.java FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/android/ExclusiveAppComponent.java FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/android/FlutterActivity.java @@ -44114,6 +44133,8 @@ FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/plugin FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/plugins/service/ServiceAware.java FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/plugins/service/ServiceControlSurface.java FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/plugins/service/ServicePluginBinding.java +FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/plugins/shim/ShimPluginRegistry.java +FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/plugins/shim/ShimRegistrar.java FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/plugins/util/GeneratedPluginRegister.java FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/renderer/FlutterRenderer.java FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/renderer/FlutterUiDisplayListener.java @@ -44193,7 +44214,10 @@ FILE: ../../../flutter/shell/platform/android/io/flutter/util/ViewUtils.java FILE: ../../../flutter/shell/platform/android/io/flutter/view/AccessibilityBridge.java FILE: ../../../flutter/shell/platform/android/io/flutter/view/AccessibilityViewEmbedder.java FILE: ../../../flutter/shell/platform/android/io/flutter/view/FlutterCallbackInformation.java +FILE: ../../../flutter/shell/platform/android/io/flutter/view/FlutterMain.java +FILE: ../../../flutter/shell/platform/android/io/flutter/view/FlutterNativeView.java FILE: ../../../flutter/shell/platform/android/io/flutter/view/FlutterRunArguments.java +FILE: ../../../flutter/shell/platform/android/io/flutter/view/FlutterView.java FILE: ../../../flutter/shell/platform/android/io/flutter/view/TextureRegistry.java FILE: ../../../flutter/shell/platform/android/io/flutter/view/VsyncWaiter.java FILE: ../../../flutter/shell/platform/android/jni/jni_mock.h diff --git a/shell/platform/android/AndroidManifest.xml b/shell/platform/android/AndroidManifest.xml index fc89aea0bdc99..aca92f0ec7887 100644 --- a/shell/platform/android/AndroidManifest.xml +++ b/shell/platform/android/AndroidManifest.xml @@ -3,13 +3,28 @@ Use of this source code is governed by a BSD-style license that can be found in the LICENSE file. --> - + + + + + + + + + + diff --git a/shell/platform/android/BUILD.gn b/shell/platform/android/BUILD.gn index 7b5456acebf48..28c78d3c8598a 100644 --- a/shell/platform/android/BUILD.gn +++ b/shell/platform/android/BUILD.gn @@ -198,6 +198,13 @@ android_java_sources = [ "io/flutter/Build.java", "io/flutter/FlutterInjector.java", "io/flutter/Log.java", + "io/flutter/app/FlutterActivity.java", + "io/flutter/app/FlutterActivityDelegate.java", + "io/flutter/app/FlutterActivityEvents.java", + "io/flutter/app/FlutterApplication.java", + "io/flutter/app/FlutterFragmentActivity.java", + "io/flutter/app/FlutterPlayStoreSplitApplication.java", + "io/flutter/app/FlutterPluginRegistry.java", "io/flutter/embedding/android/AndroidTouchProcessor.java", "io/flutter/embedding/android/ExclusiveAppComponent.java", "io/flutter/embedding/android/FlutterActivity.java", @@ -256,6 +263,8 @@ android_java_sources = [ "io/flutter/embedding/engine/plugins/service/ServiceAware.java", "io/flutter/embedding/engine/plugins/service/ServiceControlSurface.java", "io/flutter/embedding/engine/plugins/service/ServicePluginBinding.java", + "io/flutter/embedding/engine/plugins/shim/ShimPluginRegistry.java", + "io/flutter/embedding/engine/plugins/shim/ShimRegistrar.java", "io/flutter/embedding/engine/plugins/util/GeneratedPluginRegister.java", "io/flutter/embedding/engine/renderer/FlutterRenderer.java", "io/flutter/embedding/engine/renderer/FlutterUiDisplayListener.java", @@ -335,7 +344,10 @@ android_java_sources = [ "io/flutter/view/AccessibilityBridge.java", "io/flutter/view/AccessibilityViewEmbedder.java", "io/flutter/view/FlutterCallbackInformation.java", + "io/flutter/view/FlutterMain.java", + "io/flutter/view/FlutterNativeView.java", "io/flutter/view/FlutterRunArguments.java", + "io/flutter/view/FlutterView.java", "io/flutter/view/TextureRegistry.java", "io/flutter/view/VsyncWaiter.java", ] diff --git a/shell/platform/android/io/flutter/app/FlutterActivity.java b/shell/platform/android/io/flutter/app/FlutterActivity.java new file mode 100644 index 0000000000000..932ad2c5ada57 --- /dev/null +++ b/shell/platform/android/io/flutter/app/FlutterActivity.java @@ -0,0 +1,181 @@ +// 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.app; + +import android.app.Activity; +import android.content.Context; +import android.content.Intent; +import android.content.res.Configuration; +import android.os.Bundle; +import androidx.annotation.NonNull; +import io.flutter.app.FlutterActivityDelegate.ViewFactory; +import io.flutter.plugin.common.PluginRegistry; +import io.flutter.view.FlutterNativeView; +import io.flutter.view.FlutterView; + +/** + * Deprecated base class for activities that use Flutter. + * + * @deprecated {@link io.flutter.embedding.android.FlutterActivity} is the new API that now replaces + * this class. See https://flutter.dev/go/android-project-migration for more migration details. + */ +@Deprecated +public class FlutterActivity extends Activity + implements FlutterView.Provider, PluginRegistry, ViewFactory { + private static final String TAG = "FlutterActivity"; + + private final FlutterActivityDelegate delegate = new FlutterActivityDelegate(this, this); + + // These aliases ensure that the methods we forward to the delegate adhere + // to relevant interfaces versus just existing in FlutterActivityDelegate. + private final FlutterActivityEvents eventDelegate = delegate; + private final FlutterView.Provider viewProvider = delegate; + private final PluginRegistry pluginRegistry = delegate; + + /** + * Returns the Flutter view used by this activity; will be null before {@link #onCreate(Bundle)} + * is called. + */ + @Override + public FlutterView getFlutterView() { + return viewProvider.getFlutterView(); + } + + /** + * Hook for subclasses to customize the creation of the {@code FlutterView}. + * + *

The default implementation returns {@code null}, which will cause the activity to use a + * newly instantiated full-screen view. + */ + @Override + public FlutterView createFlutterView(Context context) { + return null; + } + + /** + * Hook for subclasses to customize the creation of the {@code FlutterNativeView}. + * + *

The default implementation returns {@code null}, which will cause the activity to use a + * newly instantiated native view object. + */ + @Override + public FlutterNativeView createFlutterNativeView() { + return null; + } + + @Override + public boolean retainFlutterNativeView() { + return false; + } + + @Override + public final boolean hasPlugin(String key) { + return pluginRegistry.hasPlugin(key); + } + + @Override + public final T valuePublishedByPlugin(String pluginKey) { + return pluginRegistry.valuePublishedByPlugin(pluginKey); + } + + @Override + public final Registrar registrarFor(String pluginKey) { + return pluginRegistry.registrarFor(pluginKey); + } + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + eventDelegate.onCreate(savedInstanceState); + } + + @Override + protected void onStart() { + super.onStart(); + eventDelegate.onStart(); + } + + @Override + protected void onResume() { + super.onResume(); + eventDelegate.onResume(); + } + + @Override + protected void onDestroy() { + eventDelegate.onDestroy(); + super.onDestroy(); + } + + @Override + public void onBackPressed() { + if (!eventDelegate.onBackPressed()) { + super.onBackPressed(); + } + } + + @Override + protected void onStop() { + eventDelegate.onStop(); + super.onStop(); + } + + @Override + protected void onPause() { + super.onPause(); + eventDelegate.onPause(); + } + + @Override + protected void onPostResume() { + super.onPostResume(); + eventDelegate.onPostResume(); + } + + // @Override - added in API level 23 + public void onRequestPermissionsResult( + int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { + eventDelegate.onRequestPermissionsResult(requestCode, permissions, grantResults); + } + + @Override + protected void onActivityResult(int requestCode, int resultCode, Intent data) { + if (!eventDelegate.onActivityResult(requestCode, resultCode, data)) { + super.onActivityResult(requestCode, resultCode, data); + } + } + + @Override + protected void onNewIntent(Intent intent) { + eventDelegate.onNewIntent(intent); + } + + @Override + public void onUserLeaveHint() { + eventDelegate.onUserLeaveHint(); + } + + @Override + public void onWindowFocusChanged(boolean hasFocus) { + super.onWindowFocusChanged(hasFocus); + eventDelegate.onWindowFocusChanged(hasFocus); + } + + @Override + public void onTrimMemory(int level) { + eventDelegate.onTrimMemory(level); + } + + @Override + public void onLowMemory() { + eventDelegate.onLowMemory(); + } + + @Override + public void onConfigurationChanged(Configuration newConfig) { + super.onConfigurationChanged(newConfig); + eventDelegate.onConfigurationChanged(newConfig); + } +} diff --git a/shell/platform/android/io/flutter/app/FlutterActivityDelegate.java b/shell/platform/android/io/flutter/app/FlutterActivityDelegate.java new file mode 100644 index 0000000000000..fcb553bbe8a2f --- /dev/null +++ b/shell/platform/android/io/flutter/app/FlutterActivityDelegate.java @@ -0,0 +1,495 @@ +// 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.app; + +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.app.Activity; +import android.app.Application; +import android.content.Context; +import android.content.Intent; +import android.content.pm.ActivityInfo; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageManager; +import android.content.pm.PackageManager.NameNotFoundException; +import android.content.res.Configuration; +import android.content.res.Resources.NotFoundException; +import android.graphics.drawable.Drawable; +import android.os.Bundle; +import android.util.TypedValue; +import android.view.View; +import android.view.ViewGroup; +import android.view.Window; +import android.view.WindowManager.LayoutParams; +import io.flutter.Log; +import io.flutter.plugin.common.PluginRegistry; +import io.flutter.plugin.platform.PlatformPlugin; +import io.flutter.util.Preconditions; +import io.flutter.view.FlutterMain; +import io.flutter.view.FlutterNativeView; +import io.flutter.view.FlutterRunArguments; +import io.flutter.view.FlutterView; +import java.util.ArrayList; + +/** + * Deprecated class that performs the actual work of tying Android {@link android.app.Activity} + * instances to Flutter. + * + *

This exists as a dedicated class (as opposed to being integrated directly into {@link + * FlutterActivity}) to facilitate applications that don't wish to subclass {@code FlutterActivity}. + * The most obvious example of when this may come in handy is if an application wishes to subclass + * the Android v4 support library's {@code FragmentActivity}. + * + *

Usage: + * + *

To wire this class up to your activity, simply forward the events defined in {@link + * FlutterActivityEvents} from your activity to an instance of this class. Optionally, you can make + * your activity implement {@link PluginRegistry} and/or {@link + * io.flutter.view.FlutterView.Provider} and forward those methods to this class as well. + * + * @deprecated {@link io.flutter.embedding.android.FlutterActivity} is the new API that now replaces + * this class and {@link io.flutter.app.FlutterActivity}. See + * https://flutter.dev/go/android-project-migration for more migration details. + */ +@Deprecated +public final class FlutterActivityDelegate + implements FlutterActivityEvents, FlutterView.Provider, PluginRegistry { + private static final String SPLASH_SCREEN_META_DATA_KEY = + "io.flutter.app.android.SplashScreenUntilFirstFrame"; + private static final String TAG = "FlutterActivityDelegate"; + private static final LayoutParams matchParent = + new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT); + + /** + * Specifies the mechanism by which Flutter views are created during the operation of a {@code + * FlutterActivityDelegate}. + * + *

A delegate's view factory will be consulted during {@link #onCreate(Bundle)}. If it returns + * {@code null}, then the delegate will fall back to instantiating a new full-screen {@code + * FlutterView}. + * + *

A delegate's native view factory will be consulted during {@link #onCreate(Bundle)}. If it + * returns {@code null}, then the delegate will fall back to instantiating a new {@code + * FlutterNativeView}. This is useful for applications to override to reuse the FlutterNativeView + * held e.g. by a pre-existing background service. + */ + public interface ViewFactory { + FlutterView createFlutterView(Context context); + + FlutterNativeView createFlutterNativeView(); + + /** + * Hook for subclasses to indicate that the {@code FlutterNativeView} returned by {@link + * #createFlutterNativeView()} should not be destroyed when this activity is destroyed. + * + * @return Whether the FlutterNativeView is retained. + */ + boolean retainFlutterNativeView(); + } + + private final Activity activity; + private final ViewFactory viewFactory; + private FlutterView flutterView; + private View launchView; + + public FlutterActivityDelegate(Activity activity, ViewFactory viewFactory) { + this.activity = Preconditions.checkNotNull(activity); + this.viewFactory = Preconditions.checkNotNull(viewFactory); + } + + @Override + public FlutterView getFlutterView() { + return flutterView; + } + + // The implementation of PluginRegistry forwards to flutterView. + @Override + public boolean hasPlugin(String key) { + return flutterView.getPluginRegistry().hasPlugin(key); + } + + @Override + @SuppressWarnings("unchecked") + public T valuePublishedByPlugin(String pluginKey) { + return (T) flutterView.getPluginRegistry().valuePublishedByPlugin(pluginKey); + } + + @Override + public Registrar registrarFor(String pluginKey) { + return flutterView.getPluginRegistry().registrarFor(pluginKey); + } + + @Override + public boolean onRequestPermissionsResult( + int requestCode, String[] permissions, int[] grantResults) { + return flutterView + .getPluginRegistry() + .onRequestPermissionsResult(requestCode, permissions, grantResults); + } + + @Override + public boolean onActivityResult(int requestCode, int resultCode, Intent data) { + return flutterView.getPluginRegistry().onActivityResult(requestCode, resultCode, data); + } + + @Override + public void onCreate(Bundle savedInstanceState) { + Window window = activity.getWindow(); + window.addFlags(LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS); + window.setStatusBarColor(0x40000000); + window.getDecorView().setSystemUiVisibility(PlatformPlugin.DEFAULT_SYSTEM_UI); + + String[] args = getArgsFromIntent(activity.getIntent()); + FlutterMain.ensureInitializationComplete(activity.getApplicationContext(), args); + + flutterView = viewFactory.createFlutterView(activity); + if (flutterView == null) { + FlutterNativeView nativeView = viewFactory.createFlutterNativeView(); + flutterView = new FlutterView(activity, null, nativeView); + flutterView.setLayoutParams(matchParent); + activity.setContentView(flutterView); + launchView = createLaunchView(); + if (launchView != null) { + addLaunchView(); + } + } + + if (loadIntent(activity.getIntent())) { + return; + } + + String appBundlePath = FlutterMain.findAppBundlePath(); + if (appBundlePath != null) { + runBundle(appBundlePath); + } + } + + @Override + public void onNewIntent(Intent intent) { + // Only attempt to reload the Flutter Dart code during development. Use + // the debuggable flag as an indicator that we are in development mode. + if (!isDebuggable() || !loadIntent(intent)) { + flutterView.getPluginRegistry().onNewIntent(intent); + } + } + + private boolean isDebuggable() { + return (activity.getApplicationInfo().flags & ApplicationInfo.FLAG_DEBUGGABLE) != 0; + } + + @Override + public void onPause() { + Application app = (Application) activity.getApplicationContext(); + if (app instanceof FlutterApplication) { + FlutterApplication flutterApp = (FlutterApplication) app; + if (activity.equals(flutterApp.getCurrentActivity())) { + flutterApp.setCurrentActivity(null); + } + } + if (flutterView != null) { + flutterView.onPause(); + } + } + + @Override + public void onStart() { + if (flutterView != null) { + flutterView.onStart(); + } + } + + @Override + public void onResume() { + Application app = (Application) activity.getApplicationContext(); + if (app instanceof FlutterApplication) { + FlutterApplication flutterApp = (FlutterApplication) app; + flutterApp.setCurrentActivity(activity); + } + } + + @Override + public void onStop() { + flutterView.onStop(); + } + + @Override + public void onPostResume() { + if (flutterView != null) { + flutterView.onPostResume(); + } + } + + @Override + public void onDestroy() { + Application app = (Application) activity.getApplicationContext(); + if (app instanceof FlutterApplication) { + FlutterApplication flutterApp = (FlutterApplication) app; + if (activity.equals(flutterApp.getCurrentActivity())) { + flutterApp.setCurrentActivity(null); + } + } + if (flutterView != null) { + final boolean detach = + flutterView.getPluginRegistry().onViewDestroy(flutterView.getFlutterNativeView()); + if (detach || viewFactory.retainFlutterNativeView()) { + // Detach, but do not destroy the FlutterView if a plugin + // expressed interest in its FlutterNativeView. + flutterView.detach(); + } else { + flutterView.destroy(); + } + } + } + + @Override + public boolean onBackPressed() { + if (flutterView != null) { + flutterView.popRoute(); + return true; + } + return false; + } + + @Override + public void onUserLeaveHint() { + flutterView.getPluginRegistry().onUserLeaveHint(); + } + + @Override + public void onWindowFocusChanged(boolean hasFocus) { + flutterView.getPluginRegistry().onWindowFocusChanged(hasFocus); + } + + @Override + public void onTrimMemory(int level) { + // Use a trim level delivered while the application is running so the + // framework has a chance to react to the notification. + if (level == TRIM_MEMORY_RUNNING_LOW) { + flutterView.onMemoryPressure(); + } + } + + @Override + public void onLowMemory() { + flutterView.onMemoryPressure(); + } + + @Override + public void onConfigurationChanged(Configuration newConfig) {} + + private static 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"); + } + if (intent.getBooleanExtra("start-paused", false)) { + args.add("--start-paused"); + } + if (intent.getBooleanExtra("disable-service-auth-codes", false)) { + args.add("--disable-service-auth-codes"); + } + if (intent.getBooleanExtra("use-test-fonts", false)) { + args.add("--use-test-fonts"); + } + if (intent.getBooleanExtra("enable-dart-profiling", false)) { + args.add("--enable-dart-profiling"); + } + if (intent.getBooleanExtra("enable-software-rendering", false)) { + args.add("--enable-software-rendering"); + } + if (intent.getBooleanExtra("skia-deterministic-rendering", false)) { + args.add("--skia-deterministic-rendering"); + } + if (intent.getBooleanExtra("trace-skia", false)) { + args.add("--trace-skia"); + } + if (intent.getBooleanExtra("trace-systrace", false)) { + args.add("--trace-systrace"); + } + if (intent.hasExtra("trace-to-file")) { + args.add("--trace-to-file=" + intent.getStringExtra("trace-to-file")); + } + if (intent.getBooleanExtra("dump-skp-on-shader-compilation", false)) { + args.add("--dump-skp-on-shader-compilation"); + } + if (intent.getBooleanExtra("cache-sksl", false)) { + args.add("--cache-sksl"); + } + if (intent.getBooleanExtra("purge-persistent-cache", false)) { + args.add("--purge-persistent-cache"); + } + if (intent.getBooleanExtra("verbose-logging", false)) { + args.add("--verbose-logging"); + } + int vmServicePort = intent.getIntExtra("vm-service-port", 0); + if (vmServicePort > 0) { + args.add("--vm-service-port=" + Integer.toString(vmServicePort)); + } else { + // TODO(bkonyi): remove once flutter_tools no longer uses this option. + // See https://github.com/dart-lang/sdk/issues/50233 + vmServicePort = intent.getIntExtra("observatory-port", 0); + if (vmServicePort > 0) { + args.add("--vm-service-port=" + Integer.toString(vmServicePort)); + } + } + if (intent.getBooleanExtra("endless-trace-buffer", false)) { + args.add("--endless-trace-buffer"); + } + // NOTE: 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. + if (intent.hasExtra("dart-flags")) { + args.add("--dart-flags=" + intent.getStringExtra("dart-flags")); + } + if (!args.isEmpty()) { + String[] argsArray = new String[args.size()]; + return args.toArray(argsArray); + } + return null; + } + + private boolean loadIntent(Intent intent) { + String action = intent.getAction(); + if (Intent.ACTION_RUN.equals(action)) { + String route = intent.getStringExtra("route"); + String appBundlePath = intent.getDataString(); + if (appBundlePath == null) { + // Fall back to the installation path if no bundle path was specified. + appBundlePath = FlutterMain.findAppBundlePath(); + } + if (route != null) { + flutterView.setInitialRoute(route); + } + + runBundle(appBundlePath); + return true; + } + + return false; + } + + private void runBundle(String appBundlePath) { + if (!flutterView.getFlutterNativeView().isApplicationRunning()) { + FlutterRunArguments args = new FlutterRunArguments(); + args.bundlePath = appBundlePath; + args.entrypoint = "main"; + flutterView.runFromBundle(args); + } + } + + /** + * Creates a {@link View} containing the same {@link Drawable} as the one set as the {@code + * windowBackground} of the parent activity for use as a launch splash view. + * + *

Returns null if no {@code windowBackground} is set for the activity. + */ + private View createLaunchView() { + if (!showSplashScreenUntilFirstFrame()) { + return null; + } + final Drawable launchScreenDrawable = getLaunchScreenDrawableFromActivityTheme(); + if (launchScreenDrawable == null) { + return null; + } + final View view = new View(activity); + view.setLayoutParams(matchParent); + view.setBackground(launchScreenDrawable); + return view; + } + + /** + * Extracts a {@link Drawable} from the parent activity's {@code windowBackground}. + * + *

{@code android:windowBackground} is specifically reused instead of a other attributes + * because the Android framework can display it fast enough when launching the app as opposed to + * anything defined in the Activity subclass. + * + *

Returns null if no {@code windowBackground} is set for the activity. + */ + @SuppressWarnings("deprecation") + private Drawable getLaunchScreenDrawableFromActivityTheme() { + TypedValue typedValue = new TypedValue(); + if (!activity.getTheme().resolveAttribute(android.R.attr.windowBackground, typedValue, true)) { + return null; + } + if (typedValue.resourceId == 0) { + return null; + } + try { + return activity.getResources().getDrawable(typedValue.resourceId); + } catch (NotFoundException e) { + Log.e(TAG, "Referenced launch screen windowBackground resource does not exist"); + return null; + } + } + + /** + * Let the user specify whether the activity's {@code windowBackground} is a launch screen and + * should be shown until the first frame via a tag in the activity. + */ + private Boolean showSplashScreenUntilFirstFrame() { + try { + ActivityInfo activityInfo = + activity + .getPackageManager() + .getActivityInfo(activity.getComponentName(), PackageManager.GET_META_DATA); + Bundle metadata = activityInfo.metaData; + return metadata != null && metadata.getBoolean(SPLASH_SCREEN_META_DATA_KEY); + } catch (NameNotFoundException e) { + return false; + } + } + + /** + * Show and then automatically animate out the launch view. + * + *

If a launch screen is defined in the user application's AndroidManifest.xml as the + * activity's {@code windowBackground}, display it on top of the {@link FlutterView} and remove + * the activity's {@code windowBackground}. + * + *

Fade it out and remove it when the {@link FlutterView} renders its first frame. + */ + private void addLaunchView() { + if (launchView == null) { + return; + } + + activity.addContentView(launchView, matchParent); + flutterView.addFirstFrameListener( + new FlutterView.FirstFrameListener() { + @Override + public void onFirstFrame() { + FlutterActivityDelegate.this + .launchView + .animate() + .alpha(0f) + // Use Android's default animation duration. + .setListener( + new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + // Views added to an Activity's addContentView is always added to its + // root FrameLayout. + ((ViewGroup) FlutterActivityDelegate.this.launchView.getParent()) + .removeView(FlutterActivityDelegate.this.launchView); + FlutterActivityDelegate.this.launchView = null; + } + }); + + FlutterActivityDelegate.this.flutterView.removeFirstFrameListener(this); + } + }); + + // Resets the activity theme from the one containing the launch screen in the window + // background to a blank one since the launch screen is now in a view in front of the + // FlutterView. + // + // We can make this configurable if users want it. + activity.setTheme(android.R.style.Theme_Black_NoTitleBar); + } +} diff --git a/shell/platform/android/io/flutter/app/FlutterActivityEvents.java b/shell/platform/android/io/flutter/app/FlutterActivityEvents.java new file mode 100644 index 0000000000000..abbdec4fe6afa --- /dev/null +++ b/shell/platform/android/io/flutter/app/FlutterActivityEvents.java @@ -0,0 +1,73 @@ +// 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.app; + +import android.content.ComponentCallbacks2; +import android.content.Intent; +import android.os.Bundle; +import io.flutter.plugin.common.PluginRegistry.ActivityResultListener; +import io.flutter.plugin.common.PluginRegistry.RequestPermissionsResultListener; + +/** + * A collection of Android {@code Activity} methods that are relevant to the core operation of + * Flutter applications. + * + *

Application authors that use an activity other than {@link FlutterActivity} should forward all + * events herein from their activity to an instance of {@link FlutterActivityDelegate} in order to + * wire the activity up to the Flutter framework. This forwarding is already provided in {@code + * FlutterActivity}. + */ +public interface FlutterActivityEvents + extends ComponentCallbacks2, ActivityResultListener, RequestPermissionsResultListener { + /** + * @param savedInstanceState If the activity is being re-initialized after previously being shut + * down then this Bundle contains the data it most recently supplied in {@code + * onSaveInstanceState(Bundle)}. + * @see android.app.Activity#onCreate(android.os.Bundle) + */ + void onCreate(Bundle savedInstanceState); + + /** + * @param intent The new intent that was started for the activity. + * @see android.app.Activity#onNewIntent(Intent) + */ + void onNewIntent(Intent intent); + + /** @see android.app.Activity#onPause() */ + void onPause(); + + /** @see android.app.Activity#onStart() */ + void onStart(); + + /** @see android.app.Activity#onResume() */ + void onResume(); + + /** @see android.app.Activity#onPostResume() */ + void onPostResume(); + + /** @see android.app.Activity#onDestroy() */ + void onDestroy(); + + /** @see android.app.Activity#onStop() */ + void onStop(); + + /** + * Invoked when the activity has detected the user's press of the back key. + * + * @return {@code true} if the listener handled the event; {@code false} to let the activity + * continue with its default back button handling. + * @see android.app.Activity#onBackPressed() + */ + boolean onBackPressed(); + + /** @see android.app.Activity#onUserLeaveHint() */ + void onUserLeaveHint(); + + /** + * @param hasFocus True if the current activity window has focus. + * @see android.app.Activity#onWindowFocusChanged(boolean) + */ + void onWindowFocusChanged(boolean hasFocus); +} diff --git a/shell/platform/android/io/flutter/app/FlutterApplication.java b/shell/platform/android/io/flutter/app/FlutterApplication.java new file mode 100644 index 0000000000000..a211c268548cd --- /dev/null +++ b/shell/platform/android/io/flutter/app/FlutterApplication.java @@ -0,0 +1,36 @@ +// 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.app; + +import android.app.Activity; +import android.app.Application; +import androidx.annotation.CallSuper; +import io.flutter.FlutterInjector; + +/** + * Flutter implementation of {@link android.app.Application}, managing application-level global + * initializations. + * + *

Using this {@link android.app.Application} is not required when using APIs in the package + * {@code io.flutter.embedding.android} since they self-initialize on first use. + */ +public class FlutterApplication extends Application { + @Override + @CallSuper + public void onCreate() { + super.onCreate(); + FlutterInjector.instance().flutterLoader().startInitialization(this); + } + + private Activity mCurrentActivity = null; + + public Activity getCurrentActivity() { + return mCurrentActivity; + } + + public void setCurrentActivity(Activity mCurrentActivity) { + this.mCurrentActivity = mCurrentActivity; + } +} diff --git a/shell/platform/android/io/flutter/app/FlutterFragmentActivity.java b/shell/platform/android/io/flutter/app/FlutterFragmentActivity.java new file mode 100644 index 0000000000000..778bab65bcf00 --- /dev/null +++ b/shell/platform/android/io/flutter/app/FlutterFragmentActivity.java @@ -0,0 +1,181 @@ +// 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.app; + +import android.content.Context; +import android.content.Intent; +import android.content.res.Configuration; +import android.os.Bundle; +import androidx.fragment.app.FragmentActivity; +import io.flutter.app.FlutterActivityDelegate.ViewFactory; +import io.flutter.plugin.common.PluginRegistry; +import io.flutter.view.FlutterNativeView; +import io.flutter.view.FlutterView; + +/** + * Deprecated class for activities that use Flutter who also require the use of the Android v4 + * Support library's {@link FragmentActivity}. + * + *

Applications that don't have this need will likely want to use {@link FlutterActivity} + * instead. + * + *

Important! Flutter does not bundle the necessary Android v4 Support library + * classes for this class to work at runtime. It is the responsibility of the app developer using + * this class to ensure that they link against the v4 support library .jar file when creating their + * app to ensure that {@link FragmentActivity} is available at runtime. + * + * @see https://developer.android.com/training/testing/set-up-project + * @deprecated this class is replaced by {@link + * io.flutter.embedding.android.FlutterFragmentActivity}. + */ +@Deprecated +public class FlutterFragmentActivity extends FragmentActivity + implements FlutterView.Provider, PluginRegistry, ViewFactory { + private final FlutterActivityDelegate delegate = new FlutterActivityDelegate(this, this); + + // These aliases ensure that the methods we forward to the delegate adhere + // to relevant interfaces versus just existing in FlutterActivityDelegate. + private final FlutterActivityEvents eventDelegate = delegate; + private final FlutterView.Provider viewProvider = delegate; + private final PluginRegistry pluginRegistry = delegate; + + /** + * Returns the Flutter view used by this activity; will be null before {@link #onCreate(Bundle)} + * is called. + */ + @Override + public FlutterView getFlutterView() { + return viewProvider.getFlutterView(); + } + + /** + * Hook for subclasses to customize the creation of the {@code FlutterView}. + * + *

The default implementation returns {@code null}, which will cause the activity to use a + * newly instantiated full-screen view. + */ + @Override + public FlutterView createFlutterView(Context context) { + return null; + } + + @Override + public FlutterNativeView createFlutterNativeView() { + return null; + } + + @Override + public boolean retainFlutterNativeView() { + return false; + } + + @Override + public final boolean hasPlugin(String key) { + return pluginRegistry.hasPlugin(key); + } + + @Override + public final T valuePublishedByPlugin(String pluginKey) { + return pluginRegistry.valuePublishedByPlugin(pluginKey); + } + + @Override + public final Registrar registrarFor(String pluginKey) { + return pluginRegistry.registrarFor(pluginKey); + } + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + eventDelegate.onCreate(savedInstanceState); + } + + @Override + protected void onDestroy() { + eventDelegate.onDestroy(); + super.onDestroy(); + } + + @Override + public void onBackPressed() { + if (!eventDelegate.onBackPressed()) { + super.onBackPressed(); + } + } + + @Override + protected void onStart() { + super.onStart(); + eventDelegate.onStart(); + } + + @Override + protected void onStop() { + eventDelegate.onStop(); + super.onStop(); + } + + @Override + protected void onPause() { + super.onPause(); + eventDelegate.onPause(); + } + + @Override + protected void onPostResume() { + super.onPostResume(); + eventDelegate.onPostResume(); + } + + @Override + public void onRequestPermissionsResult( + int requestCode, String[] permissions, int[] grantResults) { + super.onRequestPermissionsResult(requestCode, permissions, grantResults); + eventDelegate.onRequestPermissionsResult(requestCode, permissions, grantResults); + } + + @Override + protected void onActivityResult(int requestCode, int resultCode, Intent data) { + if (!eventDelegate.onActivityResult(requestCode, resultCode, data)) { + super.onActivityResult(requestCode, resultCode, data); + } + } + + @Override + protected void onNewIntent(Intent intent) { + super.onNewIntent(intent); + eventDelegate.onNewIntent(intent); + } + + @Override + @SuppressWarnings("MissingSuperCall") + public void onUserLeaveHint() { + eventDelegate.onUserLeaveHint(); + } + + @Override + public void onWindowFocusChanged(boolean hasFocus) { + super.onWindowFocusChanged(hasFocus); + eventDelegate.onWindowFocusChanged(hasFocus); + } + + @Override + public void onTrimMemory(int level) { + super.onTrimMemory(level); + eventDelegate.onTrimMemory(level); + } + + @Override + public void onLowMemory() { + eventDelegate.onLowMemory(); + } + + @Override + public void onConfigurationChanged(Configuration newConfig) { + super.onConfigurationChanged(newConfig); + eventDelegate.onConfigurationChanged(newConfig); + } +} diff --git a/shell/platform/android/io/flutter/app/FlutterPlayStoreSplitApplication.java b/shell/platform/android/io/flutter/app/FlutterPlayStoreSplitApplication.java new file mode 100644 index 0000000000000..7a415d71ff5e6 --- /dev/null +++ b/shell/platform/android/io/flutter/app/FlutterPlayStoreSplitApplication.java @@ -0,0 +1,53 @@ +// 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.app; + +import androidx.annotation.CallSuper; +import com.google.android.play.core.splitcompat.SplitCompatApplication; +import io.flutter.FlutterInjector; +import io.flutter.embedding.engine.deferredcomponents.PlayStoreDeferredComponentManager; + +/** + * Flutter's extension of {@link SplitCompatApplication} that injects a {@link + * PlayStoreDeferredComponentManager} with {@link FlutterInjector} to enable Split AOT Flutter apps. + * + *

To use this class, either have your custom application class extend + * FlutterPlayStoreSplitApplication or use it directly in the app's AndroidManifest.xml by adding + * the following line: + * + *

{@code
+ * 
+ *    
+ *  
+ * }
+ * + * This class is meant to be used with the Google Play store. Custom non-play store applications do + * not need to extend SplitCompatApplication and should inject a custom {@link + * io.flutter.embedding.engine.deferredcomponents.DeferredComponentManager} implementation like so: + * + *
{@code
+ * FlutterInjector.setInstance(
+ *      new FlutterInjector.Builder().setDeferredComponentManager(yourCustomManager).build());
+ * }
+ */ +public class FlutterPlayStoreSplitApplication extends SplitCompatApplication { + @Override + @CallSuper + public void onCreate() { + super.onCreate(); + // Create and inject a PlayStoreDeferredComponentManager, which is the default manager for + // interacting with the Google Play Store. + PlayStoreDeferredComponentManager deferredComponentManager = + new PlayStoreDeferredComponentManager(this, null); + FlutterInjector.setInstance( + new FlutterInjector.Builder() + .setDeferredComponentManager(deferredComponentManager) + .build()); + } +} diff --git a/shell/platform/android/io/flutter/app/FlutterPluginRegistry.java b/shell/platform/android/io/flutter/app/FlutterPluginRegistry.java new file mode 100644 index 0000000000000..887a9118979cd --- /dev/null +++ b/shell/platform/android/io/flutter/app/FlutterPluginRegistry.java @@ -0,0 +1,259 @@ +// 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.app; + +import android.app.Activity; +import android.content.Context; +import android.content.Intent; +import io.flutter.embedding.engine.FlutterEngine; +import io.flutter.plugin.common.BinaryMessenger; +import io.flutter.plugin.common.PluginRegistry; +import io.flutter.plugin.platform.PlatformViewRegistry; +import io.flutter.plugin.platform.PlatformViewsController; +import io.flutter.view.FlutterMain; +import io.flutter.view.FlutterNativeView; +import io.flutter.view.FlutterView; +import io.flutter.view.TextureRegistry; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +/** @deprecated See https://flutter.dev/go/android-project-migration for migration instructions. */ +@Deprecated +public class FlutterPluginRegistry + implements PluginRegistry, + PluginRegistry.RequestPermissionsResultListener, + PluginRegistry.ActivityResultListener, + PluginRegistry.NewIntentListener, + PluginRegistry.WindowFocusChangedListener, + PluginRegistry.UserLeaveHintListener, + PluginRegistry.ViewDestroyListener { + private static final String TAG = "FlutterPluginRegistry"; + + private Activity mActivity; + private Context mAppContext; + private FlutterNativeView mNativeView; + private FlutterView mFlutterView; + + private final PlatformViewsController mPlatformViewsController; + private final Map mPluginMap = new LinkedHashMap<>(0); + private final List mRequestPermissionsResultListeners = + new ArrayList<>(0); + private final List mActivityResultListeners = new ArrayList<>(0); + private final List mNewIntentListeners = new ArrayList<>(0); + private final List mUserLeaveHintListeners = new ArrayList<>(0); + private final List mWindowFocusChangedListeners = new ArrayList<>(0); + private final List mViewDestroyListeners = new ArrayList<>(0); + + public FlutterPluginRegistry(FlutterNativeView nativeView, Context context) { + mNativeView = nativeView; + mAppContext = context; + mPlatformViewsController = new PlatformViewsController(); + } + + public FlutterPluginRegistry(FlutterEngine engine, Context context) { + // TODO(mattcarroll): implement use of engine instead of nativeView. + mAppContext = context; + mPlatformViewsController = new PlatformViewsController(); + } + + @Override + public boolean hasPlugin(String key) { + return mPluginMap.containsKey(key); + } + + @Override + @SuppressWarnings("unchecked") + public T valuePublishedByPlugin(String pluginKey) { + return (T) mPluginMap.get(pluginKey); + } + + @Override + public Registrar registrarFor(String pluginKey) { + if (mPluginMap.containsKey(pluginKey)) { + throw new IllegalStateException("Plugin key " + pluginKey + " is already in use"); + } + mPluginMap.put(pluginKey, null); + return new FlutterRegistrar(pluginKey); + } + + public void attach(FlutterView flutterView, Activity activity) { + mFlutterView = flutterView; + mActivity = activity; + mPlatformViewsController.attach(activity, flutterView, flutterView.getDartExecutor()); + } + + public void detach() { + mPlatformViewsController.detach(); + mPlatformViewsController.onDetachedFromJNI(); + mFlutterView = null; + mActivity = null; + } + + public void onPreEngineRestart() { + mPlatformViewsController.onPreEngineRestart(); + } + + public PlatformViewsController getPlatformViewsController() { + return mPlatformViewsController; + } + + private class FlutterRegistrar implements Registrar { + private final String pluginKey; + + FlutterRegistrar(String pluginKey) { + this.pluginKey = pluginKey; + } + + @Override + public Activity activity() { + return mActivity; + } + + @Override + public Context context() { + return mAppContext; + } + + @Override + public Context activeContext() { + return (mActivity != null) ? mActivity : mAppContext; + } + + @Override + public BinaryMessenger messenger() { + return mNativeView; + } + + @Override + public TextureRegistry textures() { + return mFlutterView; + } + + @Override + public PlatformViewRegistry platformViewRegistry() { + return mPlatformViewsController.getRegistry(); + } + + @Override + public FlutterView view() { + return mFlutterView; + } + + @Override + public String lookupKeyForAsset(String asset) { + return FlutterMain.getLookupKeyForAsset(asset); + } + + @Override + public String lookupKeyForAsset(String asset, String packageName) { + return FlutterMain.getLookupKeyForAsset(asset, packageName); + } + + @Override + public Registrar publish(Object value) { + mPluginMap.put(pluginKey, value); + return this; + } + + @Override + public Registrar addRequestPermissionsResultListener( + RequestPermissionsResultListener listener) { + mRequestPermissionsResultListeners.add(listener); + return this; + } + + @Override + public Registrar addActivityResultListener(ActivityResultListener listener) { + mActivityResultListeners.add(listener); + return this; + } + + @Override + public Registrar addNewIntentListener(NewIntentListener listener) { + mNewIntentListeners.add(listener); + return this; + } + + @Override + public Registrar addUserLeaveHintListener(UserLeaveHintListener listener) { + mUserLeaveHintListeners.add(listener); + return this; + } + + @Override + public Registrar addWindowFocusChangedListener(WindowFocusChangedListener listener) { + mWindowFocusChangedListeners.add(listener); + return this; + } + + @Override + public Registrar addViewDestroyListener(ViewDestroyListener listener) { + mViewDestroyListeners.add(listener); + return this; + } + } + + @Override + public boolean onRequestPermissionsResult( + int requestCode, String[] permissions, int[] grantResults) { + for (RequestPermissionsResultListener listener : mRequestPermissionsResultListeners) { + if (listener.onRequestPermissionsResult(requestCode, permissions, grantResults)) { + return true; + } + } + return false; + } + + @Override + public boolean onActivityResult(int requestCode, int resultCode, Intent data) { + for (ActivityResultListener listener : mActivityResultListeners) { + if (listener.onActivityResult(requestCode, resultCode, data)) { + return true; + } + } + return false; + } + + @Override + public boolean onNewIntent(Intent intent) { + for (NewIntentListener listener : mNewIntentListeners) { + if (listener.onNewIntent(intent)) { + return true; + } + } + return false; + } + + @Override + public void onUserLeaveHint() { + for (UserLeaveHintListener listener : mUserLeaveHintListeners) { + listener.onUserLeaveHint(); + } + } + + @Override + public void onWindowFocusChanged(boolean hasFocus) { + for (WindowFocusChangedListener listener : mWindowFocusChangedListeners) { + listener.onWindowFocusChanged(hasFocus); + } + } + + @Override + public boolean onViewDestroy(FlutterNativeView view) { + boolean handled = false; + for (ViewDestroyListener listener : mViewDestroyListeners) { + if (listener.onViewDestroy(view)) { + handled = true; + } + } + return handled; + } + + public void destroy() { + mPlatformViewsController.onDetachedFromJNI(); + } +} diff --git a/shell/platform/android/io/flutter/embedding/engine/FlutterJNI.java b/shell/platform/android/io/flutter/embedding/engine/FlutterJNI.java index 75f02fd1292c2..e24953bd8ee1b 100644 --- a/shell/platform/android/io/flutter/embedding/engine/FlutterJNI.java +++ b/shell/platform/android/io/flutter/embedding/engine/FlutterJNI.java @@ -1027,7 +1027,8 @@ private native void nativeRunBundleAndSnapshotFromLibrary( * will be dropped (ignored). Therefore, when using {@code FlutterJNI} to integrate a Flutter * context in an app, a {@link PlatformMessageHandler} must be registered for 2-way Java/Dart * communication to operate correctly. Moreover, the handler must be implemented such that - * fundamental platform messages are handled as expected. + * fundamental platform messages are handled as expected. See {@link + * io.flutter.view.FlutterNativeView} for an example implementation. */ @UiThread public void setPlatformMessageHandler(@Nullable PlatformMessageHandler platformMessageHandler) { diff --git a/shell/platform/android/io/flutter/embedding/engine/plugins/shim/ShimPluginRegistry.java b/shell/platform/android/io/flutter/embedding/engine/plugins/shim/ShimPluginRegistry.java new file mode 100644 index 0000000000000..ed41d1d65f307 --- /dev/null +++ b/shell/platform/android/io/flutter/embedding/engine/plugins/shim/ShimPluginRegistry.java @@ -0,0 +1,153 @@ +// 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.plugins.shim; + +import androidx.annotation.NonNull; +import io.flutter.Log; +import io.flutter.embedding.engine.FlutterEngine; +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.plugin.common.PluginRegistry; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +/** + * A {@link PluginRegistry} that is shimmed to let old plugins use the new Android embedding and + * plugin API behind the scenes. + * + *

The following is an example usage of {@code ShimPluginRegistry} within a {@code + * FlutterActivity}: + * + *

+ * // Create the FlutterEngine that will back the Flutter UI.
+ * FlutterEngineGroup group = new FlutterEngineGroup(context);
+ * FlutterEngine flutterEngine = group.createAndRunDefaultEngine(context);
+ *
+ * // Create a ShimPluginRegistry and wrap the FlutterEngine with the shim.
+ * ShimPluginRegistry shimPluginRegistry = new ShimPluginRegistry(flutterEngine, platformViewsController);
+ *
+ * // Use the GeneratedPluginRegistrant to add every plugin that's in the pubspec.
+ * GeneratedPluginRegistrant.registerWith(shimPluginRegistry);
+ * 
+ */ +public class ShimPluginRegistry implements PluginRegistry { + private static final String TAG = "ShimPluginRegistry"; + + private final FlutterEngine flutterEngine; + private final Map pluginMap = new HashMap<>(); + private final ShimRegistrarAggregate shimRegistrarAggregate; + + public ShimPluginRegistry(@NonNull FlutterEngine flutterEngine) { + this.flutterEngine = flutterEngine; + this.shimRegistrarAggregate = new ShimRegistrarAggregate(); + this.flutterEngine.getPlugins().add(shimRegistrarAggregate); + } + + @Override + @NonNull + public Registrar registrarFor(@NonNull String pluginKey) { + Log.v(TAG, "Creating plugin Registrar for '" + pluginKey + "'"); + if (pluginMap.containsKey(pluginKey)) { + throw new IllegalStateException("Plugin key " + pluginKey + " is already in use"); + } + pluginMap.put(pluginKey, null); + ShimRegistrar registrar = new ShimRegistrar(pluginKey, pluginMap); + shimRegistrarAggregate.addPlugin(registrar); + return registrar; + } + + @Override + public boolean hasPlugin(@NonNull String pluginKey) { + return pluginMap.containsKey(pluginKey); + } + + @Override + @SuppressWarnings("unchecked") + public T valuePublishedByPlugin(@NonNull String pluginKey) { + return (T) pluginMap.get(pluginKey); + } + + /** + * Aggregates all {@link ShimRegistrar}s within one single {@link FlutterPlugin}. + * + *

The reason we need this aggregate is because the new embedding uniquely identifies plugins + * by their plugin class, but the plugin shim system represents every plugin with a {@link + * ShimRegistrar}. Therefore, every plugin we would register after the first plugin, would + * overwrite the previous plugin, because they're all {@link ShimRegistrar} instances. + * + *

{@code ShimRegistrarAggregate} multiplexes {@link FlutterPlugin} and {@link ActivityAware} + * calls so that we can register just one {@code ShimRegistrarAggregate} with a {@link + * FlutterEngine}, while forwarding the relevant plugin resources to any number of {@link + * ShimRegistrar}s within this {@code ShimRegistrarAggregate}. + */ + private static class ShimRegistrarAggregate implements FlutterPlugin, ActivityAware { + private final Set shimRegistrars = new HashSet<>(); + private FlutterPluginBinding flutterPluginBinding; + private ActivityPluginBinding activityPluginBinding; + + public void addPlugin(@NonNull ShimRegistrar shimRegistrar) { + shimRegistrars.add(shimRegistrar); + + if (flutterPluginBinding != null) { + shimRegistrar.onAttachedToEngine(flutterPluginBinding); + } + if (activityPluginBinding != null) { + shimRegistrar.onAttachedToActivity(activityPluginBinding); + } + } + + @Override + public void onAttachedToEngine(@NonNull FlutterPluginBinding binding) { + flutterPluginBinding = binding; + for (ShimRegistrar shimRegistrar : shimRegistrars) { + shimRegistrar.onAttachedToEngine(binding); + } + } + + @Override + public void onDetachedFromEngine(@NonNull FlutterPluginBinding binding) { + for (ShimRegistrar shimRegistrar : shimRegistrars) { + shimRegistrar.onDetachedFromEngine(binding); + } + flutterPluginBinding = null; + activityPluginBinding = null; + } + + @Override + public void onAttachedToActivity(@NonNull ActivityPluginBinding binding) { + activityPluginBinding = binding; + for (ShimRegistrar shimRegistrar : shimRegistrars) { + shimRegistrar.onAttachedToActivity(binding); + } + } + + @Override + public void onDetachedFromActivityForConfigChanges() { + for (ShimRegistrar shimRegistrar : shimRegistrars) { + shimRegistrar.onDetachedFromActivity(); + } + activityPluginBinding = null; + } + + @Override + public void onReattachedToActivityForConfigChanges(@NonNull ActivityPluginBinding binding) { + activityPluginBinding = binding; + for (ShimRegistrar shimRegistrar : shimRegistrars) { + shimRegistrar.onReattachedToActivityForConfigChanges(binding); + } + } + + @Override + public void onDetachedFromActivity() { + for (ShimRegistrar shimRegistrar : shimRegistrars) { + shimRegistrar.onDetachedFromActivity(); + } + activityPluginBinding = null; + } + } +} diff --git a/shell/platform/android/io/flutter/embedding/engine/plugins/shim/ShimRegistrar.java b/shell/platform/android/io/flutter/embedding/engine/plugins/shim/ShimRegistrar.java new file mode 100644 index 0000000000000..d27da01dae04b --- /dev/null +++ b/shell/platform/android/io/flutter/embedding/engine/plugins/shim/ShimRegistrar.java @@ -0,0 +1,234 @@ +// 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.plugins.shim; + +import android.app.Activity; +import android.content.Context; +import androidx.annotation.NonNull; +import io.flutter.FlutterInjector; +import io.flutter.Log; +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.plugin.common.BinaryMessenger; +import io.flutter.plugin.common.PluginRegistry; +import io.flutter.plugin.platform.PlatformViewRegistry; +import io.flutter.view.FlutterView; +import io.flutter.view.TextureRegistry; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +/** + * A {@link PluginRegistry.Registrar} that is shimmed let old plugins use the new Android embedding + * and plugin API behind the scenes. + * + *

Instances of {@code ShimRegistrar}s are vended internally by a {@link ShimPluginRegistry}. + */ +class ShimRegistrar implements PluginRegistry.Registrar, FlutterPlugin, ActivityAware { + private static final String TAG = "ShimRegistrar"; + + private final Map globalRegistrarMap; + private final String pluginId; + private final Set viewDestroyListeners = new HashSet<>(); + private final Set + requestPermissionsResultListeners = new HashSet<>(); + private final Set activityResultListeners = + new HashSet<>(); + private final Set newIntentListeners = new HashSet<>(); + private final Set userLeaveHintListeners = new HashSet<>(); + private final Set WindowFocusChangedListeners = + new HashSet<>(); + private FlutterPlugin.FlutterPluginBinding pluginBinding; + private ActivityPluginBinding activityPluginBinding; + + public ShimRegistrar(@NonNull String pluginId, @NonNull Map globalRegistrarMap) { + this.pluginId = pluginId; + this.globalRegistrarMap = globalRegistrarMap; + } + + @Override + public Activity activity() { + return activityPluginBinding != null ? activityPluginBinding.getActivity() : null; + } + + @Override + public Context context() { + return pluginBinding != null ? pluginBinding.getApplicationContext() : null; + } + + @Override + public Context activeContext() { + return activityPluginBinding == null ? context() : activity(); + } + + @Override + public BinaryMessenger messenger() { + return pluginBinding != null ? pluginBinding.getBinaryMessenger() : null; + } + + @Override + public TextureRegistry textures() { + return pluginBinding != null ? pluginBinding.getTextureRegistry() : null; + } + + @Override + public PlatformViewRegistry platformViewRegistry() { + return pluginBinding != null ? pluginBinding.getPlatformViewRegistry() : null; + } + + @Override + public FlutterView view() { + throw new UnsupportedOperationException( + "The new embedding does not support the old FlutterView."); + } + + @Override + public String lookupKeyForAsset(String asset) { + return FlutterInjector.instance().flutterLoader().getLookupKeyForAsset(asset); + } + + @Override + public String lookupKeyForAsset(String asset, String packageName) { + return FlutterInjector.instance().flutterLoader().getLookupKeyForAsset(asset, packageName); + } + + @Override + public PluginRegistry.Registrar publish(Object value) { + globalRegistrarMap.put(pluginId, value); + return this; + } + + @Override + public PluginRegistry.Registrar addRequestPermissionsResultListener( + PluginRegistry.RequestPermissionsResultListener listener) { + requestPermissionsResultListeners.add(listener); + + if (activityPluginBinding != null) { + activityPluginBinding.addRequestPermissionsResultListener(listener); + } + + return this; + } + + @Override + public PluginRegistry.Registrar addActivityResultListener( + PluginRegistry.ActivityResultListener listener) { + activityResultListeners.add(listener); + + if (activityPluginBinding != null) { + activityPluginBinding.addActivityResultListener(listener); + } + + return this; + } + + @Override + public PluginRegistry.Registrar addNewIntentListener(PluginRegistry.NewIntentListener listener) { + newIntentListeners.add(listener); + + if (activityPluginBinding != null) { + activityPluginBinding.addOnNewIntentListener(listener); + } + + return this; + } + + @Override + public PluginRegistry.Registrar addUserLeaveHintListener( + PluginRegistry.UserLeaveHintListener listener) { + userLeaveHintListeners.add(listener); + + if (activityPluginBinding != null) { + activityPluginBinding.addOnUserLeaveHintListener(listener); + } + + return this; + } + + @Override + public PluginRegistry.Registrar addWindowFocusChangedListener( + PluginRegistry.WindowFocusChangedListener listener) { + WindowFocusChangedListeners.add(listener); + + if (activityPluginBinding != null) { + activityPluginBinding.addOnWindowFocusChangedListener(listener); + } + + return this; + } + + @Override + @NonNull + public PluginRegistry.Registrar addViewDestroyListener( + @NonNull PluginRegistry.ViewDestroyListener listener) { + viewDestroyListeners.add(listener); + return this; + } + + @Override + public void onAttachedToEngine(@NonNull FlutterPluginBinding binding) { + Log.v(TAG, "Attached to FlutterEngine."); + pluginBinding = binding; + } + + @Override + public void onDetachedFromEngine(@NonNull FlutterPluginBinding binding) { + Log.v(TAG, "Detached from FlutterEngine."); + for (PluginRegistry.ViewDestroyListener listener : viewDestroyListeners) { + // The following invocation might produce unexpected behavior in old plugins because + // we have no FlutterNativeView to pass to onViewDestroy(). This is a limitation of this shim. + listener.onViewDestroy(null); + } + + pluginBinding = null; + activityPluginBinding = null; + } + + @Override + public void onAttachedToActivity(@NonNull ActivityPluginBinding binding) { + Log.v(TAG, "Attached to an Activity."); + activityPluginBinding = binding; + addExistingListenersToActivityPluginBinding(); + } + + @Override + public void onDetachedFromActivityForConfigChanges() { + Log.v(TAG, "Detached from an Activity for config changes."); + activityPluginBinding = null; + } + + @Override + public void onReattachedToActivityForConfigChanges(@NonNull ActivityPluginBinding binding) { + Log.v(TAG, "Reconnected to an Activity after config changes."); + activityPluginBinding = binding; + addExistingListenersToActivityPluginBinding(); + } + + @Override + public void onDetachedFromActivity() { + Log.v(TAG, "Detached from an Activity."); + activityPluginBinding = null; + } + + private void addExistingListenersToActivityPluginBinding() { + for (PluginRegistry.RequestPermissionsResultListener listener : + requestPermissionsResultListeners) { + activityPluginBinding.addRequestPermissionsResultListener(listener); + } + for (PluginRegistry.ActivityResultListener listener : activityResultListeners) { + activityPluginBinding.addActivityResultListener(listener); + } + for (PluginRegistry.NewIntentListener listener : newIntentListeners) { + activityPluginBinding.addOnNewIntentListener(listener); + } + for (PluginRegistry.UserLeaveHintListener listener : userLeaveHintListeners) { + activityPluginBinding.addOnUserLeaveHintListener(listener); + } + for (PluginRegistry.WindowFocusChangedListener listener : WindowFocusChangedListeners) { + activityPluginBinding.addOnWindowFocusChangedListener(listener); + } + } +} diff --git a/shell/platform/android/io/flutter/plugin/common/PluginRegistry.java b/shell/platform/android/io/flutter/plugin/common/PluginRegistry.java index 977b995ecf63a..4859617d93b29 100644 --- a/shell/platform/android/io/flutter/plugin/common/PluginRegistry.java +++ b/shell/platform/android/io/flutter/plugin/common/PluginRegistry.java @@ -5,13 +5,353 @@ package io.flutter.plugin.common; import android.app.Activity; +import android.content.Context; import android.content.Intent; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +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.plugin.platform.PlatformViewRegistry; +import io.flutter.view.FlutterNativeView; +import io.flutter.view.FlutterView; +import io.flutter.view.TextureRegistry; -/** Container class for Android API listeners used by {@link ActivityPluginBinding}. */ +/** + * Container class for Android API listeners used by {@link ActivityPluginBinding}. + * + *

This class also contains deprecated v1 embedding APIs used for plugin registration. + * + *

In v1 Android applications, an auto-generated and auto-updated plugin registrant class + * (GeneratedPluginRegistrant) makes use of a {@link PluginRegistry} to register contributions from + * each plugin mentioned in the application's pubspec file. The generated registrant class is, again + * by default, called from the application's main {@link android.app.Activity}, which defaults to an + * instance of {@link io.flutter.app.FlutterActivity}, itself a {@link PluginRegistry}. + */ public interface PluginRegistry { + /** + * Returns a {@link Registrar} for receiving the registrations pertaining to the specified plugin. + * + * @param pluginKey a unique String identifying the plugin; typically the fully qualified name of + * the plugin's main class. + * @return A {@link Registrar} for receiving the registrations pertianing to the specified plugin. + * @deprecated See https://flutter.dev/go/android-project-migration for migration details. + */ + @Deprecated + @NonNull + Registrar registrarFor(@NonNull String pluginKey); + + /** + * Returns whether the specified plugin is known to this registry. + * + * @param pluginKey a unique String identifying the plugin; typically the fully qualified name of + * the plugin's main class. + * @return true if this registry has handed out a registrar for the specified plugin. + * @deprecated See https://flutter.dev/go/android-project-migration for migration details. + */ + @Deprecated + boolean hasPlugin(@NonNull String pluginKey); + + /** + * Returns the value published by the specified plugin, if any. + * + *

Plugins may publish a single value, such as an instance of the plugin's main class, for + * situations where external control or interaction is needed. Clients are expected to know the + * value's type. + * + * @param The type of the value. + * @param pluginKey a unique String identifying the plugin; typically the fully qualified name of + * the plugin's main class. + * @return the published value, possibly null. + * @deprecated See https://flutter.dev/go/android-project-migration for migration details. + */ + @Deprecated + @Nullable + T valuePublishedByPlugin(@NonNull String pluginKey); + + /** + * Receiver of registrations from a single plugin. + * + * @deprecated This registrar is for Flutter's v1 embedding. For instructions on migrating a + * plugin from Flutter's v1 Android embedding to v2, visit + * http://flutter.dev/go/android-plugin-migration + */ + @Deprecated + interface Registrar { + /** + * Returns the {@link android.app.Activity} that forms the plugin's operating context. + * + *

Plugin authors should not assume the type returned by this method is any specific subclass + * of {@code Activity} (such as {@link io.flutter.app.FlutterActivity} or {@link + * io.flutter.app.FlutterFragmentActivity}), as applications are free to use any activity + * subclass. + * + *

When there is no foreground activity in the application, this will return null. If a + * {@link Context} is needed, use context() to get the application's context. + * + *

This registrar is for Flutter's v1 embedding. To access an {@code Activity} from a plugin + * using the v2 embedding, see {@link ActivityPluginBinding#getActivity()}. To obtain an + * instance of an {@link ActivityPluginBinding} in a Flutter plugin, implement the {@link + * ActivityAware} interface. A binding is provided in {@link + * ActivityAware#onAttachedToActivity(ActivityPluginBinding)} and {@link + * ActivityAware#onReattachedToActivityForConfigChanges(ActivityPluginBinding)}. + * + *

For instructions on migrating a plugin from Flutter's v1 Android embedding to v2, visit + * http://flutter.dev/go/android-plugin-migration + */ + @Nullable + Activity activity(); + + /** + * Returns the {@link android.app.Application}'s {@link Context}. + * + *

This registrar is for Flutter's v1 embedding. To access a {@code Context} from a plugin + * using the v2 embedding, see {@link + * FlutterPlugin.FlutterPluginBinding#getApplicationContext()} + * + *

For instructions on migrating a plugin from Flutter's v1 Android embedding to v2, visit + * http://flutter.dev/go/android-plugin-migration + */ + @NonNull + Context context(); + + /** + * Returns the active {@link Context}. + * + *

This registrar is for Flutter's v1 embedding. In the v2 embedding, there is no concept of + * an "active context". Either use the application {@code Context} or an attached {@code + * Activity}. See {@link #context()} and {@link #activity()} for more details. + * + *

For instructions on migrating a plugin from Flutter's v1 Android embedding to v2, visit + * http://flutter.dev/go/android-plugin-migration + * + * @return the current {@link #activity() Activity}, if not null, otherwise the {@link + * #context() Application}. + */ + @NonNull + Context activeContext(); + + /** + * Returns a {@link BinaryMessenger} which the plugin can use for creating channels for + * communicating with the Dart side. + * + *

This registrar is for Flutter's v1 embedding. To access a {@code BinaryMessenger} from a + * plugin using the v2 embedding, see {@link + * FlutterPlugin.FlutterPluginBinding#getBinaryMessenger()} + * + *

For instructions on migrating a plugin from Flutter's v1 Android embedding to v2, visit + * http://flutter.dev/go/android-plugin-migration + */ + @NonNull + BinaryMessenger messenger(); + + /** + * Returns a {@link TextureRegistry} which the plugin can use for managing backend textures. + * + *

This registrar is for Flutter's v1 embedding. To access a {@code TextureRegistry} from a + * plugin using the v2 embedding, see {@link + * FlutterPlugin.FlutterPluginBinding#getTextureRegistry()} + * + *

For instructions on migrating a plugin from Flutter's v1 Android embedding to v2, visit + * http://flutter.dev/go/android-plugin-migration + */ + @NonNull + TextureRegistry textures(); + + /** + * Returns the application's {@link PlatformViewRegistry}. + * + *

Plugins can use the platform registry to register their view factories. + * + *

This registrar is for Flutter's v1 embedding. To access a {@code PlatformViewRegistry} + * from a plugin using the v2 embedding, see {@link + * FlutterPlugin.FlutterPluginBinding#getPlatformViewRegistry()} + * + *

For instructions on migrating a plugin from Flutter's v1 Android embedding to v2, visit + * http://flutter.dev/go/android-plugin-migration + */ + @NonNull + PlatformViewRegistry platformViewRegistry(); + + /** + * Returns the {@link FlutterView} that's instantiated by this plugin's {@link #activity() + * activity}. + * + *

This registrar is for Flutter's v1 embedding. The {@link FlutterView} referenced by this + * method does not exist in the v2 embedding. Additionally, no {@code View} is exposed to any + * plugins in the v2 embedding. Platform views can access their containing {@code View} using + * the platform views APIs. If you have a use-case that absolutely requires a plugin to access + * an Android {@code View}, please file a ticket on GitHub. + * + *

For instructions on migrating a plugin from Flutter's v1 Android embedding to v2, visit + * http://flutter.dev/go/android-plugin-migration + */ + @NonNull + FlutterView view(); + + /** + * Returns the file name for the given asset. The returned file name can be used to access the + * asset in the APK through the {@link android.content.res.AssetManager} API. + * + *

TODO(mattcarroll): point this method towards new lookup method. + * + * @param asset the name of the asset. The name can be hierarchical + * @return the filename to be used with {@link android.content.res.AssetManager} + */ + @NonNull + String lookupKeyForAsset(@NonNull String asset); + + /** + * Returns the file name for the given asset which originates from the specified packageName. + * The returned file name can be used to access the asset in the APK through the {@link + * android.content.res.AssetManager} API. + * + *

TODO(mattcarroll): point this method towards new lookup method. + * + * @param asset the name of the asset. The name can be hierarchical + * @param packageName the name of the package from which the asset originates + * @return the file name to be used with {@link android.content.res.AssetManager} + */ + @NonNull + String lookupKeyForAsset(@NonNull String asset, @NonNull String packageName); + + /** + * Publishes a value associated with the plugin being registered. + * + *

The published value is available to interested clients via {@link + * PluginRegistry#valuePublishedByPlugin(String)}. + * + *

Publication should be done only when client code needs to interact with the plugin in a + * way that cannot be accomplished by the plugin registering callbacks with client APIs. + * + *

Overwrites any previously published value. + * + *

This registrar is for Flutter's v1 embedding. The concept of publishing values from + * plugins is not supported in the v2 embedding. + * + *

For instructions on migrating a plugin from Flutter's v1 Android embedding to v2, visit + * http://flutter.dev/go/android-plugin-migration + * + * @param value the value, possibly null. + * @return this {@link Registrar}. + */ + @NonNull + Registrar publish(@Nullable Object value); + + /** + * Adds a callback allowing the plugin to take part in handling incoming calls to {@code + * Activity#onRequestPermissionsResult(int, String[], int[])} or {@code + * androidx.core.app.ActivityCompat.OnRequestPermissionsResultCallback#onRequestPermissionsResult(int, + * String[], int[])}. + * + *

This registrar is for Flutter's v1 embedding. To listen for permission results in the v2 + * embedding, use {@link + * ActivityPluginBinding#addRequestPermissionsResultListener(PluginRegistry.RequestPermissionsResultListener)}. + * + *

For instructions on migrating a plugin from Flutter's v1 Android embedding to v2, visit + * http://flutter.dev/go/android-plugin-migration + * + * @param listener a {@link RequestPermissionsResultListener} callback. + * @return this {@link Registrar}. + */ + @NonNull + Registrar addRequestPermissionsResultListener( + @NonNull RequestPermissionsResultListener listener); + + /** + * Adds a callback allowing the plugin to take part in handling incoming calls to {@link + * Activity#onActivityResult(int, int, Intent)}. + * + *

This registrar is for Flutter's v1 embedding. To listen for {@code Activity} results in + * the v2 embedding, use {@link + * ActivityPluginBinding#addActivityResultListener(PluginRegistry.ActivityResultListener)}. + * + *

For instructions on migrating a plugin from Flutter's v1 Android embedding to v2, visit + * http://flutter.dev/go/android-plugin-migration + * + * @param listener an {@link ActivityResultListener} callback. + * @return this {@link Registrar}. + */ + @NonNull + Registrar addActivityResultListener(@NonNull ActivityResultListener listener); + + /** + * Adds a callback allowing the plugin to take part in handling incoming calls to {@link + * Activity#onNewIntent(Intent)}. + * + *

This registrar is for Flutter's v1 embedding. To listen for new {@code Intent}s in the v2 + * embedding, use {@link + * ActivityPluginBinding#addOnNewIntentListener(PluginRegistry.NewIntentListener)}. + * + *

For instructions on migrating a plugin from Flutter's v1 Android embedding to v2, visit + * http://flutter.dev/go/android-plugin-migration + * + * @param listener a {@link NewIntentListener} callback. + * @return this {@link Registrar}. + */ + @NonNull + Registrar addNewIntentListener(@NonNull NewIntentListener listener); + + /** + * Adds a callback allowing the plugin to take part in handling incoming calls to {@link + * Activity#onUserLeaveHint()}. + * + *

This registrar is for Flutter's v1 embedding. To listen for leave hints in the v2 + * embedding, use {@link + * ActivityPluginBinding#addOnUserLeaveHintListener(PluginRegistry.UserLeaveHintListener)}. + * + *

For instructions on migrating a plugin from Flutter's v1 Android embedding to v2, visit + * http://flutter.dev/go/android-plugin-migration + * + * @param listener a {@link UserLeaveHintListener} callback. + * @return this {@link Registrar}. + */ + @NonNull + Registrar addUserLeaveHintListener(@NonNull UserLeaveHintListener listener); + + /** + * Adds a callback allowing the plugin to take part in handling incoming calls to {@link + * Activity#onWindowFocusChanged(boolean)}. + * + *

This registrar is for Flutter's v1 embedding. To listen for leave hints in the v2 + * embedding, use {@link + * ActivityPluginBinding#addOnWindowFocusChangedListener(PluginRegistry.WindowFocusChangedListener)}. + * + *

For instructions on migrating a plugin from Flutter's v1 Android embedding to v2, visit + * http://flutter.dev/go/android-plugin-migration + * + * @param listener a {@link WindowFocusChangedListener} callback. + * @return this {@link Registrar}. + */ + @NonNull + Registrar addWindowFocusChangedListener(@NonNull WindowFocusChangedListener listener); + + /** + * Adds a callback allowing the plugin to take part in handling incoming calls to {@link + * Activity#onDestroy()}. + * + *

This registrar is for Flutter's v1 embedding. The concept of {@code View} destruction does + * not exist in the v2 embedding. However, plugins in the v2 embedding can respond to {@link + * ActivityAware#onDetachedFromActivityForConfigChanges()} and {@link + * ActivityAware#onDetachedFromActivity()}, which indicate the loss of a visual context for the + * running Flutter experience. Developers should implement {@link ActivityAware} for their + * {@link FlutterPlugin} if such callbacks are needed. Also, plugins can respond to {@link + * FlutterPlugin#onDetachedFromEngine(FlutterPlugin.FlutterPluginBinding)}, which indicates that + * the given plugin has been completely disconnected from the associated Flutter experience and + * should clean up any resources. + * + *

For instructions on migrating a plugin from Flutter's v1 Android embedding to v2, visit + * http://flutter.dev/go/android-plugin-migration + * + * @param listener a {@link ViewDestroyListener} callback. + * @return this {@link Registrar}. + */ + // TODO(amirh): Add a line in the javadoc above that points to a Platform Views website guide + // when one is available (but not a website API doc) + @NonNull + Registrar addViewDestroyListener(@NonNull ViewDestroyListener listener); + } + /** * Delegate interface for handling result of permissions requests on behalf of the main {@link * Activity}. @@ -72,4 +412,29 @@ interface UserLeaveHintListener { interface WindowFocusChangedListener { void onWindowFocusChanged(boolean hasFocus); } + + /** + * Delegate interface for handling an {@link android.app.Activity}'s onDestroy method being + * called. A plugin that implements this interface can adopt the {@link FlutterNativeView} by + * retaining a reference and returning true. + * + * @deprecated See https://flutter.dev/go/android-project-migration for migration details. + */ + @Deprecated + interface ViewDestroyListener { + boolean onViewDestroy(@NonNull FlutterNativeView view); + } + + /** + * Callback interface for registering plugins with a plugin registry. + * + *

For example, an Application may use this callback interface to provide a background service + * with a callback for calling its GeneratedPluginRegistrant.registerWith method. + * + * @deprecated See https://flutter.dev/go/android-project-migration for migration details. + */ + @Deprecated + interface PluginRegistrantCallback { + void registerWith(@NonNull PluginRegistry registry); + } } diff --git a/shell/platform/android/io/flutter/plugin/editing/ListenableEditingState.java b/shell/platform/android/io/flutter/plugin/editing/ListenableEditingState.java index ea1d75a6a173c..d887cdb8a8e9b 100644 --- a/shell/platform/android/io/flutter/plugin/editing/ListenableEditingState.java +++ b/shell/platform/android/io/flutter/plugin/editing/ListenableEditingState.java @@ -18,7 +18,7 @@ /// The current editing state (text, selection range, composing range) the text input plugin holds. /// /// As the name implies, this class also notifies its listeners when the editing state changes. When -/// there are ongoing batch edits, change notifications will be deferred until all batch edits end +/// there're ongoing batch edits, change notifications will be deferred until all batch edits end /// (i.e. when the outermost batch edit ends). Listeners added during a batch edit will always be /// notified when all batch edits end, even if there's no real change. /// diff --git a/shell/platform/android/io/flutter/plugin/platform/PlatformViewsController.java b/shell/platform/android/io/flutter/plugin/platform/PlatformViewsController.java index 19b02144d7265..6e2b2ab250f82 100644 --- a/shell/platform/android/io/flutter/plugin/platform/PlatformViewsController.java +++ b/shell/platform/android/io/flutter/plugin/platform/PlatformViewsController.java @@ -45,8 +45,9 @@ /** * Manages platform views. * - *

Each {@link io.flutter.embedding.engine.FlutterEngine} has a single platform views controller. - * A platform views controller can be attached to at most one Flutter view. + *

Each {@link io.flutter.embedding.engine.FlutterEngine} or {@link + * io.flutter.app.FlutterPluginRegistry} has a single platform views controller. A platform views + * controller can be attached to at most one Flutter view. */ public class PlatformViewsController implements PlatformViewsAccessibilityDelegate { private static final String TAG = "PlatformViewsController"; diff --git a/shell/platform/android/io/flutter/view/FlutterMain.java b/shell/platform/android/io/flutter/view/FlutterMain.java new file mode 100644 index 0000000000000..2092830111fa1 --- /dev/null +++ b/shell/platform/android/io/flutter/view/FlutterMain.java @@ -0,0 +1,132 @@ +// 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.view; + +import android.content.Context; +import android.os.Handler; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import io.flutter.FlutterInjector; +import io.flutter.embedding.engine.loader.FlutterLoader; + +/** + * A legacy class to initialize the Flutter engine. + * + * @deprecated Replaced by {@link io.flutter.embedding.engine.loader.FlutterLoader}. + */ +@Deprecated +public class FlutterMain { + + public static class Settings { + private String logTag; + + @Nullable + public String getLogTag() { + return logTag; + } + + /** + * Set the tag associated with Flutter app log messages. + * + * @param tag Log tag. + */ + public void setLogTag(String tag) { + logTag = tag; + } + } + + /** + * Starts initialization of the native system. + * + * @param applicationContext The Android application context. + */ + public static void startInitialization(@NonNull Context applicationContext) { + FlutterInjector.instance().flutterLoader().startInitialization(applicationContext); + } + + /** + * Starts initialization of the native system. + * + *

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

Calling this method multiple times has no effect. + * + * @param applicationContext The Android application context. + * @param settings Configuration settings. + */ + public static void startInitialization( + @NonNull Context applicationContext, @NonNull Settings settings) { + FlutterLoader.Settings newSettings = new FlutterLoader.Settings(); + newSettings.setLogTag(settings.getLogTag()); + FlutterInjector.instance().flutterLoader().startInitialization(applicationContext, newSettings); + } + + /** + * Blocks until initialization of the native system has completed. + * + *

Calling this method multiple times has no effect. + * + * @param applicationContext The Android application context. + * @param args Flags sent to the Flutter runtime. + */ + public static void ensureInitializationComplete( + @NonNull Context applicationContext, @Nullable String[] args) { + FlutterInjector.instance() + .flutterLoader() + .ensureInitializationComplete(applicationContext, args); + } + + /** + * Same as {@link #ensureInitializationComplete(Context, String[])} but waiting on a background + * thread, then invoking {@code callback} on the {@code callbackHandler}. + */ + public static void ensureInitializationCompleteAsync( + @NonNull Context applicationContext, + @Nullable String[] args, + @NonNull Handler callbackHandler, + @NonNull Runnable callback) { + FlutterInjector.instance() + .flutterLoader() + .ensureInitializationCompleteAsync(applicationContext, args, callbackHandler, callback); + } + + @NonNull + public static String findAppBundlePath() { + return FlutterInjector.instance().flutterLoader().findAppBundlePath(); + } + + @Deprecated + @Nullable + public static String findAppBundlePath(@NonNull Context applicationContext) { + return FlutterInjector.instance().flutterLoader().findAppBundlePath(); + } + + /** + * Returns the file name for the given asset. The returned file name can be used to access the + * asset in the APK through the {@link android.content.res.AssetManager} API. + * + * @param asset the name of the asset. The name can be hierarchical + * @return the filename to be used with {@link android.content.res.AssetManager} + */ + @NonNull + public static String getLookupKeyForAsset(@NonNull String asset) { + return FlutterInjector.instance().flutterLoader().getLookupKeyForAsset(asset); + } + + /** + * Returns the file name for the given asset which originates from the specified packageName. The + * returned file name can be used to access the asset in the APK through the {@link + * android.content.res.AssetManager} API. + * + * @param asset the name of the asset. The name can be hierarchical + * @param packageName the name of the package from which the asset originates + * @return the file name to be used with {@link android.content.res.AssetManager} + */ + @NonNull + public static String getLookupKeyForAsset(@NonNull String asset, @NonNull String packageName) { + return FlutterInjector.instance().flutterLoader().getLookupKeyForAsset(asset, packageName); + } +} diff --git a/shell/platform/android/io/flutter/view/FlutterNativeView.java b/shell/platform/android/io/flutter/view/FlutterNativeView.java new file mode 100644 index 0000000000000..2f027695b5ad3 --- /dev/null +++ b/shell/platform/android/io/flutter/view/FlutterNativeView.java @@ -0,0 +1,204 @@ +// 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.view; + +import android.app.Activity; +import android.content.Context; +import androidx.annotation.NonNull; +import androidx.annotation.UiThread; +import io.flutter.Log; +import io.flutter.app.FlutterPluginRegistry; +import io.flutter.embedding.engine.FlutterEngine.EngineLifecycleListener; +import io.flutter.embedding.engine.FlutterJNI; +import io.flutter.embedding.engine.dart.DartExecutor; +import io.flutter.embedding.engine.renderer.FlutterUiDisplayListener; +import io.flutter.plugin.common.*; +import java.nio.ByteBuffer; + +/** + * @deprecated {@link io.flutter.embedding.android.FlutterView} is the new API that now replaces + * this class. See https://flutter.dev/go/android-project-migration for more migration details. + */ +@Deprecated +public class FlutterNativeView implements BinaryMessenger { + private static final String TAG = "FlutterNativeView"; + + private final FlutterPluginRegistry mPluginRegistry; + private final DartExecutor dartExecutor; + private FlutterView mFlutterView; + private final FlutterJNI mFlutterJNI; + private final Context mContext; + private boolean applicationIsRunning; + + private final FlutterUiDisplayListener flutterUiDisplayListener = + new FlutterUiDisplayListener() { + @Override + public void onFlutterUiDisplayed() { + if (mFlutterView == null) { + return; + } + mFlutterView.onFirstFrame(); + } + + @Override + public void onFlutterUiNoLongerDisplayed() { + // no-op + } + }; + + public FlutterNativeView(@NonNull Context context) { + this(context, false); + } + + public FlutterNativeView(@NonNull Context context, boolean isBackgroundView) { + if (isBackgroundView) { + Log.w(TAG, "'isBackgroundView' is no longer supported and will be ignored"); + } + mContext = context; + mPluginRegistry = new FlutterPluginRegistry(this, context); + mFlutterJNI = new FlutterJNI(); + mFlutterJNI.addIsDisplayingFlutterUiListener(flutterUiDisplayListener); + this.dartExecutor = new DartExecutor(mFlutterJNI, context.getAssets()); + mFlutterJNI.addEngineLifecycleListener(new EngineLifecycleListenerImpl()); + attach(this); + assertAttached(); + } + + public void detachFromFlutterView() { + mPluginRegistry.detach(); + mFlutterView = null; + } + + public void destroy() { + mPluginRegistry.destroy(); + dartExecutor.onDetachedFromJNI(); + mFlutterView = null; + mFlutterJNI.removeIsDisplayingFlutterUiListener(flutterUiDisplayListener); + mFlutterJNI.detachFromNativeAndReleaseResources(); + applicationIsRunning = false; + } + + @NonNull + public DartExecutor getDartExecutor() { + return dartExecutor; + } + + @NonNull + public FlutterPluginRegistry getPluginRegistry() { + return mPluginRegistry; + } + + public void attachViewAndActivity(FlutterView flutterView, Activity activity) { + mFlutterView = flutterView; + mPluginRegistry.attach(flutterView, activity); + } + + public boolean isAttached() { + return mFlutterJNI.isAttached(); + } + + public void assertAttached() { + if (!isAttached()) throw new AssertionError("Platform view is not attached"); + } + + public void runFromBundle(FlutterRunArguments args) { + if (args.entrypoint == null) { + throw new AssertionError("An entrypoint must be specified"); + } + assertAttached(); + if (applicationIsRunning) + throw new AssertionError("This Flutter engine instance is already running an application"); + mFlutterJNI.runBundleAndSnapshotFromLibrary( + args.bundlePath, + args.entrypoint, + args.libraryPath, + mContext.getResources().getAssets(), + null); + + applicationIsRunning = true; + } + + public boolean isApplicationRunning() { + return applicationIsRunning; + } + + @Deprecated + public static String getObservatoryUri() { + return FlutterJNI.getVMServiceUri(); + } + + public static String getVMServiceUri() { + return FlutterJNI.getVMServiceUri(); + } + + @Override + @UiThread + public TaskQueue makeBackgroundTaskQueue(TaskQueueOptions options) { + return dartExecutor.getBinaryMessenger().makeBackgroundTaskQueue(options); + } + + @Override + @UiThread + public void send(String channel, ByteBuffer message) { + dartExecutor.getBinaryMessenger().send(channel, message); + } + + @Override + @UiThread + public void send(String channel, ByteBuffer message, BinaryReply callback) { + if (!isAttached()) { + Log.d(TAG, "FlutterView.send called on a detached view, channel=" + channel); + return; + } + + dartExecutor.getBinaryMessenger().send(channel, message, callback); + } + + @Override + @UiThread + public void setMessageHandler(String channel, BinaryMessageHandler handler) { + dartExecutor.getBinaryMessenger().setMessageHandler(channel, handler); + } + + @Override + @UiThread + public void setMessageHandler(String channel, BinaryMessageHandler handler, TaskQueue taskQueue) { + dartExecutor.getBinaryMessenger().setMessageHandler(channel, handler, taskQueue); + } + + @Override + public void enableBufferingIncomingMessages() {} + + @Override + public void disableBufferingIncomingMessages() {} + + /*package*/ FlutterJNI getFlutterJNI() { + return mFlutterJNI; + } + + private void attach(FlutterNativeView view) { + mFlutterJNI.attachToNative(); + dartExecutor.onAttachedToJNI(); + } + + private final class EngineLifecycleListenerImpl implements EngineLifecycleListener { + // Called by native to notify right before the engine is restarted (cold reload). + @SuppressWarnings("unused") + public void onPreEngineRestart() { + if (mFlutterView != null) { + mFlutterView.resetAccessibilityTree(); + } + if (mPluginRegistry == null) { + return; + } + mPluginRegistry.onPreEngineRestart(); + } + + public void onEngineWillDestroy() { + // The old embedding doesn't actually have a FlutterEngine. It interacts with the JNI + // directly. + } + } +} diff --git a/shell/platform/android/io/flutter/view/FlutterView.java b/shell/platform/android/io/flutter/view/FlutterView.java new file mode 100644 index 0000000000000..62f2a17505174 --- /dev/null +++ b/shell/platform/android/io/flutter/view/FlutterView.java @@ -0,0 +1,991 @@ +// 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.view; + +import static io.flutter.Build.API_LEVELS; + +import android.annotation.SuppressLint; +import android.annotation.TargetApi; +import android.app.Activity; +import android.content.Context; +import android.content.res.Configuration; +import android.graphics.Bitmap; +import android.graphics.Insets; +import android.graphics.PixelFormat; +import android.graphics.SurfaceTexture; +import android.os.Build; +import android.os.Handler; +import android.text.format.DateFormat; +import android.util.AttributeSet; +import android.util.SparseArray; +import android.view.DisplayCutout; +import android.view.KeyEvent; +import android.view.MotionEvent; +import android.view.PointerIcon; +import android.view.Surface; +import android.view.SurfaceHolder; +import android.view.SurfaceView; +import android.view.View; +import android.view.ViewConfiguration; +import android.view.ViewStructure; +import android.view.WindowInsets; +import android.view.WindowManager; +import android.view.accessibility.AccessibilityManager; +import android.view.accessibility.AccessibilityNodeProvider; +import android.view.autofill.AutofillValue; +import android.view.inputmethod.EditorInfo; +import android.view.inputmethod.InputConnection; +import android.view.inputmethod.InputMethodManager; +import android.window.BackEvent; +import androidx.annotation.NonNull; +import androidx.annotation.RequiresApi; +import androidx.annotation.UiThread; +import io.flutter.Log; +import io.flutter.app.FlutterPluginRegistry; +import io.flutter.embedding.android.AndroidTouchProcessor; +import io.flutter.embedding.android.KeyboardManager; +import io.flutter.embedding.engine.dart.DartExecutor; +import io.flutter.embedding.engine.renderer.FlutterRenderer; +import io.flutter.embedding.engine.renderer.SurfaceTextureWrapper; +import io.flutter.embedding.engine.systemchannels.AccessibilityChannel; +import io.flutter.embedding.engine.systemchannels.BackGestureChannel; +import io.flutter.embedding.engine.systemchannels.LifecycleChannel; +import io.flutter.embedding.engine.systemchannels.LocalizationChannel; +import io.flutter.embedding.engine.systemchannels.MouseCursorChannel; +import io.flutter.embedding.engine.systemchannels.NavigationChannel; +import io.flutter.embedding.engine.systemchannels.PlatformChannel; +import io.flutter.embedding.engine.systemchannels.SettingsChannel; +import io.flutter.embedding.engine.systemchannels.SystemChannel; +import io.flutter.embedding.engine.systemchannels.TextInputChannel; +import io.flutter.plugin.common.ActivityLifecycleListener; +import io.flutter.plugin.common.BinaryMessenger; +import io.flutter.plugin.editing.TextInputPlugin; +import io.flutter.plugin.localization.LocalizationPlugin; +import io.flutter.plugin.mouse.MouseCursorPlugin; +import io.flutter.plugin.platform.PlatformPlugin; +import io.flutter.plugin.platform.PlatformViewsController; +import io.flutter.util.ViewUtils; +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.atomic.AtomicLong; + +/** + * Deprecated Android view containing a Flutter app. + * + * @deprecated {@link io.flutter.embedding.android.FlutterView} is the new API that now replaces + * this class. See https://flutter.dev/go/android-project-migration for more migration details. + */ +@Deprecated +public class FlutterView extends SurfaceView + implements BinaryMessenger, + TextureRegistry, + MouseCursorPlugin.MouseCursorViewDelegate, + KeyboardManager.ViewDelegate { + /** + * Interface for those objects that maintain and expose a reference to a {@code FlutterView} (such + * as a full-screen Flutter activity). + * + *

This indirection is provided to support applications that use an activity other than {@link + * io.flutter.app.FlutterActivity} (e.g. Android v4 support library's {@code FragmentActivity}). + * It allows Flutter plugins to deal in this interface and not require that the activity be a + * subclass of {@code FlutterActivity}. + */ + public interface Provider { + /** + * Returns a reference to the Flutter view maintained by this object. This may be {@code null}. + * + * @return a reference to the Flutter view maintained by this object. + */ + FlutterView getFlutterView(); + } + + private static final String TAG = "FlutterView"; + + static final class ViewportMetrics { + float devicePixelRatio = 1.0f; + int physicalWidth = 0; + int physicalHeight = 0; + int physicalViewPaddingTop = 0; + int physicalViewPaddingRight = 0; + int physicalViewPaddingBottom = 0; + int physicalViewPaddingLeft = 0; + int physicalViewInsetTop = 0; + int physicalViewInsetRight = 0; + int physicalViewInsetBottom = 0; + int physicalViewInsetLeft = 0; + int systemGestureInsetTop = 0; + int systemGestureInsetRight = 0; + int systemGestureInsetBottom = 0; + int systemGestureInsetLeft = 0; + int physicalTouchSlop = -1; + } + + private final DartExecutor dartExecutor; + private final FlutterRenderer flutterRenderer; + private final NavigationChannel navigationChannel; + private final BackGestureChannel backGestureChannel; + private final LifecycleChannel lifecycleChannel; + private final LocalizationChannel localizationChannel; + private final PlatformChannel platformChannel; + private final SettingsChannel settingsChannel; + private final SystemChannel systemChannel; + private final InputMethodManager mImm; + private final TextInputPlugin mTextInputPlugin; + private final LocalizationPlugin mLocalizationPlugin; + private final MouseCursorPlugin mMouseCursorPlugin; + private final KeyboardManager mKeyboardManager; + private final AndroidTouchProcessor androidTouchProcessor; + private AccessibilityBridge mAccessibilityNodeProvider; + private final SurfaceHolder.Callback mSurfaceCallback; + private final ViewportMetrics mMetrics; + private final List mActivityLifecycleListeners; + private final List mFirstFrameListeners; + private final AtomicLong nextTextureId = new AtomicLong(0L); + private FlutterNativeView mNativeView; + private boolean mIsSoftwareRenderingEnabled = false; // using the software renderer or not + private boolean didRenderFirstFrame = false; + + private final AccessibilityBridge.OnAccessibilityChangeListener onAccessibilityChangeListener = + new AccessibilityBridge.OnAccessibilityChangeListener() { + @Override + public void onAccessibilityChanged( + boolean isAccessibilityEnabled, boolean isTouchExplorationEnabled) { + resetWillNotDraw(isAccessibilityEnabled, isTouchExplorationEnabled); + } + }; + + public FlutterView(Context context) { + this(context, null); + } + + public FlutterView(Context context, AttributeSet attrs) { + this(context, attrs, null); + } + + public FlutterView(Context context, AttributeSet attrs, FlutterNativeView nativeView) { + super(context, attrs); + + Activity activity = ViewUtils.getActivity(getContext()); + if (activity == null) { + throw new IllegalArgumentException("Bad context"); + } + + if (nativeView == null) { + mNativeView = new FlutterNativeView(activity.getApplicationContext()); + } else { + mNativeView = nativeView; + } + + dartExecutor = mNativeView.getDartExecutor(); + flutterRenderer = new FlutterRenderer(mNativeView.getFlutterJNI()); + mIsSoftwareRenderingEnabled = mNativeView.getFlutterJNI().getIsSoftwareRenderingEnabled(); + mMetrics = new ViewportMetrics(); + mMetrics.devicePixelRatio = context.getResources().getDisplayMetrics().density; + mMetrics.physicalTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop(); + setFocusable(true); + setFocusableInTouchMode(true); + + mNativeView.attachViewAndActivity(this, activity); + + mSurfaceCallback = + new SurfaceHolder.Callback() { + @Override + public void surfaceCreated(SurfaceHolder holder) { + assertAttached(); + mNativeView.getFlutterJNI().onSurfaceCreated(holder.getSurface()); + } + + @Override + public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) { + assertAttached(); + mNativeView.getFlutterJNI().onSurfaceChanged(width, height); + } + + @Override + public void surfaceDestroyed(SurfaceHolder holder) { + assertAttached(); + mNativeView.getFlutterJNI().onSurfaceDestroyed(); + } + }; + getHolder().addCallback(mSurfaceCallback); + + mActivityLifecycleListeners = new ArrayList<>(); + mFirstFrameListeners = new ArrayList<>(); + + // Create all platform channels + navigationChannel = new NavigationChannel(dartExecutor); + backGestureChannel = new BackGestureChannel(dartExecutor); + lifecycleChannel = new LifecycleChannel(dartExecutor); + localizationChannel = new LocalizationChannel(dartExecutor); + platformChannel = new PlatformChannel(dartExecutor); + systemChannel = new SystemChannel(dartExecutor); + settingsChannel = new SettingsChannel(dartExecutor); + + // Create and set up plugins + PlatformPlugin platformPlugin = new PlatformPlugin(activity, platformChannel); + addActivityLifecycleListener( + new ActivityLifecycleListener() { + @Override + public void onPostResume() { + platformPlugin.updateSystemUiOverlays(); + } + }); + mImm = (InputMethodManager) getContext().getSystemService(Context.INPUT_METHOD_SERVICE); + PlatformViewsController platformViewsController = + mNativeView.getPluginRegistry().getPlatformViewsController(); + mTextInputPlugin = + new TextInputPlugin(this, new TextInputChannel(dartExecutor), platformViewsController); + mKeyboardManager = new KeyboardManager(this); + + if (Build.VERSION.SDK_INT >= API_LEVELS.API_24) { + mMouseCursorPlugin = new MouseCursorPlugin(this, new MouseCursorChannel(dartExecutor)); + } else { + mMouseCursorPlugin = null; + } + mLocalizationPlugin = new LocalizationPlugin(context, localizationChannel); + androidTouchProcessor = + new AndroidTouchProcessor(flutterRenderer, /*trackMotionEvents=*/ false); + platformViewsController.attachToFlutterRenderer(flutterRenderer); + mNativeView + .getPluginRegistry() + .getPlatformViewsController() + .attachTextInputPlugin(mTextInputPlugin); + mNativeView.getFlutterJNI().setLocalizationPlugin(mLocalizationPlugin); + + // Send initial platform information to Dart + mLocalizationPlugin.sendLocalesToFlutter(getResources().getConfiguration()); + sendUserPlatformSettingsToDart(); + } + + @NonNull + public DartExecutor getDartExecutor() { + return dartExecutor; + } + + @Override + public boolean dispatchKeyEvent(KeyEvent event) { + Log.e(TAG, "dispatchKeyEvent: " + event.toString()); + if (event.getAction() == KeyEvent.ACTION_DOWN && event.getRepeatCount() == 0) { + // Tell Android to start tracking this event. + getKeyDispatcherState().startTracking(event, this); + } else if (event.getAction() == KeyEvent.ACTION_UP) { + // Stop tracking the event. + getKeyDispatcherState().handleUpEvent(event); + } + // If the key processor doesn't handle it, then send it on to the + // superclass. The key processor will typically handle all events except + // those where it has re-dispatched the event after receiving a reply from + // the framework that the framework did not handle it. + return (isAttached() && mKeyboardManager.handleEvent(event)) || super.dispatchKeyEvent(event); + } + + public FlutterNativeView getFlutterNativeView() { + return mNativeView; + } + + public FlutterPluginRegistry getPluginRegistry() { + return mNativeView.getPluginRegistry(); + } + + public String getLookupKeyForAsset(String asset) { + return FlutterMain.getLookupKeyForAsset(asset); + } + + public String getLookupKeyForAsset(String asset, String packageName) { + return FlutterMain.getLookupKeyForAsset(asset, packageName); + } + + public void addActivityLifecycleListener(ActivityLifecycleListener listener) { + mActivityLifecycleListeners.add(listener); + } + + public void onStart() { + lifecycleChannel.appIsInactive(); + } + + public void onPause() { + lifecycleChannel.appIsInactive(); + } + + public void onPostResume() { + for (ActivityLifecycleListener listener : mActivityLifecycleListeners) { + listener.onPostResume(); + } + lifecycleChannel.appIsResumed(); + } + + public void onStop() { + lifecycleChannel.appIsPaused(); + } + + public void onMemoryPressure() { + mNativeView.getFlutterJNI().notifyLowMemoryWarning(); + systemChannel.sendMemoryPressureWarning(); + } + + /** + * Returns true if the Flutter experience associated with this {@code FlutterView} has rendered + * its first frame, or false otherwise. + */ + public boolean hasRenderedFirstFrame() { + return didRenderFirstFrame; + } + + /** + * Provide a listener that will be called once when the FlutterView renders its first frame to the + * underlaying SurfaceView. + */ + public void addFirstFrameListener(FirstFrameListener listener) { + mFirstFrameListeners.add(listener); + } + + /** Remove an existing first frame listener. */ + public void removeFirstFrameListener(FirstFrameListener listener) { + mFirstFrameListeners.remove(listener); + } + + @Override + public void enableBufferingIncomingMessages() {} + + @Override + public void disableBufferingIncomingMessages() {} + + /** + * Reverts this back to the {@link SurfaceView} defaults, at the back of its window and opaque. + */ + public void disableTransparentBackground() { + setZOrderOnTop(false); + getHolder().setFormat(PixelFormat.OPAQUE); + } + + public void setInitialRoute(String route) { + navigationChannel.setInitialRoute(route); + } + + public void pushRoute(String route) { + navigationChannel.pushRoute(route); + } + + public void popRoute() { + navigationChannel.popRoute(); + } + + @TargetApi(API_LEVELS.API_34) + @RequiresApi(API_LEVELS.API_34) + public void startBackGesture(@NonNull BackEvent backEvent) { + backGestureChannel.startBackGesture(backEvent); + } + + @TargetApi(API_LEVELS.API_34) + @RequiresApi(API_LEVELS.API_34) + public void updateBackGestureProgress(@NonNull BackEvent backEvent) { + backGestureChannel.updateBackGestureProgress(backEvent); + } + + @TargetApi(API_LEVELS.API_34) + @RequiresApi(API_LEVELS.API_34) + public void commitBackGesture() { + backGestureChannel.commitBackGesture(); + } + + @TargetApi(API_LEVELS.API_34) + @RequiresApi(API_LEVELS.API_34) + public void cancelBackGesture() { + backGestureChannel.cancelBackGesture(); + } + + private void sendUserPlatformSettingsToDart() { + // Lookup the current brightness of the Android OS. + boolean isNightModeOn = + (getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK) + == Configuration.UI_MODE_NIGHT_YES; + SettingsChannel.PlatformBrightness brightness = + isNightModeOn + ? SettingsChannel.PlatformBrightness.dark + : SettingsChannel.PlatformBrightness.light; + + settingsChannel + .startMessage() + .setTextScaleFactor(getResources().getConfiguration().fontScale) + .setUse24HourFormat(DateFormat.is24HourFormat(getContext())) + .setPlatformBrightness(brightness) + .send(); + } + + @Override + protected void onConfigurationChanged(Configuration newConfig) { + super.onConfigurationChanged(newConfig); + mLocalizationPlugin.sendLocalesToFlutter(newConfig); + sendUserPlatformSettingsToDart(); + } + + float getDevicePixelRatio() { + return mMetrics.devicePixelRatio; + } + + public FlutterNativeView detach() { + if (!isAttached()) return null; + getHolder().removeCallback(mSurfaceCallback); + mNativeView.detachFromFlutterView(); + + FlutterNativeView view = mNativeView; + mNativeView = null; + return view; + } + + public void destroy() { + if (!isAttached()) return; + + getHolder().removeCallback(mSurfaceCallback); + releaseAccessibilityNodeProvider(); + + mNativeView.destroy(); + mNativeView = null; + } + + @Override + public InputConnection onCreateInputConnection(EditorInfo outAttrs) { + return mTextInputPlugin.createInputConnection(this, mKeyboardManager, outAttrs); + } + + @Override + public boolean checkInputConnectionProxy(View view) { + return mNativeView + .getPluginRegistry() + .getPlatformViewsController() + .checkInputConnectionProxy(view); + } + + @Override + public void onProvideAutofillVirtualStructure(ViewStructure structure, int flags) { + super.onProvideAutofillVirtualStructure(structure, flags); + mTextInputPlugin.onProvideAutofillVirtualStructure(structure, flags); + } + + @Override + public void autofill(SparseArray values) { + mTextInputPlugin.autofill(values); + } + + @Override + public boolean onTouchEvent(MotionEvent event) { + if (!isAttached()) { + return super.onTouchEvent(event); + } + + requestUnbufferedDispatch(event); + + return androidTouchProcessor.onTouchEvent(event); + } + + @Override + public boolean onHoverEvent(MotionEvent event) { + if (!isAttached()) { + return super.onHoverEvent(event); + } + + boolean handled = mAccessibilityNodeProvider.onAccessibilityHoverEvent(event); + if (!handled) { + // TODO(ianh): Expose hover events to the platform, + // implementing ADD, REMOVE, etc. + } + return handled; + } + + /** + * Invoked by Android when a generic motion event occurs, e.g., joystick movement, mouse hover, + * track pad touches, scroll wheel movements, etc. + * + *

Flutter handles all of its own gesture detection and processing, therefore this method + * forwards all {@link MotionEvent} data from Android to Flutter. + */ + @Override + public boolean onGenericMotionEvent(MotionEvent event) { + boolean handled = + isAttached() && androidTouchProcessor.onGenericMotionEvent(event, getContext()); + return handled ? true : super.onGenericMotionEvent(event); + } + + @Override + protected void onSizeChanged(int width, int height, int oldWidth, int oldHeight) { + mMetrics.physicalWidth = width; + mMetrics.physicalHeight = height; + updateViewportMetrics(); + super.onSizeChanged(width, height, oldWidth, oldHeight); + } + + // TODO(garyq): Add support for notch cutout API + // Decide if we want to zero the padding of the sides. When in Landscape orientation, + // android may decide to place the software navigation bars on the side. When the nav + // bar is hidden, the reported insets should be removed to prevent extra useless space + // on the sides. + private enum ZeroSides { + NONE, + LEFT, + RIGHT, + BOTH + } + + private ZeroSides calculateShouldZeroSides() { + // We get both orientation and rotation because rotation is all 4 + // rotations relative to default rotation while orientation is portrait + // or landscape. By combining both, we can obtain a more precise measure + // of the rotation. + Context context = getContext(); + int orientation = context.getResources().getConfiguration().orientation; + int rotation = + ((WindowManager) context.getSystemService(Context.WINDOW_SERVICE)) + .getDefaultDisplay() + .getRotation(); + + if (orientation == Configuration.ORIENTATION_LANDSCAPE) { + if (rotation == Surface.ROTATION_90) { + return ZeroSides.RIGHT; + } else if (rotation == Surface.ROTATION_270) { + // In android API >= 23, the nav bar always appears on the "bottom" (USB) side. + return Build.VERSION.SDK_INT >= API_LEVELS.API_23 ? ZeroSides.LEFT : ZeroSides.RIGHT; + } + // Ambiguous orientation due to landscape left/right default. Zero both sides. + else if (rotation == Surface.ROTATION_0 || rotation == Surface.ROTATION_180) { + return ZeroSides.BOTH; + } + } + // Square orientation deprecated in API 16, we will not check for it and return false + // to be safe and not remove any unique padding for the devices that do use it. + return ZeroSides.NONE; + } + + // TODO(garyq): Use new Android R getInsets API + // TODO(garyq): The keyboard detection may interact strangely with + // https://github.com/flutter/flutter/issues/22061 + + // Uses inset heights and screen heights as a heuristic to determine if the insets should + // be padded. When the on-screen keyboard is detected, we want to include the full inset + // but when the inset is just the hidden nav bar, we want to provide a zero inset so the space + // can be used. + + private int guessBottomKeyboardInset(WindowInsets insets) { + int screenHeight = getRootView().getHeight(); + // Magic number due to this being a heuristic. This should be replaced, but we have not + // found a clean way to do it yet (Sept. 2018) + final double keyboardHeightRatioHeuristic = 0.18; + if (insets.getSystemWindowInsetBottom() < screenHeight * keyboardHeightRatioHeuristic) { + // Is not a keyboard, so return zero as inset. + return 0; + } else { + // Is a keyboard, so return the full inset. + return insets.getSystemWindowInsetBottom(); + } + } + + // This callback is not present in API < 20, which means lower API devices will see + // the wider than expected padding when the status and navigation bars are hidden. + // The annotations to suppress "InlinedApi" and "NewApi" lints prevent lint warnings + // caused by usage of Android Q APIs. These calls are safe because they are + // guarded. + @Override + @SuppressLint({"InlinedApi", "NewApi"}) + public final WindowInsets onApplyWindowInsets(WindowInsets insets) { + // getSystemGestureInsets() was introduced in API 29 and immediately deprecated in 30. + if (Build.VERSION.SDK_INT == API_LEVELS.API_29) { + Insets systemGestureInsets = insets.getSystemGestureInsets(); + mMetrics.systemGestureInsetTop = systemGestureInsets.top; + mMetrics.systemGestureInsetRight = systemGestureInsets.right; + mMetrics.systemGestureInsetBottom = systemGestureInsets.bottom; + mMetrics.systemGestureInsetLeft = systemGestureInsets.left; + } + + boolean statusBarVisible = (SYSTEM_UI_FLAG_FULLSCREEN & getWindowSystemUiVisibility()) == 0; + boolean navigationBarVisible = + (SYSTEM_UI_FLAG_HIDE_NAVIGATION & getWindowSystemUiVisibility()) == 0; + + if (Build.VERSION.SDK_INT >= API_LEVELS.API_30) { + int mask = 0; + if (navigationBarVisible) { + mask = mask | android.view.WindowInsets.Type.navigationBars(); + } + if (statusBarVisible) { + mask = mask | android.view.WindowInsets.Type.statusBars(); + } + Insets uiInsets = insets.getInsets(mask); + mMetrics.physicalViewPaddingTop = uiInsets.top; + mMetrics.physicalViewPaddingRight = uiInsets.right; + mMetrics.physicalViewPaddingBottom = uiInsets.bottom; + mMetrics.physicalViewPaddingLeft = uiInsets.left; + + Insets imeInsets = insets.getInsets(android.view.WindowInsets.Type.ime()); + mMetrics.physicalViewInsetTop = imeInsets.top; + mMetrics.physicalViewInsetRight = imeInsets.right; + mMetrics.physicalViewInsetBottom = imeInsets.bottom; // Typically, only bottom is non-zero + mMetrics.physicalViewInsetLeft = imeInsets.left; + + Insets systemGestureInsets = + insets.getInsets(android.view.WindowInsets.Type.systemGestures()); + mMetrics.systemGestureInsetTop = systemGestureInsets.top; + mMetrics.systemGestureInsetRight = systemGestureInsets.right; + mMetrics.systemGestureInsetBottom = systemGestureInsets.bottom; + mMetrics.systemGestureInsetLeft = systemGestureInsets.left; + + // TODO(garyq): Expose the full rects of the display cutout. + + // Take the max of the display cutout insets and existing padding to merge them + DisplayCutout cutout = insets.getDisplayCutout(); + if (cutout != null) { + Insets waterfallInsets = cutout.getWaterfallInsets(); + mMetrics.physicalViewPaddingTop = + Math.max( + Math.max(mMetrics.physicalViewPaddingTop, waterfallInsets.top), + cutout.getSafeInsetTop()); + mMetrics.physicalViewPaddingRight = + Math.max( + Math.max(mMetrics.physicalViewPaddingRight, waterfallInsets.right), + cutout.getSafeInsetRight()); + mMetrics.physicalViewPaddingBottom = + Math.max( + Math.max(mMetrics.physicalViewPaddingBottom, waterfallInsets.bottom), + cutout.getSafeInsetBottom()); + mMetrics.physicalViewPaddingLeft = + Math.max( + Math.max(mMetrics.physicalViewPaddingLeft, waterfallInsets.left), + cutout.getSafeInsetLeft()); + } + } else { + // We zero the left and/or right sides to prevent the padding the + // navigation bar would have caused. + ZeroSides zeroSides = ZeroSides.NONE; + if (!navigationBarVisible) { + zeroSides = calculateShouldZeroSides(); + } + + // Status bar (top), navigation bar (bottom) and left/right system insets should + // partially obscure the content (padding). + mMetrics.physicalViewPaddingTop = statusBarVisible ? insets.getSystemWindowInsetTop() : 0; + mMetrics.physicalViewPaddingRight = + zeroSides == ZeroSides.RIGHT || zeroSides == ZeroSides.BOTH + ? 0 + : insets.getSystemWindowInsetRight(); + mMetrics.physicalViewPaddingBottom = + navigationBarVisible && guessBottomKeyboardInset(insets) == 0 + ? insets.getSystemWindowInsetBottom() + : 0; + mMetrics.physicalViewPaddingLeft = + zeroSides == ZeroSides.LEFT || zeroSides == ZeroSides.BOTH + ? 0 + : insets.getSystemWindowInsetLeft(); + + // Bottom system inset (keyboard) should adjust scrollable bottom edge (inset). + mMetrics.physicalViewInsetTop = 0; + mMetrics.physicalViewInsetRight = 0; + mMetrics.physicalViewInsetBottom = guessBottomKeyboardInset(insets); + mMetrics.physicalViewInsetLeft = 0; + } + + updateViewportMetrics(); + return super.onApplyWindowInsets(insets); + } + + private boolean isAttached() { + return mNativeView != null && mNativeView.isAttached(); + } + + void assertAttached() { + if (!isAttached()) throw new AssertionError("Platform view is not attached"); + } + + private void preRun() { + resetAccessibilityTree(); + } + + void resetAccessibilityTree() { + if (mAccessibilityNodeProvider != null) { + mAccessibilityNodeProvider.reset(); + } + } + + private void postRun() {} + + public void runFromBundle(FlutterRunArguments args) { + assertAttached(); + preRun(); + mNativeView.runFromBundle(args); + postRun(); + } + + /** + * Return the most recent frame as a bitmap. + * + * @return A bitmap. + */ + public Bitmap getBitmap() { + assertAttached(); + return mNativeView.getFlutterJNI().getBitmap(); + } + + private void updateViewportMetrics() { + if (!isAttached()) return; + mNativeView + .getFlutterJNI() + .setViewportMetrics( + mMetrics.devicePixelRatio, + mMetrics.physicalWidth, + mMetrics.physicalHeight, + mMetrics.physicalViewPaddingTop, + mMetrics.physicalViewPaddingRight, + mMetrics.physicalViewPaddingBottom, + mMetrics.physicalViewPaddingLeft, + mMetrics.physicalViewInsetTop, + mMetrics.physicalViewInsetRight, + mMetrics.physicalViewInsetBottom, + mMetrics.physicalViewInsetLeft, + mMetrics.systemGestureInsetTop, + mMetrics.systemGestureInsetRight, + mMetrics.systemGestureInsetBottom, + mMetrics.systemGestureInsetLeft, + mMetrics.physicalTouchSlop, + new int[0], + new int[0], + new int[0]); + } + + // Called by FlutterNativeView to notify first Flutter frame rendered. + public void onFirstFrame() { + didRenderFirstFrame = true; + + // Allow listeners to remove themselves when they are called. + List listeners = new ArrayList<>(mFirstFrameListeners); + for (FirstFrameListener listener : listeners) { + listener.onFirstFrame(); + } + } + + @Override + protected void onAttachedToWindow() { + super.onAttachedToWindow(); + + PlatformViewsController platformViewsController = + getPluginRegistry().getPlatformViewsController(); + mAccessibilityNodeProvider = + new AccessibilityBridge( + this, + new AccessibilityChannel(dartExecutor, getFlutterNativeView().getFlutterJNI()), + (AccessibilityManager) getContext().getSystemService(Context.ACCESSIBILITY_SERVICE), + getContext().getContentResolver(), + platformViewsController); + mAccessibilityNodeProvider.setOnAccessibilityChangeListener(onAccessibilityChangeListener); + + resetWillNotDraw( + mAccessibilityNodeProvider.isAccessibilityEnabled(), + mAccessibilityNodeProvider.isTouchExplorationEnabled()); + } + + @Override + protected void onDetachedFromWindow() { + super.onDetachedFromWindow(); + releaseAccessibilityNodeProvider(); + } + + // TODO(mattcarroll): Confer with Ian as to why we need this method. Delete if possible, otherwise + // add comments. + private void resetWillNotDraw(boolean isAccessibilityEnabled, boolean isTouchExplorationEnabled) { + if (!mIsSoftwareRenderingEnabled) { + setWillNotDraw(!(isAccessibilityEnabled || isTouchExplorationEnabled)); + } else { + setWillNotDraw(false); + } + } + + @Override + public AccessibilityNodeProvider getAccessibilityNodeProvider() { + if (mAccessibilityNodeProvider != null && mAccessibilityNodeProvider.isAccessibilityEnabled()) { + return mAccessibilityNodeProvider; + } else { + // TODO(goderbauer): when a11y is off this should return a one-off snapshot of + // the a11y + // tree. + return null; + } + } + + private void releaseAccessibilityNodeProvider() { + if (mAccessibilityNodeProvider != null) { + mAccessibilityNodeProvider.release(); + mAccessibilityNodeProvider = null; + } + } + + // -------- Start: Mouse ------- + + @Override + @TargetApi(API_LEVELS.API_24) + @RequiresApi(API_LEVELS.API_24) + @NonNull + public PointerIcon getSystemPointerIcon(int type) { + return PointerIcon.getSystemIcon(getContext(), type); + } + + // -------- End: Mouse ------- + + // -------- Start: Keyboard ------- + + @Override + public BinaryMessenger getBinaryMessenger() { + return this; + } + + @Override + public boolean onTextInputKeyEvent(@NonNull KeyEvent keyEvent) { + return mTextInputPlugin.handleKeyEvent(keyEvent); + } + + @Override + public void redispatch(@NonNull KeyEvent keyEvent) { + getRootView().dispatchKeyEvent(keyEvent); + } + + // -------- End: Keyboard ------- + + @Override + @UiThread + public TaskQueue makeBackgroundTaskQueue(TaskQueueOptions options) { + return null; + } + + @Override + @UiThread + public void send(String channel, ByteBuffer message) { + send(channel, message, null); + } + + @Override + @UiThread + public void send(String channel, ByteBuffer message, BinaryReply callback) { + if (!isAttached()) { + Log.d(TAG, "FlutterView.send called on a detached view, channel=" + channel); + return; + } + mNativeView.send(channel, message, callback); + } + + @Override + @UiThread + public void setMessageHandler(@NonNull String channel, @NonNull BinaryMessageHandler handler) { + mNativeView.setMessageHandler(channel, handler); + } + + @Override + @UiThread + public void setMessageHandler( + @NonNull String channel, + @NonNull BinaryMessageHandler handler, + @NonNull TaskQueue taskQueue) { + mNativeView.setMessageHandler(channel, handler, taskQueue); + } + + /** Listener will be called on the Android UI thread once when Flutter renders the first frame. */ + public interface FirstFrameListener { + void onFirstFrame(); + } + + @Override + @NonNull + public TextureRegistry.SurfaceTextureEntry createSurfaceTexture() { + final SurfaceTexture surfaceTexture = new SurfaceTexture(0); + return registerSurfaceTexture(surfaceTexture); + } + + @Override + @NonNull + public ImageTextureEntry createImageTexture() { + throw new UnsupportedOperationException("Image textures are not supported in this mode."); + } + + @Override + public SurfaceProducer createSurfaceProducer() { + throw new UnsupportedOperationException( + "SurfaceProducer textures are not supported in this mode."); + } + + @Override + @NonNull + public TextureRegistry.SurfaceTextureEntry registerSurfaceTexture( + @NonNull SurfaceTexture surfaceTexture) { + surfaceTexture.detachFromGLContext(); + final SurfaceTextureRegistryEntry entry = + new SurfaceTextureRegistryEntry(nextTextureId.getAndIncrement(), surfaceTexture); + mNativeView.getFlutterJNI().registerTexture(entry.id(), entry.textureWrapper()); + return entry; + } + + final class SurfaceTextureRegistryEntry implements TextureRegistry.SurfaceTextureEntry { + private final long id; + private final SurfaceTextureWrapper textureWrapper; + private boolean released; + + SurfaceTextureRegistryEntry(long id, SurfaceTexture surfaceTexture) { + this.id = id; + this.textureWrapper = new SurfaceTextureWrapper(surfaceTexture); + + // The callback relies on being executed on the UI thread (unsynchronised read of + // mNativeView + // and also the engine code check for platform thread in + // Shell::OnPlatformViewMarkTextureFrameAvailable), + // so we explicitly pass a Handler for the current thread. + this.surfaceTexture().setOnFrameAvailableListener(onFrameListener, new Handler()); + } + + private SurfaceTexture.OnFrameAvailableListener onFrameListener = + new SurfaceTexture.OnFrameAvailableListener() { + @Override + public void onFrameAvailable(SurfaceTexture texture) { + if (released || mNativeView == null) { + // Even though we make sure to unregister the callback before releasing, as of Android + // O + // SurfaceTexture has a data race when accessing the callback, so the callback may + // still be called by a stale reference after released==true and mNativeView==null. + return; + } + + mNativeView + .getFlutterJNI() + .markTextureFrameAvailable(SurfaceTextureRegistryEntry.this.id); + } + }; + + public SurfaceTextureWrapper textureWrapper() { + return textureWrapper; + } + + @NonNull + @Override + public SurfaceTexture surfaceTexture() { + return textureWrapper.surfaceTexture(); + } + + @Override + public long id() { + return id; + } + + @Override + public void release() { + if (released) { + return; + } + released = true; + + // The ordering of the next 3 calls is important: + // First we remove the frame listener, then we release the SurfaceTexture, and only after we + // unregister + // the texture which actually deletes the GL texture. + + // Otherwise onFrameAvailableListener might be called after mNativeView==null + // (https://github.com/flutter/flutter/issues/20951). See also the check in onFrameAvailable. + surfaceTexture().setOnFrameAvailableListener(null); + textureWrapper.release(); + mNativeView.getFlutterJNI().unregisterTexture(id); + } + } +} diff --git a/shell/platform/android/io/flutter/view/TextureRegistry.java b/shell/platform/android/io/flutter/view/TextureRegistry.java index 5ec4620c272cc..a8b5a52d0e6c5 100644 --- a/shell/platform/android/io/flutter/view/TextureRegistry.java +++ b/shell/platform/android/io/flutter/view/TextureRegistry.java @@ -13,8 +13,8 @@ // TODO(mattcarroll): re-evalute docs in this class and add nullability annotations. /** - * Registry of backend textures used with a single {@link io.flutter.embedding.android.FlutterView} - * instance. Entries may be embedded into the Flutter view using the Texture widget. */ public interface TextureRegistry { diff --git a/shell/platform/android/test/io/flutter/embedding/engine/plugins/shim/ShimPluginRegistryTest.java b/shell/platform/android/test/io/flutter/embedding/engine/plugins/shim/ShimPluginRegistryTest.java new file mode 100644 index 0000000000000..95aad07402f12 --- /dev/null +++ b/shell/platform/android/test/io/flutter/embedding/engine/plugins/shim/ShimPluginRegistryTest.java @@ -0,0 +1,153 @@ +package io.flutter.embedding.engine.plugins.shim; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.app.Activity; +import android.content.Context; +import androidx.test.ext.junit.runners.AndroidJUnit4; +import io.flutter.embedding.engine.FlutterEngine; +import io.flutter.embedding.engine.plugins.FlutterPlugin; +import io.flutter.embedding.engine.plugins.FlutterPlugin.FlutterPluginBinding; +import io.flutter.embedding.engine.plugins.PluginRegistry; +import io.flutter.embedding.engine.plugins.activity.ActivityAware; +import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.robolectric.annotation.Config; + +@Config(manifest = Config.NONE) +@RunWith(AndroidJUnit4.class) +public class ShimPluginRegistryTest { + + @Mock private FlutterEngine mockFlutterEngine; + @Mock private FlutterPluginBinding mockFlutterPluginBinding; + @Mock private ActivityPluginBinding mockActivityPluginBinding; + @Mock private PluginRegistry mockPluginRegistry; + @Mock private Context mockApplicationContext; + @Mock private Activity mockActivity; + + @Before + public void setup() { + MockitoAnnotations.openMocks(this); + when(mockFlutterEngine.getPlugins()).thenReturn(mockPluginRegistry); + when(mockFlutterPluginBinding.getApplicationContext()).thenReturn(mockApplicationContext); + when(mockActivityPluginBinding.getActivity()).thenReturn(mockActivity); + } + + @SuppressWarnings("deprecation") + // Test is intentionally verifying deprecated behavior. + @Test + public void itSuppliesOldAPIsViaTheNewFlutterPluginBinding() { + ShimPluginRegistry registryUnderTest = new ShimPluginRegistry(mockFlutterEngine); + // Fully qualifed name because imports can not have deprecation supression. + // This is the consumption side of the old plugins. + io.flutter.plugin.common.PluginRegistry.Registrar registrarUnderTest = + registryUnderTest.registrarFor("test"); + + ArgumentCaptor shimAggregateCaptor = + ArgumentCaptor.forClass(FlutterPlugin.class); + // A single shim aggregate was added as a new plugin to the FlutterEngine's PluginRegistry. + verify(mockPluginRegistry).add(shimAggregateCaptor.capture()); + // This is really a ShimRegistrarAggregate acting as a FlutterPlugin which is the + // intermediate consumption side of the new plugin inside the shim. + FlutterPlugin shimAggregateUnderTest = shimAggregateCaptor.getValue(); + // The FlutterPluginBinding is the supply side of the new plugin. + shimAggregateUnderTest.onAttachedToEngine(mockFlutterPluginBinding); + + // Consume something from the old plugin API. + assertEquals(mockApplicationContext, registrarUnderTest.context()); + // Check that the value comes from the supply side of the new plugin. + verify(mockFlutterPluginBinding).getApplicationContext(); + } + + @SuppressWarnings("deprecation") + // Test is intentionally verifying deprecated behavior. + @Test + public void itSuppliesMultipleOldPlugins() { + ShimPluginRegistry registryUnderTest = new ShimPluginRegistry(mockFlutterEngine); + // Fully qualifed name because imports can not have deprecation supression. + io.flutter.plugin.common.PluginRegistry.Registrar registrarUnderTest1 = + registryUnderTest.registrarFor("test1"); + io.flutter.plugin.common.PluginRegistry.Registrar registrarUnderTest2 = + registryUnderTest.registrarFor("test2"); + + ArgumentCaptor shimAggregateCaptor = + ArgumentCaptor.forClass(FlutterPlugin.class); + verify(mockPluginRegistry).add(shimAggregateCaptor.capture()); + // There's only one aggregate for many old plugins. + FlutterPlugin shimAggregateUnderTest = shimAggregateCaptor.getValue(); + + // The FlutterPluginBinding is the supply side of the new plugin. + shimAggregateUnderTest.onAttachedToEngine(mockFlutterPluginBinding); + + // Since the 2 old plugins are supplied by the same intermediate FlutterPlugin, they should + // get the same value. + assertEquals(registrarUnderTest1.context(), registrarUnderTest2.context()); + verify(mockFlutterPluginBinding, times(2)).getApplicationContext(); + } + + @SuppressWarnings("deprecation") + // Test is intentionally verifying deprecated behavior. + @Test + public void itCanOnlySupplyActivityBindingWhenUpstreamActivityIsAttached() { + ShimPluginRegistry registryUnderTest = new ShimPluginRegistry(mockFlutterEngine); + io.flutter.plugin.common.PluginRegistry.Registrar registrarUnderTest = + registryUnderTest.registrarFor("test"); + + ArgumentCaptor shimAggregateCaptor = + ArgumentCaptor.forClass(FlutterPlugin.class); + verify(mockPluginRegistry).add(shimAggregateCaptor.capture()); + FlutterPlugin shimAggregateAsPlugin = shimAggregateCaptor.getValue(); + ActivityAware shimAggregateAsActivityAware = (ActivityAware) shimAggregateCaptor.getValue(); + + // Nothing is retrievable when nothing is attached. + assertNull(registrarUnderTest.context()); + assertNull(registrarUnderTest.activity()); + + shimAggregateAsPlugin.onAttachedToEngine(mockFlutterPluginBinding); + + assertEquals(mockApplicationContext, registrarUnderTest.context()); + assertNull(registrarUnderTest.activity()); + + shimAggregateAsActivityAware.onAttachedToActivity(mockActivityPluginBinding); + + // Now context is the activity context. + assertEquals(mockActivity, registrarUnderTest.activeContext()); + assertEquals(mockActivity, registrarUnderTest.activity()); + + shimAggregateAsActivityAware.onDetachedFromActivityForConfigChanges(); + + assertEquals(mockApplicationContext, registrarUnderTest.activeContext()); + assertNull(registrarUnderTest.activity()); + + shimAggregateAsActivityAware.onReattachedToActivityForConfigChanges(mockActivityPluginBinding); + assertEquals(mockActivity, registrarUnderTest.activeContext()); + assertEquals(mockActivity, registrarUnderTest.activity()); + + shimAggregateAsActivityAware.onDetachedFromActivity(); + + assertEquals(mockApplicationContext, registrarUnderTest.activeContext()); + assertNull(registrarUnderTest.activity()); + + // Attach an activity again. + shimAggregateAsActivityAware.onAttachedToActivity(mockActivityPluginBinding); + + assertEquals(mockActivity, registrarUnderTest.activeContext()); + assertEquals(mockActivity, registrarUnderTest.activity()); + + // Now rip out the whole engine. + shimAggregateAsPlugin.onDetachedFromEngine(mockFlutterPluginBinding); + + // And everything should have been made unavailable. + assertNull(registrarUnderTest.activeContext()); + assertNull(registrarUnderTest.activity()); + } +} diff --git a/testing/scenario_app/android/app/src/main/AndroidManifest.xml b/testing/scenario_app/android/app/src/main/AndroidManifest.xml index a986675b5574d..e99d9ae8ae6b5 100644 --- a/testing/scenario_app/android/app/src/main/AndroidManifest.xml +++ b/testing/scenario_app/android/app/src/main/AndroidManifest.xml @@ -4,6 +4,7 @@ package="dev.flutter.scenarios">