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