Update DownloadService for Android 12
- If DownloadService is configured to run as a foreground service, it will remain started and in the foreground when downloads are waiting for requirements to be met, with a suitable "waiting for XYZ" message in the notification. This is necessary because new foreground service restrictions in Android 12 prevent to service from being restarted from the background. - Cases where requirements are not supported by the Scheduler will be handled in the same way, even on earlier versions of Android. So will cases where a Scheduler is not provided. - The Scheduler will still be used on earlier versions of Android where possible. Note: We could technically continue to use the old behavior on Android 12 in cases where the containing application still has a targetSdkVersion corresponding to Android 11 or earlier. However, in practice, there seems to be little value in doing this. PiperOrigin-RevId: 398720114
This commit is contained in:
parent
fecb8b7ec8
commit
a720380e77
@ -14,6 +14,10 @@
|
||||
* Move `Player.addListener(EventListener)` and
|
||||
`Player.removeListener(EventListener)` out of `Player` into subclasses.
|
||||
* Android 12 compatibility:
|
||||
* Keep `DownloadService` started and in the foreground whilst waiting for
|
||||
requirements to be met on Android 12. This is necessary due to new
|
||||
[foreground service launch restrictions](https://developer.android.com/about/versions/12/foreground-services).
|
||||
`DownloadService.getScheduler` will not be called on Android 12 devices.
|
||||
* Disable platform transcoding when playing content URIs on Android 12.
|
||||
* Add `ExoPlayer.setVideoChangeFrameRateStrategy` to allow disabling of
|
||||
calls from the player to `Surface.setFrameRate`. This is useful for
|
||||
@ -23,6 +27,14 @@
|
||||
* `SubtitleView` no longer implements `TextOutput`. `SubtitleView`
|
||||
implements `Player.Listener`, so can be registered to a player with
|
||||
`Player.addListener`.
|
||||
* Downloads and caching:
|
||||
* Modify `DownloadService` behavior when `DownloadService.getScheduler`
|
||||
returns `null`, or returns a `Scheduler` that does not support the
|
||||
requirements for downloads to continue. In both cases, `DownloadService`
|
||||
will now remain started and in the foreground whilst waiting for
|
||||
requirements to be met.
|
||||
* Modify `DownlaodService` behavior when running on Android 12 and above.
|
||||
See the "Android 12 compatibility" section above.
|
||||
* RTSP:
|
||||
* Support RFC4566 SDP attribute field grammar
|
||||
([#9430](https://github.com/google/ExoPlayer/issues/9430)).
|
||||
|
@ -28,6 +28,7 @@ import android.os.Looper;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.StringRes;
|
||||
import com.google.android.exoplayer2.scheduler.Requirements;
|
||||
import com.google.android.exoplayer2.scheduler.Requirements.RequirementFlags;
|
||||
import com.google.android.exoplayer2.scheduler.Scheduler;
|
||||
import com.google.android.exoplayer2.util.Assertions;
|
||||
import com.google.android.exoplayer2.util.Log;
|
||||
@ -36,6 +37,7 @@ import com.google.android.exoplayer2.util.Util;
|
||||
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. */
|
||||
public abstract class DownloadService extends Service {
|
||||
@ -179,8 +181,7 @@ public abstract class DownloadService extends Service {
|
||||
@StringRes private final int channelNameResourceId;
|
||||
@StringRes private final int channelDescriptionResourceId;
|
||||
|
||||
private @MonotonicNonNull Scheduler scheduler;
|
||||
private @MonotonicNonNull DownloadManager downloadManager;
|
||||
private @MonotonicNonNull DownloadManagerHelper downloadManagerHelper;
|
||||
private int lastStartId;
|
||||
private boolean startedInForeground;
|
||||
private boolean taskRemoved;
|
||||
@ -191,8 +192,7 @@ public abstract class DownloadService extends Service {
|
||||
* Creates a DownloadService.
|
||||
*
|
||||
* <p>If {@code foregroundNotificationId} is {@link #FOREGROUND_NOTIFICATION_ID_NONE} then the
|
||||
* service will only ever run in the background. No foreground notification will be displayed and
|
||||
* {@link #getScheduler()} will not be called.
|
||||
* 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
|
||||
@ -583,22 +583,19 @@ public abstract class DownloadService extends Service {
|
||||
@Nullable DownloadManagerHelper downloadManagerHelper = downloadManagerHelpers.get(clazz);
|
||||
if (downloadManagerHelper == null) {
|
||||
boolean foregroundAllowed = foregroundNotificationUpdater != null;
|
||||
@Nullable Scheduler scheduler = foregroundAllowed ? getScheduler() : null;
|
||||
if (scheduler != null) {
|
||||
this.scheduler = scheduler;
|
||||
}
|
||||
downloadManager = getDownloadManager();
|
||||
// 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);
|
||||
} else {
|
||||
if (downloadManagerHelper.scheduler != null) {
|
||||
scheduler = downloadManagerHelper.scheduler;
|
||||
}
|
||||
downloadManager = downloadManagerHelper.downloadManager;
|
||||
}
|
||||
this.downloadManagerHelper = downloadManagerHelper;
|
||||
downloadManagerHelper.attachService(this);
|
||||
}
|
||||
|
||||
@ -618,7 +615,8 @@ public abstract class DownloadService extends Service {
|
||||
if (intentAction == null) {
|
||||
intentAction = ACTION_INIT;
|
||||
}
|
||||
DownloadManager downloadManager = Assertions.checkNotNull(this.downloadManager);
|
||||
DownloadManager downloadManager =
|
||||
Assertions.checkNotNull(downloadManagerHelper).downloadManager;
|
||||
switch (intentAction) {
|
||||
case ACTION_INIT:
|
||||
case ACTION_RESTART:
|
||||
@ -666,21 +664,6 @@ public abstract class DownloadService extends Service {
|
||||
if (requirements == null) {
|
||||
Log.e(TAG, "Ignored SET_REQUIREMENTS: Missing " + KEY_REQUIREMENTS + " extra");
|
||||
} else {
|
||||
if (scheduler != null) {
|
||||
Requirements supportedRequirements = scheduler.getSupportedRequirements(requirements);
|
||||
if (!supportedRequirements.equals(requirements)) {
|
||||
Log.w(
|
||||
TAG,
|
||||
"Ignoring requirements not supported by the Scheduler: "
|
||||
+ (requirements.getRequirements() ^ supportedRequirements.getRequirements()));
|
||||
// We need to make sure DownloadManager only uses requirements supported by the
|
||||
// Scheduler. If we don't do this, DownloadManager can report itself as idle due to an
|
||||
// unmet requirement that the Scheduler doesn't support. This can then lead to the
|
||||
// service being destroyed, even though the Scheduler won't be able to restart it when
|
||||
// the requirement is subsequently met.
|
||||
requirements = supportedRequirements;
|
||||
}
|
||||
}
|
||||
downloadManager.setRequirements(requirements);
|
||||
}
|
||||
break;
|
||||
@ -696,7 +679,7 @@ public abstract class DownloadService extends Service {
|
||||
|
||||
isStopped = false;
|
||||
if (downloadManager.isIdle()) {
|
||||
stop();
|
||||
onIdle();
|
||||
}
|
||||
return START_STICKY;
|
||||
}
|
||||
@ -709,9 +692,7 @@ public abstract class DownloadService extends Service {
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
isDestroyed = true;
|
||||
DownloadManagerHelper downloadManagerHelper =
|
||||
Assertions.checkNotNull(downloadManagerHelpers.get(getClass()));
|
||||
downloadManagerHelper.detachService(this);
|
||||
Assertions.checkNotNull(downloadManagerHelper).detachService(this);
|
||||
if (foregroundNotificationUpdater != null) {
|
||||
foregroundNotificationUpdater.stopPeriodicUpdates();
|
||||
}
|
||||
@ -733,14 +714,35 @@ public abstract class DownloadService extends Service {
|
||||
protected abstract DownloadManager getDownloadManager();
|
||||
|
||||
/**
|
||||
* Returns a {@link Scheduler} to restart the service when requirements allowing downloads to take
|
||||
* place are met. If {@code null}, the service will only be restarted if the process is still in
|
||||
* memory when the requirements are met.
|
||||
* Returns a {@link Scheduler} to restart the service when requirements for downloads to continue
|
||||
* are met.
|
||||
*
|
||||
* <p>This method is not called for services whose {@code foregroundNotificationId} is set to
|
||||
* {@link #FOREGROUND_NOTIFICATION_ID_NONE}. Such services will only be restarted if the process
|
||||
* is still in memory and considered non-idle, meaning that it's either in the foreground or was
|
||||
* backgrounded within the last few minutes.
|
||||
* <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();
|
||||
@ -758,7 +760,7 @@ public abstract class DownloadService extends Service {
|
||||
* @return The foreground notification to display.
|
||||
*/
|
||||
protected abstract Notification getForegroundNotification(
|
||||
List<Download> downloads, @Requirements.RequirementFlags int notMetRequirements);
|
||||
List<Download> downloads, @RequirementFlags int notMetRequirements);
|
||||
|
||||
/**
|
||||
* Invalidates the current foreground notification and causes {@link
|
||||
@ -813,10 +815,21 @@ public abstract class DownloadService extends Service {
|
||||
return isStopped;
|
||||
}
|
||||
|
||||
private void stop() {
|
||||
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;
|
||||
@ -887,9 +900,10 @@ public abstract class DownloadService extends Service {
|
||||
}
|
||||
|
||||
private void update() {
|
||||
List<Download> downloads = Assertions.checkNotNull(downloadManager).getCurrentDownloads();
|
||||
@Requirements.RequirementFlags
|
||||
int notMetRequirements = downloadManager.getNotMetRequirements();
|
||||
DownloadManager downloadManager =
|
||||
Assertions.checkNotNull(downloadManagerHelper).downloadManager;
|
||||
List<Download> downloads = downloadManager.getCurrentDownloads();
|
||||
@RequirementFlags int notMetRequirements = downloadManager.getNotMetRequirements();
|
||||
Notification notification = getForegroundNotification(downloads, notMetRequirements);
|
||||
if (!notificationDisplayed) {
|
||||
startForeground(notificationId, notification);
|
||||
@ -914,7 +928,9 @@ public abstract class DownloadService extends Service {
|
||||
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,
|
||||
@ -949,8 +965,46 @@ public abstract class DownloadService extends Service {
|
||||
public void detachService(DownloadService downloadService) {
|
||||
Assertions.checkState(this.downloadService == downloadService);
|
||||
this.downloadService = null;
|
||||
if (scheduler != null && !downloadManager.isWaitingForRequirements()) {
|
||||
scheduler.cancel();
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
}
|
||||
|
||||
@ -989,10 +1043,18 @@ public abstract class DownloadService extends Service {
|
||||
@Override
|
||||
public final void onIdle(DownloadManager downloadManager) {
|
||||
if (downloadService != null) {
|
||||
downloadService.stop();
|
||||
downloadService.onIdle();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRequirementsStateChanged(
|
||||
DownloadManager downloadManager,
|
||||
Requirements requirements,
|
||||
@RequirementFlags int notMetRequirements) {
|
||||
updateScheduler();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onWaitingForRequirementsChanged(
|
||||
DownloadManager downloadManager, boolean waitingForRequirements) {
|
||||
@ -1006,23 +1068,42 @@ public abstract class DownloadService extends Service {
|
||||
for (int i = 0; i < downloads.size(); i++) {
|
||||
if (downloads.get(i).state == Download.STATE_QUEUED) {
|
||||
restartService();
|
||||
break;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
updateScheduler();
|
||||
}
|
||||
|
||||
// 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.
|
||||
@ -1032,24 +1113,8 @@ public abstract class DownloadService extends Service {
|
||||
} 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 DownloadService (process is idle).");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void updateScheduler() {
|
||||
if (scheduler == null) {
|
||||
return;
|
||||
}
|
||||
if (downloadManager.isWaitingForRequirements()) {
|
||||
String servicePackage = context.getPackageName();
|
||||
Requirements requirements = downloadManager.getRequirements();
|
||||
boolean success = scheduler.schedule(requirements, servicePackage, ACTION_RESTART);
|
||||
if (!success) {
|
||||
Log.e(TAG, "Scheduling downloads failed.");
|
||||
}
|
||||
} else {
|
||||
scheduler.cancel();
|
||||
Log.w(TAG, "Failed to restart (process is idle)");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user