FoldingFeature.kt

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

import android.graphics.Rect

/**
 * A feature that describes a fold in the flexible display
 * or a hinge between two physical display panels.
 *
 * @param [type] that is either [FoldingFeature.Type.FOLD] or [FoldingFeature.Type.HINGE]
 * @param [state] the physical state of the hinge that is either [FoldingFeature.State.FLAT] or
 * [FoldingFeature.State.HALF_OPENED]
 */
public class FoldingFeature internal constructor(
    /**
     * The bounding rectangle of the feature within the application window in the window
     * coordinate space.
     */
    private val featureBounds: Bounds,
    internal val type: Type,
    public val state: State
) : DisplayFeature {

    /**
     * Represents the type of hinge.
     */
    public class Type private constructor(private val description: String) {

        override fun toString(): String {
            return description
        }

        public companion object {
            /**
             * Represent a continuous screen that folds.
             */
            @JvmField
            public val FOLD: Type = Type("FOLD")

            /**
             * Represents a hinge connecting two separate display panels.
             */
            @JvmField
            public val HINGE: Type = Type("HINGE")

            internal fun from(value: Int): Type {
                return when (value) {
                    TYPE_FOLD -> FOLD
                    TYPE_HINGE -> HINGE
                    else -> throw IllegalArgumentException(
                        "${FoldingFeature::class.java.simpleName} incorrect type value"
                    )
                }
            }
        }
    }

    /**
     * Represents how the hinge might occlude content.
     */
    public class OcclusionType private constructor(private val description: String) {

        override fun toString(): String {
            return description
        }

        public companion object {
            /**
             * The [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.
             */
            @JvmField
            public val NONE: OcclusionType = OcclusionType("NONE")

            /**
             * The [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.
             */
            @JvmField
            public val FULL: OcclusionType = OcclusionType("FULL")
        }
    }

    /**
     * Represents the axis for which the [FoldingFeature] runs parallel to.
     */
    public class Orientation private constructor(private val description: String) {

        override fun toString(): String {
            return description
        }

        public companion object {

            /**
             * The height of the [FoldingFeature] is greater than or equal to the width.
             */
            @JvmField
            public val VERTICAL: Orientation = Orientation("VERTICAL")

            /**
             * The width of the [FoldingFeature] is greater than the height.
             */
            @JvmField
            public val HORIZONTAL: Orientation = Orientation("HORIZONTAL")
        }
    }

    /**
     * Represents the [State] of the [FoldingFeature].
     */
    public class State private constructor(private val description: String) {

        override fun toString(): String {
            return description
        }

        public companion object {
            /**
             * The foldable device is completely open, the screen space that is presented to the
             * user is flat. See the
             * [Posture](https://developer.android.com/guide/topics/ui/foldables#postures)
             * section in the official documentation for visual samples and references.
             */
            @JvmField
            public val FLAT: State = State("FLAT")

            /**
             * 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
             * [Posture](https://developer.android.com/guide/topics/ui/foldables#postures)
             * section in the official documentation for visual samples and references.
             */
            @JvmField
            public val HALF_OPENED: State = State("HALF_OPENED")
        }
    }

    override val bounds: Rect
        get() = featureBounds.toRect()

    init {
        validateFeatureBounds(featureBounds)
    }

    public constructor(
        bounds: Rect,
        type: Type,
        state: State
    ) : this(Bounds(bounds), type, state)

    /**
     * Calculates if a [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 [FoldingFeature.State.FLAT].
     *
     * Apps may use this to determine if content should lay out around the [FoldingFeature].
     * Developers should consider the placement of interactive elements. Similar to the case of
     * [FoldingFeature.OcclusionType.FULL], when a feature is separating then consider laying
     * out the controls around the [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 [FoldingFeature] and
     * the auxiliary content can be placed on the other side.
     *
     * @return `true` if the feature splits the display into two areas, `false`
     * otherwise.
     */
    public val isSeparating: Boolean
        get() = when {
            type == Type.HINGE -> true
            type == Type.FOLD && state == State.HALF_OPENED -> true
            else -> false
        }

    /**
     * Calculates the occlusion mode to determine if a [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 [FoldingFeature.OcclusionType.NONE] the feature can be treated as a
     * guideline. One example would be for a continuously folding screen. For occlusion type
     * [FoldingFeature.OcclusionType.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
     * [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 [FoldingFeature.OcclusionType.NONE] if the [FoldingFeature] has empty
     * bounds.
     */
    public val occlusionType: OcclusionType
        get() = if (featureBounds.width == 0 || featureBounds.height == 0) {
            OcclusionType.NONE
        } else {
            OcclusionType.FULL
        }

    /**
     * Returns [FoldingFeature.Orientation.HORIZONTAL] if the width is greater than the
     * height, [FoldingFeature.Orientation.VERTICAL] otherwise.
     */
    public val orientation: Orientation
        get() {
            return if (featureBounds.width > featureBounds.height) {
                Orientation.HORIZONTAL
            } else {
                Orientation.VERTICAL
            }
        }

    override fun toString(): String {
        return (
            "${FoldingFeature::class.java.simpleName} { $featureBounds, " +
                "type=$type, state=$state }"
            )
    }

    override fun equals(other: Any?): Boolean {
        if (this === other) return true
        if (javaClass != other?.javaClass) return false

        other as FoldingFeature

        if (featureBounds != other.featureBounds) return false
        if (type != other.type) return false
        if (state != other.state) return false

        return true
    }

    override fun hashCode(): Int {
        var result = featureBounds.hashCode()
        result = 31 * result + type.hashCode()
        result = 31 * result + state.hashCode()
        return result
    }

    public companion object {
        /**
         * A fold in the flexible screen without a physical gap.
         */
        public const val TYPE_FOLD: Int = 1

        /**
         * A physical separation with a hinge that allows two display panels to fold.
         */
        public const val TYPE_HINGE: Int = 2

        /**
         * The foldable device is completely open, the screen space that is presented to the user
         * is flat. See the
         * [Posture](https://developer.android.com/guide/topics/ui/foldables#postures)
         * section in the official documentation for visual samples and references.
         */
        public const val STATE_FLAT: Int = 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
         * [Posture](https://developer.android.com/guide/topics/ui/foldables#postures)
         * section in the official documentation for visual samples and references.
         */
        public const val STATE_HALF_OPENED: Int = 2

        /**
         * The [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 const val OCCLUSION_NONE: Int = 0

        /**
         * The [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 const val OCCLUSION_FULL: Int = 1

        /**
         * The height of the [FoldingFeature] is greater than or equal to the width.
         */
        public const val ORIENTATION_VERTICAL: Int = 0

        /**
         * The width of the [FoldingFeature] is greater than the height.
         */
        public const val ORIENTATION_HORIZONTAL: Int = 1

        /**
         * Verifies the bounds of the folding feature.
         */
        internal fun validateFeatureBounds(bounds: Bounds) {
            require(!(bounds.width == 0 && bounds.height == 0)) { "Bounds must be non zero" }
            require(!(bounds.left != 0 && bounds.top != 0)) {
                "Bounding rectangle must start at the top or left window edge for folding features"
            }
        }
    }
}