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