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