/*
* Copyright (C) 2014 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 org.bookdash.android;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.res.Resources;
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.Typeface;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.support.v4.content.ContextCompat;
import android.support.wearable.watchface.CanvasWatchFaceService;
import android.support.wearable.watchface.WatchFaceStyle;
import android.view.SurfaceHolder;
import android.view.WindowInsets;
import com.jakewharton.threetenabp.AndroidThreeTen;
import org.threeten.bp.ZonedDateTime;
import org.threeten.bp.format.DateTimeFormatter;
import java.lang.ref.WeakReference;
import java.util.concurrent.TimeUnit;
/**
* Digital watch face with seconds. In ambient mode, the seconds aren't displayed. On devices with
* low-bit ambient mode, the text is drawn without anti-aliasing in ambient mode.
*/
public class OwlWatchFace extends CanvasWatchFaceService {
private static final DateTimeFormatter TIME_FORMATTER = DateTimeFormatter.ofPattern("HH:mm");
private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("EEE, MMM d");
/**
* Update rate in milliseconds for interactive mode. We update once a second since seconds are
* displayed in interactive mode.
*/
private static final long INTERACTIVE_UPDATE_RATE_MS = TimeUnit.SECONDS.toMillis(1);
/**
* Handler message id for updating the time periodically in interactive mode.
*/
private static final int MSG_UPDATE_TIME = 0;
private Bitmap backgroundScaledBitmap;
@Override
public Engine onCreateEngine() {
return new Engine();
}
private static class EngineHandler extends Handler {
private final WeakReference<OwlWatchFace.Engine> mWeakReference;
EngineHandler(OwlWatchFace.Engine reference) {
mWeakReference = new WeakReference<>(reference);
}
@Override
public void handleMessage(Message msg) {
OwlWatchFace.Engine engine = mWeakReference.get();
if (engine != null) {
switch (msg.what) {
case MSG_UPDATE_TIME:
engine.handleUpdateTimeMessage();
break;
}
}
}
}
private class Engine extends CanvasWatchFaceService.Engine {
private static final String FONT_NAME_WATCHFACE = "fonts/minyna.ttf";
final Handler updateTimeHandler = new EngineHandler(this);
boolean registeredTimeZoneReceiver = false;
Paint backgroundPaint;
Bitmap owlBackgroundBitmap;
Paint textTimePaint;
Paint textPaintSmall;
boolean ambient;
ZonedDateTime currentTime;
final BroadcastReceiver timeZoneReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
AndroidThreeTen.init(getApplication());
currentTime = ZonedDateTime.now();
}
};
int tapCount;
float xOffset;
float xOffsetDate;
float yOffset;
float yOffsetDate;
/**
* Whether the display supports fewer bits for each color in ambient mode. When true, we
* disable anti-aliasing in ambient mode.
*/
boolean lowBitAmbient;
private BatteryStatusHelper batteryStatusHelper;
private float yOffsetBattery;
private float xOffsetBattery;
private Bitmap owlVectorAmbient;
private Bitmap ambientScaledBitmap;
private int width, height;
@Override
public void onDestroy() {
updateTimeHandler.removeMessages(MSG_UPDATE_TIME);
super.onDestroy();
}
@Override
public void onSurfaceChanged(SurfaceHolder holder, int format, int width, int height) {
this.width = width;
this.height = height;
if (backgroundScaledBitmap == null || backgroundScaledBitmap.getWidth() != width || backgroundScaledBitmap
.getHeight() != height) {
backgroundScaledBitmap = Bitmap
.createScaledBitmap(owlBackgroundBitmap, width, height, true /* filter */);
}
if (ambientScaledBitmap == null || ambientScaledBitmap.getWidth() != width || ambientScaledBitmap
.getHeight() != height) {
ambientScaledBitmap = Bitmap.createScaledBitmap(owlVectorAmbient, width, height, true /* filter */);
}
super.onSurfaceChanged(holder, format, width, height);
}
@Override
public void onDraw(Canvas canvas, Rect bounds) {
// Draw the background.
if (isInAmbientMode()) {
canvas.drawColor(Color.BLACK);
canvas.drawBitmap(ambientScaledBitmap, 0, 0, null);
} else {
canvas.drawBitmap(backgroundScaledBitmap, 0, 0, null);
}
currentTime = ZonedDateTime.now();
String time = currentTime.format(TIME_FORMATTER);
if (isInAmbientMode()) {
int colorWhite = ContextCompat.getColor(getApplicationContext(), R.color.white);
textTimePaint.setColor(colorWhite);
textPaintSmall.setColor(colorWhite);
} else {
int colorBlack = ContextCompat.getColor(getApplicationContext(), R.color.black);
textTimePaint.setColor(colorBlack);
textPaintSmall.setColor(colorBlack);
}
canvas.drawText(time, width - xOffset, yOffset, textTimePaint);
String date = currentTime.format(DATE_TIME_FORMATTER);
canvas.drawText(date, width - xOffsetDate, yOffsetDate, textPaintSmall);
if (!isInAmbientMode()) {
canvas.drawText(batteryStatusHelper.getBatteryPercentage() + "%", width - xOffsetBattery, yOffsetBattery,
textPaintSmall);
}
super.onDraw(canvas, bounds);
}
@Override
public void onApplyWindowInsets(WindowInsets insets) {
super.onApplyWindowInsets(insets);
// Load resources that have alternate values for round watches.
Resources resources = OwlWatchFace.this.getResources();
boolean isRound = insets.isRound();
xOffset = resources.getDimensionPixelOffset(R.dimen.digital_x_offset);
xOffsetDate = resources
.getDimensionPixelOffset(R.dimen.digital_date_offset_x);
xOffsetBattery = resources
.getDimensionPixelOffset(R.dimen.digital_battery_offset_x);
yOffset = resources.getDimensionPixelOffset(isRound ? R.dimen.digital_y_offset : R.dimen.digital_y_offset_square);
yOffsetDate = resources
.getDimensionPixelOffset(isRound ? R.dimen.digital_date_offset_y: R.dimen.digital_date_offset_y_square);
yOffsetBattery = resources
.getDimensionPixelOffset(isRound ? R.dimen.digital_battery_offset_y: R.dimen.digital_battery_offset_y_square);
float textSize = resources
.getDimensionPixelSize( R.dimen.digital_text_size);
float textSizeSmall = resources
.getDimensionPixelSize(R.dimen.digital_text_size_small);
textTimePaint.setTextSize(textSize);
textPaintSmall.setTextSize(textSizeSmall);
}
@Override
public void onAmbientModeChanged(boolean inAmbientMode) {
super.onAmbientModeChanged(inAmbientMode);
if (ambient != inAmbientMode) {
ambient = inAmbientMode;
if (lowBitAmbient) {
textTimePaint.setAntiAlias(!inAmbientMode);
textPaintSmall.setAntiAlias(!inAmbientMode);
}
invalidate();
}
// Whether the timer should be running depends on whether we're visible (as well as
// whether we're in ambient mode), so we may need to start or stop the timer.
updateTimer();
}
@Override
public void onPropertiesChanged(Bundle properties) {
super.onPropertiesChanged(properties);
lowBitAmbient = properties.getBoolean(PROPERTY_LOW_BIT_AMBIENT, false);
}
@Override
public void onTimeTick() {
super.onTimeTick();
invalidate();
}
/**
* Captures tap event (and tap type) and toggles the background color if the user finishes
* a tap.
*/
@Override
public void onTapCommand(int tapType, int x, int y, long eventTime) {
switch (tapType) {
case TAP_TYPE_TOUCH:
// The user has started touching the screen.
break;
case TAP_TYPE_TOUCH_CANCEL:
// The user has started a different gesture or otherwise cancelled the tap.
break;
case TAP_TYPE_TAP:
// The user has completed the tap gesture.
tapCount++;
backgroundPaint.setColor(ContextCompat.getColor(getApplicationContext(),
tapCount % 2 == 0 ? R.color.background : R.color.background2));
break;
}
invalidate();
}
@Override
public void onCreate(SurfaceHolder holder) {
super.onCreate(holder);
AndroidThreeTen.init(getApplication());
setWatchFaceStyle(
new WatchFaceStyle.Builder(OwlWatchFace.this).setCardPeekMode(WatchFaceStyle.PEEK_MODE_SHORT)
.setAmbientPeekMode(WatchFaceStyle.PEEK_MODE_SHORT)
.setBackgroundVisibility(WatchFaceStyle.BACKGROUND_VISIBILITY_INTERRUPTIVE)
// .setStatusBarGravity(Gravity.TOP | Gravity.RIGHT)
.setShowUnreadCountIndicator(true)
//.setShowSystemUiTime(false)
.setAcceptsTapEvents(true).build());
Resources resources = OwlWatchFace.this.getResources();
backgroundPaint = new Paint();
backgroundPaint.setColor(ContextCompat.getColor(getApplicationContext(), R.color.background));
owlBackgroundBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.owl);
textTimePaint = new Paint();
textTimePaint = createTextPaint(ContextCompat.getColor(getApplicationContext(), R.color.digital_text));
textPaintSmall = new Paint();
textPaintSmall = createTextPaint(ContextCompat.getColor(getApplicationContext(), R.color.black));
textTimePaint.setTextAlign(Paint.Align.RIGHT);
textPaintSmall.setTextAlign(Paint.Align.RIGHT);
owlVectorAmbient = BitmapFactory.decodeResource(getResources(), R.drawable.lonely_owl_thick);
currentTime = ZonedDateTime.now();
batteryStatusHelper = new BatteryStatusHelper(getApplicationContext());
}
private Paint createTextPaint(int textColor) {
Paint paint = new Paint();
paint.setColor(textColor);
Typeface typeface = Typeface.createFromAsset(getAssets(), FONT_NAME_WATCHFACE);
paint.setTypeface(typeface);
paint.setAntiAlias(true);
return paint;
}
@Override
public void onVisibilityChanged(boolean visible) {
super.onVisibilityChanged(visible);
if (visible) {
registerReceiver();
// Update time zone in case it changed while we weren't visible.
currentTime = ZonedDateTime.now();
} else {
unregisterReceiver();
}
// Whether the timer should be running depends on whether we're visible (as well as
// whether we're in ambient mode), so we may need to start or stop the timer.
updateTimer();
}
private void registerReceiver() {
if (registeredTimeZoneReceiver) {
return;
}
registeredTimeZoneReceiver = true;
IntentFilter filter = new IntentFilter(Intent.ACTION_TIMEZONE_CHANGED);
OwlWatchFace.this.registerReceiver(timeZoneReceiver, filter);
}
private void unregisterReceiver() {
if (!registeredTimeZoneReceiver) {
return;
}
registeredTimeZoneReceiver = false;
OwlWatchFace.this.unregisterReceiver(timeZoneReceiver);
}
/**
* Starts the {@link #updateTimeHandler} timer if it should be running and isn't currently
* or stops it if it shouldn't be running but currently is.
*/
private void updateTimer() {
updateTimeHandler.removeMessages(MSG_UPDATE_TIME);
if (shouldTimerBeRunning()) {
updateTimeHandler.sendEmptyMessage(MSG_UPDATE_TIME);
}
}
/**
* Returns whether the {@link #updateTimeHandler} timer should be running. The timer should
* only run when we're visible and in interactive mode.
*/
private boolean shouldTimerBeRunning() {
return isVisible() && !isInAmbientMode();
}
/**
* Handle updating the time periodically in interactive mode.
*/
private void handleUpdateTimeMessage() {
invalidate();
if (shouldTimerBeRunning()) {
long timeMs = System.currentTimeMillis();
long delayMs = INTERACTIVE_UPDATE_RATE_MS - (timeMs % INTERACTIVE_UPDATE_RATE_MS);
updateTimeHandler.sendEmptyMessageDelayed(MSG_UPDATE_TIME, delayMs);
}
}
}
}