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
17package com.android.wallpaper.polarclock;
18
19import android.service.wallpaper.WallpaperService;
20import android.graphics.Canvas;
21import android.graphics.Rect;
22import android.graphics.Paint;
23import android.graphics.Color;
24import android.graphics.RectF;
25import android.view.SurfaceHolder;
26import android.content.IntentFilter;
27import android.content.Intent;
28import android.content.BroadcastReceiver;
29import android.content.Context;
30import android.content.SharedPreferences;
31import android.content.res.XmlResourceParser;
32
33import android.os.Handler;
34import android.os.SystemClock;
35import android.text.format.Time;
36import android.util.MathUtils;
37import android.util.Log;
38
39import java.util.HashMap;
40import java.util.TimeZone;
41import java.io.IOException;
42
43import org.xmlpull.v1.XmlPullParserException;
44import static org.xmlpull.v1.XmlPullParser.*;
45
46import com.android.wallpaper.R;
47
48public 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 {
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
69        public abstract int getBackgroundColor();
70
71        // forAngle should be on [0.0,1.0) but 1.0 must be tolerated
72        public abstract int getSecondColor(float forAngle);
73
74        public abstract int getMinuteColor(float forAngle);
75
76        public abstract int getHourColor(float forAngle);
77
78        public abstract int getDayColor(float forAngle);
79
80        public abstract int getMonthColor(float forAngle);
81
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
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
112        private FixedClockPalette() { }
113
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
134        public int getBackgroundColor() {
135            return mBackgroundColor;
136        }
137
138        @Override
139        public int getSecondColor(float forAngle) {
140            return mSecondColor;
141        }
142
143        @Override
144        public int getMinuteColor(float forAngle) {
145            return mMinuteColor;
146        }
147
148        @Override
149        public int getHourColor(float forAngle) {
150            return mHourColor;
151        }
152
153        @Override
154        public int getDayColor(float forAngle) {
155            return mDayColor;
156        }
157
158        @Override
159        public int getMonthColor(float forAngle) {
160            return mMonthColor;
161        }
162
163        @Override
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
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
193        private CyclingClockPalette() { }
194
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
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
222        public int getBackgroundColor() {
223            return mBackgroundColor;
224        }
225
226        @Override
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
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
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
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
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
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
267    public void onCreate() {
268        super.onCreate();
269
270        mFilter = new IntentFilter();
271        mFilter.addAction(Intent.ACTION_TIMEZONE_CHANGED);
272    }
273
274    @Override
275    public void onDestroy() {
276        super.onDestroy();
277    }
278
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
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
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
374        public void onDestroy() {
375            super.onDestroy();
376            if (mWatcherRegistered) {
377                mWatcherRegistered = false;
378                unregisterReceiver(mWatcher);
379            }
380            mHandler.removeCallbacks(mDrawClock);
381        }
382
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
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
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
439        public void onSurfaceCreated(SurfaceHolder holder) {
440            super.onSurfaceCreated(holder);
441        }
442
443        @Override
444        public void onSurfaceDestroyed(SurfaceHolder holder) {
445            super.onSurfaceDestroyed(holder);
446            mVisible = false;
447            mHandler.removeCallbacks(mDrawClock);
448        }
449
450        @Override
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
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