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 17package com.android.deskclock; 18 19import android.animation.Animator; 20import android.animation.AnimatorSet; 21import android.animation.ObjectAnimator; 22import android.animation.TimeInterpolator; 23import android.app.AlarmManager; 24import android.content.Context; 25import android.content.Intent; 26import android.content.SharedPreferences; 27import android.content.pm.PackageInfo; 28import android.content.pm.PackageManager.NameNotFoundException; 29import android.content.res.Resources; 30import android.content.res.TypedArray; 31import android.graphics.Color; 32import android.graphics.Paint; 33import android.graphics.PorterDuff; 34import android.graphics.PorterDuffColorFilter; 35import android.graphics.Typeface; 36import android.net.Uri; 37import android.os.Build; 38import android.os.Handler; 39import android.os.SystemClock; 40import android.preference.PreferenceManager; 41import android.provider.Settings; 42import android.text.Spannable; 43import android.text.SpannableString; 44import android.text.TextUtils; 45import android.text.format.DateFormat; 46import android.text.format.DateUtils; 47import android.text.format.Time; 48import android.text.style.AbsoluteSizeSpan; 49import android.text.style.StyleSpan; 50import android.text.style.TypefaceSpan; 51import android.view.MenuItem; 52import android.view.View; 53import android.view.animation.AccelerateInterpolator; 54import android.view.animation.DecelerateInterpolator; 55import android.widget.TextClock; 56import android.widget.TextView; 57 58import com.android.deskclock.provider.AlarmInstance; 59import com.android.deskclock.provider.DaysOfWeek; 60import com.android.deskclock.stopwatch.Stopwatches; 61import com.android.deskclock.timer.Timers; 62import com.android.deskclock.worldclock.CityObj; 63 64import java.text.NumberFormat; 65import java.text.SimpleDateFormat; 66import java.util.Calendar; 67import java.util.Date; 68import java.util.GregorianCalendar; 69import java.util.HashMap; 70import java.util.Locale; 71import java.util.Map; 72import java.util.TimeZone; 73 74 75public 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