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

Reland: "Determine lifecycle by looking at window focus also" (#41094) #41702

Merged
merged 1 commit into from
May 3, 2023
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
56 changes: 43 additions & 13 deletions lib/ui/platform_dispatcher.dart
Original file line number Diff line number Diff line change
Expand Up @@ -1642,31 +1642,61 @@ class FrameTiming {
/// States that an application can be in.
///
/// The values below describe notifications from the operating system.
/// Applications should not expect to always receive all possible
/// notifications. For example, if the users pulls out the battery from the
/// device, no notification will be sent before the application is suddenly
/// terminated, along with the rest of the operating system.
/// Applications should not expect to always receive all possible notifications.
/// For example, if the users pulls out the battery from the device, no
/// notification will be sent before the application is suddenly terminated,
/// along with the rest of the operating system.
///
/// For historical and name collision reasons, Flutter's application state names
/// do not correspond one to one with the state names on all platforms. On
/// Android, for instance, when the OS calls
/// [`Activity.onPause`](https://developer.android.com/reference/android/app/Activity#onPause()),
/// Flutter will enter the [inactive] state, but when Android calls
/// [`Activity.onStop`](https://developer.android.com/reference/android/app/Activity#onStop()),
/// Flutter enters the [paused] state. See the individual state's documentation
/// for descriptions of what they mean on each platform.
///
/// See also:
///
/// * [WidgetsBindingObserver], for a mechanism to observe the lifecycle state
/// from the widgets layer.
/// * [WidgetsBindingObserver], for a mechanism to observe the lifecycle state
/// from the widgets layer.
/// * iOS's [IOKit activity lifecycle](https://developer.apple.com/documentation/uikit/app_and_environment/managing_your_app_s_life_cycle?language=objc) documentation.
/// * Android's [activity lifecycle](https://developer.android.com/guide/components/activities/activity-lifecycle) documentation.
/// * macOS's [AppKit activity lifecycle](https://developer.apple.com/documentation/appkit/nsapplicationdelegate?language=objc) documentation.
enum AppLifecycleState {
/// The application is visible and responding to user input.
/// The application is visible and responsive to user input.
///
/// On Android, this state corresponds to the Flutter host view having focus
/// ([`Activity.onWindowFocusChanged`](https://developer.android.com/reference/android/app/Activity#onWindowFocusChanged(boolean))
/// was called with true) while in Android's "resumed" state. It is possible
/// for the Flutter app to be in the [inactive] state while still being in
/// Android's
/// ["onResume"](https://developer.android.com/guide/components/activities/activity-lifecycle)
/// state if the app has lost focus
/// ([`Activity.onWindowFocusChanged`](https://developer.android.com/reference/android/app/Activity#onWindowFocusChanged(boolean))
/// was called with false), but hasn't had
/// [`Activity.onPause`](https://developer.android.com/reference/android/app/Activity#onPause())
/// called on it.
resumed,

/// The application is in an inactive state and is not receiving user input.
///
/// On iOS, this state corresponds to an app or the Flutter host view running
/// in the foreground inactive state. Apps transition to this state when in
/// a phone call, responding to a TouchID request, when entering the app
/// in the foreground inactive state. Apps transition to this state when in a
/// phone call, responding to a TouchID request, when entering the app
/// switcher or the control center, or when the UIViewController hosting the
/// Flutter app is transitioning.
///
/// On Android, this corresponds to an app or the Flutter host view running
/// in the foreground inactive state. Apps transition to this state when
/// another activity is focused, such as a split-screen app, a phone call,
/// a picture-in-picture app, a system dialog, or another view.
/// On Android, this corresponds to an app or the Flutter host view running in
/// Android's paused state (i.e.
/// [`Activity.onPause`](https://developer.android.com/reference/android/app/Activity#onPause())
/// has been called), or in Android's "resumed" state (i.e.
/// [`Activity.onResume`](https://developer.android.com/reference/android/app/Activity#onResume())
/// has been called) but it has lost window focus. Examples of when apps
/// transition to this state include when the app is partially obscured or
/// another activity is focused, such as: a split-screen app, a phone call, a
/// picture-in-picture app, a system dialog, another view, when the
/// notification window shade is down, or the application switcher is visible.
///
/// Apps in this state should assume that they may be [paused] at any time.
inactive,
Expand Down
6 changes: 6 additions & 0 deletions shell/platform/android/io/flutter/app/FlutterActivity.java
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,12 @@ public void onUserLeaveHint() {
eventDelegate.onUserLeaveHint();
}

@Override
public void onWindowFocusChanged(boolean hasFocus) {
super.onWindowFocusChanged(hasFocus);
eventDelegate.onWindowFocusChanged(hasFocus);
}

@Override
public void onTrimMemory(int level) {
eventDelegate.onTrimMemory(level);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -260,6 +260,11 @@ public void onUserLeaveHint() {
flutterView.getPluginRegistry().onUserLeaveHint();
}

@Override
public void onWindowFocusChanged(boolean hasFocus) {
flutterView.getPluginRegistry().onWindowFocusChanged(hasFocus);
}

@Override
public void onTrimMemory(int level) {
// Use a trim level delivered while the application is running so the
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,4 +64,10 @@ public interface FlutterActivityEvents

/** @see android.app.Activity#onUserLeaveHint() */
void onUserLeaveHint();

/**
* @param hasFocus True if the current activity window has focus.
* @see android.app.Activity#onWindowFocusChanged(boolean)
*/
void onWindowFocusChanged(boolean hasFocus);
}
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,12 @@ public void onUserLeaveHint() {
eventDelegate.onUserLeaveHint();
}

@Override
public void onWindowFocusChanged(boolean hasFocus) {
super.onWindowFocusChanged(hasFocus);
eventDelegate.onWindowFocusChanged(hasFocus);
}

@Override
public void onTrimMemory(int level) {
super.onTrimMemory(level);
Expand Down
15 changes: 15 additions & 0 deletions shell/platform/android/io/flutter/app/FlutterPluginRegistry.java
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ public class FlutterPluginRegistry
PluginRegistry.RequestPermissionsResultListener,
PluginRegistry.ActivityResultListener,
PluginRegistry.NewIntentListener,
PluginRegistry.WindowFocusChangedListener,
PluginRegistry.UserLeaveHintListener,
PluginRegistry.ViewDestroyListener {
private static final String TAG = "FlutterPluginRegistry";
Expand All @@ -44,6 +45,7 @@ public class FlutterPluginRegistry
private final List<ActivityResultListener> mActivityResultListeners = new ArrayList<>(0);
private final List<NewIntentListener> mNewIntentListeners = new ArrayList<>(0);
private final List<UserLeaveHintListener> mUserLeaveHintListeners = new ArrayList<>(0);
private final List<WindowFocusChangedListener> mWindowFocusChangedListeners = new ArrayList<>(0);
private final List<ViewDestroyListener> mViewDestroyListeners = new ArrayList<>(0);

public FlutterPluginRegistry(FlutterNativeView nativeView, Context context) {
Expand Down Expand Up @@ -182,6 +184,12 @@ public Registrar addUserLeaveHintListener(UserLeaveHintListener listener) {
return this;
}

@Override
public Registrar addWindowFocusChangedListener(WindowFocusChangedListener listener) {
mWindowFocusChangedListeners.add(listener);
return this;
}

@Override
public Registrar addViewDestroyListener(ViewDestroyListener listener) {
mViewDestroyListeners.add(listener);
Expand Down Expand Up @@ -227,6 +235,13 @@ public void onUserLeaveHint() {
}
}

@Override
public void onWindowFocusChanged(boolean hasFocus) {
for (WindowFocusChangedListener listener : mWindowFocusChangedListeners) {
listener.onWindowFocusChanged(hasFocus);
}
}

@Override
public boolean onViewDestroy(FlutterNativeView view) {
boolean handled = false;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -949,6 +949,14 @@ public void onUserLeaveHint() {
}
}

@Override
public void onWindowFocusChanged(boolean hasFocus) {
super.onWindowFocusChanged(hasFocus);
if (stillAttachedForEvent("onWindowFocusChanged")) {
delegate.onWindowFocusChanged(hasFocus);
}
}

@Override
public void onTrimMemory(int level) {
super.onTrimMemory(level);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -581,7 +581,7 @@ public boolean onPreDraw() {
void onResume() {
Log.v(TAG, "onResume()");
ensureAlive();
if (host.shouldDispatchAppLifecycleState()) {
if (host.shouldDispatchAppLifecycleState() && flutterEngine != null) {
flutterEngine.getLifecycleChannel().appIsResumed();
}
}
Expand Down Expand Up @@ -629,7 +629,7 @@ void updateSystemUiOverlays() {
void onPause() {
Log.v(TAG, "onPause()");
ensureAlive();
if (host.shouldDispatchAppLifecycleState()) {
if (host.shouldDispatchAppLifecycleState() && flutterEngine != null) {
flutterEngine.getLifecycleChannel().appIsInactive();
}
}
Expand All @@ -652,7 +652,7 @@ void onStop() {
Log.v(TAG, "onStop()");
ensureAlive();

if (host.shouldDispatchAppLifecycleState()) {
if (host.shouldDispatchAppLifecycleState() && flutterEngine != null) {
flutterEngine.getLifecycleChannel().appIsPaused();
}

Expand Down Expand Up @@ -763,7 +763,7 @@ void onDetach() {
platformPlugin = null;
}

if (host.shouldDispatchAppLifecycleState()) {
if (host.shouldDispatchAppLifecycleState() && flutterEngine != null) {
flutterEngine.getLifecycleChannel().appIsDetached();
}

Expand Down Expand Up @@ -898,6 +898,27 @@ void onUserLeaveHint() {
}
}

/**
* Invoke this from {@code Activity#onWindowFocusChanged()}.
*
* <p>A {@code Fragment} host must have its containing {@code Activity} forward this call so that
* the {@code Fragment} can then invoke this method.
*/
void onWindowFocusChanged(boolean hasFocus) {
ensureAlive();
Log.v(TAG, "Received onWindowFocusChanged: " + (hasFocus ? "true" : "false"));
if (host.shouldDispatchAppLifecycleState() && flutterEngine != null) {
// TODO(gspencergoog): Once we have support for multiple windows/views,
// this code will need to consult the list of windows/views to determine if
// any windows in the app are focused and call the appropriate function.
if (hasFocus) {
flutterEngine.getLifecycleChannel().aWindowIsFocused();
} else {
flutterEngine.getLifecycleChannel().noWindowsAreFocused();
}
}
}

/**
* Invoke this from {@link android.app.Activity#onTrimMemory(int)}.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,16 @@
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.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 @@ -167,6 +170,19 @@ public class FlutterFragment extends Fragment
protected static final String ARG_SHOULD_AUTOMATICALLY_HANDLE_ON_BACK_PRESSED =
"should_automatically_handle_on_back_pressed";

@RequiresApi(18)
private final OnWindowFocusChangeListener onWindowFocusChangeListener =
Build.VERSION.SDK_INT >= 18
? new OnWindowFocusChangeListener() {
@Override
public void onWindowFocusChanged(boolean hasFocus) {
if (stillAttachedForEvent("onWindowFocusChanged")) {
delegate.onWindowFocusChanged(hasFocus);
}
}
}
: null;

/**
* Creates a {@code FlutterFragment} with a default configuration.
*
Expand Down Expand Up @@ -1109,9 +1125,23 @@ public void onStop() {
}
}

@Override
public void onViewCreated(View view, Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
if (Build.VERSION.SDK_INT >= 18) {
view.getViewTreeObserver().addOnWindowFocusChangeListener(onWindowFocusChangeListener);
}
}

@Override
public void onDestroyView() {
super.onDestroyView();
if (Build.VERSION.SDK_INT >= 18) {
// onWindowFocusChangeListener is API 18+ only.
requireView()
.getViewTreeObserver()
.removeOnWindowFocusChangeListener(onWindowFocusChangeListener);
}
if (stillAttachedForEvent("onDestroyView")) {
delegate.onDestroyView();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -732,6 +732,10 @@ private static class FlutterEngineActivityPluginBinding implements ActivityPlugi
private final Set<io.flutter.plugin.common.PluginRegistry.UserLeaveHintListener>
onUserLeaveHintListeners = new HashSet<>();

@NonNull
private final Set<io.flutter.plugin.common.PluginRegistry.WindowFocusChangedListener>
onWindowFocusChangedListeners = new HashSet<>();

@NonNull
private final Set<OnSaveInstanceStateListener> onSaveInstanceStateListeners = new HashSet<>();

Expand Down Expand Up @@ -847,6 +851,25 @@ public void removeOnUserLeaveHintListener(
onUserLeaveHintListeners.remove(listener);
}

@Override
public void addOnWindowFocusChangedListener(
@NonNull io.flutter.plugin.common.PluginRegistry.WindowFocusChangedListener listener) {
onWindowFocusChangedListeners.add(listener);
}

@Override
public void removeOnWindowFocusChangedListener(
@NonNull io.flutter.plugin.common.PluginRegistry.WindowFocusChangedListener listener) {
onWindowFocusChangedListeners.remove(listener);
}

void onWindowFocusChanged(boolean hasFocus) {
for (io.flutter.plugin.common.PluginRegistry.WindowFocusChangedListener listener :
onWindowFocusChangedListeners) {
listener.onWindowFocusChanged(hasFocus);
}
}

@Override
public void addOnSaveStateListener(@NonNull OnSaveInstanceStateListener listener) {
onSaveInstanceStateListeners.add(listener);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,19 @@ void removeRequestPermissionsResultListener(
*/
void removeOnUserLeaveHintListener(@NonNull PluginRegistry.UserLeaveHintListener listener);

/**
* Adds a listener that is invoked whenever the associated {@link android.app.Activity}'s {@code
* onWindowFocusChanged()} method is invoked.
*/
void addOnWindowFocusChangedListener(@NonNull PluginRegistry.WindowFocusChangedListener listener);

/**
* Removes a listener that was added in {@link
* #addOnWindowFocusChangedListener(PluginRegistry.WindowFocusChangedListener)}.
*/
void removeOnWindowFocusChangedListener(
@NonNull PluginRegistry.WindowFocusChangedListener listener);

/**
* Adds a listener that is invoked when the associated {@code Activity} or {@code Fragment} saves
* and restores instance state.
Expand Down