// 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:io' as io; import 'dart:typed_data'; import 'package:flutter_driver/flutter_driver.dart'; import 'package:meta/meta.dart'; import 'package:path/path.dart' as p; import '../../../native_driver.dart'; import '../../common.dart'; import 'adb.dart'; /// Drives an Android device or emulator that is running a Flutter application. final class AndroidNativeDriver implements NativeDriver { /// Assumes a connected device or emulator via [adb]. /// /// This constructor is intended for testing purposes only. @visibleForTesting AndroidNativeDriver.forTesting({ required Adb adb, required FlutterDriver driver, required io.Directory tempDirectory, }) : this._(adb, driver, tempDirectory); AndroidNativeDriver._(this._adb, this._driver, this._tmpDir); final Adb _adb; final FlutterDriver _driver; final io.Directory _tmpDir; /// Connects to a device or emulator identified by [target], which defaults to /// the only running emulator or connected device. /// /// If [adbPath] is not provided, the `adb` command is assumed to be in the /// system's PATH. /// /// If [tempDirectory] is not provided, a temporary directory will be created /// in the system's temporary directory. static Future connect( FlutterDriver driver, { AndroidDeviceTarget? target, String? adbPath, io.Directory? tempDirectory, }) async { final Adb adb = await Adb.create(adbPath: adbPath, target: target); tempDirectory ??= io.Directory.systemTemp.createTempSync('native_driver.'); final nativeDriver = AndroidNativeDriver.forTesting( adb: adb, driver: driver, tempDirectory: tempDirectory, ); await nativeDriver.ping(); return nativeDriver; } @override Future close() async { await _tmpDir.delete(recursive: true); } @override Future configureForScreenshotTesting() async { await _adb.disableImmersiveModeConfirmations(); await _adb.disableAnimations(); } @override Future tap(NativeFinder finder) async { await _driver.sendCommand(NativeCommand.tap(finder)); } @override Future get sdkVersion async { final Map result = await _driver.sendCommand(NativeCommand.getSdkVersion); return result['version']! as int; } @override Future get isEmulator async { final Map result = await _driver.sendCommand(NativeCommand.getIsEmulator); return result['emulator']! as bool; } /// Waits for 2 seconds before completing. /// /// There is no perfect way, outside of polling, to know when the device is /// "stable" or has "stabilized" after a state change. The way that commands /// such as [FlutterDriver.screenshot] handle this is to wait for 2 seconds /// as a baseline, and then proceed with the command. static Future _waitFor2s() async { await Future.delayed(const Duration(seconds: 2)); } @override Future screenshot() async { // Identical wait to what `FlutterDriver.screenshot` does. await _waitFor2s(); return _AdbScreencap(await _adb.screencap(), _tmpDir); } @override Future ping() async { final stopwatch = Stopwatch()..start(); await _driver.sendCommand(NativeCommand.ping); return stopwatch.elapsed; } @override Future rotateToLandscape() async { await _driver.sendCommand(NativeCommand.rotateLandscape); } @override Future rotateResetDefault() async { await _driver.sendCommand(NativeCommand.rotateDefault); } /// Background the app by pressing the home button. Future backgroundApp() async { await _adb.sendToHome(); await _waitFor2s(); } /// Resumes the app by selecting it from the recent apps list. Future resumeApp({required String appName}) async { await _adb.resumeApp(appName: appName); await _waitFor2s(); } /// Send a trim memory signal to the app to force it to release memory. Future simulateLowMemory({required String appName}) async { await _adb.trimMemory(appName: appName); await _waitFor2s(); } } final class _AdbScreencap implements NativeScreenshot { const _AdbScreencap(this._bytes, this._tmpDir); /// Raw bytes of the screenshot in PNG format. final Uint8List _bytes; /// Temporary directory to use as a default location for saving. final io.Directory _tmpDir; static int _lastScreenshotId = 0; @override Future saveAs([String? path]) async { final int id = _lastScreenshotId++; path ??= p.join(_tmpDir.path, 'screenshot_$id.png'); await io.File(path).writeAsBytes(_bytes); return path; } @override Future readAsBytes() async => _bytes; }