diff --git a/src/com/android/settings/spa/app/appinfo/AppInfoSettings.kt b/src/com/android/settings/spa/app/appinfo/AppInfoSettings.kt index d71f889acee..82351018148 100644 --- a/src/com/android/settings/spa/app/appinfo/AppInfoSettings.kt +++ b/src/com/android/settings/spa/app/appinfo/AppInfoSettings.kt @@ -92,6 +92,7 @@ private fun AppInfoSettings(packageInfoPresenter: PackageInfoPresenter) { AppPermissionPreference(app) AppStoragePreference(app) + AppTimeSpentPreference(app) Category(title = stringResource(R.string.advanced_apps)) { DisplayOverOtherAppsAppListProvider.InfoPageEntryItem(app) diff --git a/src/com/android/settings/spa/app/appinfo/AppTimeSpentPreference.kt b/src/com/android/settings/spa/app/appinfo/AppTimeSpentPreference.kt new file mode 100644 index 00000000000..1ef893603c0 --- /dev/null +++ b/src/com/android/settings/spa/app/appinfo/AppTimeSpentPreference.kt @@ -0,0 +1,81 @@ +/* + * Copyright (C) 2022 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.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.provider.Settings +import androidx.compose.runtime.Composable +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 com.android.settings.R +import com.android.settings.overlay.FeatureFactory +import com.android.settingslib.spa.framework.compose.stateOf +import com.android.settingslib.spa.widget.preference.Preference +import com.android.settingslib.spa.widget.preference.PreferenceModel +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 + +@Composable +fun AppTimeSpentPreference(app: ApplicationInfo) { + val context = LocalContext.current + val presenter = remember { AppTimeSpentPresenter(context, app) } + if (!presenter.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), + ) + 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 { + 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 + } + + fun isEnabled() = app.hasFlag(ApplicationInfo.FLAG_INSTALLED) + + val summaryLiveData = liveData(Dispatchers.IO) { + emit(appFeatureProvider.getTimeSpentInApp(app.packageName).toString()) + } + + fun startActivity() { + context.startActivityAsUser(intent, app.userHandle) + } +} diff --git a/tests/spa_unit/src/com/android/settings/spa/app/appinfo/AppStoragePreferenceTest.kt b/tests/spa_unit/src/com/android/settings/spa/app/appinfo/AppStoragePreferenceTest.kt index 394442d4e73..39c34131518 100644 --- a/tests/spa_unit/src/com/android/settings/spa/app/appinfo/AppStoragePreferenceTest.kt +++ b/tests/spa_unit/src/com/android/settings/spa/app/appinfo/AppStoragePreferenceTest.kt @@ -54,7 +54,7 @@ class AppStoragePreferenceTest { val composeTestRule = createComposeRule() @Spy - private var context: Context = ApplicationProvider.getApplicationContext() + private val context: Context = ApplicationProvider.getApplicationContext() @Mock private lateinit var storageStatsManager: StorageStatsManager 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 new file mode 100644 index 00000000000..56ba33a1c40 --- /dev/null +++ b/tests/spa_unit/src/com/android/settings/spa/app/appinfo/AppTimeSpentPreferenceTest.kt @@ -0,0 +1,158 @@ +/* + * Copyright (C) 2022 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.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.junit4.createComposeRule +import androidx.compose.ui.test.onNodeWithText +import androidx.compose.ui.test.onRoot +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.spaprivileged.framework.common.storageStatsManager +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 +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 + +@RunWith(AndroidJUnit4::class) +class AppTimeSpentPreferenceTest { + @JvmField + @Rule + val mockito: MockitoRule = MockitoJUnit.rule() + + @get:Rule + val composeTestRule = createComposeRule() + + @Spy + private val context: Context = ApplicationProvider.getApplicationContext() + + @Mock + private lateinit var packageManager: PackageManager + + private val fakeFeatureFactory = FakeFeatureFactory() + private val appFeatureProvider = fakeFeatureFactory.applicationFeatureProvider + + @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) + } + + @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())) + + composeTestRule.setContent { + CompositionLocalProvider(LocalContext provides context) { + AppTimeSpentPreference(INSTALLED_APP) + } + } + + composeTestRule.onRoot().assertIsNotDisplayed() + } + + @Test + fun installedApp_enabled() { + mockActivitiesQueryResult(listOf(MATCHED_RESOLVE_INFO)) + + composeTestRule.setContent { + CompositionLocalProvider(LocalContext provides context) { + AppTimeSpentPreference(INSTALLED_APP) + } + } + + composeTestRule.onNodeWithText(context.getString(R.string.time_spent_in_app_pref_title)) + .assertIsDisplayed() + .assertIsEnabled() + composeTestRule.onNodeWithText(TIME_SPENT).assertIsDisplayed() + } + + @Test + fun uninstalledApp_disabled() { + mockActivitiesQueryResult(listOf(MATCHED_RESOLVE_INFO)) + val uninstalledApp = ApplicationInfo().apply { + packageName = PACKAGE_NAME + } + + composeTestRule.setContent { + CompositionLocalProvider(LocalContext provides context) { + AppTimeSpentPreference(uninstalledApp) + } + } + + 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" + + 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 + } + } + } + } +} diff --git a/tests/spa_unit/src/com/android/settings/testutils/FakeFeatureFactory.kt b/tests/spa_unit/src/com/android/settings/testutils/FakeFeatureFactory.kt new file mode 100644 index 00000000000..7a93f111493 --- /dev/null +++ b/tests/spa_unit/src/com/android/settings/testutils/FakeFeatureFactory.kt @@ -0,0 +1,175 @@ +/* + * Copyright (C) 2022 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.settings.testutils + +import android.content.Context +import com.android.settings.accessibility.AccessibilityMetricsFeatureProvider +import com.android.settings.accessibility.AccessibilitySearchFeatureProvider +import com.android.settings.accounts.AccountFeatureProvider +import com.android.settings.applications.ApplicationFeatureProvider +import com.android.settings.aware.AwareFeatureProvider +import com.android.settings.biometrics.face.FaceFeatureProvider +import com.android.settings.bluetooth.BluetoothFeatureProvider +import com.android.settings.dashboard.DashboardFeatureProvider +import com.android.settings.dashboard.suggestions.SuggestionFeatureProvider +import com.android.settings.enterprise.EnterprisePrivacyFeatureProvider +import com.android.settings.fuelgauge.BatterySettingsFeatureProvider +import com.android.settings.fuelgauge.BatteryStatusFeatureProvider +import com.android.settings.fuelgauge.PowerUsageFeatureProvider +import com.android.settings.gestures.AssistGestureFeatureProvider +import com.android.settings.homepage.contextualcards.ContextualCardFeatureProvider +import com.android.settings.localepicker.LocaleFeatureProvider +import com.android.settings.overlay.DockUpdaterFeatureProvider +import com.android.settings.overlay.FeatureFactory +import com.android.settings.overlay.SupportFeatureProvider +import com.android.settings.overlay.SurveyFeatureProvider +import com.android.settings.panel.PanelFeatureProvider +import com.android.settings.search.SearchFeatureProvider +import com.android.settings.security.SecurityFeatureProvider +import com.android.settings.security.SecuritySettingsFeatureProvider +import com.android.settings.slices.SlicesFeatureProvider +import com.android.settings.users.UserFeatureProvider +import com.android.settings.vpn2.AdvancedVpnFeatureProvider +import com.android.settings.wifi.WifiTrackerLibProvider +import com.android.settingslib.core.instrumentation.MetricsFeatureProvider +import org.mockito.Mockito.mock + +class FakeFeatureFactory : FeatureFactory() { + + val applicationFeatureProvider: ApplicationFeatureProvider = + mock(ApplicationFeatureProvider::class.java) + + init { + sFactory = this + } + + override fun getAssistGestureFeatureProvider(): AssistGestureFeatureProvider { + TODO("Not yet implemented") + } + + override fun getSuggestionFeatureProvider(): SuggestionFeatureProvider { + TODO("Not yet implemented") + } + + override fun getSupportFeatureProvider(context: Context?): SupportFeatureProvider { + TODO("Not yet implemented") + } + + override fun getMetricsFeatureProvider(): MetricsFeatureProvider { + TODO("Not yet implemented") + } + + override fun getPowerUsageFeatureProvider(context: Context?): PowerUsageFeatureProvider { + TODO("Not yet implemented") + } + + override fun getBatteryStatusFeatureProvider(context: Context?): BatteryStatusFeatureProvider { + TODO("Not yet implemented") + } + + override fun getBatterySettingsFeatureProvider( + context: Context?, + ): BatterySettingsFeatureProvider { + TODO("Not yet implemented") + } + + override fun getDashboardFeatureProvider(context: Context?): DashboardFeatureProvider { + TODO("Not yet implemented") + } + + override fun getDockUpdaterFeatureProvider(): DockUpdaterFeatureProvider { + TODO("Not yet implemented") + } + + override fun getApplicationFeatureProvider(context: Context?) = applicationFeatureProvider + + override fun getLocaleFeatureProvider(): LocaleFeatureProvider { + TODO("Not yet implemented") + } + + override fun getEnterprisePrivacyFeatureProvider( + context: Context?, + ): EnterprisePrivacyFeatureProvider { + TODO("Not yet implemented") + } + + override fun getSearchFeatureProvider(): SearchFeatureProvider { + TODO("Not yet implemented") + } + + override fun getSurveyFeatureProvider(context: Context?): SurveyFeatureProvider { + TODO("Not yet implemented") + } + + override fun getSecurityFeatureProvider(): SecurityFeatureProvider { + TODO("Not yet implemented") + } + + override fun getUserFeatureProvider(context: Context?): UserFeatureProvider { + TODO("Not yet implemented") + } + + override fun getSlicesFeatureProvider(): SlicesFeatureProvider { + TODO("Not yet implemented") + } + + override fun getAccountFeatureProvider(): AccountFeatureProvider { + TODO("Not yet implemented") + } + + override fun getPanelFeatureProvider(): PanelFeatureProvider { + TODO("Not yet implemented") + } + + override fun getContextualCardFeatureProvider( + context: Context?, + ): ContextualCardFeatureProvider { + TODO("Not yet implemented") + } + + override fun getBluetoothFeatureProvider(): BluetoothFeatureProvider { + TODO("Not yet implemented") + } + + override fun getAwareFeatureProvider(): AwareFeatureProvider { + TODO("Not yet implemented") + } + + override fun getFaceFeatureProvider(): FaceFeatureProvider { + TODO("Not yet implemented") + } + + override fun getWifiTrackerLibProvider(): WifiTrackerLibProvider { + TODO("Not yet implemented") + } + + override fun getSecuritySettingsFeatureProvider(): SecuritySettingsFeatureProvider { + TODO("Not yet implemented") + } + + override fun getAccessibilitySearchFeatureProvider(): AccessibilitySearchFeatureProvider { + TODO("Not yet implemented") + } + + override fun getAccessibilityMetricsFeatureProvider(): AccessibilityMetricsFeatureProvider { + TODO("Not yet implemented") + } + + override fun getAdvancedVpnFeatureProvider(): AdvancedVpnFeatureProvider { + TODO("Not yet implemented") + } +}