Unverified Commit 88631339 authored by Jenn Magder's avatar Jenn Magder Committed by GitHub

Support work profiles and multiple Android users for run, install, attach, drive (#58815)

parent f0174b17
......@@ -389,10 +389,21 @@ class AndroidDevice extends Device {
String get name => modelID;
@override
Future<bool> isAppInstalled(AndroidApk app) async {
Future<bool> isAppInstalled(
AndroidApk app, {
String userIdentifier,
}) async {
// This call takes 400ms - 600ms.
try {
final RunResult listOut = await runAdbCheckedAsync(<String>['shell', 'pm', 'list', 'packages', app.id]);
final RunResult listOut = await runAdbCheckedAsync(<String>[
'shell',
'pm',
'list',
'packages',
if (userIdentifier != null)
...<String>['--user', userIdentifier],
app.id
]);
return LineSplitter.split(listOut.stdout).contains('package:${app.id}');
} on Exception catch (error) {
_logger.printTrace('$error');
......@@ -407,7 +418,10 @@ class AndroidDevice extends Device {
}
@override
Future<bool> installApp(AndroidApk app) async {
Future<bool> installApp(
AndroidApk app, {
String userIdentifier,
}) async {
if (!app.file.existsSync()) {
_logger.printError('"${_fileSystem.path.relative(app.file.path)}" does not exist.');
return false;
......@@ -423,7 +437,14 @@ class AndroidDevice extends Device {
timeout: _timeoutConfiguration.slowOperation,
);
final RunResult installResult = await _processUtils.run(
adbCommandForDevice(<String>['install', '-t', '-r', app.file.path]));
adbCommandForDevice(<String>[
'install',
'-t',
'-r',
if (userIdentifier != null)
...<String>['--user', userIdentifier],
app.file.path
]));
status.stop();
// Some versions of adb exit with exit code 0 even on failure :(
// Parsing the output to check for failures.
......@@ -434,8 +455,12 @@ class AndroidDevice extends Device {
return false;
}
if (installResult.exitCode != 0) {
_logger.printError('Error: ADB exited with exit code ${installResult.exitCode}');
_logger.printError('$installResult');
if (installResult.stderr.contains('Bad user number')) {
_logger.printError('Error: User "$userIdentifier" not found. Run "adb shell pm list users" to see list of available identifiers.');
} else {
_logger.printError('Error: ADB exited with exit code ${installResult.exitCode}');
_logger.printError('$installResult');
}
return false;
}
try {
......@@ -450,7 +475,10 @@ class AndroidDevice extends Device {
}
@override
Future<bool> uninstallApp(AndroidApk app) async {
Future<bool> uninstallApp(
AndroidApk app, {
String userIdentifier,
}) async {
if (!await _checkForSupportedAdbVersion() ||
!await _checkForSupportedAndroidVersion()) {
return false;
......@@ -459,7 +487,11 @@ class AndroidDevice extends Device {
String uninstallOut;
try {
final RunResult uninstallResult = await _processUtils.run(
adbCommandForDevice(<String>['uninstall', app.id]),
adbCommandForDevice(<String>[
'uninstall',
if (userIdentifier != null)
...<String>['--user', userIdentifier],
app.id]),
throwOnError: true,
);
uninstallOut = uninstallResult.stdout;
......@@ -477,8 +509,8 @@ class AndroidDevice extends Device {
return true;
}
Future<bool> _installLatestApp(AndroidApk package) async {
final bool wasInstalled = await isAppInstalled(package);
Future<bool> _installLatestApp(AndroidApk package, String userIdentifier) async {
final bool wasInstalled = await isAppInstalled(package, userIdentifier: userIdentifier);
if (wasInstalled) {
if (await isLatestBuildInstalled(package)) {
_logger.printTrace('Latest build already installed.');
......@@ -486,15 +518,15 @@ class AndroidDevice extends Device {
}
}
_logger.printTrace('Installing APK.');
if (!await installApp(package)) {
if (!await installApp(package, userIdentifier: userIdentifier)) {
_logger.printTrace('Warning: Failed to install APK.');
if (wasInstalled) {
_logger.printStatus('Uninstalling old version...');
if (!await uninstallApp(package)) {
if (!await uninstallApp(package, userIdentifier: userIdentifier)) {
_logger.printError('Error: Uninstalling old version failed.');
return false;
}
if (!await installApp(package)) {
if (!await installApp(package, userIdentifier: userIdentifier)) {
_logger.printError('Error: Failed to install APK again.');
return false;
}
......@@ -516,6 +548,7 @@ class AndroidDevice extends Device {
Map<String, dynamic> platformArgs,
bool prebuiltApplication = false,
bool ipv6 = false,
String userIdentifier,
}) async {
if (!await _checkForSupportedAdbVersion() ||
!await _checkForSupportedAndroidVersion()) {
......@@ -570,9 +603,9 @@ class AndroidDevice extends Device {
}
_logger.printTrace("Stopping app '${package.name}' on $name.");
await stopApp(package);
await stopApp(package, userIdentifier: userIdentifier);
if (!await _installLatestApp(package)) {
if (!await _installLatestApp(package, userIdentifier)) {
return LaunchResult.failed();
}
......@@ -636,6 +669,8 @@ class AndroidDevice extends Device {
...<String>['--ez', 'use-test-fonts', 'true'],
if (debuggingOptions.verboseSystemLogs)
...<String>['--ez', 'verbose-logging', 'true'],
if (userIdentifier != null)
...<String>['--user', userIdentifier],
],
package.launchActivity,
];
......@@ -687,11 +722,21 @@ class AndroidDevice extends Device {
bool get supportsFastStart => true;
@override
Future<bool> stopApp(AndroidApk app) {
Future<bool> stopApp(
AndroidApk app, {
String userIdentifier,
}) {
if (app == null) {
return Future<bool>.value(false);
}
final List<String> command = adbCommandForDevice(<String>['shell', 'am', 'force-stop', app.id]);
final List<String> command = adbCommandForDevice(<String>[
'shell',
'am',
'force-stop',
if (userIdentifier != null)
...<String>['--user', userIdentifier],
app.id,
]);
return _processUtils.stream(command).then<bool>(
(int exitCode) => exitCode == 0 || allowHeapCorruptionOnWindows(exitCode, _platform));
}
......
......@@ -63,6 +63,7 @@ class AttachCommand extends FlutterCommand {
usesFilesystemOptions(hide: !verboseHelp);
usesFuchsiaOptions(hide: !verboseHelp);
usesDartDefineOption();
usesDeviceUserOption();
argParser
..addOption(
'debug-port',
......@@ -136,6 +137,8 @@ class AttachCommand extends FlutterCommand {
return stringArg('app-id');
}
String get userIdentifier => stringArg(FlutterOptions.kDeviceUser);
@override
Future<void> validateCommand() async {
await super.validateCommand();
......@@ -159,6 +162,13 @@ class AttachCommand extends FlutterCommand {
throwToolExit(
'Either --debugPort or --debugUri can be provided, not both.');
}
if (userIdentifier != null) {
final Device device = await findTargetDevice();
if (device is! AndroidDevice) {
throwToolExit('--${FlutterOptions.kDeviceUser} is only supported for Android');
}
}
}
@override
......@@ -356,6 +366,7 @@ class AttachCommand extends FlutterCommand {
target: stringArg('target'),
targetModel: TargetModel(stringArg('target-model')),
buildInfo: getBuildInfo(),
userIdentifier: userIdentifier,
);
flutterDevice.observatoryUris = observatoryUris;
final List<FlutterDevice> flutterDevices = <FlutterDevice>[flutterDevice];
......
......@@ -10,6 +10,7 @@ import 'package:vm_service/vm_service.dart' as vm_service;
import 'package:meta/meta.dart';
import 'package:webdriver/async_io.dart' as async_io;
import '../android/android_device.dart';
import '../application_package.dart';
import '../base/common.dart';
import '../base/file_system.dart';
......@@ -22,7 +23,7 @@ import '../device.dart';
import '../globals.dart' as globals;
import '../project.dart';
import '../resident_runner.dart';
import '../runner/flutter_command.dart' show FlutterCommandResult;
import '../runner/flutter_command.dart' show FlutterCommandResult, FlutterOptions;
import '../vmservice.dart';
import '../web/web_runner.dart';
import 'run.dart';
......@@ -136,11 +137,23 @@ class DriveCommand extends RunCommandBase {
bool get shouldBuild => boolArg('build');
bool get verboseSystemLogs => boolArg('verbose-system-logs');
String get userIdentifier => stringArg(FlutterOptions.kDeviceUser);
/// Subscription to log messages printed on the device or simulator.
// ignore: cancel_subscriptions
StreamSubscription<String> _deviceLogSubscription;
@override
Future<void> validateCommand() async {
if (userIdentifier != null) {
final Device device = await findTargetDevice();
if (device is! AndroidDevice) {
throwToolExit('--${FlutterOptions.kDeviceUser} is only supported for Android');
}
}
return super.validateCommand();
}
@override
Future<FlutterCommandResult> runCommand() async {
final String testFile = _getTestFile();
......@@ -195,7 +208,7 @@ class DriveCommand extends RunCommandBase {
device,
flutterProject: flutterProject,
target: targetFile,
buildInfo: buildInfo
buildInfo: buildInfo,
);
residentRunner = webRunnerFactory.createWebRunner(
flutterDevice,
......@@ -412,7 +425,11 @@ void restoreAppStarter() {
appStarter = _startApp;
}
Future<LaunchResult> _startApp(DriveCommand command, Uri webUri) async {
Future<LaunchResult> _startApp(
DriveCommand command,
Uri webUri, {
String userIdentifier,
}) async {
final String mainPath = findMainDartFile(command.targetFile);
if (await globals.fs.type(mainPath) != FileSystemEntityType.file) {
globals.printError('Tried to run $mainPath, but that file does not exist.');
......@@ -427,10 +444,10 @@ Future<LaunchResult> _startApp(DriveCommand command, Uri webUri) async {
if (command.shouldBuild) {
globals.printTrace('Installing application package.');
if (await command.device.isAppInstalled(package)) {
await command.device.uninstallApp(package);
if (await command.device.isAppInstalled(package, userIdentifier: userIdentifier)) {
await command.device.uninstallApp(package, userIdentifier: userIdentifier);
}
await command.device.installApp(package);
await command.device.installApp(package, userIdentifier: userIdentifier);
}
final Map<String, dynamic> platformArgs = <String, dynamic>{};
......@@ -469,6 +486,7 @@ Future<LaunchResult> _startApp(DriveCommand command, Uri webUri) async {
),
platformArgs: platformArgs,
prebuiltApplication: !command.shouldBuild,
userIdentifier: userIdentifier,
);
if (!result.started) {
......@@ -520,7 +538,7 @@ Future<bool> _stopApp(DriveCommand command) async {
await command.device.targetPlatform,
command.getBuildInfo(),
);
final bool stopped = await command.device.stopApp(package);
final bool stopped = await command.device.stopApp(package, userIdentifier: command.userIdentifier);
await command._deviceLogSubscription?.cancel();
return stopped;
}
......
......@@ -4,6 +4,7 @@
import 'dart:async';
import '../android/android_device.dart';
import '../application_package.dart';
import '../base/common.dart';
import '../base/io.dart';
......@@ -15,6 +16,7 @@ import '../runner/flutter_command.dart';
class InstallCommand extends FlutterCommand with DeviceBasedDevelopmentArtifacts {
InstallCommand() {
requiresPubspecYaml();
usesDeviceUserOption();
argParser.addFlag('uninstall-only',
negatable: true,
defaultsTo: false,
......@@ -31,6 +33,7 @@ class InstallCommand extends FlutterCommand with DeviceBasedDevelopmentArtifacts
Device device;
bool get uninstallOnly => boolArg('uninstall-only');
String get userIdentifier => stringArg(FlutterOptions.kDeviceUser);
@override
Future<void> validateCommand() async {
......@@ -39,6 +42,9 @@ class InstallCommand extends FlutterCommand with DeviceBasedDevelopmentArtifacts
if (device == null) {
throwToolExit('No target device found');
}
if (userIdentifier != null && device is! AndroidDevice) {
throwToolExit('--${FlutterOptions.kDeviceUser} is only supported for Android');
}
}
@override
......@@ -59,9 +65,9 @@ class InstallCommand extends FlutterCommand with DeviceBasedDevelopmentArtifacts
}
Future<void> _uninstallApp(ApplicationPackage package) async {
if (await device.isAppInstalled(package)) {
if (await device.isAppInstalled(package, userIdentifier: userIdentifier)) {
globals.printStatus('Uninstalling $package from $device...');
if (!await device.uninstallApp(package)) {
if (!await device.uninstallApp(package, userIdentifier: userIdentifier)) {
globals.printError('Uninstalling old version failed');
}
} else {
......@@ -72,21 +78,26 @@ class InstallCommand extends FlutterCommand with DeviceBasedDevelopmentArtifacts
Future<void> _installApp(ApplicationPackage package) async {
globals.printStatus('Installing $package to $device...');
if (!await installApp(device, package)) {
if (!await installApp(device, package, userIdentifier: userIdentifier)) {
throwToolExit('Install failed');
}
}
}
Future<bool> installApp(Device device, ApplicationPackage package, { bool uninstall = true }) async {
Future<bool> installApp(
Device device,
ApplicationPackage package, {
String userIdentifier,
bool uninstall = true
}) async {
if (package == null) {
return false;
}
try {
if (uninstall && await device.isAppInstalled(package)) {
if (uninstall && await device.isAppInstalled(package, userIdentifier: userIdentifier)) {
globals.printStatus('Uninstalling old version...');
if (!await device.uninstallApp(package)) {
if (!await device.uninstallApp(package, userIdentifier: userIdentifier)) {
globals.printError('Warning: uninstalling old version failed');
}
}
......@@ -94,5 +105,5 @@ Future<bool> installApp(Device device, ApplicationPackage package, { bool uninst
globals.printError('Error accessing device ${device.id}:\n${e.message}');
}
return device.installApp(package);
return device.installApp(package, userIdentifier: userIdentifier);
}
......@@ -6,6 +6,7 @@ import 'dart:async';
import 'package:args/command_runner.dart';
import '../android/android_device.dart';
import '../base/common.dart';
import '../base/file_system.dart';
import '../base/io.dart';
......@@ -67,6 +68,7 @@ abstract class RunCommandBase extends FlutterCommand with DeviceBasedDevelopment
usesTrackWidgetCreation(verboseHelp: verboseHelp);
usesIsolateFilterOption(hide: !verboseHelp);
addNullSafetyModeOptions();
usesDeviceUserOption();
}
bool get traceStartup => boolArg('trace-startup');
......@@ -221,6 +223,8 @@ class RunCommand extends RunCommandBase {
List<Device> devices;
String get userIdentifier => stringArg(FlutterOptions.kDeviceUser);
@override
Future<String> get usagePath async {
final String command = await super.usagePath;
......@@ -336,6 +340,13 @@ class RunCommand extends RunCommandBase {
if (deviceManager.hasSpecifiedAllDevices && runningWithPrebuiltApplication) {
throwToolExit('Using -d all with --use-application-binary is not supported');
}
if (userIdentifier != null
&& devices.every((Device device) => device is! AndroidDevice)) {
throwToolExit(
'--${FlutterOptions.kDeviceUser} is only supported for Android. At least one Android device is required.'
);
}
}
DebuggingOptions _createDebuggingOptions() {
......@@ -491,6 +502,7 @@ class RunCommand extends RunCommandBase {
experimentalFlags: expFlags,
target: stringArg('target'),
buildInfo: getBuildInfo(),
userIdentifier: userIdentifier,
),
];
// Only support "web mode" with a single web device due to resident runner
......
......@@ -33,7 +33,10 @@ abstract class DesktopDevice extends Device {
// Since the host and target devices are the same, no work needs to be done
// to install the application.
@override
Future<bool> isAppInstalled(ApplicationPackage app) async => true;
Future<bool> isAppInstalled(
ApplicationPackage app, {
String userIdentifier,
}) async => true;
// Since the host and target devices are the same, no work needs to be done
// to install the application.
......@@ -43,12 +46,18 @@ abstract class DesktopDevice extends Device {
// Since the host and target devices are the same, no work needs to be done
// to install the application.
@override
Future<bool> installApp(ApplicationPackage app) async => true;
Future<bool> installApp(
ApplicationPackage app, {
String userIdentifier,
}) async => true;
// Since the host and target devices are the same, no work needs to be done
// to uninstall the application.
@override
Future<bool> uninstallApp(ApplicationPackage app) async => true;
Future<bool> uninstallApp(
ApplicationPackage app, {
String userIdentifier,
}) async => true;
@override
Future<bool> get isLocalEmulator async => false;
......@@ -86,6 +95,7 @@ abstract class DesktopDevice extends Device {
Map<String, dynamic> platformArgs,
bool prebuiltApplication = false,
bool ipv6 = false,
String userIdentifier,
}) async {
if (!prebuiltApplication) {
Cache.releaseLockEarly();
......@@ -138,7 +148,10 @@ abstract class DesktopDevice extends Device {
}
@override
Future<bool> stopApp(ApplicationPackage app) async {
Future<bool> stopApp(
ApplicationPackage app, {
String userIdentifier,
}) async {
bool succeeded = true;
// Walk a copy of _runningProcesses, since the exit handler removes from the
// set.
......
......@@ -413,17 +413,33 @@ abstract class Device {
/// Whether the device is supported for the current project directory.
bool isSupportedForProject(FlutterProject flutterProject);
/// Check if a version of the given app is already installed
Future<bool> isAppInstalled(covariant ApplicationPackage app);
/// Check if a version of the given app is already installed.
///
/// Specify [userIdentifier] to check if installed for a particular user (Android only).
Future<bool> isAppInstalled(
covariant ApplicationPackage app, {
String userIdentifier,
});
/// Check if the latest build of the [app] is already installed.
Future<bool> isLatestBuildInstalled(covariant ApplicationPackage app);
/// Install an app package on the current device
Future<bool> installApp(covariant ApplicationPackage app);
/// Install an app package on the current device.
///
/// Specify [userIdentifier] to install for a particular user (Android only).
Future<bool> installApp(
covariant ApplicationPackage app, {
String userIdentifier,
});
/// Uninstall an app package from the current device
Future<bool> uninstallApp(covariant ApplicationPackage app);
/// Uninstall an app package from the current device.
///
/// Specify [userIdentifier] to uninstall for a particular user,
/// defaults to all users (Android only).
Future<bool> uninstallApp(
covariant ApplicationPackage app, {
String userIdentifier,
});
/// Check if the device is supported by Flutter
bool isSupported();
......@@ -471,6 +487,7 @@ abstract class Device {
Map<String, dynamic> platformArgs,
bool prebuiltApplication = false,
bool ipv6 = false,
String userIdentifier,
});
/// Whether this device implements support for hot reload.
......@@ -491,7 +508,12 @@ abstract class Device {
bool get supportsFastStart => false;
/// Stop an app package on the current device.
Future<bool> stopApp(covariant ApplicationPackage app);
///
/// Specify [userIdentifier] to stop app installed to a profile (Android only).
Future<bool> stopApp(
covariant ApplicationPackage app, {
String userIdentifier,
});
/// Query the current application memory usage..
///
......
......@@ -237,16 +237,25 @@ class FuchsiaDevice extends Device {
bool get supportsStartPaused => false;
@override
Future<bool> isAppInstalled(ApplicationPackage app) async => false;
Future<bool> isAppInstalled(
ApplicationPackage app, {
String userIdentifier,
}) async => false;
@override
Future<bool> isLatestBuildInstalled(ApplicationPackage app) async => false;
@override
Future<bool> installApp(ApplicationPackage app) => Future<bool>.value(false);
Future<bool> installApp(
ApplicationPackage app, {
String userIdentifier,
}) => Future<bool>.value(false);
@override
Future<bool> uninstallApp(ApplicationPackage app) async => false;
Future<bool> uninstallApp(
ApplicationPackage app, {
String userIdentifier,
}) async => false;
@override
bool isSupported() => true;
......@@ -263,6 +272,7 @@ class FuchsiaDevice extends Device {
Map<String, dynamic> platformArgs,
bool prebuiltApplication = false,
bool ipv6 = false,
String userIdentifier,
}) async {
if (!prebuiltApplication) {
await buildFuchsia(fuchsiaProject: FlutterProject.current().fuchsia,
......@@ -434,7 +444,10 @@ class FuchsiaDevice extends Device {
}
@override
Future<bool> stopApp(covariant FuchsiaApp app) async {
Future<bool> stopApp(
covariant FuchsiaApp app, {
String userIdentifier,
}) async {
final int appKey = await FuchsiaTilesCtl.findAppKey(this, app.id);
if (appKey != -1) {
if (!await fuchsiaDeviceTools.tilesCtl.remove(this, appKey)) {
......
......@@ -225,7 +225,10 @@ class IOSDevice extends Device {
bool get supportsStartPaused => false;
@override
Future<bool> isAppInstalled(IOSApp app) async {
Future<bool> isAppInstalled(
IOSApp app, {
String userIdentifier,
}) async {
bool result;
try {
result = await _iosDeploy.isAppInstalled(
......@@ -243,7 +246,10 @@ class IOSDevice extends Device {
Future<bool> isLatestBuildInstalled(IOSApp app) async => false;
@override
Future<bool> installApp(IOSApp app) async {
Future<bool> installApp(
IOSApp app, {
String userIdentifier,
}) async {
final Directory bundle = _fileSystem.directory(app.deviceBundlePath);
if (!bundle.existsSync()) {
_logger.printError('Could not find application bundle at ${bundle.path}; have you run "flutter build ios"?');
......@@ -273,7 +279,10 @@ class IOSDevice extends Device {
}
@override
Future<bool> uninstallApp(IOSApp app) async {
Future<bool> uninstallApp(
IOSApp app, {
String userIdentifier,
}) async {
int uninstallationResult;
try {
uninstallationResult = await _iosDeploy.uninstallApp(
......@@ -304,6 +313,7 @@ class IOSDevice extends Device {
bool prebuiltApplication = false,
bool ipv6 = false,
@visibleForTesting Duration fallbackPollingDelay,
String userIdentifier,
}) async {
String packageId;
......@@ -443,7 +453,10 @@ class IOSDevice extends Device {
}
@override
Future<bool> stopApp(IOSApp app) async {
Future<bool> stopApp(
IOSApp app, {
String userIdentifier,
}) async {
// Currently we don't have a way to stop an app running on iOS.
return false;
}
......
......@@ -319,7 +319,10 @@ class IOSSimulator extends Device {
String get xcrunPath => globals.fs.path.join('/usr', 'bin', 'xcrun');
@override
Future<bool> isAppInstalled(ApplicationPackage app) {
Future<bool> isAppInstalled(
ApplicationPackage app, {
String userIdentifier,
}) {
return _simControl.isInstalled(id, app.id);
}
......@@ -327,7 +330,10 @@ class IOSSimulator extends Device {
Future<bool> isLatestBuildInstalled(ApplicationPackage app) async => false;
@override
Future<bool> installApp(covariant IOSApp app) async {
Future<bool> installApp(
covariant IOSApp app, {
String userIdentifier,
}) async {
try {
final IOSApp iosApp = app;
await _simControl.install(id, iosApp.simulatorBundlePath);
......@@ -338,7 +344,10 @@ class IOSSimulator extends Device {
}
@override
Future<bool> uninstallApp(ApplicationPackage app) async {
Future<bool> uninstallApp(
ApplicationPackage app, {
String userIdentifier,
}) async {
try {
await _simControl.uninstall(id, app.id);
return true;
......@@ -384,6 +393,7 @@ class IOSSimulator extends Device {
Map<String, dynamic> platformArgs,
bool prebuiltApplication = false,
bool ipv6 = false,
String userIdentifier,
}) async {
if (!prebuiltApplication && package is BuildableIOSApp) {
globals.printTrace('Building ${package.name} for $id.');
......@@ -495,7 +505,10 @@ class IOSSimulator extends Device {
}
@override
Future<bool> stopApp(ApplicationPackage app) async {
Future<bool> stopApp(
ApplicationPackage app, {
String userIdentifier,
}) async {
// Currently we don't have a way to stop an app running on iOS.
return false;
}
......
......@@ -47,6 +47,7 @@ class FlutterDevice {
TargetModel targetModel = TargetModel.flutter,
TargetPlatform targetPlatform,
ResidentCompiler generator,
this.userIdentifier,
}) : assert(buildInfo.trackWidgetCreation != null),
generator = generator ?? ResidentCompiler(
globals.artifacts.getArtifactPath(
......@@ -76,6 +77,7 @@ class FlutterDevice {
TargetModel targetModel = TargetModel.flutter,
List<String> experimentalFlags,
ResidentCompiler generator,
String userIdentifier,
}) async {
ResidentCompiler generator;
final TargetPlatform targetPlatform = await device.targetPlatform;
......@@ -150,12 +152,14 @@ class FlutterDevice {
targetPlatform: targetPlatform,
generator: generator,
buildInfo: buildInfo,
userIdentifier: userIdentifier,
);
}
final Device device;
final ResidentCompiler generator;
final BuildInfo buildInfo;
final String userIdentifier;
Stream<Uri> observatoryUris;
vm_service.VmService vmService;
DevFS devFS;
......@@ -239,11 +243,11 @@ class FlutterDevice {
@visibleForTesting Duration timeoutDelay = const Duration(seconds: 10),
}) async {
if (!device.supportsFlutterExit || vmService == null) {
return device.stopApp(package);
return device.stopApp(package, userIdentifier: userIdentifier);
}
final List<FlutterView> views = await vmService.getFlutterViews();
if (views == null || views.isEmpty) {
return device.stopApp(package);
return device.stopApp(package, userIdentifier: userIdentifier);
}
// If any of the flutter views are paused, we might not be able to
// cleanly exit since the service extension may not have been registered.
......@@ -254,7 +258,7 @@ class FlutterDevice {
continue;
}
if (isPauseEvent(isolate.pauseEvent.kind)) {
return device.stopApp(package);
return device.stopApp(package, userIdentifier: userIdentifier);
}
}
for (final FlutterView view in views) {
......@@ -277,7 +281,7 @@ class FlutterDevice {
// flutter_attach_android_test. This log should help verify this
// is where the tool is getting stuck.
globals.logger.printTrace('error: vm service shutdown failed');
return device.stopApp(package);
return device.stopApp(package, userIdentifier: userIdentifier);
});
}
......@@ -502,6 +506,7 @@ class FlutterDevice {
route: route,
prebuiltApplication: prebuiltMode,
ipv6: hotRunner.ipv6,
userIdentifier: userIdentifier,
);
final LaunchResult result = await futureResult;
......@@ -575,6 +580,7 @@ class FlutterDevice {
route: route,
prebuiltApplication: prebuiltMode,
ipv6: coldRunner.ipv6,
userIdentifier: userIdentifier,
);
if (!result.started) {
......
......@@ -202,7 +202,7 @@ class ColdRunner extends ResidentRunner {
for (final FlutterDevice device in flutterDevices) {
// If we're running in release mode, stop the app using the device logic.
if (device.vmService == null) {
await device.device.stopApp(device.package);
await device.device.stopApp(device.package, userIdentifier: device.userIdentifier);
}
}
await super.preExit();
......
......@@ -110,6 +110,7 @@ class FlutterOptions {
static const String kBundleSkSLPathOption = 'bundle-sksl-path';
static const String kPerformanceMeasurementFile = 'performance-measurement-file';
static const String kNullSafety = 'sound-null-safety';
static const String kDeviceUser = 'device-user';
}
abstract class FlutterCommand extends Command<void> {
......@@ -374,6 +375,12 @@ abstract class FlutterCommand extends Command<void> {
"Normally there's only one, but when adding Flutter to a pre-existing app it's possible to create multiple.");
}
void usesDeviceUserOption() {
argParser.addOption(FlutterOptions.kDeviceUser,
help: 'Identifier number for a user or work profile on Android only. Run "adb shell pm list users" for available identifiers.',
valueHelp: '10');
}
void addBuildModeFlags({ bool defaultToRelease = true, bool verboseHelp = false, bool excludeDebug = false }) {
// A release build must be the default if a debug build is not possible.
assert(defaultToRelease || !excludeDebug);
......
......@@ -90,10 +90,16 @@ class FlutterTesterDevice extends Device {
}
@override
Future<bool> installApp(ApplicationPackage app) async => true;
Future<bool> installApp(
ApplicationPackage app, {
String userIdentifier,
}) async => true;
@override
Future<bool> isAppInstalled(ApplicationPackage app) async => false;
Future<bool> isAppInstalled(
ApplicationPackage app, {
String userIdentifier,
}) async => false;
@override
Future<bool> isLatestBuildInstalled(ApplicationPackage app) async => false;
......@@ -109,10 +115,11 @@ class FlutterTesterDevice extends Device {
ApplicationPackage package, {
@required String mainPath,
String route,
@required DebuggingOptions debuggingOptions,
DebuggingOptions debuggingOptions,
Map<String, dynamic> platformArgs,
bool prebuiltApplication = false,
bool ipv6 = false,
String userIdentifier,
}) async {
final BuildInfo buildInfo = debuggingOptions.buildInfo;
......@@ -218,14 +225,20 @@ class FlutterTesterDevice extends Device {
}
@override
Future<bool> stopApp(ApplicationPackage app) async {
Future<bool> stopApp(
ApplicationPackage app, {
String userIdentifier,
}) async {
_process?.kill();
_process = null;
return true;
}
@override
Future<bool> uninstallApp(ApplicationPackage app) async => true;
Future<bool> uninstallApp(
ApplicationPackage app, {
String userIdentifier,
}) async => true;
@override
bool isSupportedForProject(FlutterProject flutterProject) => true;
......
......@@ -87,10 +87,16 @@ abstract class ChromiumDevice extends Device {
}
@override
Future<bool> installApp(ApplicationPackage app) async => true;
Future<bool> installApp(
ApplicationPackage app, {
String userIdentifier,
}) async => true;
@override
Future<bool> isAppInstalled(ApplicationPackage app) async => true;
Future<bool> isAppInstalled(
ApplicationPackage app, {
String userIdentifier,
}) async => true;
@override
Future<bool> isLatestBuildInstalled(ApplicationPackage app) async => true;
......@@ -116,6 +122,7 @@ abstract class ChromiumDevice extends Device {
Map<String, Object> platformArgs,
bool prebuiltApplication = false,
bool ipv6 = false,
String userIdentifier,
}) async {
// See [ResidentWebRunner.run] in flutter_tools/lib/src/resident_web_runner.dart
// for the web initialization and server logic.
......@@ -137,7 +144,10 @@ abstract class ChromiumDevice extends Device {
}
@override
Future<bool> stopApp(ApplicationPackage app) async {
Future<bool> stopApp(
ApplicationPackage app, {
String userIdentifier,
}) async {
await _chrome?.close();
return true;
}
......@@ -146,7 +156,10 @@ abstract class ChromiumDevice extends Device {
Future<TargetPlatform> get targetPlatform async => TargetPlatform.web_javascript;
@override
Future<bool> uninstallApp(ApplicationPackage app) async => true;
Future<bool> uninstallApp(
ApplicationPackage app, {
String userIdentifier,
}) async => true;
@override
bool isSupportedForProject(FlutterProject flutterProject) {
......@@ -333,10 +346,16 @@ class WebServerDevice extends Device {
}
@override
Future<bool> installApp(ApplicationPackage app) async => true;
Future<bool> installApp(
ApplicationPackage app, {
String userIdentifier,
}) async => true;
@override
Future<bool> isAppInstalled(ApplicationPackage app) async => true;
Future<bool> isAppInstalled(
ApplicationPackage app, {
String userIdentifier,
}) async => true;
@override
Future<bool> isLatestBuildInstalled(ApplicationPackage app) async => true;
......@@ -375,6 +394,7 @@ class WebServerDevice extends Device {
Map<String, Object> platformArgs,
bool prebuiltApplication = false,
bool ipv6 = false,
String userIdentifier,
}) async {
final String url = platformArgs['uri'] as String;
if (debuggingOptions.startPaused) {
......@@ -387,7 +407,10 @@ class WebServerDevice extends Device {
}
@override
Future<bool> stopApp(ApplicationPackage app) async {
Future<bool> stopApp(
ApplicationPackage app, {
String userIdentifier,
}) async {
return true;
}
......@@ -395,7 +418,10 @@ class WebServerDevice extends Device {
Future<TargetPlatform> get targetPlatform async => TargetPlatform.web_javascript;
@override
Future<bool> uninstallApp(ApplicationPackage app) async {
Future<bool> uninstallApp(
ApplicationPackage app, {
String userIdentifier,
}) async {
return true;
}
......
......@@ -326,16 +326,29 @@ void main() {
globals.fs.file(globals.fs.path.join('lib', 'main.dart')).deleteSync();
final AttachCommand command = AttachCommand(hotRunnerFactory: mockHotRunnerFactory);
await createTestCommandRunner(command).run(<String>['attach', '-t', foo.path, '-v']);
await createTestCommandRunner(command).run(<String>[
'attach',
'-t',
foo.path,
'-v',
'--device-user',
'10',
]);
final VerificationResult verificationResult = verify(
mockHotRunnerFactory.build(
captureAny,
target: foo.path,
debuggingOptions: anyNamed('debuggingOptions'),
packagesFilePath: anyNamed('packagesFilePath'),
flutterProject: anyNamed('flutterProject'),
ipv6: false,
),
)..called(1);
verify(mockHotRunnerFactory.build(
any,
target: foo.path,
debuggingOptions: anyNamed('debuggingOptions'),
packagesFilePath: anyNamed('packagesFilePath'),
flutterProject: anyNamed('flutterProject'),
ipv6: false,
)).called(1);
final List<FlutterDevice> flutterDevices = verificationResult.captured.first as List<FlutterDevice>;
expect(flutterDevices, hasLength(1));
final FlutterDevice flutterDevice = flutterDevices.first;
expect(flutterDevice.userIdentifier, '10');
}, overrides: <Type, Generator>{
FileSystem: () => testFileSystem,
ProcessManager: () => FakeProcessManager.any(),
......@@ -538,6 +551,19 @@ void main() {
ProcessManager: () => FakeProcessManager.any(),
});
testUsingContext('fails when targeted device is not Android with --device-user', () async {
final MockIOSDevice device = MockIOSDevice();
testDeviceManager.addDevice(device);
expect(createTestCommandRunner(AttachCommand()).run(<String>[
'attach',
'--device-user',
'10',
]), throwsToolExit(message: '--device-user is only supported for Android'));
}, overrides: <Type, Generator>{
FileSystem: () => testFileSystem,
ProcessManager: () => FakeProcessManager.any(),
});
testUsingContext('exits when multiple devices connected', () async {
Device aDeviceWithId(String id) {
final MockAndroidDevice device = MockAndroidDevice();
......
......@@ -166,9 +166,30 @@ void main() {
ProcessManager: () => FakeProcessManager.any(),
});
testUsingContext('returns 0 when test ends successfully', () async {
testUsingContext('returns 1 when targeted device is not Android with --device-user', () async {
testDeviceManager.addDevice(MockDevice());
final String testApp = globals.fs.path.join(tempDir.path, 'test', 'e2e.dart');
globals.fs.file(testApp).createSync(recursive: true);
final List<String> args = <String>[
'drive',
'--target=$testApp',
'--no-pub',
'--device-user',
'10',
];
expect(() async => await createTestCommandRunner(command).run(args),
throwsToolExit(message: '--device-user is only supported for Android'));
}, overrides: <Type, Generator>{
FileSystem: () => fs,
ProcessManager: () => FakeProcessManager.any(),
});
testUsingContext('returns 0 when test ends successfully', () async {
testDeviceManager.addDevice(MockAndroidDevice());
final String testApp = globals.fs.path.join(tempDir.path, 'test', 'e2e.dart');
final String testFile = globals.fs.path.join(tempDir.path, 'test_driver', 'e2e_test.dart');
......@@ -195,6 +216,8 @@ void main() {
'drive',
'--target=$testApp',
'--no-pub',
'--device-user',
'10',
];
await createTestCommandRunner(command).run(args);
expect(testLogger.errorText, isEmpty);
......@@ -358,14 +381,16 @@ void main() {
final MockLaunchResult mockLaunchResult = MockLaunchResult();
when(mockLaunchResult.started).thenReturn(true);
when(mockDevice.startApp(
null,
mainPath: anyNamed('mainPath'),
route: anyNamed('route'),
debuggingOptions: anyNamed('debuggingOptions'),
platformArgs: anyNamed('platformArgs'),
prebuiltApplication: anyNamed('prebuiltApplication'),
null,
mainPath: anyNamed('mainPath'),
route: anyNamed('route'),
debuggingOptions: anyNamed('debuggingOptions'),
platformArgs: anyNamed('platformArgs'),
prebuiltApplication: anyNamed('prebuiltApplication'),
userIdentifier: anyNamed('userIdentifier'),
)).thenAnswer((_) => Future<LaunchResult>.value(mockLaunchResult));
when(mockDevice.isAppInstalled(any)).thenAnswer((_) => Future<bool>.value(false));
when(mockDevice.isAppInstalled(any, userIdentifier: anyNamed('userIdentifier')))
.thenAnswer((_) => Future<bool>.value(false));
testApp = globals.fs.path.join(tempDir.path, 'test', 'e2e.dart');
testFile = globals.fs.path.join(tempDir.path, 'test_driver', 'e2e_test.dart');
......@@ -407,6 +432,7 @@ void main() {
debuggingOptions: anyNamed('debuggingOptions'),
platformArgs: anyNamed('platformArgs'),
prebuiltApplication: false,
userIdentifier: anyNamed('userIdentifier'),
));
}, overrides: <Type, Generator>{
FileSystem: () => fs,
......@@ -435,6 +461,7 @@ void main() {
debuggingOptions: anyNamed('debuggingOptions'),
platformArgs: anyNamed('platformArgs'),
prebuiltApplication: false,
userIdentifier: anyNamed('userIdentifier'),
));
}, overrides: <Type, Generator>{
FileSystem: () => fs,
......@@ -463,6 +490,7 @@ void main() {
debuggingOptions: anyNamed('debuggingOptions'),
platformArgs: anyNamed('platformArgs'),
prebuiltApplication: true,
userIdentifier: anyNamed('userIdentifier'),
));
}, overrides: <Type, Generator>{
FileSystem: () => fs,
......@@ -494,11 +522,12 @@ void main() {
debuggingOptions: anyNamed('debuggingOptions'),
platformArgs: anyNamed('platformArgs'),
prebuiltApplication: anyNamed('prebuiltApplication'),
userIdentifier: anyNamed('userIdentifier'),
)).thenAnswer((Invocation invocation) async {
debuggingOptions = invocation.namedArguments[#debuggingOptions] as DebuggingOptions;
return mockLaunchResult;
});
when(mockDevice.isAppInstalled(any))
when(mockDevice.isAppInstalled(any, userIdentifier: anyNamed('userIdentifier')))
.thenAnswer((_) => Future<bool>.value(false));
testApp = globals.fs.path.join(tempDir.path, 'test', 'e2e.dart');
......@@ -547,6 +576,7 @@ void main() {
debuggingOptions: anyNamed('debuggingOptions'),
platformArgs: anyNamed('platformArgs'),
prebuiltApplication: false,
userIdentifier: anyNamed('userIdentifier'),
));
expect(optionValue(), setToTrue ? isTrue : isFalse);
}, overrides: <Type, Generator>{
......
......@@ -21,8 +21,10 @@ void main() {
applyMocksToCommand(command);
final MockAndroidDevice device = MockAndroidDevice();
when(device.isAppInstalled(any)).thenAnswer((_) async => false);
when(device.installApp(any)).thenAnswer((_) async => true);
when(device.isAppInstalled(any, userIdentifier: anyNamed('userIdentifier')))
.thenAnswer((_) async => false);
when(device.installApp(any, userIdentifier: anyNamed('userIdentifier')))
.thenAnswer((_) async => true);
testDeviceManager.addDevice(device);
await createTestCommandRunner(command).run(<String>['install']);
......@@ -30,6 +32,23 @@ void main() {
Cache: () => MockCache(),
});
testUsingContext('returns 1 when targeted device is not Android with --device-user', () async {
final InstallCommand command = InstallCommand();
applyMocksToCommand(command);
final MockIOSDevice device = MockIOSDevice();
when(device.isAppInstalled(any, userIdentifier: anyNamed('userIdentifier')))
.thenAnswer((_) async => false);
when(device.installApp(any, userIdentifier: anyNamed('userIdentifier')))
.thenAnswer((_) async => true);
testDeviceManager.addDevice(device);
expect(() async => await createTestCommandRunner(command).run(<String>['install', '--device-user', '10']),
throwsToolExit(message: '--device-user is only supported for Android'));
}, overrides: <Type, Generator>{
Cache: () => MockCache(),
});
testUsingContext('returns 0 when iOS is connected and ready for an install', () async {
final InstallCommand command = InstallCommand();
applyMocksToCommand(command);
......
......@@ -203,6 +203,38 @@ void main() {
ProcessManager: () => mockProcessManager,
});
testUsingContext('fails when targeted device is not Android with --device-user', () async {
globals.fs.file('pubspec.yaml').createSync();
globals.fs.file('.packages').writeAsStringSync('\n');
globals.fs.file('lib/main.dart').createSync(recursive: true);
final FakeDevice device = FakeDevice(isLocalEmulator: true);
when(deviceManager.getAllConnectedDevices()).thenAnswer((Invocation invocation) async {
return <Device>[device];
});
when(deviceManager.getDevices()).thenAnswer((Invocation invocation) async {
return <Device>[device];
});
when(deviceManager.findTargetDevices(any)).thenAnswer((Invocation invocation) async {
return <Device>[device];
});
when(deviceManager.hasSpecifiedAllDevices).thenReturn(false);
when(deviceManager.deviceDiscoverers).thenReturn(<DeviceDiscovery>[]);
final RunCommand command = RunCommand();
applyMocksToCommand(command);
await expectLater(createTestCommandRunner(command).run(<String>[
'run',
'--no-pub',
'--device-user',
'10',
]), throwsToolExit(message: '--device-user is only supported for Android. At least one Android device is required.'));
}, overrides: <Type, Generator>{
FileSystem: () => MemoryFileSystem.test(),
ProcessManager: () => FakeProcessManager.any(),
DeviceManager: () => MockDeviceManager(),
Stdio: () => mockStdio,
});
testUsingContext('shows unsupported devices when no supported devices are found', () async {
final RunCommand command = RunCommand();
applyMocksToCommand(command);
......@@ -320,6 +352,7 @@ void main() {
route: anyNamed('route'),
prebuiltApplication: anyNamed('prebuiltApplication'),
ipv6: anyNamed('ipv6'),
userIdentifier: anyNamed('userIdentifier'),
)).thenAnswer((Invocation invocation) => Future<LaunchResult>.value(LaunchResult.failed()));
when(mockArtifacts.getArtifactPath(
......@@ -690,6 +723,7 @@ class FakeDevice extends Fake implements Device {
bool prebuiltApplication = false,
bool usesTerminalUi = true,
bool ipv6 = false,
String userIdentifier,
}) async {
final String dartFlags = debuggingOptions.dartFlags;
// In release mode, --dart-flags should be set to the empty string and
......
......@@ -188,17 +188,27 @@ void main() {
command: <String>['adb', '-s', '1234', 'shell', 'getprop'],
));
processManager.addCommand(const FakeCommand(
command: <String>['adb', '-s', '1234', 'shell', 'am', 'force-stop', 'FlutterApp'],
command: <String>['adb', '-s', '1234', 'shell', 'am', 'force-stop', '--user', '10', 'FlutterApp'],
));
processManager.addCommand(const FakeCommand(
command: <String>['adb', '-s', '1234', 'shell', 'pm', 'list', 'packages', 'FlutterApp'],
command: <String>['adb', '-s', '1234', 'shell', 'pm', 'list', 'packages', '--user', '10', 'FlutterApp'],
));
processManager.addCommand(kAdbVersionCommand);
processManager.addCommand(kStartServer);
// TODO(jonahwilliams): investigate why this doesn't work.
// This doesn't work with the current Android log reader implementation.
processManager.addCommand(const FakeCommand(
command: <String>['adb', '-s', '1234', 'install', '-t', '-r', 'app.apk'],
command: <String>[
'adb',
'-s',
'1234',
'install',
'-t',
'-r',
'--user',
'10',
'app.apk'
],
stdout: '\n\nObservatory listening on http://127.0.0.1:456\n\n',
));
processManager.addCommand(kShaCommand);
......@@ -244,6 +254,7 @@ void main() {
'--es', 'dart-flags', 'foo',
'--ez', 'use-test-fonts', 'true',
'--ez', 'verbose-logging', 'true',
'--user', '10',
'FlutterActivity',
],
));
......@@ -268,6 +279,7 @@ void main() {
verboseSystemLogs: true,
),
platformArgs: <String, dynamic>{},
userIdentifier: '10',
);
// This fails to start due to observatory discovery issues.
......
......@@ -22,13 +22,46 @@ const FakeCommand kAdbStartServerCommand = FakeCommand(
command: <String>['adb', 'start-server']
);
const FakeCommand kInstallCommand = FakeCommand(
command: <String>['adb', '-s', '1234', 'install', '-t', '-r', 'app.apk'],
command: <String>[
'adb',
'-s',
'1234',
'install',
'-t',
'-r',
'--user',
'10',
'app.apk'
],
);
const FakeCommand kStoreShaCommand = FakeCommand(
command: <String>['adb', '-s', '1234', 'shell', 'echo', '-n', '', '>', '/data/local/tmp/sky.app.sha1']
);
void main() {
FileSystem fileSystem;
BufferLogger logger;
setUp(() {
fileSystem = MemoryFileSystem.test();
logger = BufferLogger.test();
});
AndroidDevice setUpAndroidDevice({
AndroidSdk androidSdk,
ProcessManager processManager,
}) {
androidSdk ??= MockAndroidSdk();
when(androidSdk.adbPath).thenReturn('adb');
return AndroidDevice('1234',
logger: logger,
platform: FakePlatform(operatingSystem: 'linux'),
androidSdk: androidSdk,
fileSystem: fileSystem ?? MemoryFileSystem.test(),
processManager: processManager ?? FakeProcessManager.any(),
);
}
testWithoutContext('Cannot install app on API level below 16', () async {
final FakeProcessManager processManager = FakeProcessManager.list(<FakeCommand>[
kAdbVersionCommand,
......@@ -38,7 +71,6 @@ void main() {
stdout: '[ro.build.version.sdk]: [11]',
),
]);
final FileSystem fileSystem = MemoryFileSystem.test();
final File apk = fileSystem.file('app.apk')..createSync();
final AndroidApk androidApk = AndroidApk(
file: apk,
......@@ -47,7 +79,6 @@ void main() {
launchActivity: 'Main',
);
final AndroidDevice androidDevice = setUpAndroidDevice(
fileSystem: fileSystem,
processManager: processManager,
);
......@@ -56,7 +87,6 @@ void main() {
});
testWithoutContext('Cannot install app if APK file is missing', () async {
final FileSystem fileSystem = MemoryFileSystem.test();
final File apk = fileSystem.file('app.apk');
final AndroidApk androidApk = AndroidApk(
file: apk,
......@@ -65,7 +95,6 @@ void main() {
launchActivity: 'Main',
);
final AndroidDevice androidDevice = setUpAndroidDevice(
fileSystem: fileSystem,
);
expect(await androidDevice.installApp(androidApk), false);
......@@ -82,7 +111,6 @@ void main() {
kInstallCommand,
kStoreShaCommand,
]);
final FileSystem fileSystem = MemoryFileSystem.test();
final File apk = fileSystem.file('app.apk')..createSync();
final AndroidApk androidApk = AndroidApk(
file: apk,
......@@ -91,11 +119,10 @@ void main() {
launchActivity: 'Main',
);
final AndroidDevice androidDevice = setUpAndroidDevice(
fileSystem: fileSystem,
processManager: processManager,
);
expect(await androidDevice.installApp(androidApk), true);
expect(await androidDevice.installApp(androidApk, userIdentifier: '10'), true);
expect(processManager.hasRemainingExpectations, false);
});
......@@ -109,7 +136,6 @@ void main() {
kInstallCommand,
kStoreShaCommand,
]);
final FileSystem fileSystem = MemoryFileSystem.test();
final File apk = fileSystem.file('app.apk')..createSync();
final AndroidApk androidApk = AndroidApk(
file: apk,
......@@ -118,29 +144,51 @@ void main() {
launchActivity: 'Main',
);
final AndroidDevice androidDevice = setUpAndroidDevice(
fileSystem: fileSystem,
processManager: processManager,
);
expect(await androidDevice.installApp(androidApk), true);
expect(await androidDevice.installApp(androidApk, userIdentifier: '10'), true);
expect(processManager.hasRemainingExpectations, false);
});
}
AndroidDevice setUpAndroidDevice({
AndroidSdk androidSdk,
FileSystem fileSystem,
ProcessManager processManager,
}) {
androidSdk ??= MockAndroidSdk();
when(androidSdk.adbPath).thenReturn('adb');
return AndroidDevice('1234',
logger: BufferLogger.test(),
platform: FakePlatform(operatingSystem: 'linux'),
androidSdk: androidSdk,
fileSystem: fileSystem ?? MemoryFileSystem.test(),
processManager: processManager ?? FakeProcessManager.any(),
);
testWithoutContext('displays error if user not found', () async {
final FakeProcessManager processManager = FakeProcessManager.list(<FakeCommand>[
kAdbVersionCommand,
kAdbStartServerCommand,
const FakeCommand(
command: <String>['adb', '-s', '1234', 'shell', 'getprop'],
),
const FakeCommand(
command: <String>[
'adb',
'-s',
'1234',
'install',
'-t',
'-r',
'--user',
'jane',
'app.apk'
],
exitCode: 1,
stderr: 'Exception occurred while executing: java.lang.IllegalArgumentException: Bad user number: jane',
),
]);
final File apk = fileSystem.file('app.apk')..createSync();
final AndroidApk androidApk = AndroidApk(
file: apk,
id: 'app',
versionCode: 22,
launchActivity: 'Main',
);
final AndroidDevice androidDevice = setUpAndroidDevice(
processManager: processManager,
);
expect(await androidDevice.installApp(androidApk, userIdentifier: 'jane'), false);
expect(logger.errorText, contains('Error: User "jane" not found. Run "adb shell pm list users" to see list of available identifiers.'));
expect(processManager.hasRemainingExpectations, false);
});
}
class MockAndroidSdk extends Mock implements AndroidSdk {}
......@@ -1080,7 +1080,7 @@ void main() {
]);
_setupMocks();
bool debugClosed = false;
when(mockDevice.stopApp(any)).thenAnswer((Invocation invocation) async {
when(mockDevice.stopApp(any, userIdentifier: anyNamed('userIdentifier'))).thenAnswer((Invocation invocation) async {
if (debugClosed) {
throw StateError('debug connection closed twice');
}
......
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