| /* |
| * 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}. |
| */ |
| static List<CommonFoldingFeature> parseListFromString(@NonNull String value, |
| @State int hingeState) { |
| List<CommonFoldingFeature> 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"); |
| } |
| } |
| } |