|  | // Copyright 2015 The Chromium 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:convert'; | 
|  | import 'dart:io'; | 
|  |  | 
|  | import 'package:path/path.dart' as path; | 
|  | import 'package:yaml/yaml.dart'; | 
|  |  | 
|  | import '../android/device_android.dart'; | 
|  | import '../artifacts.dart'; | 
|  | import '../base/context.dart'; | 
|  | import '../base/file_system.dart'; | 
|  | import '../base/process.dart'; | 
|  | import '../build_configuration.dart'; | 
|  | import '../flx.dart' as flx; | 
|  | import '../runner/flutter_command.dart'; | 
|  | import 'start.dart'; | 
|  |  | 
|  | const String _kDefaultAndroidManifestPath = 'apk/AndroidManifest.xml'; | 
|  | const String _kDefaultOutputPath = 'build/app.apk'; | 
|  | const String _kDefaultResourcesPath = 'apk/res'; | 
|  |  | 
|  | const String _kFlutterManifestPath = 'flutter.yaml'; | 
|  | const String _kPubspecYamlPath = 'pubspec.yaml'; | 
|  |  | 
|  | // Alias of the key provided in the Chromium debug keystore | 
|  | const String _kDebugKeystoreKeyAlias = "chromiumdebugkey"; | 
|  |  | 
|  | // Password for the Chromium debug keystore | 
|  | const String _kDebugKeystorePassword = "chromium"; | 
|  |  | 
|  | const String _kAndroidPlatformVersion = '22'; | 
|  | const String _kBuildToolsVersion = '22.0.1'; | 
|  |  | 
|  | /// Copies files into a new directory structure. | 
|  | class _AssetBuilder { | 
|  | final Directory outDir; | 
|  |  | 
|  | Directory _assetDir; | 
|  |  | 
|  | _AssetBuilder(this.outDir, String assetDirName) { | 
|  | _assetDir = new Directory('${outDir.path}/$assetDirName'); | 
|  | _assetDir.createSync(recursive:  true); | 
|  | } | 
|  |  | 
|  | void add(File asset, String relativePath) { | 
|  | String destPath = path.join(_assetDir.path, relativePath); | 
|  | ensureDirectoryExists(destPath); | 
|  | asset.copySync(destPath); | 
|  | } | 
|  |  | 
|  | Directory get directory => _assetDir; | 
|  | } | 
|  |  | 
|  | /// Builds an APK package using Android SDK tools. | 
|  | class _ApkBuilder { | 
|  | final String androidSdk; | 
|  |  | 
|  | File _androidJar; | 
|  | File _aapt; | 
|  | File _dx; | 
|  | File _zipalign; | 
|  | String _jarsigner; | 
|  |  | 
|  | _ApkBuilder(this.androidSdk) { | 
|  | _androidJar = new File('$androidSdk/platforms/android-$_kAndroidPlatformVersion/android.jar'); | 
|  |  | 
|  | String buildTools = '$androidSdk/build-tools/$_kBuildToolsVersion'; | 
|  | _aapt = new File('$buildTools/aapt'); | 
|  | _dx = new File('$buildTools/dx'); | 
|  | _zipalign = new File('$buildTools/zipalign'); | 
|  | _jarsigner = 'jarsigner'; | 
|  | } | 
|  |  | 
|  | bool checkSdkPath() { | 
|  | return (_androidJar.existsSync() && _aapt.existsSync() && _dx.existsSync() && _zipalign.existsSync()); | 
|  | } | 
|  |  | 
|  | void compileClassesDex(File classesDex, List<File> jars) { | 
|  | List<String> packageArgs = [_dx.path, | 
|  | '--dex', | 
|  | '--force-jumbo', | 
|  | '--output', classesDex.path | 
|  | ]; | 
|  |  | 
|  | packageArgs.addAll(jars.map((File f) => f.path)); | 
|  |  | 
|  | runCheckedSync(packageArgs); | 
|  | } | 
|  |  | 
|  | void package(File outputApk, File androidManifest, Directory assets, Directory artifacts, Directory resources) { | 
|  | List<String> packageArgs = [_aapt.path, | 
|  | 'package', | 
|  | '-M', androidManifest.path, | 
|  | '-A', assets.path, | 
|  | '-I', _androidJar.path, | 
|  | '-F', outputApk.path, | 
|  | ]; | 
|  | if (resources != null) { | 
|  | packageArgs.addAll(['-S', resources.absolute.path]); | 
|  | } | 
|  | packageArgs.add(artifacts.path); | 
|  | runCheckedSync(packageArgs); | 
|  | } | 
|  |  | 
|  | void sign(File keystore, String keystorePassword, String keyAlias, String keyPassword, File outputApk) { | 
|  | runCheckedSync([_jarsigner, | 
|  | '-keystore', keystore.path, | 
|  | '-storepass', keystorePassword, | 
|  | '-keypass', keyPassword, | 
|  | outputApk.path, | 
|  | keyAlias, | 
|  | ]); | 
|  | } | 
|  |  | 
|  | void align(File unalignedApk, File outputApk) { | 
|  | runCheckedSync([_zipalign.path, '-f', '4', unalignedApk.path, outputApk.path]); | 
|  | } | 
|  | } | 
|  |  | 
|  | class _ApkComponents { | 
|  | Directory androidSdk; | 
|  | File manifest; | 
|  | File icuData; | 
|  | List<File> jars; | 
|  | List<Map<String, String>> services = []; | 
|  | File libSkyShell; | 
|  | File debugKeystore; | 
|  | Directory resources; | 
|  | } | 
|  |  | 
|  | // TODO(mpcomplete): find a better home for this. | 
|  | dynamic _loadYamlFile(String path) { | 
|  | if (!FileSystemEntity.isFileSync(path)) | 
|  | return null; | 
|  | String manifestString = new File(path).readAsStringSync(); | 
|  | return loadYaml(manifestString); | 
|  | } | 
|  |  | 
|  | class ApkCommand extends FlutterCommand { | 
|  | final String name = 'apk'; | 
|  | final String description = 'Build an Android APK package.'; | 
|  |  | 
|  | ApkCommand() { | 
|  | argParser.addOption('manifest', | 
|  | abbr: 'm', | 
|  | defaultsTo: _kDefaultAndroidManifestPath, | 
|  | help: 'Android manifest XML file.'); | 
|  | argParser.addOption('resources', | 
|  | abbr: 'r', | 
|  | defaultsTo: _kDefaultResourcesPath, | 
|  | help: 'Resources directory path.'); | 
|  | argParser.addOption('output-file', | 
|  | abbr: 'o', | 
|  | defaultsTo: _kDefaultOutputPath, | 
|  | help: 'Output APK file.'); | 
|  | argParser.addOption('target', | 
|  | abbr: 't', | 
|  | defaultsTo: '', | 
|  | help: 'Target app path or filename used to build the FLX.'); | 
|  | argParser.addOption('flx', | 
|  | abbr: 'f', | 
|  | defaultsTo: '', | 
|  | help: 'Path to the FLX file. If this is not provided, an FLX will be built.'); | 
|  | argParser.addOption('keystore', | 
|  | defaultsTo: '', | 
|  | help: 'Path to the keystore used to sign the app.'); | 
|  | argParser.addOption('keystore-password', | 
|  | defaultsTo: '', | 
|  | help: 'Password used to access the keystore.'); | 
|  | argParser.addOption('keystore-key-alias', | 
|  | defaultsTo: '', | 
|  | help: 'Alias of the entry within the keystore.'); | 
|  | argParser.addOption('keystore-key-password', | 
|  | defaultsTo: '', | 
|  | help: 'Password for the entry within the keystore.'); | 
|  | } | 
|  |  | 
|  | Future _findServices(_ApkComponents components) async { | 
|  | if (!ArtifactStore.isPackageRootValid) | 
|  | return; | 
|  |  | 
|  | dynamic manifest = _loadYamlFile(_kFlutterManifestPath); | 
|  | if (manifest['services'] == null) | 
|  | return; | 
|  |  | 
|  | for (String service in manifest['services']) { | 
|  | String serviceRoot = '${ArtifactStore.packageRoot}/$service/apk'; | 
|  | dynamic serviceConfig = _loadYamlFile('$serviceRoot/config.yaml'); | 
|  | if (serviceConfig == null || serviceConfig['jars'] == null) | 
|  | continue; | 
|  | components.services.addAll(serviceConfig['services']); | 
|  | for (String jar in serviceConfig['jars']) { | 
|  | if (jar.startsWith("android-sdk:")) { | 
|  | // Jar is something shipped in the standard android SDK. | 
|  | jar = jar.replaceAll('android-sdk:', '${components.androidSdk.path}/'); | 
|  | components.jars.add(new File(jar)); | 
|  | } else if (jar.startsWith("http")) { | 
|  | // Jar is a URL to download. | 
|  | String cachePath = await ArtifactStore.getThirdPartyFile(jar, service); | 
|  | components.jars.add(new File(cachePath)); | 
|  | } else { | 
|  | // Assume jar is a path relative to the service's root dir. | 
|  | components.jars.add(new File(path.join(serviceRoot, jar))); | 
|  | } | 
|  | } | 
|  | } | 
|  | } | 
|  |  | 
|  | Future<_ApkComponents> _findApkComponents(BuildConfiguration config) async { | 
|  | String androidSdkPath; | 
|  | List<String> artifactPaths; | 
|  | if (runner.enginePath != null) { | 
|  | androidSdkPath = '${runner.enginePath}/third_party/android_tools/sdk'; | 
|  | artifactPaths = [ | 
|  | '${runner.enginePath}/third_party/icu/android/icudtl.dat', | 
|  | '${config.buildDir}/gen/sky/shell/shell/classes.dex.jar', | 
|  | '${config.buildDir}/gen/sky/shell/shell/shell/libs/armeabi-v7a/libsky_shell.so', | 
|  | '${runner.enginePath}/build/android/ant/chromium-debug.keystore', | 
|  | ]; | 
|  | } else { | 
|  | androidSdkPath = AndroidDevice.getAndroidSdkPath(); | 
|  | if (androidSdkPath == null) { | 
|  | return null; | 
|  | } | 
|  | List<ArtifactType> artifactTypes = <ArtifactType>[ | 
|  | ArtifactType.androidIcuData, | 
|  | ArtifactType.androidClassesJar, | 
|  | ArtifactType.androidLibSkyShell, | 
|  | ArtifactType.androidKeystore, | 
|  | ]; | 
|  | Iterable<Future<String>> pathFutures = artifactTypes.map( | 
|  | (ArtifactType type) => ArtifactStore.getPath(ArtifactStore.getArtifact( | 
|  | type: type, targetPlatform: TargetPlatform.android))); | 
|  | artifactPaths = await Future.wait(pathFutures); | 
|  | } | 
|  |  | 
|  | _ApkComponents components = new _ApkComponents(); | 
|  | components.androidSdk = new Directory(androidSdkPath); | 
|  | components.manifest = new File(argResults['manifest']); | 
|  | components.icuData = new File(artifactPaths[0]); | 
|  | components.jars = [new File(artifactPaths[1])]; | 
|  | components.libSkyShell = new File(artifactPaths[2]); | 
|  | components.debugKeystore = new File(artifactPaths[3]); | 
|  | components.resources = new Directory(argResults['resources']); | 
|  |  | 
|  | await _findServices(components); | 
|  |  | 
|  | if (!components.resources.existsSync()) { | 
|  | // TODO(eseidel): This level should be higher when path is manually set. | 
|  | printStatus('Can not locate Resources: ${components.resources}, ignoring.'); | 
|  | components.resources = null; | 
|  | } | 
|  |  | 
|  | if (!components.androidSdk.existsSync()) { | 
|  | printError('Can not locate Android SDK: $androidSdkPath'); | 
|  | return null; | 
|  | } | 
|  | if (!(new _ApkBuilder(components.androidSdk.path).checkSdkPath())) { | 
|  | printError('Can not locate expected Android SDK tools at $androidSdkPath'); | 
|  | printError('You must install version $_kAndroidPlatformVersion of the SDK platform'); | 
|  | printError('and version $_kBuildToolsVersion of the build tools.'); | 
|  | return null; | 
|  | } | 
|  | for (File f in [components.manifest, components.icuData, | 
|  | components.libSkyShell, components.debugKeystore] | 
|  | ..addAll(components.jars)) { | 
|  | if (!f.existsSync()) { | 
|  | printError('Can not locate file: ${f.path}'); | 
|  | return null; | 
|  | } | 
|  | } | 
|  |  | 
|  | return components; | 
|  | } | 
|  |  | 
|  | // Outputs a services.json file for the flutter engine to read. Format: | 
|  | // { | 
|  | //   services: [ | 
|  | //     { name: string, class: string }, | 
|  | //     ... | 
|  | //   ] | 
|  | // } | 
|  | void _generateServicesConfig(File servicesConfig, List<Map<String, String>> servicesIn) { | 
|  | List<Map<String, String>> services = | 
|  | servicesIn.map((Map<String, String> service) => { | 
|  | 'name': service['name'], | 
|  | 'class': service['registration-class'] | 
|  | }).toList(); | 
|  |  | 
|  | Map<String, dynamic> json = { 'services': services }; | 
|  | servicesConfig.writeAsStringSync(JSON.encode(json), mode: FileMode.WRITE, flush: true); | 
|  | } | 
|  |  | 
|  | int _buildApk(_ApkComponents components, String flxPath) { | 
|  | Directory tempDir = Directory.systemTemp.createTempSync('flutter_tools'); | 
|  | try { | 
|  | _ApkBuilder builder = new _ApkBuilder(components.androidSdk.path); | 
|  |  | 
|  | File classesDex = new File('${tempDir.path}/classes.dex'); | 
|  | builder.compileClassesDex(classesDex, components.jars); | 
|  |  | 
|  | File servicesConfig = new File('${tempDir.path}/services.json'); | 
|  | _generateServicesConfig(servicesConfig, components.services); | 
|  |  | 
|  | _AssetBuilder assetBuilder = new _AssetBuilder(tempDir, 'assets'); | 
|  | assetBuilder.add(components.icuData, 'icudtl.dat'); | 
|  | assetBuilder.add(new File(flxPath), 'app.flx'); | 
|  | assetBuilder.add(servicesConfig, 'services.json'); | 
|  |  | 
|  | _AssetBuilder artifactBuilder = new _AssetBuilder(tempDir, 'artifacts'); | 
|  | artifactBuilder.add(classesDex, 'classes.dex'); | 
|  | artifactBuilder.add(components.libSkyShell, 'lib/armeabi-v7a/libsky_shell.so'); | 
|  |  | 
|  | File unalignedApk = new File('${tempDir.path}/app.apk.unaligned'); | 
|  | builder.package(unalignedApk, components.manifest, assetBuilder.directory, | 
|  | artifactBuilder.directory, components.resources); | 
|  |  | 
|  | int signResult = _signApk(builder, components, unalignedApk); | 
|  | if (signResult != 0) | 
|  | return signResult; | 
|  |  | 
|  | File finalApk = new File(argResults['output-file']); | 
|  | ensureDirectoryExists(finalApk.path); | 
|  | builder.align(unalignedApk, finalApk); | 
|  |  | 
|  | printStatus('APK generated: ${finalApk.path}'); | 
|  |  | 
|  | return 0; | 
|  | } finally { | 
|  | tempDir.deleteSync(recursive: true); | 
|  | } | 
|  | } | 
|  |  | 
|  | int _signApk(_ApkBuilder builder, _ApkComponents components, File apk) { | 
|  | File keystore; | 
|  | String keystorePassword; | 
|  | String keyAlias; | 
|  | String keyPassword; | 
|  |  | 
|  | if (argResults['keystore'].isEmpty) { | 
|  | printError('Signing the APK using the debug keystore'); | 
|  | keystore = components.debugKeystore; | 
|  | keystorePassword = _kDebugKeystorePassword; | 
|  | keyAlias = _kDebugKeystoreKeyAlias; | 
|  | keyPassword = _kDebugKeystorePassword; | 
|  | } else { | 
|  | keystore = new File(argResults['keystore']); | 
|  | keystorePassword = argResults['keystore-password']; | 
|  | keyAlias = argResults['keystore-key-alias']; | 
|  | if (keystorePassword.isEmpty || keyAlias.isEmpty) { | 
|  | printError('Must provide a keystore password and a key alias'); | 
|  | return 1; | 
|  | } | 
|  | keyPassword = argResults['keystore-key-password']; | 
|  | if (keyPassword.isEmpty) | 
|  | keyPassword = keystorePassword; | 
|  | } | 
|  |  | 
|  | builder.sign(keystore, keystorePassword, keyAlias, keyPassword, apk); | 
|  |  | 
|  | return 0; | 
|  | } | 
|  |  | 
|  | @override | 
|  | Future<int> runInProject() async { | 
|  | BuildConfiguration config = buildConfigurations.firstWhere( | 
|  | (BuildConfiguration bc) => bc.targetPlatform == TargetPlatform.android | 
|  | ); | 
|  |  | 
|  | _ApkComponents components = await _findApkComponents(config); | 
|  | if (components == null) { | 
|  | printError('Unable to build APK.'); | 
|  | return 1; | 
|  | } | 
|  |  | 
|  | String flxPath = argResults['flx']; | 
|  |  | 
|  | if (!flxPath.isEmpty) { | 
|  | if (!FileSystemEntity.isFileSync(flxPath)) { | 
|  | printError('FLX does not exist: $flxPath'); | 
|  | printError('(Omit the --flx option to build the FLX automatically)'); | 
|  | return 1; | 
|  | } | 
|  | return _buildApk(components, flxPath); | 
|  | } else { | 
|  | await downloadToolchain(); | 
|  |  | 
|  | // Find the path to the main Dart file. | 
|  | String mainPath = findMainDartFile(argResults['target']); | 
|  |  | 
|  | // Build the FLX. | 
|  | flx.DirectoryResult buildResult = await flx.buildInTempDir(toolchain, mainPath: mainPath); | 
|  |  | 
|  | try { | 
|  | return _buildApk(components, buildResult.localBundlePath); | 
|  | } finally { | 
|  | buildResult.dispose(); | 
|  | } | 
|  | } | 
|  | } | 
|  | } |