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