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      */
isKitKatOrLater()99     public static boolean isKitKatOrLater() {
100         return Build.VERSION.SDK_INT > Build.VERSION_CODES.JELLY_BEAN_MR2;
101     }
102 
103 
prepareHelpMenuItem(Context context, MenuItem helpMenuItem)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      */
uriWithAddedParameters(Context context, Uri baseUri)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 
getTimeNow()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      */
calculateRadiusOffset( float strokeSize, float dotStrokeSize, float markerStrokeSize)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      */
calculateRadiusOffset(Resources resources)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     **/
getPressedColorId()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     **/
getGrayColorId()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      */
clearSwSharedPref(SharedPreferences prefs)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      */
showInUseNotifications(Context context)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      */
showTimesUpNotifications(Context context)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      */
cancelTimesUpNotifications(Context context)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 
ScreensaverMoveSaverRunnable(Handler handler)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 
registerViews(View contentView, View saverView)274         public void registerViews(View contentView, View saverView) {
275             mContentView = contentView;
276             mSaverView = saverView;
277         }
278 
279         @Override
run()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) **/
getAlarmOnQuarterHour()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.
setMidnightUpdater(Handler handler, Runnable runnable)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
cancelMidnightUpdater(Handler handler, Runnable runnable)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.
setQuarterHourUpdater(Handler handler, Runnable runnable)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
cancelQuarterHourUpdater(Handler handler, Runnable runnable)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      */
setClockStyle(Context context, View digitalClock, View analogClock, String clockStyleKey)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      */
dimClockView(boolean dim, View clockView)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      */
getNextAlarm(Context context)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. **/
refreshAlarm(Context context, View clock)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. **/
updateDate( String dateFormat, String dateFormatForAccessibility, View clock)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      */
setTimeFormat(TextClock clock, int amPmFontSize)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      */
get12ModeFormat(int amPmFontSize)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 
get24ModeFormat()556     public static CharSequence get24ModeFormat() {
557         String skeleton = "Hm";
558         return DateFormat.getBestDateTimePattern(Locale.getDefault(), skeleton);
559     }
560 
loadCitiesFromXml(Context c)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      */
getGMTHourOffset(TimeZone timezone, boolean showMinutes)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 
getCityName(CityObj city, CityObj dbCity)606     public static String getCityName(CityObj city, CityObj dbCity) {
607         return (city.mCityId == null || dbCity == null) ? city.mCityName : dbCity.mCityName;
608     }
609 
getCurrentHourColor()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 
getNextHourColor()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      */
getShortWeekdays()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