1 /*
2  * Copyright (C) 2020 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 package com.android.systemui.controls.ui
18 
19 import android.app.AlertDialog
20 import android.app.Dialog
21 import android.content.DialogInterface
22 import android.service.controls.actions.BooleanAction
23 import android.service.controls.actions.CommandAction
24 import android.service.controls.actions.ControlAction
25 import android.service.controls.actions.FloatAction
26 import android.service.controls.actions.ModeAction
27 import android.text.InputType
28 import android.util.Log
29 import android.view.LayoutInflater
30 import android.view.WindowManager
31 import android.view.inputmethod.InputMethodManager
32 import android.widget.CheckBox
33 import android.widget.EditText
34 
35 import com.android.systemui.R
36 
37 /**
38  * Creates all dialogs for challengeValues that can occur from a call to
39  * [ControlsProviderService#performControlAction]. The types of challenge responses are listed in
40  * [ControlAction.ResponseResult].
41  */
42 object ChallengeDialogs {
43 
44     private const val WINDOW_TYPE = WindowManager.LayoutParams.TYPE_VOLUME_OVERLAY
45     private const val STYLE = android.R.style.Theme_DeviceDefault_Dialog_Alert
46 
47     /**
48      * AlertDialogs to handle [ControlAction#RESPONSE_CHALLENGE_PIN] and
49      * [ControlAction#RESPONSE_CHALLENGE_PIN] responses, decided by the useAlphaNumeric
50      * parameter.
51      */
52     fun createPinDialog(
53         cvh: ControlViewHolder,
54         useAlphaNumeric: Boolean,
55         useRetryStrings: Boolean,
56         onCancel: () -> Unit
57     ): Dialog? {
58         val lastAction = cvh.lastAction
59         if (lastAction == null) {
60             Log.e(ControlsUiController.TAG,
61                 "PIN Dialog attempted but no last action is set. Will not show")
62             return null
63         }
64         val res = cvh.context.resources
65         val (title, instructions) = if (useRetryStrings) {
66             Pair(
67                 res.getString(R.string.controls_pin_wrong),
68                 R.string.controls_pin_instructions_retry
69             )
70         } else {
71             Pair(
72                 res.getString(R.string.controls_pin_verify, cvh.title.getText()),
73                 R.string.controls_pin_instructions
74             )
75         }
76         return object : AlertDialog(cvh.context, STYLE) {
77             override fun dismiss() {
78                 window?.decorView?.let {
79                     // workaround for b/159309083
80                     it.context.getSystemService(InputMethodManager::class.java)
81                             ?.hideSoftInputFromWindow(it.windowToken, 0)
82                 }
83                 super.dismiss()
84             }
85         }.apply {
86             setTitle(title)
87             setView(LayoutInflater.from(context).inflate(R.layout.controls_dialog_pin, null))
88             setButton(
89                 DialogInterface.BUTTON_POSITIVE,
90                 context.getText(android.R.string.ok),
91                 DialogInterface.OnClickListener { dialog, _ ->
92                     if (dialog is Dialog) {
93                         dialog.requireViewById<EditText>(R.id.controls_pin_input)
94                         val pin = dialog.requireViewById<EditText>(R.id.controls_pin_input)
95                             .getText().toString()
96                         cvh.action(addChallengeValue(lastAction, pin))
97                         dialog.dismiss()
98                     }
99             })
100             setButton(
101                 DialogInterface.BUTTON_NEGATIVE,
102                 context.getText(android.R.string.cancel),
103                 DialogInterface.OnClickListener { dialog, _ ->
104                     onCancel.invoke()
105                     dialog.cancel()
106                 }
107             )
108 
109             getWindow().apply {
110                 setType(WINDOW_TYPE)
111                 setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE)
112             }
113             setOnShowListener(DialogInterface.OnShowListener { _ ->
114                 val editText = requireViewById<EditText>(R.id.controls_pin_input)
115                 editText.setHint(instructions)
116                 val useAlphaCheckBox = requireViewById<CheckBox>(R.id.controls_pin_use_alpha)
117                 useAlphaCheckBox.setChecked(useAlphaNumeric)
118                 setInputType(editText, useAlphaCheckBox.isChecked())
119                 requireViewById<CheckBox>(R.id.controls_pin_use_alpha).setOnClickListener { _ ->
120                     setInputType(editText, useAlphaCheckBox.isChecked())
121                 }
122                 editText.requestFocus()
123             })
124         }
125     }
126 
127     /**
128      * AlertDialogs to handle [ControlAction#RESPONSE_CHALLENGE_ACK] response type.
129      */
130     fun createConfirmationDialog(cvh: ControlViewHolder, onCancel: () -> Unit): Dialog? {
131         val lastAction = cvh.lastAction
132         if (lastAction == null) {
133             Log.e(ControlsUiController.TAG,
134                 "Confirmation Dialog attempted but no last action is set. Will not show")
135             return null
136         }
137         val builder = AlertDialog.Builder(cvh.context, STYLE).apply {
138             val res = cvh.context.resources
139             setTitle(res.getString(
140                 R.string.controls_confirmation_message, cvh.title.getText()))
141             setPositiveButton(
142                 android.R.string.ok,
143                 DialogInterface.OnClickListener { dialog, _ ->
144                     cvh.action(addChallengeValue(lastAction, "true"))
145                     dialog.dismiss()
146             })
147             setNegativeButton(
148                 android.R.string.cancel,
149                 DialogInterface.OnClickListener { dialog, _ ->
150                     onCancel.invoke()
151                     dialog.cancel()
152                 }
153             )
154         }
155         return builder.create().apply {
156             getWindow().apply {
157                 setType(WINDOW_TYPE)
158             }
159         }
160     }
161 
162     private fun setInputType(editText: EditText, useTextInput: Boolean) {
163         if (useTextInput) {
164             editText.setInputType(
165                 InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_PASSWORD)
166         } else {
167             editText.setInputType(
168                 InputType.TYPE_CLASS_NUMBER or InputType.TYPE_NUMBER_VARIATION_PASSWORD)
169         }
170     }
171 
172     private fun addChallengeValue(action: ControlAction, challengeValue: String): ControlAction {
173         val id = action.getTemplateId()
174         return when (action) {
175             is BooleanAction -> BooleanAction(id, action.getNewState(), challengeValue)
176             is FloatAction -> FloatAction(id, action.getNewValue(), challengeValue)
177             is CommandAction -> CommandAction(id, challengeValue)
178             is ModeAction -> ModeAction(id, action.getNewMode(), challengeValue)
179             else -> throw IllegalStateException("'action' is not a known type: $action")
180         }
181     }
182 }
183