Merge changes I8415d8dc,I091a5fb9 into androidx-master-dev
* changes:
Use the union of foreground service types when multiple Workers are running in the context of a Foreground service.
Allow multiple notifications from Workers running in the context of a Foreground service.
diff --git a/work/integration-tests/testapp/src/main/java/androidx/work/integration/testapp/ForegroundWorker.kt b/work/integration-tests/testapp/src/main/java/androidx/work/integration/testapp/ForegroundWorker.kt
index 030e07a..1bc08db 100644
--- a/work/integration-tests/testapp/src/main/java/androidx/work/integration/testapp/ForegroundWorker.kt
+++ b/work/integration-tests/testapp/src/main/java/androidx/work/integration/testapp/ForegroundWorker.kt
@@ -39,20 +39,21 @@
private var progress: Data = Data.EMPTY
override suspend fun doWork(): Result {
+ val notificationId = inputData.getInt(InputNotificationId, NotificationId)
+ val delayTime = inputData.getLong(InputDelayTime, Delay)
// Run in the context of a Foreground service
- setForeground(getNotification())
-
+ setForeground(getForegroundInfo(notificationId))
val range = 20
for (i in 1..range) {
- delay(1000)
+ delay(delayTime)
progress = workDataOf(Progress to i * (100 / range))
setProgress(progress)
- setForeground(getNotification())
+ setForeground(getForegroundInfo(notificationId))
}
return Result.success()
}
- private fun getNotification(): ForegroundInfo {
+ private fun getForegroundInfo(notificationId: Int): ForegroundInfo {
val percent = progress.getInt(Progress, 0)
val id = applicationContext.getString(R.string.channel_id)
val title = applicationContext.getString(R.string.notification_title)
@@ -69,7 +70,7 @@
.setOngoing(true)
.build()
- return ForegroundInfo(NotificationId, notification)
+ return ForegroundInfo(notificationId, notification)
}
@RequiresApi(Build.VERSION_CODES.O)
@@ -85,6 +86,9 @@
companion object {
private const val NotificationId = 10
+ private const val Delay = 1000L
private const val Progress = "Progress"
+ const val InputNotificationId = "NotificationId"
+ const val InputDelayTime = "DelayTime"
}
}
diff --git a/work/integration-tests/testapp/src/main/java/androidx/work/integration/testapp/MainActivity.java b/work/integration-tests/testapp/src/main/java/androidx/work/integration/testapp/MainActivity.java
index 1d789de..63e6858 100644
--- a/work/integration-tests/testapp/src/main/java/androidx/work/integration/testapp/MainActivity.java
+++ b/work/integration-tests/testapp/src/main/java/androidx/work/integration/testapp/MainActivity.java
@@ -67,6 +67,7 @@
// Synthetic access
WorkRequest mLastForegroundWorkRequest;
+ int mLastNotificationId = 10;
@Override
protected void onCreate(Bundle savedInstanceState) {
@@ -364,8 +365,15 @@
findViewById(R.id.run_foreground_worker).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
+ mLastNotificationId += 1;
+
+ Data inputData = new Data.Builder()
+ .putInt(ForegroundWorker.InputNotificationId, mLastNotificationId)
+ .build();
+
OneTimeWorkRequest request =
new OneTimeWorkRequest.Builder(ForegroundWorker.class)
+ .setInputData(inputData)
.setConstraints(new Constraints.Builder()
.setRequiredNetworkType(NetworkType.CONNECTED).build()
).build();
@@ -378,8 +386,14 @@
findViewById(R.id.cancel_foreground_worker).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
- WorkManager.getInstance(MainActivity.this)
- .cancelAllWorkByTag(ForegroundWorker.class.getName());
+ if (mLastForegroundWorkRequest != null) {
+ WorkManager.getInstance(MainActivity.this)
+ .cancelWorkById(mLastForegroundWorkRequest.getId());
+ mLastForegroundWorkRequest = null;
+ } else {
+ WorkManager.getInstance(MainActivity.this)
+ .cancelAllWorkByTag(ForegroundWorker.class.getName());
+ }
}
});
@@ -396,6 +410,7 @@
try {
pendingIntent.send(0);
+ mLastForegroundWorkRequest = null;
} catch (PendingIntent.CanceledException exception) {
Log.e(TAG, "Pending intent was cancelled.", exception);
}
diff --git a/work/workmanager/src/androidTest/java/androidx/work/impl/foreground/SystemForegroundDispatcherTest.kt b/work/workmanager/src/androidTest/java/androidx/work/impl/foreground/SystemForegroundDispatcherTest.kt
index fdcc604..56359de 100644
--- a/work/workmanager/src/androidTest/java/androidx/work/impl/foreground/SystemForegroundDispatcherTest.kt
+++ b/work/workmanager/src/androidTest/java/androidx/work/impl/foreground/SystemForegroundDispatcherTest.kt
@@ -18,10 +18,13 @@
import android.app.Notification
import android.content.Context
+import android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_CONNECTED_DEVICE
+import android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_LOCATION
import android.util.Log
import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.MediumTest
+import androidx.test.filters.SdkSuppress
import androidx.work.Configuration
import androidx.work.Constraints
import androidx.work.ForegroundInfo
@@ -131,7 +134,7 @@
}
@Test
- fun testHandleNotify() {
+ fun testStartForeground() {
val workSpecId = "workSpecId"
val notificationId = 1
val notification = mock(Notification::class.java)
@@ -139,7 +142,139 @@
val intent = createNotifyIntent(context, workSpecId, metadata)
dispatcher.onStartCommand(intent)
verify(dispatcherCallback, times(1))
- .notify(eq(notificationId), eq(0), eq(workSpecId), any<Notification>())
+ .startForeground(eq(notificationId), eq(0), any<Notification>())
+ }
+
+ @Test
+ fun testNotify() {
+ val workSpecId = "workSpecId"
+ val notificationId = 1
+ val notification = mock(Notification::class.java)
+ val metadata = ForegroundInfo(notificationId, notification)
+ val intent = createNotifyIntent(context, workSpecId, metadata)
+ dispatcher.mCurrentForegroundWorkSpecId = "anotherWorkSpecId"
+ dispatcher.onStartCommand(intent)
+ verify(dispatcherCallback, times(1))
+ .notify(eq(notificationId), any<Notification>())
+ }
+
+ @Test
+ fun testPromoteWorkSpecForStartForeground() {
+ val firstWorkSpecId = "first"
+ val firstId = 1
+ val notification = mock(Notification::class.java)
+ val firstInfo = ForegroundInfo(firstId, notification)
+ val firstIntent = createNotifyIntent(context, firstWorkSpecId, firstInfo)
+
+ val secondWorkSpecId = "second"
+ val secondId = 2
+ val secondInfo = ForegroundInfo(secondId, notification)
+ val secondIntent = createNotifyIntent(context, secondWorkSpecId, secondInfo)
+
+ dispatcher.onStartCommand(firstIntent)
+ assertThat(dispatcher.mCurrentForegroundWorkSpecId, `is`(firstWorkSpecId))
+ verify(dispatcherCallback, times(1))
+ .startForeground(eq(firstId), eq(0), any<Notification>())
+
+ dispatcher.onStartCommand(secondIntent)
+ assertThat(dispatcher.mCurrentForegroundWorkSpecId, `is`(firstWorkSpecId))
+ verify(dispatcherCallback, times(1))
+ .notify(eq(secondId), any<Notification>())
+ assertThat(dispatcher.mForegroundInfoById.count(), `is`(2))
+
+ dispatcher.onExecuted(firstWorkSpecId, false)
+ verify(dispatcherCallback, times(1))
+ .startForeground(eq(secondId), eq(0), any<Notification>())
+ verify(dispatcherCallback, times(1))
+ .cancelNotification(secondId)
+ assertThat(dispatcher.mForegroundInfoById.count(), `is`(1))
+
+ dispatcher.onExecuted(secondWorkSpecId, false)
+ verify(dispatcherCallback, times(1))
+ .cancelNotification(secondId)
+ assertThat(dispatcher.mForegroundInfoById.count(), `is`(0))
+ }
+
+ @Test
+ fun promoteWorkSpecForStartForeground2() {
+ val firstWorkSpecId = "first"
+ val firstId = 1
+ val notification = mock(Notification::class.java)
+ val firstInfo = ForegroundInfo(firstId, notification)
+ val firstIntent = createNotifyIntent(context, firstWorkSpecId, firstInfo)
+
+ val secondWorkSpecId = "second"
+ val secondId = 2
+ val secondInfo = ForegroundInfo(secondId, notification)
+ val secondIntent = createNotifyIntent(context, secondWorkSpecId, secondInfo)
+
+ val thirdWorkSpecId = "third"
+ val thirdId = 3
+ val thirdInfo = ForegroundInfo(thirdId, notification)
+ val thirdIntent = createNotifyIntent(context, thirdWorkSpecId, thirdInfo)
+
+ dispatcher.onStartCommand(firstIntent)
+ assertThat(dispatcher.mCurrentForegroundWorkSpecId, `is`(firstWorkSpecId))
+ verify(dispatcherCallback, times(1))
+ .startForeground(eq(firstId), eq(0), any<Notification>())
+
+ dispatcher.onStartCommand(secondIntent)
+ assertThat(dispatcher.mCurrentForegroundWorkSpecId, `is`(firstWorkSpecId))
+ verify(dispatcherCallback, times(1))
+ .notify(eq(secondId), any<Notification>())
+ assertThat(dispatcher.mForegroundInfoById.count(), `is`(2))
+
+ dispatcher.onStartCommand(thirdIntent)
+ assertThat(dispatcher.mCurrentForegroundWorkSpecId, `is`(firstWorkSpecId))
+ verify(dispatcherCallback, times(1))
+ .notify(eq(secondId), any<Notification>())
+ assertThat(dispatcher.mForegroundInfoById.count(), `is`(3))
+
+ dispatcher.onExecuted(firstWorkSpecId, false)
+ verify(dispatcherCallback, times(1))
+ .startForeground(eq(thirdId), eq(0), any<Notification>())
+ verify(dispatcherCallback, times(1))
+ .cancelNotification(thirdId)
+ assertThat(dispatcher.mForegroundInfoById.count(), `is`(2))
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 29)
+ fun testUpdateNotificationWithDifferentForegroundServiceType() {
+ val firstWorkSpecId = "first"
+ val firstId = 1
+ val notification = mock(Notification::class.java)
+ val firstInfo =
+ ForegroundInfo(firstId, notification, FOREGROUND_SERVICE_TYPE_CONNECTED_DEVICE)
+ val firstIntent = createNotifyIntent(context, firstWorkSpecId, firstInfo)
+
+ val secondWorkSpecId = "second"
+ val secondId = 2
+ val secondInfo = ForegroundInfo(secondId, notification, FOREGROUND_SERVICE_TYPE_LOCATION)
+ val secondIntent = createNotifyIntent(context, secondWorkSpecId, secondInfo)
+
+ dispatcher.onStartCommand(firstIntent)
+ verify(dispatcherCallback, times(1))
+ .startForeground(
+ eq(firstId),
+ eq(FOREGROUND_SERVICE_TYPE_CONNECTED_DEVICE),
+ any<Notification>()
+ )
+
+ dispatcher.onStartCommand(secondIntent)
+ assertThat(dispatcher.mCurrentForegroundWorkSpecId, `is`(firstWorkSpecId))
+ verify(dispatcherCallback, times(1))
+ .notify(eq(secondId), any<Notification>())
+
+ val expectedNotificationType =
+ FOREGROUND_SERVICE_TYPE_LOCATION or FOREGROUND_SERVICE_TYPE_CONNECTED_DEVICE
+
+ verify(dispatcherCallback, times(1))
+ .startForeground(
+ eq(firstId),
+ eq(expectedNotificationType),
+ any<Notification>()
+ )
}
@Test
diff --git a/work/workmanager/src/main/java/androidx/work/impl/foreground/SystemForegroundDispatcher.java b/work/workmanager/src/main/java/androidx/work/impl/foreground/SystemForegroundDispatcher.java
index 91df909..12bf6fe 100644
--- a/work/workmanager/src/main/java/androidx/work/impl/foreground/SystemForegroundDispatcher.java
+++ b/work/workmanager/src/main/java/androidx/work/impl/foreground/SystemForegroundDispatcher.java
@@ -16,10 +16,13 @@
package androidx.work.impl.foreground;
+import static android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_NONE;
+
import android.app.Notification;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
+import android.os.Build;
import android.text.TextUtils;
import androidx.annotation.MainThread;
@@ -39,6 +42,8 @@
import java.util.HashMap;
import java.util.HashSet;
+import java.util.Iterator;
+import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
@@ -60,7 +65,6 @@
private static final String KEY_NOTIFICATION = "KEY_NOTIFICATION";
private static final String KEY_NOTIFICATION_ID = "KEY_NOTIFICATION_ID";
private static final String KEY_FOREGROUND_SERVICE_TYPE = "KEY_FOREGROUND_SERVICE_TYPE";
- private static final String KEY_NOTIFICATION_TAG = "KEY_NOTIFICATION_TAG";
private static final String KEY_WORKSPEC_ID = "KEY_WORKSPEC_ID";
// actions
@@ -78,6 +82,12 @@
final Object mLock;
@SuppressWarnings("WeakerAccess") // Synthetic access
+ String mCurrentForegroundWorkSpecId;
+
+ @SuppressWarnings("WeakerAccess") // Synthetic access
+ final Map<String, ForegroundInfo> mForegroundInfoById;
+
+ @SuppressWarnings("WeakerAccess") // Synthetic access
final Map<String, WorkSpec> mWorkSpecById;
@SuppressWarnings("WeakerAccess") // Synthetic access
@@ -94,6 +104,8 @@
mLock = new Object();
mWorkManagerImpl = WorkManagerImpl.getInstance(mContext);
mTaskExecutor = mWorkManagerImpl.getWorkTaskExecutor();
+ mCurrentForegroundWorkSpecId = null;
+ mForegroundInfoById = new LinkedHashMap<>();
mTrackedWorkSpecs = new HashSet<>();
mWorkSpecById = new HashMap<>();
mConstraintsTracker = new WorkConstraintsTracker(mContext, mTaskExecutor, this);
@@ -110,6 +122,8 @@
mLock = new Object();
mWorkManagerImpl = workManagerImpl;
mTaskExecutor = mWorkManagerImpl.getWorkTaskExecutor();
+ mCurrentForegroundWorkSpecId = null;
+ mForegroundInfoById = new LinkedHashMap<>();
mTrackedWorkSpecs = new HashSet<>();
mWorkSpecById = new HashMap<>();
mConstraintsTracker = tracker;
@@ -127,9 +141,48 @@
}
}
if (removed) {
- // Stop tracking
+ // Stop tracking constraints.
mConstraintsTracker.replace(mTrackedWorkSpecs);
}
+
+ // Promote new notifications to the foreground if necessary.
+ ForegroundInfo removedInfo = mForegroundInfoById.remove(workSpecId);
+ if (workSpecId.equals(mCurrentForegroundWorkSpecId)) {
+ if (mForegroundInfoById.size() > 0) {
+ // Find the next eligible ForegroundInfo
+ // LinkedHashMap uses insertion order, so find the last one because that was
+ // the most recent ForegroundInfo used. That way when different WorkSpecs share
+ // notification ids, we still end up in a reasonably good place.
+ Iterator<Map.Entry<String, ForegroundInfo>> iterator =
+ mForegroundInfoById.entrySet().iterator();
+
+ Map.Entry<String, ForegroundInfo> entry = iterator.next();
+ while (iterator.hasNext()) {
+ entry = iterator.next();
+ }
+
+ mCurrentForegroundWorkSpecId = entry.getKey();
+ if (mCallback != null) {
+ ForegroundInfo info = entry.getValue();
+ mCallback.startForeground(
+ info.getNotificationId(),
+ info.getForegroundServiceType(),
+ info.getNotification());
+
+ // We used NotificationManager before to update notifications, so ensure
+ // that we reference count the Notification instance down by
+ // cancelling the notification.
+ mCallback.cancelNotification(info.getNotificationId());
+ }
+ }
+ } else if (mCallback != null && removedInfo != null) {
+ // We don't need to worry about the current foreground WorkSpecId because if there
+ // is nothing running, the Processor will call stopForeground() which will eventually
+ // turn into a stopSelf().
+
+ // Explicitly remove this notification instance to decrease the reference count.
+ mCallback.cancelNotification(removedInfo.getNotificationId());
+ }
}
@MainThread
@@ -191,10 +244,45 @@
private void handleNotify(@NonNull Intent intent) {
int notificationId = intent.getIntExtra(KEY_NOTIFICATION_ID, 0);
int notificationType = intent.getIntExtra(KEY_FOREGROUND_SERVICE_TYPE, 0);
- String notificationTag = intent.getStringExtra(KEY_NOTIFICATION_TAG);
+ String workSpecId = intent.getStringExtra(KEY_WORKSPEC_ID);
Notification notification = intent.getParcelableExtra(KEY_NOTIFICATION);
+ Logger.get().debug(TAG,
+ String.format("Notifying with (id: %s, workSpecId: %s, notificationType: %s)",
+ notificationId, workSpecId, notificationType));
+
if (notification != null && mCallback != null) {
- mCallback.notify(notificationId, notificationType, notificationTag, notification);
+ // Keep track of this ForegroundInfo
+ ForegroundInfo info = new ForegroundInfo(
+ notificationId, notification, notificationType);
+
+ mForegroundInfoById.put(workSpecId, info);
+ if (TextUtils.isEmpty(mCurrentForegroundWorkSpecId)) {
+ // This is the current workSpecId which owns the Foreground lifecycle.
+ mCurrentForegroundWorkSpecId = workSpecId;
+ mCallback.startForeground(notificationId, notificationType, notification);
+ } else {
+ // Update notification
+ mCallback.notify(notificationId, notification);
+ // Update the notification in the foreground such that it's the union of
+ // all current foreground service types if necessary.
+ if (notificationType != FOREGROUND_SERVICE_TYPE_NONE
+ && Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
+ int foregroundServiceType = FOREGROUND_SERVICE_TYPE_NONE;
+ for (Map.Entry<String, ForegroundInfo> entry : mForegroundInfoById.entrySet()) {
+ ForegroundInfo foregroundInfo = entry.getValue();
+ foregroundServiceType |= foregroundInfo.getForegroundServiceType();
+ }
+ ForegroundInfo currentInfo =
+ mForegroundInfoById.get(mCurrentForegroundWorkSpecId);
+ if (currentInfo != null) {
+ mCallback.startForeground(
+ currentInfo.getNotificationId(),
+ foregroundServiceType,
+ currentInfo.getNotification()
+ );
+ }
+ }
+ }
}
}
@@ -288,7 +376,7 @@
intent.putExtra(KEY_NOTIFICATION_ID, info.getNotificationId());
intent.putExtra(KEY_FOREGROUND_SERVICE_TYPE, info.getForegroundServiceType());
intent.putExtra(KEY_NOTIFICATION, info.getNotification());
- intent.putExtra(KEY_NOTIFICATION_TAG, workSpecId);
+ intent.putExtra(KEY_WORKSPEC_ID, workSpecId);
return intent;
}
@@ -310,15 +398,25 @@
*/
interface Callback {
/**
- * Used to update the {@link Notification}.
+ * An implementation of this callback should call
+ * {@link android.app.Service#startForeground(int, Notification, int)}.
*/
- void notify(
+ void startForeground(
int notificationId,
int notificationType,
- @Nullable String notificationTag,
@NonNull Notification notification);
/**
+ * Used to update the {@link Notification}.
+ */
+ void notify(int notificationId, @NonNull Notification notification);
+
+ /**
+ * Used to cancel a {@link Notification}.
+ */
+ void cancelNotification(int notificationId);
+
+ /**
* Used to stop the {@link SystemForegroundService}.
*/
void stop();
diff --git a/work/workmanager/src/main/java/androidx/work/impl/foreground/SystemForegroundService.java b/work/workmanager/src/main/java/androidx/work/impl/foreground/SystemForegroundService.java
index 3298f43..3b91796 100644
--- a/work/workmanager/src/main/java/androidx/work/impl/foreground/SystemForegroundService.java
+++ b/work/workmanager/src/main/java/androidx/work/impl/foreground/SystemForegroundService.java
@@ -17,7 +17,9 @@
package androidx.work.impl.foreground;
import android.app.Notification;
+import android.app.NotificationManager;
import android.app.Service;
+import android.content.Context;
import android.content.Intent;
import android.os.Build;
import android.os.Handler;
@@ -43,6 +45,9 @@
private Handler mHandler;
private boolean mIsShutdown;
+ // Synthetic access
+ NotificationManager mNotificationManager;
+
@Override
public void onCreate() {
super.onCreate();
@@ -81,6 +86,8 @@
@MainThread
private void initializeDispatcher() {
mHandler = new Handler(Looper.getMainLooper());
+ mNotificationManager = (NotificationManager)
+ getApplicationContext().getSystemService(Context.NOTIFICATION_SERVICE);
mDispatcher = new SystemForegroundDispatcher(getApplicationContext());
mDispatcher.setCallback(this);
}
@@ -99,10 +106,9 @@
}
@Override
- public void notify(
+ public void startForeground(
final int notificationId,
final int notificationType,
- @Nullable final String notificationTag,
@NonNull final Notification notification) {
mHandler.post(new Runnable() {
@@ -116,4 +122,24 @@
}
});
}
+
+ @Override
+ public void notify(final int notificationId, @NonNull final Notification notification) {
+ mHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ mNotificationManager.notify(notificationId, notification);
+ }
+ });
+ }
+
+ @Override
+ public void cancelNotification(final int notificationId) {
+ mHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ mNotificationManager.cancel(notificationId);
+ }
+ });
+ }
}
diff --git a/work/workmanager/src/main/java/androidx/work/impl/utils/WorkProgressUpdater.java b/work/workmanager/src/main/java/androidx/work/impl/utils/WorkProgressUpdater.java
index e69446a..7aa48eb 100644
--- a/work/workmanager/src/main/java/androidx/work/impl/utils/WorkProgressUpdater.java
+++ b/work/workmanager/src/main/java/androidx/work/impl/utils/WorkProgressUpdater.java
@@ -73,7 +73,7 @@
@Override
public void run() {
String workSpecId = id.toString();
- Logger.get().info(TAG, String.format("Updating progress for %s (%s)", id, data));
+ Logger.get().debug(TAG, String.format("Updating progress for %s (%s)", id, data));
mWorkDatabase.beginTransaction();
try {
WorkSpecDao workSpecDao = mWorkDatabase.workSpecDao();