• 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.view.Gravity.CENTER
21 import android.view.LayoutInflater
22 import android.view.View
23 import android.widget.Button
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) {
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 logger: HealthConnectLogger
49 
50     constructor(fragment: Fragment) : this(fragment.requireContext())
51 
52     constructor(activity: FragmentActivity) : this(activity as Context)
53 
54     private var iconView: ImageView? = null
55     private var positiveButton: Button? = null
56     private var negativeButton: Button? = null
57 
58     private var positiveButtonKey: ElementName = ErrorPageElement.UNKNOWN_ELEMENT
59     private var negativeButtonKey: ElementName = ErrorPageElement.UNKNOWN_ELEMENT
60     private var elementName: ElementName = ErrorPageElement.UNKNOWN_ELEMENT
61     private var loggingAction = {}
62 
63     private var hasPositiveButton = false
64     private var hasNegativeButton = false
65 
66     init {
67         val hiltEntryPoint =
68             EntryPointAccessors.fromApplication(
69                 this.context.applicationContext, HealthConnectLoggerEntryPoint::class.java)
70         logger = hiltEntryPoint.logger()
71 
72         alertDialogBuilder = AlertDialog.Builder(context)
73     }
74 
75     fun setCancelable(isCancelable: Boolean): AlertDialogBuilder {
76         alertDialogBuilder.setCancelable(isCancelable)
77         return this
78     }
79 
80     fun setLogName(elementName: ElementName): AlertDialogBuilder {
81         this.elementName = elementName
82         return this
83     }
84 
85     fun setIcon(@AttrRes iconId: Int): AlertDialogBuilder {
86         iconView = customTitleLayout.findViewById(R.id.dialog_icon)
87         val iconDrawable = AttributeResolver.getNullableDrawable(context, iconId)
88         iconDrawable?.let {
89             iconView?.setImageDrawable(it)
90             iconView?.visibility = View.VISIBLE
91             alertDialogBuilder.setCustomTitle(customTitleLayout)
92         }
93 
94         return this
95     }
96 
97     /** Sets the title in the custom title layout using the given resource id. */
98     fun setTitle(@StringRes titleId: Int): AlertDialogBuilder {
99         val titleView: TextView = customTitleLayout.findViewById(R.id.dialog_title)
100         titleView.setText(titleId)
101         alertDialogBuilder.setCustomTitle(customTitleLayout)
102         return this
103     }
104 
105     /** Sets the title in the custom title layout. */
106     fun setTitle(titleString: String): AlertDialogBuilder {
107         val titleView: TextView = customTitleLayout.findViewById(R.id.dialog_title)
108         titleView.text = titleString
109         alertDialogBuilder.setCustomTitle(customTitleLayout)
110         return this
111     }
112 
113     /** Sets the message to be displayed in the dialog using the given resource id. */
114     fun setMessage(@StringRes messageId: Int): AlertDialogBuilder {
115         val messageView: TextView = customMessageLayout.findViewById(R.id.dialog_custom_message)
116         messageView.text = context.getString(messageId)
117         alertDialogBuilder.setView(customMessageLayout)
118         return this
119     }
120 
121     /** Sets the message to be displayed in the dialog. */
122     fun setMessage(message: CharSequence?): AlertDialogBuilder {
123         val messageView: TextView = customMessageLayout.findViewById(R.id.dialog_custom_message)
124         messageView.text = message
125         alertDialogBuilder.setView(customMessageLayout)
126         return this
127     }
128 
129     fun setMessage(message: String): AlertDialogBuilder {
130         val messageView: TextView = customMessageLayout.findViewById(R.id.dialog_custom_message)
131         messageView.text = message
132         alertDialogBuilder.setView(customMessageLayout)
133         return this
134     }
135 
136     fun setView(view: View): AlertDialogBuilder {
137         alertDialogBuilder.setView(view)
138         return this
139     }
140 
141     fun setNegativeButton(
142         @StringRes textId: Int,
143         buttonId: ElementName,
144         onClickListener: DialogInterface.OnClickListener? = null
145     ): AlertDialogBuilder {
146         hasNegativeButton = true
147         negativeButtonKey = buttonId
148 
149         val loggingClickListener =
150             DialogInterface.OnClickListener { dialog, which ->
151                 logger.logInteraction(negativeButtonKey)
152                 onClickListener?.onClick(dialog, which)
153             }
154 
155         alertDialogBuilder.setNegativeButton(textId, loggingClickListener)
156         return this
157     }
158 
159     fun setPositiveButton(
160         @StringRes textId: Int,
161         buttonId: ElementName,
162         onClickListener: DialogInterface.OnClickListener? = null
163     ): AlertDialogBuilder {
164         hasPositiveButton = true
165         positiveButtonKey = buttonId
166         val loggingClickListener =
167             DialogInterface.OnClickListener { dialog, which ->
168                 logger.logInteraction(positiveButtonKey)
169                 onClickListener?.onClick(dialog, which)
170             }
171         alertDialogBuilder.setPositiveButton(textId, loggingClickListener)
172         return this
173     }
174 
175     /**
176      * Allows setting additional logging actions for custom dialog elements, such as messages,
177      * checkboxes or radio buttons.
178      *
179      * Impressions should be logged only once the dialog has been created.
180      */
181     fun setAdditionalLogging(loggingAction: () -> Unit): AlertDialogBuilder {
182         this.loggingAction = loggingAction
183         return this
184     }
185 
186     fun create(): AlertDialog {
187         val dialog = alertDialogBuilder.create()
188         setDialogGravityFromTheme(dialog)
189 
190         dialog.setOnShowListener { increaseDialogTouchTargetSize(dialog) }
191 
192         // Dialog container
193         logger.logImpression(elementName)
194 
195         // Dialog buttons
196         if (hasPositiveButton) {
197             logger.logImpression(positiveButtonKey)
198         }
199         if (hasNegativeButton) {
200             logger.logImpression(negativeButtonKey)
201         }
202 
203         // Any additional logging e.g. for dialog messages
204         loggingAction()
205 
206         return dialog
207     }
208 
209     private fun increaseDialogTouchTargetSize(dialog: AlertDialog) {
210         if (hasPositiveButton) {
211             val positiveButtonView = dialog.getButton(DialogInterface.BUTTON_POSITIVE)
212             val parentView = positiveButtonView.parent as View
213             increaseViewTouchTargetSize(context, positiveButtonView, parentView)
214         }
215 
216         if (hasNegativeButton) {
217             val negativeButtonView = dialog.getButton(DialogInterface.BUTTON_NEGATIVE)
218             val parentView = negativeButtonView.parent.parent as View
219             increaseViewTouchTargetSize(context, negativeButtonView, parentView)
220         }
221     }
222 
223     private fun setDialogGravityFromTheme(dialog: AlertDialog) {
224         val typedArray = context.obtainStyledAttributes(intArrayOf(R.attr.dialogGravity))
225         try {
226             if (typedArray.hasValue(0)) {
227                 requireNotNull(dialog.window).setGravity(typedArray.getInteger(0, CENTER))
228             }
229         } finally {
230             typedArray.recycle()
231         }
232     }
233 }
234