Skip to content
This repository was archived by the owner on Feb 25, 2025. It is now read-only.

Commit 28e4a21

Browse files
authored
Reland: "Determine lifecycle by looking at window focus also" (#41094) (#41702)
## Description This reverts commit 9183bff to re-land #41094 because the Google test failures have been fixed. There are no changes to the original PR, since the fixes were in the Google code.
1 parent d3392ed commit 28e4a21

16 files changed

+469
-31
lines changed

lib/ui/platform_dispatcher.dart

Lines changed: 43 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1642,31 +1642,61 @@ class FrameTiming {
16421642
/// States that an application can be in.
16431643
///
16441644
/// The values below describe notifications from the operating system.
1645-
/// Applications should not expect to always receive all possible
1646-
/// notifications. For example, if the users pulls out the battery from the
1647-
/// device, no notification will be sent before the application is suddenly
1648-
/// terminated, along with the rest of the operating system.
1645+
/// Applications should not expect to always receive all possible notifications.
1646+
/// For example, if the users pulls out the battery from the device, no
1647+
/// notification will be sent before the application is suddenly terminated,
1648+
/// along with the rest of the operating system.
1649+
///
1650+
/// For historical and name collision reasons, Flutter's application state names
1651+
/// do not correspond one to one with the state names on all platforms. On
1652+
/// Android, for instance, when the OS calls
1653+
/// [`Activity.onPause`](https://developer.android.com/reference/android/app/Activity#onPause()),
1654+
/// Flutter will enter the [inactive] state, but when Android calls
1655+
/// [`Activity.onStop`](https://developer.android.com/reference/android/app/Activity#onStop()),
1656+
/// Flutter enters the [paused] state. See the individual state's documentation
1657+
/// for descriptions of what they mean on each platform.
16491658
///
16501659
/// See also:
16511660
///
1652-
/// * [WidgetsBindingObserver], for a mechanism to observe the lifecycle state
1653-
/// from the widgets layer.
1661+
/// * [WidgetsBindingObserver], for a mechanism to observe the lifecycle state
1662+
/// from the widgets layer.
1663+
/// * iOS's [IOKit activity lifecycle](https://developer.apple.com/documentation/uikit/app_and_environment/managing_your_app_s_life_cycle?language=objc) documentation.
1664+
/// * Android's [activity lifecycle](https://developer.android.com/guide/components/activities/activity-lifecycle) documentation.
1665+
/// * macOS's [AppKit activity lifecycle](https://developer.apple.com/documentation/appkit/nsapplicationdelegate?language=objc) documentation.
16541666
enum AppLifecycleState {
1655-
/// The application is visible and responding to user input.
1667+
/// The application is visible and responsive to user input.
1668+
///
1669+
/// On Android, this state corresponds to the Flutter host view having focus
1670+
/// ([`Activity.onWindowFocusChanged`](https://developer.android.com/reference/android/app/Activity#onWindowFocusChanged(boolean))
1671+
/// was called with true) while in Android's "resumed" state. It is possible
1672+
/// for the Flutter app to be in the [inactive] state while still being in
1673+
/// Android's
1674+
/// ["onResume"](https://developer.android.com/guide/components/activities/activity-lifecycle)
1675+
/// state if the app has lost focus
1676+
/// ([`Activity.onWindowFocusChanged`](https://developer.android.com/reference/android/app/Activity#onWindowFocusChanged(boolean))
1677+
/// was called with false), but hasn't had
1678+
/// [`Activity.onPause`](https://developer.android.com/reference/android/app/Activity#onPause())
1679+
/// called on it.
16561680
resumed,
16571681

16581682
/// The application is in an inactive state and is not receiving user input.
16591683
///
16601684
/// On iOS, this state corresponds to an app or the Flutter host view running
1661-
/// in the foreground inactive state. Apps transition to this state when in
1662-
/// a phone call, responding to a TouchID request, when entering the app
1685+
/// in the foreground inactive state. Apps transition to this state when in a
1686+
/// phone call, responding to a TouchID request, when entering the app
16631687
/// switcher or the control center, or when the UIViewController hosting the
16641688
/// Flutter app is transitioning.
16651689
///
1666-
/// On Android, this corresponds to an app or the Flutter host view running
1667-
/// in the foreground inactive state. Apps transition to this state when
1668-
/// another activity is focused, such as a split-screen app, a phone call,
1669-
/// a picture-in-picture app, a system dialog, or another view.
1690+
/// On Android, this corresponds to an app or the Flutter host view running in
1691+
/// Android's paused state (i.e.
1692+
/// [`Activity.onPause`](https://developer.android.com/reference/android/app/Activity#onPause())
1693+
/// has been called), or in Android's "resumed" state (i.e.
1694+
/// [`Activity.onResume`](https://developer.android.com/reference/android/app/Activity#onResume())
1695+
/// has been called) but it has lost window focus. Examples of when apps
1696+
/// transition to this state include when the app is partially obscured or
1697+
/// another activity is focused, such as: a split-screen app, a phone call, a
1698+
/// picture-in-picture app, a system dialog, another view, when the
1699+
/// notification window shade is down, or the application switcher is visible.
16701700
///
16711701
/// Apps in this state should assume that they may be [paused] at any time.
16721702
inactive,

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,12 @@ public void onUserLeaveHint() {
157157
eventDelegate.onUserLeaveHint();
158158
}
159159

160+
@Override
161+
public void onWindowFocusChanged(boolean hasFocus) {
162+
super.onWindowFocusChanged(hasFocus);
163+
eventDelegate.onWindowFocusChanged(hasFocus);
164+
}
165+
160166
@Override
161167
public void onTrimMemory(int level) {
162168
eventDelegate.onTrimMemory(level);

shell/platform/android/io/flutter/app/FlutterActivityDelegate.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -260,6 +260,11 @@ public void onUserLeaveHint() {
260260
flutterView.getPluginRegistry().onUserLeaveHint();
261261
}
262262

263+
@Override
264+
public void onWindowFocusChanged(boolean hasFocus) {
265+
flutterView.getPluginRegistry().onWindowFocusChanged(hasFocus);
266+
}
267+
263268
@Override
264269
public void onTrimMemory(int level) {
265270
// Use a trim level delivered while the application is running so the

shell/platform/android/io/flutter/app/FlutterActivityEvents.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,4 +64,10 @@ public interface FlutterActivityEvents
6464

6565
/** @see android.app.Activity#onUserLeaveHint() */
6666
void onUserLeaveHint();
67+
68+
/**
69+
* @param hasFocus True if the current activity window has focus.
70+
* @see android.app.Activity#onWindowFocusChanged(boolean)
71+
*/
72+
void onWindowFocusChanged(boolean hasFocus);
6773
}

shell/platform/android/io/flutter/app/FlutterFragmentActivity.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,12 @@ public void onUserLeaveHint() {
155155
eventDelegate.onUserLeaveHint();
156156
}
157157

158+
@Override
159+
public void onWindowFocusChanged(boolean hasFocus) {
160+
super.onWindowFocusChanged(hasFocus);
161+
eventDelegate.onWindowFocusChanged(hasFocus);
162+
}
163+
158164
@Override
159165
public void onTrimMemory(int level) {
160166
super.onTrimMemory(level);

shell/platform/android/io/flutter/app/FlutterPluginRegistry.java

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ public class FlutterPluginRegistry
2828
PluginRegistry.RequestPermissionsResultListener,
2929
PluginRegistry.ActivityResultListener,
3030
PluginRegistry.NewIntentListener,
31+
PluginRegistry.WindowFocusChangedListener,
3132
PluginRegistry.UserLeaveHintListener,
3233
PluginRegistry.ViewDestroyListener {
3334
private static final String TAG = "FlutterPluginRegistry";
@@ -44,6 +45,7 @@ public class FlutterPluginRegistry
4445
private final List<ActivityResultListener> mActivityResultListeners = new ArrayList<>(0);
4546
private final List<NewIntentListener> mNewIntentListeners = new ArrayList<>(0);
4647
private final List<UserLeaveHintListener> mUserLeaveHintListeners = new ArrayList<>(0);
48+
private final List<WindowFocusChangedListener> mWindowFocusChangedListeners = new ArrayList<>(0);
4749
private final List<ViewDestroyListener> mViewDestroyListeners = new ArrayList<>(0);
4850

4951
public FlutterPluginRegistry(FlutterNativeView nativeView, Context context) {
@@ -182,6 +184,12 @@ public Registrar addUserLeaveHintListener(UserLeaveHintListener listener) {
182184
return this;
183185
}
184186

187+
@Override
188+
public Registrar addWindowFocusChangedListener(WindowFocusChangedListener listener) {
189+
mWindowFocusChangedListeners.add(listener);
190+
return this;
191+
}
192+
185193
@Override
186194
public Registrar addViewDestroyListener(ViewDestroyListener listener) {
187195
mViewDestroyListeners.add(listener);
@@ -227,6 +235,13 @@ public void onUserLeaveHint() {
227235
}
228236
}
229237

238+
@Override
239+
public void onWindowFocusChanged(boolean hasFocus) {
240+
for (WindowFocusChangedListener listener : mWindowFocusChangedListeners) {
241+
listener.onWindowFocusChanged(hasFocus);
242+
}
243+
}
244+
230245
@Override
231246
public boolean onViewDestroy(FlutterNativeView view) {
232247
boolean handled = false;

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

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -949,6 +949,14 @@ public void onUserLeaveHint() {
949949
}
950950
}
951951

952+
@Override
953+
public void onWindowFocusChanged(boolean hasFocus) {
954+
super.onWindowFocusChanged(hasFocus);
955+
if (stillAttachedForEvent("onWindowFocusChanged")) {
956+
delegate.onWindowFocusChanged(hasFocus);
957+
}
958+
}
959+
952960
@Override
953961
public void onTrimMemory(int level) {
954962
super.onTrimMemory(level);

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

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -581,7 +581,7 @@ public boolean onPreDraw() {
581581
void onResume() {
582582
Log.v(TAG, "onResume()");
583583
ensureAlive();
584-
if (host.shouldDispatchAppLifecycleState()) {
584+
if (host.shouldDispatchAppLifecycleState() && flutterEngine != null) {
585585
flutterEngine.getLifecycleChannel().appIsResumed();
586586
}
587587
}
@@ -629,7 +629,7 @@ void updateSystemUiOverlays() {
629629
void onPause() {
630630
Log.v(TAG, "onPause()");
631631
ensureAlive();
632-
if (host.shouldDispatchAppLifecycleState()) {
632+
if (host.shouldDispatchAppLifecycleState() && flutterEngine != null) {
633633
flutterEngine.getLifecycleChannel().appIsInactive();
634634
}
635635
}
@@ -652,7 +652,7 @@ void onStop() {
652652
Log.v(TAG, "onStop()");
653653
ensureAlive();
654654

655-
if (host.shouldDispatchAppLifecycleState()) {
655+
if (host.shouldDispatchAppLifecycleState() && flutterEngine != null) {
656656
flutterEngine.getLifecycleChannel().appIsPaused();
657657
}
658658

@@ -763,7 +763,7 @@ void onDetach() {
763763
platformPlugin = null;
764764
}
765765

766-
if (host.shouldDispatchAppLifecycleState()) {
766+
if (host.shouldDispatchAppLifecycleState() && flutterEngine != null) {
767767
flutterEngine.getLifecycleChannel().appIsDetached();
768768
}
769769

@@ -898,6 +898,27 @@ void onUserLeaveHint() {
898898
}
899899
}
900900

901+
/**
902+
* Invoke this from {@code Activity#onWindowFocusChanged()}.
903+
*
904+
* <p>A {@code Fragment} host must have its containing {@code Activity} forward this call so that
905+
* the {@code Fragment} can then invoke this method.
906+
*/
907+
void onWindowFocusChanged(boolean hasFocus) {
908+
ensureAlive();
909+
Log.v(TAG, "Received onWindowFocusChanged: " + (hasFocus ? "true" : "false"));
910+
if (host.shouldDispatchAppLifecycleState() && flutterEngine != null) {
911+
// TODO(gspencergoog): Once we have support for multiple windows/views,
912+
// this code will need to consult the list of windows/views to determine if
913+
// any windows in the app are focused and call the appropriate function.
914+
if (hasFocus) {
915+
flutterEngine.getLifecycleChannel().aWindowIsFocused();
916+
} else {
917+
flutterEngine.getLifecycleChannel().noWindowsAreFocused();
918+
}
919+
}
920+
}
921+
901922
/**
902923
* Invoke this from {@link android.app.Activity#onTrimMemory(int)}.
903924
*

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

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,16 @@
88
import android.content.ComponentCallbacks2;
99
import android.content.Context;
1010
import android.content.Intent;
11+
import android.os.Build;
1112
import android.os.Bundle;
1213
import android.view.LayoutInflater;
1314
import android.view.View;
1415
import android.view.ViewGroup;
16+
import android.view.ViewTreeObserver.OnWindowFocusChangeListener;
1517
import androidx.activity.OnBackPressedCallback;
1618
import androidx.annotation.NonNull;
1719
import androidx.annotation.Nullable;
20+
import androidx.annotation.RequiresApi;
1821
import androidx.annotation.VisibleForTesting;
1922
import androidx.fragment.app.Fragment;
2023
import androidx.fragment.app.FragmentActivity;
@@ -167,6 +170,19 @@ public class FlutterFragment extends Fragment
167170
protected static final String ARG_SHOULD_AUTOMATICALLY_HANDLE_ON_BACK_PRESSED =
168171
"should_automatically_handle_on_back_pressed";
169172

173+
@RequiresApi(18)
174+
private final OnWindowFocusChangeListener onWindowFocusChangeListener =
175+
Build.VERSION.SDK_INT >= 18
176+
? new OnWindowFocusChangeListener() {
177+
@Override
178+
public void onWindowFocusChanged(boolean hasFocus) {
179+
if (stillAttachedForEvent("onWindowFocusChanged")) {
180+
delegate.onWindowFocusChanged(hasFocus);
181+
}
182+
}
183+
}
184+
: null;
185+
170186
/**
171187
* Creates a {@code FlutterFragment} with a default configuration.
172188
*
@@ -1109,9 +1125,23 @@ public void onStop() {
11091125
}
11101126
}
11111127

1128+
@Override
1129+
public void onViewCreated(View view, Bundle savedInstanceState) {
1130+
super.onViewCreated(view, savedInstanceState);
1131+
if (Build.VERSION.SDK_INT >= 18) {
1132+
view.getViewTreeObserver().addOnWindowFocusChangeListener(onWindowFocusChangeListener);
1133+
}
1134+
}
1135+
11121136
@Override
11131137
public void onDestroyView() {
11141138
super.onDestroyView();
1139+
if (Build.VERSION.SDK_INT >= 18) {
1140+
// onWindowFocusChangeListener is API 18+ only.
1141+
requireView()
1142+
.getViewTreeObserver()
1143+
.removeOnWindowFocusChangeListener(onWindowFocusChangeListener);
1144+
}
11151145
if (stillAttachedForEvent("onDestroyView")) {
11161146
delegate.onDestroyView();
11171147
}

shell/platform/android/io/flutter/embedding/engine/FlutterEngineConnectionRegistry.java

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -732,6 +732,10 @@ private static class FlutterEngineActivityPluginBinding implements ActivityPlugi
732732
private final Set<io.flutter.plugin.common.PluginRegistry.UserLeaveHintListener>
733733
onUserLeaveHintListeners = new HashSet<>();
734734

735+
@NonNull
736+
private final Set<io.flutter.plugin.common.PluginRegistry.WindowFocusChangedListener>
737+
onWindowFocusChangedListeners = new HashSet<>();
738+
735739
@NonNull
736740
private final Set<OnSaveInstanceStateListener> onSaveInstanceStateListeners = new HashSet<>();
737741

@@ -847,6 +851,25 @@ public void removeOnUserLeaveHintListener(
847851
onUserLeaveHintListeners.remove(listener);
848852
}
849853

854+
@Override
855+
public void addOnWindowFocusChangedListener(
856+
@NonNull io.flutter.plugin.common.PluginRegistry.WindowFocusChangedListener listener) {
857+
onWindowFocusChangedListeners.add(listener);
858+
}
859+
860+
@Override
861+
public void removeOnWindowFocusChangedListener(
862+
@NonNull io.flutter.plugin.common.PluginRegistry.WindowFocusChangedListener listener) {
863+
onWindowFocusChangedListeners.remove(listener);
864+
}
865+
866+
void onWindowFocusChanged(boolean hasFocus) {
867+
for (io.flutter.plugin.common.PluginRegistry.WindowFocusChangedListener listener :
868+
onWindowFocusChangedListeners) {
869+
listener.onWindowFocusChanged(hasFocus);
870+
}
871+
}
872+
850873
@Override
851874
public void addOnSaveStateListener(@NonNull OnSaveInstanceStateListener listener) {
852875
onSaveInstanceStateListeners.add(listener);

shell/platform/android/io/flutter/embedding/engine/plugins/activity/ActivityPluginBinding.java

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,19 @@ void removeRequestPermissionsResultListener(
9191
*/
9292
void removeOnUserLeaveHintListener(@NonNull PluginRegistry.UserLeaveHintListener listener);
9393

94+
/**
95+
* Adds a listener that is invoked whenever the associated {@link android.app.Activity}'s {@code
96+
* onWindowFocusChanged()} method is invoked.
97+
*/
98+
void addOnWindowFocusChangedListener(@NonNull PluginRegistry.WindowFocusChangedListener listener);
99+
100+
/**
101+
* Removes a listener that was added in {@link
102+
* #addOnWindowFocusChangedListener(PluginRegistry.WindowFocusChangedListener)}.
103+
*/
104+
void removeOnWindowFocusChangedListener(
105+
@NonNull PluginRegistry.WindowFocusChangedListener listener);
106+
94107
/**
95108
* Adds a listener that is invoked when the associated {@code Activity} or {@code Fragment} saves
96109
* and restores instance state.

0 commit comments

Comments
 (0)