Home | History | Annotate | Download | only in deskclock
      1 /*
      2  * Copyright (C) 2012 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.animation.Animator;
     20 import android.animation.AnimatorSet;
     21 import android.animation.ObjectAnimator;
     22 import android.animation.TimeInterpolator;
     23 import android.annotation.TargetApi;
     24 import android.app.AlarmManager;
     25 import android.content.ContentResolver;
     26 import android.content.Context;
     27 import android.content.SharedPreferences;
     28 import android.content.res.Resources;
     29 import android.content.res.TypedArray;
     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.os.Build;
     36 import android.os.Handler;
     37 import android.os.Looper;
     38 import android.preference.PreferenceManager;
     39 import android.provider.Settings;
     40 import android.support.v4.content.ContextCompat;
     41 import android.support.v4.os.BuildCompat;
     42 import android.text.Spannable;
     43 import android.text.SpannableString;
     44 import android.text.TextUtils;
     45 import android.text.format.DateFormat;
     46 import android.text.format.DateUtils;
     47 import android.text.format.Time;
     48 import android.text.style.RelativeSizeSpan;
     49 import android.text.style.StyleSpan;
     50 import android.text.style.TypefaceSpan;
     51 import android.util.ArraySet;
     52 import android.view.View;
     53 import android.view.animation.AccelerateInterpolator;
     54 import android.view.animation.DecelerateInterpolator;
     55 import android.widget.TextClock;
     56 import android.widget.TextView;
     57 
     58 import com.android.deskclock.data.DataModel;
     59 import com.android.deskclock.provider.AlarmInstance;
     60 import com.android.deskclock.provider.DaysOfWeek;
     61 import com.android.deskclock.settings.SettingsActivity;
     62 
     63 import java.io.File;
     64 import java.text.DateFormatSymbols;
     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.GregorianCalendar;
     71 import java.util.Locale;
     72 import java.util.TimeZone;
     73 
     74 public class Utils {
     75     // Single-char version of day name, e.g.: 'S', 'M', 'T', 'W', 'T', 'F', 'S'
     76     private static String[] sShortWeekdays = null;
     77     private static final String DATE_FORMAT_SHORT = "ccccc";
     78 
     79     // Long-version of day name, e.g.: 'Sunday', 'Monday', 'Tuesday', etc
     80     private static String[] sLongWeekdays = null;
     81     private static final String DATE_FORMAT_LONG = "EEEE";
     82 
     83     public static final int DEFAULT_WEEK_START = Calendar.getInstance().getFirstDayOfWeek();
     84 
     85     private static Locale sLocaleUsedForWeekdays;
     86 
     87     /**
     88      * Temporary array used by {@link #obtainStyledColor(Context, int, int)}.
     89      */
     90     private static final int[] TEMP_ARRAY = new int[1];
     91 
     92     /**
     93      * The background colors of the app - it changes throughout out the day to mimic the sky.
     94      */
     95     private static final int[] BACKGROUND_SPECTRUM = {
     96             0xFF212121 /* 12 AM */,
     97             0xFF20222A /*  1 AM */,
     98             0xFF202233 /*  2 AM */,
     99             0xFF1F2242 /*  3 AM */,
    100             0xFF1E224F /*  4 AM */,
    101             0xFF1D225C /*  5 AM */,
    102             0xFF1B236B /*  6 AM */,
    103             0xFF1A237E /*  7 AM */,
    104             0xFF1D2783 /*  8 AM */,
    105             0xFF232E8B /*  9 AM */,
    106             0xFF283593 /* 10 AM */,
    107             0xFF2C3998 /* 11 AM */,
    108             0xFF303F9F /* 12 PM */,
    109             0xFF2C3998 /*  1 PM */,
    110             0xFF283593 /*  2 PM */,
    111             0xFF232E8B /*  3 PM */,
    112             0xFF1D2783 /*  4 PM */,
    113             0xFF1A237E /*  5 PM */,
    114             0xFF1B236B /*  6 PM */,
    115             0xFF1D225C /*  7 PM */,
    116             0xFF1E224F /*  8 PM */,
    117             0xFF1F2242 /*  9 PM */,
    118             0xFF202233 /* 10 PM */,
    119             0xFF20222A /* 11 PM */
    120     };
    121 
    122     public static void enforceMainLooper() {
    123         if (Looper.getMainLooper() != Looper.myLooper()) {
    124             throw new IllegalAccessError("May only call from main thread.");
    125         }
    126     }
    127 
    128     public static void enforceNotMainLooper() {
    129         if (Looper.getMainLooper() == Looper.myLooper()) {
    130             throw new IllegalAccessError("May not call from main thread.");
    131         }
    132     }
    133 
    134     /**
    135      * @return {@code true} if the device is prior to {@link Build.VERSION_CODES#LOLLIPOP}
    136      */
    137     public static boolean isPreL() {
    138         return Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP;
    139     }
    140 
    141     /**
    142      * @return {@code true} if the device is {@link Build.VERSION_CODES#LOLLIPOP} or
    143      *      {@link Build.VERSION_CODES#LOLLIPOP_MR1}
    144      */
    145     public static boolean isLOrLMR1() {
    146         final int sdkInt = Build.VERSION.SDK_INT;
    147         return sdkInt == Build.VERSION_CODES.LOLLIPOP || sdkInt == Build.VERSION_CODES.LOLLIPOP_MR1;
    148     }
    149 
    150     /**
    151      * @return {@code true} if the device is {@link Build.VERSION_CODES#LOLLIPOP} or later
    152      */
    153     public static boolean isLOrLater() {
    154         return Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP;
    155     }
    156 
    157     /**
    158      * @return {@code true} if the device is {@link Build.VERSION_CODES#LOLLIPOP_MR1} or later
    159      */
    160     public static boolean isLMR1OrLater() {
    161         return Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP_MR1;
    162     }
    163 
    164     /**
    165      * @return {@code true} if the device is {@link Build.VERSION_CODES#M} or later
    166      */
    167     public static boolean isMOrLater() {
    168         return Build.VERSION.SDK_INT >= Build.VERSION_CODES.M;
    169     }
    170 
    171     /**
    172      * @return {@code true} if the device is {@link Build.VERSION_CODES#N} or later
    173      */
    174     public static boolean isNOrLater() {
    175         return BuildCompat.isAtLeastN();
    176     }
    177 
    178     /**
    179      * Calculate the amount by which the radius of a CircleTimerView should be offset by any
    180      * of the extra painted objects.
    181      */
    182     public static float calculateRadiusOffset(
    183             float strokeSize, float dotStrokeSize, float markerStrokeSize) {
    184         return Math.max(strokeSize, Math.max(dotStrokeSize, markerStrokeSize));
    185     }
    186 
    187     /**
    188      * Uses {@link Utils#calculateRadiusOffset(float, float, float)} after fetching the values
    189      * from the resources.
    190      */
    191     public static float calculateRadiusOffset(Resources resources) {
    192         if (resources != null) {
    193             float strokeSize = resources.getDimension(R.dimen.circletimer_circle_size);
    194             float dotStrokeSize = resources.getDimension(R.dimen.circletimer_dot_size);
    195             float markerStrokeSize = resources.getDimension(R.dimen.circletimer_marker_size);
    196             return calculateRadiusOffset(strokeSize, dotStrokeSize, markerStrokeSize);
    197         } else {
    198             return 0f;
    199         }
    200     }
    201 
    202     /** Runnable for use with screensaver and dream, to move the clock every minute.
    203      *  registerViews() must be called prior to posting.
    204      */
    205     public static class ScreensaverMoveSaverRunnable implements Runnable {
    206         static final long MOVE_DELAY = 60000; // DeskClock.SCREEN_SAVER_MOVE_DELAY;
    207         static final long SLIDE_TIME = 10000;
    208         static final long FADE_TIME = 3000;
    209 
    210         static final boolean SLIDE = false;
    211 
    212         private View mContentView, mSaverView;
    213         private final Handler mHandler;
    214 
    215         private static TimeInterpolator mSlowStartWithBrakes;
    216 
    217 
    218         public ScreensaverMoveSaverRunnable(Handler handler) {
    219             mHandler = handler;
    220             mSlowStartWithBrakes = new TimeInterpolator() {
    221                 @Override
    222                 public float getInterpolation(float x) {
    223                     return (float)(Math.cos((Math.pow(x,3) + 1) * Math.PI) / 2.0f) + 0.5f;
    224                 }
    225             };
    226         }
    227 
    228         public void registerViews(View contentView, View saverView) {
    229             mContentView = contentView;
    230             mSaverView = saverView;
    231         }
    232 
    233         @Override
    234         public void run() {
    235             long delay = MOVE_DELAY;
    236             if (mContentView == null || mSaverView == null) {
    237                 mHandler.removeCallbacks(this);
    238                 mHandler.postDelayed(this, delay);
    239                 return;
    240             }
    241 
    242             final float xrange = mContentView.getWidth() - mSaverView.getWidth();
    243             final float yrange = mContentView.getHeight() - mSaverView.getHeight();
    244 
    245             if (xrange == 0 && yrange == 0) {
    246                 delay = 500; // back in a split second
    247             } else {
    248                 final int nextx = (int) (Math.random() * xrange);
    249                 final int nexty = (int) (Math.random() * yrange);
    250 
    251                 if (mSaverView.getAlpha() == 0f) {
    252                     // jump right there
    253                     mSaverView.setX(nextx);
    254                     mSaverView.setY(nexty);
    255                     ObjectAnimator.ofFloat(mSaverView, "alpha", 0f, 1f)
    256                         .setDuration(FADE_TIME)
    257                         .start();
    258                 } else {
    259                     AnimatorSet s = new AnimatorSet();
    260                     Animator xMove   = ObjectAnimator.ofFloat(mSaverView,
    261                                          "x", mSaverView.getX(), nextx);
    262                     Animator yMove   = ObjectAnimator.ofFloat(mSaverView,
    263                                          "y", mSaverView.getY(), nexty);
    264 
    265                     Animator xShrink = ObjectAnimator.ofFloat(mSaverView, "scaleX", 1f, 0.85f);
    266                     Animator xGrow   = ObjectAnimator.ofFloat(mSaverView, "scaleX", 0.85f, 1f);
    267 
    268                     Animator yShrink = ObjectAnimator.ofFloat(mSaverView, "scaleY", 1f, 0.85f);
    269                     Animator yGrow   = ObjectAnimator.ofFloat(mSaverView, "scaleY", 0.85f, 1f);
    270                     AnimatorSet shrink = new AnimatorSet(); shrink.play(xShrink).with(yShrink);
    271                     AnimatorSet grow = new AnimatorSet(); grow.play(xGrow).with(yGrow);
    272 
    273                     Animator fadeout = ObjectAnimator.ofFloat(mSaverView, "alpha", 1f, 0f);
    274                     Animator fadein = ObjectAnimator.ofFloat(mSaverView, "alpha", 0f, 1f);
    275 
    276 
    277                     if (SLIDE) {
    278                         s.play(xMove).with(yMove);
    279                         s.setDuration(SLIDE_TIME);
    280 
    281                         s.play(shrink.setDuration(SLIDE_TIME/2));
    282                         s.play(grow.setDuration(SLIDE_TIME/2)).after(shrink);
    283                         s.setInterpolator(mSlowStartWithBrakes);
    284                     } else {
    285                         AccelerateInterpolator accel = new AccelerateInterpolator();
    286                         DecelerateInterpolator decel = new DecelerateInterpolator();
    287 
    288                         shrink.setDuration(FADE_TIME).setInterpolator(accel);
    289                         fadeout.setDuration(FADE_TIME).setInterpolator(accel);
    290                         grow.setDuration(FADE_TIME).setInterpolator(decel);
    291                         fadein.setDuration(FADE_TIME).setInterpolator(decel);
    292                         s.play(shrink);
    293                         s.play(fadeout);
    294                         s.play(xMove.setDuration(0)).after(FADE_TIME);
    295                         s.play(yMove.setDuration(0)).after(FADE_TIME);
    296                         s.play(fadein).after(FADE_TIME);
    297                         s.play(grow).after(FADE_TIME);
    298                     }
    299                     s.start();
    300                 }
    301 
    302                 long now = System.currentTimeMillis();
    303                 long adjust = (now % 60000);
    304                 delay = delay
    305                         + (MOVE_DELAY - adjust) // minute aligned
    306                         - (SLIDE ? 0 : FADE_TIME) // start moving before the fade
    307                         ;
    308             }
    309 
    310             mHandler.removeCallbacks(this);
    311             mHandler.postDelayed(this, delay);
    312         }
    313     }
    314 
    315     /** Setup to find out when the quarter-hour changes (e.g. Kathmandu is GMT+5:45) **/
    316     public static long getAlarmOnQuarterHour() {
    317         final Calendar calendarInstance = Calendar.getInstance();
    318         final long now = System.currentTimeMillis();
    319         return getAlarmOnQuarterHour(calendarInstance, now);
    320     }
    321 
    322     static long getAlarmOnQuarterHour(Calendar calendar, long now) {
    323         //  Set 1 second to ensure quarter-hour threshold passed.
    324         calendar.set(Calendar.SECOND, 1);
    325         calendar.set(Calendar.MILLISECOND, 0);
    326         int minute = calendar.get(Calendar.MINUTE);
    327         calendar.add(Calendar.MINUTE, 15 - (minute % 15));
    328         long alarmOnQuarterHour = calendar.getTimeInMillis();
    329 
    330         // Verify that alarmOnQuarterHour is within the next 15 minutes
    331         long delta = alarmOnQuarterHour - now;
    332         if (0 >= delta || delta > 901000) {
    333             // Something went wrong in the calculation, schedule something that is
    334             // about 15 minutes. Next time , it will align with the 15 minutes border.
    335             alarmOnQuarterHour = now + 901000;
    336         }
    337         return alarmOnQuarterHour;
    338     }
    339 
    340     // Setup a thread that starts at midnight plus one second. The extra second is added to ensure
    341     // the date has changed.
    342     public static void setMidnightUpdater(Handler handler, Runnable runnable) {
    343         String timezone = TimeZone.getDefault().getID();
    344         if (handler == null || runnable == null || timezone == null) {
    345             return;
    346         }
    347         long now = System.currentTimeMillis();
    348         Time time = new Time(timezone);
    349         time.set(now);
    350         long runInMillis = ((24 - time.hour) * 3600 - time.minute * 60 - time.second + 1) * 1000;
    351         handler.removeCallbacks(runnable);
    352         handler.postDelayed(runnable, runInMillis);
    353     }
    354 
    355     // Stop the midnight update thread
    356     public static void cancelMidnightUpdater(Handler handler, Runnable runnable) {
    357         if (handler == null || runnable == null) {
    358             return;
    359         }
    360         handler.removeCallbacks(runnable);
    361     }
    362 
    363     // Setup a thread that starts at the quarter-hour plus one second. The extra second is added to
    364     // ensure dates have changed.
    365     public static void setQuarterHourUpdater(Handler handler, Runnable runnable) {
    366         String timezone = TimeZone.getDefault().getID();
    367         if (handler == null || runnable == null || timezone == null) {
    368             return;
    369         }
    370         long runInMillis = getAlarmOnQuarterHour() - System.currentTimeMillis();
    371         // Ensure the delay is at least one second.
    372         if (runInMillis < 1000) {
    373             runInMillis = 1000;
    374         }
    375         handler.removeCallbacks(runnable);
    376         handler.postDelayed(runnable, runInMillis);
    377     }
    378 
    379     // Stop the quarter-hour update thread
    380     public static void cancelQuarterHourUpdater(Handler handler, Runnable runnable) {
    381         if (handler == null || runnable == null) {
    382             return;
    383         }
    384         handler.removeCallbacks(runnable);
    385     }
    386 
    387     /**
    388      * For screensavers to set whether the digital or analog clock should be displayed.
    389      * Returns the view to be displayed.
    390      */
    391     public static View setClockStyle(View digitalClock, View analogClock) {
    392         final DataModel.ClockStyle clockStyle = DataModel.getDataModel().getClockStyle();
    393         switch (clockStyle) {
    394             case ANALOG:
    395                 digitalClock.setVisibility(View.GONE);
    396                 analogClock.setVisibility(View.VISIBLE);
    397                 return analogClock;
    398             case DIGITAL:
    399                 digitalClock.setVisibility(View.VISIBLE);
    400                 analogClock.setVisibility(View.GONE);
    401                 return digitalClock;
    402         }
    403 
    404         throw new IllegalStateException("unexpected clock style: " + clockStyle);
    405     }
    406 
    407     /**
    408      * For screensavers to set whether the digital or analog clock should be displayed.
    409      * Returns the view to be displayed.
    410      */
    411     public static View setScreensaverClockStyle(View digitalClock, View analogClock) {
    412         final DataModel.ClockStyle clockStyle = DataModel.getDataModel().getScreensaverClockStyle();
    413         switch (clockStyle) {
    414             case ANALOG:
    415                 digitalClock.setVisibility(View.GONE);
    416                 analogClock.setVisibility(View.VISIBLE);
    417                 return analogClock;
    418             case DIGITAL:
    419                 digitalClock.setVisibility(View.VISIBLE);
    420                 analogClock.setVisibility(View.GONE);
    421                 return digitalClock;
    422         }
    423 
    424         throw new IllegalStateException("unexpected clock style: " + clockStyle);
    425     }
    426 
    427     /**
    428      * For screensavers to dim the lights if necessary.
    429      */
    430     public static void dimClockView(boolean dim, View clockView) {
    431         Paint paint = new Paint();
    432         paint.setColor(Color.WHITE);
    433         paint.setColorFilter(new PorterDuffColorFilter(
    434                         (dim ? 0x40FFFFFF : 0xC0FFFFFF),
    435                 PorterDuff.Mode.MULTIPLY));
    436         clockView.setLayerType(View.LAYER_TYPE_HARDWARE, paint);
    437     }
    438 
    439     /**
    440      * @return The next alarm from {@link AlarmManager}
    441      */
    442     public static String getNextAlarm(Context context) {
    443         return isPreL() ? getNextAlarmPreL(context) : getNextAlarmLOrLater(context);
    444     }
    445 
    446     @TargetApi(Build.VERSION_CODES.KITKAT)
    447     private static String getNextAlarmPreL(Context context) {
    448         final ContentResolver cr = context.getContentResolver();
    449         return Settings.System.getString(cr, Settings.System.NEXT_ALARM_FORMATTED);
    450     }
    451 
    452     @TargetApi(Build.VERSION_CODES.LOLLIPOP)
    453     private static String getNextAlarmLOrLater(Context context) {
    454         final AlarmManager am = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
    455         final AlarmManager.AlarmClockInfo info = am.getNextAlarmClock();
    456         if (info != null) {
    457             final long triggerTime = info.getTriggerTime();
    458             final Calendar alarmTime = Calendar.getInstance();
    459             alarmTime.setTimeInMillis(triggerTime);
    460             return AlarmUtils.getFormattedTime(context, alarmTime);
    461         }
    462 
    463         return null;
    464     }
    465 
    466     public static boolean isAlarmWithin24Hours(AlarmInstance alarmInstance) {
    467         final Calendar nextAlarmTime = alarmInstance.getAlarmTime();
    468         final long nextAlarmTimeMillis = nextAlarmTime.getTimeInMillis();
    469         return nextAlarmTimeMillis - System.currentTimeMillis() <= DateUtils.DAY_IN_MILLIS;
    470     }
    471 
    472     /** Clock views can call this to refresh their alarm to the next upcoming value. */
    473     public static void refreshAlarm(Context context, View clock) {
    474         final TextView nextAlarmView = (TextView) clock.findViewById(R.id.nextAlarm);
    475         if (nextAlarmView == null) {
    476             return;
    477         }
    478 
    479         final String alarm = getNextAlarm(context);
    480         if (!TextUtils.isEmpty(alarm)) {
    481             final String description = context.getString(R.string.next_alarm_description, alarm);
    482             nextAlarmView.setText(alarm);
    483             nextAlarmView.setContentDescription(description);
    484             nextAlarmView.setVisibility(View.VISIBLE);
    485         } else {
    486             nextAlarmView.setVisibility(View.GONE);
    487         }
    488     }
    489 
    490     /** Clock views can call this to refresh their date. **/
    491     public static void updateDate(String dateSkeleton, String descriptionSkeleton, View clock) {
    492         final TextView dateDisplay = (TextView) clock.findViewById(R.id.date);
    493         if (dateDisplay == null) {
    494             return;
    495         }
    496 
    497         final Locale l = Locale.getDefault();
    498         final String datePattern = DateFormat.getBestDateTimePattern(l, dateSkeleton);
    499         final String descriptionPattern = DateFormat.getBestDateTimePattern(l, descriptionSkeleton);
    500 
    501         final Date now = new Date();
    502         dateDisplay.setText(new SimpleDateFormat(datePattern, l).format(now));
    503         dateDisplay.setVisibility(View.VISIBLE);
    504         dateDisplay.setContentDescription(new SimpleDateFormat(descriptionPattern, l).format(now));
    505     }
    506 
    507     /***
    508      * Formats the time in the TextClock according to the Locale with a special
    509      * formatting treatment for the am/pm label.
    510      * @param context - Context used to get user's locale and time preferences
    511      * @param clock - TextClock to format
    512      */
    513     public static void setTimeFormat(Context context, TextClock clock) {
    514         if (clock != null) {
    515             // Get the best format for 12 hours mode according to the locale
    516             clock.setFormat12Hour(get12ModeFormat(context, true /* showAmPm */));
    517             // Get the best format for 24 hours mode according to the locale
    518             clock.setFormat24Hour(get24ModeFormat());
    519         }
    520     }
    521 
    522     /**
    523      * Returns {@code true} if the am / pm strings for the current locale are long and a reduced
    524      * text size should be used for displaying the digital clock.
    525      */
    526     public static boolean isAmPmStringLong() {
    527         final String[] amPmStrings = new DateFormatSymbols().getAmPmStrings();
    528         for (String amPmString : amPmStrings) {
    529             // Dots are small, so don't count them.
    530             final int amPmStringLength = amPmString.replace(".", "").length();
    531             if (amPmStringLength > 3) {
    532                 return true;
    533             }
    534         }
    535         return false;
    536     }
    537 
    538     /**
    539      * @param context - context used to get time format string resource
    540      * @param showAmPm - include the am/pm string if true
    541      * @return format string for 12 hours mode time
    542      */
    543     public static CharSequence get12ModeFormat(Context context, boolean showAmPm) {
    544         String pattern = DateFormat.getBestDateTimePattern(Locale.getDefault(), "hma");
    545         if (!showAmPm) {
    546             pattern = pattern.replaceAll("a", "").trim();
    547         }
    548 
    549         // Replace spaces with "Hair Space"
    550         pattern = pattern.replaceAll(" ", "\u200A");
    551         // Build a spannable so that the am/pm will be formatted
    552         int amPmPos = pattern.indexOf('a');
    553         if (amPmPos == -1) {
    554             return pattern;
    555         }
    556 
    557         final Resources resources = context.getResources();
    558         final float amPmProportion = resources.getFraction(R.fraction.ampm_font_size_scale, 1, 1);
    559         final Spannable sp = new SpannableString(pattern);
    560         sp.setSpan(new RelativeSizeSpan(amPmProportion), amPmPos, amPmPos + 1,
    561                 Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
    562         sp.setSpan(new StyleSpan(Typeface.NORMAL), amPmPos, amPmPos + 1,
    563                 Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
    564         sp.setSpan(new TypefaceSpan("sans-serif"), amPmPos, amPmPos + 1,
    565                 Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
    566 
    567         // Make the font smaller for locales with long am/pm strings.
    568         if (Utils.isAmPmStringLong()) {
    569             final float proportion = resources.getFraction(
    570                     R.fraction.reduced_clock_font_size_scale, 1, 1);
    571             sp.setSpan(new RelativeSizeSpan(proportion), 0, pattern.length(),
    572                     Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
    573         }
    574         return sp;
    575     }
    576 
    577     public static CharSequence get24ModeFormat() {
    578         return DateFormat.getBestDateTimePattern(Locale.getDefault(), "Hm");
    579     }
    580 
    581     /**
    582      * Returns string denoting the timezone hour offset (e.g. GMT -8:00)
    583      * @param useShortForm Whether to return a short form of the header that rounds to the
    584      *                     nearest hour and excludes the "GMT" prefix
    585      */
    586     public static String getGMTHourOffset(TimeZone timezone, boolean useShortForm) {
    587         final int gmtOffset = timezone.getRawOffset();
    588         final long hour = gmtOffset / DateUtils.HOUR_IN_MILLIS;
    589         final long min = (Math.abs(gmtOffset) % DateUtils.HOUR_IN_MILLIS) /
    590                 DateUtils.MINUTE_IN_MILLIS;
    591 
    592         if (useShortForm) {
    593             return String.format("%+d", hour);
    594         } else {
    595             return String.format("GMT %+d:%02d", hour, min);
    596         }
    597     }
    598 
    599     /**
    600      * Convenience method for retrieving a themed color value.
    601      *
    602      * @param context  the {@link Context} to resolve the theme attribute against
    603      * @param attr     the attribute corresponding to the color to resolve
    604      * @param defValue the default color value to use if the attribute cannot be resolved
    605      * @return the color value of the resolve attribute
    606      */
    607     public static int obtainStyledColor(Context context, int attr, int defValue) {
    608         TEMP_ARRAY[0] = attr;
    609         final TypedArray a = context.obtainStyledAttributes(TEMP_ARRAY);
    610         try {
    611             return a.getColor(0, defValue);
    612         } finally {
    613             a.recycle();
    614         }
    615     }
    616 
    617     /**
    618      * Returns the background color to use based on the current time.
    619      */
    620     public static int getCurrentHourColor() {
    621         return BACKGROUND_SPECTRUM[Calendar.getInstance().get(Calendar.HOUR_OF_DAY)];
    622     }
    623 
    624     /**
    625      * @param firstDay is the result from getZeroIndexedFirstDayOfWeek
    626      * @return Single-char version of day name, e.g.: 'S', 'M', 'T', 'W', 'T', 'F', 'S'
    627      */
    628     public static String getShortWeekday(int position, int firstDay) {
    629         generateShortAndLongWeekdaysIfNeeded();
    630         return sShortWeekdays[(position + firstDay) % DaysOfWeek.DAYS_IN_A_WEEK];
    631     }
    632 
    633     /**
    634      * @param firstDay is the result from getZeroIndexedFirstDayOfWeek
    635      * @return Long-version of day name, e.g.: 'Sunday', 'Monday', 'Tuesday', etc
    636      */
    637     public static String getLongWeekday(int position, int firstDay) {
    638         generateShortAndLongWeekdaysIfNeeded();
    639         return sLongWeekdays[(position + firstDay) % DaysOfWeek.DAYS_IN_A_WEEK];
    640     }
    641 
    642     // Return the first day of the week value corresponding to Calendar.<WEEKDAY> value, which is
    643     // 1-indexed starting with Sunday.
    644     public static int getFirstDayOfWeek(Context context) {
    645         return Integer.parseInt(getDefaultSharedPreferences(context)
    646                 .getString(SettingsActivity.KEY_WEEK_START, String.valueOf(DEFAULT_WEEK_START)));
    647     }
    648 
    649     // Return the first day of the week value corresponding to a week with Sunday at 0 index.
    650     public static int getZeroIndexedFirstDayOfWeek(Context context) {
    651         return getFirstDayOfWeek(context) - 1;
    652     }
    653 
    654     private static boolean localeHasChanged() {
    655         return sLocaleUsedForWeekdays != Locale.getDefault();
    656     }
    657 
    658     /**
    659      * Generate arrays of short and long weekdays, starting from Sunday
    660      */
    661     private static void generateShortAndLongWeekdaysIfNeeded() {
    662         if (sShortWeekdays != null && sLongWeekdays != null && !localeHasChanged()) {
    663             // nothing to do
    664             return;
    665         }
    666         if (sShortWeekdays == null) {
    667             sShortWeekdays = new String[DaysOfWeek.DAYS_IN_A_WEEK];
    668         }
    669         if (sLongWeekdays == null) {
    670             sLongWeekdays = new String[DaysOfWeek.DAYS_IN_A_WEEK];
    671         }
    672 
    673         final SimpleDateFormat shortFormat = new SimpleDateFormat(DATE_FORMAT_SHORT);
    674         final SimpleDateFormat longFormat = new SimpleDateFormat(DATE_FORMAT_LONG);
    675 
    676         // Create a date (2014/07/20) that is a Sunday
    677         final long aSunday = new GregorianCalendar(2014, Calendar.JULY, 20).getTimeInMillis();
    678 
    679         for (int i = 0; i < DaysOfWeek.DAYS_IN_A_WEEK; i++) {
    680             final long dayMillis = aSunday + i * DateUtils.DAY_IN_MILLIS;
    681             sShortWeekdays[i] = shortFormat.format(new Date(dayMillis));
    682             sLongWeekdays[i] = longFormat.format(new Date(dayMillis));
    683         }
    684 
    685         // Track the Locale used to generate these weekdays
    686         sLocaleUsedForWeekdays = Locale.getDefault();
    687     }
    688 
    689     /**
    690      * @param id Resource id of the plural
    691      * @param quantity integer value
    692      * @return string with properly localized numbers
    693      */
    694     public static String getNumberFormattedQuantityString(Context context, int id, int quantity) {
    695         final String localizedQuantity = NumberFormat.getInstance().format(quantity);
    696         return context.getResources().getQuantityString(id, quantity, localizedQuantity);
    697     }
    698 
    699     public static <E> ArraySet<E> newArraySet(Collection<E> collection) {
    700         final ArraySet<E> arraySet = new ArraySet<>(collection.size());
    701         arraySet.addAll(collection);
    702         return arraySet;
    703     }
    704 
    705     /**
    706      * Return the default shared preferences.
    707      */
    708     public static SharedPreferences getDefaultSharedPreferences(Context context) {
    709         final Context storageContext;
    710         if (isNOrLater()) {
    711             // All N devices have split storage areas, but we may need to
    712             // migrate existing preferences into the new device protected
    713             // storage area, which is where our data lives from now on.
    714             final Context deviceContext = context.createDeviceProtectedStorageContext();
    715             if (!deviceContext.moveSharedPreferencesFrom(context,
    716                     PreferenceManager.getDefaultSharedPreferencesName(context))) {
    717                 LogUtils.wtf("Failed to migrate shared preferences");
    718             }
    719             storageContext = deviceContext;
    720         } else {
    721             storageContext = context;
    722         }
    723 
    724         return PreferenceManager.getDefaultSharedPreferences(storageContext);
    725     }
    726 }
    727