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