1 /* <lambda>null2 * Copyright (C) 2022 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 * 16 */ 17 18 package com.android.systemui.controls.settings 19 20 import android.app.AlertDialog 21 import android.content.Context 22 import android.content.Context.MODE_PRIVATE 23 import android.content.DialogInterface 24 import android.content.SharedPreferences 25 import android.provider.Settings 26 import androidx.annotation.VisibleForTesting 27 import com.android.systemui.res.R 28 import com.android.systemui.controls.settings.ControlsSettingsDialogManager.Companion.MAX_NUMBER_ATTEMPTS_CONTROLS_DIALOG 29 import com.android.systemui.controls.settings.ControlsSettingsDialogManager.Companion.PREFS_SETTINGS_DIALOG_ATTEMPTS 30 import com.android.systemui.dagger.SysUISingleton 31 import com.android.systemui.plugins.ActivityStarter 32 import com.android.systemui.settings.UserFileManager 33 import com.android.systemui.settings.UserTracker 34 import com.android.systemui.statusbar.phone.SystemUIDialog 35 import com.android.systemui.statusbar.policy.DeviceControlsControllerImpl 36 import com.android.systemui.util.settings.SecureSettings 37 import javax.inject.Inject 38 39 /** 40 * Manager to display a dialog to prompt user to enable controls related Settings: 41 * * [Settings.Secure.LOCKSCREEN_SHOW_CONTROLS] 42 * * [Settings.Secure.LOCKSCREEN_ALLOW_TRIVIAL_CONTROLS] 43 */ 44 interface ControlsSettingsDialogManager { 45 46 /** 47 * Shows the corresponding dialog. In order for a dialog to appear, the following must be true 48 * * At least one of the Settings in [ControlsSettingsRepository] are `false`. 49 * * The dialog has not been seen by the user too many times (as defined by 50 * [MAX_NUMBER_ATTEMPTS_CONTROLS_DIALOG]). 51 * 52 * When the dialogs are shown, the following outcomes are possible: 53 * * User cancels the dialog by clicking outside or going back: we register that the dialog was 54 * seen but the settings don't change. 55 * * User responds negatively to the dialog: we register that the user doesn't want to change 56 * the settings (dialog will not appear again) and the settings don't change. 57 * * User responds positively to the dialog: the settings are set to `true` and the dialog will 58 * not appear again. 59 * * SystemUI closes the dialogs (for example, the activity showing it is closed). In this case, 60 * we don't modify anything. 61 * 62 * Of those four scenarios, only the first three will cause [onAttemptCompleted] to be called. 63 * It will also be called if the dialogs are not shown. 64 */ 65 fun maybeShowDialog(activityContext: Context, onAttemptCompleted: () -> Unit) 66 67 /** 68 * Closes the dialog without registering anything from the user. The state of the settings after 69 * this is called will be the same as before the dialogs were shown. 70 */ 71 fun closeDialog() 72 73 companion object { 74 @VisibleForTesting internal const val MAX_NUMBER_ATTEMPTS_CONTROLS_DIALOG = 2 75 @VisibleForTesting 76 internal const val PREFS_SETTINGS_DIALOG_ATTEMPTS = "show_settings_attempts" 77 } 78 } 79 80 @SysUISingleton 81 class ControlsSettingsDialogManagerImpl 82 @VisibleForTesting 83 internal constructor( 84 private val secureSettings: SecureSettings, 85 private val userFileManager: UserFileManager, 86 private val controlsSettingsRepository: ControlsSettingsRepository, 87 private val userTracker: UserTracker, 88 private val activityStarter: ActivityStarter, 89 private val dialogProvider: (context: Context, theme: Int) -> AlertDialog 90 ) : ControlsSettingsDialogManager { 91 92 @Inject 93 constructor( 94 secureSettings: SecureSettings, 95 userFileManager: UserFileManager, 96 controlsSettingsRepository: ControlsSettingsRepository, 97 userTracker: UserTracker, 98 activityStarter: ActivityStarter 99 ) : this( 100 secureSettings, 101 userFileManager, 102 controlsSettingsRepository, 103 userTracker, 104 activityStarter, contextnull105 { context, theme -> SettingsDialog(context, theme) } 106 ) 107 108 private var dialog: AlertDialog? = null 109 private set 110 111 private val showDeviceControlsInLockscreen: Boolean 112 get() = controlsSettingsRepository.canShowControlsInLockscreen.value 113 114 private val allowTrivialControls: Boolean 115 get() = controlsSettingsRepository.allowActionOnTrivialControlsInLockscreen.value 116 maybeShowDialognull117 override fun maybeShowDialog(activityContext: Context, onAttemptCompleted: () -> Unit) { 118 closeDialog() 119 120 val prefs = 121 userFileManager.getSharedPreferences( 122 DeviceControlsControllerImpl.PREFS_CONTROLS_FILE, 123 MODE_PRIVATE, 124 userTracker.userId 125 ) 126 val attempts = prefs.getInt(PREFS_SETTINGS_DIALOG_ATTEMPTS, 0) 127 if ( 128 attempts >= MAX_NUMBER_ATTEMPTS_CONTROLS_DIALOG || 129 (showDeviceControlsInLockscreen && allowTrivialControls) 130 ) { 131 onAttemptCompleted() 132 return 133 } 134 135 val listener = DialogListener(prefs, attempts, onAttemptCompleted) 136 val d = 137 dialogProvider(activityContext, R.style.Theme_SystemUI_Dialog).apply { 138 setIcon(R.drawable.ic_lock_locked) 139 setOnCancelListener(listener) 140 setNeutralButton(R.string.controls_settings_dialog_neutral_button, listener) 141 setPositiveButton(R.string.controls_settings_dialog_positive_button, listener) 142 if (showDeviceControlsInLockscreen) { 143 setTitle(R.string.controls_settings_trivial_controls_dialog_title) 144 setMessage(R.string.controls_settings_trivial_controls_dialog_message) 145 } else { 146 setTitle(R.string.controls_settings_show_controls_dialog_title) 147 setMessage(R.string.controls_settings_show_controls_dialog_message) 148 } 149 } 150 151 SystemUIDialog.registerDismissListener(d) { dialog = null } 152 SystemUIDialog.setDialogSize(d) 153 SystemUIDialog.setShowForAllUsers(d, true) 154 dialog = d 155 d.show() 156 } 157 turnOnSettingSecurelynull158 private fun turnOnSettingSecurely(settings: List<String>, onComplete: () -> Unit) { 159 val action = 160 ActivityStarter.OnDismissAction { 161 settings.forEach { setting -> 162 secureSettings.putIntForUser(setting, 1, userTracker.userId) 163 } 164 onComplete() 165 true 166 } 167 activityStarter.dismissKeyguardThenExecute( 168 action, 169 /* cancel */ onComplete, 170 /* afterKeyguardGone */ true 171 ) 172 } 173 closeDialognull174 override fun closeDialog() { 175 dialog?.dismiss() 176 } 177 178 private inner class DialogListener( 179 private val prefs: SharedPreferences, 180 private val attempts: Int, 181 private val onComplete: () -> Unit 182 ) : DialogInterface.OnClickListener, DialogInterface.OnCancelListener { onClicknull183 override fun onClick(dialog: DialogInterface?, which: Int) { 184 if (dialog == null) return 185 if (which == DialogInterface.BUTTON_POSITIVE) { 186 val settings = mutableListOf(Settings.Secure.LOCKSCREEN_ALLOW_TRIVIAL_CONTROLS) 187 if (!showDeviceControlsInLockscreen) { 188 settings.add(Settings.Secure.LOCKSCREEN_SHOW_CONTROLS) 189 } 190 // If we are toggling the flag, we want to call onComplete after the keyguard is 191 // dismissed (and the setting is turned on), to pass the correct value. 192 turnOnSettingSecurely(settings, onComplete) 193 } else { 194 onComplete() 195 } 196 if (attempts != MAX_NUMBER_ATTEMPTS_CONTROLS_DIALOG) { 197 prefs 198 .edit() 199 .putInt(PREFS_SETTINGS_DIALOG_ATTEMPTS, MAX_NUMBER_ATTEMPTS_CONTROLS_DIALOG) 200 .apply() 201 } 202 } 203 onCancelnull204 override fun onCancel(dialog: DialogInterface?) { 205 if (dialog == null) return 206 if (attempts < MAX_NUMBER_ATTEMPTS_CONTROLS_DIALOG) { 207 prefs.edit().putInt(PREFS_SETTINGS_DIALOG_ATTEMPTS, attempts + 1).apply() 208 } 209 onComplete() 210 } 211 } 212 AlertDialognull213 private fun AlertDialog.setNeutralButton( 214 msgId: Int, 215 listener: DialogInterface.OnClickListener 216 ) { 217 setButton(DialogInterface.BUTTON_NEUTRAL, context.getText(msgId), listener) 218 } 219 AlertDialognull220 private fun AlertDialog.setPositiveButton( 221 msgId: Int, 222 listener: DialogInterface.OnClickListener 223 ) { 224 setButton(DialogInterface.BUTTON_POSITIVE, context.getText(msgId), listener) 225 } 226 AlertDialognull227 private fun AlertDialog.setMessage(msgId: Int) { 228 setMessage(context.getText(msgId)) 229 } 230 231 /** This is necessary because the constructors are `protected`. */ 232 private class SettingsDialog(context: Context, theme: Int) : AlertDialog(context, theme) 233 } 234