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