Skip to content
Merged
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 @@ -4,18 +4,23 @@

package io.flutter.embedding.android;

import static io.flutter.Build.API_LEVELS;

import android.app.Activity;
import android.content.ComponentCallbacks2;
import android.content.Context;
import android.content.Intent;
import android.os.Build;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewTreeObserver.OnWindowFocusChangeListener;
import androidx.activity.BackEventCompat;
import androidx.activity.OnBackPressedCallback;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.annotation.VisibleForTesting;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentActivity;
Expand Down Expand Up @@ -1011,13 +1016,40 @@ public FlutterActivityAndFragmentDelegate createDelegate(
}

@VisibleForTesting
final OnBackPressedCallback onBackPressedCallback =
new OnBackPressedCallback(true) {
final OnBackPressedCallback onBackPressedCallback = createOnBackPressedCallback();

private OnBackPressedCallback createOnBackPressedCallback() {
if (Build.VERSION.SDK_INT >= API_LEVELS.API_34) {
return new OnBackPressedCallback(true) {
@Override
public void handleOnBackPressed() {
onBackPressed();
commitBackGesture();
}

@Override
public void handleOnBackCancelled() {
cancelBackGesture();
}

@Override
public void handleOnBackProgressed(@NonNull BackEventCompat backEvent) {
updateBackGestureProgress(backEvent);
}

@Override
public void handleOnBackStarted(@NonNull BackEventCompat backEvent) {
startBackGesture(backEvent);
}
};
}

return new OnBackPressedCallback(true) {
@Override
public void handleOnBackPressed() {
onBackPressed();
}
};
}

public FlutterFragment() {
// Ensure that we at least have an empty Bundle of arguments so that we don't
Expand Down Expand Up @@ -1240,6 +1272,34 @@ public void onBackPressed() {
}
}

@RequiresApi(API_LEVELS.API_34)
public void startBackGesture(@NonNull BackEventCompat backEvent) {
if (stillAttachedForEvent("startBackGesture")) {
delegate.startBackGesture(backEvent.toBackEvent());
}
}

@RequiresApi(API_LEVELS.API_34)
public void updateBackGestureProgress(@NonNull BackEventCompat backEvent) {
if (stillAttachedForEvent("updateBackGestureProgress")) {
delegate.updateBackGestureProgress(backEvent.toBackEvent());
}
}

@RequiresApi(API_LEVELS.API_34)
public void commitBackGesture() {
if (stillAttachedForEvent("commitBackGesture")) {
delegate.commitBackGesture();
}
}

@RequiresApi(API_LEVELS.API_34)
public void cancelBackGesture() {
if (stillAttachedForEvent("cancelBackGesture")) {
delegate.cancelBackGesture();
}
}

/**
* A result has been returned after an invocation of {@link
* Fragment#startActivityForResult(Intent, int)}.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,16 @@
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

import android.annotation.TargetApi;
import android.content.Context;
import androidx.activity.BackEventCompat;
import androidx.activity.OnBackPressedCallback;
import androidx.activity.OnBackPressedDispatcher;
import androidx.fragment.app.FragmentActivity;
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.embedding.engine.FlutterEngine;
import io.flutter.embedding.engine.FlutterEngineCache;
import io.flutter.embedding.engine.FlutterJNI;
Expand All @@ -34,6 +38,7 @@
import java.util.concurrent.atomic.AtomicBoolean;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.annotation.Config;

@RunWith(AndroidJUnit4.class)
public class FlutterFragmentTest {
Expand Down Expand Up @@ -286,7 +291,9 @@ public void itReturnsExclusiveAppComponent() {
}

@Test
public void itDelegatesOnBackPressedWithSetFrameworkHandlesBack() {
@Config(sdk = Build.API_LEVELS.API_33)
@TargetApi(Build.API_LEVELS.API_33)
public void itDelegatesOnBackPressedWithSetFrameworkHandlesBackForSdk33() {
// We need to mock FlutterJNI to avoid triggering native code.
FlutterJNI flutterJNI = mock(FlutterJNI.class);
when(flutterJNI.isAttached()).thenReturn(true);
Expand Down Expand Up @@ -334,6 +341,72 @@ public void itDelegatesOnBackPressedWithSetFrameworkHandlesBack() {
}
}

@Test
@Config(sdk = Build.API_LEVELS.API_34)
@TargetApi(Build.API_LEVELS.API_34)
public void itDelegatesOnBackPressedWithSetFrameworkHandlesBackForSdk34OrHigher() {
// We need to mock FlutterJNI to avoid triggering native code.
FlutterJNI flutterJNI = mock(FlutterJNI.class);
when(flutterJNI.isAttached()).thenReturn(true);

FlutterEngine flutterEngine =
new FlutterEngine(ctx, new FlutterLoader(), flutterJNI, null, false);
FlutterEngineCache.getInstance().put("my_cached_engine", flutterEngine);

FlutterFragment fragment =
FlutterFragment.withCachedEngine("my_cached_engine")
// This enables the use of onBackPressedCallback, which is what
// sends backs to the framework if setFrameworkHandlesBack is true.
.shouldAutomaticallyHandleOnBackPressed(true)
.build();

try (ActivityScenario<FragmentActivity> scenario =
ActivityScenario.launch(FragmentActivity.class)) {
scenario.onActivity(
activity -> {
activity
.getSupportFragmentManager()
.beginTransaction()
.add(android.R.id.content, fragment)
.commitNow();

FlutterActivityAndFragmentDelegate mockDelegate =
mock(FlutterActivityAndFragmentDelegate.class);
isDelegateAttached = true;
when(mockDelegate.isAttached()).thenAnswer(invocation -> isDelegateAttached);
doAnswer(invocation -> isDelegateAttached = false).when(mockDelegate).onDetach();
TestDelegateFactory delegateFactory = new TestDelegateFactory(mockDelegate);
fragment.setDelegateFactory(delegateFactory);

BackEventCompat mockBackEvent = mock(BackEventCompat.class);
OnBackPressedDispatcher dispatcher = activity.getOnBackPressedDispatcher();

// Back gesture events now will still be handled by Android (the default),
// until setFrameworkHandlesBack is set to true.
dispatcher.dispatchOnBackStarted(mockBackEvent);
dispatcher.dispatchOnBackProgressed(mockBackEvent);
dispatcher.onBackPressed();
dispatcher.dispatchOnBackCancelled();
verify(mockDelegate, times(0)).startBackGesture(any());
verify(mockDelegate, times(0)).updateBackGestureProgress(any());
verify(mockDelegate, times(0)).commitBackGesture();
verify(mockDelegate, times(0)).cancelBackGesture();

// Setting setFrameworkHandlesBack to true means the delegate will receive
// the back and Android won't handle it.
fragment.setFrameworkHandlesBack(true);
dispatcher.dispatchOnBackStarted(mockBackEvent);
dispatcher.dispatchOnBackProgressed(mockBackEvent);
dispatcher.onBackPressed();
dispatcher.dispatchOnBackCancelled();
verify(mockDelegate, times(1)).startBackGesture(any());
verify(mockDelegate, times(1)).updateBackGestureProgress(any());
verify(mockDelegate, times(1)).commitBackGesture();
verify(mockDelegate, times(1)).cancelBackGesture();
Comment on lines +402 to +405
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

This block of verification logic is very similar to the one on lines 390-393. To improve readability and reduce code duplication, consider extracting this logic into a private helper method.

For example, you could create a method like this:

private void verifyBackGestureCalls(FlutterActivityAndFragmentDelegate mockDelegate, org.mockito.verification.VerificationMode mode) {
  verify(mockDelegate, mode).startBackGesture(any());
  verify(mockDelegate, mode).updateBackGestureProgress(any());
  verify(mockDelegate, mode).commitBackGesture();
  verify(mockDelegate, mode).cancelBackGesture();
}

Then you could call it with times(0) and times(1) respectively.

});
}
}

@Test
public void itHandlesPopSystemNavigationAutomaticallyWhenEnabled() {
// We need to mock FlutterJNI to avoid triggering native code.
Expand Down