Skip to content

Commit

Permalink
Let FlutterFragment not pop the whole activity by default when more f…
Browse files Browse the repository at this point in the history
…ragments are in the activity (#22692)
  • Loading branch information
xster committed Nov 30, 2020
1 parent 81af789 commit a35e3fe
Show file tree
Hide file tree
Showing 6 changed files with 159 additions and 15 deletions.
Expand Up @@ -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);
}

/**
Expand Down Expand Up @@ -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.");
Expand Down
Expand Up @@ -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;
Expand Down Expand Up @@ -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();
Expand Down
Expand Up @@ -432,7 +432,7 @@ private CachedEngineFragmentBuilder(@NonNull String engineId) {
this(FlutterFragment.class, engineId);
}

protected CachedEngineFragmentBuilder(
public CachedEngineFragmentBuilder(
@NonNull Class<? extends FlutterFragment> subclass, @NonNull String engineId) {
this.fragmentClass = subclass;
this.engineId = engineId;
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -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.");
Expand Down
Expand Up @@ -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;
Expand All @@ -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() {
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -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 =
Expand All @@ -178,14 +206,16 @@ private void setSystemChromeApplicationSwitcherDescription(

private void setSystemChromeEnabledSystemUIOverlays(
List<PlatformChannel.SystemUiOverlay> 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;
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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) {
Expand Down
Expand Up @@ -382,5 +382,10 @@ public void onFlutterUiNoLongerDisplayed() {}

@Override
public void detachFromFlutterEngine() {}

@Override
public boolean popSystemNavigator() {
return false;
}
}
}
Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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);
}
}

0 comments on commit a35e3fe

Please sign in to comment.