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:
olly 2021-09-24 14:52:32 +01:00 committed by Oliver Woodman
parent fecb8b7ec8
commit a720380e77
2 changed files with 147 additions and 70 deletions

View File

@ -14,6 +14,10 @@
* Move `Player.addListener(EventListener)` and * Move `Player.addListener(EventListener)` and
`Player.removeListener(EventListener)` out of `Player` into subclasses. `Player.removeListener(EventListener)` out of `Player` into subclasses.
* Android 12 compatibility: * 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. * Disable platform transcoding when playing content URIs on Android 12.
* Add `ExoPlayer.setVideoChangeFrameRateStrategy` to allow disabling of * Add `ExoPlayer.setVideoChangeFrameRateStrategy` to allow disabling of
calls from the player to `Surface.setFrameRate`. This is useful for calls from the player to `Surface.setFrameRate`. This is useful for
@ -23,6 +27,14 @@
* `SubtitleView` no longer implements `TextOutput`. `SubtitleView` * `SubtitleView` no longer implements `TextOutput`. `SubtitleView`
implements `Player.Listener`, so can be registered to a player with implements `Player.Listener`, so can be registered to a player with
`Player.addListener`. `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: * RTSP:
* Support RFC4566 SDP attribute field grammar * Support RFC4566 SDP attribute field grammar
([#9430](https://github.com/google/ExoPlayer/issues/9430)). ([#9430](https://github.com/google/ExoPlayer/issues/9430)).

View File

@ -28,6 +28,7 @@ import android.os.Looper;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.annotation.StringRes; import androidx.annotation.StringRes;
import com.google.android.exoplayer2.scheduler.Requirements; 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.scheduler.Scheduler;
import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.Log; 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.HashMap;
import java.util.List; import java.util.List;
import org.checkerframework.checker.nullness.qual.MonotonicNonNull; import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
import org.checkerframework.checker.nullness.qual.RequiresNonNull;
/** A {@link Service} for downloading media. */ /** A {@link Service} for downloading media. */
public abstract class DownloadService extends Service { 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 channelNameResourceId;
@StringRes private final int channelDescriptionResourceId; @StringRes private final int channelDescriptionResourceId;
private @MonotonicNonNull Scheduler scheduler; private @MonotonicNonNull DownloadManagerHelper downloadManagerHelper;
private @MonotonicNonNull DownloadManager downloadManager;
private int lastStartId; private int lastStartId;
private boolean startedInForeground; private boolean startedInForeground;
private boolean taskRemoved; private boolean taskRemoved;
@ -191,8 +192,7 @@ public abstract class DownloadService extends Service {
* Creates a DownloadService. * Creates a DownloadService.
* *
* <p>If {@code foregroundNotificationId} is {@link #FOREGROUND_NOTIFICATION_ID_NONE} then the * <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 * service will only ever run in the background, and no foreground notification will be displayed.
* {@link #getScheduler()} will not be called.
* *
* <p>If {@code foregroundNotificationId} is not {@link #FOREGROUND_NOTIFICATION_ID_NONE} then the * <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 * 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); @Nullable DownloadManagerHelper downloadManagerHelper = downloadManagerHelpers.get(clazz);
if (downloadManagerHelper == null) { if (downloadManagerHelper == null) {
boolean foregroundAllowed = foregroundNotificationUpdater != null; boolean foregroundAllowed = foregroundNotificationUpdater != null;
@Nullable Scheduler scheduler = foregroundAllowed ? getScheduler() : null; // See https://developer.android.com/about/versions/12/foreground-services.
if (scheduler != null) { boolean canStartForegroundServiceFromBackground = Util.SDK_INT < 31;
this.scheduler = scheduler; @Nullable
} Scheduler scheduler =
downloadManager = getDownloadManager(); foregroundAllowed && canStartForegroundServiceFromBackground ? getScheduler() : null;
DownloadManager downloadManager = getDownloadManager();
downloadManager.resumeDownloads(); downloadManager.resumeDownloads();
downloadManagerHelper = downloadManagerHelper =
new DownloadManagerHelper( new DownloadManagerHelper(
getApplicationContext(), downloadManager, foregroundAllowed, scheduler, clazz); getApplicationContext(), downloadManager, foregroundAllowed, scheduler, clazz);
downloadManagerHelpers.put(clazz, downloadManagerHelper); downloadManagerHelpers.put(clazz, downloadManagerHelper);
} else {
if (downloadManagerHelper.scheduler != null) {
scheduler = downloadManagerHelper.scheduler;
}
downloadManager = downloadManagerHelper.downloadManager;
} }
this.downloadManagerHelper = downloadManagerHelper;
downloadManagerHelper.attachService(this); downloadManagerHelper.attachService(this);
} }
@ -618,7 +615,8 @@ public abstract class DownloadService extends Service {
if (intentAction == null) { if (intentAction == null) {
intentAction = ACTION_INIT; intentAction = ACTION_INIT;
} }
DownloadManager downloadManager = Assertions.checkNotNull(this.downloadManager); DownloadManager downloadManager =
Assertions.checkNotNull(downloadManagerHelper).downloadManager;
switch (intentAction) { switch (intentAction) {
case ACTION_INIT: case ACTION_INIT:
case ACTION_RESTART: case ACTION_RESTART:
@ -666,21 +664,6 @@ public abstract class DownloadService extends Service {
if (requirements == null) { if (requirements == null) {
Log.e(TAG, "Ignored SET_REQUIREMENTS: Missing " + KEY_REQUIREMENTS + " extra"); Log.e(TAG, "Ignored SET_REQUIREMENTS: Missing " + KEY_REQUIREMENTS + " extra");
} else { } 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); downloadManager.setRequirements(requirements);
} }
break; break;
@ -696,7 +679,7 @@ public abstract class DownloadService extends Service {
isStopped = false; isStopped = false;
if (downloadManager.isIdle()) { if (downloadManager.isIdle()) {
stop(); onIdle();
} }
return START_STICKY; return START_STICKY;
} }
@ -709,9 +692,7 @@ public abstract class DownloadService extends Service {
@Override @Override
public void onDestroy() { public void onDestroy() {
isDestroyed = true; isDestroyed = true;
DownloadManagerHelper downloadManagerHelper = Assertions.checkNotNull(downloadManagerHelper).detachService(this);
Assertions.checkNotNull(downloadManagerHelpers.get(getClass()));
downloadManagerHelper.detachService(this);
if (foregroundNotificationUpdater != null) { if (foregroundNotificationUpdater != null) {
foregroundNotificationUpdater.stopPeriodicUpdates(); foregroundNotificationUpdater.stopPeriodicUpdates();
} }
@ -733,14 +714,35 @@ public abstract class DownloadService extends Service {
protected abstract DownloadManager getDownloadManager(); protected abstract DownloadManager getDownloadManager();
/** /**
* Returns a {@link Scheduler} to restart the service when requirements allowing downloads to take * Returns a {@link Scheduler} to restart the service when requirements for downloads to continue
* place are met. If {@code null}, the service will only be restarted if the process is still in * are met.
* memory when the requirements are met.
* *
* <p>This method is not called for services whose {@code foregroundNotificationId} is set to * <p>This method is not called on all devices or for all service configurations. When it is
* {@link #FOREGROUND_NOTIFICATION_ID_NONE}. Such services will only be restarted if the process * called, it's called only once in the life cycle of the process. If a service has unfinished
* is still in memory and considered non-idle, meaning that it's either in the foreground or was * downloads that cannot make progress due to unmet requirements, it will behave according to the
* backgrounded within the last few minutes. * 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 @Nullable
protected abstract Scheduler getScheduler(); protected abstract Scheduler getScheduler();
@ -758,7 +760,7 @@ public abstract class DownloadService extends Service {
* @return The foreground notification to display. * @return The foreground notification to display.
*/ */
protected abstract Notification getForegroundNotification( 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 * Invalidates the current foreground notification and causes {@link
@ -813,10 +815,21 @@ public abstract class DownloadService extends Service {
return isStopped; return isStopped;
} }
private void stop() { private void onIdle() {
if (foregroundNotificationUpdater != null) { if (foregroundNotificationUpdater != null) {
// Whether the service remains started or not, we don't need periodic notification updates
// when the DownloadManager is idle.
foregroundNotificationUpdater.stopPeriodicUpdates(); 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]. if (Util.SDK_INT < 28 && taskRemoved) { // See [Internal: b/74248644].
stopSelf(); stopSelf();
isStopped = true; isStopped = true;
@ -887,9 +900,10 @@ public abstract class DownloadService extends Service {
} }
private void update() { private void update() {
List<Download> downloads = Assertions.checkNotNull(downloadManager).getCurrentDownloads(); DownloadManager downloadManager =
@Requirements.RequirementFlags Assertions.checkNotNull(downloadManagerHelper).downloadManager;
int notMetRequirements = downloadManager.getNotMetRequirements(); List<Download> downloads = downloadManager.getCurrentDownloads();
@RequirementFlags int notMetRequirements = downloadManager.getNotMetRequirements();
Notification notification = getForegroundNotification(downloads, notMetRequirements); Notification notification = getForegroundNotification(downloads, notMetRequirements);
if (!notificationDisplayed) { if (!notificationDisplayed) {
startForeground(notificationId, notification); startForeground(notificationId, notification);
@ -914,7 +928,9 @@ public abstract class DownloadService extends Service {
private final boolean foregroundAllowed; private final boolean foregroundAllowed;
@Nullable private final Scheduler scheduler; @Nullable private final Scheduler scheduler;
private final Class<? extends DownloadService> serviceClass; private final Class<? extends DownloadService> serviceClass;
@Nullable private DownloadService downloadService; @Nullable private DownloadService downloadService;
private @MonotonicNonNull Requirements scheduledRequirements;
private DownloadManagerHelper( private DownloadManagerHelper(
Context context, Context context,
@ -949,8 +965,46 @@ public abstract class DownloadService extends Service {
public void detachService(DownloadService downloadService) { public void detachService(DownloadService downloadService) {
Assertions.checkState(this.downloadService == downloadService); Assertions.checkState(this.downloadService == downloadService);
this.downloadService = null; 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 @Override
public final void onIdle(DownloadManager downloadManager) { public final void onIdle(DownloadManager downloadManager) {
if (downloadService != null) { if (downloadService != null) {
downloadService.stop(); downloadService.onIdle();
} }
} }
@Override
public void onRequirementsStateChanged(
DownloadManager downloadManager,
Requirements requirements,
@RequirementFlags int notMetRequirements) {
updateScheduler();
}
@Override @Override
public void onWaitingForRequirementsChanged( public void onWaitingForRequirementsChanged(
DownloadManager downloadManager, boolean waitingForRequirements) { DownloadManager downloadManager, boolean waitingForRequirements) {
@ -1006,23 +1068,42 @@ public abstract class DownloadService extends Service {
for (int i = 0; i < downloads.size(); i++) { for (int i = 0; i < downloads.size(); i++) {
if (downloads.get(i).state == Download.STATE_QUEUED) { if (downloads.get(i).state == Download.STATE_QUEUED) {
restartService(); restartService();
break; return;
} }
} }
} }
updateScheduler();
} }
// Internal methods. // 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() { private boolean serviceMayNeedRestart() {
return downloadService == null || downloadService.isStopped(); return downloadService == null || downloadService.isStopped();
} }
private void restartService() { private void restartService() {
if (foregroundAllowed) { if (foregroundAllowed) {
Intent intent = getIntent(context, serviceClass, DownloadService.ACTION_RESTART); try {
Util.startForegroundService(context, intent); 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 { } else {
// The service is background only. Use ACTION_INIT rather than ACTION_RESTART because // 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. // ACTION_RESTART is handled as though KEY_FOREGROUND is set to true.
@ -1032,25 +1113,9 @@ public abstract class DownloadService extends Service {
} catch (IllegalStateException e) { } catch (IllegalStateException e) {
// The process is classed as idle by the platform. Starting a background service is not // The process is classed as idle by the platform. Starting a background service is not
// allowed in this state. // allowed in this state.
Log.w(TAG, "Failed to restart DownloadService (process is idle)."); Log.w(TAG, "Failed to restart (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();
}
}
} }
} }