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