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.debug;
18 
19 import static com.android.devicelockcontroller.common.DeviceLockConstants.DeviceProvisionState.PROVISION_STATE_UNSPECIFIED;
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.common.DeviceLockConstants.READY_FOR_PROVISION;
23 import static com.android.devicelockcontroller.policy.DeviceStateController.DeviceState.CLEARED;
24 import static com.android.devicelockcontroller.policy.DeviceStateController.DeviceState.LOCKED;
25 import static com.android.devicelockcontroller.policy.DeviceStateController.DeviceState.UNLOCKED;
26 import static com.android.devicelockcontroller.policy.ProvisionStateController.ProvisionState.PROVISION_SUCCEEDED;
27 import static com.android.devicelockcontroller.schedule.DeviceLockControllerSchedulerImpl.PROVISION_PAUSED_MINUTES_DEFAULT;
28 import static com.android.devicelockcontroller.schedule.DeviceLockControllerSchedulerImpl.PROVISION_STATE_REPORT_INTERVAL_DEFAULT_MINUTES;
29 
30 import static java.lang.annotation.RetentionPolicy.SOURCE;
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.os.Build;
38 import android.os.UserManager;
39 import android.text.TextUtils;
40 
41 import androidx.annotation.StringDef;
42 import androidx.work.WorkManager;
43 
44 import com.android.devicelockcontroller.FcmRegistrationTokenProvider;
45 import com.android.devicelockcontroller.WorkManagerExceptionHandler.WorkFailureAlarmReceiver;
46 import com.android.devicelockcontroller.policy.DevicePolicyController;
47 import com.android.devicelockcontroller.policy.DeviceStateController;
48 import com.android.devicelockcontroller.policy.DeviceStateController.DeviceState;
49 import com.android.devicelockcontroller.policy.PolicyObjectsProvider;
50 import com.android.devicelockcontroller.policy.ProvisionHelperImpl;
51 import com.android.devicelockcontroller.policy.ProvisionStateController;
52 import com.android.devicelockcontroller.provision.grpc.DeviceCheckInClient;
53 import com.android.devicelockcontroller.provision.grpc.DeviceFinalizeClient;
54 import com.android.devicelockcontroller.provision.worker.DeviceCheckInWorker;
55 import com.android.devicelockcontroller.provision.worker.PauseProvisioningWorker;
56 import com.android.devicelockcontroller.provision.worker.ReportDeviceLockProgramCompleteWorker;
57 import com.android.devicelockcontroller.provision.worker.ReportDeviceProvisionStateWorker;
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.schedule.DeviceLockControllerScheduler;
62 import com.android.devicelockcontroller.schedule.DeviceLockControllerSchedulerImpl;
63 import com.android.devicelockcontroller.schedule.DeviceLockControllerSchedulerProvider;
64 import com.android.devicelockcontroller.storage.GlobalParametersClient;
65 import com.android.devicelockcontroller.storage.SetupParametersClient;
66 import com.android.devicelockcontroller.storage.UserParameters;
67 import com.android.devicelockcontroller.util.LogUtil;
68 
69 import com.google.common.util.concurrent.FutureCallback;
70 import com.google.common.util.concurrent.Futures;
71 import com.google.common.util.concurrent.ListenableFuture;
72 import com.google.common.util.concurrent.MoreExecutors;
73 
74 import java.lang.annotation.Retention;
75 import java.util.Objects;
76 
77 /**
78  * A {@link BroadcastReceiver} that can handle reset, lock, unlock command.
79  * <p>
80  * Note:
81  * Reboot device are {@link DeviceLockCommandReceiver#onReceive(Context, Intent)} has been called to
82  * take effect.
83  */
84 public final class DeviceLockCommandReceiver extends BroadcastReceiver {
85 
86     private static final String TAG = "DeviceLockCommandReceiver";
87     private static final String EXTRA_COMMAND = "command";
88     private static final String EXTRA_CHECK_IN_STATUS = "check-in-status";
89     private static final String EXTRA_CHECK_IN_RETRY_DELAY = "check-in-retry-delay";
90     private static final String EXTRA_FORCE_PROVISION = "force-provision";
91     private static final String EXTRA_IS_IN_APPROVED_COUNTRY = "is-in-approved-country";
92     private static final String EXTRA_NEXT_PROVISION_STATE = "next-provision-state";
93     private static final String EXTRA_DAYS_LEFT_UNTIL_RESET = "days-left-until-reset";
94     private static final String EXTRA_PAUSED_MINUTES = "paused-minutes";
95     private static final String EXTRA_REPORT_INTERVAL_MINUTES = "report-interval-minutes";
96     private static final String EXTRA_RESET_DEVICE_MINUTES = "reset-device-minutes";
97     private static final String EXTRA_MANDATORY_RESET_DEVICE_MINUTES =
98             "mandatory-reset-device-minutes";
99     private static final String EXTRA_HOST_NAME = "host-name";
100     public static final String EXTRA_RESET_INCLUDE_SETUP_PARAMETERS_AND_DEBUG_SETUPS =
101             "include-setup-params-and-debug-setups";
102 
103     @Retention(SOURCE)
104     @StringDef({
105             Commands.RESET,
106             Commands.LOCK,
107             Commands.UNLOCK,
108             Commands.CHECK_IN,
109             Commands.CLEAR,
110             Commands.DUMP,
111             Commands.FCM,
112             Commands.ENABLE_DEBUG_CLIENT,
113             Commands.DISABLE_DEBUG_CLIENT,
114             Commands.SET_DEBUG_CLIENT_RESPONSE,
115             Commands.DUMP_DEBUG_CLIENT_RESPONSE,
116             Commands.SET_DEBUG_CLIENT_RESPONSE,
117             Commands.DUMP_DEBUG_SCHEDULER,
118     })
119     private @interface Commands {
120         String RESET = "reset";
121         String LOCK = "lock";
122         String UNLOCK = "unlock";
123         String CHECK_IN = "check-in";
124         String CLEAR = "clear";
125         String DUMP = "dump";
126         String FCM = "fcm";
127         String ENABLE_DEBUG_CLIENT = "enable-debug-client";
128         String DISABLE_DEBUG_CLIENT = "disable-debug-client";
129         String SET_CHECK_IN_HOST_NAME = "set-check-in-host-name";
130         String SET_FINALIZE_HOST_NAME = "set-finalize-host-name";
131         String SET_DEBUG_CLIENT_RESPONSE = "set-debug-client-response";
132         String DUMP_DEBUG_CLIENT_RESPONSE = "dump-debug-client-response";
133         String SET_UP_DEBUG_SCHEDULER = "set-up-debug-scheduler";
134         String DUMP_DEBUG_SCHEDULER = "dump-debug-scheduler";
135         String ENABLE_PREINSTALLED_KIOSK = "enable-preinstalled-kiosk";
136         String DISABLE_PREINSTALLED_KIOSK = "disable-preinstalled-kiosk";
137     }
138 
139     @Override
onReceive(Context context, Intent intent)140     public void onReceive(Context context, Intent intent) {
141         if (!Build.isDebuggable()) {
142             throw new SecurityException("This should never be run in production build!");
143         }
144 
145         if (!TextUtils.equals(intent.getComponent().getClassName(), getClass().getName())) {
146             throw new IllegalArgumentException("Intent does not match this class!");
147         }
148 
149         final boolean isUserProfile =
150                 context.getSystemService(UserManager.class).isProfile();
151         if (isUserProfile) {
152             LogUtil.w(TAG, "Broadcast should not target user profiles");
153             return;
154         }
155 
156         Context appContext = context.getApplicationContext();
157 
158         ProvisionStateController provisionStateController =
159                 ((PolicyObjectsProvider) appContext).getProvisionStateController();
160         DeviceStateController deviceStateController =
161                 provisionStateController.getDeviceStateController();
162 
163         @Commands
164         String command = String.valueOf(intent.getStringExtra(EXTRA_COMMAND));
165         switch (command) {
166             case Commands.RESET:
167                 forceReset(appContext, intent.getBooleanExtra(
168                         EXTRA_RESET_INCLUDE_SETUP_PARAMETERS_AND_DEBUG_SETUPS, false));
169                 break;
170             case Commands.LOCK:
171                 Futures.addCallback(deviceStateController.lockDevice(),
172                         getSetStateCallBack(LOCKED), MoreExecutors.directExecutor());
173                 break;
174             case Commands.UNLOCK:
175                 Futures.addCallback(deviceStateController.unlockDevice(),
176                         getSetStateCallBack(UNLOCKED), MoreExecutors.directExecutor());
177                 break;
178             case Commands.CLEAR:
179                 Futures.addCallback(deviceStateController.clearDevice(),
180                         getSetStateCallBack(CLEARED), MoreExecutors.directExecutor());
181                 break;
182             case Commands.CHECK_IN:
183                 tryCheckIn(appContext);
184                 break;
185             case Commands.DUMP:
186                 dumpStorage(context);
187                 break;
188             case Commands.FCM:
189                 logFcmToken(appContext);
190                 break;
191             case Commands.ENABLE_DEBUG_CLIENT:
192                 DeviceCheckInClientDebug.setDebugClientEnabled(context, true);
193                 break;
194             case Commands.DISABLE_DEBUG_CLIENT:
195                 DeviceCheckInClientDebug.setDebugClientEnabled(context, false);
196                 break;
197             case Commands.SET_CHECK_IN_HOST_NAME:
198                 setCheckInHostName(context, intent);
199                 break;
200             case Commands.SET_FINALIZE_HOST_NAME:
201                 setFinalizeHostName(context, intent);
202                 break;
203             case Commands.SET_DEBUG_CLIENT_RESPONSE:
204                 setDebugCheckInClientResponse(context, intent);
205                 break;
206             case Commands.DUMP_DEBUG_CLIENT_RESPONSE:
207                 DeviceCheckInClientDebug.dumpDebugCheckInClientResponses(context);
208                 break;
209             case Commands.SET_UP_DEBUG_SCHEDULER:
210                 setUpDebugScheduler(context, intent);
211                 break;
212             case Commands.DUMP_DEBUG_SCHEDULER:
213                 DeviceLockControllerSchedulerImpl.dumpDebugScheduler(context);
214                 break;
215             case Commands.ENABLE_PREINSTALLED_KIOSK:
216                 ProvisionHelperImpl.setPreinstalledKioskAllowed(context, true);
217                 break;
218             case Commands.DISABLE_PREINSTALLED_KIOSK:
219                 ProvisionHelperImpl.setPreinstalledKioskAllowed(context, false);
220                 break;
221             default:
222                 throw new IllegalArgumentException("Unsupported command: " + command);
223         }
224     }
225 
setCheckInHostName(Context context, Intent intent)226     private static void setCheckInHostName(Context context, Intent intent) {
227         if (!intent.hasExtra(EXTRA_HOST_NAME)) {
228             throw new IllegalArgumentException("Missing host name override!");
229         }
230         DeviceCheckInClient.setHostNameOverride(context, intent.getStringExtra(EXTRA_HOST_NAME));
231     }
232 
setFinalizeHostName(Context context, Intent intent)233     private static void setFinalizeHostName(Context context, Intent intent) {
234         if (!intent.hasExtra(EXTRA_HOST_NAME)) {
235             throw new IllegalArgumentException("Missing host name override!");
236         }
237         DeviceFinalizeClient.setHostNameOverride(context, intent.getStringExtra(EXTRA_HOST_NAME));
238     }
239 
setDebugCheckInClientResponse(Context context, Intent intent)240     private static void setDebugCheckInClientResponse(Context context, Intent intent) {
241         if (intent.hasExtra(EXTRA_CHECK_IN_STATUS)) {
242             DeviceCheckInClientDebug.setDebugCheckInStatus(context,
243                     intent.getIntExtra(EXTRA_CHECK_IN_STATUS, READY_FOR_PROVISION));
244 
245         }
246         if (intent.hasExtra(EXTRA_FORCE_PROVISION)) {
247             DeviceCheckInClientDebug.setDebugForceProvisioning(context,
248                     intent.getBooleanExtra(EXTRA_FORCE_PROVISION, false));
249         }
250         if (intent.hasExtra(EXTRA_IS_IN_APPROVED_COUNTRY)) {
251             DeviceCheckInClientDebug.setDebugApprovedCountry(context,
252                     intent.getBooleanExtra(EXTRA_IS_IN_APPROVED_COUNTRY, true));
253         }
254         if (intent.hasExtra(EXTRA_NEXT_PROVISION_STATE)) {
255             DeviceCheckInClientDebug.setDebugNextProvisionState(context,
256                     intent.getIntExtra(EXTRA_NEXT_PROVISION_STATE,
257                             PROVISION_STATE_UNSPECIFIED));
258         }
259         if (intent.hasExtra(EXTRA_DAYS_LEFT_UNTIL_RESET)) {
260             DeviceCheckInClientDebug.setDebugDaysLeftUntilReset(
261                     context, intent.getIntExtra(EXTRA_DAYS_LEFT_UNTIL_RESET, /* days_left*/ 1));
262         }
263         if (intent.hasExtra(EXTRA_CHECK_IN_RETRY_DELAY)) {
264             DeviceCheckInClientDebug.setDebugCheckInRetryDelay(context,
265                     intent.getIntExtra(EXTRA_CHECK_IN_RETRY_DELAY, /* delay_minute= */ 1));
266         }
267     }
268 
setUpDebugScheduler(Context context, Intent intent)269     private static void setUpDebugScheduler(Context context, Intent intent) {
270         if (intent.hasExtra(EXTRA_PAUSED_MINUTES)) {
271             DeviceLockControllerSchedulerImpl.setDebugProvisionPausedMinutes(context,
272                     intent.getIntExtra(EXTRA_PAUSED_MINUTES, PROVISION_PAUSED_MINUTES_DEFAULT));
273         }
274         if (intent.hasExtra(EXTRA_REPORT_INTERVAL_MINUTES)) {
275             DeviceLockControllerSchedulerImpl.setDebugReportIntervalMinutes(context,
276                     intent.getLongExtra(EXTRA_REPORT_INTERVAL_MINUTES,
277                             PROVISION_STATE_REPORT_INTERVAL_DEFAULT_MINUTES));
278         }
279         if (intent.hasExtra(EXTRA_RESET_DEVICE_MINUTES)) {
280             DeviceLockControllerSchedulerImpl.setDebugResetDeviceMinutes(context,
281                     intent.getIntExtra(EXTRA_RESET_DEVICE_MINUTES,
282                             NON_MANDATORY_PROVISION_DEVICE_RESET_COUNTDOWN_MINUTE));
283         }
284         if (intent.hasExtra(EXTRA_MANDATORY_RESET_DEVICE_MINUTES)) {
285             DeviceLockControllerSchedulerImpl.setDebugMandatoryResetDeviceMinutes(context,
286                     intent.getIntExtra(EXTRA_MANDATORY_RESET_DEVICE_MINUTES,
287                             MANDATORY_PROVISION_DEVICE_RESET_COUNTDOWN_MINUTE));
288         }
289     }
290 
dumpStorage(Context context)291     private static void dumpStorage(Context context) {
292         Futures.addCallback(
293                 Futures.transformAsync(SetupParametersClient.getInstance().dump(),
294                         unused -> GlobalParametersClient.getInstance().dump(),
295                         MoreExecutors.directExecutor()),
296                 new FutureCallback<>() {
297                     @Override
298                     public void onSuccess(Void result) {
299                         UserParameters.dump(context);
300                         LogUtil.i(TAG, "Successfully dumped storage");
301                     }
302 
303                     @Override
304                     public void onFailure(Throwable t) {
305                         LogUtil.e(TAG, "Error encountered when dumping storage", t);
306                     }
307                 }, MoreExecutors.directExecutor());
308     }
309 
tryCheckIn(Context appContext)310     private static void tryCheckIn(Context appContext) {
311         if (!appContext.getSystemService(UserManager.class).isSystemUser()) {
312             LogUtil.e(TAG, "Only system user can perform a check-in");
313             return;
314         }
315         DeviceLockControllerSchedulerProvider schedulerProvider =
316                 (DeviceLockControllerSchedulerProvider) appContext;
317         DeviceLockControllerScheduler scheduler =
318                 schedulerProvider.getDeviceLockControllerScheduler();
319 
320         Futures.addCallback(GlobalParametersClient.getInstance().isProvisionReady(),
321                 new FutureCallback<>() {
322                     @Override
323                     public void onSuccess(Boolean provisioningInfoReady) {
324                         if (!provisioningInfoReady) {
325                             scheduler.scheduleInitialCheckInWork();
326                         } else {
327                             LogUtil.e(TAG,
328                                     "Can not check in when provisioning info has already been "
329                                             + "received. Use the \"reset\" command to reset "
330                                             + "DLC first.");
331                         }
332                     }
333 
334                     @Override
335                     public void onFailure(Throwable t) {
336                         LogUtil.e(TAG, "Failed to determine if provisioning info is ready", t);
337                     }
338                 }, MoreExecutors.directExecutor());
339     }
340 
forceReset(Context context, boolean shouldCleanSetupParametersAndDebugSetups)341     private static void forceReset(Context context,
342             boolean shouldCleanSetupParametersAndDebugSetups) {
343         // Cancel provision works
344         LogUtil.d(TAG, "cancelling works");
345         WorkManager workManager = WorkManager.getInstance(context);
346         workManager.cancelAllWorkByTag(DeviceCheckInWorker.class.getName());
347         workManager.cancelAllWorkByTag(PauseProvisioningWorker.class.getName());
348         workManager.cancelAllWorkByTag(
349                 ReportDeviceProvisionStateWorker.class.getName());
350         workManager.cancelAllWorkByTag(
351                 ReportDeviceLockProgramCompleteWorker.class.getName());
352 
353         // Cancel All alarms
354         AlarmManager alarmManager = Objects.requireNonNull(
355                 context.getSystemService(AlarmManager.class));
356         alarmManager.cancel(
357                 PendingIntent.getBroadcast(
358                         context, /* ignored */ 0,
359                         new Intent(context, ResetDeviceReceiver.class),
360                         PendingIntent.FLAG_ONE_SHOT | PendingIntent.FLAG_IMMUTABLE));
361         alarmManager.cancel(PendingIntent.getBroadcast(
362                 context, /* ignored */ 0,
363                 new Intent(context, NextProvisionFailedStepReceiver.class),
364                 PendingIntent.FLAG_ONE_SHOT | PendingIntent.FLAG_IMMUTABLE));
365         alarmManager.cancel(PendingIntent.getBroadcast(
366                 context, /* ignored */ 0,
367                 new Intent(context, ResumeProvisionReceiver.class),
368                 PendingIntent.FLAG_ONE_SHOT | PendingIntent.FLAG_IMMUTABLE));
369         alarmManager.cancel(PendingIntent.getBroadcast(
370                 context, /* ignored */ 0,
371                 new Intent(context, NextProvisionFailedStepReceiver.class),
372                 PendingIntent.FLAG_ONE_SHOT | PendingIntent.FLAG_IMMUTABLE));
373         alarmManager.cancel(PendingIntent.getBroadcast(
374                 context, /* ignored */ 0,
375                 new Intent(context, WorkFailureAlarmReceiver.class),
376                 PendingIntent.FLAG_ONE_SHOT | PendingIntent.FLAG_IMMUTABLE));
377 
378         PolicyObjectsProvider policyObjectsProvider =
379                 (PolicyObjectsProvider) context.getApplicationContext();
380         policyObjectsProvider.destroyObjects();
381         UserParameters.setProvisionState(context, PROVISION_SUCCEEDED);
382         GlobalParametersClient.getInstance().setDeviceState(CLEARED);
383         DevicePolicyController policyController = policyObjectsProvider.getPolicyController();
384         ListenableFuture<Void> clearPolicies = Futures.catching(
385                 policyController.enforceCurrentPolicies(),
386                 RuntimeException.class, unused -> null,
387                 MoreExecutors.directExecutor());
388         Futures.addCallback(Futures.transformAsync(clearPolicies,
389                         unused -> clearStorage(context, shouldCleanSetupParametersAndDebugSetups),
390                         MoreExecutors.directExecutor()),
391                 new FutureCallback<>() {
392                     @Override
393                     public void onSuccess(Void result) {
394                         LogUtil.i(TAG, "Reset device state.");
395                     }
396 
397                     @Override
398                     public void onFailure(Throwable t) {
399                         throw new RuntimeException(t);
400                     }
401                 }, MoreExecutors.directExecutor());
402     }
403 
logFcmToken(Context appContext)404     private static void logFcmToken(Context appContext) {
405         final ListenableFuture<String> fcmRegistrationToken =
406                 ((FcmRegistrationTokenProvider) appContext).getFcmRegistrationToken();
407         Futures.addCallback(fcmRegistrationToken, new FutureCallback<>() {
408             @Override
409             public void onSuccess(String token) {
410                 LogUtil.i(TAG,
411                         "FCM Registration Token: " + (token == null ? "Not set" : token));
412             }
413 
414             @Override
415             public void onFailure(Throwable t) {
416                 LogUtil.e(TAG, "Unable to get FCM registration token", t);
417             }
418         }, MoreExecutors.directExecutor());
419     }
420 
getSetStateCallBack(@eviceState int state)421     private static FutureCallback<Void> getSetStateCallBack(@DeviceState int state) {
422 
423         return new FutureCallback<>() {
424 
425             @Override
426             public void onSuccess(Void unused) {
427                 LogUtil.i(TAG, "Successfully set state to: " + state);
428             }
429 
430             @Override
431             public void onFailure(Throwable t) {
432                 LogUtil.e(TAG, "Unsuccessfully set state to: " + state, t);
433             }
434         };
435     }
436 
437     private static ListenableFuture<Void> clearStorage(Context context,
438             boolean shouldCleanSetupParametersAndDebugSetups) {
439         if (shouldCleanSetupParametersAndDebugSetups) {
440             DeviceCheckInClientDebug.clear(context);
441             DeviceLockControllerSchedulerImpl.clear(context);
442         }
443         UserParameters.clear(context);
444         return Futures.whenAllSucceed(
445                         shouldCleanSetupParametersAndDebugSetups
446                                 ? SetupParametersClient.getInstance().clear()
447                                 : Futures.immediateVoidFuture(),
448                         GlobalParametersClient.getInstance().clear())
449                 .call(() -> {
450                     ((PolicyObjectsProvider) context.getApplicationContext()).destroyObjects();
451                     return null;
452                 }, context.getMainExecutor());
453     }
454 }
455