Make it possible to disable DownloadService notifications

Issue: #4389

-------------
Created by MOE: https://github.com/google/moe
MOE_MIGRATED_REVID=204728270
This commit is contained in:
eguven 2018-07-16 05:59:01 -07:00 committed by Oliver Woodman
parent 8f0729b5ad
commit 7b2da629ea
4 changed files with 94 additions and 64 deletions

View File

@ -79,7 +79,7 @@
<service android:name="com.google.android.exoplayer2.demo.DemoDownloadService" <service android:name="com.google.android.exoplayer2.demo.DemoDownloadService"
android:exported="false"> android:exported="false">
<intent-filter> <intent-filter>
<action android:name="com.google.android.exoplayer.downloadService.action.INIT"/> <action android:name="com.google.android.exoplayer.downloadService.action.RESTART"/>
<category android:name="android.intent.category.DEFAULT"/> <category android:name="android.intent.category.DEFAULT"/>
</intent-filter> </intent-filter>
</service> </service>

View File

@ -52,17 +52,12 @@ public abstract class DownloadService extends Service {
private static final String ACTION_RESTART = private static final String ACTION_RESTART =
"com.google.android.exoplayer.downloadService.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. */ /** Key for the {@link DownloadAction} in an {@link #ACTION_ADD} intent. */
public static final String KEY_DOWNLOAD_ACTION = "download_action"; 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 * 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, * 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. // tasks the resume more quickly than when relying on the scheduler alone.
private static final HashMap<Class<? extends DownloadService>, RequirementsHelper> private static final HashMap<Class<? extends DownloadService>, RequirementsHelper>
requirementsHelpers = new HashMap<>(); 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 @Nullable String channelId;
private final @StringRes int channelName; private final @StringRes int channelName;
@ -93,16 +90,28 @@ public abstract class DownloadService extends Service {
private boolean taskRemoved; 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 * <p>If {@code foregroundNotificationId} is {@link #FOREGROUND_NOTIFICATION_ID_NONE} (value
* be 0. * {@value #FOREGROUND_NOTIFICATION_ID_NONE}) then the service runs in the background. No
* foreground notification is displayed and {@link #getScheduler()} isn't called.
*
* <p>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) { protected DownloadService(int foregroundNotificationId) {
this(foregroundNotificationId, DEFAULT_FOREGROUND_NOTIFICATION_UPDATE_INTERVAL); 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 * @param foregroundNotificationId The notification id for the foreground notification, must not
* be 0. * be 0.
* @param foregroundNotificationUpdateInterval The maximum interval to update foreground * @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 * @param foregroundNotificationId The notification id for the foreground notification. Must not
* be 0. * be 0.
* @param foregroundNotificationUpdateInterval The maximum interval between updates to the * @param foregroundNotificationUpdateInterval The maximum interval between updates to the
@ -135,7 +147,9 @@ public abstract class DownloadService extends Service {
@Nullable String channelId, @Nullable String channelId,
@StringRes int channelName) { @StringRes int channelName) {
foregroundNotificationUpdater = foregroundNotificationUpdater =
new ForegroundNotificationUpdater( foregroundNotificationId == 0
? null
: new ForegroundNotificationUpdater(
foregroundNotificationId, foregroundNotificationUpdateInterval); foregroundNotificationId, foregroundNotificationUpdateInterval);
this.channelId = channelId; this.channelId = channelId;
this.channelName = channelName; this.channelName = channelName;
@ -237,7 +251,7 @@ public abstract class DownloadService extends Service {
switch (intentAction) { switch (intentAction) {
case ACTION_INIT: case ACTION_INIT:
case ACTION_RESTART: case ACTION_RESTART:
// Do nothing. The RequirementsWatcher will start downloads when possible. // Do nothing.
break; break;
case ACTION_ADD: case ACTION_ADD:
byte[] actionData = intent.getByteArrayExtra(KEY_DOWNLOAD_ACTION); byte[] actionData = intent.getByteArrayExtra(KEY_DOWNLOAD_ACTION);
@ -251,21 +265,22 @@ public abstract class DownloadService extends Service {
} }
} }
break; break;
case ACTION_STOP_DOWNLOADS:
downloadManager.stopDownloads();
break;
case ACTION_START_DOWNLOADS:
downloadManager.startDownloads();
break;
case ACTION_RELOAD_REQUIREMENTS: case ACTION_RELOAD_REQUIREMENTS:
stopWatchingRequirements(); stopWatchingRequirements();
maybeStartWatchingRequirements();
break; break;
default: default:
Log.e(TAG, "Ignoring unrecognized action: " + intentAction); Log.e(TAG, "Ignoring unrecognized action: " + intentAction);
break; break;
} }
maybeStartWatchingRequirements();
Requirements requirements = getRequirements();
if (requirements.checkRequirements(this)) {
downloadManager.startDownloads();
} else {
downloadManager.stopDownloads();
}
maybeStartWatchingRequirements(requirements);
if (downloadManager.isIdle()) { if (downloadManager.isIdle()) {
stop(); stop();
} }
@ -281,7 +296,9 @@ public abstract class DownloadService extends Service {
@Override @Override
public void onDestroy() { public void onDestroy() {
logd("onDestroy"); logd("onDestroy");
if (foregroundNotificationUpdater != null) {
foregroundNotificationUpdater.stopPeriodicUpdates(); foregroundNotificationUpdater.stopPeriodicUpdates();
}
downloadManager.removeListener(downloadManagerListener); downloadManager.removeListener(downloadManagerListener);
maybeStopWatchingRequirements(); maybeStopWatchingRequirements();
} }
@ -312,11 +329,13 @@ public abstract class DownloadService extends Service {
* device has network connectivity. * device has network connectivity.
*/ */
protected Requirements getRequirements() { 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.
*
* <p>Returns a notification to be displayed when this service running in the foreground.
* *
* <p>This method is called when there is a task state change and periodically while there are * <p>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, * 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. * @param taskStates The states of all current tasks.
* @return The foreground notification to display. * @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. * Called when the state of a task changes.
@ -340,14 +363,14 @@ public abstract class DownloadService extends Service {
// Do nothing. // Do nothing.
} }
private void maybeStartWatchingRequirements() { private void maybeStartWatchingRequirements(Requirements requirements) {
if (downloadManager.getDownloadCount() == 0) { if (downloadManager.getDownloadCount() == 0) {
return; return;
} }
Class<? extends DownloadService> clazz = getClass(); Class<? extends DownloadService> clazz = getClass();
RequirementsHelper requirementsHelper = requirementsHelpers.get(clazz); RequirementsHelper requirementsHelper = requirementsHelpers.get(clazz);
if (requirementsHelper == null) { if (requirementsHelper == null) {
requirementsHelper = new RequirementsHelper(this, getRequirements(), getScheduler(), clazz); requirementsHelper = new RequirementsHelper(this, requirements, getScheduler(), clazz);
requirementsHelpers.put(clazz, requirementsHelper); requirementsHelpers.put(clazz, requirementsHelper);
requirementsHelper.start(); requirementsHelper.start();
logd("started watching requirements"); logd("started watching requirements");
@ -370,11 +393,13 @@ public abstract class DownloadService extends Service {
} }
private void stop() { private void stop() {
if (foregroundNotificationUpdater != null) {
foregroundNotificationUpdater.stopPeriodicUpdates(); foregroundNotificationUpdater.stopPeriodicUpdates();
// Make sure startForeground is called before stopping. Workaround for [Internal: b/69424260]. // Make sure startForeground is called before stopping. Workaround for [Internal: b/69424260].
if (startedInForeground && Util.SDK_INT >= 26) { if (startedInForeground && Util.SDK_INT >= 26) {
foregroundNotificationUpdater.showNotificationIfNotAlready(); foregroundNotificationUpdater.showNotificationIfNotAlready();
} }
}
if (Util.SDK_INT < 28 && taskRemoved) { // See [Internal: b/74248644]. if (Util.SDK_INT < 28 && taskRemoved) { // See [Internal: b/74248644].
stopSelf(); stopSelf();
logd("stopSelf()"); logd("stopSelf()");
@ -398,18 +423,20 @@ public abstract class DownloadService extends Service {
private final class DownloadManagerListener implements DownloadManager.Listener { private final class DownloadManagerListener implements DownloadManager.Listener {
@Override @Override
public void onInitialized(DownloadManager downloadManager) { public void onInitialized(DownloadManager downloadManager) {
maybeStartWatchingRequirements(); maybeStartWatchingRequirements(getRequirements());
} }
@Override @Override
public void onTaskStateChanged(DownloadManager downloadManager, TaskState taskState) { public void onTaskStateChanged(DownloadManager downloadManager, TaskState taskState) {
DownloadService.this.onTaskStateChanged(taskState); DownloadService.this.onTaskStateChanged(taskState);
if (foregroundNotificationUpdater != null) {
if (taskState.state == TaskState.STATE_STARTED) { if (taskState.state == TaskState.STATE_STARTED) {
foregroundNotificationUpdater.startPeriodicUpdates(); foregroundNotificationUpdater.startPeriodicUpdates();
} else { } else {
foregroundNotificationUpdater.update(); foregroundNotificationUpdater.update();
} }
} }
}
@Override @Override
public final void onIdle(DownloadManager downloadManager) { public final void onIdle(DownloadManager downloadManager) {
@ -497,7 +524,12 @@ public abstract class DownloadService extends Service {
@Override @Override
public void requirementsMet(RequirementsWatcher requirementsWatcher) { 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) { if (scheduler != null) {
scheduler.cancel(); scheduler.cancel();
} }
@ -505,7 +537,11 @@ public abstract class DownloadService extends Service {
@Override @Override
public void requirementsNotMet(RequirementsWatcher requirementsWatcher) { 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) { if (scheduler != null) {
String servicePackage = context.getPackageName(); String servicePackage = context.getPackageName();
boolean success = scheduler.schedule(requirements, servicePackage, ACTION_RESTART); boolean success = scheduler.schedule(requirements, servicePackage, ACTION_RESTART);
@ -515,9 +551,14 @@ public abstract class DownloadService extends Service {
} }
} }
private void startServiceWithAction(String action) { private void notifyService() throws Exception {
Intent intent = getIntent(context, serviceClass, action).putExtra(KEY_FOREGROUND, true); Intent intent = getIntent(context, serviceClass, DownloadService.ACTION_INIT);
Util.startForegroundService(context, intent); 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);
}
} }
} }
} }

View File

@ -87,7 +87,7 @@ public final class RequirementsWatcher {
public void start() { public void start() {
Assertions.checkNotNull(Looper.myLooper()); Assertions.checkNotNull(Looper.myLooper());
checkRequirements(true); requirementsWereMet = requirements.checkRequirements(context);
IntentFilter filter = new IntentFilter(); IntentFilter filter = new IntentFilter();
if (requirements.getRequiredNetworkType() != Requirements.NETWORK_TYPE_NONE) { if (requirements.getRequiredNetworkType() != Requirements.NETWORK_TYPE_NONE) {
@ -158,14 +158,12 @@ public final class RequirementsWatcher {
} }
} }
private void checkRequirements(boolean force) { private void checkRequirements() {
boolean requirementsAreMet = requirements.checkRequirements(context); boolean requirementsAreMet = requirements.checkRequirements(context);
if (!force) {
if (requirementsAreMet == requirementsWereMet) { if (requirementsAreMet == requirementsWereMet) {
logd("requirementsAreMet is still " + requirementsAreMet); logd("requirementsAreMet is still " + requirementsAreMet);
return; return;
} }
}
requirementsWereMet = requirementsAreMet; requirementsWereMet = requirementsAreMet;
if (requirementsAreMet) { if (requirementsAreMet) {
logd("start job"); logd("start job");
@ -187,7 +185,7 @@ public final class RequirementsWatcher {
public void onReceive(Context context, Intent intent) { public void onReceive(Context context, Intent intent) {
if (!isInitialStickyBroadcast()) { if (!isInitialStickyBroadcast()) {
logd(RequirementsWatcher.this + " received " + intent.getAction()); logd(RequirementsWatcher.this + " received " + intent.getAction());
checkRequirements(false); checkRequirements();
} }
} }
} }
@ -198,14 +196,14 @@ public final class RequirementsWatcher {
public void onAvailable(Network network) { public void onAvailable(Network network) {
super.onAvailable(network); super.onAvailable(network);
logd(RequirementsWatcher.this + " NetworkCallback.onAvailable"); logd(RequirementsWatcher.this + " NetworkCallback.onAvailable");
checkRequirements(false); checkRequirements();
} }
@Override @Override
public void onLost(Network network) { public void onLost(Network network) {
super.onLost(network); super.onLost(network);
logd(RequirementsWatcher.this + " NetworkCallback.onLost"); logd(RequirementsWatcher.this + " NetworkCallback.onLost");
checkRequirements(false); checkRequirements();
} }
} }
} }

View File

@ -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.assertCacheEmpty;
import static com.google.android.exoplayer2.testutil.CacheAsserts.assertCachedData; import static com.google.android.exoplayer2.testutil.CacheAsserts.assertCachedData;
import android.app.Notification;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.net.Uri; import android.net.Uri;
import android.support.annotation.Nullable; import android.support.annotation.Nullable;
import com.google.android.exoplayer2.offline.DownloadAction; import com.google.android.exoplayer2.offline.DownloadAction;
import com.google.android.exoplayer2.offline.DownloadManager; 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.DownloadService;
import com.google.android.exoplayer2.offline.DownloaderConstructorHelper; import com.google.android.exoplayer2.offline.DownloaderConstructorHelper;
import com.google.android.exoplayer2.offline.StreamKey; import com.google.android.exoplayer2.offline.StreamKey;
@ -52,7 +50,6 @@ import org.junit.Before;
import org.junit.Ignore; import org.junit.Ignore;
import org.junit.Test; import org.junit.Test;
import org.junit.runner.RunWith; import org.junit.runner.RunWith;
import org.mockito.Mockito;
import org.robolectric.RobolectricTestRunner; import org.robolectric.RobolectricTestRunner;
import org.robolectric.RuntimeEnvironment; import org.robolectric.RuntimeEnvironment;
import org.robolectric.annotation.Config; import org.robolectric.annotation.Config;
@ -135,18 +132,12 @@ public class DownloadServiceDashTest {
dashDownloadManager.startDownloads(); dashDownloadManager.startDownloads();
dashDownloadService = dashDownloadService =
new DownloadService(/*foregroundNotificationId=*/ 1) { new DownloadService(DownloadService.FOREGROUND_NOTIFICATION_ID_NONE) {
@Override @Override
protected DownloadManager getDownloadManager() { protected DownloadManager getDownloadManager() {
return dashDownloadManager; return dashDownloadManager;
} }
@Override
protected Notification getForegroundNotification(TaskState[] taskStates) {
return Mockito.mock(Notification.class);
}
@Nullable @Nullable
@Override @Override
protected Scheduler getScheduler() { protected Scheduler getScheduler() {