Utils.java revision f9c17a244f7ac320808b45cf4d9d4bbe8c5343c0
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.app.PendingIntent; 25import android.content.Context; 26import android.content.Intent; 27import android.content.SharedPreferences; 28import android.content.pm.PackageInfo; 29import android.content.pm.PackageManager.NameNotFoundException; 30import android.content.res.Resources; 31import android.graphics.Color; 32import android.graphics.Paint; 33import android.graphics.PorterDuff; 34import android.graphics.PorterDuffColorFilter; 35import android.net.Uri; 36import android.os.Build; 37import android.os.Handler; 38import android.os.SystemClock; 39import android.preference.PreferenceManager; 40import android.provider.Settings; 41import android.text.Spannable; 42import android.text.SpannableString; 43import android.text.TextUtils; 44import android.text.format.DateFormat; 45import android.text.format.DateUtils; 46import android.text.format.Time; 47import android.text.style.AbsoluteSizeSpan; 48import android.text.style.StyleSpan; 49import android.text.style.TypefaceSpan; 50import android.view.MenuItem; 51import android.view.View; 52import android.view.animation.AccelerateInterpolator; 53import android.view.animation.DecelerateInterpolator; 54import android.widget.TextClock; 55import android.widget.TextView; 56 57import com.android.deskclock.stopwatch.Stopwatches; 58import com.android.deskclock.timer.Timers; 59import com.android.deskclock.worldclock.CityObj; 60 61import java.text.SimpleDateFormat; 62import java.util.Calendar; 63import java.util.Date; 64import java.util.Locale; 65import java.util.TimeZone; 66 67 68public class Utils { 69 private final static String PARAM_LANGUAGE_CODE = "hl"; 70 71 /** 72 * Help URL query parameter key for the app version. 73 */ 74 private final static String PARAM_VERSION = "version"; 75 76 /** 77 * Cached version code to prevent repeated calls to the package manager. 78 */ 79 private static String sCachedVersionCode = null; 80 81 /** Types that may be used for clock displays. **/ 82 public static final String CLOCK_TYPE_DIGITAL = "digital"; 83 public static final String CLOCK_TYPE_ANALOG = "analog"; 84 85 /** 86 * Returns whether the SDK is KitKat or later 87 */ 88 public static boolean isKitKatOrLater() { 89 return Build.VERSION.SDK_INT > Build.VERSION_CODES.JELLY_BEAN_MR2; 90 } 91 92 93 public static void prepareHelpMenuItem(Context context, MenuItem helpMenuItem) { 94 String helpUrlString = context.getResources().getString(R.string.desk_clock_help_url); 95 if (TextUtils.isEmpty(helpUrlString)) { 96 // The help url string is empty or null, so set the help menu item to be invisible. 97 helpMenuItem.setVisible(false); 98 return; 99 } 100 // The help url string exists, so first add in some extra query parameters. 87 101 final Uri fullUri = uriWithAddedParameters(context, Uri.parse(helpUrlString)); 102 103 // Then, create an intent that will be fired when the user 104 // selects this help menu item. 105 Intent intent = new Intent(Intent.ACTION_VIEW, fullUri); 106 intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK 107 | Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS); 108 109 // Set the intent to the help menu item, show the help menu item in the overflow 110 // menu, and make it visible. 111 helpMenuItem.setIntent(intent); 112 helpMenuItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER); 113 helpMenuItem.setVisible(true); 114 } 115 116 /** 117 * Adds two query parameters into the Uri, namely the language code and the version code 118 * of the application's package as gotten via the context. 119 * @return the uri with added query parameters 120 */ 121 private static Uri uriWithAddedParameters(Context context, Uri baseUri) { 122 Uri.Builder builder = baseUri.buildUpon(); 123 124 // Add in the preferred language 125 builder.appendQueryParameter(PARAM_LANGUAGE_CODE, Locale.getDefault().toString()); 126 127 // Add in the package version code 128 if (sCachedVersionCode == null) { 129 // There is no cached version code, so try to get it from the package manager. 130 try { 131 // cache the version code 132 PackageInfo info = context.getPackageManager().getPackageInfo( 133 context.getPackageName(), 0); 134 sCachedVersionCode = Integer.toString(info.versionCode); 135 136 // append the version code to the uri 137 builder.appendQueryParameter(PARAM_VERSION, sCachedVersionCode); 138 } catch (NameNotFoundException e) { 139 // Cannot find the package name, so don't add in the version parameter 140 // This shouldn't happen. 141 Log.wtf("Invalid package name for context " + e); 142 } 143 } else { 144 builder.appendQueryParameter(PARAM_VERSION, sCachedVersionCode); 145 } 146 147 // Build the full uri and return it 148 return builder.build(); 149 } 150 151 public static long getTimeNow() { 152 return SystemClock.elapsedRealtime(); 153 } 154 155 /** 156 * Calculate the amount by which the radius of a CircleTimerView should be offset by the any 157 * of the extra painted objects. 158 */ 159 public static float calculateRadiusOffset( 160 float strokeSize, float dotStrokeSize, float markerStrokeSize) { 161 return Math.max(strokeSize, Math.max(dotStrokeSize, markerStrokeSize)); 162 } 163 164 /** 165 * Uses {@link Utils#calculateRadiusOffset(float, float, float)} after fetching the values 166 * from the resources just as {@link CircleTimerView#init(android.content.Context)} does. 167 */ 168 public static float calculateRadiusOffset(Resources resources) { 169 if (resources != null) { 170 float strokeSize = resources.getDimension(R.dimen.circletimer_circle_size); 171 float dotStrokeSize = resources.getDimension(R.dimen.circletimer_dot_size); 172 float markerStrokeSize = resources.getDimension(R.dimen.circletimer_marker_size); 173 return calculateRadiusOffset(strokeSize, dotStrokeSize, markerStrokeSize); 174 } else { 175 return 0f; 176 } 177 } 178 179 /** The pressed color used throughout the app. If this method is changed, it will not have 180 * any effect on the button press states, and those must be changed separately. 181 **/ 182 public static int getPressedColorId() { 183 return R.color.clock_red; 184 } 185 186 /** The un-pressed color used throughout the app. If this method is changed, it will not have 187 * any effect on the button press states, and those must be changed separately. 188 **/ 189 public static int getGrayColorId() { 190 return R.color.clock_gray; 191 } 192 193 /** 194 * Clears the persistent data of stopwatch (start time, state, laps, etc...). 195 */ 196 public static void clearSwSharedPref(SharedPreferences prefs) { 197 SharedPreferences.Editor editor = prefs.edit(); 198 editor.remove (Stopwatches.PREF_START_TIME); 199 editor.remove (Stopwatches.PREF_ACCUM_TIME); 200 editor.remove (Stopwatches.PREF_STATE); 201 int lapNum = prefs.getInt(Stopwatches.PREF_LAP_NUM, Stopwatches.STOPWATCH_RESET); 202 for (int i = 0; i < lapNum; i++) { 203 String key = Stopwatches.PREF_LAP_TIME + Integer.toString(i); 204 editor.remove(key); 205 } 206 editor.remove(Stopwatches.PREF_LAP_NUM); 207 editor.apply(); 208 } 209 210 /** 211 * Broadcast a message to show the in-use timers in the notifications 212 */ 213 public static void showInUseNotifications(Context context) { 214 Intent timerIntent = new Intent(); 215 timerIntent.setAction(Timers.NOTIF_IN_USE_SHOW); 216 context.sendBroadcast(timerIntent); 217 } 218 219 /** 220 * Broadcast a message to show the in-use timers in the notifications 221 */ 222 public static void showTimesUpNotifications(Context context) { 223 Intent timerIntent = new Intent(); 224 timerIntent.setAction(Timers.NOTIF_TIMES_UP_SHOW); 225 context.sendBroadcast(timerIntent); 226 } 227 228 /** 229 * Broadcast a message to cancel the in-use timers in the notifications 230 */ 231 public static void cancelTimesUpNotifications(Context context) { 232 Intent timerIntent = new Intent(); 233 timerIntent.setAction(Timers.NOTIF_TIMES_UP_CANCEL); 234 context.sendBroadcast(timerIntent); 235 } 236 237 /** Runnable for use with screensaver and dream, to move the clock every minute. 238 * registerViews() must be called prior to posting. 239 */ 240 public static class ScreensaverMoveSaverRunnable implements Runnable { 241 static final long MOVE_DELAY = 60000; // DeskClock.SCREEN_SAVER_MOVE_DELAY; 242 static final long SLIDE_TIME = 10000; 243 static final long FADE_TIME = 3000; 244 245 static final boolean SLIDE = false; 246 247 private View mContentView, mSaverView; 248 private final Handler mHandler; 249 250 private static TimeInterpolator mSlowStartWithBrakes; 251 252 253 public ScreensaverMoveSaverRunnable(Handler handler) { 254 mHandler = handler; 255 mSlowStartWithBrakes = new TimeInterpolator() { 256 @Override 257 public float getInterpolation(float x) { 258 return (float)(Math.cos((Math.pow(x,3) + 1) * Math.PI) / 2.0f) + 0.5f; 259 } 260 }; 261 } 262 263 public void registerViews(View contentView, View saverView) { 264 mContentView = contentView; 265 mSaverView = saverView; 266 } 267 268 @Override 269 public void run() { 270 long delay = MOVE_DELAY; 271 if (mContentView == null || mSaverView == null) { 272 mHandler.removeCallbacks(this); 273 mHandler.postDelayed(this, delay); 274 return; 275 } 276 277 final float xrange = mContentView.getWidth() - mSaverView.getWidth(); 278 final float yrange = mContentView.getHeight() - mSaverView.getHeight(); 279 280 if (xrange == 0 && yrange == 0) { 281 delay = 500; // back in a split second 282 } else { 283 final int nextx = (int) (Math.random() * xrange); 284 final int nexty = (int) (Math.random() * yrange); 285 286 if (mSaverView.getAlpha() == 0f) { 287 // jump right there 288 mSaverView.setX(nextx); 289 mSaverView.setY(nexty); 290 ObjectAnimator.ofFloat(mSaverView, "alpha", 0f, 1f) 291 .setDuration(FADE_TIME) 292 .start(); 293 } else { 294 AnimatorSet s = new AnimatorSet(); 295 Animator xMove = ObjectAnimator.ofFloat(mSaverView, 296 "x", mSaverView.getX(), nextx); 297 Animator yMove = ObjectAnimator.ofFloat(mSaverView, 298 "y", mSaverView.getY(), nexty); 299 300 Animator xShrink = ObjectAnimator.ofFloat(mSaverView, "scaleX", 1f, 0.85f); 301 Animator xGrow = ObjectAnimator.ofFloat(mSaverView, "scaleX", 0.85f, 1f); 302 303 Animator yShrink = ObjectAnimator.ofFloat(mSaverView, "scaleY", 1f, 0.85f); 304 Animator yGrow = ObjectAnimator.ofFloat(mSaverView, "scaleY", 0.85f, 1f); 305 AnimatorSet shrink = new AnimatorSet(); shrink.play(xShrink).with(yShrink); 306 AnimatorSet grow = new AnimatorSet(); grow.play(xGrow).with(yGrow); 307 308 Animator fadeout = ObjectAnimator.ofFloat(mSaverView, "alpha", 1f, 0f); 309 Animator fadein = ObjectAnimator.ofFloat(mSaverView, "alpha", 0f, 1f); 310 311 312 if (SLIDE) { 313 s.play(xMove).with(yMove); 314 s.setDuration(SLIDE_TIME); 315 316 s.play(shrink.setDuration(SLIDE_TIME/2)); 317 s.play(grow.setDuration(SLIDE_TIME/2)).after(shrink); 318 s.setInterpolator(mSlowStartWithBrakes); 319 } else { 320 AccelerateInterpolator accel = new AccelerateInterpolator(); 321 DecelerateInterpolator decel = new DecelerateInterpolator(); 322 323 shrink.setDuration(FADE_TIME).setInterpolator(accel); 324 fadeout.setDuration(FADE_TIME).setInterpolator(accel); 325 grow.setDuration(FADE_TIME).setInterpolator(decel); 326 fadein.setDuration(FADE_TIME).setInterpolator(decel); 327 s.play(shrink); 328 s.play(fadeout); 329 s.play(xMove.setDuration(0)).after(FADE_TIME); 330 s.play(yMove.setDuration(0)).after(FADE_TIME); 331 s.play(fadein).after(FADE_TIME); 332 s.play(grow).after(FADE_TIME); 333 } 334 s.start(); 335 } 336 337 long now = System.currentTimeMillis(); 338 long adjust = (now % 60000); 339 delay = delay 340 + (MOVE_DELAY - adjust) // minute aligned 341 - (SLIDE ? 0 : FADE_TIME) // start moving before the fade 342 ; 343 } 344 345 mHandler.removeCallbacks(this); 346 mHandler.postDelayed(this, delay); 347 } 348 } 349 350 /** Setup to find out when the quarter-hour changes (e.g. Kathmandu is GMT+5:45) **/ 351 public static long getAlarmOnQuarterHour() { 352 Calendar nextQuarter = Calendar.getInstance(); 353 // Set 1 second to ensure quarter-hour threshold passed. 354 nextQuarter.set(Calendar.SECOND, 1); 355 int minute = nextQuarter.get(Calendar.MINUTE); 356 nextQuarter.add(Calendar.MINUTE, 15 - (minute % 15)); 357 long alarmOnQuarterHour = nextQuarter.getTimeInMillis(); 358 if (0 >= (alarmOnQuarterHour - System.currentTimeMillis()) 359 || (alarmOnQuarterHour - System.currentTimeMillis()) > 901000) { 360 Log.wtf("quarterly alarm calculation error"); 361 } 362 Log.v("getAlarmOnQuarterHour returns " // STOPSHIP Don't ship with this log 363 + new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(nextQuarter.getTime()) 364 + " to fire in " + (alarmOnQuarterHour - System.currentTimeMillis())); 365 return alarmOnQuarterHour; 366 } 367 368 // Setup a thread that starts at midnight plus one second. The extra second is added to ensure 369 // the date has changed. 370 public static void setMidnightUpdater(Handler handler, Runnable runnable) { 371 String timezone = TimeZone.getDefault().getID(); 372 if (handler == null || runnable == null || timezone == null) { 373 return; 374 } 375 long now = System.currentTimeMillis(); 376 Time time = new Time(timezone); 377 time.set(now); 378 long runInMillis = ((24 - time.hour) * 3600 - time.minute * 60 - time.second + 1) * 1000; 379 handler.removeCallbacks(runnable); 380 handler.postDelayed(runnable, runInMillis); 381 } 382 383 // Stop the midnight update thread 384 public static void cancelMidnightUpdater(Handler handler, Runnable runnable) { 385 if (handler == null || runnable == null) { 386 return; 387 } 388 handler.removeCallbacks(runnable); 389 } 390 391 // Setup a thread that starts at the quarter-hour plus one second. The extra second is added to 392 // ensure dates have changed. 393 public static void setQuarterHourUpdater(Handler handler, Runnable runnable) { 394 String timezone = TimeZone.getDefault().getID(); 395 if (handler == null || runnable == null || timezone == null) { 396 return; 397 } 398 long runInMillis = getAlarmOnQuarterHour() - System.currentTimeMillis(); 399 // Ensure the delay is at least one second. 400 if (runInMillis < 1000) { 401 runInMillis = 1000; 402 } 403 handler.removeCallbacks(runnable); 404 handler.postDelayed(runnable, runInMillis); 405 } 406 407 // Stop the quarter-hour update thread 408 public static void cancelQuarterHourUpdater(Handler handler, Runnable runnable) { 409 if (handler == null || runnable == null) { 410 return; 411 } 412 handler.removeCallbacks(runnable); 413 } 414 415 /** 416 * For screensavers to set whether the digital or analog clock should be displayed. 417 * Returns the view to be displayed. 418 */ 419 public static View setClockStyle(Context context, View digitalClock, View analogClock, 420 String clockStyleKey) { 421 SharedPreferences sharedPref = PreferenceManager.getDefaultSharedPreferences(context); 422 String defaultClockStyle = context.getResources().getString(R.string.default_clock_style); 423 String style = sharedPref.getString(clockStyleKey, defaultClockStyle); 424 View returnView; 425 if (style.equals(CLOCK_TYPE_ANALOG)) { 426 digitalClock.setVisibility(View.GONE); 427 analogClock.setVisibility(View.VISIBLE); 428 returnView = analogClock; 429 } else { 430 digitalClock.setVisibility(View.VISIBLE); 431 analogClock.setVisibility(View.GONE); 432 returnView = digitalClock; 433 } 434 435 return returnView; 436 } 437 438 /** 439 * For screensavers to dim the lights if necessary. 440 */ 441 public static void dimClockView(boolean dim, View clockView) { 442 Paint paint = new Paint(); 443 paint.setColor(Color.WHITE); 444 paint.setColorFilter(new PorterDuffColorFilter( 445 (dim ? 0x40FFFFFF : 0xC0FFFFFF), 446 PorterDuff.Mode.MULTIPLY)); 447 clockView.setLayerType(View.LAYER_TYPE_HARDWARE, paint); 448 } 449 450 /** Clock views can call this to refresh their alarm to the next upcoming value. **/ 451 public static void refreshAlarm(Context context, View clock) { 452 String nextAlarm = Settings.System.getString(context.getContentResolver(), 453 Settings.System.NEXT_ALARM_FORMATTED); 454 TextView nextAlarmView; 455 nextAlarmView = (TextView) clock.findViewById(R.id.nextAlarm); 456 if (!TextUtils.isEmpty(nextAlarm) && nextAlarmView != null) { 457 nextAlarmView.setText( 458 context.getString(R.string.control_set_alarm_with_existing, nextAlarm)); 459 nextAlarmView.setContentDescription(context.getResources().getString( 460 R.string.next_alarm_description, nextAlarm)); 461 nextAlarmView.setVisibility(View.VISIBLE); 462 } else { 463 nextAlarmView.setVisibility(View.GONE); 464 } 465 } 466 467 /** Clock views can call this to refresh their date. **/ 468 public static void updateDate( 469 String dateFormat, String dateFormatForAccessibility, View clock) { 470 471 Date now = new Date(); 472 TextView dateDisplay; 473 dateDisplay = (TextView) clock.findViewById(R.id.date); 474 if (dateDisplay != null) { 475 final Locale l = Locale.getDefault(); 476 String fmt = DateFormat.getBestDateTimePattern(l, dateFormat); 477 SimpleDateFormat sdf = new SimpleDateFormat(fmt, l); 478 dateDisplay.setText(sdf.format(now)); 479 dateDisplay.setVisibility(View.VISIBLE); 480 fmt = DateFormat.getBestDateTimePattern(l, dateFormatForAccessibility); 481 sdf = new SimpleDateFormat(fmt, l); 482 dateDisplay.setContentDescription(sdf.format(now)); 483 } 484 } 485 486 /*** 487 * Formats the time in the TextClock according to the Locale with a special 488 * formatting treatment for the am/pm label. 489 * @param clock - TextClock to format 490 * @param amPmFontSize - size of the am/pm label since it is usually smaller 491 * than the clock time size. 492 */ 493 public static void setTimeFormat(TextClock clock, int amPmFontSize) { 494 if (clock != null) { 495 // Get the best format for 12 hours mode according to the locale 496 clock.setFormat12Hour(get12ModeFormat(amPmFontSize)); 497 // Get the best format for 24 hours mode according to the locale 498 clock.setFormat24Hour(get24ModeFormat()); 499 } 500 } 501 /*** 502 * @param amPmFontSize - size of am/pm label (label removed is size is 0). 503 * @return format string for 12 hours mode time 504 */ 505 public static CharSequence get12ModeFormat(int amPmFontSize) { 506 String skeleton = (amPmFontSize > 0) ? "hma" : "hm"; 507 String pattern = DateFormat.getBestDateTimePattern(Locale.getDefault(), skeleton); 508 // Replace spaces with "Hair Space" 509 pattern = pattern.replaceAll(" ", "\u200A"); 510 // Build a spannable so that the am/pm will be formatted 511 int amPmPos = pattern.indexOf('a'); 512 if (amPmPos == -1) { 513 return pattern; 514 } 515 Spannable sp = new SpannableString(pattern); 516 sp.setSpan(new StyleSpan(android.graphics.Typeface.BOLD), amPmPos, amPmPos + 1, 517 Spannable.SPAN_POINT_MARK); 518 sp.setSpan(new AbsoluteSizeSpan(amPmFontSize), amPmPos, amPmPos + 1, 519 Spannable.SPAN_POINT_MARK); 520 sp.setSpan(new TypefaceSpan("sans-serif-condensed"), amPmPos, amPmPos + 1, 521 Spannable.SPAN_POINT_MARK); 522 return sp; 523 } 524 525 public static CharSequence get24ModeFormat() { 526 String skeleton = "Hm"; 527 return DateFormat.getBestDateTimePattern(Locale.getDefault(), skeleton); 528 } 529 530 public static CityObj[] loadCitiesFromXml(Context c) { 531 Resources r = c.getResources(); 532 // Read strings array of name,timezone, id 533 // make sure the list are the same length 534 String[] cities = r.getStringArray(R.array.cities_names); 535 String[] timezones = r.getStringArray(R.array.cities_tz); 536 String[] ids = r.getStringArray(R.array.cities_id); 537 int minLength = cities.length; 538 if (cities.length != timezones.length || ids.length != cities.length) { 539 // StopShip: Make sure to remove this after we get transations for K 540 minLength = Math.min(cities.length, Math.min(timezones.length, ids.length)); 541 Log.e("City lists sizes are not the same, trancating"); 542 // return null; 543 } 544 CityObj[] tempList = new CityObj[minLength]; 545 for (int i = 0; i < cities.length; i++) { 546 tempList[i] = new CityObj(cities[i], timezones[i], ids[i]); 547 } 548 return tempList; 549 } 550 551 /** 552 * Returns string denoting the timezone hour offset (e.g. GMT-8:00) 553 */ 554 public static String getGMTHourOffset(TimeZone timezone, boolean showMinutes) { 555 StringBuilder sb = new StringBuilder(); 556 sb.append("GMT"); 557 int gmtOffset = timezone.getRawOffset(); 558 if (gmtOffset < 0) { 559 sb.append('-'); 560 } else { 561 sb.append('+'); 562 } 563 sb.append(Math.abs(gmtOffset) / DateUtils.HOUR_IN_MILLIS); // Hour 564 565 if (showMinutes) { 566 final int min = (Math.abs(gmtOffset) / (int) DateUtils.MINUTE_IN_MILLIS) % 60; 567 sb.append(':'); 568 if (min < 10) { 569 sb.append('0'); 570 } 571 sb.append(min); 572 } 573 574 return sb.toString(); 575 } 576 577 public static String getCityName(CityObj city, CityObj dbCity) { 578 return (city.mCityId == null || dbCity == null) ? city.mCityName : dbCity.mCityName; 579 } 580} 581