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