Commit 4d9d5bd3 authored by Clark DuVall's avatar Clark DuVall Committed by Commit Bot

Implement loading services from isolated splits

Due to b/169196314 the OS does not use the split's class loader to load
services even when the service is in the split's manifest. To work
around this, all services need to be defined in the base module's dex.

This uses a similar approach to what was done for the Application class
in http://crrev.com/c/2425404. Since there are multiple Service base
classes which chrome services inherit from, I ended up using jinja
templates to reduce duplication of common service methods since I
couldn't find a good way to do this in pure Java. Alternatively I could
also copy the base Service methods for the split compat version of each
Service subclass.

I converted an example Service, JobService, and IntentService to show
usage. A follow up will convert the rest of the services used in chrome.

Note that gerrit is not doing a good job of detecting renamed files
here, so Patchset 1 contains strictly the moved service files, and
future patchsets have the diffs from that. I would recommend reviewing
with the base set to Patchset 1.

Bug: 1126301
Change-Id: I8ccda76ee410e20d847791700b2987349e05959a
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2441250Reviewed-by: default avatarAndrew Grieve <agrieve@chromium.org>
Commit-Queue: Clark DuVall <cduvall@chromium.org>
Cr-Commit-Position: refs/heads/master@{#813766}
parent b89c289e
......@@ -962,6 +962,7 @@ android_library("chrome_test_java") {
sources = chrome_test_java_sources
deps = [
":base_module_java",
":browser_java_test_support",
":chrome_app_java_resources",
":chrome_test_util_java",
......@@ -2057,6 +2058,13 @@ android_library("base_module_java") {
sources = [
"java/src/org/chromium/chrome/browser/base/SplitChromeApplication.java",
"java/src/org/chromium/chrome/browser/base/SplitCompatApplication.java",
"java/src/org/chromium/chrome/browser/base/SplitCompatIntentService.java",
"java/src/org/chromium/chrome/browser/base/SplitCompatJobService.java",
"java/src/org/chromium/chrome/browser/base/SplitCompatService.java",
"java/src/org/chromium/chrome/browser/base/SplitCompatUtils.java",
"java/src/org/chromium/chrome/browser/download/DownloadForegroundService.java",
"java/src/org/chromium/chrome/browser/notifications/NotificationJobService.java",
"java/src/org/chromium/chrome/browser/notifications/NotificationService.java",
]
deps = [
":chrome_base_module_resources",
......
......@@ -526,7 +526,7 @@ chrome_java_sources = [
"java/src/org/chromium/chrome/browser/download/DownloadActivity.java",
"java/src/org/chromium/chrome/browser/download/DownloadBroadcastManager.java",
"java/src/org/chromium/chrome/browser/download/DownloadController.java",
"java/src/org/chromium/chrome/browser/download/DownloadForegroundService.java",
"java/src/org/chromium/chrome/browser/download/DownloadForegroundServiceImpl.java",
"java/src/org/chromium/chrome/browser/download/DownloadForegroundServiceManager.java",
"java/src/org/chromium/chrome/browser/download/DownloadForegroundServiceObservers.java",
"java/src/org/chromium/chrome/browser/download/DownloadInfoBarController.java",
......@@ -960,9 +960,9 @@ chrome_java_sources = [
"java/src/org/chromium/chrome/browser/notifications/NotificationBuilderBase.java",
"java/src/org/chromium/chrome/browser/notifications/NotificationConstants.java",
"java/src/org/chromium/chrome/browser/notifications/NotificationIntentInterceptor.java",
"java/src/org/chromium/chrome/browser/notifications/NotificationJobService.java",
"java/src/org/chromium/chrome/browser/notifications/NotificationJobServiceImpl.java",
"java/src/org/chromium/chrome/browser/notifications/NotificationPlatformBridge.java",
"java/src/org/chromium/chrome/browser/notifications/NotificationService.java",
"java/src/org/chromium/chrome/browser/notifications/NotificationServiceImpl.java",
"java/src/org/chromium/chrome/browser/notifications/NotificationSettingsBridge.java",
"java/src/org/chromium/chrome/browser/notifications/NotificationSystemStatusUtil.java",
"java/src/org/chromium/chrome/browser/notifications/NotificationTriggerBackgroundTask.java",
......
......@@ -926,11 +926,15 @@
<receiver # DIFF-ANCHOR: de24469c
android:exported="false"
android:name="org.chromium.chrome.browser.notifications.NotificationService$Receiver">
</receiver> # DIFF-ANCHOR: de24469c
<receiver # DIFF-ANCHOR: 2934478c
android:exported="false"
android:name="org.chromium.chrome.browser.notifications.NotificationServiceImpl$Receiver">
<intent-filter> # DIFF-ANCHOR: 1c1c5ed8
<action android:name="org.chromium.chrome.browser.notifications.CLICK_NOTIFICATION"/>
<action android:name="org.chromium.chrome.browser.notifications.CLOSE_NOTIFICATION"/>
</intent-filter> # DIFF-ANCHOR: 1c1c5ed8
</receiver> # DIFF-ANCHOR: de24469c
</receiver> # DIFF-ANCHOR: 2934478c
<receiver # DIFF-ANCHOR: e1c4d394
android:exported="false"
android:name="org.chromium.chrome.browser.notifications.scheduler.DisplayAgent$Receiver">
......
......@@ -866,11 +866,15 @@
<receiver # DIFF-ANCHOR: de24469c
android:exported="false"
android:name="org.chromium.chrome.browser.notifications.NotificationService$Receiver">
</receiver> # DIFF-ANCHOR: de24469c
<receiver # DIFF-ANCHOR: 2934478c
android:exported="false"
android:name="org.chromium.chrome.browser.notifications.NotificationServiceImpl$Receiver">
<intent-filter> # DIFF-ANCHOR: 1c1c5ed8
<action android:name="org.chromium.chrome.browser.notifications.CLICK_NOTIFICATION"/>
<action android:name="org.chromium.chrome.browser.notifications.CLOSE_NOTIFICATION"/>
</intent-filter> # DIFF-ANCHOR: 1c1c5ed8
</receiver> # DIFF-ANCHOR: de24469c
</receiver> # DIFF-ANCHOR: 2934478c
<receiver # DIFF-ANCHOR: e1c4d394
android:exported="false"
android:name="org.chromium.chrome.browser.notifications.scheduler.DisplayAgent$Receiver">
......
......@@ -1038,6 +1038,8 @@ by a child template that "extends" this file.
<service android:name="org.chromium.chrome.browser.notifications.NotificationService"
android:exported="false"/>
<receiver android:name="org.chromium.chrome.browser.notifications.NotificationService$Receiver"
android:exported="false"/>
<receiver android:name="org.chromium.chrome.browser.notifications.NotificationServiceImpl$Receiver"
android:exported="false">
<intent-filter>
<action android:name="org.chromium.chrome.browser.notifications.CLICK_NOTIFICATION" />
......
......@@ -5,10 +5,6 @@
package org.chromium.chrome.browser.base;
import android.content.Context;
import android.content.pm.PackageManager;
import android.os.Build;
import org.chromium.base.compat.ApiHelperForO;
/**
* Application class to use for Chrome when //chrome code is in an isolated split. This class will
......@@ -30,8 +26,8 @@ public class SplitChromeApplication extends SplitCompatApplication {
@Override
protected void attachBaseContext(Context context) {
if (isBrowserProcess()) {
context = createChromeContext(context);
setImpl(createChromeApplication(context));
context = SplitCompatUtils.createChromeContext(context);
setImpl((Impl) SplitCompatUtils.newInstance(context, mChromeApplicationClassName));
} else {
setImpl(createNonBrowserApplication());
}
......@@ -41,29 +37,4 @@ public class SplitChromeApplication extends SplitCompatApplication {
protected Impl createNonBrowserApplication() {
return new Impl();
}
private Impl createChromeApplication(Context context) {
try {
return (Impl) context.getClassLoader()
.loadClass(mChromeApplicationClassName)
.newInstance();
} catch (ReflectiveOperationException e) {
throw new RuntimeException(e);
}
}
private Context createChromeContext(Context base) {
assert isBrowserProcess();
// Isolated splits are only supported in O+, so just return the base context on other
// versions, since this will have access to all splits.
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
return base;
}
try {
return ApiHelperForO.createContextForSplit(base, "chrome");
} catch (PackageManager.NameNotFoundException e) {
// This application class should not be used if the chrome split does not exist.
throw new RuntimeException(e);
}
}
}
// Copyright 2020 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.chromium.chrome.browser.base;
import android.app.IntentService;
import android.content.Context;
import android.content.Intent;
import android.os.IBinder;
/**
* IntentService base class which will call through to the given {@link Impl}. This class must be
* present in the base module, while the Impl can be in the chrome module.
*/
public class SplitCompatIntentService extends IntentService {
private String mServiceClassName;
private Impl mImpl;
public SplitCompatIntentService(String serviceClassName, String name) {
super(name);
mServiceClassName = serviceClassName;
}
@Override
protected void attachBaseContext(Context context) {
context = SplitCompatUtils.createChromeContext(context);
mImpl = (Impl) SplitCompatUtils.newInstance(context, mServiceClassName);
mImpl.setService(this);
super.attachBaseContext(context);
}
private IBinder superOnBind(Intent intent) {
return super.onBind(intent);
}
@Override
protected void onHandleIntent(Intent intent) {
mImpl.onHandleIntent(intent);
}
/**
* Holds the implementation of service logic. Will be called by {@link
* SplitCompatIntentService}.
*/
public abstract static class Impl {
private SplitCompatIntentService mService;
private void setService(SplitCompatIntentService service) {
mService = service;
}
protected final IntentService getService() {
return mService;
}
public IBinder onBind(Intent intent) {
return mService.superOnBind(intent);
}
protected abstract void onHandleIntent(Intent intent);
}
}
// Copyright 2020 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.chromium.chrome.browser.base;
import android.app.job.JobParameters;
import android.app.job.JobService;
import android.content.Context;
/**
* JobService base class which will call through to the given {@link Impl}. This class must be
* present in the base module, while the Impl can be in the chrome module.
*/
public class SplitCompatJobService extends JobService {
private String mServiceClassName;
private Impl mImpl;
public SplitCompatJobService(String serviceClassName) {
mServiceClassName = serviceClassName;
}
@Override
protected void attachBaseContext(Context context) {
context = SplitCompatUtils.createChromeContext(context);
mImpl = (Impl) SplitCompatUtils.newInstance(context, mServiceClassName);
mImpl.setService(this);
super.attachBaseContext(context);
}
@Override
public boolean onStartJob(JobParameters params) {
return mImpl.onStartJob(params);
}
@Override
public boolean onStopJob(JobParameters params) {
return mImpl.onStopJob(params);
}
/**
* Holds the implementation of service logic. Will be called by {@link SplitCompatJobService}.
*/
public abstract static class Impl {
private SplitCompatJobService mService;
private void setService(SplitCompatJobService service) {
mService = service;
}
protected final JobService getService() {
return mService;
}
public abstract boolean onStartJob(JobParameters params);
public abstract boolean onStopJob(JobParameters params);
}
}
// Copyright 2020 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.chromium.chrome.browser.base;
import android.app.Service;
import android.content.Context;
import android.content.Intent;
import android.os.IBinder;
/**
* Service base class which will call through to the given {@link Impl}. This class must be present
* in the base module, while the Impl can be in the chrome module.
*/
public class SplitCompatService extends Service {
private String mServiceClassName;
private Impl mImpl;
public SplitCompatService(String serviceClassName) {
mServiceClassName = serviceClassName;
}
@Override
protected void attachBaseContext(Context context) {
context = SplitCompatUtils.createChromeContext(context);
mImpl = (Impl) SplitCompatUtils.newInstance(context, mServiceClassName);
mImpl.setService(this);
super.attachBaseContext(context);
}
@Override
public void onCreate() {
super.onCreate();
mImpl.onCreate();
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
return mImpl.onStartCommand(intent, flags, startId);
}
@Override
public void onDestroy() {
super.onDestroy();
mImpl.onDestroy();
}
@Override
public void onTaskRemoved(Intent rootIntent) {
super.onTaskRemoved(rootIntent);
mImpl.onTaskRemoved(rootIntent);
}
@Override
public void onLowMemory() {
super.onLowMemory();
mImpl.onLowMemory();
}
@Override
public IBinder onBind(Intent intent) {
return mImpl.onBind(intent);
}
private int superOnStartCommand(Intent intent, int flags, int startId) {
return super.onStartCommand(intent, flags, startId);
}
/**
* Holds the implementation of service logic. Will be called by {@link SplitCompatService}.
*/
public abstract static class Impl {
private SplitCompatService mService;
protected void setService(SplitCompatService service) {
mService = service;
}
protected final Service getService() {
return mService;
}
public void onCreate() {}
public int onStartCommand(Intent intent, int flags, int startId) {
return mService.superOnStartCommand(intent, flags, startId);
}
public void onDestroy() {}
public void onTaskRemoved(Intent rootIntent) {}
public void onLowMemory() {}
public abstract IBinder onBind(Intent intent);
}
}
// Copyright 2020 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.chromium.chrome.browser.base;
import android.content.Context;
import android.content.pm.PackageManager;
import android.os.Build;
import org.chromium.base.compat.ApiHelperForO;
/** Utils for compatibility with isolated splits. */
public class SplitCompatUtils {
private SplitCompatUtils() {}
/** Creates a context which can be used to load code and resources in the chrome split. */
public static Context createChromeContext(Context base) {
// Isolated splits are only supported in O+, so just return the base context on other
// versions, since this will have access to all splits.
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
return base;
}
try {
return ApiHelperForO.createContextForSplit(base, "chrome");
} catch (PackageManager.NameNotFoundException e) {
// This application class should not be used if the chrome split does not exist.
throw new RuntimeException(e);
}
}
/**
* Constructs a new instance of the given class name using the class loader from the context.
*/
public static Object newInstance(Context context, String className) {
try {
return context.getClassLoader().loadClass(className).newInstance();
} catch (ReflectiveOperationException e) {
throw new RuntimeException(e);
}
}
}
......@@ -80,7 +80,7 @@ public class DownloadForegroundServiceManager {
private boolean mStartForegroundCalled;
// This is non-null when onServiceConnected has been called (aka service is active).
private DownloadForegroundService mBoundService;
private DownloadForegroundServiceImpl mBoundService;
@VisibleForTesting
final Map<Integer, DownloadUpdate> mDownloadUpdateQueue = new HashMap<>();
......@@ -221,7 +221,7 @@ public class DownloadForegroundServiceManager {
@VisibleForTesting
void startAndBindServiceInternal(Context context) {
DownloadForegroundService.startDownloadForegroundService(context);
DownloadForegroundServiceImpl.startDownloadForegroundService(context);
context.bindService(new Intent(context, DownloadForegroundService.class), mConnection,
Context.BIND_AUTO_CREATE);
}
......@@ -230,13 +230,13 @@ public class DownloadForegroundServiceManager {
@Override
public void onServiceConnected(ComponentName className, IBinder service) {
Log.w(TAG, "onServiceConnected");
if (!(service instanceof DownloadForegroundService.LocalBinder)) {
if (!(service instanceof DownloadForegroundServiceImpl.LocalBinder)) {
Log.w(TAG,
"Not from DownloadNotificationService, do not connect."
+ " Component name: " + className);
return;
}
mBoundService = ((DownloadForegroundService.LocalBinder) service).getService();
mBoundService = ((DownloadForegroundServiceImpl.LocalBinder) service).getService();
DownloadForegroundServiceObservers.addObserver(
DownloadNotificationServiceObserver.class);
processDownloadUpdateQueue(true /* isProcessingPending */);
......@@ -307,13 +307,14 @@ public class DownloadForegroundServiceManager {
checkNotNull(mBoundService);
mIsServiceBound = false;
@DownloadForegroundService.StopForegroundNotification
@DownloadForegroundServiceImpl.StopForegroundNotification
int stopForegroundNotification;
if (downloadStatus == DownloadNotificationService.DownloadStatus.CANCELLED) {
stopForegroundNotification = DownloadForegroundService.StopForegroundNotification.KILL;
stopForegroundNotification =
DownloadForegroundServiceImpl.StopForegroundNotification.KILL;
} else {
stopForegroundNotification =
DownloadForegroundService.StopForegroundNotification.DETACH;
DownloadForegroundServiceImpl.StopForegroundNotification.DETACH;
}
DownloadUpdate downloadUpdate = mDownloadUpdateQueue.get(mPinnedNotificationId);
......@@ -330,7 +331,7 @@ public class DownloadForegroundServiceManager {
@VisibleForTesting
void stopAndUnbindServiceInternal(
@DownloadForegroundService.StopForegroundNotification int stopForegroundStatus,
@DownloadForegroundServiceImpl.StopForegroundNotification int stopForegroundStatus,
int pinnedNotificationId, Notification pinnedNotification) {
mBoundService.stopDownloadForegroundService(
stopForegroundStatus, pinnedNotificationId, pinnedNotification);
......@@ -343,7 +344,7 @@ public class DownloadForegroundServiceManager {
/** Helper code for testing. */
@VisibleForTesting
void setBoundService(DownloadForegroundService service) {
void setBoundService(DownloadForegroundServiceImpl service) {
mBoundService = service;
}
......
// Copyright 2017 The Chromium Authors. All rights reserved.
// Copyright 2020 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.chromium.chrome.browser.notifications;
import android.annotation.TargetApi;
import android.app.job.JobParameters;
import android.app.job.JobService;
import android.content.Intent;
import android.os.Build;
import android.os.Bundle;
import android.os.PersistableBundle;
import android.os.SystemClock;
import org.chromium.chrome.browser.base.SplitCompatJobService;
import org.chromium.base.ThreadUtils;
/**
* Processes jobs scheduled when user actions are issued on web notifications.
* We use this instead of starting the NotificationService on N+.
*/
@TargetApi(Build.VERSION_CODES.N)
public class NotificationJobService extends JobService {
static PersistableBundle getJobExtrasFromIntent(Intent intent) {
PersistableBundle bundle = new PersistableBundle();
bundle.putString(NotificationConstants.EXTRA_NOTIFICATION_ID,
intent.getStringExtra(NotificationConstants.EXTRA_NOTIFICATION_ID));
bundle.putInt(NotificationConstants.EXTRA_NOTIFICATION_TYPE,
intent.getIntExtra(NotificationConstants.EXTRA_NOTIFICATION_TYPE,
NotificationType.WEB_PERSISTENT));
bundle.putString(NotificationConstants.EXTRA_NOTIFICATION_INFO_ORIGIN,
intent.getStringExtra(NotificationConstants.EXTRA_NOTIFICATION_INFO_ORIGIN));
bundle.putString(NotificationConstants.EXTRA_NOTIFICATION_INFO_SCOPE,
intent.getStringExtra(NotificationConstants.EXTRA_NOTIFICATION_INFO_SCOPE));
bundle.putString(NotificationConstants.EXTRA_NOTIFICATION_INFO_PROFILE_ID,
intent.getStringExtra(NotificationConstants.EXTRA_NOTIFICATION_INFO_PROFILE_ID));
bundle.putBoolean(NotificationConstants.EXTRA_NOTIFICATION_INFO_PROFILE_INCOGNITO,
intent.getBooleanExtra(
NotificationConstants.EXTRA_NOTIFICATION_INFO_PROFILE_INCOGNITO, false));
bundle.putInt(NotificationConstants.EXTRA_NOTIFICATION_INFO_ACTION_INDEX,
intent.getIntExtra(NotificationConstants.EXTRA_NOTIFICATION_INFO_ACTION_INDEX, -1));
bundle.putString(NotificationConstants.EXTRA_NOTIFICATION_INFO_WEBAPK_PACKAGE,
intent.getStringExtra(
NotificationConstants.EXTRA_NOTIFICATION_INFO_WEBAPK_PACKAGE));
bundle.putString(NotificationConstants.EXTRA_NOTIFICATION_ACTION, intent.getAction());
// Only primitives can be set on a persistable bundle, so extract the raw reply.
bundle.putString(NotificationConstants.EXTRA_NOTIFICATION_REPLY,
NotificationPlatformBridge.getNotificationReply(intent));
return bundle;
}
/**
* Called when a Notification has been interacted with by the user. If we can verify that
* the Intent has a notification Id, start Chrome (if needed) on the UI thread.
*
* We get a wakelock for our process for the duration of this method.
*
* @return True if there is more work to be done to handle the job, to signal we would like our
* wakelock extended until we call {@link #jobFinished}. False if we have finished handling the
* job.
*/
@Override
public boolean onStartJob(final JobParameters params) {
PersistableBundle extras = params.getExtras();
putJobStartedTimeInExtras(extras);
if (!extras.containsKey(NotificationConstants.EXTRA_NOTIFICATION_ID)
|| !extras.containsKey(NotificationConstants.EXTRA_NOTIFICATION_INFO_ORIGIN)) {
return false;
}
Intent intent =
new Intent(extras.getString(NotificationConstants.EXTRA_NOTIFICATION_ACTION));
intent.putExtras(new Bundle(extras));
ThreadUtils.assertOnUiThread();
NotificationService.dispatchIntentOnUIThread(intent);
// TODO(crbug.com/685197): Return true here and call jobFinished to release the wake
// lock only after the event has been completely handled by the service worker.
return false;
}
private static void putJobStartedTimeInExtras(PersistableBundle extras) {
extras.putLong(
NotificationConstants.EXTRA_JOB_STARTED_TIME_MS, SystemClock.elapsedRealtime());
}
@Override
public boolean onStopJob(JobParameters params) {
// As it stands, all our job processing is done synchronously in onStartJob so there is
// nothing to do here. Even once we include further async processing in our jobs
// (crbug.com/685197) it may by infeasible to cancel this halfway through.
// TODO(crbug.com/685197): Check what we can safely do here and update comment.
return false;
/** See {@link NotificationJobServiceImpl}. */
public class NotificationJobService extends SplitCompatJobService {
public NotificationJobService() {
super("org.chromium.chrome.browser.notifications.NotificationJobServiceImpl");
}
}
// Copyright 2017 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.chromium.chrome.browser.notifications;
import android.annotation.TargetApi;
import android.app.job.JobParameters;
import android.content.Intent;
import android.os.Build;
import android.os.Bundle;
import android.os.PersistableBundle;
import android.os.SystemClock;
import org.chromium.base.ThreadUtils;
import org.chromium.base.annotations.UsedByReflection;
/**
* Processes jobs scheduled when user actions are issued on web notifications.
* We use this instead of starting the NotificationService on N+.
*/
@UsedByReflection("NotificationService.java")
@TargetApi(Build.VERSION_CODES.N)
public class NotificationJobServiceImpl extends NotificationJobService.Impl {
@UsedByReflection("NotificationService.java")
public NotificationJobServiceImpl() {}
static PersistableBundle getJobExtrasFromIntent(Intent intent) {
PersistableBundle bundle = new PersistableBundle();
bundle.putString(NotificationConstants.EXTRA_NOTIFICATION_ID,
intent.getStringExtra(NotificationConstants.EXTRA_NOTIFICATION_ID));
bundle.putInt(NotificationConstants.EXTRA_NOTIFICATION_TYPE,
intent.getIntExtra(NotificationConstants.EXTRA_NOTIFICATION_TYPE,
NotificationType.WEB_PERSISTENT));
bundle.putString(NotificationConstants.EXTRA_NOTIFICATION_INFO_ORIGIN,
intent.getStringExtra(NotificationConstants.EXTRA_NOTIFICATION_INFO_ORIGIN));
bundle.putString(NotificationConstants.EXTRA_NOTIFICATION_INFO_SCOPE,
intent.getStringExtra(NotificationConstants.EXTRA_NOTIFICATION_INFO_SCOPE));
bundle.putString(NotificationConstants.EXTRA_NOTIFICATION_INFO_PROFILE_ID,
intent.getStringExtra(NotificationConstants.EXTRA_NOTIFICATION_INFO_PROFILE_ID));
bundle.putBoolean(NotificationConstants.EXTRA_NOTIFICATION_INFO_PROFILE_INCOGNITO,
intent.getBooleanExtra(
NotificationConstants.EXTRA_NOTIFICATION_INFO_PROFILE_INCOGNITO, false));
bundle.putInt(NotificationConstants.EXTRA_NOTIFICATION_INFO_ACTION_INDEX,
intent.getIntExtra(NotificationConstants.EXTRA_NOTIFICATION_INFO_ACTION_INDEX, -1));
bundle.putString(NotificationConstants.EXTRA_NOTIFICATION_INFO_WEBAPK_PACKAGE,
intent.getStringExtra(
NotificationConstants.EXTRA_NOTIFICATION_INFO_WEBAPK_PACKAGE));
bundle.putString(NotificationConstants.EXTRA_NOTIFICATION_ACTION, intent.getAction());
// Only primitives can be set on a persistable bundle, so extract the raw reply.
bundle.putString(NotificationConstants.EXTRA_NOTIFICATION_REPLY,
NotificationPlatformBridge.getNotificationReply(intent));
return bundle;
}
/**
* Called when a Notification has been interacted with by the user. If we can verify that
* the Intent has a notification Id, start Chrome (if needed) on the UI thread.
*
* We get a wakelock for our process for the duration of this method.
*
* @return True if there is more work to be done to handle the job, to signal we would like our
* wakelock extended until we call {@link #jobFinished}. False if we have finished handling the
* job.
*/
@Override
public boolean onStartJob(final JobParameters params) {
PersistableBundle extras = params.getExtras();
putJobStartedTimeInExtras(extras);
if (!extras.containsKey(NotificationConstants.EXTRA_NOTIFICATION_ID)
|| !extras.containsKey(NotificationConstants.EXTRA_NOTIFICATION_INFO_ORIGIN)) {
return false;
}
Intent intent =
new Intent(extras.getString(NotificationConstants.EXTRA_NOTIFICATION_ACTION));
intent.putExtras(new Bundle(extras));
ThreadUtils.assertOnUiThread();
NotificationServiceImpl.dispatchIntentOnUIThread(intent);
// TODO(crbug.com/685197): Return true here and call jobFinished to release the wake
// lock only after the event has been completely handled by the service worker.
return false;
}
private static void putJobStartedTimeInExtras(PersistableBundle extras) {
extras.putLong(
NotificationConstants.EXTRA_JOB_STARTED_TIME_MS, SystemClock.elapsedRealtime());
}
@Override
public boolean onStopJob(JobParameters params) {
// As it stands, all our job processing is done synchronously in onStartJob so there is
// nothing to do here. Even once we include further async processing in our jobs
// (crbug.com/685197) it may by infeasible to cancel this halfway through.
// TODO(crbug.com/685197): Check what we can safely do here and update comment.
return false;
}
}
......@@ -327,7 +327,7 @@ public class NotificationPlatformBridge {
int actionIndex) {
Uri intentData = makeIntentData(notificationId, origin, actionIndex);
Intent intent = new Intent(action, intentData);
intent.setClass(context, NotificationService.Receiver.class);
intent.setClass(context, NotificationServiceImpl.Receiver.class);
// Make sure to update NotificationJobService.getJobExtrasFromIntent() when changing any
// of the extras included with the |intent|.
......
// Copyright 2014 The Chromium Authors. All rights reserved.
// Copyright 2020 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.chromium.chrome.browser.notifications;
import android.annotation.TargetApi;
import android.app.IntentService;
import android.app.job.JobInfo;
import android.app.job.JobScheduler;
import android.content.BroadcastReceiver;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.os.Build;
import android.os.PersistableBundle;
import android.os.SystemClock;
import android.util.Log;
import org.chromium.base.task.PostTask;
import org.chromium.chrome.browser.init.ChromeBrowserInitializer;
import org.chromium.chrome.browser.webapps.WebappRegistry;
import org.chromium.components.background_task_scheduler.TaskIds;
import org.chromium.content_public.browser.UiThreadTaskTraits;
import org.chromium.chrome.browser.base.SplitCompatIntentService;
import org.chromium.chrome.browser.base.SplitCompatUtils;
/**
* The Notification service receives intents fired as responses to user actions issued on Android
* notifications displayed in the notification tray.
*/
public class NotificationService extends IntentService {
/** See {@link NotificationServiceImpl}. */
public class NotificationService extends SplitCompatIntentService {
private static final String TAG = NotificationService.class.getSimpleName();
/**
* The class which receives the intents from the Android framework. It initializes the
* Notification service, and forward the intents there. Declared public as it needs to be
* initialized by the Android framework.
*/
public static class Receiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
Log.i(TAG, "Received a notification intent in the NotificationService's receiver.");
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
// Android encourages us not to start services directly on N+, so instead we
// schedule a job to handle the notification intent. We use the Android JobScheduler
// rather than GcmNetworkManager or FirebaseJobDispatcher since the JobScheduler
// allows us to execute immediately by setting an override deadline of zero
// milliseconds.
JobScheduler scheduler =
(JobScheduler) context.getSystemService(Context.JOB_SCHEDULER_SERVICE);
PersistableBundle extras = NotificationJobService.getJobExtrasFromIntent(intent);
putJobScheduledTimeInExtras(extras);
JobInfo job =
new JobInfo
.Builder(TaskIds.NOTIFICATION_SERVICE_JOB_ID,
new ComponentName(context, NotificationJobService.class))
.setExtras(extras)
.setOverrideDeadline(0)
.build();
scheduler.schedule(job);
} else {
// TODO(peter): Do we need to acquire a wake lock here?
intent.setClass(context, NotificationService.class);
context.startService(intent);
}
}
@TargetApi(Build.VERSION_CODES.N)
private static void putJobScheduledTimeInExtras(PersistableBundle extras) {
extras.putLong(NotificationConstants.EXTRA_JOB_SCHEDULED_TIME_MS,
SystemClock.elapsedRealtime());
}
}
public NotificationService() {
super(TAG);
super("org.chromium.chrome.browser.notifications.NotificationServiceImpl", TAG);
}
/**
* Called when a Notification has been interacted with by the user. If we can verify that
* the Intent has a notification Id, start Chrome (if needed) on the UI thread.
*
* @param intent The intent containing the specific information.
* This receiver forwards the onReceive() call to the implementation version. This is needed to
* handle pending intents referring to the old receiver name.
*/
@Override
public void onHandleIntent(final Intent intent) {
if (!intent.hasExtra(NotificationConstants.EXTRA_NOTIFICATION_ID)
|| !intent.hasExtra(NotificationConstants.EXTRA_NOTIFICATION_INFO_ORIGIN)) {
return;
}
PostTask.runOrPostTask(
UiThreadTaskTraits.DEFAULT, () -> { dispatchIntentOnUIThread(intent); });
}
/**
* Initializes Chrome and starts the browser process if it's not running as of yet, and
* dispatch |intent| to the NotificationPlatformBridge once this is done.
*
* @param intent The intent containing the notification's information.
*/
static void dispatchIntentOnUIThread(Intent intent) {
ChromeBrowserInitializer.getInstance().handleSynchronousStartup();
// Warm up the WebappRegistry, as we need to check if this notification should launch a
// standalone web app. This no-ops if the registry is already initialized and warmed.
WebappRegistry.getInstance();
WebappRegistry.warmUpSharedPrefs();
// Now that the browser process is initialized, we pass forward the call to the
// NotificationPlatformBridge which will take care of delivering the appropriate events.
if (!NotificationPlatformBridge.dispatchNotificationEvent(intent)) {
Log.w(TAG, "Unable to dispatch the notification event to Chrome.");
public static class Receiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
BroadcastReceiver receiver = (BroadcastReceiver) SplitCompatUtils.newInstance(context,
"org.chromium.chrome.browser.notifications.NotificationServiceImpl$Receiver");
receiver.onReceive(context, intent);
}
// TODO(peter): Verify that the lifetime of the NotificationService is sufficient
// when a notification event could be dispatched successfully.
}
}
// Copyright 2014 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.chromium.chrome.browser.notifications;
import android.annotation.TargetApi;
import android.app.job.JobInfo;
import android.app.job.JobScheduler;
import android.content.BroadcastReceiver;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.os.Build;
import android.os.PersistableBundle;
import android.os.SystemClock;
import android.util.Log;
import org.chromium.base.annotations.UsedByReflection;
import org.chromium.base.task.PostTask;
import org.chromium.chrome.browser.init.ChromeBrowserInitializer;
import org.chromium.chrome.browser.webapps.WebappRegistry;
import org.chromium.components.background_task_scheduler.TaskIds;
import org.chromium.content_public.browser.UiThreadTaskTraits;
/**
* The Notification service receives intents fired as responses to user actions issued on Android
* notifications displayed in the notification tray.
*/
@UsedByReflection("NotificationService.java")
public class NotificationServiceImpl extends NotificationService.Impl {
private static final String TAG = NotificationServiceImpl.class.getSimpleName();
/**
* The class which receives the intents from the Android framework. It initializes the
* Notification service, and forward the intents there. Declared public as it needs to be
* initialized by the Android framework.
*/
public static class Receiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
Log.i(TAG, "Received a notification intent in the NotificationService's receiver.");
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
// Android encourages us not to start services directly on N+, so instead we
// schedule a job to handle the notification intent. We use the Android JobScheduler
// rather than GcmNetworkManager or FirebaseJobDispatcher since the JobScheduler
// allows us to execute immediately by setting an override deadline of zero
// milliseconds.
JobScheduler scheduler =
(JobScheduler) context.getSystemService(Context.JOB_SCHEDULER_SERVICE);
PersistableBundle extras =
NotificationJobServiceImpl.getJobExtrasFromIntent(intent);
putJobScheduledTimeInExtras(extras);
JobInfo job =
new JobInfo
.Builder(TaskIds.NOTIFICATION_SERVICE_JOB_ID,
new ComponentName(context, NotificationJobService.class))
.setExtras(extras)
.setOverrideDeadline(0)
.build();
scheduler.schedule(job);
} else {
// TODO(peter): Do we need to acquire a wake lock here?
intent.setClass(context, NotificationService.class);
context.startService(intent);
}
}
@TargetApi(Build.VERSION_CODES.N)
private static void putJobScheduledTimeInExtras(PersistableBundle extras) {
extras.putLong(NotificationConstants.EXTRA_JOB_SCHEDULED_TIME_MS,
SystemClock.elapsedRealtime());
}
}
@UsedByReflection("NotificationService.java")
public NotificationServiceImpl() {}
/**
* Called when a Notification has been interacted with by the user. If we can verify that
* the Intent has a notification Id, start Chrome (if needed) on the UI thread.
*
* @param intent The intent containing the specific information.
*/
@Override
public void onHandleIntent(final Intent intent) {
if (!intent.hasExtra(NotificationConstants.EXTRA_NOTIFICATION_ID)
|| !intent.hasExtra(NotificationConstants.EXTRA_NOTIFICATION_INFO_ORIGIN)) {
return;
}
PostTask.runOrPostTask(
UiThreadTaskTraits.DEFAULT, () -> { dispatchIntentOnUIThread(intent); });
}
/**
* Initializes Chrome and starts the browser process if it's not running as of yet, and
* dispatch |intent| to the NotificationPlatformBridge once this is done.
*
* @param intent The intent containing the notification's information.
*/
static void dispatchIntentOnUIThread(Intent intent) {
ChromeBrowserInitializer.getInstance().handleSynchronousStartup();
// Warm up the WebappRegistry, as we need to check if this notification should launch a
// standalone web app. This no-ops if the registry is already initialized and warmed.
WebappRegistry.getInstance();
WebappRegistry.warmUpSharedPrefs();
// Now that the browser process is initialized, we pass forward the call to the
// NotificationPlatformBridge which will take care of delivering the appropriate events.
if (!NotificationPlatformBridge.dispatchNotificationEvent(intent)) {
Log.w(TAG, "Unable to dispatch the notification event to Chrome.");
}
// TODO(peter): Verify that the lifetime of the NotificationService is sufficient
// when a notification event could be dispatched successfully.
}
}
......@@ -72,7 +72,7 @@ public final class DownloadForegroundServiceManagerTest {
}
@Override
void stopAndUnbindServiceInternal(@DownloadForegroundService.StopForegroundNotification
void stopAndUnbindServiceInternal(@DownloadForegroundServiceImpl.StopForegroundNotification
int stopForegroundNotification,
int pinnedNotificationId, Notification pinnedNotification) {
mStopForegroundNotificationFlag = stopForegroundNotification;
......@@ -102,7 +102,7 @@ public final class DownloadForegroundServiceManagerTest {
* Implementation of DownloadForegroundService for testing.
* Does not implement startOrUpdateForegroundService to avoid test service lifecycle.
*/
public static class MockDownloadForegroundService extends DownloadForegroundService {
public static class MockDownloadForegroundService extends DownloadForegroundServiceImpl {
@Override
public void startOrUpdateForegroundService(int newNotificationId,
Notification newNotification, int oldNotificationId, Notification oldNotification,
......@@ -231,7 +231,7 @@ public final class DownloadForegroundServiceManagerTest {
mDownloadServiceManager.updateDownloadStatus(mContext,
DownloadNotificationService.DownloadStatus.PAUSED, FAKE_DOWNLOAD_1, mNotification);
assertFalse(mDownloadServiceManager.mIsServiceBound);
assertEquals(DownloadForegroundService.StopForegroundNotification.DETACH,
assertEquals(DownloadForegroundServiceImpl.StopForegroundNotification.DETACH,
mDownloadServiceManager.mStopForegroundNotificationFlag);
// Service restarts and then is cancelled, so notification is killed.
......@@ -245,7 +245,7 @@ public final class DownloadForegroundServiceManagerTest {
DownloadNotificationService.DownloadStatus.CANCELLED, FAKE_DOWNLOAD_1,
mNotification);
assertFalse(mDownloadServiceManager.mIsServiceBound);
assertEquals(DownloadForegroundService.StopForegroundNotification.KILL,
assertEquals(DownloadForegroundServiceImpl.StopForegroundNotification.KILL,
mDownloadServiceManager.mStopForegroundNotificationFlag);
// Download starts and completes, notification is either detached or killed.
......@@ -259,7 +259,7 @@ public final class DownloadForegroundServiceManagerTest {
DownloadNotificationService.DownloadStatus.COMPLETED, FAKE_DOWNLOAD_2,
mNotification);
assertFalse(mDownloadServiceManager.mIsServiceBound);
assertEquals(DownloadForegroundService.StopForegroundNotification.DETACH,
assertEquals(DownloadForegroundServiceImpl.StopForegroundNotification.DETACH,
mDownloadServiceManager.mStopForegroundNotificationFlag);
}
......
......@@ -47,7 +47,7 @@ public class DownloadForegroundServiceTest {
* Implementation of DownloadForegroundService for testing.
* Mimics behavior of DownloadForegroundService except for calls to the actual service.
*/
public static class MockDownloadForegroundService extends DownloadForegroundService {
public static class MockDownloadForegroundService extends DownloadForegroundServiceImpl {
@IntDef({MethodID.START_FOREGROUND, MethodID.STOP_FOREGROUND_FLAGS,
MethodID.RELAUNCH_NOTIFICATION})
@Retention(RetentionPolicy.SOURCE)
......@@ -65,6 +65,10 @@ public class DownloadForegroundServiceTest {
// Used for saving MethodID values.
List<Integer> mMethodCalls = new ArrayList<>();
public MockDownloadForegroundService() {
setService(new DownloadForegroundService());
}
// Clears stored flags/boolean/id/method calls. Call between tests runs.
void clearStoredState() {
mStopForegroundFlags = -1;
......@@ -214,7 +218,7 @@ public class DownloadForegroundServiceTest {
mForegroundService.clearStoredState();
mForegroundService.stopDownloadForegroundService(
DownloadForegroundService.StopForegroundNotification.DETACH,
DownloadForegroundServiceImpl.StopForegroundNotification.DETACH,
INVALID_NOTIFICATION_ID, null);
assertEquals(expectedMethodCalls, mForegroundService.mMethodCalls);
assertEquals(STOP_FOREGROUND_DETACH, mForegroundService.mStopForegroundFlags);
......@@ -225,7 +229,7 @@ public class DownloadForegroundServiceTest {
mForegroundService.clearStoredState();
mForegroundService.stopDownloadForegroundService(
DownloadForegroundService.StopForegroundNotification.DETACH,
DownloadForegroundServiceImpl.StopForegroundNotification.DETACH,
INVALID_NOTIFICATION_ID, null);
assertEquals(expectedMethodCalls, mForegroundService.mMethodCalls);
assertEquals(STOP_FOREGROUND_DETACH, mForegroundService.mStopForegroundFlags);
......@@ -236,8 +240,8 @@ public class DownloadForegroundServiceTest {
mForegroundService.clearStoredState();
mForegroundService.stopDownloadForegroundService(
DownloadForegroundService.StopForegroundNotification.KILL, INVALID_NOTIFICATION_ID,
null);
DownloadForegroundServiceImpl.StopForegroundNotification.KILL,
INVALID_NOTIFICATION_ID, null);
assertEquals(expectedMethodCalls, mForegroundService.mMethodCalls);
assertEquals(STOP_FOREGROUND_REMOVE, mForegroundService.mStopForegroundFlags);
}
......@@ -260,7 +264,7 @@ public class DownloadForegroundServiceTest {
mForegroundService.clearStoredState();
mForegroundService.stopDownloadForegroundService(
DownloadForegroundService.StopForegroundNotification.DETACH, FAKE_DOWNLOAD_ID1,
DownloadForegroundServiceImpl.StopForegroundNotification.DETACH, FAKE_DOWNLOAD_ID1,
mNotification);
List<Integer> expectedMethodCalls =
Arrays.asList(MockDownloadForegroundService.MethodID.STOP_FOREGROUND_FLAGS,
......@@ -276,7 +280,7 @@ public class DownloadForegroundServiceTest {
mForegroundService.clearStoredState();
mForegroundService.stopDownloadForegroundService(
DownloadForegroundService.StopForegroundNotification.DETACH, FAKE_DOWNLOAD_ID1,
DownloadForegroundServiceImpl.StopForegroundNotification.DETACH, FAKE_DOWNLOAD_ID1,
mNotification);
expectedMethodCalls =
Arrays.asList(MockDownloadForegroundService.MethodID.STOP_FOREGROUND_FLAGS,
......@@ -291,7 +295,7 @@ public class DownloadForegroundServiceTest {
mForegroundService.clearStoredState();
mForegroundService.stopDownloadForegroundService(
DownloadForegroundService.StopForegroundNotification.KILL, FAKE_DOWNLOAD_ID1,
DownloadForegroundServiceImpl.StopForegroundNotification.KILL, FAKE_DOWNLOAD_ID1,
mNotification);
expectedMethodCalls =
Arrays.asList(MockDownloadForegroundService.MethodID.STOP_FOREGROUND_FLAGS);
......@@ -316,7 +320,7 @@ public class DownloadForegroundServiceTest {
mForegroundService.clearStoredState();
mForegroundService.stopDownloadForegroundService(
DownloadForegroundService.StopForegroundNotification.DETACH, FAKE_DOWNLOAD_ID1,
DownloadForegroundServiceImpl.StopForegroundNotification.DETACH, FAKE_DOWNLOAD_ID1,
mNotification);
List<Integer> expectedMethodCalls =
Arrays.asList(MockDownloadForegroundService.MethodID.RELAUNCH_NOTIFICATION,
......@@ -331,7 +335,7 @@ public class DownloadForegroundServiceTest {
mForegroundService.mNextNotificationId = FAKE_DOWNLOAD_ID2;
mForegroundService.stopDownloadForegroundService(
DownloadForegroundService.StopForegroundNotification.DETACH, FAKE_DOWNLOAD_ID1,
DownloadForegroundServiceImpl.StopForegroundNotification.DETACH, FAKE_DOWNLOAD_ID1,
mNotification);
expectedMethodCalls =
Arrays.asList(MockDownloadForegroundService.MethodID.RELAUNCH_NOTIFICATION,
......@@ -347,7 +351,7 @@ public class DownloadForegroundServiceTest {
mForegroundService.clearStoredState();
mForegroundService.stopDownloadForegroundService(
DownloadForegroundService.StopForegroundNotification.KILL, FAKE_DOWNLOAD_ID1,
DownloadForegroundServiceImpl.StopForegroundNotification.KILL, FAKE_DOWNLOAD_ID1,
mNotification);
expectedMethodCalls =
Arrays.asList(MockDownloadForegroundService.MethodID.STOP_FOREGROUND_FLAGS);
......@@ -371,7 +375,7 @@ public class DownloadForegroundServiceTest {
mForegroundService.mNextNotificationId = FAKE_DOWNLOAD_ID2;
mForegroundService.stopDownloadForegroundService(
DownloadForegroundService.StopForegroundNotification.DETACH, FAKE_DOWNLOAD_ID1,
DownloadForegroundServiceImpl.StopForegroundNotification.DETACH, FAKE_DOWNLOAD_ID1,
mNotification);
List<Integer> expectedMethodCalls =
Arrays.asList(MockDownloadForegroundService.MethodID.RELAUNCH_NOTIFICATION,
......@@ -388,7 +392,7 @@ public class DownloadForegroundServiceTest {
mForegroundService.mNextNotificationId = FAKE_DOWNLOAD_ID2;
mForegroundService.stopDownloadForegroundService(
DownloadForegroundService.StopForegroundNotification.DETACH, FAKE_DOWNLOAD_ID1,
DownloadForegroundServiceImpl.StopForegroundNotification.DETACH, FAKE_DOWNLOAD_ID1,
mNotification);
assertEquals(expectedMethodCalls, mForegroundService.mMethodCalls);
assertEquals(ServiceCompat.STOP_FOREGROUND_REMOVE, mForegroundService.mStopForegroundFlags);
......@@ -401,7 +405,7 @@ public class DownloadForegroundServiceTest {
mForegroundService.clearStoredState();
mForegroundService.stopDownloadForegroundService(
DownloadForegroundService.StopForegroundNotification.KILL, FAKE_DOWNLOAD_ID1,
DownloadForegroundServiceImpl.StopForegroundNotification.KILL, FAKE_DOWNLOAD_ID1,
mNotification);
expectedMethodCalls =
Arrays.asList(MockDownloadForegroundService.MethodID.STOP_FOREGROUND_FLAGS);
......
......@@ -148,7 +148,7 @@ public class NotificationPlatformBridgeIntentTest {
.getApplicationContext();
Intent intent = new Intent(NotificationConstants.ACTION_CLICK_NOTIFICATION);
intent.setClass(context, NotificationService.Receiver.class);
intent.setClass(context, NotificationServiceImpl.Receiver.class);
intent.putExtra(NotificationConstants.EXTRA_NOTIFICATION_ID, "42");
intent.putExtra(NotificationConstants.EXTRA_NOTIFICATION_INFO_PROFILE_ID, "Default");
......
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