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.app.PendingIntent; 25 import android.content.Context; 26 import android.content.Intent; 27 import android.content.SharedPreferences; 28 import android.content.pm.PackageInfo; 29 import android.content.pm.PackageManager.NameNotFoundException; 30 import android.content.res.Resources; 31 import android.graphics.Color; 32 import android.graphics.Paint; 33 import android.graphics.PorterDuff; 34 import android.graphics.PorterDuffColorFilter; 35 import android.net.Uri; 36 import android.os.Build; 37 import android.os.Handler; 38 import android.os.SystemClock; 39 import android.preference.PreferenceManager; 40 import android.provider.Settings; 41 import android.text.Spannable; 42 import android.text.SpannableString; 43 import android.text.TextUtils; 44 import android.text.format.DateFormat; 45 import android.text.format.DateUtils; 46 import android.text.format.Time; 47 import android.text.style.AbsoluteSizeSpan; 48 import android.text.style.StyleSpan; 49 import android.text.style.TypefaceSpan; 50 import android.view.MenuItem; 51 import android.view.View; 52 import android.view.animation.AccelerateInterpolator; 53 import android.view.animation.DecelerateInterpolator; 54 import android.widget.TextClock; 55 import android.widget.TextView; 56 57 import com.android.deskclock.stopwatch.Stopwatches; 58 import com.android.deskclock.timer.Timers; 59 import com.android.deskclock.worldclock.CityObj; 60 61 import java.text.SimpleDateFormat; 62 import java.util.Calendar; 63 import java.util.Date; 64 import java.util.Locale; 65 import java.util.TimeZone; 66 67 68 public 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 nextQuarter.set(Calendar.MILLISECOND, 0); 356 int minute = nextQuarter.get(Calendar.MINUTE); 357 nextQuarter.add(Calendar.MINUTE, 15 - (minute % 15)); 358 long alarmOnQuarterHour = nextQuarter.getTimeInMillis(); 359 long now = System.currentTimeMillis(); 360 long delta = alarmOnQuarterHour - now; 361 if (0 >= delta || delta > 901000) { 362 // Something went wrong in the calculation, schedule something that is 363 // about 15 minutes. Next time , it will align with the 15 minutes border. 364 alarmOnQuarterHour = now + 901000; 365 } 366 return alarmOnQuarterHour; 367 } 368 369 // Setup a thread that starts at midnight plus one second. The extra second is added to ensure 370 // the date has changed. 371 public static void setMidnightUpdater(Handler handler, Runnable runnable) { 372 String timezone = TimeZone.getDefault().getID(); 373 if (handler == null || runnable == null || timezone == null) { 374 return; 375 } 376 long now = System.currentTimeMillis(); 377 Time time = new Time(timezone); 378 time.set(now); 379 long runInMillis = ((24 - time.hour) * 3600 - time.minute * 60 - time.second + 1) * 1000; 380 handler.removeCallbacks(runnable); 381 handler.postDelayed(runnable, runInMillis); 382 } 383 384 // Stop the midnight update thread 385 public static void cancelMidnightUpdater(Handler handler, Runnable runnable) { 386 if (handler == null || runnable == null) { 387 return; 388 } 389 handler.removeCallbacks(runnable); 390 } 391 392 // Setup a thread that starts at the quarter-hour plus one second. The extra second is added to 393 // ensure dates have changed. 394 public static void setQuarterHourUpdater(Handler handler, Runnable runnable) { 395 String timezone = TimeZone.getDefault().getID(); 396 if (handler == null || runnable == null || timezone == null) { 397 return; 398 } 399 long runInMillis = getAlarmOnQuarterHour() - System.currentTimeMillis(); 400 // Ensure the delay is at least one second. 401 if (runInMillis < 1000) { 402 runInMillis = 1000; 403 } 404 handler.removeCallbacks(runnable); 405 handler.postDelayed(runnable, runInMillis); 406 } 407 408 // Stop the quarter-hour update thread 409 public static void cancelQuarterHourUpdater(Handler handler, Runnable runnable) { 410 if (handler == null || runnable == null) { 411 return; 412 } 413 handler.removeCallbacks(runnable); 414 } 415 416 /** 417 * For screensavers to set whether the digital or analog clock should be displayed. 418 * Returns the view to be displayed. 419 */ 420 public static View setClockStyle(Context context, View digitalClock, View analogClock, 421 String clockStyleKey) { 422 SharedPreferences sharedPref = PreferenceManager.getDefaultSharedPreferences(context); 423 String defaultClockStyle = context.getResources().getString(R.string.default_clock_style); 424 String style = sharedPref.getString(clockStyleKey, defaultClockStyle); 425 View returnView; 426 if (style.equals(CLOCK_TYPE_ANALOG)) { 427 digitalClock.setVisibility(View.GONE); 428 analogClock.setVisibility(View.VISIBLE); 429 returnView = analogClock; 430 } else { 431 digitalClock.setVisibility(View.VISIBLE); 432 analogClock.setVisibility(View.GONE); 433 returnView = digitalClock; 434 } 435 436 return returnView; 437 } 438 439 /** 440 * For screensavers to dim the lights if necessary. 441 */ 442 public static void dimClockView(boolean dim, View clockView) { 443 Paint paint = new Paint(); 444 paint.setColor(Color.WHITE); 445 paint.setColorFilter(new PorterDuffColorFilter( 446 (dim ? 0x40FFFFFF : 0xC0FFFFFF), 447 PorterDuff.Mode.MULTIPLY)); 448 clockView.setLayerType(View.LAYER_TYPE_HARDWARE, paint); 449 } 450 451 /** Clock views can call this to refresh their alarm to the next upcoming value. **/ 452 public static void refreshAlarm(Context context, View clock) { 453 String nextAlarm = Settings.System.getString(context.getContentResolver(), 454 Settings.System.NEXT_ALARM_FORMATTED); 455 TextView nextAlarmView; 456 nextAlarmView = (TextView) clock.findViewById(R.id.nextAlarm); 457 if (!TextUtils.isEmpty(nextAlarm) && nextAlarmView != null) { 458 nextAlarmView.setText( 459 context.getString(R.string.control_set_alarm_with_existing, nextAlarm)); 460 nextAlarmView.setContentDescription(context.getResources().getString( 461 R.string.next_alarm_description, nextAlarm)); 462 nextAlarmView.setVisibility(View.VISIBLE); 463 } else { 464 nextAlarmView.setVisibility(View.GONE); 465 } 466 } 467 468 /** Clock views can call this to refresh their date. **/ 469 public static void updateDate( 470 String dateFormat, String dateFormatForAccessibility, View clock) { 471 472 Date now = new Date(); 473 TextView dateDisplay; 474 dateDisplay = (TextView) clock.findViewById(R.id.date); 475 if (dateDisplay != null) { 476 final Locale l = Locale.getDefault(); 477 String fmt = DateFormat.getBestDateTimePattern(l, dateFormat); 478 SimpleDateFormat sdf = new SimpleDateFormat(fmt, l); 479 dateDisplay.setText(sdf.format(now)); 480 dateDisplay.setVisibility(View.VISIBLE); 481 fmt = DateFormat.getBestDateTimePattern(l, dateFormatForAccessibility); 482 sdf = new SimpleDateFormat(fmt, l); 483 dateDisplay.setContentDescription(sdf.format(now)); 484 } 485 } 486 487 /*** 488 * Formats the time in the TextClock according to the Locale with a special 489 * formatting treatment for the am/pm label. 490 * @param clock - TextClock to format 491 * @param amPmFontSize - size of the am/pm label since it is usually smaller 492 * than the clock time size. 493 */ 494 public static void setTimeFormat(TextClock clock, int amPmFontSize) { 495 if (clock != null) { 496 // Get the best format for 12 hours mode according to the locale 497 clock.setFormat12Hour(get12ModeFormat(amPmFontSize)); 498 // Get the best format for 24 hours mode according to the locale 499 clock.setFormat24Hour(get24ModeFormat()); 500 } 501 } 502 /*** 503 * @param amPmFontSize - size of am/pm label (label removed is size is 0). 504 * @return format string for 12 hours mode time 505 */ 506 public static CharSequence get12ModeFormat(int amPmFontSize) { 507 String skeleton = "hma"; 508 String pattern = DateFormat.getBestDateTimePattern(Locale.getDefault(), skeleton); 509 // Remove the am/pm 510 if (amPmFontSize <= 0) { 511 pattern.replaceAll("a", "").trim(); 512 } 513 // Replace spaces with "Hair Space" 514 pattern = pattern.replaceAll(" ", "\u200A"); 515 // Build a spannable so that the am/pm will be formatted 516 int amPmPos = pattern.indexOf('a'); 517 if (amPmPos == -1) { 518 return pattern; 519 } 520 Spannable sp = new SpannableString(pattern); 521 sp.setSpan(new StyleSpan(android.graphics.Typeface.BOLD), amPmPos, amPmPos + 1, 522 Spannable.SPAN_POINT_MARK); 523 sp.setSpan(new AbsoluteSizeSpan(amPmFontSize), amPmPos, amPmPos + 1, 524 Spannable.SPAN_POINT_MARK); 525 sp.setSpan(new TypefaceSpan("sans-serif-condensed"), amPmPos, amPmPos + 1, 526 Spannable.SPAN_POINT_MARK); 527 return sp; 528 } 529 530 public static CharSequence get24ModeFormat() { 531 String skeleton = "Hm"; 532 return DateFormat.getBestDateTimePattern(Locale.getDefault(), skeleton); 533 } 534 535 public static CityObj[] loadCitiesFromXml(Context c) { 536 Resources r = c.getResources(); 537 // Read strings array of name,timezone, id 538 // make sure the list are the same length 539 String[] cities = r.getStringArray(R.array.cities_names); 540 String[] timezones = r.getStringArray(R.array.cities_tz); 541 String[] ids = r.getStringArray(R.array.cities_id); 542 int minLength = cities.length; 543 if (cities.length != timezones.length || ids.length != cities.length) { 544 minLength = Math.min(cities.length, Math.min(timezones.length, ids.length)); 545 Log.e("City lists sizes are not the same, trancating"); 546 } 547 CityObj[] tempList = new CityObj[minLength]; 548 for (int i = 0; i < cities.length; i++) { 549 tempList[i] = new CityObj(cities[i], timezones[i], ids[i]); 550 } 551 return tempList; 552 } 553 554 /** 555 * Returns string denoting the timezone hour offset (e.g. GMT-8:00) 556 */ 557 public static String getGMTHourOffset(TimeZone timezone, boolean showMinutes) { 558 StringBuilder sb = new StringBuilder(); 559 sb.append("GMT"); 560 int gmtOffset = timezone.getRawOffset(); 561 if (gmtOffset < 0) { 562 sb.append('-'); 563 } else { 564 sb.append('+'); 565 } 566 sb.append(Math.abs(gmtOffset) / DateUtils.HOUR_IN_MILLIS); // Hour 567 568 if (showMinutes) { 569 final int min = (Math.abs(gmtOffset) / (int) DateUtils.MINUTE_IN_MILLIS) % 60; 570 sb.append(':'); 571 if (min < 10) { 572 sb.append('0'); 573 } 574 sb.append(min); 575 } 576 577 return sb.toString(); 578 } 579 580 public static String getCityName(CityObj city, CityObj dbCity) { 581 return (city.mCityId == null || dbCity == null) ? city.mCityName : dbCity.mCityName; 582 } 583 } 584