/* * 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.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.ProvisionEvent.PROVISION_SUCCESS; import static com.android.devicelockcontroller.policy.ProvisionStateController.ProvisionState.KIOSK_PROVISIONED; import static com.android.devicelockcontroller.policy.ProvisionStateController.ProvisionState.PROVISION_SUCCEEDED; import static com.android.devicelockcontroller.policy.ProvisionStateController.ProvisionState.UNPROVISIONED; import com.android.devicelockcontroller.storage.GlobalParametersClient; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.MoreExecutors; import java.util.concurrent.Executor; /** An implementation of the {@link DeviceStateController} */ public final class DeviceStateControllerImpl implements DeviceStateController { private final ProvisionStateController mProvisionStateController; private final DevicePolicyController mPolicyController; private final GlobalParametersClient mGlobalParametersClient; private final Executor mExecutor; // Used to exercising APIs under CTS without actually applying any policies. // This is not persistent across controller restarts, but should be good enough for the // intended purpose. private volatile @DeviceState int mPseudoDeviceState; public DeviceStateControllerImpl(DevicePolicyController policyController, ProvisionStateController provisionStateController, Executor executor) { mPolicyController = policyController; mProvisionStateController = provisionStateController; mGlobalParametersClient = GlobalParametersClient.getInstance(); mExecutor = executor; mPseudoDeviceState = UNDEFINED; } @Override public ListenableFuture lockDevice() { return setDeviceState(LOCKED); } @Override public ListenableFuture unlockDevice() { return setDeviceState(UNLOCKED); } @Override public ListenableFuture clearDevice() { return setDeviceState(CLEARED); } /** * Set the global device state to be the input {@link DeviceState}. The returned * {@link ListenableFuture} will complete when both the state change and policies enforcement * for new state are done. */ private ListenableFuture setDeviceState(@DeviceState int deviceState) { if (deviceState == UNDEFINED) { throw new IllegalArgumentException("Cannot set device state to UNDEFINED"); } return Futures.transformAsync(mProvisionStateController.getState(), provisionState -> { final ListenableFuture maybeSetProvisioningSuccess; if (provisionState == KIOSK_PROVISIONED) { maybeSetProvisioningSuccess = mProvisionStateController.setNextStateForEvent(PROVISION_SUCCESS); } else if (provisionState == PROVISION_SUCCEEDED) { maybeSetProvisioningSuccess = Futures.immediateVoidFuture(); } else if (provisionState == UNPROVISIONED && (deviceState == LOCKED || deviceState == UNLOCKED)) { // During normal operation, we should not get lock/unlock requests in // the UNPROVISIONED state. Used for CTS compliance. mPseudoDeviceState = deviceState; // Do not apply any policies return Futures.immediateVoidFuture(); } else { throw new RuntimeException( "User has not been provisioned! Current state " + provisionState); } return Futures.transformAsync(maybeSetProvisioningSuccess, unused -> Futures.transformAsync(isCleared(), isCleared -> { if (isCleared) { throw new IllegalStateException("Device has been " + "cleared!"); } return Futures.transformAsync( mGlobalParametersClient.setDeviceState(deviceState), state -> mPolicyController.enforceCurrentPolicies(), mExecutor); }, mExecutor), mExecutor); }, mExecutor); } @Override public ListenableFuture isLocked() { return Futures.transformAsync(mProvisionStateController.getState(), provisionState -> { if (provisionState == UNPROVISIONED) { // Used for CTS compliance. return Futures.immediateFuture(mPseudoDeviceState == LOCKED); } else { return Futures.transform(mGlobalParametersClient.getDeviceState(), s -> { if (s == UNDEFINED) { throw new IllegalStateException("isLocked called before " + "setting the locked state " + "(lockDevice/unlockDevice)"); } return s == LOCKED; }, mExecutor); } }, mExecutor); } @Override public ListenableFuture getDeviceState() { return mGlobalParametersClient.getDeviceState(); } @Override public ListenableFuture isCleared() { return Futures.transform(mGlobalParametersClient.getDeviceState(), s -> s == CLEARED, MoreExecutors.directExecutor()); } }