| // 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:async'; | |
| import 'dart:convert'; | |
| import 'dart:io'; | |
| import 'package:path/path.dart' as path; | |
| import '../framework/devices.dart'; | |
| import '../framework/framework.dart'; | |
| import '../framework/task_result.dart'; | |
| import '../framework/utils.dart'; | |
| const String _kOrgName = 'com.example.activitydestroy'; | |
| final RegExp _lifecycleSentinelRegExp = RegExp(r'==== lifecycle\: (.+) ===='); | |
| /// Tests the following Android lifecycles: Activity#onStop(), Activity#onResume(), Activity#onPause(), | |
| /// and Activity#onDestroy() from Dart perspective in debug, profile, and release modes. | |
| TaskFunction androidLifecyclesTest({ | |
| Map<String, String>? environment, | |
| }) { | |
| final Directory tempDir = Directory.systemTemp | |
| .createTempSync('flutter_devicelab_activity_destroy.'); | |
| return () async { | |
| try { | |
| section('Create app'); | |
| await inDirectory(tempDir, () async { | |
| await flutter( | |
| 'create', | |
| options: <String>[ | |
| '--platforms', | |
| 'android', | |
| '--org', | |
| _kOrgName, | |
| 'app', | |
| ], | |
| environment: environment, | |
| ); | |
| }); | |
| final File mainDart = File(path.join( | |
| tempDir.absolute.path, | |
| 'app', | |
| 'lib', | |
| 'main.dart', | |
| )); | |
| if (!mainDart.existsSync()) { | |
| return TaskResult.failure('${mainDart.path} does not exist'); | |
| } | |
| section('Patch lib/main.dart'); | |
| await mainDart.writeAsString(r''' | |
| import 'package:flutter/widgets.dart'; | |
| class LifecycleObserver extends WidgetsBindingObserver { | |
| @override | |
| void didChangeAppLifecycleState(AppLifecycleState state) { | |
| print('==== lifecycle: $state ===='); | |
| } | |
| } | |
| void main() { | |
| WidgetsFlutterBinding.ensureInitialized(); | |
| WidgetsBinding.instance.addObserver(LifecycleObserver()); | |
| runApp(Container()); | |
| } | |
| ''', flush: true); | |
| Future<TaskResult> runTestFor(String mode) async { | |
| final AndroidDevice device = await devices.workingDevice as AndroidDevice; | |
| await device.unlock(); | |
| section('Flutter run on device running API level ${device.apiLevel} (mode: $mode)'); | |
| late Process run; | |
| await inDirectory(path.join(tempDir.path, 'app'), () async { | |
| run = await startProcess( | |
| path.join(flutterDirectory.path, 'bin', 'flutter'), | |
| flutterCommandArgs('run', <String>['--$mode']), | |
| ); | |
| }); | |
| final StreamController<String> lifecyles = StreamController<String>(); | |
| final StreamIterator<String> lifecycleItr = StreamIterator<String>(lifecyles.stream); | |
| final StreamSubscription<void> stdout = run.stdout | |
| .transform<String>(utf8.decoder) | |
| .transform<String>(const LineSplitter()) | |
| .listen((String log) { | |
| final RegExpMatch? match = _lifecycleSentinelRegExp.firstMatch(log); | |
| print('stdout: $log'); | |
| if (match == null) { | |
| return; | |
| } | |
| final String lifecycle = match[1]!; | |
| print('stdout: Found app lifecycle: $lifecycle'); | |
| lifecyles.add(lifecycle); | |
| }); | |
| final StreamSubscription<void> stderr = run.stderr | |
| .transform<String>(utf8.decoder) | |
| .transform<String>(const LineSplitter()) | |
| .listen((String log) { | |
| print('stderr: $log'); | |
| }); | |
| Future<void> expectedLifecycle(String expected) async { | |
| section('Wait for lifecycle: $expected (mode: $mode)'); | |
| await lifecycleItr.moveNext(); | |
| final String got = lifecycleItr.current; | |
| if (expected != got) { | |
| throw TaskResult.failure('expected lifecycles: `$expected`, but got` $got`'); | |
| } | |
| } | |
| await expectedLifecycle('AppLifecycleState.resumed'); | |
| section('Toggling app switch (mode: $mode)'); | |
| await device.shellExec('input', <String>['keyevent', 'KEYCODE_APP_SWITCH']); | |
| await expectedLifecycle('AppLifecycleState.inactive'); | |
| if (device.apiLevel == 28) { // Device lab currently runs 28. | |
| await expectedLifecycle('AppLifecycleState.paused'); | |
| await expectedLifecycle('AppLifecycleState.detached'); | |
| } | |
| section('Bring activity to foreground (mode: $mode)'); | |
| await device.shellExec('am', <String>['start', '-n', '$_kOrgName.app/.MainActivity']); | |
| await expectedLifecycle('AppLifecycleState.resumed'); | |
| section('Launch Settings app (mode: $mode)'); | |
| await device.shellExec('am', <String>['start', '-a', 'android.settings.SETTINGS']); | |
| await expectedLifecycle('AppLifecycleState.inactive'); | |
| if (device.apiLevel == 28) { // Device lab currently runs 28. | |
| await expectedLifecycle('AppLifecycleState.paused'); | |
| await expectedLifecycle('AppLifecycleState.detached'); | |
| } | |
| section('Bring activity to foreground (mode: $mode)'); | |
| await device.shellExec('am', <String>['start', '-n', '$_kOrgName.app/.MainActivity']); | |
| await expectedLifecycle('AppLifecycleState.resumed'); | |
| run.kill(); | |
| section('Stop subscriptions (mode: $mode)'); | |
| await lifecycleItr.cancel(); | |
| await lifecyles.close(); | |
| await stdout.cancel(); | |
| await stderr.cancel(); | |
| return TaskResult.success(null); | |
| } | |
| final TaskResult debugResult = await runTestFor('debug'); | |
| if (debugResult.failed) { | |
| return debugResult; | |
| } | |
| final TaskResult profileResult = await runTestFor('profile'); | |
| if (profileResult.failed) { | |
| return profileResult; | |
| } | |
| final TaskResult releaseResult = await runTestFor('release'); | |
| if (releaseResult.failed) { | |
| return releaseResult; | |
| } | |
| return TaskResult.success(null); | |
| } on TaskResult catch (error) { | |
| return error; | |
| } finally { | |
| rmTree(tempDir); | |
| } | |
| }; | |
| } |