Loading src/com/android/settings/spa/app/appinfo/AppTimeSpentPreference.kt +43 −24 Original line number Diff line number Diff line Loading @@ -19,14 +19,16 @@ 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.livedata.observeAsState import androidx.compose.runtime.getValue 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.ExperimentalLifecycleComposeApi import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.android.settings.R import com.android.settings.overlay.FeatureFactory import com.android.settingslib.spa.framework.compose.stateOf Loading @@ -36,44 +38,61 @@ 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 @OptIn(ExperimentalLifecycleComposeApi::class) @Composable fun AppTimeSpentPreference(app: ApplicationInfo) { val context = LocalContext.current val presenter = remember { AppTimeSpentPresenter(context, app) } if (!presenter.isAvailable()) return val presenter = remember(app) { AppTimeSpentPresenter(context, app) } val isAvailable by presenter.isAvailableFlow.collectAsStateWithLifecycle(initialValue = false) if (!isAvailable) return Preference(object : PreferenceModel { override val title = stringResource(R.string.time_spent_in_app_pref_title) override val summary = presenter.summaryLiveData.observeAsState( initial = stringResource(R.string.summary_placeholder), val summary = presenter.summaryFlow.collectAsStateWithLifecycle( initialValue = stringResource(R.string.summary_placeholder), ) Preference( object : PreferenceModel { override val title = stringResource(R.string.time_spent_in_app_pref_title) override val summary = summary override val enabled = stateOf(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.getFactory(context) .getApplicationFeatureProvider(context) 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 +23 −44 Original line number Diff line number Diff line Loading @@ -17,17 +17,16 @@ 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 Loading @@ -35,12 +34,13 @@ 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.any 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 Loading @@ -59,39 +59,26 @@ class AppTimeSpentPreferenceTest { private val context: Context = ApplicationProvider.getApplicationContext() @Mock private lateinit var packageManager: PackageManager private lateinit var mockPackageManager: PackageManager private val fakeFeatureFactory = FakeFeatureFactory() private val appFeatureProvider = fakeFeatureFactory.mockApplicationFeatureProvider @Before fun setUp() { whenever(context.packageManager).thenReturn(packageManager) whenever(context.packageManager).thenReturn(mockPackageManager) whenever(appFeatureProvider.getTimeSpentInApp(PACKAGE_NAME)).thenReturn(TIME_SPENT) } private fun mockActivitiesQueryResult(resolveInfos: List<ResolveInfo>) { private fun mockActivityQueryResult(resolveInfo: ResolveInfo?) { whenever( packageManager.queryIntentActivitiesAsUser(any(), any<ResolveInfoFlags>(), anyInt()) ).thenReturn(resolveInfos) mockPackageManager.resolveActivityAsUser(any(), anyInt(), anyInt()) ).thenReturn(resolveInfo) } @Test fun noIntentHandler_notDisplay() { mockActivitiesQueryResult(emptyList()) composeTestRule.setContent { CompositionLocalProvider(LocalContext provides context) { AppTimeSpentPreference(INSTALLED_APP) } } composeTestRule.onRoot().assertIsNotDisplayed() } @Test fun hasIntentHandler_notSystemApp_notDisplay() { mockActivitiesQueryResult(listOf(ResolveInfo())) mockActivityQueryResult(null) composeTestRule.setContent { CompositionLocalProvider(LocalContext provides context) { Loading @@ -104,7 +91,7 @@ class AppTimeSpentPreferenceTest { @Test fun installedApp_enabled() { mockActivitiesQueryResult(listOf(MATCHED_RESOLVE_INFO)) mockActivityQueryResult(ResolveInfo()) composeTestRule.setContent { CompositionLocalProvider(LocalContext provides context) { Loading @@ -112,18 +99,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 +116,19 @@ 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 PACKAGE_NAME = "package.name" private const val TIME_SPENT = "15 minutes" 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 } } } } } Loading
src/com/android/settings/spa/app/appinfo/AppTimeSpentPreference.kt +43 −24 Original line number Diff line number Diff line Loading @@ -19,14 +19,16 @@ 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.livedata.observeAsState import androidx.compose.runtime.getValue 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.ExperimentalLifecycleComposeApi import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.android.settings.R import com.android.settings.overlay.FeatureFactory import com.android.settingslib.spa.framework.compose.stateOf Loading @@ -36,44 +38,61 @@ 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 @OptIn(ExperimentalLifecycleComposeApi::class) @Composable fun AppTimeSpentPreference(app: ApplicationInfo) { val context = LocalContext.current val presenter = remember { AppTimeSpentPresenter(context, app) } if (!presenter.isAvailable()) return val presenter = remember(app) { AppTimeSpentPresenter(context, app) } val isAvailable by presenter.isAvailableFlow.collectAsStateWithLifecycle(initialValue = false) if (!isAvailable) return Preference(object : PreferenceModel { override val title = stringResource(R.string.time_spent_in_app_pref_title) override val summary = presenter.summaryLiveData.observeAsState( initial = stringResource(R.string.summary_placeholder), val summary = presenter.summaryFlow.collectAsStateWithLifecycle( initialValue = stringResource(R.string.summary_placeholder), ) Preference( object : PreferenceModel { override val title = stringResource(R.string.time_spent_in_app_pref_title) override val summary = summary override val enabled = stateOf(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.getFactory(context) .getApplicationFeatureProvider(context) 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 +23 −44 Original line number Diff line number Diff line Loading @@ -17,17 +17,16 @@ 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 Loading @@ -35,12 +34,13 @@ 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.any 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 Loading @@ -59,39 +59,26 @@ class AppTimeSpentPreferenceTest { private val context: Context = ApplicationProvider.getApplicationContext() @Mock private lateinit var packageManager: PackageManager private lateinit var mockPackageManager: PackageManager private val fakeFeatureFactory = FakeFeatureFactory() private val appFeatureProvider = fakeFeatureFactory.mockApplicationFeatureProvider @Before fun setUp() { whenever(context.packageManager).thenReturn(packageManager) whenever(context.packageManager).thenReturn(mockPackageManager) whenever(appFeatureProvider.getTimeSpentInApp(PACKAGE_NAME)).thenReturn(TIME_SPENT) } private fun mockActivitiesQueryResult(resolveInfos: List<ResolveInfo>) { private fun mockActivityQueryResult(resolveInfo: ResolveInfo?) { whenever( packageManager.queryIntentActivitiesAsUser(any(), any<ResolveInfoFlags>(), anyInt()) ).thenReturn(resolveInfos) mockPackageManager.resolveActivityAsUser(any(), anyInt(), anyInt()) ).thenReturn(resolveInfo) } @Test fun noIntentHandler_notDisplay() { mockActivitiesQueryResult(emptyList()) composeTestRule.setContent { CompositionLocalProvider(LocalContext provides context) { AppTimeSpentPreference(INSTALLED_APP) } } composeTestRule.onRoot().assertIsNotDisplayed() } @Test fun hasIntentHandler_notSystemApp_notDisplay() { mockActivitiesQueryResult(listOf(ResolveInfo())) mockActivityQueryResult(null) composeTestRule.setContent { CompositionLocalProvider(LocalContext provides context) { Loading @@ -104,7 +91,7 @@ class AppTimeSpentPreferenceTest { @Test fun installedApp_enabled() { mockActivitiesQueryResult(listOf(MATCHED_RESOLVE_INFO)) mockActivityQueryResult(ResolveInfo()) composeTestRule.setContent { CompositionLocalProvider(LocalContext provides context) { Loading @@ -112,18 +99,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 +116,19 @@ 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 PACKAGE_NAME = "package.name" private const val TIME_SPENT = "15 minutes" 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 } } } } }