/*
 * 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_RECOL_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.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.FeatureFlagProvider;
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;
import java.util.stream.Collectors;

/**
 * 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 {
    static final String ACTION_DEVICE_LOCK_KIOSK_SETUP =
            "com.android.devicelock.action.KIOSK_SETUP";
    private static final String DEVICE_LOCK_VERSION_EXTRA =
            "android.devicelock.extra.DEVICE_LOCK_VERSION";
    // Added for support to existing implementations before the
    // DEVICE_LOCK_VERSION_EXTRA is merged to 25Q4
    // TODO: b/417696889 - deprecate this.
    private static final String OLD_DEVICE_LOCK_VERSION_EXTRA = "DEVICE_LOCK_VERSION";
    private static final String TAG = "DevicePolicyControllerImpl";
    private static final int DEVICE_LOCK_VERSION = 2;
    private final List<PolicyHandler> mPolicyList = new ArrayList<>();
    private final Context mContext;
    private final DevicePolicyManager mDpm;
    private final ProvisionStateController mProvisionStateController;
    private final Executor mBgExecutor;
    private final UserManager mUserManager;
    private final FeatureFlagProvider mFeatureFlagProvider;

    /**
     * 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,
            PolicyHandler userRestrictionsPolicyHandler,
            PolicyHandler appOpsPolicyHandler,
            PolicyHandler lockTaskModePolicyHandler,
            PolicyHandler packagePolicyHandler,
            PolicyHandler rolePolicyHandler,
            PolicyHandler kioskKeepAlivePolicyHandler,
            PolicyHandler controllerKeepAlivePolicyHandler,
            PolicyHandler notificationsPolicyHandler,
            ProvisionStateController provisionStateController,
            Executor bgExecutor) {
        mContext = context;
        mProvisionStateController = provisionStateController;
        mBgExecutor = bgExecutor;
        mDpm = devicePolicyManager;
        mUserManager = userManager;
        mFeatureFlagProvider = (FeatureFlagProvider) context.getApplicationContext();
        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");
        if (mFeatureFlagProvider.isRecolEnabled()) {
            try {
                @ProvisioningType int provisioningType =
                        SetupParametersClient.getInstance().getProvisioningType().get();
                if (provisioningType == ProvisioningType.TYPE_RECOL) {
                    LogUtil.w(TAG, "Wipe is not allowed for recol provisioning type");
                    return false;
                }
            } catch (java.util.concurrent.ExecutionException | InterruptedException e) {
                // TODO: b/438125704 - Handle this edge case in a more robust way.
                LogUtil.e(TAG, "Failed to get provisioning type, proceeding with wipe.", e);
            }
        }
        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<Void> enforceCurrentPolicies() {
        return Futures.transform(
                enforceCurrentPoliciesAndResolveLockTaskType(/* failure= */ false),
                mode -> {
                    startLockTaskModeIfNeeded(mode);
                    return null;
                },
                mBgExecutor);
    }

    @Override
    public ListenableFuture<Void> 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 =
                    GlobalParametersClient.getInstance().getLockTaskType();

            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) {
                return Futures.transform(
                        GlobalParametersClient.getInstance()
                                .setLockTaskType(LockTaskType.CRITICAL_ERROR),
                        unused -> LockTaskType.CRITICAL_ERROR,
                        mBgExecutor);
            } 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.
                ListenableFuture<@LockTaskType Integer> outcomeFuture =
                        Futures.catchingAsync(
                                policiesEnforcementFuture,
                                Exception.class,
                                unused -> currentLockTaskType,
                                MoreExecutors.directExecutor());
                ListenableFuture<Void> storageFuture =
                        Futures.transformAsync(
                                outcomeFuture,
                                newLockTaskType -> {
                                    LogUtil.i(TAG, "Resolved LockTaskType: " + newLockTaskType);
                                    return GlobalParametersClient.getInstance()
                                            .setLockTaskType(newLockTaskType);
                                },
                                mBgExecutor);
                return Futures.transformAsync(
                        storageFuture, unused -> policiesEnforcementFuture, mBgExecutor);
            }
        }
    }

    private ListenableFuture<@LockTaskType Integer> enforcePoliciesForCurrentStates(
            @ProvisionState int provisionState, @DeviceState int deviceState) {
        LogUtil.i(
                TAG,
                "Enforcing policies for provision state "
                        + provisionState
                        + " and device state "
                        + deviceState);
        List<ListenableFuture<Boolean>> 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 UNPROVISIONED:
                        futures.add(policy.onUnprovisioned());
                        break;
                    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<Intent> 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<Intent> 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_RECOL:
                            if (mFeatureFlagProvider.isRecolEnabled()) {
                                return resultIntent.setAction(
                                        ACTION_START_DEVICE_RECOL_PROVISIONING);
                            }
                        case ProvisioningType.TYPE_UNDEFINED:
                        default:
                            throw new IllegalArgumentException("Provisioning type is unknown!");
                    }
                },
                mBgExecutor);
    }

    private ListenableFuture<Intent> 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);
                    // Added for support to existing implementations before the
                    // DEVICE_LOCK_VERSION_EXTRA is merged to 25Q4
                    // TODO: b/417696889 - deprecate this.
                    kioskSetupIntent.putExtra(OLD_DEVICE_LOCK_VERSION_EXTRA, DEVICE_LOCK_VERSION);
                    return kioskSetupIntent.setComponent(
                            new ComponentName(kioskPackageName, resolveInfo.activityInfo.name));
                },
                mBgExecutor);
    }

    private ListenableFuture<Intent> 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<Intent> 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) {
            final ListenableFuture<@LockTaskType Integer> lockTaskTypeFuture =
                    GlobalParametersClient.getInstance().getLockTaskType();

            return Futures.transformAsync(
                    lockTaskTypeFuture,
                    type -> {
                        LogUtil.i(TAG, "Current LockTaskType: " + type);
                        return type == LockTaskType.UNDEFINED
                                ? Futures.transform(
                                enforceCurrentPoliciesAndResolveLockTaskType(
                                        /* failure= */ false),
                                    mode -> {
                                        startLockTaskModeIfNeeded(mode);
                                        return mode;
                                    },
                                mBgExecutor)
                                : Futures.immediateFuture(type);
                    },
                    mBgExecutor);
        }
    }

    @Override
    public ListenableFuture<Void> onUserUnlocked() {
        return Futures.transformAsync(
                mProvisionStateController.onUserUnlocked(),
                unused ->
                        Futures.transform(
                                getCurrentEnforcedLockTaskType(),
                                mode -> {
                                    startLockTaskModeIfNeeded(mode);
                                    return null;
                                },
                                mBgExecutor),
                mBgExecutor);
    }

    @Override
    public ListenableFuture<Void> onUserSetupCompleted() {
        return mProvisionStateController.onUserSetupCompleted();
    }

    @Override
    public ListenableFuture<Void> 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);
    }

    @Override
    public void disableUserControlForCheckInRequiredPackage(String packageName) {
        try {
            List<String> userControlDisabledPackages =
                    mDpm.getUserControlDisabledPackages(null /* admin */);
            if (!userControlDisabledPackages.contains(packageName)) {
                List<String> updatedPackages = new ArrayList<>(userControlDisabledPackages);
                updatedPackages.add(packageName);
                mDpm.setUserControlDisabledPackages(null /* admin */, updatedPackages);
            }
        } catch (SecurityException e) {
            // We do not expect this to happen as the controller has the required permissions.
            LogUtil.e(
                    TAG,
                    "Failed to disable user control for package" + packageName + ": Exception",
                    e);
        }
    }

    @Override
    public void enableUserControlForCheckInRequiredPackage(String packageName) {
        try {
            List<String> userControlDisabledPackages =
                    mDpm.getUserControlDisabledPackages(null /* admin */);
            List<String> updatedPackages =
                    userControlDisabledPackages.stream()
                            .filter(pkg -> !pkg.equals(packageName))
                            .collect(Collectors.toCollection(ArrayList::new));

            if (userControlDisabledPackages.contains(packageName)) {
                mDpm.setUserControlDisabledPackages(null /* admin */, updatedPackages);
            }
        } catch (SecurityException e) {
            // We do not expect this to happen as the controller has the required permissions.
            LogUtil.e(
                    TAG,
                    "Failed to remove user control restrictions for package"
                            + packageName
                            + ": Exception",
                    e);
        }
    }

    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<Operation.State.SUCCESS> 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);
    }
}
