1 /* 2 * Copyright (C) 2009 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.wallpaper.polarclock; 18 19 import android.service.wallpaper.WallpaperService; 20 import android.graphics.Canvas; 21 import android.graphics.Rect; 22 import android.graphics.Paint; 23 import android.graphics.Color; 24 import android.graphics.RectF; 25 import android.view.SurfaceHolder; 26 import android.content.IntentFilter; 27 import android.content.Intent; 28 import android.content.BroadcastReceiver; 29 import android.content.Context; 30 import android.content.SharedPreferences; 31 import android.content.res.XmlResourceParser; 32 33 import android.os.Handler; 34 import android.os.SystemClock; 35 import android.text.format.Time; 36 import android.util.MathUtils; 37 import android.util.Log; 38 39 import java.util.HashMap; 40 import java.util.TimeZone; 41 import java.io.IOException; 42 43 import org.xmlpull.v1.XmlPullParserException; 44 import static org.xmlpull.v1.XmlPullParser.*; 45 46 import com.android.wallpaper.R; 47 48 public class PolarClockWallpaper extends WallpaperService { 49 private static final String LOG_TAG = "PolarClock"; 50 51 static final String SHARED_PREFS_NAME = "polar_clock_settings"; 52 53 static final String PREF_SHOW_SECONDS = "show_seconds"; 54 static final String PREF_VARIABLE_LINE_WIDTH = "variable_line_width"; 55 static final String PREF_PALETTE = "palette"; 56 57 static final int BACKGROUND_COLOR = 0xffffffff; 58 59 static abstract class ClockPalette { parseXmlPaletteTag(XmlResourceParser xrp)60 public static ClockPalette parseXmlPaletteTag(XmlResourceParser xrp) { 61 String kind = xrp.getAttributeValue(null, "kind"); 62 if ("cycling".equals(kind)) { 63 return CyclingClockPalette.parseXmlPaletteTag(xrp); 64 } else { 65 return FixedClockPalette.parseXmlPaletteTag(xrp); 66 } 67 } 68 getBackgroundColor()69 public abstract int getBackgroundColor(); 70 71 // forAngle should be on [0.0,1.0) but 1.0 must be tolerated getSecondColor(float forAngle)72 public abstract int getSecondColor(float forAngle); 73 getMinuteColor(float forAngle)74 public abstract int getMinuteColor(float forAngle); 75 getHourColor(float forAngle)76 public abstract int getHourColor(float forAngle); 77 getDayColor(float forAngle)78 public abstract int getDayColor(float forAngle); 79 getMonthColor(float forAngle)80 public abstract int getMonthColor(float forAngle); 81 getId()82 public abstract String getId(); 83 84 } 85 86 static class FixedClockPalette extends ClockPalette { 87 protected String mId; 88 protected int mBackgroundColor; 89 protected int mSecondColor; 90 protected int mMinuteColor; 91 protected int mHourColor; 92 protected int mDayColor; 93 protected int mMonthColor; 94 95 private static FixedClockPalette sFallbackPalette = null; 96 getFallback()97 public static FixedClockPalette getFallback() { 98 if (sFallbackPalette == null) { 99 sFallbackPalette = new FixedClockPalette(); 100 sFallbackPalette.mId = "default"; 101 sFallbackPalette.mBackgroundColor = Color.WHITE; 102 sFallbackPalette.mSecondColor = 103 sFallbackPalette.mMinuteColor = 104 sFallbackPalette.mHourColor = 105 sFallbackPalette.mDayColor = 106 sFallbackPalette.mMonthColor = 107 Color.BLACK; 108 } 109 return sFallbackPalette; 110 } 111 FixedClockPalette()112 private FixedClockPalette() { } 113 parseXmlPaletteTag(XmlResourceParser xrp)114 public static ClockPalette parseXmlPaletteTag(XmlResourceParser xrp) { 115 final FixedClockPalette pal = new FixedClockPalette(); 116 pal.mId = xrp.getAttributeValue(null, "id"); 117 String val; 118 if ((val = xrp.getAttributeValue(null, "background")) != null) 119 pal.mBackgroundColor = Color.parseColor(val); 120 if ((val = xrp.getAttributeValue(null, "second")) != null) 121 pal.mSecondColor = Color.parseColor(val); 122 if ((val = xrp.getAttributeValue(null, "minute")) != null) 123 pal.mMinuteColor = Color.parseColor(val); 124 if ((val = xrp.getAttributeValue(null, "hour")) != null) 125 pal.mHourColor = Color.parseColor(val); 126 if ((val = xrp.getAttributeValue(null, "day")) != null) 127 pal.mDayColor = Color.parseColor(val); 128 if ((val = xrp.getAttributeValue(null, "month")) != null) 129 pal.mMonthColor = Color.parseColor(val); 130 return (pal.mId == null) ? null : pal; 131 } 132 133 @Override getBackgroundColor()134 public int getBackgroundColor() { 135 return mBackgroundColor; 136 } 137 138 @Override getSecondColor(float forAngle)139 public int getSecondColor(float forAngle) { 140 return mSecondColor; 141 } 142 143 @Override getMinuteColor(float forAngle)144 public int getMinuteColor(float forAngle) { 145 return mMinuteColor; 146 } 147 148 @Override getHourColor(float forAngle)149 public int getHourColor(float forAngle) { 150 return mHourColor; 151 } 152 153 @Override getDayColor(float forAngle)154 public int getDayColor(float forAngle) { 155 return mDayColor; 156 } 157 158 @Override getMonthColor(float forAngle)159 public int getMonthColor(float forAngle) { 160 return mMonthColor; 161 } 162 163 @Override getId()164 public String getId() { 165 return mId; 166 } 167 168 } 169 170 static class CyclingClockPalette extends ClockPalette { 171 protected String mId; 172 protected int mBackgroundColor; 173 protected float mSaturation; 174 protected float mBrightness; 175 176 private static final int COLORS_CACHE_COUNT = 720; 177 private final int[] mColors = new int[COLORS_CACHE_COUNT]; 178 179 private static CyclingClockPalette sFallbackPalette = null; 180 getFallback()181 public static CyclingClockPalette getFallback() { 182 if (sFallbackPalette == null) { 183 sFallbackPalette = new CyclingClockPalette(); 184 sFallbackPalette.mId = "default_c"; 185 sFallbackPalette.mBackgroundColor = Color.WHITE; 186 sFallbackPalette.mSaturation = 0.8f; 187 sFallbackPalette.mBrightness = 0.9f; 188 sFallbackPalette.computeIntermediateColors(); 189 } 190 return sFallbackPalette; 191 } 192 CyclingClockPalette()193 private CyclingClockPalette() { } 194 computeIntermediateColors()195 private void computeIntermediateColors() { 196 final int[] colors = mColors; 197 final int count = colors.length; 198 float invCount = 1.0f / (float) COLORS_CACHE_COUNT; 199 for (int i = 0; i < count; i++) { 200 colors[i] = Color.HSBtoColor(i * invCount, mSaturation, mBrightness); 201 } 202 } 203 parseXmlPaletteTag(XmlResourceParser xrp)204 public static ClockPalette parseXmlPaletteTag(XmlResourceParser xrp) { 205 final CyclingClockPalette pal = new CyclingClockPalette(); 206 pal.mId = xrp.getAttributeValue(null, "id"); 207 String val; 208 if ((val = xrp.getAttributeValue(null, "background")) != null) 209 pal.mBackgroundColor = Color.parseColor(val); 210 if ((val = xrp.getAttributeValue(null, "saturation")) != null) 211 pal.mSaturation = Float.parseFloat(val); 212 if ((val = xrp.getAttributeValue(null, "brightness")) != null) 213 pal.mBrightness = Float.parseFloat(val); 214 if (pal.mId == null) { 215 return null; 216 } else { 217 pal.computeIntermediateColors(); 218 return pal; 219 } 220 } 221 @Override getBackgroundColor()222 public int getBackgroundColor() { 223 return mBackgroundColor; 224 } 225 226 @Override getSecondColor(float forAngle)227 public int getSecondColor(float forAngle) { 228 if (forAngle >= 1.0f || forAngle < 0.0f) forAngle = 0.0f; 229 return mColors[((int) (forAngle * COLORS_CACHE_COUNT))]; 230 } 231 232 @Override getMinuteColor(float forAngle)233 public int getMinuteColor(float forAngle) { 234 if (forAngle >= 1.0f || forAngle < 0.0f) forAngle = 0.0f; 235 return mColors[((int) (forAngle * COLORS_CACHE_COUNT))]; 236 } 237 238 @Override getHourColor(float forAngle)239 public int getHourColor(float forAngle) { 240 if (forAngle >= 1.0f || forAngle < 0.0f) forAngle = 0.0f; 241 return mColors[((int) (forAngle * COLORS_CACHE_COUNT))]; 242 } 243 244 @Override getDayColor(float forAngle)245 public int getDayColor(float forAngle) { 246 if (forAngle >= 1.0f || forAngle < 0.0f) forAngle = 0.0f; 247 return mColors[((int) (forAngle * COLORS_CACHE_COUNT))]; 248 } 249 250 @Override getMonthColor(float forAngle)251 public int getMonthColor(float forAngle) { 252 if (forAngle >= 1.0f || forAngle < 0.0f) forAngle = 0.0f; 253 return mColors[((int) (forAngle * COLORS_CACHE_COUNT))]; 254 } 255 256 @Override getId()257 public String getId() { 258 return mId; 259 } 260 } 261 262 private final Handler mHandler = new Handler(); 263 264 private IntentFilter mFilter; 265 266 @Override onCreate()267 public void onCreate() { 268 super.onCreate(); 269 270 mFilter = new IntentFilter(); 271 mFilter.addAction(Intent.ACTION_TIMEZONE_CHANGED); 272 } 273 274 @Override onDestroy()275 public void onDestroy() { 276 super.onDestroy(); 277 } 278 onCreateEngine()279 public Engine onCreateEngine() { 280 return new ClockEngine(); 281 } 282 283 class ClockEngine extends Engine implements SharedPreferences.OnSharedPreferenceChangeListener { 284 private static final float SMALL_RING_THICKNESS = 8.0f; 285 private static final float MEDIUM_RING_THICKNESS = 16.0f; 286 private static final float LARGE_RING_THICKNESS = 32.0f; 287 288 private static final float DEFAULT_RING_THICKNESS = 24.0f; 289 290 private static final float SMALL_GAP = 14.0f; 291 private static final float LARGE_GAP = 38.0f; 292 293 private final HashMap<String, ClockPalette> mPalettes = new HashMap<String, ClockPalette>(); 294 private ClockPalette mPalette; 295 296 private SharedPreferences mPrefs; 297 private boolean mShowSeconds; 298 private boolean mVariableLineWidth; 299 300 private boolean mWatcherRegistered; 301 private Time mCalendar; 302 303 private final Paint mPaint = new Paint(); 304 private final RectF mRect = new RectF(); 305 306 private float mOffsetX; 307 308 private final BroadcastReceiver mWatcher = new BroadcastReceiver() { 309 public void onReceive(Context context, Intent intent) { 310 final String timeZone = intent.getStringExtra("time-zone"); 311 mCalendar = new Time(TimeZone.getTimeZone(timeZone).getID()); 312 drawFrame(); 313 } 314 }; 315 316 private final Runnable mDrawClock = new Runnable() { 317 public void run() { 318 drawFrame(); 319 } 320 }; 321 private boolean mVisible; 322 ClockEngine()323 ClockEngine() { 324 XmlResourceParser xrp = getResources().getXml(R.xml.polar_clock_palettes); 325 try { 326 int what = xrp.getEventType(); 327 while (what != END_DOCUMENT) { 328 if (what == START_TAG) { 329 if ("palette".equals(xrp.getName())) { 330 ClockPalette pal = ClockPalette.parseXmlPaletteTag(xrp); 331 if (pal.getId() != null) { 332 mPalettes.put(pal.getId(), pal); 333 } 334 } 335 } 336 what = xrp.next(); 337 } 338 } catch (IOException e) { 339 Log.e(LOG_TAG, "An error occured during wallpaper configuration:", e); 340 } catch (XmlPullParserException e) { 341 Log.e(LOG_TAG, "An error occured during wallpaper configuration:", e); 342 } finally { 343 xrp.close(); 344 } 345 346 mPalette = CyclingClockPalette.getFallback(); 347 } 348 349 @Override onCreate(SurfaceHolder surfaceHolder)350 public void onCreate(SurfaceHolder surfaceHolder) { 351 super.onCreate(surfaceHolder); 352 353 mPrefs = PolarClockWallpaper.this.getSharedPreferences(SHARED_PREFS_NAME, 0); 354 mPrefs.registerOnSharedPreferenceChangeListener(this); 355 356 // load up user's settings 357 onSharedPreferenceChanged(mPrefs, null); 358 359 mCalendar = new Time(); 360 mCalendar.setToNow(); 361 362 final Paint paint = mPaint; 363 paint.setAntiAlias(true); 364 paint.setStrokeWidth(DEFAULT_RING_THICKNESS); 365 paint.setStrokeCap(Paint.Cap.ROUND); 366 paint.setStyle(Paint.Style.STROKE); 367 368 if (isPreview()) { 369 mOffsetX = 0.5f; 370 } 371 } 372 373 @Override onDestroy()374 public void onDestroy() { 375 super.onDestroy(); 376 if (mWatcherRegistered) { 377 mWatcherRegistered = false; 378 unregisterReceiver(mWatcher); 379 } 380 mHandler.removeCallbacks(mDrawClock); 381 } 382 onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key)383 public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, 384 String key) { 385 386 boolean changed = false; 387 if (key == null || PREF_SHOW_SECONDS.equals(key)) { 388 mShowSeconds = sharedPreferences.getBoolean( 389 PREF_SHOW_SECONDS, true); 390 changed = true; 391 } 392 if (key == null || PREF_VARIABLE_LINE_WIDTH.equals(key)) { 393 mVariableLineWidth = sharedPreferences.getBoolean( 394 PREF_VARIABLE_LINE_WIDTH, true); 395 changed = true; 396 } 397 if (key == null || PREF_PALETTE.equals(key)) { 398 String paletteId = sharedPreferences.getString( 399 PREF_PALETTE, ""); 400 ClockPalette pal = mPalettes.get(paletteId); 401 if (pal != null) { 402 mPalette = pal; 403 changed = true; 404 } 405 } 406 407 if (mVisible && changed) { 408 drawFrame(); 409 } 410 } 411 412 @Override onVisibilityChanged(boolean visible)413 public void onVisibilityChanged(boolean visible) { 414 mVisible = visible; 415 if (visible) { 416 if (!mWatcherRegistered) { 417 mWatcherRegistered = true; 418 registerReceiver(mWatcher, mFilter, null, mHandler); 419 } 420 mCalendar = new Time(); 421 mCalendar.setToNow(); 422 } else { 423 if (mWatcherRegistered) { 424 mWatcherRegistered = false; 425 unregisterReceiver(mWatcher); 426 } 427 mHandler.removeCallbacks(mDrawClock); 428 } 429 drawFrame(); 430 } 431 432 @Override onSurfaceChanged(SurfaceHolder holder, int format, int width, int height)433 public void onSurfaceChanged(SurfaceHolder holder, int format, int width, int height) { 434 super.onSurfaceChanged(holder, format, width, height); 435 drawFrame(); 436 } 437 438 @Override onSurfaceCreated(SurfaceHolder holder)439 public void onSurfaceCreated(SurfaceHolder holder) { 440 super.onSurfaceCreated(holder); 441 } 442 443 @Override onSurfaceDestroyed(SurfaceHolder holder)444 public void onSurfaceDestroyed(SurfaceHolder holder) { 445 super.onSurfaceDestroyed(holder); 446 mVisible = false; 447 mHandler.removeCallbacks(mDrawClock); 448 } 449 450 @Override onOffsetsChanged(float xOffset, float yOffset, float xStep, float yStep, int xPixels, int yPixels)451 public void onOffsetsChanged(float xOffset, float yOffset, 452 float xStep, float yStep, int xPixels, int yPixels) { 453 if (isPreview()) return; 454 455 mOffsetX = xOffset; 456 drawFrame(); 457 } 458 drawFrame()459 void drawFrame() { 460 if (mPalette == null) { 461 Log.w("PolarClockWallpaper", "no palette?!"); 462 return; 463 } 464 465 final SurfaceHolder holder = getSurfaceHolder(); 466 final Rect frame = holder.getSurfaceFrame(); 467 final int width = frame.width(); 468 final int height = frame.height(); 469 470 Canvas c = null; 471 try { 472 c = holder.lockCanvas(); 473 if (c != null) { 474 final Time calendar = mCalendar; 475 final Paint paint = mPaint; 476 477 final long millis = System.currentTimeMillis(); 478 calendar.set(millis); 479 calendar.normalize(false); 480 481 int s = width / 2; 482 int t = height / 2; 483 484 c.drawColor(mPalette.getBackgroundColor()); 485 486 c.translate(s + MathUtils.lerp(s, -s, mOffsetX), t); 487 c.rotate(-90.0f); 488 if (height < width) { 489 c.scale(0.9f, 0.9f); 490 } 491 492 float size = Math.min(width, height) * 0.5f - DEFAULT_RING_THICKNESS; 493 final RectF rect = mRect; 494 rect.set(-size, -size, size, size); 495 float angle; 496 497 float lastRingThickness = DEFAULT_RING_THICKNESS; 498 499 if (mShowSeconds) { 500 // Draw seconds 501 angle = (float) (millis % 60000) / 60000.0f; 502 //Log.d("PolarClock", "millis=" + millis + ", angle=" + angle); 503 paint.setColor(mPalette.getSecondColor(angle)); 504 505 if (mVariableLineWidth) { 506 lastRingThickness = SMALL_RING_THICKNESS; 507 paint.setStrokeWidth(lastRingThickness); 508 } 509 c.drawArc(rect, 0.0f, angle * 360.0f, false, paint); 510 } 511 512 // Draw minutes 513 size -= (SMALL_GAP + lastRingThickness); 514 rect.set(-size, -size, size, size); 515 516 angle = ((calendar.minute * 60.0f + calendar.second) % 3600) / 3600.0f; 517 paint.setColor(mPalette.getMinuteColor(angle)); 518 519 if (mVariableLineWidth) { 520 lastRingThickness = MEDIUM_RING_THICKNESS; 521 paint.setStrokeWidth(lastRingThickness); 522 } 523 c.drawArc(rect, 0.0f, angle * 360.0f, false, paint); 524 525 // Draw hours 526 size -= (SMALL_GAP + lastRingThickness); 527 rect.set(-size, -size, size, size); 528 529 angle = ((calendar.hour * 60.0f + calendar.minute) % 1440) / 1440.0f; 530 paint.setColor(mPalette.getHourColor(angle)); 531 532 if (mVariableLineWidth) { 533 lastRingThickness = LARGE_RING_THICKNESS; 534 paint.setStrokeWidth(lastRingThickness); 535 } 536 c.drawArc(rect, 0.0f, angle * 360.0f, false, paint); 537 538 // Draw day 539 size -= (LARGE_GAP + lastRingThickness); 540 rect.set(-size, -size, size, size); 541 542 angle = (calendar.monthDay - 1) / 543 (float) (calendar.getActualMaximum(Time.MONTH_DAY) - 1); 544 paint.setColor(mPalette.getDayColor(angle)); 545 546 if (mVariableLineWidth) { 547 lastRingThickness = MEDIUM_RING_THICKNESS; 548 paint.setStrokeWidth(lastRingThickness); 549 } 550 c.drawArc(rect, 0.0f, angle * 360.0f, false, paint); 551 552 // Draw month 553 size -= (SMALL_GAP + lastRingThickness); 554 rect.set(-size, -size, size, size); 555 556 angle = (calendar.month) / 11.0f; // NB: month is already on [0..11] 557 558 paint.setColor(mPalette.getMonthColor(angle)); 559 560 if (mVariableLineWidth) { 561 lastRingThickness = LARGE_RING_THICKNESS; 562 paint.setStrokeWidth(lastRingThickness); 563 } 564 c.drawArc(rect, 0.0f, angle * 360.0f, false, paint); 565 } 566 } finally { 567 if (c != null) holder.unlockCanvasAndPost(c); 568 } 569 570 mHandler.removeCallbacks(mDrawClock); 571 if (mVisible) { 572 if (mShowSeconds) { 573 mHandler.postDelayed(mDrawClock, 1000 / 25); 574 } else { 575 // If we aren't showing seconds, we don't need to update 576 // nearly as often. 577 mHandler.postDelayed(mDrawClock, 2000); 578 } 579 } 580 } 581 } 582 } 583