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