diff options
8 files changed, 345 insertions, 6 deletions
diff --git a/packages/SystemUI/src/com/android/systemui/assist/AssistManager.java b/packages/SystemUI/src/com/android/systemui/assist/AssistManager.java index d30f33f5ba2c..a67dcdb70b67 100644 --- a/packages/SystemUI/src/com/android/systemui/assist/AssistManager.java +++ b/packages/SystemUI/src/com/android/systemui/assist/AssistManager.java @@ -515,7 +515,7 @@ public class AssistManager { } @Nullable - private ComponentName getAssistInfo() { + public ComponentName getAssistInfo() { return getAssistInfoForUser(mSelectedUserInteractor.getSelectedUserId()); } diff --git a/packages/SystemUI/src/com/android/systemui/util/dagger/UtilModule.java b/packages/SystemUI/src/com/android/systemui/util/dagger/UtilModule.java index 9c8a481fcb76..501fee629e10 100644 --- a/packages/SystemUI/src/com/android/systemui/util/dagger/UtilModule.java +++ b/packages/SystemUI/src/com/android/systemui/util/dagger/UtilModule.java @@ -20,15 +20,15 @@ import com.android.systemui.util.RingerModeTracker; import com.android.systemui.util.RingerModeTrackerImpl; import com.android.systemui.util.animation.data.repository.AnimationStatusRepository; import com.android.systemui.util.animation.data.repository.AnimationStatusRepositoryImpl; +import com.android.systemui.util.icons.AppCategoryIconProvider; +import com.android.systemui.util.icons.AppCategoryIconProviderImpl; import com.android.systemui.util.wrapper.UtilWrapperModule; import dagger.Binds; import dagger.Module; /** Dagger Module for code in the util package. */ -@Module(includes = { - UtilWrapperModule.class - }) +@Module(includes = {UtilWrapperModule.class}) public interface UtilModule { /** */ @Binds @@ -37,4 +37,8 @@ public interface UtilModule { @Binds AnimationStatusRepository provideAnimationStatus( AnimationStatusRepositoryImpl ringerModeTrackerImpl); + + /** */ + @Binds + AppCategoryIconProvider appCategoryIconProvider(AppCategoryIconProviderImpl impl); } diff --git a/packages/SystemUI/src/com/android/systemui/util/icons/AppCategoryIconProvider.kt b/packages/SystemUI/src/com/android/systemui/util/icons/AppCategoryIconProvider.kt new file mode 100644 index 000000000000..6e3f8f16b623 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/util/icons/AppCategoryIconProvider.kt @@ -0,0 +1,95 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.util.icons + +import android.content.Intent +import android.content.pm.PackageInfo +import android.content.pm.PackageManager +import android.graphics.drawable.Icon +import android.os.RemoteException +import android.util.Log +import com.android.systemui.assist.AssistManager +import com.android.systemui.dagger.qualifiers.Background +import com.android.systemui.shared.system.PackageManagerWrapper +import javax.inject.Inject +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.withContext + +interface AppCategoryIconProvider { + /** Returns the [Icon] of the default assistant app, if it exists. */ + suspend fun assistantAppIcon(): Icon? + + /** + * Returns the [Icon] of the default app of [category], if it exists. Category can be for + * example [Intent.CATEGORY_APP_EMAIL] or [Intent.CATEGORY_APP_CALCULATOR]. + */ + suspend fun categoryAppIcon(category: String): Icon? +} + +class AppCategoryIconProviderImpl +@Inject +constructor( + @Background private val backgroundDispatcher: CoroutineDispatcher, + private val assistManager: AssistManager, + private val packageManager: PackageManager, + private val packageManagerWrapper: PackageManagerWrapper, +) : AppCategoryIconProvider { + + override suspend fun assistantAppIcon(): Icon? { + val assistInfo = assistManager.assistInfo ?: return null + return getPackageIcon(assistInfo.packageName) + } + + override suspend fun categoryAppIcon(category: String): Icon? { + val intent = Intent(Intent.ACTION_MAIN).apply { addCategory(category) } + val packageInfo = getPackageInfo(intent) ?: return null + return getPackageIcon(packageInfo.packageName) + } + + private suspend fun getPackageInfo(intent: Intent): PackageInfo? = + withContext(backgroundDispatcher) { + val packageName = + packageManagerWrapper + .resolveActivity(/* intent= */ intent, /* flags= */ 0) + ?.activityInfo + ?.packageName ?: return@withContext null + return@withContext getPackageInfo(packageName) + } + + private suspend fun getPackageIcon(packageName: String): Icon? { + val appInfo = getPackageInfo(packageName)?.applicationInfo ?: return null + return if (appInfo.icon != 0) { + Icon.createWithResource(appInfo.packageName, appInfo.icon) + } else { + null + } + } + + private suspend fun getPackageInfo(packageName: String): PackageInfo? = + withContext(backgroundDispatcher) { + try { + return@withContext packageManager.getPackageInfo(packageName, /* flags= */ 0) + } catch (e: RemoteException) { + Log.e(TAG, "Failed to retrieve package info for $packageName") + return@withContext null + } + } + + companion object { + private const val TAG = "DefaultAppsIconProvider" + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/util/icons/AppCategoryIconProviderTest.kt b/packages/SystemUI/tests/src/com/android/systemui/util/icons/AppCategoryIconProviderTest.kt new file mode 100644 index 000000000000..ef41b6ee69cc --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/util/icons/AppCategoryIconProviderTest.kt @@ -0,0 +1,154 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.util.icons + +import android.app.role.RoleManager.ROLE_ASSISTANT +import android.content.ComponentName +import android.content.Intent +import android.content.Intent.CATEGORY_APP_BROWSER +import android.content.Intent.CATEGORY_APP_CONTACTS +import android.content.Intent.CATEGORY_APP_EMAIL +import android.content.mockPackageManager +import android.content.mockPackageManagerWrapper +import android.content.pm.ActivityInfo +import android.content.pm.ApplicationInfo +import android.content.pm.PackageInfo +import android.content.pm.ResolveInfo +import android.graphics.drawable.Icon +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.assist.mockAssistManager +import com.android.systemui.kosmos.testScope +import com.android.systemui.testKosmos +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.kotlin.any +import org.mockito.kotlin.whenever + +@SmallTest +@RunWith(AndroidJUnit4::class) +class AppCategoryIconProviderTest : SysuiTestCase() { + + private val kosmos = testKosmos() + private val testScope = kosmos.testScope + private val packageManagerWrapper = kosmos.mockPackageManagerWrapper + private val packageManager = kosmos.mockPackageManager + private val assistManager = kosmos.mockAssistManager + private val provider = kosmos.appCategoryIconProvider + + @Before + fun setUp() { + whenever(packageManagerWrapper.resolveActivity(any<Intent>(), any<Int>())).thenAnswer { + invocation -> + val category = (invocation.arguments[0] as Intent).categories.first() + val categoryAppIcon = + categoryAppIcons.firstOrNull { it.category == category } ?: return@thenAnswer null + val activityInfo = ActivityInfo().also { it.packageName = categoryAppIcon.packageName } + return@thenAnswer ResolveInfo().also { it.activityInfo = activityInfo } + } + whenever(packageManager.getPackageInfo(any<String>(), any<Int>())).thenAnswer { invocation + -> + val packageName = invocation.arguments[0] as String + val categoryAppIcon = + categoryAppIcons.firstOrNull { it.packageName == packageName } + ?: return@thenAnswer null + val applicationInfo = + ApplicationInfo().also { + it.packageName = packageName + it.icon = categoryAppIcon.iconResId + } + return@thenAnswer PackageInfo().also { + it.packageName = packageName + it.applicationInfo = applicationInfo + } + } + } + + @Test + fun assistantAppIcon_defaultAssistantSet_returnsIcon() = + testScope.runTest { + whenever(assistManager.assistInfo) + .thenReturn(ComponentName(ASSISTANT_PACKAGE, ASSISTANT_CLASS)) + + val icon = provider.assistantAppIcon() as Icon + + assertThat(icon.resPackage).isEqualTo(ASSISTANT_PACKAGE) + assertThat(icon.resId).isEqualTo(ASSISTANT_ICON_RES_ID) + } + + @Test + fun assistantAppIcon_defaultAssistantNotSet_returnsNull() = + testScope.runTest { + whenever(assistManager.assistInfo).thenReturn(null) + + assertThat(provider.assistantAppIcon()).isNull() + } + + @Test + fun categoryAppIcon_returnsIconOfKnownBrowserApp() { + testScope.runTest { + val icon = provider.categoryAppIcon(CATEGORY_APP_BROWSER) as Icon + + assertThat(icon.resPackage).isEqualTo(BROWSER_PACKAGE) + assertThat(icon.resId).isEqualTo(BROWSER_ICON_RES_ID) + } + } + + @Test + fun categoryAppIcon_returnsIconOfKnownContactsApp() { + testScope.runTest { + val icon = provider.categoryAppIcon(CATEGORY_APP_CONTACTS) as Icon + + assertThat(icon.resPackage).isEqualTo(CONTACTS_PACKAGE) + assertThat(icon.resId).isEqualTo(CONTACTS_ICON_RES_ID) + } + } + + @Test + fun categoryAppIcon_noDefaultAppForCategoryEmail_returnsNull() { + testScope.runTest { + val icon = provider.categoryAppIcon(CATEGORY_APP_EMAIL) + + assertThat(icon).isNull() + } + } + + private companion object { + private const val ASSISTANT_PACKAGE = "the.assistant.app" + private const val ASSISTANT_CLASS = "the.assistant.app.class" + private const val ASSISTANT_ICON_RES_ID = 123 + + private const val BROWSER_PACKAGE = "com.test.browser" + private const val BROWSER_ICON_RES_ID = 1 + + private const val CONTACTS_PACKAGE = "app.test.contacts" + private const val CONTACTS_ICON_RES_ID = 234 + + private val categoryAppIcons = + listOf( + App(ROLE_ASSISTANT, ASSISTANT_PACKAGE, ASSISTANT_ICON_RES_ID), + App(CATEGORY_APP_BROWSER, BROWSER_PACKAGE, BROWSER_ICON_RES_ID), + App(CATEGORY_APP_CONTACTS, CONTACTS_PACKAGE, CONTACTS_ICON_RES_ID), + ) + } + + private class App(val category: String, val packageName: String, val iconResId: Int) +} diff --git a/packages/SystemUI/tests/utils/src/android/content/PackageManagerKosmos.kt b/packages/SystemUI/tests/utils/src/android/content/PackageManagerKosmos.kt index 8901314d8e76..9d7d916a222e 100644 --- a/packages/SystemUI/tests/utils/src/android/content/PackageManagerKosmos.kt +++ b/packages/SystemUI/tests/utils/src/android/content/PackageManagerKosmos.kt @@ -17,6 +17,13 @@ package android.content import android.content.pm.PackageManager import com.android.systemui.kosmos.Kosmos +import com.android.systemui.shared.system.PackageManagerWrapper import com.android.systemui.util.mockito.mock -val Kosmos.packageManager by Kosmos.Fixture { mock<PackageManager>() } +val Kosmos.mockPackageManager by Kosmos.Fixture { mock<PackageManager>() } + +var Kosmos.packageManager by Kosmos.Fixture { mockPackageManager } + +val Kosmos.mockPackageManagerWrapper by Kosmos.Fixture { mock<PackageManagerWrapper>() } + +var Kosmos.packageManagerWrapper by Kosmos.Fixture { mockPackageManagerWrapper } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/assist/AssistManagerKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/assist/AssistManagerKosmos.kt index b7d6f3a5f91f..22eb646ff48a 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/assist/AssistManagerKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/assist/AssistManagerKosmos.kt @@ -19,4 +19,6 @@ package com.android.systemui.assist import com.android.systemui.kosmos.Kosmos import com.android.systemui.util.mockito.mock -var Kosmos.assistManager by Kosmos.Fixture { mock<AssistManager>() } +val Kosmos.mockAssistManager by Kosmos.Fixture { mock<AssistManager>() } + +var Kosmos.assistManager by Kosmos.Fixture { mockAssistManager } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/util/icons/AppCategoryIconProviderKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/util/icons/AppCategoryIconProviderKosmos.kt new file mode 100644 index 000000000000..798718560c08 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/util/icons/AppCategoryIconProviderKosmos.kt @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.util.icons + +import android.content.mockPackageManager +import android.content.mockPackageManagerWrapper +import com.android.systemui.assist.mockAssistManager +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.testDispatcher + +var Kosmos.fakeAppCategoryIconProvider by Kosmos.Fixture { FakeAppCategoryIconProvider() } + +var Kosmos.appCategoryIconProvider: AppCategoryIconProvider by + Kosmos.Fixture { + AppCategoryIconProviderImpl( + testDispatcher, + mockAssistManager, + mockPackageManager, + mockPackageManagerWrapper + ) + } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/util/icons/FakeAppCategoryIconProvider.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/util/icons/FakeAppCategoryIconProvider.kt new file mode 100644 index 000000000000..3e7bf21112ba --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/util/icons/FakeAppCategoryIconProvider.kt @@ -0,0 +1,42 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.util.icons + +import android.app.role.RoleManager.ROLE_ASSISTANT +import android.graphics.drawable.Icon + +class FakeAppCategoryIconProvider : AppCategoryIconProvider { + + private val installedApps = mutableMapOf<String, App>() + + fun installCategoryApp(category: String, packageName: String, iconResId: Int) { + installedApps[category] = App(packageName, iconResId) + } + + fun installAssistantApp(packageName: String, iconResId: Int) { + installedApps[ROLE_ASSISTANT] = App(packageName, iconResId) + } + + override suspend fun assistantAppIcon() = categoryAppIcon(ROLE_ASSISTANT) + + override suspend fun categoryAppIcon(category: String): Icon? { + val app = installedApps[category] ?: return null + return Icon.createWithResource(app.packageName, app.iconResId) + } + + private class App(val packageName: String, val iconResId: Int) +} |