package net.jimblackler.yourphotoswatch;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Rect;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.os.PowerManager;
import android.os.PowerManager.WakeLock;
import android.support.wearable.watchface.CanvasWatchFaceService;
import android.support.wearable.watchface.WatchFaceService;
import android.support.wearable.watchface.WatchFaceStyle;
import android.text.format.Time;
import android.view.SurfaceHolder;
import com.google.android.gms.common.api.GoogleApiClient;
import com.google.android.gms.common.api.ResultCallback;
import com.google.android.gms.common.data.FreezableUtils;
import com.google.android.gms.wearable.DataApi;
import com.google.android.gms.wearable.DataEvent;
import com.google.android.gms.wearable.DataEventBuffer;
import com.google.android.gms.wearable.DataItem;
import com.google.android.gms.wearable.DataItemBuffer;
import com.google.android.gms.wearable.DataMapItem;
import com.google.android.gms.wearable.Wearable;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;
import java.util.Random;
import java.util.TimeZone;
import java.util.concurrent.TimeUnit;
public abstract class BaseWatchService extends CanvasWatchFaceService {
private static final long INTERACTIVE_UPDATE_RATE_MS = TimeUnit.SECONDS.toMillis(1);
static float fractional(float number) {
return number - (long) number;
}
private void extraWake() {
PowerManager powerManager = (PowerManager) getSystemService(Context.POWER_SERVICE);
WakeLock wakeLock = powerManager.newWakeLock(PowerManager.SCREEN_BRIGHT_WAKE_LOCK, "Watch");
wakeLock.acquire(12 * 1000); // Hold the screen bright for some extra time.
}
@Override
public Engine onCreateEngine() {
return new Engine();
}
private class Engine extends CanvasWatchFaceService.Engine
implements DataApi.DataListener, GoogleApiClient.ConnectionCallbacks {
private static final int MINUTES_PER_HOUR = 60;
private static final int MSG_UPDATE_TIME = 0;
final Handler updateTimeHandler = new Handler() {
@Override
public void handleMessage(Message message) {
switch (message.what) {
case MSG_UPDATE_TIME:
invalidate();
if (shouldTimerBeRunning()) {
long timeMs = System.currentTimeMillis();
long delayMs = INTERACTIVE_UPDATE_RATE_MS
- (timeMs % INTERACTIVE_UPDATE_RATE_MS);
updateTimeHandler.sendEmptyMessageDelayed(MSG_UPDATE_TIME, delayMs);
}
break;
}
}
};
private static final int SECONDS_PER_MINUTE = 60;
private static final int SECONDS_PER_HOUR = MINUTES_PER_HOUR * SECONDS_PER_MINUTE;
private static final int SECONDS_PER_HALF_DAY = SECONDS_PER_HOUR * 12;
final BroadcastReceiver timeZoneReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
time.clear(intent.getStringExtra("time-zone"));
time.setToNow();
}
};
boolean lowBitAmbient;
private Paint allFill;
private Paint handStroke;
private Paint textStroke;
private Paint secondFill;
private Bitmap backgroundBitmap;
private Bitmap backgroundScaledBitmap;
private String currentPhoto;
private Map<String, Long> shownAt = new HashMap<>();
private GoogleApiClient googleApiClient;
private boolean mute;
private Map<String, DataItem> photos;
private boolean registeredTimeZoneReceiver = false;
private Time time;
@Override
public void onAmbientModeChanged(boolean inAmbientMode) {
super.onAmbientModeChanged(inAmbientMode);
if (isInAmbientMode()) {
setEmptyBackground();
} else {
extraWake();
}
if (lowBitAmbient) {
boolean antiAlias = !inAmbientMode;
handStroke.setAntiAlias(antiAlias);
textStroke.setAntiAlias(antiAlias);
allFill.setAntiAlias(antiAlias);
secondFill.setAntiAlias(antiAlias);
}
invalidate();
// Whether the timer should be running depends on whether we're in ambient mode (as well
// as whether we're visible), so we may need to start or stop the timer.
updateTimer();
}
@Override
public void onInterruptionFilterChanged(int interruptionFilter) {
super.onInterruptionFilterChanged(interruptionFilter);
boolean inMuteMode = (interruptionFilter == WatchFaceService.INTERRUPTION_FILTER_NONE);
if (mute != inMuteMode) {
mute = inMuteMode;
allFill.setAlpha(inMuteMode ? 100 : 255);
secondFill.setAlpha(inMuteMode ? 80 : 255);
invalidate();
}
}
@Override
public void onPropertiesChanged(Bundle properties) {
super.onPropertiesChanged(properties);
lowBitAmbient = properties.getBoolean(PROPERTY_LOW_BIT_AMBIENT, false);
}
@Override
public void onTimeTick() {
super.onTimeTick();
invalidate();
}
@Override
public void onCreate(SurfaceHolder holder) {
super.onCreate(holder);
setWatchFaceStyle(new WatchFaceStyle.Builder(BaseWatchService.this)
.setCardPeekMode(WatchFaceStyle.PEEK_MODE_SHORT)
.setBackgroundVisibility(WatchFaceStyle.BACKGROUND_VISIBILITY_INTERRUPTIVE)
.setShowSystemUiTime(false)
.build());
setEmptyBackground();
allFill = new Paint();
allFill.setStyle(Paint.Style.FILL);
allFill.setColor(Color.HSVToColor(255, new float[]{238, 0.75f, 0.50f}));
allFill.setStrokeWidth(5f);
allFill.setAntiAlias(true);
allFill.setStrokeCap(Paint.Cap.ROUND);
allFill.setTextSize(getDigitalTextSize());
handStroke = new Paint();
handStroke.setStyle(Paint.Style.STROKE);
handStroke.setColor(Color.WHITE);
handStroke.setStrokeWidth(10f);
handStroke.setAntiAlias(true);
handStroke.setStrokeCap(Paint.Cap.ROUND);
textStroke = new Paint();
textStroke.setStyle(Paint.Style.FILL);
textStroke.setColor(Color.WHITE);
textStroke.setAntiAlias(true);
textStroke.setTextSize(getDigitalTextSize());
secondFill = new Paint();
secondFill.setColor(Color.WHITE);
secondFill.setStrokeWidth(2.f);
secondFill.setAntiAlias(true);
secondFill.setStrokeCap(Paint.Cap.ROUND);
photos = new HashMap<>();
time = new Time();
googleApiClient = new GoogleApiClient.Builder(BaseWatchService.this)
.addApi(Wearable.API)
.addConnectionCallbacks(this)
.build();
googleApiClient.connect();
}
@Override
public void onVisibilityChanged(boolean visible) {
super.onVisibilityChanged(visible);
if (visible) {
registerReceiver();
time.clear(TimeZone.getDefault().getID());
time.setToNow();
} else {
unregisterReceiver();
}
updateTimer();
}
private void registerReceiver() {
if (registeredTimeZoneReceiver)
return;
registeredTimeZoneReceiver = true;
IntentFilter filter = new IntentFilter(Intent.ACTION_TIMEZONE_CHANGED);
BaseWatchService.this.registerReceiver(timeZoneReceiver, filter);
}
private void unregisterReceiver() {
if (!registeredTimeZoneReceiver)
return;
registeredTimeZoneReceiver = false;
BaseWatchService.this.unregisterReceiver(timeZoneReceiver);
}
private void setEmptyBackground() {
Drawable backgroundDrawable = getResources().getDrawable(R.drawable.background);
backgroundBitmap = ((BitmapDrawable) backgroundDrawable).getBitmap();
backgroundScaledBitmap = null;
currentPhoto = null;
invalidate();
}
private void updateTimer() {
updateTimeHandler.removeMessages(MSG_UPDATE_TIME);
if (shouldTimerBeRunning()) {
updateTimeHandler.sendEmptyMessage(MSG_UPDATE_TIME);
}
}
private boolean shouldTimerBeRunning() {
return isVisible() && !isInAmbientMode();
}
@Override
public void onConnected(Bundle connectionHint) {
Wearable.DataApi.addListener(googleApiClient, this);
Wearable.DataApi.getDataItems(googleApiClient).setResultCallback(new ResultCallback<DataItemBuffer>() {
@Override
public void onResult(DataItemBuffer dataItems) {
for (DataItem dataItem : dataItems) {
String[] parts = dataItem.getUri().getPath().split("/");
switch (parts[1]) {
case "image":
photos.put(parts[2], dataItem);
break;
default:
break;
}
}
invalidate();
}
});
}
@Override
public void onConnectionSuspended(int i) {
}
@Override
public void onDataChanged(DataEventBuffer dataEvents) {
ArrayList<DataEvent> dataEvents1 = FreezableUtils.freezeIterable(dataEvents);
dataEvents.close();
for (DataEvent event : dataEvents1) {
DataItem dataItem = event.getDataItem();
String[] parts = dataItem.getUri().getPath().split("/");
switch (parts[1]) {
case "image":
String photoId = parts[2];
switch (event.getType()) {
case DataEvent.TYPE_CHANGED:
photos.put(photoId, dataItem);
// Show new image immediately (even in ambient mode).
updateImage(dataItem);
extraWake();
break;
case DataEvent.TYPE_DELETED:
photos.remove(photoId);
if (currentPhoto.equals(photoId))
setEmptyBackground();
break;
}
}
}
}
void updateImage(DataItem dataItem) {
String[] parts = dataItem.getUri().getPath().split("/");
currentPhoto = parts[2];
shownAt.put(currentPhoto, System.currentTimeMillis());
Wearable.DataApi.getFdForAsset(googleApiClient,
DataMapItem.fromDataItem(dataItem).getDataMap().getAsset("photo")).
setResultCallback(new ResultCallback<DataApi.GetFdForAssetResult>() {
@Override
public void onResult(DataApi.GetFdForAssetResult fd) {
Bitmap bitmap = BitmapFactory.decodeStream(fd.getInputStream());
if (bitmap == null)
return;
backgroundBitmap = bitmap;
backgroundScaledBitmap = null;
invalidate();
}
});
}
@Override
public void onDestroy() {
updateTimeHandler.removeMessages(MSG_UPDATE_TIME);
Wearable.DataApi.removeListener(googleApiClient, this);
super.onDestroy();
}
@Override
public void onDraw(Canvas canvas, Rect bounds) {
time.setToNow();
if (false) { // Screenshot mode.
time.set(36, 10, 10, 1, 1, 2014);
}
int width = bounds.width();
int height = bounds.height();
Bitmap clockBitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
Canvas clockCanvas = new Canvas(clockBitmap);
float centerX = width / 2f;
float centerY = height / 2f;
if (isAnalog()) {
float innerTickRadius = centerX - 9;
float topInnerTickRadius = centerX - 20;
float outerTickRadius = centerX - 6;
for (int tickIndex = 0; tickIndex < 12; tickIndex++) {
float tickRotation = (float) (tickIndex * Math.PI * 2 / 12);
float useInnerTickRadius = tickIndex == 0 ? topInnerTickRadius : innerTickRadius;
float innerX = (float) Math.sin(tickRotation) * useInnerTickRadius;
float innerY = (float) -Math.cos(tickRotation) * useInnerTickRadius;
float outerX = (float) Math.sin(tickRotation) * outerTickRadius;
float outerY = (float) -Math.cos(tickRotation) * outerTickRadius;
clockCanvas.drawLine(centerX + innerX, centerY + innerY,
centerX + outerX, centerY + outerY, handStroke);
clockCanvas.drawLine(centerX + innerX, centerY + innerY,
centerX + outerX, centerY + outerY, allFill);
}
float totalMinutes = time.minute + time.hour * MINUTES_PER_HOUR;
float totalSeconds = time.second + totalMinutes * SECONDS_PER_MINUTE;
float twoPi = (float) (Math.PI * 2);
float secondRotation = fractional(totalSeconds / SECONDS_PER_MINUTE) * twoPi;
float minuteRotation = fractional(totalSeconds / SECONDS_PER_HOUR) * twoPi;
float hourRotation = fractional(totalSeconds / SECONDS_PER_HALF_DAY) * twoPi;
float secondLength = centerX - 20;
float secondLengthReverse = -25;
float minuteLength = centerX - 35;
float hourLength = centerX - 90;
if (!isInAmbientMode()) {
float secondX0 = (float) Math.sin(secondRotation) * secondLengthReverse;
float secondY0 = (float) -Math.cos(secondRotation) * secondLengthReverse;
float secondX1 = (float) Math.sin(secondRotation) * secondLength;
float secondY1 = (float) -Math.cos(secondRotation) * secondLength;
clockCanvas.drawLine(centerX + secondX0, centerY + secondY0,
centerX + secondX1, centerY + secondY1, secondFill);
}
float minuteX = (float) Math.sin(minuteRotation) * minuteLength;
float minuteY = (float) -Math.cos(minuteRotation) * minuteLength;
float hourX = (float) Math.sin(hourRotation) * hourLength;
float hourY = (float) -Math.cos(hourRotation) * hourLength;
clockCanvas.drawLine(centerX, centerY, centerX + minuteX, centerY + minuteY, handStroke);
clockCanvas.drawLine(centerX, centerY, centerX + hourX, centerY + hourY, handStroke);
clockCanvas.drawLine(centerX, centerY, centerX + minuteX, centerY + minuteY, allFill);
clockCanvas.drawLine(centerX, centerY, centerX + hourX, centerY + hourY, allFill);
}
if (!isInAmbientMode() && currentPhoto == null && !photos.isEmpty()) {
long earliestShown = System.currentTimeMillis();
String photoToShow = null;
int unshownCount = 0;
Random random = new Random();
for (String photoId : photos.keySet()) {
if (shownAt.containsKey(photoId)) {
if (unshownCount > 0)
continue;
long shown = shownAt.get(photoId);
if (shown > earliestShown)
continue;
photoToShow = photoId;
earliestShown = shown;
} else {
unshownCount++;
if (random.nextInt(unshownCount) == 0)
photoToShow = photoId;
}
}
updateImage(photos.get(photoToShow));
}
if (isDigital()) {
int hours;
if (true) { // 12 hour
hours = time.hour % 12;
if (hours == 0)
hours += 12;
}
String text;
if (isAmPm()) {
text = String.format("%d:%02d %s", hours, time.minute, (time.hour < 12) ? "AM" : "PM");
} else {
text = String.format("%d:%02d", hours, time.minute);
}
float textWidth = textStroke.measureText(text);
clockCanvas.drawText(text, centerX - textWidth / 2, centerY - getDigitalOffset(), textStroke);
}
if (backgroundScaledBitmap == null
|| backgroundScaledBitmap.getWidth() != width
|| backgroundScaledBitmap.getHeight() != height) {
backgroundScaledBitmap =
Bitmap.createScaledBitmap(backgroundBitmap, width, height, true);
}
canvas.drawBitmap(backgroundScaledBitmap, 0, 0, null);
Bitmap shadowed = BitmapEffect.createShadow(clockBitmap);
canvas.drawBitmap(shadowed, 0, 0, null);
Paint draw = new Paint();
if (currentPhoto != null) {
draw.setAlpha(180);
}
canvas.drawBitmap(clockBitmap, 0, 0, draw);
shadowed.recycle();
clockBitmap.recycle();
}
}
protected abstract boolean isAmPm();
protected abstract float getDigitalTextSize();
protected abstract float getDigitalOffset();
protected abstract boolean isAnalog();
protected abstract boolean isDigital();
}