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