1 /* 2 * Copyright (C) 2020 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.deskclock 18 19 import android.annotation.SuppressLint 20 import android.annotation.TargetApi 21 import android.app.AlarmManager 22 import android.app.AlarmManager.AlarmClockInfo 23 import android.app.PendingIntent 24 import android.appwidget.AppWidgetManager 25 import android.appwidget.AppWidgetProviderInfo 26 import android.content.ContentResolver 27 import android.content.Context 28 import android.content.Intent 29 import android.content.res.Configuration 30 import android.graphics.Bitmap 31 import android.graphics.Canvas 32 import android.graphics.Color 33 import android.graphics.Paint 34 import android.graphics.PorterDuff 35 import android.graphics.PorterDuffColorFilter 36 import android.graphics.Typeface 37 import android.net.Uri 38 import android.os.Build 39 import android.os.Looper 40 import android.provider.Settings 41 import android.text.Spannable 42 import android.text.SpannableString 43 import android.text.TextUtils 44 import android.text.format.DateFormat 45 import android.text.format.DateUtils 46 import android.text.style.RelativeSizeSpan 47 import android.text.style.StyleSpan 48 import android.text.style.TypefaceSpan 49 import android.util.ArraySet 50 import android.view.View 51 import android.widget.TextClock 52 import android.widget.TextView 53 import androidx.annotation.AnyRes 54 import androidx.annotation.DrawableRes 55 import androidx.annotation.StringRes 56 import androidx.core.os.BuildCompat 57 import androidx.core.view.AccessibilityDelegateCompat 58 import androidx.core.view.accessibility.AccessibilityNodeInfoCompat 59 import androidx.core.view.accessibility.AccessibilityNodeInfoCompat.AccessibilityActionCompat 60 import androidx.vectordrawable.graphics.drawable.VectorDrawableCompat 61 62 import com.android.deskclock.data.DataModel 63 import com.android.deskclock.provider.AlarmInstance 64 import com.android.deskclock.uidata.UiDataModel 65 66 import java.text.NumberFormat 67 import java.text.SimpleDateFormat 68 import java.util.Calendar 69 import java.util.Date 70 import java.util.Locale 71 import java.util.TimeZone 72 73 import kotlin.math.abs 74 import kotlin.math.max 75 76 object Utils { 77 /** 78 * [Uri] signifying the "silent" ringtone. 79 */ 80 @JvmField 81 val RINGTONE_SILENT = Uri.EMPTY 82 enforceMainLoopernull83 fun enforceMainLooper() { 84 if (Looper.getMainLooper() != Looper.myLooper()) { 85 throw IllegalAccessError("May only call from main thread.") 86 } 87 } 88 enforceNotMainLoopernull89 fun enforceNotMainLooper() { 90 if (Looper.getMainLooper() == Looper.myLooper()) { 91 throw IllegalAccessError("May not call from main thread.") 92 } 93 } 94 indexOfnull95 fun indexOf(array: Array<out Any>, item: Any): Int { 96 for (i in array.indices) { 97 if (array[i] == item) { 98 return i 99 } 100 } 101 return -1 102 } 103 104 /** 105 * @return `true` if the device is prior to [Build.VERSION_CODES.LOLLIPOP] 106 */ 107 val isPreL: Boolean 108 get() = Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP 109 110 /** 111 * @return `true` if the device is [Build.VERSION_CODES.LOLLIPOP] or 112 * [Build.VERSION_CODES.LOLLIPOP_MR1] 113 */ 114 val isLOrLMR1: Boolean 115 get() { 116 val sdkInt = Build.VERSION.SDK_INT 117 return sdkInt == Build.VERSION_CODES.LOLLIPOP || 118 sdkInt == Build.VERSION_CODES.LOLLIPOP_MR1 119 } 120 121 /** 122 * @return `true` if the device is [Build.VERSION_CODES.LOLLIPOP] or later 123 */ 124 val isLOrLater: Boolean 125 get() = Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP 126 127 /** 128 * @return `true` if the device is [Build.VERSION_CODES.LOLLIPOP_MR1] or later 129 */ 130 val isLMR1OrLater: Boolean 131 get() = Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP_MR1 132 133 /** 134 * @return `true` if the device is [Build.VERSION_CODES.M] or later 135 */ 136 val isMOrLater: Boolean 137 get() = Build.VERSION.SDK_INT >= Build.VERSION_CODES.M 138 139 /** 140 * @return `true` if the device is [Build.VERSION_CODES.N] or later 141 */ 142 val isNOrLater: Boolean 143 get() = BuildCompat.isAtLeastN() 144 145 /** 146 * @return `true` if the device is [Build.VERSION_CODES.N_MR1] or later 147 */ 148 val isNMR1OrLater: Boolean 149 get() = BuildCompat.isAtLeastNMR1() 150 151 /** 152 * @return `true` if the device is [Build.VERSION_CODES.O] or later 153 */ 154 val isOOrLater: Boolean 155 get() = Build.VERSION.SDK_INT >= Build.VERSION_CODES.O 156 157 /** 158 * @param resourceId identifies an application resource 159 * @return the Uri by which the application resource is accessed 160 */ getResourceUrinull161 fun getResourceUri(context: Context, @AnyRes resourceId: Int): Uri { 162 return Uri.Builder() 163 .scheme(ContentResolver.SCHEME_ANDROID_RESOURCE) 164 .authority(context.packageName) 165 .path(resourceId.toString()) 166 .build() 167 } 168 169 /** 170 * @param view the scrollable view to test 171 * @return `true` iff the `view` content is currently scrolled to the top 172 */ isScrolledToTopnull173 fun isScrolledToTop(view: View): Boolean { 174 return !view.canScrollVertically(-1) 175 } 176 177 /** 178 * Calculate the amount by which the radius of a CircleTimerView should be offset by any 179 * of the extra painted objects. 180 */ calculateRadiusOffsetnull181 fun calculateRadiusOffset( 182 strokeSize: Float, 183 dotStrokeSize: Float, 184 markerStrokeSize: Float 185 ): Float { 186 return max(strokeSize, max(dotStrokeSize, markerStrokeSize)) 187 } 188 189 /** 190 * Configure the clock that is visible to display seconds. The clock that is not visible never 191 * displays seconds to avoid it scheduling unnecessary ticking runnables. 192 */ setClockSecondsEnablednull193 fun setClockSecondsEnabled(digitalClock: TextClock, analogClock: AnalogClock) { 194 val displaySeconds: Boolean = DataModel.dataModel.displayClockSeconds 195 when (DataModel.dataModel.clockStyle) { 196 DataModel.ClockStyle.ANALOG -> { 197 setTimeFormat(digitalClock, false) 198 analogClock.enableSeconds(displaySeconds) 199 } 200 DataModel.ClockStyle.DIGITAL -> { 201 analogClock.enableSeconds(false) 202 setTimeFormat(digitalClock, displaySeconds) 203 } 204 } 205 } 206 207 /** 208 * Set whether the digital or analog clock should be displayed in the application. 209 * Returns the view to be displayed. 210 */ setClockStylenull211 fun setClockStyle(digitalClock: View, analogClock: View): View { 212 return when (DataModel.dataModel.clockStyle) { 213 DataModel.ClockStyle.ANALOG -> { 214 digitalClock.visibility = View.GONE 215 analogClock.visibility = View.VISIBLE 216 analogClock 217 } 218 DataModel.ClockStyle.DIGITAL -> { 219 digitalClock.visibility = View.VISIBLE 220 analogClock.visibility = View.GONE 221 digitalClock 222 } 223 } 224 } 225 226 /** 227 * For screensavers to set whether the digital or analog clock should be displayed. 228 * Returns the view to be displayed. 229 */ setScreensaverClockStylenull230 fun setScreensaverClockStyle(digitalClock: View, analogClock: View): View { 231 return when (DataModel.dataModel.screensaverClockStyle) { 232 DataModel.ClockStyle.ANALOG -> { 233 digitalClock.visibility = View.GONE 234 analogClock.visibility = View.VISIBLE 235 analogClock 236 } 237 DataModel.ClockStyle.DIGITAL -> { 238 digitalClock.visibility = View.VISIBLE 239 analogClock.visibility = View.GONE 240 digitalClock 241 } 242 } 243 } 244 245 /** 246 * For screensavers to dim the lights if necessary. 247 */ dimClockViewnull248 fun dimClockView(dim: Boolean, clockView: View) { 249 val paint = Paint() 250 paint.color = Color.WHITE 251 paint.colorFilter = PorterDuffColorFilter( 252 if (dim) 0x40FFFFFF else -0x3f000001, 253 PorterDuff.Mode.MULTIPLY) 254 clockView.setLayerType(View.LAYER_TYPE_HARDWARE, paint) 255 } 256 257 /** 258 * Update and return the PendingIntent corresponding to the given `intent`. 259 * 260 * @param context the Context in which the PendingIntent should start the service 261 * @param intent an Intent describing the service to be started 262 * @return a PendingIntent that will start a service 263 */ pendingServiceIntentnull264 fun pendingServiceIntent(context: Context, intent: Intent): PendingIntent { 265 return PendingIntent.getService(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT) 266 } 267 268 /** 269 * Update and return the PendingIntent corresponding to the given `intent`. 270 * 271 * @param context the Context in which the PendingIntent should start the activity 272 * @param intent an Intent describing the activity to be started 273 * @return a PendingIntent that will start an activity 274 */ pendingActivityIntentnull275 fun pendingActivityIntent(context: Context, intent: Intent): PendingIntent { 276 return PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT) 277 } 278 279 /** 280 * @return The next alarm from [AlarmManager] 281 */ getNextAlarmnull282 fun getNextAlarm(context: Context): String? { 283 return if (isPreL) getNextAlarmPreL(context) else getNextAlarmLOrLater(context) 284 } 285 286 @TargetApi(Build.VERSION_CODES.KITKAT) getNextAlarmPreLnull287 private fun getNextAlarmPreL(context: Context): String { 288 val cr = context.contentResolver 289 return Settings.System.getString(cr, Settings.System.NEXT_ALARM_FORMATTED) 290 } 291 292 @TargetApi(Build.VERSION_CODES.LOLLIPOP) getNextAlarmLOrLaternull293 private fun getNextAlarmLOrLater(context: Context): String? { 294 val am = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager 295 val info = getNextAlarmClock(am) 296 if (info != null) { 297 val triggerTime = info.triggerTime 298 val alarmTime = Calendar.getInstance() 299 alarmTime.timeInMillis = triggerTime 300 return AlarmUtils.getFormattedTime(context, alarmTime) 301 } 302 303 return null 304 } 305 306 @TargetApi(Build.VERSION_CODES.LOLLIPOP) getNextAlarmClocknull307 private fun getNextAlarmClock(am: AlarmManager): AlarmClockInfo? { 308 return am.nextAlarmClock 309 } 310 311 @TargetApi(Build.VERSION_CODES.LOLLIPOP) updateNextAlarmnull312 fun updateNextAlarm(am: AlarmManager, info: AlarmClockInfo?, op: PendingIntent?) { 313 am.setAlarmClock(info, op) 314 } 315 isAlarmWithin24Hoursnull316 fun isAlarmWithin24Hours(alarmInstance: AlarmInstance): Boolean { 317 val nextAlarmTime: Calendar = alarmInstance.alarmTime 318 val nextAlarmTimeMillis = nextAlarmTime.timeInMillis 319 return nextAlarmTimeMillis - System.currentTimeMillis() <= DateUtils.DAY_IN_MILLIS 320 } 321 322 /** 323 * Clock views can call this to refresh their alarm to the next upcoming value. 324 */ refreshAlarmnull325 fun refreshAlarm(context: Context, clock: View?) { 326 val nextAlarmIconView = clock?.findViewById<View>(R.id.nextAlarmIcon) as TextView 327 val nextAlarmView = clock.findViewById<View>(R.id.nextAlarm) as TextView? ?: return 328 329 val alarm = getNextAlarm(context) 330 if (!TextUtils.isEmpty(alarm)) { 331 val description = context.getString(R.string.next_alarm_description, alarm) 332 nextAlarmView.text = alarm 333 nextAlarmView.contentDescription = description 334 nextAlarmView.visibility = View.VISIBLE 335 nextAlarmIconView.visibility = View.VISIBLE 336 nextAlarmIconView.contentDescription = description 337 } else { 338 nextAlarmView.visibility = View.GONE 339 nextAlarmIconView.visibility = View.GONE 340 } 341 } 342 setClockIconTypefacenull343 fun setClockIconTypeface(clock: View?) { 344 val nextAlarmIconView = clock?.findViewById<View>(R.id.nextAlarmIcon) as TextView? 345 nextAlarmIconView?.typeface = UiDataModel.uiDataModel.alarmIconTypeface 346 } 347 348 /** 349 * Clock views can call this to refresh their date. 350 */ updateDatenull351 fun updateDate(dateSkeleton: String?, descriptionSkeleton: String?, clock: View?) { 352 val dateDisplay = clock?.findViewById<View>(R.id.date) as TextView? ?: return 353 354 val l = Locale.getDefault() 355 val datePattern = DateFormat.getBestDateTimePattern(l, dateSkeleton) 356 val descriptionPattern = DateFormat.getBestDateTimePattern(l, descriptionSkeleton) 357 358 val now = Date() 359 dateDisplay.text = SimpleDateFormat(datePattern, l).format(now) 360 dateDisplay.visibility = View.VISIBLE 361 dateDisplay.contentDescription = SimpleDateFormat(descriptionPattern, l).format(now) 362 } 363 364 /*** 365 * Formats the time in the TextClock according to the Locale with a special 366 * formatting treatment for the am/pm label. 367 * 368 * @param clock TextClock to format 369 * @param includeSeconds whether or not to include seconds in the clock's time 370 */ setTimeFormatnull371 fun setTimeFormat(clock: TextClock?, includeSeconds: Boolean) { 372 // Get the best format for 12 hours mode according to the locale 373 clock?.format12Hour = get12ModeFormat(amPmRatio = 0.4f, includeSeconds = includeSeconds) 374 // Get the best format for 24 hours mode according to the locale 375 clock?.format24Hour = get24ModeFormat(includeSeconds) 376 } 377 378 /** 379 * @param amPmRatio a value between 0 and 1 that is the ratio of the relative size of the 380 * am/pm string to the time string 381 * @param includeSeconds whether or not to include seconds in the time string 382 * @return format string for 12 hours mode time, not including seconds 383 */ get12ModeFormatnull384 fun get12ModeFormat(amPmRatio: Float, includeSeconds: Boolean): CharSequence { 385 var pattern = DateFormat.getBestDateTimePattern(Locale.getDefault(), 386 if (includeSeconds) "hmsa" else "hma") 387 if (amPmRatio <= 0) { 388 pattern = pattern.replace("a".toRegex(), "").trim { it <= ' ' } 389 } 390 391 // Replace spaces with "Hair Space" 392 pattern = pattern.replace(" ".toRegex(), "\u200A") 393 // Build a spannable so that the am/pm will be formatted 394 val amPmPos = pattern.indexOf('a') 395 if (amPmPos == -1) { 396 return pattern 397 } 398 399 val sp: Spannable = SpannableString(pattern) 400 sp.setSpan(RelativeSizeSpan(amPmRatio), amPmPos, amPmPos + 1, 401 Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) 402 sp.setSpan(StyleSpan(Typeface.NORMAL), amPmPos, amPmPos + 1, 403 Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) 404 sp.setSpan(TypefaceSpan("sans-serif"), amPmPos, amPmPos + 1, 405 Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) 406 407 return sp 408 } 409 get24ModeFormatnull410 fun get24ModeFormat(includeSeconds: Boolean): CharSequence { 411 return DateFormat.getBestDateTimePattern(Locale.getDefault(), 412 if (includeSeconds) "Hms" else "Hm") 413 } 414 415 /** 416 * Returns string denoting the timezone hour offset (e.g. GMT -8:00) 417 * 418 * @param useShortForm Whether to return a short form of the header that rounds to the 419 * nearest hour and excludes the "GMT" prefix 420 */ getGMTHourOffsetnull421 fun getGMTHourOffset(timezone: TimeZone, useShortForm: Boolean): String { 422 val gmtOffset = timezone.rawOffset 423 val hour = gmtOffset / DateUtils.HOUR_IN_MILLIS 424 val min = abs(gmtOffset) % DateUtils.HOUR_IN_MILLIS / DateUtils.MINUTE_IN_MILLIS 425 426 return if (useShortForm) { 427 String.format(Locale.ENGLISH, "%+d", hour) 428 } else { 429 String.format(Locale.ENGLISH, "GMT %+d:%02d", hour, min) 430 } 431 } 432 433 /** 434 * Given a point in time, return the subsequent moment any of the time zones changes days. 435 * e.g. Given 8:00pm on 1/1/2016 and time zones in LA and NY this method would return a Date for 436 * midnight on 1/2/2016 in the NY timezone since it changes days first. 437 * 438 * @param time a point in time from which to compute midnight on the subsequent day 439 * @param zones a collection of time zones 440 * @return the nearest point in the future at which any of the time zones changes days 441 */ getNextDaynull442 fun getNextDay(time: Date, zones: Collection<TimeZone>): Date { 443 var next: Calendar? = null 444 for (tz in zones) { 445 val c = Calendar.getInstance(tz) 446 c.time = time 447 448 // Advance to the next day. 449 c.add(Calendar.DAY_OF_YEAR, 1) 450 451 // Reset the time to midnight. 452 c[Calendar.HOUR_OF_DAY] = 0 453 c[Calendar.MINUTE] = 0 454 c[Calendar.SECOND] = 0 455 c[Calendar.MILLISECOND] = 0 456 457 if (next == null || c < next) { 458 next = c 459 } 460 } 461 462 return next!!.time 463 } 464 getNumberFormattedQuantityStringnull465 fun getNumberFormattedQuantityString(context: Context, id: Int, quantity: Int): String { 466 val localizedQuantity = NumberFormat.getInstance().format(quantity.toLong()) 467 return context.resources.getQuantityString(id, quantity, localizedQuantity) 468 } 469 470 /** 471 * @return `true` iff the widget is being hosted in a container where tapping is allowed 472 */ isWidgetClickablenull473 fun isWidgetClickable(widgetManager: AppWidgetManager, widgetId: Int): Boolean { 474 val wo = widgetManager.getAppWidgetOptions(widgetId) 475 return (wo != null && 476 wo.getInt(AppWidgetManager.OPTION_APPWIDGET_HOST_CATEGORY, -1) 477 != AppWidgetProviderInfo.WIDGET_CATEGORY_KEYGUARD) 478 } 479 480 /** 481 * @return a vector-drawable inflated from the given `resId` 482 */ getVectorDrawablenull483 fun getVectorDrawable(context: Context, @DrawableRes resId: Int): VectorDrawableCompat? { 484 return VectorDrawableCompat.create(context.resources, resId, context.theme) 485 } 486 487 /** 488 * This method assumes the given `view` has already been layed out. 489 * 490 * @return a Bitmap containing an image of the `view` at its current size 491 */ createBitmapnull492 fun createBitmap(view: View): Bitmap { 493 val bitmap = Bitmap.createBitmap(view.width, view.height, Bitmap.Config.ARGB_8888) 494 val canvas = Canvas(bitmap) 495 view.draw(canvas) 496 return bitmap 497 } 498 499 /** 500 * [ArraySet] is @hide prior to [Build.VERSION_CODES.M]. 501 */ 502 @SuppressLint("NewApi") newArraySetnull503 fun <E> newArraySet(collection: Collection<E>): ArraySet<E> { 504 val arraySet = ArraySet<E>(collection.size) 505 arraySet.addAll(collection) 506 return arraySet 507 } 508 509 /** 510 * @param context from which to query the current device configuration 511 * @return `true` if the device is currently in portrait or reverse portrait orientation 512 */ isPortraitnull513 fun isPortrait(context: Context): Boolean { 514 return context.resources.configuration.orientation == Configuration.ORIENTATION_PORTRAIT 515 } 516 517 /** 518 * @param context from which to query the current device configuration 519 * @return `true` if the device is currently in landscape or reverse landscape orientation 520 */ isLandscapenull521 fun isLandscape(context: Context): Boolean { 522 return context.resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE 523 } 524 nownull525 fun now(): Long = DataModel.dataModel.elapsedRealtime() 526 527 fun wallClock(): Long = DataModel.dataModel.currentTimeMillis() 528 529 /** 530 * @param context to obtain strings. 531 * @param displayMinutes whether or not minutes should be included 532 * @param isAhead `true` if the time should be marked 'ahead', else 'behind' 533 * @param hoursDifferent the number of hours the time is ahead/behind 534 * @param minutesDifferent the number of minutes the time is ahead/behind 535 * @return String describing the hours/minutes ahead or behind 536 */ 537 fun createHoursDifferentString( 538 context: Context, 539 displayMinutes: Boolean, 540 isAhead: Boolean, 541 hoursDifferent: Int, 542 minutesDifferent: Int 543 ): String { 544 val timeString: String 545 timeString = if (displayMinutes && hoursDifferent != 0) { 546 // Both minutes and hours 547 val hoursShortQuantityString = getNumberFormattedQuantityString(context, 548 R.plurals.hours_short, abs(hoursDifferent)) 549 val minsShortQuantityString = getNumberFormattedQuantityString(context, 550 R.plurals.minutes_short, abs(minutesDifferent)) 551 @StringRes val stringType = if (isAhead) { 552 R.string.world_hours_minutes_ahead 553 } else { 554 R.string.world_hours_minutes_behind 555 } 556 context.getString(stringType, hoursShortQuantityString, 557 minsShortQuantityString) 558 } else { 559 // Minutes alone or hours alone 560 val hoursQuantityString = getNumberFormattedQuantityString( 561 context, R.plurals.hours, abs(hoursDifferent)) 562 val minutesQuantityString = getNumberFormattedQuantityString( 563 context, R.plurals.minutes, abs(minutesDifferent)) 564 @StringRes val stringType = if (isAhead) { 565 R.string.world_time_ahead 566 } else { 567 R.string.world_time_behind 568 } 569 context.getString(stringType, if (displayMinutes) { 570 minutesQuantityString 571 } else { 572 hoursQuantityString 573 }) 574 } 575 return timeString 576 } 577 578 /** 579 * @param context The context from which to obtain strings 580 * @param hours Hours to display (if any) 581 * @param minutes Minutes to display (if any) 582 * @param seconds Seconds to display 583 * @return Provided time formatted as a String 584 */ getTimeStringnull585 fun getTimeString(context: Context, hours: Int, minutes: Int, seconds: Int): String { 586 if (hours != 0) { 587 return context.getString(R.string.hours_minutes_seconds, hours, minutes, seconds) 588 } 589 return if (minutes != 0) { 590 context.getString(R.string.minutes_seconds, minutes, seconds) 591 } else { 592 context.getString(R.string.seconds, seconds) 593 } 594 } 595 596 class ClickAccessibilityDelegate @JvmOverloads constructor( 597 /** The label for talkback to apply to the view */ 598 private val mLabel: String, 599 /** Whether or not to always make the view visible to talkback */ 600 private val mIsAlwaysAccessibilityVisible: Boolean = false 601 ) : AccessibilityDelegateCompat() { 602 onInitializeAccessibilityNodeInfonull603 override fun onInitializeAccessibilityNodeInfo( 604 host: View, 605 info: AccessibilityNodeInfoCompat 606 ) { 607 super.onInitializeAccessibilityNodeInfo(host, info) 608 if (mIsAlwaysAccessibilityVisible) { 609 info.setVisibleToUser(true) 610 } 611 info.addAction(AccessibilityActionCompat( 612 AccessibilityActionCompat.ACTION_CLICK.getId(), mLabel)) 613 } 614 } 615 }