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