• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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.HealthConnectLogger
36 import com.android.healthconnect.controller.utils.logging.HealthConnectLoggerEntryPoint
37 import com.android.healthconnect.controller.utils.logging.UnknownGenericElement
38 import com.android.settingslib.widget.SettingsThemeHelper
39 import dagger.hilt.android.EntryPointAccessors
40 
41 /** {@link AlertDialog.Builder} wrapper for applying theming attributes. */
42 class AlertDialogBuilder(private val context: Context, private val containerLogName: ElementName) {
43 
44     private var alertDialogBuilder: AlertDialog.Builder
45     private var customTitleLayout: View =
46         inflateLayout(R.layout.dialog_title_expressive, R.layout.dialog_title_legacy)
47     private var customMessageLayout: View =
48         inflateLayout(R.layout.dialog_message_expressive, R.layout.dialog_message_legacy)
49     private var customDialogLayout: View =
50         inflateLayout(R.layout.dialog_layout_expressive, R.layout.dialog_layout_legacy)
51 
52     private var logger: HealthConnectLogger
53 
54     constructor(
55         fragment: Fragment,
56         containerLogName: ElementName,
57     ) : this(fragment.requireContext(), containerLogName)
58 
59     constructor(
60         activity: FragmentActivity,
61         containerLogName: ElementName,
62     ) : this(activity as Context, containerLogName)
63 
64     private var iconView: ImageView? = null
65 
66     private var positiveButtonKey: ElementName =
67         UnknownGenericElement.UNKNOWN_DIALOG_POSITIVE_BUTTON
68     private var negativeButtonKey: ElementName =
69         UnknownGenericElement.UNKNOWN_DIALOG_NEGATIVE_BUTTON
70     private var loggingAction = {}
71 
72     private var hasPositiveButton = false
73     private var hasNegativeButton = false
74 
75     init {
76         val hiltEntryPoint =
77             EntryPointAccessors.fromApplication(
78                 this.context.applicationContext,
79                 HealthConnectLoggerEntryPoint::class.java,
80             )
81         logger = hiltEntryPoint.logger()
82 
83         alertDialogBuilder = AlertDialog.Builder(context)
84         alertDialogBuilder.setView(customDialogLayout)
85     }
86 
87     fun setCancelable(isCancelable: Boolean): AlertDialogBuilder {
88         alertDialogBuilder.setCancelable(isCancelable)
89         return this
90     }
91 
92     fun setIcon(@AttrRes iconId: Int): AlertDialogBuilder {
93         iconView = customDialogLayout.findViewById(R.id.dialog_icon)
94         val iconDrawable = AttributeResolver.getNullableDrawable(context, iconId)
95         iconDrawable?.let {
96             iconView?.setImageDrawable(it)
97             iconView?.visibility = View.VISIBLE
98         }
99 
100         return this
101     }
102 
103     fun setCustomIcon(@AttrRes iconId: Int): AlertDialogBuilder {
104         iconView = customTitleLayout.findViewById(R.id.dialog_icon)
105         val iconDrawable = AttributeResolver.getNullableDrawable(context, iconId)
106         iconDrawable?.let {
107             iconView?.setImageDrawable(it)
108             iconView?.visibility = View.VISIBLE
109             alertDialogBuilder.setCustomTitle(customTitleLayout)
110         }
111 
112         return this
113     }
114 
115     /** Sets the title in the title text view using the given resource id. */
116     fun setTitle(@StringRes titleId: Int): AlertDialogBuilder {
117         val titleView: TextView = customDialogLayout.findViewById(R.id.dialog_title)
118         titleView.setText(titleId)
119         return this
120     }
121 
122     /** Sets the title in the title text view using the given string. */
123     fun setTitle(titleString: String): AlertDialogBuilder {
124         val titleView: TextView = customDialogLayout.findViewById(R.id.dialog_title)
125         titleView.text = titleString
126         return this
127     }
128 
129     /** Sets the title with custom view in the custom title layout using the given resource id. */
130     fun setCustomTitle(@StringRes titleId: Int): AlertDialogBuilder {
131         val titleView: TextView = customTitleLayout.findViewById(R.id.dialog_title)
132         titleView.setText(titleId)
133         alertDialogBuilder.setCustomTitle(customTitleLayout)
134         return this
135     }
136 
137     /** Sets the title with custom view in the custom title layout. */
138     fun setCustomTitle(titleString: String): AlertDialogBuilder {
139         val titleView: TextView = customTitleLayout.findViewById(R.id.dialog_title)
140         titleView.text = titleString
141         alertDialogBuilder.setCustomTitle(customTitleLayout)
142         return this
143     }
144 
145     /** Sets the title with custom view in the custom title layout using a Spannable String. */
146     fun setCustomTitle(titleString: SpannableString): AlertDialogBuilder {
147         val titleView: TextView = customTitleLayout.findViewById(R.id.dialog_title)
148         titleView.text = titleString
149         alertDialogBuilder.setCustomTitle(customTitleLayout)
150         return this
151     }
152 
153     /** Sets the message to be displayed in the dialog using the given resource id. */
154     fun setMessage(@StringRes messageId: Int): AlertDialogBuilder {
155         val messageView: TextView = customDialogLayout.findViewById(R.id.dialog_custom_message)
156         messageView.text = context.getString(messageId)
157         return this
158     }
159 
160     /** Sets the message to be displayed in the dialog. */
161     fun setMessage(message: CharSequence?): AlertDialogBuilder {
162         val messageView: TextView = customDialogLayout.findViewById(R.id.dialog_custom_message)
163         messageView.text = message
164         return this
165     }
166 
167     fun setMessage(message: String): AlertDialogBuilder {
168         val messageView: TextView = customDialogLayout.findViewById(R.id.dialog_custom_message)
169         messageView.text = message
170         return this
171     }
172 
173     /**
174      * Sets the message with custom view to be displayed in the dialog using the given resource id.
175      */
176     fun setCustomMessage(@StringRes messageId: Int): AlertDialogBuilder {
177         val messageView: TextView = customMessageLayout.findViewById(R.id.dialog_custom_message)
178         messageView.text = context.getString(messageId)
179         alertDialogBuilder.setView(customMessageLayout)
180         return this
181     }
182 
183     /** Sets the message with custom view to be displayed in the dialog. */
184     fun setCustomMessage(message: CharSequence?): AlertDialogBuilder {
185         val messageView: TextView = customMessageLayout.findViewById(R.id.dialog_custom_message)
186         messageView.text = message
187         alertDialogBuilder.setView(customMessageLayout)
188         return this
189     }
190 
191     fun setCustomMessage(message: String): AlertDialogBuilder {
192         val messageView: TextView = customMessageLayout.findViewById(R.id.dialog_custom_message)
193         messageView.text = message
194         alertDialogBuilder.setView(customMessageLayout)
195         return this
196     }
197 
198     fun setView(view: View): AlertDialogBuilder {
199         alertDialogBuilder.setView(view)
200         return this
201     }
202 
203     fun setNegativeButton(
204         @StringRes textId: Int,
205         buttonId: ElementName,
206         onClickListener: DialogInterface.OnClickListener? = null,
207     ): AlertDialogBuilder {
208         hasNegativeButton = true
209         negativeButtonKey = buttonId
210 
211         val loggingClickListener =
212             DialogInterface.OnClickListener { dialog, which ->
213                 logger.logInteraction(negativeButtonKey)
214                 onClickListener?.onClick(dialog, which)
215             }
216 
217         alertDialogBuilder.setNegativeButton(textId, loggingClickListener)
218         return this
219     }
220 
221     /**
222      * To ensure a clear and accessible layout for all users, this button replaces a traditional
223      * negative button with a neutral button and used as a negative button when a positive button is
224      * also present. This prevents button borders from overlapping, when display and font sizes are
225      * set to their largest in accessibility settings.
226      */
227     fun setNeutralButton(
228         @StringRes textId: Int,
229         buttonId: ElementName,
230         onClickListener: DialogInterface.OnClickListener? = null,
231     ): AlertDialogBuilder {
232         hasNegativeButton = true
233         negativeButtonKey = buttonId
234 
235         val loggingClickListener =
236             DialogInterface.OnClickListener { dialog, which ->
237                 logger.logInteraction(negativeButtonKey)
238                 onClickListener?.onClick(dialog, which)
239             }
240 
241         alertDialogBuilder.setNeutralButton(textId, loggingClickListener)
242         return this
243     }
244 
245     fun setPositiveButton(
246         @StringRes textId: Int,
247         buttonId: ElementName,
248         onClickListener: DialogInterface.OnClickListener? = null,
249     ): AlertDialogBuilder {
250         hasPositiveButton = true
251         positiveButtonKey = buttonId
252         val loggingClickListener =
253             DialogInterface.OnClickListener { dialog, which ->
254                 logger.logInteraction(positiveButtonKey)
255                 onClickListener?.onClick(dialog, which)
256             }
257 
258         alertDialogBuilder.setPositiveButton(textId, loggingClickListener)
259         return this
260     }
261 
262     /**
263      * Allows setting additional logging actions for custom dialog elements, such as messages,
264      * checkboxes or radio buttons.
265      *
266      * Impressions should be logged only once the dialog has been created.
267      */
268     fun setAdditionalLogging(loggingAction: () -> Unit): AlertDialogBuilder {
269         this.loggingAction = loggingAction
270         return this
271     }
272 
273     fun create(): AlertDialog {
274         val dialog = alertDialogBuilder.create()
275         setDialogGravityFromTheme(dialog)
276 
277         dialog.setOnShowListener { increaseDialogTouchTargetSize(dialog) }
278 
279         // Dialog container
280         logger.logImpression(this.containerLogName)
281 
282         // Dialog buttons
283         if (hasPositiveButton) {
284             logger.logImpression(positiveButtonKey)
285         }
286         if (hasNegativeButton) {
287             logger.logImpression(negativeButtonKey)
288         }
289 
290         // Any additional logging e.g. for dialog messages
291         loggingAction()
292 
293         return dialog
294     }
295 
296     private fun increaseDialogTouchTargetSize(dialog: AlertDialog) {
297         if (hasPositiveButton) {
298             val positiveButtonView = dialog.getButton(DialogInterface.BUTTON_POSITIVE)
299             val parentView = positiveButtonView.parent as View
300             increaseViewTouchTargetSize(context, positiveButtonView, parentView)
301         }
302 
303         if (hasNegativeButton) {
304             val negativeButtonView = dialog.getButton(DialogInterface.BUTTON_NEGATIVE)
305             val parentView = negativeButtonView.parent.parent as View
306             increaseViewTouchTargetSize(context, negativeButtonView, parentView)
307         }
308     }
309 
310     private fun setDialogGravityFromTheme(dialog: AlertDialog) {
311         val typedArray = context.obtainStyledAttributes(intArrayOf(R.attr.dialogGravity))
312         try {
313             if (typedArray.hasValue(0)) {
314                 requireNotNull(dialog.window).setGravity(typedArray.getInteger(0, CENTER))
315             }
316         } finally {
317             typedArray.recycle()
318         }
319     }
320 
321     private fun inflateLayout(expressiveLayout: Int, legacyLayout: Int): View {
322         return LayoutInflater.from(context)
323             .inflate(
324                 if (SettingsThemeHelper.isExpressiveTheme(context)) expressiveLayout
325                 else legacyLayout,
326                 null,
327             )
328     }
329 }
330