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.Handler; 37 import android.os.SystemClock; 38 import android.preference.PreferenceManager; 39 import android.provider.Settings; 40 import android.text.TextUtils; 41 import android.text.format.DateFormat; 42 import android.view.MenuItem; 43 import android.view.View; 44 import android.view.animation.AccelerateInterpolator; 45 import android.view.animation.DecelerateInterpolator; 46 import android.widget.TextView; 47 48 import com.android.deskclock.stopwatch.Stopwatches; 49 import com.android.deskclock.timer.Timers; 50 import com.android.deskclock.worldclock.CityObj; 51 52 import java.text.Collator; 53 import java.text.SimpleDateFormat; 54 import java.util.Arrays; 55 import java.util.Calendar; 56 import java.util.Comparator; 57 import java.util.Date; 58 import java.util.Locale; 59 60 61 public class Utils { 62 private final static String TAG = Utils.class.getName(); 63 64 private final static String PARAM_LANGUAGE_CODE = "hl"; 65 66 /** 67 * Help URL query parameter key for the app version. 68 */ 69 private final static String PARAM_VERSION = "version"; 70 71 /** 72 * Cached version code to prevent repeated calls to the package manager. 73 */ 74 private static String sCachedVersionCode = null; 75 76 /** 77 * Intent to be used for checking if a clock's date has changed. Must be every fifteen 78 * minutes because not all time zones are hour-locked. 79 **/ 80 public static final String ACTION_ON_QUARTER_HOUR = "com.android.deskclock.ON_QUARTER_HOUR"; 81 82 /** Types that may be used for clock displays. **/ 83 public static final String CLOCK_TYPE_DIGITAL = "digital"; 84 public static final String CLOCK_TYPE_ANALOG = "analog"; 85 86 /** 87 * time format constants 88 */ 89 public final static String HOURS_24 = "kk"; 90 public final static String HOURS = "h"; 91 public final static String MINUTES = ":mm"; 92 93 94 public static void prepareHelpMenuItem(Context context, MenuItem helpMenuItem) { 95 String helpUrlString = context.getResources().getString(R.string.desk_clock_help_url); 96 if (TextUtils.isEmpty(helpUrlString)) { 97 // The help url string is empty or null, so set the help menu item to be invisible. 98 helpMenuItem.setVisible(false); 99 return; 100 } 101 // The help url string exists, so first add in some extra query parameters. 87 102 final Uri fullUri = uriWithAddedParameters(context, Uri.parse(helpUrlString)); 103 104 // Then, create an intent that will be fired when the user 105 // selects this help menu item. 106 Intent intent = new Intent(Intent.ACTION_VIEW, fullUri); 107 intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK 108 | Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS); 109 110 // Set the intent to the help menu item, show the help menu item in the overflow 111 // menu, and make it visible. 112 helpMenuItem.setIntent(intent); 113 helpMenuItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER); 114 helpMenuItem.setVisible(true); 115 } 116 117 /** 118 * Adds two query parameters into the Uri, namely the language code and the version code 119 * of the app's package as gotten via the context. 120 * @return the uri with added query parameters 121 */ 122 private static Uri uriWithAddedParameters(Context context, Uri baseUri) { 123 Uri.Builder builder = baseUri.buildUpon(); 124 125 // Add in the preferred language 126 builder.appendQueryParameter(PARAM_LANGUAGE_CODE, Locale.getDefault().toString()); 127 128 // Add in the package version code 129 if (sCachedVersionCode == null) { 130 // There is no cached version code, so try to get it from the package manager. 131 try { 132 // cache the version code 133 PackageInfo info = context.getPackageManager().getPackageInfo( 134 context.getPackageName(), 0); 135 sCachedVersionCode = Integer.toString(info.versionCode); 136 137 // append the version code to the uri 138 builder.appendQueryParameter(PARAM_VERSION, sCachedVersionCode); 139 } catch (NameNotFoundException e) { 140 // Cannot find the package name, so don't add in the version parameter 141 // This shouldn't happen. 142 Log.wtf("Invalid package name for context " + e); 143 } 144 } else { 145 builder.appendQueryParameter(PARAM_VERSION, sCachedVersionCode); 146 } 147 148 // Build the full uri and return it 149 return builder.build(); 150 } 151 152 public static long getTimeNow() { 153 return SystemClock.elapsedRealtime(); 154 } 155 156 /** 157 * Calculate the amount by which the radius of a CircleTimerView should be offset by the any 158 * of the extra painted objects. 159 */ 160 public static float calculateRadiusOffset( 161 float strokeSize, float diamondStrokeSize, float markerStrokeSize) { 162 return Math.max(strokeSize, Math.max(diamondStrokeSize, markerStrokeSize)); 163 } 164 165 /** The pressed color used throughout the app. If this method is changed, it will not have 166 * any effect on the button press states, and those must be changed separately. 167 **/ 168 public static int getPressedColorId() { 169 return R.color.clock_red; 170 } 171 172 /** The un-pressed color used throughout the app. If this method is changed, it will not have 173 * any effect on the button press states, and those must be changed separately. 174 **/ 175 public static int getGrayColorId() { 176 return R.color.clock_gray; 177 } 178 179 /** 180 * Clears the persistent data of stopwatch (start time, state, laps, etc...). 181 */ 182 public static void clearSwSharedPref(SharedPreferences prefs) { 183 SharedPreferences.Editor editor = prefs.edit(); 184 editor.remove (Stopwatches.PREF_START_TIME); 185 editor.remove (Stopwatches.PREF_ACCUM_TIME); 186 editor.remove (Stopwatches.PREF_STATE); 187 int lapNum = prefs.getInt(Stopwatches.PREF_LAP_NUM, Stopwatches.STOPWATCH_RESET); 188 for (int i = 0; i < lapNum; i++) { 189 String key = Stopwatches.PREF_LAP_TIME + Integer.toString(i); 190 editor.remove(key); 191 } 192 editor.remove(Stopwatches.PREF_LAP_NUM); 193 editor.apply(); 194 } 195 196 /** 197 * Broadcast a message to show the in-use timers in the notifications 198 */ 199 public static void showInUseNotifications(Context context) { 200 Intent timerIntent = new Intent(); 201 timerIntent.setAction(Timers.NOTIF_IN_USE_SHOW); 202 context.sendBroadcast(timerIntent); 203 } 204 205 /** Runnable for use with screensaver and dream, to move the clock every minute. 206 * registerViews() must be called prior to posting. 207 */ 208 public static class ScreensaverMoveSaverRunnable implements Runnable { 209 static final long MOVE_DELAY = 60000; // DeskClock.SCREEN_SAVER_MOVE_DELAY; 210 static final long SLIDE_TIME = 10000; 211 static final long FADE_TIME = 3000; 212 213 static final boolean SLIDE = false; 214 215 private View mContentView, mSaverView; 216 private final Handler mHandler; 217 218 private static TimeInterpolator mSlowStartWithBrakes; 219 220 221 public ScreensaverMoveSaverRunnable(Handler handler) { 222 mHandler = handler; 223 mSlowStartWithBrakes = new TimeInterpolator() { 224 @Override 225 public float getInterpolation(float x) { 226 return (float)(Math.cos((Math.pow(x,3) + 1) * Math.PI) / 2.0f) + 0.5f; 227 } 228 }; 229 } 230 231 public void registerViews(View contentView, View saverView) { 232 mContentView = contentView; 233 mSaverView = saverView; 234 } 235 236 @Override 237 public void run() { 238 long delay = MOVE_DELAY; 239 if (mContentView == null || mSaverView == null) { 240 mHandler.removeCallbacks(this); 241 mHandler.postDelayed(this, delay); 242 return; 243 } 244 245 final float xrange = mContentView.getWidth() - mSaverView.getWidth(); 246 final float yrange = mContentView.getHeight() - mSaverView.getHeight(); 247 Log.v("xrange: "+xrange+" yrange: "+yrange); 248 249 if (xrange == 0 && yrange == 0) { 250 delay = 500; // back in a split second 251 } else { 252 final int nextx = (int) (Math.random() * xrange); 253 final int nexty = (int) (Math.random() * yrange); 254 255 if (mSaverView.getAlpha() == 0f) { 256 // jump right there 257 mSaverView.setX(nextx); 258 mSaverView.setY(nexty); 259 ObjectAnimator.ofFloat(mSaverView, "alpha", 0f, 1f) 260 .setDuration(FADE_TIME) 261 .start(); 262 } else { 263 AnimatorSet s = new AnimatorSet(); 264 Animator xMove = ObjectAnimator.ofFloat(mSaverView, 265 "x", mSaverView.getX(), nextx); 266 Animator yMove = ObjectAnimator.ofFloat(mSaverView, 267 "y", mSaverView.getY(), nexty); 268 269 Animator xShrink = ObjectAnimator.ofFloat(mSaverView, "scaleX", 1f, 0.85f); 270 Animator xGrow = ObjectAnimator.ofFloat(mSaverView, "scaleX", 0.85f, 1f); 271 272 Animator yShrink = ObjectAnimator.ofFloat(mSaverView, "scaleY", 1f, 0.85f); 273 Animator yGrow = ObjectAnimator.ofFloat(mSaverView, "scaleY", 0.85f, 1f); 274 AnimatorSet shrink = new AnimatorSet(); shrink.play(xShrink).with(yShrink); 275 AnimatorSet grow = new AnimatorSet(); grow.play(xGrow).with(yGrow); 276 277 Animator fadeout = ObjectAnimator.ofFloat(mSaverView, "alpha", 1f, 0f); 278 Animator fadein = ObjectAnimator.ofFloat(mSaverView, "alpha", 0f, 1f); 279 280 281 if (SLIDE) { 282 s.play(xMove).with(yMove); 283 s.setDuration(SLIDE_TIME); 284 285 s.play(shrink.setDuration(SLIDE_TIME/2)); 286 s.play(grow.setDuration(SLIDE_TIME/2)).after(shrink); 287 s.setInterpolator(mSlowStartWithBrakes); 288 } else { 289 AccelerateInterpolator accel = new AccelerateInterpolator(); 290 DecelerateInterpolator decel = new DecelerateInterpolator(); 291 292 shrink.setDuration(FADE_TIME).setInterpolator(accel); 293 fadeout.setDuration(FADE_TIME).setInterpolator(accel); 294 grow.setDuration(FADE_TIME).setInterpolator(decel); 295 fadein.setDuration(FADE_TIME).setInterpolator(decel); 296 s.play(shrink); 297 s.play(fadeout); 298 s.play(xMove.setDuration(0)).after(FADE_TIME); 299 s.play(yMove.setDuration(0)).after(FADE_TIME); 300 s.play(fadein).after(FADE_TIME); 301 s.play(grow).after(FADE_TIME); 302 } 303 s.start(); 304 } 305 306 long now = System.currentTimeMillis(); 307 long adjust = (now % 60000); 308 delay = delay 309 + (MOVE_DELAY - adjust) // minute aligned 310 - (SLIDE ? 0 : FADE_TIME) // start moving before the fade 311 ; 312 } 313 314 mHandler.removeCallbacks(this); 315 mHandler.postDelayed(this, delay); 316 } 317 } 318 319 /** Setup to find out when the quarter-hour changes (e.g. Kathmandu is GMT+5:45) **/ 320 private static long getAlarmOnQuarterHour() { 321 Calendar nextQuarter = Calendar.getInstance(); 322 // Set 1 second to ensure quarter-hour threshold passed. 323 nextQuarter.set(Calendar.SECOND, 1); 324 int minute = nextQuarter.get(Calendar.MINUTE); 325 nextQuarter.add(Calendar.MINUTE, 15 - (minute % 15)); 326 long alarmOnQuarterHour = nextQuarter.getTimeInMillis(); 327 if (0 >= (alarmOnQuarterHour - System.currentTimeMillis()) 328 || (alarmOnQuarterHour - System.currentTimeMillis()) > 901000) { 329 Log.wtf("quarterly alarm calculation error"); 330 } 331 return alarmOnQuarterHour; 332 } 333 334 /** Setup alarm refresh when the quarter-hour changes **/ 335 public static PendingIntent startAlarmOnQuarterHour(Context context) { 336 if (context != null) { 337 PendingIntent quarterlyIntent = PendingIntent.getBroadcast( 338 context, 0, new Intent(Utils.ACTION_ON_QUARTER_HOUR), 0); 339 ((AlarmManager) context.getSystemService(Context.ALARM_SERVICE)).setRepeating( 340 AlarmManager.RTC, getAlarmOnQuarterHour(), 341 AlarmManager.INTERVAL_FIFTEEN_MINUTES, quarterlyIntent); 342 return quarterlyIntent; 343 } else { 344 return null; 345 } 346 } 347 348 public static void cancelAlarmOnQuarterHour(Context context, PendingIntent quarterlyIntent) { 349 if (quarterlyIntent != null && context != null) { 350 ((AlarmManager) context.getSystemService(Context.ALARM_SERVICE)).cancel( 351 quarterlyIntent); 352 } 353 } 354 355 public static PendingIntent refreshAlarmOnQuarterHour( 356 Context context, PendingIntent quarterlyIntent) { 357 cancelAlarmOnQuarterHour(context, quarterlyIntent); 358 return startAlarmOnQuarterHour(context); 359 } 360 361 /** 362 * For screensavers to set whether the digital or analog clock should be displayed. 363 * Returns the view to be displayed. 364 */ 365 public static View setClockStyle(Context context, View digitalClock, View analogClock, 366 String clockStyleKey) { 367 SharedPreferences sharedPref = PreferenceManager.getDefaultSharedPreferences(context); 368 String defaultClockStyle = context.getResources().getString(R.string.default_clock_style); 369 String style = sharedPref.getString(clockStyleKey, defaultClockStyle); 370 View returnView; 371 if (style.equals(CLOCK_TYPE_ANALOG)) { 372 digitalClock.setVisibility(View.GONE); 373 analogClock.setVisibility(View.VISIBLE); 374 returnView = analogClock; 375 } else { 376 digitalClock.setVisibility(View.VISIBLE); 377 analogClock.setVisibility(View.GONE); 378 returnView = digitalClock; 379 } 380 381 return returnView; 382 } 383 384 /** 385 * For screensavers to dim the lights if necessary. 386 */ 387 public static void dimClockView(boolean dim, View clockView) { 388 Paint paint = new Paint(); 389 paint.setColor(Color.WHITE); 390 paint.setColorFilter(new PorterDuffColorFilter( 391 (dim ? 0x60FFFFFF : 0xC0FFFFFF), 392 PorterDuff.Mode.MULTIPLY)); 393 clockView.setLayerType(View.LAYER_TYPE_HARDWARE, paint); 394 } 395 396 /** Clock views can call this to refresh their alarm to the next upcoming value. **/ 397 public static void refreshAlarm(Context context, View clock) { 398 String nextAlarm = Settings.System.getString(context.getContentResolver(), 399 Settings.System.NEXT_ALARM_FORMATTED); 400 TextView nextAlarmView; 401 nextAlarmView = (TextView) clock.findViewById(R.id.nextAlarm); 402 if (!TextUtils.isEmpty(nextAlarm) && nextAlarmView != null) { 403 nextAlarmView.setText( 404 context.getString(R.string.control_set_alarm_with_existing, nextAlarm)); 405 nextAlarmView.setContentDescription(context.getResources().getString( 406 R.string.next_alarm_description, nextAlarm)); 407 nextAlarmView.setVisibility(View.VISIBLE); 408 } else { 409 nextAlarmView.setVisibility(View.GONE); 410 } 411 } 412 413 /** Clock views can call this to refresh their date. **/ 414 public static void updateDate( 415 String dateFormat, String dateFormatForAccessibility, View clock) { 416 417 Date now = new Date(); 418 TextView dateDisplay; 419 dateDisplay = (TextView) clock.findViewById(R.id.date); 420 if (dateDisplay != null) { 421 final Locale l = Locale.getDefault(); 422 String fmt = DateFormat.getBestDateTimePattern(l, dateFormat); 423 SimpleDateFormat sdf = new SimpleDateFormat(fmt, l); 424 dateDisplay.setText(sdf.format(now)); 425 dateDisplay.setVisibility(View.VISIBLE); 426 fmt = DateFormat.getBestDateTimePattern(l, dateFormatForAccessibility); 427 sdf = new SimpleDateFormat(fmt, l); 428 dateDisplay.setContentDescription(sdf.format(now)); 429 } 430 } 431 432 public static CityObj[] loadCitiesDataBase(Context c) { 433 final Collator collator = Collator.getInstance(); 434 Resources r = c.getResources(); 435 // Read strings array of name,timezone, id 436 // make sure the list are the same length 437 String[] cities = r.getStringArray(R.array.cities_names); 438 String[] timezones = r.getStringArray(R.array.cities_tz); 439 String[] ids = r.getStringArray(R.array.cities_id); 440 if (cities.length != timezones.length || ids.length != cities.length) { 441 Log.wtf("City lists sizes are not the same, cannot use the data"); 442 return null; 443 } 444 CityObj[] tempList = new CityObj[cities.length]; 445 for (int i = 0; i < cities.length; i++) { 446 tempList[i] = new CityObj(cities[i], timezones[i], ids[i]); 447 } 448 // Sort alphabetically 449 Arrays.sort(tempList, new Comparator<CityObj> () { 450 @Override 451 public int compare(CityObj c1, CityObj c2) { 452 Comparator<CityObj> mCollator; 453 return collator.compare(c1.mCityName, c2.mCityName); 454 } 455 }); 456 return tempList; 457 } 458 459 public static String getCityName(CityObj city, CityObj dbCity) { 460 return (city.mCityId == null || dbCity == null) ? city.mCityName : dbCity.mCityName; 461 } 462 } 463