package com.eleks.tesla.service; 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.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.AsyncTask; import android.os.Bundle; import android.os.Handler; import android.os.Message; 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 com.eleks.tesla.teslalib.ApiPathConstants; import com.eleks.tesla.teslalib.models.CarState; import com.eleks.tesla.teslalib.models.ChargeState; import com.eleks.tesla.teslalib.models.VehicleState; import com.eleks.tesla.R; import com.eleks.tesla.events.ChargeStateLoadedEvent; import com.eleks.tesla.events.VehicleStateLoadedEvent; import com.google.android.gms.common.api.GoogleApiClient; import com.google.android.gms.wearable.Node; import com.google.android.gms.wearable.NodeApi; import com.google.android.gms.wearable.Wearable; import java.util.List; import java.util.TimeZone; import java.util.concurrent.TimeUnit; import de.greenrobot.event.EventBus; /** * Created by Ihor.Demedyuk on 10.02.2015. */ public class TeslaWatchFaceService extends CanvasWatchFaceService { public final String TAG = TeslaWatchFaceService.class.getSimpleName(); private Engine mEngine; @Override public Engine onCreateEngine() { mEngine = new Engine(); return mEngine; } @Override public void onCreate() { EventBus.getDefault().register(this); super.onCreate(); } @Override public void onDestroy() { EventBus.getDefault().unregister(this); mEngine = null; super.onDestroy(); } public void onEvent(ChargeStateLoadedEvent chargeStateLoadedEvent) { ChargeState chargeState = chargeStateLoadedEvent.getChargeState(); mEngine.getCarState().updateCharge(chargeState); mEngine.invalidate(); } public void onEvent(VehicleStateLoadedEvent chargeStateLoadedEvent) { VehicleState vehicleState = chargeStateLoadedEvent.getVehicleState(); mEngine.getCarState().updateVehicleState(vehicleState); mEngine.invalidate(); } /* implement service callback methods */ private class Engine extends CanvasWatchFaceService.Engine { public static final String TIME_TEXT_EXAMPLE = "20:20"; public static final String RANGE_TEXT_EXAMPLE = "390 Mi"; public static final String TIME_FORMATTER = "%H:%M"; public final String TAG = Engine.class.getSimpleName(); public static final int MSG_UPDATE_TIME = 12; public static final int MSG_LOAD_CAR_STATE = 13; public static final int INTERACTIVE_UPDATE_RATE_MS = 1000; private static final int CONNECTION_TIME_OUT_MS = 1000; public static final int LOAD_CAR_STATE_DELAY_MS = 10 * 1000; private GoogleApiClient mGoogleApiClient; private String mNodeId; public static final String MAX_RANGE = "TYPICAL RANGE"; public static final String MILES = " Mi"; /* a time object */ private Time mTime; /* device features */ private boolean mLowBitAmbient; private boolean mBurnInProtection; /* graphic objects */ private Paint mBgPaint; private Paint mTimePaint; private Paint mMaxRangePaint; private Paint mRangeValuePaint; private Paint mDotsPaint; private Bitmap mLockedScaledBitmap; private Bitmap mUnLockedScaledBitmap; private Bitmap mChargingScaledBitmap; private Bitmap mLockedBitmap; private Bitmap mUnLockedBitmap; private Bitmap mChargingBitmap; private CarState mCarState = new CarState(); //TODO set Default values private AsyncTask<Void, Void, Void> mLoadCarStateTask; boolean mRegisteredTimeZoneReceiver; private int mTimeFontSize = 0; private int mMaxRangeFontSize = 0; private int mRangeValueFontSize = 0; public void setCarState(CarState mCarState) { this.mCarState = mCarState; } public CarState getCarState() { return mCarState; } final Handler mUpdateTimeHandler = 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); mUpdateTimeHandler .sendEmptyMessageDelayed(MSG_UPDATE_TIME, delayMs); } break; } } }; final Handler mLoadCarStateHandler = new Handler() { @Override public void handleMessage(Message message) { switch (message.what) { case MSG_LOAD_CAR_STATE: cancelLoadCarStateTask(); mLoadCarStateTask = new LoadCarStateTask(); mLoadCarStateTask.execute(); break; } } }; private class LoadCarStateTask extends AsyncTask<Void, Void, Void> { @Override protected Void doInBackground(Void... params) { fetchCarState(); return null; } @Override protected void onPostExecute(Void aVoid) { super.onPostExecute(aVoid); if (isVisible()) { mLoadCarStateHandler.sendEmptyMessageDelayed( MSG_LOAD_CAR_STATE, LOAD_CAR_STATE_DELAY_MS); } } } private void fetchCarState() { if (mNodeId != null) { new Thread(new Runnable() { @Override public void run() { connectApiClient(); Wearable.MessageApi.sendMessage(mGoogleApiClient, mNodeId, ApiPathConstants.WEAR_GET_CAR_CONFIG, null).await(); mGoogleApiClient.disconnect(); } }).start(); } } private void connectApiClient() { if (isClientDisconnected()) { mGoogleApiClient.blockingConnect(CONNECTION_TIME_OUT_MS, TimeUnit.MILLISECONDS); } } private boolean isClientDisconnected() { return mGoogleApiClient != null && !(mGoogleApiClient.isConnected() || mGoogleApiClient.isConnecting()); } private void initGoogleApiClient() { mGoogleApiClient = getGoogleApiClient(TeslaWatchFaceService.this); retrieveDeviceNode(); } private GoogleApiClient getGoogleApiClient(Context context) { return new GoogleApiClient.Builder(context) .addApi(Wearable.API) .build(); } private void retrieveDeviceNode() { new Thread(new Runnable() { @Override public void run() { connectApiClient(); NodeApi.GetConnectedNodesResult result = Wearable.NodeApi.getConnectedNodes(mGoogleApiClient).await(); List<Node> nodes = result.getNodes(); if (nodes.size() > 0) { mNodeId = nodes.get(0).getId(); } Log.v(TAG, "Node ID of phone: " + mNodeId); mGoogleApiClient.disconnect(); } }).start(); } /* receiver to update the time zone */ final BroadcastReceiver mTimeZoneReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { mTime.clear(intent.getStringExtra("time-zone")); mTime.setToNow(); } }; @Override public void onCreate(SurfaceHolder holder) { super.onCreate(holder); mBgPaint = new Paint(); mBgPaint.setARGB(255, 0, 0, 0); mTimePaint = new Paint(); mTimePaint.setARGB(255, 255, 255, 255); mTimePaint.setAntiAlias(true); Typeface tf = Typeface.createFromAsset(getAssets(), "RobotoCondensed-Light.ttf"); mTimePaint.setTypeface(tf); mMaxRangePaint = new Paint(); mMaxRangePaint.setARGB(120, 255, 255, 255); mMaxRangePaint.setAntiAlias(true); mRangeValuePaint = new Paint(); mRangeValuePaint.setARGB(190, 255, 255, 255); mRangeValuePaint.setAntiAlias(true); mRangeValuePaint.setTypeface(Typeface.create(Typeface.DEFAULT, Typeface.BOLD)); mDotsPaint = new Paint(); mDotsPaint.setAntiAlias(true); mTime = new Time(); Resources resources = TeslaWatchFaceService.this.getResources(); Drawable chargingDrawable = resources.getDrawable(R.drawable.charging); mChargingBitmap = ((BitmapDrawable) chargingDrawable).getBitmap(); Drawable mLockedBitmapDrawable = resources.getDrawable(R.drawable.locked_small); mLockedBitmap = ((BitmapDrawable) mLockedBitmapDrawable).getBitmap(); Drawable unlockedDrawable = resources.getDrawable(R.drawable.unlocked_small); mUnLockedBitmap = ((BitmapDrawable) unlockedDrawable).getBitmap(); configureStyle(); initGoogleApiClient(); } private void configureStyle() { setWatchFaceStyle(new WatchFaceStyle.Builder(TeslaWatchFaceService.this) .setCardPeekMode(WatchFaceStyle.PEEK_MODE_SHORT) .setBackgroundVisibility(WatchFaceStyle .BACKGROUND_VISIBILITY_INTERRUPTIVE) .setShowSystemUiTime(false) .build()); } @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); //TODO change Paints antiAlias if (mLowBitAmbient) { boolean antiAlias = !inAmbientMode; // mHourPaint.setAntiAlias(antiAlias); // mMinutePaint.setAntiAlias(antiAlias); // mSecondPaint.setAntiAlias(antiAlias); } invalidate(); updateTimer(); } @Override public void onDraw(Canvas canvas, Rect bounds) { // Update the time mTime.setToNow(); int width = bounds.width(); int height = bounds.height(); float centerX = width / 2f; float centerY = height / 2f; // draw a bg canvas.drawRect(0, 0, width, height, mBgPaint); // draw labels if (mTimeFontSize == 0) { initTimeFontSize(bounds); } Rect textRect = new Rect(); mTimePaint.getTextBounds("20", 0, 1, textRect); int timeYPos = (int) ((canvas.getHeight() / 2) + (textRect.height() * 0.75f)); int timeXPos = (int) ((width - mTimePaint.measureText(TIME_TEXT_EXAMPLE)) / 2); String time = mTime.format(TIME_FORMATTER); canvas.drawText(time, timeXPos, timeYPos, mTimePaint); if (mMaxRangeFontSize == 0) { initMaxRangeFontSize(bounds); } int maxRangeXPos = (int) ((width - mMaxRangePaint.measureText(MAX_RANGE)) / 2); int maxRangeYPos = (int) ((canvas.getHeight() * 0.8f) - ((mMaxRangePaint.descent() + mMaxRangePaint.ascent()) / 2)); canvas.drawText(MAX_RANGE, maxRangeXPos, maxRangeYPos, mMaxRangePaint); if (mRangeValueFontSize == 0) { initRangeValueFontSize(bounds); } String rangeText = Math.round(mCarState.getDistance()) + MILES; int rangeValueXPos = (int) ((width - mRangeValuePaint.measureText(rangeText)) / 2); int rangeValueYPos = (int) ((canvas.getHeight() * 0.88) - ((mRangeValuePaint.descent() + mRangeValuePaint.ascent()) / 2)); canvas.drawText(rangeText, rangeValueXPos, rangeValueYPos, mRangeValuePaint); // draw dots int dotsLinesCount = mCarState.getBatteryCharge(); // 100% notif_charge = 100 dots float startRot = (float) (Math.PI + Math.PI / 6f); float endRot = (float) ((3 * Math.PI) - (Math.PI / 6f)); float deltaRot = (endRot - startRot) / 101; // 100 max dots count // set dots Color if (mCarState.getBatteryCharge() <= 15) { mDotsPaint.setARGB(255, 249, 67, 58); // red } else if (mCarState.getBatteryCharge() <= 60) { mDotsPaint.setARGB(255, 50, 201, 218); // blue } else { mDotsPaint.setARGB(255, 58, 249, 103); // green } // draw dots bg float dotRadius = width / 100f; float clockRadius = centerX - 3 * dotRadius; mDotsPaint.setAlpha(60); for (int k = 0; k < 10; k++) { for (int i = 1; i <= 100; i++) { float cx = (float) (centerX + clockRadius * Math.sin(startRot + deltaRot * i)); float cy = (float) (centerY + clockRadius * -1 * Math.cos(startRot + deltaRot * i)); canvas.drawCircle(cx, cy, dotRadius * 0.8f, mDotsPaint); } dotRadius = dotRadius * 0.9f; clockRadius = clockRadius - 3 * dotRadius; } // draw first and last dots lines dotRadius = width / 100f; clockRadius = centerX - 3 * dotRadius; mDotsPaint.setAlpha(80); for (int k = 0; k < 10; k++) { for (int i = 1; i <= dotsLinesCount; i = i + dotsLinesCount - 1) { float cx = (float) (centerX + clockRadius * Math.sin(startRot + deltaRot * i)); float cy = (float) (centerY + clockRadius * -1 * Math.cos(startRot + deltaRot * i)); canvas.drawCircle(cx, cy, dotRadius * 0.8f, mDotsPaint); } dotRadius = dotRadius * 0.9f; clockRadius = clockRadius - 3 * dotRadius; int alpha = 80 - (k * 10); if (alpha > 0) { mDotsPaint.setAlpha(alpha); } else { mDotsPaint.setAlpha(0); } } // draw dots dotRadius = width / 100f; clockRadius = centerX - 3 * dotRadius; mDotsPaint.setAlpha(255); for (int k = 0; k < 10; k++) { int i; for (i = 2; i < dotsLinesCount; i++) { float cx = (float) (centerX + clockRadius * Math.sin(startRot + deltaRot * i)); float cy = (float) (centerY + clockRadius * -1 * Math.cos(startRot + deltaRot * i)); canvas.drawCircle(cx, cy, dotRadius * 0.8f, mDotsPaint); } dotRadius = dotRadius * 0.9f; clockRadius = clockRadius - 3 * dotRadius; int alpha = 195 - (k * 30); if (alpha > 0) { mDotsPaint.setAlpha(alpha); } else { mDotsPaint.setAlpha(0); } } //draw lock and notif_charge states icons int iconSideSize = width / 12; if (mLockedScaledBitmap == null || mLockedScaledBitmap.getWidth() != iconSideSize || mLockedScaledBitmap.getHeight() != iconSideSize) { mLockedScaledBitmap = Bitmap.createScaledBitmap(mLockedBitmap, iconSideSize, iconSideSize, true); } if (mUnLockedScaledBitmap == null || mUnLockedScaledBitmap.getWidth() != iconSideSize || mUnLockedScaledBitmap.getHeight() != iconSideSize) { mUnLockedScaledBitmap = Bitmap.createScaledBitmap(mUnLockedBitmap, iconSideSize, iconSideSize, true); } if (mChargingScaledBitmap == null || mChargingScaledBitmap.getWidth() != iconSideSize || mChargingScaledBitmap.getHeight() != iconSideSize) { mChargingScaledBitmap = Bitmap.createScaledBitmap(mChargingBitmap, iconSideSize, iconSideSize, true); } Bitmap lockStateBitmap = mCarState.isLocked() ? mLockedScaledBitmap : mUnLockedScaledBitmap; if (mCarState.isIsCharging()) { canvas.drawBitmap(lockStateBitmap, width / 2, height / 4, null); canvas.drawBitmap(mChargingScaledBitmap, (width / 2) - iconSideSize, height / 4, null); } else { canvas.drawBitmap(lockStateBitmap, (width - iconSideSize) / 2, height / 4, null); } } private void initTimeFontSize(Rect bounds) { float maxWidth = bounds.width() / 2f; do { mTimePaint.setTextSize(++mTimeFontSize); } while (mTimePaint.measureText(TIME_TEXT_EXAMPLE) < maxWidth); } private void initMaxRangeFontSize(Rect bounds) { float maxWidth = bounds.width() / 3.2f; do { mMaxRangePaint.setTextSize(++mMaxRangeFontSize); } while (mMaxRangePaint.measureText(MAX_RANGE) < maxWidth); } private void initRangeValueFontSize(Rect bounds) { float maxWidth = bounds.width() / 4.5f; do { mRangeValuePaint.setTextSize(++mRangeValueFontSize); } while (mRangeValuePaint.measureText(RANGE_TEXT_EXAMPLE) < maxWidth); } @Override public void onVisibilityChanged(boolean visible) { super.onVisibilityChanged(visible); if (visible) { registerReceiver(); // Update time zone in case it changed while we weren't visible. mTime.clear(TimeZone.getDefault().getID()); mTime.setToNow(); mLoadCarStateHandler.sendEmptyMessage(MSG_LOAD_CAR_STATE); } else { unregisterReceiver(); mLoadCarStateHandler.removeMessages(MSG_LOAD_CAR_STATE); cancelLoadCarStateTask(); } // Whether the timer should be running depends on whether we're visible and // whether we're in ambient mode), so we may need to start or stop the timer updateTimer(); } private void cancelLoadCarStateTask() { if (mLoadCarStateTask != null) { mLoadCarStateTask.cancel(true); } } private void updateTimer() { mUpdateTimeHandler.removeMessages(MSG_UPDATE_TIME); if (shouldTimerBeRunning()) { mUpdateTimeHandler.sendEmptyMessage(MSG_UPDATE_TIME); } } private boolean shouldTimerBeRunning() { return isVisible() && !isInAmbientMode(); } private void registerReceiver() { if (mRegisteredTimeZoneReceiver) { return; } mRegisteredTimeZoneReceiver = true; IntentFilter filter = new IntentFilter(Intent.ACTION_TIMEZONE_CHANGED); TeslaWatchFaceService.this.registerReceiver(mTimeZoneReceiver, filter); } private void unregisterReceiver() { if (!mRegisteredTimeZoneReceiver) { return; } mRegisteredTimeZoneReceiver = false; TeslaWatchFaceService.this.unregisterReceiver(mTimeZoneReceiver); } } }