blob: adc6eddfe6756ad350763b55295fa4223aa61fd6 [file] [log] [blame]
// 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:meta/meta.dart';
import 'package:process/process.dart';
import '../base/error_handling_io.dart';
import '../base/file_system.dart';
import '../base/io.dart';
import '../base/logger.dart';
import '../base/process.dart';
import '../convert.dart';
import '../device.dart';
import '../macos/xcode.dart';
/// A wrapper around the `devicectl` command line tool.
///
/// CoreDevice is a device connectivity stack introduced in Xcode 15. Devices
/// with iOS 17 or greater are CoreDevices.
///
/// `devicectl` (CoreDevice Device Control) is an Xcode CLI tool used for
/// interacting with CoreDevices.
class IOSCoreDeviceControl {
IOSCoreDeviceControl({
required Logger logger,
required ProcessManager processManager,
required Xcode xcode,
required FileSystem fileSystem,
}) : _logger = logger,
_processUtils = ProcessUtils(logger: logger, processManager: processManager),
_xcode = xcode,
_fileSystem = fileSystem;
final Logger _logger;
final ProcessUtils _processUtils;
final Xcode _xcode;
final FileSystem _fileSystem;
/// When the `--timeout` flag is used with `devicectl`, it must be at
/// least 5 seconds. If lower than 5 seconds, `devicectl` will error and not
/// run the command.
static const int _minimumTimeoutInSeconds = 5;
/// Executes `devicectl` command to get list of devices. The command will
/// likely complete before [timeout] is reached. If [timeout] is reached,
/// the command will be stopped as a failure.
Future<List<Object?>> _listCoreDevices({
Duration timeout = const Duration(seconds: _minimumTimeoutInSeconds),
}) async {
if (!_xcode.isDevicectlInstalled) {
_logger.printError('devicectl is not installed.');
return <Object?>[];
}
// Default to minimum timeout if needed to prevent error.
Duration validTimeout = timeout;
if (timeout.inSeconds < _minimumTimeoutInSeconds) {
_logger.printError(
'Timeout of ${timeout.inSeconds} seconds is below the minimum timeout value '
'for devicectl. Changing the timeout to the minimum value of $_minimumTimeoutInSeconds.');
validTimeout = const Duration(seconds: _minimumTimeoutInSeconds);
}
final Directory tempDirectory = _fileSystem.systemTempDirectory.createTempSync('core_devices.');
final File output = tempDirectory.childFile('core_device_list.json');
output.createSync();
final List<String> command = <String>[
..._xcode.xcrunCommand(),
'devicectl',
'list',
'devices',
'--timeout',
validTimeout.inSeconds.toString(),
'--json-output',
output.path,
];
try {
final RunResult result = await _processUtils.run(command, throwOnError: true);
if (!output.existsSync()) {
_logger.printError('After running the command ${command.join(' ')} the file');
_logger.printError('${output.path} was expected to exist, but it did not.');
_logger.printError('The process exited with code ${result.exitCode} and');
_logger.printError('Stdout:\n\n${result.stdout.trim()}\n');
_logger.printError('Stderr:\n\n${result.stderr.trim()}');
throw StateError('Expected the file ${output.path} to exist but it did not');
}
final String stringOutput = output.readAsStringSync();
_logger.printTrace(stringOutput);
try {
final Object? decodeResult = (json.decode(stringOutput) as Map<String, Object?>)['result'];
if (decodeResult is Map<String, Object?>) {
final Object? decodeDevices = decodeResult['devices'];
if (decodeDevices is List<Object?>) {
return decodeDevices;
}
}
_logger.printError('devicectl returned unexpected JSON response: $stringOutput');
return <Object?>[];
} on FormatException {
// We failed to parse the devicectl output, or it returned junk.
_logger.printError('devicectl returned non-JSON response: $stringOutput');
return <Object?>[];
}
} on ProcessException catch (err) {
_logger.printError('Error executing devicectl: $err');
return <Object?>[];
} finally {
ErrorHandlingFileSystem.deleteIfExists(tempDirectory, recursive: true);
}
}
Future<List<IOSCoreDevice>> getCoreDevices({
Duration timeout = const Duration(seconds: _minimumTimeoutInSeconds),
}) async {
final List<Object?> devicesSection = await _listCoreDevices(timeout: timeout);
return <IOSCoreDevice>[
for (final Object? deviceObject in devicesSection)
if (deviceObject is Map<String, Object?>)
IOSCoreDevice.fromBetaJson(deviceObject, logger: _logger),
];
}
/// Executes `devicectl` command to get list of apps installed on the device.
/// If [bundleId] is provided, it will only return apps matching the bundle
/// identifier exactly.
Future<List<Object?>> _listInstalledApps({
required String deviceId,
String? bundleId,
}) async {
if (!_xcode.isDevicectlInstalled) {
_logger.printError('devicectl is not installed.');
return <Object?>[];
}
final Directory tempDirectory = _fileSystem.systemTempDirectory.createTempSync('core_devices.');
final File output = tempDirectory.childFile('core_device_app_list.json');
output.createSync();
final List<String> command = <String>[
..._xcode.xcrunCommand(),
'devicectl',
'device',
'info',
'apps',
'--device',
deviceId,
if (bundleId != null)
'--bundle-id',
bundleId!,
'--json-output',
output.path,
];
try {
await _processUtils.run(command, throwOnError: true);
final String stringOutput = output.readAsStringSync();
try {
final Object? decodeResult = (json.decode(stringOutput) as Map<String, Object?>)['result'];
if (decodeResult is Map<String, Object?>) {
final Object? decodeApps = decodeResult['apps'];
if (decodeApps is List<Object?>) {
return decodeApps;
}
}
_logger.printError('devicectl returned unexpected JSON response: $stringOutput');
return <Object?>[];
} on FormatException {
// We failed to parse the devicectl output, or it returned junk.
_logger.printError('devicectl returned non-JSON response: $stringOutput');
return <Object?>[];
}
} on ProcessException catch (err) {
_logger.printError('Error executing devicectl: $err');
return <Object?>[];
} finally {
tempDirectory.deleteSync(recursive: true);
}
}
@visibleForTesting
Future<List<IOSCoreDeviceInstalledApp>> getInstalledApps({
required String deviceId,
String? bundleId,
}) async {
final List<Object?> appsData = await _listInstalledApps(deviceId: deviceId, bundleId: bundleId);
return <IOSCoreDeviceInstalledApp>[
for (final Object? appObject in appsData)
if (appObject is Map<String, Object?>)
IOSCoreDeviceInstalledApp.fromBetaJson(appObject),
];
}
Future<bool> isAppInstalled({
required String deviceId,
required String bundleId,
}) async {
final List<IOSCoreDeviceInstalledApp> apps = await getInstalledApps(
deviceId: deviceId,
bundleId: bundleId,
);
if (apps.isNotEmpty) {
return true;
}
return false;
}
Future<bool> installApp({
required String deviceId,
required String bundlePath,
}) async {
if (!_xcode.isDevicectlInstalled) {
_logger.printError('devicectl is not installed.');
return false;
}
final Directory tempDirectory = _fileSystem.systemTempDirectory.createTempSync('core_devices.');
final File output = tempDirectory.childFile('install_results.json');
output.createSync();
final List<String> command = <String>[
..._xcode.xcrunCommand(),
'devicectl',
'device',
'install',
'app',
'--device',
deviceId,
bundlePath,
'--json-output',
output.path,
];
try {
await _processUtils.run(command, throwOnError: true);
final String stringOutput = output.readAsStringSync();
try {
final Object? decodeResult = (json.decode(stringOutput) as Map<String, Object?>)['info'];
if (decodeResult is Map<String, Object?> && decodeResult['outcome'] == 'success') {
return true;
}
_logger.printError('devicectl returned unexpected JSON response: $stringOutput');
return false;
} on FormatException {
// We failed to parse the devicectl output, or it returned junk.
_logger.printError('devicectl returned non-JSON response: $stringOutput');
return false;
}
} on ProcessException catch (err) {
_logger.printError('Error executing devicectl: $err');
return false;
} finally {
tempDirectory.deleteSync(recursive: true);
}
}
/// Uninstalls the app from the device. Will succeed even if the app is not
/// currently installed on the device.
Future<bool> uninstallApp({
required String deviceId,
required String bundleId,
}) async {
if (!_xcode.isDevicectlInstalled) {
_logger.printError('devicectl is not installed.');
return false;
}
final Directory tempDirectory = _fileSystem.systemTempDirectory.createTempSync('core_devices.');
final File output = tempDirectory.childFile('uninstall_results.json');
output.createSync();
final List<String> command = <String>[
..._xcode.xcrunCommand(),
'devicectl',
'device',
'uninstall',
'app',
'--device',
deviceId,
bundleId,
'--json-output',
output.path,
];
try {
await _processUtils.run(command, throwOnError: true);
final String stringOutput = output.readAsStringSync();
try {
final Object? decodeResult = (json.decode(stringOutput) as Map<String, Object?>)['info'];
if (decodeResult is Map<String, Object?> && decodeResult['outcome'] == 'success') {
return true;
}
_logger.printError('devicectl returned unexpected JSON response: $stringOutput');
return false;
} on FormatException {
// We failed to parse the devicectl output, or it returned junk.
_logger.printError('devicectl returned non-JSON response: $stringOutput');
return false;
}
} on ProcessException catch (err) {
_logger.printError('Error executing devicectl: $err');
return false;
} finally {
tempDirectory.deleteSync(recursive: true);
}
}
Future<bool> launchApp({
required String deviceId,
required String bundleId,
List<String> launchArguments = const <String>[],
}) async {
if (!_xcode.isDevicectlInstalled) {
_logger.printError('devicectl is not installed.');
return false;
}
final Directory tempDirectory = _fileSystem.systemTempDirectory.createTempSync('core_devices.');
final File output = tempDirectory.childFile('launch_results.json');
output.createSync();
final List<String> command = <String>[
..._xcode.xcrunCommand(),
'devicectl',
'device',
'process',
'launch',
'--device',
deviceId,
bundleId,
if (launchArguments.isNotEmpty) ...launchArguments,
'--json-output',
output.path,
];
try {
await _processUtils.run(command, throwOnError: true);
final String stringOutput = output.readAsStringSync();
try {
final Object? decodeResult = (json.decode(stringOutput) as Map<String, Object?>)['info'];
if (decodeResult is Map<String, Object?> && decodeResult['outcome'] == 'success') {
return true;
}
_logger.printError('devicectl returned unexpected JSON response: $stringOutput');
return false;
} on FormatException {
// We failed to parse the devicectl output, or it returned junk.
_logger.printError('devicectl returned non-JSON response: $stringOutput');
return false;
}
} on ProcessException catch (err) {
_logger.printError('Error executing devicectl: $err');
return false;
} finally {
tempDirectory.deleteSync(recursive: true);
}
}
}
class IOSCoreDevice {
IOSCoreDevice._({
required this.capabilities,
required this.connectionProperties,
required this.deviceProperties,
required this.hardwareProperties,
required this.coreDeviceIdentifier,
required this.visibilityClass,
});
/// Parse JSON from `devicectl list devices --json-output` while it's in beta preview mode.
///
/// Example:
/// {
/// "capabilities" : [
/// ],
/// "connectionProperties" : {
/// },
/// "deviceProperties" : {
/// },
/// "hardwareProperties" : {
/// },
/// "identifier" : "123456BB5-AEDE-7A22-B890-1234567890DD",
/// "visibilityClass" : "default"
/// }
factory IOSCoreDevice.fromBetaJson(
Map<String, Object?> data, {
required Logger logger,
}) {
final List<_IOSCoreDeviceCapability> capabilitiesList = <_IOSCoreDeviceCapability>[
if (data['capabilities'] case final List<Object?> capabilitiesData)
for (final Object? capabilityData in capabilitiesData)
if (capabilityData != null && capabilityData is Map<String, Object?>)
_IOSCoreDeviceCapability.fromBetaJson(capabilityData),
];
_IOSCoreDeviceConnectionProperties? connectionProperties;
if (data['connectionProperties'] is Map<String, Object?>) {
final Map<String, Object?> connectionPropertiesData = data['connectionProperties']! as Map<String, Object?>;
connectionProperties = _IOSCoreDeviceConnectionProperties.fromBetaJson(
connectionPropertiesData,
logger: logger,
);
}
IOSCoreDeviceProperties? deviceProperties;
if (data['deviceProperties'] is Map<String, Object?>) {
final Map<String, Object?> devicePropertiesData = data['deviceProperties']! as Map<String, Object?>;
deviceProperties = IOSCoreDeviceProperties.fromBetaJson(devicePropertiesData);
}
_IOSCoreDeviceHardwareProperties? hardwareProperties;
if (data['hardwareProperties'] is Map<String, Object?>) {
final Map<String, Object?> hardwarePropertiesData = data['hardwareProperties']! as Map<String, Object?>;
hardwareProperties = _IOSCoreDeviceHardwareProperties.fromBetaJson(
hardwarePropertiesData,
logger: logger,
);
}
return IOSCoreDevice._(
capabilities: capabilitiesList,
connectionProperties: connectionProperties,
deviceProperties: deviceProperties,
hardwareProperties: hardwareProperties,
coreDeviceIdentifier: data['identifier']?.toString(),
visibilityClass: data['visibilityClass']?.toString(),
);
}
String? get udid => hardwareProperties?.udid;
DeviceConnectionInterface? get connectionInterface {
return switch (connectionProperties?.transportType?.toLowerCase()) {
'localnetwork' => DeviceConnectionInterface.wireless,
'wired' => DeviceConnectionInterface.attached,
_ => null,
};
}
@visibleForTesting
final List<_IOSCoreDeviceCapability> capabilities;
@visibleForTesting
final _IOSCoreDeviceConnectionProperties? connectionProperties;
final IOSCoreDeviceProperties? deviceProperties;
@visibleForTesting
final _IOSCoreDeviceHardwareProperties? hardwareProperties;
final String? coreDeviceIdentifier;
final String? visibilityClass;
}
class _IOSCoreDeviceCapability {
_IOSCoreDeviceCapability._({
required this.featureIdentifier,
required this.name,
});
/// Parse `capabilities` section of JSON from `devicectl list devices --json-output`
/// while it's in beta preview mode.
///
/// Example:
/// "capabilities" : [
/// {
/// "featureIdentifier" : "com.apple.coredevice.feature.spawnexecutable",
/// "name" : "Spawn Executable"
/// },
/// {
/// "featureIdentifier" : "com.apple.coredevice.feature.launchapplication",
/// "name" : "Launch Application"
/// }
/// ]
factory _IOSCoreDeviceCapability.fromBetaJson(Map<String, Object?> data) {
return _IOSCoreDeviceCapability._(
featureIdentifier: data['featureIdentifier']?.toString(),
name: data['name']?.toString(),
);
}
final String? featureIdentifier;
final String? name;
}
class _IOSCoreDeviceConnectionProperties {
_IOSCoreDeviceConnectionProperties._({
required this.authenticationType,
required this.isMobileDeviceOnly,
required this.lastConnectionDate,
required this.localHostnames,
required this.pairingState,
required this.potentialHostnames,
required this.transportType,
required this.tunnelIPAddress,
required this.tunnelState,
required this.tunnelTransportProtocol,
});
/// Parse `connectionProperties` section of JSON from `devicectl list devices --json-output`
/// while it's in beta preview mode.
///
/// Example:
/// "connectionProperties" : {
/// "authenticationType" : "manualPairing",
/// "isMobileDeviceOnly" : false,
/// "lastConnectionDate" : "2023-06-15T15:29:00.082Z",
/// "localHostnames" : [
/// "iPadName.coredevice.local",
/// "00001234-0001234A3C03401E.coredevice.local",
/// "12345BB5-AEDE-4A22-B653-6037262550DD.coredevice.local"
/// ],
/// "pairingState" : "paired",
/// "potentialHostnames" : [
/// "00001234-0001234A3C03401E.coredevice.local",
/// "12345BB5-AEDE-4A22-B653-6037262550DD.coredevice.local"
/// ],
/// "transportType" : "wired",
/// "tunnelIPAddress" : "fdf1:23c4:cd56::1",
/// "tunnelState" : "connected",
/// "tunnelTransportProtocol" : "tcp"
/// }
factory _IOSCoreDeviceConnectionProperties.fromBetaJson(
Map<String, Object?> data, {
required Logger logger,
}) {
List<String>? localHostnames;
if (data['localHostnames'] is List<Object?>) {
final List<Object?> values = data['localHostnames']! as List<Object?>;
try {
localHostnames = List<String>.from(values);
} on TypeError {
logger.printTrace('Error parsing localHostnames value: $values');
}
}
List<String>? potentialHostnames;
if (data['potentialHostnames'] is List<Object?>) {
final List<Object?> values = data['potentialHostnames']! as List<Object?>;
try {
potentialHostnames = List<String>.from(values);
} on TypeError {
logger.printTrace('Error parsing potentialHostnames value: $values');
}
}
return _IOSCoreDeviceConnectionProperties._(
authenticationType: data['authenticationType']?.toString(),
isMobileDeviceOnly: data['isMobileDeviceOnly'] is bool? ? data['isMobileDeviceOnly'] as bool? : null,
lastConnectionDate: data['lastConnectionDate']?.toString(),
localHostnames: localHostnames,
pairingState: data['pairingState']?.toString(),
potentialHostnames: potentialHostnames,
transportType: data['transportType']?.toString(),
tunnelIPAddress: data['tunnelIPAddress']?.toString(),
tunnelState: data['tunnelState']?.toString(),
tunnelTransportProtocol: data['tunnelTransportProtocol']?.toString(),
);
}
final String? authenticationType;
final bool? isMobileDeviceOnly;
final String? lastConnectionDate;
final List<String>? localHostnames;
final String? pairingState;
final List<String>? potentialHostnames;
final String? transportType;
final String? tunnelIPAddress;
final String? tunnelState;
final String? tunnelTransportProtocol;
}
@visibleForTesting
class IOSCoreDeviceProperties {
IOSCoreDeviceProperties._({
required this.bootedFromSnapshot,
required this.bootedSnapshotName,
required this.bootState,
required this.ddiServicesAvailable,
required this.developerModeStatus,
required this.hasInternalOSBuild,
required this.name,
required this.osBuildUpdate,
required this.osVersionNumber,
required this.rootFileSystemIsWritable,
required this.screenViewingURL,
});
/// Parse `deviceProperties` section of JSON from `devicectl list devices --json-output`
/// while it's in beta preview mode.
///
/// Example:
/// "deviceProperties" : {
/// "bootedFromSnapshot" : true,
/// "bootedSnapshotName" : "com.apple.os.update-B5336980824124F599FD39FE91016493A74331B09F475250BB010B276FE2439E3DE3537349A3A957D3FF2A4B623B4ECC",
/// "bootState" : "booted",
/// "ddiServicesAvailable" : true,
/// "developerModeStatus" : "enabled",
/// "hasInternalOSBuild" : false,
/// "name" : "iPadName",
/// "osBuildUpdate" : "21A5248v",
/// "osVersionNumber" : "17.0",
/// "rootFileSystemIsWritable" : false,
/// "screenViewingURL" : "coredevice-devices:/viewDeviceByUUID?uuid=123456BB5-AEDE-7A22-B890-1234567890DD"
/// }
factory IOSCoreDeviceProperties.fromBetaJson(Map<String, Object?> data) {
return IOSCoreDeviceProperties._(
bootedFromSnapshot: data['bootedFromSnapshot'] is bool? ? data['bootedFromSnapshot'] as bool? : null,
bootedSnapshotName: data['bootedSnapshotName']?.toString(),
bootState: data['bootState']?.toString(),
ddiServicesAvailable: data['ddiServicesAvailable'] is bool? ? data['ddiServicesAvailable'] as bool? : null,
developerModeStatus: data['developerModeStatus']?.toString(),
hasInternalOSBuild: data['hasInternalOSBuild'] is bool? ? data['hasInternalOSBuild'] as bool? : null,
name: data['name']?.toString(),
osBuildUpdate: data['osBuildUpdate']?.toString(),
osVersionNumber: data['osVersionNumber']?.toString(),
rootFileSystemIsWritable: data['rootFileSystemIsWritable'] is bool? ? data['rootFileSystemIsWritable'] as bool? : null,
screenViewingURL: data['screenViewingURL']?.toString(),
);
}
final bool? bootedFromSnapshot;
final String? bootedSnapshotName;
final String? bootState;
final bool? ddiServicesAvailable;
final String? developerModeStatus;
final bool? hasInternalOSBuild;
final String? name;
final String? osBuildUpdate;
final String? osVersionNumber;
final bool? rootFileSystemIsWritable;
final String? screenViewingURL;
}
class _IOSCoreDeviceHardwareProperties {
_IOSCoreDeviceHardwareProperties._({
required this.cpuType,
required this.deviceType,
required this.ecid,
required this.hardwareModel,
required this.internalStorageCapacity,
required this.marketingName,
required this.platform,
required this.productType,
required this.serialNumber,
required this.supportedCPUTypes,
required this.supportedDeviceFamilies,
required this.thinningProductType,
required this.udid,
});
/// Parse `hardwareProperties` section of JSON from `devicectl list devices --json-output`
/// while it's in beta preview mode.
///
/// Example:
/// "hardwareProperties" : {
/// "cpuType" : {
/// "name" : "arm64e",
/// "subType" : 2,
/// "type" : 16777228
/// },
/// "deviceType" : "iPad",
/// "ecid" : 12345678903408542,
/// "hardwareModel" : "J617AP",
/// "internalStorageCapacity" : 128000000000,
/// "marketingName" : "iPad Pro (11-inch) (4th generation)\"",
/// "platform" : "iOS",
/// "productType" : "iPad14,3",
/// "serialNumber" : "HC123DHCQV",
/// "supportedCPUTypes" : [
/// {
/// "name" : "arm64e",
/// "subType" : 2,
/// "type" : 16777228
/// },
/// {
/// "name" : "arm64",
/// "subType" : 0,
/// "type" : 16777228
/// }
/// ],
/// "supportedDeviceFamilies" : [
/// 1,
/// 2
/// ],
/// "thinningProductType" : "iPad14,3-A",
/// "udid" : "00001234-0001234A3C03401E"
/// }
factory _IOSCoreDeviceHardwareProperties.fromBetaJson(
Map<String, Object?> data, {
required Logger logger,
}) {
_IOSCoreDeviceCPUType? cpuType;
if (data['cpuType'] case final Map<String, Object?> betaJson) {
cpuType = _IOSCoreDeviceCPUType.fromBetaJson(betaJson);
}
List<_IOSCoreDeviceCPUType>? supportedCPUTypes;
if (data['supportedCPUTypes'] case final List<Object?> values) {
supportedCPUTypes = <_IOSCoreDeviceCPUType>[
for (final Object? cpuTypeData in values)
if (cpuTypeData is Map<String, Object?>)
_IOSCoreDeviceCPUType.fromBetaJson(cpuTypeData),
];
}
List<int>? supportedDeviceFamilies;
if (data['supportedDeviceFamilies'] is List<Object?>) {
final List<Object?> values = data['supportedDeviceFamilies']! as List<Object?>;
try {
supportedDeviceFamilies = List<int>.from(values);
} on TypeError {
logger.printTrace('Error parsing supportedDeviceFamilies value: $values');
}
}
return _IOSCoreDeviceHardwareProperties._(
cpuType: cpuType,
deviceType: data['deviceType']?.toString(),
ecid: data['ecid'] is int? ? data['ecid'] as int? : null,
hardwareModel: data['hardwareModel']?.toString(),
internalStorageCapacity: data['internalStorageCapacity'] is int? ? data['internalStorageCapacity'] as int? : null,
marketingName: data['marketingName']?.toString(),
platform: data['platform']?.toString(),
productType: data['productType']?.toString(),
serialNumber: data['serialNumber']?.toString(),
supportedCPUTypes: supportedCPUTypes,
supportedDeviceFamilies: supportedDeviceFamilies,
thinningProductType: data['thinningProductType']?.toString(),
udid: data['udid']?.toString(),
);
}
final _IOSCoreDeviceCPUType? cpuType;
final String? deviceType;
final int? ecid;
final String? hardwareModel;
final int? internalStorageCapacity;
final String? marketingName;
final String? platform;
final String? productType;
final String? serialNumber;
final List<_IOSCoreDeviceCPUType>? supportedCPUTypes;
final List<int>? supportedDeviceFamilies;
final String? thinningProductType;
final String? udid;
}
class _IOSCoreDeviceCPUType {
_IOSCoreDeviceCPUType._({
this.name,
this.subType,
this.cpuType,
});
/// Parse `hardwareProperties.cpuType` and `hardwareProperties.supportedCPUTypes`
/// sections of JSON from `devicectl list devices --json-output` while it's in beta preview mode.
///
/// Example:
/// "cpuType" : {
/// "name" : "arm64e",
/// "subType" : 2,
/// "type" : 16777228
/// }
factory _IOSCoreDeviceCPUType.fromBetaJson(Map<String, Object?> data) {
return _IOSCoreDeviceCPUType._(
name: data['name']?.toString(),
subType: data['subType'] is int? ? data['subType'] as int? : null,
cpuType: data['type'] is int? ? data['type'] as int? : null,
);
}
final String? name;
final int? subType;
final int? cpuType;
}
@visibleForTesting
class IOSCoreDeviceInstalledApp {
IOSCoreDeviceInstalledApp._({
required this.appClip,
required this.builtByDeveloper,
required this.bundleIdentifier,
required this.bundleVersion,
required this.defaultApp,
required this.hidden,
required this.internalApp,
required this.name,
required this.removable,
required this.url,
required this.version,
});
/// Parse JSON from `devicectl device info apps --json-output` while it's in
/// beta preview mode.
///
/// Example:
/// {
/// "appClip" : false,
/// "builtByDeveloper" : true,
/// "bundleIdentifier" : "com.example.flutterApp",
/// "bundleVersion" : "1",
/// "defaultApp" : false,
/// "hidden" : false,
/// "internalApp" : false,
/// "name" : "Flutter App",
/// "removable" : true,
/// "url" : "file:///private/var/containers/Bundle/Application/12345E6A-7F89-0C12-345E-F6A7E890CFF1/Runner.app/",
/// "version" : "1.0.0"
/// }
factory IOSCoreDeviceInstalledApp.fromBetaJson(Map<String, Object?> data) {
return IOSCoreDeviceInstalledApp._(
appClip: data['appClip'] is bool? ? data['appClip'] as bool? : null,
builtByDeveloper: data['builtByDeveloper'] is bool? ? data['builtByDeveloper'] as bool? : null,
bundleIdentifier: data['bundleIdentifier']?.toString(),
bundleVersion: data['bundleVersion']?.toString(),
defaultApp: data['defaultApp'] is bool? ? data['defaultApp'] as bool? : null,
hidden: data['hidden'] is bool? ? data['hidden'] as bool? : null,
internalApp: data['internalApp'] is bool? ? data['internalApp'] as bool? : null,
name: data['name']?.toString(),
removable: data['removable'] is bool? ? data['removable'] as bool? : null,
url: data['url']?.toString(),
version: data['version']?.toString(),
);
}
final bool? appClip;
final bool? builtByDeveloper;
final String? bundleIdentifier;
final String? bundleVersion;
final bool? defaultApp;
final bool? hidden;
final bool? internalApp;
final String? name;
final bool? removable;
final String? url;
final String? version;
}