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