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 onCreateEngine()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 onCreate(SurfaceHolder holder)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 onDestroy()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 onTapCommand(int tapType, int x, int y, long eventTime)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 onDraw(Canvas canvas, Rect bounds)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 onVisibilityChanged(boolean visible)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 onMeetingsLoaded(Integer result)226 private void onMeetingsLoaded(Integer result) { 227 if (result != null) { 228 mNumMeetings = result; 229 invalidate(); 230 } 231 } 232 cancelLoadMeetingTask()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 doInBackground(Void... voids)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 onPostExecute(Integer result)268 protected void onPostExecute(Integer result) { 269 releaseWakeLock(); 270 onMeetingsLoaded(result); 271 } 272 273 @Override onCancelled()274 protected void onCancelled() { 275 releaseWakeLock(); 276 } 277 releaseWakeLock()278 private void releaseWakeLock() { 279 if (mWakeLock != null) { 280 mWakeLock.release(); 281 mWakeLock = null; 282 } 283 } 284 } 285 } 286 }