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