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