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

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

Merge "Pause notify quick settings tiles change until...

Merge "Pause notify quick settings tiles change until AccessibilityManagerService finish initialize the user" into main
parents b8b15913 7d964c15
Loading
Loading
Loading
Loading
+3 −0
Original line number Diff line number Diff line
@@ -28,6 +28,9 @@ oneway interface IUserInitializationCompleteCallback {
    /**
     * Called when a user initialization completes.
     *
     * If the user has been initialized completely at the time the caller register the callback,
     * the caller will receive the userInitializationComplete immediately.
     *
     * @param userId the id of the initialized user
     */
    @RequiresNoPermission
+3 −0
Original line number Diff line number Diff line
@@ -26,6 +26,7 @@ import com.android.systemui.kosmos.collectLastValue
import com.android.systemui.kosmos.runTest
import com.android.systemui.kosmos.testDispatcher
import com.android.systemui.kosmos.testScope
import com.android.systemui.log.core.FakeLogBuffer
import com.android.systemui.testKosmos
import com.android.systemui.util.settings.fakeSettings
import com.google.common.truth.Truth.assertThat
@@ -69,6 +70,8 @@ class AccessibilityQsShortcutsRepositoryImplTest : SysuiTestCase() {
                a11yManager,
                userA11yQsShortcutsRepositoryFactory,
                kosmos.testDispatcher,
                kosmos.testScope.backgroundScope,
                FakeLogBuffer.Factory.create(),
            )
    }

+112 −3
Original line number Diff line number Diff line
@@ -22,10 +22,17 @@ import android.content.Context
import android.text.TextUtils
import android.util.SparseArray
import android.view.accessibility.AccessibilityManager
import android.view.accessibility.IUserInitializationCompleteCallback
import androidx.annotation.GuardedBy
import com.android.app.tracing.coroutines.asyncTraced as async
import com.android.internal.accessibility.AccessibilityShortcutController
import com.android.server.accessibility.Flags
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.kairos.awaitClose
import com.android.systemui.log.LogBuffer
import com.android.systemui.log.core.LogLevel
import com.android.systemui.log.dagger.QSLog
import com.android.systemui.qs.pipeline.shared.TileSpec
import com.android.systemui.qs.tiles.ColorCorrectionTile
import com.android.systemui.qs.tiles.ColorInversionTile
@@ -33,12 +40,20 @@ import com.android.systemui.qs.tiles.FontScalingTile
import com.android.systemui.qs.tiles.HearingDevicesTile
import com.android.systemui.qs.tiles.OneHandedModeTile
import com.android.systemui.qs.tiles.ReduceBrightColorsTile
import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow
import javax.inject.Inject
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Deferred
import com.android.app.tracing.coroutines.asyncTraced as async
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext

/** Provides data related to accessibility quick setting shortcut option. */
@@ -60,6 +75,8 @@ constructor(
    private val manager: AccessibilityManager,
    private val userA11yQsShortcutsRepositoryFactory: UserA11yQsShortcutsRepository.Factory,
    @Background private val backgroundDispatcher: CoroutineDispatcher,
    @Background private val applicationScope: CoroutineScope,
    @QSLog private val logBuffer: LogBuffer,
) : AccessibilityQsShortcutsRepository {
    companion object {
        val TILE_SPEC_TO_COMPONENT_MAPPING =
@@ -76,8 +93,63 @@ constructor(
                FontScalingTile.TILE_SPEC to
                    AccessibilityShortcutController.FONT_SIZE_TILE_COMPONENT_NAME,
                HearingDevicesTile.TILE_SPEC to
                    AccessibilityShortcutController.ACCESSIBILITY_HEARING_AIDS_TILE_COMPONENT_NAME
                    AccessibilityShortcutController.ACCESSIBILITY_HEARING_AIDS_TILE_COMPONENT_NAME,
            )

        const val LOG_TAG = "AccessibilityQsShortcutsRepository"
    }

    private val a11yUserInitializationCompleteState: StateFlow<Int?> =
        conflatedCallbackFlow {
                val callback =
                    object : IUserInitializationCompleteCallback.Stub() {
                        override fun onUserInitializationComplete(userId: Int) {
                            logBuffer.log(
                                LOG_TAG,
                                LogLevel.DEBUG,
                                { int1 = userId },
                                { "onUserInitializationComplete userId = $int1" },
                            )
                            trySend(userId)
                        }
                    }
                manager.registerUserInitializationCompleteCallback(callback)

                awaitClose { manager.unregisterUserInitializationCompleteCallback(callback) }
            }
            .flowOn(backgroundDispatcher)
            .stateIn(
                applicationScope,
                SharingStarted.WhileSubscribed(replayExpirationMillis = 0L),
                null,
            )

    private val pendingExecution: MutableStateFlow<Pair<Context, List<TileSpec>>?> =
        MutableStateFlow(null)

    init {
        if (Flags.notifyQsTileChangedAfterUserInitialization()) {
            applicationScope.launch(context = backgroundDispatcher) {
                a11yUserInitializationCompleteState.collectLatest { a11yUserId ->
                    val (userContext, tiles) = pendingExecution.value ?: return@collectLatest
                    logBuffer.log(
                        LOG_TAG,
                        LogLevel.DEBUG,
                        {
                            int1 = userContext.userId
                            str1 = "$a11yUserId"
                        },
                        {
                            "observedUserInitializationComplete: " +
                                "pendingUserContext:userId $int1, a11yUserId: $str1"
                        },
                    )
                    if (userContext.userId == a11yUserId) {
                        notifyAccessibilityManagerTilesChanged(userContext, tiles)
                    }
                }
            }
        }
    }

    @GuardedBy("userA11yQsShortcutsRepositories")
@@ -97,8 +169,43 @@ constructor(
    @SuppressLint("MissingPermission") // android.permission.STATUS_BAR_SERVICE
    override suspend fun notifyAccessibilityManagerTilesChanged(
        userContext: Context,
        tiles: List<TileSpec>
        tiles: List<TileSpec>,
    ) {

        logBuffer.log(
            LOG_TAG,
            LogLevel.DEBUG,
            {
                int1 = userContext.userId
                str1 = "$tiles"
            },
            { "notifyAccessibilityManagerTilesChanged(userId= $int1, tiles= $str1" },
        )

        if (Flags.notifyQsTileChangedAfterUserInitialization()) {
            if (tiles.isEmpty()) {
                // There is always at least one tile in the QS Panel.
                return
            }
            if (a11yUserInitializationCompleteState.value != userContext.userId) {
                logBuffer.log(
                    LOG_TAG,
                    LogLevel.DEBUG,
                    {
                        int1 = userContext.userId
                        str1 = "${a11yUserInitializationCompleteState.value}"
                    },
                    {
                        "userNotInitializedYet: pending process. " +
                            "sysUiUserId= $int1, a11yUserId= $str1"
                    },
                )
                pendingExecution.emit(Pair(userContext, tiles))
                return
            }

            pendingExecution.emit(null)
        }
        val newTiles = mutableListOf<ComponentName>()
        val accessibilityTileServices = getAccessibilityTileServices(userContext)
        tiles.forEach { tileSpec ->
@@ -108,11 +215,13 @@ constructor(
                        newTiles.add(tileSpec.componentName)
                    }
                }

                is TileSpec.PlatformTileSpec -> {
                    if (TILE_SPEC_TO_COMPONENT_MAPPING.containsKey(tileSpec.spec)) {
                        newTiles.add(TILE_SPEC_TO_COMPONENT_MAPPING[tileSpec.spec]!!)
                    }
                }

                TileSpec.Invalid -> {
                    // do nothing
                }
+90 −12
Original line number Diff line number Diff line
@@ -20,11 +20,17 @@ import android.accessibilityservice.AccessibilityServiceInfo
import android.content.ComponentName
import android.content.pm.ResolveInfo
import android.content.pm.ServiceInfo
import android.platform.test.annotations.DisableFlags
import android.platform.test.annotations.EnableFlags
import android.platform.test.flag.junit.SetFlagsRule
import android.view.accessibility.AccessibilityManager
import android.view.accessibility.IUserInitializationCompleteCallback
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.internal.accessibility.AccessibilityShortcutController
import com.android.server.accessibility.Flags
import com.android.systemui.SysuiTestCase
import com.android.systemui.log.core.FakeLogBuffer
import com.android.systemui.qs.pipeline.shared.TileSpec
import com.android.systemui.qs.tiles.ColorCorrectionTile
import com.android.systemui.qs.tiles.ColorInversionTile
@@ -32,14 +38,16 @@ import com.android.systemui.qs.tiles.FontScalingTile
import com.android.systemui.qs.tiles.HearingDevicesTile
import com.android.systemui.qs.tiles.OneHandedModeTile
import com.android.systemui.qs.tiles.ReduceBrightColorsTile
import com.android.systemui.util.mockito.whenever
import com.android.systemui.util.settings.FakeSettings
import com.android.systemui.utils.FieldSetter
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.cancel
import kotlinx.coroutines.test.StandardTestDispatcher
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.runCurrent
import kotlinx.coroutines.test.runTest
import org.junit.After
import org.junit.Before
import org.junit.Rule
import org.junit.Test
@@ -48,22 +56,28 @@ import org.mockito.Mock
import org.mockito.Mockito
import org.mockito.junit.MockitoJUnit
import org.mockito.junit.MockitoRule
import org.mockito.kotlin.any
import org.mockito.kotlin.doAnswer
import org.mockito.kotlin.whenever

/**
 * Unit tests for AccessibilityQsShortcutsRepositoryImpl that requires a device. For example, we
 * can't mock the AccessibilityShortcutInfo for test. MultiValentTest doesn't compile when using
 * newly introduced methods and constants.
 */
@OptIn(ExperimentalCoroutinesApi::class)
@SmallTest
@RunWith(AndroidJUnit4::class)
class AccessibilityQsShortcutsRepositoryImplForDeviceTest : SysuiTestCase() {
    @Rule @JvmField val mockitoRule: MockitoRule = MockitoJUnit.rule()
    @get:Rule val setFlagsRule: SetFlagsRule = SetFlagsRule()

    // mocks
    @Mock private lateinit var a11yManager: AccessibilityManager
    private var userInitializationCallback: IUserInitializationCompleteCallback? = null
    private val testDispatcher = StandardTestDispatcher()
    private val testScope = TestScope(testDispatcher)
    private val secureSettings = FakeSettings()
    private val secureSettings = FakeSettings(testDispatcher)

    private val userA11yQsShortcutsRepositoryFactory =
        object : UserA11yQsShortcutsRepository.Factory {
@@ -81,12 +95,29 @@ class AccessibilityQsShortcutsRepositoryImplForDeviceTest : SysuiTestCase() {

    @Before
    fun setUp() {
        // Use doAnswer to define behavior based on the input 'user'
        doAnswer { invocation ->
                // Get the User object passed as the first argument
                userInitializationCallback =
                    invocation.arguments[0] as IUserInitializationCompleteCallback
            }
            .whenever(a11yManager)
            .registerUserInitializationCompleteCallback(any())
        underTest =
            AccessibilityQsShortcutsRepositoryImpl(
                a11yManager,
                userA11yQsShortcutsRepositoryFactory,
                testDispatcher
                testDispatcher,
                testScope.backgroundScope,
                FakeLogBuffer.Factory.create(),
            )

        userInitializationCallback?.onUserInitializationComplete(context.userId)
    }

    @After
    fun cleanUp() {
        testScope.cancel()
    }

    @Test
@@ -112,6 +143,53 @@ class AccessibilityQsShortcutsRepositoryImplForDeviceTest : SysuiTestCase() {
            )
    }

    @DisableFlags(Flags.FLAG_NOTIFY_QS_TILE_CHANGED_AFTER_USER_INITIALIZATION)
    @Test
    fun initRepository_doesNotRegisterUserInitializationCallback() =
        testScope.runTest {
            runCurrent()

            assertThat(userInitializationCallback).isNull()
        }

    @EnableFlags(Flags.FLAG_NOTIFY_QS_TILE_CHANGED_AFTER_USER_INITIALIZATION)
    @Test
    fun initRepository_registeredUserInitializationCallback() =
        testScope.runTest {
            runCurrent()

            assertThat(userInitializationCallback).isNotNull()
        }

    @EnableFlags(Flags.FLAG_NOTIFY_QS_TILE_CHANGED_AFTER_USER_INITIALIZATION)
    @Test
    fun notifyAccessibilityManagerTilesChanged_notifyOnlyWhenUserInitializationComplete() =
        testScope.runTest {
            // Change completedUser
            userInitializationCallback?.onUserInitializationComplete(context.userId + 1)
            runCurrent()

            val changedTiles = listOf(TileSpec.create(ColorInversionTile.TILE_SPEC))

            underTest.notifyAccessibilityManagerTilesChanged(context, changedTiles)
            runCurrent()

            // Should not notify, because the user is not initialized
            Mockito.verify(a11yManager, Mockito.times(0))
                .notifyQuickSettingsTilesChanged(any(), any())

            // Change completedUser
            userInitializationCallback?.onUserInitializationComplete(context.userId)
            runCurrent()

            // Flush previous pending execution
            Mockito.verify(a11yManager, Mockito.times(1))
                .notifyQuickSettingsTilesChanged(
                    context.userId,
                    listOf(AccessibilityShortcutController.COLOR_INVERSION_TILE_COMPONENT_NAME),
                )
        }

    @Test
    fun notifyAccessibilityManagerTilesChanged_customTiles_onlyNotifyA11yTileServices() =
        testScope.runTest {
@@ -122,7 +200,7 @@ class AccessibilityQsShortcutsRepositoryImplForDeviceTest : SysuiTestCase() {
            val a11yShortcutTileService =
                ComponentName(
                    mContext.packageName,
                    "com.android.systemui.accessibility.TileService"
                    "com.android.systemui.accessibility.TileService",
                )
            setupInstalledAccessibilityShortcutTargets()
            // Other custom tile service that isn't linked to an accessibility feature
@@ -132,7 +210,7 @@ class AccessibilityQsShortcutsRepositoryImplForDeviceTest : SysuiTestCase() {
                listOf(
                    TileSpec.create(a11yServiceTileService),
                    TileSpec.create(a11yShortcutTileService),
                    TileSpec.create(nonA11yTileService)
                    TileSpec.create(nonA11yTileService),
                )

            underTest.notifyAccessibilityManagerTilesChanged(context, changedTiles)
@@ -141,7 +219,7 @@ class AccessibilityQsShortcutsRepositoryImplForDeviceTest : SysuiTestCase() {
            Mockito.verify(a11yManager, Mockito.times(1))
                .notifyQuickSettingsTilesChanged(
                    context.userId,
                    listOf(a11yServiceTileService, a11yShortcutTileService)
                    listOf(a11yServiceTileService, a11yShortcutTileService),
                )
        }

@@ -166,7 +244,7 @@ class AccessibilityQsShortcutsRepositoryImplForDeviceTest : SysuiTestCase() {
                    TileSpec.create(ColorInversionTile.TILE_SPEC),
                    TileSpec.create(OneHandedModeTile.TILE_SPEC),
                    TileSpec.create(ReduceBrightColorsTile.TILE_SPEC),
                    TileSpec.create(FontScalingTile.TILE_SPEC)
                    TileSpec.create(FontScalingTile.TILE_SPEC),
                )

            underTest.notifyAccessibilityManagerTilesChanged(context, changedTiles)
@@ -181,8 +259,8 @@ class AccessibilityQsShortcutsRepositoryImplForDeviceTest : SysuiTestCase() {
                        AccessibilityShortcutController.ONE_HANDED_TILE_COMPONENT_NAME,
                        AccessibilityShortcutController
                            .REDUCE_BRIGHT_COLORS_TILE_SERVICE_COMPONENT_NAME,
                        AccessibilityShortcutController.FONT_SIZE_TILE_COMPONENT_NAME
                    )
                        AccessibilityShortcutController.FONT_SIZE_TILE_COMPONENT_NAME,
                    ),
                )
        }

@@ -203,7 +281,7 @@ class AccessibilityQsShortcutsRepositoryImplForDeviceTest : SysuiTestCase() {
                listOf(
                    createFakeAccessibilityServiceInfo(
                        tileService.packageName,
                        tileService.className
                        tileService.className,
                    )
                )
            )
@@ -211,7 +289,7 @@ class AccessibilityQsShortcutsRepositoryImplForDeviceTest : SysuiTestCase() {

    private fun createFakeAccessibilityServiceInfo(
        packageName: String,
        tileServiceClass: String
        tileServiceClass: String,
    ): AccessibilityServiceInfo {
        val serviceInfo = ServiceInfo().also { it.packageName = packageName }
        val resolveInfo = ResolveInfo().also { it.serviceInfo = serviceInfo }
@@ -223,7 +301,7 @@ class AccessibilityQsShortcutsRepositoryImplForDeviceTest : SysuiTestCase() {
        FieldSetter.setField(
            a11yServiceInfo,
            AccessibilityServiceInfo::class.java.getDeclaredField("mTileServiceName"),
            tileServiceClass
            tileServiceClass,
        )

        return a11yServiceInfo
+10 −0
Original line number Diff line number Diff line
@@ -114,6 +114,16 @@ flag {
    }
}

flag {
    name: "notify_qs_tile_changed_after_user_initialization"
    namespace: "accessibility"
    description: "Pause SysUI notifying QuickSettings tiles changed until the user in AMS finishes initialization."
    bug: "392560794"
    metadata {
        purpose: PURPOSE_BUGFIX
    }
}

flag {
    name: "only_reset_magnification_if_needed_when_destroy_handler"
    namespace: "accessibility"
Loading