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