// 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:async'; import 'dart:collection'; import 'package:meta/meta.dart'; import 'package:xml/xml.dart'; import 'android/gradle.dart'; import 'base/common.dart'; import 'base/context.dart'; import 'base/file_system.dart'; import 'base/io.dart'; import 'base/process.dart'; import 'base/user_messages.dart'; import 'build_info.dart'; import 'fuchsia/application_package.dart'; import 'globals.dart' as globals; import 'ios/plist_parser.dart'; import 'linux/application_package.dart'; import 'macos/application_package.dart'; import 'project.dart'; import 'tester/flutter_tester.dart'; import 'web/web_device.dart'; import 'windows/application_package.dart'; class ApplicationPackageFactory { static ApplicationPackageFactory get instance => context.get<ApplicationPackageFactory>(); Future<ApplicationPackage> getPackageForPlatform( TargetPlatform platform, { BuildInfo buildInfo, File applicationBinary, }) async { switch (platform) { case TargetPlatform.android: case TargetPlatform.android_arm: case TargetPlatform.android_arm64: case TargetPlatform.android_x64: case TargetPlatform.android_x86: if (globals.androidSdk?.licensesAvailable == true && globals.androidSdk?.latestVersion == null) { await checkGradleDependencies(); } return applicationBinary == null ? await AndroidApk.fromAndroidProject(FlutterProject.current().android) : AndroidApk.fromApk(applicationBinary); case TargetPlatform.ios: return applicationBinary == null ? await IOSApp.fromIosProject(FlutterProject.current().ios, buildInfo) : IOSApp.fromPrebuiltApp(applicationBinary); case TargetPlatform.tester: return FlutterTesterApp.fromCurrentDirectory(globals.fs); case TargetPlatform.darwin_x64: return applicationBinary == null ? MacOSApp.fromMacOSProject(FlutterProject.current().macos) : MacOSApp.fromPrebuiltApp(applicationBinary); case TargetPlatform.web_javascript: if (!FlutterProject.current().web.existsSync()) { return null; } return WebApplicationPackage(FlutterProject.current()); case TargetPlatform.linux_x64: return applicationBinary == null ? LinuxApp.fromLinuxProject(FlutterProject.current().linux) : LinuxApp.fromPrebuiltApp(applicationBinary); case TargetPlatform.windows_x64: return applicationBinary == null ? WindowsApp.fromWindowsProject(FlutterProject.current().windows) : WindowsApp.fromPrebuiltApp(applicationBinary); case TargetPlatform.fuchsia_arm64: case TargetPlatform.fuchsia_x64: return applicationBinary == null ? FuchsiaApp.fromFuchsiaProject(FlutterProject.current().fuchsia) : FuchsiaApp.fromPrebuiltApp(applicationBinary); } assert(platform != null); return null; } } abstract class ApplicationPackage { ApplicationPackage({ @required this.id }) : assert(id != null); /// Package ID from the Android Manifest or equivalent. final String id; String get name; String get displayName => name; File get packagesFile => null; @override String toString() => displayName ?? id; } 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. factory AndroidApk.fromApk(File apk) { final String aaptPath = globals.androidSdk?.latestVersion?.aaptPath; if (aaptPath == null) { globals.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) { globals.printError('Failed to extract manifest from APK: $error.'); return null; } final ApkManifestData data = ApkManifestData.parseFromXmlDump(apptStdout); if (data == null) { globals.printError('Unable to read manifest info from ${apk.path}.'); return null; } if (data.packageName == null || data.launchableActivityName == null) { globals.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) async { File apkFile; if (androidProject.isUsingGradle) { apkFile = await getGradleAppOut(androidProject); 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); } // 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 = globals.fs.file(globals.fs.path.join(getAndroidBuildDirectory(), 'app.apk')); } final File manifest = androidProject.appManifestFile; if (!manifest.existsSync()) { globals.printError('AndroidManifest.xml could not be found.'); globals.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 = globals.fs.path.join(androidProject.hostAppGradleRoot.path, 'app', 'src', 'main', 'AndroidManifest.xml'); } else { manifestLocation = globals.fs.path.join(androidProject.hostAppGradleRoot.path, 'AndroidManifest.xml'); } globals.printError('AndroidManifest.xml is not a valid XML document.'); globals.printError('Please check $manifestLocation for errors.'); throwToolExit('XML Parser error message: ${exception.toString()}'); } final Iterable<XmlElement> manifests = document.findElements('manifest'); if (manifests.isEmpty) { globals.printError('AndroidManifest.xml has no manifest element.'); globals.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) { globals.printError('package identifier or launch activity not found.'); globals.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; } /// Tests whether a [Directory] is an iOS bundle directory. bool _isBundleDirectory(Directory dir) => dir.path.endsWith('.app'); abstract class IOSApp extends ApplicationPackage { IOSApp({@required String projectBundleId}) : super(id: projectBundleId); /// Creates a new IOSApp from an existing app bundle or IPA. factory IOSApp.fromPrebuiltApp(FileSystemEntity applicationBinary) { final FileSystemEntityType entityType = globals.fs.typeSync(applicationBinary.path); if (entityType == FileSystemEntityType.notFound) { globals.printError( 'File "${applicationBinary.path}" does not exist. Use an app bundle or an ipa.'); return null; } Directory bundleDir; if (entityType == FileSystemEntityType.directory) { final Directory directory = globals.fs.directory(applicationBinary); if (!_isBundleDirectory(directory)) { globals.printError('Folder "${applicationBinary.path}" is not an app bundle.'); return null; } bundleDir = globals.fs.directory(applicationBinary); } else { // Try to unpack as an ipa. final Directory tempDir = globals.fs.systemTempDirectory.createTempSync('flutter_app.'); shutdownHooks.addShutdownHook(() async { await tempDir.delete(recursive: true); }, ShutdownStage.STILL_RECORDING); globals.os.unzip(globals.fs.file(applicationBinary), tempDir); final Directory payloadDir = globals.fs.directory( globals.fs.path.join(tempDir.path, 'Payload'), ); if (!payloadDir.existsSync()) { globals.printError( 'Invalid prebuilt iOS ipa. Does not contain a "Payload" directory.'); return null; } try { bundleDir = payloadDir.listSync().whereType<Directory>().singleWhere(_isBundleDirectory); } on StateError { globals.printError( 'Invalid prebuilt iOS ipa. Does not contain a single app bundle.'); return null; } } final String plistPath = globals.fs.path.join(bundleDir.path, 'Info.plist'); if (!globals.fs.file(plistPath).existsSync()) { globals.printError('Invalid prebuilt iOS app. Does not contain Info.plist.'); return null; } final String id = globals.plistParser.getValueFromFile( plistPath, PlistParser.kCFBundleIdentifierKey, ); if (id == null) { globals.printError('Invalid prebuilt iOS app. Info.plist does not contain bundle identifier'); return null; } return PrebuiltIOSApp( bundleDir: bundleDir, bundleName: globals.fs.path.basename(bundleDir.path), projectBundleId: id, ); } static Future<IOSApp> fromIosProject(IosProject project, BuildInfo buildInfo) { if (getCurrentHostPlatform() != HostPlatform.darwin_x64) { return null; } if (!project.exists) { // If the project doesn't exist at all the current hint to run flutter // create is accurate. return null; } if (!project.xcodeProject.existsSync()) { globals.printError('Expected ios/Runner.xcodeproj but this file is missing.'); return null; } if (!project.xcodeProjectInfoFile.existsSync()) { globals.printError('Expected ios/Runner.xcodeproj/project.pbxproj but this file is missing.'); return null; } return BuildableIOSApp.fromProject(project, buildInfo); } @override String get displayName => id; String get simulatorBundlePath; String get deviceBundlePath; } class BuildableIOSApp extends IOSApp { BuildableIOSApp(this.project, String projectBundleId, String hostAppBundleName) : _hostAppBundleName = hostAppBundleName, super(projectBundleId: projectBundleId); static Future<BuildableIOSApp> fromProject(IosProject project, BuildInfo buildInfo) async { final String projectBundleId = await project.productBundleIdentifier(buildInfo); final String hostAppBundleName = await project.hostAppBundleName(buildInfo); return BuildableIOSApp(project, projectBundleId, hostAppBundleName); } final IosProject project; final String _hostAppBundleName; @override String get name => _hostAppBundleName; @override String get simulatorBundlePath => _buildAppPath('iphonesimulator'); @override String get deviceBundlePath => _buildAppPath('iphoneos'); String _buildAppPath(String type) { return globals.fs.path.join(getIosBuildDirectory(), type, _hostAppBundleName); } } class PrebuiltIOSApp extends IOSApp { PrebuiltIOSApp({ this.bundleDir, this.bundleName, @required String projectBundleId, }) : super(projectBundleId: projectBundleId); final Directory bundleDir; final String bundleName; @override String get name => bundleName; @override String get simulatorBundlePath => _bundlePath; @override String get deviceBundlePath => _bundlePath; String get _bundlePath => bundleDir.path; } class ApplicationPackageStore { ApplicationPackageStore({ this.android, this.iOS, this.fuchsia }); AndroidApk android; IOSApp iOS; FuchsiaApp fuchsia; LinuxApp linux; MacOSApp macOS; WindowsApp windows; Future<ApplicationPackage> getPackageForPlatform( TargetPlatform platform, BuildInfo buildInfo, ) async { switch (platform) { case TargetPlatform.android: case TargetPlatform.android_arm: case TargetPlatform.android_arm64: case TargetPlatform.android_x64: case TargetPlatform.android_x86: android ??= await AndroidApk.fromAndroidProject(FlutterProject.current().android); return android; case TargetPlatform.ios: iOS ??= await IOSApp.fromIosProject(FlutterProject.current().ios, buildInfo); return iOS; case TargetPlatform.fuchsia_arm64: case TargetPlatform.fuchsia_x64: fuchsia ??= FuchsiaApp.fromFuchsiaProject(FlutterProject.current().fuchsia); return fuchsia; case TargetPlatform.darwin_x64: macOS ??= MacOSApp.fromMacOSProject(FlutterProject.current().macos); return macOS; case TargetPlatform.linux_x64: linux ??= LinuxApp.fromLinuxProject(FlutterProject.current().linux); return linux; case TargetPlatform.windows_x64: windows ??= WindowsApp.fromWindowsProject(FlutterProject.current().windows); return windows; case TargetPlatform.tester: case TargetPlatform.web_javascript: return null; } return null; } } class _Entry { _Element parent; int level; } class _Element extends _Entry { _Element.fromLine(String line, _Element parent) { // E: application (line=29) final List<String> parts = line.trimLeft().split(' '); name = parts[1]; level = line.length - line.trimLeft().length; this.parent = parent; children = <_Entry>[]; } List<_Entry> children; 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 { _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('='); key = keyVal[0]; value = keyVal[1]; level = line.length - line.trimLeft().length; this.parent = parent; } String key; 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) { 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) { globals.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) { globals.printError('Error running $packageName. Manifest versionCode not found'); return null; } if (!versionCodeAttr.value.startsWith('(type 0x10)')) { globals.printError('Error running $packageName. Manifest versionCode invalid'); return null; } final int versionCode = int.tryParse(versionCodeAttr.value.substring(11)); if (versionCode == null) { globals.printError('Error running $packageName. Manifest versionCode invalid'); return null; } final Map<String, Map<String, String>> map = <String, Map<String, String>>{}; map['package'] = <String, String>{'name': packageName}; map['version-code'] = <String, String>{'name': versionCode.toString()}; map['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(); }