Commit 0f505fbf authored by Matt Perry's avatar Matt Perry

Merge pull request #1263 from mpcomplete/apk.tool

'flutter apk' now supports dynamically registered services.
parents f88c945e dcbb4960
......@@ -13,12 +13,12 @@
<uses-permission android:name="com.google.android.c2dm.permission.RECEIVE" />
<!-- Supposedly this permission prevents other apps from receiving our
messages, but it doesn't seem to have any effect. -->
<permission android:name="org.domokit.sky.shell.permission.C2D_MESSAGE"
<permission android:name="org.domokit.fitness.permission.C2D_MESSAGE"
android:protectionLevel="signature" />
<uses-permission android:name="org.domokit.sky.shell.permission.C2D_MESSAGE" />
<uses-permission android:name="org.domokit.fitness.permission.C2D_MESSAGE" />
<!-- end for GCM -->
<application android:icon="@mipmap/ic_launcher" android:label="Fitness" android:name="org.domokit.fitness.FitnessApplication">
<application android:icon="@mipmap/ic_launcher" android:label="Fitness" android:name="org.domokit.sky.shell.SkyApplication">
<activity android:configChanges="orientation|keyboardHidden|keyboard|screenSize" android:hardwareAccelerated="true" android:launchMode="singleTask" android:name="org.domokit.sky.shell.SkyActivity" android:theme="@android:style/Theme.Black.NoTitleBar">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
......
// 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.
package org.domokit.fitness;
import android.content.Context;
import org.chromium.mojo.system.Core;
import org.chromium.mojo.system.MessagePipeHandle;
import org.chromium.mojom.gcm.GcmService;
import org.domokit.gcm.RegistrationIntentService;
import org.domokit.sky.shell.ServiceFactory;
import org.domokit.sky.shell.ServiceRegistry;
import org.domokit.sky.shell.SkyApplication;
/**
* Sky implementation of {@link android.app.Application}, managing application-level global
* state and initializations.
*/
public class FitnessApplication extends SkyApplication {
/**
* Override this function to register more services.
*/
protected void onServiceRegistryAvailable(ServiceRegistry registry) {
super.onServiceRegistryAvailable(registry);
registry.register(GcmService.MANAGER.getName(), new ServiceFactory() {
@Override
public void connectToService(Context context, Core core, MessagePipeHandle pipe) {
GcmService.MANAGER.bind(
new RegistrationIntentService.MojoService(context), pipe);
}
});
}
}
......@@ -13,3 +13,5 @@ material-design-icons:
- name: navigation/close
- name: navigation/menu
- name: navigation/more_vert
services:
- gcm
......@@ -175,5 +175,6 @@ initGcm() async {
}
main() {
initGcm();
runApp(new FitnessApp());
}
......@@ -8,8 +8,8 @@ dependencies:
collection: '>=1.1.3 <2.0.0'
intl: '>=0.12.4+2 <0.13.0'
material_design_icons: '>=0.0.3 <0.1.0'
sky_engine: 0.0.85
sky_services: 0.0.85
sky_engine: 0.0.86
sky_services: 0.0.86
vector_math: '>=1.4.5 <2.0.0'
quiver: '>=0.21.4 <0.22.0'
......
......@@ -47,7 +47,7 @@ enum ArtifactType {
snapshot,
shell,
mojo,
androidClassesDex,
androidClassesJar,
androidIcuData,
androidKeystore,
androidLibSkyShell,
......@@ -124,8 +124,8 @@ class ArtifactStore {
),
const Artifact._(
name: 'Compiled Java code',
fileName: 'classes.dex',
type: ArtifactType.androidClassesDex,
fileName: 'classes.dex.jar',
type: ArtifactType.androidClassesJar,
targetPlatform: TargetPlatform.android
),
const Artifact._(
......@@ -219,14 +219,12 @@ class ArtifactStore {
);
}
/// Download the artifacts.zip archive for the given platform from GCS
/// and extract it to the local cache.
static Future _doDownloadArtifactsFromZip(String platform) async {
String url = getCloudStorageBaseUrl(platform) + 'artifacts.zip';
/// Download a file from the given URL and return the bytes.
static Future<List<int>> _downloadFile(Uri url) async {
logging.info('Downloading $url.');
HttpClient httpClient = new HttpClient();
HttpClientRequest request = await httpClient.getUrl(Uri.parse(url));
HttpClientRequest request = await httpClient.getUrl(url);
HttpClientResponse response = await request.close();
logging.fine('Received response statusCode=${response.statusCode}');
if (response.statusCode != 200)
......@@ -237,13 +235,29 @@ class ArtifactStore {
responseBody.add(chunk);
}
Archive archive = new ZipDecoder().decodeBytes(responseBody.takeBytes());
return responseBody.takeBytes();
}
/// Download a file from the given url and write it to the cache.
static Future _downloadFileToCache(Uri url, File cachedFile) async {
if (!cachedFile.parent.existsSync())
cachedFile.parent.createSync(recursive: true);
List<int> fileBytes = await _downloadFile(url);
cachedFile.writeAsBytesSync(fileBytes, flush: true);
}
/// Download the artifacts.zip archive for the given platform from GCS
/// and extract it to the local cache.
static Future _doDownloadArtifactsFromZip(String platform) async {
String url = getCloudStorageBaseUrl(platform) + 'artifacts.zip';
List<int> zipBytes = await _downloadFile(Uri.parse(url));
Archive archive = new ZipDecoder().decodeBytes(zipBytes);
Directory cacheDir = _getCacheDirForPlatform(platform);
for (ArchiveFile archiveFile in archive) {
File cacheFile = new File(path.join(cacheDir.path, archiveFile.name));
IOSink sink = cacheFile.openWrite();
sink.add(archiveFile.content);
await sink.close();
cacheFile.writeAsBytesSync(archiveFile.content, flush: true);
}
for (Artifact artifact in knownArtifacts) {
......@@ -306,6 +320,23 @@ class ArtifactStore {
return cachedFile.path;
}
static Future<String> getThirdPartyFile(String urlStr, String cacheSubdir) async {
Uri url = Uri.parse(urlStr);
Directory baseDir = _getBaseCacheDir();
Directory cacheDir = new Directory(path.join(
baseDir.path, 'third_party', cacheSubdir));
File cachedFile = new File(
path.join(cacheDir.path, url.pathSegments[url.pathSegments.length-1]));
if (!cachedFile.existsSync()) {
await _downloadFileToCache(url, cachedFile);
if (!cachedFile.existsSync()) {
logging.severe('Unable to fetch third-party artifact: $url');
throw new ProcessExit(2);
}
}
return cachedFile.path;
}
static void clear() {
Directory cacheDir = _getBaseCacheDir();
logging.fine('Clearing cache directory ${cacheDir.path}');
......
......@@ -3,9 +3,11 @@
// 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 '../artifacts.dart';
import '../base/file_system.dart';
......@@ -21,6 +23,9 @@ 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";
......@@ -56,6 +61,7 @@ class _ApkBuilder {
File _androidJar;
File _aapt;
File _dx;
File _zipalign;
String _jarsigner;
......@@ -64,12 +70,25 @@ class _ApkBuilder {
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() && _zipalign.existsSync());
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) {
......@@ -106,12 +125,21 @@ class _ApkComponents {
Directory androidSdk;
File manifest;
File icuData;
File classesDex;
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.';
......@@ -151,6 +179,36 @@ class ApkCommand extends FlutterCommand {
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']) {
// Jar might refer to an android SDK jar, or URL to download.
if (jar.startsWith("android-sdk:")) {
jar = jar.replaceAll('android-sdk:', '${components.androidSdk.path}/');
components.jars.add(new File(jar));
} else if (jar.startsWith("http")) {
String cachePath = await ArtifactStore.getThirdPartyFile(jar, service);
components.jars.add(new File(cachePath));
} else {
logging.severe('Service depends on a jar in an unrecognized format: $jar');
throw new ProcessExit(2);
}
}
}
}
Future<_ApkComponents> _findApkComponents(BuildConfiguration config) async {
String androidSdkPath;
List<String> artifactPaths;
......@@ -158,7 +216,7 @@ class ApkCommand extends FlutterCommand {
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',
'${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',
];
......@@ -169,7 +227,7 @@ class ApkCommand extends FlutterCommand {
}
List<ArtifactType> artifactTypes = <ArtifactType>[
ArtifactType.androidIcuData,
ArtifactType.androidClassesDex,
ArtifactType.androidClassesJar,
ArtifactType.androidLibSkyShell,
ArtifactType.androidKeystore,
];
......@@ -183,11 +241,13 @@ class ApkCommand extends FlutterCommand {
components.androidSdk = new Directory(androidSdkPath);
components.manifest = new File(argResults['manifest']);
components.icuData = new File(artifactPaths[0]);
components.classesDex = new File(artifactPaths[1]);
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.
logging.info('Can not locate Resources: ${components.resources}, ignoring.');
......@@ -204,8 +264,9 @@ class ApkCommand extends FlutterCommand {
logging.severe('and version $_kBuildToolsVersion of the build tools.');
return null;
}
for (File f in [components.manifest, components.icuData, components.classesDex,
components.libSkyShell, components.debugKeystore]) {
for (File f in [components.manifest, components.icuData,
components.libSkyShell, components.debugKeystore]
..addAll(components.jars)) {
if (!f.existsSync()) {
logging.severe('Can not locate file: ${f.path}');
return null;
......@@ -215,18 +276,44 @@ class ApkCommand extends FlutterCommand {
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(components.classesDex, 'classes.dex');
artifactBuilder.add(classesDex, 'classes.dex');
artifactBuilder.add(components.libSkyShell, 'lib/armeabi-v7a/libsky_shell.so');
_ApkBuilder builder = new _ApkBuilder(components.androidSdk.path);
File unalignedApk = new File('${tempDir.path}/app.apk.unaligned');
builder.package(unalignedApk, components.manifest, assetBuilder.directory,
artifactBuilder.directory, components.resources);
......
services:
- name: gcm::GcmService
registration-class: org.domokit.gcm.RegistrationIntentService$MojoService
jars:
- android-sdk:extras/google/google_play_services/libproject/google-play-services_lib/libs/google-play-services.jar
- android-sdk:extras/android/support/v13/android-support-v13.jar
- android-sdk:extras/android/support/v7/appcompat/libs/android-support-v7-appcompat.jar
- android-sdk:extras/android/support/v7/mediarouter/libs/android-support-v7-mediarouter.jar
- https://storage.googleapis.com/mojo_infra/flutter/c03a5dda7e78c50d74fe5e5ed72eed1de6ae993d/android-arm/gcm/gcm_lib.dex.jar
- https://storage.googleapis.com/mojo_infra/flutter/c03a5dda7e78c50d74fe5e5ed72eed1de6ae993d/android-arm/gcm/interfaces_java.dex.jar
todo: >
Maybe we should link to a URL to download the jars. Might need different
versions per platform, etc.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment