From 33033fe7557090b583f8ab6ba96fccbedfea361f Mon Sep 17 00:00:00 2001 From: Chaohui Wang Date: Thu, 3 Nov 2022 16:26:37 +0800 Subject: [PATCH] Add InstantAppDomainsPreference for Spa To try: 1. adb shell am start -n com.android.settings/.spa.SpaActivity 2. Go to Apps -> All apps -> [One Instant App] -> Supported links Bug: 236346018 Test: Unit test Test: Manually with Settings App Change-Id: I344ddb9c2f3dbc47d38554bf45f04ca7c26c0e5f --- src/com/android/settings/Utils.java | 9 +- .../applications/OpenSupportedLinks.java | 5 +- .../spa/app/appinfo/AppInfoSettings.kt | 2 +- .../appinfo/InstantAppDomainsPreference.kt | 111 +++++++++++ .../InstantAppDomainsPreferenceTest.kt | 173 ++++++++++++++++++ 5 files changed, 293 insertions(+), 7 deletions(-) create mode 100644 src/com/android/settings/spa/app/appinfo/InstantAppDomainsPreference.kt create mode 100644 tests/spa_unit/src/com/android/settings/spa/app/appinfo/InstantAppDomainsPreferenceTest.kt diff --git a/src/com/android/settings/Utils.java b/src/com/android/settings/Utils.java index b2de0041b1b..8ee4ebac502 100644 --- a/src/com/android/settings/Utils.java +++ b/src/com/android/settings/Utils.java @@ -117,6 +117,7 @@ import com.android.settingslib.widget.AdaptiveIcon; import java.util.Iterator; import java.util.List; import java.util.Locale; +import java.util.Set; public final class Utils extends com.android.settingslib.Utils { @@ -589,7 +590,9 @@ public final class Utils extends com.android.settingslib.Utils { return inflater.inflate(resId, parent, false); } - public static ArraySet getHandledDomains(PackageManager pm, String packageName) { + /** Gets all the domains that the given package could handled. */ + @NonNull + public static Set getHandledDomains(PackageManager pm, String packageName) { final List iviList = pm.getIntentFilterVerifications(packageName); final List filters = pm.getAllIntentFilters(packageName); @@ -597,9 +600,7 @@ public final class Utils extends com.android.settingslib.Utils { final ArraySet result = new ArraySet<>(); if (iviList != null && iviList.size() > 0) { for (IntentFilterVerificationInfo ivi : iviList) { - for (String host : ivi.getDomains()) { - result.add(host); - } + result.addAll(ivi.getDomains()); } } if (filters != null && filters.size() > 0) { diff --git a/src/com/android/settings/applications/OpenSupportedLinks.java b/src/com/android/settings/applications/OpenSupportedLinks.java index 4f5f2a8001c..c4e478cefcb 100644 --- a/src/com/android/settings/applications/OpenSupportedLinks.java +++ b/src/com/android/settings/applications/OpenSupportedLinks.java @@ -23,7 +23,6 @@ import android.app.settings.SettingsEnums; import android.content.pm.PackageManager; import android.os.Bundle; import android.text.TextUtils; -import android.util.ArraySet; import android.util.Log; import android.view.View; @@ -36,6 +35,8 @@ import com.android.settings.Utils; import com.android.settingslib.widget.FooterPreference; import com.android.settingslib.widget.SelectorWithWidgetPreference; +import java.util.Set; + /** * Display the Open Supported Links page. Allow users choose what kind supported links they need. */ @@ -195,7 +196,7 @@ public class OpenSupportedLinks extends AppInfoWithHeader implements @VisibleForTesting void addLinksToFooter(FooterPreference footer) { - final ArraySet result = Utils.getHandledDomains(mPackageManager, mPackageName); + final Set result = Utils.getHandledDomains(mPackageManager, mPackageName); if (result.isEmpty()) { Log.w(TAG, "Can't find any app links."); return; diff --git a/src/com/android/settings/spa/app/appinfo/AppInfoSettings.kt b/src/com/android/settings/spa/app/appinfo/AppInfoSettings.kt index e3d0805434b..3a4d3f6529e 100644 --- a/src/com/android/settings/spa/app/appinfo/AppInfoSettings.kt +++ b/src/com/android/settings/spa/app/appinfo/AppInfoSettings.kt @@ -98,7 +98,7 @@ private fun AppInfoSettings(packageInfoPresenter: PackageInfoPresenter) { // TODO: notification_settings AppPermissionPreference(app) AppStoragePreference(app) - // TODO: instant_app_launch_supported_domain_urls + InstantAppDomainsPreference(app) AppDataUsagePreference(app) AppTimeSpentPreference(app) AppBatteryPreference(app) diff --git a/src/com/android/settings/spa/app/appinfo/InstantAppDomainsPreference.kt b/src/com/android/settings/spa/app/appinfo/InstantAppDomainsPreference.kt new file mode 100644 index 00000000000..3a7d50d24c9 --- /dev/null +++ b/src/com/android/settings/spa/app/appinfo/InstantAppDomainsPreference.kt @@ -0,0 +1,111 @@ +/* + * 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.ApplicationInfo +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.State +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import com.android.settings.R +import com.android.settings.Utils +import com.android.settingslib.spa.framework.compose.collectAsStateWithLifecycle +import com.android.settingslib.spa.framework.theme.SettingsDimension +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.model.app.userHandle +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.map + +@Composable +fun InstantAppDomainsPreference(app: ApplicationInfo) { + val context = LocalContext.current + if (!app.isInstantApp) return + + val presenter = remember { InstantAppDomainsPresenter(context, app) } + var openDialog by rememberSaveable { mutableStateOf(false) } + + Preference(object : PreferenceModel { + override val title = stringResource(R.string.app_launch_supported_domain_urls_title) + override val summary = presenter.summaryFlow.collectAsStateWithLifecycle( + initialValue = stringResource(R.string.summary_placeholder), + ) + override val onClick = { openDialog = true } + }) + + val domainsState = presenter.domainsFlow.collectAsStateWithLifecycle(initialValue = emptySet()) + if (openDialog) { + Dialog(domainsState) { + openDialog = false + } + } +} + +@Composable +private fun Dialog(domainsState: State>, onDismissRequest: () -> Unit) { + AlertDialog( + onDismissRequest = onDismissRequest, + confirmButton = {}, + title = { + Text(stringResource(R.string.app_launch_supported_domain_urls_title)) + }, + text = { + Column { + domainsState.value.forEach { domain -> + Text( + text = domain, + modifier = Modifier.padding(vertical = SettingsDimension.itemPaddingAround), + ) + } + } + }, + ) +} + +private class InstantAppDomainsPresenter( + private val context: Context, + private val app: ApplicationInfo, +) { + private val userContext = context.asUser(app.userHandle) + private val userPackageManager = userContext.packageManager + + val domainsFlow = flow { + emit(Utils.getHandledDomains(userPackageManager, app.packageName)) + }.flowOn(Dispatchers.IO) + + val summaryFlow = domainsFlow.map { entries -> + when (entries.size) { + 0 -> context.getString(R.string.domain_urls_summary_none) + 1 -> context.getString(R.string.domain_urls_summary_one, entries.first()) + else -> context.getString(R.string.domain_urls_summary_some, entries.first()) + } + }.flowOn(Dispatchers.IO) +} diff --git a/tests/spa_unit/src/com/android/settings/spa/app/appinfo/InstantAppDomainsPreferenceTest.kt b/tests/spa_unit/src/com/android/settings/spa/app/appinfo/InstantAppDomainsPreferenceTest.kt new file mode 100644 index 00000000000..9782817f742 --- /dev/null +++ b/tests/spa_unit/src/com/android/settings/spa/app/appinfo/InstantAppDomainsPreferenceTest.kt @@ -0,0 +1,173 @@ +/* + * 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.ApplicationInfo +import android.content.pm.PackageManager +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.filterToOne +import androidx.compose.ui.test.hasAnyAncestor +import androidx.compose.ui.test.hasText +import androidx.compose.ui.test.isDialog +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.dx.mockito.inline.extended.ExtendedMockito +import com.android.settings.R +import com.android.settings.Utils +import com.android.settings.testutils.delay +import org.junit.After +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.MockitoSession +import org.mockito.Spy +import org.mockito.quality.Strictness +import org.mockito.Mockito.`when` as whenever + +@RunWith(AndroidJUnit4::class) +class InstantAppDomainsPreferenceTest { + @get:Rule + val composeTestRule = createComposeRule() + + private lateinit var mockSession: MockitoSession + + @Spy + private val context: Context = ApplicationProvider.getApplicationContext() + + @Mock + private lateinit var packageManager: PackageManager + + @Before + fun setUp() { + mockSession = ExtendedMockito.mockitoSession() + .initMocks(this) + .mockStatic(Utils::class.java) + .strictness(Strictness.LENIENT) + .startMocking() + whenever(context.packageManager).thenReturn(packageManager) + Mockito.doReturn(context).`when`(context).createContextAsUser(any(), anyInt()) + mockDomains(emptySet()) + } + + @After + fun tearDown() { + mockSession.finishMocking() + } + + private fun mockDomains(domains: Set) { + whenever(Utils.getHandledDomains(packageManager, PACKAGE_NAME)).thenReturn(domains) + } + + @Test + fun notInstantApp_notDisplayed() { + val app = ApplicationInfo() + + setContent(app) + + composeTestRule.onRoot().assertIsNotDisplayed() + } + + @Test + fun title_displayed() { + setContent() + + composeTestRule + .onNodeWithText(context.getString(R.string.app_launch_supported_domain_urls_title)) + .assertIsDisplayed() + .assertIsEnabled() + } + + @Test + fun noDomain() { + mockDomains(emptySet()) + + setContent() + + composeTestRule.onNodeWithText(context.getString(R.string.domain_urls_summary_none)) + .assertIsDisplayed() + } + + @Test + fun oneDomain() { + mockDomains(setOf("abc")) + + setContent() + + composeTestRule.onNodeWithText("Open abc").assertIsDisplayed() + } + + @Test + fun twoDomains() { + mockDomains(setOf("abc", "def")) + + setContent() + + composeTestRule.onNodeWithText("Open abc and other URLs").assertIsDisplayed() + } + + @Test + fun whenClicked() { + mockDomains(setOf("abc", "def")) + + setContent() + composeTestRule.onRoot().performClick() + composeTestRule.delay() + + assertDialogHasText(context.getString(R.string.app_launch_supported_domain_urls_title)) + assertDialogHasText("abc") + assertDialogHasText("def") + } + + private fun assertDialogHasText(text: String) { + composeTestRule.onAllNodes(hasAnyAncestor(isDialog())) + .filterToOne(hasText(text)) + .assertIsDisplayed() + } + + private fun setContent(app:ApplicationInfo = INSTANT_APP) { + composeTestRule.setContent { + CompositionLocalProvider(LocalContext provides context) { + InstantAppDomainsPreference(app) + } + } + } + + private companion object { + const val PACKAGE_NAME = "package.name" + const val UID = 123 + + val INSTANT_APP = ApplicationInfo().apply { + packageName = PACKAGE_NAME + uid = UID + privateFlags = ApplicationInfo.PRIVATE_FLAG_INSTANT + } + } +}