1 /* 2 * Copyright (C) 2022 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.ondevicepersonalization.services.download.mdd; 18 19 import static android.app.job.JobScheduler.RESULT_SUCCESS; 20 21 import static com.android.adservices.shared.proto.JobPolicy.BatteryType.BATTERY_TYPE_REQUIRE_NOT_LOW; 22 import static com.android.adservices.shared.proto.JobPolicy.NetworkType.NETWORK_TYPE_ANY; 23 import static com.android.adservices.shared.proto.JobPolicy.NetworkType.NETWORK_TYPE_NONE; 24 import static com.android.adservices.shared.proto.JobPolicy.NetworkType.NETWORK_TYPE_UNMETERED; 25 import static com.android.adservices.shared.spe.JobServiceConstants.SCHEDULING_RESULT_CODE_FAILED; 26 import static com.android.adservices.shared.spe.JobServiceConstants.SCHEDULING_RESULT_CODE_SKIPPED; 27 import static com.android.adservices.shared.spe.JobServiceConstants.SCHEDULING_RESULT_CODE_SUCCESSFUL; 28 import static com.android.ondevicepersonalization.services.OnDevicePersonalizationConfig.MDD_CELLULAR_CHARGING_PERIODIC_TASK_JOB_ID; 29 import static com.android.ondevicepersonalization.services.OnDevicePersonalizationConfig.MDD_CHARGING_PERIODIC_TASK_JOB_ID; 30 import static com.android.ondevicepersonalization.services.OnDevicePersonalizationConfig.MDD_MAINTENANCE_PERIODIC_TASK_JOB_ID; 31 import static com.android.ondevicepersonalization.services.OnDevicePersonalizationConfig.MDD_WIFI_CHARGING_PERIODIC_TASK_JOB_ID; 32 33 import android.app.job.JobInfo; 34 import android.app.job.JobScheduler; 35 import android.content.ComponentName; 36 import android.content.Context; 37 import android.content.SharedPreferences; 38 import android.os.PersistableBundle; 39 40 import com.android.adservices.shared.proto.JobPolicy; 41 import com.android.adservices.shared.proto.JobPolicy.NetworkType; 42 import com.android.adservices.shared.spe.JobServiceConstants; 43 import com.android.adservices.shared.spe.scheduling.JobSpec; 44 import com.android.internal.annotations.VisibleForTesting; 45 import com.android.ondevicepersonalization.internal.util.LoggerFactory; 46 import com.android.ondevicepersonalization.services.FlagsFactory; 47 import com.android.ondevicepersonalization.services.sharedlibrary.spe.OdpJobScheduler; 48 import com.android.ondevicepersonalization.services.sharedlibrary.spe.OdpJobServiceFactory; 49 50 import com.google.android.libraries.mobiledatadownload.TaskScheduler; 51 52 /** 53 * MddTaskScheduler that uses JobScheduler to schedule MDD background tasks 54 */ 55 public class MddTaskScheduler implements TaskScheduler { 56 private static final LoggerFactory.Logger sLogger = LoggerFactory.getLogger(); 57 private static final String TAG = MddTaskScheduler.class.getSimpleName(); 58 private static final String MDD_TASK_SHARED_PREFS = "mdd_worker_task_periods"; 59 private final Context mContext; 60 static final String MDD_NETWORK_STATE_KEY = "MDD_NETWORK_STATE_KEY"; 61 static final String MDD_PERIOD_SECONDS_KEY = "MDD_PERIOD_SECONDS_KEY"; 62 static final String MDD_TASK_TAG_KEY = "MDD_TASK_TAG_KEY"; 63 MddTaskScheduler(Context context)64 public MddTaskScheduler(Context context) { 65 this.mContext = context; 66 } 67 getMddTaskJobId(String mddTag)68 private static int getMddTaskJobId(String mddTag) { 69 switch (mddTag) { 70 case MAINTENANCE_PERIODIC_TASK: 71 return MDD_MAINTENANCE_PERIODIC_TASK_JOB_ID; 72 case CHARGING_PERIODIC_TASK: 73 return MDD_CHARGING_PERIODIC_TASK_JOB_ID; 74 case CELLULAR_CHARGING_PERIODIC_TASK: 75 return MDD_CELLULAR_CHARGING_PERIODIC_TASK_JOB_ID; 76 default: 77 return MDD_WIFI_CHARGING_PERIODIC_TASK_JOB_ID; 78 } 79 } 80 81 // Maps from the MDD-supplied NetworkState to the JobInfo equivalent int code. getNetworkConstraints(NetworkState networkState)82 static int getNetworkConstraints(NetworkState networkState) { 83 switch (networkState) { 84 case NETWORK_STATE_ANY: 85 // Network not required. 86 return JobInfo.NETWORK_TYPE_NONE; 87 case NETWORK_STATE_CONNECTED: 88 // Metered or unmetered network available. 89 return JobInfo.NETWORK_TYPE_ANY; 90 case NETWORK_STATE_UNMETERED: 91 default: 92 return JobInfo.NETWORK_TYPE_UNMETERED; 93 } 94 } 95 96 @Override schedulePeriodicTask( String mddTaskTag, long periodSeconds, NetworkState networkState)97 public void schedulePeriodicTask( 98 String mddTaskTag, long periodSeconds, NetworkState networkState) { 99 schedule(mContext, mddTaskTag, periodSeconds, networkState); 100 } 101 102 /** Schedules a unique instance of {@link MddJob}. */ schedule(Context context, PersistableBundle extras)103 public static void schedule(Context context, PersistableBundle extras) { 104 schedule(context, 105 getMddTaskTag(extras), 106 getMddPeriodSeconds(extras), 107 getMddNetworkState(extras)); 108 } 109 110 /** Schedules a unique instance of {@link MddJobService}. */ 111 @JobServiceConstants.JobSchedulingResultCode scheduleWithLegacy( Context context, PersistableBundle extras, boolean forceSchedule)112 public static int scheduleWithLegacy( 113 Context context, PersistableBundle extras, boolean forceSchedule) { 114 return scheduleWithLegacy(context, 115 getMddTaskTag(extras), 116 getMddPeriodSeconds(extras), 117 getMddNetworkState(extras), 118 forceSchedule); 119 } 120 schedule(Context context, String mddTaskTag, long periodSeconds, NetworkState networkState)121 private static void schedule(Context context, String mddTaskTag, long periodSeconds, 122 NetworkState networkState) { 123 if (FlagsFactory.getFlags().getSpeOnMddJobEnabled()) { 124 OdpJobScheduler.getInstance(context).schedule( 125 context, 126 createJobSpec(mddTaskTag, periodSeconds, networkState)); 127 return; 128 } 129 130 sLogger.d("SPE is not enabled. Schedule the job with MddJobService."); 131 int resultCode = scheduleWithLegacy( 132 context, mddTaskTag, periodSeconds, networkState, /* forceSchedule */ false); 133 134 OdpJobServiceFactory.getInstance(context) 135 .getJobSchedulingLogger() 136 .recordOnSchedulingLegacy(getMddTaskJobId(mddTaskTag), resultCode); 137 } 138 139 /** Schedules a unique instance of {@link MddJobService}. */ 140 @VisibleForTesting 141 @JobServiceConstants.JobSchedulingResultCode scheduleWithLegacy(Context context, String mddTaskTag, long periodSeconds, NetworkState networkState, boolean forceSchedule)142 static int scheduleWithLegacy(Context context, String mddTaskTag, long periodSeconds, 143 NetworkState networkState, boolean forceSchedule) { 144 SharedPreferences prefs = 145 context.getSharedPreferences(MDD_TASK_SHARED_PREFS, Context.MODE_PRIVATE); 146 147 // When the period change, we will need to update the existing works. 148 boolean updateCurrent = false; 149 if (getCurrentPeriodValue(prefs, mddTaskTag) != periodSeconds) { 150 SharedPreferences.Editor editor = prefs.edit(); 151 editor.putLong(mddTaskTag, periodSeconds); 152 editor.apply(); 153 updateCurrent = true; 154 } 155 156 JobScheduler jobScheduler = context.getSystemService(JobScheduler.class); 157 if (jobScheduler.getPendingJob(getMddTaskJobId(mddTaskTag)) == null) { 158 sLogger.d(TAG + ": MddJob %s is not scheduled, scheduling now", mddTaskTag); 159 return schedulePeriodicTaskWithUpdate( 160 context , jobScheduler, mddTaskTag, periodSeconds, networkState); 161 } else if (updateCurrent) { 162 sLogger.d(TAG + ": scheduling MddJob %s with frequency update", mddTaskTag); 163 return schedulePeriodicTaskWithUpdate( 164 context, jobScheduler, mddTaskTag, periodSeconds, networkState); 165 } else if (forceSchedule) { 166 sLogger.d(TAG + ": force scheduling MddJob %s", mddTaskTag); 167 return schedulePeriodicTaskWithUpdate( 168 context, jobScheduler, mddTaskTag, periodSeconds, networkState); 169 } 170 sLogger.d(TAG + ": MddJob %s already scheduled and frequency unchanged," 171 + " not scheduling", mddTaskTag); 172 return SCHEDULING_RESULT_CODE_SKIPPED; 173 } 174 175 @JobServiceConstants.JobSchedulingResultCode schedulePeriodicTaskWithUpdate(Context context, JobScheduler jobScheduler, String mddTaskTag, long periodSeconds, NetworkState networkState)176 private static int schedulePeriodicTaskWithUpdate(Context context, JobScheduler jobScheduler, 177 String mddTaskTag, long periodSeconds, NetworkState networkState) { 178 // We use extras to pass MDD config values. They will be used in the mdd jobs. 179 PersistableBundle extras = new PersistableBundle(); 180 extras.putString(MDD_TASK_TAG_KEY, mddTaskTag); 181 extras.putLong(MDD_PERIOD_SECONDS_KEY, periodSeconds); 182 extras.putString(MDD_NETWORK_STATE_KEY, networkState.name()); 183 184 final JobInfo job = 185 new JobInfo.Builder( 186 getMddTaskJobId(mddTaskTag), 187 new ComponentName(context, MddJobService.class)) 188 .setRequiresDeviceIdle(true) 189 .setRequiresCharging(false) 190 .setRequiresBatteryNotLow(true) 191 .setPeriodic(1000 * periodSeconds) // JobScheduler uses Milliseconds. 192 .setRequiresStorageNotLow(requireStorageNotLow(mddTaskTag)) 193 // persist this job across boots 194 .setPersisted(true) 195 .setRequiredNetworkType(getNetworkConstraints(networkState)) 196 .setExtras(extras) 197 .build(); 198 int schedulingResult = jobScheduler.schedule(job); 199 return RESULT_SUCCESS == schedulingResult ? SCHEDULING_RESULT_CODE_SUCCESSFUL 200 : SCHEDULING_RESULT_CODE_FAILED; 201 } 202 203 @VisibleForTesting createJobSpec(String mddTaskTag, long periodSeconds, NetworkState networkState)204 static JobSpec createJobSpec(String mddTaskTag, long periodSeconds, NetworkState networkState) { 205 // We use extras to pass MDD config values. They will be used in the mdd jobs. 206 PersistableBundle extras = new PersistableBundle(); 207 extras.putString(MDD_TASK_TAG_KEY, mddTaskTag); 208 extras.putLong(MDD_PERIOD_SECONDS_KEY, periodSeconds); 209 extras.putString(MDD_NETWORK_STATE_KEY, networkState.name()); 210 211 JobPolicy jobPolicy = 212 JobPolicy.newBuilder() 213 .setJobId(getMddTaskJobId(mddTaskTag)) 214 .setRequireDeviceIdle(true) 215 .setBatteryType(BATTERY_TYPE_REQUIRE_NOT_LOW) 216 .setPeriodicJobParams( 217 JobPolicy.PeriodicJobParams.newBuilder() 218 .setPeriodicIntervalMs(1000 * periodSeconds) 219 .build()) 220 .setNetworkType(getNetworkType(networkState)) 221 .setRequireStorageNotLow(requireStorageNotLow(mddTaskTag)) 222 .setIsPersisted(true) 223 .build(); 224 225 return new JobSpec.Builder(jobPolicy) 226 .setExtras(extras).build(); 227 } 228 getNetworkType(NetworkState networkState)229 private static NetworkType getNetworkType(NetworkState networkState) { 230 switch (networkState) { 231 case NETWORK_STATE_ANY: 232 // Network not required. 233 return NETWORK_TYPE_NONE; 234 case NETWORK_STATE_CONNECTED: 235 // Metered or unmetered network available. 236 return NETWORK_TYPE_ANY; 237 case NETWORK_STATE_UNMETERED: 238 default: 239 return NETWORK_TYPE_UNMETERED; 240 } 241 } 242 getMddTaskTag(final PersistableBundle extras)243 static String getMddTaskTag(final PersistableBundle extras) { 244 requireNonNullExtras(extras); 245 String mddTaskTag = extras.getString(MDD_TASK_TAG_KEY); 246 if (null == mddTaskTag) { 247 throw new IllegalArgumentException("Mdd task tag not found"); 248 } 249 return mddTaskTag; 250 } 251 getMddPeriodSeconds(final PersistableBundle extras)252 private static long getMddPeriodSeconds(final PersistableBundle extras) { 253 requireNonNullExtras(extras); 254 return extras.getLong(MDD_PERIOD_SECONDS_KEY); 255 } 256 getMddNetworkState(final PersistableBundle extras)257 private static NetworkState getMddNetworkState(final PersistableBundle extras) { 258 requireNonNullExtras(extras); 259 String networkState = extras.getString(MDD_NETWORK_STATE_KEY); 260 if (networkState == null) { 261 throw new IllegalArgumentException("MDD extra network state not found"); 262 } 263 return NetworkState.valueOf(networkState); 264 } 265 requireNonNullExtras(PersistableBundle extras)266 private static void requireNonNullExtras(PersistableBundle extras) { 267 if (null == extras) { 268 throw new IllegalArgumentException("MDD extras not found"); 269 } 270 } 271 getCurrentPeriodValue(SharedPreferences prefs, String mddTaskTag)272 private static long getCurrentPeriodValue(SharedPreferences prefs, String mddTaskTag) { 273 try { 274 return prefs.getLong(mddTaskTag, 0); 275 } catch (ClassCastException e) { 276 sLogger.w(e, TAG + ": ClassCastException retrieving long value from prefs for tag: %s", 277 mddTaskTag); 278 return 0; 279 } 280 } 281 requireStorageNotLow(String mddTaskTag)282 private static boolean requireStorageNotLow(String mddTaskTag) { 283 return WIFI_CHARGING_PERIODIC_TASK.equals(mddTaskTag) 284 || CELLULAR_CHARGING_PERIODIC_TASK.equals(mddTaskTag); 285 } 286 } 287