Commit dcbb4960 authored by Matt Perry's avatar Matt Perry

'flutter apk' now supports dynamically registered services.

Third-party libraries can now provide their own mojo services. They do
so by adding a config.yaml file to their pub package which contains
- a list of service names and java classes which handles that service's
  registration.
- a list of pre-built .jar files to statically link with the app's shell
  when building the app.
parent f88c945e
...@@ -13,12 +13,12 @@ ...@@ -13,12 +13,12 @@
<uses-permission android:name="com.google.android.c2dm.permission.RECEIVE" /> <uses-permission android:name="com.google.android.c2dm.permission.RECEIVE" />
<!-- Supposedly this permission prevents other apps from receiving our <!-- Supposedly this permission prevents other apps from receiving our
messages, but it doesn't seem to have any effect. --> 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" /> 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 --> <!-- 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"> <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> <intent-filter>
<action android:name="android.intent.action.MAIN" /> <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: ...@@ -13,3 +13,5 @@ material-design-icons:
- name: navigation/close - name: navigation/close
- name: navigation/menu - name: navigation/menu
- name: navigation/more_vert - name: navigation/more_vert
services:
- gcm
...@@ -175,5 +175,6 @@ initGcm() async { ...@@ -175,5 +175,6 @@ initGcm() async {
} }
main() { main() {
initGcm();
runApp(new FitnessApp()); runApp(new FitnessApp());
} }
...@@ -8,8 +8,8 @@ dependencies: ...@@ -8,8 +8,8 @@ dependencies:
collection: '>=1.1.3 <2.0.0' collection: '>=1.1.3 <2.0.0'
intl: '>=0.12.4+2 <0.13.0' intl: '>=0.12.4+2 <0.13.0'
material_design_icons: '>=0.0.3 <0.1.0' material_design_icons: '>=0.0.3 <0.1.0'
sky_engine: 0.0.85 sky_engine: 0.0.86
sky_services: 0.0.85 sky_services: 0.0.86
vector_math: '>=1.4.5 <2.0.0' vector_math: '>=1.4.5 <2.0.0'
quiver: '>=0.21.4 <0.22.0' quiver: '>=0.21.4 <0.22.0'
......
...@@ -47,7 +47,7 @@ enum ArtifactType { ...@@ -47,7 +47,7 @@ enum ArtifactType {
snapshot, snapshot,
shell, shell,
mojo, mojo,
androidClassesDex, androidClassesJar,
androidIcuData, androidIcuData,
androidKeystore, androidKeystore,
androidLibSkyShell, androidLibSkyShell,
...@@ -124,8 +124,8 @@ class ArtifactStore { ...@@ -124,8 +124,8 @@ class ArtifactStore {
), ),
const Artifact._( const Artifact._(
name: 'Compiled Java code', name: 'Compiled Java code',
fileName: 'classes.dex', fileName: 'classes.dex.jar',
type: ArtifactType.androidClassesDex, type: ArtifactType.androidClassesJar,
targetPlatform: TargetPlatform.android targetPlatform: TargetPlatform.android
), ),
const Artifact._( const Artifact._(
...@@ -219,14 +219,12 @@ class ArtifactStore { ...@@ -219,14 +219,12 @@ class ArtifactStore {
); );
} }
/// Download the artifacts.zip archive for the given platform from GCS /// Download a file from the given URL and return the bytes.
/// and extract it to the local cache. static Future<List<int>> _downloadFile(Uri url) async {
static Future _doDownloadArtifactsFromZip(String platform) async {
String url = getCloudStorageBaseUrl(platform) + 'artifacts.zip';
logging.info('Downloading $url.'); logging.info('Downloading $url.');
HttpClient httpClient = new HttpClient(); HttpClient httpClient = new HttpClient();
HttpClientRequest request = await httpClient.getUrl(Uri.parse(url)); HttpClientRequest request = await httpClient.getUrl(url);
HttpClientResponse response = await request.close(); HttpClientResponse response = await request.close();
logging.fine('Received response statusCode=${response.statusCode}'); logging.fine('Received response statusCode=${response.statusCode}');
if (response.statusCode != 200) if (response.statusCode != 200)
...@@ -237,13 +235,29 @@ class ArtifactStore { ...@@ -237,13 +235,29 @@ class ArtifactStore {
responseBody.add(chunk); 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); Directory cacheDir = _getCacheDirForPlatform(platform);
for (ArchiveFile archiveFile in archive) { for (ArchiveFile archiveFile in archive) {
File cacheFile = new File(path.join(cacheDir.path, archiveFile.name)); File cacheFile = new File(path.join(cacheDir.path, archiveFile.name));
IOSink sink = cacheFile.openWrite(); cacheFile.writeAsBytesSync(archiveFile.content, flush: true);
sink.add(archiveFile.content);
await sink.close();
} }
for (Artifact artifact in knownArtifacts) { for (Artifact artifact in knownArtifacts) {
...@@ -306,6 +320,23 @@ class ArtifactStore { ...@@ -306,6 +320,23 @@ class ArtifactStore {
return cachedFile.path; 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() { static void clear() {
Directory cacheDir = _getBaseCacheDir(); Directory cacheDir = _getBaseCacheDir();
logging.fine('Clearing cache directory ${cacheDir.path}'); logging.fine('Clearing cache directory ${cacheDir.path}');
......
...@@ -3,9 +3,11 @@ ...@@ -3,9 +3,11 @@
// found in the LICENSE file. // found in the LICENSE file.
import 'dart:async'; import 'dart:async';
import 'dart:convert';
import 'dart:io'; import 'dart:io';
import 'package:path/path.dart' as path; import 'package:path/path.dart' as path;
import 'package:yaml/yaml.dart';
import '../artifacts.dart'; import '../artifacts.dart';
import '../base/file_system.dart'; import '../base/file_system.dart';
...@@ -21,6 +23,9 @@ const String _kDefaultAndroidManifestPath = 'apk/AndroidManifest.xml'; ...@@ -21,6 +23,9 @@ const String _kDefaultAndroidManifestPath = 'apk/AndroidManifest.xml';
const String _kDefaultOutputPath = 'build/app.apk'; const String _kDefaultOutputPath = 'build/app.apk';
const String _kDefaultResourcesPath = 'apk/res'; 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 // Alias of the key provided in the Chromium debug keystore
const String _kDebugKeystoreKeyAlias = "chromiumdebugkey"; const String _kDebugKeystoreKeyAlias = "chromiumdebugkey";
...@@ -56,6 +61,7 @@ class _ApkBuilder { ...@@ -56,6 +61,7 @@ class _ApkBuilder {
File _androidJar; File _androidJar;
File _aapt; File _aapt;
File _dx;
File _zipalign; File _zipalign;
String _jarsigner; String _jarsigner;
...@@ -64,12 +70,25 @@ class _ApkBuilder { ...@@ -64,12 +70,25 @@ class _ApkBuilder {
String buildTools = '$androidSdk/build-tools/$_kBuildToolsVersion'; String buildTools = '$androidSdk/build-tools/$_kBuildToolsVersion';
_aapt = new File('$buildTools/aapt'); _aapt = new File('$buildTools/aapt');
_dx = new File('$buildTools/dx');
_zipalign = new File('$buildTools/zipalign'); _zipalign = new File('$buildTools/zipalign');
_jarsigner = 'jarsigner'; _jarsigner = 'jarsigner';
} }
bool checkSdkPath() { 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) { void package(File outputApk, File androidManifest, Directory assets, Directory artifacts, Directory resources) {
...@@ -106,12 +125,21 @@ class _ApkComponents { ...@@ -106,12 +125,21 @@ class _ApkComponents {
Directory androidSdk; Directory androidSdk;
File manifest; File manifest;
File icuData; File icuData;
File classesDex; List<File> jars;
List<Map<String, String>> services = [];
File libSkyShell; File libSkyShell;
File debugKeystore; File debugKeystore;
Directory resources; 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 { class ApkCommand extends FlutterCommand {
final String name = 'apk'; final String name = 'apk';
final String description = 'Build an Android APK package.'; final String description = 'Build an Android APK package.';
...@@ -151,6 +179,36 @@ class ApkCommand extends FlutterCommand { ...@@ -151,6 +179,36 @@ class ApkCommand extends FlutterCommand {
help: 'Password for the entry within the keystore.'); 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 { Future<_ApkComponents> _findApkComponents(BuildConfiguration config) async {
String androidSdkPath; String androidSdkPath;
List<String> artifactPaths; List<String> artifactPaths;
...@@ -158,7 +216,7 @@ class ApkCommand extends FlutterCommand { ...@@ -158,7 +216,7 @@ class ApkCommand extends FlutterCommand {
androidSdkPath = '${runner.enginePath}/third_party/android_tools/sdk'; androidSdkPath = '${runner.enginePath}/third_party/android_tools/sdk';
artifactPaths = [ artifactPaths = [
'${runner.enginePath}/third_party/icu/android/icudtl.dat', '${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', '${config.buildDir}/gen/sky/shell/shell/shell/libs/armeabi-v7a/libsky_shell.so',
'${runner.enginePath}/build/android/ant/chromium-debug.keystore', '${runner.enginePath}/build/android/ant/chromium-debug.keystore',
]; ];
...@@ -169,7 +227,7 @@ class ApkCommand extends FlutterCommand { ...@@ -169,7 +227,7 @@ class ApkCommand extends FlutterCommand {
} }
List<ArtifactType> artifactTypes = <ArtifactType>[ List<ArtifactType> artifactTypes = <ArtifactType>[
ArtifactType.androidIcuData, ArtifactType.androidIcuData,
ArtifactType.androidClassesDex, ArtifactType.androidClassesJar,
ArtifactType.androidLibSkyShell, ArtifactType.androidLibSkyShell,
ArtifactType.androidKeystore, ArtifactType.androidKeystore,
]; ];
...@@ -183,11 +241,13 @@ class ApkCommand extends FlutterCommand { ...@@ -183,11 +241,13 @@ class ApkCommand extends FlutterCommand {
components.androidSdk = new Directory(androidSdkPath); components.androidSdk = new Directory(androidSdkPath);
components.manifest = new File(argResults['manifest']); components.manifest = new File(argResults['manifest']);
components.icuData = new File(artifactPaths[0]); 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.libSkyShell = new File(artifactPaths[2]);
components.debugKeystore = new File(artifactPaths[3]); components.debugKeystore = new File(artifactPaths[3]);
components.resources = new Directory(argResults['resources']); components.resources = new Directory(argResults['resources']);
await _findServices(components);
if (!components.resources.existsSync()) { if (!components.resources.existsSync()) {
// TODO(eseidel): This level should be higher when path is manually set. // TODO(eseidel): This level should be higher when path is manually set.
logging.info('Can not locate Resources: ${components.resources}, ignoring.'); logging.info('Can not locate Resources: ${components.resources}, ignoring.');
...@@ -204,8 +264,9 @@ class ApkCommand extends FlutterCommand { ...@@ -204,8 +264,9 @@ class ApkCommand extends FlutterCommand {
logging.severe('and version $_kBuildToolsVersion of the build tools.'); logging.severe('and version $_kBuildToolsVersion of the build tools.');
return null; return null;
} }
for (File f in [components.manifest, components.icuData, components.classesDex, for (File f in [components.manifest, components.icuData,
components.libSkyShell, components.debugKeystore]) { components.libSkyShell, components.debugKeystore]
..addAll(components.jars)) {
if (!f.existsSync()) { if (!f.existsSync()) {
logging.severe('Can not locate file: ${f.path}'); logging.severe('Can not locate file: ${f.path}');
return null; return null;
...@@ -215,18 +276,44 @@ class ApkCommand extends FlutterCommand { ...@@ -215,18 +276,44 @@ class ApkCommand extends FlutterCommand {
return components; 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) { int _buildApk(_ApkComponents components, String flxPath) {
Directory tempDir = Directory.systemTemp.createTempSync('flutter_tools'); Directory tempDir = Directory.systemTemp.createTempSync('flutter_tools');
try { 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 assetBuilder = new _AssetBuilder(tempDir, 'assets');
assetBuilder.add(components.icuData, 'icudtl.dat'); assetBuilder.add(components.icuData, 'icudtl.dat');
assetBuilder.add(new File(flxPath), 'app.flx'); assetBuilder.add(new File(flxPath), 'app.flx');
assetBuilder.add(servicesConfig, 'services.json');
_AssetBuilder artifactBuilder = new _AssetBuilder(tempDir, 'artifacts'); _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'); 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'); File unalignedApk = new File('${tempDir.path}/app.apk.unaligned');
builder.package(unalignedApk, components.manifest, assetBuilder.directory, builder.package(unalignedApk, components.manifest, assetBuilder.directory,
artifactBuilder.directory, components.resources); 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