Unverified Commit bcdab118 authored by Greg Spencer's avatar Greg Spencer Committed by GitHub

Add support for application exit requests (#121378)

Add support for application exit requests
parent b1464e00
......@@ -27,6 +27,6 @@
<key>NSMainNibFile</key>
<string>MainMenu</string>
<key>NSPrincipalClass</key>
<string>FlutterApplication</string>
<string>NSApplication</string>
</dict>
</plist>
......@@ -27,6 +27,6 @@
<key>NSMainNibFile</key>
<string>MainMenu</string>
<key>NSPrincipalClass</key>
<string>FlutterApplication</string>
<string>NSApplication</string>
</dict>
</plist>
......@@ -27,6 +27,6 @@
<key>NSMainNibFile</key>
<string>MainMenu</string>
<key>NSPrincipalClass</key>
<string>FlutterApplication</string>
<string>NSApplication</string>
</dict>
</plist>
......@@ -29,6 +29,6 @@
<key>NSMainNibFile</key>
<string>MainMenu</string>
<key>NSPrincipalClass</key>
<string>FlutterApplication</string>
<string>NSApplication</string>
</dict>
</plist>
......@@ -27,6 +27,6 @@
<key>NSMainNibFile</key>
<string>MainMenu</string>
<key>NSPrincipalClass</key>
<string>FlutterApplication</string>
<string>NSApplication</string>
</dict>
</plist>
......@@ -27,6 +27,6 @@
<key>NSMainNibFile</key>
<string>MainMenu</string>
<key>NSPrincipalClass</key>
<string>FlutterApplication</string>
<string>NSApplication</string>
</dict>
</plist>
......@@ -27,6 +27,6 @@
<key>NSMainNibFile</key>
<string>MainMenu</string>
<key>NSPrincipalClass</key>
<string>FlutterApplication</string>
<string>NSApplication</string>
</dict>
</plist>
// Copyright 2014 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 'dart:ui';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
/// Flutter code sample for [ServicesBinding.handleRequestAppExit].
void main() {
runApp(const ApplicationExitExample());
}
class ApplicationExitExample extends StatelessWidget {
const ApplicationExitExample({super.key});
@override
Widget build(BuildContext context) {
return const MaterialApp(
home: Scaffold(body: Body()),
);
}
}
class Body extends StatefulWidget {
const Body({super.key});
@override
State<Body> createState() => _BodyState();
}
class _BodyState extends State<Body> with WidgetsBindingObserver {
bool _shouldExit = false;
String lastResponse = 'No exit requested yet';
@override
void initState() {
super.initState();
WidgetsBinding.instance.addObserver(this);
}
@override
void dispose() {
WidgetsBinding.instance.removeObserver(this);
super.dispose();
}
Future<void> _quit() async {
final AppExitType exitType = _shouldExit ? AppExitType.required : AppExitType.cancelable;
setState(() {
lastResponse = 'App requesting ${exitType.name} exit';
});
await ServicesBinding.instance.exitApplication(exitType);
}
@override
Future<AppExitResponse> didRequestAppExit() async {
final AppExitResponse response = _shouldExit ? AppExitResponse.exit : AppExitResponse.cancel;
setState(() {
lastResponse = 'App responded ${response.name} to exit request';
});
return response;
}
void _radioChanged(bool? value) {
value ??= true;
if (_shouldExit == value) {
return;
}
setState(() {
_shouldExit = value!;
});
}
@override
Widget build(BuildContext context) {
return Center(
child: SizedBox(
width: 300,
child: IntrinsicHeight(
child: Column(
children: <Widget>[
RadioListTile<bool>(
title: const Text('Do Not Allow Exit'),
groupValue: _shouldExit,
value: false,
onChanged: _radioChanged,
),
RadioListTile<bool>(
title: const Text('Allow Exit'),
groupValue: _shouldExit,
value: true,
onChanged: _radioChanged,
),
const SizedBox(height: 30),
ElevatedButton(
onPressed: _quit,
child: const Text('Quit'),
),
const SizedBox(height: 30),
Text(lastResponse),
],
),
),
),
);
}
}
......@@ -27,6 +27,6 @@
<key>NSMainNibFile</key>
<string>MainMenu</string>
<key>NSPrincipalClass</key>
<string>FlutterApplication</string>
<string>NSApplication</string>
</dict>
</plist>
// Copyright 2014 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 'package:flutter_api_samples/services/binding/handle_request_app_exit.0.dart' as example;
import 'package:flutter_test/flutter_test.dart';
void main() {
testWidgets('Application Exit example', (WidgetTester tester) async {
await tester.pumpWidget(
const example.ApplicationExitExample(),
);
expect(find.text('No exit requested yet'), findsOneWidget);
expect(find.text('Do Not Allow Exit'), findsOneWidget);
expect(find.text('Allow Exit'), findsOneWidget);
expect(find.text('Quit'), findsOneWidget);
await tester.tap(find.text('Quit'));
await tester.pump();
expect(find.text('App requesting cancelable exit'), findsOneWidget);
});
}
......@@ -27,6 +27,6 @@
<key>NSMainNibFile</key>
<string>MainMenu</string>
<key>NSPrincipalClass</key>
<string>FlutterApplication</string>
<string>NSApplication</string>
</dict>
</plist>
......@@ -27,6 +27,6 @@
<key>NSMainNibFile</key>
<string>MainMenu</string>
<key>NSPrincipalClass</key>
<string>FlutterApplication</string>
<string>NSApplication</string>
</dict>
</plist>
......@@ -27,6 +27,6 @@
<key>NSMainNibFile</key>
<string>MainMenu</string>
<key>NSPrincipalClass</key>
<string>FlutterApplication</string>
<string>NSApplication</string>
</dict>
</plist>
......@@ -27,6 +27,6 @@
<key>NSMainNibFile</key>
<string>MainMenu</string>
<key>NSPrincipalClass</key>
<string>FlutterApplication</string>
<string>NSApplication</string>
</dict>
</plist>
......@@ -27,6 +27,6 @@
<key>NSMainNibFile</key>
<string>MainMenu</string>
<key>NSPrincipalClass</key>
<string>FlutterApplication</string>
<string>NSApplication</string>
</dict>
</plist>
......@@ -263,30 +263,119 @@ mixin ServicesBinding on BindingBase, SchedulerBinding {
return null;
}
Future<void> _handlePlatformMessage(MethodCall methodCall) async {
Future<dynamic> _handlePlatformMessage(MethodCall methodCall) async {
final String method = methodCall.method;
// There is only one incoming method call currently possible.
assert(method == 'SystemChrome.systemUIChange');
final List<dynamic> args = methodCall.arguments as List<dynamic>;
if (_systemUiChangeCallback != null) {
await _systemUiChangeCallback!(args[0] as bool);
assert(method == 'SystemChrome.systemUIChange' || method == 'System.requestAppExit');
switch (method) {
case 'SystemChrome.systemUIChange':
final List<dynamic> args = methodCall.arguments as List<dynamic>;
if (_systemUiChangeCallback != null) {
await _systemUiChangeCallback!(args[0] as bool);
}
break;
case 'System.requestAppExit':
return <String, dynamic>{'response': (await handleRequestAppExit()).name};
}
}
static AppLifecycleState? _parseAppLifecycleMessage(String message) {
switch (message) {
case 'AppLifecycleState.paused':
return AppLifecycleState.paused;
case 'AppLifecycleState.resumed':
return AppLifecycleState.resumed;
case 'AppLifecycleState.inactive':
return AppLifecycleState.inactive;
case 'AppLifecycleState.paused':
return AppLifecycleState.paused;
case 'AppLifecycleState.detached':
return AppLifecycleState.detached;
}
return null;
}
/// Handles any requests for application exit that may be received on the
/// [SystemChannels.platform] method channel.
///
/// By default, returns [ui.AppExitResponse.exit].
///
/// {@template flutter.services.binding.ServicesBinding.requestAppExit}
/// Not all exits are cancelable, so not all exits will call this function. Do
/// not rely on this function as a place to save critical data, because you
/// will be disappointed. There are a number of ways that the application can
/// exit without letting the application know first: power can be unplugged,
/// the battery removed, the application can be killed in a task manager or
/// command line, or the device could have a rapid unplanned disassembly (i.e.
/// it could explode). In all of those cases (and probably others), no
/// notification will be given to the application that it is about to exit.
/// {@endtemplate}
///
/// {@tool sample}
/// This examples shows how an application can cancel (or not) OS requests for
/// quitting an application. Currently this is only supported on macOS and
/// Linux.
///
/// ** See code in examples/api/lib/services/binding/handle_request_app_exit.0.dart **
/// {@end-tool}
///
/// See also:
///
/// * [WidgetsBindingObserver.didRequestAppExit], which can be overridden to
/// respond to this message.
/// * [WidgetsBinding.handleRequestAppExit] which overrides this method to
/// notify its observers.
Future<ui.AppExitResponse> handleRequestAppExit() async {
return ui.AppExitResponse.exit;
}
/// Exits the application by calling the native application API method for
/// exiting an application cleanly.
///
/// This differs from calling `dart:io`'s [exit] function in that it gives the
/// engine a chance to clean up resources so that it doesn't crash on exit, so
/// calling this is always preferred over calling [exit]. It also optionally
/// gives handlers of [handleRequestAppExit] a chance to cancel the
/// application exit.
///
/// The [exitType] indicates what kind of exit to perform. For
/// [ui.AppExitType.cancelable] exits, the application is queried through a
/// call to [handleRequestAppExit], where the application can optionally
/// cancel the request for exit. If the [exitType] is
/// [ui.AppExitType.required], then the application exits immediately without
/// querying the application.
///
/// For [ui.AppExitType.cancelable] exits, the returned response value is the
/// response obtained from the application as to whether the exit was canceled
/// or not. Practically, the response will never be [ui.AppExitResponse.exit],
/// since the application will have already exited by the time the result
/// would have been received.
///
/// The optional [exitCode] argument will be used as the application exit code
/// on platforms where an exit code is supported. On other platforms it may be
/// ignored. It defaults to zero.
///
/// See also:
///
/// * [WidgetsBindingObserver.didRequestAppExit] for a handler you can
/// override on a [WidgetsBindingObserver] to receive exit requests.
@mustCallSuper
Future<ui.AppExitResponse> exitApplication(ui.AppExitType exitType, [int exitCode = 0]) async {
final Map<String, Object?>? result = await SystemChannels.platform.invokeMethod<Map<String, Object?>>(
'System.exitApplication',
<String, Object?>{'type': exitType.name, 'exitCode': exitCode},
);
if (result == null ) {
return ui.AppExitResponse.cancel;
}
switch (result['response']) {
case 'cancel':
return ui.AppExitResponse.cancel;
case 'exit':
default:
// In practice, this will never get returned, because the application
// will have exited before it returns.
return ui.AppExitResponse.exit;
}
}
/// The [RestorationManager] synchronizes the restoration data between
/// engine and framework.
///
......@@ -326,7 +415,6 @@ mixin ServicesBinding on BindingBase, SchedulerBinding {
void setSystemUiChangeCallback(SystemUiChangeCallback? callback) {
_systemUiChangeCallback = callback;
}
}
/// Signature for listening to changes in the [SystemUiMode].
......
......@@ -120,6 +120,11 @@ class SystemChannels {
/// * `SystemNavigator.pop`: Tells the operating system to close the
/// application, or the closest equivalent. See [SystemNavigator.pop].
///
/// * `System.exitApplication`: Tells the engine to send a request back to
/// the application to request an application exit (using
/// `System.requestAppExit` below), and if it is not canceled, to terminate
/// the application using the platform UI toolkit's termination API.
///
/// The following incoming methods are defined for this channel (registered
/// using [MethodChannel.setMethodCallHandler]):
///
......@@ -129,6 +134,9 @@ class SystemChannels {
/// [SystemChrome.setSystemUIChangeCallback] to respond to this change in
/// application state.
///
/// * `System.requestAppExit`: The application has requested that it be
/// terminated. See [ServicesBinding.exitApplication].
///
/// Calls to methods that are not implemented on the shell side are ignored
/// (so it is safe to call methods when the relevant plugin might be missing).
static const MethodChannel platform = OptionalMethodChannel(
......
......@@ -4,7 +4,7 @@
import 'dart:async';
import 'dart:developer' as developer;
import 'dart:ui' show AccessibilityFeatures, AppLifecycleState, FrameTiming, Locale, PlatformDispatcher, TimingsCallback;
import 'dart:ui' show AccessibilityFeatures, AppExitResponse, AppLifecycleState, FrameTiming, Locale, PlatformDispatcher, TimingsCallback;
import 'package:flutter/foundation.dart';
import 'package:flutter/gestures.dart';
......@@ -41,7 +41,6 @@ export 'dart:ui' show AppLifecycleState, Locale;
/// lifecycle messages. See [didChangeAppLifecycleState].
///
/// ** See code in examples/api/lib/widgets/binding/widget_binding_observer.0.dart **
///
/// {@end-tool}
///
/// To respond to other notifications, replace the [didChangeAppLifecycleState]
......@@ -228,6 +227,21 @@ abstract class WidgetsBindingObserver {
/// This method exposes notifications from [SystemChannels.lifecycle].
void didChangeAppLifecycleState(AppLifecycleState state) { }
/// Called when a request is received from the system to exit the application.
///
/// If any observer responds with [AppExitResponse.cancel], it will cancel the
/// exit. All observers will be asked before exiting.
///
/// {@macro flutter.services.binding.ServicesBinding.requestAppExit}
///
/// See also:
///
/// * [ServicesBinding.exitApplication] for a function to call that will request
/// that the application exits.
Future<AppExitResponse> didRequestAppExit() async {
return AppExitResponse.exit;
}
/// Called when the system is running low on memory.
///
/// This method exposes the `memoryPressure` notification from
......@@ -526,6 +540,20 @@ mixin WidgetsBinding on BindingBase, ServicesBinding, SchedulerBinding, GestureB
/// * [WidgetsBindingObserver], which has an example of using this method.
bool removeObserver(WidgetsBindingObserver observer) => _observers.remove(observer);
@override
Future<AppExitResponse> handleRequestAppExit() async {
bool didCancel = false;
for (final WidgetsBindingObserver observer in _observers) {
if ((await observer.didRequestAppExit()) == AppExitResponse.cancel) {
didCancel = true;
// Don't early return. For the case where someone is just using the
// observer to know when exit happens, we want to call all the
// observers, even if we already know we're going to cancel.
}
}
return didCancel ? AppExitResponse.cancel : AppExitResponse.exit;
}
@override
void handleMetricsChanged() {
super.handleMetricsChanged();
......
......@@ -4,6 +4,7 @@
import 'dart:convert';
import 'dart:io';
import 'dart:ui';
import 'package:flutter/foundation.dart';
import 'package:flutter/scheduler.dart';
......@@ -113,4 +114,35 @@ void main() {
expect(data, isNotNull);
});
});
test('Calling exitApplication sends a method call to the engine', () async {
bool sentMessage = false;
MethodCall? methodCall;
binding.defaultBinaryMessenger.setMockMessageHandler('flutter/platform', (ByteData? message) async {
methodCall = const JSONMethodCodec().decodeMethodCall(message);
sentMessage = true;
return const JSONMethodCodec().encodeSuccessEnvelope(<String, String>{'response': 'cancel'});
});
final AppExitResponse response = await binding.exitApplication(AppExitType.required);
expect(sentMessage, isTrue);
expect(methodCall, isNotNull);
expect((methodCall!.arguments as Map<String, dynamic>)['type'], equals('required'));
expect(response, equals(AppExitResponse.cancel));
});
test('Default handleRequestAppExit returns exit', () async {
const MethodCall incomingCall = MethodCall('System.requestAppExit', <dynamic>[<String, dynamic>{'type': 'cancelable'}]);
bool receivedReply = false;
Map<String, dynamic>? result;
await binding.defaultBinaryMessenger.handlePlatformMessage('flutter/platform', const JSONMethodCodec().encodeMethodCall(incomingCall),
(ByteData? message) async {
result = (const JSONMessageCodec().decodeMessage(message) as List<dynamic>)[0] as Map<String, dynamic>;
receivedReply = true;
},
);
expect(receivedReply, isTrue);
expect(result, isNotNull);
expect(result!['response'], equals('exit'));
});
}
......@@ -27,6 +27,6 @@
<key>NSMainNibFile</key>
<string>MainMenu</string>
<key>NSPrincipalClass</key>
<string>FlutterApplication</string>
<string>NSApplication</string>
</dict>
</plist>
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment