Loading src/com/android/settings/spa/app/appinfo/AppTimeSpentPreference.kt +37 −23 Original line number Diff line number Diff line Loading @@ -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), ) Preference(object : PreferenceModel { 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 }) } ) } private class AppTimeSpentPresenter( private val context: Context, private val app: ApplicationInfo, ) { private val intent = Intent(Settings.ACTION_APP_USAGE_SETTINGS).apply { 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) Loading tests/spa_unit/src/com/android/settings/spa/app/appinfo/AppTimeSpentPreferenceTest.kt +64 −49 Original line number Diff line number Diff line Loading @@ -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<PackageManager> { 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<ResolveInfo>) { whenever( packageManager.queryIntentActivitiesAsUser(any(), any<ResolveInfoFlags>(), anyInt()) ).thenReturn(resolveInfos) private fun mockActivityQueryResult(resolveInfo: ResolveInfo?) { mockPackageManager.stub { on { resolveActivityAsUser(any(), any<Int>(), any()) } doReturn resolveInfo } } @Test fun noIntentHandler_notDisplay() { mockActivitiesQueryResult(emptyList()) mockActivityQueryResult(null) composeTestRule.setContent { CompositionLocalProvider(LocalContext provides context) { Loading @@ -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<Int>(), any(), ) } @Test fun installedApp_enabled() { mockActivitiesQueryResult(listOf(MATCHED_RESOLVE_INFO)) mockActivityQueryResult(ResolveInfo()) composeTestRule.setContent { CompositionLocalProvider(LocalContext provides context) { Loading @@ -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) { Loading @@ -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() } @Test fun onClick_startActivityWithWellbeingPackageName() { mockActivityQueryResult(ResolveInfo()) 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 PACKAGE_NAME = "package.name" private const val TIME_SPENT = "15 minutes" private const val WELLBEING_PACKAGE_NAME = "wellbeing" private val INSTALLED_APP = ApplicationInfo().apply { 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 } } } } } tests/spa_unit/src/com/android/settings/testutils/FakeFeatureFactory.kt +4 −8 Original line number Diff line number Diff line Loading @@ -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<ApplicationFeatureProvider>() init { setFactory(appContext, this) Loading @@ -69,10 +66,9 @@ class FakeFeatureFactory : FeatureFactory() { override val hardwareInfoFeatureProvider: HardwareInfoFeatureProvider get() = TODO("Not yet implemented") override val metricsFeatureProvider = mockMetricsFeatureProvider override val metricsFeatureProvider = mock<MetricsFeatureProvider>() override val powerUsageFeatureProvider: PowerUsageFeatureProvider get() = TODO("Not yet implemented") override val powerUsageFeatureProvider = mock<PowerUsageFeatureProvider>() override val batteryStatusFeatureProvider: BatteryStatusFeatureProvider get() = TODO("Not yet implemented") Loading Loading
src/com/android/settings/spa/app/appinfo/AppTimeSpentPreference.kt +37 −23 Original line number Diff line number Diff line Loading @@ -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), ) Preference(object : PreferenceModel { 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 }) } ) } private class AppTimeSpentPresenter( private val context: Context, private val app: ApplicationInfo, ) { private val intent = Intent(Settings.ACTION_APP_USAGE_SETTINGS).apply { 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) Loading
tests/spa_unit/src/com/android/settings/spa/app/appinfo/AppTimeSpentPreferenceTest.kt +64 −49 Original line number Diff line number Diff line Loading @@ -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<PackageManager> { 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<ResolveInfo>) { whenever( packageManager.queryIntentActivitiesAsUser(any(), any<ResolveInfoFlags>(), anyInt()) ).thenReturn(resolveInfos) private fun mockActivityQueryResult(resolveInfo: ResolveInfo?) { mockPackageManager.stub { on { resolveActivityAsUser(any(), any<Int>(), any()) } doReturn resolveInfo } } @Test fun noIntentHandler_notDisplay() { mockActivitiesQueryResult(emptyList()) mockActivityQueryResult(null) composeTestRule.setContent { CompositionLocalProvider(LocalContext provides context) { Loading @@ -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<Int>(), any(), ) } @Test fun installedApp_enabled() { mockActivitiesQueryResult(listOf(MATCHED_RESOLVE_INFO)) mockActivityQueryResult(ResolveInfo()) composeTestRule.setContent { CompositionLocalProvider(LocalContext provides context) { Loading @@ -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) { Loading @@ -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() } @Test fun onClick_startActivityWithWellbeingPackageName() { mockActivityQueryResult(ResolveInfo()) 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 PACKAGE_NAME = "package.name" private const val TIME_SPENT = "15 minutes" private const val WELLBEING_PACKAGE_NAME = "wellbeing" private val INSTALLED_APP = ApplicationInfo().apply { 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 } } } } }
tests/spa_unit/src/com/android/settings/testutils/FakeFeatureFactory.kt +4 −8 Original line number Diff line number Diff line Loading @@ -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<ApplicationFeatureProvider>() init { setFactory(appContext, this) Loading @@ -69,10 +66,9 @@ class FakeFeatureFactory : FeatureFactory() { override val hardwareInfoFeatureProvider: HardwareInfoFeatureProvider get() = TODO("Not yet implemented") override val metricsFeatureProvider = mockMetricsFeatureProvider override val metricsFeatureProvider = mock<MetricsFeatureProvider>() override val powerUsageFeatureProvider: PowerUsageFeatureProvider get() = TODO("Not yet implemented") override val powerUsageFeatureProvider = mock<PowerUsageFeatureProvider>() override val batteryStatusFeatureProvider: BatteryStatusFeatureProvider get() = TODO("Not yet implemented") Loading