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.agendadata;
18 
19 
20 import static com.example.android.wearable.agendadata.Constants.TAG;
21 import static com.example.android.wearable.agendadata.Constants.CONNECTION_TIME_OUT_MS;
22 import static com.example.android.wearable.agendadata.Constants.CAL_DATA_ITEM_PATH_PREFIX;
23 import static com.example.android.wearable.agendadata.Constants.ALL_DAY;
24 import static com.example.android.wearable.agendadata.Constants.BEGIN;
25 import static com.example.android.wearable.agendadata.Constants.DATA_ITEM_URI;
26 import static com.example.android.wearable.agendadata.Constants.DESCRIPTION;
27 import static com.example.android.wearable.agendadata.Constants.END;
28 import static com.example.android.wearable.agendadata.Constants.EVENT_ID;
29 import static com.example.android.wearable.agendadata.Constants.ID;
30 import static com.example.android.wearable.agendadata.Constants.PROFILE_PIC;
31 import static com.example.android.wearable.agendadata.Constants.TITLE;
32 
33 import android.app.IntentService;
34 import android.content.ContentResolver;
35 import android.content.ContentUris;
36 import android.content.Context;
37 import android.content.Intent;
38 import android.content.res.Resources;
39 import android.database.Cursor;
40 import android.graphics.Bitmap;
41 import android.graphics.BitmapFactory;
42 import android.net.Uri;
43 import android.os.Bundle;
44 import android.provider.CalendarContract;
45 import android.provider.ContactsContract.CommonDataKinds.Email;
46 import android.provider.ContactsContract.Contacts;
47 import android.provider.ContactsContract.Data;
48 import android.text.format.Time;
49 import android.util.Log;
50 
51 import com.google.android.gms.common.ConnectionResult;
52 import com.google.android.gms.common.api.GoogleApiClient;
53 import com.google.android.gms.common.api.GoogleApiClient.ConnectionCallbacks;
54 import com.google.android.gms.common.api.GoogleApiClient.OnConnectionFailedListener;
55 import com.google.android.gms.wearable.Asset;
56 import com.google.android.gms.wearable.DataMap;
57 import com.google.android.gms.wearable.PutDataMapRequest;
58 import com.google.android.gms.wearable.Wearable;
59 
60 import java.io.ByteArrayOutputStream;
61 import java.io.Closeable;
62 import java.io.IOException;
63 import java.io.InputStream;
64 import java.util.ArrayList;
65 import java.util.List;
66 import java.util.concurrent.TimeUnit;
67 
68 /**
69  * Queries calendar events using Android Calendar Provider API and creates a data item for each
70  * event.
71  */
72 public class CalendarQueryService extends IntentService
73         implements ConnectionCallbacks, OnConnectionFailedListener {
74 
75     private static final String[] INSTANCE_PROJECTION = {
76             CalendarContract.Instances._ID,
77             CalendarContract.Instances.EVENT_ID,
78             CalendarContract.Instances.TITLE,
79             CalendarContract.Instances.BEGIN,
80             CalendarContract.Instances.END,
81             CalendarContract.Instances.ALL_DAY,
82             CalendarContract.Instances.DESCRIPTION,
83             CalendarContract.Instances.ORGANIZER
84     };
85 
86     private static final String[] CONTACT_PROJECTION = new String[] { Data._ID, Data.CONTACT_ID };
87     private static final String CONTACT_SELECTION = Email.ADDRESS + " = ?";
88 
89     private GoogleApiClient mGoogleApiClient;
90 
CalendarQueryService()91     public CalendarQueryService() {
92         super(CalendarQueryService.class.getSimpleName());
93     }
94 
95     @Override
onCreate()96     public void onCreate() {
97         super.onCreate();
98         mGoogleApiClient = new GoogleApiClient.Builder(this)
99                 .addApi(Wearable.API)
100                 .addConnectionCallbacks(this)
101                 .addOnConnectionFailedListener(this)
102                 .build();
103     }
104 
105     @Override
onHandleIntent(Intent intent)106     protected void onHandleIntent(Intent intent) {
107         mGoogleApiClient.blockingConnect(CONNECTION_TIME_OUT_MS, TimeUnit.MILLISECONDS);
108         // Query calendar events in the next 24 hours.
109         Time time = new Time();
110         time.setToNow();
111         long beginTime = time.toMillis(true);
112         time.monthDay++;
113         time.normalize(true);
114         long endTime = time.normalize(true);
115 
116         List<Event> events = queryEvents(this, beginTime, endTime);
117         for (Event event : events) {
118             final PutDataMapRequest putDataMapRequest = event.toPutDataMapRequest();
119             if (mGoogleApiClient.isConnected()) {
120                 Wearable.DataApi.putDataItem(
121                     mGoogleApiClient, putDataMapRequest.asPutDataRequest()).await();
122             } else {
123                 Log.e(TAG, "Failed to send data item: " + putDataMapRequest
124                          + " - Client disconnected from Google Play Services");
125             }
126         }
127         mGoogleApiClient.disconnect();
128     }
129 
makeDataItemPath(long eventId, long beginTime)130     private static String makeDataItemPath(long eventId, long beginTime) {
131         return CAL_DATA_ITEM_PATH_PREFIX + eventId + "/" + beginTime;
132     }
133 
queryEvents(Context context, long beginTime, long endTime)134     private static List<Event> queryEvents(Context context, long beginTime, long endTime) {
135         ContentResolver contentResolver = context.getContentResolver();
136         Uri.Builder builder = CalendarContract.Instances.CONTENT_URI.buildUpon();
137         ContentUris.appendId(builder, beginTime);
138         ContentUris.appendId(builder, endTime);
139 
140         Cursor cursor = contentResolver.query(builder.build(), INSTANCE_PROJECTION,
141                 null /* selection */, null /* selectionArgs */, null /* sortOrder */);
142         try {
143             int idIdx = cursor.getColumnIndex(CalendarContract.Instances._ID);
144             int eventIdIdx = cursor.getColumnIndex(CalendarContract.Instances.EVENT_ID);
145             int titleIdx = cursor.getColumnIndex(CalendarContract.Instances.TITLE);
146             int beginIdx = cursor.getColumnIndex(CalendarContract.Instances.BEGIN);
147             int endIdx = cursor.getColumnIndex(CalendarContract.Instances.END);
148             int allDayIdx = cursor.getColumnIndex(CalendarContract.Instances.ALL_DAY);
149             int descIdx = cursor.getColumnIndex(CalendarContract.Instances.DESCRIPTION);
150             int ownerEmailIdx = cursor.getColumnIndex(CalendarContract.Instances.ORGANIZER);
151 
152             List<Event> events = new ArrayList<Event>(cursor.getCount());
153             while (cursor.moveToNext()) {
154                 Event event = new Event();
155                 event.id = cursor.getLong(idIdx);
156                 event.eventId = cursor.getLong(eventIdIdx);
157                 event.title = cursor.getString(titleIdx);
158                 event.begin = cursor.getLong(beginIdx);
159                 event.end = cursor.getLong(endIdx);
160                 event.allDay = cursor.getInt(allDayIdx) != 0;
161                 event.description = cursor.getString(descIdx);
162                 String ownerEmail = cursor.getString(ownerEmailIdx);
163                 Cursor contactCursor = contentResolver.query(Data.CONTENT_URI,
164                         CONTACT_PROJECTION, CONTACT_SELECTION, new String[] {ownerEmail}, null);
165                 int ownerIdIdx = contactCursor.getColumnIndex(Data.CONTACT_ID);
166                 long ownerId = -1;
167                 if (contactCursor.moveToFirst()) {
168                     ownerId = contactCursor.getLong(ownerIdIdx);
169                 }
170                 contactCursor.close();
171                 // Use event organizer's profile picture as the notification background.
172                 event.ownerProfilePic = getProfilePicture(contentResolver, context, ownerId);
173                 events.add(event);
174             }
175             return events;
176         } finally {
177             cursor.close();
178         }
179     }
180 
181     @Override
onConnected(Bundle connectionHint)182     public void onConnected(Bundle connectionHint) {
183     }
184 
185     @Override
onConnectionSuspended(int cause)186     public void onConnectionSuspended(int cause) {
187     }
188 
189     @Override
onConnectionFailed(ConnectionResult result)190     public void onConnectionFailed(ConnectionResult result) {
191     }
192 
getDefaultProfile(Resources res)193     private static Asset getDefaultProfile(Resources res) {
194         Bitmap bitmap = BitmapFactory.decodeResource(res, R.drawable.nobody);
195         return Asset.createFromBytes(toByteArray(bitmap));
196     }
197 
getProfilePicture(ContentResolver contentResolver, Context context, long contactId)198     private static Asset getProfilePicture(ContentResolver contentResolver, Context context,
199                                            long contactId) {
200         if (contactId != -1) {
201             // Try to retrieve the profile picture for the given contact.
202             Uri contactUri = ContentUris.withAppendedId(Contacts.CONTENT_URI, contactId);
203             InputStream inputStream = Contacts.openContactPhotoInputStream(contentResolver,
204                     contactUri, true /*preferHighres*/);
205 
206             if (null != inputStream) {
207                 try {
208                     Bitmap bitmap = BitmapFactory.decodeStream(inputStream);
209                     if (bitmap != null) {
210                         return Asset.createFromBytes(toByteArray(bitmap));
211                     } else {
212                         Log.e(TAG, "Cannot decode profile picture for contact " + contactId);
213                     }
214                 } finally {
215                     closeQuietly(inputStream);
216                 }
217             }
218         }
219         // Use a default background image if the user has no profile picture or there was an error.
220         return getDefaultProfile(context.getResources());
221     }
222 
toByteArray(Bitmap bitmap)223     private static byte[] toByteArray(Bitmap bitmap) {
224         ByteArrayOutputStream stream = new ByteArrayOutputStream();
225         bitmap.compress(Bitmap.CompressFormat.PNG, 100, stream);
226         byte[] byteArray = stream.toByteArray();
227         closeQuietly(stream);
228         return byteArray;
229     }
230 
closeQuietly(Closeable closeable)231     private static void closeQuietly(Closeable closeable) {
232         try {
233             closeable.close();
234         } catch (IOException e) {
235             Log.e(TAG, "IOException while closing closeable.", e);
236         }
237     }
238 
239     private static class Event {
240 
241         public long id;
242         public long eventId;
243         public String title;
244         public long begin;
245         public long end;
246         public boolean allDay;
247         public String description;
248         public Asset ownerProfilePic;
249 
toPutDataMapRequest()250         public PutDataMapRequest toPutDataMapRequest(){
251             final PutDataMapRequest putDataMapRequest = PutDataMapRequest.create(
252                     makeDataItemPath(eventId, begin));
253             /* In most cases (as in this one), you don't need your DataItem appear instantly. By
254             default, delivery of normal DataItems to the Wear network might be delayed in order to
255             improve battery life for user devices. However, if you can't tolerate a delay in the
256             sync of your DataItems, you can mark them as urgent via setUrgent().
257              */
258             DataMap data = putDataMapRequest.getDataMap();
259             data.putString(DATA_ITEM_URI, putDataMapRequest.getUri().toString());
260             data.putLong(ID, id);
261             data.putLong(EVENT_ID, eventId);
262             data.putString(TITLE, title);
263             data.putLong(BEGIN, begin);
264             data.putLong(END, end);
265             data.putBoolean(ALL_DAY, allDay);
266             data.putString(DESCRIPTION, description);
267             data.putAsset(PROFILE_PIC, ownerProfilePic);
268 
269             return putDataMapRequest;
270         }
271     }
272 }
273