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