// 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 startFlutter( 'run', options: <String>['--$mode'], ); }); final StreamController<String> lifecycles = StreamController<String>(); final StreamIterator<String> lifecycleItr = StreamIterator<String>(lifecycles.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'); lifecycles.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 lifecycles.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); } }; }