Skip to content

Commit a35e3fe

Browse files
authored
Let FlutterFragment not pop the whole activity by default when more fragments are in the activity (flutter#22692)
1 parent 81af789 commit a35e3fe

File tree

6 files changed

+159
-15
lines changed

6 files changed

+159
-15
lines changed

shell/platform/android/io/flutter/embedding/android/FlutterActivity.java

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -902,11 +902,7 @@ protected Bundle getMetaData() throws PackageManager.NameNotFoundException {
902902
@Override
903903
public PlatformPlugin providePlatformPlugin(
904904
@Nullable Activity activity, @NonNull FlutterEngine flutterEngine) {
905-
if (activity != null) {
906-
return new PlatformPlugin(getActivity(), flutterEngine.getPlatformChannel());
907-
} else {
908-
return null;
909-
}
905+
return new PlatformPlugin(getActivity(), flutterEngine.getPlatformChannel(), this);
910906
}
911907

912908
/**
@@ -1032,6 +1028,12 @@ public boolean shouldRestoreAndSaveState() {
10321028
return true;
10331029
}
10341030

1031+
@Override
1032+
public boolean popSystemNavigator() {
1033+
// Hook for subclass. No-op if returns false.
1034+
return false;
1035+
}
1036+
10351037
private boolean stillAttachedForEvent(String event) {
10361038
if (delegate == null) {
10371039
Log.v(TAG, "FlutterActivity " + hashCode() + " " + event + " called after release.");

shell/platform/android/io/flutter/embedding/android/FlutterActivityAndFragmentDelegate.java

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@
2222
import androidx.lifecycle.Lifecycle;
2323
import io.flutter.FlutterInjector;
2424
import io.flutter.Log;
25-
import io.flutter.app.FlutterActivity;
2625
import io.flutter.embedding.engine.FlutterEngine;
2726
import io.flutter.embedding.engine.FlutterEngineCache;
2827
import io.flutter.embedding.engine.FlutterShellArgs;
@@ -752,7 +751,10 @@ private void ensureAlive() {
752751
* FlutterActivityAndFragmentDelegate}.
753752
*/
754753
/* package */ interface Host
755-
extends SplashScreenProvider, FlutterEngineProvider, FlutterEngineConfigurator {
754+
extends SplashScreenProvider,
755+
FlutterEngineProvider,
756+
FlutterEngineConfigurator,
757+
PlatformPlugin.PlatformPluginDelegate {
756758
/** Returns the {@link Context} that backs the host {@link Activity} or {@code Fragment}. */
757759
@NonNull
758760
Context getContext();

shell/platform/android/io/flutter/embedding/android/FlutterFragment.java

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -432,7 +432,7 @@ private CachedEngineFragmentBuilder(@NonNull String engineId) {
432432
this(FlutterFragment.class, engineId);
433433
}
434434

435-
protected CachedEngineFragmentBuilder(
435+
public CachedEngineFragmentBuilder(
436436
@NonNull Class<? extends FlutterFragment> subclass, @NonNull String engineId) {
437437
this.fragmentClass = subclass;
438438
this.engineId = engineId;
@@ -984,7 +984,7 @@ public FlutterEngine getFlutterEngine() {
984984
public PlatformPlugin providePlatformPlugin(
985985
@Nullable Activity activity, @NonNull FlutterEngine flutterEngine) {
986986
if (activity != null) {
987-
return new PlatformPlugin(getActivity(), flutterEngine.getPlatformChannel());
987+
return new PlatformPlugin(getActivity(), flutterEngine.getPlatformChannel(), this);
988988
} else {
989989
return null;
990990
}
@@ -1110,6 +1110,12 @@ public boolean shouldRestoreAndSaveState() {
11101110
return true;
11111111
}
11121112

1113+
@Override
1114+
public boolean popSystemNavigator() {
1115+
// Hook for subclass. No-op if returns false.
1116+
return false;
1117+
}
1118+
11131119
private boolean stillAttachedForEvent(String event) {
11141120
if (delegate == null) {
11151121
Log.v(TAG, "FlutterFragment " + hashCode() + " " + event + " called after release.");

shell/platform/android/io/flutter/plugin/platform/PlatformPlugin.java

Lines changed: 46 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
import android.view.View;
1616
import android.view.Window;
1717
import android.view.WindowManager;
18+
import androidx.activity.OnBackPressedDispatcherOwner;
1819
import androidx.annotation.NonNull;
1920
import androidx.annotation.Nullable;
2021
import androidx.annotation.VisibleForTesting;
@@ -30,10 +31,30 @@ public class PlatformPlugin {
3031

3132
private final Activity activity;
3233
private final PlatformChannel platformChannel;
34+
private final PlatformPluginDelegate platformPluginDelegate;
3335
private PlatformChannel.SystemChromeStyle currentTheme;
3436
private int mEnabledOverlays;
3537
private static final String TAG = "PlatformPlugin";
3638

39+
/**
40+
* The {@link PlatformPlugin} generally has default behaviors implemented for platform
41+
* functionalities requested by the Flutter framework. However, functionalities exposed through
42+
* this interface could be customized by the more public-facing APIs that implement this interface
43+
* such as the {@link io.flutter.embedding.android.FlutterActivity} or the {@link
44+
* io.flutter.embedding.android.FlutterFragment}.
45+
*/
46+
public interface PlatformPluginDelegate {
47+
/**
48+
* Allow implementer to customize the behavior needed when the Flutter framework calls to pop
49+
* the Android-side navigation stack.
50+
*
51+
* @return true if the implementation consumed the pop signal. If false, a default behavior of
52+
* finishing the activity or sending the signal to {@link
53+
* androidx.activity.OnBackPressedDispatcher} will be executed.
54+
*/
55+
boolean popSystemNavigator();
56+
}
57+
3758
@VisibleForTesting
3859
final PlatformChannel.PlatformMessageHandler mPlatformMessageHandler =
3960
new PlatformChannel.PlatformMessageHandler() {
@@ -101,9 +122,15 @@ public boolean clipboardHasStrings() {
101122
};
102123

103124
public PlatformPlugin(Activity activity, PlatformChannel platformChannel) {
125+
this(activity, platformChannel, null);
126+
}
127+
128+
public PlatformPlugin(
129+
Activity activity, PlatformChannel platformChannel, PlatformPluginDelegate delegate) {
104130
this.activity = activity;
105131
this.platformChannel = platformChannel;
106132
this.platformChannel.setPlatformMessageHandler(mPlatformMessageHandler);
133+
this.platformPluginDelegate = delegate;
107134

108135
mEnabledOverlays = DEFAULT_SYSTEM_UI;
109136
}
@@ -161,13 +188,14 @@ private void setSystemChromeApplicationSwitcherDescription(
161188
return;
162189
}
163190

164-
// Linter refuses to believe we're only executing this code in API 28 unless we use distinct if
191+
// Linter refuses to believe we're only executing this code in API 28 unless we
192+
// use distinct if
165193
// blocks and
166194
// hardcode the API 28 constant.
167195
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P
168196
&& Build.VERSION.SDK_INT > Build.VERSION_CODES.LOLLIPOP) {
169197
activity.setTaskDescription(
170-
new TaskDescription(description.label, /*icon=*/ null, description.color));
198+
new TaskDescription(description.label, /* icon= */ null, description.color));
171199
}
172200
if (Build.VERSION.SDK_INT >= 28) {
173201
TaskDescription taskDescription =
@@ -178,14 +206,16 @@ private void setSystemChromeApplicationSwitcherDescription(
178206

179207
private void setSystemChromeEnabledSystemUIOverlays(
180208
List<PlatformChannel.SystemUiOverlay> overlaysToShow) {
181-
// Start by assuming we want to hide all system overlays (like an immersive game).
209+
// Start by assuming we want to hide all system overlays (like an immersive
210+
// game).
182211
int enabledOverlays =
183212
DEFAULT_SYSTEM_UI
184213
| View.SYSTEM_UI_FLAG_FULLSCREEN
185214
| View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
186215
| View.SYSTEM_UI_FLAG_HIDE_NAVIGATION;
187216

188-
// The SYSTEM_UI_FLAG_IMMERSIVE_STICKY flag was introduced in API 19, so we apply it
217+
// The SYSTEM_UI_FLAG_IMMERSIVE_STICKY flag was introduced in API 19, so we
218+
// apply it
189219
// if desired, and if the current Android version is 19 or greater.
190220
if (overlaysToShow.size() == 0 && Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
191221
enabledOverlays |= View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY;
@@ -233,7 +263,8 @@ private void setSystemChromeSystemUIOverlayStyle(
233263
View view = window.getDecorView();
234264
int flags = view.getSystemUiVisibility();
235265
// You can change the navigation bar color (including translucent colors)
236-
// in Android, but you can't change the color of the navigation buttons until Android O.
266+
// in Android, but you can't change the color of the navigation buttons until
267+
// Android O.
237268
// LIGHT vs DARK effectively isn't supported until then.
238269
// Build.VERSION_CODES.O
239270
if (Build.VERSION.SDK_INT >= 26) {
@@ -279,7 +310,16 @@ private void setSystemChromeSystemUIOverlayStyle(
279310
}
280311

281312
private void popSystemNavigator() {
282-
activity.finish();
313+
if (platformPluginDelegate.popSystemNavigator()) {
314+
// A custom behavior was executed by the delegate. Don't execute default behavior.
315+
return;
316+
}
317+
318+
if (activity instanceof OnBackPressedDispatcherOwner) {
319+
((OnBackPressedDispatcherOwner) activity).getOnBackPressedDispatcher().onBackPressed();
320+
} else {
321+
activity.finish();
322+
}
283323
}
284324

285325
private CharSequence getClipboardData(PlatformChannel.ClipboardContentFormat format) {

shell/platform/android/test/io/flutter/embedding/android/FlutterAndroidComponentTest.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -382,5 +382,10 @@ public void onFlutterUiNoLongerDisplayed() {}
382382

383383
@Override
384384
public void detachFromFlutterEngine() {}
385+
386+
@Override
387+
public boolean popSystemNavigator() {
388+
return false;
389+
}
385390
}
386391
}

shell/platform/android/test/io/flutter/plugin/platform/PlatformPluginTest.java

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@
66
import static org.junit.Assert.assertNull;
77
import static org.junit.Assert.assertTrue;
88
import static org.mockito.Mockito.mock;
9+
import static org.mockito.Mockito.never;
10+
import static org.mockito.Mockito.times;
11+
import static org.mockito.Mockito.verify;
912
import static org.mockito.Mockito.when;
1013

1114
import android.app.Activity;
@@ -18,9 +21,12 @@
1821
import android.os.Build;
1922
import android.view.View;
2023
import android.view.Window;
24+
import androidx.activity.OnBackPressedDispatcher;
25+
import androidx.fragment.app.FragmentActivity;
2126
import io.flutter.embedding.engine.systemchannels.PlatformChannel;
2227
import io.flutter.embedding.engine.systemchannels.PlatformChannel.ClipboardContentFormat;
2328
import io.flutter.embedding.engine.systemchannels.PlatformChannel.SystemChromeStyle;
29+
import io.flutter.plugin.platform.PlatformPlugin.PlatformPluginDelegate;
2430
import java.io.ByteArrayInputStream;
2531
import java.io.IOException;
2632
import java.io.InputStream;
@@ -133,4 +139,87 @@ public void setNavigationBarDividerColor() {
133139
assertEquals(0XFF000000, fakeActivity.getWindow().getNavigationBarColor());
134140
}
135141
}
142+
143+
@Test
144+
public void popSystemNavigatorFlutterActivity() {
145+
Activity mockActivity = mock(Activity.class);
146+
PlatformChannel mockPlatformChannel = mock(PlatformChannel.class);
147+
PlatformPluginDelegate mockPlatformPluginDelegate = mock(PlatformPluginDelegate.class);
148+
when(mockPlatformPluginDelegate.popSystemNavigator()).thenReturn(false);
149+
PlatformPlugin platformPlugin =
150+
new PlatformPlugin(mockActivity, mockPlatformChannel, mockPlatformPluginDelegate);
151+
152+
platformPlugin.mPlatformMessageHandler.popSystemNavigator();
153+
154+
verify(mockPlatformPluginDelegate, times(1)).popSystemNavigator();
155+
verify(mockActivity, times(1)).finish();
156+
}
157+
158+
@Test
159+
public void doesNotDoAnythingByDefaultIfPopSystemNavigatorOverridden() {
160+
Activity mockActivity = mock(Activity.class);
161+
PlatformChannel mockPlatformChannel = mock(PlatformChannel.class);
162+
PlatformPluginDelegate mockPlatformPluginDelegate = mock(PlatformPluginDelegate.class);
163+
when(mockPlatformPluginDelegate.popSystemNavigator()).thenReturn(true);
164+
PlatformPlugin platformPlugin =
165+
new PlatformPlugin(mockActivity, mockPlatformChannel, mockPlatformPluginDelegate);
166+
167+
platformPlugin.mPlatformMessageHandler.popSystemNavigator();
168+
169+
verify(mockPlatformPluginDelegate, times(1)).popSystemNavigator();
170+
// No longer perform the default action when overridden.
171+
verify(mockActivity, never()).finish();
172+
}
173+
174+
@Test
175+
public void popSystemNavigatorFlutterFragment() {
176+
FragmentActivity mockFragmentActivity = mock(FragmentActivity.class);
177+
OnBackPressedDispatcher onBackPressedDispatcher = mock(OnBackPressedDispatcher.class);
178+
when(mockFragmentActivity.getOnBackPressedDispatcher()).thenReturn(onBackPressedDispatcher);
179+
PlatformChannel mockPlatformChannel = mock(PlatformChannel.class);
180+
PlatformPluginDelegate mockPlatformPluginDelegate = mock(PlatformPluginDelegate.class);
181+
when(mockPlatformPluginDelegate.popSystemNavigator()).thenReturn(false);
182+
PlatformPlugin platformPlugin =
183+
new PlatformPlugin(mockFragmentActivity, mockPlatformChannel, mockPlatformPluginDelegate);
184+
185+
platformPlugin.mPlatformMessageHandler.popSystemNavigator();
186+
187+
verify(mockFragmentActivity, never()).finish();
188+
verify(mockPlatformPluginDelegate, times(1)).popSystemNavigator();
189+
verify(mockFragmentActivity, times(1)).getOnBackPressedDispatcher();
190+
verify(onBackPressedDispatcher, times(1)).onBackPressed();
191+
}
192+
193+
@Test
194+
public void doesNotDoAnythingByDefaultIfFragmentPopSystemNavigatorOverridden() {
195+
FragmentActivity mockFragmentActivity = mock(FragmentActivity.class);
196+
OnBackPressedDispatcher onBackPressedDispatcher = mock(OnBackPressedDispatcher.class);
197+
when(mockFragmentActivity.getOnBackPressedDispatcher()).thenReturn(onBackPressedDispatcher);
198+
PlatformChannel mockPlatformChannel = mock(PlatformChannel.class);
199+
PlatformPluginDelegate mockPlatformPluginDelegate = mock(PlatformPluginDelegate.class);
200+
when(mockPlatformPluginDelegate.popSystemNavigator()).thenReturn(true);
201+
PlatformPlugin platformPlugin =
202+
new PlatformPlugin(mockFragmentActivity, mockPlatformChannel, mockPlatformPluginDelegate);
203+
204+
platformPlugin.mPlatformMessageHandler.popSystemNavigator();
205+
206+
verify(mockPlatformPluginDelegate, times(1)).popSystemNavigator();
207+
// No longer perform the default action when overridden.
208+
verify(mockFragmentActivity, never()).finish();
209+
verify(mockFragmentActivity, never()).getOnBackPressedDispatcher();
210+
}
211+
212+
@Test
213+
public void setRequestedOrientationFlutterFragment() {
214+
FragmentActivity mockFragmentActivity = mock(FragmentActivity.class);
215+
PlatformChannel mockPlatformChannel = mock(PlatformChannel.class);
216+
PlatformPluginDelegate mockPlatformPluginDelegate = mock(PlatformPluginDelegate.class);
217+
when(mockPlatformPluginDelegate.popSystemNavigator()).thenReturn(false);
218+
PlatformPlugin platformPlugin =
219+
new PlatformPlugin(mockFragmentActivity, mockPlatformChannel, mockPlatformPluginDelegate);
220+
221+
platformPlugin.mPlatformMessageHandler.setPreferredOrientations(0);
222+
223+
verify(mockFragmentActivity, times(1)).setRequestedOrientation(0);
224+
}
136225
}

0 commit comments

Comments
 (0)