blob: e6f03a466db48da04b08f461b417f1f16871229e [file] [log] [blame]
// 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.download;
import static org.chromium.chrome.browser.download.DownloadSnackbarController.INVALID_NOTIFICATION_ID;
import android.app.Notification;
import android.app.NotificationManager;
import android.app.Service;
import android.content.Context;
import android.content.Intent;
import android.os.Binder;
import android.os.Build;
import android.os.IBinder;
import android.support.annotation.IntDef;
import android.support.annotation.Nullable;
import android.support.v4.app.ServiceCompat;
import org.chromium.base.ContextUtils;
import org.chromium.base.Log;
import org.chromium.base.VisibleForTesting;
import org.chromium.chrome.browser.notifications.ForegroundServiceUtils;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
/**
* Keep-alive foreground service for downloads.
*/
public class DownloadForegroundService extends Service {
private static final String TAG = "DownloadFg";
// Deprecated, remove this after M75.
private static final String KEY_PERSISTED_NOTIFICATION_ID = "PersistedNotificationId";
private final IBinder mBinder = new LocalBinder();
private NotificationManager mNotificationManager;
@IntDef({StopForegroundNotification.KILL, StopForegroundNotification.DETACH})
@Retention(RetentionPolicy.SOURCE)
public @interface StopForegroundNotification {
int KILL = 0; // Kill notification regardless of ability to detach.
int DETACH = 1; // Try to detach, otherwise kill and relaunch.
}
@Override
public void onCreate() {
super.onCreate();
mNotificationManager =
(NotificationManager) ContextUtils.getApplicationContext().getSystemService(
Context.NOTIFICATION_SERVICE);
clearPersistedNotificationId();
}
/**
* Start the foreground service with this given context.
* @param context The context used to start service.
*/
public static void startDownloadForegroundService(Context context) {
// TODO(crbug.com/770389): Grab a WakeLock here until the service has started.
ForegroundServiceUtils.getInstance().startForegroundService(
new Intent(context, DownloadForegroundService.class));
}
/**
* Start the foreground service or update it to be pinned to a different notification.
*
* @param newNotificationId The ID of the new notification to pin the service to.
* @param newNotification The new notification to be pinned to the service.
* @param oldNotificationId The ID of the original notification that was pinned to the
* service, can be INVALID_NOTIFICATION_ID if the service is just
* starting.
* @param oldNotification The original notification the service was pinned to, in case an
* adjustment needs to be made (in the case it could not be
* detached).
* @param killOldNotification Whether or not to detach or kill the old notification.
*/
public void startOrUpdateForegroundService(int newNotificationId, Notification newNotification,
int oldNotificationId, Notification oldNotification, boolean killOldNotification) {
Log.w(TAG,
"startOrUpdateForegroundService new: " + newNotificationId
+ ", old: " + oldNotificationId + ", kill old: " + killOldNotification);
// Handle notifications and start foreground.
if (oldNotificationId == INVALID_NOTIFICATION_ID && oldNotification == null) {
// If there is no old notification or old notification id, just start foreground.
startForegroundInternal(newNotificationId, newNotification);
} else {
if (getCurrentSdk() >= 24) {
// If possible, detach notification so it doesn't get cancelled by accident.
stopForegroundInternal(killOldNotification ? ServiceCompat.STOP_FOREGROUND_REMOVE
: ServiceCompat.STOP_FOREGROUND_DETACH);
startForegroundInternal(newNotificationId, newNotification);
} else {
// Otherwise start the foreground and relaunch the originally pinned notification.
startForegroundInternal(newNotificationId, newNotification);
if (!killOldNotification) {
relaunchOldNotification(oldNotificationId, oldNotification);
}
}
}
// Record when starting foreground and when updating pinned notification.
if (oldNotificationId == INVALID_NOTIFICATION_ID) {
DownloadNotificationUmaHelper.recordForegroundServiceLifecycleHistogram(
DownloadNotificationUmaHelper.ForegroundLifecycle.START);
} else {
if (oldNotificationId != newNotificationId) {
DownloadNotificationUmaHelper.recordForegroundServiceLifecycleHistogram(
DownloadNotificationUmaHelper.ForegroundLifecycle.UPDATE);
}
}
}
/**
* Stop the foreground service that is running.
*
* @param stopForegroundNotification How to handle the notification upon the foreground
* stopping (options are: kill, detach or adjust, or detach
* or persist, see {@link StopForegroundNotification}.
* @param pinnedNotificationId Id of the notification that is pinned to the foreground
* and would need adjustment.
* @param pinnedNotification The actual notification that is pinned to the foreground
* and would need adjustment.
*/
public void stopDownloadForegroundService(
@StopForegroundNotification int stopForegroundNotification, int pinnedNotificationId,
Notification pinnedNotification) {
Log.w(TAG,
"stopDownloadForegroundService status: " + stopForegroundNotification
+ ", id: " + pinnedNotificationId);
// Record when stopping foreground.
DownloadNotificationUmaHelper.recordForegroundServiceLifecycleHistogram(
DownloadNotificationUmaHelper.ForegroundLifecycle.STOP);
DownloadNotificationUmaHelper.recordServiceStoppedHistogram(
DownloadNotificationUmaHelper.ServiceStopped.STOPPED, true /* withForeground */);
// Handle notifications and stop foreground.
if (stopForegroundNotification == StopForegroundNotification.KILL) {
// Regardless of the SDK level, stop foreground and kill if so indicated.
stopForegroundInternal(ServiceCompat.STOP_FOREGROUND_REMOVE);
} else {
if (getCurrentSdk() >= 24) {
// Android N+ has the option to detach notifications from the service, so detach or
// kill the notification as needed when stopping service.
stopForegroundInternal(ServiceCompat.STOP_FOREGROUND_DETACH);
} else if (getCurrentSdk() >= 23) {
// Android M+ can't detach the notification but doesn't have other caveats. Kill the
// notification and relaunch if detach was desired.
stopForegroundInternal(ServiceCompat.STOP_FOREGROUND_REMOVE);
relaunchOldNotification(pinnedNotificationId, pinnedNotification);
} else {
// In phones that are Lollipop and older (API < 23), the service gets killed with
// the task, which might result in the notification being unable to be relaunched
// where it needs to be. kill and relaunch the old notification before stopping the
// service.
relaunchOldNotification(
getNewNotificationIdFor(pinnedNotificationId), pinnedNotification);
stopForegroundInternal(ServiceCompat.STOP_FOREGROUND_REMOVE);
}
}
stopSelf();
}
@VisibleForTesting
void relaunchOldNotification(int notificationId, Notification notification) {
if (notificationId != INVALID_NOTIFICATION_ID && notification != null) {
mNotificationManager.notify(notificationId, notification);
}
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
// In the case the service was restarted when the intent is null.
if (intent == null) {
DownloadNotificationUmaHelper.recordServiceStoppedHistogram(
DownloadNotificationUmaHelper.ServiceStopped.START_STICKY, true);
// Allow observers to restart service on their own, if needed.
stopSelf();
}
// This should restart service after Chrome gets killed (except for Android 4.4.2).
return START_STICKY;
}
@Override
public void onDestroy() {
DownloadNotificationUmaHelper.recordServiceStoppedHistogram(
DownloadNotificationUmaHelper.ServiceStopped.DESTROYED, true /* withForeground */);
DownloadForegroundServiceObservers.alertObserversServiceDestroyed();
super.onDestroy();
}
@Override
public void onTaskRemoved(Intent rootIntent) {
DownloadNotificationUmaHelper.recordServiceStoppedHistogram(
DownloadNotificationUmaHelper.ServiceStopped.TASK_REMOVED, true /*withForeground*/);
DownloadForegroundServiceObservers.alertObserversTaskRemoved();
super.onTaskRemoved(rootIntent);
}
@Override
public void onLowMemory() {
DownloadNotificationUmaHelper.recordServiceStoppedHistogram(
DownloadNotificationUmaHelper.ServiceStopped.LOW_MEMORY, true /* withForeground */);
super.onLowMemory();
}
@Nullable
@Override
public IBinder onBind(Intent intent) {
return mBinder;
}
/**
* Class for clients to access.
*/
class LocalBinder extends Binder {
DownloadForegroundService getService() {
return DownloadForegroundService.this;
}
}
/**
* Clear stored value for the id of the notification pinned to the service.
*/
@VisibleForTesting
static void clearPersistedNotificationId() {
ContextUtils.getAppSharedPreferences().edit().remove(KEY_PERSISTED_NOTIFICATION_ID).apply();
}
/** Methods for testing. */
@VisibleForTesting
int getNewNotificationIdFor(int oldNotificationId) {
return DownloadNotificationService.getNewNotificationIdFor(oldNotificationId);
}
@VisibleForTesting
void startForegroundInternal(int notificationId, Notification notification) {
Log.w(TAG, "startForegroundInternal id: " + notificationId);
ForegroundServiceUtils.getInstance().startForeground(
this, notificationId, notification, 0 /* foregroundServiceType */);
}
@VisibleForTesting
void stopForegroundInternal(int flags) {
Log.w(TAG, "stopForegroundInternal flags: " + flags);
ServiceCompat.stopForeground(this, flags);
}
@VisibleForTesting
int getCurrentSdk() {
return Build.VERSION.SDK_INT;
}
}