From 6d39e5c911ab10c76a44a6d16616df07c0834c0e Mon Sep 17 00:00:00 2001 From: Chaohui Wang Date: Thu, 20 Oct 2022 14:37:47 +0800 Subject: [PATCH] Add AppOpenByDefaultPreference for Spa The "Open by Default" in App Info page. Bug: 236346018 Test: Manual with App Info page Test: Settings Unit tests Change-Id: I20f827241ff46bca28440b56fd32a0712ee439f9 --- .../spa/app/appinfo/AppInfoSettings.kt | 5 + .../spa/app/appinfo/AppInstallButton.kt | 2 +- .../app/appinfo/AppOpenByDefaultPreference.kt | 92 ++++++++ .../spa/app/appinfo/AppPermissionSummary.kt | 7 +- .../spa/app/appinfo/PackageInfoPresenter.kt | 5 +- tests/spa_unit/Android.bp | 5 +- tests/spa_unit/AndroidManifest.xml | 2 +- .../appinfo/AppOpenByDefaultPreferenceTest.kt | 202 ++++++++++++++++++ .../app/appinfo/AppStoragePreferenceTest.kt | 6 +- .../app/appinfo/AppTimeSpentPreferenceTest.kt | 6 +- 10 files changed, 318 insertions(+), 14 deletions(-) create mode 100644 src/com/android/settings/spa/app/appinfo/AppOpenByDefaultPreference.kt create mode 100644 tests/spa_unit/src/com/android/settings/spa/app/appinfo/AppOpenByDefaultPreferenceTest.kt diff --git a/src/com/android/settings/spa/app/appinfo/AppInfoSettings.kt b/src/com/android/settings/spa/app/appinfo/AppInfoSettings.kt index 82351018148..82fdc848111 100644 --- a/src/com/android/settings/spa/app/appinfo/AppInfoSettings.kt +++ b/src/com/android/settings/spa/app/appinfo/AppInfoSettings.kt @@ -92,7 +92,12 @@ private fun AppInfoSettings(packageInfoPresenter: PackageInfoPresenter) { AppPermissionPreference(app) AppStoragePreference(app) + // TODO: instant_app_launch_supported_domain_urls + // TODO: data_settings AppTimeSpentPreference(app) + // TODO: battery + // TODO: app_language_setting + AppOpenByDefaultPreference(app) Category(title = stringResource(R.string.advanced_apps)) { DisplayOverOtherAppsAppListProvider.InfoPageEntryItem(app) diff --git a/src/com/android/settings/spa/app/appinfo/AppInstallButton.kt b/src/com/android/settings/spa/app/appinfo/AppInstallButton.kt index a3ddfabb8e0..4ff246115c5 100644 --- a/src/com/android/settings/spa/app/appinfo/AppInstallButton.kt +++ b/src/com/android/settings/spa/app/appinfo/AppInstallButton.kt @@ -33,7 +33,7 @@ class AppInstallButton(private val packageInfoPresenter: PackageInfoPresenter) { val app = packageInfo.applicationInfo if (!app.isInstantApp) return null - return AppStoreUtil.getAppStoreLink(packageInfoPresenter.contextAsUser, app.packageName) + return AppStoreUtil.getAppStoreLink(packageInfoPresenter.userContext, app.packageName) ?.let { intent -> installButton(intent, app) } } diff --git a/src/com/android/settings/spa/app/appinfo/AppOpenByDefaultPreference.kt b/src/com/android/settings/spa/app/appinfo/AppOpenByDefaultPreference.kt new file mode 100644 index 00000000000..936dee61d9a --- /dev/null +++ b/src/com/android/settings/spa/app/appinfo/AppOpenByDefaultPreference.kt @@ -0,0 +1,92 @@ +/* + * 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.app.settings.SettingsEnums +import android.content.Context +import android.content.pm.ApplicationInfo +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.applications.appinfo.AppInfoDashboardFragment +import com.android.settings.applications.intentpicker.AppLaunchSettings +import com.android.settings.applications.intentpicker.IntentPickerUtils +import com.android.settingslib.applications.AppUtils +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.framework.common.asUser +import com.android.settingslib.spaprivileged.framework.common.domainVerificationManager +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 AppOpenByDefaultPreference(app: ApplicationInfo) { + val context = LocalContext.current + val presenter = remember { AppOpenByDefaultPresenter(context, app) } + if (!presenter.isAvailable()) return + + Preference(object : PreferenceModel { + override val title = stringResource(R.string.launch_by_default) + 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 AppOpenByDefaultPresenter( + private val context: Context, + private val app: ApplicationInfo, +) { + private val domainVerificationManager = context.asUser(app.userHandle).domainVerificationManager + + fun isAvailable() = + !app.isInstantApp && !AppUtils.isBrowserApp(context, app.packageName, app.userId) + + fun isEnabled() = app.hasFlag(ApplicationInfo.FLAG_INSTALLED) && app.enabled + + val summaryLiveData = liveData(Dispatchers.IO) { + emit(context.getString(when { + isLinkHandlingAllowed() -> R.string.app_link_open_always + else -> R.string.app_link_open_never + })) + } + + fun isLinkHandlingAllowed(): Boolean { + val userState = IntentPickerUtils.getDomainVerificationUserState( + domainVerificationManager, app.packageName + ) + return userState?.isLinkHandlingAllowed ?: false + } + + fun startActivity() { + AppInfoDashboardFragment.startAppInfoFragment( + AppLaunchSettings::class.java, + app, + context, + SettingsEnums.APPLICATIONS_INSTALLED_APP_DETAILS, + ) + } +} diff --git a/src/com/android/settings/spa/app/appinfo/AppPermissionSummary.kt b/src/com/android/settings/spa/app/appinfo/AppPermissionSummary.kt index 9c5f6737ef5..f73c35ae0b7 100644 --- a/src/com/android/settings/spa/app/appinfo/AppPermissionSummary.kt +++ b/src/com/android/settings/spa/app/appinfo/AppPermissionSummary.kt @@ -24,6 +24,7 @@ import androidx.lifecycle.LiveData import com.android.settings.R import com.android.settingslib.applications.PermissionsSummaryHelper import com.android.settingslib.applications.PermissionsSummaryHelper.PermissionsResultCallback +import com.android.settingslib.spaprivileged.framework.common.asUser import com.android.settingslib.spaprivileged.model.app.userHandle data class AppPermissionSummaryState( @@ -35,8 +36,8 @@ class AppPermissionSummaryLiveData( private val context: Context, private val app: ApplicationInfo, ) : LiveData() { - private val contextAsUser = context.createContextAsUser(app.userHandle, 0) - private val packageManager = contextAsUser.packageManager + private val userContext = context.asUser(app.userHandle) + private val packageManager = userContext.packageManager private val onPermissionsChangedListener = OnPermissionsChangedListener { uid -> if (uid == app.uid) update() @@ -53,7 +54,7 @@ class AppPermissionSummaryLiveData( private fun update() { PermissionsSummaryHelper.getPermissionSummary( - contextAsUser, app.packageName, permissionsCallback + userContext, app.packageName, permissionsCallback ) } diff --git a/src/com/android/settings/spa/app/appinfo/PackageInfoPresenter.kt b/src/com/android/settings/spa/app/appinfo/PackageInfoPresenter.kt index 2f5dda18290..050c0483e44 100644 --- a/src/com/android/settings/spa/app/appinfo/PackageInfoPresenter.kt +++ b/src/com/android/settings/spa/app/appinfo/PackageInfoPresenter.kt @@ -29,6 +29,7 @@ import android.util.Log import androidx.compose.runtime.Composable import com.android.settings.overlay.FeatureFactory import com.android.settingslib.spa.framework.compose.LocalNavController +import com.android.settingslib.spaprivileged.framework.common.asUser import com.android.settingslib.spaprivileged.framework.compose.DisposableBroadcastReceiverAsUser import com.android.settingslib.spaprivileged.model.app.PackageManagers import kotlinx.coroutines.CoroutineScope @@ -49,8 +50,8 @@ class PackageInfoPresenter( private val coroutineScope: CoroutineScope, ) { private val metricsFeatureProvider = FeatureFactory.getFactory(context).metricsFeatureProvider - val contextAsUser by lazy { context.createContextAsUser(UserHandle.of(userId), 0) } - val packageManagerAsUser: PackageManager by lazy { contextAsUser.packageManager } + val userContext by lazy { context.asUser(UserHandle.of(userId)) } + val packageManagerAsUser: PackageManager by lazy { userContext.packageManager } private val _flow: MutableStateFlow = MutableStateFlow(null) val flow: StateFlow = _flow diff --git a/tests/spa_unit/Android.bp b/tests/spa_unit/Android.bp index ed83ab2039f..9126c55a55c 100644 --- a/tests/spa_unit/Android.bp +++ b/tests/spa_unit/Android.bp @@ -35,9 +35,12 @@ android_test { "androidx.compose.ui_ui-test-manifest", "androidx.test.ext.junit", "androidx.test.runner", - "mockito-target-minus-junit4", + "mockito-target-inline-minus-junit4", "truth-prebuilt", ], + jni_libs: [ + "libdexmakerjvmtiagent", + ], kotlincflags: [ "-Xjvm-default=all", "-opt-in=kotlin.RequiresOptIn", diff --git a/tests/spa_unit/AndroidManifest.xml b/tests/spa_unit/AndroidManifest.xml index 5cf8ffd43b2..be16de319f0 100644 --- a/tests/spa_unit/AndroidManifest.xml +++ b/tests/spa_unit/AndroidManifest.xml @@ -19,7 +19,7 @@ xmlns:tools="http://schemas.android.com/tools" package="com.android.settings.tests.spa_unit"> - + diff --git a/tests/spa_unit/src/com/android/settings/spa/app/appinfo/AppOpenByDefaultPreferenceTest.kt b/tests/spa_unit/src/com/android/settings/spa/app/appinfo/AppOpenByDefaultPreferenceTest.kt new file mode 100644 index 00000000000..a402a022392 --- /dev/null +++ b/tests/spa_unit/src/com/android/settings/spa/app/appinfo/AppOpenByDefaultPreferenceTest.kt @@ -0,0 +1,202 @@ +/* + * 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.ResolveInfo +import android.content.pm.verify.domain.DomainVerificationManager +import android.content.pm.verify.domain.DomainVerificationUserState +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.settingslib.spaprivileged.framework.common.domainVerificationManager +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.Mockito.doReturn +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 AppOpenByDefaultPreferenceTest { + @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 + + @Mock + private lateinit var domainVerificationManager: DomainVerificationManager + + @Mock + private lateinit var allowedUserState: DomainVerificationUserState + + @Mock + private lateinit var notAllowedUserState: DomainVerificationUserState + + @Before + fun setUp() { + whenever(context.packageManager).thenReturn(packageManager) + doReturn(context).`when`(context).createContextAsUser(any(), anyInt()) + whenever(context.domainVerificationManager).thenReturn(domainVerificationManager) + whenever(allowedUserState.isLinkHandlingAllowed).thenReturn(true) + whenever(notAllowedUserState.isLinkHandlingAllowed).thenReturn(false) + } + + @Test + fun instantApp_notDisplay() { + val instantApp = ApplicationInfo().apply { + packageName = PACKAGE_NAME + privateFlags = ApplicationInfo.PRIVATE_FLAG_INSTANT + } + + composeTestRule.setContent { + CompositionLocalProvider(LocalContext provides context) { + AppOpenByDefaultPreference(instantApp) + } + } + + composeTestRule.onRoot().assertIsNotDisplayed() + } + + @Test + fun browserApp_notDisplay() { + val browserApp = ApplicationInfo().apply { + packageName = PACKAGE_NAME + privateFlags = ApplicationInfo.PRIVATE_FLAG_INSTANT + } + val resolveInfo = ResolveInfo().apply { + activityInfo = ActivityInfo() + handleAllWebDataURI = true + } + whenever(packageManager.queryIntentActivitiesAsUser(any(), anyInt(), anyInt())) + .thenReturn(listOf(resolveInfo)) + + composeTestRule.setContent { + CompositionLocalProvider(LocalContext provides context) { + AppOpenByDefaultPreference(browserApp) + } + } + + composeTestRule.onRoot().assertIsNotDisplayed() + } + + @Test + fun allowedUserState_alwaysOpen() { + whenever(domainVerificationManager.getDomainVerificationUserState(PACKAGE_NAME)) + .thenReturn(allowedUserState) + + composeTestRule.setContent { + CompositionLocalProvider(LocalContext provides context) { + AppOpenByDefaultPreference(INSTALLED_ENABLED_APP) + } + } + + composeTestRule.onNodeWithText(context.getString(R.string.launch_by_default)) + .assertIsDisplayed() + .assertIsEnabled() + composeTestRule.onNodeWithText(context.getString(R.string.app_link_open_always)) + .assertIsDisplayed() + } + + @Test + fun notAllowedUserState_neverOpen() { + whenever(domainVerificationManager.getDomainVerificationUserState(PACKAGE_NAME)) + .thenReturn(notAllowedUserState) + + composeTestRule.setContent { + CompositionLocalProvider(LocalContext provides context) { + AppOpenByDefaultPreference(INSTALLED_ENABLED_APP) + } + } + + composeTestRule.onNodeWithText(context.getString(R.string.launch_by_default)) + .assertIsDisplayed() + .assertIsEnabled() + composeTestRule.onNodeWithText(context.getString(R.string.app_link_open_never)) + .assertIsDisplayed() + } + + @Test + fun notInstalledApp_disabled() { + val notInstalledApp = ApplicationInfo().apply { + packageName = PACKAGE_NAME + } + + composeTestRule.setContent { + CompositionLocalProvider(LocalContext provides context) { + AppOpenByDefaultPreference(notInstalledApp) + } + } + + composeTestRule.onNodeWithText(context.getString(R.string.launch_by_default)) + .assertIsNotEnabled() + } + + @Test + fun notEnabledApp_disabled() { + val notEnabledApp = ApplicationInfo().apply { + packageName = PACKAGE_NAME + flags = ApplicationInfo.FLAG_INSTALLED + enabled = false + } + + composeTestRule.setContent { + CompositionLocalProvider(LocalContext provides context) { + AppOpenByDefaultPreference(notEnabledApp) + } + } + + composeTestRule.onNodeWithText(context.getString(R.string.launch_by_default)) + .assertIsNotEnabled() + } + + private companion object { + const val PACKAGE_NAME = "package name" + + val INSTALLED_ENABLED_APP = ApplicationInfo().apply { + packageName = PACKAGE_NAME + flags = ApplicationInfo.FLAG_INSTALLED + enabled = true + } + } +} 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 39c34131518..47f553bd262 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 @@ -68,12 +68,12 @@ class AppStoragePreferenceTest { } @Test - fun uninstalledApp_notDisplayed() { - val uninstalledApp = ApplicationInfo() + fun notInstalledApp_notDisplayed() { + val notInstalledApp = ApplicationInfo() composeTestRule.setContent { CompositionLocalProvider(LocalContext provides context) { - AppStoragePreference(uninstalledApp) + AppStoragePreference(notInstalledApp) } } 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 1842b831c51..e3fcdd904ae 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 @@ -120,15 +120,15 @@ class AppTimeSpentPreferenceTest { } @Test - fun uninstalledApp_disabled() { + fun notInstalledApp_disabled() { mockActivitiesQueryResult(listOf(MATCHED_RESOLVE_INFO)) - val uninstalledApp = ApplicationInfo().apply { + val notInstalledApp = ApplicationInfo().apply { packageName = PACKAGE_NAME } composeTestRule.setContent { CompositionLocalProvider(LocalContext provides context) { - AppTimeSpentPreference(uninstalledApp) + AppTimeSpentPreference(notInstalledApp) } }