Loading packages/SystemUI/multivalentTests/src/com/android/systemui/communal/widgets/GlanceableHubWidgetManagerServiceTest.kt +99 −3 Original line number Diff line number Diff line Loading @@ -20,6 +20,8 @@ import android.appwidget.AppWidgetHost import android.appwidget.AppWidgetProviderInfo import android.content.ComponentName import android.content.Intent import android.content.IntentSender import android.os.Binder import android.os.UserHandle import android.testing.TestableLooper import android.widget.RemoteViews Loading @@ -29,6 +31,7 @@ import com.android.systemui.SysuiTestCase import com.android.systemui.communal.data.repository.fakeCommunalWidgetRepository import com.android.systemui.communal.shared.model.CommunalWidgetContentModel import com.android.systemui.communal.shared.model.fakeGlanceableHubMultiUserHelper import com.android.systemui.communal.widgets.IGlanceableHubWidgetManagerService.IConfigureWidgetCallback import com.android.systemui.communal.widgets.IGlanceableHubWidgetManagerService.IGlanceableHubWidgetsListener import com.android.systemui.coroutines.collectLastValue import com.android.systemui.kosmos.testScope Loading @@ -43,11 +46,13 @@ import kotlinx.coroutines.test.runTest import org.junit.Before import org.junit.Test import org.junit.runner.RunWith import org.mockito.ArgumentMatchers.anyInt import org.mockito.kotlin.argumentCaptor import org.mockito.kotlin.eq import org.mockito.kotlin.mock import org.mockito.kotlin.never import org.mockito.kotlin.verify import org.mockito.kotlin.whenever @OptIn(ExperimentalCoroutinesApi::class) @SmallTest Loading Loading @@ -164,7 +169,7 @@ class GlanceableHubWidgetManagerServiceTest : SysuiTestCase() { } @Test fun addWidget_getWidgetUpdate() = fun addWidget_noConfigurationCallback_getWidgetUpdate() = testScope.runTest { setupWidgets() Loading @@ -180,7 +185,7 @@ class GlanceableHubWidgetManagerServiceTest : SysuiTestCase() { assertThat(widgets?.get(2)?.has(3, "pkg_3/cls_3", 2, 6)).isTrue() // Add a widget service.addWidget(ComponentName("pkg_4", "cls_4"), UserHandle.of(0), 3) service.addWidget(ComponentName("pkg_4", "cls_4"), UserHandle.of(0), 3, null) runCurrent() // Verify an update pushed with widget 4 added Loading @@ -191,6 +196,71 @@ class GlanceableHubWidgetManagerServiceTest : SysuiTestCase() { assertThat(widgets?.get(3)?.has(4, "pkg_4/cls_4", 3, 3)).isTrue() } @Test fun addWidget_withConfigurationCallback_configurationFails_doNotAddWidget() = testScope.runTest { setupWidgets() // Bind service val binder = underTest.onBind(Intent()) val service = IGlanceableHubWidgetManagerService.Stub.asInterface(binder) // Verify the update is as expected val widgets by collectLastValue(service.listenForWidgetUpdates()) assertThat(widgets).hasSize(3) assertThat(widgets?.get(0)?.has(1, "pkg_1/cls_1", 0, 3)).isTrue() assertThat(widgets?.get(1)?.has(2, "pkg_2/cls_2", 1, 3)).isTrue() assertThat(widgets?.get(2)?.has(3, "pkg_3/cls_3", 2, 6)).isTrue() // Add a widget with a configuration callback that fails service.addWidget( ComponentName("pkg_4", "cls_4"), UserHandle.of(0), 3, createConfigureWidgetCallback(success = false), ) runCurrent() // Verify that widget 4 is not added assertThat(widgets).hasSize(3) assertThat(widgets?.get(0)?.has(1, "pkg_1/cls_1", 0, 3)).isTrue() assertThat(widgets?.get(1)?.has(2, "pkg_2/cls_2", 1, 3)).isTrue() assertThat(widgets?.get(2)?.has(3, "pkg_3/cls_3", 2, 6)).isTrue() } @Test fun addWidget_withConfigurationCallback_configurationSucceeds_addWidget() = testScope.runTest { setupWidgets() // Bind service val binder = underTest.onBind(Intent()) val service = IGlanceableHubWidgetManagerService.Stub.asInterface(binder) // Verify the update is as expected val widgets by collectLastValue(service.listenForWidgetUpdates()) assertThat(widgets).hasSize(3) assertThat(widgets?.get(0)?.has(1, "pkg_1/cls_1", 0, 3)).isTrue() assertThat(widgets?.get(1)?.has(2, "pkg_2/cls_2", 1, 3)).isTrue() assertThat(widgets?.get(2)?.has(3, "pkg_3/cls_3", 2, 6)).isTrue() // Add a widget with a configuration callback that fails service.addWidget( ComponentName("pkg_4", "cls_4"), UserHandle.of(0), 3, createConfigureWidgetCallback(success = true), ) runCurrent() // Verify that widget 4 is added assertThat(widgets).hasSize(4) assertThat(widgets?.get(0)?.has(1, "pkg_1/cls_1", 0, 3)).isTrue() assertThat(widgets?.get(1)?.has(2, "pkg_2/cls_2", 1, 3)).isTrue() assertThat(widgets?.get(2)?.has(3, "pkg_3/cls_3", 2, 6)).isTrue() assertThat(widgets?.get(3)?.has(4, "pkg_4/cls_4", 3, 3)).isTrue() } @Test fun deleteWidget_getWidgetUpdate() = testScope.runTest { Loading Loading @@ -271,6 +341,21 @@ class GlanceableHubWidgetManagerServiceTest : SysuiTestCase() { assertThat(widgets?.get(2)?.has(3, "pkg_3/cls_3", 2, 6)).isTrue() } @Test fun getIntentSenderForConfigureActivity() = testScope.runTest { val expected = IntentSender(Binder()) whenever(appWidgetHost.getIntentSenderForConfigureActivity(anyInt(), anyInt())) .thenReturn(expected) // Bind service val binder = underTest.onBind(Intent()) val service = IGlanceableHubWidgetManagerService.Stub.asInterface(binder) val actual = service.getIntentSenderForConfigureActivity(1) assertThat(actual).isEqualTo(expected) } private fun setupWidgets() { widgetRepository.addWidget( appWidgetId = 1, Loading @@ -293,7 +378,7 @@ class GlanceableHubWidgetManagerServiceTest : SysuiTestCase() { } private fun IGlanceableHubWidgetManagerService.listenForWidgetUpdates() = conflatedCallbackFlow<List<CommunalWidgetContentModel>> { conflatedCallbackFlow { val listener = object : IGlanceableHubWidgetsListener.Stub() { override fun onWidgetsUpdated(widgets: List<CommunalWidgetContentModel>) { Loading @@ -316,4 +401,15 @@ class GlanceableHubWidgetManagerServiceTest : SysuiTestCase() { this.rank == rank && this.spanY == spanY } private fun createConfigureWidgetCallback(success: Boolean): IConfigureWidgetCallback { return object : IConfigureWidgetCallback.Stub() { override fun onConfigureWidget( appWidgetId: Int, resultReceiver: IConfigureWidgetCallback.IResultReceiver?, ) { resultReceiver?.onResult(success) } } } } packages/SystemUI/multivalentTests/src/com/android/systemui/communal/widgets/WidgetConfigurationControllerTest.kt +132 −11 Original line number Diff line number Diff line Loading @@ -18,17 +18,18 @@ package com.android.systemui.communal.widgets import android.app.Activity import android.content.ActivityNotFoundException import android.content.IntentSender import android.os.Binder import android.os.OutcomeReceiver import androidx.activity.ComponentActivity import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.communal.shared.model.fakeGlanceableHubMultiUserHelper import com.android.systemui.concurrency.fakeExecutor import com.android.systemui.kosmos.testDispatcher import com.android.systemui.kosmos.testScope import com.android.systemui.testKosmos import com.android.systemui.util.mockito.any import com.android.systemui.util.mockito.eq import com.android.systemui.util.mockito.whenever import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.async Loading @@ -38,15 +39,22 @@ import org.junit.Before import org.junit.Test import org.junit.runner.RunWith import org.mockito.ArgumentMatchers.anyInt import org.mockito.Mock import org.mockito.MockitoAnnotations import org.mockito.kotlin.any import org.mockito.kotlin.anyOrNull import org.mockito.kotlin.argumentCaptor import org.mockito.kotlin.eq import org.mockito.kotlin.mock import org.mockito.kotlin.verify import org.mockito.kotlin.whenever @OptIn(ExperimentalCoroutinesApi::class) @SmallTest @RunWith(AndroidJUnit4::class) class WidgetConfigurationControllerTest : SysuiTestCase() { @Mock private lateinit var appWidgetHost: CommunalAppWidgetHost @Mock private lateinit var ownerActivity: ComponentActivity private val appWidgetHost = mock<CommunalAppWidgetHost>() private val ownerActivity = mock<ComponentActivity>() private val outcomeReceiverCaptor = argumentCaptor<OutcomeReceiver<IntentSender?, Throwable>>() private val kosmos = testKosmos() Loading @@ -54,18 +62,19 @@ class WidgetConfigurationControllerTest : SysuiTestCase() { @Before fun setUp() { MockitoAnnotations.initMocks(this) underTest = WidgetConfigurationController( ownerActivity, { appWidgetHost }, kosmos.testDispatcher, kosmos.fakeGlanceableHubMultiUserHelper, { kosmos.mockGlanceableHubWidgetManager }, kosmos.fakeExecutor, ) } @Test fun configurationFailsWhenActivityNotFound() = fun configureWidget_activityNotFound_returnsFalse() = with(kosmos) { testScope.runTest { whenever( Loading @@ -84,13 +93,97 @@ class WidgetConfigurationControllerTest : SysuiTestCase() { } @Test fun configurationFails() = fun configureWidget_configurationFails_returnsFalse() = with(kosmos) { testScope.runTest { val result = async { underTest.configureWidget(123) } runCurrent() assertThat(result.isCompleted).isFalse() underTest.setConfigurationResult(Activity.RESULT_CANCELED) runCurrent() assertThat(result.await()).isFalse() result.cancel() } } @Test fun configureWidget_configurationSucceeds_returnsTrue() = with(kosmos) { testScope.runTest { val result = async { underTest.configureWidget(123) } runCurrent() assertThat(result.isCompleted).isFalse() underTest.setConfigurationResult(Activity.RESULT_OK) runCurrent() assertThat(result.await()).isTrue() result.cancel() } } @Test fun configureWidget_headlessSystemUser_activityNotFound_returnsFalse() = with(kosmos) { testScope.runTest { fakeGlanceableHubMultiUserHelper.setIsInHeadlessSystemUser(true) // Activity not found whenever( mockGlanceableHubWidgetManager.getIntentSenderForConfigureActivity( anyInt(), outcomeReceiverCaptor.capture(), any(), ) ) .then { outcomeReceiverCaptor.firstValue.onError(ActivityNotFoundException()) } val result = async { underTest.configureWidget(123) } runCurrent() assertThat(result.await()).isFalse() result.cancel() } } @Test fun configureWidget_headlessSystemUser_intentSenderNull_returnsFalse() = with(kosmos) { testScope.runTest { fakeGlanceableHubMultiUserHelper.setIsInHeadlessSystemUser(true) prepareIntentSender(null) assertThat(underTest.configureWidget(123)).isFalse() } } @Test fun configureWidget_headlessSystemUser_configurationFails_returnsFalse() = with(kosmos) { testScope.runTest { fakeGlanceableHubMultiUserHelper.setIsInHeadlessSystemUser(true) val intentSender = IntentSender(Binder()) prepareIntentSender(intentSender) val result = async { underTest.configureWidget(123) } runCurrent() assertThat(result.isCompleted).isFalse() verify(ownerActivity) .startIntentSenderForResult( eq(intentSender), eq(WidgetConfigurationController.REQUEST_CODE), anyOrNull(), anyInt(), anyInt(), anyInt(), any(), ) underTest.setConfigurationResult(Activity.RESULT_CANCELED) runCurrent() Loading @@ -100,13 +193,29 @@ class WidgetConfigurationControllerTest : SysuiTestCase() { } @Test fun configurationSuccessful() = fun configureWidget_headlessSystemUser_configurationSucceeds_returnsTrue() = with(kosmos) { testScope.runTest { fakeGlanceableHubMultiUserHelper.setIsInHeadlessSystemUser(true) val intentSender = IntentSender(Binder()) prepareIntentSender(intentSender) val result = async { underTest.configureWidget(123) } runCurrent() assertThat(result.isCompleted).isFalse() verify(ownerActivity) .startIntentSenderForResult( eq(intentSender), eq(WidgetConfigurationController.REQUEST_CODE), anyOrNull(), anyInt(), anyInt(), anyInt(), any(), ) underTest.setConfigurationResult(Activity.RESULT_OK) runCurrent() Loading @@ -114,4 +223,16 @@ class WidgetConfigurationControllerTest : SysuiTestCase() { result.cancel() } } private fun prepareIntentSender(intentSender: IntentSender?) = with(kosmos) { whenever( mockGlanceableHubWidgetManager.getIntentSenderForConfigureActivity( anyInt(), outcomeReceiverCaptor.capture(), any(), ) ) .then { outcomeReceiverCaptor.firstValue.onResult(intentSender) } } } packages/SystemUI/src/com/android/systemui/communal/widgets/GlanceableHubWidgetManager.kt +80 −11 Original line number Diff line number Diff line Loading @@ -19,7 +19,10 @@ package com.android.systemui.communal.widgets import android.appwidget.AppWidgetHost.AppWidgetHostListener import android.appwidget.AppWidgetProviderInfo import android.content.ComponentName import android.content.IntentSender import android.os.IBinder import android.os.OutcomeReceiver import android.os.RemoteException import android.os.UserHandle import android.widget.RemoteViews import com.android.server.servicewatcher.ServiceWatcher Loading @@ -27,14 +30,19 @@ import com.android.server.servicewatcher.ServiceWatcher.ServiceListener import com.android.systemui.communal.shared.model.CommunalWidgetContentModel import com.android.systemui.communal.shared.model.GlanceableHubMultiUserHelper import com.android.systemui.communal.widgets.IGlanceableHubWidgetManagerService.IAppWidgetHostListener import com.android.systemui.communal.widgets.IGlanceableHubWidgetManagerService.IConfigureWidgetCallback import com.android.systemui.communal.widgets.IGlanceableHubWidgetManagerService.IGlanceableHubWidgetsListener import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.log.LogBuffer import com.android.systemui.log.core.Logger import com.android.systemui.log.dagger.CommunalLog 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.launch /** * Manages updates to Glanceable Hub widgets and requests to edit them from the headless system Loading @@ -47,6 +55,8 @@ import kotlinx.coroutines.channels.awaitClose class GlanceableHubWidgetManager @Inject constructor( @Background private val bgExecutor: Executor, @Background private val bgScope: CoroutineScope, glanceableHubMultiUserHelper: GlanceableHubMultiUserHelper, @CommunalLog logBuffer: LogBuffer, serviceWatcherFactory: ServiceWatcherFactory<GlanceableHubWidgetManagerServiceInfo?>, Loading Loading @@ -101,8 +111,7 @@ constructor( rank: Int?, configurator: WidgetConfigurator?, ) = runOnService { service -> // TODO(b/375036327): Add support for widget configuration service.addWidget(provider, user, rank ?: -1) service.addWidget(provider, user, rank ?: -1, createIConfigureWidgetCallback(configurator)) } /** Requests the foreground user to delete a widget. */ Loading @@ -129,7 +138,42 @@ constructor( ) } /** * Requests the foreground user for the [IntentSender] to start a configuration activity for the * widget. * * @param appWidgetId Id of the widget to configure. * @param outcomeReceiver Callback for receiving the result or error. * @param executor Executor to run the callback on. */ fun getIntentSenderForConfigureActivity( appWidgetId: Int, outcomeReceiver: OutcomeReceiver<IntentSender?, Throwable>, executor: Executor, ) { bgExecutor.execute { serviceWatcher.runOnBinder( object : ServiceWatcher.BinderOperation { override fun run(binder: IBinder?) { val service = IGlanceableHubWidgetManagerService.Stub.asInterface(binder) try { val result = service.getIntentSenderForConfigureActivity(appWidgetId) executor.execute { outcomeReceiver.onResult(result) } } catch (e: RemoteException) { executor.execute { outcomeReceiver.onError(e) } } } override fun onError(t: Throwable?) { t?.let { executor.execute { outcomeReceiver.onError(t) } } } } ) } } private fun runOnService(block: (IGlanceableHubWidgetManagerService) -> Unit) { bgExecutor.execute { serviceWatcher.runOnBinder( object : ServiceWatcher.BinderOperation { override fun run(binder: IBinder?) { Loading @@ -142,6 +186,7 @@ constructor( } ) } } private fun createIAppWidgetHostListener( listener: AppWidgetHostListener Loading @@ -165,6 +210,30 @@ constructor( } } private fun createIConfigureWidgetCallback( configurator: WidgetConfigurator? ): IConfigureWidgetCallback? { return configurator?.let { object : IConfigureWidgetCallback.Stub() { override fun onConfigureWidget( appWidgetId: Int, resultReceiver: IConfigureWidgetCallback.IResultReceiver?, ) { bgScope.launch { val success = configurator.configureWidget(appWidgetId) try { resultReceiver?.onResult(success) } catch (e: RemoteException) { logger.e({ "Error reporting widget configuration result: $str1" }) { str1 = e.localizedMessage } } } } } } } companion object { private const val TAG = "GlanceableHubWidgetManager" } Loading packages/SystemUI/src/com/android/systemui/communal/widgets/GlanceableHubWidgetManagerService.kt +60 −5 Original line number Diff line number Diff line Loading @@ -20,6 +20,7 @@ import android.appwidget.AppWidgetHost.AppWidgetHostListener import android.appwidget.AppWidgetProviderInfo import android.content.ComponentName import android.content.Intent import android.content.IntentSender import android.os.IBinder import android.os.RemoteCallbackList import android.os.RemoteException Loading @@ -30,11 +31,13 @@ import androidx.lifecycle.lifecycleScope import com.android.systemui.communal.data.repository.CommunalWidgetRepository import com.android.systemui.communal.shared.model.GlanceableHubMultiUserHelper import com.android.systemui.communal.widgets.IGlanceableHubWidgetManagerService.IAppWidgetHostListener import com.android.systemui.communal.widgets.IGlanceableHubWidgetManagerService.IConfigureWidgetCallback import com.android.systemui.communal.widgets.IGlanceableHubWidgetManagerService.IGlanceableHubWidgetsListener import com.android.systemui.log.LogBuffer import com.android.systemui.log.core.Logger import com.android.systemui.log.dagger.CommunalLog import javax.inject.Inject import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.Job import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach Loading Loading @@ -131,7 +134,12 @@ constructor( appWidgetHost.setListener(appWidgetId, createListener(listener)) } private fun addWidgetInternal(provider: ComponentName?, user: UserHandle?, rank: Int) { private fun addWidgetInternal( provider: ComponentName?, user: UserHandle?, rank: Int, callback: IConfigureWidgetCallback?, ) { if (provider == null) { throw IllegalStateException("Provider cannot be null") } Loading @@ -140,8 +148,29 @@ constructor( throw IllegalStateException("User cannot be null") } // TODO(b/375036327): Add support for widget configuration widgetRepository.addWidget(provider, user, rank, configurator = null) val configurator = callback?.let { WidgetConfigurator { appWidgetId -> try { val result = CompletableDeferred<Boolean>() val resultReceiver = object : IConfigureWidgetCallback.IResultReceiver.Stub() { override fun onResult(success: Boolean) { result.complete(success) } } callback.onConfigureWidget(appWidgetId, resultReceiver) result.await() } catch (e: RemoteException) { logger.e({ "Error configuring widget: $str1" }) { str1 = e.localizedMessage } false } } } widgetRepository.addWidget(provider, user, rank, configurator) } private fun deleteWidgetInternal(appWidgetId: Int) { Loading Loading @@ -177,6 +206,17 @@ constructor( widgetRepository.resizeWidget(appWidgetId, spanY, appWidgetIds.zip(ranks).toMap()) } private fun getIntentSenderForConfigureActivityInternal(appWidgetId: Int): IntentSender? { return try { appWidgetHost.getIntentSenderForConfigureActivity(appWidgetId, /* intentFlags= */ 0) } catch (e: IntentSender.SendIntentException) { logger.e({ "Error getting intent sender for configure activity" }) { str1 = e.localizedMessage } null } } private fun createListener(listener: IAppWidgetHostListener): AppWidgetHostListener { return object : AppWidgetHostListener { override fun onUpdateProviderInfo(appWidget: AppWidgetProviderInfo?) { Loading Loading @@ -250,11 +290,16 @@ constructor( } } override fun addWidget(provider: ComponentName?, user: UserHandle?, rank: Int) { override fun addWidget( provider: ComponentName?, user: UserHandle?, rank: Int, callback: IConfigureWidgetCallback?, ) { val iden = clearCallingIdentity() try { addWidgetInternal(provider, user, rank) addWidgetInternal(provider, user, rank, callback) } finally { restoreCallingIdentity(iden) } Loading Loading @@ -294,6 +339,16 @@ constructor( restoreCallingIdentity(iden) } } override fun getIntentSenderForConfigureActivity(appWidgetId: Int): IntentSender? { val iden = clearCallingIdentity() try { return getIntentSenderForConfigureActivityInternal(appWidgetId) } finally { restoreCallingIdentity(iden) } } } /** Loading packages/SystemUI/src/com/android/systemui/communal/widgets/IGlanceableHubWidgetManagerService.aidl +17 −1 Original line number Diff line number Diff line Loading @@ -2,6 +2,7 @@ package com.android.systemui.communal.widgets; import android.appwidget.AppWidgetProviderInfo; import android.content.ComponentName; import android.content.IntentSender; import android.os.UserHandle; import android.widget.RemoteViews; import com.android.systemui.communal.shared.model.CommunalWidgetContentModel; Loading @@ -21,7 +22,8 @@ interface IGlanceableHubWidgetManagerService { oneway void setAppWidgetHostListener(int appWidgetId, in IAppWidgetHostListener listener); // Requests to add a widget in the Glanceable Hub. oneway void addWidget(in ComponentName provider, in UserHandle user, int rank); oneway void addWidget(in ComponentName provider, in UserHandle user, int rank, in IConfigureWidgetCallback callback); // Requests to delete a widget from the Glanceable Hub. oneway void deleteWidget(int appWidgetId); Loading @@ -32,6 +34,9 @@ interface IGlanceableHubWidgetManagerService { // Requests to resize a widget in the Glanceable Hub. oneway void resizeWidget(int appWidgetId, int spanY, in int[] appWidgetIds, in int[] ranks); // Returns the [IntentSender] for launching the configuration activity of the given widget. IntentSender getIntentSenderForConfigureActivity(int appWidgetId); // Listener for Glanceable Hub widget updates oneway interface IGlanceableHubWidgetsListener { // Called when widgets have updated. Loading @@ -48,4 +53,15 @@ interface IGlanceableHubWidgetManagerService { void onViewDataChanged(int viewId); } oneway interface IConfigureWidgetCallback { // Called when the given widget should launch its configuration activity. The caller should // report the result through the [IResultReceiver]. void onConfigureWidget(int appWidgetId, in IResultReceiver resultReceiver); interface IResultReceiver { // Called when the widget configuration operation returns a result. void onResult(boolean success); } } } Loading
packages/SystemUI/multivalentTests/src/com/android/systemui/communal/widgets/GlanceableHubWidgetManagerServiceTest.kt +99 −3 Original line number Diff line number Diff line Loading @@ -20,6 +20,8 @@ import android.appwidget.AppWidgetHost import android.appwidget.AppWidgetProviderInfo import android.content.ComponentName import android.content.Intent import android.content.IntentSender import android.os.Binder import android.os.UserHandle import android.testing.TestableLooper import android.widget.RemoteViews Loading @@ -29,6 +31,7 @@ import com.android.systemui.SysuiTestCase import com.android.systemui.communal.data.repository.fakeCommunalWidgetRepository import com.android.systemui.communal.shared.model.CommunalWidgetContentModel import com.android.systemui.communal.shared.model.fakeGlanceableHubMultiUserHelper import com.android.systemui.communal.widgets.IGlanceableHubWidgetManagerService.IConfigureWidgetCallback import com.android.systemui.communal.widgets.IGlanceableHubWidgetManagerService.IGlanceableHubWidgetsListener import com.android.systemui.coroutines.collectLastValue import com.android.systemui.kosmos.testScope Loading @@ -43,11 +46,13 @@ import kotlinx.coroutines.test.runTest import org.junit.Before import org.junit.Test import org.junit.runner.RunWith import org.mockito.ArgumentMatchers.anyInt import org.mockito.kotlin.argumentCaptor import org.mockito.kotlin.eq import org.mockito.kotlin.mock import org.mockito.kotlin.never import org.mockito.kotlin.verify import org.mockito.kotlin.whenever @OptIn(ExperimentalCoroutinesApi::class) @SmallTest Loading Loading @@ -164,7 +169,7 @@ class GlanceableHubWidgetManagerServiceTest : SysuiTestCase() { } @Test fun addWidget_getWidgetUpdate() = fun addWidget_noConfigurationCallback_getWidgetUpdate() = testScope.runTest { setupWidgets() Loading @@ -180,7 +185,7 @@ class GlanceableHubWidgetManagerServiceTest : SysuiTestCase() { assertThat(widgets?.get(2)?.has(3, "pkg_3/cls_3", 2, 6)).isTrue() // Add a widget service.addWidget(ComponentName("pkg_4", "cls_4"), UserHandle.of(0), 3) service.addWidget(ComponentName("pkg_4", "cls_4"), UserHandle.of(0), 3, null) runCurrent() // Verify an update pushed with widget 4 added Loading @@ -191,6 +196,71 @@ class GlanceableHubWidgetManagerServiceTest : SysuiTestCase() { assertThat(widgets?.get(3)?.has(4, "pkg_4/cls_4", 3, 3)).isTrue() } @Test fun addWidget_withConfigurationCallback_configurationFails_doNotAddWidget() = testScope.runTest { setupWidgets() // Bind service val binder = underTest.onBind(Intent()) val service = IGlanceableHubWidgetManagerService.Stub.asInterface(binder) // Verify the update is as expected val widgets by collectLastValue(service.listenForWidgetUpdates()) assertThat(widgets).hasSize(3) assertThat(widgets?.get(0)?.has(1, "pkg_1/cls_1", 0, 3)).isTrue() assertThat(widgets?.get(1)?.has(2, "pkg_2/cls_2", 1, 3)).isTrue() assertThat(widgets?.get(2)?.has(3, "pkg_3/cls_3", 2, 6)).isTrue() // Add a widget with a configuration callback that fails service.addWidget( ComponentName("pkg_4", "cls_4"), UserHandle.of(0), 3, createConfigureWidgetCallback(success = false), ) runCurrent() // Verify that widget 4 is not added assertThat(widgets).hasSize(3) assertThat(widgets?.get(0)?.has(1, "pkg_1/cls_1", 0, 3)).isTrue() assertThat(widgets?.get(1)?.has(2, "pkg_2/cls_2", 1, 3)).isTrue() assertThat(widgets?.get(2)?.has(3, "pkg_3/cls_3", 2, 6)).isTrue() } @Test fun addWidget_withConfigurationCallback_configurationSucceeds_addWidget() = testScope.runTest { setupWidgets() // Bind service val binder = underTest.onBind(Intent()) val service = IGlanceableHubWidgetManagerService.Stub.asInterface(binder) // Verify the update is as expected val widgets by collectLastValue(service.listenForWidgetUpdates()) assertThat(widgets).hasSize(3) assertThat(widgets?.get(0)?.has(1, "pkg_1/cls_1", 0, 3)).isTrue() assertThat(widgets?.get(1)?.has(2, "pkg_2/cls_2", 1, 3)).isTrue() assertThat(widgets?.get(2)?.has(3, "pkg_3/cls_3", 2, 6)).isTrue() // Add a widget with a configuration callback that fails service.addWidget( ComponentName("pkg_4", "cls_4"), UserHandle.of(0), 3, createConfigureWidgetCallback(success = true), ) runCurrent() // Verify that widget 4 is added assertThat(widgets).hasSize(4) assertThat(widgets?.get(0)?.has(1, "pkg_1/cls_1", 0, 3)).isTrue() assertThat(widgets?.get(1)?.has(2, "pkg_2/cls_2", 1, 3)).isTrue() assertThat(widgets?.get(2)?.has(3, "pkg_3/cls_3", 2, 6)).isTrue() assertThat(widgets?.get(3)?.has(4, "pkg_4/cls_4", 3, 3)).isTrue() } @Test fun deleteWidget_getWidgetUpdate() = testScope.runTest { Loading Loading @@ -271,6 +341,21 @@ class GlanceableHubWidgetManagerServiceTest : SysuiTestCase() { assertThat(widgets?.get(2)?.has(3, "pkg_3/cls_3", 2, 6)).isTrue() } @Test fun getIntentSenderForConfigureActivity() = testScope.runTest { val expected = IntentSender(Binder()) whenever(appWidgetHost.getIntentSenderForConfigureActivity(anyInt(), anyInt())) .thenReturn(expected) // Bind service val binder = underTest.onBind(Intent()) val service = IGlanceableHubWidgetManagerService.Stub.asInterface(binder) val actual = service.getIntentSenderForConfigureActivity(1) assertThat(actual).isEqualTo(expected) } private fun setupWidgets() { widgetRepository.addWidget( appWidgetId = 1, Loading @@ -293,7 +378,7 @@ class GlanceableHubWidgetManagerServiceTest : SysuiTestCase() { } private fun IGlanceableHubWidgetManagerService.listenForWidgetUpdates() = conflatedCallbackFlow<List<CommunalWidgetContentModel>> { conflatedCallbackFlow { val listener = object : IGlanceableHubWidgetsListener.Stub() { override fun onWidgetsUpdated(widgets: List<CommunalWidgetContentModel>) { Loading @@ -316,4 +401,15 @@ class GlanceableHubWidgetManagerServiceTest : SysuiTestCase() { this.rank == rank && this.spanY == spanY } private fun createConfigureWidgetCallback(success: Boolean): IConfigureWidgetCallback { return object : IConfigureWidgetCallback.Stub() { override fun onConfigureWidget( appWidgetId: Int, resultReceiver: IConfigureWidgetCallback.IResultReceiver?, ) { resultReceiver?.onResult(success) } } } }
packages/SystemUI/multivalentTests/src/com/android/systemui/communal/widgets/WidgetConfigurationControllerTest.kt +132 −11 Original line number Diff line number Diff line Loading @@ -18,17 +18,18 @@ package com.android.systemui.communal.widgets import android.app.Activity import android.content.ActivityNotFoundException import android.content.IntentSender import android.os.Binder import android.os.OutcomeReceiver import androidx.activity.ComponentActivity import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.communal.shared.model.fakeGlanceableHubMultiUserHelper import com.android.systemui.concurrency.fakeExecutor import com.android.systemui.kosmos.testDispatcher import com.android.systemui.kosmos.testScope import com.android.systemui.testKosmos import com.android.systemui.util.mockito.any import com.android.systemui.util.mockito.eq import com.android.systemui.util.mockito.whenever import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.async Loading @@ -38,15 +39,22 @@ import org.junit.Before import org.junit.Test import org.junit.runner.RunWith import org.mockito.ArgumentMatchers.anyInt import org.mockito.Mock import org.mockito.MockitoAnnotations import org.mockito.kotlin.any import org.mockito.kotlin.anyOrNull import org.mockito.kotlin.argumentCaptor import org.mockito.kotlin.eq import org.mockito.kotlin.mock import org.mockito.kotlin.verify import org.mockito.kotlin.whenever @OptIn(ExperimentalCoroutinesApi::class) @SmallTest @RunWith(AndroidJUnit4::class) class WidgetConfigurationControllerTest : SysuiTestCase() { @Mock private lateinit var appWidgetHost: CommunalAppWidgetHost @Mock private lateinit var ownerActivity: ComponentActivity private val appWidgetHost = mock<CommunalAppWidgetHost>() private val ownerActivity = mock<ComponentActivity>() private val outcomeReceiverCaptor = argumentCaptor<OutcomeReceiver<IntentSender?, Throwable>>() private val kosmos = testKosmos() Loading @@ -54,18 +62,19 @@ class WidgetConfigurationControllerTest : SysuiTestCase() { @Before fun setUp() { MockitoAnnotations.initMocks(this) underTest = WidgetConfigurationController( ownerActivity, { appWidgetHost }, kosmos.testDispatcher, kosmos.fakeGlanceableHubMultiUserHelper, { kosmos.mockGlanceableHubWidgetManager }, kosmos.fakeExecutor, ) } @Test fun configurationFailsWhenActivityNotFound() = fun configureWidget_activityNotFound_returnsFalse() = with(kosmos) { testScope.runTest { whenever( Loading @@ -84,13 +93,97 @@ class WidgetConfigurationControllerTest : SysuiTestCase() { } @Test fun configurationFails() = fun configureWidget_configurationFails_returnsFalse() = with(kosmos) { testScope.runTest { val result = async { underTest.configureWidget(123) } runCurrent() assertThat(result.isCompleted).isFalse() underTest.setConfigurationResult(Activity.RESULT_CANCELED) runCurrent() assertThat(result.await()).isFalse() result.cancel() } } @Test fun configureWidget_configurationSucceeds_returnsTrue() = with(kosmos) { testScope.runTest { val result = async { underTest.configureWidget(123) } runCurrent() assertThat(result.isCompleted).isFalse() underTest.setConfigurationResult(Activity.RESULT_OK) runCurrent() assertThat(result.await()).isTrue() result.cancel() } } @Test fun configureWidget_headlessSystemUser_activityNotFound_returnsFalse() = with(kosmos) { testScope.runTest { fakeGlanceableHubMultiUserHelper.setIsInHeadlessSystemUser(true) // Activity not found whenever( mockGlanceableHubWidgetManager.getIntentSenderForConfigureActivity( anyInt(), outcomeReceiverCaptor.capture(), any(), ) ) .then { outcomeReceiverCaptor.firstValue.onError(ActivityNotFoundException()) } val result = async { underTest.configureWidget(123) } runCurrent() assertThat(result.await()).isFalse() result.cancel() } } @Test fun configureWidget_headlessSystemUser_intentSenderNull_returnsFalse() = with(kosmos) { testScope.runTest { fakeGlanceableHubMultiUserHelper.setIsInHeadlessSystemUser(true) prepareIntentSender(null) assertThat(underTest.configureWidget(123)).isFalse() } } @Test fun configureWidget_headlessSystemUser_configurationFails_returnsFalse() = with(kosmos) { testScope.runTest { fakeGlanceableHubMultiUserHelper.setIsInHeadlessSystemUser(true) val intentSender = IntentSender(Binder()) prepareIntentSender(intentSender) val result = async { underTest.configureWidget(123) } runCurrent() assertThat(result.isCompleted).isFalse() verify(ownerActivity) .startIntentSenderForResult( eq(intentSender), eq(WidgetConfigurationController.REQUEST_CODE), anyOrNull(), anyInt(), anyInt(), anyInt(), any(), ) underTest.setConfigurationResult(Activity.RESULT_CANCELED) runCurrent() Loading @@ -100,13 +193,29 @@ class WidgetConfigurationControllerTest : SysuiTestCase() { } @Test fun configurationSuccessful() = fun configureWidget_headlessSystemUser_configurationSucceeds_returnsTrue() = with(kosmos) { testScope.runTest { fakeGlanceableHubMultiUserHelper.setIsInHeadlessSystemUser(true) val intentSender = IntentSender(Binder()) prepareIntentSender(intentSender) val result = async { underTest.configureWidget(123) } runCurrent() assertThat(result.isCompleted).isFalse() verify(ownerActivity) .startIntentSenderForResult( eq(intentSender), eq(WidgetConfigurationController.REQUEST_CODE), anyOrNull(), anyInt(), anyInt(), anyInt(), any(), ) underTest.setConfigurationResult(Activity.RESULT_OK) runCurrent() Loading @@ -114,4 +223,16 @@ class WidgetConfigurationControllerTest : SysuiTestCase() { result.cancel() } } private fun prepareIntentSender(intentSender: IntentSender?) = with(kosmos) { whenever( mockGlanceableHubWidgetManager.getIntentSenderForConfigureActivity( anyInt(), outcomeReceiverCaptor.capture(), any(), ) ) .then { outcomeReceiverCaptor.firstValue.onResult(intentSender) } } }
packages/SystemUI/src/com/android/systemui/communal/widgets/GlanceableHubWidgetManager.kt +80 −11 Original line number Diff line number Diff line Loading @@ -19,7 +19,10 @@ package com.android.systemui.communal.widgets import android.appwidget.AppWidgetHost.AppWidgetHostListener import android.appwidget.AppWidgetProviderInfo import android.content.ComponentName import android.content.IntentSender import android.os.IBinder import android.os.OutcomeReceiver import android.os.RemoteException import android.os.UserHandle import android.widget.RemoteViews import com.android.server.servicewatcher.ServiceWatcher Loading @@ -27,14 +30,19 @@ import com.android.server.servicewatcher.ServiceWatcher.ServiceListener import com.android.systemui.communal.shared.model.CommunalWidgetContentModel import com.android.systemui.communal.shared.model.GlanceableHubMultiUserHelper import com.android.systemui.communal.widgets.IGlanceableHubWidgetManagerService.IAppWidgetHostListener import com.android.systemui.communal.widgets.IGlanceableHubWidgetManagerService.IConfigureWidgetCallback import com.android.systemui.communal.widgets.IGlanceableHubWidgetManagerService.IGlanceableHubWidgetsListener import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.log.LogBuffer import com.android.systemui.log.core.Logger import com.android.systemui.log.dagger.CommunalLog 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.launch /** * Manages updates to Glanceable Hub widgets and requests to edit them from the headless system Loading @@ -47,6 +55,8 @@ import kotlinx.coroutines.channels.awaitClose class GlanceableHubWidgetManager @Inject constructor( @Background private val bgExecutor: Executor, @Background private val bgScope: CoroutineScope, glanceableHubMultiUserHelper: GlanceableHubMultiUserHelper, @CommunalLog logBuffer: LogBuffer, serviceWatcherFactory: ServiceWatcherFactory<GlanceableHubWidgetManagerServiceInfo?>, Loading Loading @@ -101,8 +111,7 @@ constructor( rank: Int?, configurator: WidgetConfigurator?, ) = runOnService { service -> // TODO(b/375036327): Add support for widget configuration service.addWidget(provider, user, rank ?: -1) service.addWidget(provider, user, rank ?: -1, createIConfigureWidgetCallback(configurator)) } /** Requests the foreground user to delete a widget. */ Loading @@ -129,7 +138,42 @@ constructor( ) } /** * Requests the foreground user for the [IntentSender] to start a configuration activity for the * widget. * * @param appWidgetId Id of the widget to configure. * @param outcomeReceiver Callback for receiving the result or error. * @param executor Executor to run the callback on. */ fun getIntentSenderForConfigureActivity( appWidgetId: Int, outcomeReceiver: OutcomeReceiver<IntentSender?, Throwable>, executor: Executor, ) { bgExecutor.execute { serviceWatcher.runOnBinder( object : ServiceWatcher.BinderOperation { override fun run(binder: IBinder?) { val service = IGlanceableHubWidgetManagerService.Stub.asInterface(binder) try { val result = service.getIntentSenderForConfigureActivity(appWidgetId) executor.execute { outcomeReceiver.onResult(result) } } catch (e: RemoteException) { executor.execute { outcomeReceiver.onError(e) } } } override fun onError(t: Throwable?) { t?.let { executor.execute { outcomeReceiver.onError(t) } } } } ) } } private fun runOnService(block: (IGlanceableHubWidgetManagerService) -> Unit) { bgExecutor.execute { serviceWatcher.runOnBinder( object : ServiceWatcher.BinderOperation { override fun run(binder: IBinder?) { Loading @@ -142,6 +186,7 @@ constructor( } ) } } private fun createIAppWidgetHostListener( listener: AppWidgetHostListener Loading @@ -165,6 +210,30 @@ constructor( } } private fun createIConfigureWidgetCallback( configurator: WidgetConfigurator? ): IConfigureWidgetCallback? { return configurator?.let { object : IConfigureWidgetCallback.Stub() { override fun onConfigureWidget( appWidgetId: Int, resultReceiver: IConfigureWidgetCallback.IResultReceiver?, ) { bgScope.launch { val success = configurator.configureWidget(appWidgetId) try { resultReceiver?.onResult(success) } catch (e: RemoteException) { logger.e({ "Error reporting widget configuration result: $str1" }) { str1 = e.localizedMessage } } } } } } } companion object { private const val TAG = "GlanceableHubWidgetManager" } Loading
packages/SystemUI/src/com/android/systemui/communal/widgets/GlanceableHubWidgetManagerService.kt +60 −5 Original line number Diff line number Diff line Loading @@ -20,6 +20,7 @@ import android.appwidget.AppWidgetHost.AppWidgetHostListener import android.appwidget.AppWidgetProviderInfo import android.content.ComponentName import android.content.Intent import android.content.IntentSender import android.os.IBinder import android.os.RemoteCallbackList import android.os.RemoteException Loading @@ -30,11 +31,13 @@ import androidx.lifecycle.lifecycleScope import com.android.systemui.communal.data.repository.CommunalWidgetRepository import com.android.systemui.communal.shared.model.GlanceableHubMultiUserHelper import com.android.systemui.communal.widgets.IGlanceableHubWidgetManagerService.IAppWidgetHostListener import com.android.systemui.communal.widgets.IGlanceableHubWidgetManagerService.IConfigureWidgetCallback import com.android.systemui.communal.widgets.IGlanceableHubWidgetManagerService.IGlanceableHubWidgetsListener import com.android.systemui.log.LogBuffer import com.android.systemui.log.core.Logger import com.android.systemui.log.dagger.CommunalLog import javax.inject.Inject import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.Job import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach Loading Loading @@ -131,7 +134,12 @@ constructor( appWidgetHost.setListener(appWidgetId, createListener(listener)) } private fun addWidgetInternal(provider: ComponentName?, user: UserHandle?, rank: Int) { private fun addWidgetInternal( provider: ComponentName?, user: UserHandle?, rank: Int, callback: IConfigureWidgetCallback?, ) { if (provider == null) { throw IllegalStateException("Provider cannot be null") } Loading @@ -140,8 +148,29 @@ constructor( throw IllegalStateException("User cannot be null") } // TODO(b/375036327): Add support for widget configuration widgetRepository.addWidget(provider, user, rank, configurator = null) val configurator = callback?.let { WidgetConfigurator { appWidgetId -> try { val result = CompletableDeferred<Boolean>() val resultReceiver = object : IConfigureWidgetCallback.IResultReceiver.Stub() { override fun onResult(success: Boolean) { result.complete(success) } } callback.onConfigureWidget(appWidgetId, resultReceiver) result.await() } catch (e: RemoteException) { logger.e({ "Error configuring widget: $str1" }) { str1 = e.localizedMessage } false } } } widgetRepository.addWidget(provider, user, rank, configurator) } private fun deleteWidgetInternal(appWidgetId: Int) { Loading Loading @@ -177,6 +206,17 @@ constructor( widgetRepository.resizeWidget(appWidgetId, spanY, appWidgetIds.zip(ranks).toMap()) } private fun getIntentSenderForConfigureActivityInternal(appWidgetId: Int): IntentSender? { return try { appWidgetHost.getIntentSenderForConfigureActivity(appWidgetId, /* intentFlags= */ 0) } catch (e: IntentSender.SendIntentException) { logger.e({ "Error getting intent sender for configure activity" }) { str1 = e.localizedMessage } null } } private fun createListener(listener: IAppWidgetHostListener): AppWidgetHostListener { return object : AppWidgetHostListener { override fun onUpdateProviderInfo(appWidget: AppWidgetProviderInfo?) { Loading Loading @@ -250,11 +290,16 @@ constructor( } } override fun addWidget(provider: ComponentName?, user: UserHandle?, rank: Int) { override fun addWidget( provider: ComponentName?, user: UserHandle?, rank: Int, callback: IConfigureWidgetCallback?, ) { val iden = clearCallingIdentity() try { addWidgetInternal(provider, user, rank) addWidgetInternal(provider, user, rank, callback) } finally { restoreCallingIdentity(iden) } Loading Loading @@ -294,6 +339,16 @@ constructor( restoreCallingIdentity(iden) } } override fun getIntentSenderForConfigureActivity(appWidgetId: Int): IntentSender? { val iden = clearCallingIdentity() try { return getIntentSenderForConfigureActivityInternal(appWidgetId) } finally { restoreCallingIdentity(iden) } } } /** Loading
packages/SystemUI/src/com/android/systemui/communal/widgets/IGlanceableHubWidgetManagerService.aidl +17 −1 Original line number Diff line number Diff line Loading @@ -2,6 +2,7 @@ package com.android.systemui.communal.widgets; import android.appwidget.AppWidgetProviderInfo; import android.content.ComponentName; import android.content.IntentSender; import android.os.UserHandle; import android.widget.RemoteViews; import com.android.systemui.communal.shared.model.CommunalWidgetContentModel; Loading @@ -21,7 +22,8 @@ interface IGlanceableHubWidgetManagerService { oneway void setAppWidgetHostListener(int appWidgetId, in IAppWidgetHostListener listener); // Requests to add a widget in the Glanceable Hub. oneway void addWidget(in ComponentName provider, in UserHandle user, int rank); oneway void addWidget(in ComponentName provider, in UserHandle user, int rank, in IConfigureWidgetCallback callback); // Requests to delete a widget from the Glanceable Hub. oneway void deleteWidget(int appWidgetId); Loading @@ -32,6 +34,9 @@ interface IGlanceableHubWidgetManagerService { // Requests to resize a widget in the Glanceable Hub. oneway void resizeWidget(int appWidgetId, int spanY, in int[] appWidgetIds, in int[] ranks); // Returns the [IntentSender] for launching the configuration activity of the given widget. IntentSender getIntentSenderForConfigureActivity(int appWidgetId); // Listener for Glanceable Hub widget updates oneway interface IGlanceableHubWidgetsListener { // Called when widgets have updated. Loading @@ -48,4 +53,15 @@ interface IGlanceableHubWidgetManagerService { void onViewDataChanged(int viewId); } oneway interface IConfigureWidgetCallback { // Called when the given widget should launch its configuration activity. The caller should // report the result through the [IResultReceiver]. void onConfigureWidget(int appWidgetId, in IResultReceiver resultReceiver); interface IResultReceiver { // Called when the widget configuration operation returns a result. void onResult(boolean success); } } }