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_IDENTIFIER; 20 21 import static androidx.window.common.CommonFoldingFeature.COMMON_STATE_UNKNOWN; 22 import static androidx.window.common.CommonFoldingFeature.parseListFromString; 23 24 import android.annotation.NonNull; 25 import android.content.Context; 26 import android.hardware.devicestate.DeviceState; 27 import android.hardware.devicestate.DeviceStateManager; 28 import android.hardware.devicestate.DeviceStateManager.DeviceStateCallback; 29 import android.hardware.devicestate.DeviceStateUtil; 30 import android.text.TextUtils; 31 import android.util.Log; 32 import android.util.SparseIntArray; 33 34 import androidx.window.util.AcceptOnceConsumer; 35 import androidx.window.util.BaseDataProducer; 36 37 import com.android.internal.R; 38 39 import java.util.ArrayList; 40 import java.util.Collections; 41 import java.util.List; 42 import java.util.Objects; 43 import java.util.Optional; 44 import java.util.function.Consumer; 45 46 /** 47 * An implementation of {@link androidx.window.util.BaseDataProducer} that returns 48 * the device's posture by mapping the state returned from {@link DeviceStateManager} to 49 * values provided in the resources' config at {@link R.array#config_device_state_postures}. 50 */ 51 public final class DeviceStateManagerFoldingFeatureProducer 52 extends BaseDataProducer<List<CommonFoldingFeature>> { 53 private static final String TAG = 54 DeviceStateManagerFoldingFeatureProducer.class.getSimpleName(); 55 private static final boolean DEBUG = false; 56 57 /** 58 * Emulated device state 59 * {@link DeviceStateManager.DeviceStateCallback#onDeviceStateChanged(DeviceState)} to 60 * {@link CommonFoldingFeature.State} map. 61 */ 62 private final SparseIntArray mDeviceStateToPostureMap = new SparseIntArray(); 63 64 /** 65 * Device state received via 66 * {@link DeviceStateManager.DeviceStateCallback#onDeviceStateChanged(DeviceState)}. 67 * The identifier returned through {@link DeviceState#getIdentifier()} may not correspond 1:1 68 * with the physical state of the device. This could correspond to the system state of the 69 * device when various software features or overrides are applied. The emulated states generally 70 * consist of all "base" states, but may have additional states such as "concurrent" or 71 * "rear display". Concurrent mode for example is activated via public API and can be active in 72 * both the "open" and "half folded" device states. 73 */ 74 private DeviceState mCurrentDeviceState = new DeviceState( 75 new DeviceState.Configuration.Builder(INVALID_DEVICE_STATE_IDENTIFIER, 76 "INVALID").build()); 77 78 private List<DeviceState> mSupportedStates; 79 80 @NonNull 81 private final RawFoldingFeatureProducer mRawFoldSupplier; 82 83 private final boolean mIsHalfOpenedSupported; 84 85 private final DeviceStateCallback mDeviceStateCallback = new DeviceStateCallback() { 86 @Override 87 public void onDeviceStateChanged(@NonNull DeviceState state) { 88 mCurrentDeviceState = state; 89 mRawFoldSupplier.getData(DeviceStateManagerFoldingFeatureProducer 90 .this::notifyFoldingFeatureChange); 91 } 92 }; 93 DeviceStateManagerFoldingFeatureProducer(@onNull Context context, @NonNull RawFoldingFeatureProducer rawFoldSupplier, @NonNull DeviceStateManager deviceStateManager)94 public DeviceStateManagerFoldingFeatureProducer(@NonNull Context context, 95 @NonNull RawFoldingFeatureProducer rawFoldSupplier, 96 @NonNull DeviceStateManager deviceStateManager) { 97 mRawFoldSupplier = rawFoldSupplier; 98 String[] deviceStatePosturePairs = context.getResources() 99 .getStringArray(R.array.config_device_state_postures); 100 mSupportedStates = deviceStateManager.getSupportedDeviceStates(); 101 boolean isHalfOpenedSupported = false; 102 for (String deviceStatePosturePair : deviceStatePosturePairs) { 103 String[] deviceStatePostureMapping = deviceStatePosturePair.split(":"); 104 if (deviceStatePostureMapping.length != 2) { 105 if (DEBUG) { 106 Log.e(TAG, "Malformed device state posture pair: " 107 + deviceStatePosturePair); 108 } 109 continue; 110 } 111 112 int deviceState; 113 int posture; 114 try { 115 deviceState = Integer.parseInt(deviceStatePostureMapping[0]); 116 posture = Integer.parseInt(deviceStatePostureMapping[1]); 117 } catch (NumberFormatException e) { 118 if (DEBUG) { 119 Log.e(TAG, "Failed to parse device state or posture: " 120 + deviceStatePosturePair, 121 e); 122 } 123 continue; 124 } 125 isHalfOpenedSupported = isHalfOpenedSupported 126 || posture == CommonFoldingFeature.COMMON_STATE_HALF_OPENED; 127 mDeviceStateToPostureMap.put(deviceState, posture); 128 } 129 mIsHalfOpenedSupported = isHalfOpenedSupported; 130 if (mDeviceStateToPostureMap.size() > 0) { 131 Objects.requireNonNull(deviceStateManager) 132 .registerCallback(context.getMainExecutor(), mDeviceStateCallback); 133 } 134 } 135 136 /** 137 * Add a callback to mCallbacks if there is no device state. This callback will be run 138 * once a device state is set. Otherwise,run the callback immediately. 139 */ runCallbackWhenValidState(@onNull Consumer<List<CommonFoldingFeature>> callback, String displayFeaturesString)140 private void runCallbackWhenValidState(@NonNull Consumer<List<CommonFoldingFeature>> callback, 141 String displayFeaturesString) { 142 if (isCurrentStateValid()) { 143 callback.accept(calculateFoldingFeature(displayFeaturesString)); 144 } else { 145 // This callback will be added to mCallbacks and removed once it runs once. 146 AcceptOnceConsumer<List<CommonFoldingFeature>> singleRunCallback = 147 new AcceptOnceConsumer<>(this, callback); 148 addDataChangedCallback(singleRunCallback); 149 } 150 } 151 152 /** 153 * Checks to find {@link DeviceStateManagerFoldingFeatureProducer#mCurrentDeviceState} in the 154 * {@link DeviceStateManagerFoldingFeatureProducer#mDeviceStateToPostureMap} which was 155 * initialized in the constructor of {@link DeviceStateManagerFoldingFeatureProducer}. 156 * Returns a boolean value of whether the device state is valid. 157 */ isCurrentStateValid()158 private boolean isCurrentStateValid() { 159 // If the device state is not found in the map, indexOfKey returns a negative number. 160 return mDeviceStateToPostureMap.indexOfKey(mCurrentDeviceState.getIdentifier()) >= 0; 161 } 162 163 @Override onListenersChanged()164 protected void onListenersChanged() { 165 super.onListenersChanged(); 166 if (hasListeners()) { 167 mRawFoldSupplier.addDataChangedCallback(this::notifyFoldingFeatureChange); 168 } else { 169 mCurrentDeviceState = new DeviceState( 170 new DeviceState.Configuration.Builder(INVALID_DEVICE_STATE_IDENTIFIER, 171 "INVALID").build()); 172 mRawFoldSupplier.removeDataChangedCallback(this::notifyFoldingFeatureChange); 173 } 174 } 175 176 @NonNull 177 @Override getCurrentData()178 public Optional<List<CommonFoldingFeature>> getCurrentData() { 179 Optional<String> displayFeaturesString = mRawFoldSupplier.getCurrentData(); 180 if (!isCurrentStateValid()) { 181 return Optional.empty(); 182 } else { 183 return displayFeaturesString.map(this::calculateFoldingFeature); 184 } 185 } 186 187 /** 188 * Returns a {@link List} of all the {@link CommonFoldingFeature} with the state set to 189 * {@link CommonFoldingFeature#COMMON_STATE_UNKNOWN}. This method parses a {@link String} so a 190 * caller should consider caching the value or the derived value. 191 */ 192 @NonNull getFoldsWithUnknownState()193 public List<CommonFoldingFeature> getFoldsWithUnknownState() { 194 Optional<String> optionalFoldingFeatureString = mRawFoldSupplier.getCurrentData(); 195 196 if (optionalFoldingFeatureString.isPresent()) { 197 return CommonFoldingFeature.parseListFromString( 198 optionalFoldingFeatureString.get(), CommonFoldingFeature.COMMON_STATE_UNKNOWN 199 ); 200 } 201 return Collections.emptyList(); 202 } 203 204 205 /** 206 * Returns {@code true} if the device supports half-opened mode, {@code false} otherwise. 207 */ isHalfOpenedSupported()208 public boolean isHalfOpenedSupported() { 209 return mIsHalfOpenedSupported; 210 } 211 212 /** 213 * Adds the data to the storeFeaturesConsumer when the data is ready. 214 * @param storeFeaturesConsumer a consumer to collect the data when it is first available. 215 */ 216 @Override getData(Consumer<List<CommonFoldingFeature>> storeFeaturesConsumer)217 public void getData(Consumer<List<CommonFoldingFeature>> storeFeaturesConsumer) { 218 mRawFoldSupplier.getData((String displayFeaturesString) -> { 219 if (TextUtils.isEmpty(displayFeaturesString)) { 220 storeFeaturesConsumer.accept(new ArrayList<>()); 221 } else { 222 runCallbackWhenValidState(storeFeaturesConsumer, displayFeaturesString); 223 } 224 }); 225 } 226 notifyFoldingFeatureChange(String displayFeaturesString)227 private void notifyFoldingFeatureChange(String displayFeaturesString) { 228 if (!isCurrentStateValid()) { 229 return; 230 } 231 if (TextUtils.isEmpty(displayFeaturesString)) { 232 notifyDataChanged(new ArrayList<>()); 233 } else { 234 notifyDataChanged(calculateFoldingFeature(displayFeaturesString)); 235 } 236 } 237 calculateFoldingFeature(String displayFeaturesString)238 private List<CommonFoldingFeature> calculateFoldingFeature(String displayFeaturesString) { 239 return parseListFromString(displayFeaturesString, currentHingeState()); 240 } 241 242 @CommonFoldingFeature.State currentHingeState()243 private int currentHingeState() { 244 @CommonFoldingFeature.State 245 int posture = mDeviceStateToPostureMap.get(mCurrentDeviceState.getIdentifier(), 246 COMMON_STATE_UNKNOWN); 247 248 if (posture == CommonFoldingFeature.COMMON_STATE_USE_BASE_STATE) { 249 posture = mDeviceStateToPostureMap.get( 250 DeviceStateUtil.calculateBaseStateIdentifier(mCurrentDeviceState, 251 mSupportedStates), COMMON_STATE_UNKNOWN); 252 } 253 254 return posture; 255 } 256 } 257