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