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