/* * Copyright (C) 2014 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.example.android.wearable.agendadata; import static com.example.android.wearable.agendadata.Constants.TAG; import static com.example.android.wearable.agendadata.Constants.CONNECTION_TIME_OUT_MS; import static com.example.android.wearable.agendadata.Constants.CAL_DATA_ITEM_PATH_PREFIX; import static com.example.android.wearable.agendadata.Constants.ALL_DAY; import static com.example.android.wearable.agendadata.Constants.BEGIN; import static com.example.android.wearable.agendadata.Constants.DATA_ITEM_URI; import static com.example.android.wearable.agendadata.Constants.DESCRIPTION; import static com.example.android.wearable.agendadata.Constants.END; import static com.example.android.wearable.agendadata.Constants.EVENT_ID; import static com.example.android.wearable.agendadata.Constants.ID; import static com.example.android.wearable.agendadata.Constants.PROFILE_PIC; import static com.example.android.wearable.agendadata.Constants.TITLE; import android.app.IntentService; import android.content.ContentResolver; import android.content.ContentUris; import android.content.Context; import android.content.Intent; import android.content.res.Resources; import android.database.Cursor; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.net.Uri; import android.os.Bundle; import android.provider.CalendarContract; import android.provider.ContactsContract.CommonDataKinds.Email; import android.provider.ContactsContract.Contacts; import android.provider.ContactsContract.Data; import android.text.format.Time; import android.util.Log; import com.google.android.gms.common.ConnectionResult; import com.google.android.gms.common.api.GoogleApiClient; import com.google.android.gms.common.api.GoogleApiClient.ConnectionCallbacks; import com.google.android.gms.common.api.GoogleApiClient.OnConnectionFailedListener; import com.google.android.gms.wearable.Asset; import com.google.android.gms.wearable.DataMap; import com.google.android.gms.wearable.PutDataMapRequest; import com.google.android.gms.wearable.Wearable; import java.io.ByteArrayOutputStream; import java.io.Closeable; import java.io.IOException; import java.io.InputStream; import java.util.ArrayList; import java.util.List; import java.util.concurrent.TimeUnit; /** * Queries calendar events using Android Calendar Provider API and creates a data item for each * event. */ public class CalendarQueryService extends IntentService implements ConnectionCallbacks, OnConnectionFailedListener { private static final String[] INSTANCE_PROJECTION = { CalendarContract.Instances._ID, CalendarContract.Instances.EVENT_ID, CalendarContract.Instances.TITLE, CalendarContract.Instances.BEGIN, CalendarContract.Instances.END, CalendarContract.Instances.ALL_DAY, CalendarContract.Instances.DESCRIPTION, CalendarContract.Instances.ORGANIZER }; private static final String[] CONTACT_PROJECTION = new String[] { Data._ID, Data.CONTACT_ID }; private static final String CONTACT_SELECTION = Email.ADDRESS + " = ?"; private GoogleApiClient mGoogleApiClient; public CalendarQueryService() { super(CalendarQueryService.class.getSimpleName()); } @Override public void onCreate() { super.onCreate(); mGoogleApiClient = new GoogleApiClient.Builder(this) .addApi(Wearable.API) .addConnectionCallbacks(this) .addOnConnectionFailedListener(this) .build(); } @Override protected void onHandleIntent(Intent intent) { mGoogleApiClient.blockingConnect(CONNECTION_TIME_OUT_MS, TimeUnit.MILLISECONDS); // Query calendar events in the next 24 hours. Time time = new Time(); time.setToNow(); long beginTime = time.toMillis(true); time.monthDay++; time.normalize(true); long endTime = time.normalize(true); List events = queryEvents(this, beginTime, endTime); for (Event event : events) { final PutDataMapRequest putDataMapRequest = event.toPutDataMapRequest(); if (mGoogleApiClient.isConnected()) { Wearable.DataApi.putDataItem( mGoogleApiClient, putDataMapRequest.asPutDataRequest()).await(); } else { Log.e(TAG, "Failed to send data item: " + putDataMapRequest + " - Client disconnected from Google Play Services"); } } mGoogleApiClient.disconnect(); } private static String makeDataItemPath(long eventId, long beginTime) { return CAL_DATA_ITEM_PATH_PREFIX + eventId + "/" + beginTime; } private static List queryEvents(Context context, long beginTime, long endTime) { ContentResolver contentResolver = context.getContentResolver(); Uri.Builder builder = CalendarContract.Instances.CONTENT_URI.buildUpon(); ContentUris.appendId(builder, beginTime); ContentUris.appendId(builder, endTime); Cursor cursor = contentResolver.query(builder.build(), INSTANCE_PROJECTION, null /* selection */, null /* selectionArgs */, null /* sortOrder */); try { int idIdx = cursor.getColumnIndex(CalendarContract.Instances._ID); int eventIdIdx = cursor.getColumnIndex(CalendarContract.Instances.EVENT_ID); int titleIdx = cursor.getColumnIndex(CalendarContract.Instances.TITLE); int beginIdx = cursor.getColumnIndex(CalendarContract.Instances.BEGIN); int endIdx = cursor.getColumnIndex(CalendarContract.Instances.END); int allDayIdx = cursor.getColumnIndex(CalendarContract.Instances.ALL_DAY); int descIdx = cursor.getColumnIndex(CalendarContract.Instances.DESCRIPTION); int ownerEmailIdx = cursor.getColumnIndex(CalendarContract.Instances.ORGANIZER); List events = new ArrayList(cursor.getCount()); while (cursor.moveToNext()) { Event event = new Event(); event.id = cursor.getLong(idIdx); event.eventId = cursor.getLong(eventIdIdx); event.title = cursor.getString(titleIdx); event.begin = cursor.getLong(beginIdx); event.end = cursor.getLong(endIdx); event.allDay = cursor.getInt(allDayIdx) != 0; event.description = cursor.getString(descIdx); String ownerEmail = cursor.getString(ownerEmailIdx); Cursor contactCursor = contentResolver.query(Data.CONTENT_URI, CONTACT_PROJECTION, CONTACT_SELECTION, new String[] {ownerEmail}, null); int ownerIdIdx = contactCursor.getColumnIndex(Data.CONTACT_ID); long ownerId = -1; if (contactCursor.moveToFirst()) { ownerId = contactCursor.getLong(ownerIdIdx); } contactCursor.close(); // Use event organizer's profile picture as the notification background. event.ownerProfilePic = getProfilePicture(contentResolver, context, ownerId); events.add(event); } return events; } finally { cursor.close(); } } @Override public void onConnected(Bundle connectionHint) { } @Override public void onConnectionSuspended(int cause) { } @Override public void onConnectionFailed(ConnectionResult result) { } private static Asset getDefaultProfile(Resources res) { Bitmap bitmap = BitmapFactory.decodeResource(res, R.drawable.nobody); return Asset.createFromBytes(toByteArray(bitmap)); } private static Asset getProfilePicture(ContentResolver contentResolver, Context context, long contactId) { if (contactId != -1) { // Try to retrieve the profile picture for the given contact. Uri contactUri = ContentUris.withAppendedId(Contacts.CONTENT_URI, contactId); InputStream inputStream = Contacts.openContactPhotoInputStream(contentResolver, contactUri, true /*preferHighres*/); if (null != inputStream) { try { Bitmap bitmap = BitmapFactory.decodeStream(inputStream); if (bitmap != null) { return Asset.createFromBytes(toByteArray(bitmap)); } else { Log.e(TAG, "Cannot decode profile picture for contact " + contactId); } } finally { closeQuietly(inputStream); } } } // Use a default background image if the user has no profile picture or there was an error. return getDefaultProfile(context.getResources()); } private static byte[] toByteArray(Bitmap bitmap) { ByteArrayOutputStream stream = new ByteArrayOutputStream(); bitmap.compress(Bitmap.CompressFormat.PNG, 100, stream); byte[] byteArray = stream.toByteArray(); closeQuietly(stream); return byteArray; } private static void closeQuietly(Closeable closeable) { try { closeable.close(); } catch (IOException e) { Log.e(TAG, "IOException while closing closeable.", e); } } private static class Event { public long id; public long eventId; public String title; public long begin; public long end; public boolean allDay; public String description; public Asset ownerProfilePic; public PutDataMapRequest toPutDataMapRequest(){ final PutDataMapRequest putDataMapRequest = PutDataMapRequest.create( makeDataItemPath(eventId, begin)); /* In most cases (as in this one), you don't need your DataItem appear instantly. By default, delivery of normal DataItems to the Wear network might be delayed in order to improve battery life for user devices. However, if you can't tolerate a delay in the sync of your DataItems, you can mark them as urgent via setUrgent(). */ DataMap data = putDataMapRequest.getDataMap(); data.putString(DATA_ITEM_URI, putDataMapRequest.getUri().toString()); data.putLong(ID, id); data.putLong(EVENT_ID, eventId); data.putString(TITLE, title); data.putLong(BEGIN, begin); data.putLong(END, end); data.putBoolean(ALL_DAY, allDay); data.putString(DESCRIPTION, description); data.putAsset(PROFILE_PIC, ownerProfilePic); return putDataMapRequest; } } }