• 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.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      */
isKitKatOrLater()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      */
isJBMR2OrLater()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      */
isLOrLater()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      */
isLMR1OrLater()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      */
isMOrLater()170     public static boolean isMOrLater() {
171         return Build.VERSION.SDK_INT >= Build.VERSION_CODES.M;
172     }
173 
prepareHelpMenuItem(Context context, MenuItem helpMenuItem)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      */
uriWithAddedParameters(Context context, Uri baseUri)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 
getTimeNow()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      */
calculateRadiusOffset( float strokeSize, float dotStrokeSize, float markerStrokeSize)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      */
calculateRadiusOffset(Resources resources)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      */
clearSwSharedPref(SharedPreferences prefs)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      */
showInUseNotifications(Context context)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      */
showTimesUpNotifications(Context context)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      */
cancelTimesUpNotifications(Context context)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 
ScreensaverMoveSaverRunnable(Handler handler)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 
registerViews(View contentView, View saverView)330         public void registerViews(View contentView, View saverView) {
331             mContentView = contentView;
332             mSaverView = saverView;
333         }
334 
335         @Override
run()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) **/
getAlarmOnQuarterHour()418     public static long getAlarmOnQuarterHour() {
419         final Calendar calendarInstance = Calendar.getInstance();
420         final long now = System.currentTimeMillis();
421         return getAlarmOnQuarterHour(calendarInstance, now);
422     }
423 
getAlarmOnQuarterHour(Calendar calendar, long now)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.
setMidnightUpdater(Handler handler, Runnable runnable)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
cancelMidnightUpdater(Handler handler, Runnable runnable)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.
setQuarterHourUpdater(Handler handler, Runnable runnable)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
cancelQuarterHourUpdater(Handler handler, Runnable runnable)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      */
setClockStyle(Context context, View digitalClock, View analogClock, String clockStyleKey)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      */
dimClockView(boolean dim, View clockView)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      */
getNextAlarm(Context context)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 
isAlarmWithin24Hours(AlarmInstance alarmInstance)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. **/
refreshAlarm(Context context, View clock)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. **/
updateDate( String dateFormat, String dateFormatForAccessibility, View clock)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      */
setTimeFormat(Context context, TextClock clock, int amPmFontSize)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      */
get12ModeFormat(Context context, int amPmFontSize)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 
get24ModeFormat()635     public static CharSequence get24ModeFormat() {
636         return isJBMR2OrLater()
637                 ? DateFormat.getBestDateTimePattern(Locale.getDefault(), "Hm")
638                 : (new SimpleDateFormat("k:mm", Locale.getDefault())).toLocalizedPattern();
639     }
640 
loadCitiesFromXml(Context c)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
loadCityMapFromXml(Context c)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      */
getGMTHourOffset(TimeZone timezone, boolean useShortForm)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 
getCityName(CityObj city, CityObj dbCity)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      */
obtainStyledColor(Context context, int attr, int defValue)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      */
getCurrentHourColor()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      */
getShortWeekday(int position, int firstDay)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      */
getLongWeekday(int position, int firstDay)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.
getFirstDayOfWeek(Context context)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.
getZeroIndexedFirstDayOfWeek(Context context)769     public static int getZeroIndexedFirstDayOfWeek(Context context) {
770         return getFirstDayOfWeek(context) - 1;
771     }
772 
localeHasChanged()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      */
generateShortAndLongWeekdaysIfNeeded()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      */
getNumberFormattedQuantityString(Context context, int id, int quantity)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