Loading packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt +24 −4 Original line number Diff line number Diff line Loading @@ -221,7 +221,7 @@ fun CommunalHub( val layoutDirection = LocalLayoutDirection.current if (viewModel.isEditMode) { ScrollOnNewWidgetAddedEffect(communalContent, gridState) ObserveNewWidgetAddedEffect(communalContent, gridState, viewModel) } else { ScrollOnUpdatedLiveContentEffect(communalContent, gridState) } Loading Loading @@ -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) } Loading @@ -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) { Loading packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalEditModeViewModelTest.kt +44 −4 Original line number Diff line number Diff line Loading @@ -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 Loading @@ -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 Loading @@ -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 Loading @@ -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 Loading @@ -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) Loading @@ -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 Loading @@ -119,6 +125,7 @@ class CommunalEditModeViewModelTest : SysuiTestCase() { selectedUserIndex = 0, ) kosmos.fakeFeatureFlagsClassic.set(Flags.COMMUNAL_SERVICE_ENABLED, true) accessibilityManager = kosmos.accessibilityManager underTest = CommunalEditModeViewModel( Loading @@ -130,8 +137,10 @@ class CommunalEditModeViewModelTest : SysuiTestCase() { uiEventLogger, logcatLogBuffer("CommunalEditModeViewModelTest"), kosmos.testDispatcher, kosmos.communalPrefsInteractor, metricsLogger, context, accessibilityManager, packageManager, ) } Loading Loading @@ -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" Loading packages/SystemUI/res/values/strings.xml +3 −0 Original line number Diff line number Diff line Loading @@ -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> Loading packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/BaseCommunalViewModel.kt +4 −0 Original line number Diff line number Diff line Loading @@ -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 Loading Loading @@ -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 Loading packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalEditModeViewModel.kt +26 −2 Original line number Diff line number Diff line Loading @@ -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 Loading @@ -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 Loading Loading @@ -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") Loading Loading @@ -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}. */ Loading Loading
packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt +24 −4 Original line number Diff line number Diff line Loading @@ -221,7 +221,7 @@ fun CommunalHub( val layoutDirection = LocalLayoutDirection.current if (viewModel.isEditMode) { ScrollOnNewWidgetAddedEffect(communalContent, gridState) ObserveNewWidgetAddedEffect(communalContent, gridState, viewModel) } else { ScrollOnUpdatedLiveContentEffect(communalContent, gridState) } Loading Loading @@ -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) } Loading @@ -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) { Loading
packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalEditModeViewModelTest.kt +44 −4 Original line number Diff line number Diff line Loading @@ -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 Loading @@ -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 Loading @@ -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 Loading @@ -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 Loading @@ -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) Loading @@ -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 Loading @@ -119,6 +125,7 @@ class CommunalEditModeViewModelTest : SysuiTestCase() { selectedUserIndex = 0, ) kosmos.fakeFeatureFlagsClassic.set(Flags.COMMUNAL_SERVICE_ENABLED, true) accessibilityManager = kosmos.accessibilityManager underTest = CommunalEditModeViewModel( Loading @@ -130,8 +137,10 @@ class CommunalEditModeViewModelTest : SysuiTestCase() { uiEventLogger, logcatLogBuffer("CommunalEditModeViewModelTest"), kosmos.testDispatcher, kosmos.communalPrefsInteractor, metricsLogger, context, accessibilityManager, packageManager, ) } Loading Loading @@ -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" Loading
packages/SystemUI/res/values/strings.xml +3 −0 Original line number Diff line number Diff line Loading @@ -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> Loading
packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/BaseCommunalViewModel.kt +4 −0 Original line number Diff line number Diff line Loading @@ -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 Loading Loading @@ -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 Loading
packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalEditModeViewModel.kt +26 −2 Original line number Diff line number Diff line Loading @@ -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 Loading @@ -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 Loading Loading @@ -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") Loading Loading @@ -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}. */ Loading