1 /*
2  * Copyright 2017 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;
18 
19 import static androidx.work.State.CANCELLED;
20 import static androidx.work.State.ENQUEUED;
21 import static androidx.work.State.FAILED;
22 import static androidx.work.State.RUNNING;
23 import static androidx.work.State.SUCCEEDED;
24 
25 import android.content.Context;
26 import android.support.annotation.NonNull;
27 import android.support.annotation.Nullable;
28 import android.support.annotation.RestrictTo;
29 import android.support.annotation.VisibleForTesting;
30 import android.support.annotation.WorkerThread;
31 import android.util.Log;
32 
33 import androidx.work.Configuration;
34 import androidx.work.Data;
35 import androidx.work.InputMerger;
36 import androidx.work.State;
37 import androidx.work.Worker;
38 import androidx.work.impl.model.DependencyDao;
39 import androidx.work.impl.model.WorkSpec;
40 import androidx.work.impl.model.WorkSpecDao;
41 import androidx.work.impl.model.WorkTagDao;
42 import androidx.work.impl.utils.taskexecutor.WorkManagerTaskExecutor;
43 
44 import java.lang.reflect.Method;
45 import java.util.ArrayList;
46 import java.util.List;
47 import java.util.UUID;
48 
49 /**
50  * A runnable that looks up the {@link WorkSpec} from the database for a given id, instantiates
51  * its Worker, and then calls it.
52  *
53  * @hide
54  */
55 @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
56 public class WorkerWrapper implements Runnable {
57 
58     private static final String TAG = "WorkerWrapper";
59     private Context mAppContext;
60     private String mWorkSpecId;
61     private ExecutionListener mListener;
62     private List<Scheduler> mSchedulers;
63     private Extras.RuntimeExtras mRuntimeExtras;
64     private WorkSpec mWorkSpec;
65     Worker mWorker;
66 
67     private Configuration mConfiguration;
68     private WorkDatabase mWorkDatabase;
69     private WorkSpecDao mWorkSpecDao;
70     private DependencyDao mDependencyDao;
71     private WorkTagDao mWorkTagDao;
72 
73     private volatile boolean mInterrupted;
74 
WorkerWrapper(Builder builder)75     private WorkerWrapper(Builder builder) {
76         mAppContext = builder.mAppContext;
77         mWorkSpecId = builder.mWorkSpecId;
78         mListener = builder.mListener;
79         mSchedulers = builder.mSchedulers;
80         mRuntimeExtras = builder.mRuntimeExtras;
81         mWorker = builder.mWorker;
82 
83         mConfiguration = builder.mConfiguration;
84         mWorkDatabase = builder.mWorkDatabase;
85         mWorkSpecDao = mWorkDatabase.workSpecDao();
86         mDependencyDao = mWorkDatabase.dependencyDao();
87         mWorkTagDao = mWorkDatabase.workTagDao();
88     }
89 
90     @WorkerThread
91     @Override
run()92     public void run() {
93         if (tryCheckForInterruptionAndNotify()) {
94             return;
95         }
96 
97         mWorkSpec = mWorkSpecDao.getWorkSpec(mWorkSpecId);
98         if (mWorkSpec == null) {
99             Log.e(TAG,  String.format("Didn't find WorkSpec for id %s", mWorkSpecId));
100             notifyListener(false, false);
101             return;
102         }
103 
104         // Do a quick check to make sure we don't need to bail out in case this work is already
105         // running, finished, or is blocked.
106         if (mWorkSpec.state != ENQUEUED) {
107             notifyIncorrectStatus();
108             return;
109         }
110 
111         // Merge inputs.  This can be potentially expensive code, so this should not be done inside
112         // a database transaction.
113         Data input;
114         if (mWorkSpec.isPeriodic()) {
115             input = mWorkSpec.input;
116         } else {
117             InputMerger inputMerger = InputMerger.fromClassName(mWorkSpec.inputMergerClassName);
118             if (inputMerger == null) {
119                 Log.e(TAG, String.format("Could not create Input Merger %s",
120                         mWorkSpec.inputMergerClassName));
121                 setFailedAndNotify();
122                 return;
123             }
124             List<Data> inputs = new ArrayList<>();
125             inputs.add(mWorkSpec.input);
126             inputs.addAll(mWorkSpecDao.getInputsFromPrerequisites(mWorkSpecId));
127             input = inputMerger.merge(inputs);
128         }
129 
130         Extras extras = new Extras(
131                 input,
132                 mWorkTagDao.getTagsForWorkSpecId(mWorkSpecId),
133                 mRuntimeExtras,
134                 mWorkSpec.runAttemptCount);
135 
136         // Not always creating a worker here, as the WorkerWrapper.Builder can set a worker override
137         // in test mode.
138         if (mWorker == null) {
139             mWorker = workerFromWorkSpec(mAppContext, mWorkSpec, extras);
140         }
141 
142         if (mWorker == null) {
143             Log.e(TAG, String.format("Could for create Worker %s", mWorkSpec.workerClassName));
144             setFailedAndNotify();
145             return;
146         }
147 
148         // Try to set the work to the running state.  Note that this may fail because another thread
149         // may have modified the DB since we checked last at the top of this function.
150         if (trySetRunning()) {
151             if (tryCheckForInterruptionAndNotify()) {
152                 return;
153             }
154 
155             Worker.Result result;
156             try {
157                 result = mWorker.doWork();
158             } catch (Exception | Error e) {
159                 result = Worker.Result.FAILURE;
160             }
161 
162             try {
163                 mWorkDatabase.beginTransaction();
164                 if (!tryCheckForInterruptionAndNotify()) {
165                     State state = mWorkSpecDao.getState(mWorkSpecId);
166                     if (state == null) {
167                         // state can be null here with a REPLACE on beginUniqueWork().
168                         // Treat it as a failure, and rescheduleAndNotify() will
169                         // turn into a no-op. We still need to notify potential observers
170                         // holding on to wake locks on our behalf.
171                         notifyListener(false, false);
172                     } else if (state == RUNNING) {
173                         handleResult(result);
174                     } else if (!state.isFinished()) {
175                         rescheduleAndNotify();
176                     }
177                     mWorkDatabase.setTransactionSuccessful();
178                 }
179             } finally {
180                 mWorkDatabase.endTransaction();
181             }
182         } else {
183             notifyIncorrectStatus();
184         }
185     }
186 
187     /**
188      * @hide
189      */
190     @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
interrupt(boolean cancelled)191     public void interrupt(boolean cancelled) {
192         mInterrupted = true;
193         // Worker can be null if run() hasn't been called yet.
194         if (mWorker != null) {
195             mWorker.stop(cancelled);
196         }
197     }
198 
notifyIncorrectStatus()199     private void notifyIncorrectStatus() {
200         State status = mWorkSpecDao.getState(mWorkSpecId);
201         if (status == RUNNING) {
202             Log.d(TAG, String.format("Status for %s is RUNNING;"
203                     + "not doing any work and rescheduling for later execution", mWorkSpecId));
204             notifyListener(false, true);
205         } else {
206             Log.e(TAG,
207                     String.format("Status for %s is %s; not doing any work", mWorkSpecId, status));
208             notifyListener(false, false);
209         }
210     }
211 
tryCheckForInterruptionAndNotify()212     private boolean tryCheckForInterruptionAndNotify() {
213         if (mInterrupted) {
214             Log.d(TAG, String.format("Work interrupted for %s", mWorkSpecId));
215             State currentState = mWorkSpecDao.getState(mWorkSpecId);
216             if (currentState == null) {
217                 // This can happen because of a beginUniqueWork(..., REPLACE, ...).  Notify the
218                 // listeners so we can clean up any wake locks, etc.
219                 notifyListener(false, false);
220             } else {
221                 notifyListener(currentState == SUCCEEDED, !currentState.isFinished());
222             }
223             return true;
224         }
225         return false;
226     }
227 
notifyListener(final boolean isSuccessful, final boolean needsReschedule)228     private void notifyListener(final boolean isSuccessful, final boolean needsReschedule) {
229         if (mListener == null) {
230             return;
231         }
232         WorkManagerTaskExecutor.getInstance().postToMainThread(new Runnable() {
233             @Override
234             public void run() {
235                 mListener.onExecuted(mWorkSpecId, isSuccessful, needsReschedule);
236             }
237         });
238     }
239 
handleResult(Worker.Result result)240     private void handleResult(Worker.Result result) {
241         switch (result) {
242             case SUCCESS: {
243                 Log.d(TAG, String.format("Worker result SUCCESS for %s", mWorkSpecId));
244                 if (mWorkSpec.isPeriodic()) {
245                     resetPeriodicAndNotify(true);
246                 } else {
247                     setSucceededAndNotify();
248                 }
249                 break;
250             }
251 
252             case RETRY: {
253                 Log.d(TAG, String.format("Worker result RETRY for %s", mWorkSpecId));
254                 rescheduleAndNotify();
255                 break;
256             }
257 
258             case FAILURE:
259             default: {
260                 Log.d(TAG, String.format("Worker result FAILURE for %s", mWorkSpecId));
261                 if (mWorkSpec.isPeriodic()) {
262                     resetPeriodicAndNotify(false);
263                 } else {
264                     setFailedAndNotify();
265                 }
266             }
267         }
268     }
269 
trySetRunning()270     private boolean trySetRunning() {
271         boolean setToRunning = false;
272         mWorkDatabase.beginTransaction();
273         try {
274             State currentState = mWorkSpecDao.getState(mWorkSpecId);
275             if (currentState == ENQUEUED) {
276                 mWorkSpecDao.setState(RUNNING, mWorkSpecId);
277                 mWorkSpecDao.incrementWorkSpecRunAttemptCount(mWorkSpecId);
278                 mWorkDatabase.setTransactionSuccessful();
279                 setToRunning = true;
280             }
281         } finally {
282             mWorkDatabase.endTransaction();
283         }
284         return setToRunning;
285     }
286 
setFailedAndNotify()287     private void setFailedAndNotify() {
288         mWorkDatabase.beginTransaction();
289         try {
290             recursivelyFailWorkAndDependents(mWorkSpecId);
291 
292             // Try to set the output for the failed work but check if the worker exists; this could
293             // be a permanent error where we couldn't find or create the worker class.
294             if (mWorker != null) {
295                 // Update Data as necessary.
296                 Data output = mWorker.getOutputData();
297                 mWorkSpecDao.setOutput(mWorkSpecId, output);
298             }
299 
300             mWorkDatabase.setTransactionSuccessful();
301         } finally {
302             mWorkDatabase.endTransaction();
303             notifyListener(false, false);
304         }
305 
306         Schedulers.schedule(mConfiguration, mWorkDatabase, mSchedulers);
307     }
308 
recursivelyFailWorkAndDependents(String workSpecId)309     private void recursivelyFailWorkAndDependents(String workSpecId) {
310         List<String> dependentIds = mDependencyDao.getDependentWorkIds(workSpecId);
311         for (String id : dependentIds) {
312             recursivelyFailWorkAndDependents(id);
313         }
314 
315         // Don't fail already cancelled work.
316         if (mWorkSpecDao.getState(workSpecId) != CANCELLED) {
317             mWorkSpecDao.setState(FAILED, workSpecId);
318         }
319     }
320 
rescheduleAndNotify()321     private void rescheduleAndNotify() {
322         mWorkDatabase.beginTransaction();
323         try {
324             mWorkSpecDao.setState(ENQUEUED, mWorkSpecId);
325             // TODO(xbhatnag): Period Start Time is confusing for non-periodic work. Rename.
326             mWorkSpecDao.setPeriodStartTime(mWorkSpecId, System.currentTimeMillis());
327             mWorkDatabase.setTransactionSuccessful();
328         } finally {
329             mWorkDatabase.endTransaction();
330             notifyListener(false, true);
331         }
332     }
333 
resetPeriodicAndNotify(boolean isSuccessful)334     private void resetPeriodicAndNotify(boolean isSuccessful) {
335         mWorkDatabase.beginTransaction();
336         try {
337             long currentPeriodStartTime = mWorkSpec.periodStartTime;
338             long nextPeriodStartTime = currentPeriodStartTime + mWorkSpec.intervalDuration;
339             mWorkSpecDao.setPeriodStartTime(mWorkSpecId, nextPeriodStartTime);
340             mWorkSpecDao.setState(ENQUEUED, mWorkSpecId);
341             mWorkSpecDao.resetWorkSpecRunAttemptCount(mWorkSpecId);
342             mWorkDatabase.setTransactionSuccessful();
343         } finally {
344             mWorkDatabase.endTransaction();
345             notifyListener(isSuccessful, false);
346         }
347     }
348 
setSucceededAndNotify()349     private void setSucceededAndNotify() {
350         mWorkDatabase.beginTransaction();
351         try {
352             mWorkSpecDao.setState(SUCCEEDED, mWorkSpecId);
353 
354             // Update Data as necessary.
355             Data output = mWorker.getOutputData();
356             mWorkSpecDao.setOutput(mWorkSpecId, output);
357 
358             // Unblock Dependencies and set Period Start Time
359             long currentTimeMillis = System.currentTimeMillis();
360             List<String> dependentWorkIds = mDependencyDao.getDependentWorkIds(mWorkSpecId);
361             for (String dependentWorkId : dependentWorkIds) {
362                 if (mDependencyDao.hasCompletedAllPrerequisites(dependentWorkId)) {
363                     Log.d(TAG, String.format("Setting status to enqueued for %s", dependentWorkId));
364                     mWorkSpecDao.setState(ENQUEUED, dependentWorkId);
365                     mWorkSpecDao.setPeriodStartTime(dependentWorkId, currentTimeMillis);
366                 }
367             }
368 
369             mWorkDatabase.setTransactionSuccessful();
370         } finally {
371             mWorkDatabase.endTransaction();
372             notifyListener(true, false);
373         }
374 
375         // This takes of scheduling the dependent workers as they have been marked ENQUEUED.
376         Schedulers.schedule(mConfiguration, mWorkDatabase, mSchedulers);
377     }
378 
workerFromWorkSpec(@onNull Context context, @NonNull WorkSpec workSpec, @NonNull Extras extras)379     static Worker workerFromWorkSpec(@NonNull Context context,
380             @NonNull WorkSpec workSpec,
381             @NonNull Extras extras) {
382         String workerClassName = workSpec.workerClassName;
383         UUID workSpecId = UUID.fromString(workSpec.id);
384         return workerFromClassName(
385                 context,
386                 workerClassName,
387                 workSpecId,
388                 extras);
389     }
390 
391     /**
392      * Creates a {@link Worker} reflectively & initializes the worker.
393      *
394      * @param context         The application {@link Context}
395      * @param workerClassName The fully qualified class name for the {@link Worker}
396      * @param workSpecId      The {@link WorkSpec} identifier
397      * @param extras          The {@link Extras} for the worker
398      * @return The instance of {@link Worker}
399      *
400      * @hide
401      */
402     @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
403     @SuppressWarnings("ClassNewInstance")
workerFromClassName( @onNull Context context, @NonNull String workerClassName, @NonNull UUID workSpecId, @NonNull Extras extras)404     public static Worker workerFromClassName(
405             @NonNull Context context,
406             @NonNull String workerClassName,
407             @NonNull UUID workSpecId,
408             @NonNull Extras extras) {
409         Context appContext = context.getApplicationContext();
410         try {
411             Class<?> clazz = Class.forName(workerClassName);
412             Worker worker = (Worker) clazz.newInstance();
413             Method internalInitMethod = Worker.class.getDeclaredMethod(
414                     "internalInit",
415                     Context.class,
416                     UUID.class,
417                     Extras.class);
418             internalInitMethod.setAccessible(true);
419             internalInitMethod.invoke(
420                     worker,
421                     appContext,
422                     workSpecId,
423                     extras);
424             return worker;
425         } catch (Exception e) {
426             Log.e(TAG, "Trouble instantiating " + workerClassName, e);
427         }
428         return null;
429     }
430 
431     /**
432      * Builder class for {@link WorkerWrapper}
433      * @hide
434      */
435     @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
436     public static class Builder {
437         private Context mAppContext;
438         @Nullable
439         private Worker mWorker;
440         private Configuration mConfiguration;
441         private WorkDatabase mWorkDatabase;
442         private String mWorkSpecId;
443         private ExecutionListener mListener;
444         private List<Scheduler> mSchedulers;
445         private Extras.RuntimeExtras mRuntimeExtras;
446 
Builder(@onNull Context context, @NonNull Configuration configuration, @NonNull WorkDatabase database, @NonNull String workSpecId)447         public Builder(@NonNull Context context,
448                 @NonNull Configuration configuration,
449                 @NonNull WorkDatabase database,
450                 @NonNull String workSpecId) {
451             mAppContext = context.getApplicationContext();
452             mConfiguration = configuration;
453             mWorkDatabase = database;
454             mWorkSpecId = workSpecId;
455         }
456 
457         /**
458          * @param listener The {@link ExecutionListener} which gets notified on completion of the
459          *                 {@link Worker} with the given {@code workSpecId}.
460          * @return The instance of {@link Builder} for chaining.
461          */
withListener(ExecutionListener listener)462         public Builder withListener(ExecutionListener listener) {
463             mListener = listener;
464             return this;
465         }
466 
467         /**
468          * @param schedulers The list of {@link Scheduler}s used for scheduling {@link Worker}s.
469          * @return The instance of {@link Builder} for chaining.
470          */
withSchedulers(List<Scheduler> schedulers)471         public Builder withSchedulers(List<Scheduler> schedulers) {
472             mSchedulers = schedulers;
473             return this;
474         }
475 
476         /**
477          * @param runtimeExtras The {@link Extras.RuntimeExtras} for the {@link Worker}.
478          * @return The instance of {@link Builder} for chaining.
479          */
withRuntimeExtras(Extras.RuntimeExtras runtimeExtras)480         public Builder withRuntimeExtras(Extras.RuntimeExtras runtimeExtras) {
481             mRuntimeExtras = runtimeExtras;
482             return this;
483         }
484 
485         /**
486          * @param worker The instance of {@link Worker} to be executed by {@link WorkerWrapper}.
487          *               Useful in the context of testing.
488          * @return The instance of {@link Builder} for chaining.
489          */
490         @VisibleForTesting
withWorker(Worker worker)491         public Builder withWorker(Worker worker) {
492             mWorker = worker;
493             return this;
494         }
495 
496         /**
497          * @return The instance of {@link WorkerWrapper}.
498          */
build()499         public WorkerWrapper build() {
500             return new WorkerWrapper(this);
501         }
502     }
503 }
504