[Android] Save back handling state in Activity/Fragment bundle (flutter/engine#56715)

Fixes https://github.com/flutter/flutter/issues/159158, and fixes b/355556397

[C++, Objective-C, Java style guides]: https://github.com/flutter/engine/blob/main/CONTRIBUTING.md#style
This commit is contained in:
Gray Mackall 2024-12-03 17:07:27 -08:00 committed by GitHub
parent dcc250c8a5
commit ec896de5c4
6 changed files with 146 additions and 2 deletions

View File

@ -212,7 +212,7 @@ public class FlutterActivity extends Activity
implements FlutterActivityAndFragmentDelegate.Host, LifecycleOwner {
private static final String TAG = "FlutterActivity";
private boolean hasRegisteredBackCallback = false;
@VisibleForTesting boolean hasRegisteredBackCallback = false;
/**
* The ID of the {@code FlutterView} created by this activity.
@ -634,6 +634,13 @@ public class FlutterActivity extends Activity
super.onCreate(savedInstanceState);
if (savedInstanceState != null) {
boolean frameworkHandlesBack =
savedInstanceState.getBoolean(
FlutterActivityAndFragmentDelegate.ON_BACK_CALLBACK_ENABLED_KEY);
setFrameworkHandlesBack(frameworkHandlesBack);
}
delegate = new FlutterActivityAndFragmentDelegate(this);
delegate.onAttach(this);
delegate.onRestoreInstanceState(savedInstanceState);
@ -1477,6 +1484,11 @@ public class FlutterActivity extends Activity
return true;
}
@Override
public boolean getBackCallbackState() {
return hasRegisteredBackCallback;
}
@Override
public boolean popSystemNavigator() {
// Hook for subclass. No-op if returns false.

View File

@ -76,6 +76,7 @@ import java.util.List;
private static final String TAG = "FlutterActivityAndFragmentDelegate";
private static final String FRAMEWORK_RESTORATION_BUNDLE_KEY = "framework";
private static final String PLUGINS_RESTORATION_BUNDLE_KEY = "plugins";
static final String ON_BACK_CALLBACK_ENABLED_KEY = "enableOnBackInvokedCallbackState";
private static final int FLUTTER_SPLASH_VIEW_FALLBACK_ID = 486947586;
/** Factory to obtain a FlutterActivityAndFragmentDelegate instance. */
@ -691,6 +692,12 @@ import java.util.List;
flutterEngine.getActivityControlSurface().onSaveInstanceState(plugins);
bundle.putBundle(PLUGINS_RESTORATION_BUNDLE_KEY, plugins);
}
// If using a cached engine, we need to save whether the framework or the system should handle
// backs.
if (host.getCachedEngineId() != null && !host.shouldDestroyEngineWithHost()) {
bundle.putBoolean(ON_BACK_CALLBACK_ENABLED_KEY, host.getBackCallbackState());
}
}
@Override
@ -1297,5 +1304,7 @@ import java.util.List;
* <p>Defaults to {@code true}.
*/
boolean attachToEngineAutomatically();
boolean getBackCallbackState();
}
}

View File

@ -1009,7 +1009,8 @@ public class FlutterFragment extends Fragment
return new FlutterActivityAndFragmentDelegate(host);
}
private final OnBackPressedCallback onBackPressedCallback =
@VisibleForTesting
final OnBackPressedCallback onBackPressedCallback =
new OnBackPressedCallback(true) {
@Override
public void handleOnBackPressed() {
@ -1071,6 +1072,12 @@ public class FlutterFragment extends Fragment
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if (savedInstanceState != null) {
boolean frameworkHandlesBack =
savedInstanceState.getBoolean(
FlutterActivityAndFragmentDelegate.ON_BACK_CALLBACK_ENABLED_KEY);
onBackPressedCallback.setEnabled(frameworkHandlesBack);
}
delegate.onRestoreInstanceState(savedInstanceState);
}
@ -1655,6 +1662,11 @@ public class FlutterFragment extends Fragment
return true;
}
@Override
public boolean getBackCallbackState() {
return onBackPressedCallback.isEnabled();
}
/**
* {@inheritDoc}
*

View File

@ -5,6 +5,7 @@
package io.flutter.embedding.android;
import static io.flutter.Build.API_LEVELS;
import static io.flutter.embedding.android.FlutterActivityLaunchConfigs.EXTRA_CACHED_ENGINE_ID;
import static io.flutter.embedding.android.FlutterActivityLaunchConfigs.HANDLE_DEEPLINKING_META_DATA_KEY;
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
@ -34,6 +35,7 @@ import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.lifecycle.DefaultLifecycleObserver;
import androidx.lifecycle.LifecycleOwner;
import androidx.test.core.app.ActivityScenario;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import io.flutter.FlutterInjector;
@ -94,6 +96,48 @@ public class FlutterActivityTest {
assertTrue(activity.findViewById(FlutterActivity.FLUTTER_VIEW_ID) instanceof FlutterView);
}
@Test
@Config(minSdk = API_LEVELS.API_34)
@TargetApi(API_LEVELS.API_34)
public void whenUsingCachedEngine_predictiveBackStateIsSaved() {
FlutterLoader mockFlutterLoader = mock(FlutterLoader.class);
FlutterJNI mockFlutterJni = mock(FlutterJNI.class);
when(mockFlutterJni.isAttached()).thenReturn(true);
FlutterEngine cachedEngine = new FlutterEngine(ctx, mockFlutterLoader, mockFlutterJni);
FlutterEngineCache.getInstance().put("my_cached_engine", cachedEngine);
ActivityScenario<FlutterActivity> flutterActivityScenario =
ActivityScenario.launch(FlutterActivity.class);
// Set to framework handling and then recreate the activity and check the state is preserved.
flutterActivityScenario.onActivity(activity -> activity.setFrameworkHandlesBack(true));
flutterActivityScenario.onActivity(
activity -> activity.getIntent().putExtra(EXTRA_CACHED_ENGINE_ID, "my_cached_engine"));
flutterActivityScenario.recreate();
flutterActivityScenario.onActivity(activity -> assertTrue(activity.hasRegisteredBackCallback));
// Clean up.
flutterActivityScenario.close();
}
@Test
@Config(minSdk = API_LEVELS.API_34)
@TargetApi(API_LEVELS.API_34)
public void whenNotUsingCachedEngine_predictiveBackStateIsNotSaved() {
ActivityScenario<FlutterActivity> flutterActivityScenario =
ActivityScenario.launch(FlutterActivity.class);
// Set to framework handling and then recreate the activity and check the state is preserved.
flutterActivityScenario.onActivity(activity -> activity.setFrameworkHandlesBack(true));
flutterActivityScenario.recreate();
flutterActivityScenario.onActivity(activity -> assertFalse(activity.hasRegisteredBackCallback));
// Clean up.
flutterActivityScenario.close();
}
// TODO(garyq): Robolectric does not yet support android api 33 yet. Switch to a robolectric
// test that directly exercises the OnBackInvoked APIs when API 33 is supported.
@Test

View File

@ -401,6 +401,11 @@ public class FlutterAndroidComponentTest {
return true;
}
@Override
public boolean getBackCallbackState() {
return false;
}
@Override
public void onFlutterSurfaceViewCreated(@NonNull FlutterSurfaceView flutterSurfaceView) {}

View File

@ -5,6 +5,8 @@
package io.flutter.embedding.android;
import static io.flutter.embedding.android.FlutterActivityLaunchConfigs.HANDLE_DEEPLINKING_META_DATA_KEY;
import static io.flutter.embedding.android.FlutterFragment.ARG_CACHED_ENGINE_ID;
import static io.flutter.embedding.android.FlutterFragment.ARG_DESTROY_ENGINE_WITH_FRAGMENT;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
@ -13,6 +15,7 @@ import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.when;
import android.annotation.TargetApi;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
@ -25,9 +28,11 @@ import androidx.annotation.Nullable;
import androidx.test.core.app.ActivityScenario;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import io.flutter.Build;
import io.flutter.FlutterInjector;
import io.flutter.embedding.android.FlutterActivityLaunchConfigs.BackgroundMode;
import io.flutter.embedding.engine.FlutterEngine;
import io.flutter.embedding.engine.FlutterEngineCache;
import io.flutter.embedding.engine.FlutterJNI;
import io.flutter.embedding.engine.loader.FlutterLoader;
import io.flutter.plugins.GeneratedPluginRegistrant;
@ -254,6 +259,63 @@ public class FlutterFragmentActivityTest {
assertEquals(0, activity.numberOfEnginesCreated);
}
@Test
@Config(minSdk = Build.API_LEVELS.API_34)
@TargetApi(Build.API_LEVELS.API_34)
public void whenUsingCachedEngine_predictiveBackStateIsSaved() {
FlutterLoader mockFlutterLoader = mock(FlutterLoader.class);
FlutterJNI mockFlutterJni = mock(FlutterJNI.class);
when(mockFlutterJni.isAttached()).thenReturn(true);
FlutterEngine cachedEngine = new FlutterEngine(ctx, mockFlutterLoader, mockFlutterJni);
FlutterEngineCache.getInstance().put("my_cached_engine", cachedEngine);
ActivityScenario<FlutterFragmentActivity> flutterFragmentActivityActivityScenario =
ActivityScenario.launch(FlutterFragmentActivity.class);
// Set to framework handling and then recreate the activity and check the state is preserved.
flutterFragmentActivityActivityScenario.onActivity(
activity -> {
FlutterFragment flutterFragment = activity.retrieveExistingFlutterFragmentIfPossible();
flutterFragment.setFrameworkHandlesBack(true);
Bundle bundle = flutterFragment.getArguments();
bundle.putString(ARG_CACHED_ENGINE_ID, "my_cached_engine");
bundle.putBoolean(ARG_DESTROY_ENGINE_WITH_FRAGMENT, false);
FlutterEngineCache.getInstance().put("my_cached_engine", cachedEngine);
flutterFragment.setArguments(bundle);
});
flutterFragmentActivityActivityScenario.recreate();
flutterFragmentActivityActivityScenario.onActivity(
activity -> {
assertTrue(
activity
.retrieveExistingFlutterFragmentIfPossible()
.onBackPressedCallback
.isEnabled());
});
// Clean up.
flutterFragmentActivityActivityScenario.close();
}
@Test
@Config(minSdk = Build.API_LEVELS.API_34)
@TargetApi(Build.API_LEVELS.API_34)
public void whenNotUsingCachedEngine_predictiveBackStateIsNotSaved() {
ActivityScenario<FlutterActivity> flutterActivityScenario =
ActivityScenario.launch(FlutterActivity.class);
// Set to framework handling and then recreate the activity and check the state is preserved.
flutterActivityScenario.onActivity(activity -> activity.setFrameworkHandlesBack(true));
flutterActivityScenario.recreate();
flutterActivityScenario.onActivity(activity -> assertFalse(activity.hasRegisteredBackCallback));
// Clean up.
flutterActivityScenario.close();
}
static class FlutterFragmentActivityWithProvidedEngine extends FlutterFragmentActivity {
int numberOfEnginesCreated = 0;