• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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             DataMap data = putDataMapRequest.getDataMap();
254             data.putString(DATA_ITEM_URI, putDataMapRequest.getUri().toString());
255             data.putLong(ID, id);
256             data.putLong(EVENT_ID, eventId);
257             data.putString(TITLE, title);
258             data.putLong(BEGIN, begin);
259             data.putLong(END, end);
260             data.putBoolean(ALL_DAY, allDay);
261             data.putString(DESCRIPTION, description);
262             data.putAsset(PROFILE_PIC, ownerProfilePic);
263 
264             return putDataMapRequest;
265         }
266     }
267 }
268