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();