blob: a184dff5005b5c56cd83bbf4877e742ef397f5e8 [file] [log] [blame]
/*
* Copyright (C) 2021 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 androidx.window.common;
import static android.hardware.devicestate.DeviceStateManager.INVALID_DEVICE_STATE;
import static androidx.window.common.CommonFoldingFeature.COMMON_STATE_UNKNOWN;
import static androidx.window.common.CommonFoldingFeature.COMMON_STATE_USE_BASE_STATE;
import static androidx.window.common.CommonFoldingFeature.parseListFromString;
import android.annotation.NonNull;
import android.content.Context;
import android.hardware.devicestate.DeviceStateManager;
import android.hardware.devicestate.DeviceStateManager.DeviceStateCallback;
import android.text.TextUtils;
import android.util.Log;
import android.util.SparseIntArray;
import androidx.window.util.AcceptOnceConsumer;
import androidx.window.util.BaseDataProducer;
import com.android.internal.R;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.function.Consumer;
/**
* An implementation of {@link androidx.window.util.BaseDataProducer} that returns
* the device's posture by mapping the state returned from {@link DeviceStateManager} to
* values provided in the resources' config at {@link R.array#config_device_state_postures}.
*/
public final class DeviceStateManagerFoldingFeatureProducer
extends BaseDataProducer<List<CommonFoldingFeature>> {
private static final String TAG =
DeviceStateManagerFoldingFeatureProducer.class.getSimpleName();
private static final boolean DEBUG = false;
/**
* Emulated device state {@link DeviceStateManager.DeviceStateCallback#onStateChanged(int)} to
* {@link CommonFoldingFeature.State} map.
*/
private final SparseIntArray mDeviceStateToPostureMap = new SparseIntArray();
/**
* Emulated device state received via
* {@link DeviceStateManager.DeviceStateCallback#onStateChanged(int)}.
* "Emulated" states differ from "base" state in the sense that they may not correspond 1:1 with
* physical device states. They represent the state of the device when various software
* features and APIs are applied. The emulated states generally consist of all "base" states,
* but may have additional states such as "concurrent" or "rear display". Concurrent mode for
* example is activated via public API and can be active in both the "open" and "half folded"
* device states.
*/
private int mCurrentDeviceState = INVALID_DEVICE_STATE;
/**
* Base device state received via
* {@link DeviceStateManager.DeviceStateCallback#onBaseStateChanged(int)}.
* "Base" in this context means the "physical" state of the device.
*/
private int mCurrentBaseDeviceState = INVALID_DEVICE_STATE;
@NonNull
private final BaseDataProducer<String> mRawFoldSupplier;
private final DeviceStateCallback mDeviceStateCallback = new DeviceStateCallback() {
@Override
public void onStateChanged(int state) {
mCurrentDeviceState = state;
mRawFoldSupplier.getData(DeviceStateManagerFoldingFeatureProducer
.this::notifyFoldingFeatureChange);
}
@Override
public void onBaseStateChanged(int state) {
mCurrentBaseDeviceState = state;
if (mDeviceStateToPostureMap.get(mCurrentDeviceState)
== COMMON_STATE_USE_BASE_STATE) {
mRawFoldSupplier.getData(DeviceStateManagerFoldingFeatureProducer
.this::notifyFoldingFeatureChange);
}
}
};
public DeviceStateManagerFoldingFeatureProducer(@NonNull Context context,
@NonNull BaseDataProducer<String> rawFoldSupplier) {
mRawFoldSupplier = rawFoldSupplier;
String[] deviceStatePosturePairs = context.getResources()
.getStringArray(R.array.config_device_state_postures);
for (String deviceStatePosturePair : deviceStatePosturePairs) {
String[] deviceStatePostureMapping = deviceStatePosturePair.split(":");
if (deviceStatePostureMapping.length != 2) {
if (DEBUG) {
Log.e(TAG, "Malformed device state posture pair: "
+ deviceStatePosturePair);
}
continue;
}
int deviceState;
int posture;
try {
deviceState = Integer.parseInt(deviceStatePostureMapping[0]);
posture = Integer.parseInt(deviceStatePostureMapping[1]);
} catch (NumberFormatException e) {
if (DEBUG) {
Log.e(TAG, "Failed to parse device state or posture: "
+ deviceStatePosturePair,
e);
}
continue;
}
mDeviceStateToPostureMap.put(deviceState, posture);
}
if (mDeviceStateToPostureMap.size() > 0) {
Objects.requireNonNull(context.getSystemService(DeviceStateManager.class))
.registerCallback(context.getMainExecutor(), mDeviceStateCallback);
}
}
/**
* Add a callback to mCallbacks if there is no device state. This callback will be run
* once a device state is set. Otherwise,run the callback immediately.
*/
private void runCallbackWhenValidState(@NonNull Consumer<List<CommonFoldingFeature>> callback,
String displayFeaturesString) {
if (isCurrentStateValid()) {
callback.accept(calculateFoldingFeature(displayFeaturesString));
} else {
// This callback will be added to mCallbacks and removed once it runs once.
AcceptOnceConsumer<List<CommonFoldingFeature>> singleRunCallback =
new AcceptOnceConsumer<>(this, callback);
addDataChangedCallback(singleRunCallback);
}
}
/**
* Checks to find {@link DeviceStateManagerFoldingFeatureProducer#mCurrentDeviceState} in the
* {@link DeviceStateManagerFoldingFeatureProducer#mDeviceStateToPostureMap} which was
* initialized in the constructor of {@link DeviceStateManagerFoldingFeatureProducer}.
* Returns a boolean value of whether the device state is valid.
*/
private boolean isCurrentStateValid() {
// If the device state is not found in the map, indexOfKey returns a negative number.
return mDeviceStateToPostureMap.indexOfKey(mCurrentDeviceState) >= 0;
}
@Override
protected void onListenersChanged() {
super.onListenersChanged();
if (hasListeners()) {
mRawFoldSupplier.addDataChangedCallback(this::notifyFoldingFeatureChange);
} else {
mCurrentDeviceState = INVALID_DEVICE_STATE;
mRawFoldSupplier.removeDataChangedCallback(this::notifyFoldingFeatureChange);
}
}
@NonNull
@Override
public Optional<List<CommonFoldingFeature>> getCurrentData() {
Optional<String> displayFeaturesString = mRawFoldSupplier.getCurrentData();
if (!isCurrentStateValid()) {
return Optional.empty();
} else {
return displayFeaturesString.map(this::calculateFoldingFeature);
}
}
/**
* Adds the data to the storeFeaturesConsumer when the data is ready.
* @param storeFeaturesConsumer a consumer to collect the data when it is first available.
*/
@Override
public void getData(Consumer<List<CommonFoldingFeature>> storeFeaturesConsumer) {
mRawFoldSupplier.getData((String displayFeaturesString) -> {
if (TextUtils.isEmpty(displayFeaturesString)) {
storeFeaturesConsumer.accept(new ArrayList<>());
} else {
runCallbackWhenValidState(storeFeaturesConsumer, displayFeaturesString);
}
});
}
private void notifyFoldingFeatureChange(String displayFeaturesString) {
if (!isCurrentStateValid()) {
return;
}
if (TextUtils.isEmpty(displayFeaturesString)) {
notifyDataChanged(new ArrayList<>());
} else {
notifyDataChanged(calculateFoldingFeature(displayFeaturesString));
}
}
private List<CommonFoldingFeature> calculateFoldingFeature(String displayFeaturesString) {
return parseListFromString(displayFeaturesString, currentHingeState());
}
@CommonFoldingFeature.State
private int currentHingeState() {
@CommonFoldingFeature.State
int posture = mDeviceStateToPostureMap.get(mCurrentDeviceState, COMMON_STATE_UNKNOWN);
if (posture == CommonFoldingFeature.COMMON_STATE_USE_BASE_STATE) {
posture = mDeviceStateToPostureMap.get(mCurrentBaseDeviceState, COMMON_STATE_UNKNOWN);
}
return posture;
}
}