1 /*
2  * Copyright 2019 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package androidx.work.impl.foreground;
18 
19 import static android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_NONE;
20 
21 import static androidx.work.impl.model.WorkSpecKt.generationalId;
22 
23 import android.app.Notification;
24 import android.content.Context;
25 import android.content.Intent;
26 import android.net.Uri;
27 import android.os.Build;
28 import android.text.TextUtils;
29 
30 import androidx.annotation.MainThread;
31 import androidx.annotation.RestrictTo;
32 import androidx.annotation.VisibleForTesting;
33 import androidx.work.ForegroundInfo;
34 import androidx.work.Logger;
35 import androidx.work.WorkInfo;
36 import androidx.work.impl.ExecutionListener;
37 import androidx.work.impl.WorkManagerImpl;
38 import androidx.work.impl.constraints.ConstraintsState;
39 import androidx.work.impl.constraints.OnConstraintsStateChangedListener;
40 import androidx.work.impl.constraints.WorkConstraintsTracker;
41 import androidx.work.impl.constraints.WorkConstraintsTrackerKt;
42 import androidx.work.impl.model.WorkGenerationalId;
43 import androidx.work.impl.model.WorkSpec;
44 import androidx.work.impl.utils.taskexecutor.TaskExecutor;
45 
46 import kotlinx.coroutines.Job;
47 
48 import org.jspecify.annotations.NonNull;
49 import org.jspecify.annotations.Nullable;
50 
51 import java.util.HashMap;
52 import java.util.Iterator;
53 import java.util.LinkedHashMap;
54 import java.util.Map;
55 import java.util.UUID;
56 
57 /**
58  * Handles requests for executing {@link androidx.work.WorkRequest}s on behalf of
59  * {@link SystemForegroundService}.
60  */
61 @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
62 public class SystemForegroundDispatcher implements OnConstraintsStateChangedListener,
63         ExecutionListener {
64     // Synthetic access
65     @SuppressWarnings("WeakerAccess")
66     static final String TAG = Logger.tagWithPrefix("SystemFgDispatcher");
67 
68     // keys
69     private static final String KEY_NOTIFICATION = "KEY_NOTIFICATION";
70     private static final String KEY_NOTIFICATION_ID = "KEY_NOTIFICATION_ID";
71     private static final String KEY_FOREGROUND_SERVICE_TYPE = "KEY_FOREGROUND_SERVICE_TYPE";
72     private static final String KEY_WORKSPEC_ID = "KEY_WORKSPEC_ID";
73     private static final String KEY_GENERATION = "KEY_GENERATION";
74 
75     // actions
76     private static final String ACTION_START_FOREGROUND = "ACTION_START_FOREGROUND";
77     private static final String ACTION_NOTIFY = "ACTION_NOTIFY";
78     private static final String ACTION_CANCEL_WORK = "ACTION_CANCEL_WORK";
79     private static final String ACTION_STOP_FOREGROUND = "ACTION_STOP_FOREGROUND";
80 
81     private Context mContext;
82     private WorkManagerImpl mWorkManagerImpl;
83     private final TaskExecutor mTaskExecutor;
84 
85 
86     @SuppressWarnings("WeakerAccess") // Synthetic access
87     final Object mLock;
88 
89     @SuppressWarnings("WeakerAccess") // Synthetic access
90     WorkGenerationalId mCurrentForegroundId;
91 
92     @SuppressWarnings("WeakerAccess") // Synthetic access
93     final Map<WorkGenerationalId, ForegroundInfo> mForegroundInfoById;
94 
95     @SuppressWarnings("WeakerAccess") // Synthetic access
96     final Map<WorkGenerationalId, WorkSpec> mWorkSpecById;
97 
98     @SuppressWarnings("WeakerAccess") // Synthetic access
99     final Map<WorkGenerationalId, Job> mTrackedWorkSpecs;
100 
101     @SuppressWarnings("WeakerAccess") // Synthetic access
102     final WorkConstraintsTracker mConstraintsTracker;
103 
104     private @Nullable Callback mCallback;
105 
SystemForegroundDispatcher(@onNull Context context)106     SystemForegroundDispatcher(@NonNull Context context) {
107         mContext = context;
108         mLock = new Object();
109         mWorkManagerImpl = WorkManagerImpl.getInstance(mContext);
110         mTaskExecutor = mWorkManagerImpl.getWorkTaskExecutor();
111         mCurrentForegroundId = null;
112         mForegroundInfoById = new LinkedHashMap<>();
113         mTrackedWorkSpecs = new HashMap<>();
114         mWorkSpecById = new HashMap<>();
115         mConstraintsTracker = new WorkConstraintsTracker(mWorkManagerImpl.getTrackers());
116         mWorkManagerImpl.getProcessor().addExecutionListener(this);
117     }
118 
119     @VisibleForTesting
SystemForegroundDispatcher( @onNull Context context, @NonNull WorkManagerImpl workManagerImpl, @NonNull WorkConstraintsTracker tracker)120     SystemForegroundDispatcher(
121             @NonNull Context context,
122             @NonNull WorkManagerImpl workManagerImpl,
123             @NonNull WorkConstraintsTracker tracker) {
124 
125         mContext = context;
126         mLock = new Object();
127         mWorkManagerImpl = workManagerImpl;
128         mTaskExecutor = mWorkManagerImpl.getWorkTaskExecutor();
129         mCurrentForegroundId = null;
130         mForegroundInfoById = new LinkedHashMap<>();
131         mTrackedWorkSpecs = new HashMap<>();
132         mWorkSpecById = new HashMap<>();
133         mConstraintsTracker = tracker;
134         mWorkManagerImpl.getProcessor().addExecutionListener(this);
135     }
136 
137     @MainThread
138     @Override
onExecuted(@onNull WorkGenerationalId id, boolean needsReschedule)139     public void onExecuted(@NonNull WorkGenerationalId id, boolean needsReschedule) {
140         Job removed = null;
141         synchronized (mLock) {
142             WorkSpec workSpec = mWorkSpecById.remove(id);
143             if (workSpec != null) {
144                 removed = mTrackedWorkSpecs.remove(id);
145             }
146             if (removed != null) {
147                 // Stop tracking constraints.
148                 removed.cancel(null);
149             }
150         }
151 
152         ForegroundInfo removedInfo = mForegroundInfoById.remove(id);
153         // Promote new notifications to the foreground if necessary.
154         if (id.equals(mCurrentForegroundId)) {
155             if (mForegroundInfoById.size() > 0) {
156                 // Find the next eligible ForegroundInfo
157                 // LinkedHashMap uses insertion order, so find the last one because that was
158                 // the most recent ForegroundInfo used. That way when different WorkSpecs share
159                 // notification ids, we still end up in a reasonably good place.
160                 Iterator<Map.Entry<WorkGenerationalId, ForegroundInfo>> iterator =
161                         mForegroundInfoById.entrySet().iterator();
162 
163                 Map.Entry<WorkGenerationalId, ForegroundInfo> entry = iterator.next();
164                 while (iterator.hasNext()) {
165                     entry = iterator.next();
166                 }
167 
168                 mCurrentForegroundId = entry.getKey();
169                 if (mCallback != null) {
170                     ForegroundInfo info = entry.getValue();
171                     mCallback.startForeground(
172                             info.getNotificationId(),
173                             info.getForegroundServiceType(),
174                             info.getNotification());
175 
176                     // We used NotificationManager before to update notifications, so ensure
177                     // that we reference count the Notification instance down by
178                     // cancelling the notification.
179                     mCallback.cancelNotification(info.getNotificationId());
180                 }
181             } else {
182                 mCurrentForegroundId = null;
183             }
184         }
185         // Keep track of the reference and use that when cancelling Notification. This is because
186         // the work-testing library uses a direct executor and does *not* call this method
187         // on the main thread.
188         Callback callback = mCallback;
189         if (removedInfo != null && callback != null) {
190             // Explicitly decrement the reference count for the notification
191 
192             // We are doing this without having to wait for the handleStop() to clean up
193             // Notifications. This is because the Processor stops foreground workers on the
194             // dedicated task executor thread. Meanwhile Notifications are managed on the main
195             // thread, so there is a chance that handleStop() fires before onExecuted() is called
196             // on the main thread.
197             Logger.get().debug(TAG,
198                     "Removing Notification (id: " + removedInfo.getNotificationId()
199                             + ", workSpecId: " + id
200                             + ", notificationType: " + removedInfo.getForegroundServiceType());
201             callback.cancelNotification(removedInfo.getNotificationId());
202         }
203     }
204 
205     @MainThread
setCallback(@onNull Callback callback)206     void setCallback(@NonNull Callback callback) {
207         if (mCallback != null) {
208             Logger.get().error(TAG, "A callback already exists.");
209             return;
210         }
211         mCallback = callback;
212     }
213 
214     @MainThread
onStartCommand(@onNull Intent intent)215     void onStartCommand(@NonNull Intent intent) {
216         String action = intent.getAction();
217         if (ACTION_START_FOREGROUND.equals(action)) {
218             handleStartForeground(intent);
219             // Call handleNotify() which in turn calls startForeground() as part of handing this
220             // command. This is important for some OEMs.
221             handleNotify(intent);
222         } else if (ACTION_NOTIFY.equals(action)) {
223             handleNotify(intent);
224         } else if (ACTION_CANCEL_WORK.equals(action)) {
225             handleCancelWork(intent);
226         } else if (ACTION_STOP_FOREGROUND.equals(action)) {
227             handleStop(intent);
228         }
229     }
230 
231     @MainThread
onDestroy()232     void onDestroy() {
233         mCallback = null;
234         synchronized (mLock) {
235             for (Job job : mTrackedWorkSpecs.values()) {
236                 job.cancel(null);
237             }
238         }
239         mWorkManagerImpl.getProcessor().removeExecutionListener(this);
240     }
241 
242     @MainThread
onTimeout(int startId, int fgsType)243     void onTimeout(int startId, int fgsType) {
244         Logger.get().info(TAG, "Foreground service timed out, FGS type: " + fgsType);
245         for (Map.Entry<WorkGenerationalId, ForegroundInfo> entry : mForegroundInfoById.entrySet()) {
246             ForegroundInfo info = entry.getValue();
247             if (info.getForegroundServiceType() == fgsType) {
248                 WorkGenerationalId id = entry.getKey();
249                 mWorkManagerImpl.stopForegroundWork(id,
250                         WorkInfo.STOP_REASON_FOREGROUND_SERVICE_TIMEOUT);
251             }
252         }
253         if (mCallback != null) {
254             mCallback.stop();
255         }
256     }
257 
258     @MainThread
handleStartForeground(@onNull Intent intent)259     private void handleStartForeground(@NonNull Intent intent) {
260         Logger.get().info(TAG, "Started foreground service " + intent);
261         final String workSpecId = intent.getStringExtra(KEY_WORKSPEC_ID);
262         mTaskExecutor.executeOnTaskThread(new Runnable() {
263             @Override
264             public void run() {
265                 WorkSpec workSpec = mWorkManagerImpl.getProcessor().getRunningWorkSpec(workSpecId);
266                 // Only track constraints if there are constraints that need to be tracked
267                 // (constraints are immutable)
268                 if (workSpec != null && workSpec.hasConstraints()) {
269                     synchronized (mLock) {
270                         mWorkSpecById.put(generationalId(workSpec), workSpec);
271                         Job job = WorkConstraintsTrackerKt.listen(mConstraintsTracker, workSpec,
272                                 mTaskExecutor.getTaskCoroutineDispatcher(),
273                                 SystemForegroundDispatcher.this
274                         );
275                         mTrackedWorkSpecs.put(generationalId(workSpec), job);
276                     }
277                 }
278             }
279         });
280     }
281 
282     @SuppressWarnings("deprecation")
283     @MainThread
handleNotify(@onNull Intent intent)284     private void handleNotify(@NonNull Intent intent) {
285         if (mCallback == null) {
286             throw new IllegalStateException("handleNotify was called on the destroyed dispatcher");
287         }
288         int notificationId = intent.getIntExtra(KEY_NOTIFICATION_ID, 0);
289         int notificationType = intent.getIntExtra(KEY_FOREGROUND_SERVICE_TYPE, 0);
290         String workSpecId = intent.getStringExtra(KEY_WORKSPEC_ID);
291         int generation = intent.getIntExtra(KEY_GENERATION, 0);
292         WorkGenerationalId workId = new WorkGenerationalId(workSpecId, generation);
293         Notification notification = intent.getParcelableExtra(KEY_NOTIFICATION);
294 
295         Logger.get().debug(TAG,
296                 "Notifying with (id:" + notificationId
297                         + ", workSpecId: " + workSpecId
298                         + ", notificationType :" + notificationType + ")");
299         if (notification == null) {
300             throw new IllegalArgumentException("Notification passed in the intent was null.");
301         }
302 
303         // Keep track of this ForegroundInfo
304         ForegroundInfo info = new ForegroundInfo(notificationId, notification, notificationType);
305         mForegroundInfoById.put(workId, info);
306         ForegroundInfo currentInfo = mForegroundInfoById.get(mCurrentForegroundId);
307         ForegroundInfo resultInfo;
308         if (currentInfo == null) {
309             // This is the current workSpecId which owns the Foreground lifecycle.
310             mCurrentForegroundId = workId;
311             resultInfo = info;
312         } else {
313             // Update notification
314             mCallback.notify(notificationId, notification);
315             // Update the notification in the foreground such that it's the union of
316             // all current foreground service types if necessary.
317             // Before Q startForeground didn't receive foregroundServiceType, so no need to
318             // calculate it.
319             if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
320                 int foregroundServiceType = FOREGROUND_SERVICE_TYPE_NONE;
321                 for (Map.Entry<WorkGenerationalId, ForegroundInfo> entry
322                         : mForegroundInfoById.entrySet()) {
323                     ForegroundInfo foregroundInfo = entry.getValue();
324                     foregroundServiceType |= foregroundInfo.getForegroundServiceType();
325                 }
326                 resultInfo = new ForegroundInfo(currentInfo.getNotificationId(),
327                         currentInfo.getNotification(), foregroundServiceType);
328             } else {
329                 resultInfo = currentInfo;
330             }
331         }
332         mCallback.startForeground(resultInfo.getNotificationId(),
333                 resultInfo.getForegroundServiceType(), resultInfo.getNotification());
334     }
335 
336     @MainThread
handleStop(@onNull Intent intent)337     void handleStop(@NonNull Intent intent) {
338         Logger.get().info(TAG, "Stopping foreground service");
339         if (mCallback != null) {
340             mCallback.stop();
341         }
342     }
343 
344     @MainThread
handleCancelWork(@onNull Intent intent)345     private void handleCancelWork(@NonNull Intent intent) {
346         Logger.get().info(TAG, "Stopping foreground work for " + intent);
347         String workSpecId = intent.getStringExtra(KEY_WORKSPEC_ID);
348         if (workSpecId != null && !TextUtils.isEmpty(workSpecId)) {
349             mWorkManagerImpl.cancelWorkById(UUID.fromString(workSpecId));
350         }
351     }
352 
353     @Override
onConstraintsStateChanged(@onNull WorkSpec workSpec, @NonNull ConstraintsState state)354     public void onConstraintsStateChanged(@NonNull WorkSpec workSpec,
355             @NonNull ConstraintsState state) {
356         if (state instanceof ConstraintsState.ConstraintsNotMet) {
357             String workSpecId = workSpec.id;
358             Logger.get().debug(TAG, "Constraints unmet for WorkSpec " + workSpecId);
359             mWorkManagerImpl.stopForegroundWork(
360                     generationalId(workSpec),
361                     ((ConstraintsState.ConstraintsNotMet) state).getReason());
362         }
363     }
364 
365     /**
366      * The {@link Intent} is used to start a foreground {@link android.app.Service}.
367      *
368      * @param context    The application {@link Context}
369      * @param workSpecId The WorkSpec id of the Worker being executed in the context of the
370      *                   foreground service
371      * @return The {@link Intent}
372      */
createStartForegroundIntent( @onNull Context context, @NonNull WorkGenerationalId id, @NonNull ForegroundInfo info)373     public static @NonNull Intent createStartForegroundIntent(
374             @NonNull Context context,
375             @NonNull WorkGenerationalId id,
376             @NonNull ForegroundInfo info) {
377         Intent intent = new Intent(context, SystemForegroundService.class);
378         intent.setAction(ACTION_START_FOREGROUND);
379         intent.putExtra(KEY_WORKSPEC_ID, id.getWorkSpecId());
380         intent.putExtra(KEY_GENERATION, id.getGeneration());
381         intent.putExtra(KEY_NOTIFICATION_ID, info.getNotificationId());
382         intent.putExtra(KEY_FOREGROUND_SERVICE_TYPE, info.getForegroundServiceType());
383         intent.putExtra(KEY_NOTIFICATION, info.getNotification());
384         return intent;
385     }
386 
387     /**
388      * The {@link Intent} is used to cancel foreground work for a given {@link String} workSpecId.
389      *
390      * @param context    The application {@link Context}
391      * @param workSpecId The WorkSpec id of the Worker being executed in the context of the
392      *                   foreground service
393      * @return The {@link Intent}
394      */
createCancelWorkIntent( @onNull Context context, @NonNull String workSpecId)395     public static @NonNull Intent createCancelWorkIntent(
396             @NonNull Context context,
397             @NonNull String workSpecId) {
398         Intent intent = new Intent(context, SystemForegroundService.class);
399         intent.setAction(ACTION_CANCEL_WORK);
400         // Set data to make it unique for filterEquals()
401         intent.setData(Uri.parse("workspec://" + workSpecId));
402         intent.putExtra(KEY_WORKSPEC_ID, workSpecId);
403         return intent;
404     }
405 
406     /**
407      * The {@link Intent} which is used to display a {@link Notification} via
408      * {@link SystemForegroundService}.
409      *
410      * @param context    The application {@link Context}
411      * @param id The {@link WorkSpec} id
412      * @param info       The {@link ForegroundInfo}
413      * @return The {@link Intent}
414      */
createNotifyIntent( @onNull Context context, @NonNull WorkGenerationalId id, @NonNull ForegroundInfo info)415     public static @NonNull Intent createNotifyIntent(
416             @NonNull Context context,
417             @NonNull WorkGenerationalId id,
418             @NonNull ForegroundInfo info) {
419         Intent intent = new Intent(context, SystemForegroundService.class);
420         intent.setAction(ACTION_NOTIFY);
421         intent.putExtra(KEY_NOTIFICATION_ID, info.getNotificationId());
422         intent.putExtra(KEY_FOREGROUND_SERVICE_TYPE, info.getForegroundServiceType());
423         intent.putExtra(KEY_NOTIFICATION, info.getNotification());
424         intent.putExtra(KEY_WORKSPEC_ID, id.getWorkSpecId());
425         intent.putExtra(KEY_GENERATION, id.getGeneration());
426         return intent;
427     }
428 
429     /**
430      * The {@link Intent} which can be used to stop {@link SystemForegroundService}.
431      *
432      * @param context The application {@link Context}
433      * @return The {@link Intent}
434      */
createStopForegroundIntent(@onNull Context context)435     public static @NonNull Intent createStopForegroundIntent(@NonNull Context context) {
436         Intent intent = new Intent(context, SystemForegroundService.class);
437         intent.setAction(ACTION_STOP_FOREGROUND);
438         return intent;
439     }
440 
441     /**
442      * Used to notify that all pending commands are now completed.
443      */
444     interface Callback {
445         /**
446          * An implementation of this callback should call
447          * {@link android.app.Service#startForeground(int, Notification, int)}.
448          */
449         @MainThread
startForeground( int notificationId, int notificationType, @NonNull Notification notification)450         void startForeground(
451                 int notificationId,
452                 int notificationType,
453                 @NonNull Notification notification);
454 
455         /**
456          * Used to update the {@link Notification}.
457          */
458         @MainThread
notify(int notificationId, @NonNull Notification notification)459         void notify(int notificationId, @NonNull Notification notification);
460 
461         /**
462          * Used to cancel a {@link Notification}.
463          */
464         @MainThread
cancelNotification(int notificationId)465         void cancelNotification(int notificationId);
466 
467         /**
468          * Used to stop the {@link SystemForegroundService}.
469          */
470         @MainThread
stop()471         void stop();
472     }
473 }
474