1 /* 2 * Copyright (C) 2023 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.devicelockcontroller.schedule; 18 19 import static com.android.devicelockcontroller.WorkManagerExceptionHandler.AlarmReason; 20 import static com.android.devicelockcontroller.common.DeviceLockConstants.MANDATORY_PROVISION_DEVICE_RESET_COUNTDOWN_MINUTE; 21 import static com.android.devicelockcontroller.common.DeviceLockConstants.NON_MANDATORY_PROVISION_DEVICE_RESET_COUNTDOWN_MINUTE; 22 import static com.android.devicelockcontroller.policy.ProvisionStateController.ProvisionState.PROVISION_FAILED; 23 import static com.android.devicelockcontroller.policy.ProvisionStateController.ProvisionState.PROVISION_PAUSED; 24 import static com.android.devicelockcontroller.policy.ProvisionStateController.ProvisionState.UNPROVISIONED; 25 import static com.android.devicelockcontroller.provision.worker.AbstractCheckInWorker.BACKOFF_DELAY; 26 27 import android.app.AlarmManager; 28 import android.app.PendingIntent; 29 import android.content.BroadcastReceiver; 30 import android.content.Context; 31 import android.content.Intent; 32 import android.content.SharedPreferences; 33 import android.os.Build; 34 import android.os.SystemClock; 35 36 import androidx.annotation.VisibleForTesting; 37 import androidx.work.BackoffPolicy; 38 import androidx.work.Constraints; 39 import androidx.work.ExistingWorkPolicy; 40 import androidx.work.NetworkType; 41 import androidx.work.OneTimeWorkRequest; 42 import androidx.work.Operation; 43 import androidx.work.OutOfQuotaPolicy; 44 import androidx.work.WorkManager; 45 46 import com.android.devicelockcontroller.DeviceLockControllerApplication; 47 import com.android.devicelockcontroller.WorkManagerExceptionHandler; 48 import com.android.devicelockcontroller.activities.DeviceLockNotificationManager; 49 import com.android.devicelockcontroller.policy.ProvisionStateController; 50 import com.android.devicelockcontroller.policy.ProvisionStateController.ProvisionState; 51 import com.android.devicelockcontroller.provision.worker.DeviceCheckInWorker; 52 import com.android.devicelockcontroller.receivers.NextProvisionFailedStepReceiver; 53 import com.android.devicelockcontroller.receivers.ResetDeviceReceiver; 54 import com.android.devicelockcontroller.receivers.ResumeProvisionReceiver; 55 import com.android.devicelockcontroller.storage.GlobalParametersClient; 56 import com.android.devicelockcontroller.storage.UserParameters; 57 import com.android.devicelockcontroller.util.LogUtil; 58 import com.android.devicelockcontroller.util.ThreadUtils; 59 60 import com.google.common.base.Function; 61 import com.google.common.util.concurrent.FluentFuture; 62 import com.google.common.util.concurrent.FutureCallback; 63 import com.google.common.util.concurrent.Futures; 64 import com.google.common.util.concurrent.ListenableFuture; 65 import com.google.common.util.concurrent.MoreExecutors; 66 67 import java.time.Clock; 68 import java.time.Duration; 69 import java.time.Instant; 70 import java.util.Objects; 71 import java.util.concurrent.Executor; 72 import java.util.concurrent.TimeUnit; 73 74 /** 75 * Implementation of {@link DeviceLockControllerScheduler}. 76 * WARNING: Do not create an instance directly, instead you should retrieve it using the 77 * {@link DeviceLockControllerApplication#getDeviceLockControllerScheduler()} API. 78 */ 79 public final class DeviceLockControllerSchedulerImpl implements DeviceLockControllerScheduler { 80 private static final String TAG = "DeviceLockControllerSchedulerImpl"; 81 private static final String FILENAME = "device-lock-controller-scheduler-preferences"; 82 public static final String DEVICE_CHECK_IN_WORK_NAME = "device-check-in"; 83 private static final String DEBUG_DEVICELOCK_PAUSED_MINUTES = "debug.devicelock.paused-minutes"; 84 private static final String DEBUG_DEVICELOCK_REPORT_INTERVAL_MINUTES = 85 "debug.devicelock.report-interval-minutes"; 86 private static final String DEBUG_DEVICELOCK_RESET_DEVICE_MINUTES = 87 "debug.devicelock.reset-device-minutes"; 88 private static final String DEBUG_DEVICELOCK_MANDATORY_RESET_DEVICE_MINUTES = 89 "debug.devicelock.mandatory-reset-device-minutes"; 90 91 // The default minute value of the duration that provision UI can be paused. 92 public static final int PROVISION_PAUSED_MINUTES_DEFAULT = 60; 93 // The default minute value of the interval between steps of provision failed flow. 94 public static final long PROVISION_STATE_REPORT_INTERVAL_DEFAULT_MINUTES = 95 TimeUnit.DAYS.toMinutes(1); 96 private final Context mContext; 97 private final Clock mClock; 98 private final Executor mSequentialExecutor; 99 private final ProvisionStateController mProvisionStateController; 100 101 private static volatile SharedPreferences sSharedPreferences; 102 getSharedPreferences( Context context)103 private static synchronized SharedPreferences getSharedPreferences( 104 Context context) { 105 if (sSharedPreferences == null) { 106 sSharedPreferences = context.createDeviceProtectedStorageContext().getSharedPreferences( 107 FILENAME, 108 Context.MODE_PRIVATE); 109 } 110 return sSharedPreferences; 111 } 112 113 /** 114 * Set how long provision should be paused after user hit the "Do it in 1 hour" button, in 115 * minutes. 116 */ setDebugProvisionPausedMinutes(Context context, int minutes)117 public static void setDebugProvisionPausedMinutes(Context context, int minutes) { 118 getSharedPreferences(context).edit().putInt(DEBUG_DEVICELOCK_PAUSED_MINUTES, 119 minutes).apply(); 120 } 121 122 /** 123 * Set the length of the interval of provisioning failure reporting for debugging purpose. 124 */ setDebugReportIntervalMinutes(Context context, long minutes)125 public static void setDebugReportIntervalMinutes(Context context, long minutes) { 126 getSharedPreferences(context).edit().putLong(DEBUG_DEVICELOCK_REPORT_INTERVAL_MINUTES, 127 minutes).apply(); 128 } 129 130 /** 131 * Set the length of the countdown minutes when device is about to factory reset in 132 * non-mandatory provisioning case for debugging purpose. 133 */ setDebugResetDeviceMinutes(Context context, int minutes)134 public static void setDebugResetDeviceMinutes(Context context, int minutes) { 135 getSharedPreferences(context).edit().putInt(DEBUG_DEVICELOCK_RESET_DEVICE_MINUTES, 136 minutes).apply(); 137 } 138 139 /** 140 * Set the length of the countdown minutes when device is about to factory reset in mandatory 141 * provisioning case for debugging purpose. 142 */ setDebugMandatoryResetDeviceMinutes(Context context, int minutes)143 public static void setDebugMandatoryResetDeviceMinutes(Context context, int minutes) { 144 getSharedPreferences(context).edit().putInt(DEBUG_DEVICELOCK_MANDATORY_RESET_DEVICE_MINUTES, 145 minutes).apply(); 146 } 147 148 /** 149 * Dump current debugging setup to logcat. 150 */ dumpDebugScheduler(Context context)151 public static void dumpDebugScheduler(Context context) { 152 LogUtil.d(TAG, 153 "Current Debug Scheduler setups:\n" + getSharedPreferences(context).getAll()); 154 } 155 156 /** 157 * Clear current debugging setup. 158 */ clear(Context context)159 public static void clear(Context context) { 160 getSharedPreferences(context).edit().clear().apply(); 161 } 162 DeviceLockControllerSchedulerImpl(Context context, ProvisionStateController provisionStateController)163 public DeviceLockControllerSchedulerImpl(Context context, 164 ProvisionStateController provisionStateController) { 165 this(context, Clock.systemUTC(), provisionStateController); 166 } 167 168 @VisibleForTesting DeviceLockControllerSchedulerImpl(Context context, Clock clock, ProvisionStateController provisionStateController)169 DeviceLockControllerSchedulerImpl(Context context, Clock clock, 170 ProvisionStateController provisionStateController) { 171 mContext = context; 172 mProvisionStateController = provisionStateController; 173 mClock = clock; 174 mSequentialExecutor = ThreadUtils.getSequentialSchedulerExecutor(); 175 } 176 177 @Override notifyTimeChanged()178 public void notifyTimeChanged() { 179 Futures.addCallback(mProvisionStateController.getState(), 180 new FutureCallback<>() { 181 @Override 182 public void onSuccess(@ProvisionState Integer currentState) { 183 correctStoredTime(currentState); 184 } 185 186 @Override 187 public void onFailure(Throwable t) { 188 throw new RuntimeException(t); 189 } 190 }, mSequentialExecutor); 191 } 192 193 /** 194 * Correct the stored time for when a scheduled work/alarm should execute based on the 195 * difference between current time and stored time. 196 * 197 * @param currentState The current {@link ProvisionState} used to determine which work/alarm may 198 * be possibly scheduled. 199 */ 200 @VisibleForTesting correctStoredTime(@rovisionState Integer currentState)201 void correctStoredTime(@ProvisionState Integer currentState) { 202 long bootTimestamp = UserParameters.getBootTimeMillis(mContext); 203 long delta = 204 mClock.millis() - (bootTimestamp + SystemClock.elapsedRealtime()); 205 UserParameters.setBootTimeMillis(mContext, 206 UserParameters.getBootTimeMillis(mContext) + delta); 207 if (currentState == UNPROVISIONED) { 208 long before = UserParameters.getNextCheckInTimeMillis(mContext); 209 if (before > 0) { 210 UserParameters.setNextCheckInTimeMillis(mContext, 211 before + delta); 212 } 213 // We have to reschedule (update) the check-in work, because, otherwise, if device 214 // reboots, WorkManager will reschedule the work based on the changed system clock, 215 // which will result in inaccurate schedule. (see b/285221785) 216 rescheduleRetryCheckInWork(); 217 } else if (currentState == PROVISION_PAUSED) { 218 long before = UserParameters.getResumeProvisionTimeMillis(mContext); 219 if (before > 0) { 220 UserParameters.setResumeProvisionTimeMillis(mContext, 221 before + delta); 222 } 223 } else if (currentState == PROVISION_FAILED) { 224 long before = UserParameters.getNextProvisionFailedStepTimeMills( 225 mContext); 226 if (before > 0) { 227 UserParameters.setNextProvisionFailedStepTimeMills(mContext, 228 before + delta); 229 } 230 before = UserParameters.getResetDeviceTimeMillis(mContext); 231 if (before > 0) { 232 UserParameters.setResetDeviceTimeMillis(mContext, 233 before + delta); 234 } 235 } 236 } 237 238 @Override scheduleResumeProvisionAlarm()239 public void scheduleResumeProvisionAlarm() { 240 Duration delay = Duration.ofMinutes(PROVISION_PAUSED_MINUTES_DEFAULT); 241 if (Build.isDebuggable()) { 242 delay = Duration.ofMinutes( 243 getSharedPreferences(mContext).getInt(DEBUG_DEVICELOCK_PAUSED_MINUTES, 244 PROVISION_PAUSED_MINUTES_DEFAULT)); 245 } 246 LogUtil.i(TAG, "Scheduling resume provision work with delay: " + delay); 247 scheduleResumeProvisionAlarm(delay); 248 Instant whenExpectedToRun = Instant.now(mClock).plus(delay); 249 UserParameters.setResumeProvisionTimeMillis(mContext, 250 whenExpectedToRun.toEpochMilli()); 251 } 252 253 @Override notifyRebootWhenProvisionPaused()254 public void notifyRebootWhenProvisionPaused() { 255 dispatchFuture(this::rescheduleResumeProvisionAlarmIfNeeded, 256 "notifyRebootWhenProvisionPaused"); 257 } 258 259 @Override scheduleInitialCheckInWork()260 public ListenableFuture<Void> scheduleInitialCheckInWork() { 261 LogUtil.i(TAG, "Scheduling initial check-in work"); 262 final Operation operation = 263 enqueueCheckInWorkRequest(/* isExpedited= */ true, Duration.ZERO); 264 final ListenableFuture<Operation.State.SUCCESS> result = operation.getResult(); 265 266 return FluentFuture.from(result) 267 .transform((Function<Operation.State.SUCCESS, Void>) ignored -> { 268 UserParameters.initialCheckInScheduled(mContext); 269 return null; 270 }, mSequentialExecutor) 271 .catching(Throwable.class, (e) -> { 272 LogUtil.e(TAG, "Failed to enqueue initial check in work", e); 273 WorkManagerExceptionHandler.scheduleAlarm(mContext, 274 AlarmReason.INITIAL_CHECK_IN); 275 throw new RuntimeException(e); 276 }, mSequentialExecutor); 277 } 278 279 @Override scheduleRetryCheckInWork(Duration delay)280 public ListenableFuture<Void> scheduleRetryCheckInWork(Duration delay) { 281 LogUtil.i(TAG, "Scheduling retry check-in work with delay: " + delay); 282 final Operation operation = 283 enqueueCheckInWorkRequest(/* isExpedited= */ false, delay); 284 final ListenableFuture<Operation.State.SUCCESS> result = operation.getResult(); 285 286 return FluentFuture.from(result) 287 .transform((Function<Operation.State.SUCCESS, Void>) ignored -> { 288 Instant whenExpectedToRun = Instant.now(mClock).plus(delay); 289 UserParameters.setNextCheckInTimeMillis(mContext, 290 whenExpectedToRun.toEpochMilli()); 291 return null; 292 }, mSequentialExecutor) 293 .catching(Throwable.class, (e) -> { 294 LogUtil.e(TAG, "Failed to enqueue retry check in work", e); 295 WorkManagerExceptionHandler.scheduleAlarm(mContext, 296 AlarmReason.RETRY_CHECK_IN); 297 throw new RuntimeException(e); 298 }, mSequentialExecutor); 299 } 300 301 @Override 302 public ListenableFuture<Void> notifyNeedRescheduleCheckIn() { 303 final ListenableFuture<Void> result = 304 Futures.submit(this::rescheduleRetryCheckInWork, mSequentialExecutor); 305 Futures.addCallback(result, 306 new FutureCallback<>() { 307 @Override 308 public void onSuccess(Void unused) { 309 LogUtil.i(TAG, "Successfully called notifyNeedRescheduleCheckIn"); 310 } 311 312 @Override 313 public void onFailure(Throwable t) { 314 throw new RuntimeException("failed to call notifyNeedRescheduleCheckIn", t); 315 } 316 }, MoreExecutors.directExecutor()); 317 return result; 318 } 319 320 @VisibleForTesting 321 void rescheduleRetryCheckInWork() { 322 long nextCheckInTimeMillis = UserParameters.getNextCheckInTimeMillis(mContext); 323 if (nextCheckInTimeMillis > 0) { 324 Duration delay = Duration.between( 325 Instant.now(mClock), 326 Instant.ofEpochMilli(nextCheckInTimeMillis)); 327 LogUtil.i(TAG, "Rescheduling retry check-in work with delay: " + delay); 328 final Operation operation = 329 enqueueCheckInWorkRequest(/* isExpedited= */ false, delay); 330 Futures.addCallback(operation.getResult(), new FutureCallback<>() { 331 @Override 332 public void onSuccess(Operation.State.SUCCESS result) { 333 // No-op 334 } 335 336 @Override 337 public void onFailure(Throwable t) { 338 LogUtil.e(TAG, "Failed to reschedule retry check in work", t); 339 WorkManagerExceptionHandler.scheduleAlarm(mContext, 340 AlarmReason.RESCHEDULE_CHECK_IN); 341 } 342 }, mSequentialExecutor); 343 } 344 } 345 346 @Override 347 public ListenableFuture<Void> maybeScheduleInitialCheckIn() { 348 return FluentFuture.from(Futures.submit(() -> UserParameters.needInitialCheckIn(mContext), 349 mSequentialExecutor)) 350 .transformAsync(needCheckIn -> { 351 if (needCheckIn) { 352 return Futures.transform(scheduleInitialCheckInWork(), 353 input -> false /* reschedule */, mSequentialExecutor); 354 } else { 355 return Futures.transform( 356 GlobalParametersClient.getInstance().isProvisionReady(), 357 ready -> !ready, mSequentialExecutor); 358 } 359 }, mSequentialExecutor) 360 .transformAsync(reschedule -> { 361 if (reschedule) { 362 return notifyNeedRescheduleCheckIn(); 363 } 364 return Futures.immediateVoidFuture(); 365 }, mSequentialExecutor); 366 } 367 368 @Override 369 public void scheduleNextProvisionFailedStepAlarm() { 370 LogUtil.d(TAG, 371 "Scheduling next provision failed step alarm"); 372 long lastTimestamp = UserParameters.getNextProvisionFailedStepTimeMills(mContext); 373 long nextTimestamp; 374 if (lastTimestamp == 0) { 375 lastTimestamp = Instant.now(mClock).toEpochMilli(); 376 } 377 long minutes = Build.isDebuggable() ? getSharedPreferences(mContext).getLong( 378 DEBUG_DEVICELOCK_REPORT_INTERVAL_MINUTES, 379 PROVISION_STATE_REPORT_INTERVAL_DEFAULT_MINUTES) 380 : PROVISION_STATE_REPORT_INTERVAL_DEFAULT_MINUTES; 381 Duration delay = Duration.ofMinutes(minutes); 382 nextTimestamp = lastTimestamp + delay.toMillis(); 383 scheduleNextProvisionFailedStepAlarm( 384 Duration.between(Instant.now(mClock), Instant.ofEpochMilli(nextTimestamp))); 385 UserParameters.setNextProvisionFailedStepTimeMills(mContext, nextTimestamp); 386 } 387 388 @Override 389 public void notifyRebootWhenProvisionFailed() { 390 dispatchFuture(() -> { 391 rescheduleNextProvisionFailedStepAlarmIfNeeded(); 392 rescheduleResetDeviceAlarmIfNeeded(); 393 }, "notifyRebootWhenProvisionFailed"); 394 } 395 396 397 @Override 398 public void scheduleResetDeviceAlarm() { 399 Duration delay = Duration.ofMinutes(NON_MANDATORY_PROVISION_DEVICE_RESET_COUNTDOWN_MINUTE); 400 if (Build.isDebuggable()) { 401 delay = Duration.ofMinutes( 402 getSharedPreferences(mContext) 403 .getInt(DEBUG_DEVICELOCK_RESET_DEVICE_MINUTES, 404 NON_MANDATORY_PROVISION_DEVICE_RESET_COUNTDOWN_MINUTE)); 405 } 406 scheduleResetDeviceAlarm(delay); 407 } 408 409 @Override 410 public void scheduleMandatoryResetDeviceAlarm() { 411 Duration delay = Duration.ofMinutes(MANDATORY_PROVISION_DEVICE_RESET_COUNTDOWN_MINUTE); 412 if (Build.isDebuggable()) { 413 delay = Duration.ofMinutes( 414 getSharedPreferences(mContext) 415 .getInt(DEBUG_DEVICELOCK_MANDATORY_RESET_DEVICE_MINUTES, 416 MANDATORY_PROVISION_DEVICE_RESET_COUNTDOWN_MINUTE)); 417 } 418 scheduleResetDeviceAlarm(delay); 419 } 420 421 private void scheduleResetDeviceAlarm(Duration delay) { 422 scheduleResetDeviceAlarmInternal(delay); 423 Instant whenExpectedToRun = Instant.now(mClock).plus(delay); 424 DeviceLockNotificationManager.getInstance().sendDeviceResetTimerNotification(mContext, 425 SystemClock.elapsedRealtime() + delay.toMillis()); 426 UserParameters.setResetDeviceTimeMillis(mContext, whenExpectedToRun.toEpochMilli()); 427 } 428 429 @VisibleForTesting 430 void rescheduleNextProvisionFailedStepAlarmIfNeeded() { 431 long timestamp = UserParameters.getNextProvisionFailedStepTimeMills(mContext); 432 if (timestamp > 0) { 433 Duration delay = Duration.between( 434 Instant.now(mClock), 435 Instant.ofEpochMilli(timestamp)); 436 scheduleNextProvisionFailedStepAlarm(delay); 437 } 438 } 439 440 @VisibleForTesting 441 void rescheduleResetDeviceAlarmIfNeeded() { 442 long timestamp = UserParameters.getResetDeviceTimeMillis(mContext); 443 if (timestamp > 0) { 444 Duration delay = Duration.between( 445 Instant.now(mClock), 446 Instant.ofEpochMilli(timestamp)); 447 scheduleResetDeviceAlarmInternal(delay); 448 } 449 } 450 451 @VisibleForTesting 452 void rescheduleResumeProvisionAlarmIfNeeded() { 453 long resumeProvisionTimeMillis = UserParameters.getResumeProvisionTimeMillis(mContext); 454 if (resumeProvisionTimeMillis > 0) { 455 Duration delay = Duration.between( 456 Instant.now(mClock), 457 Instant.ofEpochMilli(resumeProvisionTimeMillis)); 458 scheduleResumeProvisionAlarm(delay); 459 } 460 } 461 462 /** 463 * Run the input runnable in order on the scheduler's sequential executor 464 * 465 * @param runnable The runnable to run on worker thread. 466 * @param methodName The name of the method that requested to run runnable. 467 */ 468 private void dispatchFuture(Runnable runnable, String methodName) { 469 Futures.addCallback(Futures.submit(runnable, mSequentialExecutor), 470 new FutureCallback<>() { 471 @Override 472 public void onSuccess(Void unused) { 473 LogUtil.i(TAG, "Successfully called " + methodName); 474 } 475 476 @Override 477 public void onFailure(Throwable t) { 478 throw new RuntimeException("failed to call " + methodName, t); 479 } 480 }, MoreExecutors.directExecutor()); 481 } 482 483 private Operation enqueueCheckInWorkRequest(boolean isExpedited, Duration delay) { 484 OneTimeWorkRequest.Builder builder = 485 new OneTimeWorkRequest.Builder(DeviceCheckInWorker.class) 486 .setConstraints( 487 new Constraints.Builder().setRequiredNetworkType( 488 NetworkType.CONNECTED).build()) 489 .setInitialDelay(delay) 490 .setBackoffCriteria(BackoffPolicy.EXPONENTIAL, BACKOFF_DELAY); 491 if (isExpedited) builder.setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST); 492 493 return WorkManager.getInstance(mContext).enqueueUniqueWork(DEVICE_CHECK_IN_WORK_NAME, 494 ExistingWorkPolicy.REPLACE, builder.build()); 495 } 496 497 private void scheduleResumeProvisionAlarm(Duration delay) { 498 scheduleAlarmWithPendingIntentAndDelay(ResumeProvisionReceiver.class, delay); 499 } 500 501 private void scheduleNextProvisionFailedStepAlarm(Duration delay) { 502 scheduleAlarmWithPendingIntentAndDelay(NextProvisionFailedStepReceiver.class, delay); 503 } 504 505 private void scheduleResetDeviceAlarmInternal(Duration delay) { 506 scheduleAlarmWithPendingIntentAndDelay(ResetDeviceReceiver.class, delay); 507 } 508 509 private void scheduleAlarmWithPendingIntentAndDelay( 510 Class<? extends BroadcastReceiver> receiverClass, Duration delay) { 511 long countDownBase = SystemClock.elapsedRealtime() + delay.toMillis(); 512 AlarmManager alarmManager = mContext.getSystemService(AlarmManager.class); 513 PendingIntent pendingIntent = PendingIntent.getBroadcast(mContext, /* ignored */ 0, 514 new Intent(mContext, receiverClass), 515 PendingIntent.FLAG_ONE_SHOT | PendingIntent.FLAG_IMMUTABLE); 516 Objects.requireNonNull(alarmManager).setExactAndAllowWhileIdle( 517 AlarmManager.ELAPSED_REALTIME_WAKEUP, 518 countDownBase, 519 pendingIntent); 520 } 521 } 522