/* * Copyright (C) 2020 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 android.hardware.devicestate; import android.Manifest; import android.annotation.CallbackExecutor; import android.annotation.FlaggedApi; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.RequiresPermission; import android.annotation.SuppressLint; import android.annotation.SystemApi; import android.annotation.SystemService; import android.annotation.TestApi; import android.content.Context; import com.android.internal.util.ArrayUtils; import java.util.List; import java.util.concurrent.Executor; import java.util.function.Consumer; /** * Manages the state of the system for devices with user-configurable hardware like a foldable * phone. * * @hide */ @SystemApi @FlaggedApi(android.hardware.devicestate.feature.flags.Flags.FLAG_DEVICE_STATE_PROPERTY_API) @SystemService(Context.DEVICE_STATE_SERVICE) public final class DeviceStateManager { /** * Invalid device state. * * @hide */ @TestApi public static final int INVALID_DEVICE_STATE_IDENTIFIER = -1; /** * The minimum allowed device state identifier. * @hide */ @TestApi public static final int MINIMUM_DEVICE_STATE_IDENTIFIER = 0; /** * The maximum allowed device state identifier. * @hide */ @TestApi public static final int MAXIMUM_DEVICE_STATE_IDENTIFIER = 10000; /** * Intent needed to launch the rear display overlay activity from SysUI * * @hide */ public static final String ACTION_SHOW_REAR_DISPLAY_OVERLAY = "com.android.intent.action.SHOW_REAR_DISPLAY_OVERLAY"; /** * Intent extra sent to the rear display overlay activity of the current base state * * @hide */ public static final String EXTRA_ORIGINAL_DEVICE_BASE_STATE = "original_device_base_state"; private final DeviceStateManagerGlobal mGlobal; /** @hide */ public DeviceStateManager() { DeviceStateManagerGlobal global = DeviceStateManagerGlobal.getInstance(); if (global == null) { throw new IllegalStateException( "Failed to get instance of global device state manager."); } mGlobal = global; } /** * Returns the list of device states that are supported and can be requested with * {@link #requestState(DeviceStateRequest, Executor, DeviceStateRequest.Callback)}. */ @NonNull public List getSupportedDeviceStates() { return mGlobal.getSupportedDeviceStates(); } /** * Submits a {@link DeviceStateRequest request} to modify the device state. *

* By default, the request is kept active until one of the following occurs: *

* However, this behavior can be changed by setting flags on the {@link DeviceStateRequest}. * * @throws IllegalArgumentException if the requested state is unsupported. * @throws SecurityException if the caller is neither the current top-focused activity nor if * the {@link android.Manifest.permission#CONTROL_DEVICE_STATE} permission is held. * * @see DeviceStateRequest * @hide */ @SuppressLint("RequiresPermission") // Lint doesn't handle conditional permission checks today @TestApi @RequiresPermission(value = android.Manifest.permission.CONTROL_DEVICE_STATE, conditional = true) public void requestState(@NonNull DeviceStateRequest request, @Nullable @CallbackExecutor Executor executor, @Nullable DeviceStateRequest.Callback callback) { mGlobal.requestState(request, executor, callback); } /** * Cancels the active {@link DeviceStateRequest} previously submitted with a call to * {@link #requestState(DeviceStateRequest, Executor, DeviceStateRequest.Callback)}. *

* This method is noop if there is no request currently active. * * @throws SecurityException if the caller is neither the current top-focused activity nor if * the {@link android.Manifest.permission#CONTROL_DEVICE_STATE} permission is held. * @hide */ @SuppressLint("RequiresPermission") // Lint doesn't handle conditional permission checks today @TestApi @RequiresPermission(value = android.Manifest.permission.CONTROL_DEVICE_STATE, conditional = true) public void cancelStateRequest() { mGlobal.cancelStateRequest(); } /** * Submits a {@link DeviceStateRequest request} to override the base state of the device. This * should only be used for testing, where you want to simulate the physical change to the * device state. *

* By default, the request is kept active until one of the following occurs: *

* * Submitting a base state override request may not cause any change in the presentation * of the system if there is an emulated request made through {@link #requestState}, as the * emulated override requests take priority. * * @throws IllegalArgumentException if the requested state is unsupported. * * @see DeviceStateRequest * @hide */ @TestApi @RequiresPermission(android.Manifest.permission.CONTROL_DEVICE_STATE) public void requestBaseStateOverride(@NonNull DeviceStateRequest request, @Nullable @CallbackExecutor Executor executor, @Nullable DeviceStateRequest.Callback callback) { mGlobal.requestBaseStateOverride(request, executor, callback); } /** * Cancels the active {@link DeviceStateRequest} previously submitted with a call to * {@link #requestBaseStateOverride(DeviceStateRequest, Executor, DeviceStateRequest.Callback)}. *

* This method is noop if there is no base state request currently active. * * @hide */ @TestApi @RequiresPermission(Manifest.permission.CONTROL_DEVICE_STATE) public void cancelBaseStateOverride() { mGlobal.cancelBaseStateOverride(); } /** * Registers a callback to receive notifications about changes in device state. * * @param executor the executor to process notifications. * @param callback the callback to register. * * @see DeviceStateCallback */ public void registerCallback(@NonNull @CallbackExecutor Executor executor, @NonNull DeviceStateCallback callback) { mGlobal.registerDeviceStateCallback(callback, executor); } /** * Unregisters a callback previously registered with * {@link #registerCallback(Executor, DeviceStateCallback)}. */ public void unregisterCallback(@NonNull DeviceStateCallback callback) { mGlobal.unregisterDeviceStateCallback(callback); } /** Callback to receive notifications about changes in device state. */ public interface DeviceStateCallback { /** * Called in response to a change in the states supported by the device. *

* Guaranteed to be called once on registration of the callback with the initial value and * then on every subsequent change in the supported states. * * The supported device states may change due to certain states becoming unavailable * due to device configuration or device conditions such as if the device is too hot or * external monitors have been connected. * * @param supportedStates the new supported states. * * @see DeviceStateManager#getSupportedDeviceStates() */ default void onSupportedStatesChanged(@NonNull List supportedStates) {} /** * Called in response to device state changes. *

* Guaranteed to be called once on registration of the callback with the initial value and * then on every subsequent change in device state. * * @param state the new device state. */ void onDeviceStateChanged(@NonNull DeviceState state); } /** * Listens to changes in device state and reports the state as folded if the device state * matches the value in the {@link com.android.internal.R.integer.config_foldedDeviceState} * resource. * @hide */ public static class FoldStateListener implements DeviceStateCallback { private final int[] mFoldedDeviceStates; private final Consumer mDelegate; private final android.hardware.devicestate.feature.flags.FeatureFlags mFeatureFlags; @Nullable private Boolean lastResult; public FoldStateListener(Context context) { this(context, folded -> {}); } public FoldStateListener(Context context, Consumer listener) { mFoldedDeviceStates = context.getResources().getIntArray( com.android.internal.R.array.config_foldedDeviceStates); mDelegate = listener; mFeatureFlags = new android.hardware.devicestate.feature.flags.FeatureFlagsImpl(); } @Override public final void onDeviceStateChanged(@NonNull DeviceState deviceState) { final boolean folded; if (mFeatureFlags.deviceStatePropertyApi()) { // TODO(b/325124054): Update when system server refactor is completed folded = deviceState.hasProperty( DeviceState.PROPERTY_FOLDABLE_DISPLAY_CONFIGURATION_OUTER_PRIMARY) || ArrayUtils.contains(mFoldedDeviceStates, deviceState.getIdentifier()); } else { folded = ArrayUtils.contains(mFoldedDeviceStates, deviceState.getIdentifier()); } if (lastResult == null || !lastResult.equals(folded)) { lastResult = folded; mDelegate.accept(folded); } } @Nullable public Boolean getFolded() { return lastResult; } } }