// 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. // @dart = 2.8 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 { AndroidApk({ String id, @required this.file, @required this.versionCode, @required this.launchActivity, }) : assert(file != null), assert(launchActivity != null), super(id: id); /// Creates a new AndroidApk from an existing APK. /// /// Returns `null` if the APK was invalid or any required tooling was missing. factory 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; } if (data.packageName == null || data.launchableActivityName == null) { logger.printError('Unable to read manifest info from ${apk.path}.'); return null; } return AndroidApk( id: data.packageName, file: apk, versionCode: int.tryParse(data.versionCode), launchActivity: '${data.packageName}/${data.launchableActivityName}', ); } /// Path to the actual apk file. final File file; /// 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, }) async { File apkFile; if (androidProject.isUsingGradle && androidProject.isSupportedVersion) { apkFile = getApkDirectory(androidProject.parent).childFile('app.apk'); 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(), 'app.apk')); } 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 XmlParserException 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.toString()}'); } 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 XmlElement xmlElement = node as XmlElement; final String name = xmlElement.getAttribute('android:name'); if (name == 'android.intent.action.MAIN') { actionName = name; } else if (name == 'android.intent.category.LAUNCHER') { categoryName = name; } } if (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, file: apkFile, versionCode: null, launchActivity: launchActivity, ); } @override File get packagesFile => file; @override String get name => file.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) { return children.whereType<_Attribute>().firstWhere( (_Attribute e) => e.key.startsWith(name), orElse: () => null, ); } _Element firstElement(String name) { return children.whereType<_Element>().firstWhere( (_Element e) => e.name.startsWith(name), orElse: () => null, ); } Iterable<_Element> allElements(String name) { return children.whereType<_Element>().where((_Element e) => e.name.startsWith(name)); } } 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'); 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)')) { logger.printError('Error running $packageName. Manifest versionCode invalid'); return null; } final int versionCode = 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(); }