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