• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2024 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 com.android.adservices.shared.spe.scheduling;
18 
19 import static com.android.adservices.shared.proto.JobPolicy.BatteryType.BATTERY_TYPE_REQUIRE_CHARGING;
20 import static com.android.adservices.shared.spe.JobErrorMessage.ERROR_MESSAGE_JOB_PROCESSOR_INVALID_JOB_POLICY_CHARGING_IDLE;
21 import static com.android.adservices.shared.spe.JobErrorMessage.ERROR_MESSAGE_JOB_PROCESSOR_INVALID_NETWORK_TYPE;
22 import static com.android.adservices.shared.spe.JobErrorMessage.ERROR_MESSAGE_JOB_PROCESSOR_MISMATCHED_JOB_ID_WHEN_MERGING_JOB_POLICY;
23 import static com.android.adservices.shared.spe.JobErrorMessage.ERROR_MESSAGE_POLICY_JOB_SCHEDULER_PERIODIC_JOB_INVALID_FLEX_INTERVAL;
24 import static com.android.adservices.shared.spe.JobErrorMessage.ERROR_MESSAGE_POLICY_JOB_SCHEDULER_PERIODIC_JOB_INVALID_PERIODIC_INTERVAL;
25 import static com.android.adservices.shared.spe.JobServiceConstants.MILLISECONDS_PER_MINUTE;
26 import static com.android.adservices.shared.spe.JobServiceConstants.MIN_FLEX_INTERVAL_MINUTES;
27 import static com.android.adservices.shared.spe.JobServiceConstants.MIN_FLEX_INTERVAL_PERCENTAGE;
28 import static com.android.adservices.shared.spe.JobServiceConstants.MIN_PERIODIC_INTERVAL_MINUTES;
29 
30 import android.annotation.Nullable;
31 import android.app.job.JobInfo;
32 import android.net.Uri;
33 
34 import com.android.adservices.shared.proto.JobPolicy;
35 import com.android.adservices.shared.proto.JobPolicy.NetworkType;
36 import com.android.adservices.shared.proto.JobPolicy.OneOffJobParams;
37 import com.android.adservices.shared.proto.JobPolicy.PeriodicJobParams;
38 import com.android.adservices.shared.proto.JobPolicy.TriggerContentJobParams;
39 import com.android.internal.annotations.VisibleForTesting;
40 
41 /** A class to process proto-based {@link JobPolicy}. */
42 public final class PolicyProcessor {
43     /**
44      * Apply {@link JobPolicy} synced from server to the default {@link JobInfo}. Note {@link
45      * JobPolicy} prevails for the same field.
46      *
47      * @param builder a builder for the default {@link JobInfo}
48      * @param jobPolicy the {@link JobPolicy} synced from server
49      * @return a merged {@link JobInfo}. {@link JobPolicy} will override the value if a field
50      *     presents in both {@code builder} and {@code jobPolicy}.
51      */
applyPolicyToJobInfo( JobInfo.Builder builder, @Nullable JobPolicy jobPolicy)52     public static JobInfo applyPolicyToJobInfo(
53             JobInfo.Builder builder, @Nullable JobPolicy jobPolicy) {
54         if (jobPolicy == null) {
55             return builder.build();
56         }
57 
58         if (jobPolicy.hasNetworkType()) {
59             builder.setRequiredNetworkType(convertNetworkType(jobPolicy.getNetworkType()));
60         }
61 
62         if (jobPolicy.hasBatteryType()) {
63             setBatteryConstraint(builder, jobPolicy);
64         }
65 
66         if (jobPolicy.hasRequireDeviceIdle()) {
67             builder.setRequiresDeviceIdle(jobPolicy.getRequireDeviceIdle());
68         }
69 
70         if (jobPolicy.hasRequireStorageNotLow()) {
71             builder.setRequiresStorageNotLow(jobPolicy.getRequireStorageNotLow());
72         }
73 
74         if (jobPolicy.hasIsPersisted()) {
75             builder.setPersisted(jobPolicy.getIsPersisted());
76         }
77 
78         if (jobPolicy.hasPeriodicJobParams()) {
79             setPeriodicJobParams(builder, jobPolicy.getPeriodicJobParams());
80         }
81 
82         if (jobPolicy.hasOneOffJobParams()) {
83             setOneOffJobParams(builder, jobPolicy.getOneOffJobParams());
84         }
85 
86         if (jobPolicy.hasTriggerContentJobParams()) {
87             setTriggerContentJobParams(builder, jobPolicy.getTriggerContentJobParams());
88         }
89 
90         return builder.build();
91     }
92 
93     /**
94      * Merges two JobPolicy. The strategy is left-join, i.e. the second JobPolicy overrides the same
95      * field if it also presents in the first JobPolicy.
96      *
97      * @param jobPolicy1 the {@link JobPolicy} to be merged to. (destination)
98      * @param jobPolicy2 the {@link JobPolicy} to merge from. (source)
99      * @return a merged {@link JobPolicy}
100      */
101     @Nullable
mergeTwoJobPolicies(JobPolicy jobPolicy1, JobPolicy jobPolicy2)102     public static JobPolicy mergeTwoJobPolicies(JobPolicy jobPolicy1, JobPolicy jobPolicy2) {
103         JobPolicy mergedPolicy;
104         if (jobPolicy1 == null && jobPolicy2 == null) {
105             return null;
106         } else if (jobPolicy1 == null) {
107             mergedPolicy = jobPolicy2;
108         } else if (jobPolicy2 == null) {
109             mergedPolicy = jobPolicy1;
110         } else {
111             // It requires the job ID of two Policies are same.
112             if (!jobPolicy1.hasJobId()
113                     || !jobPolicy2.hasJobId()
114                     || jobPolicy1.getJobId() != jobPolicy2.getJobId()) {
115                 throw new IllegalArgumentException(
116                         ERROR_MESSAGE_JOB_PROCESSOR_MISMATCHED_JOB_ID_WHEN_MERGING_JOB_POLICY);
117             }
118 
119             // mergeFrom() merges the contents of other into this message, overwriting singular
120             // scalar fields, merging composite fields, and concatenating repeated fields.
121             mergedPolicy = jobPolicy1.toBuilder().mergeFrom(jobPolicy2).build();
122         }
123 
124         enforceJobPolicyValidity(mergedPolicy);
125 
126         return mergedPolicy;
127     }
128 
129     // An extra validation for jobPolicy before JobInfo.enforceValidity().
130     @VisibleForTesting
enforceJobPolicyValidity(JobPolicy jobPolicy)131     static void enforceJobPolicyValidity(JobPolicy jobPolicy) {
132         // Charging cannot be set with Device Idle. See b/221454240 for details.
133         if (jobPolicy.hasRequireDeviceIdle()
134                 && jobPolicy.getRequireDeviceIdle()
135                 && jobPolicy.hasBatteryType()
136                 && jobPolicy.getBatteryType() == BATTERY_TYPE_REQUIRE_CHARGING) {
137             throw new IllegalArgumentException(
138                     ERROR_MESSAGE_JOB_PROCESSOR_INVALID_JOB_POLICY_CHARGING_IDLE);
139         }
140 
141         // Periodic interval needs to be more than 15 minutes and flex interval needs to be
142         // at least 5 minutes or 5% of the periodic interval
143         if (jobPolicy.hasPeriodicJobParams()) {
144             PeriodicJobParams periodicJobParams = jobPolicy.getPeriodicJobParams();
145             if (!periodicJobParams.hasPeriodicIntervalMs()) {
146                 throw new IllegalArgumentException(
147                         ERROR_MESSAGE_POLICY_JOB_SCHEDULER_PERIODIC_JOB_INVALID_PERIODIC_INTERVAL);
148             }
149             long periodicIntervalMs = periodicJobParams.getPeriodicIntervalMs();
150             if (periodicIntervalMs < MILLISECONDS_PER_MINUTE * MIN_PERIODIC_INTERVAL_MINUTES) {
151                 throw new IllegalArgumentException(
152                         ERROR_MESSAGE_POLICY_JOB_SCHEDULER_PERIODIC_JOB_INVALID_PERIODIC_INTERVAL);
153             }
154             if (periodicJobParams.hasFlexInternalMs()) {
155                 long flexIntervalMs = periodicJobParams.getFlexInternalMs();
156                 if (flexIntervalMs < MILLISECONDS_PER_MINUTE * MIN_FLEX_INTERVAL_MINUTES
157                         || flexIntervalMs <= MIN_FLEX_INTERVAL_PERCENTAGE * periodicIntervalMs) {
158                     throw new IllegalArgumentException(
159                             ERROR_MESSAGE_POLICY_JOB_SCHEDULER_PERIODIC_JOB_INVALID_FLEX_INTERVAL);
160                 }
161             }
162         }
163     }
164 
165     // Map network type from Policy's NetworkType to JobInfo.NetworkType.
166     @VisibleForTesting
convertNetworkType(NetworkType networkType)167     static int convertNetworkType(NetworkType networkType) {
168         switch (networkType) {
169             case NETWORK_TYPE_NONE:
170                 return JobInfo.NETWORK_TYPE_NONE;
171             case NETWORK_TYPE_ANY:
172                 return JobInfo.NETWORK_TYPE_ANY;
173             case NETWORK_TYPE_UNMETERED:
174                 return JobInfo.NETWORK_TYPE_UNMETERED;
175             case NETWORK_TYPE_NOT_ROAMING:
176                 return JobInfo.NETWORK_TYPE_NOT_ROAMING;
177             case NETWORK_TYPE_CELLULAR:
178                 return JobInfo.NETWORK_TYPE_CELLULAR;
179             default:
180                 // The error will be caught in the PolicyJobScheduler#applyPolicyFromServer().
181                 throw new IllegalArgumentException(
182                         String.format(
183                                 ERROR_MESSAGE_JOB_PROCESSOR_INVALID_NETWORK_TYPE,
184                                 networkType.getNumber()));
185         }
186     }
187 
188     // Process the battery constraint. Allow one condition to be true and others will be overridden
189     // to false.
190     //
191     // Note: Based on current charging speed, Charging and BatteryNotLow should be mutual excluded.
192     // That says, if a job is defined as requiring charging, it should not care if the battery level
193     // is low or not. To set both conditions to be true will harm the expected job execution
194     // frequency. Therefore, SPE limits to use one condition or none.
setBatteryConstraint(JobInfo.Builder builder, JobPolicy jobPolicy)195     private static void setBatteryConstraint(JobInfo.Builder builder, JobPolicy jobPolicy) {
196         switch (jobPolicy.getBatteryType()) {
197             case BATTERY_TYPE_REQUIRE_CHARGING:
198                 builder.setRequiresCharging(true);
199                 builder.setRequiresBatteryNotLow(false);
200                 return;
201             case BATTERY_TYPE_REQUIRE_NOT_LOW:
202                 builder.setRequiresBatteryNotLow(true);
203                 builder.setRequiresCharging(false);
204                 return;
205             case BATTERY_TYPE_REQUIRE_NONE:
206             default:
207                 builder.setRequiresCharging(false);
208                 builder.setRequiresBatteryNotLow(false);
209         }
210     }
211 
setPeriodicJobParams(JobInfo.Builder builder, PeriodicJobParams params)212     private static void setPeriodicJobParams(JobInfo.Builder builder, PeriodicJobParams params) {
213         if (!params.hasPeriodicIntervalMs()) {
214             return;
215         }
216 
217         if (params.hasFlexInternalMs()) {
218             builder.setPeriodic(params.getPeriodicIntervalMs(), params.getFlexInternalMs());
219         } else {
220             builder.setPeriodic(params.getPeriodicIntervalMs());
221         }
222     }
223 
setOneOffJobParams(JobInfo.Builder builder, OneOffJobParams params)224     private static void setOneOffJobParams(JobInfo.Builder builder, OneOffJobParams params) {
225         if (params.hasMinimumLatencyMs()) {
226             builder.setMinimumLatency(params.getMinimumLatencyMs());
227         }
228 
229         if (params.hasOverrideDeadlineMs()) {
230             builder.setOverrideDeadline(params.getOverrideDeadlineMs());
231         }
232     }
233 
setTriggerContentJobParams( JobInfo.Builder builder, TriggerContentJobParams params)234     private static void setTriggerContentJobParams(
235             JobInfo.Builder builder, TriggerContentJobParams params) {
236         if (params.hasTriggerContentUriString()) {
237             builder.addTriggerContentUri(
238                     new JobInfo.TriggerContentUri(
239                             Uri.parse(params.getTriggerContentUriString()),
240                             // There is only one flag value, and it's a required field to construct
241                             // TriggerContentUri. Set it by default.
242                             JobInfo.TriggerContentUri.FLAG_NOTIFY_FOR_DESCENDANTS));
243         }
244 
245         if (params.hasTriggerContentMaxDelayMs()) {
246             builder.setTriggerContentMaxDelay(params.getTriggerContentMaxDelayMs());
247         }
248 
249         if (params.hasTriggerContentUpdateDelayMs()) {
250             builder.setTriggerContentUpdateDelay(params.getTriggerContentUpdateDelayMs());
251         }
252     }
253 }
254