Home | History | Annotate | Download | only in deskclock
      1 /*
      2  * Copyright (C) 2012 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package com.android.deskclock;
     18 
     19 import android.animation.Animator;
     20 import android.animation.AnimatorSet;
     21 import android.animation.ObjectAnimator;
     22 import android.animation.TimeInterpolator;
     23 import android.app.AlarmManager;
     24 import android.content.Context;
     25 import android.content.Intent;
     26 import android.content.SharedPreferences;
     27 import android.content.pm.PackageInfo;
     28 import android.content.pm.PackageManager.NameNotFoundException;
     29 import android.content.res.Resources;
     30 import android.graphics.Color;
     31 import android.graphics.Paint;
     32 import android.graphics.PorterDuff;
     33 import android.graphics.PorterDuffColorFilter;
     34 import android.graphics.Typeface;
     35 import android.net.Uri;
     36 import android.os.Build;
     37 import android.os.Handler;
     38 import android.os.SystemClock;
     39 import android.preference.PreferenceManager;
     40 import android.text.Spannable;
     41 import android.text.SpannableString;
     42 import android.text.TextUtils;
     43 import android.text.format.DateFormat;
     44 import android.text.format.DateUtils;
     45 import android.text.format.Time;
     46 import android.text.style.AbsoluteSizeSpan;
     47 import android.text.style.StyleSpan;
     48 import android.text.style.TypefaceSpan;
     49 import android.view.MenuItem;
     50 import android.view.View;
     51 import android.view.animation.AccelerateInterpolator;
     52 import android.view.animation.DecelerateInterpolator;
     53 import android.widget.TextClock;
     54 import android.widget.TextView;
     55 
     56 import com.android.deskclock.stopwatch.Stopwatches;
     57 import com.android.deskclock.timer.Timers;
     58 import com.android.deskclock.worldclock.CityObj;
     59 
     60 import java.text.SimpleDateFormat;
     61 import java.util.Calendar;
     62 import java.util.Date;
     63 import java.util.GregorianCalendar;
     64 import java.util.Locale;
     65 import java.util.TimeZone;
     66 
     67 
     68 public class Utils {
     69     private final static String PARAM_LANGUAGE_CODE = "hl";
     70 
     71     /**
     72      * Help URL query parameter key for the app version.
     73      */
     74     private final static String PARAM_VERSION = "version";
     75 
     76     /**
     77      * Cached version code to prevent repeated calls to the package manager.
     78      */
     79     private static String sCachedVersionCode = null;
     80 
     81     /**
     82      * Array of single-character day of week symbols {'S', 'M', 'T', 'W', 'T', 'F', 'S'}
     83      */
     84     private static String[] sShortWeekdays = null;
     85 
     86     /** Types that may be used for clock displays. **/
     87     public static final String CLOCK_TYPE_DIGITAL = "digital";
     88     public static final String CLOCK_TYPE_ANALOG = "analog";
     89 
     90     /** The background colors of the app, it changes thru out the day to mimic the sky. **/
     91     public static final String[] BACKGROUND_SPECTRUM = { "#212121", "#27232e", "#2d253a",
     92             "#332847", "#382a53", "#3e2c5f", "#442e6c", "#393a7a", "#2e4687", "#235395", "#185fa2",
     93             "#0d6baf", "#0277bd", "#0d6cb1", "#1861a6", "#23569b", "#2d4a8f", "#383f84", "#433478",
     94             "#3d3169", "#382e5b", "#322b4d", "#2c273e", "#272430" };
     95 
     96     /**
     97      * Returns whether the SDK is KitKat or later
     98      */
     99     public static boolean isKitKatOrLater() {
    100         return Build.VERSION.SDK_INT > Build.VERSION_CODES.JELLY_BEAN_MR2;
    101     }
    102 
    103 
    104     public static void prepareHelpMenuItem(Context context, MenuItem helpMenuItem) {
    105         String helpUrlString = context.getResources().getString(R.string.desk_clock_help_url);
    106         if (TextUtils.isEmpty(helpUrlString)) {
    107             // The help url string is empty or null, so set the help menu item to be invisible.
    108             helpMenuItem.setVisible(false);
    109             return;
    110         }
    111         // The help url string exists, so first add in some extra query parameters.  87
    112         final Uri fullUri = uriWithAddedParameters(context, Uri.parse(helpUrlString));
    113 
    114         // Then, create an intent that will be fired when the user
    115         // selects this help menu item.
    116         Intent intent = new Intent(Intent.ACTION_VIEW, fullUri);
    117         intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK
    118                 | Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS);
    119 
    120         // Set the intent to the help menu item, show the help menu item in the overflow
    121         // menu, and make it visible.
    122         helpMenuItem.setIntent(intent);
    123         helpMenuItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER);
    124         helpMenuItem.setVisible(true);
    125     }
    126 
    127     /**
    128      * Adds two query parameters into the Uri, namely the language code and the version code
    129      * of the application's package as gotten via the context.
    130      * @return the uri with added query parameters
    131      */
    132     private static Uri uriWithAddedParameters(Context context, Uri baseUri) {
    133         Uri.Builder builder = baseUri.buildUpon();
    134 
    135         // Add in the preferred language
    136         builder.appendQueryParameter(PARAM_LANGUAGE_CODE, Locale.getDefault().toString());
    137 
    138         // Add in the package version code
    139         if (sCachedVersionCode == null) {
    140             // There is no cached version code, so try to get it from the package manager.
    141             try {
    142                 // cache the version code
    143                 PackageInfo info = context.getPackageManager().getPackageInfo(
    144                         context.getPackageName(), 0);
    145                 sCachedVersionCode = Integer.toString(info.versionCode);
    146 
    147                 // append the version code to the uri
    148                 builder.appendQueryParameter(PARAM_VERSION, sCachedVersionCode);
    149             } catch (NameNotFoundException e) {
    150                 // Cannot find the package name, so don't add in the version parameter
    151                 // This shouldn't happen.
    152                 LogUtils.wtf("Invalid package name for context " + e);
    153             }
    154         } else {
    155             builder.appendQueryParameter(PARAM_VERSION, sCachedVersionCode);
    156         }
    157 
    158         // Build the full uri and return it
    159         return builder.build();
    160     }
    161 
    162     public static long getTimeNow() {
    163         return SystemClock.elapsedRealtime();
    164     }
    165 
    166     /**
    167      * Calculate the amount by which the radius of a CircleTimerView should be offset by the any
    168      * of the extra painted objects.
    169      */
    170     public static float calculateRadiusOffset(
    171             float strokeSize, float dotStrokeSize, float markerStrokeSize) {
    172         return Math.max(strokeSize, Math.max(dotStrokeSize, markerStrokeSize));
    173     }
    174 
    175     /**
    176      * Uses {@link Utils#calculateRadiusOffset(float, float, float)} after fetching the values
    177      * from the resources just as {@link CircleTimerView#init(android.content.Context)} does.
    178      */
    179     public static float calculateRadiusOffset(Resources resources) {
    180         if (resources != null) {
    181             float strokeSize = resources.getDimension(R.dimen.circletimer_circle_size);
    182             float dotStrokeSize = resources.getDimension(R.dimen.circletimer_dot_size);
    183             float markerStrokeSize = resources.getDimension(R.dimen.circletimer_marker_size);
    184             return calculateRadiusOffset(strokeSize, dotStrokeSize, markerStrokeSize);
    185         } else {
    186             return 0f;
    187         }
    188     }
    189 
    190     /**  The pressed color used throughout the app. If this method is changed, it will not have
    191      *   any effect on the button press states, and those must be changed separately.
    192     **/
    193     public static int getPressedColorId() {
    194         return R.color.hot_pink;
    195     }
    196 
    197     /**  The un-pressed color used throughout the app. If this method is changed, it will not have
    198      *   any effect on the button press states, and those must be changed separately.
    199     **/
    200     public static int getGrayColorId() {
    201         return R.color.clock_gray;
    202     }
    203 
    204     /**
    205      * Clears the persistent data of stopwatch (start time, state, laps, etc...).
    206      */
    207     public static void clearSwSharedPref(SharedPreferences prefs) {
    208         SharedPreferences.Editor editor = prefs.edit();
    209         editor.remove (Stopwatches.PREF_START_TIME);
    210         editor.remove (Stopwatches.PREF_ACCUM_TIME);
    211         editor.remove (Stopwatches.PREF_STATE);
    212         int lapNum = prefs.getInt(Stopwatches.PREF_LAP_NUM, Stopwatches.STOPWATCH_RESET);
    213         for (int i = 0; i < lapNum; i++) {
    214             String key = Stopwatches.PREF_LAP_TIME + Integer.toString(i);
    215             editor.remove(key);
    216         }
    217         editor.remove(Stopwatches.PREF_LAP_NUM);
    218         editor.apply();
    219     }
    220 
    221     /**
    222      * Broadcast a message to show the in-use timers in the notifications
    223      */
    224     public static void showInUseNotifications(Context context) {
    225         Intent timerIntent = new Intent();
    226         timerIntent.setAction(Timers.NOTIF_IN_USE_SHOW);
    227         context.sendBroadcast(timerIntent);
    228     }
    229 
    230     /**
    231      * Broadcast a message to show the in-use timers in the notifications
    232      */
    233     public static void showTimesUpNotifications(Context context) {
    234         Intent timerIntent = new Intent();
    235         timerIntent.setAction(Timers.NOTIF_TIMES_UP_SHOW);
    236         context.sendBroadcast(timerIntent);
    237     }
    238 
    239     /**
    240      * Broadcast a message to cancel the in-use timers in the notifications
    241      */
    242     public static void cancelTimesUpNotifications(Context context) {
    243         Intent timerIntent = new Intent();
    244         timerIntent.setAction(Timers.NOTIF_TIMES_UP_CANCEL);
    245         context.sendBroadcast(timerIntent);
    246     }
    247 
    248     /** Runnable for use with screensaver and dream, to move the clock every minute.
    249      *  registerViews() must be called prior to posting.
    250      */
    251     public static class ScreensaverMoveSaverRunnable implements Runnable {
    252         static final long MOVE_DELAY = 60000; // DeskClock.SCREEN_SAVER_MOVE_DELAY;
    253         static final long SLIDE_TIME = 10000;
    254         static final long FADE_TIME = 3000;
    255 
    256         static final boolean SLIDE = false;
    257 
    258         private View mContentView, mSaverView;
    259         private final Handler mHandler;
    260 
    261         private static TimeInterpolator mSlowStartWithBrakes;
    262 
    263 
    264         public ScreensaverMoveSaverRunnable(Handler handler) {
    265             mHandler = handler;
    266             mSlowStartWithBrakes = new TimeInterpolator() {
    267                 @Override
    268                 public float getInterpolation(float x) {
    269                     return (float)(Math.cos((Math.pow(x,3) + 1) * Math.PI) / 2.0f) + 0.5f;
    270                 }
    271             };
    272         }
    273 
    274         public void registerViews(View contentView, View saverView) {
    275             mContentView = contentView;
    276             mSaverView = saverView;
    277         }
    278 
    279         @Override
    280         public void run() {
    281             long delay = MOVE_DELAY;
    282             if (mContentView == null || mSaverView == null) {
    283                 mHandler.removeCallbacks(this);
    284                 mHandler.postDelayed(this, delay);
    285                 return;
    286             }
    287 
    288             final float xrange = mContentView.getWidth() - mSaverView.getWidth();
    289             final float yrange = mContentView.getHeight() - mSaverView.getHeight();
    290 
    291             if (xrange == 0 && yrange == 0) {
    292                 delay = 500; // back in a split second
    293             } else {
    294                 final int nextx = (int) (Math.random() * xrange);
    295                 final int nexty = (int) (Math.random() * yrange);
    296 
    297                 if (mSaverView.getAlpha() == 0f) {
    298                     // jump right there
    299                     mSaverView.setX(nextx);
    300                     mSaverView.setY(nexty);
    301                     ObjectAnimator.ofFloat(mSaverView, "alpha", 0f, 1f)
    302                         .setDuration(FADE_TIME)
    303                         .start();
    304                 } else {
    305                     AnimatorSet s = new AnimatorSet();
    306                     Animator xMove   = ObjectAnimator.ofFloat(mSaverView,
    307                                          "x", mSaverView.getX(), nextx);
    308                     Animator yMove   = ObjectAnimator.ofFloat(mSaverView,
    309                                          "y", mSaverView.getY(), nexty);
    310 
    311                     Animator xShrink = ObjectAnimator.ofFloat(mSaverView, "scaleX", 1f, 0.85f);
    312                     Animator xGrow   = ObjectAnimator.ofFloat(mSaverView, "scaleX", 0.85f, 1f);
    313 
    314                     Animator yShrink = ObjectAnimator.ofFloat(mSaverView, "scaleY", 1f, 0.85f);
    315                     Animator yGrow   = ObjectAnimator.ofFloat(mSaverView, "scaleY", 0.85f, 1f);
    316                     AnimatorSet shrink = new AnimatorSet(); shrink.play(xShrink).with(yShrink);
    317                     AnimatorSet grow = new AnimatorSet(); grow.play(xGrow).with(yGrow);
    318 
    319                     Animator fadeout = ObjectAnimator.ofFloat(mSaverView, "alpha", 1f, 0f);
    320                     Animator fadein = ObjectAnimator.ofFloat(mSaverView, "alpha", 0f, 1f);
    321 
    322 
    323                     if (SLIDE) {
    324                         s.play(xMove).with(yMove);
    325                         s.setDuration(SLIDE_TIME);
    326 
    327                         s.play(shrink.setDuration(SLIDE_TIME/2));
    328                         s.play(grow.setDuration(SLIDE_TIME/2)).after(shrink);
    329                         s.setInterpolator(mSlowStartWithBrakes);
    330                     } else {
    331                         AccelerateInterpolator accel = new AccelerateInterpolator();
    332                         DecelerateInterpolator decel = new DecelerateInterpolator();
    333 
    334                         shrink.setDuration(FADE_TIME).setInterpolator(accel);
    335                         fadeout.setDuration(FADE_TIME).setInterpolator(accel);
    336                         grow.setDuration(FADE_TIME).setInterpolator(decel);
    337                         fadein.setDuration(FADE_TIME).setInterpolator(decel);
    338                         s.play(shrink);
    339                         s.play(fadeout);
    340                         s.play(xMove.setDuration(0)).after(FADE_TIME);
    341                         s.play(yMove.setDuration(0)).after(FADE_TIME);
    342                         s.play(fadein).after(FADE_TIME);
    343                         s.play(grow).after(FADE_TIME);
    344                     }
    345                     s.start();
    346                 }
    347 
    348                 long now = System.currentTimeMillis();
    349                 long adjust = (now % 60000);
    350                 delay = delay
    351                         + (MOVE_DELAY - adjust) // minute aligned
    352                         - (SLIDE ? 0 : FADE_TIME) // start moving before the fade
    353                         ;
    354             }
    355 
    356             mHandler.removeCallbacks(this);
    357             mHandler.postDelayed(this, delay);
    358         }
    359     }
    360 
    361     /** Setup to find out when the quarter-hour changes (e.g. Kathmandu is GMT+5:45) **/
    362     public static long getAlarmOnQuarterHour() {
    363         Calendar nextQuarter = Calendar.getInstance();
    364         //  Set 1 second to ensure quarter-hour threshold passed.
    365         nextQuarter.set(Calendar.SECOND, 1);
    366         nextQuarter.set(Calendar.MILLISECOND, 0);
    367         int minute = nextQuarter.get(Calendar.MINUTE);
    368         nextQuarter.add(Calendar.MINUTE, 15 - (minute % 15));
    369         long alarmOnQuarterHour = nextQuarter.getTimeInMillis();
    370         long now = System.currentTimeMillis();
    371         long delta = alarmOnQuarterHour - now;
    372         if (0 >= delta || delta > 901000) {
    373             // Something went wrong in the calculation, schedule something that is
    374             // about 15 minutes. Next time , it will align with the 15 minutes border.
    375             alarmOnQuarterHour = now + 901000;
    376         }
    377         return alarmOnQuarterHour;
    378     }
    379 
    380     // Setup a thread that starts at midnight plus one second. The extra second is added to ensure
    381     // the date has changed.
    382     public static void setMidnightUpdater(Handler handler, Runnable runnable) {
    383         String timezone = TimeZone.getDefault().getID();
    384         if (handler == null || runnable == null || timezone == null) {
    385             return;
    386         }
    387         long now = System.currentTimeMillis();
    388         Time time = new Time(timezone);
    389         time.set(now);
    390         long runInMillis = ((24 - time.hour) * 3600 - time.minute * 60 - time.second + 1) * 1000;
    391         handler.removeCallbacks(runnable);
    392         handler.postDelayed(runnable, runInMillis);
    393     }
    394 
    395     // Stop the midnight update thread
    396     public static void cancelMidnightUpdater(Handler handler, Runnable runnable) {
    397         if (handler == null || runnable == null) {
    398             return;
    399         }
    400         handler.removeCallbacks(runnable);
    401     }
    402 
    403     // Setup a thread that starts at the quarter-hour plus one second. The extra second is added to
    404     // ensure dates have changed.
    405     public static void setQuarterHourUpdater(Handler handler, Runnable runnable) {
    406         String timezone = TimeZone.getDefault().getID();
    407         if (handler == null || runnable == null || timezone == null) {
    408             return;
    409         }
    410         long runInMillis = getAlarmOnQuarterHour() - System.currentTimeMillis();
    411         // Ensure the delay is at least one second.
    412         if (runInMillis < 1000) {
    413             runInMillis = 1000;
    414         }
    415         handler.removeCallbacks(runnable);
    416         handler.postDelayed(runnable, runInMillis);
    417     }
    418 
    419     // Stop the quarter-hour update thread
    420     public static void cancelQuarterHourUpdater(Handler handler, Runnable runnable) {
    421         if (handler == null || runnable == null) {
    422             return;
    423         }
    424         handler.removeCallbacks(runnable);
    425     }
    426 
    427     /**
    428      * For screensavers to set whether the digital or analog clock should be displayed.
    429      * Returns the view to be displayed.
    430      */
    431     public static View setClockStyle(Context context, View digitalClock, View analogClock,
    432             String clockStyleKey) {
    433         SharedPreferences sharedPref = PreferenceManager.getDefaultSharedPreferences(context);
    434         String defaultClockStyle = context.getResources().getString(R.string.default_clock_style);
    435         String style = sharedPref.getString(clockStyleKey, defaultClockStyle);
    436         View returnView;
    437         if (style.equals(CLOCK_TYPE_ANALOG)) {
    438             digitalClock.setVisibility(View.GONE);
    439             analogClock.setVisibility(View.VISIBLE);
    440             returnView = analogClock;
    441         } else {
    442             digitalClock.setVisibility(View.VISIBLE);
    443             analogClock.setVisibility(View.GONE);
    444             returnView = digitalClock;
    445         }
    446 
    447         return returnView;
    448     }
    449 
    450     /**
    451      * For screensavers to dim the lights if necessary.
    452      */
    453     public static void dimClockView(boolean dim, View clockView) {
    454         Paint paint = new Paint();
    455         paint.setColor(Color.WHITE);
    456         paint.setColorFilter(new PorterDuffColorFilter(
    457                         (dim ? 0x40FFFFFF : 0xC0FFFFFF),
    458                 PorterDuff.Mode.MULTIPLY));
    459         clockView.setLayerType(View.LAYER_TYPE_HARDWARE, paint);
    460     }
    461 
    462     /**
    463      * @return The next alarm from {@link AlarmManager}
    464      */
    465     public static String getNextAlarm(Context context) {
    466         String timeString = null;
    467         final AlarmManager.AlarmClockInfo info = ((AlarmManager) context.getSystemService(
    468                 Context.ALARM_SERVICE)).getNextAlarmClock();
    469         if (info != null) {
    470             final long triggerTime = info.getTriggerTime();
    471             final Calendar alarmTime = Calendar.getInstance();
    472             alarmTime.setTimeInMillis(triggerTime);
    473             timeString = AlarmUtils.getFormattedTime(context, alarmTime);
    474         }
    475         return timeString;
    476     }
    477 
    478     /** Clock views can call this to refresh their alarm to the next upcoming value. **/
    479     public static void refreshAlarm(Context context, View clock) {
    480         final String nextAlarm = getNextAlarm(context);
    481         TextView nextAlarmView;
    482         nextAlarmView = (TextView) clock.findViewById(R.id.nextAlarm);
    483         if (!TextUtils.isEmpty(nextAlarm) && nextAlarmView != null) {
    484             nextAlarmView.setText(
    485                     context.getString(R.string.control_set_alarm_with_existing, nextAlarm));
    486             nextAlarmView.setContentDescription(context.getResources().getString(
    487                     R.string.next_alarm_description, nextAlarm));
    488             nextAlarmView.setVisibility(View.VISIBLE);
    489         } else  {
    490             nextAlarmView.setVisibility(View.GONE);
    491         }
    492     }
    493 
    494     /** Clock views can call this to refresh their date. **/
    495     public static void updateDate(
    496             String dateFormat, String dateFormatForAccessibility, View clock) {
    497 
    498         Date now = new Date();
    499         TextView dateDisplay;
    500         dateDisplay = (TextView) clock.findViewById(R.id.date);
    501         if (dateDisplay != null) {
    502             final Locale l = Locale.getDefault();
    503             String fmt = DateFormat.getBestDateTimePattern(l, dateFormat);
    504             SimpleDateFormat sdf = new SimpleDateFormat(fmt, l);
    505             dateDisplay.setText(sdf.format(now));
    506             dateDisplay.setVisibility(View.VISIBLE);
    507             fmt = DateFormat.getBestDateTimePattern(l, dateFormatForAccessibility);
    508             sdf = new SimpleDateFormat(fmt, l);
    509             dateDisplay.setContentDescription(sdf.format(now));
    510         }
    511     }
    512 
    513     /***
    514      * Formats the time in the TextClock according to the Locale with a special
    515      * formatting treatment for the am/pm label.
    516      * @param clock - TextClock to format
    517      * @param amPmFontSize - size of the am/pm label since it is usually smaller
    518      *        than the clock time size.
    519      */
    520     public static void setTimeFormat(TextClock clock, int amPmFontSize) {
    521         if (clock != null) {
    522             // Get the best format for 12 hours mode according to the locale
    523             clock.setFormat12Hour(get12ModeFormat(amPmFontSize));
    524             // Get the best format for 24 hours mode according to the locale
    525             clock.setFormat24Hour(get24ModeFormat());
    526         }
    527     }
    528     /***
    529      * @param amPmFontSize - size of am/pm label (label removed is size is 0).
    530      * @return format string for 12 hours mode time
    531      */
    532     public static CharSequence get12ModeFormat(int amPmFontSize) {
    533         String skeleton = "hma";
    534         String pattern = DateFormat.getBestDateTimePattern(Locale.getDefault(), skeleton);
    535         // Remove the am/pm
    536         if (amPmFontSize <= 0) {
    537             pattern.replaceAll("a", "").trim();
    538         }
    539         // Replace spaces with "Hair Space"
    540         pattern = pattern.replaceAll(" ", "\u200A");
    541         // Build a spannable so that the am/pm will be formatted
    542         int amPmPos = pattern.indexOf('a');
    543         if (amPmPos == -1) {
    544             return pattern;
    545         }
    546         Spannable sp = new SpannableString(pattern);
    547         sp.setSpan(new StyleSpan(Typeface.NORMAL), amPmPos, amPmPos + 1,
    548                 Spannable.SPAN_POINT_MARK);
    549         sp.setSpan(new AbsoluteSizeSpan(amPmFontSize), amPmPos, amPmPos + 1,
    550                 Spannable.SPAN_POINT_MARK);
    551         sp.setSpan(new TypefaceSpan("sans-serif"), amPmPos, amPmPos + 1,
    552                 Spannable.SPAN_POINT_MARK);
    553         return sp;
    554     }
    555 
    556     public static CharSequence get24ModeFormat() {
    557         String skeleton = "Hm";
    558         return DateFormat.getBestDateTimePattern(Locale.getDefault(), skeleton);
    559     }
    560 
    561     public static CityObj[] loadCitiesFromXml(Context c) {
    562         Resources r = c.getResources();
    563         // Read strings array of name,timezone, id
    564         // make sure the list are the same length
    565         String[] cities = r.getStringArray(R.array.cities_names);
    566         String[] timezones = r.getStringArray(R.array.cities_tz);
    567         String[] ids = r.getStringArray(R.array.cities_id);
    568         int minLength = cities.length;
    569         if (cities.length != timezones.length || ids.length != cities.length) {
    570             minLength = Math.min(cities.length, Math.min(timezones.length, ids.length));
    571             LogUtils.e("City lists sizes are not the same, truncating");
    572         }
    573         CityObj[] tempList = new CityObj[minLength];
    574         for (int i = 0; i < cities.length; i++) {
    575             tempList[i] = new CityObj(cities[i], timezones[i], ids[i]);
    576         }
    577         return tempList;
    578     }
    579 
    580     /**
    581      * Returns string denoting the timezone hour offset (e.g. GMT-8:00)
    582      */
    583     public static String getGMTHourOffset(TimeZone timezone, boolean showMinutes) {
    584         StringBuilder sb = new StringBuilder();
    585         sb.append("GMT  ");
    586         int gmtOffset = timezone.getRawOffset();
    587         if (gmtOffset < 0) {
    588             sb.append('-');
    589         } else {
    590             sb.append('+');
    591         }
    592         sb.append(Math.abs(gmtOffset) / DateUtils.HOUR_IN_MILLIS); // Hour
    593 
    594         if (showMinutes) {
    595             final int min = (Math.abs(gmtOffset) / (int) DateUtils.MINUTE_IN_MILLIS) % 60;
    596             sb.append(':');
    597             if (min < 10) {
    598                 sb.append('0');
    599             }
    600             sb.append(min);
    601         }
    602 
    603         return sb.toString();
    604     }
    605 
    606     public static String getCityName(CityObj city, CityObj dbCity) {
    607         return (city.mCityId == null || dbCity == null) ? city.mCityName : dbCity.mCityName;
    608     }
    609 
    610     public static int getCurrentHourColor() {
    611         final int hourOfDay = Calendar.getInstance().get(Calendar.HOUR_OF_DAY);
    612         return Color.parseColor(BACKGROUND_SPECTRUM[hourOfDay]);
    613     }
    614 
    615     public static int getNextHourColor() {
    616         final int currHour = Calendar.getInstance().get(Calendar.HOUR_OF_DAY);
    617         return Color.parseColor(BACKGROUND_SPECTRUM[currHour < 24 ? currHour + 1 : 1]);
    618     }
    619 
    620     /**
    621      * To get an array of single-character day of week symbols {'S', 'M', 'T', 'W', 'T', 'F', 'S'}
    622      * @return the array of symbols
    623      */
    624     public static String[] getShortWeekdays() {
    625         if (sShortWeekdays == null) {
    626             final String[] shortWeekdays = new String[7];
    627             final SimpleDateFormat format = new SimpleDateFormat("EEEEE");
    628             // Create a date (2014/07/20) that is a Sunday
    629             long aSunday = new GregorianCalendar(2014, Calendar.JULY, 20).getTimeInMillis();
    630             for (int day = 0; day < 7; day++) {
    631                 shortWeekdays[day] = format.format(new Date(aSunday + day * DateUtils.DAY_IN_MILLIS));
    632             }
    633             sShortWeekdays = shortWeekdays;
    634         }
    635         return sShortWeekdays;
    636     }
    637 }
    638