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.PowerManager;
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.constraints.WorkConstraintsCallback;
30 import androidx.work.impl.constraints.WorkConstraintsTracker;
31 import androidx.work.impl.model.WorkSpec;
32 import androidx.work.impl.utils.WakeLocks;
33 
34 import java.util.Collections;
35 import java.util.List;
36 
37 /**
38  * This is a command handler which attempts to run a work spec given its id.
39  * Also handles constraints gracefully.
40  *
41  * @hide
42  */
43 @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
44 public class DelayMetCommandHandler implements
45         WorkConstraintsCallback,
46         ExecutionListener,
47         WorkTimer.TimeLimitExceededListener {
48 
49     private static final String TAG = "DelayMetCommandHandler";
50 
51     private final Context mContext;
52     private final int mStartId;
53     private final String mWorkSpecId;
54     private final SystemAlarmDispatcher mDispatcher;
55     private final WorkConstraintsTracker mWorkConstraintsTracker;
56     private final Object mLock;
57     private boolean mHasPendingStopWorkCommand;
58 
59     @Nullable private PowerManager.WakeLock mWakeLock;
60     private boolean mHasConstraints;
61 
DelayMetCommandHandler( @onNull Context context, int startId, @NonNull String workSpecId, @NonNull SystemAlarmDispatcher dispatcher)62     DelayMetCommandHandler(
63             @NonNull Context context,
64             int startId,
65             @NonNull String workSpecId,
66             @NonNull SystemAlarmDispatcher dispatcher) {
67 
68         mContext = context;
69         mStartId = startId;
70         mDispatcher = dispatcher;
71         mWorkSpecId = workSpecId;
72         mWorkConstraintsTracker = new WorkConstraintsTracker(mContext, this);
73         mHasConstraints = false;
74         mHasPendingStopWorkCommand = false;
75         mLock = new Object();
76     }
77 
78     @Override
onAllConstraintsMet(@onNull List<String> ignored)79     public void onAllConstraintsMet(@NonNull List<String> ignored) {
80         Log.d(TAG, String.format("onAllConstraintsMet for %s", mWorkSpecId));
81         // Constraints met, schedule execution
82 
83         // Not using WorkManagerImpl#startWork() here because we need to know if the processor
84         // actually enqueued the work here.
85         // TODO(rahulrav@) Once WorkManagerImpl provides a callback for acknowledging if
86         // work was enqueued, call WorkManagerImpl#startWork().
87         boolean isEnqueued = mDispatcher.getProcessor().startWork(mWorkSpecId);
88 
89         if (isEnqueued) {
90             // setup timers to enforce quotas on workers that have
91             // been enqueued
92             mDispatcher.getWorkTimer()
93                     .startTimer(mWorkSpecId, CommandHandler.WORK_PROCESSING_TIME_IN_MS, this);
94         } else {
95             // if we did not actually enqueue the work, it was enqueued before
96             // cleanUp and pretend this never happened.
97             cleanUp();
98         }
99     }
100 
101     @Override
onExecuted( @onNull String workSpecId, boolean isSuccessful, boolean needsReschedule)102     public void onExecuted(
103             @NonNull String workSpecId,
104             boolean isSuccessful,
105             boolean needsReschedule) {
106 
107         Log.d(TAG, String.format(
108                 "onExecuted %s, %s, %s", workSpecId, isSuccessful, needsReschedule));
109 
110         cleanUp();
111 
112         if (mHasConstraints) {
113             // The WorkSpec had constraints. Once the execution of the worker is complete,
114             // we might need to disable constraint proxies which were previously enabled for
115             // this WorkSpec. Hence, trigger a constraints changed command.
116             Intent intent = CommandHandler.createConstraintsChangedIntent(mContext);
117             mDispatcher.postOnMainThread(
118                     new SystemAlarmDispatcher.AddRunnable(mDispatcher, intent, mStartId));
119         }
120     }
121 
122     @Override
onTimeLimitExceeded(@onNull String workSpecId)123     public void onTimeLimitExceeded(@NonNull String workSpecId) {
124         Log.d(TAG, String.format("Exceeded time limits on execution for %s", workSpecId));
125         stopWork();
126     }
127 
128     @Override
onAllConstraintsNotMet(@onNull List<String> ignored)129     public void onAllConstraintsNotMet(@NonNull List<String> ignored) {
130         stopWork();
131     }
132 
133     @WorkerThread
handleProcessWork()134     void handleProcessWork() {
135         mWakeLock = WakeLocks.newWakeLock(
136                 mContext,
137                 String.format("%s (%s)", mWorkSpecId, mStartId));
138         Log.d(TAG, String.format("Acquiring wakelock %s for WorkSpec %s", mWakeLock, mWorkSpecId));
139         mWakeLock.acquire();
140 
141         WorkSpec workSpec = mDispatcher.getWorkManager()
142                 .getWorkDatabase()
143                 .workSpecDao()
144                 .getWorkSpec(mWorkSpecId);
145 
146         // Keep track of whether the WorkSpec had constraints. This is useful for updating the
147         // state of constraint proxies when onExecuted().
148         mHasConstraints = workSpec.hasConstraints();
149 
150         if (!mHasConstraints) {
151             Log.d(TAG, String.format("No constraints for %s", mWorkSpecId));
152             onAllConstraintsMet(Collections.singletonList(mWorkSpecId));
153         } else {
154             // Allow tracker to report constraint changes
155             mWorkConstraintsTracker.replace(Collections.singletonList(workSpec));
156         }
157     }
158 
stopWork()159     private void stopWork() {
160         // No need to release the wake locks here. The stopWork command will eventually call
161         // onExecuted() if there is a corresponding pending delay met command handler; which in
162         // turn calls cleanUp().
163 
164         // Needs to be synchronized, as the stopWork() request can potentially come from the
165         // WorkTimer thread as well as the command executor service in SystemAlarmDispatcher.
166         synchronized (mLock) {
167             if (!mHasPendingStopWorkCommand) {
168                 Log.d(TAG, String.format("Stopping work for workspec %s", mWorkSpecId));
169                 Intent stopWork = CommandHandler.createStopWorkIntent(mContext, mWorkSpecId);
170                 mDispatcher.postOnMainThread(
171                         new SystemAlarmDispatcher.AddRunnable(mDispatcher, stopWork, mStartId));
172                 // There are cases where the work may not have been enqueued at all, and therefore
173                 // the processor is completely unaware of such a workSpecId in which case a
174                 // reschedule should not happen. For e.g. DELAY_MET when constraints are not met,
175                 // should not result in a reschedule.
176                 if (mDispatcher.getProcessor().isEnqueued(mWorkSpecId)) {
177                     Log.d(TAG, String.format("WorkSpec %s needs to be rescheduled", mWorkSpecId));
178                     Intent reschedule = CommandHandler.createScheduleWorkIntent(mContext,
179                             mWorkSpecId);
180                     mDispatcher.postOnMainThread(
181                             new SystemAlarmDispatcher.AddRunnable(mDispatcher, reschedule,
182                                     mStartId));
183                 } else {
184                     Log.d(TAG, String.format(
185                             "Processor does not have WorkSpec %s. No need to reschedule ",
186                             mWorkSpecId));
187                 }
188                 mHasPendingStopWorkCommand = true;
189             } else {
190                 Log.d(TAG, String.format("Already stopped work for %s", mWorkSpecId));
191             }
192         }
193     }
194 
cleanUp()195     private void cleanUp() {
196         // cleanUp() may occur from one of 2 threads.
197         // * In the call to bgProcessor.startWork() returns false,
198         //   it probably means that the worker is already being processed
199         //   so we just need to call cleanUp to release wakelocks on the command processor thread.
200         // * It could also happen on the onExecutionCompleted() pass of the bgProcessor.
201         // To avoid calling mWakeLock.release() twice, we are synchronizing here.
202         synchronized (mLock) {
203             // stop timers
204             mDispatcher.getWorkTimer().stopTimer(mWorkSpecId);
205 
206             // release wake locks
207             if (mWakeLock != null && mWakeLock.isHeld()) {
208                 Log.d(TAG, String.format(
209                         "Releasing wakelock %s for WorkSpec %s", mWakeLock, mWorkSpecId));
210                 mWakeLock.release();
211             }
212         }
213     }
214 }
215