FoldingFeature.java

/*
 * Copyright 2020 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;

import android.graphics.Rect;

import androidx.annotation.IntDef;
import androidx.annotation.NonNull;

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;

/**
 * A feature that describes a fold in the flexible display
 * or a hinge between two physical display panels.
 */
public class FoldingFeature implements DisplayFeature {

    /**
     * A fold in the flexible screen without a physical gap.
     */
    public static final int TYPE_FOLD = 1;

    /**
     * A physical separation with a hinge that allows two display panels to fold.
     */
    public static final int TYPE_HINGE = 2;

    @Retention(RetentionPolicy.SOURCE)
    @IntDef({
            TYPE_FOLD,
            TYPE_HINGE,
    })
    @interface Type{}

    /**
     * The foldable device is completely open, the screen space that is presented to the user is
     * flat. See the
     * <a href="https://developer.android.com/guide/topics/ui/foldables#postures">Posture</a>
     * section in the official documentation for visual samples and references.
     */
    public static final int STATE_FLAT = 1;

    /**
     * The foldable device's hinge is in an intermediate position between opened and closed state,
     * there is a non-flat angle between parts of the flexible screen or between physical screen
     * panels. See the
     * <a href="https://developer.android.com/guide/topics/ui/foldables#postures">Posture</a>
     * section in the official documentation for visual samples and references.
     */
    public static final int STATE_HALF_OPENED = 2;

    /**
     * The foldable device is flipped with the flexible screen parts or physical screens facing
     * opposite directions. See the
     * <a href="https://developer.android.com/guide/topics/ui/foldables#postures">Posture</a>
     * section in the official documentation for visual samples and references.
     */
    public static final int STATE_FLIPPED = 3;

    /**
     * The {@link FoldingFeature} does not occlude the content in any way. One example is a flat
     * continuous fold where content can stretch across the fold. Another example is a hinge that
     * has width or height equal to 0. In this case the content is physically split across both
     * displays, but fully visible.
     */
    public static final int OCCLUSION_NONE = 0;

    /**
     * The {@link FoldingFeature} occludes all content. One example is a hinge that is considered to
     * be part of the window, so that part of the UI is not visible to the user. Any content shown
     * in the same area as the hinge may not be accessible in any way. Fully occluded areas should
     * always be avoided when placing interactive UI elements and text.
     */
    public static final int OCCLUSION_FULL = 1;

    @Retention(RetentionPolicy.SOURCE)
    @IntDef({
            OCCLUSION_NONE,
            OCCLUSION_FULL
    })
    @interface OcclusionType {}

    /**
     * The height of the {@link FoldingFeature} is greater than or equal to the width.
     */
    public static final int ORIENTATION_VERTICAL = 0;

    /**
     * The width of the {@link FoldingFeature} is greater than the height.
     */
    public static final int ORIENTATION_HORIZONTAL = 1;

    @Retention(RetentionPolicy.SOURCE)
    @IntDef({
            ORIENTATION_HORIZONTAL,
            ORIENTATION_VERTICAL
    })
    @interface Orientation {}

    @Retention(RetentionPolicy.SOURCE)
    @IntDef({
            STATE_HALF_OPENED,
            STATE_FLAT,
            STATE_FLIPPED,
    })
    @interface State {}

    /**
     * The bounding rectangle of the feature within the application window in the window
     * coordinate space.
     */
    @NonNull
    private final Rect mBounds;

    /**
     * The physical type of the feature.
     */
    @Type
    private final int mType;

    /**
     * The state of the feature.
     */
    @State
    private final int mState;

    public FoldingFeature(@NonNull Rect bounds, @Type int type, @State int state) {
        validateState(state);
        validateType(type);
        validateFeatureBounds(bounds);
        mBounds = new Rect(bounds);
        mType = type;
        mState = state;
    }

    @NonNull
    @Override
    public Rect getBounds() {
        return new Rect(mBounds);
    }

    /**
     * Returns type that is either {@link FoldingFeature#TYPE_FOLD} or
     * {@link FoldingFeature#TYPE_HINGE}
     * @deprecated visibility will be reduced.
     */
    @Type
    @Deprecated
    public int getType() {
        return mType;
    }

    @State
    public int getState() {
        return mState;
    }

    /**
     * Calculates if a {@link FoldingFeature} should be thought of as splitting the window into
     * multiple physical areas that can be seen by users as logically separate. Display panels
     * connected by a hinge are always separated. Folds on flexible screens should be treated as
     * separating when they are not {@link FoldingFeature#STATE_FLAT}.
     *
     * Apps may use this to determine if content should lay out around the {@link FoldingFeature}.
     * Developers should consider the placement of interactive elements. Similar to the case of
     * {@link FoldingFeature#OCCLUSION_FULL}, when a feature is separating then consider laying
     * out the controls around the {@link FoldingFeature}.
     *
     * An example use case is to determine if the UI should be split into two logical areas. A media
     * app where there is some auxiliary content, such as comments or description of a video, may
     * need to adapt the layout. The media can be put on one side of the {@link FoldingFeature} and
     * the auxiliary content can be placed on the other side.
     *
     * @return {@code true} if the feature splits the display into two areas, {@code false}
     * otherwise.
     */
    public boolean isSeparating() {
        if (mType == TYPE_HINGE) {
            return true;
        }
        if (mType == TYPE_FOLD && (mState == STATE_FLIPPED || mState == STATE_HALF_OPENED)) {
            return true;
        }
        return false;
    }

    /**
     * Calculates the occlusion mode to determine if a {@link FoldingFeature} occludes a part of
     * the window. This flag is useful for determining if UI elements need to be moved
     * around so that the user can access them. For some devices occluded elements can not be
     * accessed by the user at all.
     *
     * For occlusion type {@link FoldingFeature#OCCLUSION_NONE} the feature can be treated as a
     * guideline. One example would be for a continuously folding screen. For occlusion type
     * {@link FoldingFeature#OCCLUSION_FULL} the feature should be avoided completely since content
     * will not be visible or touchable, like a hinge device with two displays.
     *
     * The occlusion mode is useful to determine if the UI needs to adapt to the
     * {@link FoldingFeature}. For example, full screen games should consider avoiding anything in
     * the occluded region if it negatively affects the gameplay.  The user can not tap
     * on the occluded interactive UI elements nor can they see important information.
     *
     * @return {@link FoldingFeature#OCCLUSION_NONE} if the {@link FoldingFeature} has empty
     * bounds.
     */
    @OcclusionType
    public int getOcclusionMode() {
        if (mBounds.width() == 0 || mBounds.height() == 0) {
            return OCCLUSION_NONE;
        }
        return OCCLUSION_FULL;
    }

    /**
     * Returns {@link FoldingFeature#ORIENTATION_HORIZONTAL} if the width is greater than the
     * height, {@link FoldingFeature#ORIENTATION_VERTICAL} otherwise.
     */
    @Orientation
    public int getOrientation() {
        return mBounds.width() > mBounds.height()
                ? ORIENTATION_HORIZONTAL
                : ORIENTATION_VERTICAL;
    }

    static String occlusionTypeToString(@OcclusionType int type) {
        switch (type) {
            case OCCLUSION_NONE:
                return "OCCLUSION_NONE";
            case OCCLUSION_FULL:
                return "OCCLUSION_FULL";
            default:
                return "UNKNOWN";
        }
    }

    static String orientationToString(@Orientation int direction) {
        switch (direction) {
            case ORIENTATION_HORIZONTAL:
                return "ORIENTATION_HORIZONTAL";
            case ORIENTATION_VERTICAL:
                return "ORIENTATION_VERTICAL";
            default:
                return "UNKNOWN";
        }
    }

    /**
     * Verifies the state is {@link FoldingFeature#STATE_FLAT},
     * {@link FoldingFeature#STATE_HALF_OPENED} or {@link FoldingFeature#STATE_FLIPPED}.
     */
    private static void validateState(int state) {
        if (state != STATE_FLAT && state != STATE_HALF_OPENED && state != STATE_FLIPPED) {
            throw new IllegalArgumentException("State must be either " + stateToString(STATE_FLAT)
                    + ", " + stateToString(STATE_HALF_OPENED) + ", or "
                    + stateToString(STATE_FLIPPED));
        }
    }

    /**
     * Verifies the type is either {@link FoldingFeature#TYPE_HINGE} or
     * {@link FoldingFeature#TYPE_FOLD}
     */
    private static void validateType(int type) {
        if (type != TYPE_FOLD && type != TYPE_HINGE) {
            throw new IllegalArgumentException("Type must be either " + typeToString(TYPE_FOLD)
                    + " or " + typeToString(TYPE_HINGE));
        }
    }

    /**
     * Verifies the bounds of the folding feature.
     */
    private static void validateFeatureBounds(@NonNull Rect bounds) {
        if (bounds.width() == 0 && bounds.height() == 0) {
            throw new IllegalArgumentException("Bounds must be non zero");
        }
        if (bounds.left != 0 && bounds.top != 0) {
            throw new IllegalArgumentException("Bounding rectangle must start at the top or "
                    + "left window edge for folding features");
        }
    }

    @NonNull
    private static String typeToString(int type) {
        switch (type) {
            case TYPE_FOLD:
                return "FOLD";
            case TYPE_HINGE:
                return "HINGE";
            default:
                return "Unknown feature type (" + type + ")";
        }
    }

    @NonNull
    private static String stateToString(int state) {
        switch (state) {
            case STATE_FLAT:
                return "FLAT";
            case STATE_FLIPPED:
                return "FLIPPED";
            case STATE_HALF_OPENED:
                return "HALF_OPENED";
            default:
                return "Unknown feature state (" + state + ")";
        }
    }

    @NonNull
    @Override
    public String toString() {
        return FoldingFeature.class.getSimpleName() + " { " + mBounds + ", type="
                + typeToString(mType) + ", state=" + stateToString(mState) + " }";
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (!(o instanceof FoldingFeature)) return false;
        FoldingFeature that = (FoldingFeature) o;
        return mType == that.mType
            && mState == that.mState
            && mBounds.equals(that.mBounds);
    }

    @Override
    public int hashCode() {
        int result = mBounds.hashCode();
        result = 31 * result + mType;
        result = 31 * result + mState;
        return result;
    }
}