1 /* 2 * Copyright 2018 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.background.systemalarm; 18 19 import android.content.Context; 20 import android.content.Intent; 21 import android.os.Looper; 22 import android.os.PowerManager; 23 import android.text.TextUtils; 24 25 import androidx.annotation.MainThread; 26 import androidx.annotation.RestrictTo; 27 import androidx.annotation.VisibleForTesting; 28 import androidx.work.Logger; 29 import androidx.work.impl.ExecutionListener; 30 import androidx.work.impl.Processor; 31 import androidx.work.impl.StartStopTokens; 32 import androidx.work.impl.WorkLauncher; 33 import androidx.work.impl.WorkLauncherImpl; 34 import androidx.work.impl.WorkManagerImpl; 35 import androidx.work.impl.model.WorkGenerationalId; 36 import androidx.work.impl.utils.WakeLocks; 37 import androidx.work.impl.utils.WorkTimer; 38 import androidx.work.impl.utils.taskexecutor.SerialExecutor; 39 import androidx.work.impl.utils.taskexecutor.TaskExecutor; 40 41 import org.jspecify.annotations.NonNull; 42 import org.jspecify.annotations.Nullable; 43 44 import java.util.ArrayList; 45 import java.util.List; 46 47 /** 48 * The dispatcher used by the background processor which is based on 49 * {@link android.app.AlarmManager}. 50 * 51 */ 52 @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) 53 public class SystemAlarmDispatcher implements ExecutionListener { 54 55 // Synthetic accessor 56 static final String TAG = Logger.tagWithPrefix("SystemAlarmDispatcher"); 57 58 private static final String PROCESS_COMMAND_TAG = "ProcessCommand"; 59 private static final String KEY_START_ID = "KEY_START_ID"; 60 private static final int DEFAULT_START_ID = 0; 61 62 @SuppressWarnings("WeakerAccess") /* synthetic access */ 63 final Context mContext; 64 @SuppressWarnings("WeakerAccess") /* synthetic access */ 65 final TaskExecutor mTaskExecutor; 66 private final WorkTimer mWorkTimer; 67 private final Processor mProcessor; 68 private final WorkManagerImpl mWorkManager; 69 @SuppressWarnings("WeakerAccess") /* synthetic access */ 70 final CommandHandler mCommandHandler; 71 @SuppressWarnings("WeakerAccess") /* synthetic access */ 72 final List<Intent> mIntents; 73 Intent mCurrentIntent; 74 75 private @Nullable CommandsCompletedListener mCompletedListener; 76 77 private StartStopTokens mStartStopTokens; 78 private final WorkLauncher mWorkLauncher; 79 SystemAlarmDispatcher(@onNull Context context)80 SystemAlarmDispatcher(@NonNull Context context) { 81 this(context, null, null, null); 82 } 83 84 @VisibleForTesting SystemAlarmDispatcher( @onNull Context context, @Nullable Processor processor, @Nullable WorkManagerImpl workManager, @Nullable WorkLauncher launcher )85 SystemAlarmDispatcher( 86 @NonNull Context context, 87 @Nullable Processor processor, 88 @Nullable WorkManagerImpl workManager, 89 @Nullable WorkLauncher launcher 90 ) { 91 mContext = context.getApplicationContext(); 92 mStartStopTokens = StartStopTokens.create(); 93 mWorkManager = workManager != null ? workManager : WorkManagerImpl.getInstance(context); 94 mCommandHandler = new CommandHandler( 95 mContext, mWorkManager.getConfiguration().getClock(), mStartStopTokens); 96 mWorkTimer = new WorkTimer(mWorkManager.getConfiguration().getRunnableScheduler()); 97 mProcessor = processor != null ? processor : mWorkManager.getProcessor(); 98 mTaskExecutor = mWorkManager.getWorkTaskExecutor(); 99 mWorkLauncher = launcher != null ? launcher : 100 new WorkLauncherImpl(mProcessor, mTaskExecutor); 101 mProcessor.addExecutionListener(this); 102 // a list of pending intents which need to be processed 103 mIntents = new ArrayList<>(); 104 // the current intent (command) being processed. 105 mCurrentIntent = null; 106 } 107 108 /** 109 * This method needs to be idempotent. This could be called more than once, and therefore, 110 * this method should only perform cleanup when necessary. 111 */ onDestroy()112 void onDestroy() { 113 Logger.get().debug(TAG, "Destroying SystemAlarmDispatcher"); 114 mProcessor.removeExecutionListener(this); 115 mCompletedListener = null; 116 } 117 118 @Override onExecuted(@onNull WorkGenerationalId id, boolean needsReschedule)119 public void onExecuted(@NonNull WorkGenerationalId id, boolean needsReschedule) { 120 121 // When there are lots of workers completing at around the same time, 122 // this creates lock contention for the DelayMetCommandHandlers inside the CommandHandler. 123 // So move the actual execution of the post completion callbacks on the command executor 124 // thread. 125 mTaskExecutor.getMainThreadExecutor().execute( 126 new AddRunnable( 127 this, 128 CommandHandler.createExecutionCompletedIntent( 129 mContext, 130 id, 131 needsReschedule), 132 DEFAULT_START_ID)); 133 } 134 135 /** 136 * Adds the {@link Intent} intent and the startId to the command processor queue. 137 * 138 * @param intent The {@link Intent} command that needs to be added to the command queue. 139 * @param startId The command startId 140 * @return <code>true</code> when the command was added to the command processor queue. 141 */ 142 @MainThread add(final @NonNull Intent intent, final int startId)143 public boolean add(final @NonNull Intent intent, final int startId) { 144 Logger.get().debug(TAG, "Adding command " + intent + " (" + startId + ")"); 145 assertMainThread(); 146 String action = intent.getAction(); 147 if (TextUtils.isEmpty(action)) { 148 Logger.get().warning(TAG, "Unknown command. Ignoring"); 149 return false; 150 } 151 152 // If we have a constraints changed intent in the queue don't add a second one. We are 153 // treating this intent as special because every time a worker with constraints is complete 154 // it kicks off an update for constraint proxies. 155 if (CommandHandler.ACTION_CONSTRAINTS_CHANGED.equals(action) 156 && hasIntentWithAction(CommandHandler.ACTION_CONSTRAINTS_CHANGED)) { 157 return false; 158 } 159 160 intent.putExtra(KEY_START_ID, startId); 161 synchronized (mIntents) { 162 boolean hasCommands = !mIntents.isEmpty(); 163 mIntents.add(intent); 164 if (!hasCommands) { 165 // Only call processCommand if this is the first command. 166 // The call to dequeueAndCheckForCompletion will process the remaining commands 167 // in the order that they were added. 168 processCommand(); 169 } 170 } 171 return true; 172 } 173 setCompletedListener(@onNull CommandsCompletedListener listener)174 void setCompletedListener(@NonNull CommandsCompletedListener listener) { 175 if (mCompletedListener != null) { 176 Logger.get().error( 177 TAG, 178 "A completion listener for SystemAlarmDispatcher already exists."); 179 return; 180 } 181 mCompletedListener = listener; 182 } 183 getProcessor()184 Processor getProcessor() { 185 return mProcessor; 186 } 187 getWorkTimer()188 WorkTimer getWorkTimer() { 189 return mWorkTimer; 190 } 191 getWorkManager()192 WorkManagerImpl getWorkManager() { 193 return mWorkManager; 194 } 195 getTaskExecutor()196 TaskExecutor getTaskExecutor() { 197 return mTaskExecutor; 198 } 199 getWorkerLauncher()200 WorkLauncher getWorkerLauncher() { 201 return mWorkLauncher; 202 } 203 204 @MainThread 205 @SuppressWarnings("WeakerAccess") /* synthetic access */ dequeueAndCheckForCompletion()206 void dequeueAndCheckForCompletion() { 207 Logger.get().debug(TAG, "Checking if commands are complete."); 208 assertMainThread(); 209 210 synchronized (mIntents) { 211 // Remove the intent from the list of processed commands. 212 // We are doing this to avoid a race condition between completion of a 213 // command in the command handler, and the checkForCompletion triggered 214 // by a worker's onExecutionComplete(). 215 // For e.g. 216 // t0 -> delay_met_intent 217 // t1 -> bgProcessor.startWork(workSpec) 218 // t2 -> constraints_changed_intent 219 // t3 -> bgProcessor.onExecutionCompleted(...) 220 // t4 -> DequeueAndCheckForCompletion (while constraints_changed_intent is 221 // still being processed). 222 223 // Note: this works only because mCommandExecutor service is a single 224 // threaded executor. If that assumption changes in the future, use a 225 // ReentrantLock, and lock the queue while command processor processes 226 // an intent. Synchronized to prevent ConcurrentModificationExceptions. 227 if (mCurrentIntent != null) { 228 Logger.get().debug(TAG, "Removing command " + mCurrentIntent); 229 if (!mIntents.remove(0).equals(mCurrentIntent)) { 230 throw new IllegalStateException("Dequeue-d command is not the first."); 231 } 232 mCurrentIntent = null; 233 } 234 SerialExecutor serialExecutor = mTaskExecutor.getSerialTaskExecutor(); 235 if (!mCommandHandler.hasPendingCommands() 236 && mIntents.isEmpty() 237 && !serialExecutor.hasPendingTasks()) { 238 239 // If there are no more intents to process, and the command handler 240 // has no more pending commands, stop the service. 241 Logger.get().debug(TAG, "No more commands & intents."); 242 if (mCompletedListener != null) { 243 mCompletedListener.onAllCommandsCompleted(); 244 } 245 } else if (!mIntents.isEmpty()) { 246 // Only process the next command if we have more commands. 247 processCommand(); 248 } 249 } 250 } 251 252 @MainThread 253 @SuppressWarnings("FutureReturnValueIgnored") processCommand()254 private void processCommand() { 255 assertMainThread(); 256 PowerManager.WakeLock processCommandLock = 257 WakeLocks.newWakeLock(mContext, PROCESS_COMMAND_TAG); 258 try { 259 processCommandLock.acquire(); 260 // Process commands on the background thread. 261 mWorkManager.getWorkTaskExecutor().executeOnTaskThread(new Runnable() { 262 @Override 263 public void run() { 264 synchronized (mIntents) { 265 mCurrentIntent = mIntents.get(0); 266 } 267 268 if (mCurrentIntent != null) { 269 final String action = mCurrentIntent.getAction(); 270 final int startId = mCurrentIntent.getIntExtra(KEY_START_ID, 271 DEFAULT_START_ID); 272 Logger.get().debug(TAG, 273 "Processing command " + mCurrentIntent + ", " + startId); 274 final PowerManager.WakeLock wakeLock = WakeLocks.newWakeLock( 275 mContext, 276 action + " (" + startId + ")"); 277 try { 278 Logger.get().debug(TAG, 279 "Acquiring operation wake lock (" + action + ") " + wakeLock); 280 wakeLock.acquire(); 281 mCommandHandler.onHandleIntent(mCurrentIntent, startId, 282 SystemAlarmDispatcher.this); 283 } catch (Throwable throwable) { 284 Logger.get().error( 285 TAG, 286 "Unexpected error in onHandleIntent", 287 throwable); 288 } finally { 289 Logger.get().debug( 290 TAG, 291 "Releasing operation wake lock (" + action + ") " + wakeLock); 292 wakeLock.release(); 293 // Check if we have processed all commands 294 mTaskExecutor.getMainThreadExecutor().execute( 295 new DequeueAndCheckForCompletion(SystemAlarmDispatcher.this) 296 ); 297 } 298 } 299 } 300 }); 301 } finally { 302 processCommandLock.release(); 303 } 304 } 305 306 @MainThread hasIntentWithAction(@onNull String action)307 private boolean hasIntentWithAction(@NonNull String action) { 308 assertMainThread(); 309 synchronized (mIntents) { 310 for (Intent intent : mIntents) { 311 if (action.equals(intent.getAction())) { 312 return true; 313 } 314 } 315 return false; 316 } 317 } 318 assertMainThread()319 private void assertMainThread() { 320 if (Looper.getMainLooper().getThread() != Thread.currentThread()) { 321 throw new IllegalStateException("Needs to be invoked on the main thread."); 322 } 323 } 324 325 /** 326 * Checks if we are done executing all commands after dequeue-ing the current command. 327 */ 328 static class DequeueAndCheckForCompletion implements Runnable { 329 private final SystemAlarmDispatcher mDispatcher; 330 DequeueAndCheckForCompletion(@onNull SystemAlarmDispatcher dispatcher)331 DequeueAndCheckForCompletion(@NonNull SystemAlarmDispatcher dispatcher) { 332 mDispatcher = dispatcher; 333 } 334 335 @Override run()336 public void run() { 337 mDispatcher.dequeueAndCheckForCompletion(); 338 } 339 } 340 341 /** 342 * Adds a new intent to the SystemAlarmDispatcher. 343 */ 344 static class AddRunnable implements Runnable { 345 private final SystemAlarmDispatcher mDispatcher; 346 private final Intent mIntent; 347 private final int mStartId; 348 AddRunnable(@onNull SystemAlarmDispatcher dispatcher, @NonNull Intent intent, int startId)349 AddRunnable(@NonNull SystemAlarmDispatcher dispatcher, 350 @NonNull Intent intent, 351 int startId) { 352 mDispatcher = dispatcher; 353 mIntent = intent; 354 mStartId = startId; 355 } 356 357 @Override run()358 public void run() { 359 mDispatcher.add(mIntent, mStartId); 360 } 361 } 362 363 /** 364 * Used to notify interested parties when all pending commands and work is complete. 365 */ 366 interface CommandsCompletedListener { onAllCommandsCompleted()367 void onAllCommandsCompleted(); 368 } 369 } 370