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