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