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
17package androidx.work.impl;
18
19import static androidx.work.State.CANCELLED;
20import static androidx.work.State.ENQUEUED;
21import static androidx.work.State.FAILED;
22import static androidx.work.State.RUNNING;
23import static androidx.work.State.SUCCEEDED;
24
25import android.content.Context;
26import android.support.annotation.NonNull;
27import android.support.annotation.Nullable;
28import android.support.annotation.RestrictTo;
29import android.support.annotation.VisibleForTesting;
30import android.support.annotation.WorkerThread;
31import android.util.Log;
32
33import androidx.work.Configuration;
34import androidx.work.Data;
35import androidx.work.InputMerger;
36import androidx.work.State;
37import androidx.work.Worker;
38import androidx.work.impl.model.DependencyDao;
39import androidx.work.impl.model.WorkSpec;
40import androidx.work.impl.model.WorkSpecDao;
41import androidx.work.impl.model.WorkTagDao;
42import androidx.work.impl.utils.taskexecutor.WorkManagerTaskExecutor;
43
44import java.lang.reflect.Method;
45import java.util.ArrayList;
46import java.util.List;
47import 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)
56public 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
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
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)
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
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
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
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
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
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
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
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
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
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
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
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")
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
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         */
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         */
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         */
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
491        public Builder withWorker(Worker worker) {
492            mWorker = worker;
493            return this;
494        }
495
496        /**
497         * @return The instance of {@link WorkerWrapper}.
498         */
499        public WorkerWrapper build() {
500            return new WorkerWrapper(this);
501        }
502    }
503}
504