From 426cbd623dbd4b59c68fc321de6a82e3962ae0a5 Mon Sep 17 00:00:00 2001 From: Chaohui Wang Date: Thu, 29 Aug 2024 13:17:36 +0800 Subject: [PATCH] [Safer intents] App Time Spent Preference Set package com.android.internal.R.string.config_systemWellbeing to the intent. Fix: 356117796 Flag: EXEMPT bug fix Test: manual - on App info Test: atest AppTimeSpentPreferenceTest Change-Id: I2af7b53a75fe5c6915dd9781406039822789c18c --- .../spa/app/appinfo/AppTimeSpentPreference.kt | 60 +++++---- .../app/appinfo/AppTimeSpentPreferenceTest.kt | 115 ++++++++++-------- .../settings/testutils/FakeFeatureFactory.kt | 12 +- 3 files changed, 106 insertions(+), 81 deletions(-) diff --git a/src/com/android/settings/spa/app/appinfo/AppTimeSpentPreference.kt b/src/com/android/settings/spa/app/appinfo/AppTimeSpentPreference.kt index 837df67ab8a..19953f920e8 100644 --- a/src/com/android/settings/spa/app/appinfo/AppTimeSpentPreference.kt +++ b/src/com/android/settings/spa/app/appinfo/AppTimeSpentPreference.kt @@ -19,61 +19,75 @@ package com.android.settings.spa.app.appinfo import android.content.Context import android.content.Intent import android.content.pm.ApplicationInfo -import android.content.pm.PackageManager.ResolveInfoFlags +import android.content.pm.PackageManager +import android.content.pm.ResolveInfo import android.provider.Settings import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue -import androidx.compose.runtime.livedata.observeAsState import androidx.compose.runtime.remember import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource -import androidx.lifecycle.liveData +import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.android.settings.R import com.android.settings.overlay.FeatureFactory.Companion.featureFactory import com.android.settingslib.spa.widget.preference.Preference import com.android.settingslib.spa.widget.preference.PreferenceModel +import com.android.settingslib.spaprivileged.framework.compose.placeholder import com.android.settingslib.spaprivileged.model.app.hasFlag import com.android.settingslib.spaprivileged.model.app.userHandle import com.android.settingslib.spaprivileged.model.app.userId import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.flowOn @Composable fun AppTimeSpentPreference(app: ApplicationInfo) { val context = LocalContext.current val presenter = remember(app) { AppTimeSpentPresenter(context, app) } - if (!presenter.isAvailable()) return + val isAvailable by presenter.isAvailableFlow.collectAsStateWithLifecycle(initialValue = false) + if (!isAvailable) return - val summary by presenter.summaryLiveData.observeAsState( - initial = stringResource(R.string.summary_placeholder), + val summary by presenter.summaryFlow.collectAsStateWithLifecycle(initialValue = placeholder()) + Preference( + object : PreferenceModel { + override val title = stringResource(R.string.time_spent_in_app_pref_title) + override val summary = { summary } + override val enabled = { presenter.isEnabled() } + override val onClick = presenter::startActivity + } ) - Preference(object : PreferenceModel { - override val title = stringResource(R.string.time_spent_in_app_pref_title) - override val summary = { summary } - override val enabled = { presenter.isEnabled() } - override val onClick = presenter::startActivity - }) } private class AppTimeSpentPresenter( private val context: Context, private val app: ApplicationInfo, ) { - private val intent = Intent(Settings.ACTION_APP_USAGE_SETTINGS).apply { - putExtra(Intent.EXTRA_PACKAGE_NAME, app.packageName) - } + private val intent = + Intent(Settings.ACTION_APP_USAGE_SETTINGS).apply { + // Limit the package for safer intents, since string resource is not null, + // we restrict the target to this single package. + setPackage(context.getString(com.android.internal.R.string.config_systemWellbeing)) + putExtra(Intent.EXTRA_PACKAGE_NAME, app.packageName) + } + private val appFeatureProvider = featureFactory.applicationFeatureProvider - fun isAvailable() = context.packageManager.queryIntentActivitiesAsUser( - intent, ResolveInfoFlags.of(0), app.userId - ).any { resolveInfo -> - resolveInfo?.activityInfo?.applicationInfo?.isSystemApp == true - } + val isAvailableFlow = flow { emit(resolveIntent() != null) }.flowOn(Dispatchers.Default) + + // Resolve the intent first with PackageManager.MATCH_SYSTEM_ONLY flag to ensure that + // only system apps are resolved. + private fun resolveIntent(): ResolveInfo? = + context.packageManager.resolveActivityAsUser( + intent, + PackageManager.MATCH_SYSTEM_ONLY, + app.userId, + ) fun isEnabled() = app.hasFlag(ApplicationInfo.FLAG_INSTALLED) - val summaryLiveData = liveData(Dispatchers.IO) { - emit(appFeatureProvider.getTimeSpentInApp(app.packageName).toString()) - } + val summaryFlow = + flow { emit(appFeatureProvider.getTimeSpentInApp(app.packageName).toString()) } + .flowOn(Dispatchers.Default) fun startActivity() { context.startActivityAsUser(intent, app.userHandle) diff --git a/tests/spa_unit/src/com/android/settings/spa/app/appinfo/AppTimeSpentPreferenceTest.kt b/tests/spa_unit/src/com/android/settings/spa/app/appinfo/AppTimeSpentPreferenceTest.kt index 6cc3e3c57e7..f261770ae31 100644 --- a/tests/spa_unit/src/com/android/settings/spa/app/appinfo/AppTimeSpentPreferenceTest.kt +++ b/tests/spa_unit/src/com/android/settings/spa/app/appinfo/AppTimeSpentPreferenceTest.kt @@ -17,68 +17,70 @@ package com.android.settings.spa.app.appinfo import android.content.Context -import android.content.pm.ActivityInfo import android.content.pm.ApplicationInfo import android.content.pm.PackageManager -import android.content.pm.PackageManager.ResolveInfoFlags import android.content.pm.ResolveInfo import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.test.assertIsDisplayed -import androidx.compose.ui.test.assertIsEnabled import androidx.compose.ui.test.assertIsNotDisplayed import androidx.compose.ui.test.assertIsNotEnabled +import androidx.compose.ui.test.hasText +import androidx.compose.ui.test.isEnabled import androidx.compose.ui.test.junit4.createComposeRule import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.onRoot +import androidx.compose.ui.test.performClick import androidx.test.core.app.ApplicationProvider import androidx.test.ext.junit.runners.AndroidJUnit4 import com.android.settings.R import com.android.settings.testutils.FakeFeatureFactory +import com.android.settingslib.spa.testutils.waitUntilExists import org.junit.Before import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith -import org.mockito.Mock -import org.mockito.Mockito.any -import org.mockito.Mockito.anyInt -import org.mockito.Spy -import org.mockito.junit.MockitoJUnit -import org.mockito.junit.MockitoRule -import org.mockito.Mockito.`when` as whenever +import org.mockito.kotlin.any +import org.mockito.kotlin.argThat +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.mock +import org.mockito.kotlin.spy +import org.mockito.kotlin.stub +import org.mockito.kotlin.verify +import org.mockito.kotlin.whenever @RunWith(AndroidJUnit4::class) class AppTimeSpentPreferenceTest { - @get:Rule - val mockito: MockitoRule = MockitoJUnit.rule() - @get:Rule - val composeTestRule = createComposeRule() + @get:Rule val composeTestRule = createComposeRule() - @Spy - private val context: Context = ApplicationProvider.getApplicationContext() + private val mockPackageManager = + mock { on { wellbeingPackageName } doReturn WELLBEING_PACKAGE_NAME } - @Mock - private lateinit var packageManager: PackageManager + private val context: Context = + spy(ApplicationProvider.getApplicationContext()) { + on { getString(com.android.internal.R.string.config_systemWellbeing) } doReturn + WELLBEING_PACKAGE_NAME + on { packageManager } doReturn mockPackageManager + } private val fakeFeatureFactory = FakeFeatureFactory() private val appFeatureProvider = fakeFeatureFactory.mockApplicationFeatureProvider @Before fun setUp() { - whenever(context.packageManager).thenReturn(packageManager) whenever(appFeatureProvider.getTimeSpentInApp(PACKAGE_NAME)).thenReturn(TIME_SPENT) } - private fun mockActivitiesQueryResult(resolveInfos: List) { - whenever( - packageManager.queryIntentActivitiesAsUser(any(), any(), anyInt()) - ).thenReturn(resolveInfos) + private fun mockActivityQueryResult(resolveInfo: ResolveInfo?) { + mockPackageManager.stub { + on { resolveActivityAsUser(any(), any(), any()) } doReturn resolveInfo + } } @Test fun noIntentHandler_notDisplay() { - mockActivitiesQueryResult(emptyList()) + mockActivityQueryResult(null) composeTestRule.setContent { CompositionLocalProvider(LocalContext provides context) { @@ -90,21 +92,24 @@ class AppTimeSpentPreferenceTest { } @Test - fun hasIntentHandler_notSystemApp_notDisplay() { - mockActivitiesQueryResult(listOf(ResolveInfo())) - + fun resolveActivityAsUser_calledWithWellbeingPackageName() { composeTestRule.setContent { CompositionLocalProvider(LocalContext provides context) { AppTimeSpentPreference(INSTALLED_APP) } } - composeTestRule.onRoot().assertIsNotDisplayed() + verify(mockPackageManager) + .resolveActivityAsUser( + argThat { `package` == WELLBEING_PACKAGE_NAME }, + any(), + any(), + ) } @Test fun installedApp_enabled() { - mockActivitiesQueryResult(listOf(MATCHED_RESOLVE_INFO)) + mockActivityQueryResult(ResolveInfo()) composeTestRule.setContent { CompositionLocalProvider(LocalContext provides context) { @@ -112,18 +117,16 @@ class AppTimeSpentPreferenceTest { } } - composeTestRule.onNodeWithText(context.getString(R.string.time_spent_in_app_pref_title)) - .assertIsDisplayed() - .assertIsEnabled() + composeTestRule.waitUntilExists( + hasText(context.getString(R.string.time_spent_in_app_pref_title)) and isEnabled() + ) composeTestRule.onNodeWithText(TIME_SPENT).assertIsDisplayed() } @Test fun notInstalledApp_disabled() { - mockActivitiesQueryResult(listOf(MATCHED_RESOLVE_INFO)) - val notInstalledApp = ApplicationInfo().apply { - packageName = PACKAGE_NAME - } + mockActivityQueryResult(ResolveInfo()) + val notInstalledApp = ApplicationInfo().apply { packageName = PACKAGE_NAME } composeTestRule.setContent { CompositionLocalProvider(LocalContext provides context) { @@ -131,25 +134,37 @@ class AppTimeSpentPreferenceTest { } } - composeTestRule.onNodeWithText(context.getString(R.string.time_spent_in_app_pref_title)) + composeTestRule + .onNodeWithText(context.getString(R.string.time_spent_in_app_pref_title)) .assertIsNotEnabled() } - companion object { - private const val PACKAGE_NAME = "package name" - private const val TIME_SPENT = "15 minutes" + @Test + fun onClick_startActivityWithWellbeingPackageName() { + mockActivityQueryResult(ResolveInfo()) - private val INSTALLED_APP = ApplicationInfo().apply { - packageName = PACKAGE_NAME - flags = ApplicationInfo.FLAG_INSTALLED - } - - private val MATCHED_RESOLVE_INFO = ResolveInfo().apply { - activityInfo = ActivityInfo().apply { - applicationInfo = ApplicationInfo().apply { - flags = ApplicationInfo.FLAG_SYSTEM - } + composeTestRule.setContent { + CompositionLocalProvider(LocalContext provides context) { + AppTimeSpentPreference(INSTALLED_APP) } } + composeTestRule.waitUntilExists( + hasText(context.getString(R.string.time_spent_in_app_pref_title)) and isEnabled() + ) + composeTestRule.onRoot().performClick() + + verify(context).startActivityAsUser(argThat { `package` == WELLBEING_PACKAGE_NAME }, any()) + } + + companion object { + private const val PACKAGE_NAME = "package.name" + private const val TIME_SPENT = "15 minutes" + private const val WELLBEING_PACKAGE_NAME = "wellbeing" + + private val INSTALLED_APP = + ApplicationInfo().apply { + packageName = PACKAGE_NAME + flags = ApplicationInfo.FLAG_INSTALLED + } } } diff --git a/tests/spa_unit/src/com/android/settings/testutils/FakeFeatureFactory.kt b/tests/spa_unit/src/com/android/settings/testutils/FakeFeatureFactory.kt index d7241b9419c..7c99ab1889a 100644 --- a/tests/spa_unit/src/com/android/settings/testutils/FakeFeatureFactory.kt +++ b/tests/spa_unit/src/com/android/settings/testutils/FakeFeatureFactory.kt @@ -51,14 +51,11 @@ import com.android.settings.vpn2.AdvancedVpnFeatureProvider import com.android.settings.wifi.WifiTrackerLibProvider import com.android.settings.wifi.factory.WifiFeatureProvider import com.android.settingslib.core.instrumentation.MetricsFeatureProvider -import org.mockito.Mockito.mock +import org.mockito.kotlin.mock class FakeFeatureFactory : FeatureFactory() { - private val mockMetricsFeatureProvider: MetricsFeatureProvider = - mock(MetricsFeatureProvider::class.java) - val mockApplicationFeatureProvider: ApplicationFeatureProvider = - mock(ApplicationFeatureProvider::class.java) + val mockApplicationFeatureProvider = mock() init { setFactory(appContext, this) @@ -69,10 +66,9 @@ class FakeFeatureFactory : FeatureFactory() { override val hardwareInfoFeatureProvider: HardwareInfoFeatureProvider get() = TODO("Not yet implemented") - override val metricsFeatureProvider = mockMetricsFeatureProvider + override val metricsFeatureProvider = mock() - override val powerUsageFeatureProvider: PowerUsageFeatureProvider - get() = TODO("Not yet implemented") + override val powerUsageFeatureProvider = mock() override val batteryStatusFeatureProvider: BatteryStatusFeatureProvider get() = TODO("Not yet implemented")