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