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.annotation.TargetApi; 24 import android.app.AlarmManager; 25 import android.content.ContentResolver; 26 import android.content.Context; 27 import android.content.SharedPreferences; 28 import android.content.res.Resources; 29 import android.content.res.TypedArray; 30 import android.graphics.Color; 31 import android.graphics.Paint; 32 import android.graphics.PorterDuff; 33 import android.graphics.PorterDuffColorFilter; 34 import android.graphics.Typeface; 35 import android.os.Build; 36 import android.os.Handler; 37 import android.os.Looper; 38 import android.preference.PreferenceManager; 39 import android.provider.Settings; 40 import android.support.v4.content.ContextCompat; 41 import android.support.v4.os.BuildCompat; 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.RelativeSizeSpan; 49 import android.text.style.StyleSpan; 50 import android.text.style.TypefaceSpan; 51 import android.util.ArraySet; 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.data.DataModel; 59 import com.android.deskclock.provider.AlarmInstance; 60 import com.android.deskclock.provider.DaysOfWeek; 61 import com.android.deskclock.settings.SettingsActivity; 62 63 import java.io.File; 64 import java.text.DateFormatSymbols; 65 import java.text.NumberFormat; 66 import java.text.SimpleDateFormat; 67 import java.util.Calendar; 68 import java.util.Collection; 69 import java.util.Date; 70 import java.util.GregorianCalendar; 71 import java.util.Locale; 72 import java.util.TimeZone; 73 74 public class Utils { 75 // Single-char version of day name, e.g.: 'S', 'M', 'T', 'W', 'T', 'F', 'S' 76 private static String[] sShortWeekdays = null; 77 private static final String DATE_FORMAT_SHORT = "ccccc"; 78 79 // Long-version of day name, e.g.: 'Sunday', 'Monday', 'Tuesday', etc 80 private static String[] sLongWeekdays = null; 81 private static final String DATE_FORMAT_LONG = "EEEE"; 82 83 public static final int DEFAULT_WEEK_START = Calendar.getInstance().getFirstDayOfWeek(); 84 85 private static Locale sLocaleUsedForWeekdays; 86 87 /** 88 * Temporary array used by {@link #obtainStyledColor(Context, int, int)}. 89 */ 90 private static final int[] TEMP_ARRAY = new int[1]; 91 92 /** 93 * The background colors of the app - it changes throughout out the day to mimic the sky. 94 */ 95 private static final int[] BACKGROUND_SPECTRUM = { 96 0xFF212121 /* 12 AM */, 97 0xFF20222A /* 1 AM */, 98 0xFF202233 /* 2 AM */, 99 0xFF1F2242 /* 3 AM */, 100 0xFF1E224F /* 4 AM */, 101 0xFF1D225C /* 5 AM */, 102 0xFF1B236B /* 6 AM */, 103 0xFF1A237E /* 7 AM */, 104 0xFF1D2783 /* 8 AM */, 105 0xFF232E8B /* 9 AM */, 106 0xFF283593 /* 10 AM */, 107 0xFF2C3998 /* 11 AM */, 108 0xFF303F9F /* 12 PM */, 109 0xFF2C3998 /* 1 PM */, 110 0xFF283593 /* 2 PM */, 111 0xFF232E8B /* 3 PM */, 112 0xFF1D2783 /* 4 PM */, 113 0xFF1A237E /* 5 PM */, 114 0xFF1B236B /* 6 PM */, 115 0xFF1D225C /* 7 PM */, 116 0xFF1E224F /* 8 PM */, 117 0xFF1F2242 /* 9 PM */, 118 0xFF202233 /* 10 PM */, 119 0xFF20222A /* 11 PM */ 120 }; 121 enforceMainLooper()122 public static void enforceMainLooper() { 123 if (Looper.getMainLooper() != Looper.myLooper()) { 124 throw new IllegalAccessError("May only call from main thread."); 125 } 126 } 127 enforceNotMainLooper()128 public static void enforceNotMainLooper() { 129 if (Looper.getMainLooper() == Looper.myLooper()) { 130 throw new IllegalAccessError("May not call from main thread."); 131 } 132 } 133 134 /** 135 * @return {@code true} if the device is prior to {@link Build.VERSION_CODES#LOLLIPOP} 136 */ isPreL()137 public static boolean isPreL() { 138 return Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP; 139 } 140 141 /** 142 * @return {@code true} if the device is {@link Build.VERSION_CODES#LOLLIPOP} or 143 * {@link Build.VERSION_CODES#LOLLIPOP_MR1} 144 */ isLOrLMR1()145 public static boolean isLOrLMR1() { 146 final int sdkInt = Build.VERSION.SDK_INT; 147 return sdkInt == Build.VERSION_CODES.LOLLIPOP || sdkInt == Build.VERSION_CODES.LOLLIPOP_MR1; 148 } 149 150 /** 151 * @return {@code true} if the device is {@link Build.VERSION_CODES#LOLLIPOP} or later 152 */ isLOrLater()153 public static boolean isLOrLater() { 154 return Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP; 155 } 156 157 /** 158 * @return {@code true} if the device is {@link Build.VERSION_CODES#LOLLIPOP_MR1} or later 159 */ isLMR1OrLater()160 public static boolean isLMR1OrLater() { 161 return Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP_MR1; 162 } 163 164 /** 165 * @return {@code true} if the device is {@link Build.VERSION_CODES#M} or later 166 */ isMOrLater()167 public static boolean isMOrLater() { 168 return Build.VERSION.SDK_INT >= Build.VERSION_CODES.M; 169 } 170 171 /** 172 * @return {@code true} if the device is {@link Build.VERSION_CODES#N} or later 173 */ isNOrLater()174 public static boolean isNOrLater() { 175 return BuildCompat.isAtLeastN(); 176 } 177 178 /** 179 * Calculate the amount by which the radius of a CircleTimerView should be offset by any 180 * of the extra painted objects. 181 */ calculateRadiusOffset( float strokeSize, float dotStrokeSize, float markerStrokeSize)182 public static float calculateRadiusOffset( 183 float strokeSize, float dotStrokeSize, float markerStrokeSize) { 184 return Math.max(strokeSize, Math.max(dotStrokeSize, markerStrokeSize)); 185 } 186 187 /** 188 * Uses {@link Utils#calculateRadiusOffset(float, float, float)} after fetching the values 189 * from the resources. 190 */ calculateRadiusOffset(Resources resources)191 public static float calculateRadiusOffset(Resources resources) { 192 if (resources != null) { 193 float strokeSize = resources.getDimension(R.dimen.circletimer_circle_size); 194 float dotStrokeSize = resources.getDimension(R.dimen.circletimer_dot_size); 195 float markerStrokeSize = resources.getDimension(R.dimen.circletimer_marker_size); 196 return calculateRadiusOffset(strokeSize, dotStrokeSize, markerStrokeSize); 197 } else { 198 return 0f; 199 } 200 } 201 202 /** Runnable for use with screensaver and dream, to move the clock every minute. 203 * registerViews() must be called prior to posting. 204 */ 205 public static class ScreensaverMoveSaverRunnable implements Runnable { 206 static final long MOVE_DELAY = 60000; // DeskClock.SCREEN_SAVER_MOVE_DELAY; 207 static final long SLIDE_TIME = 10000; 208 static final long FADE_TIME = 3000; 209 210 static final boolean SLIDE = false; 211 212 private View mContentView, mSaverView; 213 private final Handler mHandler; 214 215 private static TimeInterpolator mSlowStartWithBrakes; 216 217 ScreensaverMoveSaverRunnable(Handler handler)218 public ScreensaverMoveSaverRunnable(Handler handler) { 219 mHandler = handler; 220 mSlowStartWithBrakes = new TimeInterpolator() { 221 @Override 222 public float getInterpolation(float x) { 223 return (float)(Math.cos((Math.pow(x,3) + 1) * Math.PI) / 2.0f) + 0.5f; 224 } 225 }; 226 } 227 registerViews(View contentView, View saverView)228 public void registerViews(View contentView, View saverView) { 229 mContentView = contentView; 230 mSaverView = saverView; 231 } 232 233 @Override run()234 public void run() { 235 long delay = MOVE_DELAY; 236 if (mContentView == null || mSaverView == null) { 237 mHandler.removeCallbacks(this); 238 mHandler.postDelayed(this, delay); 239 return; 240 } 241 242 final float xrange = mContentView.getWidth() - mSaverView.getWidth(); 243 final float yrange = mContentView.getHeight() - mSaverView.getHeight(); 244 245 if (xrange == 0 && yrange == 0) { 246 delay = 500; // back in a split second 247 } else { 248 final int nextx = (int) (Math.random() * xrange); 249 final int nexty = (int) (Math.random() * yrange); 250 251 if (mSaverView.getAlpha() == 0f) { 252 // jump right there 253 mSaverView.setX(nextx); 254 mSaverView.setY(nexty); 255 ObjectAnimator.ofFloat(mSaverView, "alpha", 0f, 1f) 256 .setDuration(FADE_TIME) 257 .start(); 258 } else { 259 AnimatorSet s = new AnimatorSet(); 260 Animator xMove = ObjectAnimator.ofFloat(mSaverView, 261 "x", mSaverView.getX(), nextx); 262 Animator yMove = ObjectAnimator.ofFloat(mSaverView, 263 "y", mSaverView.getY(), nexty); 264 265 Animator xShrink = ObjectAnimator.ofFloat(mSaverView, "scaleX", 1f, 0.85f); 266 Animator xGrow = ObjectAnimator.ofFloat(mSaverView, "scaleX", 0.85f, 1f); 267 268 Animator yShrink = ObjectAnimator.ofFloat(mSaverView, "scaleY", 1f, 0.85f); 269 Animator yGrow = ObjectAnimator.ofFloat(mSaverView, "scaleY", 0.85f, 1f); 270 AnimatorSet shrink = new AnimatorSet(); shrink.play(xShrink).with(yShrink); 271 AnimatorSet grow = new AnimatorSet(); grow.play(xGrow).with(yGrow); 272 273 Animator fadeout = ObjectAnimator.ofFloat(mSaverView, "alpha", 1f, 0f); 274 Animator fadein = ObjectAnimator.ofFloat(mSaverView, "alpha", 0f, 1f); 275 276 277 if (SLIDE) { 278 s.play(xMove).with(yMove); 279 s.setDuration(SLIDE_TIME); 280 281 s.play(shrink.setDuration(SLIDE_TIME/2)); 282 s.play(grow.setDuration(SLIDE_TIME/2)).after(shrink); 283 s.setInterpolator(mSlowStartWithBrakes); 284 } else { 285 AccelerateInterpolator accel = new AccelerateInterpolator(); 286 DecelerateInterpolator decel = new DecelerateInterpolator(); 287 288 shrink.setDuration(FADE_TIME).setInterpolator(accel); 289 fadeout.setDuration(FADE_TIME).setInterpolator(accel); 290 grow.setDuration(FADE_TIME).setInterpolator(decel); 291 fadein.setDuration(FADE_TIME).setInterpolator(decel); 292 s.play(shrink); 293 s.play(fadeout); 294 s.play(xMove.setDuration(0)).after(FADE_TIME); 295 s.play(yMove.setDuration(0)).after(FADE_TIME); 296 s.play(fadein).after(FADE_TIME); 297 s.play(grow).after(FADE_TIME); 298 } 299 s.start(); 300 } 301 302 long now = System.currentTimeMillis(); 303 long adjust = (now % 60000); 304 delay = delay 305 + (MOVE_DELAY - adjust) // minute aligned 306 - (SLIDE ? 0 : FADE_TIME) // start moving before the fade 307 ; 308 } 309 310 mHandler.removeCallbacks(this); 311 mHandler.postDelayed(this, delay); 312 } 313 } 314 315 /** Setup to find out when the quarter-hour changes (e.g. Kathmandu is GMT+5:45) **/ getAlarmOnQuarterHour()316 public static long getAlarmOnQuarterHour() { 317 final Calendar calendarInstance = Calendar.getInstance(); 318 final long now = System.currentTimeMillis(); 319 return getAlarmOnQuarterHour(calendarInstance, now); 320 } 321 getAlarmOnQuarterHour(Calendar calendar, long now)322 static long getAlarmOnQuarterHour(Calendar calendar, long now) { 323 // Set 1 second to ensure quarter-hour threshold passed. 324 calendar.set(Calendar.SECOND, 1); 325 calendar.set(Calendar.MILLISECOND, 0); 326 int minute = calendar.get(Calendar.MINUTE); 327 calendar.add(Calendar.MINUTE, 15 - (minute % 15)); 328 long alarmOnQuarterHour = calendar.getTimeInMillis(); 329 330 // Verify that alarmOnQuarterHour is within the next 15 minutes 331 long delta = alarmOnQuarterHour - now; 332 if (0 >= delta || delta > 901000) { 333 // Something went wrong in the calculation, schedule something that is 334 // about 15 minutes. Next time , it will align with the 15 minutes border. 335 alarmOnQuarterHour = now + 901000; 336 } 337 return alarmOnQuarterHour; 338 } 339 340 // Setup a thread that starts at midnight plus one second. The extra second is added to ensure 341 // the date has changed. setMidnightUpdater(Handler handler, Runnable runnable)342 public static void setMidnightUpdater(Handler handler, Runnable runnable) { 343 String timezone = TimeZone.getDefault().getID(); 344 if (handler == null || runnable == null || timezone == null) { 345 return; 346 } 347 long now = System.currentTimeMillis(); 348 Time time = new Time(timezone); 349 time.set(now); 350 long runInMillis = ((24 - time.hour) * 3600 - time.minute * 60 - time.second + 1) * 1000; 351 handler.removeCallbacks(runnable); 352 handler.postDelayed(runnable, runInMillis); 353 } 354 355 // Stop the midnight update thread cancelMidnightUpdater(Handler handler, Runnable runnable)356 public static void cancelMidnightUpdater(Handler handler, Runnable runnable) { 357 if (handler == null || runnable == null) { 358 return; 359 } 360 handler.removeCallbacks(runnable); 361 } 362 363 // Setup a thread that starts at the quarter-hour plus one second. The extra second is added to 364 // ensure dates have changed. setQuarterHourUpdater(Handler handler, Runnable runnable)365 public static void setQuarterHourUpdater(Handler handler, Runnable runnable) { 366 String timezone = TimeZone.getDefault().getID(); 367 if (handler == null || runnable == null || timezone == null) { 368 return; 369 } 370 long runInMillis = getAlarmOnQuarterHour() - System.currentTimeMillis(); 371 // Ensure the delay is at least one second. 372 if (runInMillis < 1000) { 373 runInMillis = 1000; 374 } 375 handler.removeCallbacks(runnable); 376 handler.postDelayed(runnable, runInMillis); 377 } 378 379 // Stop the quarter-hour update thread cancelQuarterHourUpdater(Handler handler, Runnable runnable)380 public static void cancelQuarterHourUpdater(Handler handler, Runnable runnable) { 381 if (handler == null || runnable == null) { 382 return; 383 } 384 handler.removeCallbacks(runnable); 385 } 386 387 /** 388 * For screensavers to set whether the digital or analog clock should be displayed. 389 * Returns the view to be displayed. 390 */ setClockStyle(View digitalClock, View analogClock)391 public static View setClockStyle(View digitalClock, View analogClock) { 392 final DataModel.ClockStyle clockStyle = DataModel.getDataModel().getClockStyle(); 393 switch (clockStyle) { 394 case ANALOG: 395 digitalClock.setVisibility(View.GONE); 396 analogClock.setVisibility(View.VISIBLE); 397 return analogClock; 398 case DIGITAL: 399 digitalClock.setVisibility(View.VISIBLE); 400 analogClock.setVisibility(View.GONE); 401 return digitalClock; 402 } 403 404 throw new IllegalStateException("unexpected clock style: " + clockStyle); 405 } 406 407 /** 408 * For screensavers to set whether the digital or analog clock should be displayed. 409 * Returns the view to be displayed. 410 */ setScreensaverClockStyle(View digitalClock, View analogClock)411 public static View setScreensaverClockStyle(View digitalClock, View analogClock) { 412 final DataModel.ClockStyle clockStyle = DataModel.getDataModel().getScreensaverClockStyle(); 413 switch (clockStyle) { 414 case ANALOG: 415 digitalClock.setVisibility(View.GONE); 416 analogClock.setVisibility(View.VISIBLE); 417 return analogClock; 418 case DIGITAL: 419 digitalClock.setVisibility(View.VISIBLE); 420 analogClock.setVisibility(View.GONE); 421 return digitalClock; 422 } 423 424 throw new IllegalStateException("unexpected clock style: " + clockStyle); 425 } 426 427 /** 428 * For screensavers to dim the lights if necessary. 429 */ dimClockView(boolean dim, View clockView)430 public static void dimClockView(boolean dim, View clockView) { 431 Paint paint = new Paint(); 432 paint.setColor(Color.WHITE); 433 paint.setColorFilter(new PorterDuffColorFilter( 434 (dim ? 0x40FFFFFF : 0xC0FFFFFF), 435 PorterDuff.Mode.MULTIPLY)); 436 clockView.setLayerType(View.LAYER_TYPE_HARDWARE, paint); 437 } 438 439 /** 440 * @return The next alarm from {@link AlarmManager} 441 */ getNextAlarm(Context context)442 public static String getNextAlarm(Context context) { 443 return isPreL() ? getNextAlarmPreL(context) : getNextAlarmLOrLater(context); 444 } 445 446 @TargetApi(Build.VERSION_CODES.KITKAT) getNextAlarmPreL(Context context)447 private static String getNextAlarmPreL(Context context) { 448 final ContentResolver cr = context.getContentResolver(); 449 return Settings.System.getString(cr, Settings.System.NEXT_ALARM_FORMATTED); 450 } 451 452 @TargetApi(Build.VERSION_CODES.LOLLIPOP) getNextAlarmLOrLater(Context context)453 private static String getNextAlarmLOrLater(Context context) { 454 final AlarmManager am = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE); 455 final AlarmManager.AlarmClockInfo info = am.getNextAlarmClock(); 456 if (info != null) { 457 final long triggerTime = info.getTriggerTime(); 458 final Calendar alarmTime = Calendar.getInstance(); 459 alarmTime.setTimeInMillis(triggerTime); 460 return AlarmUtils.getFormattedTime(context, alarmTime); 461 } 462 463 return null; 464 } 465 isAlarmWithin24Hours(AlarmInstance alarmInstance)466 public static boolean isAlarmWithin24Hours(AlarmInstance alarmInstance) { 467 final Calendar nextAlarmTime = alarmInstance.getAlarmTime(); 468 final long nextAlarmTimeMillis = nextAlarmTime.getTimeInMillis(); 469 return nextAlarmTimeMillis - System.currentTimeMillis() <= DateUtils.DAY_IN_MILLIS; 470 } 471 472 /** Clock views can call this to refresh their alarm to the next upcoming value. */ refreshAlarm(Context context, View clock)473 public static void refreshAlarm(Context context, View clock) { 474 final TextView nextAlarmView = (TextView) clock.findViewById(R.id.nextAlarm); 475 if (nextAlarmView == null) { 476 return; 477 } 478 479 final String alarm = getNextAlarm(context); 480 if (!TextUtils.isEmpty(alarm)) { 481 final String description = context.getString(R.string.next_alarm_description, alarm); 482 nextAlarmView.setText(alarm); 483 nextAlarmView.setContentDescription(description); 484 nextAlarmView.setVisibility(View.VISIBLE); 485 } else { 486 nextAlarmView.setVisibility(View.GONE); 487 } 488 } 489 490 /** Clock views can call this to refresh their date. **/ updateDate(String dateSkeleton, String descriptionSkeleton, View clock)491 public static void updateDate(String dateSkeleton, String descriptionSkeleton, View clock) { 492 final TextView dateDisplay = (TextView) clock.findViewById(R.id.date); 493 if (dateDisplay == null) { 494 return; 495 } 496 497 final Locale l = Locale.getDefault(); 498 final String datePattern = DateFormat.getBestDateTimePattern(l, dateSkeleton); 499 final String descriptionPattern = DateFormat.getBestDateTimePattern(l, descriptionSkeleton); 500 501 final Date now = new Date(); 502 dateDisplay.setText(new SimpleDateFormat(datePattern, l).format(now)); 503 dateDisplay.setVisibility(View.VISIBLE); 504 dateDisplay.setContentDescription(new SimpleDateFormat(descriptionPattern, l).format(now)); 505 } 506 507 /*** 508 * Formats the time in the TextClock according to the Locale with a special 509 * formatting treatment for the am/pm label. 510 * @param context - Context used to get user's locale and time preferences 511 * @param clock - TextClock to format 512 */ setTimeFormat(Context context, TextClock clock)513 public static void setTimeFormat(Context context, TextClock clock) { 514 if (clock != null) { 515 // Get the best format for 12 hours mode according to the locale 516 clock.setFormat12Hour(get12ModeFormat(context, true /* showAmPm */)); 517 // Get the best format for 24 hours mode according to the locale 518 clock.setFormat24Hour(get24ModeFormat()); 519 } 520 } 521 522 /** 523 * Returns {@code true} if the am / pm strings for the current locale are long and a reduced 524 * text size should be used for displaying the digital clock. 525 */ isAmPmStringLong()526 public static boolean isAmPmStringLong() { 527 final String[] amPmStrings = new DateFormatSymbols().getAmPmStrings(); 528 for (String amPmString : amPmStrings) { 529 // Dots are small, so don't count them. 530 final int amPmStringLength = amPmString.replace(".", "").length(); 531 if (amPmStringLength > 3) { 532 return true; 533 } 534 } 535 return false; 536 } 537 538 /** 539 * @param context - context used to get time format string resource 540 * @param showAmPm - include the am/pm string if true 541 * @return format string for 12 hours mode time 542 */ get12ModeFormat(Context context, boolean showAmPm)543 public static CharSequence get12ModeFormat(Context context, boolean showAmPm) { 544 String pattern = DateFormat.getBestDateTimePattern(Locale.getDefault(), "hma"); 545 if (!showAmPm) { 546 pattern = pattern.replaceAll("a", "").trim(); 547 } 548 549 // Replace spaces with "Hair Space" 550 pattern = pattern.replaceAll(" ", "\u200A"); 551 // Build a spannable so that the am/pm will be formatted 552 int amPmPos = pattern.indexOf('a'); 553 if (amPmPos == -1) { 554 return pattern; 555 } 556 557 final Resources resources = context.getResources(); 558 final float amPmProportion = resources.getFraction(R.fraction.ampm_font_size_scale, 1, 1); 559 final Spannable sp = new SpannableString(pattern); 560 sp.setSpan(new RelativeSizeSpan(amPmProportion), amPmPos, amPmPos + 1, 561 Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); 562 sp.setSpan(new StyleSpan(Typeface.NORMAL), amPmPos, amPmPos + 1, 563 Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); 564 sp.setSpan(new TypefaceSpan("sans-serif"), amPmPos, amPmPos + 1, 565 Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); 566 567 // Make the font smaller for locales with long am/pm strings. 568 if (Utils.isAmPmStringLong()) { 569 final float proportion = resources.getFraction( 570 R.fraction.reduced_clock_font_size_scale, 1, 1); 571 sp.setSpan(new RelativeSizeSpan(proportion), 0, pattern.length(), 572 Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); 573 } 574 return sp; 575 } 576 get24ModeFormat()577 public static CharSequence get24ModeFormat() { 578 return DateFormat.getBestDateTimePattern(Locale.getDefault(), "Hm"); 579 } 580 581 /** 582 * Returns string denoting the timezone hour offset (e.g. GMT -8:00) 583 * @param useShortForm Whether to return a short form of the header that rounds to the 584 * nearest hour and excludes the "GMT" prefix 585 */ getGMTHourOffset(TimeZone timezone, boolean useShortForm)586 public static String getGMTHourOffset(TimeZone timezone, boolean useShortForm) { 587 final int gmtOffset = timezone.getRawOffset(); 588 final long hour = gmtOffset / DateUtils.HOUR_IN_MILLIS; 589 final long min = (Math.abs(gmtOffset) % DateUtils.HOUR_IN_MILLIS) / 590 DateUtils.MINUTE_IN_MILLIS; 591 592 if (useShortForm) { 593 return String.format("%+d", hour); 594 } else { 595 return String.format("GMT %+d:%02d", hour, min); 596 } 597 } 598 599 /** 600 * Convenience method for retrieving a themed color value. 601 * 602 * @param context the {@link Context} to resolve the theme attribute against 603 * @param attr the attribute corresponding to the color to resolve 604 * @param defValue the default color value to use if the attribute cannot be resolved 605 * @return the color value of the resolve attribute 606 */ obtainStyledColor(Context context, int attr, int defValue)607 public static int obtainStyledColor(Context context, int attr, int defValue) { 608 TEMP_ARRAY[0] = attr; 609 final TypedArray a = context.obtainStyledAttributes(TEMP_ARRAY); 610 try { 611 return a.getColor(0, defValue); 612 } finally { 613 a.recycle(); 614 } 615 } 616 617 /** 618 * Returns the background color to use based on the current time. 619 */ getCurrentHourColor()620 public static int getCurrentHourColor() { 621 return BACKGROUND_SPECTRUM[Calendar.getInstance().get(Calendar.HOUR_OF_DAY)]; 622 } 623 624 /** 625 * @param firstDay is the result from getZeroIndexedFirstDayOfWeek 626 * @return Single-char version of day name, e.g.: 'S', 'M', 'T', 'W', 'T', 'F', 'S' 627 */ getShortWeekday(int position, int firstDay)628 public static String getShortWeekday(int position, int firstDay) { 629 generateShortAndLongWeekdaysIfNeeded(); 630 return sShortWeekdays[(position + firstDay) % DaysOfWeek.DAYS_IN_A_WEEK]; 631 } 632 633 /** 634 * @param firstDay is the result from getZeroIndexedFirstDayOfWeek 635 * @return Long-version of day name, e.g.: 'Sunday', 'Monday', 'Tuesday', etc 636 */ getLongWeekday(int position, int firstDay)637 public static String getLongWeekday(int position, int firstDay) { 638 generateShortAndLongWeekdaysIfNeeded(); 639 return sLongWeekdays[(position + firstDay) % DaysOfWeek.DAYS_IN_A_WEEK]; 640 } 641 642 // Return the first day of the week value corresponding to Calendar.<WEEKDAY> value, which is 643 // 1-indexed starting with Sunday. getFirstDayOfWeek(Context context)644 public static int getFirstDayOfWeek(Context context) { 645 return Integer.parseInt(getDefaultSharedPreferences(context) 646 .getString(SettingsActivity.KEY_WEEK_START, String.valueOf(DEFAULT_WEEK_START))); 647 } 648 649 // Return the first day of the week value corresponding to a week with Sunday at 0 index. getZeroIndexedFirstDayOfWeek(Context context)650 public static int getZeroIndexedFirstDayOfWeek(Context context) { 651 return getFirstDayOfWeek(context) - 1; 652 } 653 localeHasChanged()654 private static boolean localeHasChanged() { 655 return sLocaleUsedForWeekdays != Locale.getDefault(); 656 } 657 658 /** 659 * Generate arrays of short and long weekdays, starting from Sunday 660 */ generateShortAndLongWeekdaysIfNeeded()661 private static void generateShortAndLongWeekdaysIfNeeded() { 662 if (sShortWeekdays != null && sLongWeekdays != null && !localeHasChanged()) { 663 // nothing to do 664 return; 665 } 666 if (sShortWeekdays == null) { 667 sShortWeekdays = new String[DaysOfWeek.DAYS_IN_A_WEEK]; 668 } 669 if (sLongWeekdays == null) { 670 sLongWeekdays = new String[DaysOfWeek.DAYS_IN_A_WEEK]; 671 } 672 673 final SimpleDateFormat shortFormat = new SimpleDateFormat(DATE_FORMAT_SHORT); 674 final SimpleDateFormat longFormat = new SimpleDateFormat(DATE_FORMAT_LONG); 675 676 // Create a date (2014/07/20) that is a Sunday 677 final long aSunday = new GregorianCalendar(2014, Calendar.JULY, 20).getTimeInMillis(); 678 679 for (int i = 0; i < DaysOfWeek.DAYS_IN_A_WEEK; i++) { 680 final long dayMillis = aSunday + i * DateUtils.DAY_IN_MILLIS; 681 sShortWeekdays[i] = shortFormat.format(new Date(dayMillis)); 682 sLongWeekdays[i] = longFormat.format(new Date(dayMillis)); 683 } 684 685 // Track the Locale used to generate these weekdays 686 sLocaleUsedForWeekdays = Locale.getDefault(); 687 } 688 689 /** 690 * @param id Resource id of the plural 691 * @param quantity integer value 692 * @return string with properly localized numbers 693 */ getNumberFormattedQuantityString(Context context, int id, int quantity)694 public static String getNumberFormattedQuantityString(Context context, int id, int quantity) { 695 final String localizedQuantity = NumberFormat.getInstance().format(quantity); 696 return context.getResources().getQuantityString(id, quantity, localizedQuantity); 697 } 698 newArraySet(Collection<E> collection)699 public static <E> ArraySet<E> newArraySet(Collection<E> collection) { 700 final ArraySet<E> arraySet = new ArraySet<>(collection.size()); 701 arraySet.addAll(collection); 702 return arraySet; 703 } 704 705 /** 706 * Return the default shared preferences. 707 */ getDefaultSharedPreferences(Context context)708 public static SharedPreferences getDefaultSharedPreferences(Context context) { 709 final Context storageContext; 710 if (isNOrLater()) { 711 // All N devices have split storage areas, but we may need to 712 // migrate existing preferences into the new device protected 713 // storage area, which is where our data lives from now on. 714 final Context deviceContext = context.createDeviceProtectedStorageContext(); 715 if (!deviceContext.moveSharedPreferencesFrom(context, 716 PreferenceManager.getDefaultSharedPreferencesName(context))) { 717 LogUtils.wtf("Failed to migrate shared preferences"); 718 } 719 storageContext = deviceContext; 720 } else { 721 storageContext = context; 722 } 723 724 return PreferenceManager.getDefaultSharedPreferences(storageContext); 725 } 726 } 727