diff options
Diffstat (limited to 'mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/offline/DownloadService.java')
-rw-r--r-- | mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/offline/DownloadService.java | 1049 |
1 files changed, 1049 insertions, 0 deletions
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/offline/DownloadService.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/offline/DownloadService.java new file mode 100644 index 0000000000..a2d7d82438 --- /dev/null +++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/offline/DownloadService.java @@ -0,0 +1,1049 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.mozilla.thirdparty.com.google.android.exoplayer2.offline; + +import static org.mozilla.thirdparty.com.google.android.exoplayer2.offline.Download.STOP_REASON_NONE; + +import android.app.Notification; +import android.app.Service; +import android.content.Context; +import android.content.Intent; +import android.os.Handler; +import android.os.IBinder; +import android.os.Looper; +import androidx.annotation.Nullable; +import androidx.annotation.StringRes; +import org.mozilla.thirdparty.com.google.android.exoplayer2.scheduler.Requirements; +import org.mozilla.thirdparty.com.google.android.exoplayer2.scheduler.Scheduler; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Log; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.NotificationUtil; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util; +import java.util.HashMap; +import java.util.List; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; + +/** A {@link Service} for downloading media. */ +public abstract class DownloadService extends Service { + + /** + * Starts a download service to resume any ongoing downloads. Extras: + * + * <ul> + * <li>{@link #KEY_FOREGROUND} - See {@link #KEY_FOREGROUND}. + * </ul> + */ + public static final String ACTION_INIT = + "com.google.android.exoplayer.downloadService.action.INIT"; + + /** Like {@link #ACTION_INIT}, but with {@link #KEY_FOREGROUND} implicitly set to true. */ + private static final String ACTION_RESTART = + "com.google.android.exoplayer.downloadService.action.RESTART"; + + /** + * Adds a new download. Extras: + * + * <ul> + * <li>{@link #KEY_DOWNLOAD_REQUEST} - A {@link DownloadRequest} defining the download to be + * added. + * <li>{@link #KEY_STOP_REASON} - An initial stop reason for the download. If omitted {@link + * Download#STOP_REASON_NONE} is used. + * <li>{@link #KEY_FOREGROUND} - See {@link #KEY_FOREGROUND}. + * </ul> + */ + public static final String ACTION_ADD_DOWNLOAD = + "com.google.android.exoplayer.downloadService.action.ADD_DOWNLOAD"; + + /** + * Removes a download. Extras: + * + * <ul> + * <li>{@link #KEY_CONTENT_ID} - The content id of a download to remove. + * <li>{@link #KEY_FOREGROUND} - See {@link #KEY_FOREGROUND}. + * </ul> + */ + public static final String ACTION_REMOVE_DOWNLOAD = + "com.google.android.exoplayer.downloadService.action.REMOVE_DOWNLOAD"; + + /** + * Removes all downloads. Extras: + * + * <ul> + * <li>{@link #KEY_FOREGROUND} - See {@link #KEY_FOREGROUND}. + * </ul> + */ + public static final String ACTION_REMOVE_ALL_DOWNLOADS = + "com.google.android.exoplayer.downloadService.action.REMOVE_ALL_DOWNLOADS"; + + /** + * Resumes all downloads except those that have a non-zero {@link Download#stopReason}. Extras: + * + * <ul> + * <li>{@link #KEY_FOREGROUND} - See {@link #KEY_FOREGROUND}. + * </ul> + */ + public static final String ACTION_RESUME_DOWNLOADS = + "com.google.android.exoplayer.downloadService.action.RESUME_DOWNLOADS"; + + /** + * Pauses all downloads. Extras: + * + * <ul> + * <li>{@link #KEY_FOREGROUND} - See {@link #KEY_FOREGROUND}. + * </ul> + */ + public static final String ACTION_PAUSE_DOWNLOADS = + "com.google.android.exoplayer.downloadService.action.PAUSE_DOWNLOADS"; + + /** + * Sets the stop reason for one or all downloads. To clear the stop reason, pass {@link + * Download#STOP_REASON_NONE}. Extras: + * + * <ul> + * <li>{@link #KEY_CONTENT_ID} - The content id of a single download to update with the stop + * reason. If omitted, all downloads will be updated. + * <li>{@link #KEY_STOP_REASON} - An application provided reason for stopping the download or + * downloads, or {@link Download#STOP_REASON_NONE} to clear the stop reason. + * <li>{@link #KEY_FOREGROUND} - See {@link #KEY_FOREGROUND}. + * </ul> + */ + public static final String ACTION_SET_STOP_REASON = + "com.google.android.exoplayer.downloadService.action.SET_STOP_REASON"; + + /** + * Sets the requirements that need to be met for downloads to progress. Extras: + * + * <ul> + * <li>{@link #KEY_REQUIREMENTS} - A {@link Requirements}. + * <li>{@link #KEY_FOREGROUND} - See {@link #KEY_FOREGROUND}. + * </ul> + */ + public static final String ACTION_SET_REQUIREMENTS = + "com.google.android.exoplayer.downloadService.action.SET_REQUIREMENTS"; + + /** Key for the {@link DownloadRequest} in {@link #ACTION_ADD_DOWNLOAD} intents. */ + public static final String KEY_DOWNLOAD_REQUEST = "download_request"; + + /** + * Key for the {@link String} content id in {@link #ACTION_SET_STOP_REASON} and {@link + * #ACTION_REMOVE_DOWNLOAD} intents. + */ + public static final String KEY_CONTENT_ID = "content_id"; + + /** + * Key for the integer stop reason in {@link #ACTION_SET_STOP_REASON} and {@link + * #ACTION_ADD_DOWNLOAD} intents. + */ + public static final String KEY_STOP_REASON = "stop_reason"; + + /** Key for the {@link Requirements} in {@link #ACTION_SET_REQUIREMENTS} intents. */ + public static final String KEY_REQUIREMENTS = "requirements"; + + /** + * Key for a boolean extra that can be set on any intent to indicate whether the service was + * started in the foreground. If set, the service is guaranteed to call {@link + * #startForeground(int, Notification)}. + */ + public static final String KEY_FOREGROUND = "foreground"; + + /** Invalid foreground notification id that can be used to run the service in the background. */ + public static final int FOREGROUND_NOTIFICATION_ID_NONE = 0; + + /** Default foreground notification update interval in milliseconds. */ + public static final long DEFAULT_FOREGROUND_NOTIFICATION_UPDATE_INTERVAL = 1000; + + private static final String TAG = "DownloadService"; + + // Keep a DownloadManagerHelper for each DownloadService as long as the process is running. The + // helper is needed to restart the DownloadService when there's no scheduler. Even when there is a + // scheduler, the DownloadManagerHelper is typically able to restart the DownloadService faster. + private static final HashMap<Class<? extends DownloadService>, DownloadManagerHelper> + downloadManagerHelpers = new HashMap<>(); + + @Nullable private final ForegroundNotificationUpdater foregroundNotificationUpdater; + @Nullable private final String channelId; + @StringRes private final int channelNameResourceId; + @StringRes private final int channelDescriptionResourceId; + + @MonotonicNonNull private DownloadManager downloadManager; + private int lastStartId; + private boolean startedInForeground; + private boolean taskRemoved; + private boolean isStopped; + private boolean isDestroyed; + + /** + * Creates a DownloadService. + * + * <p>If {@code foregroundNotificationId} is {@link #FOREGROUND_NOTIFICATION_ID_NONE} then the + * service will only ever run in the background. No foreground notification will be displayed and + * {@link #getScheduler()} will not be called. + * + * <p>If {@code foregroundNotificationId} is not {@link #FOREGROUND_NOTIFICATION_ID_NONE} then the + * service will run in the foreground. The foreground notification will be updated at least as + * often as the interval specified by {@link #DEFAULT_FOREGROUND_NOTIFICATION_UPDATE_INTERVAL}. + * + * @param foregroundNotificationId The notification id for the foreground notification, or {@link + * #FOREGROUND_NOTIFICATION_ID_NONE} if the service should only ever run in the background. + */ + protected DownloadService(int foregroundNotificationId) { + this(foregroundNotificationId, DEFAULT_FOREGROUND_NOTIFICATION_UPDATE_INTERVAL); + } + + /** + * Creates a DownloadService. + * + * @param foregroundNotificationId The notification id for the foreground notification, or {@link + * #FOREGROUND_NOTIFICATION_ID_NONE} if the service should only ever run in the background. + * @param foregroundNotificationUpdateInterval The maximum interval between updates to the + * foreground notification, in milliseconds. Ignored if {@code foregroundNotificationId} is + * {@link #FOREGROUND_NOTIFICATION_ID_NONE}. + */ + protected DownloadService( + int foregroundNotificationId, long foregroundNotificationUpdateInterval) { + this( + foregroundNotificationId, + foregroundNotificationUpdateInterval, + /* channelId= */ null, + /* channelNameResourceId= */ 0, + /* channelDescriptionResourceId= */ 0); + } + + /** @deprecated Use {@link #DownloadService(int, long, String, int, int)}. */ + @Deprecated + protected DownloadService( + int foregroundNotificationId, + long foregroundNotificationUpdateInterval, + @Nullable String channelId, + @StringRes int channelNameResourceId) { + this( + foregroundNotificationId, + foregroundNotificationUpdateInterval, + channelId, + channelNameResourceId, + /* channelDescriptionResourceId= */ 0); + } + + /** + * Creates a DownloadService. + * + * @param foregroundNotificationId The notification id for the foreground notification, or {@link + * #FOREGROUND_NOTIFICATION_ID_NONE} if the service should only ever run in the background. + * @param foregroundNotificationUpdateInterval The maximum interval between updates to the + * foreground notification, in milliseconds. Ignored if {@code foregroundNotificationId} is + * {@link #FOREGROUND_NOTIFICATION_ID_NONE}. + * @param channelId An id for a low priority notification channel to create, or {@code null} if + * the app will take care of creating a notification channel if needed. If specified, must be + * unique per package. The value may be truncated if it's too long. Ignored if {@code + * foregroundNotificationId} is {@link #FOREGROUND_NOTIFICATION_ID_NONE}. + * @param channelNameResourceId A string resource identifier for the user visible name of the + * notification channel. The recommended maximum length is 40 characters. The value may be + * truncated if it's too long. Ignored if {@code channelId} is null or if {@code + * foregroundNotificationId} is {@link #FOREGROUND_NOTIFICATION_ID_NONE}. + * @param channelDescriptionResourceId A string resource identifier for the user visible + * description of the notification channel, or 0 if no description is provided. The + * recommended maximum length is 300 characters. The value may be truncated if it is too long. + * Ignored if {@code channelId} is null or if {@code foregroundNotificationId} is {@link + * #FOREGROUND_NOTIFICATION_ID_NONE}. + */ + protected DownloadService( + int foregroundNotificationId, + long foregroundNotificationUpdateInterval, + @Nullable String channelId, + @StringRes int channelNameResourceId, + @StringRes int channelDescriptionResourceId) { + if (foregroundNotificationId == FOREGROUND_NOTIFICATION_ID_NONE) { + this.foregroundNotificationUpdater = null; + this.channelId = null; + this.channelNameResourceId = 0; + this.channelDescriptionResourceId = 0; + } else { + this.foregroundNotificationUpdater = + new ForegroundNotificationUpdater( + foregroundNotificationId, foregroundNotificationUpdateInterval); + this.channelId = channelId; + this.channelNameResourceId = channelNameResourceId; + this.channelDescriptionResourceId = channelDescriptionResourceId; + } + } + + /** + * Builds an {@link Intent} for adding a new download. + * + * @param context A {@link Context}. + * @param clazz The concrete download service being targeted by the intent. + * @param downloadRequest The request to be executed. + * @param foreground Whether this intent will be used to start the service in the foreground. + * @return The created intent. + */ + public static Intent buildAddDownloadIntent( + Context context, + Class<? extends DownloadService> clazz, + DownloadRequest downloadRequest, + boolean foreground) { + return buildAddDownloadIntent(context, clazz, downloadRequest, STOP_REASON_NONE, foreground); + } + + /** + * Builds an {@link Intent} for adding a new download. + * + * @param context A {@link Context}. + * @param clazz The concrete download service being targeted by the intent. + * @param downloadRequest The request to be executed. + * @param stopReason An initial stop reason for the download, or {@link Download#STOP_REASON_NONE} + * if the download should be started. + * @param foreground Whether this intent will be used to start the service in the foreground. + * @return The created intent. + */ + public static Intent buildAddDownloadIntent( + Context context, + Class<? extends DownloadService> clazz, + DownloadRequest downloadRequest, + int stopReason, + boolean foreground) { + return getIntent(context, clazz, ACTION_ADD_DOWNLOAD, foreground) + .putExtra(KEY_DOWNLOAD_REQUEST, downloadRequest) + .putExtra(KEY_STOP_REASON, stopReason); + } + + /** + * Builds an {@link Intent} for removing the download with the {@code id}. + * + * @param context A {@link Context}. + * @param clazz The concrete download service being targeted by the intent. + * @param id The content id. + * @param foreground Whether this intent will be used to start the service in the foreground. + * @return The created intent. + */ + public static Intent buildRemoveDownloadIntent( + Context context, Class<? extends DownloadService> clazz, String id, boolean foreground) { + return getIntent(context, clazz, ACTION_REMOVE_DOWNLOAD, foreground) + .putExtra(KEY_CONTENT_ID, id); + } + + /** + * Builds an {@link Intent} for removing all downloads. + * + * @param context A {@link Context}. + * @param clazz The concrete download service being targeted by the intent. + * @param foreground Whether this intent will be used to start the service in the foreground. + * @return The created intent. + */ + public static Intent buildRemoveAllDownloadsIntent( + Context context, Class<? extends DownloadService> clazz, boolean foreground) { + return getIntent(context, clazz, ACTION_REMOVE_ALL_DOWNLOADS, foreground); + } + + /** + * Builds an {@link Intent} for resuming all downloads. + * + * @param context A {@link Context}. + * @param clazz The concrete download service being targeted by the intent. + * @param foreground Whether this intent will be used to start the service in the foreground. + * @return The created intent. + */ + public static Intent buildResumeDownloadsIntent( + Context context, Class<? extends DownloadService> clazz, boolean foreground) { + return getIntent(context, clazz, ACTION_RESUME_DOWNLOADS, foreground); + } + + /** + * Builds an {@link Intent} to pause all downloads. + * + * @param context A {@link Context}. + * @param clazz The concrete download service being targeted by the intent. + * @param foreground Whether this intent will be used to start the service in the foreground. + * @return The created intent. + */ + public static Intent buildPauseDownloadsIntent( + Context context, Class<? extends DownloadService> clazz, boolean foreground) { + return getIntent(context, clazz, ACTION_PAUSE_DOWNLOADS, foreground); + } + + /** + * Builds an {@link Intent} for setting the stop reason for one or all downloads. To clear the + * stop reason, pass {@link Download#STOP_REASON_NONE}. + * + * @param context A {@link Context}. + * @param clazz The concrete download service being targeted by the intent. + * @param id The content id, or {@code null} to set the stop reason for all downloads. + * @param stopReason An application defined stop reason. + * @param foreground Whether this intent will be used to start the service in the foreground. + * @return The created intent. + */ + public static Intent buildSetStopReasonIntent( + Context context, + Class<? extends DownloadService> clazz, + @Nullable String id, + int stopReason, + boolean foreground) { + return getIntent(context, clazz, ACTION_SET_STOP_REASON, foreground) + .putExtra(KEY_CONTENT_ID, id) + .putExtra(KEY_STOP_REASON, stopReason); + } + + /** + * Builds an {@link Intent} for setting the requirements that need to be met for downloads to + * progress. + * + * @param context A {@link Context}. + * @param clazz The concrete download service being targeted by the intent. + * @param requirements A {@link Requirements}. + * @param foreground Whether this intent will be used to start the service in the foreground. + * @return The created intent. + */ + public static Intent buildSetRequirementsIntent( + Context context, + Class<? extends DownloadService> clazz, + Requirements requirements, + boolean foreground) { + return getIntent(context, clazz, ACTION_SET_REQUIREMENTS, foreground) + .putExtra(KEY_REQUIREMENTS, requirements); + } + + /** + * Starts the service if not started already and adds a new download. + * + * @param context A {@link Context}. + * @param clazz The concrete download service to be started. + * @param downloadRequest The request to be executed. + * @param foreground Whether the service is started in the foreground. + */ + public static void sendAddDownload( + Context context, + Class<? extends DownloadService> clazz, + DownloadRequest downloadRequest, + boolean foreground) { + Intent intent = buildAddDownloadIntent(context, clazz, downloadRequest, foreground); + startService(context, intent, foreground); + } + + /** + * Starts the service if not started already and adds a new download. + * + * @param context A {@link Context}. + * @param clazz The concrete download service to be started. + * @param downloadRequest The request to be executed. + * @param stopReason An initial stop reason for the download, or {@link Download#STOP_REASON_NONE} + * if the download should be started. + * @param foreground Whether the service is started in the foreground. + */ + public static void sendAddDownload( + Context context, + Class<? extends DownloadService> clazz, + DownloadRequest downloadRequest, + int stopReason, + boolean foreground) { + Intent intent = buildAddDownloadIntent(context, clazz, downloadRequest, stopReason, foreground); + startService(context, intent, foreground); + } + + /** + * Starts the service if not started already and removes a download. + * + * @param context A {@link Context}. + * @param clazz The concrete download service to be started. + * @param id The content id. + * @param foreground Whether the service is started in the foreground. + */ + public static void sendRemoveDownload( + Context context, Class<? extends DownloadService> clazz, String id, boolean foreground) { + Intent intent = buildRemoveDownloadIntent(context, clazz, id, foreground); + startService(context, intent, foreground); + } + + /** + * Starts the service if not started already and removes all downloads. + * + * @param context A {@link Context}. + * @param clazz The concrete download service to be started. + * @param foreground Whether the service is started in the foreground. + */ + public static void sendRemoveAllDownloads( + Context context, Class<? extends DownloadService> clazz, boolean foreground) { + Intent intent = buildRemoveAllDownloadsIntent(context, clazz, foreground); + startService(context, intent, foreground); + } + + /** + * Starts the service if not started already and resumes all downloads. + * + * @param context A {@link Context}. + * @param clazz The concrete download service to be started. + * @param foreground Whether the service is started in the foreground. + */ + public static void sendResumeDownloads( + Context context, Class<? extends DownloadService> clazz, boolean foreground) { + Intent intent = buildResumeDownloadsIntent(context, clazz, foreground); + startService(context, intent, foreground); + } + + /** + * Starts the service if not started already and pauses all downloads. + * + * @param context A {@link Context}. + * @param clazz The concrete download service to be started. + * @param foreground Whether the service is started in the foreground. + */ + public static void sendPauseDownloads( + Context context, Class<? extends DownloadService> clazz, boolean foreground) { + Intent intent = buildPauseDownloadsIntent(context, clazz, foreground); + startService(context, intent, foreground); + } + + /** + * Starts the service if not started already and sets the stop reason for one or all downloads. To + * clear stop reason, pass {@link Download#STOP_REASON_NONE}. + * + * @param context A {@link Context}. + * @param clazz The concrete download service to be started. + * @param id The content id, or {@code null} to set the stop reason for all downloads. + * @param stopReason An application defined stop reason. + * @param foreground Whether the service is started in the foreground. + */ + public static void sendSetStopReason( + Context context, + Class<? extends DownloadService> clazz, + @Nullable String id, + int stopReason, + boolean foreground) { + Intent intent = buildSetStopReasonIntent(context, clazz, id, stopReason, foreground); + startService(context, intent, foreground); + } + + /** + * Starts the service if not started already and sets the requirements that need to be met for + * downloads to progress. + * + * @param context A {@link Context}. + * @param clazz The concrete download service to be started. + * @param requirements A {@link Requirements}. + * @param foreground Whether the service is started in the foreground. + */ + public static void sendSetRequirements( + Context context, + Class<? extends DownloadService> clazz, + Requirements requirements, + boolean foreground) { + Intent intent = buildSetRequirementsIntent(context, clazz, requirements, foreground); + startService(context, intent, foreground); + } + + /** + * Starts a download service to resume any ongoing downloads. + * + * @param context A {@link Context}. + * @param clazz The concrete download service to be started. + * @see #startForeground(Context, Class) + */ + public static void start(Context context, Class<? extends DownloadService> clazz) { + context.startService(getIntent(context, clazz, ACTION_INIT)); + } + + /** + * Starts the service in the foreground without adding a new download request. If there are any + * not finished downloads and the requirements are met, the service resumes downloading. Otherwise + * it stops immediately. + * + * @param context A {@link Context}. + * @param clazz The concrete download service to be started. + * @see #start(Context, Class) + */ + public static void startForeground(Context context, Class<? extends DownloadService> clazz) { + Intent intent = getIntent(context, clazz, ACTION_INIT, true); + Util.startForegroundService(context, intent); + } + + @Override + public void onCreate() { + if (channelId != null) { + NotificationUtil.createNotificationChannel( + this, + channelId, + channelNameResourceId, + channelDescriptionResourceId, + NotificationUtil.IMPORTANCE_LOW); + } + Class<? extends DownloadService> clazz = getClass(); + @Nullable DownloadManagerHelper downloadManagerHelper = downloadManagerHelpers.get(clazz); + if (downloadManagerHelper == null) { + boolean foregroundAllowed = foregroundNotificationUpdater != null; + @Nullable Scheduler scheduler = foregroundAllowed ? getScheduler() : null; + downloadManager = getDownloadManager(); + downloadManager.resumeDownloads(); + downloadManagerHelper = + new DownloadManagerHelper( + getApplicationContext(), downloadManager, foregroundAllowed, scheduler, clazz); + downloadManagerHelpers.put(clazz, downloadManagerHelper); + } else { + downloadManager = downloadManagerHelper.downloadManager; + } + downloadManagerHelper.attachService(this); + } + + @Override + public int onStartCommand(@Nullable Intent intent, int flags, int startId) { + lastStartId = startId; + taskRemoved = false; + @Nullable String intentAction = null; + @Nullable String contentId = null; + if (intent != null) { + intentAction = intent.getAction(); + contentId = intent.getStringExtra(KEY_CONTENT_ID); + startedInForeground |= + intent.getBooleanExtra(KEY_FOREGROUND, false) || ACTION_RESTART.equals(intentAction); + } + // intentAction is null if the service is restarted or no action is specified. + if (intentAction == null) { + intentAction = ACTION_INIT; + } + DownloadManager downloadManager = Assertions.checkNotNull(this.downloadManager); + switch (intentAction) { + case ACTION_INIT: + case ACTION_RESTART: + // Do nothing. + break; + case ACTION_ADD_DOWNLOAD: + @Nullable + DownloadRequest downloadRequest = + Assertions.checkNotNull(intent).getParcelableExtra(KEY_DOWNLOAD_REQUEST); + if (downloadRequest == null) { + Log.e(TAG, "Ignored ADD_DOWNLOAD: Missing " + KEY_DOWNLOAD_REQUEST + " extra"); + } else { + int stopReason = intent.getIntExtra(KEY_STOP_REASON, Download.STOP_REASON_NONE); + downloadManager.addDownload(downloadRequest, stopReason); + } + break; + case ACTION_REMOVE_DOWNLOAD: + if (contentId == null) { + Log.e(TAG, "Ignored REMOVE_DOWNLOAD: Missing " + KEY_CONTENT_ID + " extra"); + } else { + downloadManager.removeDownload(contentId); + } + break; + case ACTION_REMOVE_ALL_DOWNLOADS: + downloadManager.removeAllDownloads(); + break; + case ACTION_RESUME_DOWNLOADS: + downloadManager.resumeDownloads(); + break; + case ACTION_PAUSE_DOWNLOADS: + downloadManager.pauseDownloads(); + break; + case ACTION_SET_STOP_REASON: + if (!Assertions.checkNotNull(intent).hasExtra(KEY_STOP_REASON)) { + Log.e(TAG, "Ignored SET_STOP_REASON: Missing " + KEY_STOP_REASON + " extra"); + } else { + int stopReason = intent.getIntExtra(KEY_STOP_REASON, /* defaultValue= */ 0); + downloadManager.setStopReason(contentId, stopReason); + } + break; + case ACTION_SET_REQUIREMENTS: + @Nullable + Requirements requirements = + Assertions.checkNotNull(intent).getParcelableExtra(KEY_REQUIREMENTS); + if (requirements == null) { + Log.e(TAG, "Ignored SET_REQUIREMENTS: Missing " + KEY_REQUIREMENTS + " extra"); + } else { + downloadManager.setRequirements(requirements); + } + break; + default: + Log.e(TAG, "Ignored unrecognized action: " + intentAction); + break; + } + + if (Util.SDK_INT >= 26 && startedInForeground && foregroundNotificationUpdater != null) { + // From API level 26, services started in the foreground are required to show a notification. + foregroundNotificationUpdater.showNotificationIfNotAlready(); + } + + isStopped = false; + if (downloadManager.isIdle()) { + stop(); + } + return START_STICKY; + } + + @Override + public void onTaskRemoved(Intent rootIntent) { + taskRemoved = true; + } + + @Override + public void onDestroy() { + isDestroyed = true; + DownloadManagerHelper downloadManagerHelper = + Assertions.checkNotNull(downloadManagerHelpers.get(getClass())); + downloadManagerHelper.detachService(this); + if (foregroundNotificationUpdater != null) { + foregroundNotificationUpdater.stopPeriodicUpdates(); + } + } + + /** + * Throws {@link UnsupportedOperationException} because this service is not designed to be bound. + */ + @Nullable + @Override + public final IBinder onBind(Intent intent) { + throw new UnsupportedOperationException(); + } + + /** + * Returns a {@link DownloadManager} to be used to downloaded content. Called only once in the + * life cycle of the process. + */ + protected abstract DownloadManager getDownloadManager(); + + /** + * Returns a {@link Scheduler} to restart the service when requirements allowing downloads to take + * place are met. If {@code null}, the service will only be restarted if the process is still in + * memory when the requirements are met. + * + * <p>This method is not called for services whose {@code foregroundNotificationId} is set to + * {@link #FOREGROUND_NOTIFICATION_ID_NONE}. Such services will only be restarted if the process + * is still in memory and considered non-idle, meaning that it's either in the foreground or was + * backgrounded within the last few minutes. + */ + @Nullable + protected abstract Scheduler getScheduler(); + + /** + * Returns a notification to be displayed when this service running in the foreground. + * + * <p>Download services that do not wish to run in the foreground should be created by setting the + * {@code foregroundNotificationId} constructor argument to {@link + * #FOREGROUND_NOTIFICATION_ID_NONE}. This method is not called for such services, meaning it can + * be implemented to throw {@link UnsupportedOperationException}. + * + * @param downloads The current downloads. + * @return The foreground notification to display. + */ + protected abstract Notification getForegroundNotification(List<Download> downloads); + + /** + * Invalidates the current foreground notification and causes {@link + * #getForegroundNotification(List)} to be invoked again if the service isn't stopped. + */ + protected final void invalidateForegroundNotification() { + if (foregroundNotificationUpdater != null && !isDestroyed) { + foregroundNotificationUpdater.invalidate(); + } + } + + /** + * @deprecated Some state change events may not be delivered to this method. Instead, use {@link + * DownloadManager#addListener(DownloadManager.Listener)} to register a listener directly to + * the {@link DownloadManager} that you return through {@link #getDownloadManager()}. + */ + @Deprecated + protected void onDownloadChanged(Download download) { + // Do nothing. + } + + /** + * @deprecated Some download removal events may not be delivered to this method. Instead, use + * {@link DownloadManager#addListener(DownloadManager.Listener)} to register a listener + * directly to the {@link DownloadManager} that you return through {@link + * #getDownloadManager()}. + */ + @Deprecated + protected void onDownloadRemoved(Download download) { + // Do nothing. + } + + /** + * Called after the service is created, once the downloads are known. + * + * @param downloads The current downloads. + */ + private void notifyDownloads(List<Download> downloads) { + if (foregroundNotificationUpdater != null) { + for (int i = 0; i < downloads.size(); i++) { + if (needsStartedService(downloads.get(i).state)) { + foregroundNotificationUpdater.startPeriodicUpdates(); + break; + } + } + } + } + + /** + * Called when the state of a download changes. + * + * @param download The state of the download. + */ + @SuppressWarnings("deprecation") + private void notifyDownloadChanged(Download download) { + onDownloadChanged(download); + if (foregroundNotificationUpdater != null) { + if (needsStartedService(download.state)) { + foregroundNotificationUpdater.startPeriodicUpdates(); + } else { + foregroundNotificationUpdater.invalidate(); + } + } + } + + /** + * Called when a download is removed. + * + * @param download The last state of the download before it was removed. + */ + @SuppressWarnings("deprecation") + private void notifyDownloadRemoved(Download download) { + onDownloadRemoved(download); + if (foregroundNotificationUpdater != null) { + foregroundNotificationUpdater.invalidate(); + } + } + + /** Returns whether the service is stopped. */ + private boolean isStopped() { + return isStopped; + } + + private void stop() { + if (foregroundNotificationUpdater != null) { + foregroundNotificationUpdater.stopPeriodicUpdates(); + } + if (Util.SDK_INT < 28 && taskRemoved) { // See [Internal: b/74248644]. + stopSelf(); + isStopped = true; + } else { + isStopped |= stopSelfResult(lastStartId); + } + } + + private static boolean needsStartedService(@Download.State int state) { + return state == Download.STATE_DOWNLOADING + || state == Download.STATE_REMOVING + || state == Download.STATE_RESTARTING; + } + + private static Intent getIntent( + Context context, Class<? extends DownloadService> clazz, String action, boolean foreground) { + return getIntent(context, clazz, action).putExtra(KEY_FOREGROUND, foreground); + } + + private static Intent getIntent( + Context context, Class<? extends DownloadService> clazz, String action) { + return new Intent(context, clazz).setAction(action); + } + + private static void startService(Context context, Intent intent, boolean foreground) { + if (foreground) { + Util.startForegroundService(context, intent); + } else { + context.startService(intent); + } + } + + private final class ForegroundNotificationUpdater { + + private final int notificationId; + private final long updateInterval; + private final Handler handler; + + private boolean periodicUpdatesStarted; + private boolean notificationDisplayed; + + public ForegroundNotificationUpdater(int notificationId, long updateInterval) { + this.notificationId = notificationId; + this.updateInterval = updateInterval; + this.handler = new Handler(Looper.getMainLooper()); + } + + public void startPeriodicUpdates() { + periodicUpdatesStarted = true; + update(); + } + + public void stopPeriodicUpdates() { + periodicUpdatesStarted = false; + handler.removeCallbacksAndMessages(null); + } + + public void showNotificationIfNotAlready() { + if (!notificationDisplayed) { + update(); + } + } + + public void invalidate() { + if (notificationDisplayed) { + update(); + } + } + + private void update() { + List<Download> downloads = Assertions.checkNotNull(downloadManager).getCurrentDownloads(); + startForeground(notificationId, getForegroundNotification(downloads)); + notificationDisplayed = true; + if (periodicUpdatesStarted) { + handler.removeCallbacksAndMessages(null); + handler.postDelayed(this::update, updateInterval); + } + } + } + + private static final class DownloadManagerHelper implements DownloadManager.Listener { + + private final Context context; + private final DownloadManager downloadManager; + private final boolean foregroundAllowed; + @Nullable private final Scheduler scheduler; + private final Class<? extends DownloadService> serviceClass; + @Nullable private DownloadService downloadService; + + private DownloadManagerHelper( + Context context, + DownloadManager downloadManager, + boolean foregroundAllowed, + @Nullable Scheduler scheduler, + Class<? extends DownloadService> serviceClass) { + this.context = context; + this.downloadManager = downloadManager; + this.foregroundAllowed = foregroundAllowed; + this.scheduler = scheduler; + this.serviceClass = serviceClass; + downloadManager.addListener(this); + updateScheduler(); + } + + public void attachService(DownloadService downloadService) { + Assertions.checkState(this.downloadService == null); + this.downloadService = downloadService; + if (downloadManager.isInitialized()) { + // The call to DownloadService.notifyDownloads is posted to avoid it being called directly + // from DownloadService.onCreate. This is a good idea because it may in turn call + // DownloadService.getForegroundNotification, and concrete subclass implementations may + // not anticipate the possibility of this method being called before their onCreate + // implementation has finished executing. + new Handler() + .postAtFrontOfQueue( + () -> downloadService.notifyDownloads(downloadManager.getCurrentDownloads())); + } + } + + public void detachService(DownloadService downloadService) { + Assertions.checkState(this.downloadService == downloadService); + this.downloadService = null; + if (scheduler != null && !downloadManager.isWaitingForRequirements()) { + scheduler.cancel(); + } + } + + // DownloadManager.Listener implementation. + + @Override + public void onInitialized(DownloadManager downloadManager) { + if (downloadService != null) { + downloadService.notifyDownloads(downloadManager.getCurrentDownloads()); + } + } + + @Override + public void onDownloadChanged(DownloadManager downloadManager, Download download) { + if (downloadService != null) { + downloadService.notifyDownloadChanged(download); + } + if (serviceMayNeedRestart() && needsStartedService(download.state)) { + // This shouldn't happen unless (a) application code is changing the downloads by calling + // the DownloadManager directly rather than sending actions through the service, or (b) if + // the service is background only and a previous attempt to start it was prevented. Try and + // restart the service to robust against such cases. + Log.w(TAG, "DownloadService wasn't running. Restarting."); + restartService(); + } + } + + @Override + public void onDownloadRemoved(DownloadManager downloadManager, Download download) { + if (downloadService != null) { + downloadService.notifyDownloadRemoved(download); + } + } + + @Override + public final void onIdle(DownloadManager downloadManager) { + if (downloadService != null) { + downloadService.stop(); + } + } + + @Override + public void onWaitingForRequirementsChanged( + DownloadManager downloadManager, boolean waitingForRequirements) { + if (!waitingForRequirements + && !downloadManager.getDownloadsPaused() + && serviceMayNeedRestart()) { + // We're no longer waiting for requirements and downloads aren't paused, meaning the manager + // will be able to resume downloads that are currently queued. If there exist queued + // downloads then we should ensure the service is started. + List<Download> downloads = downloadManager.getCurrentDownloads(); + for (int i = 0; i < downloads.size(); i++) { + if (downloads.get(i).state == Download.STATE_QUEUED) { + restartService(); + break; + } + } + } + updateScheduler(); + } + + // Internal methods. + + private boolean serviceMayNeedRestart() { + return downloadService == null || downloadService.isStopped(); + } + + private void restartService() { + if (foregroundAllowed) { + Intent intent = getIntent(context, serviceClass, DownloadService.ACTION_RESTART); + Util.startForegroundService(context, intent); + } else { + // The service is background only. Use ACTION_INIT rather than ACTION_RESTART because + // ACTION_RESTART is handled as though KEY_FOREGROUND is set to true. + try { + Intent intent = getIntent(context, serviceClass, DownloadService.ACTION_INIT); + context.startService(intent); + } catch (IllegalArgumentException e) { + // The process is classed as idle by the platform. Starting a background service is not + // allowed in this state. + Log.w(TAG, "Failed to restart DownloadService (process is idle)."); + } + } + } + + private void updateScheduler() { + if (scheduler == null) { + return; + } + if (downloadManager.isWaitingForRequirements()) { + String servicePackage = context.getPackageName(); + Requirements requirements = downloadManager.getRequirements(); + boolean success = scheduler.schedule(requirements, servicePackage, ACTION_RESTART); + if (!success) { + Log.e(TAG, "Scheduling downloads failed."); + } + } else { + scheduler.cancel(); + } + } + } +} |