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

Commit bd11bbb6 authored by Darrell Shi's avatar Darrell Shi Committed by Android (Google) Code Review
Browse files

Merge "Announce widget added for accessibility" into main

parents cfb360be 828c5c8f
Loading
Loading
Loading
Loading
+24 −4
Original line number Diff line number Diff line
@@ -221,7 +221,7 @@ fun CommunalHub(
    val layoutDirection = LocalLayoutDirection.current

    if (viewModel.isEditMode) {
        ScrollOnNewWidgetAddedEffect(communalContent, gridState)
        ObserveNewWidgetAddedEffect(communalContent, gridState, viewModel)
    } else {
        ScrollOnUpdatedLiveContentEffect(communalContent, gridState)
    }
@@ -553,19 +553,37 @@ private fun ScrollOnUpdatedLiveContentEffect(
    }
}

/** Observes communal content and scrolls to a newly added widget if any. */
/**
 * Observes communal content and determines whether a new widget has been added, upon which case:
 * - Announce for accessibility
 * - Scroll if the new widget is not visible
 */
@Composable
private fun ScrollOnNewWidgetAddedEffect(
private fun ObserveNewWidgetAddedEffect(
    communalContent: List<CommunalContentModel>,
    gridState: LazyGridState,
    viewModel: BaseCommunalViewModel,
) {
    val coroutineScope = rememberCoroutineScope()
    val widgetKeys = remember { mutableListOf<String>() }
    var communalContentPending by remember { mutableStateOf(true) }

    LaunchedEffect(communalContent) {
        // Do nothing until any communal content comes in
        if (communalContentPending && communalContent.isEmpty()) {
            return@LaunchedEffect
        }

        val oldWidgetKeys = widgetKeys.toList()
        val widgets = communalContent.filterIsInstance<CommunalContentModel.WidgetContent.Widget>()
        widgetKeys.clear()
        widgetKeys.addAll(communalContent.filter { it.isWidgetContent() }.map { it.key })
        widgetKeys.addAll(widgets.map { it.key })

        // Do nothing on first communal content since we don't have a delta
        if (communalContentPending) {
            communalContentPending = false
            return@LaunchedEffect
        }

        // Do nothing if there is no new widget
        val indexOfFirstNewWidget = widgetKeys.indexOfFirst { !oldWidgetKeys.contains(it) }
@@ -573,6 +591,8 @@ private fun ScrollOnNewWidgetAddedEffect(
            return@LaunchedEffect
        }

        viewModel.onNewWidgetAdded(widgets[indexOfFirstNewWidget].providerInfo)

        // Scroll if the new widget is not visible
        val lastVisibleItemIndex = gridState.layoutInfo.visibleItemsInfo.lastOrNull()?.index
        if (lastVisibleItemIndex != null && indexOfFirstNewWidget > lastVisibleItemIndex) {
+44 −4
Original line number Diff line number Diff line
@@ -16,6 +16,7 @@

package com.android.systemui.communal.view.viewmodel

import android.appwidget.AppWidgetProviderInfo
import android.content.ActivityNotFoundException
import android.content.ComponentName
import android.content.Intent
@@ -24,6 +25,9 @@ import android.content.pm.PackageManager
import android.content.pm.ResolveInfo
import android.content.pm.UserInfo
import android.provider.Settings
import android.view.accessibility.AccessibilityEvent
import android.view.accessibility.AccessibilityManager
import android.view.accessibility.accessibilityManager
import android.widget.RemoteViews
import androidx.activity.result.ActivityResultLauncher
import androidx.test.ext.junit.runners.AndroidJUnit4
@@ -42,7 +46,6 @@ import com.android.systemui.communal.data.repository.fakeCommunalWidgetRepositor
import com.android.systemui.communal.domain.interactor.CommunalInteractor
import com.android.systemui.communal.domain.interactor.CommunalSceneInteractor
import com.android.systemui.communal.domain.interactor.communalInteractor
import com.android.systemui.communal.domain.interactor.communalPrefsInteractor
import com.android.systemui.communal.domain.interactor.communalSceneInteractor
import com.android.systemui.communal.domain.interactor.communalSettingsInteractor
import com.android.systemui.communal.domain.model.CommunalContentModel
@@ -61,8 +64,6 @@ import com.android.systemui.media.controls.ui.view.MediaHost
import com.android.systemui.settings.fakeUserTracker
import com.android.systemui.testKosmos
import com.android.systemui.user.data.repository.fakeUserRepository
import com.android.systemui.util.mockito.any
import com.android.systemui.util.mockito.whenever
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.test.advanceTimeBy
import kotlinx.coroutines.test.runTest
@@ -77,8 +78,12 @@ import org.mockito.Mockito
import org.mockito.Mockito.never
import org.mockito.Mockito.verify
import org.mockito.MockitoAnnotations
import org.mockito.kotlin.any
import org.mockito.kotlin.argumentCaptor
import org.mockito.kotlin.eq
import org.mockito.kotlin.mock
import org.mockito.kotlin.spy
import org.mockito.kotlin.whenever

@SmallTest
@RunWith(AndroidJUnit4::class)
@@ -98,6 +103,7 @@ class CommunalEditModeViewModelTest : SysuiTestCase() {
    private lateinit var mediaRepository: FakeCommunalMediaRepository
    private lateinit var communalSceneInteractor: CommunalSceneInteractor
    private lateinit var communalInteractor: CommunalInteractor
    private lateinit var accessibilityManager: AccessibilityManager

    private val testableResources = context.orCreateTestableResources

@@ -119,6 +125,7 @@ class CommunalEditModeViewModelTest : SysuiTestCase() {
            selectedUserIndex = 0,
        )
        kosmos.fakeFeatureFlagsClassic.set(Flags.COMMUNAL_SERVICE_ENABLED, true)
        accessibilityManager = kosmos.accessibilityManager

        underTest =
            CommunalEditModeViewModel(
@@ -130,8 +137,10 @@ class CommunalEditModeViewModelTest : SysuiTestCase() {
                uiEventLogger,
                logcatLogBuffer("CommunalEditModeViewModelTest"),
                kosmos.testDispatcher,
                kosmos.communalPrefsInteractor,
                metricsLogger,
                context,
                accessibilityManager,
                packageManager,
            )
    }

@@ -356,6 +365,37 @@ class CommunalEditModeViewModelTest : SysuiTestCase() {
        verify(communalInteractor).setScrollPosition(eq(index), eq(offset))
    }

    @Test
    fun onNewWidgetAdded_accessibilityDisabled_doNothing() {
        whenever(accessibilityManager.isEnabled).thenReturn(false)

        val provider =
            mock<AppWidgetProviderInfo> {
                on { loadLabel(packageManager) }.thenReturn("Test Clock")
            }
        underTest.onNewWidgetAdded(provider)

        verify(accessibilityManager, never()).sendAccessibilityEvent(any())
    }

    @Test
    fun onNewWidgetAdded_accessibilityEnabled_sendAccessibilityAnnouncement() {
        whenever(accessibilityManager.isEnabled).thenReturn(true)

        val provider =
            mock<AppWidgetProviderInfo> {
                on { loadLabel(packageManager) }.thenReturn("Test Clock")
            }
        underTest.onNewWidgetAdded(provider)

        val captor = argumentCaptor<AccessibilityEvent>()
        verify(accessibilityManager).sendAccessibilityEvent(captor.capture())

        val event = captor.firstValue
        assertThat(event.eventType).isEqualTo(AccessibilityEvent.TYPE_ANNOUNCEMENT)
        assertThat(event.contentDescription).isEqualTo("Test Clock widget added to lock screen")
    }

    private companion object {
        val MAIN_USER_INFO = UserInfo(0, "primary", UserInfo.FLAG_MAIN)
        const val WIDGET_PICKER_PACKAGE_NAME = "widget_picker_package_name"
+3 −0
Original line number Diff line number Diff line
@@ -1225,6 +1225,9 @@
    <!-- Label for accessibility action that shows widgets on lock screen on click. [CHAR LIMIT=NONE] -->
    <string name="accessibility_action_open_communal_hub">Widgets on lock screen</string>

    <!-- Label for an accessibility announcement when a widget has been added to the lock screen. [CHAR LIMIT=NONE] -->
    <string name="accessibility_announcement_communal_widget_added"><xliff:g id="widget_name" example="Calendar month view">%1$s</xliff:g> widget added to lock screen</string>

    <!-- Indicator on keyguard to start the communal tutorial. [CHAR LIMIT=100] -->
    <string name="communal_tutorial_indicator_text">Swipe left to start the communal tutorial</string>

+4 −0
Original line number Diff line number Diff line
@@ -16,6 +16,7 @@

package com.android.systemui.communal.ui.viewmodel

import android.appwidget.AppWidgetProviderInfo
import android.content.ComponentName
import android.os.UserHandle
import android.view.View
@@ -197,6 +198,9 @@ abstract class BaseCommunalViewModel(
    /** Called as the user request to show the customize widget button. */
    open fun onLongClick() {}

    /** Called as the UI determines that a new widget has been added to the grid. */
    open fun onNewWidgetAdded(provider: AppWidgetProviderInfo) {}

    /** Called when the grid scroll position has been updated. */
    open fun onScrollPositionUpdated(firstVisibleItemIndex: Int, firstVisibleItemScroll: Int) {
        currentScrollIndex = firstVisibleItemIndex
+26 −2
Original line number Diff line number Diff line
@@ -19,16 +19,18 @@ package com.android.systemui.communal.ui.viewmodel
import android.appwidget.AppWidgetManager
import android.appwidget.AppWidgetProviderInfo
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.content.res.Resources
import android.os.UserHandle
import android.util.Log
import android.view.accessibility.AccessibilityEvent
import android.view.accessibility.AccessibilityManager
import androidx.activity.result.ActivityResultLauncher
import com.android.internal.logging.UiEventLogger
import com.android.systemui.communal.data.model.CommunalWidgetCategories
import com.android.systemui.communal.domain.interactor.CommunalInteractor
import com.android.systemui.communal.domain.interactor.CommunalPrefsInteractor
import com.android.systemui.communal.domain.interactor.CommunalSceneInteractor
import com.android.systemui.communal.domain.interactor.CommunalSettingsInteractor
import com.android.systemui.communal.domain.model.CommunalContentModel
@@ -37,6 +39,7 @@ import com.android.systemui.communal.shared.log.CommunalUiEvent
import com.android.systemui.communal.shared.model.EditModeState
import com.android.systemui.communal.widgets.WidgetConfigurator
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
import com.android.systemui.keyguard.shared.model.KeyguardState
@@ -74,8 +77,10 @@ constructor(
    private val uiEventLogger: UiEventLogger,
    @CommunalLog logBuffer: LogBuffer,
    @Background private val backgroundDispatcher: CoroutineDispatcher,
    private val communalPrefsInteractor: CommunalPrefsInteractor,
    private val metricsLogger: CommunalMetricsLogger,
    @Application private val context: Context,
    private val accessibilityManager: AccessibilityManager,
    private val packageManager: PackageManager,
) : BaseCommunalViewModel(communalSceneInteractor, communalInteractor, mediaHost) {

    private val logger = Logger(logBuffer, "CommunalEditModeViewModel")
@@ -156,6 +161,25 @@ constructor(
        uiEventLogger.log(CommunalUiEvent.COMMUNAL_HUB_REORDER_WIDGET_CANCEL)
    }

    override fun onNewWidgetAdded(provider: AppWidgetProviderInfo) {
        if (!accessibilityManager.isEnabled) {
            return
        }

        // Send an accessibility announcement for the newly added widget
        val widgetLabel = provider.loadLabel(packageManager)
        val announcementText =
            context.getString(
                R.string.accessibility_announcement_communal_widget_added,
                widgetLabel
            )
        accessibilityManager.sendAccessibilityEvent(
            AccessibilityEvent(AccessibilityEvent.TYPE_ANNOUNCEMENT).apply {
                contentDescription = announcementText
            }
        )
    }

    val isIdleOnCommunal: StateFlow<Boolean> = communalInteractor.isIdleOnCommunal

    /** Launch the widget picker activity using the given {@link ActivityResultLauncher}. */