• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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