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
17package androidx.work.impl.background.systemalarm;
18
19import android.content.Context;
20import android.content.Intent;
21import android.os.PowerManager;
22import android.support.annotation.NonNull;
23import android.support.annotation.Nullable;
24import android.support.annotation.RestrictTo;
25import android.support.annotation.WorkerThread;
26import android.util.Log;
27
28import androidx.work.impl.ExecutionListener;
29import androidx.work.impl.constraints.WorkConstraintsCallback;
30import androidx.work.impl.constraints.WorkConstraintsTracker;
31import androidx.work.impl.model.WorkSpec;
32import androidx.work.impl.utils.WakeLocks;
33
34import java.util.Collections;
35import 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)
44public 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
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
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
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
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
129    public void onAllConstraintsNotMet(@NonNull List<String> ignored) {
130        stopWork();
131    }
132
133    @WorkerThread
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
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
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