| /* |
| * Copyright 2014 Google Inc. All rights reserved. |
| * |
| * 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.google.samples.apps.iosched.service; |
| |
| import android.util.Log; |
| import com.google.samples.apps.iosched.Config; |
| import com.google.samples.apps.iosched.R; |
| import com.google.samples.apps.iosched.provider.ScheduleContract; |
| import com.google.samples.apps.iosched.util.AccountUtils; |
| |
| import android.annotation.TargetApi; |
| import android.app.IntentService; |
| import android.content.ContentProviderOperation; |
| import android.content.ContentResolver; |
| import android.content.ContentValues; |
| import android.content.Intent; |
| import android.content.OperationApplicationException; |
| import android.database.Cursor; |
| import android.net.Uri; |
| import android.os.Bundle; |
| import android.os.RemoteException; |
| import android.provider.CalendarContract; |
| import android.text.TextUtils; |
| import com.google.samples.apps.iosched.util.PrefUtils; |
| |
| import java.util.ArrayList; |
| |
| import static com.google.samples.apps.iosched.util.LogUtils.LOGE; |
| import static com.google.samples.apps.iosched.util.LogUtils.LOGW; |
| import static com.google.samples.apps.iosched.util.LogUtils.makeLogTag; |
| |
| /** |
| * Background {@link android.app.Service} that adds or removes session Calendar events through |
| * the {@link CalendarContract} API available in Android 4.0 or above. |
| */ |
| public class SessionCalendarService extends IntentService { |
| private static final String TAG = makeLogTag(SessionCalendarService.class); |
| |
| public static final String ACTION_ADD_SESSION_CALENDAR = |
| "com.google.samples.apps.iosched.action.ADD_SESSION_CALENDAR"; |
| public static final String ACTION_REMOVE_SESSION_CALENDAR = |
| "com.google.samples.apps.iosched.action.REMOVE_SESSION_CALENDAR"; |
| public static final String ACTION_UPDATE_ALL_SESSIONS_CALENDAR = |
| "com.google.samples.apps.iosched.action.UPDATE_ALL_SESSIONS_CALENDAR"; |
| public static final String ACTION_UPDATE_ALL_SESSIONS_CALENDAR_COMPLETED = |
| "com.google.samples.apps.iosched.action.UPDATE_CALENDAR_COMPLETED"; |
| public static final String ACTION_CLEAR_ALL_SESSIONS_CALENDAR = |
| "com.google.samples.apps.iosched.action.CLEAR_ALL_SESSIONS_CALENDAR"; |
| public static final String EXTRA_ACCOUNT_NAME = |
| "com.google.samples.apps.iosched.extra.ACCOUNT_NAME"; |
| public static final String EXTRA_SESSION_START = |
| "com.google.samples.apps.iosched.extra.SESSION_BLOCK_START"; |
| public static final String EXTRA_SESSION_END = |
| "com.google.samples.apps.iosched.extra.SESSION_BLOCK_END"; |
| public static final String EXTRA_SESSION_TITLE = |
| "com.google.samples.apps.iosched.extra.SESSION_TITLE"; |
| public static final String EXTRA_SESSION_ROOM = |
| "com.google.samples.apps.iosched.extra.SESSION_ROOM"; |
| |
| private static final long INVALID_CALENDAR_ID = -1; |
| |
| // TODO: localize |
| private static final String CALENDAR_CLEAR_SEARCH_LIKE_EXPRESSION = |
| "%added by Google I/O Android app%"; |
| |
| public SessionCalendarService() { |
| super(TAG); |
| } |
| |
| @Override |
| protected void onHandleIntent(Intent intent) { |
| final String action = intent.getAction(); |
| Log.d(TAG, "Received intent: " + action); |
| |
| final ContentResolver resolver = getContentResolver(); |
| |
| boolean isAddEvent = false; |
| |
| if (ACTION_ADD_SESSION_CALENDAR.equals(action)) { |
| isAddEvent = true; |
| |
| } else if (ACTION_REMOVE_SESSION_CALENDAR.equals(action)) { |
| isAddEvent = false; |
| |
| } else if (ACTION_UPDATE_ALL_SESSIONS_CALENDAR.equals(action) && |
| PrefUtils.shouldSyncCalendar(this)) { |
| try { |
| getContentResolver().applyBatch(CalendarContract.AUTHORITY, |
| processAllSessionsCalendar(resolver, getCalendarId(intent))); |
| sendBroadcast(new Intent( |
| SessionCalendarService.ACTION_UPDATE_ALL_SESSIONS_CALENDAR_COMPLETED)); |
| } catch (RemoteException e) { |
| LOGE(TAG, "Error adding all sessions to Google Calendar", e); |
| } catch (OperationApplicationException e) { |
| LOGE(TAG, "Error adding all sessions to Google Calendar", e); |
| } |
| |
| } else if (ACTION_CLEAR_ALL_SESSIONS_CALENDAR.equals(action)) { |
| try { |
| getContentResolver().applyBatch(CalendarContract.AUTHORITY, |
| processClearAllSessions(resolver, getCalendarId(intent))); |
| } catch (RemoteException e) { |
| LOGE(TAG, "Error clearing all sessions from Google Calendar", e); |
| } catch (OperationApplicationException e) { |
| LOGE(TAG, "Error clearing all sessions from Google Calendar", e); |
| } |
| |
| } else { |
| return; |
| } |
| |
| final Uri uri = intent.getData(); |
| final Bundle extras = intent.getExtras(); |
| if (uri == null || extras == null || !PrefUtils.shouldSyncCalendar(this)) { |
| return; |
| } |
| |
| try { |
| resolver.applyBatch(CalendarContract.AUTHORITY, |
| processSessionCalendar(resolver, getCalendarId(intent), isAddEvent, uri, |
| extras.getLong(EXTRA_SESSION_START), |
| extras.getLong(EXTRA_SESSION_END), |
| extras.getString(EXTRA_SESSION_TITLE), |
| extras.getString(EXTRA_SESSION_ROOM))); |
| } catch (RemoteException e) { |
| LOGE(TAG, "Error adding session to Google Calendar", e); |
| } catch (OperationApplicationException e) { |
| LOGE(TAG, "Error adding session to Google Calendar", e); |
| } |
| } |
| |
| /** |
| * Gets the currently-logged in user's Google Calendar, or the Google Calendar for the user |
| * specified in the given intent's {@link #EXTRA_ACCOUNT_NAME}. |
| */ |
| private long getCalendarId(Intent intent) { |
| final String accountName; |
| if (intent != null && intent.hasExtra(EXTRA_ACCOUNT_NAME)) { |
| accountName = intent.getStringExtra(EXTRA_ACCOUNT_NAME); |
| } else { |
| accountName = AccountUtils.getActiveAccountName(this); |
| } |
| |
| if (TextUtils.isEmpty(accountName)) { |
| return INVALID_CALENDAR_ID; |
| } |
| |
| // TODO: The calendar ID should be stored in shared preferences upon choosing an account. |
| Cursor calendarsCursor = getContentResolver().query( |
| CalendarContract.Calendars.CONTENT_URI, |
| new String[]{"_id"}, |
| // TODO: What if the calendar is not displayed or not sync'd? |
| "account_name = ownerAccount and account_name = ?", |
| new String[]{accountName}, |
| null); |
| |
| long calendarId = INVALID_CALENDAR_ID; |
| if (calendarsCursor != null && calendarsCursor.moveToFirst()) { |
| calendarId = calendarsCursor.getLong(0); |
| calendarsCursor.close(); |
| } |
| |
| return calendarId; |
| } |
| |
| private String makeCalendarEventTitle(String sessionTitle) { |
| return sessionTitle + getResources().getString(R.string.session_calendar_suffix); |
| } |
| |
| /** |
| * Processes all sessions in the |
| * {@link com.google.samples.apps.iosched.provider.ScheduleProvider}, adding or removing |
| * calendar events to/from the specified Google Calendar depending on whether a session is |
| * in the user's schedule or not. |
| */ |
| private ArrayList<ContentProviderOperation> processAllSessionsCalendar(ContentResolver resolver, |
| final long calendarId) { |
| |
| ArrayList<ContentProviderOperation> batch = new ArrayList<ContentProviderOperation>(); |
| |
| // Unable to find the Calendar associated with the user. Stop here. |
| if (calendarId == INVALID_CALENDAR_ID) { |
| return batch; |
| } |
| |
| // Retrieves all sessions. For each session, add to Calendar if starred and attempt to |
| // remove from Calendar if unstarred. |
| Cursor cursor = resolver.query( |
| ScheduleContract.Sessions.CONTENT_URI, |
| SessionsQuery.PROJECTION, |
| null, null, null); |
| |
| if (cursor != null) { |
| while (cursor.moveToNext()) { |
| Uri uri = ScheduleContract.Sessions.buildSessionUri( |
| Long.valueOf(cursor.getLong(0)).toString()); |
| boolean isAddEvent = (cursor.getInt(SessionsQuery.SESSION_IN_MY_SCHEDULE) == 1); |
| if (isAddEvent) { |
| batch.addAll(processSessionCalendar(resolver, |
| calendarId, isAddEvent, uri, |
| cursor.getLong(SessionsQuery.SESSION_START), |
| cursor.getLong(SessionsQuery.SESSION_END), |
| cursor.getString(SessionsQuery.SESSION_TITLE), |
| cursor.getString(SessionsQuery.ROOM_NAME))); |
| } |
| } |
| cursor.close(); |
| } |
| |
| return batch; |
| } |
| |
| /** |
| * Adds or removes a single session to/from the specified Google Calendar. |
| */ |
| private ArrayList<ContentProviderOperation> processSessionCalendar( |
| final ContentResolver resolver, |
| final long calendarId, final boolean isAddEvent, |
| final Uri sessionUri, final long sessionBlockStart, final long sessionBlockEnd, |
| final String sessionTitle, final String sessionRoom) { |
| ArrayList<ContentProviderOperation> batch = new ArrayList<ContentProviderOperation>(); |
| |
| // Unable to find the Calendar associated with the user. Stop here. |
| if (calendarId == INVALID_CALENDAR_ID) { |
| return batch; |
| } |
| |
| final String calendarEventTitle = makeCalendarEventTitle(sessionTitle); |
| |
| Cursor cursor; |
| ContentValues values = new ContentValues(); |
| |
| // Add Calendar event. |
| if (isAddEvent) { |
| if (sessionBlockStart == 0L || sessionBlockEnd == 0L || sessionTitle == null) { |
| LOGW(TAG, "Unable to add a Calendar event due to insufficient input parameters."); |
| return batch; |
| } |
| |
| // Check if the calendar event exists first. If it does, we don't want to add a |
| // duplicate one. |
| cursor = resolver.query( |
| CalendarContract.Events.CONTENT_URI, // URI |
| new String[] {CalendarContract.Events._ID}, // Projection |
| CalendarContract.Events.CALENDAR_ID + "=? and " // Selection |
| + CalendarContract.Events.TITLE + "=? and " |
| + CalendarContract.Events.DTSTART + ">=? and " |
| + CalendarContract.Events.DTEND + "<=?", |
| new String[]{ // Selection args |
| Long.valueOf(calendarId).toString(), |
| calendarEventTitle, |
| Long.toString(Config.CONFERENCE_START_MILLIS), |
| Long.toString(Config.CONFERENCE_END_MILLIS) |
| }, |
| null); |
| |
| long newEventId = -1; |
| |
| if (cursor != null && cursor.moveToFirst()) { |
| // Calendar event already exists for this session. |
| newEventId = cursor.getLong(0); |
| cursor.close(); |
| |
| // Data fix (workaround): |
| batch.add( |
| ContentProviderOperation.newUpdate(CalendarContract.Events.CONTENT_URI) |
| .withValue(CalendarContract.Events.EVENT_TIMEZONE, |
| Config.CONFERENCE_TIMEZONE.getID()) |
| .withSelection(CalendarContract.Events._ID + "=?", |
| new String[]{Long.valueOf(newEventId).toString()}) |
| .build() |
| ); |
| // End data fix. |
| |
| } else { |
| // Calendar event doesn't exist, create it. |
| |
| // NOTE: we can't use batch processing here because we need the result of |
| // the insert. |
| values.clear(); |
| values.put(CalendarContract.Events.DTSTART, sessionBlockStart); |
| values.put(CalendarContract.Events.DTEND, sessionBlockEnd); |
| values.put(CalendarContract.Events.EVENT_LOCATION, sessionRoom); |
| values.put(CalendarContract.Events.TITLE, calendarEventTitle); |
| values.put(CalendarContract.Events.CALENDAR_ID, calendarId); |
| values.put(CalendarContract.Events.EVENT_TIMEZONE, |
| Config.CONFERENCE_TIMEZONE.getID()); |
| Uri eventUri = resolver.insert(CalendarContract.Events.CONTENT_URI, values); |
| String eventId = eventUri.getLastPathSegment(); |
| if (eventId == null) { |
| return batch; // Should be empty at this point |
| } |
| |
| newEventId = Long.valueOf(eventId); |
| // Since we're adding session reminder to system notification, we're not creating |
| // Calendar event reminders. If we were to create Calendar event reminders, this |
| // is how we would do it. |
| //values.put(CalendarContract.Reminders.EVENT_ID, Integer.valueOf(eventId)); |
| //values.put(CalendarContract.Reminders.MINUTES, 10); |
| //values.put(CalendarContract.Reminders.METHOD, |
| // CalendarContract.Reminders.METHOD_ALERT); // Or default? |
| //cr.insert(CalendarContract.Reminders.CONTENT_URI, values); |
| //values.clear(); |
| } |
| |
| // Update the session in our own provider with the newly created calendar event ID. |
| values.clear(); |
| values.put(ScheduleContract.Sessions.SESSION_CAL_EVENT_ID, newEventId); |
| resolver.update(sessionUri, values, null, null); |
| |
| } else { |
| // Remove Calendar event, if exists. |
| |
| // Get the event calendar id. |
| cursor = resolver.query(sessionUri, |
| new String[] {ScheduleContract.Sessions.SESSION_CAL_EVENT_ID}, |
| null, null, null); |
| long calendarEventId = -1; |
| if (cursor != null && cursor.moveToFirst()) { |
| calendarEventId = cursor.getLong(0); |
| cursor.close(); |
| } |
| |
| // Try to remove the Calendar Event based on key. If successful, move on; |
| // otherwise, remove the event based on Event title. |
| int affectedRows = 0; |
| if (calendarEventId != -1) { |
| affectedRows = resolver.delete( |
| CalendarContract.Events.CONTENT_URI, |
| CalendarContract.Events._ID + "=?", |
| new String[]{Long.valueOf(calendarEventId).toString()}); |
| } |
| |
| if (affectedRows == 0) { |
| resolver.delete(CalendarContract.Events.CONTENT_URI, |
| String.format("%s=? and %s=? and %s=? and %s=?", |
| CalendarContract.Events.CALENDAR_ID, |
| CalendarContract.Events.TITLE, |
| CalendarContract.Events.DTSTART, |
| CalendarContract.Events.DTEND), |
| new String[]{Long.valueOf(calendarId).toString(), |
| calendarEventTitle, |
| Long.valueOf(sessionBlockStart).toString(), |
| Long.valueOf(sessionBlockEnd).toString()}); |
| } |
| |
| // Remove the session and calendar event association. |
| values.clear(); |
| values.put(ScheduleContract.Sessions.SESSION_CAL_EVENT_ID, (Long) null); |
| resolver.update(sessionUri, values, null, null); |
| } |
| |
| return batch; |
| } |
| |
| /** |
| * Removes all calendar entries associated with Google I/O 2013. |
| */ |
| private ArrayList<ContentProviderOperation> processClearAllSessions( |
| ContentResolver resolver, long calendarId) { |
| |
| ArrayList<ContentProviderOperation> batch = new ArrayList<ContentProviderOperation>(); |
| |
| // Unable to find the Calendar associated with the user. Stop here. |
| if (calendarId == INVALID_CALENDAR_ID) { |
| Log.e(TAG, "Unable to find Calendar for user"); |
| return batch; |
| } |
| |
| // Delete all calendar entries matching the given title within the given time period |
| batch.add(ContentProviderOperation |
| .newDelete(CalendarContract.Events.CONTENT_URI) |
| .withSelection( |
| CalendarContract.Events.CALENDAR_ID + " = ? and " |
| + CalendarContract.Events.TITLE + " LIKE ? and " |
| + CalendarContract.Events.DTSTART + ">= ? and " |
| + CalendarContract.Events.DTEND + "<= ?", |
| new String[]{ |
| Long.toString(calendarId), |
| CALENDAR_CLEAR_SEARCH_LIKE_EXPRESSION, |
| Long.toString(Config.CONFERENCE_START_MILLIS), |
| Long.toString(Config.CONFERENCE_END_MILLIS) |
| } |
| ) |
| .build()); |
| |
| return batch; |
| } |
| |
| private interface SessionsQuery { |
| String[] PROJECTION = { |
| ScheduleContract.Sessions._ID, |
| ScheduleContract.Sessions.SESSION_START, |
| ScheduleContract.Sessions.SESSION_END, |
| ScheduleContract.Sessions.SESSION_TITLE, |
| ScheduleContract.Sessions.ROOM_NAME, |
| ScheduleContract.Sessions.SESSION_IN_MY_SCHEDULE, |
| }; |
| |
| int _ID = 0; |
| int SESSION_START = 1; |
| int SESSION_END = 2; |
| int SESSION_TITLE = 3; |
| int ROOM_NAME = 4; |
| int SESSION_IN_MY_SCHEDULE = 5; |
| } |
| } |