/* * Copyright (C) 2021 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package androidx.window.common; import static androidx.window.util.ExtensionHelper.isZero; import android.annotation.IntDef; import android.annotation.Nullable; import android.graphics.Rect; import android.hardware.devicestate.DeviceStateManager; import android.util.Log; import androidx.annotation.NonNull; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.ArrayList; import java.util.List; import java.util.Objects; import java.util.regex.Matcher; import java.util.regex.Pattern; /** * A representation of a folding feature for both Extension and Sidecar. * For Sidecar this is the same as combining {@link androidx.window.sidecar.SidecarDeviceState} and * {@link androidx.window.sidecar.SidecarDisplayFeature}. For Extensions this is the mirror of * {@link androidx.window.extensions.layout.FoldingFeature}. */ public final class CommonFoldingFeature { private static final boolean DEBUG = false; public static final String TAG = CommonFoldingFeature.class.getSimpleName(); /** * A common type to represent a hinge where the screen is continuous. */ public static final int COMMON_TYPE_FOLD = 1; /** * A common type to represent a hinge where there is a physical gap separating multiple * displays. */ public static final int COMMON_TYPE_HINGE = 2; @IntDef({COMMON_TYPE_FOLD, COMMON_TYPE_HINGE}) @Retention(RetentionPolicy.SOURCE) public @interface Type { } /** * A common state to represent when the state is not known. One example is if the device is * closed. We do not emit this value for developers but is useful for implementation reasons. */ public static final int COMMON_STATE_UNKNOWN = -1; /** * A common state that contains no folding features. For example, an in-folding device in the * "closed" device state. */ public static final int COMMON_STATE_NO_FOLDING_FEATURES = 1; /** * A common state to represent a HALF_OPENED hinge. This is needed because the definitions in * Sidecar and Extensions do not match exactly. */ public static final int COMMON_STATE_HALF_OPENED = 2; /** * A common state to represent a FLAT hinge. This is needed because the definitions in Sidecar * and Extensions do not match exactly. */ public static final int COMMON_STATE_FLAT = 3; /** * A common state where the hinge state should be derived using the base state from * {@link DeviceStateManager.DeviceStateCallback#onBaseStateChanged(int)} instead of the * emulated state. This is an internal state and must not be passed to clients. */ public static final int COMMON_STATE_USE_BASE_STATE = 1000; /** * The possible states for a folding hinge. Common in this context means normalized between * extensions and sidecar. */ @IntDef({COMMON_STATE_UNKNOWN, COMMON_STATE_NO_FOLDING_FEATURES, COMMON_STATE_HALF_OPENED, COMMON_STATE_FLAT, COMMON_STATE_USE_BASE_STATE}) @Retention(RetentionPolicy.SOURCE) public @interface State { } private static final Pattern FEATURE_PATTERN = Pattern.compile("([a-z]+)-\\[(\\d+),(\\d+),(\\d+),(\\d+)]-?(flat|half-opened)?"); private static final String FEATURE_TYPE_FOLD = "fold"; private static final String FEATURE_TYPE_HINGE = "hinge"; private static final String PATTERN_STATE_FLAT = "flat"; private static final String PATTERN_STATE_HALF_OPENED = "half-opened"; /** * Parse a {@link List} of {@link CommonFoldingFeature} from a {@link String}. * @param value a {@link String} representation of multiple {@link CommonFoldingFeature} * separated by a ":". * @param hingeState a global fallback value for a {@link CommonFoldingFeature} if one is not * specified in the input. * @throws IllegalArgumentException if the provided string is improperly formatted or could not * otherwise be parsed. * @see #FEATURE_PATTERN * @return {@link List} of {@link CommonFoldingFeature}. */ public static List parseListFromString(@NonNull String value, @State int hingeState) { List features = new ArrayList<>(); String[] featureStrings = value.split(";"); for (String featureString : featureStrings) { CommonFoldingFeature feature; try { feature = CommonFoldingFeature.parseFromString(featureString, hingeState); } catch (IllegalArgumentException e) { if (DEBUG) { Log.w(TAG, "Failed to parse display feature: " + featureString, e); } continue; } features.add(feature); } return features; } /** * Parses a display feature from a string. * * @param string A {@link String} representation of a {@link CommonFoldingFeature}. * @param hingeState A fallback value for the {@link State} if it is not specified in the input. * @throws IllegalArgumentException if the provided string is improperly formatted or could not * otherwise be parsed. * @return {@link CommonFoldingFeature} represented by the {@link String} value. * @see #FEATURE_PATTERN */ @NonNull private static CommonFoldingFeature parseFromString(@NonNull String string, @State int hingeState) { Matcher featureMatcher = FEATURE_PATTERN.matcher(string); if (!featureMatcher.matches()) { throw new IllegalArgumentException("Malformed feature description format: " + string); } try { String featureType = featureMatcher.group(1); featureType = featureType == null ? "" : featureType; int type; switch (featureType) { case FEATURE_TYPE_FOLD: type = COMMON_TYPE_FOLD; break; case FEATURE_TYPE_HINGE: type = COMMON_TYPE_HINGE; break; default: { throw new IllegalArgumentException("Malformed feature type: " + featureType); } } int left = Integer.parseInt(featureMatcher.group(2)); int top = Integer.parseInt(featureMatcher.group(3)); int right = Integer.parseInt(featureMatcher.group(4)); int bottom = Integer.parseInt(featureMatcher.group(5)); Rect featureRect = new Rect(left, top, right, bottom); if (isZero(featureRect)) { throw new IllegalArgumentException("Feature has empty bounds: " + string); } String stateString = featureMatcher.group(6); stateString = stateString == null ? "" : stateString; @State final int state; switch (stateString) { case PATTERN_STATE_FLAT: state = COMMON_STATE_FLAT; break; case PATTERN_STATE_HALF_OPENED: state = COMMON_STATE_HALF_OPENED; break; default: state = hingeState; break; } return new CommonFoldingFeature(type, state, featureRect); } catch (NumberFormatException e) { throw new IllegalArgumentException("Malformed feature description: " + string, e); } } private final int mType; @Nullable private final int mState; @NonNull private final Rect mRect; CommonFoldingFeature(int type, @State int state, @NonNull Rect rect) { assertReportableState(state); this.mType = type; this.mState = state; if (rect.width() == 0 && rect.height() == 0) { throw new IllegalArgumentException( "Display feature rectangle cannot have zero width and height simultaneously."); } this.mRect = new Rect(rect); } /** Returns the type of the feature. */ @Type public int getType() { return mType; } /** Returns the state of the feature.*/ @State public int getState() { return mState; } /** Returns the bounds of the feature. */ @NonNull public Rect getRect() { return new Rect(mRect); } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; CommonFoldingFeature that = (CommonFoldingFeature) o; return mType == that.mType && Objects.equals(mState, that.mState) && mRect.equals(that.mRect); } @Override public String toString() { return "CommonFoldingFeature=[Type: " + mType + ", state: " + mState + "]"; } @Override public int hashCode() { return Objects.hash(mType, mState, mRect); } /** * Checks if the provided folding feature state should be reported to clients. See * {@link androidx.window.extensions.layout.FoldingFeature} */ private static void assertReportableState(@State int state) { if (state != COMMON_STATE_FLAT && state != COMMON_STATE_HALF_OPENED && state != COMMON_STATE_UNKNOWN) { throw new IllegalArgumentException("Invalid state: " + state + "must be either COMMON_STATE_FLAT or COMMON_STATE_HALF_OPENED"); } } }