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

Commit f5d09a50 authored by Treehugger Robot's avatar Treehugger Robot Committed by Android (Google) Code Review
Browse files

Merge "Add animated chip in statusbar on external display connected" into udc-qpr-dev

parents abeaa64d 16938928
Loading
Loading
Loading
Loading
+48 −0
Original line number Diff line number Diff line
<?xml version="1.0" encoding="utf-8"?><!--
     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.
-->


<merge xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/connected_display_chip"
    android:layout_width="wrap_content"
    android:layout_height="match_parent"
    android:layout_gravity="center_vertical|end"
    tools:parentTag="com.android.systemui.statusbar.ConnectedDisplayChip">


    <FrameLayout
        android:id="@+id/icons_rounded_container"
        android:layout_width="wrap_content"
        android:layout_height="@dimen/ongoing_appops_chip_height"
        android:layout_gravity="center"
        android:background="@drawable/statusbar_chip_bg"
        android:clipToOutline="true"
        android:clipToPadding="false"
        android:gravity="center"
        android:maxWidth="@dimen/ongoing_appops_chip_max_width"
        android:minWidth="@dimen/ongoing_appops_chip_min_width">

        <ImageView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="center"
            android:layout_marginHorizontal="10dp"
            android:scaleType="centerInside"
            android:src="@drawable/stat_sys_connected_display"
            android:tint="@android:color/black" />
    </FrameLayout>
</merge>
 No newline at end of file
+55 −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.statusbar

import android.annotation.SuppressLint
import android.content.Context
import android.content.res.Configuration
import android.util.AttributeSet
import android.widget.FrameLayout
import com.android.systemui.R
import com.android.systemui.statusbar.events.BackgroundAnimatableView

/** Chip that appears in the status bar when an external display is connected. */
class ConnectedDisplayChip
@JvmOverloads
constructor(context: Context, attrs: AttributeSet? = null) :
    FrameLayout(context, attrs), BackgroundAnimatableView {

    private val iconContainer: FrameLayout
    init {
        inflate(context, R.layout.connected_display_chip, this)
        iconContainer = requireViewById(R.id.icons_rounded_container)
    }

    /**
     * When animating as a chip in the status bar, we want to animate the width for the rounded
     * container. We have to subtract our own top and left offset because the bounds come to us as
     * absolute on-screen bounds.
     */
    override fun setBoundsForAnimation(l: Int, t: Int, r: Int, b: Int) {
        iconContainer.setLeftTopRightBottom(l - left, t - top, r - left, b - top)
    }

    override fun onConfigurationChanged(newConfig: Configuration) {
        super.onConfigurationChanged(newConfig)
        updateResources()
    }

    @SuppressLint("UseCompatLoadingForDrawables")
    private fun updateResources() {
        iconContainer.background = context.getDrawable(R.drawable.statusbar_chip_bg)
    }
}
+18 −0
Original line number Diff line number Diff line
@@ -24,6 +24,7 @@ import android.widget.ImageView
import com.android.systemui.privacy.OngoingPrivacyChip
import com.android.systemui.privacy.PrivacyItem
import com.android.systemui.statusbar.BatteryStatusChip
import com.android.systemui.statusbar.ConnectedDisplayChip

typealias ViewCreator = (context: Context) -> BackgroundAnimatableView

@@ -87,6 +88,23 @@ class BatteryEvent(@IntRange(from = 0, to = 100) val batteryLevel: Int) : Status
    }
}

/** Event that triggers a connected display chip in the status bar. */
class ConnectedDisplayEvent : StatusEvent {
    /** Priority is set higher than [BatteryEvent]. */
    override val priority = 60
    override var forceVisible = false
    override val showAnimation = true
    override var contentDescription: String? = ""

    override val viewCreator: ViewCreator = { context ->
        ConnectedDisplayChip(context)
    }

    override fun toString(): String {
        return javaClass.simpleName
    }
}

/** open only for testing purposes. (See [FakeStatusEvent.kt]) */
open class PrivacyEvent(override val showAnimation: Boolean = true) : StatusEvent {
    override var contentDescription: String? = null
+30 −4
Original line number Diff line number Diff line
@@ -22,6 +22,9 @@ import android.provider.DeviceConfig
import android.provider.DeviceConfig.NAMESPACE_PRIVACY
import com.android.systemui.R
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.display.domain.interactor.ConnectedDisplayInteractor
import com.android.systemui.display.domain.interactor.ConnectedDisplayInteractor.State
import com.android.systemui.flags.FeatureFlags
import com.android.systemui.flags.Flags
import com.android.systemui.privacy.PrivacyChipBuilder
@@ -30,29 +33,45 @@ import com.android.systemui.privacy.PrivacyItemController
import com.android.systemui.statusbar.policy.BatteryController
import com.android.systemui.util.time.SystemClock
import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach

/**
 * Listens for system events (battery, privacy, connectivity) and allows listeners
 * to show status bar animations when they happen
 * Listens for system events (battery, privacy, connectivity) and allows listeners to show status
 * bar animations when they happen
 */
@SysUISingleton
class SystemEventCoordinator @Inject constructor(
class SystemEventCoordinator
@Inject
constructor(
    private val systemClock: SystemClock,
    private val batteryController: BatteryController,
    private val privacyController: PrivacyItemController,
    private val context: Context,
    private val featureFlags: FeatureFlags
    private val featureFlags: FeatureFlags,
    @Application private val appScope: CoroutineScope,
    connectedDisplayInteractor: ConnectedDisplayInteractor
) {
    private val onDisplayConnectedFlow =
        connectedDisplayInteractor.connectedDisplayState
                .filter { it != State.DISCONNECTED }

    private var connectedDisplayCollectionJob: Job? = null
    private lateinit var scheduler: SystemStatusAnimationScheduler

    fun startObserving() {
        batteryController.addCallback(batteryStateListener)
        privacyController.addCallback(privacyStateListener)
        startConnectedDisplayCollection()
    }

    fun stopObserving() {
        batteryController.removeCallback(batteryStateListener)
        privacyController.removeCallback(privacyStateListener)
        connectedDisplayCollectionJob?.cancel()
    }

    fun attachScheduler(s: SystemStatusAnimationScheduler) {
@@ -80,6 +99,13 @@ class SystemEventCoordinator @Inject constructor(
        scheduler.onStatusEvent(event)
    }

    private fun startConnectedDisplayCollection() {
        connectedDisplayCollectionJob =
                onDisplayConnectedFlow
                        .onEach { scheduler.onStatusEvent(ConnectedDisplayEvent()) }
                        .launchIn(appScope)
    }

    private val batteryStateListener = object : BatteryController.BatteryStateChangeCallback {
        private var plugged = false
        private var stateKnown = false
+109 −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.statusbar.events

import android.testing.AndroidTestingRunner
import android.testing.TestableLooper
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.display.domain.interactor.ConnectedDisplayInteractor
import com.android.systemui.display.domain.interactor.ConnectedDisplayInteractor.State.CONNECTED
import com.android.systemui.flags.FakeFeatureFlags
import com.android.systemui.privacy.PrivacyItemController
import com.android.systemui.statusbar.policy.BatteryController
import com.android.systemui.util.mockito.any
import com.android.systemui.util.time.FakeSystemClock
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.UnconfinedTestDispatcher
import kotlinx.coroutines.test.runTest
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.Mock
import org.mockito.Mockito.times
import org.mockito.Mockito.verify
import org.mockito.Mockito.verifyNoMoreInteractions
import org.mockito.MockitoAnnotations

@RunWith(AndroidTestingRunner::class)
@TestableLooper.RunWithLooper(setAsMainLooper = true)
@SmallTest
@OptIn(ExperimentalCoroutinesApi::class)
class SystemEventCoordinatorTest : SysuiTestCase() {

    private val fakeSystemClock = FakeSystemClock()
    private val featureFlags = FakeFeatureFlags()
    private val testScope = TestScope(UnconfinedTestDispatcher())
    private val connectedDisplayInteractor = FakeConnectedDisplayInteractor()

    @Mock lateinit var batteryController: BatteryController
    @Mock lateinit var privacyController: PrivacyItemController
    @Mock lateinit var scheduler: SystemStatusAnimationScheduler

    private lateinit var systemEventCoordinator: SystemEventCoordinator
    @Before
    fun setup() {
        MockitoAnnotations.initMocks(this)
        systemEventCoordinator =
            SystemEventCoordinator(
                    fakeSystemClock,
                    batteryController,
                    privacyController,
                    context,
                    featureFlags,
                    TestScope(UnconfinedTestDispatcher()),
                    connectedDisplayInteractor
                )
                .apply { attachScheduler(scheduler) }
    }

    @Test
    fun startObserving_propagatesConnectedDisplayStatusEvents() =
        testScope.runTest {
            systemEventCoordinator.startObserving()

            connectedDisplayInteractor.emit(CONNECTED)
            connectedDisplayInteractor.emit(CONNECTED)

            verify(scheduler, times(2)).onStatusEvent(any<ConnectedDisplayEvent>())
        }

    @Test
    fun stopObserving_doesNotPropagateConnectedDisplayStatusEvents() =
        testScope.runTest {
            systemEventCoordinator.startObserving()

            connectedDisplayInteractor.emit(CONNECTED)

            verify(scheduler).onStatusEvent(any<ConnectedDisplayEvent>())

            systemEventCoordinator.stopObserving()

            connectedDisplayInteractor.emit(CONNECTED)

            verifyNoMoreInteractions(scheduler)
        }

    class FakeConnectedDisplayInteractor : ConnectedDisplayInteractor {
        private val flow = MutableSharedFlow<ConnectedDisplayInteractor.State>()
        suspend fun emit(value: ConnectedDisplayInteractor.State) = flow.emit(value)
        override val connectedDisplayState: Flow<ConnectedDisplayInteractor.State>
            get() = flow
    }
}