1 /* 2 * Copyright (C) 2021 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package androidx.window.common; 18 19 import static android.hardware.devicestate.DeviceStateManager.INVALID_DEVICE_STATE; 20 21 import static androidx.window.common.CommonFoldingFeature.COMMON_STATE_UNKNOWN; 22 import static androidx.window.common.CommonFoldingFeature.COMMON_STATE_USE_BASE_STATE; 23 import static androidx.window.common.CommonFoldingFeature.parseListFromString; 24 25 import android.annotation.NonNull; 26 import android.content.Context; 27 import android.hardware.devicestate.DeviceStateManager; 28 import android.hardware.devicestate.DeviceStateManager.DeviceStateCallback; 29 import android.text.TextUtils; 30 import android.util.Log; 31 import android.util.SparseIntArray; 32 33 import androidx.window.util.AcceptOnceConsumer; 34 import androidx.window.util.BaseDataProducer; 35 36 import com.android.internal.R; 37 38 import java.util.ArrayList; 39 import java.util.List; 40 import java.util.Objects; 41 import java.util.Optional; 42 import java.util.function.Consumer; 43 44 /** 45 * An implementation of {@link androidx.window.util.BaseDataProducer} that returns 46 * the device's posture by mapping the state returned from {@link DeviceStateManager} to 47 * values provided in the resources' config at {@link R.array#config_device_state_postures}. 48 */ 49 public final class DeviceStateManagerFoldingFeatureProducer 50 extends BaseDataProducer<List<CommonFoldingFeature>> { 51 private static final String TAG = 52 DeviceStateManagerFoldingFeatureProducer.class.getSimpleName(); 53 private static final boolean DEBUG = false; 54 55 /** 56 * Emulated device state {@link DeviceStateManager.DeviceStateCallback#onStateChanged(int)} to 57 * {@link CommonFoldingFeature.State} map. 58 */ 59 private final SparseIntArray mDeviceStateToPostureMap = new SparseIntArray(); 60 61 /** 62 * Emulated device state received via 63 * {@link DeviceStateManager.DeviceStateCallback#onStateChanged(int)}. 64 * "Emulated" states differ from "base" state in the sense that they may not correspond 1:1 with 65 * physical device states. They represent the state of the device when various software 66 * features and APIs are applied. The emulated states generally consist of all "base" states, 67 * but may have additional states such as "concurrent" or "rear display". Concurrent mode for 68 * example is activated via public API and can be active in both the "open" and "half folded" 69 * device states. 70 */ 71 private int mCurrentDeviceState = INVALID_DEVICE_STATE; 72 73 /** 74 * Base device state received via 75 * {@link DeviceStateManager.DeviceStateCallback#onBaseStateChanged(int)}. 76 * "Base" in this context means the "physical" state of the device. 77 */ 78 private int mCurrentBaseDeviceState = INVALID_DEVICE_STATE; 79 80 @NonNull 81 private final BaseDataProducer<String> mRawFoldSupplier; 82 83 private final DeviceStateCallback mDeviceStateCallback = new DeviceStateCallback() { 84 @Override 85 public void onStateChanged(int state) { 86 mCurrentDeviceState = state; 87 mRawFoldSupplier.getData(DeviceStateManagerFoldingFeatureProducer 88 .this::notifyFoldingFeatureChange); 89 } 90 91 @Override 92 public void onBaseStateChanged(int state) { 93 mCurrentBaseDeviceState = state; 94 95 if (mDeviceStateToPostureMap.get(mCurrentDeviceState) 96 == COMMON_STATE_USE_BASE_STATE) { 97 mRawFoldSupplier.getData(DeviceStateManagerFoldingFeatureProducer 98 .this::notifyFoldingFeatureChange); 99 } 100 } 101 }; 102 DeviceStateManagerFoldingFeatureProducer(@onNull Context context, @NonNull BaseDataProducer<String> rawFoldSupplier)103 public DeviceStateManagerFoldingFeatureProducer(@NonNull Context context, 104 @NonNull BaseDataProducer<String> rawFoldSupplier) { 105 mRawFoldSupplier = rawFoldSupplier; 106 String[] deviceStatePosturePairs = context.getResources() 107 .getStringArray(R.array.config_device_state_postures); 108 for (String deviceStatePosturePair : deviceStatePosturePairs) { 109 String[] deviceStatePostureMapping = deviceStatePosturePair.split(":"); 110 if (deviceStatePostureMapping.length != 2) { 111 if (DEBUG) { 112 Log.e(TAG, "Malformed device state posture pair: " 113 + deviceStatePosturePair); 114 } 115 continue; 116 } 117 118 int deviceState; 119 int posture; 120 try { 121 deviceState = Integer.parseInt(deviceStatePostureMapping[0]); 122 posture = Integer.parseInt(deviceStatePostureMapping[1]); 123 } catch (NumberFormatException e) { 124 if (DEBUG) { 125 Log.e(TAG, "Failed to parse device state or posture: " 126 + deviceStatePosturePair, 127 e); 128 } 129 continue; 130 } 131 132 mDeviceStateToPostureMap.put(deviceState, posture); 133 } 134 135 if (mDeviceStateToPostureMap.size() > 0) { 136 Objects.requireNonNull(context.getSystemService(DeviceStateManager.class)) 137 .registerCallback(context.getMainExecutor(), mDeviceStateCallback); 138 } 139 } 140 141 /** 142 * Add a callback to mCallbacks if there is no device state. This callback will be run 143 * once a device state is set. Otherwise,run the callback immediately. 144 */ runCallbackWhenValidState(@onNull Consumer<List<CommonFoldingFeature>> callback, String displayFeaturesString)145 private void runCallbackWhenValidState(@NonNull Consumer<List<CommonFoldingFeature>> callback, 146 String displayFeaturesString) { 147 if (isCurrentStateValid()) { 148 callback.accept(calculateFoldingFeature(displayFeaturesString)); 149 } else { 150 // This callback will be added to mCallbacks and removed once it runs once. 151 AcceptOnceConsumer<List<CommonFoldingFeature>> singleRunCallback = 152 new AcceptOnceConsumer<>(this, callback); 153 addDataChangedCallback(singleRunCallback); 154 } 155 } 156 157 /** 158 * Checks to find {@link DeviceStateManagerFoldingFeatureProducer#mCurrentDeviceState} in the 159 * {@link DeviceStateManagerFoldingFeatureProducer#mDeviceStateToPostureMap} which was 160 * initialized in the constructor of {@link DeviceStateManagerFoldingFeatureProducer}. 161 * Returns a boolean value of whether the device state is valid. 162 */ isCurrentStateValid()163 private boolean isCurrentStateValid() { 164 // If the device state is not found in the map, indexOfKey returns a negative number. 165 return mDeviceStateToPostureMap.indexOfKey(mCurrentDeviceState) >= 0; 166 } 167 168 @Override onListenersChanged()169 protected void onListenersChanged() { 170 super.onListenersChanged(); 171 if (hasListeners()) { 172 mRawFoldSupplier.addDataChangedCallback(this::notifyFoldingFeatureChange); 173 } else { 174 mCurrentDeviceState = INVALID_DEVICE_STATE; 175 mRawFoldSupplier.removeDataChangedCallback(this::notifyFoldingFeatureChange); 176 } 177 } 178 179 @NonNull 180 @Override getCurrentData()181 public Optional<List<CommonFoldingFeature>> getCurrentData() { 182 Optional<String> displayFeaturesString = mRawFoldSupplier.getCurrentData(); 183 if (!isCurrentStateValid()) { 184 return Optional.empty(); 185 } else { 186 return displayFeaturesString.map(this::calculateFoldingFeature); 187 } 188 } 189 190 /** 191 * Adds the data to the storeFeaturesConsumer when the data is ready. 192 * @param storeFeaturesConsumer a consumer to collect the data when it is first available. 193 */ 194 @Override getData(Consumer<List<CommonFoldingFeature>> storeFeaturesConsumer)195 public void getData(Consumer<List<CommonFoldingFeature>> storeFeaturesConsumer) { 196 mRawFoldSupplier.getData((String displayFeaturesString) -> { 197 if (TextUtils.isEmpty(displayFeaturesString)) { 198 storeFeaturesConsumer.accept(new ArrayList<>()); 199 } else { 200 runCallbackWhenValidState(storeFeaturesConsumer, displayFeaturesString); 201 } 202 }); 203 } 204 notifyFoldingFeatureChange(String displayFeaturesString)205 private void notifyFoldingFeatureChange(String displayFeaturesString) { 206 if (!isCurrentStateValid()) { 207 return; 208 } 209 if (TextUtils.isEmpty(displayFeaturesString)) { 210 notifyDataChanged(new ArrayList<>()); 211 } else { 212 notifyDataChanged(calculateFoldingFeature(displayFeaturesString)); 213 } 214 } 215 calculateFoldingFeature(String displayFeaturesString)216 private List<CommonFoldingFeature> calculateFoldingFeature(String displayFeaturesString) { 217 return parseListFromString(displayFeaturesString, currentHingeState()); 218 } 219 220 @CommonFoldingFeature.State currentHingeState()221 private int currentHingeState() { 222 @CommonFoldingFeature.State 223 int posture = mDeviceStateToPostureMap.get(mCurrentDeviceState, COMMON_STATE_UNKNOWN); 224 225 if (posture == CommonFoldingFeature.COMMON_STATE_USE_BASE_STATE) { 226 posture = mDeviceStateToPostureMap.get(mCurrentBaseDeviceState, COMMON_STATE_UNKNOWN); 227 } 228 229 return posture; 230 } 231 } 232