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 androidx.window.util.ExtensionHelper.isZero;
20 
21 import android.annotation.IntDef;
22 import android.annotation.Nullable;
23 import android.graphics.Rect;
24 import android.hardware.devicestate.DeviceStateManager;
25 import android.util.Log;
26 
27 import androidx.annotation.NonNull;
28 
29 import java.lang.annotation.Retention;
30 import java.lang.annotation.RetentionPolicy;
31 import java.util.ArrayList;
32 import java.util.List;
33 import java.util.Objects;
34 import java.util.regex.Matcher;
35 import java.util.regex.Pattern;
36 
37 /**
38  * A representation of a folding feature for both Extension and Sidecar.
39  * For Sidecar this is the same as combining {@link androidx.window.sidecar.SidecarDeviceState} and
40  * {@link androidx.window.sidecar.SidecarDisplayFeature}. For Extensions this is the mirror of
41  * {@link androidx.window.extensions.layout.FoldingFeature}.
42  */
43 public final class CommonFoldingFeature {
44 
45     private static final boolean DEBUG = false;
46 
47     public static final String TAG = CommonFoldingFeature.class.getSimpleName();
48 
49     /**
50      * A common type to represent a hinge where the screen is continuous.
51      */
52     public static final int COMMON_TYPE_FOLD = 1;
53 
54     /**
55      * A common type to represent a hinge where there is a physical gap separating multiple
56      * displays.
57      */
58     public static final int COMMON_TYPE_HINGE = 2;
59 
60     @IntDef({COMMON_TYPE_FOLD, COMMON_TYPE_HINGE})
61     @Retention(RetentionPolicy.SOURCE)
62     public @interface Type {
63     }
64 
65     /**
66      * A common state to represent when the state is not known. One example is if the device is
67      * closed. We do not emit this value for developers but is useful for implementation reasons.
68      */
69     public static final int COMMON_STATE_UNKNOWN = -1;
70 
71     /**
72      * A common state that contains no folding features. For example, an in-folding device in the
73      * "closed" device state.
74      */
75     public static final int COMMON_STATE_NO_FOLDING_FEATURES = 1;
76 
77     /**
78      * A common state to represent a HALF_OPENED hinge. This is needed because the definitions in
79      * Sidecar and Extensions do not match exactly.
80      */
81     public static final int COMMON_STATE_HALF_OPENED = 2;
82 
83     /**
84      * A common state to represent a FLAT hinge. This is needed because the definitions in Sidecar
85      * and Extensions do not match exactly.
86      */
87     public static final int COMMON_STATE_FLAT = 3;
88 
89     /**
90      * A common state where the hinge state should be derived using the base state from
91      * {@link DeviceStateManager.DeviceStateCallback#onBaseStateChanged(int)} instead of the
92      * emulated state. This is an internal state and must not be passed to clients.
93      */
94     public static final int COMMON_STATE_USE_BASE_STATE = 1000;
95 
96     /**
97      * The possible states for a folding hinge. Common in this context means normalized between
98      * extensions and sidecar.
99      */
100     @IntDef({COMMON_STATE_UNKNOWN,
101             COMMON_STATE_NO_FOLDING_FEATURES,
102             COMMON_STATE_HALF_OPENED,
103             COMMON_STATE_FLAT,
104             COMMON_STATE_USE_BASE_STATE})
105     @Retention(RetentionPolicy.SOURCE)
106     public @interface State {
107     }
108 
109     private static final Pattern FEATURE_PATTERN =
110             Pattern.compile("([a-z]+)-\\[(\\d+),(\\d+),(\\d+),(\\d+)]-?(flat|half-opened)?");
111 
112     private static final String FEATURE_TYPE_FOLD = "fold";
113     private static final String FEATURE_TYPE_HINGE = "hinge";
114 
115     private static final String PATTERN_STATE_FLAT = "flat";
116     private static final String PATTERN_STATE_HALF_OPENED = "half-opened";
117 
118     /**
119      * Parse a {@link List} of {@link CommonFoldingFeature} from a {@link String}.
120      * @param value a {@link String} representation of multiple {@link CommonFoldingFeature}
121      *              separated by a ":".
122      * @param hingeState a global fallback value for a {@link CommonFoldingFeature} if one is not
123      *                   specified in the input.
124      * @throws IllegalArgumentException if the provided string is improperly formatted or could not
125      * otherwise be parsed.
126      * @see #FEATURE_PATTERN
127      * @return {@link List} of {@link CommonFoldingFeature}.
128      */
parseListFromString(@onNull String value, @State int hingeState)129     public static List<CommonFoldingFeature> parseListFromString(@NonNull String value,
130             @State int hingeState) {
131         List<CommonFoldingFeature> features = new ArrayList<>();
132         String[] featureStrings =  value.split(";");
133         for (String featureString : featureStrings) {
134             CommonFoldingFeature feature;
135             try {
136                 feature = CommonFoldingFeature.parseFromString(featureString, hingeState);
137             } catch (IllegalArgumentException e) {
138                 if (DEBUG) {
139                     Log.w(TAG, "Failed to parse display feature: " + featureString, e);
140                 }
141                 continue;
142             }
143             features.add(feature);
144         }
145         return features;
146     }
147 
148     /**
149      * Parses a display feature from a string.
150      *
151      * @param string A {@link String} representation of a {@link CommonFoldingFeature}.
152      * @param hingeState A fallback value for the {@link State} if it is not specified in the input.
153      * @throws IllegalArgumentException if the provided string is improperly formatted or could not
154      *                                  otherwise be parsed.
155      * @return {@link CommonFoldingFeature} represented by the {@link String} value.
156      * @see #FEATURE_PATTERN
157      */
158     @NonNull
parseFromString(@onNull String string, @State int hingeState)159     private static CommonFoldingFeature parseFromString(@NonNull String string,
160             @State int hingeState) {
161         Matcher featureMatcher = FEATURE_PATTERN.matcher(string);
162         if (!featureMatcher.matches()) {
163             throw new IllegalArgumentException("Malformed feature description format: " + string);
164         }
165         try {
166             String featureType = featureMatcher.group(1);
167             featureType = featureType == null ? "" : featureType;
168             int type;
169             switch (featureType) {
170                 case FEATURE_TYPE_FOLD:
171                     type = COMMON_TYPE_FOLD;
172                     break;
173                 case FEATURE_TYPE_HINGE:
174                     type = COMMON_TYPE_HINGE;
175                     break;
176                 default: {
177                     throw new IllegalArgumentException("Malformed feature type: " + featureType);
178                 }
179             }
180 
181             int left = Integer.parseInt(featureMatcher.group(2));
182             int top = Integer.parseInt(featureMatcher.group(3));
183             int right = Integer.parseInt(featureMatcher.group(4));
184             int bottom = Integer.parseInt(featureMatcher.group(5));
185             Rect featureRect = new Rect(left, top, right, bottom);
186             if (isZero(featureRect)) {
187                 throw new IllegalArgumentException("Feature has empty bounds: " + string);
188             }
189             String stateString = featureMatcher.group(6);
190             stateString = stateString == null ? "" : stateString;
191             @State final int state;
192             switch (stateString) {
193                 case PATTERN_STATE_FLAT:
194                     state = COMMON_STATE_FLAT;
195                     break;
196                 case PATTERN_STATE_HALF_OPENED:
197                     state = COMMON_STATE_HALF_OPENED;
198                     break;
199                 default:
200                     state = hingeState;
201                     break;
202             }
203             return new CommonFoldingFeature(type, state, featureRect);
204         } catch (NumberFormatException e) {
205             throw new IllegalArgumentException("Malformed feature description: " + string, e);
206         }
207     }
208 
209     private final int mType;
210     @Nullable
211     private final int mState;
212     @NonNull
213     private final Rect mRect;
214 
CommonFoldingFeature(int type, @State int state, @NonNull Rect rect)215     CommonFoldingFeature(int type, @State int state, @NonNull Rect rect) {
216         assertReportableState(state);
217         this.mType = type;
218         this.mState = state;
219         if (rect.width() == 0 && rect.height() == 0) {
220             throw new IllegalArgumentException(
221                     "Display feature rectangle cannot have zero width and height simultaneously.");
222         }
223         this.mRect = new Rect(rect);
224     }
225 
226     /** Returns the type of the feature. */
227     @Type
getType()228     public int getType() {
229         return mType;
230     }
231 
232     /** Returns the state of the feature.*/
233     @State
getState()234     public int getState() {
235         return mState;
236     }
237 
238     /** Returns the bounds of the feature. */
239     @NonNull
getRect()240     public Rect getRect() {
241         return new Rect(mRect);
242     }
243 
244     @Override
equals(Object o)245     public boolean equals(Object o) {
246         if (this == o) return true;
247         if (o == null || getClass() != o.getClass()) return false;
248         CommonFoldingFeature that = (CommonFoldingFeature) o;
249         return mType == that.mType
250                 && Objects.equals(mState, that.mState)
251                 && mRect.equals(that.mRect);
252     }
253 
254     @Override
toString()255     public String toString() {
256         return "CommonFoldingFeature=[Type: " + mType + ", state: " + mState + "]";
257     }
258 
259     @Override
hashCode()260     public int hashCode() {
261         return Objects.hash(mType, mState, mRect);
262     }
263 
264     /**
265      * Checks if the provided folding feature state should be reported to clients. See
266      * {@link androidx.window.extensions.layout.FoldingFeature}
267      */
assertReportableState(@tate int state)268     private static void assertReportableState(@State int state) {
269         if (state != COMMON_STATE_FLAT && state != COMMON_STATE_HALF_OPENED
270                 && state != COMMON_STATE_UNKNOWN) {
271             throw new IllegalArgumentException("Invalid state: " + state
272                     + "must be either COMMON_STATE_FLAT or COMMON_STATE_HALF_OPENED");
273         }
274     }
275 }
276