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
This commit is contained in:
@@ -117,6 +117,7 @@ import com.android.settingslib.widget.AdaptiveIcon;
|
|||||||
import java.util.Iterator;
|
import java.util.Iterator;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
public final class Utils extends com.android.settingslib.Utils {
|
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);
|
return inflater.inflate(resId, parent, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static ArraySet<String> getHandledDomains(PackageManager pm, String packageName) {
|
/** Gets all the domains that the given package could handled. */
|
||||||
|
@NonNull
|
||||||
|
public static Set<String> getHandledDomains(PackageManager pm, String packageName) {
|
||||||
final List<IntentFilterVerificationInfo> iviList =
|
final List<IntentFilterVerificationInfo> iviList =
|
||||||
pm.getIntentFilterVerifications(packageName);
|
pm.getIntentFilterVerifications(packageName);
|
||||||
final List<IntentFilter> filters = pm.getAllIntentFilters(packageName);
|
final List<IntentFilter> filters = pm.getAllIntentFilters(packageName);
|
||||||
@@ -597,9 +600,7 @@ public final class Utils extends com.android.settingslib.Utils {
|
|||||||
final ArraySet<String> result = new ArraySet<>();
|
final ArraySet<String> result = new ArraySet<>();
|
||||||
if (iviList != null && iviList.size() > 0) {
|
if (iviList != null && iviList.size() > 0) {
|
||||||
for (IntentFilterVerificationInfo ivi : iviList) {
|
for (IntentFilterVerificationInfo ivi : iviList) {
|
||||||
for (String host : ivi.getDomains()) {
|
result.addAll(ivi.getDomains());
|
||||||
result.add(host);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (filters != null && filters.size() > 0) {
|
if (filters != null && filters.size() > 0) {
|
||||||
|
|||||||
@@ -23,7 +23,6 @@ import android.app.settings.SettingsEnums;
|
|||||||
import android.content.pm.PackageManager;
|
import android.content.pm.PackageManager;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.text.TextUtils;
|
import android.text.TextUtils;
|
||||||
import android.util.ArraySet;
|
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
|
|
||||||
@@ -36,6 +35,8 @@ import com.android.settings.Utils;
|
|||||||
import com.android.settingslib.widget.FooterPreference;
|
import com.android.settingslib.widget.FooterPreference;
|
||||||
import com.android.settingslib.widget.SelectorWithWidgetPreference;
|
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.
|
* 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
|
@VisibleForTesting
|
||||||
void addLinksToFooter(FooterPreference footer) {
|
void addLinksToFooter(FooterPreference footer) {
|
||||||
final ArraySet<String> result = Utils.getHandledDomains(mPackageManager, mPackageName);
|
final Set<String> result = Utils.getHandledDomains(mPackageManager, mPackageName);
|
||||||
if (result.isEmpty()) {
|
if (result.isEmpty()) {
|
||||||
Log.w(TAG, "Can't find any app links.");
|
Log.w(TAG, "Can't find any app links.");
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -98,7 +98,7 @@ private fun AppInfoSettings(packageInfoPresenter: PackageInfoPresenter) {
|
|||||||
// TODO: notification_settings
|
// TODO: notification_settings
|
||||||
AppPermissionPreference(app)
|
AppPermissionPreference(app)
|
||||||
AppStoragePreference(app)
|
AppStoragePreference(app)
|
||||||
// TODO: instant_app_launch_supported_domain_urls
|
InstantAppDomainsPreference(app)
|
||||||
AppDataUsagePreference(app)
|
AppDataUsagePreference(app)
|
||||||
AppTimeSpentPreference(app)
|
AppTimeSpentPreference(app)
|
||||||
AppBatteryPreference(app)
|
AppBatteryPreference(app)
|
||||||
|
|||||||
@@ -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<Set<String>>, 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)
|
||||||
|
}
|
||||||
@@ -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<String>) {
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user