mirror of
https://github.com/encounter/engine.git
synced 2026-03-30 11:09:55 -07:00
Merge pull request #1414 from mpcomplete/use.changes.2
Implement working UpdateTask in Dart
This commit is contained in:
@@ -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) {
|
||||
|
||||
@@ -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<DartController> dart_controller_;
|
||||
Client* client_;
|
||||
|
||||
DISALLOW_COPY_AND_ASSIGN(SkyHeadless);
|
||||
};
|
||||
|
||||
@@ -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<String> getFilesDir() async => (await _activityProxy.ptr.getFilesDir()).path;
|
||||
Future<String> getCacheDir() async => (await _activityProxy.ptr.getCacheDir()).path;
|
||||
Future<String> getAppDataDir() async => (await _pathServiceProxy.ptr.getAppDataDir()).path;
|
||||
Future<String> getFilesDir() async => (await _pathServiceProxy.ptr.getFilesDir()).path;
|
||||
Future<String> getCacheDir() async => (await _pathServiceProxy.ptr.getCacheDir()).path;
|
||||
|
||||
@@ -25,6 +25,8 @@ action("updater") {
|
||||
]
|
||||
|
||||
deps = [
|
||||
"//sky/services/activity:interfaces",
|
||||
"//sky/services/updater:interfaces",
|
||||
"//sky/tools/sky_snapshot($host_toolchain)",
|
||||
]
|
||||
}
|
||||
|
||||
@@ -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<String> 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<yaml.YamlMap> _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<MojoResult> _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();
|
||||
}
|
||||
|
||||
@@ -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<MojoResult> _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<MojoResult> drain() async {
|
||||
var completer = new Completer();
|
||||
// TODO(mpcomplete): Is it legit to pass an async callback to listen?
|
||||
_eventStream.listen((List<int> 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<MojoResult> copyToFile(MojoDataPipeConsumer consumer, String outputPath) {
|
||||
var drainer = new PipeToFile(consumer, outputPath);
|
||||
return drainer.drain();
|
||||
}
|
||||
}
|
||||
@@ -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<int> _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);
|
||||
}
|
||||
@@ -1,9 +1,16 @@
|
||||
name: sky_updater
|
||||
name: updater
|
||||
version: 0.0.1
|
||||
author: Flutter Authors <flutter-dev@googlegroups.com>
|
||||
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'
|
||||
|
||||
@@ -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",
|
||||
]
|
||||
|
||||
|
||||
@@ -73,6 +73,10 @@ interface PathService {
|
||||
GetCacheDir() => (string path);
|
||||
};
|
||||
|
||||
interface UpdateService {
|
||||
NotifyUpdateCheckComplete();
|
||||
};
|
||||
|
||||
enum HapticFeedbackType {
|
||||
LONG_PRESS,
|
||||
VIRTUAL_KEY,
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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<blink::SkyHeadless> headless_;
|
||||
base::android::ScopedJavaGlobalRef<jobject> update_service_;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user