/* * 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 com.google.android.apps.santatracker; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.content.res.Resources; import android.content.res.TypedArray; import android.graphics.Bitmap; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Paint; import android.graphics.Rect; import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.Drawable; import android.os.Bundle; import android.support.wearable.watchface.CanvasWatchFaceService; import android.support.wearable.watchface.WatchFaceService; import android.support.wearable.watchface.WatchFaceStyle; import android.text.format.Time; import android.util.Log; import android.util.TypedValue; import android.view.SurfaceHolder; import java.util.TimeZone; public class SantaWatchFaceService extends CanvasWatchFaceService { private static final String TAG = "SantaWatchFaceService"; @Override public Engine onCreateEngine() { return new Engine(); } private class Engine extends CanvasWatchFaceService.Engine { private final Resources mResources = getResources(); private Paint mFilterPaint; private Paint[] mCloudFilterPaints; private Paint mAmbientBackgroundPaint; private Paint mAmbientPeekCardBorderPaint; private boolean mAmbient; private boolean mMute; private Time mTime; private boolean mRegisteredTimeZoneReceiver = false; /** * Whether the display supports fewer bits for each color in ambient mode. When true, we * disable anti-aliasing in ambient mode. */ private boolean mLowBitAmbient; /** * Whether the display is OLED and subject to pixel burn-in. When true, we display only * outlines and a 95%+ black screen. */ private boolean mBurnInProtection; private AudioPlayer mAudioPlayer; // Figure and head positioning offsets private static final int SANTA_FIGURE_OFFSET_X = -10; private static final int SANTA_FIGURE_OFFSET_Y = 30; private static final int SANTA_FIGURE_ADDITIONAL_OFFSET_X = -8; private static final float BORDER_WIDTH_PX = 3.0f; // Hyper-speed settings private static final int HYPERSPEED_DURATION_MS = 1500; private static final float HYPERSPEED_HANDOVER_EPSILON = 10f; private static final int HYPERSPEED_HOUR_TO_MINUTE_SPEED_RATIO = 2; private Bitmap[] mBackgroundBitmap; private Bitmap[] mFigureBitmap; private Bitmap[] mFaceBitmap; private Bitmap[] mHourHandBitmap; private Bitmap[] mMinuteHandBitmap; private Bitmap[] mCloudBitmaps; private int[] mCloudSpeeds; private int[] mCloudDegrees; private long mHyperSpeedStartTime = -1; private boolean[] mHyperSpeedOverrun; private int[] mCloudHyperSpeeds; private long mTimeMotionStart = -1; private final Rect mCardBounds = new Rect(); // Variables for onDraw private int mWidth = -1; private int mHeight = -1; private float mScale; private float mCenterX; private float mCenterY; private int mMinutes; private float mMinDeg; private float mHrDeg; private Bitmap mFigure; private Bitmap mMinHand; private Bitmap mHrHand; private Bitmap mFace; private float mScaledXOffset; private float mScaledXAdditionalOffset; private float mScaledYOffset; private long mTimeElapsed; private int mLoop; private float mRadius; 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) { if (Log.isLoggable(TAG, Log.DEBUG)) { Log.d(TAG, "onCreate"); } super.onCreate(holder); setWatchFaceStyle(new WatchFaceStyle.Builder(SantaWatchFaceService.this) .setCardPeekMode(WatchFaceStyle.PEEK_MODE_SHORT) .setBackgroundVisibility(WatchFaceStyle.BACKGROUND_VISIBILITY_INTERRUPTIVE) .setShowSystemUiTime(false) .setAcceptsTapEvents(true) .build()); init(); } @Override public void onTapCommand(@TapType int tapType, int x, int y, long eventTime) { if (Log.isLoggable(TAG, Log.DEBUG)) { Log.d(TAG, "onTapCommand"); } super.onTapCommand(tapType, x, y, eventTime); if (System.currentTimeMillis() >= mHyperSpeedStartTime + HYPERSPEED_DURATION_MS && !mAmbient && tapType == TAP_TYPE_TAP) { mAudioPlayer.playTrackIfNotAlreadyPlaying(R.raw.ho_ho_ho, false); mHyperSpeedStartTime = System.currentTimeMillis(); for (int i = 0; i < mCloudBitmaps.length; i++) { mHyperSpeedOverrun[i] = true; } } } /** * Setting up settings variables (paint, settings, etc) and pre-load images */ private void init() { // Pre-loading bitmaps mBackgroundBitmap = loadBitmaps(R.array.backgroundIds); mFigureBitmap = loadBitmaps(R.array.figureIds); mFaceBitmap = loadBitmaps(R.array.faceIds); mHourHandBitmap = loadBitmaps(R.array.hourHandIds); mMinuteHandBitmap = loadBitmaps(R.array.minuteHandIds); // Initialising paint object for Bitmap draws mFilterPaint = new Paint(); mFilterPaint.setFilterBitmap(true); // Initialising background paint mAmbientBackgroundPaint = new Paint(); mAmbientBackgroundPaint.setARGB(255, 0, 0, 0); mAmbientPeekCardBorderPaint = new Paint(); mAmbientPeekCardBorderPaint.setColor(Color.WHITE); mAmbientPeekCardBorderPaint.setStrokeWidth(BORDER_WIDTH_PX); // Initialing cloud bitmaps and settings mCloudDegrees = mResources.getIntArray(R.array.cloudDegrees); mCloudBitmaps = loadBitmaps(R.array.cloudIds); mCloudSpeeds = mResources.getIntArray(R.array.cloudSpeed); mCloudFilterPaints = new Paint[mCloudBitmaps.length]; mCloudHyperSpeeds = mResources.getIntArray(R.array.cloudHyperSpeeds); mHyperSpeedOverrun = new boolean[mCloudBitmaps.length]; // We need different paints because the alpha applies is different for different cloud for (int i = 0; i < mCloudBitmaps.length; i++) { Paint paint = new Paint(); paint.setFilterBitmap(true); mCloudFilterPaints[i] = paint; } // Initialising time mTime = new Time(); // Initialising audio player mAudioPlayer = new AudioPlayer(getApplicationContext()); } /** * Loading all versions (interactive, ambient and low bit) into a bitmap array. The correct * version will be pluck out at runtime. * * @param arrayId Key to the type of bitmap that we are initialising. The full list can be * found in res/values/images_santa_watchface.xml * @return Array of three bitmaps for interactive, ambient and low bit modes */ private Bitmap[] loadBitmaps(int arrayId) { int[] bitmapIds = getIntArray(arrayId); Bitmap[] bitmaps = new Bitmap[bitmapIds.length]; for (int i = 0; i < bitmapIds.length; i++) { Drawable backgroundDrawable = mResources.getDrawable(bitmapIds[i]); bitmaps[i] = ((BitmapDrawable) backgroundDrawable).getBitmap(); } return bitmaps; } /** * At runtime, this is used to load the appropriate bitmap depending on display mode * dynamically. * * @param bitmaps A bitmap array containing all bitmaps appropriate to all the display * modes * @return Bitmap determined to be appropriate for the display mode */ private Bitmap getBitmap(Bitmap[] bitmaps) { if (!mAmbient) { // Active mode return bitmaps[0]; } else if (!mLowBitAmbient && !mBurnInProtection) { // Ambient mode return bitmaps[1]; } else if (mBurnInProtection) { // Burn in protection mode return bitmaps[3]; } else { // Low bit ambient mode return bitmaps[2]; } } /** * Used to pick out whether the device is a low-bit device (i.e. pixels only capable of * displaying on and off in ambient mode) or requires burn-in protection (has an OLED screen * subject to pixel burn-out) * * @param properties device properties */ @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); if (Log.isLoggable(TAG, Log.DEBUG)) { Log.d(TAG, "onPropertiesChanged: low-bit ambient = " + mLowBitAmbient + ", " + "burn-in protection = " + mBurnInProtection); } } /** * Called periodically to update the watchface. Update at least once a minute - appropriate * for ambient mode */ @Override public void onTimeTick() { super.onTimeTick(); if (Log.isLoggable(TAG, Log.DEBUG)) { Log.d(TAG, "onTimeTick: ambient = " + mAmbient); } invalidate(); } @Override public void onAmbientModeChanged(boolean inAmbientMode) { Log.e(TAG, "onAmbientModeChanged, " + (inAmbientMode ? "ambient" : "ACTIVE")); super.onAmbientModeChanged(inAmbientMode); if (!inAmbientMode) { // Watch has just been set to active mode. mTimeMotionStart = System.currentTimeMillis(); } else { mHyperSpeedStartTime = -1; mAudioPlayer.stopAll(); } if (mAmbient != inAmbientMode) { mAmbient = inAmbientMode; invalidate(); } } @Override public void onInterruptionFilterChanged(int interruptionFilter) { super.onInterruptionFilterChanged(interruptionFilter); boolean inMuteMode = (interruptionFilter == WatchFaceService.INTERRUPTION_FILTER_NONE); if (mMute != inMuteMode) { mMute = inMuteMode; // Do nothing at present // If we add more informational element to the watchface in the future we can // remove those elements from display from here. } } @Override public void onDraw(Canvas canvas, Rect rect) { mTime.setToNow(); //Draw background. canvas.drawRect(0, 0, mWidth, mHeight, mAmbientBackgroundPaint); canvas.drawBitmap(getBitmap(mBackgroundBitmap), 0, 0, mFilterPaint); if (!mAmbient) { // Draw animation layer (above the background, below the figure and arms.) drawAnimationLayer(canvas, mCenterX, mCenterY); } mFigure = getBitmap(mFigureBitmap); //Draw figure. canvas.drawBitmap(mFigure, mCenterX - mFigure.getWidth() / 2 + mScaledXAdditionalOffset, mCenterY - mFigure.getHeight() / 2 + mScaledYOffset, mFilterPaint); if (mAmbient) { // Draw a black box as the peek card background canvas.drawRect(mCardBounds, mAmbientBackgroundPaint); } mMinutes = mTime.minute; mMinDeg = mMinutes * 6; mHrDeg = ((mTime.hour + (mMinutes / 60f)) * 30); // HYPER SPEEEEEED if (System.currentTimeMillis() <= mHyperSpeedStartTime + HYPERSPEED_DURATION_MS) { // Spin the hour hand around for 1 rotation during the hyper speed cycle long hyperDeg = (long) (((System.currentTimeMillis() - mHyperSpeedStartTime) / (float) HYPERSPEED_DURATION_MS) * 360); // Spin the minute hand around based on the defined ratio mMinDeg = (mMinDeg + (hyperDeg * HYPERSPEED_HOUR_TO_MINUTE_SPEED_RATIO)) % 360; mHrDeg = (mHrDeg + hyperDeg) % 360; } canvas.save(); // Draw the minute hand canvas.rotate(mMinDeg, mCenterX, mCenterY); mMinHand = getBitmap(mMinuteHandBitmap); canvas.drawBitmap(mMinHand, mCenterX - mMinHand.getWidth() / 2f, mCenterY - mMinHand.getHeight(), mFilterPaint); // Draw the hour hand canvas.rotate(360 - mMinDeg + mHrDeg, mCenterX, mCenterY); mHrHand = getBitmap(mHourHandBitmap); canvas.drawBitmap(mHrHand, mCenterX - mHrHand.getWidth() / 2f, mCenterY - mHrHand.getHeight(), mFilterPaint); canvas.restore(); // Draw face. (We do this last so it's not obscured by the arms.) mFace = getBitmap(mFaceBitmap); canvas.drawBitmap(mFace, mCenterX - mFace.getWidth() / 2 + mScaledXOffset, mCenterY - mFigure.getHeight() / 2 + mScaledYOffset, mFilterPaint); // While watch face is active, immediately request next animation frame. if (isVisible() && !isInAmbientMode()) { invalidate(); } } @Override public void onSurfaceChanged(SurfaceHolder holder, int format, int width, int height) { super.onSurfaceChanged(holder, format, width, height); mWidth = width; mHeight = height; // Find the center. Ignore the window insets so that, on round watches with a // "chin", the watch face is centered on the entire screen, not just the usable // portion. mCenterX = mWidth / 2f; mCenterY = mHeight / 2f; mScale = ((float) mWidth) / (float) mBackgroundBitmap[0].getWidth(); scaleBitmaps(mBackgroundBitmap, mScale); scaleBitmaps(mFigureBitmap, mScale); scaleBitmaps(mFaceBitmap, mScale); scaleBitmaps(mHourHandBitmap, mScale); scaleBitmaps(mMinuteHandBitmap, mScale); mScaledXOffset = SANTA_FIGURE_OFFSET_X * mScale; mScaledXAdditionalOffset = (SANTA_FIGURE_OFFSET_X + SANTA_FIGURE_ADDITIONAL_OFFSET_X) * mScale; mScaledYOffset = SANTA_FIGURE_OFFSET_Y * mScale; scaleBitmaps(mCloudBitmaps, mScale); } /** * Drawing the moving cloud * * @param canvas Canvas to be drawn on * @param centerX Center of the display * @param centerY Center of the display */ private void drawAnimationLayer(Canvas canvas, float centerX, float centerY) { if (mAmbient) { // Do nothing - static background in ambient mode mTimeMotionStart = -1; } else { if (mTimeMotionStart < 0) { mTimeMotionStart = System.currentTimeMillis(); } mTimeElapsed = System.currentTimeMillis() - mTimeMotionStart; for (mLoop = 0; mLoop < mCloudBitmaps.length; mLoop++) { canvas.save(); canvas.rotate(mCloudDegrees[mLoop], centerX, centerY); int speed = mCloudSpeeds[mLoop]; if (mHyperSpeedOverrun[mLoop]) { speed = mCloudHyperSpeeds[mLoop]; } mRadius = centerX - (mTimeElapsed / speed) % centerX; // if hyper-speed has finished, but this cloud is still racing if (System.currentTimeMillis() >= mHyperSpeedStartTime + HYPERSPEED_DURATION_MS && mHyperSpeedOverrun[mLoop]) { // then let it ride until it syncs up with its original trajectory, so the // clouds appear to be moving smoothly. float slowRadius = centerX - (mTimeElapsed / mCloudSpeeds[mLoop]) % centerX; if (Math.abs(mRadius - slowRadius) < HYPERSPEED_HANDOVER_EPSILON) { mHyperSpeedOverrun[mLoop] = false; } } mCloudFilterPaints[mLoop].setAlpha((int) (mRadius / centerX * 255)); canvas.drawBitmap(mCloudBitmaps[mLoop], centerX, centerY - mRadius, mCloudFilterPaints[mLoop]); canvas.restore(); } } } /** * Scale bitmap array in place. * * @param bitmaps Bitmaps to be scaled * @param scale Scale factor. 1.0 represents the original size. */ private void scaleBitmaps(Bitmap[] bitmaps, float scale) { for (int i = 0; i < bitmaps.length; i++) { bitmaps[i] = scaleBitmap(bitmaps[i], scale); } } /** * Scale individual bitmap inputs by creating a new bitmap according to the scale * * @param bitmap Original bitmap * @param scale Scale factor. 1.0 represents the original size. * @return Scaled bitmap */ private Bitmap scaleBitmap(Bitmap bitmap, float scale) { int width = (int) ((float) bitmap.getWidth() * scale); int height = (int) ((float) bitmap.getHeight() * scale); if (bitmap.getWidth() != width || bitmap.getHeight() != height) { return Bitmap.createScaledBitmap(bitmap, width, height, true /* filter */); } else { return bitmap; } } /** * Loading an int array from resource file * * @param resId ResourceId of the integer array * @return int array */ private int[] getIntArray(int resId) { TypedArray array = mResources.obtainTypedArray(resId); int[] rc = new int[array.length()]; TypedValue value = new TypedValue(); for (int i = 0; i < array.length(); i++) { array.getValue(i, value); rc[i] = value.resourceId; } return rc; } @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(); invalidate(); } else { unregisterReceiver(); } } private void registerReceiver() { if (mRegisteredTimeZoneReceiver) { return; } mRegisteredTimeZoneReceiver = true; IntentFilter filter = new IntentFilter(Intent.ACTION_TIMEZONE_CHANGED); SantaWatchFaceService.this.registerReceiver(mTimeZoneReceiver, filter); } private void unregisterReceiver() { if (!mRegisteredTimeZoneReceiver) { return; } mRegisteredTimeZoneReceiver = false; SantaWatchFaceService.this.unregisterReceiver(mTimeZoneReceiver); } @Override public void onPeekCardPositionUpdate(Rect bounds) { if (Log.isLoggable(TAG, Log.DEBUG)) { Log.d(TAG, "onPeekCardPositionUpdate: " + bounds); } super.onPeekCardPositionUpdate(bounds); if (!bounds.equals(mCardBounds)) { mCardBounds.set(bounds); invalidate(); } } } }