blob: f523cf26a60c77c3ce11a761a67d173c7a7a23ba [file] [log] [blame]
/*
* 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;
}
}