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

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

Merge "Create DefaultAppsIconProvider" into main

parents 57add385 f8979f06
Loading
Loading
Loading
Loading
+1 −1
Original line number Diff line number Diff line
@@ -515,7 +515,7 @@ public class AssistManager {
    }

    @Nullable
    private ComponentName getAssistInfo() {
    public ComponentName getAssistInfo() {
        return getAssistInfoForUser(mSelectedUserInteractor.getSelectedUserId());
    }

+7 −3
Original line number Diff line number Diff line
@@ -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);
}
+95 −0
Original line number Diff line number Diff line
/*
 * 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"
    }
}
+154 −0
Original line number Diff line number Diff line
/*
 * 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)
}
+8 −1
Original line number Diff line number Diff line
@@ -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 }
Loading