diff --git a/demos/main/src/main/AndroidManifest.xml b/demos/main/src/main/AndroidManifest.xml index 2232a8b3eb..2234048ac1 100644 --- a/demos/main/src/main/AndroidManifest.xml +++ b/demos/main/src/main/AndroidManifest.xml @@ -79,7 +79,7 @@ - + diff --git a/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadService.java b/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadService.java index 61d72c3dae..f7ca793b22 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadService.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadService.java @@ -52,17 +52,12 @@ public abstract class DownloadService extends Service { private static final String ACTION_RESTART = "com.google.android.exoplayer.downloadService.action.RESTART"; - /** Starts download tasks. */ - private static final String ACTION_START_DOWNLOADS = - "com.google.android.exoplayer.downloadService.action.START_DOWNLOADS"; - - /** Stops download tasks. */ - private static final String ACTION_STOP_DOWNLOADS = - "com.google.android.exoplayer.downloadService.action.STOP_DOWNLOADS"; - /** Key for the {@link DownloadAction} in an {@link #ACTION_ADD} intent. */ public static final String KEY_DOWNLOAD_ACTION = "download_action"; + /** Invalid foreground notification id which can be used to run the service in the background. */ + public static final int FOREGROUND_NOTIFICATION_ID_NONE = 0; + /** * Key for a boolean flag in any intent to indicate whether the service was started in the * foreground. If set, the service is guaranteed to call {@link #startForeground(int, @@ -81,8 +76,10 @@ public abstract class DownloadService extends Service { // tasks the resume more quickly than when relying on the scheduler alone. private static final HashMap, RequirementsHelper> requirementsHelpers = new HashMap<>(); + private static final Requirements DEFAULT_REQUIREMENTS = + new Requirements(Requirements.NETWORK_TYPE_ANY, false, false); - private final ForegroundNotificationUpdater foregroundNotificationUpdater; + private final @Nullable ForegroundNotificationUpdater foregroundNotificationUpdater; private final @Nullable String channelId; private final @StringRes int channelName; @@ -93,16 +90,28 @@ public abstract class DownloadService extends Service { private boolean taskRemoved; /** - * Creates a DownloadService with {@link #DEFAULT_FOREGROUND_NOTIFICATION_UPDATE_INTERVAL}. + * Creates a DownloadService. * - * @param foregroundNotificationId The notification id for the foreground notification, must not - * be 0. + *

If {@code foregroundNotificationId} is {@link #FOREGROUND_NOTIFICATION_ID_NONE} (value + * {@value #FOREGROUND_NOTIFICATION_ID_NONE}) then the service runs in the background. No + * foreground notification is displayed and {@link #getScheduler()} isn't called. + * + *

If {@code foregroundNotificationId} isn't {@link #FOREGROUND_NOTIFICATION_ID_NONE} (value + * {@value #FOREGROUND_NOTIFICATION_ID_NONE}) the service runs in the foreground with {@link + * #DEFAULT_FOREGROUND_NOTIFICATION_UPDATE_INTERVAL}. In that case {@link + * #getForegroundNotification(TaskState[])} should be overridden in the subclass. + * + * @param foregroundNotificationId The notification id for the foreground notification, or {@link + * #FOREGROUND_NOTIFICATION_ID_NONE} (value {@value #FOREGROUND_NOTIFICATION_ID_NONE}) */ protected DownloadService(int foregroundNotificationId) { this(foregroundNotificationId, DEFAULT_FOREGROUND_NOTIFICATION_UPDATE_INTERVAL); } /** + * Creates a DownloadService which will run in the foreground. {@link + * #getForegroundNotification(TaskState[])} should be overridden in the subclass. + * * @param foregroundNotificationId The notification id for the foreground notification, must not * be 0. * @param foregroundNotificationUpdateInterval The maximum interval to update foreground @@ -118,6 +127,9 @@ public abstract class DownloadService extends Service { } /** + * Creates a DownloadService which will run in the foreground. {@link + * #getForegroundNotification(TaskState[])} should be overridden in the subclass. + * * @param foregroundNotificationId The notification id for the foreground notification. Must not * be 0. * @param foregroundNotificationUpdateInterval The maximum interval between updates to the @@ -135,8 +147,10 @@ public abstract class DownloadService extends Service { @Nullable String channelId, @StringRes int channelName) { foregroundNotificationUpdater = - new ForegroundNotificationUpdater( - foregroundNotificationId, foregroundNotificationUpdateInterval); + foregroundNotificationId == 0 + ? null + : new ForegroundNotificationUpdater( + foregroundNotificationId, foregroundNotificationUpdateInterval); this.channelId = channelId; this.channelName = channelName; } @@ -237,7 +251,7 @@ public abstract class DownloadService extends Service { switch (intentAction) { case ACTION_INIT: case ACTION_RESTART: - // Do nothing. The RequirementsWatcher will start downloads when possible. + // Do nothing. break; case ACTION_ADD: byte[] actionData = intent.getByteArrayExtra(KEY_DOWNLOAD_ACTION); @@ -251,21 +265,22 @@ public abstract class DownloadService extends Service { } } break; - case ACTION_STOP_DOWNLOADS: - downloadManager.stopDownloads(); - break; - case ACTION_START_DOWNLOADS: - downloadManager.startDownloads(); - break; case ACTION_RELOAD_REQUIREMENTS: stopWatchingRequirements(); - maybeStartWatchingRequirements(); break; default: Log.e(TAG, "Ignoring unrecognized action: " + intentAction); break; } - maybeStartWatchingRequirements(); + + Requirements requirements = getRequirements(); + if (requirements.checkRequirements(this)) { + downloadManager.startDownloads(); + } else { + downloadManager.stopDownloads(); + } + maybeStartWatchingRequirements(requirements); + if (downloadManager.isIdle()) { stop(); } @@ -281,7 +296,9 @@ public abstract class DownloadService extends Service { @Override public void onDestroy() { logd("onDestroy"); - foregroundNotificationUpdater.stopPeriodicUpdates(); + if (foregroundNotificationUpdater != null) { + foregroundNotificationUpdater.stopPeriodicUpdates(); + } downloadManager.removeListener(downloadManagerListener); maybeStopWatchingRequirements(); } @@ -312,11 +329,13 @@ public abstract class DownloadService extends Service { * device has network connectivity. */ protected Requirements getRequirements() { - return new Requirements(Requirements.NETWORK_TYPE_ANY, false, false); + return DEFAULT_REQUIREMENTS; } /** - * Returns a notification to be displayed when this service running in the foreground. + * Should be overridden in the subclass if the service will be run in the foreground. + * + *

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

This method is called when there is a task state change and periodically while there are * active tasks. The periodic update interval can be set using {@link #DownloadService(int, @@ -329,7 +348,11 @@ public abstract class DownloadService extends Service { * @param taskStates The states of all current tasks. * @return The foreground notification to display. */ - protected abstract Notification getForegroundNotification(TaskState[] taskStates); + protected Notification getForegroundNotification(TaskState[] taskStates) { + throw new IllegalStateException( + getClass().getName() + + " is started in the foreground but getForegroundNotification() is not implemented."); + } /** * Called when the state of a task changes. @@ -340,14 +363,14 @@ public abstract class DownloadService extends Service { // Do nothing. } - private void maybeStartWatchingRequirements() { + private void maybeStartWatchingRequirements(Requirements requirements) { if (downloadManager.getDownloadCount() == 0) { return; } Class clazz = getClass(); RequirementsHelper requirementsHelper = requirementsHelpers.get(clazz); if (requirementsHelper == null) { - requirementsHelper = new RequirementsHelper(this, getRequirements(), getScheduler(), clazz); + requirementsHelper = new RequirementsHelper(this, requirements, getScheduler(), clazz); requirementsHelpers.put(clazz, requirementsHelper); requirementsHelper.start(); logd("started watching requirements"); @@ -370,10 +393,12 @@ public abstract class DownloadService extends Service { } private void stop() { - foregroundNotificationUpdater.stopPeriodicUpdates(); - // Make sure startForeground is called before stopping. Workaround for [Internal: b/69424260]. - if (startedInForeground && Util.SDK_INT >= 26) { - foregroundNotificationUpdater.showNotificationIfNotAlready(); + if (foregroundNotificationUpdater != null) { + foregroundNotificationUpdater.stopPeriodicUpdates(); + // Make sure startForeground is called before stopping. Workaround for [Internal: b/69424260]. + if (startedInForeground && Util.SDK_INT >= 26) { + foregroundNotificationUpdater.showNotificationIfNotAlready(); + } } if (Util.SDK_INT < 28 && taskRemoved) { // See [Internal: b/74248644]. stopSelf(); @@ -398,16 +423,18 @@ public abstract class DownloadService extends Service { private final class DownloadManagerListener implements DownloadManager.Listener { @Override public void onInitialized(DownloadManager downloadManager) { - maybeStartWatchingRequirements(); + maybeStartWatchingRequirements(getRequirements()); } @Override public void onTaskStateChanged(DownloadManager downloadManager, TaskState taskState) { DownloadService.this.onTaskStateChanged(taskState); - if (taskState.state == TaskState.STATE_STARTED) { - foregroundNotificationUpdater.startPeriodicUpdates(); - } else { - foregroundNotificationUpdater.update(); + if (foregroundNotificationUpdater != null) { + if (taskState.state == TaskState.STATE_STARTED) { + foregroundNotificationUpdater.startPeriodicUpdates(); + } else { + foregroundNotificationUpdater.update(); + } } } @@ -497,7 +524,12 @@ public abstract class DownloadService extends Service { @Override public void requirementsMet(RequirementsWatcher requirementsWatcher) { - startServiceWithAction(DownloadService.ACTION_START_DOWNLOADS); + try { + notifyService(); + } catch (Exception e) { + /* If we can't notify the service, don't stop the scheduler. */ + return; + } if (scheduler != null) { scheduler.cancel(); } @@ -505,7 +537,11 @@ public abstract class DownloadService extends Service { @Override public void requirementsNotMet(RequirementsWatcher requirementsWatcher) { - startServiceWithAction(DownloadService.ACTION_STOP_DOWNLOADS); + try { + notifyService(); + } catch (Exception e) { + /* Do nothing. The service isn't running anyway. */ + } if (scheduler != null) { String servicePackage = context.getPackageName(); boolean success = scheduler.schedule(requirements, servicePackage, ACTION_RESTART); @@ -515,9 +551,14 @@ public abstract class DownloadService extends Service { } } - private void startServiceWithAction(String action) { - Intent intent = getIntent(context, serviceClass, action).putExtra(KEY_FOREGROUND, true); - Util.startForegroundService(context, intent); + private void notifyService() throws Exception { + Intent intent = getIntent(context, serviceClass, DownloadService.ACTION_INIT); + try { + context.startService(intent); + } catch (IllegalStateException e) { + /* startService will fail if the app is in the background and the service isn't running. */ + throw new Exception(e); + } } } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/scheduler/RequirementsWatcher.java b/library/core/src/main/java/com/google/android/exoplayer2/scheduler/RequirementsWatcher.java index 46aa55f094..acd88182f9 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/scheduler/RequirementsWatcher.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/scheduler/RequirementsWatcher.java @@ -87,7 +87,7 @@ public final class RequirementsWatcher { public void start() { Assertions.checkNotNull(Looper.myLooper()); - checkRequirements(true); + requirementsWereMet = requirements.checkRequirements(context); IntentFilter filter = new IntentFilter(); if (requirements.getRequiredNetworkType() != Requirements.NETWORK_TYPE_NONE) { @@ -158,13 +158,11 @@ public final class RequirementsWatcher { } } - private void checkRequirements(boolean force) { + private void checkRequirements() { boolean requirementsAreMet = requirements.checkRequirements(context); - if (!force) { - if (requirementsAreMet == requirementsWereMet) { - logd("requirementsAreMet is still " + requirementsAreMet); - return; - } + if (requirementsAreMet == requirementsWereMet) { + logd("requirementsAreMet is still " + requirementsAreMet); + return; } requirementsWereMet = requirementsAreMet; if (requirementsAreMet) { @@ -187,7 +185,7 @@ public final class RequirementsWatcher { public void onReceive(Context context, Intent intent) { if (!isInitialStickyBroadcast()) { logd(RequirementsWatcher.this + " received " + intent.getAction()); - checkRequirements(false); + checkRequirements(); } } } @@ -198,14 +196,14 @@ public final class RequirementsWatcher { public void onAvailable(Network network) { super.onAvailable(network); logd(RequirementsWatcher.this + " NetworkCallback.onAvailable"); - checkRequirements(false); + checkRequirements(); } @Override public void onLost(Network network) { super.onLost(network); logd(RequirementsWatcher.this + " NetworkCallback.onLost"); - checkRequirements(false); + checkRequirements(); } } } diff --git a/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/offline/DownloadServiceDashTest.java b/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/offline/DownloadServiceDashTest.java index 4553eaf5c4..c0f48857c2 100644 --- a/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/offline/DownloadServiceDashTest.java +++ b/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/offline/DownloadServiceDashTest.java @@ -20,14 +20,12 @@ import static com.google.android.exoplayer2.source.dash.offline.DashDownloadTest import static com.google.android.exoplayer2.testutil.CacheAsserts.assertCacheEmpty; import static com.google.android.exoplayer2.testutil.CacheAsserts.assertCachedData; -import android.app.Notification; import android.content.Context; import android.content.Intent; import android.net.Uri; import android.support.annotation.Nullable; import com.google.android.exoplayer2.offline.DownloadAction; import com.google.android.exoplayer2.offline.DownloadManager; -import com.google.android.exoplayer2.offline.DownloadManager.TaskState; import com.google.android.exoplayer2.offline.DownloadService; import com.google.android.exoplayer2.offline.DownloaderConstructorHelper; import com.google.android.exoplayer2.offline.StreamKey; @@ -52,7 +50,6 @@ import org.junit.Before; import org.junit.Ignore; import org.junit.Test; import org.junit.runner.RunWith; -import org.mockito.Mockito; import org.robolectric.RobolectricTestRunner; import org.robolectric.RuntimeEnvironment; import org.robolectric.annotation.Config; @@ -135,18 +132,12 @@ public class DownloadServiceDashTest { dashDownloadManager.startDownloads(); dashDownloadService = - new DownloadService(/*foregroundNotificationId=*/ 1) { - + new DownloadService(DownloadService.FOREGROUND_NOTIFICATION_ID_NONE) { @Override protected DownloadManager getDownloadManager() { return dashDownloadManager; } - @Override - protected Notification getForegroundNotification(TaskState[] taskStates) { - return Mockito.mock(Notification.class); - } - @Nullable @Override protected Scheduler getScheduler() {