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.Handler; 22 import android.os.Looper; 23 import android.os.PowerManager; 24 import android.support.annotation.MainThread; 25 import android.support.annotation.NonNull; 26 import android.support.annotation.Nullable; 27 import android.support.annotation.RestrictTo; 28 import android.support.annotation.VisibleForTesting; 29 import android.text.TextUtils; 30 import android.util.Log; 31 32 import androidx.work.impl.ExecutionListener; 33 import androidx.work.impl.Processor; 34 import androidx.work.impl.WorkManagerImpl; 35 import androidx.work.impl.utils.WakeLocks; 36 37 import java.util.ArrayList; 38 import java.util.List; 39 import java.util.concurrent.ExecutorService; 40 import java.util.concurrent.Executors; 41 42 /** 43 * The dispatcher used by the background processor which is based on 44 * {@link android.app.AlarmManager}. 45 * 46 * @hide 47 */ 48 @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) 49 public class SystemAlarmDispatcher implements ExecutionListener { 50 51 private static final String TAG = "SystemAlarmDispatcher"; 52 private static final String PROCESS_COMMAND_TAG = "ProcessCommand"; 53 private static final String KEY_START_ID = "KEY_START_ID"; 54 private static final int DEFAULT_START_ID = 0; 55 56 private final Context mContext; 57 private final WorkTimer mWorkTimer; 58 private final Processor mProcessor; 59 private final WorkManagerImpl mWorkManager; 60 private final CommandHandler mCommandHandler; 61 private final Handler mMainHandler; 62 private final List<Intent> mIntents; 63 // The executor service responsible for dispatching all the commands. 64 private final ExecutorService mCommandExecutorService; 65 66 @Nullable private CommandsCompletedListener mCompletedListener; 67 SystemAlarmDispatcher(@onNull Context context)68 SystemAlarmDispatcher(@NonNull Context context) { 69 this(context, null, null); 70 } 71 72 @VisibleForTesting SystemAlarmDispatcher( @onNull Context context, @Nullable Processor processor, @Nullable WorkManagerImpl workManager)73 SystemAlarmDispatcher( 74 @NonNull Context context, 75 @Nullable Processor processor, 76 @Nullable WorkManagerImpl workManager) { 77 78 mContext = context.getApplicationContext(); 79 mCommandHandler = new CommandHandler(mContext); 80 mWorkTimer = new WorkTimer(); 81 mWorkManager = workManager != null ? workManager : WorkManagerImpl.getInstance(); 82 mProcessor = processor != null ? processor : mWorkManager.getProcessor(); 83 mProcessor.addExecutionListener(this); 84 // a list of pending intents which need to be processed 85 mIntents = new ArrayList<>(); 86 mMainHandler = new Handler(Looper.getMainLooper()); 87 // Use a single thread executor for handling the actual 88 // execution of the commands themselves 89 mCommandExecutorService = Executors.newSingleThreadExecutor(); 90 } 91 onDestroy()92 void onDestroy() { 93 mProcessor.removeExecutionListener(this); 94 mCompletedListener = null; 95 } 96 97 @Override onExecuted( @onNull String workSpecId, boolean isSuccessful, boolean needsReschedule)98 public void onExecuted( 99 @NonNull String workSpecId, 100 boolean isSuccessful, 101 boolean needsReschedule) { 102 103 // When there are lots of workers completing at around the same time, 104 // this creates lock contention for the DelayMetCommandHandlers inside the CommandHandler. 105 // So move the actual execution of the post completion callbacks on the command executor 106 // thread. 107 postOnMainThread( 108 new AddRunnable( 109 this, 110 CommandHandler.createExecutionCompletedIntent( 111 mContext, 112 workSpecId, 113 isSuccessful, 114 needsReschedule), 115 DEFAULT_START_ID)); 116 } 117 118 /** 119 * Adds the {@link Intent} intent and the startId to the command processor queue. 120 * 121 * @param intent The {@link Intent} command that needs to be added to the command queue. 122 * @param startId The command startId 123 * @return <code>true</code> when the command was added to the command processor queue. 124 */ 125 @MainThread add(@onNull final Intent intent, final int startId)126 public boolean add(@NonNull final Intent intent, final int startId) { 127 assertMainThread(); 128 String action = intent.getAction(); 129 if (TextUtils.isEmpty(action)) { 130 Log.w(TAG, "Unknown command. Ignoring"); 131 return false; 132 } 133 134 // If we have a constraints changed intent in the queue don't add a second one. We are 135 // treating this intent as special because every time a worker with constraints is complete 136 // it kicks off an update for constraint proxies. 137 if (CommandHandler.ACTION_CONSTRAINTS_CHANGED.equals(action) 138 && hasIntentWithAction(CommandHandler.ACTION_CONSTRAINTS_CHANGED)) { 139 return false; 140 } 141 142 intent.putExtra(KEY_START_ID, startId); 143 synchronized (mIntents) { 144 mIntents.add(intent); 145 } 146 processCommand(); 147 return true; 148 } 149 setCompletedListener(@onNull CommandsCompletedListener listener)150 void setCompletedListener(@NonNull CommandsCompletedListener listener) { 151 if (mCompletedListener != null) { 152 Log.e(TAG, "A completion listener for SystemAlarmDispatcher already exists."); 153 return; 154 } 155 mCompletedListener = listener; 156 } 157 getProcessor()158 Processor getProcessor() { 159 return mProcessor; 160 } 161 getWorkTimer()162 WorkTimer getWorkTimer() { 163 return mWorkTimer; 164 } 165 getWorkManager()166 WorkManagerImpl getWorkManager() { 167 return mWorkManager; 168 } 169 postOnMainThread(@onNull Runnable runnable)170 void postOnMainThread(@NonNull Runnable runnable) { 171 mMainHandler.post(runnable); 172 } 173 174 @MainThread checkForCommandsCompleted()175 private void checkForCommandsCompleted() { 176 assertMainThread(); 177 // if there are no more intents to process, and the command handler 178 // has no more pending commands, stop the service. 179 synchronized (mIntents) { 180 if (!mCommandHandler.hasPendingCommands() && mIntents.isEmpty()) { 181 Log.d(TAG, "No more commands & intents."); 182 if (mCompletedListener != null) { 183 mCompletedListener.onAllCommandsCompleted(); 184 } 185 } 186 } 187 } 188 189 @MainThread 190 @SuppressWarnings("FutureReturnValueIgnored") processCommand()191 private void processCommand() { 192 assertMainThread(); 193 PowerManager.WakeLock processCommandLock = 194 WakeLocks.newWakeLock(mContext, PROCESS_COMMAND_TAG); 195 try { 196 processCommandLock.acquire(); 197 // Process commands on the actual executor service, 198 // so we are no longer blocking the main thread. 199 mCommandExecutorService.submit(new Runnable() { 200 @Override 201 public void run() { 202 final Intent intent; 203 synchronized (mIntents) { 204 intent = mIntents.get(0); 205 } 206 207 if (intent != null) { 208 final String action = intent.getAction(); 209 final int startId = intent.getIntExtra(KEY_START_ID, DEFAULT_START_ID); 210 Log.d(TAG, String.format("Processing command %s, %s", intent, startId)); 211 final PowerManager.WakeLock wakeLock = WakeLocks.newWakeLock( 212 mContext, 213 String.format("%s (%s)", action, startId)); 214 try { 215 Log.d(TAG, String.format( 216 "Acquiring operation wake lock (%s) %s", 217 action, 218 wakeLock)); 219 220 wakeLock.acquire(); 221 mCommandHandler.onHandleIntent(intent, startId, 222 SystemAlarmDispatcher.this); 223 } finally { 224 // Remove the intent from the queue, only after it has been processed. 225 226 // We are doing this to avoid a race condition between completion of a 227 // command in the command handler, and the checkForCompletion triggered 228 // by a worker's onExecutionComplete(). 229 // For e.g. 230 // t0 -> delay_met_intent 231 // t1 -> bgProcessor.startWork(workSpec) 232 // t2 -> constraints_changed_intent 233 // t3 -> bgProcessor.onExecutionCompleted(...) 234 // t4 -> CheckForCompletionRunnable (while constraints_changed_intent is 235 // still being processed). 236 237 // Note: this works only because mCommandExecutor service is a single 238 // threaded executor. If that assumption changes in the future, use a 239 // ReentrantLock, and lock the queue while command processor processes 240 // an intent. Synchronized to prevent ConcurrentModificationExceptions. 241 synchronized (mIntents) { 242 mIntents.remove(0); 243 } 244 245 Log.d(TAG, String.format( 246 "Releasing operation wake lock (%s) %s", 247 action, 248 wakeLock)); 249 250 wakeLock.release(); 251 // Check if we have processed all commands 252 postOnMainThread( 253 new CheckForCompletionRunnable(SystemAlarmDispatcher.this)); 254 } 255 } 256 } 257 }); 258 } finally { 259 processCommandLock.release(); 260 } 261 } 262 263 @MainThread hasIntentWithAction(@onNull String action)264 private boolean hasIntentWithAction(@NonNull String action) { 265 assertMainThread(); 266 synchronized (mIntents) { 267 for (Intent intent : mIntents) { 268 if (action.equals(intent.getAction())) { 269 return true; 270 } 271 } 272 return false; 273 } 274 } 275 assertMainThread()276 private void assertMainThread() { 277 if (mMainHandler.getLooper().getThread() != Thread.currentThread()) { 278 throw new IllegalStateException("Needs to be invoked on the main thread."); 279 } 280 } 281 282 /** 283 * Checks if we are done executing all commands. 284 */ 285 static class CheckForCompletionRunnable implements Runnable { 286 private final SystemAlarmDispatcher mDispatcher; 287 CheckForCompletionRunnable(@onNull SystemAlarmDispatcher dispatcher)288 CheckForCompletionRunnable(@NonNull SystemAlarmDispatcher dispatcher) { 289 mDispatcher = dispatcher; 290 } 291 292 @Override run()293 public void run() { 294 mDispatcher.checkForCommandsCompleted(); 295 } 296 } 297 298 /** 299 * Adds a new intent to the SystemAlarmDispatcher. 300 */ 301 static class AddRunnable implements Runnable { 302 private final SystemAlarmDispatcher mDispatcher; 303 private final Intent mIntent; 304 private final int mStartId; 305 AddRunnable(@onNull SystemAlarmDispatcher dispatcher, @NonNull Intent intent, int startId)306 AddRunnable(@NonNull SystemAlarmDispatcher dispatcher, 307 @NonNull Intent intent, 308 int startId) { 309 mDispatcher = dispatcher; 310 mIntent = intent; 311 mStartId = startId; 312 } 313 314 @Override run()315 public void run() { 316 mDispatcher.add(mIntent, mStartId); 317 } 318 } 319 320 /** 321 * Used to notify interested parties when all pending commands and work is complete. 322 */ 323 interface CommandsCompletedListener { onAllCommandsCompleted()324 void onAllCommandsCompleted(); 325 } 326 } 327