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.Bundle; 22 import android.support.annotation.NonNull; 23 import android.support.annotation.Nullable; 24 import android.support.annotation.RestrictTo; 25 import android.support.annotation.WorkerThread; 26 import android.util.Log; 27 28 import androidx.work.impl.ExecutionListener; 29 import androidx.work.impl.WorkDatabase; 30 import androidx.work.impl.WorkManagerImpl; 31 import androidx.work.impl.model.WorkSpec; 32 import androidx.work.impl.model.WorkSpecDao; 33 34 import java.util.HashMap; 35 import java.util.Map; 36 37 /** 38 * The command handler used by {@link SystemAlarmDispatcher}. 39 * 40 * @hide 41 */ 42 @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) 43 public class CommandHandler implements ExecutionListener { 44 45 private static final String TAG = "CommandHandler"; 46 47 // actions 48 static final String ACTION_SCHEDULE_WORK = "ACTION_SCHEDULE_WORK"; 49 static final String ACTION_DELAY_MET = "ACTION_DELAY_MET"; 50 static final String ACTION_STOP_WORK = "ACTION_STOP_WORK"; 51 static final String ACTION_CONSTRAINTS_CHANGED = "ACTION_CONSTRAINTS_CHANGED"; 52 static final String ACTION_RESCHEDULE = "ACTION_RESCHEDULE"; 53 static final String ACTION_EXECUTION_COMPLETED = "ACTION_EXECUTION_COMPLETED"; 54 55 // keys 56 private static final String KEY_WORKSPEC_ID = "KEY_WORKSPEC_ID"; 57 private static final String KEY_IS_SUCCESSFUL = "KEY_IS_SUCCESSFUL"; 58 private static final String KEY_NEEDS_RESCHEDULE = "KEY_NEEDS_RESCHEDULE"; 59 60 // constants 61 static final long WORK_PROCESSING_TIME_IN_MS = 10 * 60 * 1000L; 62 63 // utilities createScheduleWorkIntent(@onNull Context context, @NonNull String workSpecId)64 static Intent createScheduleWorkIntent(@NonNull Context context, @NonNull String workSpecId) { 65 Intent intent = new Intent(context, SystemAlarmService.class); 66 intent.setAction(ACTION_SCHEDULE_WORK); 67 intent.putExtra(KEY_WORKSPEC_ID, workSpecId); 68 return intent; 69 } 70 createDelayMetIntent(@onNull Context context, @NonNull String workSpecId)71 static Intent createDelayMetIntent(@NonNull Context context, @NonNull String workSpecId) { 72 Intent intent = new Intent(context, SystemAlarmService.class); 73 intent.setAction(ACTION_DELAY_MET); 74 intent.putExtra(KEY_WORKSPEC_ID, workSpecId); 75 return intent; 76 } 77 createStopWorkIntent(@onNull Context context, @NonNull String workSpecId)78 static Intent createStopWorkIntent(@NonNull Context context, @NonNull String workSpecId) { 79 Intent intent = new Intent(context, SystemAlarmService.class); 80 intent.setAction(ACTION_STOP_WORK); 81 intent.putExtra(KEY_WORKSPEC_ID, workSpecId); 82 return intent; 83 } 84 createConstraintsChangedIntent(@onNull Context context)85 static Intent createConstraintsChangedIntent(@NonNull Context context) { 86 Intent intent = new Intent(context, SystemAlarmService.class); 87 intent.setAction(ACTION_CONSTRAINTS_CHANGED); 88 return intent; 89 } 90 createRescheduleIntent(@onNull Context context)91 static Intent createRescheduleIntent(@NonNull Context context) { 92 Intent intent = new Intent(context, SystemAlarmService.class); 93 intent.setAction(ACTION_RESCHEDULE); 94 return intent; 95 } 96 createExecutionCompletedIntent( @onNull Context context, @NonNull String workSpecId, boolean isSuccessful, boolean needsReschedule)97 static Intent createExecutionCompletedIntent( 98 @NonNull Context context, 99 @NonNull String workSpecId, 100 boolean isSuccessful, 101 boolean needsReschedule) { 102 103 Intent intent = new Intent(context, SystemAlarmService.class); 104 intent.setAction(ACTION_EXECUTION_COMPLETED); 105 intent.putExtra(KEY_WORKSPEC_ID, workSpecId); 106 intent.putExtra(KEY_IS_SUCCESSFUL, isSuccessful); 107 intent.putExtra(KEY_NEEDS_RESCHEDULE, needsReschedule); 108 return intent; 109 } 110 111 // members 112 private final Context mContext; 113 private final Map<String, ExecutionListener> mPendingDelayMet; 114 private final Object mLock; 115 CommandHandler(@onNull Context context)116 CommandHandler(@NonNull Context context) { 117 mContext = context; 118 mPendingDelayMet = new HashMap<>(); 119 mLock = new Object(); 120 } 121 122 @Override onExecuted( @onNull String workSpecId, boolean isSuccessful, boolean needsReschedule)123 public void onExecuted( 124 @NonNull String workSpecId, 125 boolean isSuccessful, 126 boolean needsReschedule) { 127 128 synchronized (mLock) { 129 // This listener is only necessary for knowing when a pending work is complete. 130 // Delegate to the underlying execution listener itself. 131 ExecutionListener listener = mPendingDelayMet.remove(workSpecId); 132 if (listener != null) { 133 listener.onExecuted(workSpecId, isSuccessful, needsReschedule); 134 } 135 } 136 } 137 138 /** 139 * @return <code>true</code> if there is work pending. 140 */ hasPendingCommands()141 boolean hasPendingCommands() { 142 // Needs to be synchronized as this could be checked from 143 // both the command processing thread, as well as the 144 // onExecuted callback. 145 synchronized (mLock) { 146 // If we have pending work being executed on the background 147 // processor - we are not done yet. 148 return !mPendingDelayMet.isEmpty(); 149 } 150 } 151 152 /** 153 * The actual command handler. 154 */ 155 @WorkerThread onHandleIntent( @onNull Intent intent, int startId, @NonNull SystemAlarmDispatcher dispatcher)156 void onHandleIntent( 157 @NonNull Intent intent, 158 int startId, 159 @NonNull SystemAlarmDispatcher dispatcher) { 160 161 String action = intent.getAction(); 162 163 if (ACTION_CONSTRAINTS_CHANGED.equals(action)) { 164 handleConstraintsChanged(intent, startId, dispatcher); 165 } else if (ACTION_RESCHEDULE.equals(action)) { 166 handleReschedule(intent, startId, dispatcher); 167 } else { 168 Bundle extras = intent.getExtras(); 169 if (!hasKeys(extras, KEY_WORKSPEC_ID)) { 170 Log.e(TAG, String.format( 171 "Invalid request for %s, requires %s.", action, KEY_WORKSPEC_ID)); 172 } else { 173 if (ACTION_SCHEDULE_WORK.equals(action)) { 174 handleScheduleWorkIntent(intent, startId, dispatcher); 175 } else if (ACTION_DELAY_MET.equals(action)) { 176 handleDelayMet(intent, startId, dispatcher); 177 } else if (ACTION_STOP_WORK.equals(action)) { 178 handleStopWork(intent, startId, dispatcher); 179 } else if (ACTION_EXECUTION_COMPLETED.equals(action)) { 180 handleExecutionCompleted(intent, startId, dispatcher); 181 } else { 182 Log.w(TAG, String.format("Ignoring intent %s", intent)); 183 } 184 } 185 } 186 } 187 handleScheduleWorkIntent( @onNull Intent intent, int startId, @NonNull SystemAlarmDispatcher dispatcher)188 private void handleScheduleWorkIntent( 189 @NonNull Intent intent, 190 int startId, 191 @NonNull SystemAlarmDispatcher dispatcher) { 192 193 Bundle extras = intent.getExtras(); 194 String workSpecId = extras.getString(KEY_WORKSPEC_ID); 195 Log.d(TAG, String.format("Handling schedule work for %s", workSpecId)); 196 197 WorkManagerImpl workManager = dispatcher.getWorkManager(); 198 WorkDatabase workDatabase = workManager.getWorkDatabase(); 199 WorkSpecDao workSpecDao = workDatabase.workSpecDao(); 200 201 WorkSpec workSpec = workSpecDao.getWorkSpec(workSpecId); 202 long triggerAt = workSpec.calculateNextRunTime(); 203 204 if (!workSpec.hasConstraints()) { 205 Log.d(TAG, String.format("Setting up Alarms for %s", workSpecId)); 206 Alarms.setAlarm(mContext, dispatcher.getWorkManager(), workSpecId, triggerAt); 207 } else { 208 // Schedule an alarm irrespective of whether all constraints matched. 209 Log.d(TAG, String.format("Opportunistically setting an alarm for %s", workSpecId)); 210 Alarms.setAlarm( 211 mContext, 212 dispatcher.getWorkManager(), 213 workSpecId, 214 triggerAt); 215 216 // Schedule an update for constraint proxies 217 // This in turn sets enables us to track changes in constraints 218 Intent constraintsUpdate = CommandHandler.createConstraintsChangedIntent(mContext); 219 dispatcher.postOnMainThread( 220 new SystemAlarmDispatcher.AddRunnable( 221 dispatcher, 222 constraintsUpdate, 223 startId)); 224 } 225 } 226 handleDelayMet( @onNull Intent intent, int startId, @NonNull SystemAlarmDispatcher dispatcher)227 private void handleDelayMet( 228 @NonNull Intent intent, 229 int startId, 230 @NonNull SystemAlarmDispatcher dispatcher) { 231 232 Bundle extras = intent.getExtras(); 233 synchronized (mLock) { 234 String workSpecId = extras.getString(KEY_WORKSPEC_ID); 235 Log.d(TAG, String.format("Handing delay met for %s", workSpecId)); 236 DelayMetCommandHandler delayMetCommandHandler = 237 new DelayMetCommandHandler(mContext, startId, workSpecId, dispatcher); 238 mPendingDelayMet.put(workSpecId, delayMetCommandHandler); 239 delayMetCommandHandler.handleProcessWork(); 240 } 241 } 242 handleStopWork( @onNull Intent intent, int startId, @NonNull SystemAlarmDispatcher dispatcher)243 private void handleStopWork( 244 @NonNull Intent intent, int startId, 245 @NonNull SystemAlarmDispatcher dispatcher) { 246 247 Bundle extras = intent.getExtras(); 248 String workSpecId = extras.getString(KEY_WORKSPEC_ID); 249 Log.d(TAG, String.format("Handing stopWork work for %s", workSpecId)); 250 251 dispatcher.getWorkManager().stopWork(workSpecId); 252 Alarms.cancelAlarm(mContext, dispatcher.getWorkManager(), workSpecId); 253 254 // Notify dispatcher, so it can clean up. 255 dispatcher.onExecuted(workSpecId, false, false /* never reschedule */); 256 } 257 handleConstraintsChanged( @onNull Intent intent, int startId, @NonNull SystemAlarmDispatcher dispatcher)258 private void handleConstraintsChanged( 259 @NonNull Intent intent, int startId, 260 @NonNull SystemAlarmDispatcher dispatcher) { 261 262 Log.d(TAG, String.format("Handling constraints changed %s", intent)); 263 // Constraints changed command handler is synchronous. No cleanup 264 // is necessary. 265 ConstraintsCommandHandler changedCommandHandler = 266 new ConstraintsCommandHandler(mContext, startId, dispatcher); 267 changedCommandHandler.handleConstraintsChanged(); 268 } 269 handleReschedule( @onNull Intent intent, int startId, @NonNull SystemAlarmDispatcher dispatcher)270 private void handleReschedule( 271 @NonNull Intent intent, 272 int startId, 273 @NonNull SystemAlarmDispatcher dispatcher) { 274 275 Log.d(TAG, String.format("Handling reschedule %s, %s", intent, startId)); 276 dispatcher.getWorkManager().rescheduleEligibleWork(); 277 } 278 handleExecutionCompleted( @onNull Intent intent, int startId, @NonNull SystemAlarmDispatcher dispatcher)279 private void handleExecutionCompleted( 280 @NonNull Intent intent, 281 int startId, 282 @NonNull SystemAlarmDispatcher dispatcher) { 283 284 Bundle extras = intent.getExtras(); 285 String workSpecId = extras.getString(KEY_WORKSPEC_ID); 286 boolean isSuccessful = extras.getBoolean(KEY_IS_SUCCESSFUL); 287 boolean needsReschedule = extras.getBoolean(KEY_NEEDS_RESCHEDULE); 288 Log.d(TAG, String.format("Handling onExecutionCompleted %s, %s", intent, startId)); 289 // Delegate onExecuted() to the command handler. 290 onExecuted(workSpecId, isSuccessful, needsReschedule); 291 // Check if we need to stop service. 292 dispatcher.postOnMainThread( 293 new SystemAlarmDispatcher.CheckForCompletionRunnable(dispatcher)); 294 } 295 hasKeys(@ullable Bundle bundle, @NonNull String... keys)296 private static boolean hasKeys(@Nullable Bundle bundle, @NonNull String... keys) { 297 if (bundle == null || bundle.isEmpty()) { 298 return false; 299 } else { 300 for (String key : keys) { 301 if (!bundle.containsKey(key) || bundle.get(key) == null) { 302 return false; 303 } 304 } 305 return true; 306 } 307 } 308 } 309