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