Repository: square/leakcanary
Issue: Heap Dump Expedited Work not starting in Android < API31 with LeakCanary 2.8

### Description

When trying to do a 'Dump Heap Now' on an emulator with API30/29, the Work does not start, and and an exception is logged (see the trace in additional information section)

If the emulator runs API31, it works fine.
If I don't include the work-runtime, running an emulator with API30/29, it runs ok (I understand that in this case, the job is being done in a simple Thread) 

### Steps to Reproduce

1. Open Leaks Screen on an Emulator running Android API29 or API30
2. Go to 'Heap Dumps'
3. Tap on 'Dump Heap Now'

**Expected behavior:** 
The 'Dump Heap' work to run 

### Version Information

* LeakCanary version: 2.8
* Android OS version: 11, 10, 
* Gradle version: 7.0.2

### Additional Information

I think this is related to that Expedited work, when run in an Android version prior to API31, runs in a foreground service, and because of that, the Work should provide foreground info (like the icon and the title) for the notification the system displays when a foreground service is run

Trace:

```
2022-01-05 12:38:07.901 4829-4896/com.XXX.XXX.XXX D/LeakCanary: Enqueuing heap analysis for /storage/emulated/0/Download/leakcanary-com.XXX.XXX.XXX/2022-01-05_12-38-04_891.hprof on WorkManager remote worker
2022-01-05 12:38:07.984 4829-4895/com.XXX.XXX.XXX E/WM-WorkerWrapper: Work [ id=xxx, tags={ leakcanary.internal.HeapAnalyzerWorker } ] failed because it threw an exception/error
    java.util.concurrent.ExecutionException: java.util.concurrent.ExecutionException: java.util.concurrent.ExecutionException: java.lang.IllegalStateException: Expedited WorkRequests require a ListenableWorker to provide an implementation for `getForegroundInfoAsync()`
        at androidx.work.impl.utils.futures.AbstractFuture.getDoneValue(AbstractFuture.java:516)
        at androidx.work.impl.utils.futures.AbstractFuture.get(AbstractFuture.java:475)
        at androidx.work.impl.WorkerWrapper$2.run(WorkerWrapper.java:311)
        at androidx.work.impl.utils.SerialExecutor$Task.run(SerialExecutor.java:91)
        at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1167)
        at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:641)
        at java.lang.Thread.run(Thread.java:923)
     Caused by: java.util.concurrent.ExecutionException: java.util.concurrent.ExecutionException: java.lang.IllegalStateException: Expedited WorkRequests require a ListenableWorker to provide an implementation for `getForegroundInfoAsync()`
        at androidx.work.impl.utils.futures.AbstractFuture.getDoneValue(AbstractFuture.java:516)
        at androidx.work.impl.utils.futures.AbstractFuture.get(AbstractFuture.java:475)
        at androidx.work.impl.WorkerWrapper$1.run(WorkerWrapper.java:291)
        at android.os.Handler.handleCallback(Handler.java:938)
        at android.os.Handler.dispatchMessage(Handler.java:99)
        at android.os.Looper.loop(Looper.java:223)
        at android.app.ActivityThread.main(ActivityThread.java:7656)
        at java.lang.reflect.Method.invoke(Native Method)
        at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:592)
        at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:947)
     Caused by: java.util.concurrent.ExecutionException: java.lang.IllegalStateException: Expedited WorkRequests require a ListenableWorker to provide an implementation for `getForegroundInfoAsync()`
        at androidx.work.impl.utils.futures.AbstractFuture.getDoneValue(AbstractFuture.java:516)
        at androidx.work.impl.utils.futures.AbstractFuture.get(AbstractFuture.java:475)
        at androidx.work.impl.utils.WorkForegroundRunnable$2.run(WorkForegroundRunnable.java:93)
        at android.os.Handler.handleCallback(Handler.java:938) 
        at android.os.Handler.dispatchMessage(Handler.java:99) 
        at android.os.Looper.loop(Looper.java:223) 
        at android.app.ActivityThread.main(ActivityThread.java:7656) 
        at java.lang.reflect.Method.invoke(Native Method) 
        at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:592) 
        at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:947) 
     Caused by: java.lang.IllegalStateException: Expedited WorkRequests require a ListenableWorker to provide an implementation for `getForegroundInfoAsync()`
        at androidx.work.ListenableWorker.getForegroundInfoAsync(ListenableWorker.java:260)
        at androidx.work.impl.utils.WorkForegroundRunnable$1.run(WorkForegroundRunnable.java:85)
        at android.os.Handler.handleCallback(Handler.java:938) 
        at android.os.Handler.dispatchMessage(Handler.java:99) 
        at android.os.Looper.loop(Looper.java:223) 
        at android.app.ActivityThread.main(ActivityThread.java:7656) 
        at java.lang.reflect.Method.invoke(Native Method) 
        at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:592) 
        at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:947) 
2022-01-05 12:38:07.985 4829-4895/com.xxxx.xxx.xxx I/WM-WorkerWrapper: Worker result FAILURE for Work [ id=xxx, tags={ leakcanary.internal.HeapAnalyzerWorker } ]
2022-01-05 12:38:07.991 4829-4829/com.xxx.xxx.xxx D/LeakCanary: Watching instance of androidx.work.impl.background.systemjob.SystemJobService (androidx.work.impl.background.systemjob.SystemJobService received Service#onDestroy() callback) with key xxx
```




Changed files:
leakcanary-android-core/src/main/java/leakcanary/internal/HeapAnalyzerWorker.kt
leakcanary-android-core/src/main/java/leakcanary/internal/RemoteHeapAnalyzerWorker.kt

diff --git a/leakcanary-android-core/src/main/java/leakcanary/internal/HeapAnalyzerWorker.kt b/leakcanary-android-core/src/main/java/leakcanary/internal/HeapAnalyzerWorker.kt
index 2a6d22bcd..f2186c406 100644
--- a/leakcanary-android-core/src/main/java/leakcanary/internal/HeapAnalyzerWorker.kt
+++ b/leakcanary-android-core/src/main/java/leakcanary/internal/HeapAnalyzerWorker.kt
@@ -1,9 +1,14 @@
 package leakcanary.internal
 
+import android.app.Notification
 import android.content.Context
 import androidx.work.Data
+import androidx.work.ForegroundInfo
 import androidx.work.Worker
 import androidx.work.WorkerParameters
+import androidx.work.impl.utils.futures.SettableFuture
+import com.google.common.util.concurrent.ListenableFuture
+import com.squareup.leakcanary.core.R
 import leakcanary.EventListener.Event
 
 internal class HeapAnalyzerWorker(appContext: Context, workerParams: WorkerParameters) :
@@ -17,6 +22,10 @@ internal class HeapAnalyzerWorker(appContext: Context, workerParams: WorkerParam
     return Result.success()
   }
 
+  override fun getForegroundInfoAsync(): ListenableFuture<ForegroundInfo> {
+    return applicationContext.heapAnalysisForegroundInfoAsync()
+  }
+
   companion object {
     private const val EVENT_BYTES = "EVENT_BYTES"
 
@@ -24,6 +33,24 @@ internal class HeapAnalyzerWorker(appContext: Context, workerParams: WorkerParam
       .putByteArray(EVENT_BYTES, toByteArray())
       .build()
 
-    inline fun <reified T> Data.asEvent(): T = Serializables.fromByteArray<T>(getByteArray(EVENT_BYTES)!!)!!
+    inline fun <reified T> Data.asEvent(): T =
+      Serializables.fromByteArray<T>(getByteArray(EVENT_BYTES)!!)!!
+
+    fun Context.heapAnalysisForegroundInfoAsync(): ListenableFuture<ForegroundInfo> {
+      val infoFuture = SettableFuture.create<ForegroundInfo>()
+      val builder = Notification.Builder(this)
+        .setContentTitle(getString(R.string.leak_canary_notification_analysing))
+        .setContentText("LeakCanary is working.")
+        .setProgress(100, 0, true)
+      val notification =
+        Notifications.buildNotification(this, builder, NotificationType.LEAKCANARY_LOW)
+      infoFuture.set(
+        ForegroundInfo(
+          R.id.leak_canary_notification_analyzing_heap,
+          notification
+        )
+      )
+      return infoFuture
+    }
   }
 }
diff --git a/leakcanary-android-core/src/main/java/leakcanary/internal/RemoteHeapAnalyzerWorker.kt b/leakcanary-android-core/src/main/java/leakcanary/internal/RemoteHeapAnalyzerWorker.kt
index 72073ccbc..31223ccfd 100644
--- a/leakcanary-android-core/src/main/java/leakcanary/internal/RemoteHeapAnalyzerWorker.kt
+++ b/leakcanary-android-core/src/main/java/leakcanary/internal/RemoteHeapAnalyzerWorker.kt
@@ -1,6 +1,7 @@
 package leakcanary.internal
 
 import android.content.Context
+import androidx.work.ForegroundInfo
 import androidx.work.WorkerParameters
 import androidx.work.impl.utils.futures.SettableFuture
 import androidx.work.multiprocess.RemoteListenableWorker
@@ -8,6 +9,7 @@ import com.google.common.util.concurrent.ListenableFuture
 import leakcanary.BackgroundThreadHeapAnalyzer.heapAnalyzerThreadHandler
 import leakcanary.EventListener.Event.HeapDump
 import leakcanary.internal.HeapAnalyzerWorker.Companion.asEvent
+import leakcanary.internal.HeapAnalyzerWorker.Companion.heapAnalysisForegroundInfoAsync
 import shark.SharkLog
 
 internal class RemoteHeapAnalyzerWorker(appContext: Context, workerParams: WorkerParameters) :
@@ -33,4 +35,8 @@ internal class RemoteHeapAnalyzerWorker(appContext: Context, workerParams: Worke
     }
     return result
   }
+
+  override fun getForegroundInfoAsync(): ListenableFuture<ForegroundInfo> {
+    return applicationContext.heapAnalysisForegroundInfoAsync()
+  }
 }