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

View File

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

View File

@@ -82,7 +82,6 @@ object CustomizationProviderContract {
val URI: Uri = val URI: Uri =
LOCK_SCREEN_QUICK_AFFORDANCE_BASE_URI.buildUpon().appendPath(TABLE_NAME).build() LOCK_SCREEN_QUICK_AFFORDANCE_BASE_URI.buildUpon().appendPath(TABLE_NAME).build()
const val ENABLEMENT_INSTRUCTIONS_DELIMITER = "][" const val ENABLEMENT_INSTRUCTIONS_DELIMITER = "]["
const val COMPONENT_NAME_SEPARATOR = "/"
object Columns { object Columns {
/** String. Unique ID for this affordance. */ /** String. Unique ID for this affordance. */
@@ -108,11 +107,11 @@ object CustomizationProviderContract {
*/ */
const val ENABLEMENT_ACTION_TEXT = "enablement_action_text" const val ENABLEMENT_ACTION_TEXT = "enablement_action_text"
/** /**
* String. Optional package name and activity action string, delimited by * String. Optional URI-formatted `Intent` (formatted using
* [COMPONENT_NAME_SEPARATOR] to use with an `Intent` to start an activity that * `Intent#toUri(Intent.URI_INTENT_SCHEME)` used to start an activity that opens a
* opens a destination where the user can re-enable the disabled affordance. * 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 * Byte array. Optional parcelled `Intent` to use to start an activity that can be
* used to configure the affordance. * 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 <!-- Time (in ms) to delay the bouncer views from showing when passive auth may be used for
device entry. --> device entry. -->
<integer name="primary_bouncer_passive_auth_delay">500</integer> <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> </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] --> <!--- 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> <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> </resources>

View File

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

View File

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

View File

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

View File

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

View File

@@ -17,6 +17,7 @@
package com.android.systemui.keyguard.data.quickaffordance package com.android.systemui.keyguard.data.quickaffordance
import android.content.ComponentName
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import androidx.annotation.DrawableRes 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.controls.ui.ControlsUiController
import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Application 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 com.android.systemui.util.kotlin.getOrNull
import javax.inject.Inject import javax.inject.Inject
import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.channels.awaitClose
@@ -76,19 +78,32 @@ constructor(
component.getControlsListingController().getOrNull()?.getCurrentServices() component.getControlsListingController().getOrNull()?.getCurrentServices()
val hasFavorites = val hasFavorites =
component.getControlsController().getOrNull()?.getFavorites()?.isNotEmpty() == true component.getControlsController().getOrNull()?.getFavorites()?.isNotEmpty() == true
if (currentServices.isNullOrEmpty() || !hasFavorites) { val componentPackageName = component.getPackageName()
return KeyguardQuickAffordanceConfig.PickerScreenState.Disabled( when {
instructions = currentServices.isNullOrEmpty() && !componentPackageName.isNullOrEmpty() -> {
listOf( // No home app installed but we know which app we want to install.
context.getString( return disabledPickerState(
R.string.keyguard_affordance_enablement_dialog_home_instruction_1 actionText = context.getString(R.string.install_app),
), actionIntent = appStoreIntent(context, componentPackageName),
context.getString(
R.string.keyguard_affordance_enablement_dialog_home_instruction_2
),
),
) )
} }
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() 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 { companion object {
private const val TAG = "HomeControlsKeyguardQuickAffordanceConfig" private const val TAG = "HomeControlsKeyguardQuickAffordanceConfig"
} }

View File

@@ -18,11 +18,13 @@
package com.android.systemui.keyguard.data.quickaffordance package com.android.systemui.keyguard.data.quickaffordance
import android.app.AlertDialog import android.app.AlertDialog
import android.content.Context
import android.content.Intent import android.content.Intent
import android.net.Uri
import com.android.systemui.R
import com.android.systemui.animation.Expandable import com.android.systemui.animation.Expandable
import com.android.systemui.common.shared.model.Icon import com.android.systemui.common.shared.model.Icon
import com.android.systemui.keyguard.shared.quickaffordance.ActivationState import com.android.systemui.keyguard.shared.quickaffordance.ActivationState
import com.android.systemui.shared.customization.data.content.CustomizationProviderContract as Contract
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
/** Defines interface that can act as data source for a single quick affordance model. */ /** 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 * 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 * show the given instructions to the user. If [actionText] and [actionIntent] are provided
* provided (optional) a button will be shown to open an activity to help the user complete * (optional) a button will be shown to open an activity to help the user complete the steps
* the steps described in the instructions. * described in the instructions.
*/ */
data class Disabled( data class Disabled(
/** List of human-readable instructions for setting up the quick affordance. */ /** List of human-readable instructions for setting up the quick affordance. */
@@ -93,24 +95,22 @@ interface KeyguardQuickAffordanceConfig {
*/ */
val actionText: String? = null, val actionText: String? = null,
/** /**
* Optional component name to be able to build an `Intent` that opens an `Activity` for * Optional [Intent] that opens an `Activity` for the user to be able to set up the
* the user to be able to set up the quick affordance and make it enabled. * 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.
*/ */
val actionComponentName: String? = null, val actionIntent: Intent? = null,
) : PickerScreenState() { ) : PickerScreenState() {
init { init {
check(instructions.isNotEmpty()) { "Instructions must not be empty!" } check(instructions.isNotEmpty()) { "Instructions must not be empty!" }
check( check(
(actionText.isNullOrEmpty() && actionComponentName.isNullOrEmpty()) || (actionText.isNullOrEmpty() && actionIntent == null) ||
(!actionText.isNullOrEmpty() && !actionComponentName.isNullOrEmpty()) (!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 { companion object {
fun componentName(
packageName: String? = null, /**
action: String?, * Returns an [Intent] that can be used to start an activity that opens the app store app to
): String? { * a page showing the app with the passed-in [packageName].
return when { *
action.isNullOrEmpty() -> null * If the feature isn't enabled on this device/variant/configuration, a `null` will be
!packageName.isNullOrEmpty() -> * returned.
"$packageName${Contract.LockScreenQuickAffordances.AffordanceTable */
.COMPONENT_NAME_SEPARATOR}$action" fun appStoreIntent(context: Context, packageName: String?): Intent? {
else -> action 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 package com.android.systemui.keyguard.data.repository
import android.content.Context import android.content.Context
import android.content.Intent
import android.os.UserHandle import android.os.UserHandle
import android.util.LayoutDirection import android.util.LayoutDirection
import com.android.systemui.Dumpable import com.android.systemui.Dumpable
@@ -176,8 +177,18 @@ constructor(
pickerState is KeyguardQuickAffordanceConfig.PickerScreenState.Default, pickerState is KeyguardQuickAffordanceConfig.PickerScreenState.Default,
instructions = disabledPickerState?.instructions, instructions = disabledPickerState?.instructions,
actionText = disabledPickerState?.actionText, actionText = disabledPickerState?.actionText,
actionComponentName = disabledPickerState?.actionComponentName, actionIntent =
configureIntent = defaultPickerState?.configureIntent, 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, val actionText: String? = null,
/** /** Optional [Intent] to use to start an activity to re-enable this affordance. */
* If not enabled, an optional component name (package and action) for a button that takes the val actionIntent: Intent? = null,
* user to a destination where they can re-enable it.
*/
val actionComponentName: String? = null,
/** Optional [Intent] to use to start an activity to configure this affordance. */ /** Optional [Intent] to use to start an activity to configure this affordance. */
val configureIntent: Intent? = null, 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.OnRoleHoldersChangedListener
import android.app.role.RoleManager import android.app.role.RoleManager
import android.content.Context import android.content.Context
import android.content.Intent
import android.hardware.input.InputSettings import android.hardware.input.InputSettings
import android.os.Build import android.os.Build
import android.os.UserHandle 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.NoteTaskEnabledKey
import com.android.systemui.notetask.NoteTaskEntryPoint.QUICK_AFFORDANCE import com.android.systemui.notetask.NoteTaskEntryPoint.QUICK_AFFORDANCE
import com.android.systemui.notetask.NoteTaskInfoResolver 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.stylus.StylusManager
import dagger.Lazy import dagger.Lazy
import java.util.concurrent.Executor import java.util.concurrent.Executor
@@ -130,8 +130,9 @@ constructor(
context.getString( context.getString(
R.string.keyguard_affordance_enablement_dialog_notes_app_action R.string.keyguard_affordance_enablement_dialog_notes_app_action
), ),
"${context.packageName}$COMPONENT_NAME_SEPARATOR" + Intent(ACTION_MANAGE_NOTES_ROLE_FROM_QUICK_AFFORDANCE).apply {
"$ACTION_MANAGE_NOTES_ROLE_FROM_QUICK_AFFORDANCE", setPackage(context.packageName)
}
) )
} }
else -> PickerScreenState.UnavailableOnDevice else -> PickerScreenState.UnavailableOnDevice

View File

@@ -20,6 +20,7 @@ import android.testing.AndroidTestingRunner
import androidx.test.filters.SmallTest import androidx.test.filters.SmallTest
import com.android.systemui.R import com.android.systemui.R
import com.android.systemui.SysuiTestCase import com.android.systemui.SysuiTestCase
import com.google.common.truth.Truth.assertThat
import org.junit.Assert.assertEquals import org.junit.Assert.assertEquals
import org.junit.Test import org.junit.Test
import org.junit.runner.RunWith import org.junit.runner.RunWith
@@ -27,16 +28,20 @@ import org.junit.runner.RunWith
@RunWith(AndroidTestingRunner::class) @RunWith(AndroidTestingRunner::class)
@SmallTest @SmallTest
class ControlsTileResourceConfigurationImplTest : SysuiTestCase() { class ControlsTileResourceConfigurationImplTest : SysuiTestCase() {
@Test
fun getPackageName() {
assertThat(ControlsTileResourceConfigurationImpl().getPackageName()).isNull()
}
@Test @Test
fun getTileImageId() { fun getTileImageId() {
val instance = ControlsTileResourceConfigurationImpl() val instance = ControlsTileResourceConfigurationImpl()
assertEquals(instance.getTileImageId(), assertEquals(instance.getTileImageId(), R.drawable.controls_icon)
R.drawable.controls_icon)
} }
@Test @Test
fun getTileTitleId() { fun getTileTitleId() {
val instance = ControlsTileResourceConfigurationImpl() val instance = ControlsTileResourceConfigurationImpl()
assertEquals(instance.getTileTitleId(), assertEquals(instance.getTileTitleId(), R.string.quick_controls_title)
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_NOT_REQUIRED
import com.android.internal.widget.LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_BOOT import com.android.internal.widget.LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_BOOT
import com.android.systemui.SysuiTestCase 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.ControlsController
import com.android.systemui.controls.controller.ControlsTileResourceConfiguration import com.android.systemui.controls.controller.ControlsTileResourceConfiguration
import com.android.systemui.controls.management.ControlsListingController 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.controls.ui.ControlsUiController
import com.android.systemui.settings.UserTracker import com.android.systemui.settings.UserTracker
import com.android.systemui.statusbar.policy.KeyguardStateController import com.android.systemui.statusbar.policy.KeyguardStateController
@@ -40,32 +40,25 @@ import org.junit.runner.RunWith
import org.mockito.Answers import org.mockito.Answers
import org.mockito.Mock import org.mockito.Mock
import org.mockito.Mockito import org.mockito.Mockito
import org.mockito.Mockito.any
import org.mockito.Mockito.anyInt import org.mockito.Mockito.anyInt
import org.mockito.Mockito.`when` import org.mockito.Mockito.`when`
import org.mockito.Mockito.any
import org.mockito.MockitoAnnotations import org.mockito.MockitoAnnotations
@SmallTest @SmallTest
@RunWith(AndroidTestingRunner::class) @RunWith(AndroidTestingRunner::class)
class ControlsComponentTest : SysuiTestCase() { class ControlsComponentTest : SysuiTestCase() {
@Mock @Mock private lateinit var controller: ControlsController
private lateinit var controller: ControlsController @Mock private lateinit var uiController: ControlsUiController
@Mock @Mock private lateinit var listingController: ControlsListingController
private lateinit var uiController: ControlsUiController @Mock private lateinit var keyguardStateController: KeyguardStateController
@Mock @Mock(answer = Answers.RETURNS_DEEP_STUBS) private lateinit var userTracker: UserTracker
private lateinit var listingController: ControlsListingController @Mock private lateinit var lockPatternUtils: LockPatternUtils
@Mock
private lateinit var keyguardStateController: KeyguardStateController
@Mock(answer = Answers.RETURNS_DEEP_STUBS)
private lateinit var userTracker: UserTracker
@Mock
private lateinit var lockPatternUtils: LockPatternUtils
@Mock @Mock
private lateinit var optionalControlsTileResourceConfiguration: private lateinit var optionalControlsTileResourceConfiguration:
Optional<ControlsTileResourceConfiguration> Optional<ControlsTileResourceConfiguration>
@Mock @Mock private lateinit var controlsTileResourceConfiguration: ControlsTileResourceConfiguration
private lateinit var controlsTileResourceConfiguration: ControlsTileResourceConfiguration
private lateinit var controlsSettingsRepository: FakeControlsSettingsRepository private lateinit var controlsSettingsRepository: FakeControlsSettingsRepository
@@ -123,8 +116,7 @@ class ControlsComponentTest : SysuiTestCase() {
@Test @Test
fun testFeatureEnabledAndCannotShowOnLockScreenVisibility() { fun testFeatureEnabledAndCannotShowOnLockScreenVisibility() {
`when`(lockPatternUtils.getStrongAuthForUser(anyInt())) `when`(lockPatternUtils.getStrongAuthForUser(anyInt())).thenReturn(STRONG_AUTH_NOT_REQUIRED)
.thenReturn(STRONG_AUTH_NOT_REQUIRED)
`when`(keyguardStateController.isUnlocked()).thenReturn(false) `when`(keyguardStateController.isUnlocked()).thenReturn(false)
controlsSettingsRepository.setCanShowControlsInLockscreen(false) controlsSettingsRepository.setCanShowControlsInLockscreen(false)
val component = setupComponent(true) val component = setupComponent(true)
@@ -134,8 +126,7 @@ class ControlsComponentTest : SysuiTestCase() {
@Test @Test
fun testFeatureEnabledAndCanShowOnLockScreenVisibility() { fun testFeatureEnabledAndCanShowOnLockScreenVisibility() {
`when`(lockPatternUtils.getStrongAuthForUser(anyInt())) `when`(lockPatternUtils.getStrongAuthForUser(anyInt())).thenReturn(STRONG_AUTH_NOT_REQUIRED)
.thenReturn(STRONG_AUTH_NOT_REQUIRED)
`when`(keyguardStateController.isUnlocked()).thenReturn(false) `when`(keyguardStateController.isUnlocked()).thenReturn(false)
controlsSettingsRepository.setCanShowControlsInLockscreen(true) controlsSettingsRepository.setCanShowControlsInLockscreen(true)
val component = setupComponent(true) val component = setupComponent(true)
@@ -146,8 +137,7 @@ class ControlsComponentTest : SysuiTestCase() {
@Test @Test
fun testFeatureEnabledAndCanShowWhileUnlockedVisibility() { fun testFeatureEnabledAndCanShowWhileUnlockedVisibility() {
controlsSettingsRepository.setCanShowControlsInLockscreen(false) controlsSettingsRepository.setCanShowControlsInLockscreen(false)
`when`(lockPatternUtils.getStrongAuthForUser(anyInt())) `when`(lockPatternUtils.getStrongAuthForUser(anyInt())).thenReturn(STRONG_AUTH_NOT_REQUIRED)
.thenReturn(STRONG_AUTH_NOT_REQUIRED)
`when`(keyguardStateController.isUnlocked()).thenReturn(true) `when`(keyguardStateController.isUnlocked()).thenReturn(true)
val component = setupComponent(true) val component = setupComponent(true)
@@ -158,8 +148,7 @@ class ControlsComponentTest : SysuiTestCase() {
fun testGetTileImageId() { fun testGetTileImageId() {
val tileImageId = 0 val tileImageId = 0
`when`(controlsTileResourceConfiguration.getTileImageId()) `when`(controlsTileResourceConfiguration.getTileImageId()).thenReturn(tileImageId)
.thenReturn(tileImageId)
val component = setupComponent(true) val component = setupComponent(true)
assertEquals(component.getTileImageId(), tileImageId) assertEquals(component.getTileImageId(), tileImageId)
} }
@@ -168,12 +157,19 @@ class ControlsComponentTest : SysuiTestCase() {
fun testGetTileTitleId() { fun testGetTileTitleId() {
val tileTitleId = 0 val tileTitleId = 0
`when`(controlsTileResourceConfiguration.getTileTitleId()) `when`(controlsTileResourceConfiguration.getTileTitleId()).thenReturn(tileTitleId)
.thenReturn(tileTitleId)
val component = setupComponent(true) val component = setupComponent(true)
assertEquals(component.getTileTitleId(), tileTitleId) 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 { private fun setupComponent(enabled: Boolean): ControlsComponent {
return ControlsComponent( return ControlsComponent(
enabled, enabled,

View File

@@ -17,6 +17,7 @@
package com.android.systemui.keyguard.data.quickaffordance package com.android.systemui.keyguard.data.quickaffordance
import android.app.Activity
import androidx.test.filters.SmallTest import androidx.test.filters.SmallTest
import com.android.systemui.R import com.android.systemui.R
import com.android.systemui.SysuiTestCase 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.controller.ControlsController
import com.android.systemui.controls.dagger.ControlsComponent import com.android.systemui.controls.dagger.ControlsComponent
import com.android.systemui.controls.management.ControlsListingController 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.mock
import com.android.systemui.util.mockito.whenever import com.android.systemui.util.mockito.whenever
import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertThat
@@ -75,6 +77,7 @@ class HomeControlsKeyguardQuickAffordanceConfigParameterizedStateTest : SysuiTes
@Mock private lateinit var component: ControlsComponent @Mock private lateinit var component: ControlsComponent
@Mock private lateinit var controlsController: ControlsController @Mock private lateinit var controlsController: ControlsController
@Mock private lateinit var controlsListingController: ControlsListingController @Mock private lateinit var controlsListingController: ControlsListingController
@Mock private lateinit var controlsUiController: ControlsUiController
@Mock private lateinit var controlsServiceInfo: ControlsServiceInfo @Mock private lateinit var controlsServiceInfo: ControlsServiceInfo
@Captor @Captor
private lateinit var callbackCaptor: private lateinit var callbackCaptor:
@@ -98,6 +101,8 @@ class HomeControlsKeyguardQuickAffordanceConfigParameterizedStateTest : SysuiTes
whenever(component.getControlsController()).thenReturn(Optional.of(controlsController)) whenever(component.getControlsController()).thenReturn(Optional.of(controlsController))
whenever(component.getControlsListingController()) whenever(component.getControlsListingController())
.thenReturn(Optional.of(controlsListingController)) .thenReturn(Optional.of(controlsListingController))
whenever(controlsUiController.resolveActivity()).thenReturn(FakeActivity::class.java)
whenever(component.getControlsUiController()).thenReturn(Optional.of(controlsUiController))
if (hasPanels) { if (hasPanels) {
whenever(controlsServiceInfo.panelActivity).thenReturn(mock()) whenever(controlsServiceInfo.panelActivity).thenReturn(mock())
} }
@@ -178,4 +183,6 @@ class HomeControlsKeyguardQuickAffordanceConfigParameterizedStateTest : SysuiTes
) )
job.cancel() 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.NoteTaskController
import com.android.systemui.notetask.NoteTaskEntryPoint import com.android.systemui.notetask.NoteTaskEntryPoint
import com.android.systemui.notetask.NoteTaskInfoResolver 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.stylus.StylusManager
import com.android.systemui.util.concurrency.FakeExecutor import com.android.systemui.util.concurrency.FakeExecutor
import com.android.systemui.util.mockito.any import com.android.systemui.util.mockito.any
@@ -283,7 +282,7 @@ internal class NoteTaskQuickAffordanceConfigTest : SysuiTestCase() {
} }
@Test @Test
fun getPickerScreenState_nodefaultNoteAppSet_shouldReturnDisable() = runTest { fun getPickerScreenState_noDefaultNoteAppSet_shouldReturnDisabled() = runTest {
val underTest = createUnderTest(isEnabled = true) val underTest = createUnderTest(isEnabled = true)
whenever( whenever(
roleManager.getRoleHoldersAsUser( roleManager.getRoleHoldersAsUser(
@@ -293,16 +292,16 @@ internal class NoteTaskQuickAffordanceConfigTest : SysuiTestCase() {
) )
.thenReturn(emptyList()) .thenReturn(emptyList())
assertThat(underTest.getPickerScreenState()) val pickerScreenState = underTest.getPickerScreenState()
.isEqualTo( assertThat(pickerScreenState is KeyguardQuickAffordanceConfig.PickerScreenState.Disabled)
KeyguardQuickAffordanceConfig.PickerScreenState.Disabled( .isTrue()
listOf("Select a default notes app to use the notetaking shortcut"), val disabled = pickerScreenState as KeyguardQuickAffordanceConfig.PickerScreenState.Disabled
actionText = "Select app", assertThat(disabled.instructions)
actionComponentName = .isEqualTo(listOf("Select a default notes app to use the notetaking shortcut"))
"${context.packageName}$COMPONENT_NAME_SEPARATOR" + assertThat(disabled.actionText).isEqualTo("Select app")
"$ACTION_MANAGE_NOTES_ROLE_FROM_QUICK_AFFORDANCE" assertThat(disabled.actionIntent?.action)
) .isEqualTo(ACTION_MANAGE_NOTES_ROLE_FROM_QUICK_AFFORDANCE)
) assertThat(disabled.actionIntent?.`package`).isEqualTo(context.packageName)
} }
// endregion // endregion