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 
17 package androidx.work.impl.background.systemjob;
18 
19 import static androidx.work.impl.background.systemjob.SystemJobInfoConverterExtKt.setRequiredNetworkRequest;
20 
21 import android.app.job.JobInfo;
22 import android.content.ComponentName;
23 import android.content.Context;
24 import android.net.NetworkCapabilities;
25 import android.net.NetworkRequest;
26 import android.os.Build;
27 import android.os.PersistableBundle;
28 
29 import androidx.annotation.RequiresApi;
30 import androidx.annotation.RestrictTo;
31 import androidx.work.BackoffPolicy;
32 import androidx.work.Clock;
33 import androidx.work.Constraints;
34 import androidx.work.Logger;
35 import androidx.work.NetworkType;
36 import androidx.work.impl.WorkManagerImpl;
37 import androidx.work.impl.model.WorkSpec;
38 
39 import org.jspecify.annotations.NonNull;
40 
41 /**
42  * Converts a {@link WorkSpec} into a JobInfo.
43  *
44  */
45 @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
46 @RequiresApi(api = WorkManagerImpl.MIN_JOB_SCHEDULER_API_LEVEL)
47 class SystemJobInfoConverter {
48     private static final String TAG = Logger.tagWithPrefix("SystemJobInfoConverter");
49 
50     static final String EXTRA_WORK_SPEC_ID = "EXTRA_WORK_SPEC_ID";
51     static final String EXTRA_IS_PERIODIC = "EXTRA_IS_PERIODIC";
52     static final String EXTRA_WORK_SPEC_GENERATION = "EXTRA_WORK_SPEC_GENERATION";
53 
54     private final ComponentName mWorkServiceComponent;
55     private final Clock mClock;
56     private final boolean mMarkImportantWhileForeground;
57 
SystemJobInfoConverter(@onNull Context context, Clock clock, boolean markImportantWhileForeground)58     SystemJobInfoConverter(@NonNull Context context,
59             Clock clock, boolean markImportantWhileForeground) {
60         mClock = clock;
61         Context appContext = context.getApplicationContext();
62         mWorkServiceComponent = new ComponentName(appContext, SystemJobService.class);
63         mMarkImportantWhileForeground = markImportantWhileForeground;
64     }
65 
66     /**
67      * Converts a {@link WorkSpec} into a {@link JobInfo}.
68      *
69      * Note: All {@link JobInfo} are set to persist on reboot.
70      *
71      * @param workSpec The {@link WorkSpec} to convert
72      * @param jobId    The {@code jobId} to use. This is useful when de-duping jobs on reschedule.
73      * @return The {@link JobInfo} representing the same information as the {@link WorkSpec}
74      */
convert(WorkSpec workSpec, int jobId)75     JobInfo convert(WorkSpec workSpec, int jobId) {
76         Constraints constraints = workSpec.constraints;
77         PersistableBundle extras = new PersistableBundle();
78         extras.putString(EXTRA_WORK_SPEC_ID, workSpec.id);
79         extras.putInt(EXTRA_WORK_SPEC_GENERATION, workSpec.getGeneration());
80         extras.putBoolean(EXTRA_IS_PERIODIC, workSpec.isPeriodic());
81         JobInfo.Builder builder = new JobInfo.Builder(jobId, mWorkServiceComponent)
82                 .setRequiresCharging(constraints.requiresCharging())
83                 .setRequiresDeviceIdle(constraints.requiresDeviceIdle())
84                 .setExtras(extras);
85         NetworkRequest networkRequest = constraints.getRequiredNetworkRequest();
86         if (Build.VERSION.SDK_INT >= 28 && networkRequest != null) {
87             setRequiredNetworkRequest(builder, networkRequest);
88         } else {
89             setRequiredNetwork(builder, constraints.getRequiredNetworkType());
90         }
91 
92         if (!constraints.requiresDeviceIdle()) {
93             // Device Idle and Backoff Criteria cannot be set together
94             int backoffPolicy = workSpec.backoffPolicy == BackoffPolicy.LINEAR
95                     ? JobInfo.BACKOFF_POLICY_LINEAR : JobInfo.BACKOFF_POLICY_EXPONENTIAL;
96             builder.setBackoffCriteria(workSpec.backoffDelayDuration, backoffPolicy);
97         }
98 
99         long nextRunTime = workSpec.calculateNextRunTime();
100         long now = mClock.currentTimeMillis();
101         long offset = Math.max(nextRunTime - now, 0);
102 
103         if (Build.VERSION.SDK_INT <= 28) {
104             // Before API 29, Jobs needed at least one constraint. Therefore before API 29 we
105             // always setMinimumLatency to make sure we have at least one constraint.
106             // See aosp/5434530 & b/6771687
107             builder.setMinimumLatency(offset);
108         } else {
109             if (offset > 0) {
110                 // Only set a minimum latency when applicable.
111                 builder.setMinimumLatency(offset);
112             } else if (!workSpec.expedited && mMarkImportantWhileForeground) {
113                 // Only set this if the workSpec is not expedited.
114                 builder.setImportantWhileForeground(true);
115             }
116         }
117 
118         if (Build.VERSION.SDK_INT >= 24 && constraints.hasContentUriTriggers()) {
119             //noinspection ConstantConditions
120             for (Constraints.ContentUriTrigger trigger : constraints.getContentUriTriggers()) {
121                 builder.addTriggerContentUri(convertContentUriTrigger(trigger));
122             }
123             builder.setTriggerContentUpdateDelay(constraints.getContentTriggerUpdateDelayMillis());
124             builder.setTriggerContentMaxDelay(constraints.getContentTriggerMaxDelayMillis());
125         }
126 
127         // We don't want to persist these jobs because we reschedule these jobs on BOOT_COMPLETED.
128         // That way ForceStopRunnable correctly reschedules Jobs when necessary.
129         builder.setPersisted(false);
130         if (Build.VERSION.SDK_INT >= 26) {
131             builder.setRequiresBatteryNotLow(constraints.requiresBatteryNotLow());
132             builder.setRequiresStorageNotLow(constraints.requiresStorageNotLow());
133         }
134         // Retries cannot be expedited jobs, given they will occur at some point in the future.
135         boolean isRetry = workSpec.runAttemptCount > 0;
136         boolean isDelayed = offset > 0;
137         if (Build.VERSION.SDK_INT >= 31 && workSpec.expedited && !isRetry && !isDelayed) {
138             //noinspection NewApi
139             builder.setExpedited(true);
140         }
141         if (Build.VERSION.SDK_INT >= 35) {
142             // Add a trace tag that shows the actual worker running.
143             String traceTag = workSpec.getTraceTag();
144             if (traceTag != null) {
145                 builder.setTraceTag(traceTag);
146             }
147         }
148         return builder.build();
149     }
150 
151     @RequiresApi(24)
convertContentUriTrigger( Constraints.ContentUriTrigger trigger)152     private static JobInfo.TriggerContentUri convertContentUriTrigger(
153             Constraints.ContentUriTrigger trigger) {
154         int flag = trigger.isTriggeredForDescendants()
155                 ? JobInfo.TriggerContentUri.FLAG_NOTIFY_FOR_DESCENDANTS : 0;
156         return new JobInfo.TriggerContentUri(trigger.getUri(), flag);
157     }
158 
159     /**
160      * Adds the required network capabilities on the {@link JobInfo.Builder} instance.
161      *
162      * @param builder     The instance of {@link JobInfo.Builder}.
163      * @param networkType The {@link NetworkType} instance.
164      */
setRequiredNetwork( JobInfo.@onNull Builder builder, @NonNull NetworkType networkType)165     static void setRequiredNetwork(
166             JobInfo.@NonNull Builder builder,
167             @NonNull NetworkType networkType) {
168 
169         if (Build.VERSION.SDK_INT >= 30 && networkType == NetworkType.TEMPORARILY_UNMETERED) {
170             NetworkRequest networkRequest = new NetworkRequest.Builder()
171                     .addCapability(NetworkCapabilities.NET_CAPABILITY_TEMPORARILY_NOT_METERED)
172                     .build();
173 
174             builder.setRequiredNetwork(networkRequest);
175         } else {
176             builder.setRequiredNetworkType(convertNetworkType(networkType));
177         }
178     }
179 
180     /**
181      * Converts {@link NetworkType} into {@link JobInfo}'s network values.
182      *
183      * @param networkType The {@link NetworkType} network type
184      * @return The {@link JobInfo} network type
185      */
186     @SuppressWarnings("MissingCasesInEnumSwitch")
convertNetworkType(NetworkType networkType)187     static int convertNetworkType(NetworkType networkType) {
188         switch (networkType) {
189             case NOT_REQUIRED:
190                 return JobInfo.NETWORK_TYPE_NONE;
191             case CONNECTED:
192                 return JobInfo.NETWORK_TYPE_ANY;
193             case UNMETERED:
194                 return JobInfo.NETWORK_TYPE_UNMETERED;
195             case NOT_ROAMING:
196                 if (Build.VERSION.SDK_INT >= 24) {
197                     return JobInfo.NETWORK_TYPE_NOT_ROAMING;
198                 }
199                 break;
200             case METERED:
201                 if (Build.VERSION.SDK_INT >= 26) {
202                     return JobInfo.NETWORK_TYPE_METERED;
203                 }
204                 break;
205         }
206         Logger.get().debug(TAG, "API version too low. Cannot convert network type value " + networkType);
207         return JobInfo.NETWORK_TYPE_ANY;
208     }
209 }
210