/* * Copyright (C) 2009 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.wallpaper.polarclock; import android.service.wallpaper.WallpaperService; import android.graphics.Canvas; import android.graphics.Rect; import android.graphics.Paint; import android.graphics.Color; import android.graphics.RectF; import android.view.SurfaceHolder; import android.content.IntentFilter; import android.content.Intent; import android.content.BroadcastReceiver; import android.content.Context; import android.content.SharedPreferences; import android.content.res.XmlResourceParser; import android.os.Handler; import android.os.SystemClock; import android.text.format.Time; import android.util.MathUtils; import android.util.Log; import java.util.HashMap; import java.util.TimeZone; import java.io.IOException; import org.xmlpull.v1.XmlPullParserException; import static org.xmlpull.v1.XmlPullParser.*; import com.android.wallpaper.R; public class PolarClockWallpaper extends WallpaperService { private static final String LOG_TAG = "PolarClock"; static final String SHARED_PREFS_NAME = "polar_clock_settings"; static final String PREF_SHOW_SECONDS = "show_seconds"; static final String PREF_VARIABLE_LINE_WIDTH = "variable_line_width"; static final String PREF_PALETTE = "palette"; static final int BACKGROUND_COLOR = 0xffffffff; static abstract class ClockPalette { public static ClockPalette parseXmlPaletteTag(XmlResourceParser xrp) { String kind = xrp.getAttributeValue(null, "kind"); if ("cycling".equals(kind)) { return CyclingClockPalette.parseXmlPaletteTag(xrp); } else { return FixedClockPalette.parseXmlPaletteTag(xrp); } } public abstract int getBackgroundColor(); // forAngle should be on [0.0,1.0) but 1.0 must be tolerated public abstract int getSecondColor(float forAngle); public abstract int getMinuteColor(float forAngle); public abstract int getHourColor(float forAngle); public abstract int getDayColor(float forAngle); public abstract int getMonthColor(float forAngle); public abstract String getId(); } static class FixedClockPalette extends ClockPalette { protected String mId; protected int mBackgroundColor; protected int mSecondColor; protected int mMinuteColor; protected int mHourColor; protected int mDayColor; protected int mMonthColor; private static FixedClockPalette sFallbackPalette = null; public static FixedClockPalette getFallback() { if (sFallbackPalette == null) { sFallbackPalette = new FixedClockPalette(); sFallbackPalette.mId = "default"; sFallbackPalette.mBackgroundColor = Color.WHITE; sFallbackPalette.mSecondColor = sFallbackPalette.mMinuteColor = sFallbackPalette.mHourColor = sFallbackPalette.mDayColor = sFallbackPalette.mMonthColor = Color.BLACK; } return sFallbackPalette; } private FixedClockPalette() { } public static ClockPalette parseXmlPaletteTag(XmlResourceParser xrp) { final FixedClockPalette pal = new FixedClockPalette(); pal.mId = xrp.getAttributeValue(null, "id"); String val; if ((val = xrp.getAttributeValue(null, "background")) != null) pal.mBackgroundColor = Color.parseColor(val); if ((val = xrp.getAttributeValue(null, "second")) != null) pal.mSecondColor = Color.parseColor(val); if ((val = xrp.getAttributeValue(null, "minute")) != null) pal.mMinuteColor = Color.parseColor(val); if ((val = xrp.getAttributeValue(null, "hour")) != null) pal.mHourColor = Color.parseColor(val); if ((val = xrp.getAttributeValue(null, "day")) != null) pal.mDayColor = Color.parseColor(val); if ((val = xrp.getAttributeValue(null, "month")) != null) pal.mMonthColor = Color.parseColor(val); return (pal.mId == null) ? null : pal; } @Override public int getBackgroundColor() { return mBackgroundColor; } @Override public int getSecondColor(float forAngle) { return mSecondColor; } @Override public int getMinuteColor(float forAngle) { return mMinuteColor; } @Override public int getHourColor(float forAngle) { return mHourColor; } @Override public int getDayColor(float forAngle) { return mDayColor; } @Override public int getMonthColor(float forAngle) { return mMonthColor; } @Override public String getId() { return mId; } } static class CyclingClockPalette extends ClockPalette { protected String mId; protected int mBackgroundColor; protected float mSaturation; protected float mBrightness; private static final int COLORS_CACHE_COUNT = 720; private final int[] mColors = new int[COLORS_CACHE_COUNT]; private static CyclingClockPalette sFallbackPalette = null; public static CyclingClockPalette getFallback() { if (sFallbackPalette == null) { sFallbackPalette = new CyclingClockPalette(); sFallbackPalette.mId = "default_c"; sFallbackPalette.mBackgroundColor = Color.WHITE; sFallbackPalette.mSaturation = 0.8f; sFallbackPalette.mBrightness = 0.9f; sFallbackPalette.computeIntermediateColors(); } return sFallbackPalette; } private CyclingClockPalette() { } private void computeIntermediateColors() { final int[] colors = mColors; final int count = colors.length; float invCount = 1.0f / (float) COLORS_CACHE_COUNT; for (int i = 0; i < count; i++) { colors[i] = Color.HSBtoColor(i * invCount, mSaturation, mBrightness); } } public static ClockPalette parseXmlPaletteTag(XmlResourceParser xrp) { final CyclingClockPalette pal = new CyclingClockPalette(); pal.mId = xrp.getAttributeValue(null, "id"); String val; if ((val = xrp.getAttributeValue(null, "background")) != null) pal.mBackgroundColor = Color.parseColor(val); if ((val = xrp.getAttributeValue(null, "saturation")) != null) pal.mSaturation = Float.parseFloat(val); if ((val = xrp.getAttributeValue(null, "brightness")) != null) pal.mBrightness = Float.parseFloat(val); if (pal.mId == null) { return null; } else { pal.computeIntermediateColors(); return pal; } } @Override public int getBackgroundColor() { return mBackgroundColor; } @Override public int getSecondColor(float forAngle) { if (forAngle >= 1.0f || forAngle < 0.0f) forAngle = 0.0f; return mColors[((int) (forAngle * COLORS_CACHE_COUNT))]; } @Override public int getMinuteColor(float forAngle) { if (forAngle >= 1.0f || forAngle < 0.0f) forAngle = 0.0f; return mColors[((int) (forAngle * COLORS_CACHE_COUNT))]; } @Override public int getHourColor(float forAngle) { if (forAngle >= 1.0f || forAngle < 0.0f) forAngle = 0.0f; return mColors[((int) (forAngle * COLORS_CACHE_COUNT))]; } @Override public int getDayColor(float forAngle) { if (forAngle >= 1.0f || forAngle < 0.0f) forAngle = 0.0f; return mColors[((int) (forAngle * COLORS_CACHE_COUNT))]; } @Override public int getMonthColor(float forAngle) { if (forAngle >= 1.0f || forAngle < 0.0f) forAngle = 0.0f; return mColors[((int) (forAngle * COLORS_CACHE_COUNT))]; } @Override public String getId() { return mId; } } private final Handler mHandler = new Handler(); private IntentFilter mFilter; @Override public void onCreate() { super.onCreate(); mFilter = new IntentFilter(); mFilter.addAction(Intent.ACTION_TIMEZONE_CHANGED); } @Override public void onDestroy() { super.onDestroy(); } public Engine onCreateEngine() { return new ClockEngine(); } class ClockEngine extends Engine implements SharedPreferences.OnSharedPreferenceChangeListener { private static final float SMALL_RING_THICKNESS = 8.0f; private static final float MEDIUM_RING_THICKNESS = 16.0f; private static final float LARGE_RING_THICKNESS = 32.0f; private static final float DEFAULT_RING_THICKNESS = 24.0f; private static final float SMALL_GAP = 14.0f; private static final float LARGE_GAP = 38.0f; private final HashMap mPalettes = new HashMap(); private ClockPalette mPalette; private SharedPreferences mPrefs; private boolean mShowSeconds; private boolean mVariableLineWidth; private boolean mWatcherRegistered; private Time mCalendar; private final Paint mPaint = new Paint(); private final RectF mRect = new RectF(); private float mOffsetX; private final BroadcastReceiver mWatcher = new BroadcastReceiver() { public void onReceive(Context context, Intent intent) { final String timeZone = intent.getStringExtra("time-zone"); mCalendar = new Time(TimeZone.getTimeZone(timeZone).getID()); drawFrame(); } }; private final Runnable mDrawClock = new Runnable() { public void run() { drawFrame(); } }; private boolean mVisible; ClockEngine() { XmlResourceParser xrp = getResources().getXml(R.xml.polar_clock_palettes); try { int what = xrp.getEventType(); while (what != END_DOCUMENT) { if (what == START_TAG) { if ("palette".equals(xrp.getName())) { ClockPalette pal = ClockPalette.parseXmlPaletteTag(xrp); if (pal.getId() != null) { mPalettes.put(pal.getId(), pal); } } } what = xrp.next(); } } catch (IOException e) { Log.e(LOG_TAG, "An error occured during wallpaper configuration:", e); } catch (XmlPullParserException e) { Log.e(LOG_TAG, "An error occured during wallpaper configuration:", e); } finally { xrp.close(); } mPalette = CyclingClockPalette.getFallback(); } @Override public void onCreate(SurfaceHolder surfaceHolder) { super.onCreate(surfaceHolder); mPrefs = PolarClockWallpaper.this.getSharedPreferences(SHARED_PREFS_NAME, 0); mPrefs.registerOnSharedPreferenceChangeListener(this); // load up user's settings onSharedPreferenceChanged(mPrefs, null); mCalendar = new Time(); mCalendar.setToNow(); final Paint paint = mPaint; paint.setAntiAlias(true); paint.setStrokeWidth(DEFAULT_RING_THICKNESS); paint.setStrokeCap(Paint.Cap.ROUND); paint.setStyle(Paint.Style.STROKE); if (isPreview()) { mOffsetX = 0.5f; } } @Override public void onDestroy() { super.onDestroy(); if (mWatcherRegistered) { mWatcherRegistered = false; unregisterReceiver(mWatcher); } mHandler.removeCallbacks(mDrawClock); } public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) { boolean changed = false; if (key == null || PREF_SHOW_SECONDS.equals(key)) { mShowSeconds = sharedPreferences.getBoolean( PREF_SHOW_SECONDS, true); changed = true; } if (key == null || PREF_VARIABLE_LINE_WIDTH.equals(key)) { mVariableLineWidth = sharedPreferences.getBoolean( PREF_VARIABLE_LINE_WIDTH, true); changed = true; } if (key == null || PREF_PALETTE.equals(key)) { String paletteId = sharedPreferences.getString( PREF_PALETTE, ""); ClockPalette pal = mPalettes.get(paletteId); if (pal != null) { mPalette = pal; changed = true; } } if (mVisible && changed) { drawFrame(); } } @Override public void onVisibilityChanged(boolean visible) { mVisible = visible; if (visible) { if (!mWatcherRegistered) { mWatcherRegistered = true; registerReceiver(mWatcher, mFilter, null, mHandler); } mCalendar = new Time(); mCalendar.setToNow(); } else { if (mWatcherRegistered) { mWatcherRegistered = false; unregisterReceiver(mWatcher); } mHandler.removeCallbacks(mDrawClock); } drawFrame(); } @Override public void onSurfaceChanged(SurfaceHolder holder, int format, int width, int height) { super.onSurfaceChanged(holder, format, width, height); drawFrame(); } @Override public void onSurfaceCreated(SurfaceHolder holder) { super.onSurfaceCreated(holder); } @Override public void onSurfaceDestroyed(SurfaceHolder holder) { super.onSurfaceDestroyed(holder); mVisible = false; mHandler.removeCallbacks(mDrawClock); } @Override public void onOffsetsChanged(float xOffset, float yOffset, float xStep, float yStep, int xPixels, int yPixels) { if (isPreview()) return; mOffsetX = xOffset; drawFrame(); } void drawFrame() { if (mPalette == null) { Log.w("PolarClockWallpaper", "no palette?!"); return; } final SurfaceHolder holder = getSurfaceHolder(); final Rect frame = holder.getSurfaceFrame(); final int width = frame.width(); final int height = frame.height(); Canvas c = null; try { c = holder.lockCanvas(); if (c != null) { final Time calendar = mCalendar; final Paint paint = mPaint; final long millis = System.currentTimeMillis(); calendar.set(millis); calendar.normalize(false); int s = width / 2; int t = height / 2; c.drawColor(mPalette.getBackgroundColor()); c.translate(s + MathUtils.lerp(s, -s, mOffsetX), t); c.rotate(-90.0f); if (height < width) { c.scale(0.9f, 0.9f); } float size = Math.min(width, height) * 0.5f - DEFAULT_RING_THICKNESS; final RectF rect = mRect; rect.set(-size, -size, size, size); float angle; float lastRingThickness = DEFAULT_RING_THICKNESS; if (mShowSeconds) { // Draw seconds angle = (float) (millis % 60000) / 60000.0f; //Log.d("PolarClock", "millis=" + millis + ", angle=" + angle); paint.setColor(mPalette.getSecondColor(angle)); if (mVariableLineWidth) { lastRingThickness = SMALL_RING_THICKNESS; paint.setStrokeWidth(lastRingThickness); } c.drawArc(rect, 0.0f, angle * 360.0f, false, paint); } // Draw minutes size -= (SMALL_GAP + lastRingThickness); rect.set(-size, -size, size, size); angle = ((calendar.minute * 60.0f + calendar.second) % 3600) / 3600.0f; paint.setColor(mPalette.getMinuteColor(angle)); if (mVariableLineWidth) { lastRingThickness = MEDIUM_RING_THICKNESS; paint.setStrokeWidth(lastRingThickness); } c.drawArc(rect, 0.0f, angle * 360.0f, false, paint); // Draw hours size -= (SMALL_GAP + lastRingThickness); rect.set(-size, -size, size, size); angle = ((calendar.hour * 60.0f + calendar.minute) % 1440) / 1440.0f; paint.setColor(mPalette.getHourColor(angle)); if (mVariableLineWidth) { lastRingThickness = LARGE_RING_THICKNESS; paint.setStrokeWidth(lastRingThickness); } c.drawArc(rect, 0.0f, angle * 360.0f, false, paint); // Draw day size -= (LARGE_GAP + lastRingThickness); rect.set(-size, -size, size, size); angle = (calendar.monthDay - 1) / (float) (calendar.getActualMaximum(Time.MONTH_DAY) - 1); paint.setColor(mPalette.getDayColor(angle)); if (mVariableLineWidth) { lastRingThickness = MEDIUM_RING_THICKNESS; paint.setStrokeWidth(lastRingThickness); } c.drawArc(rect, 0.0f, angle * 360.0f, false, paint); // Draw month size -= (SMALL_GAP + lastRingThickness); rect.set(-size, -size, size, size); angle = (calendar.month) / 11.0f; // NB: month is already on [0..11] paint.setColor(mPalette.getMonthColor(angle)); if (mVariableLineWidth) { lastRingThickness = LARGE_RING_THICKNESS; paint.setStrokeWidth(lastRingThickness); } c.drawArc(rect, 0.0f, angle * 360.0f, false, paint); } } finally { if (c != null) holder.unlockCanvasAndPost(c); } mHandler.removeCallbacks(mDrawClock); if (mVisible) { if (mShowSeconds) { mHandler.postDelayed(mDrawClock, 1000 / 25); } else { // If we aren't showing seconds, we don't need to update // nearly as often. mHandler.postDelayed(mDrawClock, 2000); } } } } }