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 }