• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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