diff --git a/shell/platform/android/io/flutter/embedding/android/FlutterActivity.java b/shell/platform/android/io/flutter/embedding/android/FlutterActivity.java index 5f5ae30566d12..a99f86a0777ce 100644 --- a/shell/platform/android/io/flutter/embedding/android/FlutterActivity.java +++ b/shell/platform/android/io/flutter/embedding/android/FlutterActivity.java @@ -902,11 +902,7 @@ protected Bundle getMetaData() throws PackageManager.NameNotFoundException { @Override public PlatformPlugin providePlatformPlugin( @Nullable Activity activity, @NonNull FlutterEngine flutterEngine) { - if (activity != null) { - return new PlatformPlugin(getActivity(), flutterEngine.getPlatformChannel()); - } else { - return null; - } + return new PlatformPlugin(getActivity(), flutterEngine.getPlatformChannel(), this); } /** @@ -1032,6 +1028,12 @@ public boolean shouldRestoreAndSaveState() { return true; } + @Override + public boolean popSystemNavigator() { + // Hook for subclass. No-op if returns false. + return false; + } + private boolean stillAttachedForEvent(String event) { if (delegate == null) { Log.v(TAG, "FlutterActivity " + hashCode() + " " + event + " called after release."); diff --git a/shell/platform/android/io/flutter/embedding/android/FlutterActivityAndFragmentDelegate.java b/shell/platform/android/io/flutter/embedding/android/FlutterActivityAndFragmentDelegate.java index 4495ac7a09fe8..a7be71dce0789 100644 --- a/shell/platform/android/io/flutter/embedding/android/FlutterActivityAndFragmentDelegate.java +++ b/shell/platform/android/io/flutter/embedding/android/FlutterActivityAndFragmentDelegate.java @@ -22,7 +22,6 @@ import androidx.lifecycle.Lifecycle; import io.flutter.FlutterInjector; import io.flutter.Log; -import io.flutter.app.FlutterActivity; import io.flutter.embedding.engine.FlutterEngine; import io.flutter.embedding.engine.FlutterEngineCache; import io.flutter.embedding.engine.FlutterShellArgs; @@ -752,7 +751,10 @@ private void ensureAlive() { * FlutterActivityAndFragmentDelegate}. */ /* package */ interface Host - extends SplashScreenProvider, FlutterEngineProvider, FlutterEngineConfigurator { + extends SplashScreenProvider, + FlutterEngineProvider, + FlutterEngineConfigurator, + PlatformPlugin.PlatformPluginDelegate { /** Returns the {@link Context} that backs the host {@link Activity} or {@code Fragment}. */ @NonNull Context getContext(); diff --git a/shell/platform/android/io/flutter/embedding/android/FlutterFragment.java b/shell/platform/android/io/flutter/embedding/android/FlutterFragment.java index a1e20defb7577..ba464084800dc 100644 --- a/shell/platform/android/io/flutter/embedding/android/FlutterFragment.java +++ b/shell/platform/android/io/flutter/embedding/android/FlutterFragment.java @@ -432,7 +432,7 @@ private CachedEngineFragmentBuilder(@NonNull String engineId) { this(FlutterFragment.class, engineId); } - protected CachedEngineFragmentBuilder( + public CachedEngineFragmentBuilder( @NonNull Class subclass, @NonNull String engineId) { this.fragmentClass = subclass; this.engineId = engineId; @@ -984,7 +984,7 @@ public FlutterEngine getFlutterEngine() { public PlatformPlugin providePlatformPlugin( @Nullable Activity activity, @NonNull FlutterEngine flutterEngine) { if (activity != null) { - return new PlatformPlugin(getActivity(), flutterEngine.getPlatformChannel()); + return new PlatformPlugin(getActivity(), flutterEngine.getPlatformChannel(), this); } else { return null; } @@ -1110,6 +1110,12 @@ public boolean shouldRestoreAndSaveState() { return true; } + @Override + public boolean popSystemNavigator() { + // Hook for subclass. No-op if returns false. + return false; + } + private boolean stillAttachedForEvent(String event) { if (delegate == null) { Log.v(TAG, "FlutterFragment " + hashCode() + " " + event + " called after release."); diff --git a/shell/platform/android/io/flutter/plugin/platform/PlatformPlugin.java b/shell/platform/android/io/flutter/plugin/platform/PlatformPlugin.java index bd53c2e87c168..5dba56b4fc920 100644 --- a/shell/platform/android/io/flutter/plugin/platform/PlatformPlugin.java +++ b/shell/platform/android/io/flutter/plugin/platform/PlatformPlugin.java @@ -15,6 +15,7 @@ import android.view.View; import android.view.Window; import android.view.WindowManager; +import androidx.activity.OnBackPressedDispatcherOwner; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; @@ -30,10 +31,30 @@ public class PlatformPlugin { private final Activity activity; private final PlatformChannel platformChannel; + private final PlatformPluginDelegate platformPluginDelegate; private PlatformChannel.SystemChromeStyle currentTheme; private int mEnabledOverlays; private static final String TAG = "PlatformPlugin"; + /** + * The {@link PlatformPlugin} generally has default behaviors implemented for platform + * functionalities requested by the Flutter framework. However, functionalities exposed through + * this interface could be customized by the more public-facing APIs that implement this interface + * such as the {@link io.flutter.embedding.android.FlutterActivity} or the {@link + * io.flutter.embedding.android.FlutterFragment}. + */ + public interface PlatformPluginDelegate { + /** + * Allow implementer to customize the behavior needed when the Flutter framework calls to pop + * the Android-side navigation stack. + * + * @return true if the implementation consumed the pop signal. If false, a default behavior of + * finishing the activity or sending the signal to {@link + * androidx.activity.OnBackPressedDispatcher} will be executed. + */ + boolean popSystemNavigator(); + } + @VisibleForTesting final PlatformChannel.PlatformMessageHandler mPlatformMessageHandler = new PlatformChannel.PlatformMessageHandler() { @@ -101,9 +122,15 @@ public boolean clipboardHasStrings() { }; public PlatformPlugin(Activity activity, PlatformChannel platformChannel) { + this(activity, platformChannel, null); + } + + public PlatformPlugin( + Activity activity, PlatformChannel platformChannel, PlatformPluginDelegate delegate) { this.activity = activity; this.platformChannel = platformChannel; this.platformChannel.setPlatformMessageHandler(mPlatformMessageHandler); + this.platformPluginDelegate = delegate; mEnabledOverlays = DEFAULT_SYSTEM_UI; } @@ -161,13 +188,14 @@ private void setSystemChromeApplicationSwitcherDescription( return; } - // Linter refuses to believe we're only executing this code in API 28 unless we use distinct if + // Linter refuses to believe we're only executing this code in API 28 unless we + // use distinct if // blocks and // hardcode the API 28 constant. if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P && Build.VERSION.SDK_INT > Build.VERSION_CODES.LOLLIPOP) { activity.setTaskDescription( - new TaskDescription(description.label, /*icon=*/ null, description.color)); + new TaskDescription(description.label, /* icon= */ null, description.color)); } if (Build.VERSION.SDK_INT >= 28) { TaskDescription taskDescription = @@ -178,14 +206,16 @@ private void setSystemChromeApplicationSwitcherDescription( private void setSystemChromeEnabledSystemUIOverlays( List overlaysToShow) { - // Start by assuming we want to hide all system overlays (like an immersive game). + // Start by assuming we want to hide all system overlays (like an immersive + // game). int enabledOverlays = DEFAULT_SYSTEM_UI | View.SYSTEM_UI_FLAG_FULLSCREEN | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION; - // The SYSTEM_UI_FLAG_IMMERSIVE_STICKY flag was introduced in API 19, so we apply it + // The SYSTEM_UI_FLAG_IMMERSIVE_STICKY flag was introduced in API 19, so we + // apply it // if desired, and if the current Android version is 19 or greater. if (overlaysToShow.size() == 0 && Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { enabledOverlays |= View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY; @@ -233,7 +263,8 @@ private void setSystemChromeSystemUIOverlayStyle( View view = window.getDecorView(); int flags = view.getSystemUiVisibility(); // You can change the navigation bar color (including translucent colors) - // in Android, but you can't change the color of the navigation buttons until Android O. + // in Android, but you can't change the color of the navigation buttons until + // Android O. // LIGHT vs DARK effectively isn't supported until then. // Build.VERSION_CODES.O if (Build.VERSION.SDK_INT >= 26) { @@ -279,7 +310,16 @@ private void setSystemChromeSystemUIOverlayStyle( } private void popSystemNavigator() { - activity.finish(); + if (platformPluginDelegate.popSystemNavigator()) { + // A custom behavior was executed by the delegate. Don't execute default behavior. + return; + } + + if (activity instanceof OnBackPressedDispatcherOwner) { + ((OnBackPressedDispatcherOwner) activity).getOnBackPressedDispatcher().onBackPressed(); + } else { + activity.finish(); + } } private CharSequence getClipboardData(PlatformChannel.ClipboardContentFormat format) { diff --git a/shell/platform/android/test/io/flutter/embedding/android/FlutterAndroidComponentTest.java b/shell/platform/android/test/io/flutter/embedding/android/FlutterAndroidComponentTest.java index d5d618f3ff0a9..b4de9f1ea71b7 100644 --- a/shell/platform/android/test/io/flutter/embedding/android/FlutterAndroidComponentTest.java +++ b/shell/platform/android/test/io/flutter/embedding/android/FlutterAndroidComponentTest.java @@ -382,5 +382,10 @@ public void onFlutterUiNoLongerDisplayed() {} @Override public void detachFromFlutterEngine() {} + + @Override + public boolean popSystemNavigator() { + return false; + } } } diff --git a/shell/platform/android/test/io/flutter/plugin/platform/PlatformPluginTest.java b/shell/platform/android/test/io/flutter/plugin/platform/PlatformPluginTest.java index 4d8cf0c55cb4b..fd67f7e38fa4e 100644 --- a/shell/platform/android/test/io/flutter/plugin/platform/PlatformPluginTest.java +++ b/shell/platform/android/test/io/flutter/plugin/platform/PlatformPluginTest.java @@ -6,6 +6,9 @@ import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import android.app.Activity; @@ -18,9 +21,12 @@ import android.os.Build; import android.view.View; import android.view.Window; +import androidx.activity.OnBackPressedDispatcher; +import androidx.fragment.app.FragmentActivity; import io.flutter.embedding.engine.systemchannels.PlatformChannel; import io.flutter.embedding.engine.systemchannels.PlatformChannel.ClipboardContentFormat; import io.flutter.embedding.engine.systemchannels.PlatformChannel.SystemChromeStyle; +import io.flutter.plugin.platform.PlatformPlugin.PlatformPluginDelegate; import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; @@ -133,4 +139,87 @@ public void setNavigationBarDividerColor() { assertEquals(0XFF000000, fakeActivity.getWindow().getNavigationBarColor()); } } + + @Test + public void popSystemNavigatorFlutterActivity() { + Activity mockActivity = mock(Activity.class); + PlatformChannel mockPlatformChannel = mock(PlatformChannel.class); + PlatformPluginDelegate mockPlatformPluginDelegate = mock(PlatformPluginDelegate.class); + when(mockPlatformPluginDelegate.popSystemNavigator()).thenReturn(false); + PlatformPlugin platformPlugin = + new PlatformPlugin(mockActivity, mockPlatformChannel, mockPlatformPluginDelegate); + + platformPlugin.mPlatformMessageHandler.popSystemNavigator(); + + verify(mockPlatformPluginDelegate, times(1)).popSystemNavigator(); + verify(mockActivity, times(1)).finish(); + } + + @Test + public void doesNotDoAnythingByDefaultIfPopSystemNavigatorOverridden() { + Activity mockActivity = mock(Activity.class); + PlatformChannel mockPlatformChannel = mock(PlatformChannel.class); + PlatformPluginDelegate mockPlatformPluginDelegate = mock(PlatformPluginDelegate.class); + when(mockPlatformPluginDelegate.popSystemNavigator()).thenReturn(true); + PlatformPlugin platformPlugin = + new PlatformPlugin(mockActivity, mockPlatformChannel, mockPlatformPluginDelegate); + + platformPlugin.mPlatformMessageHandler.popSystemNavigator(); + + verify(mockPlatformPluginDelegate, times(1)).popSystemNavigator(); + // No longer perform the default action when overridden. + verify(mockActivity, never()).finish(); + } + + @Test + public void popSystemNavigatorFlutterFragment() { + FragmentActivity mockFragmentActivity = mock(FragmentActivity.class); + OnBackPressedDispatcher onBackPressedDispatcher = mock(OnBackPressedDispatcher.class); + when(mockFragmentActivity.getOnBackPressedDispatcher()).thenReturn(onBackPressedDispatcher); + PlatformChannel mockPlatformChannel = mock(PlatformChannel.class); + PlatformPluginDelegate mockPlatformPluginDelegate = mock(PlatformPluginDelegate.class); + when(mockPlatformPluginDelegate.popSystemNavigator()).thenReturn(false); + PlatformPlugin platformPlugin = + new PlatformPlugin(mockFragmentActivity, mockPlatformChannel, mockPlatformPluginDelegate); + + platformPlugin.mPlatformMessageHandler.popSystemNavigator(); + + verify(mockFragmentActivity, never()).finish(); + verify(mockPlatformPluginDelegate, times(1)).popSystemNavigator(); + verify(mockFragmentActivity, times(1)).getOnBackPressedDispatcher(); + verify(onBackPressedDispatcher, times(1)).onBackPressed(); + } + + @Test + public void doesNotDoAnythingByDefaultIfFragmentPopSystemNavigatorOverridden() { + FragmentActivity mockFragmentActivity = mock(FragmentActivity.class); + OnBackPressedDispatcher onBackPressedDispatcher = mock(OnBackPressedDispatcher.class); + when(mockFragmentActivity.getOnBackPressedDispatcher()).thenReturn(onBackPressedDispatcher); + PlatformChannel mockPlatformChannel = mock(PlatformChannel.class); + PlatformPluginDelegate mockPlatformPluginDelegate = mock(PlatformPluginDelegate.class); + when(mockPlatformPluginDelegate.popSystemNavigator()).thenReturn(true); + PlatformPlugin platformPlugin = + new PlatformPlugin(mockFragmentActivity, mockPlatformChannel, mockPlatformPluginDelegate); + + platformPlugin.mPlatformMessageHandler.popSystemNavigator(); + + verify(mockPlatformPluginDelegate, times(1)).popSystemNavigator(); + // No longer perform the default action when overridden. + verify(mockFragmentActivity, never()).finish(); + verify(mockFragmentActivity, never()).getOnBackPressedDispatcher(); + } + + @Test + public void setRequestedOrientationFlutterFragment() { + FragmentActivity mockFragmentActivity = mock(FragmentActivity.class); + PlatformChannel mockPlatformChannel = mock(PlatformChannel.class); + PlatformPluginDelegate mockPlatformPluginDelegate = mock(PlatformPluginDelegate.class); + when(mockPlatformPluginDelegate.popSystemNavigator()).thenReturn(false); + PlatformPlugin platformPlugin = + new PlatformPlugin(mockFragmentActivity, mockPlatformChannel, mockPlatformPluginDelegate); + + platformPlugin.mPlatformMessageHandler.setPreferredOrientations(0); + + verify(mockFragmentActivity, times(1)).setRequestedOrientation(0); + } }