Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 00b49c07 authored by Alejandro Nijamkin's avatar Alejandro Nijamkin Committed by Ale Nijamkin
Browse files

Multi-shade foundation - data layer (1/5).

Early foundation for the multi-shade framework.

Data layer for the multi-shade foundation. Includes cross-layer shared
models, a proxy for sending touch events into the framework, and a
repository to store and retrieve application state.

Bug: 272130181
Test: includes unit tests for code in this layer. Tested end-to-end in
the last CL in this chain.

Change-Id: Ic1dfd8deb0bdde95288d824f91986e4749810ca2
parent fb818bed
Loading
Loading
Loading
Loading
+40 −0
Original line number Diff line number Diff line
@@ -844,4 +844,44 @@

    <!-- Configuration to set Learn more in device logs as URL link -->
    <bool name="log_access_confirmation_learn_more_as_link">true</bool>

    <!-- [START] MULTI SHADE -->
    <!-- Whether the device should use dual shade. If false, the device uses single shade. -->
    <bool name="dual_shade_enabled">true</bool>
    <!--
    When in dual shade, where should the horizontal split be on the screen to help determine whether
    the user is pulling down the left shade or the right shade. Must be between 0.0 and 1.0,
    inclusive. In other words: how much of the left-hand side of the screen, when pulled down on,
    would reveal the left-hand side shade.

    More concretely:
    A value of 0.67 means that the left two-thirds of the screen are dedicated to the left-hand side
    shade and the remaining one-third of the screen on the right is dedicated to the right-hand side
    shade.
    -->
    <dimen name="dual_shade_split_fraction">0.67</dimen>
    <!-- Width of the left-hand side shade. -->
    <dimen name="left_shade_width">436dp</dimen>
    <!-- Width of the right-hand side shade. -->
    <dimen name="right_shade_width">436dp</dimen>
    <!--
    Opaque version of the scrim that shows up behind dual shades. The alpha channel is driven
    programmatically.
    -->
    <color name="opaque_scrim">#D9D9D9</color>
    <!-- Maximum opacity when the scrim that shows up behind the dual shades is fully visible. -->
    <dimen name="dual_shade_scrim_alpha">0.1</dimen>
    <!--
    The amount that the user must swipe down when the shade is fully collapsed to automatically
    expand once the user lets go of the shade. If the user swipes less than this amount, the shade
    will automatically revert back to fully collapsed once the user stops swiping.
    -->
    <dimen name="shade_swipe_expand_threshold">0.5</dimen>
    <!--
    The amount that the user must swipe up when the shade is fully expanded to automatically
    collapse once the user lets go of the shade. If the user swipes less than this amount, the shade
    will automatically revert back to fully expanded once the user stops swiping.
    -->
    <dimen name="shade_swipe_collapse_threshold">0.5</dimen>
    <!-- [END] MULTI SHADE -->
</resources>
+28 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2023 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 com.android.systemui.multishade.data.model

import com.android.systemui.multishade.shared.model.ShadeId

/** Models the current interaction with one of the shades. */
data class MultiShadeInteractionModel(
    /** The ID of the shade that the user is currently interacting with. */
    val shadeId: ShadeId,
    /** Whether the interaction is proxied (as in: coming from an external app or different UI). */
    val isProxied: Boolean,
)
+47 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2023 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 com.android.systemui.multishade.data.remoteproxy

import com.android.systemui.multishade.shared.model.ProxiedInputModel
import javax.inject.Inject
import javax.inject.Singleton
import kotlinx.coroutines.channels.BufferOverflow
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.asSharedFlow

/**
 * Acts as a hub for routing proxied user input into the multi shade system.
 *
 * "Proxied" user input is coming through a proxy; typically from an external app or different UI.
 * In other words: it's not user input that's occurring directly on the shade UI itself. This class
 * is that proxy.
 */
@Singleton
class MultiShadeInputProxy @Inject constructor() {
    private val _proxiedTouch =
        MutableSharedFlow<ProxiedInputModel>(
            replay = 1,
            onBufferOverflow = BufferOverflow.DROP_OLDEST,
        )
    val proxiedInput: Flow<ProxiedInputModel> = _proxiedTouch.asSharedFlow()

    fun onProxiedInput(proxiedInput: ProxiedInputModel) {
        _proxiedTouch.tryEmit(proxiedInput)
    }
}
+157 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2023 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 com.android.systemui.multishade.data.repository

import android.content.Context
import androidx.annotation.FloatRange
import com.android.systemui.R
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.multishade.data.model.MultiShadeInteractionModel
import com.android.systemui.multishade.data.remoteproxy.MultiShadeInputProxy
import com.android.systemui.multishade.shared.model.ProxiedInputModel
import com.android.systemui.multishade.shared.model.ShadeConfig
import com.android.systemui.multishade.shared.model.ShadeId
import com.android.systemui.multishade.shared.model.ShadeModel
import javax.inject.Inject
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow

/** Encapsulates application state for all shades. */
@SysUISingleton
class MultiShadeRepository
@Inject
constructor(
    @Application private val applicationContext: Context,
    inputProxy: MultiShadeInputProxy,
) {
    /**
     * Remote input coming from sources outside of system UI (for example, swiping down on the
     * Launcher or from the status bar).
     */
    val proxiedInput: Flow<ProxiedInputModel> = inputProxy.proxiedInput

    /** Width of the left-hand side shade, in pixels. */
    private val leftShadeWidthPx =
        applicationContext.resources.getDimensionPixelSize(R.dimen.left_shade_width)

    /** Width of the right-hand side shade, in pixels. */
    private val rightShadeWidthPx =
        applicationContext.resources.getDimensionPixelSize(R.dimen.right_shade_width)

    /**
     * The amount that the user must swipe up when the shade is fully expanded to automatically
     * collapse once the user lets go of the shade. If the user swipes less than this amount, the
     * shade will automatically revert back to fully expanded once the user stops swiping.
     *
     * This is a fraction between `0` and `1`.
     */
    private val swipeCollapseThreshold =
        checkInBounds(applicationContext.resources.getFloat(R.dimen.shade_swipe_collapse_threshold))

    /**
     * The amount that the user must swipe down when the shade is fully collapsed to automatically
     * expand once the user lets go of the shade. If the user swipes less than this amount, the
     * shade will automatically revert back to fully collapsed once the user stops swiping.
     *
     * This is a fraction between `0` and `1`.
     */
    private val swipeExpandThreshold =
        checkInBounds(applicationContext.resources.getFloat(R.dimen.shade_swipe_expand_threshold))

    /**
     * Maximum opacity when the scrim that shows up behind the dual shades is fully visible.
     *
     * This is a fraction between `0` and `1`.
     */
    private val dualShadeScrimAlpha =
        checkInBounds(applicationContext.resources.getFloat(R.dimen.dual_shade_scrim_alpha))

    /** The current configuration of the shade system. */
    val shadeConfig: StateFlow<ShadeConfig> =
        MutableStateFlow(
                if (applicationContext.resources.getBoolean(R.bool.dual_shade_enabled)) {
                    ShadeConfig.DualShadeConfig(
                        leftShadeWidthPx = leftShadeWidthPx,
                        rightShadeWidthPx = rightShadeWidthPx,
                        swipeCollapseThreshold = swipeCollapseThreshold,
                        swipeExpandThreshold = swipeExpandThreshold,
                        splitFraction =
                            applicationContext.resources.getFloat(
                                R.dimen.dual_shade_split_fraction
                            ),
                        scrimAlpha = dualShadeScrimAlpha,
                    )
                } else {
                    ShadeConfig.SingleShadeConfig(
                        swipeCollapseThreshold = swipeCollapseThreshold,
                        swipeExpandThreshold = swipeExpandThreshold,
                    )
                }
            )
            .asStateFlow()

    private val _forceCollapseAll = MutableStateFlow(false)
    /** Whether all shades should be collapsed. */
    val forceCollapseAll: StateFlow<Boolean> = _forceCollapseAll.asStateFlow()

    private val _shadeInteraction = MutableStateFlow<MultiShadeInteractionModel?>(null)
    /** The current shade interaction or `null` if no shade is interacted with currently. */
    val shadeInteraction: StateFlow<MultiShadeInteractionModel?> = _shadeInteraction.asStateFlow()

    private val stateByShade = mutableMapOf<ShadeId, MutableStateFlow<ShadeModel>>()

    /** The model for the shade with the given ID. */
    fun getShade(
        shadeId: ShadeId,
    ): StateFlow<ShadeModel> {
        return getMutableShade(shadeId).asStateFlow()
    }

    /** Sets the expansion amount for the shade with the given ID. */
    fun setExpansion(
        shadeId: ShadeId,
        @FloatRange(from = 0.0, to = 1.0) expansion: Float,
    ) {
        getMutableShade(shadeId).let { mutableState ->
            mutableState.value = mutableState.value.copy(expansion = expansion)
        }
    }

    /** Sets whether all shades should be immediately forced to collapse. */
    fun setForceCollapseAll(isForced: Boolean) {
        _forceCollapseAll.value = isForced
    }

    /** Sets the current shade interaction; use `null` if no shade is interacted with currently. */
    fun setShadeInteraction(shadeInteraction: MultiShadeInteractionModel?) {
        _shadeInteraction.value = shadeInteraction
    }

    private fun getMutableShade(id: ShadeId): MutableStateFlow<ShadeModel> {
        return stateByShade.getOrPut(id) { MutableStateFlow(ShadeModel(id)) }
    }

    /** Asserts that the given [Float] is in the range of `0` and `1`, inclusive. */
    private fun checkInBounds(float: Float): Float {
        check(float in 0f..1f) { "$float isn't between 0 and 1." }
        return float
    }
}
+50 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2023 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 com.android.systemui.multishade.shared.model

import androidx.annotation.FloatRange

/**
 * Models a part of an ongoing proxied user input gesture.
 *
 * "Proxied" user input is coming through a proxy; typically from an external app or different UI.
 * In other words: it's not user input that's occurring directly on the shade UI itself.
 */
sealed class ProxiedInputModel {
    /** The user is dragging their pointer. */
    data class OnDrag(
        /**
         * The relative position of the pointer as a fraction of its container width where `0` is
         * all the way to the left and `1` is all the way to the right.
         */
        @FloatRange(from = 0.0, to = 1.0) val xFraction: Float,
        /** The amount that the pointer was dragged, in pixels. */
        val yDragAmountPx: Float,
    ) : ProxiedInputModel()

    /** The user finished dragging by lifting up their pointer. */
    object OnDragEnd : ProxiedInputModel()

    /**
     * The drag gesture has been canceled. Usually because the pointer exited the draggable area.
     */
    object OnDragCancel : ProxiedInputModel()

    /** The user has tapped (clicked). */
    object OnTap : ProxiedInputModel()
}
Loading