/*
* Copyright 2015 Google Inc.
*
* 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 net.nurik.roman.formwatchface;
import android.animation.ValueAnimator;
import android.annotation.TargetApi;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.Loader;
import android.content.SharedPreferences;
import android.database.ContentObserver;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.PointF;
import android.graphics.Rect;
import android.graphics.RectF;
import android.graphics.Typeface;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.preference.PreferenceManager;
import android.provider.Settings;
import android.support.wearable.watchface.CanvasWatchFaceService;
import android.support.wearable.watchface.WatchFaceService;
import android.support.wearable.watchface.WatchFaceStyle;
import android.text.format.DateFormat;
import android.util.DisplayMetrics;
import android.util.TypedValue;
import android.view.Gravity;
import android.view.SurfaceHolder;
import android.view.WindowInsets;
import android.view.animation.DecelerateInterpolator;
import com.google.android.apps.muzei.api.MuzeiContract;
import net.nurik.roman.formwatchface.common.FormClockRenderer;
import net.nurik.roman.formwatchface.common.MathUtil;
import net.nurik.roman.formwatchface.common.config.ConfigHelper;
import net.nurik.roman.formwatchface.common.config.Themes;
import java.util.Calendar;
import static net.nurik.roman.formwatchface.LogUtil.LOGD;
import static net.nurik.roman.formwatchface.common.FormClockRenderer.ClockPaints;
import static net.nurik.roman.formwatchface.common.MathUtil.constrain;
import static net.nurik.roman.formwatchface.common.MathUtil.decelerate3;
import static net.nurik.roman.formwatchface.common.MathUtil.interpolate;
import static net.nurik.roman.formwatchface.common.MuzeiArtworkImageLoader.LoadedArtwork;
import static net.nurik.roman.formwatchface.common.config.Themes.MUZEI_THEME;
import static net.nurik.roman.formwatchface.common.config.Themes.Theme;
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
public class FormWatchFace extends CanvasWatchFaceService {
private static final String TAG = "FormWatchFace";
private static final int UPDATE_THEME_ANIM_DURATION = 1000;
@Override
public Engine onCreateEngine() {
return new Engine();
}
private class Engine extends CanvasWatchFaceService.Engine {
private Paint mAmbientBackgroundPaint;
private Paint mBackgroundPaint;
private boolean mMute;
private Rect mCardBounds = new Rect();
private ValueAnimator mBottomBoundAnimator = new ValueAnimator();
private ValueAnimator mSecondsAlphaAnimator = new ValueAnimator();
private int mWidth = 0;
private int mHeight = 0;
private int mDisplayMetricsWidth = 0;
private int mDisplayMetricsHeight = 0;
private Handler mMainThreadHandler = new Handler();
// For Muzei
private WatchfaceArtworkImageLoader mMuzeiLoader;
private Paint mMuzeiArtworkPaint;
private LoadedArtwork mMuzeiLoadedArtwork;
// FORM clock renderer specific stuff
private FormClockRenderer mHourMinRenderer;
private FormClockRenderer mSecondsRenderer;
private long mUpdateThemeStartAnimTimeMillis;
private long mLastDrawTimeMin;
private String mDateStr;
/**
* Whether the display supports fewer bits for each color in ambient mode. When true, we
* disable anti-aliasing in ambient mode.
*/
private boolean mLowBitAmbient;
private boolean mBurnInProtection;
private boolean mShowNotificationCount;
private boolean mShowSeconds;
private boolean mShowDate;
private Typeface mDateTypeface;
private ClockPaints mNormalPaints;
private ClockPaints mAmbientPaints;
private boolean mDrawMuzeiBitmap;
private Theme mCurrentTheme;
private Theme mAnimateFromTheme;
private Path mUpdateThemeClipPath = new Path();
private RectF mTempRectF = new RectF();
@Override
public void onCreate(SurfaceHolder holder) {
LOGD(TAG, "onCreate");
super.onCreate(holder);
updateDateStr();
mMute = getInterruptionFilter() == WatchFaceService.INTERRUPTION_FILTER_NONE;
handleConfigUpdated();
mDateTypeface = Typeface.createFromAsset(getAssets(), "VT323-Regular.ttf");
initClockRenderers();
registerSystemSettingsListener();
registerSharedPrefsListener();
registerTimeZoneReceiver();
initMuzei();
}
@Override
public void onDestroy() {
super.onDestroy();
unregisterSystemSettingsListener();
unregisterSharedPrefsListener();
unregisterTimeZoneReceiver();
destroyMuzei();
}
private void initClockRenderers() {
// Init paints
mAmbientBackgroundPaint = new Paint();
mAmbientBackgroundPaint.setColor(Color.BLACK);
mBackgroundPaint = new Paint();
Paint paint = new Paint();
paint.setAntiAlias(true);
mNormalPaints = new ClockPaints();
mNormalPaints.fills[0] = paint;
mNormalPaints.fills[1] = new Paint(paint);
mNormalPaints.fills[2] = new Paint(paint);
mNormalPaints.date = new Paint(paint);
mNormalPaints.date.setTypeface(mDateTypeface);
mNormalPaints.date.setTextSize(
getResources().getDimensionPixelSize(R.dimen.seconds_clock_height));
rebuildAmbientPaints();
// General config
FormClockRenderer.Options options = new FormClockRenderer.Options();
options.is24hour = DateFormat.is24HourFormat(FormWatchFace.this);
options.textSize = getResources().getDimensionPixelSize(R.dimen.main_clock_height);
options.charSpacing = getResources().getDimensionPixelSize(R.dimen.main_clock_spacing);
options.glyphAnimAverageDelay = getResources().getInteger(R.integer.main_clock_glyph_anim_delay);
options.glyphAnimDuration = getResources().getInteger(R.integer.main_clock_glyph_anim_duration);
mHourMinRenderer = new FormClockRenderer(options, mNormalPaints);
options = new FormClockRenderer.Options(options);
options.textSize = getResources().getDimensionPixelSize(R.dimen.seconds_clock_height);
options.onlySeconds = true;
options.charSpacing = getResources().getDimensionPixelSize(R.dimen.seconds_clock_spacing);
options.glyphAnimAverageDelay = getResources().getInteger(R.integer.seconds_clock_glyph_anim_delay);
options.glyphAnimDuration = getResources().getInteger(R.integer.seconds_clock_glyph_anim_duration);
mSecondsRenderer = new FormClockRenderer(options, mNormalPaints);
}
private void handleConfigUpdated() {
SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(FormWatchFace.this);
String themeId = sp.getString(ConfigHelper.KEY_THEME, Themes.DEFAULT_THEME.id);
Theme newCurrentTheme = Themes.getThemeById(themeId);
if (newCurrentTheme != mCurrentTheme) {
mAnimateFromTheme = mCurrentTheme;
mCurrentTheme = newCurrentTheme;
mUpdateThemeStartAnimTimeMillis = System.currentTimeMillis() + 200;
}
mShowNotificationCount = sp.getBoolean(ConfigHelper.KEY_SHOW_NOTIFICATION_COUNT, false);
mShowSeconds = sp.getBoolean(ConfigHelper.KEY_SHOW_SECONDS, false);
mShowDate = sp.getBoolean(ConfigHelper.KEY_SHOW_DATE, false);
updateWatchFaceStyle();
postInvalidate();
}
private void updateWatchFaceStyle() {
setWatchFaceStyle(new WatchFaceStyle.Builder(FormWatchFace.this)
.setBackgroundVisibility(WatchFaceStyle.BACKGROUND_VISIBILITY_INTERRUPTIVE)
.setCardPeekMode(WatchFaceStyle.PEEK_MODE_VARIABLE)
.setPeekOpacityMode(WatchFaceStyle.PEEK_OPACITY_MODE_TRANSLUCENT)
.setStatusBarGravity(Gravity.TOP | Gravity.CENTER)
.setHotwordIndicatorGravity(Gravity.TOP | Gravity.CENTER)
.setViewProtection(0)
.setShowUnreadCountIndicator(mShowNotificationCount && !mMute)
.build());
}
@Override
public void onSurfaceChanged(SurfaceHolder holder, int format, int width, int height) {
super.onSurfaceChanged(holder, format, width, height);
mWidth = width;
mHeight = height;
DisplayMetrics dm = getResources().getDisplayMetrics();
mDisplayMetricsWidth = dm.widthPixels;
mDisplayMetricsHeight = dm.heightPixels;
mBottomBoundAnimator.cancel();
mBottomBoundAnimator.setFloatValues(mHeight, mHeight);
mBottomBoundAnimator.setInterpolator(new DecelerateInterpolator(3));
mBottomBoundAnimator.setDuration(0);
mBottomBoundAnimator.start();
mSecondsAlphaAnimator.cancel();
mSecondsAlphaAnimator.setFloatValues(1f, 1f);
mSecondsAlphaAnimator.setDuration(0);
mSecondsAlphaAnimator.start();
}
@Override
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
public void onApplyWindowInsets(WindowInsets insets) {
super.onApplyWindowInsets(insets);
updateWatchFaceStyle();
}
@Override
public void onVisibilityChanged(boolean visible) {
super.onVisibilityChanged(visible);
if (visible) {
postInvalidate();
}
}
private void initMuzei() {
mMuzeiArtworkPaint = new Paint();
mMuzeiArtworkPaint.setAlpha(102);
mMuzeiLoader = new WatchfaceArtworkImageLoader(FormWatchFace.this);
mMuzeiLoader.registerListener(0, mMuzeiLoadCompleteListener);
mMuzeiLoader.startLoading();
// Watch for artwork changes
IntentFilter artworkChangedIntent = new IntentFilter();
artworkChangedIntent.addAction(MuzeiContract.Artwork.ACTION_ARTWORK_CHANGED);
registerReceiver(mMuzeiArtworkChangedReceiver, artworkChangedIntent);
}
private BroadcastReceiver mMuzeiArtworkChangedReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
mMuzeiLoader.startLoading();
}
};
private void destroyMuzei() {
unregisterReceiver(mMuzeiArtworkChangedReceiver);
if (mMuzeiLoader != null) {
mMuzeiLoader.unregisterListener(mMuzeiLoadCompleteListener);
mMuzeiLoader.reset();
mMuzeiLoader = null;
}
}
private Loader.OnLoadCompleteListener<LoadedArtwork> mMuzeiLoadCompleteListener
= new Loader.OnLoadCompleteListener<LoadedArtwork>() {
public void onLoadComplete(Loader<LoadedArtwork> loader, LoadedArtwork data) {
if (data != null) {
mMuzeiLoadedArtwork = data;
} else {
mMuzeiLoadedArtwork = null;
}
postInvalidate();
}
};
private void registerSystemSettingsListener() {
getContentResolver().registerContentObserver(
Settings.System.getUriFor(Settings.System.TIME_12_24),
false, mSystemSettingsObserver);
}
private void unregisterSystemSettingsListener() {
getContentResolver().unregisterContentObserver(mSystemSettingsObserver);
}
private ContentObserver mSystemSettingsObserver = new ContentObserver(mMainThreadHandler) {
@Override
public void onChange(boolean selfChange) {
super.onChange(selfChange);
initClockRenderers();
postInvalidate();
}
};
private void registerSharedPrefsListener() {
PreferenceManager.getDefaultSharedPreferences(FormWatchFace.this)
.registerOnSharedPreferenceChangeListener(mOnSharedPreferenceChangeListener);
}
private void unregisterSharedPrefsListener() {
PreferenceManager.getDefaultSharedPreferences(FormWatchFace.this)
.unregisterOnSharedPreferenceChangeListener(mOnSharedPreferenceChangeListener);
}
private SharedPreferences.OnSharedPreferenceChangeListener mOnSharedPreferenceChangeListener
= new SharedPreferences.OnSharedPreferenceChangeListener() {
@Override
public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
if (ConfigHelper.isConfigPrefKey(key)) {
handleConfigUpdated();
}
}
};
private void registerTimeZoneReceiver() {
IntentFilter timeZoneIntentFilter = new IntentFilter();
timeZoneIntentFilter.addAction(Intent.ACTION_TIMEZONE_CHANGED);
registerReceiver(mTimeZoneReceiver, timeZoneIntentFilter);
}
private void unregisterTimeZoneReceiver() {
unregisterReceiver(mTimeZoneReceiver);
}
private final BroadcastReceiver mTimeZoneReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
final String action = intent.getAction();
if (Intent.ACTION_TIMEZONE_CHANGED.equals(action)) {
initClockRenderers();
postInvalidate();
}
}
};
@Override
public void onPropertiesChanged(Bundle properties) {
super.onPropertiesChanged(properties);
mBurnInProtection = properties.getBoolean(PROPERTY_BURN_IN_PROTECTION, false);
mLowBitAmbient = properties.getBoolean(PROPERTY_LOW_BIT_AMBIENT, false);
rebuildAmbientPaints();
LOGD(TAG, "onPropertiesChanged: burn-in protection = " + mBurnInProtection
+ ", low-bit ambient = " + mLowBitAmbient);
}
private void rebuildAmbientPaints() {
Paint paint = new Paint();
mAmbientPaints = new ClockPaints();
if (mBurnInProtection || mLowBitAmbient) {
paint.setAntiAlias(false);
paint.setColor(Color.BLACK);
mAmbientPaints.fills[0] = mAmbientPaints.fills[1] = mAmbientPaints.fills[2] = paint;
paint = new Paint();
paint.setAntiAlias(!mLowBitAmbient);
mAmbientPaints.date = new Paint(paint);
mAmbientPaints.date.setColor(Color.WHITE);
paint.setStyle(Paint.Style.STROKE);
paint.setStrokeWidth(TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 5,
getResources().getDisplayMetrics()));
paint.setStrokeJoin(Paint.Join.BEVEL);
paint.setColor(Color.WHITE);
mAmbientPaints.strokes[0] = mAmbientPaints.strokes[1] = mAmbientPaints.strokes[2]
= paint;
mAmbientPaints.hasStroke = true;
} else {
paint.setAntiAlias(true);
mAmbientPaints.fills[0] = paint;
mAmbientPaints.fills[0].setColor(0xFFCCCCCC);
mAmbientPaints.fills[1] = new Paint(paint);
mAmbientPaints.fills[1].setColor(0xFFAAAAAA);
mAmbientPaints.fills[2] = new Paint(paint);
mAmbientPaints.fills[2].setColor(Color.WHITE);
mAmbientPaints.date = new Paint(paint);
mAmbientPaints.date.setColor(0xFFCCCCCC);
}
mAmbientPaints.date.setTypeface(mDateTypeface);
mAmbientPaints.date.setTextSize(
getResources().getDimensionPixelSize(R.dimen.seconds_clock_height));
}
@Override
public void onPeekCardPositionUpdate(Rect bounds) {
super.onPeekCardPositionUpdate(bounds);
LOGD(TAG, "onPeekCardPositionUpdate: " + bounds);
if (!bounds.equals(mCardBounds)) {
mCardBounds.set(bounds);
mBottomBoundAnimator.cancel();
mBottomBoundAnimator.setFloatValues(
(Float) mBottomBoundAnimator.getAnimatedValue(),
mCardBounds.top > 0 ? mCardBounds.top : mHeight);
mBottomBoundAnimator.setDuration(200);
mBottomBoundAnimator.start();
mSecondsAlphaAnimator.cancel();
mSecondsAlphaAnimator.setFloatValues(
(Float) mSecondsAlphaAnimator.getAnimatedValue(),
mCardBounds.top > 0 ? 0f : 1f);
mSecondsAlphaAnimator.setDuration(200);
mSecondsAlphaAnimator.start();
LOGD(TAG, "onPeekCardPositionUpdate: " + mCardBounds);
postInvalidate();
}
}
@Override
public void onTimeTick() {
super.onTimeTick();
LOGD(TAG, "onTimeTick: ambient = " + isInAmbientMode());
postInvalidate();
}
@Override
public void onAmbientModeChanged(boolean inAmbientMode) {
LOGD(TAG, "onAmbientModeChanged: " + inAmbientMode);
super.onAmbientModeChanged(inAmbientMode);
postInvalidate();
}
@Override
public void onInterruptionFilterChanged(int interruptionFilter) {
LOGD(TAG, "onInterruptionFilterChanged: " + interruptionFilter);
super.onInterruptionFilterChanged(interruptionFilter);
boolean inMuteMode = interruptionFilter == WatchFaceService.INTERRUPTION_FILTER_NONE;
if (mMute != inMuteMode) {
mMute = inMuteMode;
updateWatchFaceStyle();
postInvalidate();
}
}
@Override
public void onDraw(Canvas canvas, Rect bounds) {
boolean ambientMode = isInAmbientMode();
updatePaintsForTheme(mCurrentTheme);
// Figure out what to animate
long currentTimeMillis = System.currentTimeMillis();
long currentTimeMin = currentTimeMillis / 60000;
if (currentTimeMin != mLastDrawTimeMin) {
mLastDrawTimeMin = currentTimeMin;
updateDateStr();
}
mHourMinRenderer.setPaints(ambientMode ? mAmbientPaints : mNormalPaints);
mSecondsRenderer.setPaints(ambientMode ? mAmbientPaints : mNormalPaints);
mHourMinRenderer.updateTime();
if (mShowSeconds) {
mSecondsRenderer.updateTime();
}
if (ambientMode) {
drawClock(canvas);
} else {
int sc = -1;
if (isAnimatingThemeChange()) {
// show a reveal animation
updatePaintsForTheme(mAnimateFromTheme);
drawClock(canvas);
sc = canvas.save(Canvas.CLIP_SAVE_FLAG);
mUpdateThemeClipPath.reset();
float cx = mWidth / 2;
float bottom = (Float) mBottomBoundAnimator.getAnimatedValue();
float cy = bottom / 2;
float maxRadius = MathUtil.maxDistanceToCorner(0, 0, mWidth, mHeight, cx, cy);
float radius = interpolate(
decelerate3(constrain(
(currentTimeMillis - mUpdateThemeStartAnimTimeMillis)
* 1f / UPDATE_THEME_ANIM_DURATION,
0 , 1)),
0 , maxRadius);
mTempRectF.set(cx - radius, cy - radius, cx + radius, cy + radius);
mUpdateThemeClipPath.addOval(mTempRectF, Path.Direction.CW);
canvas.clipPath(mUpdateThemeClipPath);
}
updatePaintsForTheme(mCurrentTheme);
drawClock(canvas);
if (sc >= 0) {
canvas.restoreToCount(sc);
}
}
if (mBottomBoundAnimator.isRunning() || isAnimatingThemeChange()) {
postInvalidate();
} else if (isVisible() && !ambientMode) {
float secondsOpacity = (Float) mSecondsAlphaAnimator.getAnimatedValue();
boolean showingSeconds = mShowSeconds && secondsOpacity > 0;
long timeToNextSecondsAnimation = showingSeconds
? mSecondsRenderer.timeToNextAnimation()
: 10000;
long timeToNextHourMinAnimation = mHourMinRenderer.timeToNextAnimation();
if (timeToNextHourMinAnimation < 0 || timeToNextSecondsAnimation < 0) {
postInvalidate();
} else {
mInvalidateHandler.sendEmptyMessageDelayed(0,
Math.min(timeToNextHourMinAnimation, timeToNextSecondsAnimation));
}
}
}
private boolean isAnimatingThemeChange() {
return mAnimateFromTheme != null
&& System.currentTimeMillis() - mUpdateThemeStartAnimTimeMillis
< UPDATE_THEME_ANIM_DURATION;
}
private void updateDateStr() {
mDateStr = DateFormat.format("EEE d", Calendar.getInstance()).toString().toUpperCase();
}
private void updatePaintsForTheme(Theme theme) {
if (theme == MUZEI_THEME) {
mBackgroundPaint.setColor(Color.BLACK);
if (mMuzeiLoadedArtwork != null) {
mNormalPaints.fills[0].setColor(mMuzeiLoadedArtwork.color1);
mNormalPaints.fills[1].setColor(mMuzeiLoadedArtwork.color2);
mNormalPaints.fills[2].setColor(Color.WHITE);
mNormalPaints.date.setColor(mMuzeiLoadedArtwork.color1);
}
mDrawMuzeiBitmap = true;
} else {
mBackgroundPaint.setColor(getResources().getColor(theme.darkRes));
mNormalPaints.fills[0].setColor(getResources().getColor(theme.lightRes));
mNormalPaints.fills[1].setColor(getResources().getColor(theme.midRes));
mNormalPaints.fills[2].setColor(Color.WHITE);
mNormalPaints.date.setColor(getResources().getColor(theme.lightRes));
mDrawMuzeiBitmap = false;
}
}
private void drawClock(Canvas canvas) {
boolean ambientMode = isInAmbientMode();
boolean offscreenGlyphs = !ambientMode;
boolean allowAnimate = !ambientMode;
if (ambientMode) {
canvas.drawRect(0, 0, mWidth, mHeight, mAmbientBackgroundPaint);
} else if (mDrawMuzeiBitmap && mMuzeiLoadedArtwork != null) {
canvas.drawRect(0, 0, mWidth, mHeight, mAmbientBackgroundPaint);
canvas.drawBitmap(mMuzeiLoadedArtwork.bitmap,
(mDisplayMetricsWidth - mMuzeiLoadedArtwork.bitmap.getWidth()) / 2,
(mDisplayMetricsHeight - mMuzeiLoadedArtwork.bitmap.getHeight()) / 2,
mMuzeiArtworkPaint);
} else {
canvas.drawRect(0, 0, mWidth, mHeight, mBackgroundPaint);
}
float bottom = (Float) mBottomBoundAnimator.getAnimatedValue();
PointF hourMinSize = mHourMinRenderer.measure(allowAnimate);
mHourMinRenderer.draw(canvas,
(mWidth - hourMinSize.x) / 2, (bottom - hourMinSize.y) / 2,
allowAnimate,
offscreenGlyphs);
float clockSecondsSpacing = getResources().getDimension(R.dimen.clock_seconds_spacing);
float secondsOpacity = (Float) mSecondsAlphaAnimator.getAnimatedValue();
if (mShowSeconds && !ambientMode && secondsOpacity > 0) {
PointF secondsSize = mSecondsRenderer.measure(allowAnimate);
int sc = -1;
if (secondsOpacity != 1) {
sc = canvas.saveLayerAlpha(0, 0, canvas.getWidth(), canvas.getHeight(),
(int) (secondsOpacity * 255));
}
mSecondsRenderer.draw(canvas,
(mWidth + hourMinSize.x) / 2 - secondsSize.x,
(bottom + hourMinSize.y) / 2 + clockSecondsSpacing,
allowAnimate,
offscreenGlyphs);
if (sc >= 0) {
canvas.restoreToCount(sc);
}
}
if (mShowDate) {
Paint paint = ambientMode ? mAmbientPaints.date : mNormalPaints.date;
float x = (mWidth - hourMinSize.x) / 2;
if (!mShowSeconds) {
x = (mWidth - paint.measureText(mDateStr)) / 2;
}
canvas.drawText(
mDateStr,
x,
(bottom + hourMinSize.y) / 2 + clockSecondsSpacing - paint.ascent(),
paint);
}
}
private Handler mInvalidateHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
postInvalidate();
}
};
}
}