Merge "More actionable reenablement dialogs for quick affordances." into udc-dev
This commit is contained in:
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
package com.android.systemui.controls.controller
|
||||
|
||||
interface ControlsTileResourceConfiguration {
|
||||
fun getPackageName(): String?
|
||||
fun getTileTitleId(): Int
|
||||
fun getTileImageId(): Int
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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),
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user