package com.aegiswallet.services; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.content.SharedPreferences; import android.content.res.Resources; import android.graphics.Bitmap; import android.graphics.Canvas; import android.graphics.Paint; import android.graphics.Rect; import android.graphics.Typeface; import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.Drawable; import android.os.Bundle; import android.os.Handler; import android.os.Message; import android.preference.PreferenceManager; import android.support.wearable.watchface.CanvasWatchFaceService; import android.support.wearable.watchface.WatchFaceStyle; import android.text.format.Time; import android.util.Log; import android.view.SurfaceHolder; import android.view.WindowInsets; import com.aegiswallet.R; import com.aegiswallet.utils.DigitalWatchFaceUtil; import com.google.android.gms.common.ConnectionResult; import com.google.android.gms.common.api.GoogleApiClient; 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.TimeZone; import java.util.concurrent.TimeUnit; import java.util.prefs.Preferences; /** * WatchFaceService */ public class WatchFaceService extends CanvasWatchFaceService { private static final String TAG = "WatchFaceService"; private static final Typeface BOLD_TYPEFACE = Typeface.create(Typeface.SANS_SERIF, Typeface.BOLD); private static final Typeface NORMAL_TYPEFACE = Typeface.create(Typeface.SANS_SERIF, Typeface.NORMAL); private Context mainContext = this; /** * Update rate in milliseconds for normal (not ambient and not mute) mode. We update twice * a second to blink the colons. */ private static final long NORMAL_UPDATE_RATE_MS = 500; /** * Update rate in milliseconds for mute mode. We update every minute, like in ambient mode. */ private static final long MUTE_UPDATE_RATE_MS = TimeUnit.MINUTES.toMillis(1); @Override public Engine onCreateEngine() { return new Engine(); } private class Engine extends CanvasWatchFaceService.Engine implements DataApi.DataListener, GoogleApiClient.ConnectionCallbacks, GoogleApiClient.OnConnectionFailedListener { static final String COLON_STRING = ":"; /** Alpha value for drawing time when in mute mode. */ static final int MUTE_ALPHA = 100; /** Alpha value for drawing time when not in mute mode. */ static final int NORMAL_ALPHA = 255; static final int MSG_UPDATE_TIME = 0; /** How often {@link #mUpdateTimeHandler} ticks in milliseconds. */ long mInteractiveUpdateRateMs = NORMAL_UPDATE_RATE_MS; /** Handler to update the time periodically in interactive mode. */ final Handler mUpdateTimeHandler = new Handler() { @Override public void handleMessage(Message message) { switch (message.what) { case MSG_UPDATE_TIME: if (Log.isLoggable(TAG, Log.VERBOSE)) { Log.v(TAG, "updating time"); } invalidate(); if (shouldTimerBeRunning()) { long timeMs = System.currentTimeMillis(); long delayMs = mInteractiveUpdateRateMs - (timeMs % mInteractiveUpdateRateMs); mUpdateTimeHandler.sendEmptyMessageDelayed(MSG_UPDATE_TIME, delayMs); } break; } } }; GoogleApiClient mGoogleApiClient = new GoogleApiClient.Builder(WatchFaceService.this) .addConnectionCallbacks(this) .addOnConnectionFailedListener(this) .addApi(Wearable.API) .build(); final BroadcastReceiver mTimeZoneReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { mTime.clear(intent.getStringExtra("time-zone")); mTime.setToNow(); } }; boolean mRegisteredTimeZoneReceiver = false; Paint mBackgroundPaint; Paint mHourPaint; Paint mMinutePaint; //Paint mSecondPaint; Paint mBalancePaint; //Paint mAmPmPaint; Paint mColonPaint; float mColonWidth; boolean mMute; Time mTime; boolean mShouldDrawColons; float mXOffset; float mYOffset; String mAmString; String mPmString; int mInteractiveBackgroundColor = DigitalWatchFaceUtil.COLOR_VALUE_DEFAULT_AND_AMBIENT_BACKGROUND; int mInteractiveHourDigitsColor = DigitalWatchFaceUtil.COLOR_VALUE_DEFAULT_AND_AMBIENT_HOUR_DIGITS; int mInteractiveMinuteDigitsColor = DigitalWatchFaceUtil.COLOR_VALUE_DEFAULT_AND_AMBIENT_MINUTE_DIGITS; int mInteractiveBalanceDigitsColor = DigitalWatchFaceUtil.COLOR_VALUE_DEFAULT_AND_AMBIENT_BALANCE_DIGITS; Bitmap mBackgroundBitmap; Bitmap mBackgroundScaledBitmap; /** * Whether the display supports fewer bits for each color in ambient mode. When true, we * disable anti-aliasing in ambient mode. */ boolean mLowBitAmbient; @Override public void onCreate(SurfaceHolder holder) { if (Log.isLoggable(TAG, Log.DEBUG)) { Log.d(TAG, "onCreate"); } super.onCreate(holder); setWatchFaceStyle(new WatchFaceStyle.Builder(WatchFaceService.this) .setCardPeekMode(WatchFaceStyle.PEEK_MODE_VARIABLE) .setBackgroundVisibility(WatchFaceStyle.BACKGROUND_VISIBILITY_INTERRUPTIVE) .setShowSystemUiTime(false) .build()); Resources resources = WatchFaceService.this.getResources(); mYOffset = resources.getDimension(R.dimen.digital_y_offset); mAmString = resources.getString(R.string.digital_am); mPmString = resources.getString(R.string.digital_pm); mBackgroundPaint = new Paint(); mBackgroundPaint.setColor(resources.getColor(R.color.aegis_blue)); mHourPaint = createTextPaint(mInteractiveHourDigitsColor); mMinutePaint = createTextPaint(mInteractiveMinuteDigitsColor); mBalancePaint = createTextPaint(mInteractiveMinuteDigitsColor); //mAmPmPaint = createTextPaint(resources.getColor(R.color.digital_am_pm)); mColonPaint = createTextPaint(resources.getColor(R.color.digital_colons)); mTime = new Time(); Drawable backgroundDrawable = resources.getDrawable(R.drawable.btcwhite); mBackgroundBitmap = ((BitmapDrawable) backgroundDrawable).getBitmap(); } @Override public void onDestroy() { mUpdateTimeHandler.removeMessages(MSG_UPDATE_TIME); super.onDestroy(); } private Paint createTextPaint(int defaultInteractiveColor) { return createTextPaint(defaultInteractiveColor, NORMAL_TYPEFACE); } private Paint createTextPaint(int defaultInteractiveColor, Typeface typeface) { Paint paint = new Paint(); paint.setColor(defaultInteractiveColor); paint.setTypeface(typeface); paint.setAntiAlias(true); return paint; } @Override public void onVisibilityChanged(boolean visible) { if (Log.isLoggable(TAG, Log.DEBUG)) { Log.d(TAG, "onVisibilityChanged: " + visible); } super.onVisibilityChanged(visible); if (visible) { mGoogleApiClient.connect(); registerReceiver(); // 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(); } private void registerReceiver() { if (mRegisteredTimeZoneReceiver) { return; } mRegisteredTimeZoneReceiver = true; IntentFilter filter = new IntentFilter(Intent.ACTION_TIMEZONE_CHANGED); WatchFaceService.this.registerReceiver(mTimeZoneReceiver, filter); } private void unregisterReceiver() { if (!mRegisteredTimeZoneReceiver) { return; } mRegisteredTimeZoneReceiver = false; WatchFaceService.this.unregisterReceiver(mTimeZoneReceiver); } @Override public void onPropertiesChanged(Bundle properties) { super.onPropertiesChanged(properties); boolean burnInProtection = properties.getBoolean(PROPERTY_BURN_IN_PROTECTION, false); mHourPaint.setTypeface(burnInProtection ? NORMAL_TYPEFACE : NORMAL_TYPEFACE); mLowBitAmbient = properties.getBoolean(PROPERTY_LOW_BIT_AMBIENT, false); if (Log.isLoggable(TAG, Log.DEBUG)) { Log.d(TAG, "onPropertiesChanged: burn-in protection = " + burnInProtection + ", low-bit ambient = " + mLowBitAmbient); } } @Override public void onTimeTick() { super.onTimeTick(); if (Log.isLoggable(TAG, Log.DEBUG)) { Log.d(TAG, "onTimeTick: ambient = " + isInAmbientMode()); } invalidate(); } @Override public void onAmbientModeChanged(boolean inAmbientMode) { super.onAmbientModeChanged(inAmbientMode); if (Log.isLoggable(TAG, Log.DEBUG)) { Log.d(TAG, "onAmbientModeChanged: " + inAmbientMode); } adjustPaintColorToCurrentMode(mBackgroundPaint, mInteractiveBackgroundColor, DigitalWatchFaceUtil.COLOR_VALUE_DEFAULT_AND_AMBIENT_BACKGROUND); adjustPaintColorToCurrentMode(mHourPaint, mInteractiveHourDigitsColor, DigitalWatchFaceUtil.COLOR_VALUE_DEFAULT_AND_AMBIENT_HOUR_DIGITS); adjustPaintColorToCurrentMode(mMinutePaint, mInteractiveMinuteDigitsColor, DigitalWatchFaceUtil.COLOR_VALUE_DEFAULT_AND_AMBIENT_MINUTE_DIGITS); adjustPaintColorToCurrentMode(mBalancePaint, mInteractiveMinuteDigitsColor, DigitalWatchFaceUtil.COLOR_VALUE_DEFAULT_AND_AMBIENT_BALANCE_DIGITS); // Actually, the seconds are not rendered in the ambient mode, so we could pass just any // value as ambientColor here. //adjustPaintColorToCurrentMode(mSecondPaint, mInteractiveSecondDigitsColor, // DigitalWatchFaceUtil.COLOR_VALUE_DEFAULT_AND_AMBIENT_SECOND_DIGITS); if (mLowBitAmbient) { boolean antiAlias = !inAmbientMode; mHourPaint.setAntiAlias(antiAlias); mMinutePaint.setAntiAlias(antiAlias); mBalancePaint.setAntiAlias(antiAlias); //mAmPmPaint.setAntiAlias(antiAlias); mColonPaint.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(); } private void adjustPaintColorToCurrentMode(Paint paint, int interactiveColor, int ambientColor) { paint.setColor(isInAmbientMode() ? ambientColor : interactiveColor); } @Override public void onInterruptionFilterChanged(int interruptionFilter) { if (Log.isLoggable(TAG, Log.DEBUG)) { Log.d(TAG, "onInterruptionFilterChanged: " + interruptionFilter); } super.onInterruptionFilterChanged(interruptionFilter); boolean inMuteMode = interruptionFilter == WatchFaceService.INTERRUPTION_FILTER_NONE; // We only need to update once a minute in mute mode. setInteractiveUpdateRateMs(inMuteMode ? MUTE_UPDATE_RATE_MS : NORMAL_UPDATE_RATE_MS); if (mMute != inMuteMode) { mMute = inMuteMode; int alpha = inMuteMode ? MUTE_ALPHA : NORMAL_ALPHA; mHourPaint.setAlpha(alpha); mMinutePaint.setAlpha(alpha); mBalancePaint.setAlpha(alpha); mColonPaint.setAlpha(alpha); //mAmPmPaint.setAlpha(alpha); invalidate(); } } public void setInteractiveUpdateRateMs(long updateRateMs) { if (updateRateMs == mInteractiveUpdateRateMs) { return; } mInteractiveUpdateRateMs = updateRateMs; // Stop and restart the timer so the new update rate takes effect immediately. if (shouldTimerBeRunning()) { updateTimer(); } } private void updatePaintIfInteractive(Paint paint, int interactiveColor) { if (!isInAmbientMode() && paint != null) { paint.setColor(interactiveColor); } } private void setInteractiveBackgroundColor(int color) { mInteractiveBackgroundColor = color; updatePaintIfInteractive(mBackgroundPaint, getResources().getColor(R.color.aegis_blue)); } private void setInteractiveHourDigitsColor(int color) { mInteractiveHourDigitsColor = color; updatePaintIfInteractive(mHourPaint, color); } private void setInteractiveMinuteDigitsColor(int color) { mInteractiveMinuteDigitsColor = color; updatePaintIfInteractive(mMinutePaint, color); } private void setInteractiveBalanceDigitsColor(int color) { mInteractiveBalanceDigitsColor = color; updatePaintIfInteractive(mBalancePaint, color); } private String formatTwoDigitNumber(int hour) { return String.format("%02d", hour); } private int convertTo12Hour(int hour) { int result = hour % 12; return (result == 0) ? 12 : result; } private String getAmPmString(int hour) { return (hour < 12) ? mAmString : mPmString; } @Override public void onDraw(Canvas canvas, Rect bounds) { mTime.setToNow(); int width = bounds.width(); int height = bounds.height(); if (mBackgroundScaledBitmap == null || mBackgroundScaledBitmap.getWidth() != width || mBackgroundScaledBitmap.getHeight() != height) { mBackgroundScaledBitmap = Bitmap.createScaledBitmap(mBackgroundBitmap, 50, 66, true /* filter */); } // Load resources that have alternate values for round watches. Resources resources = WatchFaceService.this.getResources(); boolean isRound = true; mXOffset = resources.getDimension(isRound ? R.dimen.digital_x_offset_round : R.dimen.digital_x_offset); float textSize = resources.getDimension(isRound ? R.dimen.digital_text_size_round : R.dimen.digital_text_size); mHourPaint.setTextSize(textSize); mMinutePaint.setTextSize(textSize); //mAmPmPaint.setTextSize(amPmSize); mColonPaint.setTextSize(textSize); mColonWidth = mColonPaint.measureText(COLON_STRING); // Show colons for the first half of each second so the colons blink on when the time // updates. mShouldDrawColons = (System.currentTimeMillis() % 1000) < 500; // Draw the background. canvas.drawRect(0, 0, bounds.width(), bounds.height(), mBackgroundPaint); // Draw the bitcoin bitmap Paint p = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.FILTER_BITMAP_FLAG); canvas.drawBitmap(mBackgroundScaledBitmap, (bounds.width()/2) - 25, (bounds.height()/2) - 33, p); String hourString = String.valueOf(convertTo12Hour(mTime.hour)); String minuteString = formatTwoDigitNumber(mTime.minute); // Draw the hours. float x = (width / 2) - (mHourPaint.measureText(hourString + COLON_STRING + minuteString) / 2); canvas.drawText(hourString, x, mYOffset, mHourPaint); x += mHourPaint.measureText(hourString); // In ambient and mute modes, always draw the first colon. Otherwise, draw the // first colon for the first half of each second. if (isInAmbientMode() || mMute || mShouldDrawColons) { canvas.drawText(COLON_STRING, x, mYOffset, mColonPaint); } x += mColonWidth; // Draw the minutes. canvas.drawText(minuteString, x, mYOffset, mMinutePaint); SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(mainContext); String balance = prefs.getString("BALANCE", "N/A"); mBalancePaint = createTextPaint(mInteractiveHourDigitsColor); float balanceSize = resources.getDimension(isRound ? R.dimen.digital_am_pm_size_round : R.dimen.digital_am_pm_size); mBalancePaint.setTextSize(balanceSize); float balanceWidth = mBalancePaint.measureText(balance); canvas.drawText(balance, (width/2) - (balanceWidth/2), 240, mBalancePaint); } /** * 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() { if (Log.isLoggable(TAG, Log.DEBUG)) { Log.d(TAG, "updateTimer"); } mUpdateTimeHandler.removeMessages(MSG_UPDATE_TIME); if (shouldTimerBeRunning()) { mUpdateTimeHandler.sendEmptyMessage(MSG_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 void updateConfigDataItemAndUiOnStartup() { DigitalWatchFaceUtil.fetchConfigDataMap(mGoogleApiClient, new DigitalWatchFaceUtil.FetchConfigDataMapCallback() { @Override public void onConfigDataMapFetched(DataMap startupConfig) { // If the DataItem hasn't been created yet or some keys are missing, // use the default values. setDefaultValuesForMissingConfigKeys(startupConfig); DigitalWatchFaceUtil.putConfigDataItem(mGoogleApiClient, startupConfig); updateUiForConfigDataMap(startupConfig); } } ); } private void setDefaultValuesForMissingConfigKeys(DataMap config) { addIntKeyIfMissing(config, DigitalWatchFaceUtil.KEY_BACKGROUND_COLOR, DigitalWatchFaceUtil.COLOR_VALUE_DEFAULT_AND_AMBIENT_BACKGROUND); addIntKeyIfMissing(config, DigitalWatchFaceUtil.KEY_HOURS_COLOR, DigitalWatchFaceUtil.COLOR_VALUE_DEFAULT_AND_AMBIENT_HOUR_DIGITS); addIntKeyIfMissing(config, DigitalWatchFaceUtil.KEY_MINUTES_COLOR, DigitalWatchFaceUtil.COLOR_VALUE_DEFAULT_AND_AMBIENT_MINUTE_DIGITS); addIntKeyIfMissing(config, DigitalWatchFaceUtil.KEY_SECONDS_COLOR, DigitalWatchFaceUtil.COLOR_VALUE_DEFAULT_AND_AMBIENT_SECOND_DIGITS); addIntKeyIfMissing(config, DigitalWatchFaceUtil.KEY_BALANCE_COLOR, DigitalWatchFaceUtil.COLOR_VALUE_DEFAULT_AND_AMBIENT_BALANCE_DIGITS); } private void addIntKeyIfMissing(DataMap config, String key, int color) { if (!config.containsKey(key)) { config.putInt(key, color); } } @Override // DataApi.DataListener public void onDataChanged(DataEventBuffer dataEvents) { try { for (DataEvent dataEvent : dataEvents) { if (dataEvent.getType() != DataEvent.TYPE_CHANGED) { continue; } DataItem dataItem = dataEvent.getDataItem(); if (!dataItem.getUri().getPath().equals( DigitalWatchFaceUtil.PATH_WITH_FEATURE)) { continue; } DataMapItem dataMapItem = DataMapItem.fromDataItem(dataItem); DataMap config = dataMapItem.getDataMap(); if (Log.isLoggable(TAG, Log.DEBUG)) { Log.d(TAG, "Config DataItem updated:" + config); } updateUiForConfigDataMap(config); } } finally { dataEvents.close(); } } private void updateUiForConfigDataMap(final DataMap config) { boolean uiUpdated = false; for (String configKey : config.keySet()) { if (!config.containsKey(configKey)) { continue; } int color = config.getInt(configKey); if (Log.isLoggable(TAG, Log.DEBUG)) { Log.d(TAG, "Found watch face config key: " + configKey + " -> " + Integer.toHexString(color)); } if (updateUiForKey(configKey, color)) { uiUpdated = true; } } if (uiUpdated) { invalidate(); } } /** * Updates the color of a UI item according to the given {@code configKey}. Does nothing if * {@code configKey} isn't recognized. * * @return whether UI has been updated */ private boolean updateUiForKey(String configKey, int color) { if (configKey.equals(DigitalWatchFaceUtil.KEY_BACKGROUND_COLOR)) { setInteractiveBackgroundColor(color); } else if (configKey.equals(DigitalWatchFaceUtil.KEY_HOURS_COLOR)) { setInteractiveHourDigitsColor(color); } else if (configKey.equals(DigitalWatchFaceUtil.KEY_MINUTES_COLOR)) { setInteractiveMinuteDigitsColor(color); } else if (configKey.equals(DigitalWatchFaceUtil.KEY_SECONDS_COLOR)) { //setInteractiveSecondDigitsColor(color); } else if(configKey.equals(DigitalWatchFaceUtil.KEY_BALANCE_COLOR)){ setInteractiveBalanceDigitsColor(color); } else { Log.w(TAG, "Ignoring unknown config key: " + configKey); return false; } return true; } @Override // GoogleApiClient.ConnectionCallbacks public void onConnected(Bundle connectionHint) { if (Log.isLoggable(TAG, Log.DEBUG)) { Log.d(TAG, "onConnected: " + connectionHint); } Wearable.DataApi.addListener(mGoogleApiClient, Engine.this); updateConfigDataItemAndUiOnStartup(); } @Override // GoogleApiClient.ConnectionCallbacks public void onConnectionSuspended(int cause) { if (Log.isLoggable(TAG, Log.DEBUG)) { Log.d(TAG, "onConnectionSuspended: " + cause); } } @Override // GoogleApiClient.OnConnectionFailedListener public void onConnectionFailed(ConnectionResult result) { if (Log.isLoggable(TAG, Log.DEBUG)) { Log.d(TAG, "onConnectionFailed: " + result); } } } }