Home | History | Annotate | Download | only in deskclock
      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 android.support.annotation.AnyRes;
     41 import android.support.annotation.DrawableRes;
     42 import android.support.annotation.StringRes;
     43 import android.support.graphics.drawable.VectorDrawableCompat;
     44 import android.support.v4.os.BuildCompat;
     45 import android.support.v4.view.AccessibilityDelegateCompat;
     46 import android.support.v4.view.accessibility.AccessibilityNodeInfoCompat;
     47 import android.support.v4.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 
     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 }