Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Android] Save back handling state in Activity/Fragment bundle #56715

Open
wants to merge 8 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -634,6 +634,12 @@ protected void onCreate(@Nullable Bundle savedInstanceState) {

super.onCreate(savedInstanceState);

if (savedInstanceState != null
&& savedInstanceState.getBoolean(
FlutterActivityAndFragmentDelegate.ON_BACK_CALLBACK_ENABLED_KEY)) {
setFrameworkHandlesBack(true);
}

delegate = new FlutterActivityAndFragmentDelegate(this);
delegate.onAttach(this);
delegate.onRestoreInstanceState(savedInstanceState);
Expand Down Expand Up @@ -1477,6 +1483,11 @@ public boolean attachToEngineAutomatically() {
return true;
}

@Override
public boolean getBackCallbackState() {
return hasRegisteredBackCallback;
}

@Override
public boolean popSystemNavigator() {
// Hook for subclass. No-op if returns false.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@
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. */
Expand Down Expand Up @@ -691,6 +692,12 @@ void onSaveInstanceState(@Nullable Bundle bundle) {
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
Expand Down Expand Up @@ -1297,5 +1304,7 @@ PlatformPlugin providePlatformPlugin(
* <p>Defaults to {@code true}.
*/
boolean attachToEngineAutomatically();

boolean getBackCallbackState();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -1009,7 +1009,8 @@ public FlutterActivityAndFragmentDelegate createDelegate(
return new FlutterActivityAndFragmentDelegate(host);
}

private final OnBackPressedCallback onBackPressedCallback =
@VisibleForTesting
final OnBackPressedCallback onBackPressedCallback =
new OnBackPressedCallback(true) {
@Override
public void handleOnBackPressed() {
Expand Down Expand Up @@ -1071,6 +1072,11 @@ public void onAttach(@NonNull Context context) {
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if (savedInstanceState != null
&& savedInstanceState.getBoolean(
FlutterActivityAndFragmentDelegate.ON_BACK_CALLBACK_ENABLED_KEY)) {
onBackPressedCallback.setEnabled(true);
}
delegate.onRestoreInstanceState(savedInstanceState);
}

Expand Down Expand Up @@ -1655,6 +1661,11 @@ public boolean attachToEngineAutomatically() {
return true;
}

@Override
public boolean getBackCallbackState() {
return onBackPressedCallback.isEnabled();
}

/**
* {@inheritDoc}
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -34,6 +35,7 @@
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;
Expand Down Expand Up @@ -94,6 +96,48 @@ public void flutterViewHasId() {
assertTrue(activity.findViewById(FlutterActivity.FLUTTER_VIEW_ID) instanceof FlutterView);
}

@Test
@Config(sdk = 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(sdk = 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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -401,6 +401,11 @@ public boolean attachToEngineAutomatically() {
return true;
}

@Override
public boolean getBackCallbackState() {
return false;
}

@Override
public void onFlutterSurfaceViewCreated(@NonNull FlutterSurfaceView flutterSurfaceView) {}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -13,6 +15,7 @@
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;
Expand All @@ -25,9 +28,11 @@
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;
Expand Down Expand Up @@ -254,6 +259,63 @@ public void itHandlesNewFragmentRecreationDuringRestoreWhenActivityIsRecreated()
assertEquals(0, activity.numberOfEnginesCreated);
}

@Test
@Config(sdk = 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(sdk = 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;

Expand Down