Loading packages/SystemUI/multivalentTests/src/com/android/systemui/underlay/data/repository/UnderlayRepositoryTest.kt +107 −16 Original line number Diff line number Diff line /* * Copyright (C) 2020 The Android Open Source Project * Copyright (C) 2025 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. Loading @@ -16,49 +16,140 @@ package com.android.systemui.underlay.data.repository import android.content.Intent import android.app.smartspace.SmartspaceAction import android.app.smartspace.SmartspaceManager import android.app.smartspace.SmartspaceSession import android.app.smartspace.SmartspaceSession.OnTargetsAvailableListener import android.app.smartspace.SmartspaceTarget import android.content.testableContext import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.broadcast.broadcastDispatcher import com.android.systemui.concurrency.fakeExecutor import com.android.systemui.kosmos.advanceUntilIdle import com.android.systemui.kosmos.backgroundScope import com.android.systemui.kosmos.collectLastValue import com.android.systemui.kosmos.runCurrent import com.android.systemui.kosmos.runTest import com.android.systemui.testKosmos import com.android.systemui.underlay.data.repository.UnderlayRepositoryImpl.Companion.AMBIENT_ACTION_FEATURE import com.android.systemui.underlay.data.repository.UnderlayRepositoryImpl.Companion.UNDERLAY_SURFACE import com.google.common.truth.Truth.assertThat import org.junit.Test import org.junit.runner.RunWith import org.mockito.kotlin.any import org.mockito.kotlin.argumentCaptor import org.mockito.kotlin.doReturn import org.mockito.kotlin.mock import org.mockito.kotlin.verify @RunWith(AndroidJUnit4::class) @SmallTest class UnderlayRepositoryTest : SysuiTestCase() { private val kosmos = testKosmos() private val smartSpaceSession = mock<SmartspaceSession>() private val smartSpaceManager = mock<SmartspaceManager>() { on { createSmartspaceSession(any()) } doReturn smartSpaceSession } val onTargetsAvailableListenerCaptor = argumentCaptor<OnTargetsAvailableListener>() private val underTest = UnderlayRepositoryImpl( backgroundScope = kosmos.backgroundScope, broadcastDispatcher = kosmos.broadcastDispatcher, smartSpaceManager = smartSpaceManager, executor = kosmos.fakeExecutor, applicationContext = kosmos.testableContext, ) @Test fun isUnderlayAttached_whenCreated_true() = fun isUnderlayAttached_whenHasActions_true() = kosmos.runTest { val isUnderlayAttached by collectLastValue(underlayRepository.isUnderlayAttached) val isUnderlayAttached by collectLastValue(underTest.isAttached) runCurrent() verify(smartSpaceSession) .addOnTargetsAvailableListener(any(), onTargetsAvailableListenerCaptor.capture()) onTargetsAvailableListenerCaptor.firstValue.onTargetsAvailable(allTargets) advanceUntilIdle() assertThat(isUnderlayAttached).isTrue() } broadcastDispatcher.sendIntentToMatchingReceiversOnly( context, Intent(UnderlayRepository.ACTION_CREATE_UNDERLAY), @Test fun isUnderlayAttached_whenNoActions_false() = kosmos.runTest { val isUnderlayAttached by collectLastValue(underTest.isAttached) runCurrent() verify(smartSpaceSession) .addOnTargetsAvailableListener(any(), onTargetsAvailableListenerCaptor.capture()) onTargetsAvailableListenerCaptor.firstValue.onTargetsAvailable( listOf(invalidTarget1, invalidTarget2) ) assertThat(isUnderlayAttached).isTrue() advanceUntilIdle() assertThat(isUnderlayAttached).isFalse() } @Test fun isUnderlayAttached_whenDestroyed_false() = fun actions_whenHasSmartSpaceAction() = kosmos.runTest { val isUnderlayAttached by collectLastValue(underlayRepository.isUnderlayAttached) val actions by collectLastValue(underTest.actions) runCurrent() verify(smartSpaceSession) .addOnTargetsAvailableListener(any(), onTargetsAvailableListenerCaptor.capture()) onTargetsAvailableListenerCaptor.firstValue.onTargetsAvailable(allTargets) runCurrent() broadcastDispatcher.sendIntentToMatchingReceiversOnly( context, Intent(UnderlayRepository.ACTION_DESTROY_UNDERLAY), actions.let { requireNotNull(it) assertThat(it.size).isEqualTo(2) val firstAction = it.first() assertThat(firstAction.label).isEqualTo(TITLE_1) assertThat(firstAction.attribution).isEqualTo(SUBTITLE_1) val lastAction = it.last() assertThat(lastAction.label).isEqualTo(TITLE_2) assertThat(lastAction.attribution).isEqualTo(SUBTITLE_2) } } companion object { private const val TITLE_1 = "title 1" private const val TITLE_2 = "title 2" private const val SUBTITLE_1 = "subtitle 1" private const val SUBTITLE_2 = "subtitle 2" private val validTarget = mock<SmartspaceTarget> { on { featureType } doReturn AMBIENT_ACTION_FEATURE on { smartspaceTargetId } doReturn UNDERLAY_SURFACE on { actionChips } doReturn listOf( SmartspaceAction.Builder("action1-id", "title 1") .setSubtitle("subtitle 1") .build(), SmartspaceAction.Builder("action2-id", "title 2") .setSubtitle("subtitle 2") .build(), ) } assertThat(isUnderlayAttached).isFalse() private val invalidTarget1 = mock<SmartspaceTarget> { on { featureType } doReturn 1 on { smartspaceTargetId } doReturn UNDERLAY_SURFACE on { actionChips } doReturn listOf(SmartspaceAction.Builder("id", "title").setSubtitle("subtitle").build()) } private val invalidTarget2 = mock<SmartspaceTarget> { on { featureType } doReturn AMBIENT_ACTION_FEATURE on { smartspaceTargetId } doReturn "home" on { actionChips } doReturn listOf(SmartspaceAction.Builder("id", "title").setSubtitle("subtitle").build()) } private val allTargets = listOf(validTarget, invalidTarget1, invalidTarget2) } } packages/SystemUI/multivalentTests/src/com/android/systemui/underlay/domain/interactor/UnderlayInteractorTest.kt +7 −25 Original line number Diff line number Diff line /* * Copyright (C) 2020 The Android Open Source Project * Copyright (C) 2025 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. Loading @@ -21,13 +21,12 @@ import android.content.applicationContext import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.broadcast.broadcastDispatcher import com.android.systemui.kosmos.collectLastValue import com.android.systemui.kosmos.runCurrent import com.android.systemui.kosmos.runTest import com.android.systemui.res.R import com.android.systemui.testKosmos import com.android.systemui.underlay.data.repository.UnderlayRepository import com.android.systemui.underlay.data.repository.fake import com.android.systemui.underlay.data.repository.underlayRepository import com.android.systemui.underlay.shared.model.ActionModel import com.google.common.truth.Truth.assertThat import org.junit.Test Loading @@ -42,13 +41,7 @@ class UnderlayInteractorTest : SysuiTestCase() { fun isUnderlayAttached_whenCreated_true() = kosmos.runTest { val isUnderlayAttached by collectLastValue(underlayInteractor.isUnderlayAttached) runCurrent() broadcastDispatcher.sendIntentToMatchingReceiversOnly( context, Intent(UnderlayRepository.ACTION_CREATE_UNDERLAY), ) underlayRepository.fake.setIsUnderlayAttached(true) assertThat(isUnderlayAttached).isTrue() } Loading @@ -56,13 +49,7 @@ class UnderlayInteractorTest : SysuiTestCase() { fun isUnderlayAttached_whenDestroyed_false() = kosmos.runTest { val isUnderlayAttached by collectLastValue(underlayInteractor.isUnderlayAttached) runCurrent() broadcastDispatcher.sendIntentToMatchingReceiversOnly( context, Intent(UnderlayRepository.ACTION_DESTROY_UNDERLAY), ) underlayRepository.fake.setIsUnderlayAttached(false) assertThat(isUnderlayAttached).isFalse() } Loading @@ -70,9 +57,7 @@ class UnderlayInteractorTest : SysuiTestCase() { fun isOverlayVisible_setTrue_true() = kosmos.runTest { val isOverlayVisible by collectLastValue(underlayInteractor.isOverlayVisible) underlayInteractor.setIsOverlayVisible(true) assertThat(isOverlayVisible).isTrue() } Loading @@ -80,9 +65,7 @@ class UnderlayInteractorTest : SysuiTestCase() { fun isOverlayVisible_setFalse_False() = kosmos.runTest { val isOverlayVisible by collectLastValue(underlayInteractor.isOverlayVisible) underlayInteractor.setIsOverlayVisible(false) assertThat(isOverlayVisible).isFalse() } Loading @@ -100,11 +83,10 @@ class UnderlayInteractorTest : SysuiTestCase() { ), label = "Sunday Morning", attribution = null, intent = Intent(), ) ) underlayInteractor.setActions(testActions) underlayRepository.fake.setActions(testActions) assertThat(actions).isEqualTo(testActions) } } packages/SystemUI/src/com/android/systemui/underlay/UnderlayModule.kt +4 −0 Original line number Diff line number Diff line Loading @@ -17,6 +17,8 @@ package com.android.systemui.underlay import com.android.systemui.CoreStartable import com.android.systemui.underlay.data.repository.UnderlayRepository import com.android.systemui.underlay.data.repository.UnderlayRepositoryImpl import com.android.systemui.underlay.ui.startable.UnderlayCoreStartable import dagger.Binds import dagger.Module Loading @@ -30,4 +32,6 @@ interface UnderlayModule { @IntoMap @ClassKey(UnderlayCoreStartable::class) fun bindUnderlayCoreStartable(startable: UnderlayCoreStartable): CoreStartable @Binds fun bindsUnderlayRepository(impl: UnderlayRepositoryImpl): UnderlayRepository } packages/SystemUI/src/com/android/systemui/underlay/data/repository/UnderlayRepository.kt +100 −10 Original line number Diff line number Diff line Loading @@ -16,28 +16,57 @@ package com.android.systemui.underlay.data.repository import android.app.smartspace.SmartspaceConfig import android.app.smartspace.SmartspaceManager import android.app.smartspace.SmartspaceSession.OnTargetsAvailableListener import android.content.Context import android.content.IntentFilter import android.util.Log import androidx.annotation.VisibleForTesting import com.android.systemui.broadcast.BroadcastDispatcher import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.res.R import com.android.systemui.underlay.shared.model.ActionModel import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow import java.util.concurrent.Executor import javax.inject.Inject import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.stateIn /** Source of truth for ambient actions and visibility of their system space. */ interface UnderlayRepository { /** Chips that should be visible on the UI. */ val actions: StateFlow<List<ActionModel>> /** If window should be added to the navbar area or not. */ val isAttached: StateFlow<Boolean> /** If hint (or chips list) should be visible. */ val isVisible: MutableStateFlow<Boolean> } @SysUISingleton class UnderlayRepository class UnderlayRepositoryImpl @Inject constructor( @Background private val backgroundScope: CoroutineScope, broadcastDispatcher: BroadcastDispatcher, ) { val isUnderlayAttached: StateFlow<Boolean> = broadcastDispatcher .broadcastFlow( private val smartSpaceManager: SmartspaceManager?, @Background executor: Executor, @Application applicationContext: Context, ) : UnderlayRepository { private val debugBroadcastFlow: Flow<Boolean> = if (DEBUG) { broadcastDispatcher.broadcastFlow( filter = IntentFilter().apply { addAction(ACTION_CREATE_UNDERLAY) Loading @@ -46,18 +75,79 @@ constructor( ) { intent, _ -> intent.action == ACTION_CREATE_UNDERLAY } } else { MutableStateFlow(false).asStateFlow() } override val actions: StateFlow<List<ActionModel>> = conflatedCallbackFlow { if (smartSpaceManager == null) { Log.i(TAG, "Cannot connect to SmartSpaceManager, it's null.") return@conflatedCallbackFlow } val session = smartSpaceManager.createSmartspaceSession( SmartspaceConfig.Builder(applicationContext, UNDERLAY_SURFACE).build() ) val smartSpaceListener = OnTargetsAvailableListener { targets -> val actions = targets .filter { target -> target.featureType == AMBIENT_ACTION_FEATURE } .filter { it.smartspaceTargetId == UNDERLAY_SURFACE } .flatMap { target -> target.actionChips } .map { chip -> ActionModel( icon = chip.icon?.loadDrawable(applicationContext) ?: applicationContext.getDrawable( R.drawable.clipboard )!!, intent = chip.intent, label = chip.title.toString(), attribution = chip.subtitle.toString(), ) } if (DEBUG) { Log.d(TAG, "SmartSpace OnTargetsAvailableListener $targets") Log.d(TAG, "SmartSpace actions $actions") } trySend(actions) } session.addOnTargetsAvailableListener(executor, smartSpaceListener) awaitClose { session.removeOnTargetsAvailableListener(smartSpaceListener) session.close() } } .stateIn( scope = backgroundScope, started = SharingStarted.WhileSubscribed(), initialValue = false, initialValue = emptyList(), ) val isOverlayVisible: MutableStateFlow<Boolean> = MutableStateFlow(false) override val isAttached: StateFlow<Boolean> = combine(actions, debugBroadcastFlow) { actions, createdViaBroadcast -> actions.isNotEmpty() || createdViaBroadcast } .stateIn( scope = backgroundScope, started = SharingStarted.WhileSubscribed(), initialValue = false, ) val actions: MutableStateFlow<List<ActionModel>> = MutableStateFlow(listOf()) override val isVisible: MutableStateFlow<Boolean> = MutableStateFlow(false) companion object { const val ACTION_CREATE_UNDERLAY = "com.systemui.underlay.action.CREATE_UNDERLAY" const val ACTION_DESTROY_UNDERLAY = "com.systemui.underlay.action.DESTROY_UNDERLAY" // Privately defined card type, exclusive for ambient actions @VisibleForTesting const val AMBIENT_ACTION_FEATURE = 72 // Surface that PCC wants to push cards into @VisibleForTesting const val UNDERLAY_SURFACE = "underlay" private const val TAG = "underlay" private const val DEBUG = false private const val ACTION_CREATE_UNDERLAY = "com.systemui.underlay.action.CREATE_UNDERLAY" private const val ACTION_DESTROY_UNDERLAY = "com.systemui.underlay.action.DESTROY_UNDERLAY" } } packages/SystemUI/src/com/android/systemui/underlay/domain/interactor/UnderlayInteractor.kt +3 −7 Original line number Diff line number Diff line Loading @@ -23,15 +23,11 @@ import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.update class UnderlayInteractor @Inject constructor(private val repository: UnderlayRepository) { val isUnderlayAttached: StateFlow<Boolean> = repository.isUnderlayAttached val isOverlayVisible: StateFlow<Boolean> = repository.isOverlayVisible val isUnderlayAttached: StateFlow<Boolean> = repository.isAttached val isOverlayVisible: StateFlow<Boolean> = repository.isVisible val actions: StateFlow<List<ActionModel>> = repository.actions fun setIsOverlayVisible(visible: Boolean) { repository.isOverlayVisible.update { visible } } fun setActions(actions: List<ActionModel>) { repository.actions.update { actions } repository.isVisible.update { visible } } } Loading
packages/SystemUI/multivalentTests/src/com/android/systemui/underlay/data/repository/UnderlayRepositoryTest.kt +107 −16 Original line number Diff line number Diff line /* * Copyright (C) 2020 The Android Open Source Project * Copyright (C) 2025 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. Loading @@ -16,49 +16,140 @@ package com.android.systemui.underlay.data.repository import android.content.Intent import android.app.smartspace.SmartspaceAction import android.app.smartspace.SmartspaceManager import android.app.smartspace.SmartspaceSession import android.app.smartspace.SmartspaceSession.OnTargetsAvailableListener import android.app.smartspace.SmartspaceTarget import android.content.testableContext import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.broadcast.broadcastDispatcher import com.android.systemui.concurrency.fakeExecutor import com.android.systemui.kosmos.advanceUntilIdle import com.android.systemui.kosmos.backgroundScope import com.android.systemui.kosmos.collectLastValue import com.android.systemui.kosmos.runCurrent import com.android.systemui.kosmos.runTest import com.android.systemui.testKosmos import com.android.systemui.underlay.data.repository.UnderlayRepositoryImpl.Companion.AMBIENT_ACTION_FEATURE import com.android.systemui.underlay.data.repository.UnderlayRepositoryImpl.Companion.UNDERLAY_SURFACE import com.google.common.truth.Truth.assertThat import org.junit.Test import org.junit.runner.RunWith import org.mockito.kotlin.any import org.mockito.kotlin.argumentCaptor import org.mockito.kotlin.doReturn import org.mockito.kotlin.mock import org.mockito.kotlin.verify @RunWith(AndroidJUnit4::class) @SmallTest class UnderlayRepositoryTest : SysuiTestCase() { private val kosmos = testKosmos() private val smartSpaceSession = mock<SmartspaceSession>() private val smartSpaceManager = mock<SmartspaceManager>() { on { createSmartspaceSession(any()) } doReturn smartSpaceSession } val onTargetsAvailableListenerCaptor = argumentCaptor<OnTargetsAvailableListener>() private val underTest = UnderlayRepositoryImpl( backgroundScope = kosmos.backgroundScope, broadcastDispatcher = kosmos.broadcastDispatcher, smartSpaceManager = smartSpaceManager, executor = kosmos.fakeExecutor, applicationContext = kosmos.testableContext, ) @Test fun isUnderlayAttached_whenCreated_true() = fun isUnderlayAttached_whenHasActions_true() = kosmos.runTest { val isUnderlayAttached by collectLastValue(underlayRepository.isUnderlayAttached) val isUnderlayAttached by collectLastValue(underTest.isAttached) runCurrent() verify(smartSpaceSession) .addOnTargetsAvailableListener(any(), onTargetsAvailableListenerCaptor.capture()) onTargetsAvailableListenerCaptor.firstValue.onTargetsAvailable(allTargets) advanceUntilIdle() assertThat(isUnderlayAttached).isTrue() } broadcastDispatcher.sendIntentToMatchingReceiversOnly( context, Intent(UnderlayRepository.ACTION_CREATE_UNDERLAY), @Test fun isUnderlayAttached_whenNoActions_false() = kosmos.runTest { val isUnderlayAttached by collectLastValue(underTest.isAttached) runCurrent() verify(smartSpaceSession) .addOnTargetsAvailableListener(any(), onTargetsAvailableListenerCaptor.capture()) onTargetsAvailableListenerCaptor.firstValue.onTargetsAvailable( listOf(invalidTarget1, invalidTarget2) ) assertThat(isUnderlayAttached).isTrue() advanceUntilIdle() assertThat(isUnderlayAttached).isFalse() } @Test fun isUnderlayAttached_whenDestroyed_false() = fun actions_whenHasSmartSpaceAction() = kosmos.runTest { val isUnderlayAttached by collectLastValue(underlayRepository.isUnderlayAttached) val actions by collectLastValue(underTest.actions) runCurrent() verify(smartSpaceSession) .addOnTargetsAvailableListener(any(), onTargetsAvailableListenerCaptor.capture()) onTargetsAvailableListenerCaptor.firstValue.onTargetsAvailable(allTargets) runCurrent() broadcastDispatcher.sendIntentToMatchingReceiversOnly( context, Intent(UnderlayRepository.ACTION_DESTROY_UNDERLAY), actions.let { requireNotNull(it) assertThat(it.size).isEqualTo(2) val firstAction = it.first() assertThat(firstAction.label).isEqualTo(TITLE_1) assertThat(firstAction.attribution).isEqualTo(SUBTITLE_1) val lastAction = it.last() assertThat(lastAction.label).isEqualTo(TITLE_2) assertThat(lastAction.attribution).isEqualTo(SUBTITLE_2) } } companion object { private const val TITLE_1 = "title 1" private const val TITLE_2 = "title 2" private const val SUBTITLE_1 = "subtitle 1" private const val SUBTITLE_2 = "subtitle 2" private val validTarget = mock<SmartspaceTarget> { on { featureType } doReturn AMBIENT_ACTION_FEATURE on { smartspaceTargetId } doReturn UNDERLAY_SURFACE on { actionChips } doReturn listOf( SmartspaceAction.Builder("action1-id", "title 1") .setSubtitle("subtitle 1") .build(), SmartspaceAction.Builder("action2-id", "title 2") .setSubtitle("subtitle 2") .build(), ) } assertThat(isUnderlayAttached).isFalse() private val invalidTarget1 = mock<SmartspaceTarget> { on { featureType } doReturn 1 on { smartspaceTargetId } doReturn UNDERLAY_SURFACE on { actionChips } doReturn listOf(SmartspaceAction.Builder("id", "title").setSubtitle("subtitle").build()) } private val invalidTarget2 = mock<SmartspaceTarget> { on { featureType } doReturn AMBIENT_ACTION_FEATURE on { smartspaceTargetId } doReturn "home" on { actionChips } doReturn listOf(SmartspaceAction.Builder("id", "title").setSubtitle("subtitle").build()) } private val allTargets = listOf(validTarget, invalidTarget1, invalidTarget2) } }
packages/SystemUI/multivalentTests/src/com/android/systemui/underlay/domain/interactor/UnderlayInteractorTest.kt +7 −25 Original line number Diff line number Diff line /* * Copyright (C) 2020 The Android Open Source Project * Copyright (C) 2025 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. Loading @@ -21,13 +21,12 @@ import android.content.applicationContext import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.broadcast.broadcastDispatcher import com.android.systemui.kosmos.collectLastValue import com.android.systemui.kosmos.runCurrent import com.android.systemui.kosmos.runTest import com.android.systemui.res.R import com.android.systemui.testKosmos import com.android.systemui.underlay.data.repository.UnderlayRepository import com.android.systemui.underlay.data.repository.fake import com.android.systemui.underlay.data.repository.underlayRepository import com.android.systemui.underlay.shared.model.ActionModel import com.google.common.truth.Truth.assertThat import org.junit.Test Loading @@ -42,13 +41,7 @@ class UnderlayInteractorTest : SysuiTestCase() { fun isUnderlayAttached_whenCreated_true() = kosmos.runTest { val isUnderlayAttached by collectLastValue(underlayInteractor.isUnderlayAttached) runCurrent() broadcastDispatcher.sendIntentToMatchingReceiversOnly( context, Intent(UnderlayRepository.ACTION_CREATE_UNDERLAY), ) underlayRepository.fake.setIsUnderlayAttached(true) assertThat(isUnderlayAttached).isTrue() } Loading @@ -56,13 +49,7 @@ class UnderlayInteractorTest : SysuiTestCase() { fun isUnderlayAttached_whenDestroyed_false() = kosmos.runTest { val isUnderlayAttached by collectLastValue(underlayInteractor.isUnderlayAttached) runCurrent() broadcastDispatcher.sendIntentToMatchingReceiversOnly( context, Intent(UnderlayRepository.ACTION_DESTROY_UNDERLAY), ) underlayRepository.fake.setIsUnderlayAttached(false) assertThat(isUnderlayAttached).isFalse() } Loading @@ -70,9 +57,7 @@ class UnderlayInteractorTest : SysuiTestCase() { fun isOverlayVisible_setTrue_true() = kosmos.runTest { val isOverlayVisible by collectLastValue(underlayInteractor.isOverlayVisible) underlayInteractor.setIsOverlayVisible(true) assertThat(isOverlayVisible).isTrue() } Loading @@ -80,9 +65,7 @@ class UnderlayInteractorTest : SysuiTestCase() { fun isOverlayVisible_setFalse_False() = kosmos.runTest { val isOverlayVisible by collectLastValue(underlayInteractor.isOverlayVisible) underlayInteractor.setIsOverlayVisible(false) assertThat(isOverlayVisible).isFalse() } Loading @@ -100,11 +83,10 @@ class UnderlayInteractorTest : SysuiTestCase() { ), label = "Sunday Morning", attribution = null, intent = Intent(), ) ) underlayInteractor.setActions(testActions) underlayRepository.fake.setActions(testActions) assertThat(actions).isEqualTo(testActions) } }
packages/SystemUI/src/com/android/systemui/underlay/UnderlayModule.kt +4 −0 Original line number Diff line number Diff line Loading @@ -17,6 +17,8 @@ package com.android.systemui.underlay import com.android.systemui.CoreStartable import com.android.systemui.underlay.data.repository.UnderlayRepository import com.android.systemui.underlay.data.repository.UnderlayRepositoryImpl import com.android.systemui.underlay.ui.startable.UnderlayCoreStartable import dagger.Binds import dagger.Module Loading @@ -30,4 +32,6 @@ interface UnderlayModule { @IntoMap @ClassKey(UnderlayCoreStartable::class) fun bindUnderlayCoreStartable(startable: UnderlayCoreStartable): CoreStartable @Binds fun bindsUnderlayRepository(impl: UnderlayRepositoryImpl): UnderlayRepository }
packages/SystemUI/src/com/android/systemui/underlay/data/repository/UnderlayRepository.kt +100 −10 Original line number Diff line number Diff line Loading @@ -16,28 +16,57 @@ package com.android.systemui.underlay.data.repository import android.app.smartspace.SmartspaceConfig import android.app.smartspace.SmartspaceManager import android.app.smartspace.SmartspaceSession.OnTargetsAvailableListener import android.content.Context import android.content.IntentFilter import android.util.Log import androidx.annotation.VisibleForTesting import com.android.systemui.broadcast.BroadcastDispatcher import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.res.R import com.android.systemui.underlay.shared.model.ActionModel import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow import java.util.concurrent.Executor import javax.inject.Inject import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.stateIn /** Source of truth for ambient actions and visibility of their system space. */ interface UnderlayRepository { /** Chips that should be visible on the UI. */ val actions: StateFlow<List<ActionModel>> /** If window should be added to the navbar area or not. */ val isAttached: StateFlow<Boolean> /** If hint (or chips list) should be visible. */ val isVisible: MutableStateFlow<Boolean> } @SysUISingleton class UnderlayRepository class UnderlayRepositoryImpl @Inject constructor( @Background private val backgroundScope: CoroutineScope, broadcastDispatcher: BroadcastDispatcher, ) { val isUnderlayAttached: StateFlow<Boolean> = broadcastDispatcher .broadcastFlow( private val smartSpaceManager: SmartspaceManager?, @Background executor: Executor, @Application applicationContext: Context, ) : UnderlayRepository { private val debugBroadcastFlow: Flow<Boolean> = if (DEBUG) { broadcastDispatcher.broadcastFlow( filter = IntentFilter().apply { addAction(ACTION_CREATE_UNDERLAY) Loading @@ -46,18 +75,79 @@ constructor( ) { intent, _ -> intent.action == ACTION_CREATE_UNDERLAY } } else { MutableStateFlow(false).asStateFlow() } override val actions: StateFlow<List<ActionModel>> = conflatedCallbackFlow { if (smartSpaceManager == null) { Log.i(TAG, "Cannot connect to SmartSpaceManager, it's null.") return@conflatedCallbackFlow } val session = smartSpaceManager.createSmartspaceSession( SmartspaceConfig.Builder(applicationContext, UNDERLAY_SURFACE).build() ) val smartSpaceListener = OnTargetsAvailableListener { targets -> val actions = targets .filter { target -> target.featureType == AMBIENT_ACTION_FEATURE } .filter { it.smartspaceTargetId == UNDERLAY_SURFACE } .flatMap { target -> target.actionChips } .map { chip -> ActionModel( icon = chip.icon?.loadDrawable(applicationContext) ?: applicationContext.getDrawable( R.drawable.clipboard )!!, intent = chip.intent, label = chip.title.toString(), attribution = chip.subtitle.toString(), ) } if (DEBUG) { Log.d(TAG, "SmartSpace OnTargetsAvailableListener $targets") Log.d(TAG, "SmartSpace actions $actions") } trySend(actions) } session.addOnTargetsAvailableListener(executor, smartSpaceListener) awaitClose { session.removeOnTargetsAvailableListener(smartSpaceListener) session.close() } } .stateIn( scope = backgroundScope, started = SharingStarted.WhileSubscribed(), initialValue = false, initialValue = emptyList(), ) val isOverlayVisible: MutableStateFlow<Boolean> = MutableStateFlow(false) override val isAttached: StateFlow<Boolean> = combine(actions, debugBroadcastFlow) { actions, createdViaBroadcast -> actions.isNotEmpty() || createdViaBroadcast } .stateIn( scope = backgroundScope, started = SharingStarted.WhileSubscribed(), initialValue = false, ) val actions: MutableStateFlow<List<ActionModel>> = MutableStateFlow(listOf()) override val isVisible: MutableStateFlow<Boolean> = MutableStateFlow(false) companion object { const val ACTION_CREATE_UNDERLAY = "com.systemui.underlay.action.CREATE_UNDERLAY" const val ACTION_DESTROY_UNDERLAY = "com.systemui.underlay.action.DESTROY_UNDERLAY" // Privately defined card type, exclusive for ambient actions @VisibleForTesting const val AMBIENT_ACTION_FEATURE = 72 // Surface that PCC wants to push cards into @VisibleForTesting const val UNDERLAY_SURFACE = "underlay" private const val TAG = "underlay" private const val DEBUG = false private const val ACTION_CREATE_UNDERLAY = "com.systemui.underlay.action.CREATE_UNDERLAY" private const val ACTION_DESTROY_UNDERLAY = "com.systemui.underlay.action.DESTROY_UNDERLAY" } }
packages/SystemUI/src/com/android/systemui/underlay/domain/interactor/UnderlayInteractor.kt +3 −7 Original line number Diff line number Diff line Loading @@ -23,15 +23,11 @@ import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.update class UnderlayInteractor @Inject constructor(private val repository: UnderlayRepository) { val isUnderlayAttached: StateFlow<Boolean> = repository.isUnderlayAttached val isOverlayVisible: StateFlow<Boolean> = repository.isOverlayVisible val isUnderlayAttached: StateFlow<Boolean> = repository.isAttached val isOverlayVisible: StateFlow<Boolean> = repository.isVisible val actions: StateFlow<List<ActionModel>> = repository.actions fun setIsOverlayVisible(visible: Boolean) { repository.isOverlayVisible.update { visible } } fun setActions(actions: List<ActionModel>) { repository.actions.update { actions } repository.isVisible.update { visible } } }