Home | History | Annotate | Download | only in 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.Manifest;
     20 import android.content.BroadcastReceiver;
     21 import android.content.ContentUris;
     22 import android.content.Context;
     23 import android.content.Intent;
     24 import android.content.IntentFilter;
     25 import android.content.pm.PackageManager;
     26 import android.database.Cursor;
     27 import android.graphics.Canvas;
     28 import android.graphics.Color;
     29 import android.graphics.Rect;
     30 import android.net.Uri;
     31 import android.os.AsyncTask;
     32 import android.os.Handler;
     33 import android.os.Message;
     34 import android.os.PowerManager;
     35 import android.support.v4.app.ActivityCompat;
     36 import android.support.wearable.provider.WearableCalendarContract;
     37 import android.support.wearable.watchface.CanvasWatchFaceService;
     38 import android.support.wearable.watchface.WatchFaceService;
     39 import android.support.wearable.watchface.WatchFaceStyle;
     40 import android.text.DynamicLayout;
     41 import android.text.Editable;
     42 import android.text.Html;
     43 import android.text.Layout;
     44 import android.text.SpannableStringBuilder;
     45 import android.text.TextPaint;
     46 import android.text.format.DateUtils;
     47 import android.util.Log;
     48 import android.view.SurfaceHolder;
     49 
     50 /**
     51  * Proof of concept sample watch face that demonstrates how a watch face can load calendar data.
     52  */
     53 public class CalendarWatchFaceService extends CanvasWatchFaceService {
     54     private static final String TAG = "CalendarWatchFace";
     55 
     56     @Override
     57     public Engine onCreateEngine() {
     58         return new Engine();
     59     }
     60 
     61     private class Engine extends CanvasWatchFaceService.Engine {
     62 
     63         static final int BACKGROUND_COLOR = Color.BLACK;
     64         static final int FOREGROUND_COLOR = Color.WHITE;
     65         static final int TEXT_SIZE = 25;
     66         static final int MSG_LOAD_MEETINGS = 0;
     67 
     68         /** Editable string containing the text to draw with the number of meetings in bold. */
     69         final Editable mEditable = new SpannableStringBuilder();
     70 
     71         /** Width specified when {@link #mLayout} was created. */
     72         int mLayoutWidth;
     73 
     74         /** Layout to wrap {@link #mEditable} onto multiple lines. */
     75         DynamicLayout mLayout;
     76 
     77         /** Paint used to draw text. */
     78         final TextPaint mTextPaint = new TextPaint();
     79 
     80         int mNumMeetings;
     81         private boolean mCalendarPermissionApproved;
     82         private String mCalendarNotApprovedMessage;
     83 
     84         private AsyncTask<Void, Void, Integer> mLoadMeetingsTask;
     85 
     86         private boolean mIsReceiverRegistered;
     87 
     88         /** Handler to load the meetings once a minute in interactive mode. */
     89         final Handler mLoadMeetingsHandler = new Handler() {
     90             @Override
     91             public void handleMessage(Message message) {
     92                 switch (message.what) {
     93                     case MSG_LOAD_MEETINGS:
     94 
     95                         cancelLoadMeetingTask();
     96 
     97                         // Loads meetings.
     98                         if (mCalendarPermissionApproved) {
     99                             mLoadMeetingsTask = new LoadMeetingsTask();
    100                             mLoadMeetingsTask.execute();
    101                         }
    102                         break;
    103                 }
    104             }
    105         };
    106 
    107         private BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() {
    108             @Override
    109             public void onReceive(Context context, Intent intent) {
    110                 if (Intent.ACTION_PROVIDER_CHANGED.equals(intent.getAction())
    111                         && WearableCalendarContract.CONTENT_URI.equals(intent.getData())) {
    112                     mLoadMeetingsHandler.sendEmptyMessage(MSG_LOAD_MEETINGS);
    113                 }
    114             }
    115         };
    116 
    117         @Override
    118         public void onCreate(SurfaceHolder holder) {
    119             super.onCreate(holder);
    120             Log.d(TAG, "onCreate");
    121 
    122             mCalendarNotApprovedMessage =
    123                     getResources().getString(R.string.calendar_permission_not_approved);
    124 
    125             /* Accepts tap events to allow permission changes by user. */
    126             setWatchFaceStyle(new WatchFaceStyle.Builder(CalendarWatchFaceService.this)
    127                     .setCardPeekMode(WatchFaceStyle.PEEK_MODE_VARIABLE)
    128                     .setBackgroundVisibility(WatchFaceStyle.BACKGROUND_VISIBILITY_INTERRUPTIVE)
    129                     .setShowSystemUiTime(false)
    130                     .setAcceptsTapEvents(true)
    131                     .build());
    132 
    133             mTextPaint.setColor(FOREGROUND_COLOR);
    134             mTextPaint.setTextSize(TEXT_SIZE);
    135 
    136             // Enables app to handle 23+ (M+) style permissions.
    137             mCalendarPermissionApproved =
    138                     ActivityCompat.checkSelfPermission(
    139                             getApplicationContext(),
    140                             Manifest.permission.READ_CALENDAR) == PackageManager.PERMISSION_GRANTED;
    141 
    142             if (mCalendarPermissionApproved) {
    143                 mLoadMeetingsHandler.sendEmptyMessage(MSG_LOAD_MEETINGS);
    144             }
    145         }
    146 
    147         @Override
    148         public void onDestroy() {
    149             mLoadMeetingsHandler.removeMessages(MSG_LOAD_MEETINGS);
    150             super.onDestroy();
    151         }
    152 
    153         /*
    154          * Captures tap event (and tap type) and increments correct tap type total.
    155          */
    156         @Override
    157         public void onTapCommand(int tapType, int x, int y, long eventTime) {
    158             if (Log.isLoggable(TAG, Log.DEBUG)) {
    159                 Log.d(TAG, "Tap Command: " + tapType);
    160             }
    161 
    162             // Ignore lint error (fixed in wearable support library 1.4)
    163             if (tapType == WatchFaceService.TAP_TYPE_TAP && !mCalendarPermissionApproved) {
    164                 Intent permissionIntent = new Intent(
    165                         getApplicationContext(),
    166                         CalendarWatchFacePermissionActivity.class);
    167                 permissionIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
    168                 startActivity(permissionIntent);
    169             }
    170         }
    171 
    172         @Override
    173         public void onDraw(Canvas canvas, Rect bounds) {
    174             // Create or update mLayout if necessary.
    175             if (mLayout == null || mLayoutWidth != bounds.width()) {
    176                 mLayoutWidth = bounds.width();
    177                 mLayout = new DynamicLayout(mEditable, mTextPaint, mLayoutWidth,
    178                         Layout.Alignment.ALIGN_NORMAL, 1 /* spacingMult */, 0 /* spacingAdd */,
    179                         false /* includePad */);
    180             }
    181 
    182             // Update the contents of mEditable.
    183             mEditable.clear();
    184 
    185             if (mCalendarPermissionApproved) {
    186                 mEditable.append(Html.fromHtml(getResources().getQuantityString(
    187                         R.plurals.calendar_meetings, mNumMeetings, mNumMeetings)));
    188             } else {
    189                 mEditable.append(Html.fromHtml(mCalendarNotApprovedMessage));
    190             }
    191 
    192             // Draw the text on a solid background.
    193             canvas.drawColor(BACKGROUND_COLOR);
    194             mLayout.draw(canvas);
    195         }
    196 
    197         @Override
    198         public void onVisibilityChanged(boolean visible) {
    199             Log.d(TAG, "onVisibilityChanged()");
    200             super.onVisibilityChanged(visible);
    201             if (visible) {
    202 
    203                 // Enables app to handle 23+ (M+) style permissions.
    204                 mCalendarPermissionApproved = ActivityCompat.checkSelfPermission(
    205                         getApplicationContext(),
    206                         Manifest.permission.READ_CALENDAR) == PackageManager.PERMISSION_GRANTED;
    207 
    208                 if (mCalendarPermissionApproved) {
    209                     IntentFilter filter = new IntentFilter(Intent.ACTION_PROVIDER_CHANGED);
    210                     filter.addDataScheme("content");
    211                     filter.addDataAuthority(WearableCalendarContract.AUTHORITY, null);
    212                     registerReceiver(mBroadcastReceiver, filter);
    213                     mIsReceiverRegistered = true;
    214 
    215                     mLoadMeetingsHandler.sendEmptyMessage(MSG_LOAD_MEETINGS);
    216                 }
    217             } else {
    218                 if (mIsReceiverRegistered) {
    219                     unregisterReceiver(mBroadcastReceiver);
    220                     mIsReceiverRegistered = false;
    221                 }
    222                 mLoadMeetingsHandler.removeMessages(MSG_LOAD_MEETINGS);
    223             }
    224         }
    225 
    226         private void onMeetingsLoaded(Integer result) {
    227             if (result != null) {
    228                 mNumMeetings = result;
    229                 invalidate();
    230             }
    231         }
    232 
    233         private void cancelLoadMeetingTask() {
    234             if (mLoadMeetingsTask != null) {
    235                 mLoadMeetingsTask.cancel(true);
    236             }
    237         }
    238 
    239         /**
    240          * Asynchronous task to load the meetings from the content provider and report the number of
    241          * meetings back via {@link #onMeetingsLoaded}.
    242          */
    243         private class LoadMeetingsTask extends AsyncTask<Void, Void, Integer> {
    244             private PowerManager.WakeLock mWakeLock;
    245 
    246             @Override
    247             protected Integer doInBackground(Void... voids) {
    248                 PowerManager powerManager = (PowerManager) getSystemService(POWER_SERVICE);
    249                 mWakeLock = powerManager.newWakeLock(
    250                         PowerManager.PARTIAL_WAKE_LOCK, "CalendarWatchFaceWakeLock");
    251                 mWakeLock.acquire();
    252 
    253                 long begin = System.currentTimeMillis();
    254                 Uri.Builder builder =
    255                         WearableCalendarContract.Instances.CONTENT_URI.buildUpon();
    256                 ContentUris.appendId(builder, begin);
    257                 ContentUris.appendId(builder, begin + DateUtils.DAY_IN_MILLIS);
    258                 final Cursor cursor = getContentResolver().query(builder.build(),
    259                         null, null, null, null);
    260                 int numMeetings = cursor.getCount();
    261 
    262                 Log.d(TAG, "Num meetings: " + numMeetings);
    263 
    264                 return numMeetings;
    265             }
    266 
    267             @Override
    268             protected void onPostExecute(Integer result) {
    269                 releaseWakeLock();
    270                 onMeetingsLoaded(result);
    271             }
    272 
    273             @Override
    274             protected void onCancelled() {
    275                 releaseWakeLock();
    276             }
    277 
    278             private void releaseWakeLock() {
    279                 if (mWakeLock != null) {
    280                     mWakeLock.release();
    281                     mWakeLock = null;
    282                 }
    283             }
    284         }
    285     }
    286 }