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
17package com.android.deskclock;
18
19import android.animation.Animator;
20import android.animation.AnimatorSet;
21import android.animation.ObjectAnimator;
22import android.animation.TimeInterpolator;
23import android.annotation.TargetApi;
24import android.app.AlarmManager;
25import android.content.ContentResolver;
26import android.content.Context;
27import android.content.SharedPreferences;
28import android.content.res.Resources;
29import android.content.res.TypedArray;
30import android.graphics.Color;
31import android.graphics.Paint;
32import android.graphics.PorterDuff;
33import android.graphics.PorterDuffColorFilter;
34import android.graphics.Typeface;
35import android.os.Build;
36import android.os.Handler;
37import android.os.Looper;
38import android.preference.PreferenceManager;
39import android.provider.Settings;
40import android.support.v4.content.ContextCompat;
41import android.support.v4.os.BuildCompat;
42import android.text.Spannable;
43import android.text.SpannableString;
44import android.text.TextUtils;
45import android.text.format.DateFormat;
46import android.text.format.DateUtils;
47import android.text.format.Time;
48import android.text.style.RelativeSizeSpan;
49import android.text.style.StyleSpan;
50import android.text.style.TypefaceSpan;
51import android.util.ArraySet;
52import android.view.View;
53import android.view.animation.AccelerateInterpolator;
54import android.view.animation.DecelerateInterpolator;
55import android.widget.TextClock;
56import android.widget.TextView;
57
58import com.android.deskclock.data.DataModel;
59import com.android.deskclock.provider.AlarmInstance;
60import com.android.deskclock.provider.DaysOfWeek;
61import com.android.deskclock.settings.SettingsActivity;
62
63import java.io.File;
64import java.text.DateFormatSymbols;
65import java.text.NumberFormat;
66import java.text.SimpleDateFormat;
67import java.util.Calendar;
68import java.util.Collection;
69import java.util.Date;
70import java.util.GregorianCalendar;
71import java.util.Locale;
72import java.util.TimeZone;
73
74public 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