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.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      */
     88     public static boolean isKitKatOrLater() {
     89         return Build.VERSION.SDK_INT > Build.VERSION_CODES.JELLY_BEAN_MR2;
     90     }
     91 
     92 
     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      */
    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 
    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      */
    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      */
    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     **/
    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     **/
    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      */
    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      */
    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      */
    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      */
    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 
    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 
    263         public void registerViews(View contentView, View saverView) {
    264             mContentView = contentView;
    265             mSaverView = saverView;
    266         }
    267 
    268         @Override
    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) **/
    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.
    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
    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.
    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
    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      */
    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      */
    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. **/
    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. **/
    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      */
    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      */
    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 
    530     public static CharSequence get24ModeFormat() {
    531         String skeleton = "Hm";
    532         return DateFormat.getBestDateTimePattern(Locale.getDefault(), skeleton);
    533     }
    534 
    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      */
    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 
    580     public static String getCityName(CityObj city, CityObj dbCity) {
    581         return (city.mCityId == null || dbCity == null) ? city.mCityName : dbCity.mCityName;
    582     }
    583 }
    584