diff --git a/sky/engine/public/sky/sky_headless.cc b/sky/engine/public/sky/sky_headless.cc index 8401aa78c..37fce23c7 100644 --- a/sky/engine/public/sky/sky_headless.cc +++ b/sky/engine/public/sky/sky_headless.cc @@ -9,7 +9,7 @@ namespace blink { -SkyHeadless::SkyHeadless() { +SkyHeadless::SkyHeadless(Client* client) : client_(client) { } SkyHeadless::~SkyHeadless() { @@ -20,6 +20,11 @@ void SkyHeadless::Init(const String& name) { dart_controller_ = adoptPtr(new DartController); dart_controller_->CreateIsolateFor(adoptPtr(new DOMDartState(name))); + + Dart_Isolate isolate = dart_controller_->dart_state()->isolate(); + DartIsolateScope scope(isolate); + DartApiScope api_scope; + client_->DidCreateIsolate(isolate); } void SkyHeadless::RunFromSnapshotBuffer(const uint8_t* buffer, size_t size) { diff --git a/sky/engine/public/sky/sky_headless.h b/sky/engine/public/sky/sky_headless.h index 7f016e65c..3a95a641a 100644 --- a/sky/engine/public/sky/sky_headless.h +++ b/sky/engine/public/sky/sky_headless.h @@ -9,13 +9,23 @@ #include "sky/engine/wtf/OwnPtr.h" #include "sky/engine/wtf/text/WTFString.h" +typedef struct _Dart_Isolate* Dart_Isolate; + namespace blink { class DartController; // This class provides a way to run Dart script without a View. class SkyHeadless { public: - SkyHeadless(); + class Client { + public: + virtual void DidCreateIsolate(Dart_Isolate isolate) = 0; + + protected: + virtual ~Client() {} + }; + + SkyHeadless(Client* client); ~SkyHeadless(); void Init(const String& name); @@ -23,6 +33,7 @@ class SkyHeadless { private: OwnPtr dart_controller_; + Client* client_; DISALLOW_COPY_AND_ASSIGN(SkyHeadless); }; diff --git a/sky/packages/sky/lib/src/services/activity.dart b/sky/packages/sky/lib/src/services/activity.dart index df1f2cabb..f8b73ee7a 100644 --- a/sky/packages/sky/lib/src/services/activity.dart +++ b/sky/packages/sky/lib/src/services/activity.dart @@ -37,6 +37,15 @@ UserFeedbackProxy _initUserFeedbackProxy() { final UserFeedbackProxy _userFeedbackProxy = _initUserFeedbackProxy(); final UserFeedback userFeedback = _userFeedbackProxy.ptr; +PathServiceProxy _initPathServiceProxy() { + PathServiceProxy proxy = new PathServiceProxy.unbound(); + shell.requestService(null, proxy); + return proxy; +} + +final PathServiceProxy _pathServiceProxy = _initPathServiceProxy(); +final PathService pathService = _pathServiceProxy.ptr; + Color _cachedPrimaryColor; String _cachedLabel; @@ -55,5 +64,6 @@ void updateTaskDescription(String label, Color color) { _activityProxy.ptr.setTaskDescription(description); } -Future getFilesDir() async => (await _activityProxy.ptr.getFilesDir()).path; -Future getCacheDir() async => (await _activityProxy.ptr.getCacheDir()).path; +Future getAppDataDir() async => (await _pathServiceProxy.ptr.getAppDataDir()).path; +Future getFilesDir() async => (await _pathServiceProxy.ptr.getFilesDir()).path; +Future getCacheDir() async => (await _pathServiceProxy.ptr.getCacheDir()).path; diff --git a/sky/packages/updater/BUILD.gn b/sky/packages/updater/BUILD.gn index a900d0913..65185a67b 100644 --- a/sky/packages/updater/BUILD.gn +++ b/sky/packages/updater/BUILD.gn @@ -25,6 +25,8 @@ action("updater") { ] deps = [ + "//sky/services/activity:interfaces", + "//sky/services/updater:interfaces", "//sky/tools/sky_snapshot($host_toolchain)", ] } diff --git a/sky/packages/updater/lib/main.dart b/sky/packages/updater/lib/main.dart index 4c71346b8..ea168da29 100644 --- a/sky/packages/updater/lib/main.dart +++ b/sky/packages/updater/lib/main.dart @@ -2,13 +2,104 @@ // 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:io'; + +import 'package:mojo/core.dart'; +import 'package:sky/services.dart'; +import 'package:sky_services/updater/update_service.mojom.dart'; +import 'package:path/path.dart' as path; +import 'package:yaml/yaml.dart' as yaml; + +import 'version.dart'; +import 'pipe_to_file.dart'; + +const String kManifestFile = 'sky.yaml'; +const String kBundleFile = 'app.skyx'; + +UpdateServiceProxy _initUpdateService() { + UpdateServiceProxy updateService = new UpdateServiceProxy.unbound(); + shell.requestService(null, updateService); + return updateService; +} + +final UpdateServiceProxy _updateService = _initUpdateService(); + +String cachedDataDir = null; +Future getDataDir() async { + if (cachedDataDir == null) + cachedDataDir = await getAppDataDir(); + return cachedDataDir; +} + class UpdateTask { UpdateTask() {} - String toString() => "UpdateTask()"; + run() async { + try { + await _runImpl(); + } catch(e) { + print('Update failed: $e'); + } finally { + _updateService.ptr.notifyUpdateCheckComplete(); + } + } + + _runImpl() async { + _dataDir = await getDataDir(); + + await _readLocalManifest(); + yaml.YamlMap remoteManifest = await _fetchManifest(); + if (!_shouldUpdate(remoteManifest)) { + print('Update skipped. No new version.'); + return; + } + MojoResult result = await _fetchBundle(); + if (!result.isOk) { + print('Update failed while fetching new skyx bundle.'); + return; + } + await _replaceBundle(); + print('Update success.'); + } + + yaml.YamlMap _currentManifest; + String _dataDir; + String _tempPath; + + _readLocalManifest() async { + String manifestPath = path.join(_dataDir, kManifestFile); + String manifestData = await new File(manifestPath).readAsString(); + _currentManifest = yaml.loadYaml(manifestData, sourceUrl: manifestPath); + } + + Future _fetchManifest() async { + String manifestUrl = _currentManifest['update_url'] + '/' + kManifestFile; + String manifestData = await fetchString(manifestUrl); + return yaml.loadYaml(manifestData, sourceUrl: manifestUrl); + } + + bool _shouldUpdate(yaml.YamlMap remoteManifest) { + Version currentVersion = new Version(_currentManifest['version']); + Version remoteVersion = new Version(remoteManifest['version']); + return (currentVersion < remoteVersion); + } + + Future _fetchBundle() async { + // TODO(mpcomplete): Use the cache dir. We need an equivalent of mkstemp(). + _tempPath = path.join(_dataDir, 'tmp.skyx'); + String bundleUrl = _currentManifest['update_url'] + '/' + kBundleFile; + UrlResponse response = await fetchUrl(bundleUrl); + return PipeToFile.copyToFile(response.body, _tempPath); + } + + _replaceBundle() async { + String bundlePath = path.join(_dataDir, kBundleFile); + await new File(_tempPath).rename(bundlePath); + } } void main() { - var x = new UpdateTask(); - print("Success: $x"); + var task = new UpdateTask(); + task.run(); } diff --git a/sky/packages/updater/lib/pipe_to_file.dart b/sky/packages/updater/lib/pipe_to_file.dart new file mode 100644 index 000000000..f6a62d0a0 --- /dev/null +++ b/sky/packages/updater/lib/pipe_to_file.dart @@ -0,0 +1,64 @@ +// 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:io'; +import 'dart:typed_data'; + +import 'package:mojo/core.dart'; + +// Helper class to drain the contents of a mojo data pipe to a file. +class PipeToFile { + MojoDataPipeConsumer _consumer; + MojoEventStream _eventStream; + IOSink _outputStream; + + PipeToFile(this._consumer, String outputPath) { + _eventStream = new MojoEventStream(_consumer.handle); + _outputStream = new File(outputPath).openWrite(); + } + + Future _doRead() async { + ByteData thisRead = _consumer.beginRead(); + if (thisRead == null) { + throw 'Data pipe beginRead failed: ${_consumer.status}'; + } + // TODO(mpcomplete): Should I worry about the _eventStream listen callback + // being invoked again before this completes? + await _outputStream.add(thisRead.buffer.asUint8List()); + return _consumer.endRead(thisRead.lengthInBytes); + } + + Future drain() async { + var completer = new Completer(); + // TODO(mpcomplete): Is it legit to pass an async callback to listen? + _eventStream.listen((List event) async { + var mojoSignals = new MojoHandleSignals(event[1]); + if (mojoSignals.isReadable) { + var result = await _doRead(); + if (!result.isOk) { + _eventStream.close(); + _eventStream = null; + _outputStream.close(); + completer.complete(result); + } else { + _eventStream.enableReadEvents(); + } + } else if (mojoSignals.isPeerClosed) { + _eventStream.close(); + _eventStream = null; + _outputStream.close(); + completer.complete(MojoResult.OK); + } else { + throw 'Unexpected handle event: $mojoSignals'; + } + }); + return completer.future; + } + + static Future copyToFile(MojoDataPipeConsumer consumer, String outputPath) { + var drainer = new PipeToFile(consumer, outputPath); + return drainer.drain(); + } +} diff --git a/sky/packages/updater/lib/version.dart b/sky/packages/updater/lib/version.dart new file mode 100644 index 000000000..975927736 --- /dev/null +++ b/sky/packages/updater/lib/version.dart @@ -0,0 +1,32 @@ +// 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:math'; + +// This class represents a dot-separated version string. Used for comparing +// versions. +// Usage: assert(new Version('1.1.0') < new Version('1.2.1')); +class Version { + Version(String versionStr) : + _parts = versionStr.split('.').map((val) => int.parse(val)).toList(); + + List _parts; + + bool operator<(Version other) => _compare(other) < 0; + bool operator==(dynamic other) => other is Version && _compare(other) == 0; + bool operator>(Version other) => _compare(other) > 0; + + int _compare(Version other) { + int length = min(_parts.length, other._parts.length); + for (int i = 0; i < length; ++i) { + if (_parts[i] < other._parts[i]) + return -1; + if (_parts[i] > other._parts[i]) + return 1; + } + return _parts.length - other._parts.length; // results in 1.0 < 1.0.0 + } + + int get hashCode => _parts.fold(373, (acc, part) => 37*acc + part); +} diff --git a/sky/packages/updater/pubspec.yaml b/sky/packages/updater/pubspec.yaml index 1e41597ad..28de3eed1 100644 --- a/sky/packages/updater/pubspec.yaml +++ b/sky/packages/updater/pubspec.yaml @@ -1,9 +1,16 @@ -name: sky_updater +name: updater version: 0.0.1 author: Flutter Authors description: The autoupdater for flutter homepage: http://flutter.io dependencies: - mojo: ^0.0.21 + mojo: '>=0.1.0 <0.2.0' + sky: any + sky_services: any + path: any + yaml: any +dependency_overrides: + sky: + path: ../sky environment: sdk: '>=1.12.0 <2.0.0' diff --git a/sky/services/activity/BUILD.gn b/sky/services/activity/BUILD.gn index 531ed058e..7bf874a69 100644 --- a/sky/services/activity/BUILD.gn +++ b/sky/services/activity/BUILD.gn @@ -29,6 +29,7 @@ if (is_android) { android_library("activity_lib") { java_files = [ "src/org/domokit/activity/ActivityImpl.java", + "src/org/domokit/activity/PathServiceImpl.java", "src/org/domokit/activity/UserFeedbackImpl.java", ] diff --git a/sky/services/activity/activity.mojom b/sky/services/activity/activity.mojom index 4178c2060..fc2588deb 100644 --- a/sky/services/activity/activity.mojom +++ b/sky/services/activity/activity.mojom @@ -73,6 +73,10 @@ interface PathService { GetCacheDir() => (string path); }; +interface UpdateService { + NotifyUpdateCheckComplete(); +}; + enum HapticFeedbackType { LONG_PRESS, VIRTUAL_KEY, diff --git a/sky/services/activity/src/org/domokit/activity/PathServiceImpl.java b/sky/services/activity/src/org/domokit/activity/PathServiceImpl.java new file mode 100644 index 000000000..09b101690 --- /dev/null +++ b/sky/services/activity/src/org/domokit/activity/PathServiceImpl.java @@ -0,0 +1,43 @@ +// 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.activity; + +import android.content.Context; +import org.chromium.base.PathUtils; +import org.chromium.mojo.system.MojoException; +import org.chromium.mojom.activity.PathService; + +/** + * Android implementation of PathService. + */ +public class PathServiceImpl implements PathService { + private static final String TAG = "PathServiceImpl"; + private static android.content.Context context; + + public PathServiceImpl(android.content.Context context) { + this.context = context; + } + + @Override + public void close() {} + + @Override + public void onConnectionError(MojoException e) {} + + @Override + public void getAppDataDir(GetAppDataDirResponse callback) { + callback.call(PathUtils.getDataDirectory(context)); + } + + @Override + public void getFilesDir(GetFilesDirResponse callback) { + callback.call(context.getFilesDir().getPath()); + } + + @Override + public void getCacheDir(GetCacheDirResponse callback) { + callback.call(context.getCacheDir().getPath()); + } +} diff --git a/sky/shell/android/org/domokit/sky/shell/SkyApplication.java b/sky/shell/android/org/domokit/sky/shell/SkyApplication.java index 27f0abf40..3268d82c4 100644 --- a/sky/shell/android/org/domokit/sky/shell/SkyApplication.java +++ b/sky/shell/android/org/domokit/sky/shell/SkyApplication.java @@ -17,12 +17,14 @@ import org.chromium.mojo.sensors.SensorServiceImpl; import org.chromium.mojo.system.Core; import org.chromium.mojo.system.MessagePipeHandle; import org.chromium.mojom.activity.Activity; +import org.chromium.mojom.activity.PathService; import org.chromium.mojom.keyboard.KeyboardService; import org.chromium.mojom.media.MediaService; import org.chromium.mojom.mojo.NetworkService; import org.chromium.mojom.sensors.SensorService; import org.chromium.mojom.vsync.VSyncProvider; import org.domokit.activity.ActivityImpl; +import org.domokit.activity.PathServiceImpl; import org.domokit.media.MediaServiceImpl; import org.domokit.oknet.NetworkServiceImpl; import org.domokit.vsync.VSyncProviderImpl; @@ -74,6 +76,22 @@ public class SkyApplication extends BaseChromiumApplication { } }); + registry.register(org.chromium.mojom.updater.UpdateService.MANAGER.getName(), + new ServiceFactory() { + @Override + public void connectToService(Context context, Core core, MessagePipeHandle pipe) { + org.chromium.mojom.updater.UpdateService.MANAGER.bind( + new UpdateService.MojoService(), pipe); + } + }); + + registry.register(PathService.MANAGER.getName(), new ServiceFactory() { + @Override + public void connectToService(Context context, Core core, MessagePipeHandle pipe) { + PathService.MANAGER.bind(new PathServiceImpl(getApplicationContext()), pipe); + } + }); + registry.register(KeyboardService.MANAGER.getName(), new ServiceFactory() { @Override public void connectToService(Context context, Core core, MessagePipeHandle pipe) { diff --git a/sky/shell/android/org/domokit/sky/shell/UpdateService.java b/sky/shell/android/org/domokit/sky/shell/UpdateService.java index ef6992fdd..2dd259923 100644 --- a/sky/shell/android/org/domokit/sky/shell/UpdateService.java +++ b/sky/shell/android/org/domokit/sky/shell/UpdateService.java @@ -15,6 +15,7 @@ import java.io.File; import org.chromium.base.CalledByNative; import org.chromium.base.JNINamespace; import org.chromium.base.PathUtils; +import org.chromium.mojo.system.MojoException; /** * A class that schedules and runs periodic autoupdate checks. @@ -24,11 +25,28 @@ public class UpdateService extends Service { private static final String TAG = "UpdateService"; private static final int REQUEST_CODE = 0; // Not sure why this is needed. private static final boolean ENABLED = false; + private static final boolean TESTING = false; private long mNativePtr = 0; + private static UpdateService sCurrentUpdateService = null; + + static class MojoService implements org.chromium.mojom.updater.UpdateService { + @Override + public void close() {} + + @Override + public void onConnectionError(MojoException e) {} + + @Override + public void notifyUpdateCheckComplete() { + if (sCurrentUpdateService != null) + sCurrentUpdateService.stopSelf(); + } + } + public static void init(Context context) { - if (ENABLED) + if (ENABLED || TESTING) maybeScheduleUpdateCheck(context); } @@ -36,7 +54,7 @@ public class UpdateService extends Service { Intent alarm = new Intent(context, UpdateService.class); PendingIntent existingIntent = PendingIntent.getService( context, REQUEST_CODE, alarm, PendingIntent.FLAG_NO_CREATE); - if (existingIntent != null) { + if (existingIntent != null && !TESTING) { Log.i(TAG, "Update alarm exists: " + PathUtils.getDataDirectory(context)); return; } @@ -52,6 +70,7 @@ public class UpdateService extends Service { @Override public void onCreate() { + sCurrentUpdateService = this; super.onCreate(); SkyMain.ensureInitialized(getApplicationContext(), null); } @@ -61,6 +80,7 @@ public class UpdateService extends Service { if (mNativePtr != 0) nativeDestroy(mNativePtr); mNativePtr = 0; + sCurrentUpdateService = null; } @Override @@ -74,12 +94,6 @@ public class UpdateService extends Service { return null; } - @SuppressWarnings("unused") - @CalledByNative - public void onUpdateFinished() { - stopSelf(); - } - private native long nativeCheckForUpdates(); private native void nativeDestroy(long nativeUpdateTaskAndroid); } diff --git a/sky/shell/android/update_service_android.cc b/sky/shell/android/update_service_android.cc index d3f020571..86f13c597 100644 --- a/sky/shell/android/update_service_android.cc +++ b/sky/shell/android/update_service_android.cc @@ -10,6 +10,7 @@ #include "sky/engine/bindings/updater_snapshot.h" #include "sky/engine/public/sky/sky_headless.h" #include "sky/shell/shell.h" +#include "sky/shell/ui/internals.h" namespace sky { namespace shell { @@ -25,7 +26,7 @@ bool RegisterUpdateService(JNIEnv* env) { } UpdateTaskAndroid::UpdateTaskAndroid(JNIEnv* env, jobject update_service) - : headless_(new blink::SkyHeadless) { + : headless_(new blink::SkyHeadless(this)) { update_service_.Reset(env, update_service); } @@ -38,24 +39,20 @@ void UpdateTaskAndroid::Start() { base::Unretained(this))); } -void UpdateTaskAndroid::RunDartOnUIThread() { - // TODO(mpcomplete): pass variables into main. +void UpdateTaskAndroid::DidCreateIsolate(Dart_Isolate isolate) { + Internals::Create(isolate, CreateServiceProvider( + Shell::Shared().service_provider_context()), + nullptr); +} +void UpdateTaskAndroid::RunDartOnUIThread() { headless_->Init("sky:updater"); headless_->RunFromSnapshotBuffer(kUpdaterSnapshotBuffer, kUpdaterSnapshotBufferSize); - - // TODO(mpcomplete): Have Dart notify us when done so we can notify Java - // and be deleted. Or can Dart talk to Java directly? -} - -void UpdateTaskAndroid::Finish() { - // The Java side is responsible for deleting us when finished. - Java_UpdateService_onUpdateFinished(base::android::AttachCurrentThread(), - update_service_.obj()); } void UpdateTaskAndroid::Destroy(JNIEnv* env, jobject jcaller) { + Shell::Shared().ui_task_runner()->DeleteSoon(FROM_HERE, headless_.release()); delete this; } diff --git a/sky/shell/android/update_service_android.h b/sky/shell/android/update_service_android.h index 0da15a111..56a8c14e4 100644 --- a/sky/shell/android/update_service_android.h +++ b/sky/shell/android/update_service_android.h @@ -9,21 +9,17 @@ #include "base/android/scoped_java_ref.h" #include "base/memory/scoped_ptr.h" - -namespace blink { -class SkyHeadless; -} +#include "sky/engine/public/sky/sky_headless.h" namespace sky { namespace shell { -class UpdateTaskAndroid { +class UpdateTaskAndroid : public blink::SkyHeadless::Client { public: UpdateTaskAndroid(JNIEnv* env, jobject update_service); virtual ~UpdateTaskAndroid(); void Start(); - void Finish(); // This C++ object is owned by the Java UpdateTask. This is called by // UpdateTask when it is destroyed. @@ -32,6 +28,9 @@ class UpdateTaskAndroid { private: void RunDartOnUIThread(); + // SkyHeadless::Client: + void DidCreateIsolate(Dart_Isolate isolate) override; + scoped_ptr headless_; base::android::ScopedJavaGlobalRef update_service_; };