java.lang.Object
↳Service
↳androidx.media3.exoplayer.offline.DownloadService
Gradle dependencies
compile group: 'androidx.media3', name: 'media3-exoplayer', version: '1.5.0-alpha01'
- groupId: androidx.media3
- artifactId: media3-exoplayer
- version: 1.5.0-alpha01
Artifact androidx.media3:media3-exoplayer:1.5.0-alpha01 it located at Google repository (https://maven.google.com/)
Overview
A for downloading media.
Apps with target SDK 33 and greater need to add the android.permission.POST_NOTIFICATIONS permission to the manifest and request the permission at
runtime before starting downloads. Without that permission granted by the user, notifications
posted by this service are not displayed. See the
official UI guide for more detailed information.
Summary
Constructors |
---|
protected | DownloadService(int foregroundNotificationId)
Creates a DownloadService. |
protected | DownloadService(int foregroundNotificationId, long foregroundNotificationUpdateInterval)
Creates a DownloadService. |
protected | DownloadService(int foregroundNotificationId, long foregroundNotificationUpdateInterval, java.lang.String channelId, int channelNameResourceId, int channelDescriptionResourceId)
Creates a DownloadService. |
Methods |
---|
public static Intent | buildAddDownloadIntent(Context context, java.lang.Class<DownloadService> clazz, DownloadRequest downloadRequest, boolean foreground)
Builds an for adding a new download. |
public static Intent | buildAddDownloadIntent(Context context, java.lang.Class<DownloadService> clazz, DownloadRequest downloadRequest, int stopReason, boolean foreground)
Builds an for adding a new download. |
public static Intent | buildPauseDownloadsIntent(Context context, java.lang.Class<DownloadService> clazz, boolean foreground)
Builds an to pause all downloads. |
public static Intent | buildRemoveAllDownloadsIntent(Context context, java.lang.Class<DownloadService> clazz, boolean foreground)
Builds an for removing all downloads. |
public static Intent | buildRemoveDownloadIntent(Context context, java.lang.Class<DownloadService> clazz, java.lang.String id, boolean foreground)
Builds an for removing the download with the id. |
public static Intent | buildResumeDownloadsIntent(Context context, java.lang.Class<DownloadService> clazz, boolean foreground)
Builds an for resuming all downloads. |
public static Intent | buildSetRequirementsIntent(Context context, java.lang.Class<DownloadService> clazz, Requirements requirements, boolean foreground)
Builds an for setting the requirements that need to be met for downloads to
progress. |
public static Intent | buildSetStopReasonIntent(Context context, java.lang.Class<DownloadService> clazz, java.lang.String id, int stopReason, boolean foreground)
Builds an for setting the stop reason for one or all downloads. |
public static void | clearDownloadManagerHelpers()
Clear all before restarting the
service. |
protected abstract DownloadManager | getDownloadManager()
Returns a DownloadManager to be used to downloaded content. |
protected abstract Notification | getForegroundNotification(java.util.List<Download> downloads, int notMetRequirements)
Returns a notification to be displayed when this service running in the foreground. |
protected abstract Scheduler | getScheduler()
Returns a Scheduler to restart the service when requirements for downloads to continue
are met. |
protected final void | invalidateForegroundNotification()
Invalidates the current foreground notification and causes DownloadService.getForegroundNotification(List, int) to be invoked again if the service isn't stopped. |
public final IBinder | onBind(Intent intent)
Throws java.lang.UnsupportedOperationException because this service is not designed to be bound. |
public void | onCreate()
|
public void | onDestroy()
|
public int | onStartCommand(Intent intent, int flags, int startId)
|
public void | onTaskRemoved(Intent rootIntent)
|
public static void | sendAddDownload(Context context, java.lang.Class<DownloadService> clazz, DownloadRequest downloadRequest, boolean foreground)
Starts the service if not started already and adds a new download. |
public static void | sendAddDownload(Context context, java.lang.Class<DownloadService> clazz, DownloadRequest downloadRequest, int stopReason, boolean foreground)
Starts the service if not started already and adds a new download. |
public static void | sendPauseDownloads(Context context, java.lang.Class<DownloadService> clazz, boolean foreground)
Starts the service if not started already and pauses all downloads. |
public static void | sendRemoveAllDownloads(Context context, java.lang.Class<DownloadService> clazz, boolean foreground)
Starts the service if not started already and removes all downloads. |
public static void | sendRemoveDownload(Context context, java.lang.Class<DownloadService> clazz, java.lang.String id, boolean foreground)
Starts the service if not started already and removes a download. |
public static void | sendResumeDownloads(Context context, java.lang.Class<DownloadService> clazz, boolean foreground)
Starts the service if not started already and resumes all downloads. |
public static void | sendSetRequirements(Context context, java.lang.Class<DownloadService> clazz, Requirements requirements, boolean foreground)
Starts the service if not started already and sets the requirements that need to be met for
downloads to progress. |
public static void | sendSetStopReason(Context context, java.lang.Class<DownloadService> clazz, java.lang.String id, int stopReason, boolean foreground)
Starts the service if not started already and sets the stop reason for one or all downloads. |
public static void | start(Context context, java.lang.Class<DownloadService> clazz)
Starts a download service to resume any ongoing downloads. |
public static void | startForeground(Context context, java.lang.Class<DownloadService> clazz)
Starts the service in the foreground without adding a new download request. |
from java.lang.Object | clone, equals, finalize, getClass, hashCode, notify, notifyAll, toString, wait, wait, wait |
Fields
public static final java.lang.String
ACTION_INITStarts a download service to resume any ongoing downloads. Extras:
public static final java.lang.String
ACTION_ADD_DOWNLOADAdds a new download. Extras:
public static final java.lang.String
ACTION_REMOVE_DOWNLOADRemoves a download. Extras:
public static final java.lang.String
ACTION_REMOVE_ALL_DOWNLOADSRemoves all downloads. Extras:
public static final java.lang.String
ACTION_RESUME_DOWNLOADSResumes all downloads except those that have a non-zero Download.stopReason. Extras:
public static final java.lang.String
ACTION_PAUSE_DOWNLOADSPauses all downloads. Extras:
public static final java.lang.String
ACTION_SET_STOP_REASONSets the stop reason for one or all downloads. To clear the stop reason, pass Download.STOP_REASON_NONE. Extras:
public static final java.lang.String
ACTION_SET_REQUIREMENTSSets the requirements that need to be met for downloads to progress. Extras:
public static final java.lang.String
KEY_DOWNLOAD_REQUESTKey for the DownloadRequest in DownloadService.ACTION_ADD_DOWNLOAD intents.
public static final java.lang.String
KEY_CONTENT_IDKey for the java.lang.String
content id in DownloadService.ACTION_SET_STOP_REASON and DownloadService.ACTION_REMOVE_DOWNLOAD intents.
public static final java.lang.String
KEY_STOP_REASONKey for the integer stop reason in DownloadService.ACTION_SET_STOP_REASON and DownloadService.ACTION_ADD_DOWNLOAD intents.
public static final java.lang.String
KEY_REQUIREMENTSKey for the Requirements in DownloadService.ACTION_SET_REQUIREMENTS intents.
public static final java.lang.String
KEY_FOREGROUNDKey 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 DownloadService.
public static final int
FOREGROUND_NOTIFICATION_ID_NONEInvalid foreground notification id that can be used to run the service in the background.
public static final long
DEFAULT_FOREGROUND_NOTIFICATION_UPDATE_INTERVALDefault foreground notification update interval in milliseconds.
Constructors
protected
DownloadService(int foregroundNotificationId)
Creates a DownloadService.
If foregroundNotificationId is DownloadService.FOREGROUND_NOTIFICATION_ID_NONE then the
service will only ever run in the background, and no foreground notification will be displayed.
If foregroundNotificationId is not DownloadService.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 DownloadService.DEFAULT_FOREGROUND_NOTIFICATION_UPDATE_INTERVAL.
Parameters:
foregroundNotificationId: The notification id for the foreground notification, or DownloadService.FOREGROUND_NOTIFICATION_ID_NONE if the service should only ever run in the background.
protected
DownloadService(int foregroundNotificationId, long foregroundNotificationUpdateInterval)
Creates a DownloadService.
Parameters:
foregroundNotificationId: The notification id for the foreground notification, or DownloadService.FOREGROUND_NOTIFICATION_ID_NONE if the service should only ever run in the background.
foregroundNotificationUpdateInterval: The maximum interval between updates to the
foreground notification, in milliseconds. Ignored if foregroundNotificationId is
DownloadService.FOREGROUND_NOTIFICATION_ID_NONE.
protected
DownloadService(int foregroundNotificationId, long foregroundNotificationUpdateInterval, java.lang.String channelId, int channelNameResourceId, int channelDescriptionResourceId)
Creates a DownloadService.
Parameters:
foregroundNotificationId: The notification id for the foreground notification, or DownloadService.FOREGROUND_NOTIFICATION_ID_NONE if the service should only ever run in the background.
foregroundNotificationUpdateInterval: The maximum interval between updates to the
foreground notification, in milliseconds. Ignored if foregroundNotificationId is
DownloadService.FOREGROUND_NOTIFICATION_ID_NONE.
channelId: An id for a low priority notification channel to create, or 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 foregroundNotificationId is DownloadService.FOREGROUND_NOTIFICATION_ID_NONE.
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 channelId is null or if foregroundNotificationId is DownloadService.FOREGROUND_NOTIFICATION_ID_NONE.
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 channelId is null or if foregroundNotificationId is DownloadService.FOREGROUND_NOTIFICATION_ID_NONE.
Methods
public static Intent
buildAddDownloadIntent(Context context, java.lang.Class<DownloadService> clazz,
DownloadRequest downloadRequest, boolean foreground)
Builds an for adding a new download.
Parameters:
context: A .
clazz: The concrete download service being targeted by the intent.
downloadRequest: The request to be executed.
foreground: Whether this intent will be used to start the service in the foreground.
Returns:
The created intent.
public static Intent
buildAddDownloadIntent(Context context, java.lang.Class<DownloadService> clazz,
DownloadRequest downloadRequest, int stopReason, boolean foreground)
Builds an for adding a new download.
Parameters:
context: A .
clazz: The concrete download service being targeted by the intent.
downloadRequest: The request to be executed.
stopReason: An initial stop reason for the download, or Download.STOP_REASON_NONE
if the download should be started.
foreground: Whether this intent will be used to start the service in the foreground.
Returns:
The created intent.
public static Intent
buildRemoveDownloadIntent(Context context, java.lang.Class<DownloadService> clazz, java.lang.String id, boolean foreground)
Builds an for removing the download with the id.
Parameters:
context: A .
clazz: The concrete download service being targeted by the intent.
id: The content id.
foreground: Whether this intent will be used to start the service in the foreground.
Returns:
The created intent.
public static Intent
buildRemoveAllDownloadsIntent(Context context, java.lang.Class<DownloadService> clazz, boolean foreground)
Builds an for removing all downloads.
Parameters:
context: A .
clazz: The concrete download service being targeted by the intent.
foreground: Whether this intent will be used to start the service in the foreground.
Returns:
The created intent.
public static Intent
buildResumeDownloadsIntent(Context context, java.lang.Class<DownloadService> clazz, boolean foreground)
Builds an for resuming all downloads.
Parameters:
context: A .
clazz: The concrete download service being targeted by the intent.
foreground: Whether this intent will be used to start the service in the foreground.
Returns:
The created intent.
public static Intent
buildPauseDownloadsIntent(Context context, java.lang.Class<DownloadService> clazz, boolean foreground)
Builds an to pause all downloads.
Parameters:
context: A .
clazz: The concrete download service being targeted by the intent.
foreground: Whether this intent will be used to start the service in the foreground.
Returns:
The created intent.
public static Intent
buildSetStopReasonIntent(Context context, java.lang.Class<DownloadService> clazz, java.lang.String id, int stopReason, boolean foreground)
Builds an for setting the stop reason for one or all downloads. To clear the
stop reason, pass Download.STOP_REASON_NONE.
Parameters:
context: A .
clazz: The concrete download service being targeted by the intent.
id: The content id, or null to set the stop reason for all downloads.
stopReason: An application defined stop reason.
foreground: Whether this intent will be used to start the service in the foreground.
Returns:
The created intent.
public static Intent
buildSetRequirementsIntent(Context context, java.lang.Class<DownloadService> clazz,
Requirements requirements, boolean foreground)
Builds an for setting the requirements that need to be met for downloads to
progress.
Parameters:
context: A .
clazz: The concrete download service being targeted by the intent.
requirements: A Requirements.
foreground: Whether this intent will be used to start the service in the foreground.
Returns:
The created intent.
public static void
sendAddDownload(Context context, java.lang.Class<DownloadService> clazz,
DownloadRequest downloadRequest, boolean foreground)
Starts the service if not started already and adds a new download.
Parameters:
context: A .
clazz: The concrete download service to be started.
downloadRequest: The request to be executed.
foreground: Whether the service is started in the foreground.
public static void
sendAddDownload(Context context, java.lang.Class<DownloadService> clazz,
DownloadRequest downloadRequest, int stopReason, boolean foreground)
Starts the service if not started already and adds a new download.
Parameters:
context: A .
clazz: The concrete download service to be started.
downloadRequest: The request to be executed.
stopReason: An initial stop reason for the download, or Download.STOP_REASON_NONE
if the download should be started.
foreground: Whether the service is started in the foreground.
public static void
sendRemoveDownload(Context context, java.lang.Class<DownloadService> clazz, java.lang.String id, boolean foreground)
Starts the service if not started already and removes a download.
Parameters:
context: A .
clazz: The concrete download service to be started.
id: The content id.
foreground: Whether the service is started in the foreground.
public static void
sendRemoveAllDownloads(Context context, java.lang.Class<DownloadService> clazz, boolean foreground)
Starts the service if not started already and removes all downloads.
Parameters:
context: A .
clazz: The concrete download service to be started.
foreground: Whether the service is started in the foreground.
public static void
sendResumeDownloads(Context context, java.lang.Class<DownloadService> clazz, boolean foreground)
Starts the service if not started already and resumes all downloads.
Parameters:
context: A .
clazz: The concrete download service to be started.
foreground: Whether the service is started in the foreground.
public static void
sendPauseDownloads(Context context, java.lang.Class<DownloadService> clazz, boolean foreground)
Starts the service if not started already and pauses all downloads.
Parameters:
context: A .
clazz: The concrete download service to be started.
foreground: Whether the service is started in the foreground.
public static void
sendSetStopReason(Context context, java.lang.Class<DownloadService> clazz, java.lang.String id, int stopReason, boolean foreground)
Starts the service if not started already and sets the stop reason for one or all downloads. To
clear stop reason, pass Download.STOP_REASON_NONE.
Parameters:
context: A .
clazz: The concrete download service to be started.
id: The content id, or null to set the stop reason for all downloads.
stopReason: An application defined stop reason.
foreground: Whether the service is started in the foreground.
public static void
sendSetRequirements(Context context, java.lang.Class<DownloadService> clazz,
Requirements requirements, boolean foreground)
Starts the service if not started already and sets the requirements that need to be met for
downloads to progress.
Parameters:
context: A .
clazz: The concrete download service to be started.
requirements: A Requirements.
foreground: Whether the service is started in the foreground.
public static void
start(Context context, java.lang.Class<DownloadService> clazz)
Starts a download service to resume any ongoing downloads.
Parameters:
context: A .
clazz: The concrete download service to be started.
See also: DownloadService.startForeground(Context, Class extends DownloadService>)
public static void
startForeground(Context context, java.lang.Class<DownloadService> clazz)
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.
Parameters:
context: A .
clazz: The concrete download service to be started.
See also: DownloadService.start(Context, Class extends DownloadService>)
public static void
clearDownloadManagerHelpers()
Clear all before restarting the
service.
Calling this method is normally only required if an app supports downloading content for
multiple users for which different download directories should be used.
public int
onStartCommand(Intent intent, int flags, int startId)
public void
onTaskRemoved(Intent rootIntent)
public final IBinder
onBind(Intent intent)
Throws java.lang.UnsupportedOperationException
because this service is not designed to be bound.
Returns a DownloadManager to be used to downloaded content. For each concrete download
service subclass, this is called once in the lifecycle of the process when DownloadService.onCreate() is
called on the first instance of the service. If the service is destroyed and a new instance is
created later, the new instance will use the previously returned DownloadManager
without this method being called again.
Returns a Scheduler to restart the service when requirements for downloads to continue
are met.
This method is not called on all devices or for all service configurations. When it is
called, it's called only once in the life cycle of the process. If a service has unfinished
downloads that cannot make progress due to unmet requirements, it will behave according to the
first matching case below:
- If the service has foregroundNotificationId set to DownloadService.FOREGROUND_NOTIFICATION_ID_NONE, then this method will not be called. The service will
remain in the background until the downloads are able to continue to completion or the
service is killed by the platform.
- If the device API level is less than 31, a Scheduler is returned from this
method, and the returned Scheduler supports all of the requirements that have been specified for downloads to continue,
then the service will stop itself and the Scheduler will be used to restart it in
the foreground when the requirements are met.
- If the device API level is less than 31 and either null or a Scheduler
that does not support all of the requirements
is returned from this method, then the service will remain in the foreground until the
downloads are able to continue to completion.
- If the device API level is 31 or above, then this method will not be called and the
service will remain in the foreground until the downloads are able to continue to
completion. A Scheduler cannot be used for this case due to Android 12
foreground service launch restrictions.
-
protected abstract Notification
getForegroundNotification(java.util.List<Download> downloads, int notMetRequirements)
Returns a notification to be displayed when this service running in the foreground.
Download services that do not wish to run in the foreground should be created by setting the
foregroundNotificationId constructor argument to DownloadService.FOREGROUND_NOTIFICATION_ID_NONE. This method is not called for such services, meaning it can
be implemented to throw java.lang.UnsupportedOperationException
.
Parameters:
downloads: The current downloads.
notMetRequirements: Any requirements for downloads that are not currently met.
Returns:
The foreground notification to display.
protected final void
invalidateForegroundNotification()
Invalidates the current foreground notification and causes DownloadService.getForegroundNotification(List, int) to be invoked again if the service isn't stopped.
Source
/*
* 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 androidx.media3.exoplayer.offline;
import static androidx.media3.exoplayer.offline.Download.STOP_REASON_NONE;
import android.annotation.SuppressLint;
import android.app.Notification;
import android.app.NotificationManager;
import android.app.Service;
import android.content.Context;
import android.content.Intent;
import android.content.pm.ServiceInfo;
import android.os.Handler;
import android.os.IBinder;
import android.os.Looper;
import androidx.annotation.Nullable;
import androidx.annotation.StringRes;
import androidx.media3.common.util.Assertions;
import androidx.media3.common.util.Log;
import androidx.media3.common.util.NotificationUtil;
import androidx.media3.common.util.UnstableApi;
import androidx.media3.common.util.Util;
import androidx.media3.exoplayer.scheduler.Requirements;
import androidx.media3.exoplayer.scheduler.Requirements.RequirementFlags;
import androidx.media3.exoplayer.scheduler.Scheduler;
import java.util.HashMap;
import java.util.List;
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
import org.checkerframework.checker.nullness.qual.RequiresNonNull;
/**
* A {@link Service} for downloading media.
*
* <p>Apps with target SDK 33 and greater need to add the {@code
* android.permission.POST_NOTIFICATIONS} permission to the manifest and request the permission at
* runtime before starting downloads. Without that permission granted by the user, notifications
* posted by this service are not displayed. See <a
* href="https://developer.android.com/develop/ui/views/notifications/notification-permission">the
* official UI guide</a> for more detailed information.
*/
@UnstableApi
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 = "androidx.media3.exoplayer.downloadService.action.INIT";
/** Like {@link #ACTION_INIT}, but with {@link #KEY_FOREGROUND} implicitly set to true. */
private static final String ACTION_RESTART =
"androidx.media3.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 =
"androidx.media3.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 =
"androidx.media3.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 =
"androidx.media3.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 =
"androidx.media3.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 =
"androidx.media3.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 =
"androidx.media3.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 =
"androidx.media3.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";
// Maps each concrete DownloadService subclass to a single DownloadManagerHelper instance. This
// ensures getDownloadManager is only called once per subclass, even if a new instance of the
// service is created. The DownloadManagerHelper wrapper also takes care of restarting the service
// when there's no scheduler, and is often able to restart the service faster than the scheduler
// even when there is one.
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;
private @MonotonicNonNull DownloadManagerHelper downloadManagerHelper;
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, and no foreground notification will be displayed.
*
* <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);
}
/**
* 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);
}
/**
* Clear all {@linkplain DownloadManagerHelper download manager helpers} before restarting the
* service.
*
* <p>Calling this method is normally only required if an app supports downloading content for
* multiple users for which different download directories should be used.
*/
public static void clearDownloadManagerHelpers() {
downloadManagerHelpers.clear();
}
@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;
// See https://developer.android.com/about/versions/12/foreground-services.
boolean canStartForegroundServiceFromBackground = Util.SDK_INT < 31;
@Nullable
Scheduler scheduler =
foregroundAllowed && canStartForegroundServiceFromBackground ? getScheduler() : null;
DownloadManager downloadManager = getDownloadManager();
downloadManager.resumeDownloads();
downloadManagerHelper =
new DownloadManagerHelper(
getApplicationContext(), downloadManager, foregroundAllowed, scheduler, clazz);
downloadManagerHelpers.put(clazz, downloadManagerHelper);
}
this.downloadManagerHelper = downloadManagerHelper;
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(downloadManagerHelper).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()) {
onIdle();
}
return START_STICKY;
}
@Override
public void onTaskRemoved(Intent rootIntent) {
taskRemoved = true;
}
@Override
public void onDestroy() {
isDestroyed = true;
Assertions.checkNotNull(downloadManagerHelper).detachService(this);
if (foregroundNotificationUpdater != null) {
foregroundNotificationUpdater.stopPeriodicUpdates();
}
}
/**
* Throws {@link UnsupportedOperationException} because this service is not designed to be bound.
*/
@Override
@Nullable
public final IBinder onBind(Intent intent) {
throw new UnsupportedOperationException();
}
/**
* Returns a {@link DownloadManager} to be used to downloaded content. For each concrete download
* service subclass, this is called once in the lifecycle of the process when {@link #onCreate} is
* called on the first instance of the service. If the service is destroyed and a new instance is
* created later, the new instance will use the previously returned {@link DownloadManager}
* without this method being called again.
*/
protected abstract DownloadManager getDownloadManager();
/**
* Returns a {@link Scheduler} to restart the service when requirements for downloads to continue
* are met.
*
* <p>This method is not called on all devices or for all service configurations. When it is
* called, it's called only once in the life cycle of the process. If a service has unfinished
* downloads that cannot make progress due to unmet requirements, it will behave according to the
* first matching case below:
*
* <ul>
* <li>If the service has {@code foregroundNotificationId} set to {@link
* #FOREGROUND_NOTIFICATION_ID_NONE}, then this method will not be called. The service will
* remain in the background until the downloads are able to continue to completion or the
* service is killed by the platform.
* <li>If the device API level is less than 31, a {@link Scheduler} is returned from this
* method, and the returned {@link Scheduler} {@link Scheduler#getSupportedRequirements
* supports} all of the requirements that have been specified for downloads to continue,
* then the service will stop itself and the {@link Scheduler} will be used to restart it in
* the foreground when the requirements are met.
* <li>If the device API level is less than 31 and either {@code null} or a {@link Scheduler}
* that does not {@link Scheduler#getSupportedRequirements support} all of the requirements
* is returned from this method, then the service will remain in the foreground until the
* downloads are able to continue to completion.
* <li>If the device API level is 31 or above, then this method will not be called and the
* service will remain in the foreground until the downloads are able to continue to
* completion. A {@link Scheduler} cannot be used for this case due to <a
* href="https://developer.android.com/about/versions/12/foreground-services">Android 12
* foreground service launch restrictions</a>.
* <li>
* </ul>
*/
@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.
* @param notMetRequirements Any requirements for downloads that are not currently met.
* @return The foreground notification to display.
*/
protected abstract Notification getForegroundNotification(
List<Download> downloads, @RequirementFlags int notMetRequirements);
/**
* Invalidates the current foreground notification and causes {@link
* #getForegroundNotification(List, int)} to be invoked again if the service isn't stopped.
*/
protected final void invalidateForegroundNotification() {
if (foregroundNotificationUpdater != null && !isDestroyed) {
foregroundNotificationUpdater.invalidate();
}
}
/**
* 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.
*/
private void notifyDownloadChanged(Download download) {
if (foregroundNotificationUpdater != null) {
if (needsStartedService(download.state)) {
foregroundNotificationUpdater.startPeriodicUpdates();
} else {
foregroundNotificationUpdater.invalidate();
}
}
}
/** Called when a download is removed. */
private void notifyDownloadRemoved() {
if (foregroundNotificationUpdater != null) {
foregroundNotificationUpdater.invalidate();
}
}
/** Returns whether the service is stopped. */
private boolean isStopped() {
return isStopped;
}
private void onIdle() {
if (foregroundNotificationUpdater != null) {
// Whether the service remains started or not, we don't need periodic notification updates
// when the DownloadManager is idle.
foregroundNotificationUpdater.stopPeriodicUpdates();
}
if (!Assertions.checkNotNull(downloadManagerHelper).updateScheduler()) {
// We failed to schedule the service to restart when requirements that the DownloadManager is
// waiting for are met, so remain started.
return;
}
// Stop the service, either because the DownloadManager is not waiting for requirements to be
// met, or because we've scheduled the service to be restarted when they are.
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();
}
}
@SuppressLint("InlinedApi") // Using compile time constant FOREGROUND_SERVICE_TYPE_DATA_SYNC
private void update() {
DownloadManager downloadManager =
Assertions.checkNotNull(downloadManagerHelper).downloadManager;
List<Download> downloads = downloadManager.getCurrentDownloads();
@RequirementFlags int notMetRequirements = downloadManager.getNotMetRequirements();
Notification notification = getForegroundNotification(downloads, notMetRequirements);
if (!notificationDisplayed) {
Util.setForegroundServiceNotification(
/* service= */ DownloadService.this,
notificationId,
notification,
ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC,
"dataSync");
notificationDisplayed = true;
} else {
// Update the notification via NotificationManager rather than by repeatedly calling
// startForeground, since the latter can cause ActivityManager log spam.
((NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE))
.notify(notificationId, notification);
}
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 @MonotonicNonNull Requirements scheduledRequirements;
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.
Util.createHandlerForCurrentOrMainLooper()
.postAtFrontOfQueue(
() -> downloadService.notifyDownloads(downloadManager.getCurrentDownloads()));
}
}
public void detachService(DownloadService downloadService) {
Assertions.checkState(this.downloadService == downloadService);
this.downloadService = null;
}
/**
* Schedules or cancels restarting the service, as needed for the current state.
*
* @return True if the DownloadManager is not waiting for requirements, or if it is waiting for
* requirements and the service has been successfully scheduled to be restarted when they
* are met. False if the DownloadManager is waiting for requirements and the service has not
* been scheduled for restart.
*/
public boolean updateScheduler() {
boolean waitingForRequirements = downloadManager.isWaitingForRequirements();
if (scheduler == null) {
return !waitingForRequirements;
}
if (!waitingForRequirements) {
cancelScheduler();
return true;
}
Requirements requirements = downloadManager.getRequirements();
Requirements supportedRequirements = scheduler.getSupportedRequirements(requirements);
if (!supportedRequirements.equals(requirements)) {
cancelScheduler();
return false;
}
if (!schedulerNeedsUpdate(requirements)) {
return true;
}
String servicePackage = context.getPackageName();
if (scheduler.schedule(requirements, servicePackage, ACTION_RESTART)) {
scheduledRequirements = requirements;
return true;
} else {
Log.w(TAG, "Failed to schedule restart");
cancelScheduler();
return false;
}
}
// DownloadManager.Listener implementation.
@Override
public void onInitialized(DownloadManager downloadManager) {
if (downloadService != null) {
downloadService.notifyDownloads(downloadManager.getCurrentDownloads());
}
}
@Override
public void onDownloadChanged(
DownloadManager downloadManager, Download download, @Nullable Exception finalException) {
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();
}
}
@Override
public final void onIdle(DownloadManager downloadManager) {
if (downloadService != null) {
downloadService.onIdle();
}
}
@Override
public void onRequirementsStateChanged(
DownloadManager downloadManager,
Requirements requirements,
@RequirementFlags int notMetRequirements) {
updateScheduler();
}
@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();
return;
}
}
}
}
// Internal methods.
private boolean schedulerNeedsUpdate(Requirements requirements) {
return !Util.areEqual(scheduledRequirements, requirements);
}
@RequiresNonNull("scheduler")
private void cancelScheduler() {
Requirements canceledRequirements = new Requirements(/* requirements= */ 0);
if (schedulerNeedsUpdate(canceledRequirements)) {
scheduler.cancel();
scheduledRequirements = canceledRequirements;
}
}
private boolean serviceMayNeedRestart() {
return downloadService == null || downloadService.isStopped();
}
private void restartService() {
if (foregroundAllowed) {
try {
Intent intent = getIntent(context, serviceClass, DownloadService.ACTION_RESTART);
Util.startForegroundService(context, intent);
} catch (IllegalStateException e) {
// The process is running in the background, and is not allowed to start a foreground
// service due to foreground service launch restrictions
// (https://developer.android.com/about/versions/12/foreground-services).
Log.w(TAG, "Failed to restart (foreground launch restriction)");
}
} 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 (IllegalStateException 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 (process is idle)");
}
}
}
}
}