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