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