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

Commit 6bcabf7e authored by Anton Potapov's avatar Anton Potapov
Browse files

Remove TileServiceManager dependency from

CustomTilePackageUpdatesRepository

Flag: aconfig qs_new_tiles DISABLED
Test: atest CustomTilePackageUpdatesRepository
Bug: 301055700
Change-Id: Id034592bfc20361d1c097fc3c9cf632b7140c67d
parent 992170bd
Loading
Loading
Loading
Loading
+70 −28
Original line number Diff line number Diff line
@@ -16,23 +16,27 @@

package com.android.systemui.qs.tiles.impl.custom

import android.annotation.SuppressLint
import android.content.BroadcastReceiver
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.os.UserHandle
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.qs.external.TileLifecycleManager
import com.android.systemui.qs.external.TileServiceManager
import com.android.systemui.coroutines.collectValues
import com.android.systemui.qs.pipeline.shared.TileSpec
import com.android.systemui.qs.tiles.impl.custom.data.repository.CustomTilePackageUpdatesRepository
import com.android.systemui.qs.tiles.impl.custom.data.repository.CustomTilePackageUpdatesRepositoryImpl
import com.android.systemui.qs.tiles.impl.custom.data.repository.FakeCustomTileDefaultsRepository
import com.android.systemui.qs.tiles.impl.custom.data.repository.FakeCustomTileDefaultsRepository.DefaultsRequest
import com.android.systemui.util.mockito.any
import com.android.systemui.util.mockito.capture
import com.android.systemui.util.mockito.eq
import com.android.systemui.util.mockito.nullable
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.test.StandardTestDispatcher
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.runCurrent
@@ -49,14 +53,12 @@ import org.mockito.MockitoAnnotations
@OptIn(ExperimentalCoroutinesApi::class)
@SmallTest
@RunWith(AndroidJUnit4::class)
@SuppressLint("UnspecifiedRegisterReceiverFlag") // Not needed in the test
class CustomTilePackageUpdatesRepositoryTest : SysuiTestCase() {

    @Mock private lateinit var tileServiceManager: TileServiceManager
    @Mock private lateinit var mockedContext: Context
    @Captor private lateinit var listenerCaptor: ArgumentCaptor<BroadcastReceiver>

    @Captor
    private lateinit var listenerCaptor: ArgumentCaptor<TileLifecycleManager.TileChangeListener>

    private val defaultsRepository = FakeCustomTileDefaultsRepository()
    private val testDispatcher = StandardTestDispatcher()
    private val testScope = TestScope(testDispatcher)

@@ -69,37 +71,29 @@ class CustomTilePackageUpdatesRepositoryTest : SysuiTestCase() {
        underTest =
            CustomTilePackageUpdatesRepositoryImpl(
                TileSpec.create(COMPONENT_1),
                USER,
                tileServiceManager,
                defaultsRepository,
                mockedContext,
                testScope.backgroundScope,
                testDispatcher,
            )
    }

    @Test
    fun packageChangesUpdatesDefaults() =
    fun packageChangesEmittedForTilePassing() =
        testScope.runTest {
            val events = mutableListOf<Unit>()
            underTest.packageChanges.onEach { events.add(it) }.launchIn(backgroundScope)
            val events by collectValues(underTest.getPackageChangesForUser(USER_1))
            runCurrent()
            verify(tileServiceManager).setTileChangeListener(capture(listenerCaptor))

            emitPackageChange()
            emitPackageChange(COMPONENT_1)
            runCurrent()

            assertThat(events).hasSize(1)
            assertThat(defaultsRepository.defaultsRequests).isNotEmpty()
            assertThat(defaultsRepository.defaultsRequests.last())
                .isEqualTo(DefaultsRequest(USER, COMPONENT_1, true))
        }

    @Test
    fun packageChangesEmittedOnlyForTheTile() =
    fun packageChangesEmittedForAnotherTileIgnored() =
        testScope.runTest {
            val events = mutableListOf<Unit>()
            underTest.packageChanges.onEach { events.add(it) }.launchIn(backgroundScope)
            val events by collectValues(underTest.getPackageChangesForUser(USER_1))
            runCurrent()
            verify(tileServiceManager).setTileChangeListener(capture(listenerCaptor))

            emitPackageChange(COMPONENT_2)
            runCurrent()
@@ -107,12 +101,60 @@ class CustomTilePackageUpdatesRepositoryTest : SysuiTestCase() {
            assertThat(events).isEmpty()
        }

    private fun emitPackageChange(componentName: ComponentName = COMPONENT_1) {
        listenerCaptor.value.onTileChanged(componentName)
    @Test
    fun unsupportedActionDoesntEmmit() =
        testScope.runTest {
            val events by collectValues(underTest.getPackageChangesForUser(USER_1))
            runCurrent()

            verify(mockedContext)
                .registerReceiverAsUser(
                    capture(listenerCaptor),
                    any(),
                    any(),
                    nullable(),
                    nullable()
                )
            listenerCaptor.value.onReceive(mockedContext, Intent(Intent.ACTION_MAIN))
            runCurrent()

            assertThat(events).isEmpty()
        }

    @Test
    fun cachesCallsPerUser() =
        testScope.runTest {
            underTest.getPackageChangesForUser(USER_1).launchIn(backgroundScope)
            underTest.getPackageChangesForUser(USER_1).launchIn(backgroundScope)
            underTest.getPackageChangesForUser(USER_2).launchIn(backgroundScope)
            underTest.getPackageChangesForUser(USER_2).launchIn(backgroundScope)
            runCurrent()

            // Register receiver once per each user
            verify(mockedContext)
                .registerReceiverAsUser(any(), eq(USER_1), any(), nullable(), nullable())
            verify(mockedContext)
                .registerReceiverAsUser(any(), eq(USER_2), any(), nullable(), nullable())
        }

    private fun emitPackageChange(componentName: ComponentName) {
        verify(mockedContext)
            .registerReceiverAsUser(capture(listenerCaptor), any(), any(), nullable(), nullable())
        listenerCaptor.value.onReceive(
            mockedContext,
            Intent(Intent.ACTION_PACKAGE_CHANGED).apply {
                type = IntentFilter.SCHEME_PACKAGE
                putExtra(
                    Intent.EXTRA_CHANGED_COMPONENT_NAME_LIST,
                    arrayOf(componentName.packageName)
                )
            }
        )
    }

    private companion object {
        val USER = UserHandle(0)
        val USER_1 = UserHandle(1)
        val USER_2 = UserHandle(2)
        val COMPONENT_1 = ComponentName("pkg.test.1", "cls.test")
        val COMPONENT_2 = ComponentName("pkg.test.2", "cls.test")
    }
+31 −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.qs.tiles.base.viewmodel

import com.android.systemui.dagger.qualifiers.Application
import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.SupervisorJob

/** Creates a [CoroutineScope] for the [QSTileViewModelImpl]. */
class QSTileCoroutineScopeFactory
@Inject
constructor(@Application private val applicationScope: CoroutineScope) {

    fun create(): CoroutineScope =
        CoroutineScope(applicationScope.coroutineContext + SupervisorJob())
}
+3 −0
Original line number Diff line number Diff line
@@ -82,6 +82,7 @@ sealed interface QSTileViewModelFactory<T> {
                qsTileLogger,
                systemClock,
                backgroundDispatcher,
                component.coroutineScope(),
            )
        }
    }
@@ -101,6 +102,7 @@ sealed interface QSTileViewModelFactory<T> {
        private val qsTileConfigProvider: QSTileConfigProvider,
        private val systemClock: SystemClock,
        @Background private val backgroundDispatcher: CoroutineDispatcher,
        private val coroutineScopeFactory: QSTileCoroutineScopeFactory,
    ) : QSTileViewModelFactory<T> {

        /**
@@ -130,6 +132,7 @@ sealed interface QSTileViewModelFactory<T> {
                qsTileLogger,
                systemClock,
                backgroundDispatcher,
                coroutineScopeFactory.create(),
            )
    }
}
+1 −2
Original line number Diff line number Diff line
@@ -39,7 +39,6 @@ import java.io.PrintWriter
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancel
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow
@@ -82,7 +81,7 @@ class QSTileViewModelImpl<DATA_TYPE>(
    private val qsTileLogger: QSTileLogger,
    private val systemClock: SystemClock,
    private val backgroundDispatcher: CoroutineDispatcher,
    private val tileScope: CoroutineScope = CoroutineScope(SupervisorJob()),
    private val tileScope: CoroutineScope,
) : QSTileViewModel, Dumpable {

    private val users: MutableStateFlow<UserHandle> =
+75 −18
Original line number Diff line number Diff line
@@ -16,46 +16,103 @@

package com.android.systemui.qs.tiles.impl.custom.data.repository

import android.annotation.SuppressLint
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.os.UserHandle
import androidx.annotation.GuardedBy
import com.android.systemui.common.coroutine.ConflatedCallbackFlow
import com.android.systemui.qs.external.TileServiceManager
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.qs.pipeline.shared.TileSpec
import com.android.systemui.qs.tiles.impl.custom.di.bound.CustomTileBoundScope
import com.android.systemui.qs.tiles.impl.custom.di.bound.CustomTileUser
import com.android.systemui.qs.tiles.impl.di.QSTileScope
import javax.inject.Inject
import kotlin.coroutines.CoroutineContext
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onCompletion
import kotlinx.coroutines.flow.shareIn
import kotlinx.coroutines.launch

interface CustomTilePackageUpdatesRepository {

    val packageChanges: Flow<Unit>
    fun getPackageChangesForUser(user: UserHandle): Flow<Unit>
}

@CustomTileBoundScope
@QSTileScope
class CustomTilePackageUpdatesRepositoryImpl
@Inject
constructor(
    tileSpec: TileSpec.CustomTileSpec,
    @CustomTileUser user: UserHandle,
    serviceManager: TileServiceManager,
    defaultsRepository: CustomTileDefaultsRepository,
    @CustomTileBoundScope boundScope: CoroutineScope,
    private val tileSpec: TileSpec.CustomTileSpec,
    @Application private val context: Context,
    @QSTileScope private val tileScope: CoroutineScope,
    @Background private val backgroundCoroutineContext: CoroutineContext,
) : CustomTilePackageUpdatesRepository {

    override val packageChanges: Flow<Unit> =
    @GuardedBy("perUserCache")
    private val perUserCache: MutableMap<UserHandle, Flow<Unit>> = mutableMapOf()

    override fun getPackageChangesForUser(user: UserHandle): Flow<Unit> =
        synchronized(perUserCache) {
            perUserCache.getOrPut(user) {
                createPackageChangesFlowForUser(user)
                    .onCompletion {
                        // clear cache when nobody listens
                        synchronized(perUserCache) { perUserCache.remove(user) }
                    }
                    .flowOn(backgroundCoroutineContext)
                    .shareIn(tileScope, SharingStarted.WhileSubscribed())
            }
        }

    @SuppressLint(
        "MissingPermission", // android.permission.INTERACT_ACROSS_USERS_FULL
        "UnspecifiedRegisterReceiverFlag",
        "RegisterReceiverViaContext",
    )
    private fun createPackageChangesFlowForUser(user: UserHandle): Flow<Unit> =
        ConflatedCallbackFlow.conflatedCallbackFlow {
                serviceManager.setTileChangeListener { changedComponentName ->
                    if (changedComponentName == tileSpec.componentName) {
                        trySend(Unit)
                val receiver =
                    object : BroadcastReceiver() {
                        override fun onReceive(context: Context?, intent: Intent?) {
                            launch { send(intent) }
                        }
                    }
                context.registerReceiverAsUser(
                    receiver,
                    user,
                    INTENT_FILTER,
                    /* broadcastPermission = */ null,
                    /* scheduler = */ null,
                )

                awaitClose { context.unregisterReceiver(receiver) }
            }
            .filter { intent ->
                intent ?: return@filter false
                if (intent.action?.let(INTENT_FILTER::matchAction) != true) {
                    return@filter false
                }
                val changedComponentNames =
                    intent.getStringArrayExtra(Intent.EXTRA_CHANGED_COMPONENT_NAME_LIST)
                changedComponentNames?.contains(tileSpec.componentName.packageName) == true
            }
            .map {}

                awaitClose { serviceManager.setTileChangeListener(null) }
    private companion object {

        val INTENT_FILTER =
            IntentFilter().apply {
                addDataScheme(IntentFilter.SCHEME_PACKAGE)

                addAction(Intent.ACTION_PACKAGE_CHANGED)
            }
    }
            .onEach { defaultsRepository.requestNewDefaults(user, tileSpec.componentName, true) }
            .shareIn(boundScope, SharingStarted.WhileSubscribed())
}
Loading