diff --git a/ci/licenses_golden/licenses_flutter b/ci/licenses_golden/licenses_flutter index 893d479e0dbe4..7d0eab4a8cada 100644 --- a/ci/licenses_golden/licenses_flutter +++ b/ci/licenses_golden/licenses_flutter @@ -2463,6 +2463,7 @@ ORIGIN: ../../../flutter/shell/platform/common/accessibility_bridge.cc + ../../. ORIGIN: ../../../flutter/shell/platform/common/accessibility_bridge.h + ../../../flutter/LICENSE ORIGIN: ../../../flutter/shell/platform/common/alert_platform_node_delegate.cc + ../../../flutter/LICENSE ORIGIN: ../../../flutter/shell/platform/common/alert_platform_node_delegate.h + ../../../flutter/LICENSE +ORIGIN: ../../../flutter/shell/platform/common/app_lifecycle_state.h + ../../../flutter/LICENSE ORIGIN: ../../../flutter/shell/platform/common/client_wrapper/binary_messenger_impl.h + ../../../flutter/LICENSE ORIGIN: ../../../flutter/shell/platform/common/client_wrapper/byte_buffer_streams.h + ../../../flutter/LICENSE ORIGIN: ../../../flutter/shell/platform/common/client_wrapper/core_implementations.cc + ../../../flutter/LICENSE @@ -2680,6 +2681,7 @@ ORIGIN: ../../../flutter/shell/platform/darwin/ios/platform_view_ios.mm + ../../ ORIGIN: ../../../flutter/shell/platform/darwin/ios/rendering_api_selection.h + ../../../flutter/LICENSE ORIGIN: ../../../flutter/shell/platform/darwin/ios/rendering_api_selection.mm + ../../../flutter/LICENSE ORIGIN: ../../../flutter/shell/platform/darwin/macos/framework/Headers/FlutterAppDelegate.h + ../../../flutter/LICENSE +ORIGIN: ../../../flutter/shell/platform/darwin/macos/framework/Headers/FlutterAppLifecycleDelegate.h + ../../../flutter/LICENSE ORIGIN: ../../../flutter/shell/platform/darwin/macos/framework/Headers/FlutterEngine.h + ../../../flutter/LICENSE ORIGIN: ../../../flutter/shell/platform/darwin/macos/framework/Headers/FlutterMacOS.h + ../../../flutter/LICENSE ORIGIN: ../../../flutter/shell/platform/darwin/macos/framework/Headers/FlutterPlatformViews.h + ../../../flutter/LICENSE @@ -2691,6 +2693,8 @@ ORIGIN: ../../../flutter/shell/platform/darwin/macos/framework/Source/Accessibil ORIGIN: ../../../flutter/shell/platform/darwin/macos/framework/Source/AccessibilityBridgeMacTest.mm + ../../../flutter/LICENSE ORIGIN: ../../../flutter/shell/platform/darwin/macos/framework/Source/FlutterAppDelegate.mm + ../../../flutter/LICENSE ORIGIN: ../../../flutter/shell/platform/darwin/macos/framework/Source/FlutterAppDelegate_Internal.h + ../../../flutter/LICENSE +ORIGIN: ../../../flutter/shell/platform/darwin/macos/framework/Source/FlutterAppLifecycleDelegate.mm + ../../../flutter/LICENSE +ORIGIN: ../../../flutter/shell/platform/darwin/macos/framework/Source/FlutterAppLifecycleDelegateTest.mm + ../../../flutter/LICENSE ORIGIN: ../../../flutter/shell/platform/darwin/macos/framework/Source/FlutterBackingStore.h + ../../../flutter/LICENSE ORIGIN: ../../../flutter/shell/platform/darwin/macos/framework/Source/FlutterBackingStore.mm + ../../../flutter/LICENSE ORIGIN: ../../../flutter/shell/platform/darwin/macos/framework/Source/FlutterChannelKeyResponder.h + ../../../flutter/LICENSE @@ -5130,6 +5134,7 @@ FILE: ../../../flutter/shell/platform/common/accessibility_bridge.cc FILE: ../../../flutter/shell/platform/common/accessibility_bridge.h FILE: ../../../flutter/shell/platform/common/alert_platform_node_delegate.cc FILE: ../../../flutter/shell/platform/common/alert_platform_node_delegate.h +FILE: ../../../flutter/shell/platform/common/app_lifecycle_state.h FILE: ../../../flutter/shell/platform/common/client_wrapper/binary_messenger_impl.h FILE: ../../../flutter/shell/platform/common/client_wrapper/byte_buffer_streams.h FILE: ../../../flutter/shell/platform/common/client_wrapper/core_implementations.cc @@ -5349,6 +5354,7 @@ FILE: ../../../flutter/shell/platform/darwin/ios/platform_view_ios.mm FILE: ../../../flutter/shell/platform/darwin/ios/rendering_api_selection.h FILE: ../../../flutter/shell/platform/darwin/ios/rendering_api_selection.mm FILE: ../../../flutter/shell/platform/darwin/macos/framework/Headers/FlutterAppDelegate.h +FILE: ../../../flutter/shell/platform/darwin/macos/framework/Headers/FlutterAppLifecycleDelegate.h FILE: ../../../flutter/shell/platform/darwin/macos/framework/Headers/FlutterEngine.h FILE: ../../../flutter/shell/platform/darwin/macos/framework/Headers/FlutterMacOS.h FILE: ../../../flutter/shell/platform/darwin/macos/framework/Headers/FlutterPlatformViews.h @@ -5361,6 +5367,8 @@ FILE: ../../../flutter/shell/platform/darwin/macos/framework/Source/Accessibilit FILE: ../../../flutter/shell/platform/darwin/macos/framework/Source/AccessibilityBridgeMacTest.mm FILE: ../../../flutter/shell/platform/darwin/macos/framework/Source/FlutterAppDelegate.mm FILE: ../../../flutter/shell/platform/darwin/macos/framework/Source/FlutterAppDelegate_Internal.h +FILE: ../../../flutter/shell/platform/darwin/macos/framework/Source/FlutterAppLifecycleDelegate.mm +FILE: ../../../flutter/shell/platform/darwin/macos/framework/Source/FlutterAppLifecycleDelegateTest.mm FILE: ../../../flutter/shell/platform/darwin/macos/framework/Source/FlutterBackingStore.h FILE: ../../../flutter/shell/platform/darwin/macos/framework/Source/FlutterBackingStore.mm FILE: ../../../flutter/shell/platform/darwin/macos/framework/Source/FlutterChannelKeyResponder.h diff --git a/lib/ui/platform_dispatcher.dart b/lib/ui/platform_dispatcher.dart index 0e26b0ca215a1..42a56a36c345a 100644 --- a/lib/ui/platform_dispatcher.dart +++ b/lib/ui/platform_dispatcher.dart @@ -1679,13 +1679,15 @@ class FrameTiming { } } -/// States that an application can be in. +/// States that an application can be in once it is running. /// -/// 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. +/// States not supported on a platform will be synthesized by the framework when +/// transitioning between states which are supported, so that all +/// implementations share the same state machine. +/// +/// The initial value for the state is the [detached] state, updated to the +/// current state (usually [resumed]) as soon as the first lifecycle update is +/// received from the platform. /// /// 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 @@ -1696,15 +1698,52 @@ class FrameTiming { /// Flutter enters the [paused] state. See the individual state's documentation /// for descriptions of what they mean on each platform. /// +/// The current application state can be obtained from +/// [SchedulerBinding.instance.lifecycleState], and changes to the state can be +/// observed by creating an [AppLifecycleListener], or by using a +/// [WidgetsBindingObserver] by overriding the +/// [WidgetsBindingObserver.didChangeAppLifecycleState] method. +/// +/// Applications should not rely on always receiving all possible notifications. +/// +/// For example, if the application is killed with a task manager, a kill +/// signal, the user pulls the power from the device, or there is a rapid +/// unscheduled disassembly of the device, no notification will be sent before +/// the application is suddenly terminated, and some states may be skipped. +/// /// See also: /// +/// * [AppLifecycleListener], an object used observe the lifecycle state that +/// provides state transition callbacks. /// * [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. +/// * 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 responsive to user input. + /// The application is still hosted by a Flutter engine but is detached from + /// any host views. + /// + /// The application defaults to this state before it initializes, and can be + /// in this state (on Android and iOS only) after all views have been + /// detached. + /// + /// When the application is in this state, the engine is running without a + /// view. + /// + /// This state is only entered on iOS and Android, although on all platforms + /// it is the default state before the application begins running. + detached, + + /// On all platforms, this state indicates that the application is in the + /// default running mode for a running application that has input focus and is + /// visible. /// /// On Android, this state corresponds to the Flutter host view having focus /// ([`Activity.onWindowFocusChanged`](https://developer.android.com/reference/android/app/Activity#onWindowFocusChanged(boolean)) @@ -1717,54 +1756,71 @@ enum AppLifecycleState { /// was called with false), but hasn't had /// [`Activity.onPause`](https://developer.android.com/reference/android/app/Activity#onPause()) /// called on it. + /// + /// On iOS and macOS, this corresponds to the app running in the foreground + /// active state. resumed, - /// The application is in an inactive state and is not receiving user input. + /// At least one view of the application is visible, but none have input + /// focus. The application is otherwise running normally. + /// + /// On non-web desktop platforms, this corresponds to an application that is + /// not in the foreground, but still has visible windows. + /// + /// On the web, this corresponds to an application that is running in a + /// window or tab that does not have input focus. /// - /// 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 - /// switcher or the control center, or when the UIViewController hosting the - /// Flutter app is transitioning. + /// On iOS and macOS, this state corresponds to the Flutter host view running in the + /// foreground inactive state. Apps transition to this state when in a phone + /// call, when 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 - /// Android's paused state (i.e. + /// On Android, this corresponds to 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 + /// has been called) but does not have 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 + /// another activity is focused, a app running in a split screen that isn't + /// the current app, an app interrupted by a phone call, a picture-in-picture + /// app, a system dialog, another view. It will also be inactive 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. + /// On Android and iOS, apps in this state should assume that they may be + /// [hidden] and [paused] at any time. inactive, - /// The application is not currently visible to the user, not responding to - /// user input, and running in the background. + /// All views of an application are hidden, either because the application is + /// about to be paused (on iOS and Android), or because it has been minimized + /// or placed on a desktop that is no longer visible (on non-web desktop), or + /// is running in a window or tab that is no longer visible (on the web). + /// + /// On iOS and Android, in order to keep the state machine the same on all + /// platforms, a transition to this state is synthesized before the [paused] + /// state is entered when coming from [inactive], and before the [inactive] + /// state is entered when coming from [paused]. This allows cross-platform + /// implementations that want to know when an app is conceptually "hidden" to + /// only write one handler. + hidden, + + /// The application is not currently visible to the user, and not responding + /// to user input. /// /// When the application is in this state, the engine will not call the /// [PlatformDispatcher.onBeginFrame] and [PlatformDispatcher.onDrawFrame] /// callbacks. - paused, - - /// The application is still hosted on a flutter engine but is detached from - /// any host views. /// - /// When the application is in this state, the engine is running without - /// a view. It can either be in the progress of attaching a view when engine - /// was first initializes, or after the view being destroyed due to a Navigator - /// pop. - detached, + /// This state is only entered on iOS and Android. + paused, } /// The possible responses to a request to exit the application. /// -/// The request is typically responded to by a [WidgetsBindingObserver]. -// TODO(gspencergoog): Insert doc references here to AppLifecycleListener and to -// the actual function called on WidgetsBindingObserver once those have landed -// in the framework. https://github.com/flutter/flutter/issues/121721 +/// The request is typically responded to by creating an [AppLifecycleListener] +/// and supplying an [AppLifecycleListener.onExitRequested] callback, or by +/// overriding [WidgetsBindingObserver.didRequestAppExit]. enum AppExitResponse { /// Exiting the application can proceed. exit, @@ -1773,10 +1829,7 @@ enum AppExitResponse { } /// The type of application exit to perform when calling -/// `ServicesBinding.exitApplication`. -// TODO(gspencergoog): Insert doc references here to -// ServicesBinding.exitApplication that has landed in the framework. -// https://github.com/flutter/flutter/issues/121721 +/// [ServicesBinding.exitApplication]. enum AppExitType { /// Requests that the application start an orderly exit, sending a request /// back to the framework through the [WidgetsBinding]. If that responds diff --git a/lib/web_ui/lib/platform_dispatcher.dart b/lib/web_ui/lib/platform_dispatcher.dart index 2546404fbacce..2059122d8c8a5 100644 --- a/lib/web_ui/lib/platform_dispatcher.dart +++ b/lib/web_ui/lib/platform_dispatcher.dart @@ -104,7 +104,7 @@ abstract class PlatformDispatcher { VoidCallback? get onLocaleChanged; set onLocaleChanged(VoidCallback? callback); - String get initialLifecycleState => 'AppLifecycleState.resumed'; + String get initialLifecycleState => ''; bool get alwaysUse24HourFormat; @@ -247,10 +247,11 @@ class FrameTiming { } enum AppLifecycleState { + detached, resumed, inactive, + hidden, paused, - detached, } enum AppExitResponse { diff --git a/lib/web_ui/lib/src/engine/platform_dispatcher.dart b/lib/web_ui/lib/src/engine/platform_dispatcher.dart index 7ff4c674121f4..0d54cba677de7 100644 --- a/lib/web_ui/lib/src/engine/platform_dispatcher.dart +++ b/lib/web_ui/lib/src/engine/platform_dispatcher.dart @@ -104,6 +104,7 @@ class EnginePlatformDispatcher extends ui.PlatformDispatcher { _addFontSizeObserver(); _addLocaleChangedListener(); registerHotRestartListener(dispose); + _setAppLifecycleState(ui.AppLifecycleState.resumed); } /// The [EnginePlatformDispatcher] singleton. @@ -1005,6 +1006,14 @@ class EnginePlatformDispatcher extends ui.PlatformDispatcher { _fontSizeObserver = null; } + void _setAppLifecycleState(ui.AppLifecycleState state) { + sendPlatformMessage( + 'flutter/lifecycle', + Uint8List.fromList(utf8.encode(state.toString())).buffer.asByteData(), + null, + ); + } + /// A callback that is invoked whenever [textScaleFactor] changes value. /// /// The framework invokes this callback in the same zone in which the diff --git a/shell/platform/android/io/flutter/embedding/engine/systemchannels/LifecycleChannel.java b/shell/platform/android/io/flutter/embedding/engine/systemchannels/LifecycleChannel.java index 6ea0814fe0ba5..2e6858baf67f6 100644 --- a/shell/platform/android/io/flutter/embedding/engine/systemchannels/LifecycleChannel.java +++ b/shell/platform/android/io/flutter/embedding/engine/systemchannels/LifecycleChannel.java @@ -22,14 +22,21 @@ public class LifecycleChannel { private static final String TAG = "LifecycleChannel"; private static final String CHANNEL_NAME = "flutter/lifecycle"; - // These should stay in sync with the AppLifecycleState enum in the framework. - private static final String RESUMED = "AppLifecycleState.resumed"; - private static final String INACTIVE = "AppLifecycleState.inactive"; - private static final String PAUSED = "AppLifecycleState.paused"; - private static final String DETACHED = "AppLifecycleState.detached"; - - private String lastAndroidState = ""; - private String lastFlutterState = ""; + // This enum should match the Dart enum of the same name. + // + // HIDDEN isn't used on Android (it's synthesized in the Framework code). It's + // only listed here so that apicheck_test.dart can make sure that the states here + // match the Dart code. + private enum AppLifecycleState { + DETACHED, + RESUMED, + INACTIVE, + HIDDEN, + PAUSED, + }; + + private AppLifecycleState lastAndroidState = null; + private AppLifecycleState lastFlutterState = null; private boolean lastFocus = true; @NonNull private final BasicMessageChannel channel; @@ -55,21 +62,39 @@ public LifecycleChannel(@NonNull BasicMessageChannel channel) { // | Stopped | false | paused | // | Detached | true | detached | // | Detached | false | detached | - private void sendState(String state, boolean hasFocus) { + // + // The hidden state isn't used on Android, it's synthesized in the Framework + // code when transitioning between paused and inactive in either direction. + private void sendState(AppLifecycleState state, boolean hasFocus) { if (lastAndroidState == state && hasFocus == lastFocus) { // No inputs changed, so Flutter state could not have changed. return; } - String newState; - if (state == RESUMED) { - // Focus is only taken into account when the Android state is "Resumed". - // In all other states, focus is ignored, because we can't know what order - // Android lifecycle notifications and window focus notifications events - // will arrive in, and those states don't send input events anyhow. - newState = hasFocus ? RESUMED : INACTIVE; - } else { - newState = state; + if (state == null && lastAndroidState == null) { + // If we're responding to a focus change before the state is set, just + // keep the last reported focus state and don't send anything to the + // framework. This could happen if focus events and lifecycle events are + // delivered out of the expected order. + lastFocus = hasFocus; + return; + } + AppLifecycleState newState = null; + switch (state) { + case RESUMED: + // Focus is only taken into account when the Android state is "Resumed". + // In all other states, focus is ignored, because we can't know what order + // Android lifecycle notifications and window focus notifications events + // will arrive in, and those states don't send input events anyhow. + newState = hasFocus ? AppLifecycleState.RESUMED : AppLifecycleState.INACTIVE; + break; + case INACTIVE: + case HIDDEN: + case PAUSED: + case DETACHED: + newState = state; + break; } + // Keep the last reported values for future updates. lastAndroidState = state; lastFocus = hasFocus; @@ -77,12 +102,14 @@ private void sendState(String state, boolean hasFocus) { // No change in the resulting Flutter state, so don't report anything. return; } - Log.v(TAG, "Sending " + newState + " message."); - channel.send(newState); + String message = "AppLifecycleState." + newState.name().toLowerCase(); + Log.v(TAG, "Sending " + message + " message."); + channel.send(message); lastFlutterState = newState; } - // Called if at least one window in the app has focus. + // Called if at least one window in the app has focus, even if the focused + // window doesn't contain a Flutter view. public void aWindowIsFocused() { sendState(lastAndroidState, true); } @@ -93,18 +120,18 @@ public void noWindowsAreFocused() { } public void appIsResumed() { - sendState(RESUMED, lastFocus); + sendState(AppLifecycleState.RESUMED, lastFocus); } public void appIsInactive() { - sendState(INACTIVE, lastFocus); + sendState(AppLifecycleState.INACTIVE, lastFocus); } public void appIsPaused() { - sendState(PAUSED, lastFocus); + sendState(AppLifecycleState.PAUSED, lastFocus); } public void appIsDetached() { - sendState(DETACHED, lastFocus); + sendState(AppLifecycleState.DETACHED, lastFocus); } } diff --git a/shell/platform/common/BUILD.gn b/shell/platform/common/BUILD.gn index 0d45a8ad1eb5f..13f2a51e20e97 100644 --- a/shell/platform/common/BUILD.gn +++ b/shell/platform/common/BUILD.gn @@ -58,7 +58,10 @@ source_set("common_cpp_input") { } source_set("common_cpp_enums") { - public = [ "platform_provided_menu.h" ] + public = [ + "app_lifecycle_state.h", + "platform_provided_menu.h", + ] public_configs = [ "//flutter:config", diff --git a/shell/platform/common/app_lifecycle_state.h b/shell/platform/common/app_lifecycle_state.h new file mode 100644 index 0000000000000..a3d79dcb74c81 --- /dev/null +++ b/shell/platform/common/app_lifecycle_state.h @@ -0,0 +1,89 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef FLUTTER_SHELL_PLATFORM_COMMON_APP_LIFECYCLE_STATE_H_ +#define FLUTTER_SHELL_PLATFORM_COMMON_APP_LIFECYCLE_STATE_H_ + +namespace flutter { + +/** + * These constants describe the possible lifecycle states of the application. + * They must be kept up to date with changes in the framework's + * AppLifecycleState enum. They are passed to the embedder's |SetLifecycleState| + * function. + * + * States not supported on a platform will be synthesized by the framework when + * transitioning between states which are supported, so that all implementations + * share the same state machine. + * + * Here is the state machine: + * + * +-----------+ +-----------+ + * | detached |------------------------------>| resumed | + * +-----------+ +-----------+ + * ^ ^ + * | | + * | v + * +-----------+ +--------------+ +-----------+ + * | paused |<------>| hidden |<----->| inactive | + * +-----------+ +--------------+ +-----------+ + */ +enum class AppLifecycleState { + /** + * Corresponds to the Framework's AppLifecycleState.detached: The initial + * state of the state machine. On Android and iOS, also the final state of the + * state machine when all views are detached. Other platforms do not enter + * this state again after initially leaving it. + */ + kDetached, + + /** + * Corresponds to the Framework's AppLifecycleState.resumed: The nominal + * "running" state of the application. The application is visible, has input + * focus, and is running. + */ + kResumed, + + /** + * Corresponds to the Framework's AppLifecycleState.inactive: At least one + * view of the application is visible, but none have input focus. The + * application is otherwise running normally. + */ + kInactive, + + /** + * Corresponds to the Framework's AppLifecycleState.hidden: All views of an + * application are hidden, either because the application is being stopped (on + * iOS and Android), or because it is being minimized or on a desktop that is + * no longer visible (on desktop), or on a tab that is no longer visible (on + * web). + */ + kHidden, + + /** + * Corresponds to the Framework's AppLifecycleState.paused: The application is + * not running, and can be detached or started again at any time. This state + * is typically only entered into on iOS and Android. + */ + kPaused, +}; + +constexpr const char* AppLifecycleStateToString(AppLifecycleState state) { + switch (state) { + case AppLifecycleState::kDetached: + return "AppLifecycleState.detached"; + case AppLifecycleState::kResumed: + return "AppLifecycleState.resumed"; + case AppLifecycleState::kInactive: + return "AppLifecycleState.inactive"; + case AppLifecycleState::kHidden: + return "AppLifecycleState.hidden"; + case AppLifecycleState::kPaused: + return "AppLifecycleState.paused"; + } +} + +} // namespace flutter + +#endif // FLUTTER_SHELL_PLATFORM_COMMON_APP_LIFECYCLE_STATE_H_ diff --git a/shell/platform/darwin/macos/BUILD.gn b/shell/platform/darwin/macos/BUILD.gn index 16ffb24cd3f19..86f681b21fe57 100644 --- a/shell/platform/darwin/macos/BUILD.gn +++ b/shell/platform/darwin/macos/BUILD.gn @@ -38,6 +38,7 @@ _framework_binary_subpath = "Versions/A/$_flutter_framework_name" # the Flutter engine source root. _flutter_framework_headers = [ "framework/Headers/FlutterAppDelegate.h", + "framework/Headers/FlutterAppLifecycleDelegate.h", "framework/Headers/FlutterEngine.h", "framework/Headers/FlutterMacOS.h", "framework/Headers/FlutterPlatformViews.h", @@ -56,6 +57,7 @@ source_set("flutter_framework_source") { "framework/Source/AccessibilityBridgeMac.h", "framework/Source/AccessibilityBridgeMac.mm", "framework/Source/FlutterAppDelegate.mm", + "framework/Source/FlutterAppLifecycleDelegate.mm", "framework/Source/FlutterBackingStore.h", "framework/Source/FlutterBackingStore.mm", "framework/Source/FlutterChannelKeyResponder.h", @@ -167,6 +169,7 @@ executable("flutter_desktop_darwin_unittests") { sources = [ "framework/Source/AccessibilityBridgeMacTest.mm", + "framework/Source/FlutterAppLifecycleDelegateTest.mm", "framework/Source/FlutterChannelKeyResponderTest.mm", "framework/Source/FlutterCompositorTest.mm", "framework/Source/FlutterEmbedderExternalTextureTest.mm", diff --git a/shell/platform/darwin/macos/framework/Headers/FlutterAppDelegate.h b/shell/platform/darwin/macos/framework/Headers/FlutterAppDelegate.h index 092c0115d077f..c7aaeca37e6cc 100644 --- a/shell/platform/darwin/macos/framework/Headers/FlutterAppDelegate.h +++ b/shell/platform/darwin/macos/framework/Headers/FlutterAppDelegate.h @@ -7,10 +7,11 @@ #import +#import "FlutterAppLifecycleDelegate.h" #import "FlutterMacros.h" /** - * `NSApplicationDelegate` subclass for simple apps that want default behavior. + * |NSApplicationDelegate| subclass for simple apps that want default behavior. * * This class implements the following behaviors: * * Updates the application name of items in the application menu to match the name in @@ -33,11 +34,23 @@ FLUTTER_DARWIN_EXPORT @property(weak, nonatomic) IBOutlet NSMenu* applicationMenu; /** - * The primary application window containing a FlutterViewController. This is primarily intended - * for use in single-window applications. + * The primary application window containing a FlutterViewController. This is + * primarily intended for use in single-window applications. */ @property(weak, nonatomic) IBOutlet NSWindow* mainFlutterWindow; +/** + * Adds an object implementing |FlutterAppLifecycleDelegate| to the list of + * delegates to be informed of application lifecycle events. + */ +- (void)addApplicationLifecycleDelegate:(NSObject*)delegate; + +/** + * Removes an object implementing |FlutterAppLifecycleDelegate| to the list of + * delegates to be informed of application lifecycle events. + */ +- (void)removeApplicationLifecycleDelegate:(NSObject*)delegate; + @end #endif // FLUTTER_FLUTTERAPPDELEGATE_H_ diff --git a/shell/platform/darwin/macos/framework/Headers/FlutterAppLifecycleDelegate.h b/shell/platform/darwin/macos/framework/Headers/FlutterAppLifecycleDelegate.h new file mode 100644 index 0000000000000..fd8d52de20e52 --- /dev/null +++ b/shell/platform/darwin/macos/framework/Headers/FlutterAppLifecycleDelegate.h @@ -0,0 +1,137 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef FLUTTER_FLUTTERAPPLIFECYCLEDELEGATE_H_ +#define FLUTTER_FLUTTERAPPLIFECYCLEDELEGATE_H_ + +#import +#include + +#import "FlutterMacros.h" +#import "FlutterPluginMacOS.h" + +NS_ASSUME_NONNULL_BEGIN + +#pragma mark - +/** + * Protocol for listener of lifecycle events from the NSApplication, typically a + * FlutterPlugin. + */ +@protocol FlutterAppLifecycleDelegate + +@optional +/** + * Called when the |FlutterAppDelegate| gets the applicationWillFinishLaunching + * notification. + */ +- (void)handleWillFinishLaunching:(NSNotification*)notification; + +/** + * Called when the |FlutterAppDelegate| gets the applicationDidFinishLaunching + * notification. + */ +- (void)handleDidFinishLaunching:(NSNotification*)notification; + +/** + * Called when the |FlutterAppDelegate| gets the applicationWillBecomeActive + * notification. + */ +- (void)handleWillBecomeActive:(NSNotification*)notification; + +/** + * Called when the |FlutterAppDelegate| gets the applicationDidBecomeActive + * notification. + */ +- (void)handleDidBecomeActive:(NSNotification*)notification; + +/** + * Called when the |FlutterAppDelegate| gets the applicationWillResignActive + * notification. + */ +- (void)handleWillResignActive:(NSNotification*)notification; + +/** + * Called when the |FlutterAppDelegate| gets the applicationWillResignActive + * notification. + */ +- (void)handleDidResignActive:(NSNotification*)notification; + +/** + * Called when the |FlutterAppDelegate| gets the applicationWillHide + * notification. + */ +- (void)handleWillHide:(NSNotification*)notification; + +/** + * Called when the |FlutterAppDelegate| gets the applicationDidHide + * notification. + */ +- (void)handleDidHide:(NSNotification*)notification; + +/** + * Called when the |FlutterAppDelegate| gets the applicationWillUnhide + * notification. + */ +- (void)handleWillUnhide:(NSNotification*)notification; + +/** + * Called when the |FlutterAppDelegate| gets the applicationDidUnhide + * notification. + */ +- (void)handleDidUnhide:(NSNotification*)notification; + +/** + * Called when the |FlutterAppDelegate| gets the applicationDidUnhide + * notification. + */ +- (void)handleDidChangeScreenParameters:(NSNotification*)notification; + +/** + * Called when the |FlutterAppDelegate| gets the applicationDidUnhide + * notification. + */ +- (void)handleDidChangeOcclusionState:(NSNotification*)notification API_AVAILABLE(macos(10.9)); + +/** + * Called when the |FlutterAppDelegate| gets the applicationWillTerminate + * notification. + * + * Applications should not rely on always receiving all possible notifications. + * + * For example, if the application is killed with a task manager, a kill signal, + * the user pulls the power from the device, or there is a rapid unscheduled + * disassembly of the device, no notification will be sent before the + * application is suddenly terminated, and this notification may be skipped. + */ +- (void)handleWillTerminate:(NSNotification*)notification; +@end + +#pragma mark - + +/** + * Propagates `NSAppDelegate` callbacks to registered delegates. + */ +FLUTTER_DARWIN_EXPORT +@interface FlutterAppLifecycleRegistrar : NSObject + +/** + * Registers `delegate` to receive lifecycle callbacks via this + * FlutterAppLifecycleDelegate as long as it is alive. + * + * `delegate` will only be referenced weakly. + */ +- (void)addDelegate:(NSObject*)delegate; + +/** + * Unregisters `delegate` so that it will no longer receive life cycle callbacks + * via this FlutterAppLifecycleDelegate. + * + * `delegate` will only be referenced weakly. + */ +- (void)removeDelegate:(NSObject*)delegate; +@end + +NS_ASSUME_NONNULL_END + +#endif // FLUTTER_FLUTTERAPPLIFECYCLEDELEGATE_H_ diff --git a/shell/platform/darwin/macos/framework/Headers/FlutterEngine.h b/shell/platform/darwin/macos/framework/Headers/FlutterEngine.h index f854976ebd611..bb12d5cd2ae76 100644 --- a/shell/platform/darwin/macos/framework/Headers/FlutterEngine.h +++ b/shell/platform/darwin/macos/framework/Headers/FlutterEngine.h @@ -9,6 +9,7 @@ #include +#import "FlutterAppLifecycleDelegate.h" #import "FlutterBinaryMessenger.h" #import "FlutterDartProject.h" #import "FlutterMacros.h" @@ -26,7 +27,8 @@ * code. */ FLUTTER_DARWIN_EXPORT -@interface FlutterEngine : NSObject +@interface FlutterEngine + : NSObject /** * Initializes an engine with the given project. diff --git a/shell/platform/darwin/macos/framework/Headers/FlutterMacOS.h b/shell/platform/darwin/macos/framework/Headers/FlutterMacOS.h index 5fc794b92995d..a7753e578a3fd 100644 --- a/shell/platform/darwin/macos/framework/Headers/FlutterMacOS.h +++ b/shell/platform/darwin/macos/framework/Headers/FlutterMacOS.h @@ -3,6 +3,7 @@ // found in the LICENSE file. #import "FlutterAppDelegate.h" +#import "FlutterAppLifecycleDelegate.h" #import "FlutterBinaryMessenger.h" #import "FlutterChannels.h" #import "FlutterCodecs.h" diff --git a/shell/platform/darwin/macos/framework/Headers/FlutterPluginMacOS.h b/shell/platform/darwin/macos/framework/Headers/FlutterPluginMacOS.h index cbedb0566a767..af8e0dd686a25 100644 --- a/shell/platform/darwin/macos/framework/Headers/FlutterPluginMacOS.h +++ b/shell/platform/darwin/macos/framework/Headers/FlutterPluginMacOS.h @@ -8,8 +8,7 @@ #import "FlutterCodecs.h" #import "FlutterMacros.h" -// TODO: Merge this file and FlutterPluginRegistrarMacOS.h with the iOS FlutterPlugin.h, sharing -// all but the platform-specific methods. +NS_ASSUME_NONNULL_BEGIN @protocol FlutterPluginRegistrar; @@ -29,7 +28,7 @@ FLUTTER_DARWIN_EXPORT * Creates an instance of the plugin to register with |registrar| using the desired * FlutterPluginRegistrar methods. */ -+ (void)registerWithRegistrar:(nonnull id)registrar; ++ (void)registerWithRegistrar:(id)registrar; @optional @@ -44,6 +43,8 @@ FLUTTER_DARWIN_EXPORT * - Any other value (including nil) to indicate success. The value will * be returned to the Flutter caller, and must be serializable to JSON. */ -- (void)handleMethodCall:(nonnull FlutterMethodCall*)call result:(nonnull FlutterResult)result; +- (void)handleMethodCall:(FlutterMethodCall*)call result:(FlutterResult)result; + +NS_ASSUME_NONNULL_END @end diff --git a/shell/platform/darwin/macos/framework/Source/FlutterAppDelegate.mm b/shell/platform/darwin/macos/framework/Source/FlutterAppDelegate.mm index e934c0a6b663e..782266419edba 100644 --- a/shell/platform/darwin/macos/framework/Source/FlutterAppDelegate.mm +++ b/shell/platform/darwin/macos/framework/Source/FlutterAppDelegate.mm @@ -8,6 +8,7 @@ #import #include "flutter/fml/logging.h" +#import "flutter/shell/platform/darwin/macos/framework/Headers/FlutterAppLifecycleDelegate.h" #include "flutter/shell/platform/embedder/embedder.h" @interface FlutterAppDelegate () @@ -17,17 +18,15 @@ @interface FlutterAppDelegate () */ - (NSString*)applicationName; +@property(nonatomic) FlutterAppLifecycleRegistrar* lifecycleRegistrar; @end @implementation FlutterAppDelegate -// TODO(gspencergoog): Implement application lifecycle forwarding to plugins here, as is done -// on iOS. Currently macOS plugins don't have access to lifecycle messages. -// https://github.com/flutter/flutter/issues/30735 - - (instancetype)init { if (self = [super init]) { _terminationHandler = nil; + _lifecycleRegistrar = [[FlutterAppLifecycleRegistrar alloc] init]; } return self; } @@ -42,6 +41,16 @@ - (void)applicationWillFinishLaunching:(NSNotification*)notification { } } +#pragma mark - Delegate handling + +- (void)addApplicationLifecycleDelegate:(NSObject*)delegate { + [[self lifecycleRegistrar] addDelegate:delegate]; +} + +- (void)removeApplicationLifecycleDelegate:(NSObject*)delegate { + [[self lifecycleRegistrar] removeDelegate:delegate]; +} + #pragma mark Private Methods - (NSString*)applicationName { diff --git a/shell/platform/darwin/macos/framework/Source/FlutterAppLifecycleDelegate.mm b/shell/platform/darwin/macos/framework/Source/FlutterAppLifecycleDelegate.mm new file mode 100644 index 0000000000000..4b552b94ca1a0 --- /dev/null +++ b/shell/platform/darwin/macos/framework/Source/FlutterAppLifecycleDelegate.mm @@ -0,0 +1,130 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#import "flutter/shell/platform/darwin/macos/framework/Headers/FlutterAppLifecycleDelegate.h" + +#include +#include +#include +#include + +#include "flutter/fml/logging.h" +#include "flutter/fml/paths.h" + +@interface FlutterAppLifecycleRegistrar () +@end + +@implementation FlutterAppLifecycleRegistrar { + NSMutableArray* _notificationUnsubscribers; + + // Weak references to registered plugins. + NSPointerArray* _delegates; +} + +- (void)addObserverFor:(NSString*)name selector:(SEL)selector { + [[NSNotificationCenter defaultCenter] addObserver:self selector:selector name:name object:nil]; + __block NSObject* blockSelf = self; + dispatch_block_t unsubscribe = ^{ + [[NSNotificationCenter defaultCenter] removeObserver:blockSelf name:name object:nil]; + }; + [_notificationUnsubscribers addObject:[unsubscribe copy]]; +} + +- (instancetype)init { + if (self = [super init]) { + _notificationUnsubscribers = [[NSMutableArray alloc] init]; + +// Using a macro to avoid errors where the notification doesn't match the +// selector. +#ifdef OBSERVE_NOTIFICATION +#error OBSERVE_NOTIFICATION ALREADY DEFINED! +#else +#define OBSERVE_NOTIFICATION(SELECTOR) \ + [self addObserverFor:NSApplication##SELECTOR##Notification selector:@selector(handle##SELECTOR:)] +#endif + + OBSERVE_NOTIFICATION(WillFinishLaunching); + OBSERVE_NOTIFICATION(DidFinishLaunching); + OBSERVE_NOTIFICATION(WillBecomeActive); + OBSERVE_NOTIFICATION(DidBecomeActive); + OBSERVE_NOTIFICATION(WillResignActive); + OBSERVE_NOTIFICATION(DidResignActive); + OBSERVE_NOTIFICATION(WillTerminate); + OBSERVE_NOTIFICATION(WillHide); + OBSERVE_NOTIFICATION(DidHide); + OBSERVE_NOTIFICATION(WillUnhide); + OBSERVE_NOTIFICATION(DidUnhide); + OBSERVE_NOTIFICATION(DidChangeScreenParameters); + OBSERVE_NOTIFICATION(DidChangeOcclusionState); + +#undef OBSERVE_NOTIFICATION + + _delegates = [NSPointerArray weakObjectsPointerArray]; + } + return self; +} + +- (void)dealloc { + for (dispatch_block_t unsubscribe in _notificationUnsubscribers) { + unsubscribe(); + } + [_notificationUnsubscribers removeAllObjects]; + _delegates = nil; + _notificationUnsubscribers = nil; +} + +static BOOL IsPowerOfTwo(NSUInteger x) { + return x != 0 && (x & (x - 1)) == 0; +} + +- (void)addDelegate:(NSObject*)delegate { + [_delegates addPointer:(__bridge void*)delegate]; + if (IsPowerOfTwo([_delegates count])) { + [_delegates compact]; + } +} + +- (void)removeDelegate:(NSObject*)delegate { + NSUInteger index = [[_delegates allObjects] indexOfObject:delegate]; + if (index >= 0) { + [_delegates removePointerAtIndex:index]; + } +} + +// This isn't done via performSelector because that can cause leaks due to the +// selector not being known. Using a macro to avoid mismatch errors between the +// notification and the selector. +#ifdef DISTRIBUTE_NOTIFICATION +#error DISTRIBUTE_NOTIFICATION ALREADY DEFINED! +#else +#define DISTRIBUTE_NOTIFICATION(SELECTOR) \ + -(void)handle##SELECTOR : (NSNotification*)notification { \ + for (NSObject * delegate in _delegates) { \ + if (!delegate) { \ + continue; \ + } \ + if ([delegate respondsToSelector:@selector(handle##SELECTOR:)]) { \ + [delegate handle##SELECTOR:notification]; \ + } \ + } \ + } +#endif + +DISTRIBUTE_NOTIFICATION(WillFinishLaunching) +DISTRIBUTE_NOTIFICATION(DidFinishLaunching) +DISTRIBUTE_NOTIFICATION(WillBecomeActive) +DISTRIBUTE_NOTIFICATION(DidBecomeActive) +DISTRIBUTE_NOTIFICATION(WillResignActive) +DISTRIBUTE_NOTIFICATION(DidResignActive) +DISTRIBUTE_NOTIFICATION(WillTerminate) +DISTRIBUTE_NOTIFICATION(WillHide) +DISTRIBUTE_NOTIFICATION(WillUnhide) +DISTRIBUTE_NOTIFICATION(DidHide) +DISTRIBUTE_NOTIFICATION(DidUnhide) +DISTRIBUTE_NOTIFICATION(DidChangeScreenParameters) +DISTRIBUTE_NOTIFICATION(DidChangeOcclusionState) + +#undef DISTRIBUTE_NOTIFICATION + +@end diff --git a/shell/platform/darwin/macos/framework/Source/FlutterAppLifecycleDelegateTest.mm b/shell/platform/darwin/macos/framework/Source/FlutterAppLifecycleDelegateTest.mm new file mode 100644 index 0000000000000..079ddbb7ff26b --- /dev/null +++ b/shell/platform/darwin/macos/framework/Source/FlutterAppLifecycleDelegateTest.mm @@ -0,0 +1,223 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#import "flutter/shell/platform/darwin/macos/framework/Headers/FlutterAppLifecycleDelegate.h" + +#import "flutter/testing/testing.h" +#include "third_party/googletest/googletest/include/gtest/gtest.h" + +@interface TestFlutterAppLifecycleDelegate : NSObject +@property(nonatomic, readwrite, nullable) NSNotification* lastNotification; +@end + +@implementation TestFlutterAppLifecycleDelegate + +- (void)setNotification:(NSNotification*)notification { + self.lastNotification = notification; +} + +- (void)handleWillFinishLaunching:(NSNotification*)notification { + [self setNotification:notification]; +} + +- (void)handleDidFinishLaunching:(NSNotification*)notification { + [self setNotification:notification]; +} + +- (void)handleWillBecomeActive:(NSNotification*)notification { + [self setNotification:notification]; +} + +- (void)handleDidBecomeActive:(NSNotification*)notification { + [self setNotification:notification]; +} + +- (void)handleWillResignActive:(NSNotification*)notification { + [self setNotification:notification]; +} + +- (void)handleDidResignActive:(NSNotification*)notification { + [self setNotification:notification]; +} + +- (void)handleWillHide:(NSNotification*)notification { + [self setNotification:notification]; +} + +- (void)handleDidHide:(NSNotification*)notification { + [self setNotification:notification]; +} + +- (void)handleWillUnhide:(NSNotification*)notification { + [self setNotification:notification]; +} + +- (void)handleDidUnhide:(NSNotification*)notification { + [self setNotification:notification]; +} + +- (void)handleDidChangeScreenParameters:(NSNotification*)notification { + [self setNotification:notification]; +} + +- (void)handleDidChangeOcclusionState:(NSNotification*)notification API_AVAILABLE(macos(10.9)) { + [self setNotification:notification]; +} + +- (void)handleWillTerminate:(NSNotification*)notification { + [self setNotification:notification]; +} + +@end + +namespace flutter::testing { + +TEST(FlutterAppLifecycleDelegateTest, RespondsToWillFinishLaunching) { + FlutterAppLifecycleRegistrar* registrar = [[FlutterAppLifecycleRegistrar alloc] init]; + TestFlutterAppLifecycleDelegate* delegate = [[TestFlutterAppLifecycleDelegate alloc] init]; + [registrar addDelegate:delegate]; + + NSNotification* willFinishLaunching = + [NSNotification notificationWithName:NSApplicationWillFinishLaunchingNotification object:nil]; + [registrar handleWillFinishLaunching:willFinishLaunching]; + EXPECT_EQ([delegate lastNotification], willFinishLaunching); +} + +TEST(FlutterAppLifecycleDelegateTest, RespondsToDidFinishLaunching) { + FlutterAppLifecycleRegistrar* registrar = [[FlutterAppLifecycleRegistrar alloc] init]; + TestFlutterAppLifecycleDelegate* delegate = [[TestFlutterAppLifecycleDelegate alloc] init]; + [registrar addDelegate:delegate]; + + NSNotification* didFinishLaunching = + [NSNotification notificationWithName:NSApplicationDidFinishLaunchingNotification object:nil]; + [registrar handleDidFinishLaunching:didFinishLaunching]; + EXPECT_EQ([delegate lastNotification], didFinishLaunching); +} + +TEST(FlutterAppLifecycleDelegateTest, RespondsToWillBecomeActive) { + FlutterAppLifecycleRegistrar* registrar = [[FlutterAppLifecycleRegistrar alloc] init]; + TestFlutterAppLifecycleDelegate* delegate = [[TestFlutterAppLifecycleDelegate alloc] init]; + [registrar addDelegate:delegate]; + + NSNotification* willBecomeActive = + [NSNotification notificationWithName:NSApplicationWillBecomeActiveNotification object:nil]; + [registrar handleWillBecomeActive:willBecomeActive]; + EXPECT_EQ([delegate lastNotification], willBecomeActive); +} + +TEST(FlutterAppLifecycleDelegateTest, RespondsToDidBecomeActive) { + FlutterAppLifecycleRegistrar* registrar = [[FlutterAppLifecycleRegistrar alloc] init]; + TestFlutterAppLifecycleDelegate* delegate = [[TestFlutterAppLifecycleDelegate alloc] init]; + [registrar addDelegate:delegate]; + + NSNotification* didBecomeActive = + [NSNotification notificationWithName:NSApplicationDidBecomeActiveNotification object:nil]; + [registrar handleDidBecomeActive:didBecomeActive]; + EXPECT_EQ([delegate lastNotification], didBecomeActive); +} + +TEST(FlutterAppLifecycleDelegateTest, RespondsToWillResignActive) { + FlutterAppLifecycleRegistrar* registrar = [[FlutterAppLifecycleRegistrar alloc] init]; + TestFlutterAppLifecycleDelegate* delegate = [[TestFlutterAppLifecycleDelegate alloc] init]; + [registrar addDelegate:delegate]; + + NSNotification* willResignActive = + [NSNotification notificationWithName:NSApplicationWillResignActiveNotification object:nil]; + [registrar handleWillResignActive:willResignActive]; + EXPECT_EQ([delegate lastNotification], willResignActive); +} + +TEST(FlutterAppLifecycleDelegateTest, RespondsToDidResignActive) { + FlutterAppLifecycleRegistrar* registrar = [[FlutterAppLifecycleRegistrar alloc] init]; + TestFlutterAppLifecycleDelegate* delegate = [[TestFlutterAppLifecycleDelegate alloc] init]; + [registrar addDelegate:delegate]; + + NSNotification* didResignActive = + [NSNotification notificationWithName:NSApplicationDidResignActiveNotification object:nil]; + [registrar handleDidResignActive:didResignActive]; + EXPECT_EQ([delegate lastNotification], didResignActive); +} + +TEST(FlutterAppLifecycleDelegateTest, RespondsToWillTerminate) { + FlutterAppLifecycleRegistrar* registrar = [[FlutterAppLifecycleRegistrar alloc] init]; + TestFlutterAppLifecycleDelegate* delegate = [[TestFlutterAppLifecycleDelegate alloc] init]; + [registrar addDelegate:delegate]; + + NSNotification* applicationWillTerminate = + [NSNotification notificationWithName:NSApplicationWillTerminateNotification object:nil]; + [registrar handleWillTerminate:applicationWillTerminate]; + EXPECT_EQ([delegate lastNotification], applicationWillTerminate); +} + +TEST(FlutterAppLifecycleDelegateTest, RespondsToWillHide) { + FlutterAppLifecycleRegistrar* registrar = [[FlutterAppLifecycleRegistrar alloc] init]; + TestFlutterAppLifecycleDelegate* delegate = [[TestFlutterAppLifecycleDelegate alloc] init]; + [registrar addDelegate:delegate]; + + NSNotification* willHide = [NSNotification notificationWithName:NSApplicationWillHideNotification + object:nil]; + [registrar handleWillHide:willHide]; + EXPECT_EQ([delegate lastNotification], willHide); +} + +TEST(FlutterAppLifecycleDelegateTest, RespondsToWillUnhide) { + FlutterAppLifecycleRegistrar* registrar = [[FlutterAppLifecycleRegistrar alloc] init]; + TestFlutterAppLifecycleDelegate* delegate = [[TestFlutterAppLifecycleDelegate alloc] init]; + [registrar addDelegate:delegate]; + + NSNotification* willUnhide = + [NSNotification notificationWithName:NSApplicationWillUnhideNotification object:nil]; + [registrar handleWillUnhide:willUnhide]; + EXPECT_EQ([delegate lastNotification], willUnhide); +} + +TEST(FlutterAppLifecycleDelegateTest, RespondsToDidHide) { + FlutterAppLifecycleRegistrar* registrar = [[FlutterAppLifecycleRegistrar alloc] init]; + TestFlutterAppLifecycleDelegate* delegate = [[TestFlutterAppLifecycleDelegate alloc] init]; + [registrar addDelegate:delegate]; + + NSNotification* didHide = [NSNotification notificationWithName:NSApplicationDidHideNotification + object:nil]; + [registrar handleDidHide:didHide]; + EXPECT_EQ([delegate lastNotification], didHide); +} + +TEST(FlutterAppLifecycleDelegateTest, RespondsToDidUnhide) { + FlutterAppLifecycleRegistrar* registrar = [[FlutterAppLifecycleRegistrar alloc] init]; + TestFlutterAppLifecycleDelegate* delegate = [[TestFlutterAppLifecycleDelegate alloc] init]; + [registrar addDelegate:delegate]; + + NSNotification* didUnhide = + [NSNotification notificationWithName:NSApplicationDidUnhideNotification object:nil]; + [registrar handleDidUnhide:didUnhide]; + EXPECT_EQ([delegate lastNotification], didUnhide); +} + +TEST(FlutterAppLifecycleDelegateTest, RespondsToDidChangeScreenParameters) { + FlutterAppLifecycleRegistrar* registrar = [[FlutterAppLifecycleRegistrar alloc] init]; + TestFlutterAppLifecycleDelegate* delegate = [[TestFlutterAppLifecycleDelegate alloc] init]; + [registrar addDelegate:delegate]; + + NSNotification* didChangeScreenParameters = + [NSNotification notificationWithName:NSApplicationDidChangeScreenParametersNotification + object:nil]; + [registrar handleDidChangeScreenParameters:didChangeScreenParameters]; + EXPECT_EQ([delegate lastNotification], didChangeScreenParameters); +} + +TEST(FlutterAppLifecycleDelegateTest, RespondsToDidChangeOcclusionState) { + FlutterAppLifecycleRegistrar* registrar = [[FlutterAppLifecycleRegistrar alloc] init]; + TestFlutterAppLifecycleDelegate* delegate = [[TestFlutterAppLifecycleDelegate alloc] init]; + [registrar addDelegate:delegate]; + + NSNotification* didChangeOcclusionState = + [NSNotification notificationWithName:NSApplicationDidChangeOcclusionStateNotification + object:nil]; + if ([registrar respondsToSelector:@selector(handleDidChangeOcclusionState:)]) { + [registrar handleDidChangeOcclusionState:didChangeOcclusionState]; + EXPECT_EQ([delegate lastNotification], didChangeOcclusionState); + } +} + +} // namespace flutter::testing diff --git a/shell/platform/darwin/macos/framework/Source/FlutterEngine.mm b/shell/platform/darwin/macos/framework/Source/FlutterEngine.mm index d4f194cfca01e..2be2f6b70af8f 100644 --- a/shell/platform/darwin/macos/framework/Source/FlutterEngine.mm +++ b/shell/platform/darwin/macos/framework/Source/FlutterEngine.mm @@ -9,6 +9,7 @@ #include #include +#include "flutter/shell/platform/common/app_lifecycle_state.h" #include "flutter/shell/platform/common/engine_switches.h" #include "flutter/shell/platform/embedder/embedder.h" @@ -24,6 +25,8 @@ #import "flutter/shell/platform/darwin/macos/framework/Source/FlutterViewEngineProvider.h" NSString* const kFlutterPlatformChannel = @"flutter/platform"; +NSString* const kFlutterSettingsChannel = @"flutter/settings"; +NSString* const kFlutterLifecycleChannel = @"flutter/lifecycle"; /** * Constructs and returns a FlutterLocale struct corresponding to |locale|, which must outlive @@ -173,8 +176,7 @@ - (instancetype)initWithEngine:(FlutterEngine*)engine _terminator = terminator ? terminator : ^(id sender) { // Default to actually terminating the application. The terminator exists to // allow tests to override it so that an actual exit doesn't occur. - NSApplication* flutterApp = [NSApplication sharedApplication]; - [flutterApp terminate:sender]; + [[NSApplication sharedApplication] terminate:sender]; }; FlutterAppDelegate* appDelegate = (FlutterAppDelegate*)[[NSApplication sharedApplication] delegate]; @@ -390,7 +392,14 @@ @implementation FlutterEngine { FlutterThreadSynchronizer* _threadSynchronizer; + // The next available view ID. int _nextViewId; + + // Whether the application is currently the active application. + BOOL _active; + + // Whether any portion of the application is currently visible. + BOOL _visible; } - (instancetype)initWithName:(NSString*)labelPrefix project:(FlutterDartProject*)project { @@ -402,7 +411,8 @@ - (instancetype)initWithName:(NSString*)labelPrefix allowHeadlessExecution:(BOOL)allowHeadlessExecution { self = [super init]; NSAssert(self, @"Super init cannot be nil"); - + _active = NO; + _visible = NO; _project = project ?: [[FlutterDartProject alloc] init]; _messengerHandlers = [[NSMutableDictionary alloc] init]; _currentMessengerConnection = 1; @@ -433,11 +443,19 @@ - (instancetype)initWithName:(NSString*)labelPrefix [self setUpPlatformViewChannel]; [self setUpAccessibilityChannel]; [self setUpNotificationCenterListeners]; + FlutterAppDelegate* appDelegate = + reinterpret_cast([[NSApplication sharedApplication] delegate]); + [appDelegate addApplicationLifecycleDelegate:self]; return self; } - (void)dealloc { + FlutterAppDelegate* appDelegate = + reinterpret_cast([[NSApplication sharedApplication] delegate]); + if (appDelegate != nil) { + [appDelegate removeApplicationLifecycleDelegate:self]; + } @synchronized(_isResponseValid) { [_isResponseValid removeAllObjects]; [_isResponseValid addObject:@NO]; @@ -997,11 +1015,11 @@ - (void)addInternalPlugins { [FlutterMouseCursorPlugin registerWithRegistrar:[self registrarForPlugin:@"mousecursor"]]; [FlutterMenuPlugin registerWithRegistrar:[self registrarForPlugin:@"menu"]]; _settingsChannel = - [FlutterBasicMessageChannel messageChannelWithName:@"flutter/settings" + [FlutterBasicMessageChannel messageChannelWithName:kFlutterSettingsChannel binaryMessenger:self.binaryMessenger codec:[FlutterJSONMessageCodec sharedInstance]]; _platformChannel = - [FlutterMethodChannel methodChannelWithName:@"flutter/platform" + [FlutterMethodChannel methodChannelWithName:kFlutterPlatformChannel binaryMessenger:self.binaryMessenger codec:[FlutterJSONMethodCodec sharedInstance]]; [_platformChannel setMethodCallHandler:^(FlutterMethodCall* call, FlutterResult result) { @@ -1059,7 +1077,7 @@ - (void)announceAccessibilityMessage:(NSString*)message } - (void)handleMethodCall:(FlutterMethodCall*)call result:(FlutterResult)result { if ([call.method isEqualToString:@"SystemNavigator.pop"]) { - [NSApp terminate:self]; + [[NSApplication sharedApplication] terminate:self]; result(nil); } else if ([call.method isEqualToString:@"SystemSound.play"]) { [self playSystemSound:call.arguments]; @@ -1121,6 +1139,60 @@ - (FlutterThreadSynchronizer*)testThreadSynchronizer { return _threadSynchronizer; } +#pragma mark - FlutterAppLifecycleDelegate + +- (void)setApplicationState:(flutter::AppLifecycleState)state { + NSString* nextState = + [[NSString alloc] initWithCString:flutter::AppLifecycleStateToString(state)]; + [self sendOnChannel:kFlutterLifecycleChannel + message:[nextState dataUsingEncoding:NSUTF8StringEncoding]]; +} + +/** + * Called when the |FlutterAppDelegate| gets the applicationWillBecomeActive + * notification. + */ +- (void)handleWillBecomeActive:(NSNotification*)notification { + _active = YES; + if (!_visible) { + [self setApplicationState:flutter::AppLifecycleState::kHidden]; + } else { + [self setApplicationState:flutter::AppLifecycleState::kResumed]; + } +} + +/** + * Called when the |FlutterAppDelegate| gets the applicationWillResignActive + * notification. + */ +- (void)handleWillResignActive:(NSNotification*)notification { + _active = NO; + if (!_visible) { + [self setApplicationState:flutter::AppLifecycleState::kHidden]; + } else { + [self setApplicationState:flutter::AppLifecycleState::kInactive]; + } +} + +/** + * Called when the |FlutterAppDelegate| gets the applicationDidUnhide + * notification. + */ +- (void)handleDidChangeOcclusionState:(NSNotification*)notification API_AVAILABLE(macos(10.9)) { + NSApplicationOcclusionState occlusionState = [[NSApplication sharedApplication] occlusionState]; + if (occlusionState & NSApplicationOcclusionStateVisible) { + _visible = YES; + if (_active) { + [self setApplicationState:flutter::AppLifecycleState::kResumed]; + } else { + [self setApplicationState:flutter::AppLifecycleState::kInactive]; + } + } else { + _visible = NO; + [self setApplicationState:flutter::AppLifecycleState::kHidden]; + } +} + #pragma mark - FlutterBinaryMessenger - (void)sendOnChannel:(nonnull NSString*)channel message:(nullable NSData*)message { diff --git a/shell/platform/darwin/macos/framework/Source/FlutterEngineTest.mm b/shell/platform/darwin/macos/framework/Source/FlutterEngineTest.mm index cf81a8ad798c2..60e8bc363f8df 100644 --- a/shell/platform/darwin/macos/framework/Source/FlutterEngineTest.mm +++ b/shell/platform/darwin/macos/framework/Source/FlutterEngineTest.mm @@ -805,55 +805,75 @@ - (nonnull NSView*)createWithViewIdentifier:(int64_t)viewId arguments:(nullable EXPECT_TRUE(announced); } -TEST_F(FlutterEngineTest, RunWithEntrypointUpdatesDisplayConfig) { - BOOL updated = NO; - FlutterEngine* engine = GetFlutterEngine(); - auto original_update_displays = engine.embedderAPI.NotifyDisplayUpdate; - engine.embedderAPI.NotifyDisplayUpdate = MOCK_ENGINE_PROC( - NotifyDisplayUpdate, ([&updated, &original_update_displays]( - auto engine, auto update_type, auto* displays, auto display_count) { - updated = YES; - return original_update_displays(engine, update_type, displays, display_count); +TEST_F(FlutterEngineTest, HandleLifecycleStates) API_AVAILABLE(macos(10.9)) { + __block flutter::AppLifecycleState sentState; + id engineMock = CreateMockFlutterEngine(nil); + + // Have to enumerate all the values because OCMStub can't capture + // non-Objective-C object arguments. + OCMStub([engineMock setApplicationState:flutter::AppLifecycleState::kDetached]) + .andDo((^(NSInvocation* invocation) { + sentState = flutter::AppLifecycleState::kDetached; + })); + OCMStub([engineMock setApplicationState:flutter::AppLifecycleState::kResumed]) + .andDo((^(NSInvocation* invocation) { + sentState = flutter::AppLifecycleState::kResumed; + })); + OCMStub([engineMock setApplicationState:flutter::AppLifecycleState::kInactive]) + .andDo((^(NSInvocation* invocation) { + sentState = flutter::AppLifecycleState::kInactive; + })); + OCMStub([engineMock setApplicationState:flutter::AppLifecycleState::kHidden]) + .andDo((^(NSInvocation* invocation) { + sentState = flutter::AppLifecycleState::kHidden; + })); + OCMStub([engineMock setApplicationState:flutter::AppLifecycleState::kPaused]) + .andDo((^(NSInvocation* invocation) { + sentState = flutter::AppLifecycleState::kPaused; })); - EXPECT_TRUE([engine runWithEntrypoint:@"main"]); - EXPECT_TRUE(updated); + __block NSApplicationOcclusionState visibility = NSApplicationOcclusionStateVisible; + id mockApplication = OCMPartialMock([NSApplication sharedApplication]); + OCMStub((NSApplicationOcclusionState)[mockApplication occlusionState]) + .andDo(^(NSInvocation* invocation) { + [invocation setReturnValue:&visibility]; + }); - updated = NO; - [[NSNotificationCenter defaultCenter] - postNotificationName:NSApplicationDidChangeScreenParametersNotification - object:nil]; - EXPECT_TRUE(updated); -} + NSNotification* willBecomeActive = + [[NSNotification alloc] initWithName:NSApplicationWillBecomeActiveNotification + object:nil + userInfo:nil]; + NSNotification* willResignActive = + [[NSNotification alloc] initWithName:NSApplicationWillResignActiveNotification + object:nil + userInfo:nil]; -TEST_F(FlutterEngineTest, NotificationsUpdateDisplays) { - BOOL updated = NO; - FlutterEngine* engine = GetFlutterEngine(); - auto original_set_viewport_metrics = engine.embedderAPI.SendWindowMetricsEvent; - engine.embedderAPI.SendWindowMetricsEvent = MOCK_ENGINE_PROC( - SendWindowMetricsEvent, - ([&updated, &original_set_viewport_metrics](auto engine, auto* window_metrics) { - updated = YES; - return original_set_viewport_metrics(engine, window_metrics); - })); + NSNotification* didChangeOcclusionState; + didChangeOcclusionState = + [[NSNotification alloc] initWithName:NSApplicationDidChangeOcclusionStateNotification + object:nil + userInfo:nil]; - EXPECT_TRUE([engine runWithEntrypoint:@"main"]); + [engineMock handleDidChangeOcclusionState:didChangeOcclusionState]; + EXPECT_EQ(sentState, flutter::AppLifecycleState::kInactive); - updated = NO; - [[NSNotificationCenter defaultCenter] postNotificationName:NSWindowDidChangeScreenNotification - object:nil]; - // No VC. - EXPECT_FALSE(updated); + [engineMock handleWillBecomeActive:willBecomeActive]; + EXPECT_EQ(sentState, flutter::AppLifecycleState::kResumed); - FlutterViewController* viewController = [[FlutterViewController alloc] initWithEngine:engine - nibName:nil - bundle:nil]; - [viewController loadView]; - viewController.flutterView.frame = CGRectMake(0, 0, 800, 600); + [engineMock handleWillResignActive:willResignActive]; + EXPECT_EQ(sentState, flutter::AppLifecycleState::kInactive); + + visibility = 0; + [engineMock handleDidChangeOcclusionState:didChangeOcclusionState]; + EXPECT_EQ(sentState, flutter::AppLifecycleState::kHidden); + + [engineMock handleWillBecomeActive:willBecomeActive]; + EXPECT_EQ(sentState, flutter::AppLifecycleState::kHidden); + + [engineMock handleWillResignActive:willResignActive]; + EXPECT_EQ(sentState, flutter::AppLifecycleState::kHidden); - [[NSNotificationCenter defaultCenter] postNotificationName:NSWindowDidChangeScreenNotification - object:nil]; - EXPECT_TRUE(updated); + [mockApplication stopMocking]; } } // namespace flutter::testing diff --git a/shell/platform/darwin/macos/framework/Source/FlutterEngine_Internal.h b/shell/platform/darwin/macos/framework/Source/FlutterEngine_Internal.h index be973222ca95d..71fe5481cf72b 100644 --- a/shell/platform/darwin/macos/framework/Source/FlutterEngine_Internal.h +++ b/shell/platform/darwin/macos/framework/Source/FlutterEngine_Internal.h @@ -8,6 +8,8 @@ #include +#include "flutter/shell/platform/common/app_lifecycle_state.h" + #import "flutter/shell/platform/darwin/macos/framework/Source/AccessibilityBridgeMac.h" #import "flutter/shell/platform/darwin/macos/framework/Source/FlutterCompositor.h" #import "flutter/shell/platform/darwin/macos/framework/Source/FlutterPlatformViewController.h" @@ -172,6 +174,14 @@ typedef NS_ENUM(NSInteger, FlutterAppExitResponse) { - (nonnull FlutterPlatformViewController*)platformViewController; +/** + * Handles changes to the application state, sending them to the framework. + * + * @param state One of the lifecycle constants in app_lifecycle_state.h, + * corresponding to the Dart enum AppLifecycleState. + */ +- (void)setApplicationState:(flutter::AppLifecycleState)state; + // Accessibility API. /** diff --git a/shell/platform/linux/BUILD.gn b/shell/platform/linux/BUILD.gn index 5cb9f2839aaa8..4af1f89fe6bd3 100644 --- a/shell/platform/linux/BUILD.gn +++ b/shell/platform/linux/BUILD.gn @@ -158,6 +158,7 @@ source_set("flutter_linux_sources") { ] deps = [ + "//flutter/shell/platform/common:common_cpp_enums", "//flutter/shell/platform/common:common_cpp_input", "//flutter/shell/platform/common:common_cpp_switches", "//flutter/shell/platform/embedder:embedder_headers", @@ -257,6 +258,7 @@ executable("flutter_linux_unittests") { ":flutter_linux_gschemas", ":flutter_linux_sources", "//flutter/runtime:libdart", + "//flutter/shell/platform/common:common_cpp_enums", "//flutter/shell/platform/embedder:embedder_headers", "//flutter/shell/platform/embedder:embedder_test_utils", "//flutter/testing", diff --git a/shell/platform/linux/fl_engine.cc b/shell/platform/linux/fl_engine.cc index 0ae331b328375..507d9e2271470 100644 --- a/shell/platform/linux/fl_engine.cc +++ b/shell/platform/linux/fl_engine.cc @@ -10,6 +10,7 @@ #include #include +#include "flutter/shell/platform/common/app_lifecycle_state.h" #include "flutter/shell/platform/common/engine_switches.h" #include "flutter/shell/platform/embedder/embedder.h" #include "flutter/shell/platform/linux/fl_binary_messenger_private.h" @@ -23,6 +24,7 @@ #include "flutter/shell/platform/linux/fl_texture_gl_private.h" #include "flutter/shell/platform/linux/fl_texture_registrar_private.h" #include "flutter/shell/platform/linux/public/flutter_linux/fl_plugin_registry.h" +#include "flutter/shell/platform/linux/public/flutter_linux/fl_string_codec.h" // Unique number associated with platform tasks. static constexpr size_t kPlatformTaskRunnerIdentifier = 1; @@ -32,6 +34,8 @@ static constexpr size_t kPlatformTaskRunnerIdentifier = 1; static constexpr int32_t kMousePointerDeviceId = 0; static constexpr int32_t kPointerPanZoomDeviceId = 1; +static constexpr const char* kFlutterLifecycleChannel = "flutter/lifecycle"; + struct _FlEngine { GObject parent_instance; @@ -122,6 +126,25 @@ static void parse_locale(const gchar* locale, } } +static void set_app_lifecycle_state(FlEngine* self, + const flutter::AppLifecycleState state) { + FlBinaryMessenger* binary_messenger = fl_engine_get_binary_messenger(self); + + g_autoptr(FlValue) value = + fl_value_new_string(flutter::AppLifecycleStateToString(state)); + g_autoptr(FlStringCodec) codec = fl_string_codec_new(); + g_autoptr(GBytes) message = + fl_message_codec_encode_message(FL_MESSAGE_CODEC(codec), value, nullptr); + + if (message == nullptr) { + return; + } + + fl_binary_messenger_send_on_channel(binary_messenger, + kFlutterLifecycleChannel, message, + nullptr, nullptr, nullptr); +} + // Passes locale information to the Flutter engine. static void setup_locales(FlEngine* self) { const gchar* const* languages = g_get_language_names(); @@ -721,6 +744,18 @@ GBytes* fl_engine_send_platform_message_finish(FlEngine* self, return static_cast(g_task_propagate_pointer(G_TASK(result), error)); } +void fl_engine_send_window_state_event(FlEngine* self, + gboolean visible, + gboolean focused) { + if (visible && focused) { + set_app_lifecycle_state(self, flutter::AppLifecycleState::kResumed); + } else if (visible) { + set_app_lifecycle_state(self, flutter::AppLifecycleState::kInactive); + } else { + set_app_lifecycle_state(self, flutter::AppLifecycleState::kHidden); + } +} + void fl_engine_send_window_metrics_event(FlEngine* self, size_t width, size_t height, diff --git a/shell/platform/linux/fl_engine_private.h b/shell/platform/linux/fl_engine_private.h index 0d1dd1c958436..4b0227a62750a 100644 --- a/shell/platform/linux/fl_engine_private.h +++ b/shell/platform/linux/fl_engine_private.h @@ -167,6 +167,18 @@ void fl_engine_send_window_metrics_event(FlEngine* engine, size_t height, double pixel_ratio); +/** + * fl_engine_send_window_state_event: + * @engine: an #FlEngine. + * @visible: whether the window is currently visible or not. + * @focused: whether the window is currently focused or not. + * + * Sends a window state event to the engine. + */ +void fl_engine_send_window_state_event(FlEngine* engine, + gboolean visible, + gboolean focused); + /** * fl_engine_send_mouse_pointer_event: * @engine: an #FlEngine. diff --git a/shell/platform/linux/fl_engine_test.cc b/shell/platform/linux/fl_engine_test.cc index a1d6e1d650eaf..a7329a6997b0f 100644 --- a/shell/platform/linux/fl_engine_test.cc +++ b/shell/platform/linux/fl_engine_test.cc @@ -5,10 +5,12 @@ // Included first as it collides with the X11 headers. #include "gtest/gtest.h" +#include "flutter/shell/platform/common/app_lifecycle_state.h" #include "flutter/shell/platform/embedder/test_utils/proc_table_replacement.h" #include "flutter/shell/platform/linux/fl_engine_private.h" #include "flutter/shell/platform/linux/public/flutter_linux/fl_engine.h" #include "flutter/shell/platform/linux/public/flutter_linux/fl_json_message_codec.h" +#include "flutter/shell/platform/linux/public/flutter_linux/fl_string_codec.h" #include "flutter/shell/platform/linux/testing/fl_test.h" // MOCK_ENGINE_PROC is leaky by design @@ -423,6 +425,42 @@ TEST(FlEngineTest, SwitchesEmpty) { EXPECT_EQ(switches->len, 0U); } +TEST(FlEngineTest, SendWindowStateEvent) { + g_autoptr(FlEngine) engine = make_mock_engine(); + FlutterEngineProcTable* embedder_api = fl_engine_get_embedder_api(engine); + + bool called = false; + std::string state; + embedder_api->SendPlatformMessage = MOCK_ENGINE_PROC( + SendPlatformMessage, + ([&called, &state](auto engine, const FlutterPlatformMessage* message) { + EXPECT_STREQ(message->channel, "flutter/lifecycle"); + called = true; + g_autoptr(FlStringCodec) codec = fl_string_codec_new(); + g_autoptr(GBytes) data = + g_bytes_new(message->message, message->message_size); + g_autoptr(GError) error = nullptr; + g_autoptr(FlValue) parsed_state = fl_message_codec_decode_message( + FL_MESSAGE_CODEC(codec), data, &error); + + state = fl_value_get_string(parsed_state); + return kSuccess; + })); + fl_engine_send_window_state_event(engine, false, false); + EXPECT_STREQ(state.c_str(), flutter::AppLifecycleStateToString( + flutter::AppLifecycleState::kHidden)); + fl_engine_send_window_state_event(engine, false, true); + EXPECT_STREQ(state.c_str(), flutter::AppLifecycleStateToString( + flutter::AppLifecycleState::kHidden)); + fl_engine_send_window_state_event(engine, true, false); + EXPECT_STREQ(state.c_str(), flutter::AppLifecycleStateToString( + flutter::AppLifecycleState::kInactive)); + fl_engine_send_window_state_event(engine, true, true); + EXPECT_STREQ(state.c_str(), flutter::AppLifecycleStateToString( + flutter::AppLifecycleState::kResumed)); + EXPECT_TRUE(called); +} + #ifndef FLUTTER_RELEASE TEST(FlEngineTest, Switches) { g_autoptr(FlEngine) engine = make_mock_engine(); diff --git a/shell/platform/linux/fl_view.cc b/shell/platform/linux/fl_view.cc index 58f11ebf68b10..46540d3b60c43 100644 --- a/shell/platform/linux/fl_view.cc +++ b/shell/platform/linux/fl_view.cc @@ -42,6 +42,9 @@ struct _FlView { // Pointer button state recorded for sending status updates. int64_t button_state; + // Current state information for the window associated with this view. + GdkWindowState window_state; + // Flutter system channel handlers. FlAccessibilityPlugin* accessibility_plugin; FlKeyboardManager* keyboard_manager; @@ -59,7 +62,9 @@ struct _FlView { /* FlKeyboardViewDelegate related properties */ KeyboardLayoutNotifier keyboard_layout_notifier; GdkKeymap* keymap; - gulong keymap_keys_changed_cb_id; // Signal connection ID. + gulong keymap_keys_changed_cb_id; // Signal connection ID for + // keymap-keys-changed + gulong window_state_cb_id; // Signal connection ID for window-state-changed }; enum { kPropFlutterProject = 1, kPropLast }; @@ -235,6 +240,8 @@ static void on_pre_engine_restart_cb(FlEngine* engine, gpointer user_data) { g_clear_object(&self->scrolling_manager); init_keyboard(self); init_scrolling(self); + self->window_state = + gdk_window_get_state(gtk_widget_get_window(GTK_WIDGET(self))); } // Implements FlPluginRegistry::get_registrar_for_plugin. @@ -480,12 +487,42 @@ static void gesture_zoom_end_cb(GtkGestureZoom* gesture, fl_scrolling_manager_handle_zoom_end(self->scrolling_manager); } +static gboolean window_state_event_cb(GtkWidget* widget, + GdkEvent* event, + gpointer user_data) { + g_return_val_if_fail(FL_IS_VIEW(user_data), FALSE); + g_return_val_if_fail(FL_IS_ENGINE(FL_VIEW(user_data)->engine), FALSE); + FlView* self = FL_VIEW(user_data); + GdkWindowState state = event->window_state.new_window_state; + GdkWindowState previous_state = self->window_state; + self->window_state = state; + bool was_visible = !((previous_state & GDK_WINDOW_STATE_WITHDRAWN) || + (previous_state & GDK_WINDOW_STATE_ICONIFIED)); + bool is_visible = !((state & GDK_WINDOW_STATE_WITHDRAWN) || + (state & GDK_WINDOW_STATE_ICONIFIED)); + bool was_focused = (previous_state & GDK_WINDOW_STATE_FOCUSED); + bool is_focused = (state & GDK_WINDOW_STATE_FOCUSED); + if (was_visible != is_visible || was_focused != is_focused) { + if (self->engine != nullptr) { + fl_engine_send_window_state_event(FL_ENGINE(self->engine), is_visible, + is_focused); + } + } + return FALSE; +} + static void realize_cb(GtkWidget* widget) { FlView* self = FL_VIEW(widget); g_autoptr(GError) error = nullptr; // Handle requests by the user to close the application. GtkWidget* toplevel_window = gtk_widget_get_toplevel(GTK_WIDGET(self)); + + // Listen to window state changes. + self->window_state_cb_id = + g_signal_connect(toplevel_window, "window-state-event", + G_CALLBACK(window_state_event_cb), self); + g_signal_connect(toplevel_window, "delete-event", G_CALLBACK(window_delete_event_cb), self); @@ -624,6 +661,12 @@ static void fl_view_dispose(GObject* object) { nullptr); } + if (self->window_state_cb_id != 0) { + GtkWidget* toplevel_window = gtk_widget_get_toplevel(GTK_WIDGET(self)); + g_signal_handler_disconnect(toplevel_window, self->window_state_cb_id); + self->window_state_cb_id = 0; + } + g_clear_object(&self->project); g_clear_object(&self->renderer); g_clear_object(&self->engine); @@ -683,6 +726,8 @@ static void fl_view_class_init(FlViewClass* klass) { static void fl_view_init(FlView* self) { gtk_widget_set_can_focus(GTK_WIDGET(self), TRUE); + self->window_state = gdk_window_get_state( + gtk_widget_get_window(gtk_widget_get_toplevel(GTK_WIDGET(self)))); } G_MODULE_EXPORT FlView* fl_view_new(FlDartProject* project) { diff --git a/shell/platform/windows/BUILD.gn b/shell/platform/windows/BUILD.gn index db9053a34e453..0effa94ed4dc8 100644 --- a/shell/platform/windows/BUILD.gn +++ b/shell/platform/windows/BUILD.gn @@ -129,6 +129,7 @@ source_set("flutter_windows_source") { public_deps = [ "//flutter/fml:string_conversion", "//flutter/shell/platform/common:common_cpp_accessibility", + "//flutter/shell/platform/common:common_cpp_enums", ] deps = [ diff --git a/shell/platform/windows/flutter_windows_engine.cc b/shell/platform/windows/flutter_windows_engine.cc index 8d8f3f15d358d..c6c46d7301776 100644 --- a/shell/platform/windows/flutter_windows_engine.cc +++ b/shell/platform/windows/flutter_windows_engine.cc @@ -397,6 +397,7 @@ bool FlutterWindowsEngine::Run(std::string_view entrypoint) { displays.data(), displays.size()); SendSystemLocales(); + SetLifecycleState(flutter::AppLifecycleState::kResumed); settings_plugin_->StartWatching(); settings_plugin_->SendSettings(); @@ -562,6 +563,13 @@ void FlutterWindowsEngine::SetNextFrameCallback(fml::closure callback) { this); } +void FlutterWindowsEngine::SetLifecycleState(flutter::AppLifecycleState state) { + const char* state_name = flutter::AppLifecycleStateToString(state); + SendPlatformMessage("flutter/lifecycle", + reinterpret_cast(state_name), + strlen(state_name), nullptr, nullptr); +} + void FlutterWindowsEngine::SendSystemLocales() { std::vector languages = GetPreferredLanguageInfo(*windows_registry_); diff --git a/shell/platform/windows/flutter_windows_engine.h b/shell/platform/windows/flutter_windows_engine.h index 36d21f822df54..9fd7df9f4985a 100644 --- a/shell/platform/windows/flutter_windows_engine.h +++ b/shell/platform/windows/flutter_windows_engine.h @@ -16,6 +16,7 @@ #include "flutter/fml/closure.h" #include "flutter/fml/macros.h" #include "flutter/shell/platform/common/accessibility_bridge.h" +#include "flutter/shell/platform/common/app_lifecycle_state.h" #include "flutter/shell/platform/common/client_wrapper/binary_messenger_impl.h" #include "flutter/shell/platform/common/client_wrapper/include/flutter/basic_message_channel.h" #include "flutter/shell/platform/common/incoming_message_dispatcher.h" @@ -306,6 +307,9 @@ class FlutterWindowsEngine { // system changes. void SendSystemLocales(); + // Sends the current lifecycle state to the framework. + void SetLifecycleState(flutter::AppLifecycleState state); + // Create the keyboard & text input sub-systems. // // This requires that a view is attached to the engine. diff --git a/tools/api_check/test/apicheck_test.dart b/tools/api_check/test/apicheck_test.dart index 22dea01379613..f32bb339f8f8a 100644 --- a/tools/api_check/test/apicheck_test.dart +++ b/tools/api_check/test/apicheck_test.dart @@ -43,10 +43,6 @@ void checkApiConsistency(String flutterRoot) { sourcePath: path.join(flutterRoot, 'lib', 'ui', 'window.dart'), className: 'AccessibilityFeatures', ); - final List webuiFields = getDartClassFields( - sourcePath: path.join(flutterRoot, 'lib', 'ui', 'window.dart'), - className: 'AccessibilityFeatures', - ); // C values: kFlutterAccessibilityFeatureFooBar = 1 << N, final List embedderEnumValues = getCppEnumValues( sourcePath: path.join(flutterRoot, 'shell', 'platform', 'embedder', 'embedder.h'), @@ -64,7 +60,6 @@ void checkApiConsistency(String flutterRoot) { enumName: 'AccessibilityFeature', ).map(allCapsToCamelCase).toList(); - expect(webuiFields, uiFields); expect(embedderEnumValues, uiFields); expect(internalEnumValues, uiFields); expect(javaEnumValues, uiFields); @@ -77,7 +72,7 @@ void checkApiConsistency(String flutterRoot) { className: 'SemanticsAction', ); final List webuiFields = getDartClassFields( - sourcePath: path.join(flutterRoot, 'lib', 'ui', 'semantics.dart'), + sourcePath: path.join(flutterRoot, 'lib', 'web_ui', 'lib', 'semantics.dart'), className: 'SemanticsAction', ); // C values: kFlutterSemanticsActionFooBar = 1 << N. @@ -103,6 +98,33 @@ void checkApiConsistency(String flutterRoot) { expect(javaEnumValues, uiFields); }); + test('AppLifecycleState enums match', () { + // Dart values: _kFooBarIndex = 1 << N. + final List uiFields = getDartClassFields( + sourcePath: path.join(flutterRoot, 'lib', 'ui', 'platform_dispatcher.dart'), + className: 'AppLifecycleState', + ); + final List webuiFields = getDartClassFields( + sourcePath: path.join(flutterRoot, 'lib', 'web_ui', 'lib', 'platform_dispatcher.dart'), + className: 'AppLifecycleState', + ); + // C++ values: kFooBar = 1 << N. + final List internalEnumValues = getCppEnumClassValues( + sourcePath: path.join(flutterRoot, 'shell', 'platform', 'common', 'app_lifecycle_state.h'), + enumName: 'AppLifecycleState', + ); + // Java values: FOO_BAR(1 << N). + final List javaEnumValues = getJavaEnumValues( + sourcePath: path.join(flutterRoot, 'shell', 'platform', 'android', 'io', + 'flutter', 'embedding', 'engine', 'systemchannels', 'LifecycleChannel.java'), + enumName: 'AppLifecycleState', + ).map(allCapsToCamelCase).toList(); + + expect(webuiFields, uiFields); + expect(internalEnumValues, uiFields); + expect(javaEnumValues, uiFields); + }); + test('SemanticsFlag enums match', () { // Dart values: _kFooBarIndex = 1 << N. final List uiFields = getDartClassFields(