|  | // 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:collection'; | 
|  |  | 
|  | import 'package:meta/meta.dart'; | 
|  | import 'package:process/process.dart'; | 
|  | import 'package:xml/xml.dart'; | 
|  |  | 
|  | import '../application_package.dart'; | 
|  | import '../base/common.dart'; | 
|  | import '../base/file_system.dart'; | 
|  | import '../base/io.dart'; | 
|  | import '../base/logger.dart'; | 
|  | import '../base/process.dart'; | 
|  | import '../base/user_messages.dart'; | 
|  | import '../build_info.dart'; | 
|  | import '../project.dart'; | 
|  | import 'android_sdk.dart'; | 
|  | import 'gradle.dart'; | 
|  |  | 
|  | /// An application package created from an already built Android APK. | 
|  | class AndroidApk extends ApplicationPackage implements PrebuiltApplicationPackage { | 
|  | AndroidApk({ | 
|  | required super.id, | 
|  | required this.applicationPackage, | 
|  | required this.versionCode, | 
|  | required this.launchActivity, | 
|  | }) : assert(applicationPackage != null), | 
|  | assert(launchActivity != null); | 
|  |  | 
|  | /// Creates a new AndroidApk from an existing APK. | 
|  | /// | 
|  | /// Returns `null` if the APK was invalid or any required tooling was missing. | 
|  | static AndroidApk? fromApk( | 
|  | File apk, { | 
|  | required AndroidSdk androidSdk, | 
|  | required ProcessManager processManager, | 
|  | required UserMessages userMessages, | 
|  | required Logger logger, | 
|  | required ProcessUtils processUtils, | 
|  | }) { | 
|  | final String? aaptPath = androidSdk.latestVersion?.aaptPath; | 
|  | if (aaptPath == null || !processManager.canRun(aaptPath)) { | 
|  | logger.printError(userMessages.aaptNotFound); | 
|  | return null; | 
|  | } | 
|  |  | 
|  | String apptStdout; | 
|  | try { | 
|  | apptStdout = processUtils.runSync( | 
|  | <String>[ | 
|  | aaptPath, | 
|  | 'dump', | 
|  | 'xmltree', | 
|  | apk.path, | 
|  | 'AndroidManifest.xml', | 
|  | ], | 
|  | throwOnError: true, | 
|  | ).stdout.trim(); | 
|  | } on ProcessException catch (error) { | 
|  | logger.printError('Failed to extract manifest from APK: $error.'); | 
|  | return null; | 
|  | } | 
|  |  | 
|  | final ApkManifestData? data = ApkManifestData.parseFromXmlDump(apptStdout, logger); | 
|  |  | 
|  | if (data == null) { | 
|  | logger.printError('Unable to read manifest info from ${apk.path}.'); | 
|  | return null; | 
|  | } | 
|  |  | 
|  | final String? packageName = data.packageName; | 
|  | if (packageName == null || data.launchableActivityName == null) { | 
|  | logger.printError('Unable to read manifest info from ${apk.path}.'); | 
|  | return null; | 
|  | } | 
|  |  | 
|  | return AndroidApk( | 
|  | id: packageName, | 
|  | applicationPackage: apk, | 
|  | versionCode: data.versionCode == null ? null : int.tryParse(data.versionCode!), | 
|  | launchActivity: '${data.packageName}/${data.launchableActivityName}', | 
|  | ); | 
|  | } | 
|  |  | 
|  | @override | 
|  | final FileSystemEntity applicationPackage; | 
|  |  | 
|  | /// The path to the activity that should be launched. | 
|  | final String launchActivity; | 
|  |  | 
|  | /// The version code of the APK. | 
|  | final int? versionCode; | 
|  |  | 
|  | /// Creates a new AndroidApk based on the information in the Android manifest. | 
|  | static Future<AndroidApk?> fromAndroidProject( | 
|  | AndroidProject androidProject, { | 
|  | required AndroidSdk? androidSdk, | 
|  | required ProcessManager processManager, | 
|  | required UserMessages userMessages, | 
|  | required ProcessUtils processUtils, | 
|  | required Logger logger, | 
|  | required FileSystem fileSystem, | 
|  | BuildInfo? buildInfo, | 
|  | }) async { | 
|  | final File apkFile; | 
|  | final String filename; | 
|  | if (buildInfo == null) { | 
|  | filename = 'app.apk'; | 
|  | } else if (buildInfo.flavor == null) { | 
|  | filename = 'app-${buildInfo.mode.name}.apk'; | 
|  | } else { | 
|  | filename = 'app-${buildInfo.lowerCasedFlavor}-${buildInfo.mode.name}.apk'; | 
|  | } | 
|  |  | 
|  | if (androidProject.isUsingGradle && androidProject.isSupportedVersion) { | 
|  | apkFile = getApkDirectory(androidProject.parent).childFile(filename); | 
|  | if (apkFile.existsSync()) { | 
|  | // Grab information from the .apk. The gradle build script might alter | 
|  | // the application Id, so we need to look at what was actually built. | 
|  | return AndroidApk.fromApk( | 
|  | apkFile, | 
|  | androidSdk: androidSdk!, | 
|  | processManager: processManager, | 
|  | logger: logger, | 
|  | userMessages: userMessages, | 
|  | processUtils: processUtils, | 
|  | ); | 
|  | } | 
|  | // The .apk hasn't been built yet, so we work with what we have. The run | 
|  | // command will grab a new AndroidApk after building, to get the updated | 
|  | // IDs. | 
|  | } else { | 
|  | apkFile = fileSystem.file(fileSystem.path.join(getAndroidBuildDirectory(), filename)); | 
|  | } | 
|  |  | 
|  | final File manifest = androidProject.appManifestFile; | 
|  |  | 
|  | if (!manifest.existsSync()) { | 
|  | logger.printError('AndroidManifest.xml could not be found.'); | 
|  | logger.printError('Please check ${manifest.path} for errors.'); | 
|  | return null; | 
|  | } | 
|  |  | 
|  | final String manifestString = manifest.readAsStringSync(); | 
|  | XmlDocument document; | 
|  | try { | 
|  | document = XmlDocument.parse(manifestString); | 
|  | } on XmlException catch (exception) { | 
|  | String manifestLocation; | 
|  | if (androidProject.isUsingGradle) { | 
|  | manifestLocation = fileSystem.path.join(androidProject.hostAppGradleRoot.path, 'app', 'src', 'main', 'AndroidManifest.xml'); | 
|  | } else { | 
|  | manifestLocation = fileSystem.path.join(androidProject.hostAppGradleRoot.path, 'AndroidManifest.xml'); | 
|  | } | 
|  | logger.printError('AndroidManifest.xml is not a valid XML document.'); | 
|  | logger.printError('Please check $manifestLocation for errors.'); | 
|  | throwToolExit('XML Parser error message: $exception'); | 
|  | } | 
|  |  | 
|  | final Iterable<XmlElement> manifests = document.findElements('manifest'); | 
|  | if (manifests.isEmpty) { | 
|  | logger.printError('AndroidManifest.xml has no manifest element.'); | 
|  | logger.printError('Please check ${manifest.path} for errors.'); | 
|  | return null; | 
|  | } | 
|  | final String? packageId = manifests.first.getAttribute('package'); | 
|  |  | 
|  | String? launchActivity; | 
|  | for (final XmlElement activity in document.findAllElements('activity')) { | 
|  | final String? enabled = activity.getAttribute('android:enabled'); | 
|  | if (enabled != null && enabled == 'false') { | 
|  | continue; | 
|  | } | 
|  |  | 
|  | for (final XmlElement element in activity.findElements('intent-filter')) { | 
|  | String? actionName = ''; | 
|  | String? categoryName = ''; | 
|  | for (final XmlNode node in element.children) { | 
|  | if (node is! XmlElement) { | 
|  | continue; | 
|  | } | 
|  | final String? name = node.getAttribute('android:name'); | 
|  | if (name == 'android.intent.action.MAIN') { | 
|  | actionName = name; | 
|  | } else if (name == 'android.intent.category.LAUNCHER') { | 
|  | categoryName = name; | 
|  | } | 
|  | } | 
|  | if (actionName != null && categoryName != null && actionName.isNotEmpty && categoryName.isNotEmpty) { | 
|  | final String? activityName = activity.getAttribute('android:name'); | 
|  | launchActivity = '$packageId/$activityName'; | 
|  | break; | 
|  | } | 
|  | } | 
|  | } | 
|  |  | 
|  | if (packageId == null || launchActivity == null) { | 
|  | logger.printError('package identifier or launch activity not found.'); | 
|  | logger.printError('Please check ${manifest.path} for errors.'); | 
|  | return null; | 
|  | } | 
|  |  | 
|  | return AndroidApk( | 
|  | id: packageId, | 
|  | applicationPackage: apkFile, | 
|  | versionCode: null, | 
|  | launchActivity: launchActivity, | 
|  | ); | 
|  | } | 
|  |  | 
|  | @override | 
|  | String get name => applicationPackage.basename; | 
|  | } | 
|  |  | 
|  | abstract class _Entry { | 
|  | const _Entry(this.parent, this.level); | 
|  |  | 
|  | final _Element? parent; | 
|  | final int level; | 
|  | } | 
|  |  | 
|  | class _Element extends _Entry { | 
|  | _Element._(this.name, _Element? parent, int level) : super(parent, level); | 
|  |  | 
|  | factory _Element.fromLine(String line, _Element? parent) { | 
|  | //      E: application (line=29) | 
|  | final List<String> parts = line.trimLeft().split(' '); | 
|  | return _Element._(parts[1], parent, line.length - line.trimLeft().length); | 
|  | } | 
|  |  | 
|  | final List<_Entry> children = <_Entry>[]; | 
|  | final String? name; | 
|  |  | 
|  | void addChild(_Entry child) { | 
|  | children.add(child); | 
|  | } | 
|  |  | 
|  | _Attribute? firstAttribute(String name) { | 
|  | for (final _Attribute child in children.whereType<_Attribute>()) { | 
|  | if (child.key?.startsWith(name) ?? false) { | 
|  | return child; | 
|  | } | 
|  | } | 
|  | return null; | 
|  | } | 
|  |  | 
|  | _Element? firstElement(String name) { | 
|  | for (final _Element child in children.whereType<_Element>()) { | 
|  | if (child.name?.startsWith(name) ?? false) { | 
|  | return child; | 
|  | } | 
|  | } | 
|  | return null; | 
|  | } | 
|  |  | 
|  | Iterable<_Element> allElements(String name) { | 
|  | return children.whereType<_Element>().where((_Element e) => e.name?.startsWith(name) ?? false); | 
|  | } | 
|  | } | 
|  |  | 
|  | class _Attribute extends _Entry { | 
|  | const _Attribute._(this.key, this.value, _Element? parent, int level) : super(parent, level); | 
|  |  | 
|  | factory _Attribute.fromLine(String line, _Element parent) { | 
|  | //     A: android:label(0x01010001)="hello_world" (Raw: "hello_world") | 
|  | const String attributePrefix = 'A: '; | 
|  | final List<String> keyVal = line.substring(line.indexOf(attributePrefix) + attributePrefix.length).split('='); | 
|  | return _Attribute._(keyVal[0], keyVal[1], parent, line.length - line.trimLeft().length); | 
|  | } | 
|  |  | 
|  | final String? key; | 
|  | final String? value; | 
|  | } | 
|  |  | 
|  | class ApkManifestData { | 
|  | ApkManifestData._(this._data); | 
|  |  | 
|  | static bool _isAttributeWithValuePresent( | 
|  | _Element baseElement, String childElement, String attributeName, String attributeValue) { | 
|  | final Iterable<_Element> allElements = baseElement.allElements(childElement); | 
|  | for (final _Element oneElement in allElements) { | 
|  | final String? elementAttributeValue = oneElement | 
|  | .firstAttribute(attributeName) | 
|  | ?.value; | 
|  | if (elementAttributeValue != null && | 
|  | elementAttributeValue.startsWith(attributeValue)) { | 
|  | return true; | 
|  | } | 
|  | } | 
|  | return false; | 
|  | } | 
|  |  | 
|  | static ApkManifestData? parseFromXmlDump(String data, Logger logger) { | 
|  | if (data == null || data.trim().isEmpty) { | 
|  | return null; | 
|  | } | 
|  |  | 
|  | final List<String> lines = data.split('\n'); | 
|  | assert(lines.length > 3); | 
|  |  | 
|  | final int manifestLine = lines.indexWhere((String line) => line.contains('E: manifest')); | 
|  | final _Element manifest = _Element.fromLine(lines[manifestLine], null); | 
|  | _Element currentElement = manifest; | 
|  |  | 
|  | for (final String line in lines.skip(manifestLine)) { | 
|  | final String trimLine = line.trimLeft(); | 
|  | final int level = line.length - trimLine.length; | 
|  |  | 
|  | // Handle level out | 
|  | while (currentElement.parent != null && level <= currentElement.level) { | 
|  | currentElement = currentElement.parent!; | 
|  | } | 
|  |  | 
|  | if (level > currentElement.level) { | 
|  | switch (trimLine[0]) { | 
|  | case 'A': | 
|  | currentElement | 
|  | .addChild(_Attribute.fromLine(line, currentElement)); | 
|  | break; | 
|  | case 'E': | 
|  | final _Element element = _Element.fromLine(line, currentElement); | 
|  | currentElement.addChild(element); | 
|  | currentElement = element; | 
|  | } | 
|  | } | 
|  | } | 
|  |  | 
|  | final _Element? application = manifest.firstElement('application'); | 
|  | if (application == null) { | 
|  | return null; | 
|  | } | 
|  |  | 
|  | final Iterable<_Element> activities = application.allElements('activity'); | 
|  |  | 
|  | _Element? launchActivity; | 
|  | for (final _Element activity in activities) { | 
|  | final _Attribute? enabled = activity.firstAttribute('android:enabled'); | 
|  | final Iterable<_Element> intentFilters = activity.allElements('intent-filter'); | 
|  | final bool isEnabledByDefault = enabled == null; | 
|  | final bool isExplicitlyEnabled = enabled != null && (enabled.value?.contains('0xffffffff') ?? false); | 
|  | if (!(isEnabledByDefault || isExplicitlyEnabled)) { | 
|  | continue; | 
|  | } | 
|  |  | 
|  | for (final _Element element in intentFilters) { | 
|  | final bool isMainAction = _isAttributeWithValuePresent( | 
|  | element, 'action', 'android:name', '"android.intent.action.MAIN"'); | 
|  | if (!isMainAction) { | 
|  | continue; | 
|  | } | 
|  | final bool isLauncherCategory = _isAttributeWithValuePresent( | 
|  | element, 'category', 'android:name', | 
|  | '"android.intent.category.LAUNCHER"'); | 
|  | if (!isLauncherCategory) { | 
|  | continue; | 
|  | } | 
|  | launchActivity = activity; | 
|  | break; | 
|  | } | 
|  | if (launchActivity != null) { | 
|  | break; | 
|  | } | 
|  | } | 
|  |  | 
|  | final _Attribute? package = manifest.firstAttribute('package'); | 
|  | // "io.flutter.examples.hello_world" (Raw: "io.flutter.examples.hello_world") | 
|  | final String? packageName = package?.value?.substring(1, package.value?.indexOf('" ')); | 
|  |  | 
|  | if (launchActivity == null) { | 
|  | logger.printError('Error running $packageName. Default activity not found'); | 
|  | return null; | 
|  | } | 
|  |  | 
|  | final _Attribute? nameAttribute = launchActivity.firstAttribute('android:name'); | 
|  | // "io.flutter.examples.hello_world.MainActivity" (Raw: "io.flutter.examples.hello_world.MainActivity") | 
|  | final String? activityName = nameAttribute?.value?.substring(1, nameAttribute.value?.indexOf('" ')); | 
|  |  | 
|  | // Example format: (type 0x10)0x1 | 
|  | final _Attribute? versionCodeAttr = manifest.firstAttribute('android:versionCode'); | 
|  | if (versionCodeAttr == null) { | 
|  | logger.printError('Error running $packageName. Manifest versionCode not found'); | 
|  | return null; | 
|  | } | 
|  | if (versionCodeAttr.value?.startsWith('(type 0x10)') != true) { | 
|  | logger.printError('Error running $packageName. Manifest versionCode invalid'); | 
|  | return null; | 
|  | } | 
|  | final int? versionCode = versionCodeAttr.value == null ? null : int.tryParse(versionCodeAttr.value!.substring(11)); | 
|  | if (versionCode == null) { | 
|  | logger.printError('Error running $packageName. Manifest versionCode invalid'); | 
|  | return null; | 
|  | } | 
|  |  | 
|  | final Map<String, Map<String, String>> map = <String, Map<String, String>>{ | 
|  | if (packageName != null) | 
|  | 'package': <String, String>{'name': packageName}, | 
|  | 'version-code': <String, String>{'name': versionCode.toString()}, | 
|  | if (activityName != null) | 
|  | 'launchable-activity': <String, String>{'name': activityName}, | 
|  | }; | 
|  |  | 
|  | return ApkManifestData._(map); | 
|  | } | 
|  |  | 
|  | final Map<String, Map<String, String>> _data; | 
|  |  | 
|  | @visibleForTesting | 
|  | Map<String, Map<String, String>> get data => | 
|  | UnmodifiableMapView<String, Map<String, String>>(_data); | 
|  |  | 
|  | String? get packageName => _data['package'] == null ? null : _data['package']?['name']; | 
|  |  | 
|  | String? get versionCode => _data['version-code'] == null ? null : _data['version-code']?['name']; | 
|  |  | 
|  | String? get launchableActivityName { | 
|  | return _data['launchable-activity'] == null ? null : _data['launchable-activity']?['name']; | 
|  | } | 
|  |  | 
|  | @override | 
|  | String toString() => _data.toString(); | 
|  | } |