• 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.policy;
18 
19 import static androidx.work.WorkInfo.State.CANCELLED;
20 import static androidx.work.WorkInfo.State.FAILED;
21 import static androidx.work.WorkInfo.State.SUCCEEDED;
22 
23 import static com.android.devicelockcontroller.common.DeviceLockConstants.EXTRA_KIOSK_PACKAGE;
24 import static com.android.devicelockcontroller.policy.ProvisionStateController.ProvisionEvent.PROVISION_KIOSK;
25 import static com.android.devicelockcontroller.policy.ProvisionStateController.ProvisionEvent.PROVISION_PAUSE;
26 import static com.android.devicelockcontroller.provision.worker.IsDeviceInApprovedCountryWorker.KEY_IS_IN_APPROVED_COUNTRY;
27 
28 import android.app.PendingIntent;
29 import android.content.Context;
30 import android.content.Intent;
31 import android.content.SharedPreferences;
32 import android.content.pm.ApplicationInfo;
33 import android.content.pm.PackageManager.NameNotFoundException;
34 import android.database.sqlite.SQLiteException;
35 import android.os.Build;
36 import android.os.Handler;
37 import android.os.Looper;
38 
39 import androidx.annotation.NonNull;
40 import androidx.annotation.VisibleForTesting;
41 import androidx.lifecycle.LifecycleOwner;
42 import androidx.work.BackoffPolicy;
43 import androidx.work.Constraints;
44 import androidx.work.Data;
45 import androidx.work.ExistingWorkPolicy;
46 import androidx.work.ListenableWorker;
47 import androidx.work.NetworkType;
48 import androidx.work.OneTimeWorkRequest;
49 import androidx.work.Operation;
50 import androidx.work.OutOfQuotaPolicy;
51 import androidx.work.WorkInfo;
52 import androidx.work.WorkManager;
53 import androidx.work.WorkRequest;
54 
55 import com.android.devicelockcontroller.PlayInstallPackageTaskClassProvider;
56 import com.android.devicelockcontroller.activities.DeviceLockNotificationManager;
57 import com.android.devicelockcontroller.activities.ProvisioningProgress;
58 import com.android.devicelockcontroller.activities.ProvisioningProgressController;
59 import com.android.devicelockcontroller.common.DeviceLockConstants.ProvisionFailureReason;
60 import com.android.devicelockcontroller.provision.worker.IsDeviceInApprovedCountryWorker;
61 import com.android.devicelockcontroller.provision.worker.PauseProvisioningWorker;
62 import com.android.devicelockcontroller.provision.worker.ReportDeviceProvisionStateWorker;
63 import com.android.devicelockcontroller.provision.worker.ReviewDeviceProvisionStateWorker;
64 import com.android.devicelockcontroller.receivers.ResumeProvisionReceiver;
65 import com.android.devicelockcontroller.schedule.DeviceLockControllerScheduler;
66 import com.android.devicelockcontroller.schedule.DeviceLockControllerSchedulerProvider;
67 import com.android.devicelockcontroller.stats.StatsLogger;
68 import com.android.devicelockcontroller.stats.StatsLoggerProvider;
69 import com.android.devicelockcontroller.storage.GlobalParametersClient;
70 import com.android.devicelockcontroller.storage.SetupParametersClient;
71 import com.android.devicelockcontroller.util.LogUtil;
72 
73 import com.google.common.util.concurrent.FutureCallback;
74 import com.google.common.util.concurrent.Futures;
75 import com.google.common.util.concurrent.ListenableFuture;
76 
77 import java.time.Duration;
78 import java.time.LocalDateTime;
79 import java.util.UUID;
80 import java.util.concurrent.Executor;
81 import java.util.concurrent.Executors;
82 
83 /**
84  * An implementation of {@link ProvisionHelper}.
85  */
86 public final class ProvisionHelperImpl implements ProvisionHelper {
87     private static final String TAG = "ProvisionHelperImpl";
88     private static final String FILENAME = "device-lock-controller-provisioning-preferences";
89     private static final String USE_PREINSTALLED_KIOSK_PREF =
90             "debug.devicelock.usepreinstalledkiosk";
91     private static volatile SharedPreferences sSharedPreferences;
92     // For Play Install exponential backoff due to Play being updated, use a short delay of
93     // 10 seconds since the situation should resolve relatively quickly.
94     private static final Duration PLAY_INSTALL_BACKOFF_DELAY =
95             Duration.ofMillis(WorkRequest.MIN_BACKOFF_MILLIS);
96     private static final long IS_DEVICE_IN_APPROVED_COUNTRY_NETWORK_TIMEOUT_MS = 60_000;
97 
98     @VisibleForTesting
getSharedPreferences(Context context)99     static synchronized SharedPreferences getSharedPreferences(Context context) {
100         if (sSharedPreferences == null) {
101             sSharedPreferences = context.createDeviceProtectedStorageContext().getSharedPreferences(
102                     FILENAME, Context.MODE_PRIVATE);
103         }
104         return sSharedPreferences;
105     }
106 
107     private final Context mContext;
108     private final ProvisionStateController mStateController;
109     private final Executor mExecutor;
110     private final DeviceLockControllerScheduler mScheduler;
111 
ProvisionHelperImpl(Context context, ProvisionStateController stateController)112     public ProvisionHelperImpl(Context context, ProvisionStateController stateController) {
113         this(context, stateController, Executors.newCachedThreadPool());
114     }
115 
116     @VisibleForTesting
ProvisionHelperImpl(Context context, ProvisionStateController stateController, Executor executor)117     ProvisionHelperImpl(Context context, ProvisionStateController stateController,
118             Executor executor) {
119         mContext = context;
120         mStateController = stateController;
121         DeviceLockControllerSchedulerProvider schedulerProvider =
122                 (DeviceLockControllerSchedulerProvider) mContext.getApplicationContext();
123         mScheduler = schedulerProvider.getDeviceLockControllerScheduler();
124         mExecutor = executor;
125     }
126 
127     @Override
pauseProvision()128     public void pauseProvision() {
129         Futures.addCallback(Futures.transformAsync(
130                         GlobalParametersClient.getInstance().setProvisionForced(true),
131                         unused -> mStateController.setNextStateForEvent(PROVISION_PAUSE),
132                         mExecutor),
133                 new FutureCallback<>() {
134                     @Override
135                     public void onSuccess(Void unused) {
136                         createNotification();
137                         WorkManager workManager = WorkManager.getInstance(mContext);
138                         PauseProvisioningWorker.reportProvisionPausedByUser(workManager);
139                         mScheduler.scheduleResumeProvisionAlarm();
140                     }
141 
142                     @Override
143                     public void onFailure(Throwable t) {
144                         throw new RuntimeException("Failed to delay setup", t);
145                     }
146                 }, mExecutor);
147     }
148 
149     @Override
scheduleKioskAppInstallation(LifecycleOwner owner, ProvisioningProgressController progressController, boolean isMandatory)150     public void scheduleKioskAppInstallation(LifecycleOwner owner,
151             ProvisioningProgressController progressController, boolean isMandatory) {
152         LogUtil.v(TAG, "Schedule installation work");
153         progressController.setProvisioningProgress(ProvisioningProgress.GETTING_DEVICE_READY);
154         WorkManager workManager = WorkManager.getInstance(mContext);
155         OneTimeWorkRequest isDeviceInApprovedCountryWork = getIsDeviceInApprovedCountryWork();
156 
157         final ListenableFuture<Operation.State.SUCCESS> enqueueResult =
158                 workManager.enqueueUniqueWork(IsDeviceInApprovedCountryWorker.class.getSimpleName(),
159                 ExistingWorkPolicy.REPLACE, isDeviceInApprovedCountryWork).getResult();
160         Futures.addCallback(enqueueResult, new FutureCallback<Operation.State.SUCCESS>() {
161                     @Override
162                     public void onSuccess(Operation.State.SUCCESS result) {
163                         // Enqueued
164                     }
165 
166                     @Override
167                     public void onFailure(Throwable t) {
168                         LogUtil.e(TAG, "Failed to enqueue 'device in approved country' work",
169                                 t);
170                         if (t instanceof SQLiteException) {
171                             mStateController.getDevicePolicyController().wipeDevice();
172                         } else {
173                             LogUtil.e(TAG, "Not wiping device (non SQL exception)");
174                         }
175                     }
176                 }, mExecutor);
177 
178         FutureCallback<String> isInApprovedCountryCallback = new FutureCallback<>() {
179             @Override
180             public void onSuccess(String kioskPackage) {
181                 progressController.setProvisioningProgress(
182                         ProvisioningProgress.INSTALLING_KIOSK_APP);
183                 if (getPreinstalledKioskAllowed(mContext)) {
184                     try {
185                         mContext.getPackageManager().getPackageInfo(kioskPackage,
186                                 ApplicationInfo.FLAG_INSTALLED);
187                         LogUtil.i(TAG, "Kiosk app is pre-installed");
188                         progressController.setProvisioningProgress(
189                                 ProvisioningProgress.OPENING_KIOSK_APP);
190 
191                         ReportDeviceProvisionStateWorker.reportSetupCompleted(workManager);
192                         ReviewDeviceProvisionStateWorker.cancelJobs(
193                                 WorkManager.getInstance(mContext));
194                         mStateController.postSetNextStateForEventRequest(PROVISION_KIOSK);
195                     } catch (NameNotFoundException e) {
196                         LogUtil.i(TAG, "Kiosk app is not pre-installed");
197                         installFromPlay(owner, kioskPackage, isMandatory, progressController);
198                     }
199                 } else {
200                     installFromPlay(owner, kioskPackage, isMandatory, progressController);
201                 }
202             }
203 
204             @Override
205             public void onFailure(Throwable t) {
206                 LogUtil.w(TAG, "Failed to install kiosk app!", t);
207                 handleFailure(ProvisionFailureReason.PLAY_INSTALLATION_FAILED, isMandatory,
208                         progressController);
209             }
210         };
211 
212         UUID isDeviceInApprovedCountryWorkId = isDeviceInApprovedCountryWork.getId();
213         workManager.getWorkInfoByIdLiveData(isDeviceInApprovedCountryWorkId)
214                 .observe(owner, workInfo -> {
215                     if (workInfo == null) return;
216                     WorkInfo.State state = workInfo.getState();
217                     LogUtil.d(TAG, "WorkInfo changed: " + workInfo);
218                     if (state == SUCCEEDED) {
219                         if (workInfo.getOutputData().getBoolean(KEY_IS_IN_APPROVED_COUNTRY,
220                                 false)) {
221                             Futures.addCallback(
222                                     SetupParametersClient.getInstance().getKioskPackage(),
223                                     isInApprovedCountryCallback, mExecutor);
224                         } else {
225                             LogUtil.i(TAG, "Not in eligible country");
226                             handleFailure(ProvisionFailureReason.NOT_IN_ELIGIBLE_COUNTRY,
227                                     isMandatory, progressController);
228                         }
229                     } else if (state == FAILED || state == CANCELLED) {
230                         LogUtil.w(TAG, "Failed to get country eligibility!");
231                         handleFailure(ProvisionFailureReason.COUNTRY_INFO_UNAVAILABLE, isMandatory,
232                                 progressController);
233                     }
234                 });
235 
236         // If the network is not available while checking if the device is in an approved country,
237         // wait for a finite amount of time for the network to come back up, to avoid blocking
238         // indefinitely.
239         Handler handler = new Handler(Looper.getMainLooper());
240         handler.postDelayed(() -> {
241             ListenableFuture<WorkInfo> workInfoFuture =
242                     WorkManager.getInstance(mContext)
243                             .getWorkInfoById(isDeviceInApprovedCountryWorkId);
244             Futures.addCallback(workInfoFuture, new FutureCallback<>() {
245                 @Override
246                 public void onSuccess(WorkInfo workInfo) {
247                     WorkInfo.State state = workInfo.getState();
248                     if (!(state == WorkInfo.State.SUCCEEDED
249                             || state == WorkInfo.State.FAILED
250                             || state == WorkInfo.State.CANCELLED)) {
251                         LogUtil.e(TAG, "Cannot determine if device "
252                                 + "is in an approved country, cancelling job");
253                         WorkManager.getInstance(mContext)
254                                 .cancelWorkById(isDeviceInApprovedCountryWorkId);
255                     }
256                 }
257 
258                 @Override
259                 public void onFailure(Throwable t) {
260                     LogUtil.e(TAG, "Cannot determine work state for device in approved "
261                             + "country", t);
262                 }
263             }, mExecutor);
264         }, IS_DEVICE_IN_APPROVED_COUNTRY_NETWORK_TIMEOUT_MS);
265     }
266 
installFromPlay(LifecycleOwner owner, String kioskPackage, boolean isMandatory, ProvisioningProgressController progressController)267     private void installFromPlay(LifecycleOwner owner, String kioskPackage, boolean isMandatory,
268             ProvisioningProgressController progressController) {
269         Context applicationContext = mContext.getApplicationContext();
270         final Class<? extends ListenableWorker> playInstallTaskClass =
271                 ((PlayInstallPackageTaskClassProvider) applicationContext)
272                         .getPlayInstallPackageTaskClass();
273         if (playInstallTaskClass == null) {
274             LogUtil.w(TAG, "Play installation not supported!");
275             handleFailure(
276                     ProvisionFailureReason.PLAY_TASK_UNAVAILABLE, isMandatory, progressController);
277             return;
278         }
279         OneTimeWorkRequest playInstallPackageTask =
280                 getPlayInstallPackageTask(playInstallTaskClass, kioskPackage);
281         WorkManager workManager = WorkManager.getInstance(mContext);
282         final ListenableFuture<Operation.State.SUCCESS> enqueueResult =
283                 workManager.enqueueUniqueWork(playInstallTaskClass.getSimpleName(),
284                         ExistingWorkPolicy.REPLACE, playInstallPackageTask).getResult();
285         Futures.addCallback(enqueueResult, new FutureCallback<Operation.State.SUCCESS>() {
286             @Override
287             public void onSuccess(Operation.State.SUCCESS result) {
288                 // Enqueued
289             }
290 
291             @Override
292             public void onFailure(Throwable t) {
293                 LogUtil.e(TAG, "Failed to enqueue 'play install' work", t);
294                 if (t instanceof SQLiteException) {
295                     mStateController.getDevicePolicyController().wipeDevice();
296                 } else {
297                     LogUtil.e(TAG, "Not wiping device (non SQL exception)");
298                 }
299             }
300         }, mExecutor);
301 
302         mContext.getMainExecutor().execute(
303                 () -> workManager.getWorkInfoByIdLiveData(playInstallPackageTask.getId())
304                         .observe(owner, workInfo -> {
305                             if (workInfo == null) return;
306                             WorkInfo.State state = workInfo.getState();
307                             LogUtil.d(TAG, "WorkInfo changed: " + workInfo);
308                             if (state == SUCCEEDED) {
309                                 progressController.setProvisioningProgress(
310                                         ProvisioningProgress.OPENING_KIOSK_APP);
311                                 ReportDeviceProvisionStateWorker.reportSetupCompleted(workManager);
312                                 ReviewDeviceProvisionStateWorker.cancelJobs(
313                                         WorkManager.getInstance(mContext));
314                                 mStateController.postSetNextStateForEventRequest(PROVISION_KIOSK);
315                             } else if (state == FAILED) {
316                                 LogUtil.w(TAG, "Play installation failed!");
317                                 handleFailure(ProvisionFailureReason.PLAY_INSTALLATION_FAILED,
318                                         isMandatory, progressController);
319                             }
320                         }));
321     }
322 
handleFailure(@rovisionFailureReason int reason, boolean isMandatory, ProvisioningProgressController progressController)323     private void handleFailure(@ProvisionFailureReason int reason, boolean isMandatory,
324             ProvisioningProgressController progressController) {
325         StatsLogger logger =
326                 ((StatsLoggerProvider) mContext.getApplicationContext()).getStatsLogger();
327         switch (reason) {
328             case ProvisionFailureReason.PLAY_TASK_UNAVAILABLE -> {
329                 logger.logProvisionFailure(
330                         StatsLogger.ProvisionFailureReasonStats.PLAY_TASK_UNAVAILABLE);
331             }
332             case ProvisionFailureReason.PLAY_INSTALLATION_FAILED -> {
333                 logger.logProvisionFailure(
334                         StatsLogger.ProvisionFailureReasonStats.PLAY_INSTALLATION_FAILED);
335             }
336             case ProvisionFailureReason.COUNTRY_INFO_UNAVAILABLE -> {
337                 logger.logProvisionFailure(
338                         StatsLogger.ProvisionFailureReasonStats.COUNTRY_INFO_UNAVAILABLE);
339             }
340             case ProvisionFailureReason.NOT_IN_ELIGIBLE_COUNTRY -> {
341                 logger.logProvisionFailure(
342                         StatsLogger.ProvisionFailureReasonStats.NOT_IN_ELIGIBLE_COUNTRY);
343             }
344             case ProvisionFailureReason.POLICY_ENFORCEMENT_FAILED -> {
345                 logger.logProvisionFailure(
346                         StatsLogger.ProvisionFailureReasonStats.POLICY_ENFORCEMENT_FAILED);
347             }
348             default -> {
349                 logger.logProvisionFailure(StatsLogger.ProvisionFailureReasonStats.UNKNOWN);
350             }
351         }
352         if (isMandatory) {
353             ReportDeviceProvisionStateWorker.reportSetupFailed(
354                     WorkManager.getInstance(mContext), reason);
355             progressController.setProvisioningProgress(
356                     ProvisioningProgress.getMandatoryProvisioningFailedProgress(reason));
357             mScheduler.scheduleMandatoryResetDeviceAlarm();
358         } else {
359             // For non-mandatory provisioning, failure should only be reported after
360             // user exits the provisioning UI; otherwise, it could be reported
361             // multiple times if user choose to retry, which can break the
362             // 7-days failure flow.
363             progressController.setProvisioningProgress(
364                     ProvisioningProgress.getNonMandatoryProvisioningFailedProgress(reason));
365         }
366     }
367 
368     @NonNull
getIsDeviceInApprovedCountryWork()369     private static OneTimeWorkRequest getIsDeviceInApprovedCountryWork() {
370         return new OneTimeWorkRequest.Builder(IsDeviceInApprovedCountryWorker.class)
371                 .setConstraints(new Constraints.Builder().setRequiredNetworkType(
372                         NetworkType.CONNECTED).build())
373                 // Set the request as expedited and use a short retry backoff time since the
374                 // user is in the setup flow while we check if the device is in an approved country
375                 .setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST)
376                 .setBackoffCriteria(BackoffPolicy.EXPONENTIAL,
377                         Duration.ofMillis(WorkRequest.MIN_BACKOFF_MILLIS))
378                 .build();
379     }
380 
381     @NonNull
getPlayInstallPackageTask( Class<? extends ListenableWorker> playInstallTaskClass, String kioskPackageName)382     private static OneTimeWorkRequest getPlayInstallPackageTask(
383             Class<? extends ListenableWorker> playInstallTaskClass, String kioskPackageName) {
384         return new OneTimeWorkRequest.Builder(playInstallTaskClass)
385                 .setInputData(new Data.Builder().putString(
386                         EXTRA_KIOSK_PACKAGE, kioskPackageName).build())
387                 .setConstraints(new Constraints.Builder().setRequiredNetworkType(
388                         NetworkType.CONNECTED).build())
389                 .setBackoffCriteria(BackoffPolicy.EXPONENTIAL, PLAY_INSTALL_BACKOFF_DELAY)
390                 .build();
391     }
392 
createNotification()393     private void createNotification() {
394         LogUtil.d(TAG, "createNotification");
395         Context context = mContext;
396 
397         PendingIntent pendingIntent = PendingIntent.getBroadcast(context,
398                 /* requestCode= */ 0, new Intent(context, ResumeProvisionReceiver.class),
399                 PendingIntent.FLAG_ONE_SHOT | PendingIntent.FLAG_IMMUTABLE);
400         LocalDateTime resumeDateTime = LocalDateTime.now().plusHours(1);
401         DeviceLockNotificationManager.getInstance()
402                 .sendDeferredProvisioningNotification(context, resumeDateTime, pendingIntent);
403     }
404 
405     /**
406      * Sets whether provisioning should skip play install if there is already a preinstalled kiosk
407      * app.
408      */
setPreinstalledKioskAllowed(Context context, boolean enabled)409     public static void setPreinstalledKioskAllowed(Context context, boolean enabled) {
410         getSharedPreferences(context).edit().putBoolean(USE_PREINSTALLED_KIOSK_PREF, enabled)
411                 .apply();
412     }
413 
414     /**
415      * Returns true if provisioning should skip play install if there is already a preinstalled
416      * kiosk app. By default, this returns true for debuggable build.
417      */
getPreinstalledKioskAllowed(Context context)418     private static boolean getPreinstalledKioskAllowed(Context context) {
419         return Build.isDebuggable() && getSharedPreferences(context).getBoolean(
420                 USE_PREINSTALLED_KIOSK_PREF, Build.isDebuggable());
421     }
422 }
423