WorkSpec.java revision 4401f79faa35469c257262b85b1cdc808c266c4b
1/*
2 * Copyright (C) 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.model;
18
19import static androidx.work.BaseWork.MAX_BACKOFF_MILLIS;
20import static androidx.work.BaseWork.MIN_BACKOFF_MILLIS;
21import static androidx.work.PeriodicWork.MIN_PERIODIC_FLEX_MILLIS;
22import static androidx.work.PeriodicWork.MIN_PERIODIC_INTERVAL_MILLIS;
23import static androidx.work.State.ENQUEUED;
24
25import android.arch.core.util.Function;
26import android.arch.persistence.room.ColumnInfo;
27import android.arch.persistence.room.Embedded;
28import android.arch.persistence.room.Entity;
29import android.arch.persistence.room.PrimaryKey;
30import android.arch.persistence.room.Relation;
31import android.support.annotation.NonNull;
32import android.support.annotation.RestrictTo;
33
34import androidx.work.Arguments;
35import androidx.work.BackoffPolicy;
36import androidx.work.BaseWork;
37import androidx.work.Constraints;
38import androidx.work.State;
39import androidx.work.WorkStatus;
40import androidx.work.impl.logger.Logger;
41
42import java.util.ArrayList;
43import java.util.List;
44
45/**
46 * Stores information about a logical unit of work.
47 *
48 * @hide
49 */
50@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
51@Entity
52public class WorkSpec {
53    private static final String TAG = "WorkSpec";
54
55    @ColumnInfo(name = "id")
56    @PrimaryKey
57    @NonNull
58    public String id;
59
60    @ColumnInfo(name = "state")
61    @NonNull
62    public State state = ENQUEUED;
63
64    @ColumnInfo(name = "worker_class_name")
65    @NonNull
66    public String workerClassName;
67
68    @ColumnInfo(name = "input_merger_class_name")
69    public String inputMergerClassName;
70
71    @ColumnInfo(name = "arguments")
72    @NonNull
73    public Arguments arguments = Arguments.EMPTY;
74
75    @ColumnInfo(name = "output")
76    @NonNull
77    public Arguments output = Arguments.EMPTY;
78
79    @ColumnInfo(name = "initial_delay")
80    public long initialDelay;
81
82    @ColumnInfo(name = "interval_duration")
83    public long intervalDuration;
84
85    @ColumnInfo(name = "flex_duration")
86    public long flexDuration;
87
88    @Embedded
89    @NonNull
90    public Constraints constraints = Constraints.NONE;
91
92    @ColumnInfo(name = "run_attempt_count")
93    public int runAttemptCount;
94
95    @ColumnInfo(name = "backoff_policy")
96    @NonNull
97    public BackoffPolicy backoffPolicy = BackoffPolicy.EXPONENTIAL;
98
99    @ColumnInfo(name = "backoff_delay_duration")
100    public long backoffDelayDuration = BaseWork.DEFAULT_BACKOFF_DELAY_MILLIS;
101
102    /**
103     * For one-off work, this is the time that the work was unblocked by prerequisites.
104     * For periodic work, this is the time that the period started.
105     */
106    @ColumnInfo(name = "period_start_time")
107    public long periodStartTime;
108
109    @ColumnInfo(name = "minimum_retention_duration")
110    public long minimumRetentionDuration;
111
112    public WorkSpec(@NonNull String id, @NonNull String workerClassName) {
113        this.id = id;
114        this.workerClassName = workerClassName;
115    }
116
117    /**
118     * @param backoffDelayDuration The backoff delay duration in milliseconds
119     */
120    public void setBackoffDelayDuration(long backoffDelayDuration) {
121        if (backoffDelayDuration > MAX_BACKOFF_MILLIS) {
122            Logger.warn(TAG, "Backoff delay duration exceeds maximum value");
123            backoffDelayDuration = MAX_BACKOFF_MILLIS;
124        }
125        if (backoffDelayDuration < MIN_BACKOFF_MILLIS) {
126            Logger.warn(TAG, "Backoff delay duration less than minimum value");
127            backoffDelayDuration = MIN_BACKOFF_MILLIS;
128        }
129        this.backoffDelayDuration = backoffDelayDuration;
130    }
131
132
133    public boolean isPeriodic() {
134        return intervalDuration != 0L;
135    }
136
137    public boolean isBackedOff() {
138        return state == ENQUEUED && runAttemptCount > 0;
139    }
140
141    /**
142     * Sets the periodic interval for this unit of work.
143     *
144     * @param intervalDuration The interval in milliseconds
145     */
146    public void setPeriodic(long intervalDuration) {
147        if (intervalDuration < MIN_PERIODIC_INTERVAL_MILLIS) {
148            Logger.warn(TAG, "Interval duration lesser than minimum allowed value; Changed to %s",
149                    MIN_PERIODIC_INTERVAL_MILLIS);
150            intervalDuration = MIN_PERIODIC_INTERVAL_MILLIS;
151        }
152        setPeriodic(intervalDuration, intervalDuration);
153    }
154
155    /**
156     * Sets the periodic interval for this unit of work.
157     *
158     * @param intervalDuration The interval in milliseconds
159     * @param flexDuration The flex duration in milliseconds
160     */
161    public void setPeriodic(long intervalDuration, long flexDuration) {
162        if (intervalDuration < MIN_PERIODIC_INTERVAL_MILLIS) {
163            Logger.warn(TAG, "Interval duration lesser than minimum allowed value; Changed to %s",
164                    MIN_PERIODIC_INTERVAL_MILLIS);
165            intervalDuration = MIN_PERIODIC_INTERVAL_MILLIS;
166        }
167        if (flexDuration < MIN_PERIODIC_FLEX_MILLIS) {
168            Logger.warn(TAG, "Flex duration lesser than minimum allowed value; Changed to %s",
169                    MIN_PERIODIC_FLEX_MILLIS);
170            flexDuration = MIN_PERIODIC_FLEX_MILLIS;
171        }
172        if (flexDuration > intervalDuration) {
173            Logger.warn(TAG, "Flex duration greater than interval duration; Changed to %s",
174                    intervalDuration);
175            flexDuration = intervalDuration;
176        }
177        this.intervalDuration = intervalDuration;
178        this.flexDuration = flexDuration;
179    }
180
181    /**
182     * Calculates the UTC time at which this {@link WorkSpec} should be allowed to run.
183     * This method accounts for work that is backed off or periodic.
184     *
185     * If Backoff Policy is set to {@link BackoffPolicy#EXPONENTIAL}, then delay
186     * increases at an exponential rate with respect to the run attempt count and is capped at
187     * {@link BaseWork#MAX_BACKOFF_MILLIS}.
188     *
189     * If Backoff Policy is set to {@link BackoffPolicy#LINEAR}, then delay
190     * increases at an linear rate with respect to the run attempt count and is capped at
191     * {@link BaseWork#MAX_BACKOFF_MILLIS}.
192     *
193     * Based on {@see https://android.googlesource.com/platform/frameworks/base/+/master/services/core/java/com/android/server/job/JobSchedulerService.java#1125}
194     *
195     * Note that this runtime is for WorkManager internal use and may not match what the OS
196     * considers to be the next runtime.
197     *
198     * For jobs with constraints, this represents the earliest time at which constraints
199     * should be monitored for this work.
200     *
201     * For jobs without constraints, this represents the earliest time at which this work is
202     * allowed to run.
203     *
204     * @return UTC time at which this {@link WorkSpec} should be allowed to run.
205     */
206    public long calculateNextRunTime() {
207        if (isBackedOff()) {
208            boolean isLinearBackoff = (backoffPolicy == BackoffPolicy.LINEAR);
209            long delay = isLinearBackoff ? (backoffDelayDuration * runAttemptCount)
210                    : (long) Math.scalb(backoffDelayDuration, runAttemptCount - 1);
211            return periodStartTime + Math.min(BaseWork.MAX_BACKOFF_MILLIS, delay);
212        } else if (isPeriodic()) {
213            return periodStartTime + intervalDuration - flexDuration;
214        } else {
215            return periodStartTime + initialDelay;
216        }
217    }
218
219    /**
220     * @return <code>true</code> if the {@link WorkSpec} has constraints.
221     */
222    public boolean hasConstraints() {
223        return !Constraints.NONE.equals(constraints);
224    }
225
226    @Override
227    public boolean equals(Object o) {
228        if (this == o) return true;
229        if (o == null || getClass() != o.getClass()) return false;
230
231        WorkSpec workSpec = (WorkSpec) o;
232
233        if (initialDelay != workSpec.initialDelay) return false;
234        if (intervalDuration != workSpec.intervalDuration) return false;
235        if (flexDuration != workSpec.flexDuration) return false;
236        if (runAttemptCount != workSpec.runAttemptCount) return false;
237        if (backoffDelayDuration != workSpec.backoffDelayDuration) return false;
238        if (periodStartTime != workSpec.periodStartTime) return false;
239        if (minimumRetentionDuration != workSpec.minimumRetentionDuration) return false;
240        if (!id.equals(workSpec.id)) return false;
241        if (state != workSpec.state) return false;
242        if (!workerClassName.equals(workSpec.workerClassName)) return false;
243        if (inputMergerClassName != null ? !inputMergerClassName.equals(
244                workSpec.inputMergerClassName) : workSpec.inputMergerClassName != null) {
245            return false;
246        }
247        if (!arguments.equals(workSpec.arguments)) return false;
248        if (!output.equals(workSpec.output)) return false;
249        if (!constraints.equals(workSpec.constraints)) return false;
250        return backoffPolicy == workSpec.backoffPolicy;
251    }
252
253    @Override
254    public int hashCode() {
255        int result = id.hashCode();
256        result = 31 * result + state.hashCode();
257        result = 31 * result + workerClassName.hashCode();
258        result = 31 * result + (inputMergerClassName != null ? inputMergerClassName.hashCode()
259                : 0);
260        result = 31 * result + arguments.hashCode();
261        result = 31 * result + output.hashCode();
262        result = 31 * result + (int) (initialDelay ^ (initialDelay >>> 32));
263        result = 31 * result + (int) (intervalDuration ^ (intervalDuration >>> 32));
264        result = 31 * result + (int) (flexDuration ^ (flexDuration >>> 32));
265        result = 31 * result + constraints.hashCode();
266        result = 31 * result + runAttemptCount;
267        result = 31 * result + backoffPolicy.hashCode();
268        result = 31 * result + (int) (backoffDelayDuration ^ (backoffDelayDuration >>> 32));
269        result = 31 * result + (int) (periodStartTime ^ (periodStartTime >>> 32));
270        result = 31 * result + (int) (minimumRetentionDuration ^ (minimumRetentionDuration
271                >>> 32));
272        return result;
273    }
274
275    @Override
276    public String toString() {
277        return "{WorkSpec: " + id + "}";
278    }
279
280    /**
281     * A POJO containing the ID and state of a WorkSpec.
282     */
283    public static class IdAndState {
284
285        @ColumnInfo(name = "id")
286        public String id;
287
288        @ColumnInfo(name = "state")
289        public State state;
290
291        @Override
292        public boolean equals(Object o) {
293            if (this == o) return true;
294            if (o == null || getClass() != o.getClass()) return false;
295
296            IdAndState that = (IdAndState) o;
297
298            if (state != that.state) return false;
299            return id.equals(that.id);
300        }
301
302        @Override
303        public int hashCode() {
304            int result = id.hashCode();
305            result = 31 * result + state.hashCode();
306            return result;
307        }
308    }
309
310    /**
311     * A POJO containing the ID, state, output, and tags of a WorkSpec.
312     */
313    public static class WorkStatusPojo {
314
315        @ColumnInfo(name = "id")
316        public String id;
317
318        @ColumnInfo(name = "state")
319        public State state;
320
321        @ColumnInfo(name = "output")
322        public Arguments output;
323
324        @Relation(
325                parentColumn = "id",
326                entityColumn = "work_spec_id",
327                entity = WorkTag.class,
328                projection = {"tag"})
329        public List<String> tags;
330
331        /**
332         * Converts this POJO to a {@link WorkStatus}.
333         *
334         * @return The {@link WorkStatus} represented by this POJO
335         */
336        public WorkStatus toWorkStatus() {
337            return new WorkStatus(id, state, output, tags);
338        }
339
340        @Override
341        public boolean equals(Object o) {
342            if (this == o) return true;
343            if (o == null || getClass() != o.getClass()) return false;
344
345            WorkStatusPojo that = (WorkStatusPojo) o;
346
347            if (id != null ? !id.equals(that.id) : that.id != null) return false;
348            if (state != that.state) return false;
349            if (output != null ? !output.equals(that.output) : that.output != null) return false;
350            return tags != null ? tags.equals(that.tags) : that.tags == null;
351        }
352
353        @Override
354        public int hashCode() {
355            int result = id != null ? id.hashCode() : 0;
356            result = 31 * result + (state != null ? state.hashCode() : 0);
357            result = 31 * result + (output != null ? output.hashCode() : 0);
358            result = 31 * result + (tags != null ? tags.hashCode() : 0);
359            return result;
360        }
361    }
362
363    public static final Function<List<WorkStatusPojo>, List<WorkStatus>> WORK_STATUS_MAPPER =
364            new Function<List<WorkStatusPojo>, List<WorkStatus>>() {
365                @Override
366                public List<WorkStatus> apply(List<WorkStatusPojo> input) {
367                    if (input == null) {
368                        return null;
369                    }
370                    List<WorkStatus> output = new ArrayList<>(input.size());
371                    for (WorkStatusPojo in : input) {
372                        output.add(in.toWorkStatus());
373                    }
374                    return output;
375                }
376            };
377}
378