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 
enforceMainLooper()122     public static void enforceMainLooper() {
123         if (Looper.getMainLooper() != Looper.myLooper()) {
124             throw new IllegalAccessError("May only call from main thread.");
125         }
126     }
127 
enforceNotMainLooper()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      */
isPreL()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      */
isLOrLMR1()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      */
isLOrLater()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      */
isLMR1OrLater()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      */
isMOrLater()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      */
isNOrLater()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      */
calculateRadiusOffset( float strokeSize, float dotStrokeSize, float markerStrokeSize)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      */
calculateRadiusOffset(Resources resources)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 
ScreensaverMoveSaverRunnable(Handler handler)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 
registerViews(View contentView, View saverView)228         public void registerViews(View contentView, View saverView) {
229             mContentView = contentView;
230             mSaverView = saverView;
231         }
232 
233         @Override
run()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) **/
getAlarmOnQuarterHour()316     public static long getAlarmOnQuarterHour() {
317         final Calendar calendarInstance = Calendar.getInstance();
318         final long now = System.currentTimeMillis();
319         return getAlarmOnQuarterHour(calendarInstance, now);
320     }
321 
getAlarmOnQuarterHour(Calendar calendar, long now)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.
setMidnightUpdater(Handler handler, Runnable runnable)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
cancelMidnightUpdater(Handler handler, Runnable runnable)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.
setQuarterHourUpdater(Handler handler, Runnable runnable)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
cancelQuarterHourUpdater(Handler handler, Runnable runnable)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      */
setClockStyle(View digitalClock, View analogClock)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      */
setScreensaverClockStyle(View digitalClock, View analogClock)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      */
dimClockView(boolean dim, View clockView)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      */
getNextAlarm(Context context)442     public static String getNextAlarm(Context context) {
443         return isPreL() ? getNextAlarmPreL(context) : getNextAlarmLOrLater(context);
444     }
445 
446     @TargetApi(Build.VERSION_CODES.KITKAT)
getNextAlarmPreL(Context context)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)
getNextAlarmLOrLater(Context context)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 
isAlarmWithin24Hours(AlarmInstance alarmInstance)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. */
refreshAlarm(Context context, View clock)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. **/
updateDate(String dateSkeleton, String descriptionSkeleton, View clock)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      */
setTimeFormat(Context context, TextClock clock)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      */
isAmPmStringLong()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      */
get12ModeFormat(Context context, boolean showAmPm)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 
get24ModeFormat()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      */
getGMTHourOffset(TimeZone timezone, boolean useShortForm)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      */
obtainStyledColor(Context context, int attr, int defValue)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      */
getCurrentHourColor()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      */
getShortWeekday(int position, int firstDay)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      */
getLongWeekday(int position, int firstDay)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.
getFirstDayOfWeek(Context context)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.
getZeroIndexedFirstDayOfWeek(Context context)650     public static int getZeroIndexedFirstDayOfWeek(Context context) {
651         return getFirstDayOfWeek(context) - 1;
652     }
653 
localeHasChanged()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      */
generateShortAndLongWeekdaysIfNeeded()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      */
getNumberFormattedQuantityString(Context context, int id, int quantity)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 
newArraySet(Collection<E> collection)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      */
getDefaultSharedPreferences(Context context)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