diff --git a/packages/SystemUI/src/com/android/systemui/controls/dagger/ControlsComponent.kt b/packages/SystemUI/src/com/android/systemui/controls/dagger/ControlsComponent.kt index 77d0496e43dba..27466d4f58bce 100644 --- a/packages/SystemUI/src/com/android/systemui/controls/dagger/ControlsComponent.kt +++ b/packages/SystemUI/src/com/android/systemui/controls/dagger/ControlsComponent.kt @@ -19,7 +19,7 @@ package com.android.systemui.controls.dagger import android.content.Context import com.android.internal.widget.LockPatternUtils import com.android.internal.widget.LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_BOOT -import com.android.systemui.controls.ControlsSettingsRepository +import com.android.systemui.controls.settings.ControlsSettingsRepository import com.android.systemui.controls.controller.ControlsController import com.android.systemui.controls.controller.ControlsTileResourceConfiguration import com.android.systemui.controls.controller.ControlsTileResourceConfigurationImpl diff --git a/packages/SystemUI/src/com/android/systemui/controls/dagger/ControlsModule.kt b/packages/SystemUI/src/com/android/systemui/controls/dagger/ControlsModule.kt index 9ae605e30a838..6d6410de1a91d 100644 --- a/packages/SystemUI/src/com/android/systemui/controls/dagger/ControlsModule.kt +++ b/packages/SystemUI/src/com/android/systemui/controls/dagger/ControlsModule.kt @@ -20,8 +20,8 @@ import android.app.Activity import android.content.pm.PackageManager import com.android.systemui.controls.ControlsMetricsLogger import com.android.systemui.controls.ControlsMetricsLoggerImpl -import com.android.systemui.controls.ControlsSettingsRepository -import com.android.systemui.controls.ControlsSettingsRepositoryImpl +import com.android.systemui.controls.settings.ControlsSettingsRepository +import com.android.systemui.controls.settings.ControlsSettingsRepositoryImpl import com.android.systemui.controls.controller.ControlsBindingController import com.android.systemui.controls.controller.ControlsBindingControllerImpl import com.android.systemui.controls.controller.ControlsController @@ -34,6 +34,8 @@ import com.android.systemui.controls.management.ControlsListingController import com.android.systemui.controls.management.ControlsListingControllerImpl import com.android.systemui.controls.management.ControlsProviderSelectorActivity import com.android.systemui.controls.management.ControlsRequestDialog +import com.android.systemui.controls.settings.ControlsSettingsDialogManager +import com.android.systemui.controls.settings.ControlsSettingsDialogManagerImpl import com.android.systemui.controls.ui.ControlActionCoordinator import com.android.systemui.controls.ui.ControlActionCoordinatorImpl import com.android.systemui.controls.ui.ControlsActivity @@ -89,6 +91,11 @@ abstract class ControlsModule { manager: ControlsSettingsRepositoryImpl ): ControlsSettingsRepository + @Binds + abstract fun provideDialogManager( + manager: ControlsSettingsDialogManagerImpl + ): ControlsSettingsDialogManager + @Binds abstract fun provideMetricsLogger(logger: ControlsMetricsLoggerImpl): ControlsMetricsLogger diff --git a/packages/SystemUI/src/com/android/systemui/controls/settings/ControlsSettingsDialogManager.kt b/packages/SystemUI/src/com/android/systemui/controls/settings/ControlsSettingsDialogManager.kt new file mode 100644 index 0000000000000..bb2e2d701aa0b --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/controls/settings/ControlsSettingsDialogManager.kt @@ -0,0 +1,231 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.android.systemui.controls.settings + +import android.app.AlertDialog +import android.content.Context +import android.content.Context.MODE_PRIVATE +import android.content.DialogInterface +import android.content.SharedPreferences +import android.provider.Settings +import androidx.annotation.VisibleForTesting +import com.android.systemui.R +import com.android.systemui.controls.settings.ControlsSettingsDialogManager.Companion.MAX_NUMBER_ATTEMPTS_CONTROLS_DIALOG +import com.android.systemui.controls.settings.ControlsSettingsDialogManager.Companion.PREFS_SETTINGS_DIALOG_ATTEMPTS +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.plugins.ActivityStarter +import com.android.systemui.settings.UserFileManager +import com.android.systemui.settings.UserTracker +import com.android.systemui.statusbar.phone.SystemUIDialog +import com.android.systemui.statusbar.policy.DeviceControlsControllerImpl +import com.android.systemui.util.settings.SecureSettings +import javax.inject.Inject + +/** + * Manager to display a dialog to prompt user to enable controls related Settings: + * + * * [Settings.Secure.LOCKSCREEN_SHOW_CONTROLS] + * * [Settings.Secure.LOCKSCREEN_ALLOW_TRIVIAL_CONTROLS] + */ +interface ControlsSettingsDialogManager { + + /** + * Shows the corresponding dialog. In order for a dialog to appear, the following must be true + * + * * At least one of the Settings in [ControlsSettingsRepository] are `false`. + * * The dialog has not been seen by the user too many times (as defined by + * [MAX_NUMBER_ATTEMPTS_CONTROLS_DIALOG]). + * + * When the dialogs are shown, the following outcomes are possible: + * * User cancels the dialog by clicking outside or going back: we register that the dialog was + * seen but the settings don't change. + * * User responds negatively to the dialog: we register that the user doesn't want to change + * the settings (dialog will not appear again) and the settings don't change. + * * User responds positively to the dialog: the settings are set to `true` and the dialog will + * not appear again. + * * SystemUI closes the dialogs (for example, the activity showing it is closed). In this case, + * we don't modify anything. + * + * Of those four scenarios, only the first three will cause [onAttemptCompleted] to be called. + * It will also be called if the dialogs are not shown. + */ + fun maybeShowDialog(activityContext: Context, onAttemptCompleted: () -> Unit) + + /** + * Closes the dialog without registering anything from the user. The state of the settings after + * this is called will be the same as before the dialogs were shown. + */ + fun closeDialog() + + companion object { + @VisibleForTesting internal const val MAX_NUMBER_ATTEMPTS_CONTROLS_DIALOG = 2 + @VisibleForTesting + internal const val PREFS_SETTINGS_DIALOG_ATTEMPTS = "show_settings_attempts" + } +} + +@SysUISingleton +class ControlsSettingsDialogManagerImpl +@VisibleForTesting +internal constructor( + private val secureSettings: SecureSettings, + private val userFileManager: UserFileManager, + private val controlsSettingsRepository: ControlsSettingsRepository, + private val userTracker: UserTracker, + private val activityStarter: ActivityStarter, + private val dialogProvider: (context: Context, theme: Int) -> AlertDialog +) : ControlsSettingsDialogManager { + + @Inject + constructor( + secureSettings: SecureSettings, + userFileManager: UserFileManager, + controlsSettingsRepository: ControlsSettingsRepository, + userTracker: UserTracker, + activityStarter: ActivityStarter + ) : this( + secureSettings, + userFileManager, + controlsSettingsRepository, + userTracker, + activityStarter, + { context, theme -> SettingsDialog(context, theme) } + ) + + private var dialog: AlertDialog? = null + private set + + private val showDeviceControlsInLockscreen: Boolean + get() = controlsSettingsRepository.canShowControlsInLockscreen.value + + private val allowTrivialControls: Boolean + get() = controlsSettingsRepository.allowActionOnTrivialControlsInLockscreen.value + + override fun maybeShowDialog(activityContext: Context, onAttemptCompleted: () -> Unit) { + closeDialog() + + val prefs = + userFileManager.getSharedPreferences( + DeviceControlsControllerImpl.PREFS_CONTROLS_FILE, + MODE_PRIVATE, + userTracker.userId + ) + val attempts = prefs.getInt(PREFS_SETTINGS_DIALOG_ATTEMPTS, 0) + if ( + attempts >= MAX_NUMBER_ATTEMPTS_CONTROLS_DIALOG || + (showDeviceControlsInLockscreen && allowTrivialControls) + ) { + onAttemptCompleted() + return + } + + val listener = DialogListener(prefs, attempts, onAttemptCompleted) + val d = + dialogProvider(activityContext, R.style.Theme_SystemUI_Dialog).apply { + setIcon(R.drawable.ic_warning) + setOnCancelListener(listener) + setNeutralButton(R.string.controls_settings_dialog_neutral_button, listener) + setPositiveButton(R.string.controls_settings_dialog_positive_button, listener) + if (showDeviceControlsInLockscreen) { + setTitle(R.string.controls_settings_trivial_controls_dialog_title) + setMessage(R.string.controls_settings_trivial_controls_dialog_message) + } else { + setTitle(R.string.controls_settings_show_controls_dialog_title) + setMessage(R.string.controls_settings_show_controls_dialog_message) + } + } + + SystemUIDialog.registerDismissListener(d) { dialog = null } + SystemUIDialog.setDialogSize(d) + SystemUIDialog.setShowForAllUsers(d, true) + dialog = d + d.show() + } + + private fun turnOnSettingSecurely(settings: List) { + val action = + ActivityStarter.OnDismissAction { + settings.forEach { setting -> + secureSettings.putIntForUser(setting, 1, userTracker.userId) + } + true + } + activityStarter.dismissKeyguardThenExecute( + action, + /* cancel */ null, + /* afterKeyguardGone */ true + ) + } + + override fun closeDialog() { + dialog?.dismiss() + } + + private inner class DialogListener( + private val prefs: SharedPreferences, + private val attempts: Int, + private val onComplete: () -> Unit + ) : DialogInterface.OnClickListener, DialogInterface.OnCancelListener { + override fun onClick(dialog: DialogInterface?, which: Int) { + if (dialog == null) return + if (which == DialogInterface.BUTTON_POSITIVE) { + val settings = mutableListOf(Settings.Secure.LOCKSCREEN_ALLOW_TRIVIAL_CONTROLS) + if (!showDeviceControlsInLockscreen) { + settings.add(Settings.Secure.LOCKSCREEN_SHOW_CONTROLS) + } + turnOnSettingSecurely(settings) + } + if (attempts != MAX_NUMBER_ATTEMPTS_CONTROLS_DIALOG) { + prefs + .edit() + .putInt(PREFS_SETTINGS_DIALOG_ATTEMPTS, MAX_NUMBER_ATTEMPTS_CONTROLS_DIALOG) + .apply() + } + onComplete() + } + + override fun onCancel(dialog: DialogInterface?) { + if (dialog == null) return + if (attempts < MAX_NUMBER_ATTEMPTS_CONTROLS_DIALOG) { + prefs.edit().putInt(PREFS_SETTINGS_DIALOG_ATTEMPTS, attempts + 1).apply() + } + onComplete() + } + } + + private fun AlertDialog.setNeutralButton( + msgId: Int, + listener: DialogInterface.OnClickListener + ) { + setButton(DialogInterface.BUTTON_NEUTRAL, context.getText(msgId), listener) + } + + private fun AlertDialog.setPositiveButton( + msgId: Int, + listener: DialogInterface.OnClickListener + ) { + setButton(DialogInterface.BUTTON_POSITIVE, context.getText(msgId), listener) + } + + private fun AlertDialog.setMessage(msgId: Int) { + setMessage(context.getText(msgId)) + } + + /** This is necessary because the constructors are `protected`. */ + private class SettingsDialog(context: Context, theme: Int) : AlertDialog(context, theme) +} diff --git a/packages/SystemUI/src/com/android/systemui/controls/ControlsSettingsRepository.kt b/packages/SystemUI/src/com/android/systemui/controls/settings/ControlsSettingsRepository.kt similarity index 95% rename from packages/SystemUI/src/com/android/systemui/controls/ControlsSettingsRepository.kt rename to packages/SystemUI/src/com/android/systemui/controls/settings/ControlsSettingsRepository.kt index 3d10ab906f2f4..df2831c6eb964 100644 --- a/packages/SystemUI/src/com/android/systemui/controls/ControlsSettingsRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/controls/settings/ControlsSettingsRepository.kt @@ -15,7 +15,7 @@ * */ -package com.android.systemui.controls +package com.android.systemui.controls.settings import kotlinx.coroutines.flow.StateFlow diff --git a/packages/SystemUI/src/com/android/systemui/controls/ControlsSettingsRepositoryImpl.kt b/packages/SystemUI/src/com/android/systemui/controls/settings/ControlsSettingsRepositoryImpl.kt similarity index 98% rename from packages/SystemUI/src/com/android/systemui/controls/ControlsSettingsRepositoryImpl.kt rename to packages/SystemUI/src/com/android/systemui/controls/settings/ControlsSettingsRepositoryImpl.kt index 9dc422a09674a..8e3b5109339c2 100644 --- a/packages/SystemUI/src/com/android/systemui/controls/ControlsSettingsRepositoryImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/controls/settings/ControlsSettingsRepositoryImpl.kt @@ -15,7 +15,7 @@ * */ -package com.android.systemui.controls +package com.android.systemui.controls.settings import android.provider.Settings import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow diff --git a/packages/SystemUI/src/com/android/systemui/controls/ui/ControlActionCoordinatorImpl.kt b/packages/SystemUI/src/com/android/systemui/controls/ui/ControlActionCoordinatorImpl.kt index 041ed1d557d7c..99a10a33ab0f3 100644 --- a/packages/SystemUI/src/com/android/systemui/controls/ui/ControlActionCoordinatorImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/controls/ui/ControlActionCoordinatorImpl.kt @@ -19,15 +19,12 @@ package com.android.systemui.controls.ui import android.annotation.AnyThread import android.annotation.MainThread import android.app.Activity -import android.app.AlertDialog import android.app.Dialog import android.app.PendingIntent import android.content.Context import android.content.pm.PackageManager import android.content.pm.ResolveInfo -import android.os.UserHandle import android.os.VibrationEffect -import android.provider.Settings.Secure import android.service.controls.Control import android.service.controls.actions.BooleanAction import android.service.controls.actions.CommandAction @@ -35,39 +32,36 @@ import android.service.controls.actions.FloatAction import android.util.Log import android.view.HapticFeedbackConstants import com.android.internal.annotations.VisibleForTesting -import com.android.systemui.R import com.android.systemui.broadcast.BroadcastSender import com.android.systemui.controls.ControlsMetricsLogger -import com.android.systemui.controls.ControlsSettingsRepository +import com.android.systemui.controls.settings.ControlsSettingsDialogManager +import com.android.systemui.controls.settings.ControlsSettingsRepository import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Main +import com.android.systemui.flags.FeatureFlags +import com.android.systemui.flags.Flags import com.android.systemui.plugins.ActivityStarter -import com.android.systemui.settings.UserContextProvider import com.android.systemui.statusbar.VibratorHelper -import com.android.systemui.statusbar.phone.SystemUIDialog -import com.android.systemui.statusbar.policy.DeviceControlsControllerImpl.Companion.PREFS_CONTROLS_FILE -import com.android.systemui.statusbar.policy.DeviceControlsControllerImpl.Companion.PREFS_SETTINGS_DIALOG_ATTEMPTS import com.android.systemui.statusbar.policy.KeyguardStateController import com.android.systemui.util.concurrency.DelayableExecutor -import com.android.systemui.util.settings.SecureSettings import com.android.wm.shell.TaskViewFactory import java.util.Optional import javax.inject.Inject @SysUISingleton class ControlActionCoordinatorImpl @Inject constructor( - private val context: Context, - private val bgExecutor: DelayableExecutor, - @Main private val uiExecutor: DelayableExecutor, - private val activityStarter: ActivityStarter, - private val broadcastSender: BroadcastSender, - private val keyguardStateController: KeyguardStateController, - private val taskViewFactory: Optional, - private val controlsMetricsLogger: ControlsMetricsLogger, - private val vibrator: VibratorHelper, - private val secureSettings: SecureSettings, - private val userContextProvider: UserContextProvider, - private val controlsSettingsRepository: ControlsSettingsRepository, + private val context: Context, + private val bgExecutor: DelayableExecutor, + @Main private val uiExecutor: DelayableExecutor, + private val activityStarter: ActivityStarter, + private val broadcastSender: BroadcastSender, + private val keyguardStateController: KeyguardStateController, + private val taskViewFactory: Optional, + private val controlsMetricsLogger: ControlsMetricsLogger, + private val vibrator: VibratorHelper, + private val controlsSettingsRepository: ControlsSettingsRepository, + private val controlsSettingsDialogManager: ControlsSettingsDialogManager, + private val featureFlags: FeatureFlags, ) : ControlActionCoordinator { private var dialog: Dialog? = null private var pendingAction: Action? = null @@ -76,16 +70,16 @@ class ControlActionCoordinatorImpl @Inject constructor( get() = !keyguardStateController.isUnlocked() private val allowTrivialControls: Boolean get() = controlsSettingsRepository.allowActionOnTrivialControlsInLockscreen.value - private val showDeviceControlsInLockscreen: Boolean - get() = controlsSettingsRepository.canShowControlsInLockscreen.value override lateinit var activityContext: Context companion object { private const val RESPONSE_TIMEOUT_IN_MILLIS = 3000L - private const val MAX_NUMBER_ATTEMPTS_CONTROLS_DIALOG = 2 } override fun closeDialogs() { + if (!featureFlags.isEnabled(Flags.USE_APP_PANELS)) { + controlsSettingsDialogManager.closeDialog() + } val isActivityFinishing = (activityContext as? Activity)?.let { it.isFinishing || it.isDestroyed } if (isActivityFinishing == true) { @@ -253,71 +247,9 @@ class ControlActionCoordinatorImpl @Inject constructor( if (action.authIsRequired) { return } - val prefs = userContextProvider.userContext.getSharedPreferences( - PREFS_CONTROLS_FILE, Context.MODE_PRIVATE) - val attempts = prefs.getInt(PREFS_SETTINGS_DIALOG_ATTEMPTS, 0) - if (attempts >= MAX_NUMBER_ATTEMPTS_CONTROLS_DIALOG || - (showDeviceControlsInLockscreen && allowTrivialControls)) { - return + if (!featureFlags.isEnabled(Flags.USE_APP_PANELS)) { + controlsSettingsDialogManager.maybeShowDialog(activityContext) {} } - val builder = AlertDialog - .Builder(activityContext, R.style.Theme_SystemUI_Dialog) - .setIcon(R.drawable.ic_warning) - .setOnCancelListener { - if (attempts < MAX_NUMBER_ATTEMPTS_CONTROLS_DIALOG) { - prefs.edit().putInt(PREFS_SETTINGS_DIALOG_ATTEMPTS, attempts + 1) - .commit() - } - true - } - .setNeutralButton(R.string.controls_settings_dialog_neutral_button) { _, _ -> - if (attempts != MAX_NUMBER_ATTEMPTS_CONTROLS_DIALOG) { - prefs.edit().putInt(PREFS_SETTINGS_DIALOG_ATTEMPTS, - MAX_NUMBER_ATTEMPTS_CONTROLS_DIALOG) - .commit() - } - true - } - - if (showDeviceControlsInLockscreen) { - dialog = builder - .setTitle(R.string.controls_settings_trivial_controls_dialog_title) - .setMessage(R.string.controls_settings_trivial_controls_dialog_message) - .setPositiveButton(R.string.controls_settings_dialog_positive_button) { _, _ -> - if (attempts != MAX_NUMBER_ATTEMPTS_CONTROLS_DIALOG) { - prefs.edit().putInt(PREFS_SETTINGS_DIALOG_ATTEMPTS, - MAX_NUMBER_ATTEMPTS_CONTROLS_DIALOG) - .commit() - } - secureSettings.putIntForUser(Secure.LOCKSCREEN_ALLOW_TRIVIAL_CONTROLS, 1, - UserHandle.USER_CURRENT) - true - } - .create() - } else { - dialog = builder - .setTitle(R.string.controls_settings_show_controls_dialog_title) - .setMessage(R.string.controls_settings_show_controls_dialog_message) - .setPositiveButton(R.string.controls_settings_dialog_positive_button) { _, _ -> - if (attempts != MAX_NUMBER_ATTEMPTS_CONTROLS_DIALOG) { - prefs.edit().putInt(PREFS_SETTINGS_DIALOG_ATTEMPTS, - MAX_NUMBER_ATTEMPTS_CONTROLS_DIALOG) - .commit() - } - secureSettings.putIntForUser(Secure.LOCKSCREEN_SHOW_CONTROLS, - 1, UserHandle.USER_CURRENT) - secureSettings.putIntForUser(Secure.LOCKSCREEN_ALLOW_TRIVIAL_CONTROLS, - 1, UserHandle.USER_CURRENT) - true - } - .create() - } - - SystemUIDialog.registerDismissListener(dialog) - SystemUIDialog.setDialogSize(dialog) - - dialog?.create() - dialog?.show() } @VisibleForTesting diff --git a/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsActivity.kt b/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsActivity.kt index bd704c1ff0861..5d611c4c82121 100644 --- a/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsActivity.kt +++ b/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsActivity.kt @@ -32,8 +32,10 @@ import androidx.activity.ComponentActivity import com.android.systemui.R import com.android.systemui.broadcast.BroadcastDispatcher import com.android.systemui.controls.management.ControlsAnimations +import com.android.systemui.controls.settings.ControlsSettingsDialogManager import com.android.systemui.flags.FeatureFlags import com.android.systemui.flags.Flags +import com.android.systemui.statusbar.policy.KeyguardStateController import javax.inject.Inject /** @@ -47,7 +49,9 @@ class ControlsActivity @Inject constructor( private val uiController: ControlsUiController, private val broadcastDispatcher: BroadcastDispatcher, private val dreamManager: IDreamManager, - private val featureFlags: FeatureFlags + private val featureFlags: FeatureFlags, + private val controlsSettingsDialogManager: ControlsSettingsDialogManager, + private val keyguardStateController: KeyguardStateController ) : ComponentActivity() { private lateinit var parent: ViewGroup @@ -92,7 +96,13 @@ class ControlsActivity @Inject constructor( parent = requireViewById(R.id.global_actions_controls) parent.alpha = 0f - uiController.show(parent, { finishOrReturnToDream() }, this) + if (featureFlags.isEnabled(Flags.USE_APP_PANELS) && !keyguardStateController.isUnlocked) { + controlsSettingsDialogManager.maybeShowDialog(this) { + uiController.show(parent, { finishOrReturnToDream() }, this) + } + } else { + uiController.show(parent, { finishOrReturnToDream() }, this) + } ControlsAnimations.enterAnimation(parent).start() } @@ -124,6 +134,7 @@ class ControlsActivity @Inject constructor( mExitToDream = false uiController.hide() + controlsSettingsDialogManager.closeDialog() } override fun onDestroy() { diff --git a/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsUiControllerImpl.kt b/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsUiControllerImpl.kt index a07c716bc8f96..fb678aa420bf3 100644 --- a/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsUiControllerImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsUiControllerImpl.kt @@ -49,7 +49,7 @@ import com.android.systemui.Dumpable import com.android.systemui.R import com.android.systemui.controls.ControlsMetricsLogger import com.android.systemui.controls.ControlsServiceInfo -import com.android.systemui.controls.ControlsSettingsRepository +import com.android.systemui.controls.settings.ControlsSettingsRepository import com.android.systemui.controls.CustomIconCache import com.android.systemui.controls.controller.ControlsController import com.android.systemui.controls.controller.StructureInfo diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/DeviceControlsControllerImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/DeviceControlsControllerImpl.kt index 6c66f0bb1e475..341eb3b0425c5 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/DeviceControlsControllerImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/DeviceControlsControllerImpl.kt @@ -68,7 +68,6 @@ public class DeviceControlsControllerImpl @Inject constructor( internal const val PREFS_CONTROLS_SEEDING_COMPLETED = "SeedingCompleted" const val PREFS_CONTROLS_FILE = "controls_prefs" - internal const val PREFS_SETTINGS_DIALOG_ATTEMPTS = "show_settings_attempts" private const val SEEDING_MAX = 2 } diff --git a/packages/SystemUI/tests/src/com/android/systemui/controls/controller/ControlActionCoordinatorImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/controls/controller/ControlActionCoordinatorImplTest.kt index 1d00d6b055687..16fb50c15af66 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/controls/controller/ControlActionCoordinatorImplTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/controls/controller/ControlActionCoordinatorImplTest.kt @@ -16,21 +16,19 @@ package com.android.systemui.controls.ui -import android.content.Context -import android.content.SharedPreferences import android.test.suitebuilder.annotation.SmallTest import android.testing.AndroidTestingRunner import com.android.systemui.SysuiTestCase import com.android.systemui.broadcast.BroadcastSender import com.android.systemui.controls.ControlsMetricsLogger -import com.android.systemui.controls.FakeControlsSettingsRepository +import com.android.systemui.controls.settings.ControlsSettingsDialogManager +import com.android.systemui.controls.settings.FakeControlsSettingsRepository +import com.android.systemui.flags.FeatureFlags +import com.android.systemui.flags.Flags import com.android.systemui.plugins.ActivityStarter -import com.android.systemui.settings.UserContextProvider import com.android.systemui.statusbar.VibratorHelper -import com.android.systemui.statusbar.policy.DeviceControlsControllerImpl import com.android.systemui.statusbar.policy.KeyguardStateController import com.android.systemui.util.concurrency.DelayableExecutor -import com.android.systemui.util.settings.SecureSettings import com.android.wm.shell.TaskViewFactory import org.junit.Before import org.junit.Test @@ -40,8 +38,8 @@ import org.mockito.Mock import org.mockito.Mockito import org.mockito.Mockito.`when` import org.mockito.Mockito.anyBoolean +import org.mockito.Mockito.doNothing import org.mockito.Mockito.doReturn -import org.mockito.Mockito.mock import org.mockito.Mockito.never import org.mockito.Mockito.reset import org.mockito.Mockito.spy @@ -71,9 +69,9 @@ class ControlActionCoordinatorImplTest : SysuiTestCase() { @Mock private lateinit var metricsLogger: ControlsMetricsLogger @Mock - private lateinit var secureSettings: SecureSettings + private lateinit var featureFlags: FeatureFlags @Mock - private lateinit var userContextProvider: UserContextProvider + private lateinit var controlsSettingsDialogManager: ControlsSettingsDialogManager companion object { fun any(): T = Mockito.any() @@ -103,23 +101,16 @@ class ControlActionCoordinatorImplTest : SysuiTestCase() { taskViewFactory, metricsLogger, vibratorHelper, - secureSettings, - userContextProvider, - controlsSettingsRepository + controlsSettingsRepository, + controlsSettingsDialogManager, + featureFlags )) - - val userContext = mock(Context::class.java) - val pref = mock(SharedPreferences::class.java) - `when`(userContextProvider.userContext).thenReturn(userContext) - `when`(userContext.getSharedPreferences( - DeviceControlsControllerImpl.PREFS_CONTROLS_FILE, Context.MODE_PRIVATE)) - .thenReturn(pref) - // Just return 2 so we don't test any Dialog logic which requires a launched activity. - `when`(pref.getInt(DeviceControlsControllerImpl.PREFS_SETTINGS_DIALOG_ATTEMPTS, 0)) - .thenReturn(2) + coordinator.activityContext = mContext `when`(cvh.cws.ci.controlId).thenReturn(ID) `when`(cvh.cws.control?.isAuthRequired()).thenReturn(true) + `when`(featureFlags.isEnabled(Flags.USE_APP_PANELS)).thenReturn(false) + action = spy(coordinator.Action(ID, {}, false, true)) doReturn(action).`when`(coordinator).createAction(any(), any(), anyBoolean(), anyBoolean()) } @@ -160,14 +151,30 @@ class ControlActionCoordinatorImplTest : SysuiTestCase() { doReturn(action).`when`(coordinator).createAction(any(), any(), anyBoolean(), anyBoolean()) `when`(keyguardStateController.isShowing()).thenReturn(true) - `when`(keyguardStateController.isUnlocked()).thenReturn(false) coordinator.toggle(cvh, "", true) verify(coordinator).bouncerOrRun(action) + verify(controlsSettingsDialogManager).maybeShowDialog(any(), any()) verify(action).invoke() } + @Test + fun testToggleWhenLockedDoesNotTriggerDialog_featureFlagEnabled() { + `when`(featureFlags.isEnabled(Flags.USE_APP_PANELS)).thenReturn(true) + action = spy(coordinator.Action(ID, {}, false, false)) + doReturn(action).`when`(coordinator).createAction(any(), any(), anyBoolean(), anyBoolean()) + + `when`(keyguardStateController.isShowing()).thenReturn(true) + `when`(keyguardStateController.isUnlocked()).thenReturn(false) + doNothing().`when`(controlsSettingsDialogManager).maybeShowDialog(any(), any()) + + coordinator.toggle(cvh, "", true) + + verify(coordinator).bouncerOrRun(action) + verify(controlsSettingsDialogManager, never()).maybeShowDialog(any(), any()) + } + @Test fun testToggleDoesNotRunsWhenLockedAndAuthRequired() { action = spy(coordinator.Action(ID, {}, false, true)) diff --git a/packages/SystemUI/tests/src/com/android/systemui/controls/dagger/ControlsComponentTest.kt b/packages/SystemUI/tests/src/com/android/systemui/controls/dagger/ControlsComponentTest.kt index 48fc46b7e730f..9144b13c7f3e6 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/controls/dagger/ControlsComponentTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/controls/dagger/ControlsComponentTest.kt @@ -22,7 +22,7 @@ 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.FakeControlsSettingsRepository +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 diff --git a/packages/SystemUI/tests/src/com/android/systemui/controls/settings/ControlsSettingsDialogManagerImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/controls/settings/ControlsSettingsDialogManagerImplTest.kt new file mode 100644 index 0000000000000..0c9986d82447c --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/controls/settings/ControlsSettingsDialogManagerImplTest.kt @@ -0,0 +1,328 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.android.systemui.controls.settings + +import android.content.DialogInterface +import android.content.SharedPreferences +import android.database.ContentObserver +import android.provider.Settings.Secure.LOCKSCREEN_ALLOW_TRIVIAL_CONTROLS +import android.provider.Settings.Secure.LOCKSCREEN_SHOW_CONTROLS +import android.testing.AndroidTestingRunner +import android.testing.TestableLooper +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.controls.settings.ControlsSettingsDialogManager.Companion.PREFS_SETTINGS_DIALOG_ATTEMPTS +import com.android.systemui.plugins.ActivityStarter +import com.android.systemui.settings.UserFileManager +import com.android.systemui.settings.UserTracker +import com.android.systemui.statusbar.policy.DeviceControlsControllerImpl +import com.android.systemui.util.FakeSharedPreferences +import com.android.systemui.util.TestableAlertDialog +import com.android.systemui.util.mockito.any +import com.android.systemui.util.mockito.eq +import com.android.systemui.util.mockito.nullable +import com.android.systemui.util.settings.FakeSettings +import com.google.common.truth.Truth.assertThat +import org.junit.After +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.ArgumentMatchers.anyBoolean +import org.mockito.Mock +import org.mockito.Mockito.anyInt +import org.mockito.Mockito.never +import org.mockito.Mockito.verify +import org.mockito.Mockito.`when` +import org.mockito.MockitoAnnotations + +@SmallTest +@RunWith(AndroidTestingRunner::class) +@TestableLooper.RunWithLooper +class ControlsSettingsDialogManagerImplTest : SysuiTestCase() { + + companion object { + private const val SETTING_SHOW = LOCKSCREEN_SHOW_CONTROLS + private const val SETTING_ACTION = LOCKSCREEN_ALLOW_TRIVIAL_CONTROLS + private const val MAX_NUMBER_ATTEMPTS_CONTROLS_DIALOG = 2 + } + + @Mock private lateinit var userFileManager: UserFileManager + @Mock private lateinit var userTracker: UserTracker + @Mock private lateinit var activityStarter: ActivityStarter + @Mock private lateinit var completedRunnable: () -> Unit + + private lateinit var controlsSettingsRepository: FakeControlsSettingsRepository + private lateinit var sharedPreferences: FakeSharedPreferences + private lateinit var secureSettings: FakeSettings + + private lateinit var underTest: ControlsSettingsDialogManagerImpl + + private var dialog: TestableAlertDialog? = null + + @Before + fun setUp() { + MockitoAnnotations.initMocks(this) + + controlsSettingsRepository = FakeControlsSettingsRepository() + sharedPreferences = FakeSharedPreferences() + secureSettings = FakeSettings() + + `when`(userTracker.userId).thenReturn(0) + secureSettings.userId = userTracker.userId + `when`( + userFileManager.getSharedPreferences( + eq(DeviceControlsControllerImpl.PREFS_CONTROLS_FILE), + anyInt(), + anyInt() + ) + ) + .thenReturn(sharedPreferences) + + `when`(activityStarter.dismissKeyguardThenExecute(any(), nullable(), anyBoolean())) + .thenAnswer { (it.arguments[0] as ActivityStarter.OnDismissAction).onDismiss() } + + attachRepositoryToSettings() + underTest = + ControlsSettingsDialogManagerImpl( + secureSettings, + userFileManager, + controlsSettingsRepository, + userTracker, + activityStarter + ) { context, _ -> TestableAlertDialog(context).also { dialog = it } } + } + + @After + fun tearDown() { + underTest.closeDialog() + } + + @Test + fun dialogNotShownIfPrefsAtMaximum() { + sharedPreferences.putAttempts(MAX_NUMBER_ATTEMPTS_CONTROLS_DIALOG) + + underTest.maybeShowDialog(context, completedRunnable) + + assertThat(dialog?.isShowing ?: false).isFalse() + verify(completedRunnable).invoke() + } + + @Test + fun dialogNotShownIfSettingsAreTrue() { + sharedPreferences.putAttempts(0) + secureSettings.putBool(SETTING_SHOW, true) + secureSettings.putBool(SETTING_ACTION, true) + + underTest.maybeShowDialog(context, completedRunnable) + + assertThat(dialog?.isShowing ?: false).isFalse() + verify(completedRunnable).invoke() + } + + @Test + fun dialogShownIfAllowTrivialControlsFalse() { + sharedPreferences.putAttempts(0) + secureSettings.putBool(SETTING_SHOW, true) + secureSettings.putBool(SETTING_ACTION, false) + + underTest.maybeShowDialog(context, completedRunnable) + + assertThat(dialog?.isShowing ?: false).isTrue() + } + + @Test + fun dialogDispossedAfterClosing() { + sharedPreferences.putAttempts(0) + secureSettings.putBool(SETTING_SHOW, true) + secureSettings.putBool(SETTING_ACTION, false) + + underTest.maybeShowDialog(context, completedRunnable) + underTest.closeDialog() + + assertThat(dialog?.isShowing ?: false).isFalse() + } + + @Test + fun dialogNeutralButtonDoesntChangeSetting() { + sharedPreferences.putAttempts(0) + secureSettings.putBool(SETTING_SHOW, true) + secureSettings.putBool(SETTING_ACTION, false) + + underTest.maybeShowDialog(context, completedRunnable) + clickButton(DialogInterface.BUTTON_NEUTRAL) + + assertThat(secureSettings.getBool(SETTING_ACTION, false)).isFalse() + } + + @Test + fun dialogNeutralButtonPutsMaxAttempts() { + sharedPreferences.putAttempts(0) + secureSettings.putBool(SETTING_SHOW, true) + secureSettings.putBool(SETTING_ACTION, false) + + underTest.maybeShowDialog(context, completedRunnable) + clickButton(DialogInterface.BUTTON_NEUTRAL) + + assertThat(sharedPreferences.getInt(PREFS_SETTINGS_DIALOG_ATTEMPTS, 0)) + .isEqualTo(MAX_NUMBER_ATTEMPTS_CONTROLS_DIALOG) + } + + @Test + fun dialogNeutralButtonCallsOnComplete() { + sharedPreferences.putAttempts(0) + secureSettings.putBool(SETTING_SHOW, true) + secureSettings.putBool(SETTING_ACTION, false) + + underTest.maybeShowDialog(context, completedRunnable) + clickButton(DialogInterface.BUTTON_NEUTRAL) + + verify(completedRunnable).invoke() + } + + @Test + fun dialogPositiveButtonChangesSetting() { + sharedPreferences.putAttempts(0) + secureSettings.putBool(SETTING_SHOW, true) + secureSettings.putBool(SETTING_ACTION, false) + + underTest.maybeShowDialog(context, completedRunnable) + clickButton(DialogInterface.BUTTON_POSITIVE) + + assertThat(secureSettings.getBool(SETTING_ACTION, false)).isTrue() + } + + @Test + fun dialogPositiveButtonPutsMaxAttempts() { + sharedPreferences.putAttempts(0) + secureSettings.putBool(SETTING_SHOW, true) + secureSettings.putBool(SETTING_ACTION, false) + + underTest.maybeShowDialog(context, completedRunnable) + clickButton(DialogInterface.BUTTON_POSITIVE) + + assertThat(sharedPreferences.getInt(PREFS_SETTINGS_DIALOG_ATTEMPTS, 0)) + .isEqualTo(MAX_NUMBER_ATTEMPTS_CONTROLS_DIALOG) + } + + @Test + fun dialogPositiveButtonCallsOnComplete() { + sharedPreferences.putAttempts(0) + secureSettings.putBool(SETTING_SHOW, true) + secureSettings.putBool(SETTING_ACTION, false) + + underTest.maybeShowDialog(context, completedRunnable) + clickButton(DialogInterface.BUTTON_POSITIVE) + + verify(completedRunnable).invoke() + } + + @Test + fun dialogCancelDoesntChangeSetting() { + sharedPreferences.putAttempts(0) + secureSettings.putBool(SETTING_SHOW, true) + secureSettings.putBool(SETTING_ACTION, false) + + underTest.maybeShowDialog(context, completedRunnable) + dialog?.cancel() + + assertThat(secureSettings.getBool(SETTING_ACTION, false)).isFalse() + } + + @Test + fun dialogCancelPutsOneExtraAttempt() { + val attempts = 0 + sharedPreferences.putAttempts(attempts) + secureSettings.putBool(SETTING_SHOW, true) + secureSettings.putBool(SETTING_ACTION, false) + + underTest.maybeShowDialog(context, completedRunnable) + dialog?.cancel() + + assertThat(sharedPreferences.getInt(PREFS_SETTINGS_DIALOG_ATTEMPTS, 0)) + .isEqualTo(attempts + 1) + } + + @Test + fun dialogCancelCallsOnComplete() { + sharedPreferences.putAttempts(0) + secureSettings.putBool(SETTING_SHOW, true) + secureSettings.putBool(SETTING_ACTION, false) + + underTest.maybeShowDialog(context, completedRunnable) + dialog?.cancel() + + verify(completedRunnable).invoke() + } + + @Test + fun closeDialogDoesNotCallOnComplete() { + sharedPreferences.putAttempts(0) + secureSettings.putBool(SETTING_SHOW, true) + secureSettings.putBool(SETTING_ACTION, false) + + underTest.maybeShowDialog(context, completedRunnable) + underTest.closeDialog() + + verify(completedRunnable, never()).invoke() + } + + @Test + fun dialogPositiveWithBothSettingsFalseTogglesBothSettings() { + sharedPreferences.putAttempts(0) + secureSettings.putBool(SETTING_SHOW, false) + secureSettings.putBool(SETTING_ACTION, false) + + underTest.maybeShowDialog(context, completedRunnable) + clickButton(DialogInterface.BUTTON_POSITIVE) + + assertThat(secureSettings.getBool(SETTING_SHOW)).isTrue() + assertThat(secureSettings.getBool(SETTING_ACTION)).isTrue() + } + + private fun clickButton(which: Int) { + dialog?.clickButton(which) + } + + private fun attachRepositoryToSettings() { + secureSettings.registerContentObserver( + SETTING_SHOW, + object : ContentObserver(null) { + override fun onChange(selfChange: Boolean) { + controlsSettingsRepository.setCanShowControlsInLockscreen( + secureSettings.getBool(SETTING_SHOW, false) + ) + } + } + ) + + secureSettings.registerContentObserver( + SETTING_ACTION, + object : ContentObserver(null) { + override fun onChange(selfChange: Boolean) { + controlsSettingsRepository.setAllowActionOnTrivialControlsInLockscreen( + secureSettings.getBool(SETTING_ACTION, false) + ) + } + } + ) + } + + private fun SharedPreferences.putAttempts(value: Int) { + edit().putInt(PREFS_SETTINGS_DIALOG_ATTEMPTS, value).commit() + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/controls/ControlsSettingsRepositoryImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/controls/settings/ControlsSettingsRepositoryImplTest.kt similarity index 99% rename from packages/SystemUI/tests/src/com/android/systemui/controls/ControlsSettingsRepositoryImplTest.kt rename to packages/SystemUI/tests/src/com/android/systemui/controls/settings/ControlsSettingsRepositoryImplTest.kt index 4b88b44c3f03c..b904ac14e7073 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/controls/ControlsSettingsRepositoryImplTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/controls/settings/ControlsSettingsRepositoryImplTest.kt @@ -15,7 +15,7 @@ * */ -package com.android.systemui.controls +package com.android.systemui.controls.settings import android.content.pm.UserInfo import android.provider.Settings diff --git a/packages/SystemUI/tests/src/com/android/systemui/controls/FakeControlsSettingsRepository.kt b/packages/SystemUI/tests/src/com/android/systemui/controls/settings/FakeControlsSettingsRepository.kt similarity index 96% rename from packages/SystemUI/tests/src/com/android/systemui/controls/FakeControlsSettingsRepository.kt rename to packages/SystemUI/tests/src/com/android/systemui/controls/settings/FakeControlsSettingsRepository.kt index 8a1bed20e700f..b6628db142350 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/controls/FakeControlsSettingsRepository.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/controls/settings/FakeControlsSettingsRepository.kt @@ -15,7 +15,7 @@ * */ -package com.android.systemui.controls +package com.android.systemui.controls.settings import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow diff --git a/packages/SystemUI/tests/src/com/android/systemui/controls/ui/ControlsUiControllerImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/controls/ui/ControlsUiControllerImplTest.kt index d965e337f47a6..779788aa0075f 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/controls/ui/ControlsUiControllerImplTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/controls/ui/ControlsUiControllerImplTest.kt @@ -35,10 +35,10 @@ import com.android.systemui.SysuiTestCase import com.android.systemui.controls.ControlsMetricsLogger import com.android.systemui.controls.ControlsServiceInfo import com.android.systemui.controls.CustomIconCache -import com.android.systemui.controls.FakeControlsSettingsRepository import com.android.systemui.controls.controller.ControlsController import com.android.systemui.controls.controller.StructureInfo import com.android.systemui.controls.management.ControlsListingController +import com.android.systemui.controls.settings.FakeControlsSettingsRepository import com.android.systemui.dump.DumpManager import com.android.systemui.plugins.ActivityStarter import com.android.systemui.settings.UserFileManager