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 * @return {@code true} if the device is {@link Build.VERSION_CODES#P} or later 159 */ 160 val isPOrLater: Boolean 161 get() = Build.VERSION.SDK_INT >= Build.VERSION_CODES.P 162 163 /** 164 * @param resourceId identifies an application resource 165 * @return the Uri by which the application resource is accessed 166 */ getResourceUrinull167 fun getResourceUri(context: Context, @AnyRes resourceId: Int): Uri { 168 return Uri.Builder() 169 .scheme(ContentResolver.SCHEME_ANDROID_RESOURCE) 170 .authority(context.packageName) 171 .path(resourceId.toString()) 172 .build() 173 } 174 175 /** 176 * @param view the scrollable view to test 177 * @return `true` iff the `view` content is currently scrolled to the top 178 */ isScrolledToTopnull179 fun isScrolledToTop(view: View): Boolean { 180 return !view.canScrollVertically(-1) 181 } 182 183 /** 184 * Calculate the amount by which the radius of a CircleTimerView should be offset by any 185 * of the extra painted objects. 186 */ calculateRadiusOffsetnull187 fun calculateRadiusOffset( 188 strokeSize: Float, 189 dotStrokeSize: Float, 190 markerStrokeSize: Float 191 ): Float { 192 return max(strokeSize, max(dotStrokeSize, markerStrokeSize)) 193 } 194 195 /** 196 * Configure the clock that is visible to display seconds. The clock that is not visible never 197 * displays seconds to avoid it scheduling unnecessary ticking runnables. 198 */ setClockSecondsEnablednull199 fun setClockSecondsEnabled(digitalClock: TextClock, analogClock: AnalogClock) { 200 val displaySeconds: Boolean = DataModel.dataModel.displayClockSeconds 201 when (DataModel.dataModel.clockStyle) { 202 DataModel.ClockStyle.ANALOG -> { 203 setTimeFormat(digitalClock, false) 204 analogClock.enableSeconds(displaySeconds) 205 } 206 DataModel.ClockStyle.DIGITAL -> { 207 analogClock.enableSeconds(false) 208 setTimeFormat(digitalClock, displaySeconds) 209 } 210 } 211 } 212 213 /** 214 * Set whether the digital or analog clock should be displayed in the application. 215 * Returns the view to be displayed. 216 */ setClockStylenull217 fun setClockStyle(digitalClock: View, analogClock: View): View { 218 return when (DataModel.dataModel.clockStyle) { 219 DataModel.ClockStyle.ANALOG -> { 220 digitalClock.visibility = View.GONE 221 analogClock.visibility = View.VISIBLE 222 analogClock 223 } 224 DataModel.ClockStyle.DIGITAL -> { 225 digitalClock.visibility = View.VISIBLE 226 analogClock.visibility = View.GONE 227 digitalClock 228 } 229 } 230 } 231 232 /** 233 * For screensavers to set whether the digital or analog clock should be displayed. 234 * Returns the view to be displayed. 235 */ setScreensaverClockStylenull236 fun setScreensaverClockStyle(digitalClock: View, analogClock: View): View { 237 return when (DataModel.dataModel.screensaverClockStyle) { 238 DataModel.ClockStyle.ANALOG -> { 239 digitalClock.visibility = View.GONE 240 analogClock.visibility = View.VISIBLE 241 analogClock 242 } 243 DataModel.ClockStyle.DIGITAL -> { 244 digitalClock.visibility = View.VISIBLE 245 analogClock.visibility = View.GONE 246 digitalClock 247 } 248 } 249 } 250 251 /** 252 * For screensavers to dim the lights if necessary. 253 */ dimClockViewnull254 fun dimClockView(dim: Boolean, clockView: View) { 255 val paint = Paint() 256 paint.color = Color.WHITE 257 paint.colorFilter = PorterDuffColorFilter( 258 if (dim) 0x40FFFFFF else -0x3f000001, 259 PorterDuff.Mode.MULTIPLY) 260 clockView.setLayerType(View.LAYER_TYPE_HARDWARE, paint) 261 } 262 263 /** 264 * Update and return the PendingIntent corresponding to the given `intent`. 265 * 266 * @param context the Context in which the PendingIntent should start the service 267 * @param intent an Intent describing the service to be started 268 * @return a PendingIntent that will start a service 269 */ pendingServiceIntentnull270 fun pendingServiceIntent(context: Context, intent: Intent): PendingIntent { 271 return PendingIntent.getService(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT) 272 } 273 274 /** 275 * Update and return the PendingIntent corresponding to the given `intent`. 276 * 277 * @param context the Context in which the PendingIntent should start the activity 278 * @param intent an Intent describing the activity to be started 279 * @return a PendingIntent that will start an activity 280 */ pendingActivityIntentnull281 fun pendingActivityIntent(context: Context, intent: Intent): PendingIntent { 282 return PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT) 283 } 284 285 /** 286 * @return The next alarm from [AlarmManager] 287 */ getNextAlarmnull288 fun getNextAlarm(context: Context): String? { 289 return if (isPreL) getNextAlarmPreL(context) else getNextAlarmLOrLater(context) 290 } 291 292 @TargetApi(Build.VERSION_CODES.KITKAT) getNextAlarmPreLnull293 private fun getNextAlarmPreL(context: Context): String { 294 val cr = context.contentResolver 295 return Settings.System.getString(cr, Settings.System.NEXT_ALARM_FORMATTED) 296 } 297 298 @TargetApi(Build.VERSION_CODES.LOLLIPOP) getNextAlarmLOrLaternull299 private fun getNextAlarmLOrLater(context: Context): String? { 300 val am = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager 301 val info = getNextAlarmClock(am) 302 if (info != null) { 303 val triggerTime = info.triggerTime 304 val alarmTime = Calendar.getInstance() 305 alarmTime.timeInMillis = triggerTime 306 return AlarmUtils.getFormattedTime(context, alarmTime) 307 } 308 309 return null 310 } 311 312 @TargetApi(Build.VERSION_CODES.LOLLIPOP) getNextAlarmClocknull313 private fun getNextAlarmClock(am: AlarmManager): AlarmClockInfo? { 314 return am.nextAlarmClock 315 } 316 317 @TargetApi(Build.VERSION_CODES.LOLLIPOP) updateNextAlarmnull318 fun updateNextAlarm(am: AlarmManager, info: AlarmClockInfo, op: PendingIntent) { 319 am.setAlarmClock(info, op) 320 } 321 isAlarmWithin24Hoursnull322 fun isAlarmWithin24Hours(alarmInstance: AlarmInstance): Boolean { 323 val nextAlarmTime: Calendar = alarmInstance.alarmTime 324 val nextAlarmTimeMillis = nextAlarmTime.timeInMillis 325 return nextAlarmTimeMillis - System.currentTimeMillis() <= DateUtils.DAY_IN_MILLIS 326 } 327 328 /** 329 * Clock views can call this to refresh their alarm to the next upcoming value. 330 */ refreshAlarmnull331 fun refreshAlarm(context: Context, clock: View?) { 332 val nextAlarmIconView = clock?.findViewById<View>(R.id.nextAlarmIcon) as TextView 333 val nextAlarmView = clock.findViewById<View>(R.id.nextAlarm) as TextView? ?: return 334 335 val alarm = getNextAlarm(context) 336 if (!TextUtils.isEmpty(alarm)) { 337 val description = context.getString(R.string.next_alarm_description, alarm) 338 nextAlarmView.text = alarm 339 nextAlarmView.contentDescription = description 340 nextAlarmView.visibility = View.VISIBLE 341 nextAlarmIconView.visibility = View.VISIBLE 342 nextAlarmIconView.contentDescription = description 343 } else { 344 nextAlarmView.visibility = View.GONE 345 nextAlarmIconView.visibility = View.GONE 346 } 347 } 348 setClockIconTypefacenull349 fun setClockIconTypeface(clock: View?) { 350 val nextAlarmIconView = clock?.findViewById<View>(R.id.nextAlarmIcon) as TextView? 351 nextAlarmIconView?.typeface = UiDataModel.uiDataModel.alarmIconTypeface 352 } 353 354 /** 355 * Clock views can call this to refresh their date. 356 */ updateDatenull357 fun updateDate(dateSkeleton: String?, descriptionSkeleton: String?, clock: View?) { 358 val dateDisplay = clock?.findViewById<View>(R.id.date) as TextView? ?: return 359 360 val l = Locale.getDefault() 361 val datePattern = DateFormat.getBestDateTimePattern(l, dateSkeleton) 362 val descriptionPattern = DateFormat.getBestDateTimePattern(l, descriptionSkeleton) 363 364 val now = Date() 365 dateDisplay.text = SimpleDateFormat(datePattern, l).format(now) 366 dateDisplay.visibility = View.VISIBLE 367 dateDisplay.contentDescription = SimpleDateFormat(descriptionPattern, l).format(now) 368 } 369 370 /*** 371 * Formats the time in the TextClock according to the Locale with a special 372 * formatting treatment for the am/pm label. 373 * 374 * @param clock TextClock to format 375 * @param includeSeconds whether or not to include seconds in the clock's time 376 */ setTimeFormatnull377 fun setTimeFormat(clock: TextClock?, includeSeconds: Boolean) { 378 // Get the best format for 12 hours mode according to the locale 379 clock?.format12Hour = get12ModeFormat(amPmRatio = 0.4f, includeSeconds = includeSeconds) 380 // Get the best format for 24 hours mode according to the locale 381 clock?.format24Hour = get24ModeFormat(includeSeconds) 382 } 383 384 /** 385 * @param amPmRatio a value between 0 and 1 that is the ratio of the relative size of the 386 * am/pm string to the time string 387 * @param includeSeconds whether or not to include seconds in the time string 388 * @return format string for 12 hours mode time, not including seconds 389 */ get12ModeFormatnull390 fun get12ModeFormat(amPmRatio: Float, includeSeconds: Boolean): CharSequence { 391 var pattern = DateFormat.getBestDateTimePattern(Locale.getDefault(), 392 if (includeSeconds) "hmsa" else "hma") 393 if (amPmRatio <= 0) { 394 pattern = pattern.replace("a".toRegex(), "").trim { it <= ' ' } 395 } 396 397 // Replace spaces with "Hair Space" 398 pattern = pattern.replace(" ".toRegex(), "\u200A") 399 // Build a spannable so that the am/pm will be formatted 400 val amPmPos = pattern.indexOf('a') 401 if (amPmPos == -1) { 402 return pattern 403 } 404 405 val sp: Spannable = SpannableString(pattern) 406 sp.setSpan(RelativeSizeSpan(amPmRatio), amPmPos, amPmPos + 1, 407 Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) 408 sp.setSpan(StyleSpan(Typeface.NORMAL), amPmPos, amPmPos + 1, 409 Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) 410 sp.setSpan(TypefaceSpan("sans-serif"), amPmPos, amPmPos + 1, 411 Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) 412 413 return sp 414 } 415 get24ModeFormatnull416 fun get24ModeFormat(includeSeconds: Boolean): CharSequence { 417 return DateFormat.getBestDateTimePattern(Locale.getDefault(), 418 if (includeSeconds) "Hms" else "Hm") 419 } 420 421 /** 422 * Returns string denoting the timezone hour offset (e.g. GMT -8:00) 423 * 424 * @param useShortForm Whether to return a short form of the header that rounds to the 425 * nearest hour and excludes the "GMT" prefix 426 */ getGMTHourOffsetnull427 fun getGMTHourOffset(timezone: TimeZone, useShortForm: Boolean): String { 428 val gmtOffset = timezone.rawOffset 429 val hour = gmtOffset / DateUtils.HOUR_IN_MILLIS 430 val min = abs(gmtOffset) % DateUtils.HOUR_IN_MILLIS / DateUtils.MINUTE_IN_MILLIS 431 432 return if (useShortForm) { 433 String.format(Locale.ENGLISH, "%+d", hour) 434 } else { 435 String.format(Locale.ENGLISH, "GMT %+d:%02d", hour, min) 436 } 437 } 438 439 /** 440 * Given a point in time, return the subsequent moment any of the time zones changes days. 441 * e.g. Given 8:00pm on 1/1/2016 and time zones in LA and NY this method would return a Date for 442 * midnight on 1/2/2016 in the NY timezone since it changes days first. 443 * 444 * @param time a point in time from which to compute midnight on the subsequent day 445 * @param zones a collection of time zones 446 * @return the nearest point in the future at which any of the time zones changes days 447 */ getNextDaynull448 fun getNextDay(time: Date, zones: Collection<TimeZone>): Date { 449 var next: Calendar? = null 450 for (tz in zones) { 451 val c = Calendar.getInstance(tz) 452 c.time = time 453 454 // Advance to the next day. 455 c.add(Calendar.DAY_OF_YEAR, 1) 456 457 // Reset the time to midnight. 458 c[Calendar.HOUR_OF_DAY] = 0 459 c[Calendar.MINUTE] = 0 460 c[Calendar.SECOND] = 0 461 c[Calendar.MILLISECOND] = 0 462 463 if (next == null || c < next) { 464 next = c 465 } 466 } 467 468 return next!!.time 469 } 470 getNumberFormattedQuantityStringnull471 fun getNumberFormattedQuantityString(context: Context, id: Int, quantity: Int): String { 472 val localizedQuantity = NumberFormat.getInstance().format(quantity.toLong()) 473 return context.resources.getQuantityString(id, quantity, localizedQuantity) 474 } 475 476 /** 477 * @return `true` iff the widget is being hosted in a container where tapping is allowed 478 */ isWidgetClickablenull479 fun isWidgetClickable(widgetManager: AppWidgetManager, widgetId: Int): Boolean { 480 val wo = widgetManager.getAppWidgetOptions(widgetId) 481 return (wo != null && 482 wo.getInt(AppWidgetManager.OPTION_APPWIDGET_HOST_CATEGORY, -1) 483 != AppWidgetProviderInfo.WIDGET_CATEGORY_KEYGUARD) 484 } 485 486 /** 487 * @return a vector-drawable inflated from the given `resId` 488 */ getVectorDrawablenull489 fun getVectorDrawable(context: Context, @DrawableRes resId: Int): VectorDrawableCompat? { 490 return VectorDrawableCompat.create(context.resources, resId, context.theme) 491 } 492 493 /** 494 * This method assumes the given `view` has already been layed out. 495 * 496 * @return a Bitmap containing an image of the `view` at its current size 497 */ createBitmapnull498 fun createBitmap(view: View): Bitmap { 499 val bitmap = Bitmap.createBitmap(view.width, view.height, Bitmap.Config.ARGB_8888) 500 val canvas = Canvas(bitmap) 501 view.draw(canvas) 502 return bitmap 503 } 504 505 /** 506 * [ArraySet] is @hide prior to [Build.VERSION_CODES.M]. 507 */ 508 @SuppressLint("NewApi") newArraySetnull509 fun <E> newArraySet(collection: Collection<E>): ArraySet<E> { 510 val arraySet = ArraySet<E>(collection.size) 511 arraySet.addAll(collection) 512 return arraySet 513 } 514 515 /** 516 * @param context from which to query the current device configuration 517 * @return `true` if the device is currently in portrait or reverse portrait orientation 518 */ isPortraitnull519 fun isPortrait(context: Context): Boolean { 520 return context.resources.configuration.orientation == Configuration.ORIENTATION_PORTRAIT 521 } 522 523 /** 524 * @param context from which to query the current device configuration 525 * @return `true` if the device is currently in landscape or reverse landscape orientation 526 */ isLandscapenull527 fun isLandscape(context: Context): Boolean { 528 return context.resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE 529 } 530 nownull531 fun now(): Long = DataModel.dataModel.elapsedRealtime() 532 533 fun wallClock(): Long = DataModel.dataModel.currentTimeMillis() 534 535 /** 536 * @param context to obtain strings. 537 * @param displayMinutes whether or not minutes should be included 538 * @param isAhead `true` if the time should be marked 'ahead', else 'behind' 539 * @param hoursDifferent the number of hours the time is ahead/behind 540 * @param minutesDifferent the number of minutes the time is ahead/behind 541 * @return String describing the hours/minutes ahead or behind 542 */ 543 fun createHoursDifferentString( 544 context: Context, 545 displayMinutes: Boolean, 546 isAhead: Boolean, 547 hoursDifferent: Int, 548 minutesDifferent: Int 549 ): String { 550 val timeString: String 551 timeString = if (displayMinutes && hoursDifferent != 0) { 552 // Both minutes and hours 553 val hoursShortQuantityString = getNumberFormattedQuantityString(context, 554 R.plurals.hours_short, abs(hoursDifferent)) 555 val minsShortQuantityString = getNumberFormattedQuantityString(context, 556 R.plurals.minutes_short, abs(minutesDifferent)) 557 @StringRes val stringType = if (isAhead) { 558 R.string.world_hours_minutes_ahead 559 } else { 560 R.string.world_hours_minutes_behind 561 } 562 context.getString(stringType, hoursShortQuantityString, 563 minsShortQuantityString) 564 } else { 565 // Minutes alone or hours alone 566 val hoursQuantityString = getNumberFormattedQuantityString( 567 context, R.plurals.hours, abs(hoursDifferent)) 568 val minutesQuantityString = getNumberFormattedQuantityString( 569 context, R.plurals.minutes, abs(minutesDifferent)) 570 @StringRes val stringType = if (isAhead) { 571 R.string.world_time_ahead 572 } else { 573 R.string.world_time_behind 574 } 575 context.getString(stringType, if (displayMinutes) { 576 minutesQuantityString 577 } else { 578 hoursQuantityString 579 }) 580 } 581 return timeString 582 } 583 584 /** 585 * @param context The context from which to obtain strings 586 * @param hours Hours to display (if any) 587 * @param minutes Minutes to display (if any) 588 * @param seconds Seconds to display 589 * @return Provided time formatted as a String 590 */ getTimeStringnull591 fun getTimeString(context: Context, hours: Int, minutes: Int, seconds: Int): String { 592 if (hours != 0) { 593 return context.getString(R.string.hours_minutes_seconds, hours, minutes, seconds) 594 } 595 return if (minutes != 0) { 596 context.getString(R.string.minutes_seconds, minutes, seconds) 597 } else { 598 context.getString(R.string.seconds, seconds) 599 } 600 } 601 602 class ClickAccessibilityDelegate @JvmOverloads constructor( 603 /** The label for talkback to apply to the view */ 604 private val mLabel: String, 605 /** Whether or not to always make the view visible to talkback */ 606 private val mIsAlwaysAccessibilityVisible: Boolean = false 607 ) : AccessibilityDelegateCompat() { 608 onInitializeAccessibilityNodeInfonull609 override fun onInitializeAccessibilityNodeInfo( 610 host: View, 611 info: AccessibilityNodeInfoCompat 612 ) { 613 super.onInitializeAccessibilityNodeInfo(host, info) 614 if (mIsAlwaysAccessibilityVisible) { 615 info.setVisibleToUser(true) 616 } 617 info.addAction(AccessibilityActionCompat( 618 AccessibilityActionCompat.ACTION_CLICK.getId(), mLabel)) 619 } 620 } 621 } 622