Add DefaultAppShortcuts for Spa

Including the following,
- Home app
- Browser app
- Phone app
- Emergency app
- SMS app

Bug: 236346018
Test: Manual with App Info page
Test: Settings Unit tests
Change-Id: I4ceb31ed521b758a6f91d7e86fd34c780442b1ac
This commit is contained in:
Chaohui Wang
2022-10-21 14:34:04 +08:00
parent b267f66627
commit a8e19e0f2c
4 changed files with 327 additions and 0 deletions

View File

@@ -98,6 +98,7 @@ private fun AppInfoSettings(packageInfoPresenter: PackageInfoPresenter) {
// TODO: battery
// TODO: app_language_setting
AppOpenByDefaultPreference(app)
DefaultAppShortcuts(app)
Category(title = stringResource(R.string.advanced_apps)) {
DisplayOverOtherAppsAppListProvider.InfoPageEntryItem(app)

View File

@@ -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.app.role.RoleManager
import android.content.Context
import android.content.Intent
import android.content.pm.ApplicationInfo
import androidx.annotation.StringRes
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.settingslib.spa.widget.preference.Preference
import com.android.settingslib.spa.widget.preference.PreferenceModel
import com.android.settingslib.spaprivileged.framework.common.userManager
import com.android.settingslib.spaprivileged.model.app.userHandle
import com.android.settingslib.spaprivileged.model.app.userId
import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.asExecutor
import kotlinx.coroutines.async
import kotlinx.coroutines.coroutineScope
data class DefaultAppShortcut(
val roleName: String,
@StringRes val titleResId: Int,
)
@Composable
fun DefaultAppShortcutPreference(shortcut: DefaultAppShortcut, app: ApplicationInfo) {
val context = LocalContext.current
val presenter = remember { DefaultAppShortcutPresenter(context, shortcut.roleName, app) }
if (!presenter.isAvailable()) return
if (presenter.isVisible().observeAsState().value != true) return
Preference(object : PreferenceModel {
override val title = stringResource(shortcut.titleResId)
override val summary = presenter.summaryLiveData.observeAsState(
initial = stringResource(R.string.summary_placeholder),
)
override val onClick = presenter::startActivity
})
}
private class DefaultAppShortcutPresenter(
private val context: Context,
private val roleName: String,
private val app: ApplicationInfo,
) {
private val roleManager = context.getSystemService(RoleManager::class.java)!!
private val executor = Dispatchers.IO.asExecutor()
fun isAvailable() = !context.userManager.isManagedProfile(app.userId)
fun isVisible() = liveData {
coroutineScope {
val roleVisible = async { isRoleVisible() }
val applicationVisibleForRole = async { isApplicationVisibleForRole() }
emit(roleVisible.await() && applicationVisibleForRole.await())
}
}
private suspend fun isRoleVisible(): Boolean {
return suspendCoroutine { continuation ->
roleManager.isRoleVisible(roleName, executor) {
continuation.resume(it)
}
}
}
private suspend fun isApplicationVisibleForRole() = suspendCoroutine { continuation ->
roleManager.isApplicationVisibleForRole(roleName, app.packageName, executor) {
continuation.resume(it)
}
}
val summaryLiveData = liveData(Dispatchers.IO) {
val defaultApp = roleManager.getRoleHoldersAsUser(roleName, app.userHandle).firstOrNull()
emit(context.getString(when (defaultApp) {
app.packageName -> R.string.yes
else -> R.string.no
}))
}
fun startActivity() {
val intent = Intent(Intent.ACTION_MANAGE_DEFAULT_APP).apply {
putExtra(Intent.EXTRA_ROLE_NAME, roleName)
}
context.startActivityAsUser(intent, app.userHandle)
}
}

View File

@@ -0,0 +1,37 @@
/*
* 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.role.RoleManager
import android.content.pm.ApplicationInfo
import androidx.compose.runtime.Composable
import com.android.settings.R
@Composable
fun DefaultAppShortcuts(app: ApplicationInfo) {
for (shortCut in SHORT_CUTS) {
DefaultAppShortcutPreference(shortCut, app)
}
}
private val SHORT_CUTS = listOf(
DefaultAppShortcut(RoleManager.ROLE_HOME, R.string.home_app),
DefaultAppShortcut(RoleManager.ROLE_BROWSER, R.string.default_browser_title),
DefaultAppShortcut(RoleManager.ROLE_DIALER, R.string.default_phone_title),
DefaultAppShortcut(RoleManager.ROLE_EMERGENCY, R.string.default_emergency_app),
DefaultAppShortcut(RoleManager.ROLE_SMS, R.string.sms_application_title),
)

View File

@@ -0,0 +1,178 @@
/*
* 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.role.RoleManager
import android.content.Context
import android.content.Intent
import android.content.pm.ApplicationInfo
import android.os.UserManager
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.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.settings.R
import com.android.settingslib.spaprivileged.framework.common.userManager
import com.google.common.truth.Truth.assertThat
import java.util.function.Consumer
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.ArgumentCaptor
import org.mockito.Mock
import org.mockito.Mockito.any
import org.mockito.Mockito.anyInt
import org.mockito.Mockito.doAnswer
import org.mockito.Mockito.doNothing
import org.mockito.Mockito.eq
import org.mockito.Mockito.verify
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 DefaultAppShortcutPreferenceTest {
@JvmField
@Rule
val mockito: MockitoRule = MockitoJUnit.rule()
@get:Rule
val composeTestRule = createComposeRule()
@Spy
private val context: Context = ApplicationProvider.getApplicationContext()
@Mock
private lateinit var userManager: UserManager
@Mock
private lateinit var roleManager: RoleManager
@Before
fun setUp() {
whenever(context.userManager).thenReturn(userManager)
whenever(userManager.isManagedProfile(anyInt())).thenReturn(false)
whenever(context.getSystemService(RoleManager::class.java)).thenReturn(roleManager)
mockIsRoleVisible(true)
mockIsApplicationVisibleForRole(true)
}
private fun mockIsRoleVisible(visible: Boolean) {
doAnswer {
@Suppress("UNCHECKED_CAST")
(it.arguments[2] as Consumer<Boolean>).accept(visible)
}.`when`(roleManager).isRoleVisible(eq(ROLE), any(), any())
}
private fun mockIsApplicationVisibleForRole(visible: Boolean) {
doAnswer {
@Suppress("UNCHECKED_CAST")
(it.arguments[3] as Consumer<Boolean>).accept(visible)
}.`when`(roleManager).isApplicationVisibleForRole(eq(ROLE), eq(PACKAGE_NAME), any(), any())
}
@Test
fun isManagedProfile_notDisplay() {
whenever(userManager.isManagedProfile(anyInt())).thenReturn(true)
setContent()
composeTestRule.onRoot().assertIsNotDisplayed()
}
@Test
fun roleNotVisible_notDisplay() {
mockIsRoleVisible(false)
setContent()
composeTestRule.onRoot().assertIsNotDisplayed()
}
@Test
fun applicationVisibleForRole_notDisplay() {
mockIsApplicationVisibleForRole(false)
setContent()
composeTestRule.onRoot().assertIsNotDisplayed()
}
@Test
fun isRoleHolder_summaryIsYes() {
whenever(roleManager.getRoleHoldersAsUser(eq(ROLE), any())).thenReturn(listOf(PACKAGE_NAME))
setContent()
composeTestRule.onNodeWithText(context.getString(R.string.yes))
.assertIsDisplayed()
.assertIsEnabled()
}
@Test
fun notRoleHolder_summaryIsNo() {
whenever(roleManager.getRoleHoldersAsUser(eq(ROLE), any())).thenReturn(emptyList())
setContent()
composeTestRule.onNodeWithText(context.getString(R.string.no))
.assertIsDisplayed()
.assertIsEnabled()
}
@Test
fun onClick_startManageDefaultAppIntent() {
whenever(roleManager.getRoleHoldersAsUser(eq(ROLE), any())).thenReturn(emptyList())
doNothing().`when`(context).startActivityAsUser(any(), any())
setContent()
composeTestRule.onRoot().performClick()
val intentCaptor = ArgumentCaptor.forClass(Intent::class.java)
verify(context).startActivityAsUser(intentCaptor.capture(), any())
val intent = intentCaptor.value
assertThat(intent.action).isEqualTo(Intent.ACTION_MANAGE_DEFAULT_APP)
assertThat(intent.getStringExtra(Intent.EXTRA_ROLE_NAME)).isEqualTo(ROLE)
}
private fun setContent() {
composeTestRule.setContent {
CompositionLocalProvider(LocalContext provides context) {
DefaultAppShortcutPreference(SHORTCUT, App)
}
}
}
private companion object {
const val ROLE = RoleManager.ROLE_HOME
val SHORTCUT = DefaultAppShortcut(roleName = ROLE, titleResId = R.string.home_app)
const val PACKAGE_NAME = "package name"
val App = ApplicationInfo().apply {
packageName = PACKAGE_NAME
}
}
}