Home | History | Annotate | Download | only in com.example.android.wearable.watchface
      1 /*
      2  * Copyright (C) 2014 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package com.example.android.wearable.watchface;
     18 
     19 import android.app.PendingIntent;
     20 import android.content.BroadcastReceiver;
     21 import android.content.ComponentName;
     22 import android.content.Context;
     23 import android.content.Intent;
     24 import android.content.IntentFilter;
     25 import android.graphics.Bitmap;
     26 import android.graphics.BitmapFactory;
     27 import android.graphics.Canvas;
     28 import android.graphics.Color;
     29 import android.graphics.ColorMatrix;
     30 import android.graphics.ColorMatrixColorFilter;
     31 import android.graphics.Paint;
     32 import android.graphics.Rect;
     33 import android.graphics.Typeface;
     34 import android.os.Bundle;
     35 import android.os.Handler;
     36 import android.os.Message;
     37 import android.support.v7.graphics.Palette;
     38 import android.support.wearable.complications.ComplicationData;
     39 import android.support.wearable.complications.ComplicationHelperActivity;
     40 import android.support.wearable.complications.ComplicationText;
     41 import android.support.wearable.watchface.CanvasWatchFaceService;
     42 import android.support.wearable.watchface.WatchFaceService;
     43 import android.support.wearable.watchface.WatchFaceStyle;
     44 import android.text.TextUtils;
     45 import android.util.Log;
     46 import android.util.SparseArray;
     47 import android.view.SurfaceHolder;
     48 
     49 import java.util.Calendar;
     50 import java.util.TimeZone;
     51 import java.util.concurrent.TimeUnit;
     52 
     53 /**
     54  * Demonstrates two simple complications in a watch face.
     55  */
     56 public class ComplicationSimpleWatchFaceService extends CanvasWatchFaceService {
     57     private static final String TAG = "SimpleComplicationWF";
     58 
     59     // Unique IDs for each complication.
     60     private static final int LEFT_DIAL_COMPLICATION = 0;
     61     private static final int RIGHT_DIAL_COMPLICATION = 1;
     62 
     63     // Left and right complication IDs as array for Complication API.
     64     public static final int[] COMPLICATION_IDS = {LEFT_DIAL_COMPLICATION, RIGHT_DIAL_COMPLICATION};
     65 
     66     // Left and right dial supported types.
     67     public static final int[][] COMPLICATION_SUPPORTED_TYPES = {
     68             {ComplicationData.TYPE_SHORT_TEXT},
     69             {ComplicationData.TYPE_SHORT_TEXT}
     70     };
     71 
     72     /*
     73      * Update rate in milliseconds for interactive mode. We update once a second to advance the
     74      * second hand.
     75      */
     76     private static final long INTERACTIVE_UPDATE_RATE_MS = TimeUnit.SECONDS.toMillis(1);
     77 
     78     @Override
     79     public Engine onCreateEngine() {
     80         return new Engine();
     81     }
     82 
     83     private class Engine extends CanvasWatchFaceService.Engine {
     84         private static final int MSG_UPDATE_TIME = 0;
     85 
     86         private static final float COMPLICATION_TEXT_SIZE = 38f;
     87         private static final int COMPLICATION_TAP_BUFFER = 40;
     88 
     89         private static final float HOUR_STROKE_WIDTH = 5f;
     90         private static final float MINUTE_STROKE_WIDTH = 3f;
     91         private static final float SECOND_TICK_STROKE_WIDTH = 2f;
     92 
     93         private static final float CENTER_GAP_AND_CIRCLE_RADIUS = 4f;
     94 
     95         private static final int SHADOW_RADIUS = 6;
     96 
     97         private Calendar mCalendar;
     98         private boolean mRegisteredTimeZoneReceiver = false;
     99         private boolean mMuteMode;
    100 
    101         private int mWidth;
    102         private int mHeight;
    103         private float mCenterX;
    104         private float mCenterY;
    105 
    106         private float mSecondHandLength;
    107         private float mMinuteHandLength;
    108         private float mHourHandLength;
    109 
    110         // Colors for all hands (hour, minute, seconds, ticks) based on photo loaded.
    111         private int mWatchHandColor;
    112         private int mWatchHandHighlightColor;
    113         private int mWatchHandShadowColor;
    114 
    115         private Paint mHourPaint;
    116         private Paint mMinutePaint;
    117         private Paint mSecondPaint;
    118         private Paint mTickAndCirclePaint;
    119 
    120         private Paint mBackgroundPaint;
    121         private Bitmap mBackgroundBitmap;
    122         private Bitmap mGrayBackgroundBitmap;
    123 
    124         // Variables for painting Complications
    125         private Paint mComplicationPaint;
    126 
    127         /* To properly place each complication, we need their x and y coordinates. While the width
    128          * may change from moment to moment based on the time, the height will not change, so we
    129          * store it as a local variable and only calculate it only when the surface changes
    130          * (onSurfaceChanged()).
    131          */
    132         private int mComplicationsY;
    133 
    134         /* Maps active complication ids to the data for that complication. Note: Data will only be
    135          * present if the user has chosen a provider via the settings activity for the watch face.
    136          */
    137         private SparseArray<ComplicationData> mActiveComplicationDataSparseArray;
    138 
    139         private boolean mAmbient;
    140         private boolean mLowBitAmbient;
    141         private boolean mBurnInProtection;
    142 
    143         private Rect mPeekCardBounds = new Rect();
    144 
    145         private final BroadcastReceiver mTimeZoneReceiver = new BroadcastReceiver() {
    146             @Override
    147             public void onReceive(Context context, Intent intent) {
    148                 mCalendar.setTimeZone(TimeZone.getDefault());
    149                 invalidate();
    150             }
    151         };
    152 
    153         // Handler to update the time once a second in interactive mode.
    154         private final Handler mUpdateTimeHandler = new Handler() {
    155             @Override
    156             public void handleMessage(Message message) {
    157 
    158                 if (Log.isLoggable(TAG, Log.DEBUG)) {
    159                     Log.d(TAG, "updating time");
    160                 }
    161                 invalidate();
    162                 if (shouldTimerBeRunning()) {
    163                     long timeMs = System.currentTimeMillis();
    164                     long delayMs = INTERACTIVE_UPDATE_RATE_MS
    165                             - (timeMs % INTERACTIVE_UPDATE_RATE_MS);
    166                     mUpdateTimeHandler.sendEmptyMessageDelayed(MSG_UPDATE_TIME, delayMs);
    167                 }
    168 
    169             }
    170         };
    171 
    172         @Override
    173         public void onCreate(SurfaceHolder holder) {
    174             if (Log.isLoggable(TAG, Log.DEBUG)) {
    175                 Log.d(TAG, "onCreate");
    176             }
    177             super.onCreate(holder);
    178 
    179             mCalendar = Calendar.getInstance();
    180 
    181             setWatchFaceStyle(new WatchFaceStyle.Builder(ComplicationSimpleWatchFaceService.this)
    182                     .setCardPeekMode(WatchFaceStyle.PEEK_MODE_SHORT)
    183                     .setAcceptsTapEvents(true)
    184                     .setBackgroundVisibility(WatchFaceStyle.BACKGROUND_VISIBILITY_INTERRUPTIVE)
    185                     .setShowSystemUiTime(false)
    186                     .build());
    187 
    188             initializeBackground();
    189             initializeComplication();
    190             initializeWatchFace();
    191         }
    192 
    193         private void initializeBackground() {
    194             mBackgroundPaint = new Paint();
    195             mBackgroundPaint.setColor(Color.BLACK);
    196             mBackgroundBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.bg);
    197         }
    198 
    199         private void initializeComplication() {
    200             if (Log.isLoggable(TAG, Log.DEBUG)) {
    201                 Log.d(TAG, "initializeComplications()");
    202             }
    203             mActiveComplicationDataSparseArray = new SparseArray<>(COMPLICATION_IDS.length);
    204 
    205             mComplicationPaint = new Paint();
    206             mComplicationPaint.setColor(Color.WHITE);
    207             mComplicationPaint.setTextSize(COMPLICATION_TEXT_SIZE);
    208             mComplicationPaint.setTypeface(Typeface.create(Typeface.DEFAULT, Typeface.BOLD));
    209             mComplicationPaint.setAntiAlias(true);
    210 
    211             setActiveComplications(COMPLICATION_IDS);
    212         }
    213 
    214         private void initializeWatchFace() {
    215             /* Set defaults for colors */
    216             mWatchHandColor = Color.WHITE;
    217             mWatchHandHighlightColor = Color.RED;
    218             mWatchHandShadowColor = Color.BLACK;
    219 
    220             mHourPaint = new Paint();
    221             mHourPaint.setColor(mWatchHandColor);
    222             mHourPaint.setStrokeWidth(HOUR_STROKE_WIDTH);
    223             mHourPaint.setAntiAlias(true);
    224             mHourPaint.setStrokeCap(Paint.Cap.ROUND);
    225             mHourPaint.setShadowLayer(SHADOW_RADIUS, 0, 0, mWatchHandShadowColor);
    226 
    227             mMinutePaint = new Paint();
    228             mMinutePaint.setColor(mWatchHandColor);
    229             mMinutePaint.setStrokeWidth(MINUTE_STROKE_WIDTH);
    230             mMinutePaint.setAntiAlias(true);
    231             mMinutePaint.setStrokeCap(Paint.Cap.ROUND);
    232             mMinutePaint.setShadowLayer(SHADOW_RADIUS, 0, 0, mWatchHandShadowColor);
    233 
    234             mSecondPaint = new Paint();
    235             mSecondPaint.setColor(mWatchHandHighlightColor);
    236             mSecondPaint.setStrokeWidth(SECOND_TICK_STROKE_WIDTH);
    237             mSecondPaint.setAntiAlias(true);
    238             mSecondPaint.setStrokeCap(Paint.Cap.ROUND);
    239             mSecondPaint.setShadowLayer(SHADOW_RADIUS, 0, 0, mWatchHandShadowColor);
    240 
    241             mTickAndCirclePaint = new Paint();
    242             mTickAndCirclePaint.setColor(mWatchHandColor);
    243             mTickAndCirclePaint.setStrokeWidth(SECOND_TICK_STROKE_WIDTH);
    244             mTickAndCirclePaint.setAntiAlias(true);
    245             mTickAndCirclePaint.setStyle(Paint.Style.STROKE);
    246             mTickAndCirclePaint.setShadowLayer(SHADOW_RADIUS, 0, 0, mWatchHandShadowColor);
    247 
    248             // Asynchronous call extract colors from background image to improve watch face style.
    249             Palette.from(mBackgroundBitmap).generate(
    250                     new Palette.PaletteAsyncListener() {
    251                         public void onGenerated(Palette palette) {
    252                             /*
    253                              * Sometimes, palette is unable to generate a color palette
    254                              * so we need to check that we have one.
    255                              */
    256                             if (palette != null) {
    257                                 Log.d("onGenerated", palette.toString());
    258                                 mWatchHandColor = palette.getVibrantColor(Color.WHITE);
    259                                 mWatchHandShadowColor = palette.getDarkMutedColor(Color.BLACK);
    260                                 updateWatchHandStyle();
    261                             }
    262                         }
    263                     });
    264         }
    265 
    266         @Override
    267         public void onDestroy() {
    268             mUpdateTimeHandler.removeMessages(MSG_UPDATE_TIME);
    269             super.onDestroy();
    270         }
    271 
    272         @Override
    273         public void onPropertiesChanged(Bundle properties) {
    274             super.onPropertiesChanged(properties);
    275             if (Log.isLoggable(TAG, Log.DEBUG)) {
    276                 Log.d(TAG, "onPropertiesChanged: low-bit ambient = " + mLowBitAmbient);
    277             }
    278 
    279             mLowBitAmbient = properties.getBoolean(PROPERTY_LOW_BIT_AMBIENT, false);
    280             mBurnInProtection = properties.getBoolean(PROPERTY_BURN_IN_PROTECTION, false);
    281         }
    282 
    283         /*
    284          * Called when there is updated data for a complication id.
    285          */
    286         @Override
    287         public void onComplicationDataUpdate(
    288                 int complicationId, ComplicationData complicationData) {
    289             Log.d(TAG, "onComplicationDataUpdate() id: " + complicationId);
    290 
    291             // Adds/updates active complication data in the array.
    292             mActiveComplicationDataSparseArray.put(complicationId, complicationData);
    293             invalidate();
    294         }
    295 
    296         @Override
    297         public void onTapCommand(int tapType, int x, int y, long eventTime) {
    298             Log.d(TAG, "OnTapCommand()");
    299             switch (tapType) {
    300                 case TAP_TYPE_TAP:
    301                     int tappedComplicationId = getTappedComplicationId(x, y);
    302                     if (tappedComplicationId != -1) {
    303                         onComplicationTap(tappedComplicationId);
    304                     }
    305                     break;
    306             }
    307         }
    308 
    309         /*
    310          * Determines if tap inside a complication area or returns -1.
    311          */
    312         private int getTappedComplicationId(int x, int y) {
    313             ComplicationData complicationData;
    314             long currentTimeMillis = System.currentTimeMillis();
    315 
    316             for (int i = 0; i < COMPLICATION_IDS.length; i++) {
    317                 complicationData = mActiveComplicationDataSparseArray.get(COMPLICATION_IDS[i]);
    318 
    319                 if ((complicationData != null)
    320                         && (complicationData.isActive(currentTimeMillis))
    321                         && (complicationData.getType() != ComplicationData.TYPE_NOT_CONFIGURED)
    322                         && (complicationData.getType() != ComplicationData.TYPE_EMPTY)) {
    323 
    324                     Rect complicationBoundingRect = new Rect(0, 0, 0, 0);
    325 
    326                     switch (COMPLICATION_IDS[i]) {
    327                         case LEFT_DIAL_COMPLICATION:
    328                             complicationBoundingRect.set(
    329                                     0,                                          // left
    330                                     mComplicationsY - COMPLICATION_TAP_BUFFER,  // top
    331                                     (mWidth / 2),                               // right
    332                                     ((int) COMPLICATION_TEXT_SIZE               // bottom
    333                                             + mComplicationsY
    334                                             + COMPLICATION_TAP_BUFFER));
    335                             break;
    336 
    337                         case RIGHT_DIAL_COMPLICATION:
    338                             complicationBoundingRect.set(
    339                                     (mWidth / 2),                               // left
    340                                     mComplicationsY - COMPLICATION_TAP_BUFFER,  // top
    341                                     mWidth,                                     // right
    342                                     ((int) COMPLICATION_TEXT_SIZE               // bottom
    343                                             + mComplicationsY
    344                                             + COMPLICATION_TAP_BUFFER));
    345                             break;
    346                     }
    347 
    348                     if (complicationBoundingRect.width() > 0) {
    349                         if (complicationBoundingRect.contains(x, y)) {
    350                             return COMPLICATION_IDS[i];
    351                         }
    352                     } else {
    353                         Log.e(TAG, "Not a recognized complication id.");
    354                     }
    355                 }
    356             }
    357             return -1;
    358         }
    359 
    360         /*
    361          * Fires PendingIntent associated with complication (if it has one).
    362          */
    363         private void onComplicationTap(int complicationId) {
    364             if (Log.isLoggable(TAG, Log.DEBUG)) {
    365                 Log.d(TAG, "onComplicationTap()");
    366             }
    367 
    368             ComplicationData complicationData =
    369                     mActiveComplicationDataSparseArray.get(complicationId);
    370 
    371             if (complicationData != null) {
    372 
    373                 if (complicationData.getTapAction() != null) {
    374                     try {
    375                         complicationData.getTapAction().send();
    376                     } catch (PendingIntent.CanceledException e) {
    377                         Log.e(TAG, "On complication tap action error " + e);
    378                     }
    379 
    380                 } else if (complicationData.getType() == ComplicationData.TYPE_NO_PERMISSION) {
    381 
    382                     // Watch face does not have permission to receive complication data, so launch
    383                     // permission request.
    384                     ComponentName componentName = new ComponentName(
    385                             getApplicationContext(),
    386                             ComplicationSimpleWatchFaceService.class);
    387 
    388                     Intent permissionRequestIntent =
    389                             ComplicationHelperActivity.createPermissionRequestHelperIntent(
    390                                     getApplicationContext(), componentName);
    391 
    392                     startActivity(permissionRequestIntent);
    393                 }
    394 
    395             } else {
    396                 if (Log.isLoggable(TAG, Log.DEBUG)) {
    397                     Log.d(TAG, "No PendingIntent for complication " + complicationId + ".");
    398                 }
    399             }
    400         }
    401 
    402         @Override
    403         public void onTimeTick() {
    404             super.onTimeTick();
    405             invalidate();
    406         }
    407 
    408         @Override
    409         public void onAmbientModeChanged(boolean inAmbientMode) {
    410             super.onAmbientModeChanged(inAmbientMode);
    411             if (Log.isLoggable(TAG, Log.DEBUG)) {
    412                 Log.d(TAG, "onAmbientModeChanged: " + inAmbientMode);
    413             }
    414             mAmbient = inAmbientMode;
    415 
    416             updateWatchHandStyle();
    417 
    418             // Updates complication style
    419             mComplicationPaint.setAntiAlias(!inAmbientMode);
    420 
    421             // Check and trigger whether or not timer should be running (only in active mode).
    422             updateTimer();
    423         }
    424 
    425         private void updateWatchHandStyle(){
    426             if (mAmbient){
    427                 mHourPaint.setColor(Color.WHITE);
    428                 mMinutePaint.setColor(Color.WHITE);
    429                 mSecondPaint.setColor(Color.WHITE);
    430                 mTickAndCirclePaint.setColor(Color.WHITE);
    431 
    432                 mHourPaint.setAntiAlias(false);
    433                 mMinutePaint.setAntiAlias(false);
    434                 mSecondPaint.setAntiAlias(false);
    435                 mTickAndCirclePaint.setAntiAlias(false);
    436 
    437                 mHourPaint.clearShadowLayer();
    438                 mMinutePaint.clearShadowLayer();
    439                 mSecondPaint.clearShadowLayer();
    440                 mTickAndCirclePaint.clearShadowLayer();
    441 
    442             } else {
    443                 mHourPaint.setColor(mWatchHandColor);
    444                 mMinutePaint.setColor(mWatchHandColor);
    445                 mSecondPaint.setColor(mWatchHandHighlightColor);
    446                 mTickAndCirclePaint.setColor(mWatchHandColor);
    447 
    448                 mHourPaint.setAntiAlias(true);
    449                 mMinutePaint.setAntiAlias(true);
    450                 mSecondPaint.setAntiAlias(true);
    451                 mTickAndCirclePaint.setAntiAlias(true);
    452 
    453                 mHourPaint.setShadowLayer(SHADOW_RADIUS, 0, 0, mWatchHandShadowColor);
    454                 mMinutePaint.setShadowLayer(SHADOW_RADIUS, 0, 0, mWatchHandShadowColor);
    455                 mSecondPaint.setShadowLayer(SHADOW_RADIUS, 0, 0, mWatchHandShadowColor);
    456                 mTickAndCirclePaint.setShadowLayer(SHADOW_RADIUS, 0, 0, mWatchHandShadowColor);
    457             }
    458         }
    459 
    460         @Override
    461         public void onInterruptionFilterChanged(int interruptionFilter) {
    462             super.onInterruptionFilterChanged(interruptionFilter);
    463             boolean inMuteMode = (interruptionFilter == WatchFaceService.INTERRUPTION_FILTER_NONE);
    464 
    465             /* Dim display in mute mode. */
    466             if (mMuteMode != inMuteMode) {
    467                 mMuteMode = inMuteMode;
    468                 mHourPaint.setAlpha(inMuteMode ? 100 : 255);
    469                 mMinutePaint.setAlpha(inMuteMode ? 100 : 255);
    470                 mSecondPaint.setAlpha(inMuteMode ? 80 : 255);
    471                 invalidate();
    472             }
    473         }
    474 
    475         @Override
    476         public void onSurfaceChanged(SurfaceHolder holder, int format, int width, int height) {
    477             super.onSurfaceChanged(holder, format, width, height);
    478 
    479             // Used for complications
    480             mWidth = width;
    481             mHeight = height;
    482 
    483             /*
    484              * Find the coordinates of the center point on the screen, and ignore the window
    485              * insets, so that, on round watches with a "chin", the watch face is centered on the
    486              * entire screen, not just the usable portion.
    487              */
    488             mCenterX = mWidth / 2f;
    489             mCenterY = mHeight / 2f;
    490 
    491             /*
    492              * Since the height of the complications text does not change, we only have to
    493              * recalculate when the surface changes.
    494              */
    495             mComplicationsY = (int) ((mHeight / 2) + (mComplicationPaint.getTextSize() / 2));
    496 
    497             /*
    498              * Calculate lengths of different hands based on watch screen size.
    499              */
    500             mSecondHandLength = (float) (mCenterX * 0.875);
    501             mMinuteHandLength = (float) (mCenterX * 0.75);
    502             mHourHandLength = (float) (mCenterX * 0.5);
    503 
    504 
    505             /* Scale loaded background image (more efficient) if surface dimensions change. */
    506             float scale = ((float) width) / (float) mBackgroundBitmap.getWidth();
    507 
    508             mBackgroundBitmap = Bitmap.createScaledBitmap(mBackgroundBitmap,
    509                     (int) (mBackgroundBitmap.getWidth() * scale),
    510                     (int) (mBackgroundBitmap.getHeight() * scale), true);
    511 
    512             /*
    513              * Create a gray version of the image only if it will look nice on the device in
    514              * ambient mode. That means we don't want devices that support burn-in
    515              * protection (slight movements in pixels, not great for images going all the way to
    516              * edges) and low ambient mode (degrades image quality).
    517              *
    518              * Also, if your watch face will know about all images ahead of time (users aren't
    519              * selecting their own photos for the watch face), it will be more
    520              * efficient to create a black/white version (png, etc.) and load that when you need it.
    521              */
    522             if (!mBurnInProtection && !mLowBitAmbient) {
    523                 initGrayBackgroundBitmap();
    524             }
    525         }
    526 
    527         private void initGrayBackgroundBitmap() {
    528             mGrayBackgroundBitmap = Bitmap.createBitmap(
    529                     mBackgroundBitmap.getWidth(),
    530                     mBackgroundBitmap.getHeight(),
    531                     Bitmap.Config.ARGB_8888);
    532             Canvas canvas = new Canvas(mGrayBackgroundBitmap);
    533             Paint grayPaint = new Paint();
    534             ColorMatrix colorMatrix = new ColorMatrix();
    535             colorMatrix.setSaturation(0);
    536             ColorMatrixColorFilter filter = new ColorMatrixColorFilter(colorMatrix);
    537             grayPaint.setColorFilter(filter);
    538             canvas.drawBitmap(mBackgroundBitmap, 0, 0, grayPaint);
    539         }
    540 
    541         @Override
    542         public void onDraw(Canvas canvas, Rect bounds) {
    543             if (Log.isLoggable(TAG, Log.DEBUG)) {
    544                 Log.d(TAG, "onDraw");
    545             }
    546             long now = System.currentTimeMillis();
    547             mCalendar.setTimeInMillis(now);
    548 
    549             drawBackground(canvas);
    550             drawComplications(canvas, now);
    551             drawWatchFace(canvas);
    552 
    553 
    554         }
    555 
    556         private void drawBackground(Canvas canvas) {
    557             if (mAmbient && (mLowBitAmbient || mBurnInProtection)) {
    558                 canvas.drawColor(Color.BLACK);
    559             } else if (mAmbient) {
    560                 canvas.drawBitmap(mGrayBackgroundBitmap, 0, 0, mBackgroundPaint);
    561             } else {
    562                 canvas.drawBitmap(mBackgroundBitmap, 0, 0, mBackgroundPaint);
    563             }
    564         }
    565 
    566         private void drawComplications(Canvas canvas, long currentTimeMillis) {
    567             ComplicationData complicationData;
    568 
    569             for (int i = 0; i < COMPLICATION_IDS.length; i++) {
    570 
    571                 complicationData = mActiveComplicationDataSparseArray.get(COMPLICATION_IDS[i]);
    572 
    573                 if ((complicationData != null)
    574                         && (complicationData.isActive(currentTimeMillis))) {
    575 
    576                     // Both Short Text and No Permission Types can be rendered with the same code.
    577                     // No Permission will display "--" with an Intent to launch a permission prompt.
    578                     // If you want to support more types, just add a "else if" below with your
    579                     // rendering code inside.
    580                     if (complicationData.getType() == ComplicationData.TYPE_SHORT_TEXT
    581                             || complicationData.getType() == ComplicationData.TYPE_NO_PERMISSION) {
    582 
    583                         ComplicationText mainText = complicationData.getShortText();
    584                         ComplicationText subText = complicationData.getShortTitle();
    585 
    586                         CharSequence complicationMessage =
    587                                 mainText.getText(getApplicationContext(), currentTimeMillis);
    588 
    589                         /* In most cases you would want the subText (Title) under the
    590                          * mainText (Text), but to keep it simple for the code lab, we are
    591                          * concatenating them all on one line.
    592                          */
    593                         if (subText != null) {
    594                             complicationMessage = TextUtils.concat(
    595                                     complicationMessage,
    596                                     " ",
    597                                     subText.getText(getApplicationContext(), currentTimeMillis));
    598                         }
    599 
    600                         //Log.d(TAG, "Com id: " + COMPLICATION_IDS[i] + "\t" + complicationMessage);
    601                         double textWidth =
    602                                 mComplicationPaint.measureText(
    603                                         complicationMessage,
    604                                         0,
    605                                         complicationMessage.length());
    606 
    607                         int complicationsX;
    608 
    609                         if (COMPLICATION_IDS[i] == LEFT_DIAL_COMPLICATION) {
    610                             complicationsX = (int) ((mWidth / 2) - textWidth) / 2;
    611                         } else {
    612                             // RIGHT_DIAL_COMPLICATION calculations
    613                             int offset = (int) ((mWidth / 2) - textWidth) / 2;
    614                             complicationsX = (mWidth / 2) + offset;
    615                         }
    616 
    617                         canvas.drawText(
    618                                 complicationMessage,
    619                                 0,
    620                                 complicationMessage.length(),
    621                                 complicationsX,
    622                                 mComplicationsY,
    623                                 mComplicationPaint);
    624                     }
    625                 }
    626             }
    627         }
    628 
    629         private void drawWatchFace(Canvas canvas) {
    630             /*
    631              * Draw ticks. Usually you will want to bake this directly into the photo, but in
    632              * cases where you want to allow users to select their own photos, this dynamically
    633              * creates them on top of the photo.
    634              */
    635             float innerTickRadius = mCenterX - 10;
    636             float outerTickRadius = mCenterX;
    637             for (int tickIndex = 0; tickIndex < 12; tickIndex++) {
    638                 float tickRot = (float) (tickIndex * Math.PI * 2 / 12);
    639                 float innerX = (float) Math.sin(tickRot) * innerTickRadius;
    640                 float innerY = (float) -Math.cos(tickRot) * innerTickRadius;
    641                 float outerX = (float) Math.sin(tickRot) * outerTickRadius;
    642                 float outerY = (float) -Math.cos(tickRot) * outerTickRadius;
    643                 canvas.drawLine(mCenterX + innerX, mCenterY + innerY,
    644                         mCenterX + outerX, mCenterY + outerY, mTickAndCirclePaint);
    645             }
    646 
    647             /*
    648              * These calculations reflect the rotation in degrees per unit of time, e.g.,
    649              * 360 / 60 = 6 and 360 / 12 = 30.
    650              */
    651             final float seconds =
    652                     (mCalendar.get(Calendar.SECOND) + mCalendar.get(Calendar.MILLISECOND) / 1000f);
    653             final float secondsRotation = seconds * 6f;
    654 
    655             final float minutesRotation = mCalendar.get(Calendar.MINUTE) * 6f;
    656 
    657             final float hourHandOffset = mCalendar.get(Calendar.MINUTE) / 2f;
    658             final float hoursRotation = (mCalendar.get(Calendar.HOUR) * 30) + hourHandOffset;
    659 
    660             /*
    661              * Save the canvas state before we can begin to rotate it.
    662              */
    663             canvas.save();
    664 
    665             canvas.rotate(hoursRotation, mCenterX, mCenterY);
    666             canvas.drawLine(
    667                     mCenterX,
    668                     mCenterY - CENTER_GAP_AND_CIRCLE_RADIUS,
    669                     mCenterX,
    670                     mCenterY - mHourHandLength,
    671                     mHourPaint);
    672 
    673             canvas.rotate(minutesRotation - hoursRotation, mCenterX, mCenterY);
    674             canvas.drawLine(
    675                     mCenterX,
    676                     mCenterY - CENTER_GAP_AND_CIRCLE_RADIUS,
    677                     mCenterX,
    678                     mCenterY - mMinuteHandLength,
    679                     mMinutePaint);
    680 
    681             /*
    682              * Ensure the "seconds" hand is drawn only when we are in interactive mode.
    683              * Otherwise, we only update the watch face once a minute.
    684              */
    685             if (!mAmbient) {
    686                 canvas.rotate(secondsRotation - minutesRotation, mCenterX, mCenterY);
    687                 canvas.drawLine(
    688                         mCenterX,
    689                         mCenterY - CENTER_GAP_AND_CIRCLE_RADIUS,
    690                         mCenterX,
    691                         mCenterY - mSecondHandLength,
    692                         mSecondPaint);
    693 
    694             }
    695             canvas.drawCircle(
    696                     mCenterX,
    697                     mCenterY,
    698                     CENTER_GAP_AND_CIRCLE_RADIUS,
    699                     mTickAndCirclePaint);
    700 
    701             /* Restore the canvas' original orientation. */
    702             canvas.restore();
    703 
    704             /* Draw rectangle behind peek card in ambient mode to improve readability. */
    705             if (mAmbient) {
    706                 canvas.drawRect(mPeekCardBounds, mBackgroundPaint);
    707             }
    708         }
    709 
    710         @Override
    711         public void onVisibilityChanged(boolean visible) {
    712             super.onVisibilityChanged(visible);
    713 
    714             if (visible) {
    715                 registerReceiver();
    716                 // Update time zone in case it changed while we weren't visible.
    717                 mCalendar.setTimeZone(TimeZone.getDefault());
    718                 invalidate();
    719             } else {
    720                 unregisterReceiver();
    721             }
    722 
    723             /* Check and trigger whether or not timer should be running (only in active mode). */
    724             updateTimer();
    725         }
    726 
    727         @Override
    728         public void onPeekCardPositionUpdate(Rect rect) {
    729             super.onPeekCardPositionUpdate(rect);
    730             mPeekCardBounds.set(rect);
    731         }
    732 
    733         private void registerReceiver() {
    734             if (mRegisteredTimeZoneReceiver) {
    735                 return;
    736             }
    737             mRegisteredTimeZoneReceiver = true;
    738             IntentFilter filter = new IntentFilter(Intent.ACTION_TIMEZONE_CHANGED);
    739             ComplicationSimpleWatchFaceService.this.registerReceiver(mTimeZoneReceiver, filter);
    740         }
    741 
    742         private void unregisterReceiver() {
    743             if (!mRegisteredTimeZoneReceiver) {
    744                 return;
    745             }
    746             mRegisteredTimeZoneReceiver = false;
    747             ComplicationSimpleWatchFaceService.this.unregisterReceiver(mTimeZoneReceiver);
    748         }
    749 
    750         /**
    751          * Starts/stops the {@link #mUpdateTimeHandler} timer based on the state of the watch face.
    752          */
    753         private void updateTimer() {
    754             if (Log.isLoggable(TAG, Log.DEBUG)) {
    755                 Log.d(TAG, "updateTimer");
    756             }
    757             mUpdateTimeHandler.removeMessages(MSG_UPDATE_TIME);
    758             if (shouldTimerBeRunning()) {
    759                 mUpdateTimeHandler.sendEmptyMessage(MSG_UPDATE_TIME);
    760             }
    761         }
    762 
    763         /**
    764          * Returns whether the {@link #mUpdateTimeHandler} timer should be running. The timer
    765          * should only run in active mode.
    766          */
    767         private boolean shouldTimerBeRunning() {
    768             return isVisible() && !mAmbient;
    769         }
    770     }
    771 }