/* * Copyright (C) 2022 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.devicelockcontroller.policy; import static com.android.devicelockcontroller.activities.ProvisioningActivity.EXTRA_SHOW_CRITICAL_PROVISION_FAILED_UI_ON_START; import static com.android.devicelockcontroller.common.DeviceLockConstants.ACTION_START_DEVICE_FINANCING_PROVISIONING; import static com.android.devicelockcontroller.common.DeviceLockConstants.ACTION_START_DEVICE_SUBSIDY_PROVISIONING; import static com.android.devicelockcontroller.policy.DeviceStateController.DeviceState.CLEARED; import static com.android.devicelockcontroller.policy.DeviceStateController.DeviceState.LOCKED; import static com.android.devicelockcontroller.policy.DeviceStateController.DeviceState.UNDEFINED; import static com.android.devicelockcontroller.policy.DeviceStateController.DeviceState.UNLOCKED; import static com.android.devicelockcontroller.policy.ProvisionStateController.ProvisionState.KIOSK_PROVISIONED; import static com.android.devicelockcontroller.policy.ProvisionStateController.ProvisionState.PROVISION_FAILED; import static com.android.devicelockcontroller.policy.ProvisionStateController.ProvisionState.PROVISION_IN_PROGRESS; import static com.android.devicelockcontroller.policy.ProvisionStateController.ProvisionState.PROVISION_PAUSED; import static com.android.devicelockcontroller.policy.ProvisionStateController.ProvisionState.PROVISION_SUCCEEDED; import static com.android.devicelockcontroller.policy.ProvisionStateController.ProvisionState.UNPROVISIONED; import static com.android.devicelockcontroller.policy.StartLockTaskModeWorker.START_LOCK_TASK_MODE_WORK_NAME; import android.app.admin.DevicePolicyManager; import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; import android.database.sqlite.SQLiteException; import android.os.Build; import android.os.UserManager; import androidx.annotation.GuardedBy; import androidx.annotation.VisibleForTesting; import androidx.work.ExistingWorkPolicy; import androidx.work.OneTimeWorkRequest; import androidx.work.Operation; import androidx.work.OutOfQuotaPolicy; import androidx.work.WorkManager; import com.android.devicelockcontroller.SystemDeviceLockManager; import com.android.devicelockcontroller.activities.LandingActivity; import com.android.devicelockcontroller.activities.ProvisioningActivity; import com.android.devicelockcontroller.common.DeviceLockConstants; import com.android.devicelockcontroller.common.DeviceLockConstants.ProvisioningType; import com.android.devicelockcontroller.policy.DeviceStateController.DeviceState; import com.android.devicelockcontroller.policy.ProvisionStateController.ProvisionState; import com.android.devicelockcontroller.provision.worker.ReportDeviceProvisionStateWorker; import com.android.devicelockcontroller.schedule.DeviceLockControllerScheduler; import com.android.devicelockcontroller.schedule.DeviceLockControllerSchedulerProvider; import com.android.devicelockcontroller.storage.GlobalParametersClient; import com.android.devicelockcontroller.storage.SetupParametersClient; import com.android.devicelockcontroller.util.LogUtil; import com.google.common.util.concurrent.FutureCallback; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.MoreExecutors; import java.util.ArrayList; import java.util.List; import java.util.concurrent.Executor; /** * An implementation of {@link DevicePolicyController}. This class guarantees thread safety by * synchronizing policies enforcement on background threads in the order of when the API calls * happen. That is, a pre-exist enforcement request will always blocks a incoming enforcement * request until the former completes. */ public final class DevicePolicyControllerImpl implements DevicePolicyController { private static final String TAG = "DevicePolicyControllerImpl"; private final List mPolicyList = new ArrayList<>(); private final Context mContext; private final DevicePolicyManager mDpm; private final ProvisionStateController mProvisionStateController; // A future that returns the current lock task type for the current provision/device state // after policies enforcement are done. @GuardedBy("this") private ListenableFuture<@LockTaskType Integer> mCurrentEnforcedLockTaskTypeFuture = Futures.immediateFuture(LockTaskType.UNDEFINED); private final Executor mBgExecutor; static final String ACTION_DEVICE_LOCK_KIOSK_SETUP = "com.android.devicelock.action.KIOSK_SETUP"; private static final String DEVICE_LOCK_VERSION_EXTRA = "DEVICE_LOCK_VERSION"; private static final int DEVICE_LOCK_VERSION = 2; private final UserManager mUserManager; /** * Create a new policy controller. * * @param context The context used by this policy controller. * @param devicePolicyManager The device policy manager. * @param userManager The user manager. * @param systemDeviceLockManager The system device lock manager. * @param provisionStateController The provision state controller. * @param bgExecutor The background executor. */ public DevicePolicyControllerImpl(Context context, DevicePolicyManager devicePolicyManager, UserManager userManager, SystemDeviceLockManager systemDeviceLockManager, ProvisionStateController provisionStateController, Executor bgExecutor) { this(context, devicePolicyManager, userManager, new UserRestrictionsPolicyHandler(devicePolicyManager, userManager, Build.isDebuggable(), bgExecutor), new AppOpsPolicyHandler(systemDeviceLockManager, bgExecutor), new LockTaskModePolicyHandler(context, devicePolicyManager, bgExecutor), new PackagePolicyHandler(context, devicePolicyManager, bgExecutor), new RolePolicyHandler(systemDeviceLockManager, bgExecutor), new KioskKeepAlivePolicyHandler(systemDeviceLockManager, bgExecutor), new ControllerKeepAlivePolicyHandler(systemDeviceLockManager, bgExecutor), new NotificationsPolicyHandler(systemDeviceLockManager, bgExecutor), provisionStateController, bgExecutor); } @VisibleForTesting DevicePolicyControllerImpl(Context context, DevicePolicyManager devicePolicyManager, UserManager userManager, UserRestrictionsPolicyHandler userRestrictionsPolicyHandler, AppOpsPolicyHandler appOpsPolicyHandler, LockTaskModePolicyHandler lockTaskModePolicyHandler, PackagePolicyHandler packagePolicyHandler, RolePolicyHandler rolePolicyHandler, KioskKeepAlivePolicyHandler kioskKeepAlivePolicyHandler, ControllerKeepAlivePolicyHandler controllerKeepAlivePolicyHandler, NotificationsPolicyHandler notificationsPolicyHandler, ProvisionStateController provisionStateController, Executor bgExecutor) { mContext = context; mProvisionStateController = provisionStateController; mBgExecutor = bgExecutor; mDpm = devicePolicyManager; mUserManager = userManager; mPolicyList.add(userRestrictionsPolicyHandler); mPolicyList.add(appOpsPolicyHandler); mPolicyList.add(lockTaskModePolicyHandler); mPolicyList.add(packagePolicyHandler); mPolicyList.add(rolePolicyHandler); mPolicyList.add(kioskKeepAlivePolicyHandler); mPolicyList.add(controllerKeepAlivePolicyHandler); mPolicyList.add(notificationsPolicyHandler); } @Override public boolean wipeDevice() { LogUtil.i(TAG, "Wiping device"); try { mDpm.wipeDevice(DevicePolicyManager.WIPE_SILENTLY | DevicePolicyManager.WIPE_RESET_PROTECTION_DATA); } catch (SecurityException e) { LogUtil.e(TAG, "Cannot wipe device", e); return false; } return true; } @Override public ListenableFuture enforceCurrentPolicies() { return Futures.transform(enforceCurrentPoliciesAndResolveLockTaskType( /* failure= */ false), mode -> { startLockTaskModeIfNeeded(mode); return null; }, mBgExecutor); } @Override public ListenableFuture enforceCurrentPoliciesForCriticalFailure() { return Futures.transform(enforceCurrentPoliciesAndResolveLockTaskType( /* failure= */ true), mode -> { startLockTaskModeIfNeeded(mode); handlePolicyEnforcementFailure(); return null; }, mBgExecutor); } private void handlePolicyEnforcementFailure() { final DeviceLockControllerSchedulerProvider schedulerProvider = (DeviceLockControllerSchedulerProvider) mContext.getApplicationContext(); final DeviceLockControllerScheduler scheduler = schedulerProvider.getDeviceLockControllerScheduler(); // Hard failure due to policy enforcement, treat it as mandatory reset device alarm. scheduler.scheduleMandatoryResetDeviceAlarm(); ReportDeviceProvisionStateWorker.reportSetupFailed(WorkManager.getInstance(mContext), DeviceLockConstants.ProvisionFailureReason.POLICY_ENFORCEMENT_FAILED); } /** * Enforce current policies and then return the resulting lock task type * * @param failure true if this enforcement is due to resetting policies in case of failure. * @return A future for the lock task type corresponding to the current policies. */ private ListenableFuture<@LockTaskType Integer> enforceCurrentPoliciesAndResolveLockTaskType( boolean failure) { synchronized (this) { // current lock task type must be assigned to a local variable; otherwise, if // retrieved down the execution flow, it will be returning the new type after execution. ListenableFuture<@LockTaskType Integer> currentLockTaskType = mCurrentEnforcedLockTaskTypeFuture; ListenableFuture<@LockTaskType Integer> policiesEnforcementFuture = Futures.transformAsync( currentLockTaskType, unused -> { final ListenableFuture<@ProvisionState Integer> provisionState = mProvisionStateController.getState(); final ListenableFuture<@DeviceState Integer> deviceState = GlobalParametersClient.getInstance().getDeviceState(); return Futures.whenAllSucceed(provisionState, deviceState) .callAsync( () -> enforcePoliciesForCurrentStates( Futures.getDone(provisionState), Futures.getDone(deviceState)), mBgExecutor ); }, mBgExecutor); if (failure) { mCurrentEnforcedLockTaskTypeFuture = Futures.immediateFuture( LockTaskType.CRITICAL_ERROR); return mCurrentEnforcedLockTaskTypeFuture; } else { // To prevent exception propagate to future policies enforcement, catch any // exceptions that might happen during the execution and fallback to previous type // if exception happens. mCurrentEnforcedLockTaskTypeFuture = Futures.catchingAsync( policiesEnforcementFuture, Exception.class, unused -> currentLockTaskType, MoreExecutors.directExecutor()); } return policiesEnforcementFuture; } } private ListenableFuture<@LockTaskType Integer> enforcePoliciesForCurrentStates( @ProvisionState int provisionState, @DeviceState int deviceState) { LogUtil.i(TAG, "Enforcing policies for provision state " + provisionState + " and device state " + deviceState); if (provisionState == UNPROVISIONED) { return Futures.immediateFuture(resolveLockTaskType(provisionState, deviceState)); } List> futures = new ArrayList<>(); if (deviceState == CLEARED) { // If device is cleared, then ignore provision state and add cleared policies for (int i = 0, policyLen = mPolicyList.size(); i < policyLen; i++) { PolicyHandler policy = mPolicyList.get(i); futures.add(policy.onCleared()); } } else if (provisionState == PROVISION_SUCCEEDED) { // If provisioning has succeeded, then ignore provision state and add device state // policies for (int i = 0, policyLen = mPolicyList.size(); i < policyLen; i++) { PolicyHandler policy = mPolicyList.get(i); switch (deviceState) { case UNLOCKED: futures.add(policy.onUnlocked()); break; case LOCKED: futures.add(policy.onLocked()); break; case UNDEFINED: // No policies to enforce in this state. break; default: throw new IllegalArgumentException( "Invalid device state to enforce: " + deviceState); } } } else { for (int i = 0, policyLen = mPolicyList.size(); i < policyLen; i++) { PolicyHandler policy = mPolicyList.get(i); switch (provisionState) { case PROVISION_IN_PROGRESS: futures.add(policy.onProvisionInProgress()); break; case KIOSK_PROVISIONED: futures.add(policy.onProvisioned()); break; case PROVISION_PAUSED: futures.add(policy.onProvisionPaused()); break; case PROVISION_FAILED: futures.add(policy.onProvisionFailed()); break; default: throw new IllegalArgumentException( "Invalid provision state to enforce: " + provisionState); } } } return Futures.transform(Futures.allAsList(futures), results -> { if (results.stream().reduce(true, (a, r) -> a && r)) { return resolveLockTaskType(provisionState, deviceState); } else { throw new IllegalStateException( "Failed to enforce policies for provision state " + provisionState + " and device state " + deviceState); } }, MoreExecutors.directExecutor()); } /** * Determines the lock task type based on the current provision and device state */ private @LockTaskType int resolveLockTaskType(int provisionState, int deviceState) { if (provisionState == UNPROVISIONED || deviceState == CLEARED) { return LockTaskType.NOT_IN_LOCK_TASK; } if (provisionState == PROVISION_IN_PROGRESS) { return LockTaskType.LANDING_ACTIVITY; } if (provisionState == KIOSK_PROVISIONED) { return LockTaskType.KIOSK_SETUP_ACTIVITY; } if (provisionState == PROVISION_SUCCEEDED && deviceState == LOCKED) { return LockTaskType.KIOSK_LOCK_ACTIVITY; } return LockTaskType.NOT_IN_LOCK_TASK; } private ListenableFuture getLockScreenActivityIntent() { return Futures.transform( SetupParametersClient.getInstance().getKioskPackage(), kioskPackage -> { if (kioskPackage == null) { throw new IllegalStateException("Missing kiosk package parameter!"); } Intent homeIntent = new Intent(Intent.ACTION_MAIN) .addCategory(Intent.CATEGORY_HOME) .setPackage(kioskPackage); PackageManager pm = mContext.getPackageManager(); ResolveInfo resolvedInfo = pm.resolveActivity(homeIntent, PackageManager.MATCH_DEFAULT_ONLY); if (resolvedInfo != null && resolvedInfo.activityInfo != null) { return homeIntent.setComponent( new ComponentName(kioskPackage, resolvedInfo.activityInfo.name)); } // Kiosk app does not have an activity to handle the default // home intent. Fall back to the launch activity. // Note that in this case, Kiosk App can't be effectively set as // the default home activity. Intent launchIntent = pm.getLaunchIntentForPackage(kioskPackage); if (launchIntent == null) { throw new IllegalStateException( "Failed to get launch intent for kiosk app!"); } return launchIntent; }, mBgExecutor); } private ListenableFuture getLandingActivityIntent() { SetupParametersClient client = SetupParametersClient.getInstance(); ListenableFuture<@ProvisioningType Integer> provisioningType = client.getProvisioningType(); return Futures.transform(provisioningType, type -> { Intent resultIntent = new Intent(mContext, LandingActivity.class); switch (type) { case ProvisioningType.TYPE_FINANCED: // TODO(b/288923554) this used to return an intent with action // ACTION_START_DEVICE_FINANCING_SECONDARY_USER_PROVISIONING // for secondary users. Rework once a decision has been made about // what to show to users. return resultIntent.setAction( ACTION_START_DEVICE_FINANCING_PROVISIONING); case ProvisioningType.TYPE_SUBSIDY: return resultIntent.setAction(ACTION_START_DEVICE_SUBSIDY_PROVISIONING); case ProvisioningType.TYPE_UNDEFINED: default: throw new IllegalArgumentException("Provisioning type is unknown!"); } }, mBgExecutor); } private ListenableFuture getKioskSetupActivityIntent() { return Futures.transform(SetupParametersClient.getInstance().getKioskPackage(), kioskPackageName -> { if (kioskPackageName == null) { throw new IllegalStateException("Missing kiosk package parameter!"); } final Intent kioskSetupIntent = new Intent(ACTION_DEVICE_LOCK_KIOSK_SETUP); kioskSetupIntent.setPackage(kioskPackageName); final ResolveInfo resolveInfo = mContext.getPackageManager() .resolveActivity(kioskSetupIntent, PackageManager.MATCH_DEFAULT_ONLY); if (resolveInfo == null || resolveInfo.activityInfo == null) { throw new IllegalStateException( "Failed to get setup activity intent for kiosk app!"); } kioskSetupIntent.putExtra(DEVICE_LOCK_VERSION_EXTRA, DEVICE_LOCK_VERSION); return kioskSetupIntent.setComponent(new ComponentName(kioskPackageName, resolveInfo.activityInfo.name)); }, mBgExecutor); } private ListenableFuture getProvisioningActivityIntentForCriticalFailure() { final Intent intent = new Intent(mContext, ProvisioningActivity.class) .putExtra(EXTRA_SHOW_CRITICAL_PROVISION_FAILED_UI_ON_START, true); return Futures.immediateFuture(intent); } @Override public ListenableFuture getLaunchIntentForCurrentState() { return Futures.transformAsync(getCurrentEnforcedLockTaskType(), type -> { switch (type) { case LockTaskType.NOT_IN_LOCK_TASK: return Futures.immediateFuture(null); case LockTaskType.LANDING_ACTIVITY: return getLandingActivityIntent(); case LockTaskType.CRITICAL_ERROR: return getProvisioningActivityIntentForCriticalFailure(); case LockTaskType.KIOSK_SETUP_ACTIVITY: return getKioskSetupActivityIntent(); case LockTaskType.KIOSK_LOCK_ACTIVITY: return getLockScreenActivityIntent(); default: throw new IllegalArgumentException("Invalid lock task type!"); } }, mBgExecutor); } /** * Gets the currently enforced lock task type, enforcing current policies if they haven't been * enforced yet. */ private ListenableFuture<@LockTaskType Integer> getCurrentEnforcedLockTaskType() { synchronized (this) { return Futures.transformAsync( mCurrentEnforcedLockTaskTypeFuture, type -> type == LockTaskType.UNDEFINED ? Futures.transform(enforceCurrentPoliciesAndResolveLockTaskType( /* failure= */ false), mode -> { startLockTaskModeIfNeeded(mode); return mode; }, mBgExecutor) : Futures.immediateFuture(type), mBgExecutor); } } @Override public ListenableFuture onUserUnlocked() { return Futures.transformAsync(mProvisionStateController.onUserUnlocked(), unused -> Futures.transform(getCurrentEnforcedLockTaskType(), mode -> { startLockTaskModeIfNeeded(mode); return null; }, mBgExecutor), mBgExecutor); } @Override public ListenableFuture onUserSetupCompleted() { return mProvisionStateController.onUserSetupCompleted(); } @Override public ListenableFuture onAppCrashed(boolean isKiosk) { final String crashedApp = isKiosk ? "kiosk" : "dlc"; LogUtil.i(TAG, "Controller notified about " + crashedApp + " having crashed while in lock task mode"); return Futures.transform(getCurrentEnforcedLockTaskType(), mode -> { startLockTaskModeIfNeeded(mode); return null; }, mBgExecutor); } private void startLockTaskModeIfNeeded(@LockTaskType Integer type) { if (type == LockTaskType.NOT_IN_LOCK_TASK || !mUserManager.isUserUnlocked()) { return; } WorkManager workManager = WorkManager.getInstance(mContext); OneTimeWorkRequest startLockTask = new OneTimeWorkRequest.Builder( StartLockTaskModeWorker.class) .setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST) .build(); final ListenableFuture enqueueResult = workManager.enqueueUniqueWork(START_LOCK_TASK_MODE_WORK_NAME, ExistingWorkPolicy.REPLACE, startLockTask).getResult(); Futures.addCallback(enqueueResult, new FutureCallback<>() { @Override public void onSuccess(Operation.State.SUCCESS result) { // Enqueued } @Override public void onFailure(Throwable t) { LogUtil.e(TAG, "Failed to enqueue 'start lock task mode' work", t); if (t instanceof SQLiteException) { wipeDevice(); } else { LogUtil.e(TAG, "Not wiping device (non SQL exception)"); } } }, mBgExecutor); } }