• 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_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