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