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