| /* |
| * 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<String>) { |
| 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) |
| } |