diff --git a/shell/platform/android/io/flutter/embedding/android/FlutterActivity.java b/shell/platform/android/io/flutter/embedding/android/FlutterActivity.java index 5f5ae30566d12c7c21c0ec11891d92af8ba658a5..a99f86a0777ce014acbdc22b2e086f132cb4b3f8 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 @@ public class FlutterActivity extends Activity @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 class FlutterActivity extends Activity 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 4495ac7a09fe8bb5dc9d3772269f80aa762bafe8..a7be71dce0789edd2ca802441dae3408e90b445f 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.annotation.VisibleForTesting; 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 @@ import java.util.Arrays; * 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 a1e20defb7577a2c2a9cad01428cb5148f95f9eb..ba464084800dcf023aecfc656391edf419f7db18 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 @@ public class FlutterFragment extends Fragment implements FlutterActivityAndFragm 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 class FlutterFragment extends Fragment implements FlutterActivityAndFragm 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 class FlutterFragment extends Fragment implements FlutterActivityAndFragm 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 bd53c2e87c1688f30c667ab04bd399796f561fdf..5dba56b4fc9200bc3b123072c7930a4d067ff177 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.SoundEffectConstants; 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 class PlatformPlugin { }; 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 @@ public class PlatformPlugin { 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 @@ public class PlatformPlugin { 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 @@ public class PlatformPlugin { 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 @@ public class PlatformPlugin { } 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 d5d618f3ff0a90c5f604bc6719853bc49d052dc6..b4de9f1ea71b75945f4d89d3c8d5471a887310e4 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 class FlutterAndroidComponentTest { @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 4d8cf0c55cb4b6821e87bb3493cfb632f088e5a7..fd67f7e38fa4ee9e443e43c72d2ba03fb033c77d 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.assertNotNull; 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.net.Uri; 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 class PlatformPluginTest { 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); + } }