1 /**
<lambda>null2  * Copyright (C) 2022 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
5  * in compliance with the License. You may obtain a copy of the License at
6  *
7  * ```
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  * ```
10  *
11  * Unless required by applicable law or agreed to in writing, software distributed under the License
12  * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
13  * or implied. See the License for the specific language governing permissions and limitations under
14  * the License.
15  */
16 package com.android.healthconnect.controller.shared.dialog
17 
18 import android.content.Context
19 import android.content.DialogInterface
20 import android.text.SpannableString
21 import android.view.Gravity.CENTER
22 import android.view.LayoutInflater
23 import android.view.View
24 import android.widget.ImageView
25 import android.widget.TextView
26 import androidx.annotation.AttrRes
27 import androidx.annotation.StringRes
28 import androidx.appcompat.app.AlertDialog
29 import androidx.fragment.app.Fragment
30 import androidx.fragment.app.FragmentActivity
31 import com.android.healthconnect.controller.R
32 import com.android.healthconnect.controller.utils.AttributeResolver
33 import com.android.healthconnect.controller.utils.increaseViewTouchTargetSize
34 import com.android.healthconnect.controller.utils.logging.ElementName
35 import com.android.healthconnect.controller.utils.logging.ErrorPageElement
36 import com.android.healthconnect.controller.utils.logging.HealthConnectLogger
37 import com.android.healthconnect.controller.utils.logging.HealthConnectLoggerEntryPoint
38 import dagger.hilt.android.EntryPointAccessors
39 
40 /** {@link AlertDialog.Builder} wrapper for applying theming attributes. */
41 class AlertDialogBuilder(private val context: Context, private val containerLogName: ElementName) {
42 
43     private var alertDialogBuilder: AlertDialog.Builder
44     private var customTitleLayout: View =
45         LayoutInflater.from(context).inflate(R.layout.dialog_title, null)
46     private var customMessageLayout: View =
47         LayoutInflater.from(context).inflate(R.layout.dialog_message, null)
48     private var customDialogLayout: View =
49         LayoutInflater.from(context).inflate(R.layout.dialog_custom_layout, null)
50     private var logger: HealthConnectLogger
51 
52     constructor(
53         fragment: Fragment,
54         containerLogName: ElementName
55     ) : this(fragment.requireContext(), containerLogName)
56 
57     constructor(
58         activity: FragmentActivity,
59         containerLogName: ElementName
60     ) : this(activity as Context, containerLogName)
61 
62     private var iconView: ImageView? = null
63 
64     private var positiveButtonKey: ElementName = ErrorPageElement.UNKNOWN_ELEMENT
65     private var negativeButtonKey: ElementName = ErrorPageElement.UNKNOWN_ELEMENT
66     private var loggingAction = {}
67 
68     private var hasPositiveButton = false
69     private var hasNegativeButton = false
70 
71     init {
72         val hiltEntryPoint =
73             EntryPointAccessors.fromApplication(
74                 this.context.applicationContext, HealthConnectLoggerEntryPoint::class.java)
75         logger = hiltEntryPoint.logger()
76 
77         alertDialogBuilder = AlertDialog.Builder(context)
78         alertDialogBuilder.setView(customDialogLayout)
79     }
80 
81     fun setCancelable(isCancelable: Boolean): AlertDialogBuilder {
82         alertDialogBuilder.setCancelable(isCancelable)
83         return this
84     }
85 
86     fun setIcon(@AttrRes iconId: Int): AlertDialogBuilder {
87         iconView = customDialogLayout.findViewById(R.id.dialog_icon)
88         val iconDrawable = AttributeResolver.getNullableDrawable(context, iconId)
89         iconDrawable?.let {
90             iconView?.setImageDrawable(it)
91             iconView?.visibility = View.VISIBLE
92         }
93 
94         return this
95     }
96 
97     fun setCustomIcon(@AttrRes iconId: Int): AlertDialogBuilder {
98         iconView = customTitleLayout.findViewById(R.id.dialog_icon)
99         val iconDrawable = AttributeResolver.getNullableDrawable(context, iconId)
100         iconDrawable?.let {
101             iconView?.setImageDrawable(it)
102             iconView?.visibility = View.VISIBLE
103             alertDialogBuilder.setCustomTitle(customTitleLayout)
104         }
105 
106         return this
107     }
108 
109     /** Sets the title in the title text view using the given resource id. */
110     fun setTitle(@StringRes titleId: Int): AlertDialogBuilder {
111         val titleView: TextView = customDialogLayout.findViewById(R.id.dialog_title)
112         titleView.setText(titleId)
113         return this
114     }
115 
116     /** Sets the title in the title text view using the given string. */
117     fun setTitle(titleString: String): AlertDialogBuilder {
118         val titleView: TextView = customDialogLayout.findViewById(R.id.dialog_title)
119         titleView.text = titleString
120         return this
121     }
122 
123     /** Sets the title with custom view in the custom title layout using the given resource id. */
124     fun setCustomTitle(@StringRes titleId: Int): AlertDialogBuilder {
125         val titleView: TextView = customTitleLayout.findViewById(R.id.dialog_title)
126         titleView.setText(titleId)
127         alertDialogBuilder.setCustomTitle(customTitleLayout)
128         return this
129     }
130 
131     /** Sets the title with custom view in the custom title layout. */
132     fun setCustomTitle(titleString: String): AlertDialogBuilder {
133         val titleView: TextView = customTitleLayout.findViewById(R.id.dialog_title)
134         titleView.text = titleString
135         alertDialogBuilder.setCustomTitle(customTitleLayout)
136         return this
137     }
138 
139     /** Sets the title with custom view in the custom title layout using a Spannable String. */
140     fun setCustomTitle(titleString: SpannableString): AlertDialogBuilder {
141         val titleView: TextView = customTitleLayout.findViewById(R.id.dialog_title)
142         titleView.text = titleString
143         alertDialogBuilder.setCustomTitle(customTitleLayout)
144         return this
145     }
146 
147     /** Sets the message to be displayed in the dialog using the given resource id. */
148     fun setMessage(@StringRes messageId: Int): AlertDialogBuilder {
149         val messageView: TextView = customDialogLayout.findViewById(R.id.dialog_custom_message)
150         messageView.text = context.getString(messageId)
151         return this
152     }
153 
154     /** Sets the message to be displayed in the dialog. */
155     fun setMessage(message: CharSequence?): AlertDialogBuilder {
156         val messageView: TextView = customDialogLayout.findViewById(R.id.dialog_custom_message)
157         messageView.text = message
158         return this
159     }
160 
161     fun setMessage(message: String): AlertDialogBuilder {
162         val messageView: TextView = customDialogLayout.findViewById(R.id.dialog_custom_message)
163         messageView.text = message
164         return this
165     }
166 
167     /**
168      * Sets the message with custom view to be displayed in the dialog using the given resource id.
169      */
170     fun setCustomMessage(@StringRes messageId: Int): AlertDialogBuilder {
171         val messageView: TextView = customMessageLayout.findViewById(R.id.dialog_custom_message)
172         messageView.text = context.getString(messageId)
173         alertDialogBuilder.setView(customMessageLayout)
174         return this
175     }
176 
177     /** Sets the message with custom view to be displayed in the dialog. */
178     fun setCustomMessage(message: CharSequence?): AlertDialogBuilder {
179         val messageView: TextView = customMessageLayout.findViewById(R.id.dialog_custom_message)
180         messageView.text = message
181         alertDialogBuilder.setView(customMessageLayout)
182         return this
183     }
184 
185     fun setCustomMessage(message: String): AlertDialogBuilder {
186         val messageView: TextView = customMessageLayout.findViewById(R.id.dialog_custom_message)
187         messageView.text = message
188         alertDialogBuilder.setView(customMessageLayout)
189         return this
190     }
191 
192     fun setView(view: View): AlertDialogBuilder {
193         alertDialogBuilder.setView(view)
194         return this
195     }
196 
197     fun setNegativeButton(
198         @StringRes textId: Int,
199         buttonId: ElementName,
200         onClickListener: DialogInterface.OnClickListener? = null
201     ): AlertDialogBuilder {
202         hasNegativeButton = true
203         negativeButtonKey = buttonId
204 
205         val loggingClickListener =
206             DialogInterface.OnClickListener { dialog, which ->
207                 logger.logInteraction(negativeButtonKey)
208                 onClickListener?.onClick(dialog, which)
209             }
210 
211         alertDialogBuilder.setNegativeButton(textId, loggingClickListener)
212         return this
213     }
214 
215     /**
216      * To ensure a clear and accessible layout for all users, this button replaces a traditional
217      * negative button with a neutral button and used as a negative button when a positive button is
218      * also present. This prevents button borders from overlapping, when display and font sizes are
219      * set to their largest in accessibility settings.
220      */
221     fun setNeutralButton(
222         @StringRes textId: Int,
223         buttonId: ElementName,
224         onClickListener: DialogInterface.OnClickListener? = null
225     ): AlertDialogBuilder {
226         hasNegativeButton = true
227         negativeButtonKey = buttonId
228 
229         val loggingClickListener =
230             DialogInterface.OnClickListener { dialog, which ->
231                 logger.logInteraction(negativeButtonKey)
232                 onClickListener?.onClick(dialog, which)
233             }
234 
235         alertDialogBuilder.setNeutralButton(textId, loggingClickListener)
236         return this
237     }
238 
239     fun setPositiveButton(
240         @StringRes textId: Int,
241         buttonId: ElementName,
242         onClickListener: DialogInterface.OnClickListener? = null
243     ): AlertDialogBuilder {
244         hasPositiveButton = true
245         positiveButtonKey = buttonId
246         val loggingClickListener =
247             DialogInterface.OnClickListener { dialog, which ->
248                 logger.logInteraction(positiveButtonKey)
249                 onClickListener?.onClick(dialog, which)
250             }
251 
252         alertDialogBuilder.setPositiveButton(textId, loggingClickListener)
253         return this
254     }
255 
256     /**
257      * Allows setting additional logging actions for custom dialog elements, such as messages,
258      * checkboxes or radio buttons.
259      *
260      * Impressions should be logged only once the dialog has been created.
261      */
262     fun setAdditionalLogging(loggingAction: () -> Unit): AlertDialogBuilder {
263         this.loggingAction = loggingAction
264         return this
265     }
266 
267     fun create(): AlertDialog {
268         val dialog = alertDialogBuilder.create()
269         setDialogGravityFromTheme(dialog)
270 
271         dialog.setOnShowListener { increaseDialogTouchTargetSize(dialog) }
272 
273         // Dialog container
274         logger.logImpression(this.containerLogName)
275 
276         // Dialog buttons
277         if (hasPositiveButton) {
278             logger.logImpression(positiveButtonKey)
279         }
280         if (hasNegativeButton) {
281             logger.logImpression(negativeButtonKey)
282         }
283 
284         // Any additional logging e.g. for dialog messages
285         loggingAction()
286 
287         return dialog
288     }
289 
290     private fun increaseDialogTouchTargetSize(dialog: AlertDialog) {
291         if (hasPositiveButton) {
292             val positiveButtonView = dialog.getButton(DialogInterface.BUTTON_POSITIVE)
293             val parentView = positiveButtonView.parent as View
294             increaseViewTouchTargetSize(context, positiveButtonView, parentView)
295         }
296 
297         if (hasNegativeButton) {
298             val negativeButtonView = dialog.getButton(DialogInterface.BUTTON_NEGATIVE)
299             val parentView = negativeButtonView.parent.parent as View
300             increaseViewTouchTargetSize(context, negativeButtonView, parentView)
301         }
302     }
303 
304     private fun setDialogGravityFromTheme(dialog: AlertDialog) {
305         val typedArray = context.obtainStyledAttributes(intArrayOf(R.attr.dialogGravity))
306         try {
307             if (typedArray.hasValue(0)) {
308                 requireNotNull(dialog.window).setGravity(typedArray.getInteger(0, CENTER))
309             }
310         } finally {
311             typedArray.recycle()
312         }
313     }
314 }
315