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