/*
* 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.gdg.frisbee.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.ColorMatrix;
import android.graphics.ColorMatrixColorFilter;
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.text.format.Time;
import android.util.Log;
import android.view.Gravity;
import android.view.SurfaceHolder;
import com.google.android.gms.common.ConnectionResult;
import com.google.android.gms.common.api.GoogleApiClient;
import com.google.android.gms.common.api.ResultCallback;
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.DataMap;
import com.google.android.gms.wearable.DataMapItem;
import com.google.android.gms.wearable.Wearable;
import java.util.Locale;
import java.util.TimeZone;
import java.util.concurrent.TimeUnit;
import timber.log.Timber;
/**
* Analog watch face with a ticking second hand. In ambient mode, the second hand isn't shown. On
* devices with low-bit ambient mode, the hands are drawn without anti-aliasing in ambient mode.
*/
public class GdgWatchFace extends CanvasWatchFaceService {
private static final String TAG = "GdgWatchFace";
/**
* Update rate in milliseconds for interactive mode. We update once a second to advance the
* second hand.
*/
private static final long INTERACTIVE_UPDATE_RATE_MS = TimeUnit.SECONDS.toMillis(1);
@Override
public Engine onCreateEngine() {
return new Engine();
}
private class Engine extends CanvasWatchFaceService.Engine implements DataApi.DataListener {
private static final float HAND_END_CAP_RADIUS = 5f;
/**
* Handler to update the time once a second in interactive mode.
*/
final Handler mUpdateTimeHandler = new Handler() {
@Override
public void handleMessage(Message message) {
if (R.id.message_update_time == message.what) {
invalidate();
if (shouldTimerBeRunning()) {
long timeMs = System.currentTimeMillis();
long delayMs = INTERACTIVE_UPDATE_RATE_MS
- (timeMs % INTERACTIVE_UPDATE_RATE_MS);
mUpdateTimeHandler.sendEmptyMessageDelayed(R.id.message_update_time, delayMs);
}
}
}
};
Paint mBackgroundPaint;
Paint mHourHandPaint;
Paint mMinuteHandPaint;
Paint mSecondHandPaint;
Paint mHourMarkerPaint;
Paint mDateTimePaint;
Bitmap mBackgroundBitmap;
Bitmap mGrayBackgroundBitmap;
int mBackgroundColor;
int mTimeSetting;
boolean mAmbient;
boolean mLightMode = false;
boolean mDisplayDate = true;
boolean mDisplayTime = true;
Time mTime;
final BroadcastReceiver mTimeZoneReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
mTime.clear(intent.getStringExtra("time-zone"));
mTime.setToNow();
}
};
boolean mRegisteredTimeZoneReceiver = false;
/**
* Whether the display supports fewer bits for each color in ambient mode. When true, we
* disable anti-aliasing in ambient mode.
*/
boolean mLowBitAmbient;
boolean mBurnInProtection;
private Rect mCardBounds = new Rect();
private float mHourHandLength;
private float mMinuteHandLength;
private float mSecondHandLength;
private int mWidth;
private int mHeight;
private float mCenterX;
private float mCenterY;
private GoogleApiClient mGoogleApiClient;
@Override
public void onCreate(SurfaceHolder holder) {
super.onCreate(holder);
setWatchFaceStyle(new WatchFaceStyle.Builder(GdgWatchFace.this)
.setCardPeekMode(WatchFaceStyle.PEEK_MODE_SHORT)
.setBackgroundVisibility(WatchFaceStyle.BACKGROUND_VISIBILITY_INTERRUPTIVE)
.setShowSystemUiTime(false)
.setViewProtectionMode(WatchFaceStyle.PROTECT_STATUS_BAR | WatchFaceStyle.PROTECT_HOTWORD_INDICATOR)
.setHotwordIndicatorGravity(Gravity.CENTER_HORIZONTAL | Gravity.CENTER_VERTICAL)
.setStatusBarGravity(Gravity.TOP | Gravity.CENTER_HORIZONTAL)
.setPeekOpacityMode(WatchFaceStyle.PEEK_OPACITY_MODE_TRANSLUCENT)
.build());
Resources resources = GdgWatchFace.this.getResources();
mBackgroundPaint = new Paint();
mBackgroundPaint.setColor(ContextCompat.getColor(GdgWatchFace.this, R.color.gdg_black));
mBackgroundBitmap = BitmapFactory.decodeResource(resources, R.drawable.gdg_logo);
mBackgroundColor = Color.BLACK;
mHourHandPaint = new Paint();
mHourHandPaint.setColor(ContextCompat.getColor(GdgWatchFace.this, R.color.gdg_gray));
mHourHandPaint.setStrokeWidth(resources.getDimension(R.dimen.watch_hand_stroke));
mHourHandPaint.setAntiAlias(true);
mHourHandPaint.setStrokeCap(Paint.Cap.ROUND);
mMinuteHandPaint = new Paint();
mMinuteHandPaint.setColor(ContextCompat.getColor(GdgWatchFace.this, R.color.gdg_gray));
mMinuteHandPaint.setStrokeWidth(resources.getDimension(R.dimen.watch_hand_stroke));
mMinuteHandPaint.setAntiAlias(true);
mMinuteHandPaint.setStrokeCap(Paint.Cap.ROUND);
mSecondHandPaint = new Paint();
mSecondHandPaint.setColor(ContextCompat.getColor(GdgWatchFace.this, R.color.gdg_white));
mSecondHandPaint.setStrokeWidth(resources.getDimension(R.dimen.second_hand_stroke));
mSecondHandPaint.setAntiAlias(true);
mSecondHandPaint.setStrokeCap(Paint.Cap.ROUND);
mHourMarkerPaint = new Paint();
mHourMarkerPaint.setColor(Color.WHITE);
mHourMarkerPaint.setStrokeWidth(resources.getDimension(R.dimen.hour_marker_stroke));
mHourMarkerPaint.setAntiAlias(true);
mDateTimePaint = new Paint();
mDateTimePaint.setColor(ContextCompat.getColor(GdgWatchFace.this, R.color.gdg_white));
mDateTimePaint.setTypeface(Typeface.create(Typeface.SANS_SERIF, Typeface.BOLD));
mDateTimePaint.setTextSize(resources.getDimension(R.dimen.font_hour_marker));
mDateTimePaint.setAntiAlias(true);
mTime = new Time();
mGoogleApiClient = new GoogleApiClient.Builder(GdgWatchFace.this)
.addConnectionCallbacks(new GoogleApiClient.ConnectionCallbacks() {
@Override
public void onConnected(Bundle bundle) {
Timber.d("onConnected:" + bundle);
Wearable.DataApi.addListener(mGoogleApiClient, Engine.this);
updateConfigDataItemAndUi();
}
@Override
public void onConnectionSuspended(int i) {
Timber.d("onConnectionSuspended:" + i);
}
})
.addOnConnectionFailedListener(new GoogleApiClient.OnConnectionFailedListener() {
@Override
public void onConnectionFailed(ConnectionResult connectionResult) {
Timber.d("onConnectionFailed");
}
})
.addApi(Wearable.API)
.build();
}
@Override
public void onDestroy() {
mUpdateTimeHandler.removeMessages(R.id.message_update_time);
super.onDestroy();
}
@Override
public void onPropertiesChanged(Bundle properties) {
super.onPropertiesChanged(properties);
mLowBitAmbient = properties.getBoolean(PROPERTY_LOW_BIT_AMBIENT, false);
mBurnInProtection = properties.getBoolean(PROPERTY_BURN_IN_PROTECTION, false);
}
@Override
public void onTimeTick() {
super.onTimeTick();
invalidate();
}
@Override
public void onAmbientModeChanged(boolean inAmbientMode) {
super.onAmbientModeChanged(inAmbientMode);
if (mAmbient != inAmbientMode) {
mAmbient = inAmbientMode;
if (mLowBitAmbient || mBurnInProtection) {
mHourHandPaint.setAntiAlias(!inAmbientMode);
mMinuteHandPaint.setAntiAlias(!inAmbientMode);
mSecondHandPaint.setAntiAlias(!inAmbientMode);
mHourMarkerPaint.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 onSurfaceChanged(SurfaceHolder holder, int format, int width, int height) {
super.onSurfaceChanged(holder, format, width, height);
mWidth = width;
mHeight = height;
mCenterX = mWidth / 2f;
mCenterY = mHeight / 2f;
mHourHandLength = mCenterX * 0.3f;
mMinuteHandLength = mCenterX * 0.5f;
mSecondHandLength = mCenterX * 0.7f;
float scale = ((float) width / (float) mBackgroundBitmap.getWidth());
mBackgroundBitmap = Bitmap.createScaledBitmap(mBackgroundBitmap,
(int) (mBackgroundBitmap.getWidth() * scale),
(int) (mBackgroundBitmap.getHeight() * scale), true);
if (!mBurnInProtection || !mLowBitAmbient) {
initializeGrayBackgroundBitmap();
}
}
private void initializeGrayBackgroundBitmap() {
mGrayBackgroundBitmap = Bitmap.createBitmap(mBackgroundBitmap.getWidth(), mBackgroundBitmap.getHeight(), Bitmap.Config.ARGB_8888);
ColorMatrix colorMatrix = new ColorMatrix();
colorMatrix.setSaturation(0);
ColorMatrixColorFilter filter = new ColorMatrixColorFilter(colorMatrix);
Paint grayPaint = new Paint();
grayPaint.setColorFilter(filter);
Canvas canvas = new Canvas(mGrayBackgroundBitmap);
canvas.drawBitmap(mBackgroundBitmap, 0, 0, grayPaint);
}
@Override
public void onDraw(Canvas canvas, Rect bounds) {
mTime.setToNow();
if (mAmbient && (mLowBitAmbient || mBurnInProtection)) {
canvas.drawColor(Color.BLACK);
} else if (mAmbient) {//TODO gray ambient BG for light mode
canvas.drawBitmap(mGrayBackgroundBitmap, 0, 0, mBackgroundPaint);
} else {
canvas.drawColor(mBackgroundColor);
canvas.drawBitmap(mBackgroundBitmap, 0, 0, mBackgroundPaint);
}
float textHeightOffset = (mDateTimePaint.descent() + mDateTimePaint.ascent()) / 2f;
float innerTickRadius = mCenterX - 25;
float outerTickRadius = mCenterX;
for (int tickIndex = 0; tickIndex < 12; tickIndex++) {
float tickRot = (float) (tickIndex * Math.PI * 2 / 12);
float innerX = (float) Math.sin(tickRot) * innerTickRadius;
float innerY = (float) -Math.cos(tickRot) * innerTickRadius;
float outerX = (float) Math.sin(tickRot) * outerTickRadius;
float outerY = (float) -Math.cos(tickRot) * outerTickRadius;
canvas.drawLine(mCenterX + innerX, mCenterY + innerY,
mCenterX + outerX, mCenterY + outerY, getAdjustedPaintColor(mHourMarkerPaint));
}
if (mDisplayDate) {
canvas.drawText(formatTwoDigitNumber(mTime.monthDay), mCenterX + mMinuteHandLength,
mCenterY - textHeightOffset, getAdjustedPaintColor(mDateTimePaint));
}
if(mDisplayTime) {
canvas.drawText(formatHour(mTime.hour) + ":" + formatTwoDigitNumber(mTime.minute),
mCenterX - mSecondHandLength, mCenterY - textHeightOffset, getAdjustedPaintColor(mDateTimePaint));
}
/*
* These calculations reflect the rotation in degrees per unit of
* time, e.g. 360 / 60 = 6 and 360 / 12 = 30
*/
final float secondsRotation = mTime.second * 6f;
final float minutesRotation = mTime.minute * 6f;
// account for the offset of the hour hand due to minutes of the hour.
final float hourHandOffset = mTime.minute / 2f;
final float hoursRotation = (mTime.hour * 30) + hourHandOffset;
// save the canvas state before we begin to rotate it
canvas.save();
canvas.rotate(hoursRotation, mCenterX, mCenterY);
canvas.drawLine(mCenterX, mCenterY - HAND_END_CAP_RADIUS, mCenterX,
mCenterY - mHourHandLength, getAdjustedPaintColor(mHourHandPaint));
canvas.rotate(minutesRotation - hoursRotation, mCenterX, mCenterY);
canvas.drawLine(mCenterX, mCenterY - HAND_END_CAP_RADIUS, mCenterX,
mCenterY - mMinuteHandLength, getAdjustedPaintColor(mMinuteHandPaint));
if (!mAmbient) {
canvas.rotate(secondsRotation - minutesRotation, mCenterX, mCenterY);
canvas.drawLine(mCenterX, mCenterY - HAND_END_CAP_RADIUS, mCenterX,
mCenterY - mSecondHandLength, mSecondHandPaint);
}
canvas.drawCircle(mCenterX, mCenterY, HAND_END_CAP_RADIUS, getAdjustedPaintColor(mHourHandPaint));
// restore the canvas' original orientation.
canvas.restore();
if (mAmbient) {
canvas.drawRect(mCardBounds, mBackgroundPaint);
}
}
@Override
public void onVisibilityChanged(boolean visible) {
super.onVisibilityChanged(visible);
if (visible) {
registerReceiver();
mGoogleApiClient.connect();
// Update time zone in case it changed while we weren't visible.
mTime.clear(TimeZone.getDefault().getID());
mTime.setToNow();
} else {
unregisterReceiver();
if (mGoogleApiClient != null && mGoogleApiClient.isConnected()) {
Wearable.DataApi.removeListener(mGoogleApiClient, this);
mGoogleApiClient.disconnect();
}
}
// 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 onPeekCardPositionUpdate(Rect rect) {
super.onPeekCardPositionUpdate(rect);
mCardBounds.set(rect);
}
private void registerReceiver() {
if (mRegisteredTimeZoneReceiver) {
return;
}
mRegisteredTimeZoneReceiver = true;
IntentFilter filter = new IntentFilter(Intent.ACTION_TIMEZONE_CHANGED);
GdgWatchFace.this.registerReceiver(mTimeZoneReceiver, filter);
}
private void unregisterReceiver() {
if (!mRegisteredTimeZoneReceiver) {
return;
}
mRegisteredTimeZoneReceiver = false;
GdgWatchFace.this.unregisterReceiver(mTimeZoneReceiver);
}
/**
* Starts the {@link #mUpdateTimeHandler} 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() {
mUpdateTimeHandler.removeMessages(R.id.message_update_time);
if (shouldTimerBeRunning()) {
mUpdateTimeHandler.sendEmptyMessage(R.id.message_update_time);
}
}
/**
* Returns whether the {@link #mUpdateTimeHandler} timer should be running. The timer should
* only run when we're visible and in interactive mode.
*/
private boolean shouldTimerBeRunning() {
return isVisible() && !isInAmbientMode();
}
private Paint getAdjustedPaintColor(Paint paint) {
if (mAmbient) {
int ambientColor = mLightMode ? R.color.black : R.color.gdg_gray;
paint = new Paint(paint);
paint.setColor(ContextCompat.getColor(GdgWatchFace.this, ambientColor));
}
return paint;
}
private String formatHour(int hour) {
int hourToDisplay = hour;
if(mTimeSetting == WearableConfigurationUtil.TIME_12_HOUR) {
hourToDisplay = (hour % WearableConfigurationUtil.TIME_12_HOUR) == 0 ?
WearableConfigurationUtil.TIME_12_HOUR : hour % WearableConfigurationUtil.TIME_12_HOUR;
}
return String.format(Locale.getDefault(), "%02d", hourToDisplay);
}
private String formatTwoDigitNumber(int number) {
return String.format(Locale.getDefault(), "%02d", number);
}
@Override
public void onDataChanged(DataEventBuffer dataEvents) {
for (DataEvent dataEvent : dataEvents) {
if (dataEvent.getType() != DataEvent.TYPE_CHANGED) {
continue;
}
DataItem dataItem = dataEvent.getDataItem();
if (!dataItem.getUri().getPath().equals(WearableConfigurationUtil.PATH_ANALOG)) {
continue;
}
DataMapItem dataMapItem = DataMapItem.fromDataItem(dataItem);
DataMap dataMap = dataMapItem.getDataMap();
Timber.d("Config DataItem updated:" + dataMap);
updateUi(dataMap);
}
}
private void updateConfigDataItemAndUi() {
WearableConfigurationUtil.fetchConfigDataMap(mGoogleApiClient,
WearableConfigurationUtil.PATH_ANALOG,
new ResultCallback<DataApi.DataItemResult>() {
@Override
public void onResult(DataApi.DataItemResult dataItemResult) {
if (dataItemResult.getStatus().isSuccess()) {
if (dataItemResult.getDataItem() != null) {
DataItem configDataItem = dataItemResult.getDataItem();
DataMapItem dataMapItem = DataMapItem.fromDataItem(configDataItem);
DataMap config = dataMapItem.getDataMap();
updateUi(config);
}
}
}
});
}
private void updateUi(DataMap dataMap) {
if (dataMap.containsKey(WearableConfigurationUtil.CONFIG_BACKGROUND)) {
int background = dataMap.getInt(WearableConfigurationUtil.CONFIG_BACKGROUND);
updateBackground(background);
}
if (dataMap.containsKey(WearableConfigurationUtil.CONFIG_DATE_TIME)) {
int color = dataMap.getInt(WearableConfigurationUtil.CONFIG_DATE_TIME);
updateDateTimeColor(color);
}
if (dataMap.containsKey(WearableConfigurationUtil.CONFIG_HAND_HOUR)) {
int color = dataMap.getInt(WearableConfigurationUtil.CONFIG_HAND_HOUR);
updateHourHand(color);
}
if (dataMap.containsKey(WearableConfigurationUtil.CONFIG_HAND_MINUTE)) {
int color = dataMap.getInt(WearableConfigurationUtil.CONFIG_HAND_MINUTE);
updateMinuteHand(color);
}
if (dataMap.containsKey(WearableConfigurationUtil.CONFIG_HAND_SECOND)) {
int color = dataMap.getInt(WearableConfigurationUtil.CONFIG_HAND_SECOND);
updateSecondHand(color);
}
if (dataMap.containsKey(WearableConfigurationUtil.CONFIG_HOUR_MARKER)) {
int color = dataMap.getInt(WearableConfigurationUtil.CONFIG_HOUR_MARKER);
updateHourMarker(color);
}
if (dataMap.containsKey(WearableConfigurationUtil.CONFIG_DATE)) {
mDisplayDate = dataMap.getInt(WearableConfigurationUtil.CONFIG_DATE) == 1;
}
if (dataMap.containsKey(WearableConfigurationUtil.CONFIG_DIGITAL_TIME)) {
mTimeSetting = dataMap.getInt(WearableConfigurationUtil.CONFIG_DIGITAL_TIME);
mDisplayTime = mTimeSetting > 0;
}
invalidateIfNecessary();
}
private void updateBackground(int background) {
mBackgroundColor = background;
}
private void updateDateTimeColor(int color) {
mDateTimePaint.setColor(color);
}
private void updateHourHand(int color) {
mHourHandPaint.setColor(color);
}
private void updateMinuteHand(int color) {
mMinuteHandPaint.setColor(color);
}
private void updateSecondHand(int color) {
mSecondHandPaint.setColor(color);
}
private void updateHourMarker(int color) {
mHourMarkerPaint.setColor(color);
}
private void invalidateIfNecessary() {
if (isVisible() && !isInAmbientMode()) {
invalidate();
}
}
}
}