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.app.AlarmManager;
24import android.content.Context;
25import android.content.Intent;
26import android.content.SharedPreferences;
27import android.content.pm.PackageInfo;
28import android.content.pm.PackageManager.NameNotFoundException;
29import android.content.res.Resources;
30import android.content.res.TypedArray;
31import android.graphics.Color;
32import android.graphics.Paint;
33import android.graphics.PorterDuff;
34import android.graphics.PorterDuffColorFilter;
35import android.graphics.Typeface;
36import android.net.Uri;
37import android.os.Build;
38import android.os.Handler;
39import android.os.SystemClock;
40import android.preference.PreferenceManager;
41import android.provider.Settings;
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.AbsoluteSizeSpan;
49import android.text.style.StyleSpan;
50import android.text.style.TypefaceSpan;
51import android.view.MenuItem;
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.provider.AlarmInstance;
59import com.android.deskclock.provider.DaysOfWeek;
60import com.android.deskclock.stopwatch.Stopwatches;
61import com.android.deskclock.timer.Timers;
62import com.android.deskclock.worldclock.CityObj;
63
64import java.text.NumberFormat;
65import java.text.SimpleDateFormat;
66import java.util.Calendar;
67import java.util.Date;
68import java.util.GregorianCalendar;
69import java.util.HashMap;
70import java.util.Locale;
71import java.util.Map;
72import java.util.TimeZone;
73
74
75public class Utils {
76    private final static String PARAM_LANGUAGE_CODE = "hl";
77
78    /**
79     * Help URL query parameter key for the app version.
80     */
81    private final static String PARAM_VERSION = "version";
82
83    /**
84     * Cached version code to prevent repeated calls to the package manager.
85     */
86    private static String sCachedVersionCode = null;
87
88    // Single-char version of day name, e.g.: 'S', 'M', 'T', 'W', 'T', 'F', 'S'
89    private static String[] sShortWeekdays = null;
90    private static final String DATE_FORMAT_SHORT = isJBMR2OrLater() ? "ccccc" : "ccc";
91
92    // Long-version of day name, e.g.: 'Sunday', 'Monday', 'Tuesday', etc
93    private static String[] sLongWeekdays = null;
94    private static final String DATE_FORMAT_LONG = "EEEE";
95
96    public static final int DEFAULT_WEEK_START = Calendar.getInstance().getFirstDayOfWeek();
97
98    private static Locale sLocaleUsedForWeekdays;
99
100    /** Types that may be used for clock displays. **/
101    public static final String CLOCK_TYPE_DIGITAL = "digital";
102    public static final String CLOCK_TYPE_ANALOG = "analog";
103
104    /**
105     * Temporary array used by {@link #obtainStyledColor(Context, int, int)}.
106     */
107    private static final int[] TEMP_ARRAY = new int[1];
108
109    /**
110     * The background colors of the app - it changes throughout out the day to mimic the sky.
111     */
112    private static final int[] BACKGROUND_SPECTRUM = {
113            0xFF212121 /* 12 AM */,
114            0xFF20222A /*  1 AM */,
115            0xFF202233 /*  2 AM */,
116            0xFF1F2242 /*  3 AM */,
117            0xFF1E224F /*  4 AM */,
118            0xFF1D225C /*  5 AM */,
119            0xFF1B236B /*  6 AM */,
120            0xFF1A237E /*  7 AM */,
121            0xFF1D2783 /*  8 AM */,
122            0xFF232E8B /*  9 AM */,
123            0xFF283593 /* 10 AM */,
124            0xFF2C3998 /* 11 AM */,
125            0xFF303F9F /* 12 PM */,
126            0xFF2C3998 /*  1 PM */,
127            0xFF283593 /*  2 PM */,
128            0xFF232E8B /*  3 PM */,
129            0xFF1D2783 /*  4 PM */,
130            0xFF1A237E /*  5 PM */,
131            0xFF1B236B /*  6 PM */,
132            0xFF1D225C /*  7 PM */,
133            0xFF1E224F /*  8 PM */,
134            0xFF1F2242 /*  9 PM */,
135            0xFF202233 /* 10 PM */,
136            0xFF20222A /* 11 PM */
137    };
138
139    /**
140     * Returns whether the SDK is KitKat or later
141     */
142    public static boolean isKitKatOrLater() {
143        return Build.VERSION.SDK_INT > Build.VERSION_CODES.JELLY_BEAN_MR2;
144    }
145
146    /**
147     * @return {@code true} if the device is {@link Build.VERSION_CODES#JELLY_BEAN_MR2} or later
148     */
149    public static boolean isJBMR2OrLater() {
150        return Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2;
151    }
152
153    /**
154     * @return {@code true} if the device is {@link Build.VERSION_CODES#LOLLIPOP} or later
155     */
156    public static boolean isLOrLater() {
157        return Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP;
158    }
159
160    /**
161     * @return {@code true} if the device is {@link Build.VERSION_CODES#LOLLIPOP_MR1} or later
162     */
163    public static boolean isLMR1OrLater() {
164        return Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP_MR1;
165    }
166
167    /**
168     * @return {@code true} if the device is {@link Build.VERSION_CODES#M} or later
169     */
170    public static boolean isMOrLater() {
171        return Build.VERSION.SDK_INT >= Build.VERSION_CODES.M;
172    }
173
174    public static void prepareHelpMenuItem(Context context, MenuItem helpMenuItem) {
175        String helpUrlString = context.getResources().getString(R.string.desk_clock_help_url);
176        if (TextUtils.isEmpty(helpUrlString)) {
177            // The help url string is empty or null, so set the help menu item to be invisible.
178            helpMenuItem.setVisible(false);
179            return;
180        }
181        // The help url string exists, so first add in some extra query parameters.  87
182        final Uri fullUri = uriWithAddedParameters(context, Uri.parse(helpUrlString));
183
184        // Then, create an intent that will be fired when the user
185        // selects this help menu item.
186        Intent intent = new Intent(Intent.ACTION_VIEW, fullUri);
187        intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK
188                | Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS);
189
190        // Set the intent to the help menu item, show the help menu item in the overflow
191        // menu, and make it visible.
192        helpMenuItem.setIntent(intent);
193        helpMenuItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER);
194        helpMenuItem.setVisible(true);
195    }
196
197    /**
198     * Adds two query parameters into the Uri, namely the language code and the version code
199     * of the application's package as gotten via the context.
200     * @return the uri with added query parameters
201     */
202    private static Uri uriWithAddedParameters(Context context, Uri baseUri) {
203        Uri.Builder builder = baseUri.buildUpon();
204
205        // Add in the preferred language
206        builder.appendQueryParameter(PARAM_LANGUAGE_CODE, Locale.getDefault().toString());
207
208        // Add in the package version code
209        if (sCachedVersionCode == null) {
210            // There is no cached version code, so try to get it from the package manager.
211            try {
212                // cache the version code
213                PackageInfo info = context.getPackageManager().getPackageInfo(
214                        context.getPackageName(), 0);
215                sCachedVersionCode = Integer.toString(info.versionCode);
216
217                // append the version code to the uri
218                builder.appendQueryParameter(PARAM_VERSION, sCachedVersionCode);
219            } catch (NameNotFoundException e) {
220                // Cannot find the package name, so don't add in the version parameter
221                // This shouldn't happen.
222                LogUtils.wtf("Invalid package name for context " + e);
223            }
224        } else {
225            builder.appendQueryParameter(PARAM_VERSION, sCachedVersionCode);
226        }
227
228        // Build the full uri and return it
229        return builder.build();
230    }
231
232    public static long getTimeNow() {
233        return SystemClock.elapsedRealtime();
234    }
235
236    /**
237     * Calculate the amount by which the radius of a CircleTimerView should be offset by the any
238     * of the extra painted objects.
239     */
240    public static float calculateRadiusOffset(
241            float strokeSize, float dotStrokeSize, float markerStrokeSize) {
242        return Math.max(strokeSize, Math.max(dotStrokeSize, markerStrokeSize));
243    }
244
245    /**
246     * Uses {@link Utils#calculateRadiusOffset(float, float, float)} after fetching the values
247     * from the resources just as {@link CircleTimerView#init(android.content.Context)} does.
248     */
249    public static float calculateRadiusOffset(Resources resources) {
250        if (resources != null) {
251            float strokeSize = resources.getDimension(R.dimen.circletimer_circle_size);
252            float dotStrokeSize = resources.getDimension(R.dimen.circletimer_dot_size);
253            float markerStrokeSize = resources.getDimension(R.dimen.circletimer_marker_size);
254            return calculateRadiusOffset(strokeSize, dotStrokeSize, markerStrokeSize);
255        } else {
256            return 0f;
257        }
258    }
259
260    /**
261     * Clears the persistent data of stopwatch (start time, state, laps, etc...).
262     */
263    public static void clearSwSharedPref(SharedPreferences prefs) {
264        SharedPreferences.Editor editor = prefs.edit();
265        editor.remove (Stopwatches.PREF_START_TIME);
266        editor.remove (Stopwatches.PREF_ACCUM_TIME);
267        editor.remove (Stopwatches.PREF_STATE);
268        int lapNum = prefs.getInt(Stopwatches.PREF_LAP_NUM, Stopwatches.STOPWATCH_RESET);
269        for (int i = 0; i < lapNum; i++) {
270            String key = Stopwatches.PREF_LAP_TIME + Integer.toString(i);
271            editor.remove(key);
272        }
273        editor.remove(Stopwatches.PREF_LAP_NUM);
274        editor.apply();
275    }
276
277    /**
278     * Broadcast a message to show the in-use timers in the notifications
279     */
280    public static void showInUseNotifications(Context context) {
281        Intent timerIntent = new Intent();
282        timerIntent.setAction(Timers.NOTIF_IN_USE_SHOW);
283        context.sendBroadcast(timerIntent);
284    }
285
286    /**
287     * Broadcast a message to show the in-use timers in the notifications
288     */
289    public static void showTimesUpNotifications(Context context) {
290        Intent timerIntent = new Intent();
291        timerIntent.setAction(Timers.NOTIF_TIMES_UP_SHOW);
292        context.sendBroadcast(timerIntent);
293    }
294
295    /**
296     * Broadcast a message to cancel the in-use timers in the notifications
297     */
298    public static void cancelTimesUpNotifications(Context context) {
299        Intent timerIntent = new Intent();
300        timerIntent.setAction(Timers.NOTIF_TIMES_UP_CANCEL);
301        context.sendBroadcast(timerIntent);
302    }
303
304    /** Runnable for use with screensaver and dream, to move the clock every minute.
305     *  registerViews() must be called prior to posting.
306     */
307    public static class ScreensaverMoveSaverRunnable implements Runnable {
308        static final long MOVE_DELAY = 60000; // DeskClock.SCREEN_SAVER_MOVE_DELAY;
309        static final long SLIDE_TIME = 10000;
310        static final long FADE_TIME = 3000;
311
312        static final boolean SLIDE = false;
313
314        private View mContentView, mSaverView;
315        private final Handler mHandler;
316
317        private static TimeInterpolator mSlowStartWithBrakes;
318
319
320        public ScreensaverMoveSaverRunnable(Handler handler) {
321            mHandler = handler;
322            mSlowStartWithBrakes = new TimeInterpolator() {
323                @Override
324                public float getInterpolation(float x) {
325                    return (float)(Math.cos((Math.pow(x,3) + 1) * Math.PI) / 2.0f) + 0.5f;
326                }
327            };
328        }
329
330        public void registerViews(View contentView, View saverView) {
331            mContentView = contentView;
332            mSaverView = saverView;
333        }
334
335        @Override
336        public void run() {
337            long delay = MOVE_DELAY;
338            if (mContentView == null || mSaverView == null) {
339                mHandler.removeCallbacks(this);
340                mHandler.postDelayed(this, delay);
341                return;
342            }
343
344            final float xrange = mContentView.getWidth() - mSaverView.getWidth();
345            final float yrange = mContentView.getHeight() - mSaverView.getHeight();
346
347            if (xrange == 0 && yrange == 0) {
348                delay = 500; // back in a split second
349            } else {
350                final int nextx = (int) (Math.random() * xrange);
351                final int nexty = (int) (Math.random() * yrange);
352
353                if (mSaverView.getAlpha() == 0f) {
354                    // jump right there
355                    mSaverView.setX(nextx);
356                    mSaverView.setY(nexty);
357                    ObjectAnimator.ofFloat(mSaverView, "alpha", 0f, 1f)
358                        .setDuration(FADE_TIME)
359                        .start();
360                } else {
361                    AnimatorSet s = new AnimatorSet();
362                    Animator xMove   = ObjectAnimator.ofFloat(mSaverView,
363                                         "x", mSaverView.getX(), nextx);
364                    Animator yMove   = ObjectAnimator.ofFloat(mSaverView,
365                                         "y", mSaverView.getY(), nexty);
366
367                    Animator xShrink = ObjectAnimator.ofFloat(mSaverView, "scaleX", 1f, 0.85f);
368                    Animator xGrow   = ObjectAnimator.ofFloat(mSaverView, "scaleX", 0.85f, 1f);
369
370                    Animator yShrink = ObjectAnimator.ofFloat(mSaverView, "scaleY", 1f, 0.85f);
371                    Animator yGrow   = ObjectAnimator.ofFloat(mSaverView, "scaleY", 0.85f, 1f);
372                    AnimatorSet shrink = new AnimatorSet(); shrink.play(xShrink).with(yShrink);
373                    AnimatorSet grow = new AnimatorSet(); grow.play(xGrow).with(yGrow);
374
375                    Animator fadeout = ObjectAnimator.ofFloat(mSaverView, "alpha", 1f, 0f);
376                    Animator fadein = ObjectAnimator.ofFloat(mSaverView, "alpha", 0f, 1f);
377
378
379                    if (SLIDE) {
380                        s.play(xMove).with(yMove);
381                        s.setDuration(SLIDE_TIME);
382
383                        s.play(shrink.setDuration(SLIDE_TIME/2));
384                        s.play(grow.setDuration(SLIDE_TIME/2)).after(shrink);
385                        s.setInterpolator(mSlowStartWithBrakes);
386                    } else {
387                        AccelerateInterpolator accel = new AccelerateInterpolator();
388                        DecelerateInterpolator decel = new DecelerateInterpolator();
389
390                        shrink.setDuration(FADE_TIME).setInterpolator(accel);
391                        fadeout.setDuration(FADE_TIME).setInterpolator(accel);
392                        grow.setDuration(FADE_TIME).setInterpolator(decel);
393                        fadein.setDuration(FADE_TIME).setInterpolator(decel);
394                        s.play(shrink);
395                        s.play(fadeout);
396                        s.play(xMove.setDuration(0)).after(FADE_TIME);
397                        s.play(yMove.setDuration(0)).after(FADE_TIME);
398                        s.play(fadein).after(FADE_TIME);
399                        s.play(grow).after(FADE_TIME);
400                    }
401                    s.start();
402                }
403
404                long now = System.currentTimeMillis();
405                long adjust = (now % 60000);
406                delay = delay
407                        + (MOVE_DELAY - adjust) // minute aligned
408                        - (SLIDE ? 0 : FADE_TIME) // start moving before the fade
409                        ;
410            }
411
412            mHandler.removeCallbacks(this);
413            mHandler.postDelayed(this, delay);
414        }
415    }
416
417    /** Setup to find out when the quarter-hour changes (e.g. Kathmandu is GMT+5:45) **/
418    public static long getAlarmOnQuarterHour() {
419        final Calendar calendarInstance = Calendar.getInstance();
420        final long now = System.currentTimeMillis();
421        return getAlarmOnQuarterHour(calendarInstance, now);
422    }
423
424    static long getAlarmOnQuarterHour(Calendar calendar, long now) {
425        //  Set 1 second to ensure quarter-hour threshold passed.
426        calendar.set(Calendar.SECOND, 1);
427        calendar.set(Calendar.MILLISECOND, 0);
428        int minute = calendar.get(Calendar.MINUTE);
429        calendar.add(Calendar.MINUTE, 15 - (minute % 15));
430        long alarmOnQuarterHour = calendar.getTimeInMillis();
431
432        // Verify that alarmOnQuarterHour is within the next 15 minutes
433        long delta = alarmOnQuarterHour - now;
434        if (0 >= delta || delta > 901000) {
435            // Something went wrong in the calculation, schedule something that is
436            // about 15 minutes. Next time , it will align with the 15 minutes border.
437            alarmOnQuarterHour = now + 901000;
438        }
439        return alarmOnQuarterHour;
440    }
441
442    // Setup a thread that starts at midnight plus one second. The extra second is added to ensure
443    // the date has changed.
444    public static void setMidnightUpdater(Handler handler, Runnable runnable) {
445        String timezone = TimeZone.getDefault().getID();
446        if (handler == null || runnable == null || timezone == null) {
447            return;
448        }
449        long now = System.currentTimeMillis();
450        Time time = new Time(timezone);
451        time.set(now);
452        long runInMillis = ((24 - time.hour) * 3600 - time.minute * 60 - time.second + 1) * 1000;
453        handler.removeCallbacks(runnable);
454        handler.postDelayed(runnable, runInMillis);
455    }
456
457    // Stop the midnight update thread
458    public static void cancelMidnightUpdater(Handler handler, Runnable runnable) {
459        if (handler == null || runnable == null) {
460            return;
461        }
462        handler.removeCallbacks(runnable);
463    }
464
465    // Setup a thread that starts at the quarter-hour plus one second. The extra second is added to
466    // ensure dates have changed.
467    public static void setQuarterHourUpdater(Handler handler, Runnable runnable) {
468        String timezone = TimeZone.getDefault().getID();
469        if (handler == null || runnable == null || timezone == null) {
470            return;
471        }
472        long runInMillis = getAlarmOnQuarterHour() - System.currentTimeMillis();
473        // Ensure the delay is at least one second.
474        if (runInMillis < 1000) {
475            runInMillis = 1000;
476        }
477        handler.removeCallbacks(runnable);
478        handler.postDelayed(runnable, runInMillis);
479    }
480
481    // Stop the quarter-hour update thread
482    public static void cancelQuarterHourUpdater(Handler handler, Runnable runnable) {
483        if (handler == null || runnable == null) {
484            return;
485        }
486        handler.removeCallbacks(runnable);
487    }
488
489    /**
490     * For screensavers to set whether the digital or analog clock should be displayed.
491     * Returns the view to be displayed.
492     */
493    public static View setClockStyle(Context context, View digitalClock, View analogClock,
494            String clockStyleKey) {
495        SharedPreferences sharedPref = PreferenceManager.getDefaultSharedPreferences(context);
496        String defaultClockStyle = context.getResources().getString(R.string.default_clock_style);
497        String style = sharedPref.getString(clockStyleKey, defaultClockStyle);
498        View returnView;
499        if (style.equals(CLOCK_TYPE_ANALOG)) {
500            digitalClock.setVisibility(View.GONE);
501            analogClock.setVisibility(View.VISIBLE);
502            returnView = analogClock;
503        } else {
504            digitalClock.setVisibility(View.VISIBLE);
505            analogClock.setVisibility(View.GONE);
506            returnView = digitalClock;
507        }
508
509        return returnView;
510    }
511
512    /**
513     * For screensavers to dim the lights if necessary.
514     */
515    public static void dimClockView(boolean dim, View clockView) {
516        Paint paint = new Paint();
517        paint.setColor(Color.WHITE);
518        paint.setColorFilter(new PorterDuffColorFilter(
519                        (dim ? 0x40FFFFFF : 0xC0FFFFFF),
520                PorterDuff.Mode.MULTIPLY));
521        clockView.setLayerType(View.LAYER_TYPE_HARDWARE, paint);
522    }
523
524    /**
525     * @return The next alarm from {@link AlarmManager}
526     */
527    public static String getNextAlarm(Context context) {
528        String timeString = null;
529        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
530            timeString = Settings.System.getString(context.getContentResolver(),
531                    Settings.System.NEXT_ALARM_FORMATTED);
532        } else {
533            final AlarmManager.AlarmClockInfo info = ((AlarmManager) context.getSystemService(
534                    Context.ALARM_SERVICE)).getNextAlarmClock();
535            if (info != null) {
536                final long triggerTime = info.getTriggerTime();
537                final Calendar alarmTime = Calendar.getInstance();
538                alarmTime.setTimeInMillis(triggerTime);
539                timeString = AlarmUtils.getFormattedTime(context, alarmTime);
540            }
541        }
542        return timeString;
543    }
544
545    public static boolean isAlarmWithin24Hours(AlarmInstance alarmInstance) {
546        final Calendar nextAlarmTime = alarmInstance.getAlarmTime();
547        final long nextAlarmTimeMillis = nextAlarmTime.getTimeInMillis();
548        return nextAlarmTimeMillis - System.currentTimeMillis() <= DateUtils.DAY_IN_MILLIS;
549    }
550
551    /** Clock views can call this to refresh their alarm to the next upcoming value. **/
552    public static void refreshAlarm(Context context, View clock) {
553        final String nextAlarm = getNextAlarm(context);
554        TextView nextAlarmView;
555        nextAlarmView = (TextView) clock.findViewById(R.id.nextAlarm);
556        if (!TextUtils.isEmpty(nextAlarm) && nextAlarmView != null) {
557            nextAlarmView.setText(
558                    context.getString(R.string.control_set_alarm_with_existing, nextAlarm));
559            nextAlarmView.setContentDescription(context.getResources().getString(
560                    R.string.next_alarm_description, nextAlarm));
561            nextAlarmView.setVisibility(View.VISIBLE);
562        } else  {
563            nextAlarmView.setVisibility(View.GONE);
564        }
565    }
566
567    /** Clock views can call this to refresh their date. **/
568    public static void updateDate(
569            String dateFormat, String dateFormatForAccessibility, View clock) {
570
571        Date now = new Date();
572        TextView dateDisplay;
573        dateDisplay = (TextView) clock.findViewById(R.id.date);
574        if (dateDisplay != null) {
575            final Locale l = Locale.getDefault();
576            dateDisplay.setText(isJBMR2OrLater()
577                    ? new SimpleDateFormat(
578                            DateFormat.getBestDateTimePattern(l, dateFormat), l).format(now)
579                    : SimpleDateFormat.getDateInstance().format(now));
580            dateDisplay.setVisibility(View.VISIBLE);
581            dateDisplay.setContentDescription(isJBMR2OrLater()
582                    ? new SimpleDateFormat(
583                    DateFormat.getBestDateTimePattern(l, dateFormatForAccessibility), l)
584                    .format(now)
585                    : SimpleDateFormat.getDateInstance(java.text.DateFormat.FULL).format(now));
586        }
587    }
588
589    /***
590     * Formats the time in the TextClock according to the Locale with a special
591     * formatting treatment for the am/pm label.
592     * @param context - Context used to get user's locale and time preferences
593     * @param clock - TextClock to format
594     * @param amPmFontSize - size of the am/pm label since it is usually smaller
595     */
596    public static void setTimeFormat(Context context, TextClock clock, int amPmFontSize) {
597        if (clock != null) {
598            // Get the best format for 12 hours mode according to the locale
599            clock.setFormat12Hour(get12ModeFormat(context, amPmFontSize));
600            // Get the best format for 24 hours mode according to the locale
601            clock.setFormat24Hour(get24ModeFormat());
602        }
603    }
604    /***
605     * @param context - context used to get time format string resource
606     * @param amPmFontSize - size of am/pm label (label removed is size is 0).
607     * @return format string for 12 hours mode time
608     */
609    public static CharSequence get12ModeFormat(Context context, int amPmFontSize) {
610        String pattern = isJBMR2OrLater()
611                ? DateFormat.getBestDateTimePattern(Locale.getDefault(), "hma")
612                : context.getString(R.string.time_format_12_mode);
613
614        // Remove the am/pm
615        if (amPmFontSize <= 0) {
616            pattern.replaceAll("a", "").trim();
617        }
618        // Replace spaces with "Hair Space"
619        pattern = pattern.replaceAll(" ", "\u200A");
620        // Build a spannable so that the am/pm will be formatted
621        int amPmPos = pattern.indexOf('a');
622        if (amPmPos == -1) {
623            return pattern;
624        }
625        Spannable sp = new SpannableString(pattern);
626        sp.setSpan(new StyleSpan(Typeface.NORMAL), amPmPos, amPmPos + 1,
627                Spannable.SPAN_POINT_MARK);
628        sp.setSpan(new AbsoluteSizeSpan(amPmFontSize), amPmPos, amPmPos + 1,
629                Spannable.SPAN_POINT_MARK);
630        sp.setSpan(new TypefaceSpan("sans-serif"), amPmPos, amPmPos + 1,
631                Spannable.SPAN_POINT_MARK);
632        return sp;
633    }
634
635    public static CharSequence get24ModeFormat() {
636        return isJBMR2OrLater()
637                ? DateFormat.getBestDateTimePattern(Locale.getDefault(), "Hm")
638                : (new SimpleDateFormat("k:mm", Locale.getDefault())).toLocalizedPattern();
639    }
640
641    public static CityObj[] loadCitiesFromXml(Context c) {
642        Resources r = c.getResources();
643        // Read strings array of name,timezone, id
644        // make sure the list are the same length
645        String[] cityNames = r.getStringArray(R.array.cities_names);
646        String[] timezones = r.getStringArray(R.array.cities_tz);
647        String[] ids = r.getStringArray(R.array.cities_id);
648        int minLength = cityNames.length;
649        if (cityNames.length != timezones.length || ids.length != cityNames.length) {
650            minLength = Math.min(cityNames.length, Math.min(timezones.length, ids.length));
651            LogUtils.e("City lists sizes are not the same, truncating");
652        }
653        CityObj[] cities = new CityObj[minLength];
654        for (int i = 0; i < cities.length; i++) {
655            // Default to using the first character of the city name as the index unless one is
656            // specified. The indicator for a specified index is the addition of character(s)
657            // before the "=" separator.
658            final String parseString = cityNames[i];
659            final int separatorIndex = parseString.indexOf("=");
660            final String index;
661            final String cityName;
662            if (parseString.length() <= 1 && separatorIndex >= 0) {
663                LogUtils.w("Cannot parse city name %s; skipping", parseString);
664                continue;
665            }
666            if (separatorIndex == 0) {
667                // Default to using second character (the first character after the = separator)
668                // as the index.
669                index = parseString.substring(1, 2);
670                cityName = parseString.substring(1);
671            } else if (separatorIndex == -1) {
672                // Default to using the first character as the index
673                index = parseString.substring(0, 1);
674                cityName = parseString;
675                LogUtils.e("Missing expected separator character =");
676            } else {
677                 index = parseString.substring(0, separatorIndex);
678                 cityName = parseString.substring(separatorIndex + 1);
679            }
680            cities[i] = new CityObj(cityName, timezones[i], ids[i], index);
681        }
682        return cities;
683    }
684    // Returns a map of cities where the key is lowercase
685    public static Map<String, CityObj> loadCityMapFromXml(Context c) {
686        CityObj[] cities = loadCitiesFromXml(c);
687
688        final Map<String, CityObj> map = new HashMap<>(cities.length);
689        for (CityObj city : cities) {
690            map.put(city.mCityName.toLowerCase(), city);
691        }
692        return map;
693    }
694
695    /**
696     * Returns string denoting the timezone hour offset (e.g. GMT -8:00)
697     * @param useShortForm Whether to return a short form of the header that rounds to the
698     *                     nearest hour and excludes the "GMT" prefix
699     */
700    public static String getGMTHourOffset(TimeZone timezone, boolean useShortForm) {
701        final int gmtOffset = timezone.getRawOffset();
702        final long hour = gmtOffset / DateUtils.HOUR_IN_MILLIS;
703        final long min = (Math.abs(gmtOffset) % DateUtils.HOUR_IN_MILLIS) /
704                DateUtils.MINUTE_IN_MILLIS;
705
706        if (useShortForm) {
707            return String.format("%+d", hour);
708        } else {
709            return String.format("GMT %+d:%02d", hour, min);
710        }
711    }
712
713    public static String getCityName(CityObj city, CityObj dbCity) {
714        return (city.mCityId == null || dbCity == null) ? city.mCityName : dbCity.mCityName;
715    }
716
717    /**
718     * Convenience method for retrieving a themed color value.
719     *
720     * @param context  the {@link Context} to resolve the theme attribute against
721     * @param attr     the attribute corresponding to the color to resolve
722     * @param defValue the default color value to use if the attribute cannot be resolved
723     * @return the color value of the resolve attribute
724     */
725    public static int obtainStyledColor(Context context, int attr, int defValue) {
726        TEMP_ARRAY[0] = attr;
727        final TypedArray a = context.obtainStyledAttributes(TEMP_ARRAY);
728        try {
729            return a.getColor(0, defValue);
730        } finally {
731            a.recycle();
732        }
733    }
734
735    /**
736     * Returns the background color to use based on the current time.
737     */
738    public static int getCurrentHourColor() {
739        return BACKGROUND_SPECTRUM[Calendar.getInstance().get(Calendar.HOUR_OF_DAY)];
740    }
741
742    /**
743     * @param firstDay is the result from getZeroIndexedFirstDayOfWeek
744     * @return Single-char version of day name, e.g.: 'S', 'M', 'T', 'W', 'T', 'F', 'S'
745     */
746    public static String getShortWeekday(int position, int firstDay) {
747        generateShortAndLongWeekdaysIfNeeded();
748        return sShortWeekdays[(position + firstDay) % DaysOfWeek.DAYS_IN_A_WEEK];
749    }
750
751    /**
752     * @param firstDay is the result from getZeroIndexedFirstDayOfWeek
753     * @return Long-version of day name, e.g.: 'Sunday', 'Monday', 'Tuesday', etc
754     */
755    public static String getLongWeekday(int position, int firstDay) {
756        generateShortAndLongWeekdaysIfNeeded();
757        return sLongWeekdays[(position + firstDay) % DaysOfWeek.DAYS_IN_A_WEEK];
758    }
759
760    // Return the first day of the week value corresponding to Calendar.<WEEKDAY> value, which is
761    // 1-indexed starting with Sunday.
762    public static int getFirstDayOfWeek(Context context) {
763        return Integer.parseInt(PreferenceManager
764                .getDefaultSharedPreferences(context)
765                .getString(SettingsActivity.KEY_WEEK_START, String.valueOf(DEFAULT_WEEK_START)));
766    }
767
768    // Return the first day of the week value corresponding to a week with Sunday at 0 index.
769    public static int getZeroIndexedFirstDayOfWeek(Context context) {
770        return getFirstDayOfWeek(context) - 1;
771    }
772
773    private static boolean localeHasChanged() {
774        return sLocaleUsedForWeekdays != Locale.getDefault();
775    }
776
777    /**
778     * Generate arrays of short and long weekdays, starting from Sunday
779     */
780    private static void generateShortAndLongWeekdaysIfNeeded() {
781        if (sShortWeekdays != null && sLongWeekdays != null && !localeHasChanged()) {
782            // nothing to do
783            return;
784        }
785        if (sShortWeekdays == null) {
786            sShortWeekdays = new String[DaysOfWeek.DAYS_IN_A_WEEK];
787        }
788        if (sLongWeekdays == null) {
789            sLongWeekdays = new String[DaysOfWeek.DAYS_IN_A_WEEK];
790        }
791
792        final SimpleDateFormat shortFormat = new SimpleDateFormat(DATE_FORMAT_SHORT);
793        final SimpleDateFormat longFormat = new SimpleDateFormat(DATE_FORMAT_LONG);
794
795        // Create a date (2014/07/20) that is a Sunday
796        final long aSunday = new GregorianCalendar(2014, Calendar.JULY, 20).getTimeInMillis();
797
798        for (int i = 0; i < DaysOfWeek.DAYS_IN_A_WEEK; i++) {
799            final long dayMillis = aSunday + i * DateUtils.DAY_IN_MILLIS;
800            sShortWeekdays[i] = shortFormat.format(new Date(dayMillis));
801            sLongWeekdays[i] = longFormat.format(new Date(dayMillis));
802        }
803
804        // Track the Locale used to generate these weekdays
805        sLocaleUsedForWeekdays = Locale.getDefault();
806    }
807
808    /**
809     * @param context
810     * @param id Resource id of the plural
811     * @param quantity integer value
812     * @return string with properly localized numbers
813     */
814    public static String getNumberFormattedQuantityString(Context context, int id, int quantity) {
815        final String localizedQuantity = NumberFormat.getInstance().format(quantity);
816        return context.getResources().getQuantityString(id, quantity, localizedQuantity);
817    }
818}
819