/*
* Copyright (C) Winson Chiu
*
* 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 cw.kop.autobackground;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.Rect;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.preference.PreferenceManager;
import android.support.v4.content.LocalBroadcastManager;
import android.support.v7.graphics.Palette;
import android.support.wearable.watchface.CanvasWatchFaceService;
import android.support.wearable.watchface.WatchFaceStyle;
import android.text.format.Time;
import android.util.DisplayMetrics;
import android.util.Log;
import android.util.TypedValue;
import android.view.SurfaceHolder;
import android.view.WindowInsets;
import java.util.TimeZone;
/**
* Created by TheKeeperOfPie on 12/11/2014.
*/
public class WatchFace extends CanvasWatchFaceService {
private static final String TAG = WatchFace.class.getCanonicalName();
@Override
public void onCreate() {
super.onCreate();
WearSettings.initPrefs(PreferenceManager.getDefaultSharedPreferences(this));
}
@Override
public Engine onCreateEngine() {
return new Engine();
}
/* implement service callback methods */
private class Engine extends CanvasWatchFaceService.Engine {
private static final int MSG_UPDATE_TIME = 0;
private static final long INTERACTIVE_UPDATE_RATE_MS = 500;
private static final int NUM_DRAW_CALLS = 5;
/* a time object */
private Time time;
private long timeOffset;
/* device features */
private boolean lowBitMode = false;
private boolean burnProtectionMode = false;
private boolean registered;
/* graphic objects */
private Paint bitmapPaint;
private Paint indicatorPaint;
private Paint hourPaint;
private Paint minutePaint;
private Paint secondPaint;
private String timeSeparator = ":";
private float separatorWidth = 0f;
private float xOffset = 0f;
private Bitmap backgroundImage;
private Palette imagePalette;
private IntentFilter localIntentFilter;
private IntentFilter timeZoneIntentFilter;
/* handler to update the time once a second in interactive mode */
private Handler timeHandler = new Handler(new Handler.Callback() {
@Override
public boolean handleMessage(Message msg) {
switch (msg.what) {
case MSG_UPDATE_TIME:
try {
invalidate();
if (shouldTimerBeRunning()) {
long timeMs = System.currentTimeMillis();
long delayMs = INTERACTIVE_UPDATE_RATE_MS
- (timeMs % INTERACTIVE_UPDATE_RATE_MS);
timeHandler
.sendEmptyMessageDelayed(MSG_UPDATE_TIME, delayMs);
}
}
catch (NullPointerException e) {
Log.e(TAG, "Null pointer");
}
break;
}
return true;
}
});
/* receiver to update the time zone */
final BroadcastReceiver timeZoneReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
time.clear(intent.getStringExtra("time-zone"));
time.setToNow();
}
};
private BroadcastReceiver localBroadcastReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context arg0, Intent intent) {
switch (intent.getAction()) {
case EventListenerService.LOAD_IMAGE:
backgroundImage = EventListenerService.getBitmap();
EventListenerService.recycleLast();
if (WearSettings.useTimePalette()) {
Palette.generateAsync(backgroundImage, new Palette.PaletteAsyncListener() {
@Override
public void onGenerated(Palette palette) {
imagePalette = palette;
if (!isInAmbientMode()) {
syncSettings();
}
}
});
}
break;
case EventListenerService.LOAD_SETTINGS:
syncSettings();
break;
}
}
};
private boolean isDigital;
private float tickRadius;
private float hourRadius;
private float minuteRadius;
private float secondRadius;
private float tickWidth;
private float hourWidth;
private float minuteWidth;
private float secondWidth;
@Override
public void onCreate(SurfaceHolder holder) {
/* initialize your watch face */
super.onCreate(holder);
setWatchFaceStyle(new WatchFaceStyle.Builder(WatchFace.this)
.setCardPeekMode(WatchFaceStyle.PEEK_MODE_SHORT)
.setBackgroundVisibility(WatchFaceStyle.BACKGROUND_VISIBILITY_INTERRUPTIVE)
.setShowSystemUiTime(false)
.build());
localIntentFilter = new IntentFilter();
localIntentFilter.addAction(EventListenerService.LOAD_IMAGE);
localIntentFilter.addAction(EventListenerService.LOAD_SETTINGS);
timeZoneIntentFilter = new IntentFilter(Intent.ACTION_TIMEZONE_CHANGED);
time = new Time();
bitmapPaint = new Paint();
bitmapPaint.setAntiAlias(false);
bitmapPaint.setDither(true);
indicatorPaint = new Paint();
indicatorPaint.setStrokeCap(Paint.Cap.BUTT);
indicatorPaint.setTextAlign(Paint.Align.LEFT);
indicatorPaint.setShadowLayer(WearSettings.SHADOW_RADIUS,
0f,
0f,
WearSettings.getSeparatorShadowColor());
hourPaint = new Paint();
hourPaint.setStrokeCap(Paint.Cap.BUTT);
hourPaint.setTextAlign(Paint.Align.LEFT);
hourPaint.setShadowLayer(WearSettings.SHADOW_RADIUS, 0f, 0f, WearSettings.getHourShadowColor());
minutePaint = new Paint();
minutePaint.setStrokeCap(Paint.Cap.BUTT);
minutePaint.setTextAlign(Paint.Align.LEFT);
minutePaint.setShadowLayer(WearSettings.SHADOW_RADIUS, 0f, 0f, WearSettings.getMinuteShadowColor());
secondPaint = new Paint();
secondPaint.setStrokeCap(Paint.Cap.BUTT);
secondPaint.setTextAlign(Paint.Align.LEFT);
secondPaint.setShadowLayer(WearSettings.SHADOW_RADIUS,
0f,
0f,
WearSettings.getSecondShadowColor());
syncSettings();
DisplayMetrics displayMetrics = getResources().getDisplayMetrics();
float textSize = displayMetrics.heightPixels / 4;
float radius = displayMetrics.widthPixels / 2;
float width = (float) Math.sqrt(Math.pow(radius, 2f) - Math.pow(textSize, 2f)) * 2f;
float textScale = 1.0f;
xOffset = radius - width / 2f;
Log.i(TAG, "textSize: " + textSize);
Log.i(TAG, "radius: " + radius);
Log.i(TAG, "width: " + width);
indicatorPaint.setTextSize(textSize);
hourPaint.setTextSize(textSize);
minutePaint.setTextSize(textSize);
secondPaint.setTextSize(textSize);
while (getTimeWidth() > width) {
textScale -= 0.05f;
indicatorPaint.setTextScaleX(textScale);
hourPaint.setTextScaleX(textScale);
minutePaint.setTextScaleX(textScale);
secondPaint.setTextScaleX(textScale);
Log.i(TAG, "Time width: " + getTimeWidth());
}
separatorWidth = indicatorPaint.measureText(timeSeparator);
}
@Override
public void onApplyWindowInsets(WindowInsets insets) {
super.onApplyWindowInsets(insets);
}
private void syncSettings() {
isDigital = WearSettings.getTimeType().equals(WearSettings.DIGITAL);
timeOffset = WearSettings.getTimeOffset();
timeSeparator = WearSettings.getSeparatorText();
indicatorPaint.setAntiAlias(true);
hourPaint.setAntiAlias(true);
minutePaint.setAntiAlias(true);
secondPaint.setAntiAlias(true);
indicatorPaint.setStyle(Paint.Style.FILL_AND_STROKE);
hourPaint.setStyle(Paint.Style.FILL_AND_STROKE);
minutePaint.setStyle(Paint.Style.FILL_AND_STROKE);
secondPaint.setStyle(Paint.Style.FILL_AND_STROKE);
if (imagePalette != null) {
indicatorPaint.setColor(imagePalette.getMutedColor(WearSettings.getSeparatorColor()));
indicatorPaint.setColor(imagePalette.getVibrantColor(WearSettings.getSeparatorColor()));
hourPaint.setColor(imagePalette.getVibrantColor(WearSettings.getHourColor()));
minutePaint.setColor(imagePalette.getVibrantColor(WearSettings.getMinuteColor()));
secondPaint.setColor(imagePalette.getVibrantColor(WearSettings.getSecondColor()));
indicatorPaint.setShadowLayer(WearSettings.SHADOW_RADIUS,
0f,
0f,
imagePalette.getDarkMutedColor(WearSettings.getSeparatorShadowColor()));
hourPaint.setShadowLayer(WearSettings.SHADOW_RADIUS, 0f, 0f, imagePalette.getDarkMutedColor(WearSettings.getHourShadowColor()));
minutePaint.setShadowLayer(WearSettings.SHADOW_RADIUS, 0f, 0f, imagePalette.getDarkMutedColor(WearSettings.getMinuteShadowColor()));
secondPaint.setShadowLayer(WearSettings.SHADOW_RADIUS, 0f, 0f, imagePalette.getDarkMutedColor(WearSettings.getSecondShadowColor()));
}
else {
indicatorPaint.setColor(WearSettings.getSeparatorColor());
hourPaint.setColor(WearSettings.getHourColor());
minutePaint.setColor(WearSettings.getMinuteColor());
secondPaint.setColor(WearSettings.getSecondColor());
indicatorPaint.setShadowLayer(WearSettings.SHADOW_RADIUS,
0f,
0f,
WearSettings.getSeparatorShadowColor());
hourPaint.setShadowLayer(WearSettings.SHADOW_RADIUS, 0f, 0f, WearSettings.getHourShadowColor());
minutePaint.setShadowLayer(WearSettings.SHADOW_RADIUS, 0f, 0f, WearSettings.getMinuteShadowColor());
secondPaint.setShadowLayer(WearSettings.SHADOW_RADIUS, 0f, 0f, WearSettings.getSecondShadowColor());
}
tickRadius = WearSettings.getTickLengthRatio();
hourRadius = WearSettings.getHourLengthRatio();
minuteRadius = WearSettings.getMinuteLengthRatio();
secondRadius = WearSettings.getSecondLengthRatio();
DisplayMetrics displayMetrics = getResources().getDisplayMetrics();
tickWidth = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,
WearSettings.getTickWidth(),
displayMetrics);
hourWidth = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,
WearSettings.getHourWidth(),
displayMetrics);
minuteWidth = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,
WearSettings.getMinuteWidth(),
displayMetrics);
secondWidth = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,
WearSettings.getSecondWidth(),
displayMetrics);
if (isDigital) {
indicatorPaint.setStrokeWidth(1.0f);
}
else {
indicatorPaint.setStrokeWidth(tickWidth);
}
invalidate();
}
private float getTimeWidth() {
float width = 0;
width += hourPaint.measureText("00");
width += minutePaint.measureText("00");
width += secondPaint.measureText("00");
width += indicatorPaint.measureText(timeSeparator);
width += indicatorPaint.measureText(timeSeparator);
return width;
}
@Override
public void onDestroy() {
timeHandler.removeMessages(MSG_UPDATE_TIME);
unregisterReceivers();
super.onDestroy();
}
@Override
public void onTimeTick() {
/* the time changed */
super.onTimeTick();
invalidate();
}
private void updateTimer() {
timeHandler.removeMessages(MSG_UPDATE_TIME);
if (shouldTimerBeRunning()) {
timeHandler.sendEmptyMessage(MSG_UPDATE_TIME);
}
}
@Override
public void onAmbientModeChanged(boolean inAmbientMode) {
boolean wasInAmbientMode = isInAmbientMode();
super.onAmbientModeChanged(inAmbientMode);
// if (inAmbientMode != wasInAmbientMode) {
if (isInAmbientMode()) {
applyAmbientMode();
}
else {
syncSettings();
}
// }
invalidate();
updateTimer();
}
private void applyAmbientMode() {
indicatorPaint.setAntiAlias(!lowBitMode);
hourPaint.setAntiAlias(!lowBitMode);
minutePaint.setAntiAlias(!lowBitMode);
indicatorPaint.setColor(Color.WHITE);
hourPaint.setColor(Color.WHITE);
minutePaint.setColor(Color.WHITE);
indicatorPaint.setShadowLayer(0.0f, 0.0f, 0.0f, Color.TRANSPARENT);
hourPaint.setShadowLayer(0.0f, 0.0f, 0.0f, Color.TRANSPARENT);
minutePaint.setShadowLayer(0.0f, 0.0f, 0.0f, Color.TRANSPARENT);
if (burnProtectionMode) {
indicatorPaint.setStyle(Paint.Style.STROKE);
hourPaint.setStyle(Paint.Style.STROKE);
minutePaint.setStyle(Paint.Style.STROKE);
}
}
@Override
public void onDraw(Canvas canvas, Rect bounds) {
/* draw your watch face */
time.setToNow();
time.set(time.toMillis(false) + timeOffset);
if (isInAmbientMode() || backgroundImage == null) {
canvas.drawColor(Color.BLACK);
}
else {
canvas.drawBitmap(backgroundImage, 0, 0, bitmapPaint);
}
if (isDigital) {
drawDigital(canvas, bounds);
}
else {
drawAnalog(canvas, bounds);
}
}
private void drawAnalog(Canvas canvas, Rect bounds) {
float hour = time.hour + time.minute / 60f;
float minute = time.minute + time.second / 60f;
float second = time.second;
float radius = bounds.width() < bounds.height() ? bounds.width() / 2 : bounds.height() / 2;
float centerX = bounds.exactCenterX();
float centerY = bounds.exactCenterY();
// Draw tick marks
if (!isInAmbientMode()) {
for (int position = 0; position < 12; position++) {
for (int i = 0; i < NUM_DRAW_CALLS; i++) {
canvas.drawLine(
(float) (centerX + (radius * (100f - tickRadius) / 100f) * Math.cos(
Math.toRadians(
position * 30f))),
(float) (centerY + (radius * (100f - tickRadius) / 100f) * Math.sin(
Math.toRadians(
position * 30f))),
(float) (centerX + (radius) * Math.cos(Math.toRadians(position * 30f))),
(float) (centerY + (radius) * Math.sin(Math.toRadians(position * 30f))),
indicatorPaint);
}
}
}
Path hourPath = new Path();
hourPath.moveTo((float) (centerX + hourWidth / 2f * Math.cos(Math.toRadians(hour % 12 * 30f))),
(float) (centerY + hourWidth / 2f * Math.sin(Math.toRadians(hour % 12 * 30f))));
hourPath.quadTo(
(float) (centerX - (hourWidth / 2f) * Math.cos(Math.toRadians(
hour % 12 * 30f - 90f))),
(float) (centerY - (hourWidth / 2f) * Math.sin(Math.toRadians(hour % 12 * 30f - 90f))),
(float) (centerX + hourWidth / 2f * Math.cos(Math.toRadians(hour % 12 * 30f + 180f))),
(float) (centerY + hourWidth / 2f * Math.sin(Math.toRadians(hour % 12 * 30f + 180f))));
hourPath.lineTo((float) (centerX + (radius * hourRadius / 100) * Math.cos(Math.toRadians(
hour % 12 * 30f - 90f))),
(float) (centerY + (radius * hourRadius / 100) * Math.sin(Math.toRadians(hour % 12 * 30f - 90f))));
hourPath.close();
for (int i = 0; i < NUM_DRAW_CALLS; i++) {
canvas.drawPath(hourPath, hourPaint);
}
Path minutePath = new Path();
minutePath.moveTo((float) (centerX + minuteWidth / 2f * Math.cos(Math.toRadians(minute * 6f))),
(float) (centerY + minuteWidth / 2f * Math.sin(Math.toRadians(minute * 6f))));
minutePath.quadTo(
(float) (centerX - (minuteWidth / 2) * Math.cos(Math.toRadians(
minute * 6f - 90f))),
(float) (centerY + (minuteWidth / 2) * Math.sin(Math.toRadians(
minute * 6f - 90f))),
(float) (centerX + (minuteWidth / 2f) * Math.cos(Math.toRadians(minute * 6f + 180f))),
(float) (centerY + (minuteWidth / 2f) * Math.sin(Math.toRadians(minute * 6f + 180f))));
minutePath.lineTo((float) (centerX + (radius * minuteRadius / 100) * Math.cos(Math.toRadians(
minute * 6f - 90f))),
(float) (centerY + (radius * minuteRadius / 100) * Math.sin(Math.toRadians(
minute * 6f - 90f))));
minutePath.close();
for (int i = 0; i < NUM_DRAW_CALLS; i++) {
canvas.drawPath(minutePath, minutePaint);
}
if (!isInAmbientMode()) {
Path secondPath = new Path();
secondPath.moveTo((float) (centerX + secondWidth / 2f * Math.cos(Math.toRadians(second * 6f))),
(float) (centerY + secondWidth / 2f * Math.sin(Math.toRadians(second * 6f))));
secondPath.quadTo(
(float) (centerX - (secondWidth / 2) * Math.cos(Math.toRadians(second * 6f - 90f))),
(float) (centerY - (secondWidth / 2) * Math.sin(Math.toRadians(second * 6f - 90f))),
(float) (centerX + (secondWidth / 2f) * Math.cos(Math.toRadians(second * 6f + 180f))),
(float) (centerY + (secondWidth / 2f) * Math.sin(Math.toRadians(second * 6f + 180f))));
secondPath.lineTo(
(float) (centerX + (radius * secondRadius / 100) * Math.cos(Math.toRadians(
second * 6f - 90f))),
(float) (centerY + (radius * secondRadius / 100) * Math.sin(Math.toRadians(
second * 6f - 90f))));
secondPath.close();
for (int i = 0; i < NUM_DRAW_CALLS; i++) {
canvas.drawPath(secondPath, secondPaint);
}
}
}
private void drawDigital(Canvas canvas, Rect bounds) {
boolean isAmbient = isInAmbientMode();
// Show colons for the first half of each second so the colons blink on when the time
// updates.
boolean drawSeparator = isAmbient || (System.currentTimeMillis() % 1000) < 500;
// Draw the hours.
// Each is drawn NUM_DRAW_CALL(5) times to make shadow darker
float x = xOffset + (time.hour < 10 ? hourPaint.measureText("0") : 0);
float hourWidth = hourPaint.measureText("" + time.hour);
float minuteWidth = minutePaint.measureText(String.format("%02d", time.minute));
float yOffset = bounds.height() / 2;
if (drawSeparator) {
x += hourWidth;
for (int i = 0; i < NUM_DRAW_CALLS; i++){
canvas.drawText(timeSeparator, x, yOffset, indicatorPaint);
}
if (!isAmbient) {
x += separatorWidth;
x += minuteWidth;
for (int i = 0; i < NUM_DRAW_CALLS; i++){
canvas.drawText(timeSeparator, x, yOffset, indicatorPaint);
}
}
x = xOffset + (time.hour < 10 ? hourPaint.measureText("0") : 0);
}
for (int i = 0; i < NUM_DRAW_CALLS; i++) {
canvas.drawText("" + time.hour, x, yOffset, hourPaint);
}
x += hourWidth;
x += separatorWidth;
for (int i = 0; i < NUM_DRAW_CALLS; i++) {
canvas.drawText(String.format("%02d", time.minute), x, yOffset, minutePaint);
}
if (!isAmbient) {
x += separatorWidth;
x += minutePaint.measureText("" + (time.minute / 10));
x += minutePaint.measureText("" + (time.minute % 10));
for (int i = 0; i < NUM_DRAW_CALLS; i++) {
canvas.drawText(String.format("%02d", time.second), x, yOffset,
secondPaint);
}
}
}
@Override
public void onVisibilityChanged(boolean visible) {
/* the watch face became visible or invisible */
super.onVisibilityChanged(visible);
if (visible) {
registerReceivers();
// Update time zone in case it changed while we weren't visible.
time.clear(TimeZone.getDefault().getID());
time.setToNow();
} else {
unregisterReceivers();
}
updateTimer();
}
private void registerReceivers() {
if (registered) {
return;
}
LocalBroadcastManager.getInstance(WatchFace.this).registerReceiver(
localBroadcastReceiver,
localIntentFilter);
registerReceiver(timeZoneReceiver, timeZoneIntentFilter);
registered = true;
}
private void unregisterReceivers() {
if (!registered) {
return;
}
LocalBroadcastManager.getInstance(WatchFace.this).unregisterReceiver(
localBroadcastReceiver);
unregisterReceiver(timeZoneReceiver);
registered = false;
}
private boolean shouldTimerBeRunning() {
return isVisible() && !isInAmbientMode();
}
@Override
public void onPropertiesChanged(Bundle properties) {
super.onPropertiesChanged(properties);
lowBitMode = properties.getBoolean(PROPERTY_LOW_BIT_AMBIENT, false);
burnProtectionMode = properties.getBoolean(PROPERTY_BURN_IN_PROTECTION,
false);
}
}
}