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 
23 import androidx.annotation.RestrictTo;
24 import androidx.annotation.WorkerThread;
25 import androidx.work.Clock;
26 import androidx.work.Logger;
27 import androidx.work.impl.ExecutionListener;
28 import androidx.work.impl.StartStopToken;
29 import androidx.work.impl.StartStopTokens;
30 import androidx.work.impl.WorkDatabase;
31 import androidx.work.impl.WorkManagerImpl;
32 import androidx.work.impl.model.WorkGenerationalId;
33 import androidx.work.impl.model.WorkSpec;
34 import androidx.work.impl.model.WorkSpecDao;
35 
36 import org.jspecify.annotations.NonNull;
37 import org.jspecify.annotations.Nullable;
38 
39 import java.util.ArrayList;
40 import java.util.HashMap;
41 import java.util.List;
42 import java.util.Map;
43 
44 /**
45  * The command handler used by {@link SystemAlarmDispatcher}.
46  *
47  */
48 @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
49 public class CommandHandler implements ExecutionListener {
50 
51     private static final String TAG = Logger.tagWithPrefix("CommandHandler");
52 
53     // actions
54     static final String ACTION_SCHEDULE_WORK = "ACTION_SCHEDULE_WORK";
55     static final String ACTION_DELAY_MET = "ACTION_DELAY_MET";
56     static final String ACTION_STOP_WORK = "ACTION_STOP_WORK";
57     static final String ACTION_CONSTRAINTS_CHANGED = "ACTION_CONSTRAINTS_CHANGED";
58     static final String ACTION_RESCHEDULE = "ACTION_RESCHEDULE";
59     static final String ACTION_EXECUTION_COMPLETED = "ACTION_EXECUTION_COMPLETED";
60 
61     // keys
62     private static final String KEY_WORKSPEC_ID = "KEY_WORKSPEC_ID";
63     private static final String KEY_WORKSPEC_GENERATION = "KEY_WORKSPEC_GENERATION";
64     private static final String KEY_NEEDS_RESCHEDULE = "KEY_NEEDS_RESCHEDULE";
65 
66     // constants
67     static final long WORK_PROCESSING_TIME_IN_MS = 10 * 60 * 1000L;
68 
69     // utilities
createScheduleWorkIntent(@onNull Context context, @NonNull WorkGenerationalId id)70     static Intent createScheduleWorkIntent(@NonNull Context context,
71             @NonNull WorkGenerationalId id) {
72         Intent intent = new Intent(context, SystemAlarmService.class);
73         intent.setAction(ACTION_SCHEDULE_WORK);
74         return writeWorkGenerationalId(intent, id);
75     }
76 
writeWorkGenerationalId(@onNull Intent intent, @NonNull WorkGenerationalId id)77     private static Intent writeWorkGenerationalId(@NonNull Intent intent,
78             @NonNull WorkGenerationalId id) {
79         intent.putExtra(KEY_WORKSPEC_ID, id.getWorkSpecId());
80         intent.putExtra(KEY_WORKSPEC_GENERATION, id.getGeneration());
81         return intent;
82     }
83 
readWorkGenerationalId(@onNull Intent intent)84     static WorkGenerationalId readWorkGenerationalId(@NonNull Intent intent) {
85         return new WorkGenerationalId(intent.getStringExtra(KEY_WORKSPEC_ID),
86                 intent.getIntExtra(KEY_WORKSPEC_GENERATION, 0));
87     }
88 
createDelayMetIntent(@onNull Context context, @NonNull WorkGenerationalId id)89     static Intent createDelayMetIntent(@NonNull Context context, @NonNull WorkGenerationalId id) {
90         Intent intent = new Intent(context, SystemAlarmService.class);
91         intent.setAction(ACTION_DELAY_MET);
92         return writeWorkGenerationalId(intent, id);
93     }
94 
createStopWorkIntent(@onNull Context context, @NonNull String workSpecId)95     static Intent createStopWorkIntent(@NonNull Context context, @NonNull String workSpecId) {
96         Intent intent = new Intent(context, SystemAlarmService.class);
97         intent.setAction(ACTION_STOP_WORK);
98         intent.putExtra(KEY_WORKSPEC_ID, workSpecId);
99         return intent;
100     }
createStopWorkIntent(@onNull Context context, @NonNull WorkGenerationalId id)101     static Intent createStopWorkIntent(@NonNull Context context, @NonNull WorkGenerationalId id) {
102         Intent intent = new Intent(context, SystemAlarmService.class);
103         intent.setAction(ACTION_STOP_WORK);
104         return writeWorkGenerationalId(intent, id);
105     }
106 
createConstraintsChangedIntent(@onNull Context context)107     static Intent createConstraintsChangedIntent(@NonNull Context context) {
108         Intent intent = new Intent(context, SystemAlarmService.class);
109         intent.setAction(ACTION_CONSTRAINTS_CHANGED);
110         return intent;
111     }
112 
createRescheduleIntent(@onNull Context context)113     static Intent createRescheduleIntent(@NonNull Context context) {
114         Intent intent = new Intent(context, SystemAlarmService.class);
115         intent.setAction(ACTION_RESCHEDULE);
116         return intent;
117     }
118 
createExecutionCompletedIntent( @onNull Context context, @NonNull WorkGenerationalId id, boolean needsReschedule)119     static Intent createExecutionCompletedIntent(
120             @NonNull Context context,
121             @NonNull WorkGenerationalId id,
122             boolean needsReschedule) {
123         Intent intent = new Intent(context, SystemAlarmService.class);
124         intent.setAction(ACTION_EXECUTION_COMPLETED);
125         intent.putExtra(KEY_NEEDS_RESCHEDULE, needsReschedule);
126         return writeWorkGenerationalId(intent, id);
127     }
128 
129     // members
130     private final Context mContext;
131     private final Map<WorkGenerationalId, DelayMetCommandHandler> mPendingDelayMet;
132     private final Object mLock;
133     private final Clock mClock;
134     private final StartStopTokens mStartStopTokens;
135 
CommandHandler(@onNull Context context, Clock clock, @NonNull StartStopTokens startStopTokens)136     CommandHandler(@NonNull Context context, Clock clock,
137             @NonNull StartStopTokens startStopTokens) {
138         mContext = context;
139         mClock = clock;
140         mStartStopTokens = startStopTokens;
141         mPendingDelayMet = new HashMap<>();
142         mLock = new Object();
143     }
144 
145     @Override
onExecuted(@onNull WorkGenerationalId id, boolean needsReschedule)146     public void onExecuted(@NonNull WorkGenerationalId id, boolean needsReschedule) {
147         synchronized (mLock) {
148             // This listener is only necessary for knowing when a pending work is complete.
149             // Delegate to the underlying execution listener itself.
150             DelayMetCommandHandler listener = mPendingDelayMet.remove(id);
151             mStartStopTokens.remove(id);
152             if (listener != null) {
153                 listener.onExecuted(needsReschedule);
154             }
155         }
156     }
157 
158     /**
159      * @return <code>true</code> if there is work pending.
160      */
hasPendingCommands()161     boolean hasPendingCommands() {
162         // Needs to be synchronized as this could be checked from
163         // both the command processing thread, as well as the
164         // onExecuted callback.
165         synchronized (mLock) {
166             // If we have pending work being executed on the background
167             // processor - we are not done yet.
168             return !mPendingDelayMet.isEmpty();
169         }
170     }
171 
172     /**
173      * The actual command handler.
174      */
175     @WorkerThread
onHandleIntent( @onNull Intent intent, int startId, @NonNull SystemAlarmDispatcher dispatcher)176     void onHandleIntent(
177             @NonNull Intent intent,
178             int startId,
179             @NonNull SystemAlarmDispatcher dispatcher) {
180 
181         String action = intent.getAction();
182 
183         if (ACTION_CONSTRAINTS_CHANGED.equals(action)) {
184             handleConstraintsChanged(intent, startId, dispatcher);
185         } else if (ACTION_RESCHEDULE.equals(action)) {
186             handleReschedule(intent, startId, dispatcher);
187         } else {
188             Bundle extras = intent.getExtras();
189             if (!hasKeys(extras, KEY_WORKSPEC_ID)) {
190                 Logger.get().error(TAG,
191                         "Invalid request for " + action + " , requires " + KEY_WORKSPEC_ID + " .");
192             } else {
193                 if (ACTION_SCHEDULE_WORK.equals(action)) {
194                     handleScheduleWorkIntent(intent, startId, dispatcher);
195                 } else if (ACTION_DELAY_MET.equals(action)) {
196                     handleDelayMet(intent, startId, dispatcher);
197                 } else if (ACTION_STOP_WORK.equals(action)) {
198                     handleStopWork(intent, dispatcher);
199                 } else if (ACTION_EXECUTION_COMPLETED.equals(action)) {
200                     handleExecutionCompleted(intent, startId);
201                 } else {
202                     Logger.get().warning(TAG, "Ignoring intent " + intent);
203                 }
204             }
205         }
206     }
207 
handleScheduleWorkIntent( @onNull Intent intent, int startId, @NonNull SystemAlarmDispatcher dispatcher)208     private void handleScheduleWorkIntent(
209             @NonNull Intent intent,
210             int startId,
211             @NonNull SystemAlarmDispatcher dispatcher) {
212 
213         WorkGenerationalId id = readWorkGenerationalId(intent);
214         Logger.get().debug(TAG, "Handling schedule work for " + id);
215 
216         WorkManagerImpl workManager = dispatcher.getWorkManager();
217         WorkDatabase workDatabase = workManager.getWorkDatabase();
218         workDatabase.beginTransaction();
219 
220         try {
221             WorkSpecDao workSpecDao = workDatabase.workSpecDao();
222             WorkSpec workSpec = workSpecDao.getWorkSpec(id.getWorkSpecId());
223 
224             // It is possible that this WorkSpec got cancelled/pruned since this isn't part of
225             // the same database transaction as marking it enqueued (for example, if we using
226             // any of the synchronous operations).  For now, handle this gracefully by exiting
227             // the loop.  When we plumb ListenableFutures all the way through, we can remove the
228             // *sync methods and return ListenableFutures, which will block on an operation on
229             // the background task thread so all database operations happen on the same thread.
230             // See b/114705286.
231             if (workSpec == null) {
232                 Logger.get().warning(TAG,
233                         "Skipping scheduling " + id + " because it's no longer in "
234                         + "the DB");
235                 return;
236             } else if (workSpec.state.isFinished()) {
237                 // We need to schedule the Alarms, even when the Worker is RUNNING. This is because
238                 // if the process gets killed, the Alarm is necessary to pick up the execution of
239                 // Work.
240                 Logger.get().warning(TAG,
241                         "Skipping scheduling " + id + "because it is finished.");
242                 return;
243             }
244 
245             // Note: The first instance of PeriodicWorker getting scheduled will set an alarm in the
246             // past. This is because periodStartTime = 0.
247             long triggerAt = workSpec.calculateNextRunTime();
248 
249             if (!workSpec.hasConstraints()) {
250                 Logger.get().debug(TAG,
251                         "Setting up Alarms for " + id + "at " + triggerAt);
252                 Alarms.setAlarm(mContext, workDatabase, id, triggerAt);
253             } else {
254                 // Schedule an alarm irrespective of whether all constraints matched.
255                 Logger.get().debug(TAG,
256                         "Opportunistically setting an alarm for " + id + "at " + triggerAt);
257                 Alarms.setAlarm(
258                         mContext,
259                         workDatabase,
260                         id,
261                         triggerAt);
262 
263                 // Schedule an update for constraint proxies
264                 // This in turn sets enables us to track changes in constraints
265                 Intent constraintsUpdate = CommandHandler.createConstraintsChangedIntent(mContext);
266                 dispatcher.getTaskExecutor().getMainThreadExecutor().execute(
267                         new SystemAlarmDispatcher.AddRunnable(
268                                 dispatcher,
269                                 constraintsUpdate,
270                                 startId));
271             }
272 
273             workDatabase.setTransactionSuccessful();
274         } finally {
275             workDatabase.endTransaction();
276         }
277     }
278 
handleDelayMet( @onNull Intent intent, int startId, @NonNull SystemAlarmDispatcher dispatcher)279     private void handleDelayMet(
280             @NonNull Intent intent,
281             int startId,
282             @NonNull SystemAlarmDispatcher dispatcher) {
283 
284         synchronized (mLock) {
285             WorkGenerationalId id = readWorkGenerationalId(intent);
286             Logger.get().debug(TAG, "Handing delay met for " + id);
287 
288             // Check to see if we are already handling an ACTION_DELAY_MET for the WorkSpec.
289             // If we are, then there is nothing for us to do.
290             if (!mPendingDelayMet.containsKey(id)) {
291                 DelayMetCommandHandler delayMetCommandHandler =
292                         new DelayMetCommandHandler(mContext, startId,
293                                 dispatcher, mStartStopTokens.tokenFor(id));
294                 mPendingDelayMet.put(id, delayMetCommandHandler);
295                 delayMetCommandHandler.handleProcessWork();
296             } else {
297                 Logger.get().debug(TAG, "WorkSpec " + id
298                         + " is is already being handled for ACTION_DELAY_MET");
299             }
300         }
301     }
302 
handleStopWork( @onNull Intent intent, @NonNull SystemAlarmDispatcher dispatcher)303     private void handleStopWork(
304             @NonNull Intent intent,
305             @NonNull SystemAlarmDispatcher dispatcher) {
306 
307         Bundle extras = intent.getExtras();
308         String workSpecId = extras.getString(KEY_WORKSPEC_ID);
309         List<StartStopToken> tokens;
310         if (extras.containsKey(KEY_WORKSPEC_GENERATION)) {
311             int generation = extras.getInt(KEY_WORKSPEC_GENERATION);
312             tokens = new ArrayList<>(1);
313             StartStopToken id = mStartStopTokens.remove(
314                     new WorkGenerationalId(workSpecId, generation));
315             if (id != null) {
316                 tokens.add(id);
317             }
318         } else {
319             tokens = mStartStopTokens.remove(workSpecId);
320         }
321         for (StartStopToken token: tokens) {
322             Logger.get().debug(TAG, "Handing stopWork work for " + workSpecId);
323             dispatcher.getWorkerLauncher().stopWork(token);
324             Alarms.cancelAlarm(mContext,
325                     dispatcher.getWorkManager().getWorkDatabase(), token.getId());
326 
327             // Notify dispatcher, so it can clean up.
328             dispatcher.onExecuted(token.getId(), false /* never reschedule */);
329         }
330     }
331 
handleConstraintsChanged( @onNull Intent intent, int startId, @NonNull SystemAlarmDispatcher dispatcher)332     private void handleConstraintsChanged(
333             @NonNull Intent intent, int startId,
334             @NonNull SystemAlarmDispatcher dispatcher) {
335 
336         Logger.get().debug(TAG, "Handling constraints changed " + intent);
337         // Constraints changed command handler is synchronous. No cleanup
338         // is necessary.
339         ConstraintsCommandHandler changedCommandHandler =
340                 new ConstraintsCommandHandler(mContext, mClock, startId, dispatcher);
341         changedCommandHandler.handleConstraintsChanged();
342     }
343 
handleReschedule( @onNull Intent intent, int startId, @NonNull SystemAlarmDispatcher dispatcher)344     private void handleReschedule(
345             @NonNull Intent intent,
346             int startId,
347             @NonNull SystemAlarmDispatcher dispatcher) {
348 
349         Logger.get().debug(TAG, "Handling reschedule " + intent + ", " + startId);
350         dispatcher.getWorkManager().rescheduleEligibleWork();
351     }
352 
handleExecutionCompleted( @onNull Intent intent, int startId)353     private void handleExecutionCompleted(
354             @NonNull Intent intent,
355             int startId) {
356         WorkGenerationalId id = readWorkGenerationalId(intent);
357         boolean needsReschedule = intent.getExtras().getBoolean(KEY_NEEDS_RESCHEDULE);
358         Logger.get().debug(
359                 TAG,
360                 "Handling onExecutionCompleted " + intent + ", " + startId);
361         // Delegate onExecuted() to the command handler.
362         onExecuted(id, needsReschedule);
363     }
364 
365     @SuppressWarnings("deprecation")
hasKeys(@ullable Bundle bundle, String @NonNull ... keys)366     private static boolean hasKeys(@Nullable Bundle bundle, String @NonNull ... keys) {
367         if (bundle == null || bundle.isEmpty()) {
368             return false;
369         } else {
370             for (String key : keys) {
371                 if (bundle.get(key) == null) {
372                     return false;
373                 }
374             }
375             return true;
376         }
377     }
378 }
379