public abstract class

DownloadService

extends Service

 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

Fields
public static final java.lang.StringACTION_ADD_DOWNLOAD

Adds a new download.

public static final java.lang.StringACTION_INIT

Starts a download service to resume any ongoing downloads.

public static final java.lang.StringACTION_PAUSE_DOWNLOADS

Pauses all downloads.

public static final java.lang.StringACTION_REMOVE_ALL_DOWNLOADS

Removes all downloads.

public static final java.lang.StringACTION_REMOVE_DOWNLOAD

Removes a download.

public static final java.lang.StringACTION_RESUME_DOWNLOADS

Resumes all downloads except those that have a non-zero Download.stopReason.

public static final java.lang.StringACTION_SET_REQUIREMENTS

Sets the requirements that need to be met for downloads to progress.

public static final java.lang.StringACTION_SET_STOP_REASON

Sets the stop reason for one or all downloads.

public static final longDEFAULT_FOREGROUND_NOTIFICATION_UPDATE_INTERVAL

Default foreground notification update interval in milliseconds.

public static final intFOREGROUND_NOTIFICATION_ID_NONE

Invalid foreground notification id that can be used to run the service in the background.

public static final java.lang.StringKEY_CONTENT_ID

Key for the java.lang.String content id in DownloadService.ACTION_SET_STOP_REASON and DownloadService.ACTION_REMOVE_DOWNLOAD intents.

public static final java.lang.StringKEY_DOWNLOAD_REQUEST

Key for the DownloadRequest in DownloadService.ACTION_ADD_DOWNLOAD intents.

public static final java.lang.StringKEY_FOREGROUND

Key for a boolean extra that can be set on any intent to indicate whether the service was started in the foreground.

public static final java.lang.StringKEY_REQUIREMENTS

Key for the Requirements in DownloadService.ACTION_SET_REQUIREMENTS intents.

public static final java.lang.StringKEY_STOP_REASON

Key for the integer stop reason in DownloadService.ACTION_SET_STOP_REASON and DownloadService.ACTION_ADD_DOWNLOAD intents.

Constructors
protectedDownloadService(int foregroundNotificationId)

Creates a DownloadService.

protectedDownloadService(int foregroundNotificationId, long foregroundNotificationUpdateInterval)

Creates a DownloadService.

protectedDownloadService(int foregroundNotificationId, long foregroundNotificationUpdateInterval, java.lang.String channelId, int channelNameResourceId, int channelDescriptionResourceId)

Creates a DownloadService.

Methods
public static IntentbuildAddDownloadIntent(Context context, java.lang.Class<DownloadService> clazz, DownloadRequest downloadRequest, boolean foreground)

Builds an for adding a new download.

public static IntentbuildAddDownloadIntent(Context context, java.lang.Class<DownloadService> clazz, DownloadRequest downloadRequest, int stopReason, boolean foreground)

Builds an for adding a new download.

public static IntentbuildPauseDownloadsIntent(Context context, java.lang.Class<DownloadService> clazz, boolean foreground)

Builds an to pause all downloads.

public static IntentbuildRemoveAllDownloadsIntent(Context context, java.lang.Class<DownloadService> clazz, boolean foreground)

Builds an for removing all downloads.

public static IntentbuildRemoveDownloadIntent(Context context, java.lang.Class<DownloadService> clazz, java.lang.String id, boolean foreground)

Builds an for removing the download with the id.

public static IntentbuildResumeDownloadsIntent(Context context, java.lang.Class<DownloadService> clazz, boolean foreground)

Builds an for resuming all downloads.

public static IntentbuildSetRequirementsIntent(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 IntentbuildSetStopReasonIntent(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 voidclearDownloadManagerHelpers()

Clear all before restarting the service.

protected abstract DownloadManagergetDownloadManager()

Returns a DownloadManager to be used to downloaded content.

protected abstract NotificationgetForegroundNotification(java.util.List<Download> downloads, int notMetRequirements)

Returns a notification to be displayed when this service running in the foreground.

protected abstract SchedulergetScheduler()

Returns a Scheduler to restart the service when requirements for downloads to continue are met.

protected final voidinvalidateForegroundNotification()

Invalidates the current foreground notification and causes DownloadService.getForegroundNotification(List, int) to be invoked again if the service isn't stopped.

public final IBinderonBind(Intent intent)

Throws java.lang.UnsupportedOperationException because this service is not designed to be bound.

public voidonCreate()

public voidonDestroy()

public intonStartCommand(Intent intent, int flags, int startId)

public voidonTaskRemoved(Intent rootIntent)

public static voidsendAddDownload(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 voidsendAddDownload(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 voidsendPauseDownloads(Context context, java.lang.Class<DownloadService> clazz, boolean foreground)

Starts the service if not started already and pauses all downloads.

public static voidsendRemoveAllDownloads(Context context, java.lang.Class<DownloadService> clazz, boolean foreground)

Starts the service if not started already and removes all downloads.

public static voidsendRemoveDownload(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 voidsendResumeDownloads(Context context, java.lang.Class<DownloadService> clazz, boolean foreground)

Starts the service if not started already and resumes all downloads.

public static voidsendSetRequirements(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 voidsendSetStopReason(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 voidstart(Context context, java.lang.Class<DownloadService> clazz)

Starts a download service to resume any ongoing downloads.

public static voidstartForeground(Context context, java.lang.Class<DownloadService> clazz)

Starts the service in the foreground without adding a new download request.

from java.lang.Objectclone, equals, finalize, getClass, hashCode, notify, notifyAll, toString, wait, wait, wait

Fields

public static final java.lang.String ACTION_INIT

Starts a download service to resume any ongoing downloads. Extras:

public static final java.lang.String ACTION_ADD_DOWNLOAD

Adds a new download. Extras:

public static final java.lang.String ACTION_REMOVE_DOWNLOAD

Removes a download. Extras:

public static final java.lang.String ACTION_REMOVE_ALL_DOWNLOADS

Removes all downloads. Extras:

public static final java.lang.String ACTION_RESUME_DOWNLOADS

Resumes all downloads except those that have a non-zero Download.stopReason. Extras:

public static final java.lang.String ACTION_PAUSE_DOWNLOADS

Pauses all downloads. Extras:

public static final java.lang.String ACTION_SET_STOP_REASON

Sets 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_REQUIREMENTS

Sets the requirements that need to be met for downloads to progress. Extras:

public static final java.lang.String KEY_DOWNLOAD_REQUEST

Key for the DownloadRequest in DownloadService.ACTION_ADD_DOWNLOAD intents.

public static final java.lang.String KEY_CONTENT_ID

Key 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_REASON

Key for the integer stop reason in DownloadService.ACTION_SET_STOP_REASON and DownloadService.ACTION_ADD_DOWNLOAD intents.

public static final java.lang.String KEY_REQUIREMENTS

Key for the Requirements in DownloadService.ACTION_SET_REQUIREMENTS intents.

public static final java.lang.String KEY_FOREGROUND

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 DownloadService.

public static final int FOREGROUND_NOTIFICATION_ID_NONE

Invalid foreground notification id that can be used to run the service in the background.

public static final long DEFAULT_FOREGROUND_NOTIFICATION_UPDATE_INTERVAL

Default 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)

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)

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 void onCreate()

public int onStartCommand(Intent intent, int flags, int startId)

public void onTaskRemoved(Intent rootIntent)

public void onDestroy()

public final IBinder onBind(Intent intent)

Throws java.lang.UnsupportedOperationException because this service is not designed to be bound.

protected abstract DownloadManager getDownloadManager()

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.

protected abstract Scheduler getScheduler()

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)");
        }
      }
    }
  }
}