Merge "More actionable reenablement dialogs for quick affordances." into udc-dev

This commit is contained in:
Ale Nijamkin
2023-06-06 03:23:40 +00:00
committed by Android (Google) Code Review
19 changed files with 339 additions and 146 deletions

View File

@@ -913,8 +913,9 @@
android:excludeFromRecents="true"
android:launchMode="singleInstance"
android:configChanges="screenSize|smallestScreenSize|screenLayout|orientation|keyboard|keyboardHidden"
android:visibleToInstantApps="true">
</activity>
android:visibleToInstantApps="true"
android:exported="true"
/>
<activity android:name=".controls.management.ControlsEditingActivity"
android:label="@string/controls_menu_edit"
@@ -947,8 +948,9 @@
android:finishOnTaskLaunch="true"
android:launchMode="singleInstance"
android:configChanges="screenSize|smallestScreenSize|screenLayout|keyboard|keyboardHidden|orientation"
android:visibleToInstantApps="true">
</activity>
android:visibleToInstantApps="true"
android:exported="true"
/>
<activity android:name=".wallet.ui.WalletActivity"
android:label="@string/wallet_title"

View File

@@ -163,15 +163,14 @@ interface CustomizationProviderClient {
*/
val enablementActionText: String? = null,
/**
* If the affordance is disabled, this is a "component name" of the format
* `packageName/action` to be used as an `Intent` for `startActivity` when the action button
* (shown together with the set of instruction messages when the disabled affordance is
* selected) is clicked by the user. The button should help send the user to a flow that
* would help them achieve the instructions and re-enable this affordance.
* If the affordance is disabled, this is an [Intent] to be used with `startActivity` when
* the action button (shown together with the set of instruction messages when the disabled
* affordance is selected) is clicked by the user. The button should help send the user to a
* flow that would help them achieve the instructions and re-enable this affordance.
*
* If `null`, the button should not be shown.
*/
val enablementActionComponentName: String? = null,
val enablementActionIntent: Intent? = null,
/** Optional [Intent] to use to start an activity to configure this affordance. */
val configureIntent: Intent? = null,
)
@@ -337,10 +336,10 @@ class CustomizationProviderClientImpl(
Contract.LockScreenQuickAffordances.AffordanceTable.Columns
.ENABLEMENT_ACTION_TEXT
)
val enablementComponentNameColumnIndex =
val enablementActionIntentColumnIndex =
cursor.getColumnIndex(
Contract.LockScreenQuickAffordances.AffordanceTable.Columns
.ENABLEMENT_COMPONENT_NAME
.ENABLEMENT_ACTION_INTENT
)
val configureIntentColumnIndex =
cursor.getColumnIndex(
@@ -354,7 +353,7 @@ class CustomizationProviderClientImpl(
isEnabledColumnIndex == -1 ||
enablementInstructionsColumnIndex == -1 ||
enablementActionTextColumnIndex == -1 ||
enablementComponentNameColumnIndex == -1 ||
enablementActionIntentColumnIndex == -1 ||
configureIntentColumnIndex == -1
) {
return@buildList
@@ -377,12 +376,18 @@ class CustomizationProviderClientImpl(
),
enablementActionText =
cursor.getString(enablementActionTextColumnIndex),
enablementActionComponentName =
cursor.getString(enablementComponentNameColumnIndex),
enablementActionIntent =
cursor
.getString(enablementActionIntentColumnIndex)
?.toIntent(
affordanceId = affordanceId,
),
configureIntent =
cursor
.getString(configureIntentColumnIndex)
?.toIntent(affordanceId = affordanceId),
?.toIntent(
affordanceId = affordanceId,
),
)
)
}
@@ -524,7 +529,7 @@ class CustomizationProviderClientImpl(
affordanceId: String,
): Intent? {
return try {
Intent.parseUri(this, 0)
Intent.parseUri(this, Intent.URI_INTENT_SCHEME)
} catch (e: URISyntaxException) {
Log.w(TAG, "Cannot parse Uri into Intent for affordance with ID \"$affordanceId\"!")
null

View File

@@ -82,7 +82,6 @@ object CustomizationProviderContract {
val URI: Uri =
LOCK_SCREEN_QUICK_AFFORDANCE_BASE_URI.buildUpon().appendPath(TABLE_NAME).build()
const val ENABLEMENT_INSTRUCTIONS_DELIMITER = "]["
const val COMPONENT_NAME_SEPARATOR = "/"
object Columns {
/** String. Unique ID for this affordance. */
@@ -108,11 +107,11 @@ object CustomizationProviderContract {
*/
const val ENABLEMENT_ACTION_TEXT = "enablement_action_text"
/**
* String. Optional package name and activity action string, delimited by
* [COMPONENT_NAME_SEPARATOR] to use with an `Intent` to start an activity that
* opens a destination where the user can re-enable the disabled affordance.
* String. Optional URI-formatted `Intent` (formatted using
* `Intent#toUri(Intent.URI_INTENT_SCHEME)` used to start an activity that opens a
* destination where the user can re-enable the disabled affordance.
*/
const val ENABLEMENT_COMPONENT_NAME = "enablement_action_intent"
const val ENABLEMENT_ACTION_INTENT = "enablement_action_intent"
/**
* Byte array. Optional parcelled `Intent` to use to start an activity that can be
* used to configure the affordance.

View File

@@ -902,4 +902,16 @@
<!-- Time (in ms) to delay the bouncer views from showing when passive auth may be used for
device entry. -->
<integer name="primary_bouncer_passive_auth_delay">500</integer>
<!--
The package name of the app store app. If empty, features using this should be gracefully
disabled.
-->
<string name="config_appStorePackageName" translatable="false"></string>
<!-- Template for a link that leads to an app page in the relevant app store. If empty,
features using this should be gracefully disabled. If not empty, it must include a
"$packageName" part that will be replaced by the code with the package name of the target app.
-->
<string name="config_appStoreAppLinkTemplate" translatable="false"></string>
</resources>

View File

@@ -3153,4 +3153,11 @@
<!--- Content of toast triggered when the notes app entry point is triggered without setting a default notes app. [CHAR LIMIT=NONE] -->
<string name="set_default_notes_app_toast_content">Set default notes app in Settings</string>
<!--
Label for a button that, when clicked, sends the user to the app store to install an app.
[CHAR LIMIT=64].
-->
<string name="install_app">Install app</string>
</resources>

View File

@@ -17,6 +17,7 @@
package com.android.systemui.controls.controller
interface ControlsTileResourceConfiguration {
fun getPackageName(): String?
fun getTileTitleId(): Int
fun getTileImageId(): Int
}
}

View File

@@ -20,12 +20,14 @@ import com.android.systemui.R
import com.android.systemui.dagger.SysUISingleton
import javax.inject.Inject
/**
* Default Instance for ControlsTileResourceConfiguration.
*/
/** Default Instance for ControlsTileResourceConfiguration. */
@SysUISingleton
class ControlsTileResourceConfigurationImpl @Inject constructor()
: ControlsTileResourceConfiguration {
class ControlsTileResourceConfigurationImpl @Inject constructor() :
ControlsTileResourceConfiguration {
override fun getPackageName(): String? {
return null
}
override fun getTileTitleId(): Int {
return R.string.quick_controls_title
}
@@ -33,4 +35,4 @@ class ControlsTileResourceConfigurationImpl @Inject constructor()
override fun getTileImageId(): Int {
return R.drawable.controls_icon
}
}
}

View File

@@ -29,9 +29,9 @@ import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.settings.UserTracker
import com.android.systemui.statusbar.policy.KeyguardStateController
import dagger.Lazy
import kotlinx.coroutines.flow.StateFlow
import java.util.Optional
import javax.inject.Inject
import kotlinx.coroutines.flow.StateFlow
/**
* Pseudo-component to inject into classes outside `com.android.systemui.controls`.
@@ -40,26 +40,26 @@ import javax.inject.Inject
* instantiated if `featureEnabled` is true. Can also be queried for the availability of controls.
*/
@SysUISingleton
class ControlsComponent @Inject constructor(
@ControlsFeatureEnabled private val featureEnabled: Boolean,
private val context: Context,
private val lazyControlsController: Lazy<ControlsController>,
private val lazyControlsUiController: Lazy<ControlsUiController>,
private val lazyControlsListingController: Lazy<ControlsListingController>,
private val lockPatternUtils: LockPatternUtils,
private val keyguardStateController: KeyguardStateController,
private val userTracker: UserTracker,
controlsSettingsRepository: ControlsSettingsRepository,
optionalControlsTileResourceConfiguration: Optional<ControlsTileResourceConfiguration>
class ControlsComponent
@Inject
constructor(
@ControlsFeatureEnabled private val featureEnabled: Boolean,
private val context: Context,
private val lazyControlsController: Lazy<ControlsController>,
private val lazyControlsUiController: Lazy<ControlsUiController>,
private val lazyControlsListingController: Lazy<ControlsListingController>,
private val lockPatternUtils: LockPatternUtils,
private val keyguardStateController: KeyguardStateController,
private val userTracker: UserTracker,
controlsSettingsRepository: ControlsSettingsRepository,
optionalControlsTileResourceConfiguration: Optional<ControlsTileResourceConfiguration>
) {
val canShowWhileLockedSetting: StateFlow<Boolean> =
controlsSettingsRepository.canShowControlsInLockscreen
controlsSettingsRepository.canShowControlsInLockscreen
private val controlsTileResourceConfiguration: ControlsTileResourceConfiguration =
optionalControlsTileResourceConfiguration.orElse(
ControlsTileResourceConfigurationImpl()
)
optionalControlsTileResourceConfiguration.orElse(ControlsTileResourceConfigurationImpl())
fun getControlsController(): Optional<ControlsController> {
return if (featureEnabled) Optional.of(lazyControlsController.get()) else Optional.empty()
@@ -77,9 +77,7 @@ class ControlsComponent @Inject constructor(
}
}
/**
* @return true if controls are feature-enabled and the user has the setting enabled
*/
/** @return true if controls are feature-enabled and the user has the setting enabled */
fun isEnabled() = featureEnabled
/**
@@ -90,8 +88,10 @@ class ControlsComponent @Inject constructor(
*/
fun getVisibility(): Visibility {
if (!isEnabled()) return Visibility.UNAVAILABLE
if (lockPatternUtils.getStrongAuthForUser(userTracker.userHandle.identifier)
== STRONG_AUTH_REQUIRED_AFTER_BOOT) {
if (
lockPatternUtils.getStrongAuthForUser(userTracker.userHandle.identifier) ==
STRONG_AUTH_REQUIRED_AFTER_BOOT
) {
return Visibility.AVAILABLE_AFTER_UNLOCK
}
if (!canShowWhileLockedSetting.value && !keyguardStateController.isUnlocked()) {
@@ -102,7 +102,13 @@ class ControlsComponent @Inject constructor(
}
enum class Visibility {
AVAILABLE, AVAILABLE_AFTER_UNLOCK, UNAVAILABLE
AVAILABLE,
AVAILABLE_AFTER_UNLOCK,
UNAVAILABLE
}
fun getPackageName(): String? {
return controlsTileResourceConfiguration.getPackageName()
}
fun getTileTitleId(): Int {

View File

@@ -20,6 +20,7 @@ package com.android.systemui.keyguard
import android.content.ContentProvider
import android.content.ContentValues
import android.content.Context
import android.content.Intent
import android.content.UriMatcher
import android.content.pm.PackageManager
import android.content.pm.ProviderInfo
@@ -286,7 +287,7 @@ class CustomizationProvider :
Contract.LockScreenQuickAffordances.AffordanceTable.Columns
.ENABLEMENT_ACTION_TEXT,
Contract.LockScreenQuickAffordances.AffordanceTable.Columns
.ENABLEMENT_COMPONENT_NAME,
.ENABLEMENT_ACTION_INTENT,
Contract.LockScreenQuickAffordances.AffordanceTable.Columns.CONFIGURE_INTENT,
)
)
@@ -303,8 +304,8 @@ class CustomizationProvider :
.ENABLEMENT_INSTRUCTIONS_DELIMITER
),
representation.actionText,
representation.actionComponentName,
representation.configureIntent?.toUri(0),
representation.actionIntent?.toUri(Intent.URI_INTENT_SCHEME),
representation.configureIntent?.toUri(Intent.URI_INTENT_SCHEME),
)
)
}

View File

@@ -17,6 +17,7 @@
package com.android.systemui.keyguard.data.quickaffordance
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import androidx.annotation.DrawableRes
@@ -34,6 +35,7 @@ import com.android.systemui.controls.ui.ControlsActivity
import com.android.systemui.controls.ui.ControlsUiController
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceConfig.Companion.appStoreIntent
import com.android.systemui.util.kotlin.getOrNull
import javax.inject.Inject
import kotlinx.coroutines.channels.awaitClose
@@ -76,18 +78,31 @@ constructor(
component.getControlsListingController().getOrNull()?.getCurrentServices()
val hasFavorites =
component.getControlsController().getOrNull()?.getFavorites()?.isNotEmpty() == true
if (currentServices.isNullOrEmpty() || !hasFavorites) {
return KeyguardQuickAffordanceConfig.PickerScreenState.Disabled(
instructions =
listOf(
context.getString(
R.string.keyguard_affordance_enablement_dialog_home_instruction_1
),
context.getString(
R.string.keyguard_affordance_enablement_dialog_home_instruction_2
),
),
)
val componentPackageName = component.getPackageName()
when {
currentServices.isNullOrEmpty() && !componentPackageName.isNullOrEmpty() -> {
// No home app installed but we know which app we want to install.
return disabledPickerState(
actionText = context.getString(R.string.install_app),
actionIntent = appStoreIntent(context, componentPackageName),
)
}
currentServices.isNullOrEmpty() && componentPackageName.isNullOrEmpty() -> {
// No home app installed and we don't know which app we want to install.
return disabledPickerState()
}
!hasFavorites -> {
// Home app installed but no favorites selected.
val activityClass = component.getControlsUiController().get().resolveActivity()
return disabledPickerState(
actionText = context.getString(R.string.controls_open_app),
actionIntent =
Intent().apply {
component = ComponentName(context, activityClass)
putExtra(ControlsUiController.EXTRA_ANIMATE, true)
},
)
}
}
return KeyguardQuickAffordanceConfig.PickerScreenState.Default()
@@ -172,6 +187,27 @@ constructor(
}
}
private fun disabledPickerState(
actionText: String? = null,
actionIntent: Intent? = null,
): KeyguardQuickAffordanceConfig.PickerScreenState.Disabled {
check(actionIntent == null || actionText != null)
return KeyguardQuickAffordanceConfig.PickerScreenState.Disabled(
instructions =
listOf(
context.getString(
R.string.keyguard_affordance_enablement_dialog_home_instruction_1
),
context.getString(
R.string.keyguard_affordance_enablement_dialog_home_instruction_2
),
),
actionText = actionText,
actionIntent = actionIntent,
)
}
companion object {
private const val TAG = "HomeControlsKeyguardQuickAffordanceConfig"
}

View File

@@ -18,11 +18,13 @@
package com.android.systemui.keyguard.data.quickaffordance
import android.app.AlertDialog
import android.content.Context
import android.content.Intent
import android.net.Uri
import com.android.systemui.R
import com.android.systemui.animation.Expandable
import com.android.systemui.common.shared.model.Icon
import com.android.systemui.keyguard.shared.quickaffordance.ActivationState
import com.android.systemui.shared.customization.data.content.CustomizationProviderContract as Contract
import kotlinx.coroutines.flow.Flow
/** Defines interface that can act as data source for a single quick affordance model. */
@@ -80,9 +82,9 @@ interface KeyguardQuickAffordanceConfig {
/**
* The picker shows the item for selecting this affordance as disabled. Clicking on it will
* show the given instructions to the user. If [actionText] and [actionComponentName] are
* provided (optional) a button will be shown to open an activity to help the user complete
* the steps described in the instructions.
* show the given instructions to the user. If [actionText] and [actionIntent] are provided
* (optional) a button will be shown to open an activity to help the user complete the steps
* described in the instructions.
*/
data class Disabled(
/** List of human-readable instructions for setting up the quick affordance. */
@@ -93,24 +95,22 @@ interface KeyguardQuickAffordanceConfig {
*/
val actionText: String? = null,
/**
* Optional component name to be able to build an `Intent` that opens an `Activity` for
* the user to be able to set up the quick affordance and make it enabled.
*
* This is either just an action for the `Intent` or a package name and action,
* separated by
* [Contract.LockScreenQuickAffordances.AffordanceTable.COMPONENT_NAME_SEPARATOR] for
* convenience, you can use the [componentName] function.
* Optional [Intent] that opens an `Activity` for the user to be able to set up the
* quick affordance and make it enabled.
*/
val actionComponentName: String? = null,
val actionIntent: Intent? = null,
) : PickerScreenState() {
init {
check(instructions.isNotEmpty()) { "Instructions must not be empty!" }
check(
(actionText.isNullOrEmpty() && actionComponentName.isNullOrEmpty()) ||
(!actionText.isNullOrEmpty() && !actionComponentName.isNullOrEmpty())
(actionText.isNullOrEmpty() && actionIntent == null) ||
(!actionText.isNullOrEmpty() && actionIntent != null)
) {
"actionText and actionComponentName must either both be null/empty or both be" +
" non-empty!"
"""
actionText and actionIntent must either both be null/empty or both be
non-null and non-empty!
"""
.trimIndent()
}
}
}
@@ -163,17 +163,33 @@ interface KeyguardQuickAffordanceConfig {
}
companion object {
fun componentName(
packageName: String? = null,
action: String?,
): String? {
return when {
action.isNullOrEmpty() -> null
!packageName.isNullOrEmpty() ->
"$packageName${Contract.LockScreenQuickAffordances.AffordanceTable
.COMPONENT_NAME_SEPARATOR}$action"
else -> action
/**
* Returns an [Intent] that can be used to start an activity that opens the app store app to
* a page showing the app with the passed-in [packageName].
*
* If the feature isn't enabled on this device/variant/configuration, a `null` will be
* returned.
*/
fun appStoreIntent(context: Context, packageName: String?): Intent? {
if (packageName.isNullOrEmpty()) {
return null
}
val appStorePackageName = context.getString(R.string.config_appStorePackageName)
val linkTemplate = context.getString(R.string.config_appStoreAppLinkTemplate)
if (appStorePackageName.isEmpty() || linkTemplate.isEmpty()) {
return null
}
check(linkTemplate.contains(APP_PACKAGE_NAME_PLACEHOLDER))
return Intent(Intent.ACTION_VIEW).apply {
setPackage(appStorePackageName)
data = Uri.parse(linkTemplate.replace(APP_PACKAGE_NAME_PLACEHOLDER, packageName))
}
}
private const val APP_PACKAGE_NAME_PLACEHOLDER = "\$packageName"
}
}

View File

@@ -18,6 +18,7 @@
package com.android.systemui.keyguard.data.repository
import android.content.Context
import android.content.Intent
import android.os.UserHandle
import android.util.LayoutDirection
import com.android.systemui.Dumpable
@@ -176,8 +177,18 @@ constructor(
pickerState is KeyguardQuickAffordanceConfig.PickerScreenState.Default,
instructions = disabledPickerState?.instructions,
actionText = disabledPickerState?.actionText,
actionComponentName = disabledPickerState?.actionComponentName,
configureIntent = defaultPickerState?.configureIntent,
actionIntent =
disabledPickerState?.actionIntent?.apply {
addFlags(
Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_NEW_TASK
)
},
configureIntent =
defaultPickerState?.configureIntent?.apply {
addFlags(
Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_NEW_TASK
)
},
)
}
}

View File

@@ -41,11 +41,8 @@ data class KeyguardQuickAffordancePickerRepresentation(
*/
val actionText: String? = null,
/**
* If not enabled, an optional component name (package and action) for a button that takes the
* user to a destination where they can re-enable it.
*/
val actionComponentName: String? = null,
/** Optional [Intent] to use to start an activity to re-enable this affordance. */
val actionIntent: Intent? = null,
/** Optional [Intent] to use to start an activity to configure this affordance. */
val configureIntent: Intent? = null,

View File

@@ -19,6 +19,7 @@ package com.android.systemui.notetask.quickaffordance
import android.app.role.OnRoleHoldersChangedListener
import android.app.role.RoleManager
import android.content.Context
import android.content.Intent
import android.hardware.input.InputSettings
import android.os.Build
import android.os.UserHandle
@@ -42,7 +43,6 @@ import com.android.systemui.notetask.NoteTaskController
import com.android.systemui.notetask.NoteTaskEnabledKey
import com.android.systemui.notetask.NoteTaskEntryPoint.QUICK_AFFORDANCE
import com.android.systemui.notetask.NoteTaskInfoResolver
import com.android.systemui.shared.customization.data.content.CustomizationProviderContract.LockScreenQuickAffordances.AffordanceTable.COMPONENT_NAME_SEPARATOR
import com.android.systemui.stylus.StylusManager
import dagger.Lazy
import java.util.concurrent.Executor
@@ -130,8 +130,9 @@ constructor(
context.getString(
R.string.keyguard_affordance_enablement_dialog_notes_app_action
),
"${context.packageName}$COMPONENT_NAME_SEPARATOR" +
"$ACTION_MANAGE_NOTES_ROLE_FROM_QUICK_AFFORDANCE",
Intent(ACTION_MANAGE_NOTES_ROLE_FROM_QUICK_AFFORDANCE).apply {
setPackage(context.packageName)
}
)
}
else -> PickerScreenState.UnavailableOnDevice

View File

@@ -20,6 +20,7 @@ import android.testing.AndroidTestingRunner
import androidx.test.filters.SmallTest
import com.android.systemui.R
import com.android.systemui.SysuiTestCase
import com.google.common.truth.Truth.assertThat
import org.junit.Assert.assertEquals
import org.junit.Test
import org.junit.runner.RunWith
@@ -27,16 +28,20 @@ import org.junit.runner.RunWith
@RunWith(AndroidTestingRunner::class)
@SmallTest
class ControlsTileResourceConfigurationImplTest : SysuiTestCase() {
@Test
fun getPackageName() {
assertThat(ControlsTileResourceConfigurationImpl().getPackageName()).isNull()
}
@Test
fun getTileImageId() {
val instance = ControlsTileResourceConfigurationImpl()
assertEquals(instance.getTileImageId(),
R.drawable.controls_icon)
assertEquals(instance.getTileImageId(), R.drawable.controls_icon)
}
@Test
fun getTileTitleId() {
val instance = ControlsTileResourceConfigurationImpl()
assertEquals(instance.getTileTitleId(),
R.string.quick_controls_title)
assertEquals(instance.getTileTitleId(), R.string.quick_controls_title)
}
}
}

View File

@@ -22,10 +22,10 @@ import com.android.internal.widget.LockPatternUtils
import com.android.internal.widget.LockPatternUtils.StrongAuthTracker.STRONG_AUTH_NOT_REQUIRED
import com.android.internal.widget.LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_BOOT
import com.android.systemui.SysuiTestCase
import com.android.systemui.controls.settings.FakeControlsSettingsRepository
import com.android.systemui.controls.controller.ControlsController
import com.android.systemui.controls.controller.ControlsTileResourceConfiguration
import com.android.systemui.controls.management.ControlsListingController
import com.android.systemui.controls.settings.FakeControlsSettingsRepository
import com.android.systemui.controls.ui.ControlsUiController
import com.android.systemui.settings.UserTracker
import com.android.systemui.statusbar.policy.KeyguardStateController
@@ -40,32 +40,25 @@ import org.junit.runner.RunWith
import org.mockito.Answers
import org.mockito.Mock
import org.mockito.Mockito
import org.mockito.Mockito.any
import org.mockito.Mockito.anyInt
import org.mockito.Mockito.`when`
import org.mockito.Mockito.any
import org.mockito.MockitoAnnotations
@SmallTest
@RunWith(AndroidTestingRunner::class)
class ControlsComponentTest : SysuiTestCase() {
@Mock
private lateinit var controller: ControlsController
@Mock
private lateinit var uiController: ControlsUiController
@Mock
private lateinit var listingController: ControlsListingController
@Mock
private lateinit var keyguardStateController: KeyguardStateController
@Mock(answer = Answers.RETURNS_DEEP_STUBS)
private lateinit var userTracker: UserTracker
@Mock
private lateinit var lockPatternUtils: LockPatternUtils
@Mock private lateinit var controller: ControlsController
@Mock private lateinit var uiController: ControlsUiController
@Mock private lateinit var listingController: ControlsListingController
@Mock private lateinit var keyguardStateController: KeyguardStateController
@Mock(answer = Answers.RETURNS_DEEP_STUBS) private lateinit var userTracker: UserTracker
@Mock private lateinit var lockPatternUtils: LockPatternUtils
@Mock
private lateinit var optionalControlsTileResourceConfiguration:
Optional<ControlsTileResourceConfiguration>
@Mock
private lateinit var controlsTileResourceConfiguration: ControlsTileResourceConfiguration
Optional<ControlsTileResourceConfiguration>
@Mock private lateinit var controlsTileResourceConfiguration: ControlsTileResourceConfiguration
private lateinit var controlsSettingsRepository: FakeControlsSettingsRepository
@@ -123,8 +116,7 @@ class ControlsComponentTest : SysuiTestCase() {
@Test
fun testFeatureEnabledAndCannotShowOnLockScreenVisibility() {
`when`(lockPatternUtils.getStrongAuthForUser(anyInt()))
.thenReturn(STRONG_AUTH_NOT_REQUIRED)
`when`(lockPatternUtils.getStrongAuthForUser(anyInt())).thenReturn(STRONG_AUTH_NOT_REQUIRED)
`when`(keyguardStateController.isUnlocked()).thenReturn(false)
controlsSettingsRepository.setCanShowControlsInLockscreen(false)
val component = setupComponent(true)
@@ -134,8 +126,7 @@ class ControlsComponentTest : SysuiTestCase() {
@Test
fun testFeatureEnabledAndCanShowOnLockScreenVisibility() {
`when`(lockPatternUtils.getStrongAuthForUser(anyInt()))
.thenReturn(STRONG_AUTH_NOT_REQUIRED)
`when`(lockPatternUtils.getStrongAuthForUser(anyInt())).thenReturn(STRONG_AUTH_NOT_REQUIRED)
`when`(keyguardStateController.isUnlocked()).thenReturn(false)
controlsSettingsRepository.setCanShowControlsInLockscreen(true)
val component = setupComponent(true)
@@ -146,8 +137,7 @@ class ControlsComponentTest : SysuiTestCase() {
@Test
fun testFeatureEnabledAndCanShowWhileUnlockedVisibility() {
controlsSettingsRepository.setCanShowControlsInLockscreen(false)
`when`(lockPatternUtils.getStrongAuthForUser(anyInt()))
.thenReturn(STRONG_AUTH_NOT_REQUIRED)
`when`(lockPatternUtils.getStrongAuthForUser(anyInt())).thenReturn(STRONG_AUTH_NOT_REQUIRED)
`when`(keyguardStateController.isUnlocked()).thenReturn(true)
val component = setupComponent(true)
@@ -158,8 +148,7 @@ class ControlsComponentTest : SysuiTestCase() {
fun testGetTileImageId() {
val tileImageId = 0
`when`(controlsTileResourceConfiguration.getTileImageId())
.thenReturn(tileImageId)
`when`(controlsTileResourceConfiguration.getTileImageId()).thenReturn(tileImageId)
val component = setupComponent(true)
assertEquals(component.getTileImageId(), tileImageId)
}
@@ -168,12 +157,19 @@ class ControlsComponentTest : SysuiTestCase() {
fun testGetTileTitleId() {
val tileTitleId = 0
`when`(controlsTileResourceConfiguration.getTileTitleId())
.thenReturn(tileTitleId)
`when`(controlsTileResourceConfiguration.getTileTitleId()).thenReturn(tileTitleId)
val component = setupComponent(true)
assertEquals(component.getTileTitleId(), tileTitleId)
}
@Test
fun getPackageName() {
val packageName = "packageName"
`when`(controlsTileResourceConfiguration.getPackageName()).thenReturn(packageName)
val component = setupComponent(true)
assertEquals(component.getPackageName(), packageName)
}
private fun setupComponent(enabled: Boolean): ControlsComponent {
return ControlsComponent(
enabled,

View File

@@ -17,6 +17,7 @@
package com.android.systemui.keyguard.data.quickaffordance
import android.app.Activity
import androidx.test.filters.SmallTest
import com.android.systemui.R
import com.android.systemui.SysuiTestCase
@@ -24,6 +25,7 @@ import com.android.systemui.controls.ControlsServiceInfo
import com.android.systemui.controls.controller.ControlsController
import com.android.systemui.controls.dagger.ControlsComponent
import com.android.systemui.controls.management.ControlsListingController
import com.android.systemui.controls.ui.ControlsUiController
import com.android.systemui.util.mockito.mock
import com.android.systemui.util.mockito.whenever
import com.google.common.truth.Truth.assertThat
@@ -75,6 +77,7 @@ class HomeControlsKeyguardQuickAffordanceConfigParameterizedStateTest : SysuiTes
@Mock private lateinit var component: ControlsComponent
@Mock private lateinit var controlsController: ControlsController
@Mock private lateinit var controlsListingController: ControlsListingController
@Mock private lateinit var controlsUiController: ControlsUiController
@Mock private lateinit var controlsServiceInfo: ControlsServiceInfo
@Captor
private lateinit var callbackCaptor:
@@ -98,6 +101,8 @@ class HomeControlsKeyguardQuickAffordanceConfigParameterizedStateTest : SysuiTes
whenever(component.getControlsController()).thenReturn(Optional.of(controlsController))
whenever(component.getControlsListingController())
.thenReturn(Optional.of(controlsListingController))
whenever(controlsUiController.resolveActivity()).thenReturn(FakeActivity::class.java)
whenever(component.getControlsUiController()).thenReturn(Optional.of(controlsUiController))
if (hasPanels) {
whenever(controlsServiceInfo.panelActivity).thenReturn(mock())
}
@@ -178,4 +183,6 @@ class HomeControlsKeyguardQuickAffordanceConfigParameterizedStateTest : SysuiTes
)
job.cancel()
}
private class FakeActivity : Activity()
}

View File

@@ -0,0 +1,90 @@
/*
* Copyright 2023 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.systemui.keyguard.data.quickaffordance
import android.content.Intent
import android.net.Uri
import androidx.test.filters.SmallTest
import com.android.systemui.R
import com.android.systemui.SysuiTestCase
import com.google.common.truth.Truth.assertThat
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.JUnit4
@SmallTest
@RunWith(JUnit4::class)
class KeyguardQuickAffordanceConfigTest : SysuiTestCase() {
@Test
fun appStoreIntent() {
overrideResource(R.string.config_appStorePackageName, "app.store.package.name")
overrideResource(R.string.config_appStoreAppLinkTemplate, "prefix?id=\$packageName")
val packageName = "com.app.package.name"
val intent = KeyguardQuickAffordanceConfig.appStoreIntent(context, packageName)
assertThat(intent).isNotNull()
assertThat(intent?.`package`).isEqualTo("app.store.package.name")
assertThat(intent?.action).isEqualTo(Intent.ACTION_VIEW)
assertThat(intent?.data).isEqualTo(Uri.parse("prefix?id=$packageName"))
}
@Test
fun appStoreIntent_packageNameNotConfigured_returnNull() {
overrideResource(R.string.config_appStorePackageName, "")
overrideResource(R.string.config_appStoreAppLinkTemplate, "prefix?id=\$packageName")
val packageName = "com.app.package.name"
val intent = KeyguardQuickAffordanceConfig.appStoreIntent(context, packageName)
assertThat(intent).isNull()
}
@Test(expected = IllegalStateException::class)
fun appStoreIntent_packageNameMisconfigured_throwsIllegalStateException() {
overrideResource(R.string.config_appStorePackageName, "app.store.package.name")
overrideResource(
R.string.config_appStoreAppLinkTemplate,
"prefix?id=\$misconfiguredPackageName"
)
val packageName = "com.app.package.name"
KeyguardQuickAffordanceConfig.appStoreIntent(context, packageName)
}
@Test
fun appStoreIntent_linkTemplateNotConfigured_returnNull() {
overrideResource(R.string.config_appStorePackageName, "app.store.package.name")
overrideResource(R.string.config_appStoreAppLinkTemplate, "")
val packageName = "com.app.package.name"
val intent = KeyguardQuickAffordanceConfig.appStoreIntent(context, packageName)
assertThat(intent).isNull()
}
@Test
fun appStoreIntent_appPackageNameNull_returnNull() {
overrideResource(R.string.config_appStorePackageName, "app.store.package.name")
overrideResource(R.string.config_appStoreAppLinkTemplate, "prefix?id=\$packageName")
val intent = KeyguardQuickAffordanceConfig.appStoreIntent(context, null)
assertThat(intent).isNull()
}
}

View File

@@ -40,7 +40,6 @@ import com.android.systemui.notetask.LaunchNotesRoleSettingsTrampolineActivity.C
import com.android.systemui.notetask.NoteTaskController
import com.android.systemui.notetask.NoteTaskEntryPoint
import com.android.systemui.notetask.NoteTaskInfoResolver
import com.android.systemui.shared.customization.data.content.CustomizationProviderContract.LockScreenQuickAffordances.AffordanceTable.COMPONENT_NAME_SEPARATOR
import com.android.systemui.stylus.StylusManager
import com.android.systemui.util.concurrency.FakeExecutor
import com.android.systemui.util.mockito.any
@@ -283,7 +282,7 @@ internal class NoteTaskQuickAffordanceConfigTest : SysuiTestCase() {
}
@Test
fun getPickerScreenState_nodefaultNoteAppSet_shouldReturnDisable() = runTest {
fun getPickerScreenState_noDefaultNoteAppSet_shouldReturnDisabled() = runTest {
val underTest = createUnderTest(isEnabled = true)
whenever(
roleManager.getRoleHoldersAsUser(
@@ -293,16 +292,16 @@ internal class NoteTaskQuickAffordanceConfigTest : SysuiTestCase() {
)
.thenReturn(emptyList())
assertThat(underTest.getPickerScreenState())
.isEqualTo(
KeyguardQuickAffordanceConfig.PickerScreenState.Disabled(
listOf("Select a default notes app to use the notetaking shortcut"),
actionText = "Select app",
actionComponentName =
"${context.packageName}$COMPONENT_NAME_SEPARATOR" +
"$ACTION_MANAGE_NOTES_ROLE_FROM_QUICK_AFFORDANCE"
)
)
val pickerScreenState = underTest.getPickerScreenState()
assertThat(pickerScreenState is KeyguardQuickAffordanceConfig.PickerScreenState.Disabled)
.isTrue()
val disabled = pickerScreenState as KeyguardQuickAffordanceConfig.PickerScreenState.Disabled
assertThat(disabled.instructions)
.isEqualTo(listOf("Select a default notes app to use the notetaking shortcut"))
assertThat(disabled.actionText).isEqualTo("Select app")
assertThat(disabled.actionIntent?.action)
.isEqualTo(ACTION_MANAGE_NOTES_ROLE_FROM_QUICK_AFFORDANCE)
assertThat(disabled.actionIntent?.`package`).isEqualTo(context.packageName)
}
// endregion