/*
* Copyright (C) 2013 Simon Vig Therkildsen
*
* 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 net.simonvt.cathode.service;
import android.accounts.Account;
import android.content.AbstractThreadedSyncAdapter;
import android.content.ContentProviderClient;
import android.content.ContentProviderOperation;
import android.content.ContentValues;
import android.content.Context;
import android.content.OperationApplicationException;
import android.content.SharedPreferences;
import android.content.SyncResult;
import android.database.Cursor;
import android.net.Uri;
import android.os.Bundle;
import android.os.RemoteException;
import android.preference.PreferenceManager;
import android.provider.BaseColumns;
import android.provider.CalendarContract;
import android.provider.ContactsContract;
import android.text.format.Time;
import android.util.LongSparseArray;
import java.util.ArrayList;
import java.util.List;
import net.simonvt.cathode.R;
import net.simonvt.cathode.provider.DatabaseContract.EpisodeColumns;
import net.simonvt.cathode.provider.DatabaseContract.ShowColumns;
import net.simonvt.cathode.provider.ProviderSchematic.Episodes;
import net.simonvt.cathode.provider.ProviderSchematic.Shows;
import net.simonvt.cathode.settings.Permissions;
import net.simonvt.cathode.settings.Settings;
import net.simonvt.cathode.util.DataHelper;
import net.simonvt.cathode.util.DateUtils;
import net.simonvt.schematic.Cursors;
import timber.log.Timber;
public class CalendarSyncAdapter extends AbstractThreadedSyncAdapter {
private static class Event {
long id;
long start;
long end;
long episodeId;
public Event(Cursor c) {
id = Cursors.getLong(c, CalendarContract.Events._ID);
start = Cursors.getLong(c, CalendarContract.Events.DTSTART);
end = Cursors.getLong(c, CalendarContract.Events.DTEND);
episodeId = Cursors.getLong(c, CalendarContract.Events.SYNC_DATA1);
}
}
private static final long INVALID_ID = -1L;
private Context context;
public CalendarSyncAdapter(Context context) {
super(context, true);
this.context = context;
}
@Override public void onPerformSync(Account account, Bundle extras, String authority,
ContentProviderClient provider, SyncResult syncResult) {
Timber.d("onPerformSync");
if (!Permissions.hasCalendarPermission(context)) {
Timber.d("Calendar permission not granted");
return;
}
final long calendarId = getCalendar(account);
if (calendarId == INVALID_ID) {
return;
}
SharedPreferences settings = PreferenceManager.getDefaultSharedPreferences(context);
final boolean syncCalendar = settings.getBoolean(Settings.CALENDAR_SYNC, false);
if (!syncCalendar) {
deleteCalendar(account, calendarId);
return;
}
final boolean updateCalendarColor =
settings.getBoolean(Settings.CALENDAR_COLOR_NEEDS_UPDATE, false);
if (updateCalendarColor) {
updateCalendarColor(calendarId);
}
ArrayList<ContentProviderOperation> ops = new ArrayList<>();
//noinspection MissingPermission
Cursor c =
context.getContentResolver().query(CalendarContract.Events.CONTENT_URI, new String[] {
CalendarContract.Events._ID, CalendarContract.Events.DTSTART,
CalendarContract.Events.DTEND, CalendarContract.Events.SYNC_DATA1,
}, CalendarContract.Events.CALENDAR_ID + "=?", new String[] {
String.valueOf(calendarId),
}, null);
if (c == null) {
return;
}
LongSparseArray<Event> events = new LongSparseArray<>();
while (c.moveToNext()) {
final long id = Cursors.getLong(c, CalendarContract.Events._ID);
final Long episodeId = Cursors.getLongOrNull(c, CalendarContract.Events.SYNC_DATA1);
if (episodeId != null) {
Event event = events.get(episodeId);
Event newEvent = new Event(c);
if (event == null) {
events.put(episodeId, newEvent);
} else {
ContentProviderOperation op =
ContentProviderOperation.newDelete(CalendarContract.Events.CONTENT_URI)
.withSelection(CalendarContract.Events._ID + "=?", new String[] {
String.valueOf(id),
})
.build();
ops.add(op);
}
} else {
ContentProviderOperation op =
ContentProviderOperation.newDelete(CalendarContract.Events.CONTENT_URI)
.withSelection(CalendarContract.Events._ID + "=?", new String[] {
String.valueOf(id),
})
.build();
ops.add(op);
}
}
c.close();
final long time = System.currentTimeMillis();
Cursor shows = context.getContentResolver().query(Shows.SHOWS, new String[] {
ShowColumns.ID, ShowColumns.TITLE, ShowColumns.RUNTIME,
}, "("
+ ShowColumns.WATCHED_COUNT
+ ">0 OR "
+ ShowColumns.IN_WATCHLIST_COUNT
+ ">0 OR "
+ ShowColumns.IN_COLLECTION_COUNT
+ ">0"
+ ") AND "
+ ShowColumns.HIDDEN_CALENDAR
+ "=0"
+ " AND "
+ ShowColumns.NEEDS_SYNC
+ "=0", null, null);
while (shows.moveToNext()) {
final long showId = Cursors.getLong(shows, ShowColumns.ID);
final String showTitle = Cursors.getString(shows, ShowColumns.TITLE);
final long runtime = Cursors.getLong(shows, ShowColumns.RUNTIME);
Cursor episodes = context.getContentResolver().query(Episodes.fromShow(showId), new String[] {
EpisodeColumns.ID, EpisodeColumns.TITLE, EpisodeColumns.SEASON, EpisodeColumns.EPISODE,
EpisodeColumns.FIRST_AIRED,
}, EpisodeColumns.FIRST_AIRED + ">? AND " + EpisodeColumns.NEEDS_SYNC + "=0", new String[] {
String.valueOf(time - 30 * DateUtils.DAY_IN_MILLIS),
}, null);
while (episodes.moveToNext()) {
final long episodeId = Cursors.getLong(episodes, EpisodeColumns.ID);
final int season = Cursors.getInt(episodes, EpisodeColumns.SEASON);
final int episode = Cursors.getInt(episodes, EpisodeColumns.EPISODE);
final String title = DataHelper.getEpisodeTitle(context, episodes, season, episode);
final long firstAired = DataHelper.getFirstAired(episodes);
String eventTitle = showTitle + " - " + season + "x" + episode + " " + title;
addEvent(ops, events, calendarId, episodeId, eventTitle, firstAired, runtime);
}
episodes.close();
}
shows.close();
Cursor watchlistShows = context.getContentResolver().query(Shows.SHOWS_WATCHLIST, new String[] {
ShowColumns.ID, ShowColumns.TITLE, ShowColumns.RUNTIME,
}, null, null, null);
while (watchlistShows.moveToNext()) {
final long showId = Cursors.getLong(watchlistShows, ShowColumns.ID);
final String showTitle = Cursors.getString(watchlistShows, ShowColumns.TITLE);
final long runtime = Cursors.getLong(watchlistShows, ShowColumns.RUNTIME);
Cursor episodes = context.getContentResolver().query(Episodes.fromShow(showId), new String[] {
EpisodeColumns.ID, EpisodeColumns.TITLE, EpisodeColumns.SEASON, EpisodeColumns.EPISODE,
EpisodeColumns.FIRST_AIRED,
}, EpisodeColumns.FIRST_AIRED
+ ">? AND "
+ EpisodeColumns.EPISODE
+ "=1 AND "
+ EpisodeColumns.SEASON
+ ">0", new String[] {
String.valueOf(time - 30 * DateUtils.DAY_IN_MILLIS),
}, EpisodeColumns.SEASON + " ASC LIMIT 1");
if (episodes.moveToFirst()) {
final long episodeId = Cursors.getLong(episodes, EpisodeColumns.ID);
final int season = Cursors.getInt(episodes, EpisodeColumns.SEASON);
final int episode = Cursors.getInt(episodes, EpisodeColumns.EPISODE);
final String title = DataHelper.getEpisodeTitle(context, episodes, season, episode);
final long firstAired = DataHelper.getFirstAired(episodes);
String eventTitle = showTitle + " - " + season + "x" + episode + " " + title;
addEvent(ops, events, calendarId, episodeId, eventTitle, firstAired, runtime);
}
episodes.close();
}
watchlistShows.close();
for (int i = 0, size = events.size(); i < size; i++) {
Event event = events.valueAt(i);
ContentProviderOperation op =
ContentProviderOperation.newDelete(CalendarContract.Events.CONTENT_URI)
.withSelection(CalendarContract.Events._ID + "=?", new String[] {
String.valueOf(event.id),
})
.build();
ops.add(op);
}
try {
context.getContentResolver().applyBatch(CalendarContract.AUTHORITY, ops);
} catch (RemoteException e) {
e.printStackTrace();
} catch (OperationApplicationException e) {
e.printStackTrace();
}
}
private void addEvent(List<ContentProviderOperation> ops, LongSparseArray<Event> events,
long calendarId, long episodeId, String eventTitle, long firstAired, long runtime) {
Event event = events.get(episodeId);
if (event != null) {
ContentProviderOperation op =
ContentProviderOperation.newUpdate(CalendarContract.Events.CONTENT_URI)
.withValue(CalendarContract.Events.DTSTART, firstAired)
.withValue(CalendarContract.Events.DTEND,
firstAired + runtime * DateUtils.MINUTE_IN_MILLIS)
.withValue(CalendarContract.Events.EVENT_TIMEZONE, Time.TIMEZONE_UTC)
.withValue(CalendarContract.Events.TITLE, eventTitle)
.withSelection(CalendarContract.Events._ID + "=?", new String[] {
String.valueOf(event.id),
})
.build();
ops.add(op);
events.remove(episodeId);
} else {
ContentProviderOperation op = ContentProviderOperation.newInsert(
CalendarContract.Events.CONTENT_URI.buildUpon()
.appendQueryParameter(ContactsContract.CALLER_IS_SYNCADAPTER, "true")
.appendQueryParameter(CalendarContract.Calendars.ACCOUNT_NAME,
context.getString(R.string.accountName))
.appendQueryParameter(CalendarContract.Calendars.ACCOUNT_TYPE,
context.getString(R.string.accountType))
.build())
.withValue(CalendarContract.Events.DTSTART, firstAired)
.withValue(CalendarContract.Events.DTEND,
firstAired + runtime * DateUtils.MINUTE_IN_MILLIS)
.withValue(CalendarContract.Events.EVENT_TIMEZONE, Time.TIMEZONE_UTC)
.withValue(CalendarContract.Events.TITLE, eventTitle)
.withValue(CalendarContract.Events.SYNC_DATA1, episodeId)
.withValue(CalendarContract.Events.CALENDAR_ID, calendarId)
.build();
ops.add(op);
}
}
private int getCalendarColor() {
SharedPreferences settings = PreferenceManager.getDefaultSharedPreferences(context);
return settings.getInt(Settings.CALENDAR_COLOR, Settings.CALENDAR_COLOR_DEFAULT);
}
private void updateCalendarColor(long calendarId) {
final int calendarColor = getCalendarColor();
ContentValues values = new ContentValues();
values.put(CalendarContract.Calendars.CALENDAR_COLOR, calendarColor);
//noinspection MissingPermission
context.getContentResolver()
.update(CalendarContract.Calendars.CONTENT_URI, values, BaseColumns._ID + "=?",
new String[] {
String.valueOf(calendarId),
});
PreferenceManager.getDefaultSharedPreferences(context)
.edit()
.putBoolean(Settings.CALENDAR_COLOR_NEEDS_UPDATE, false)
.apply();
}
private long getCalendar(Account account) {
return getCalendar(account, true);
}
private long getCalendar(Account account, boolean recall) {
Cursor c = null;
try {
Uri calenderUri = CalendarContract.Calendars.CONTENT_URI.buildUpon()
.appendQueryParameter(CalendarContract.Calendars.ACCOUNT_NAME, account.name)
.appendQueryParameter(CalendarContract.Calendars.ACCOUNT_TYPE, account.type)
.build();
c = context.getContentResolver().query(calenderUri, new String[] {
BaseColumns._ID,
}, null, null, null);
if (c == null) {
return INVALID_ID;
}
if (c.moveToNext()) {
return c.getLong(0);
} else {
if (!recall) {
return INVALID_ID;
}
ArrayList<ContentProviderOperation> ops = new ArrayList<>();
ContentProviderOperation.Builder builder = ContentProviderOperation.newInsert(
CalendarContract.Calendars.CONTENT_URI.buildUpon()
.appendQueryParameter(CalendarContract.CALLER_IS_SYNCADAPTER, "true")
.appendQueryParameter(CalendarContract.Calendars.ACCOUNT_NAME, account.name)
.appendQueryParameter(CalendarContract.Calendars.ACCOUNT_TYPE, account.type)
.build());
builder.withValue(CalendarContract.Calendars.ACCOUNT_NAME, account.name);
builder.withValue(CalendarContract.Calendars.ACCOUNT_TYPE, account.type);
builder.withValue(CalendarContract.Calendars.NAME, context.getString(R.string.app_name));
builder.withValue(CalendarContract.Calendars.CALENDAR_DISPLAY_NAME,
context.getString(R.string.calendarName));
builder.withValue(CalendarContract.Calendars.CALENDAR_COLOR, getCalendarColor());
builder.withValue(CalendarContract.Calendars.CALENDAR_ACCESS_LEVEL,
CalendarContract.Calendars.CAL_ACCESS_READ);
builder.withValue(CalendarContract.Calendars.OWNER_ACCOUNT, account.name);
builder.withValue(CalendarContract.Calendars.SYNC_EVENTS, 1);
ops.add(builder.build());
try {
context.getContentResolver().applyBatch(CalendarContract.AUTHORITY, ops);
} catch (Exception e) {
Timber.e(e, "Unable to create calendar");
return INVALID_ID;
}
return getCalendar(account, false);
}
} finally {
if (c != null) c.close();
}
}
private void deleteCalendar(Account account, long calendarId) {
Timber.d("Deleting calendar");
//noinspection MissingPermission
context.getContentResolver()
.delete(CalendarContract.Events.CONTENT_URI, CalendarContract.Events.CALENDAR_ID + "=?",
new String[] {
String.valueOf(calendarId),
});
Uri calenderUri = CalendarContract.Calendars.CONTENT_URI.buildUpon()
.appendQueryParameter(CalendarContract.Calendars.ACCOUNT_NAME, account.name)
.appendQueryParameter(CalendarContract.Calendars.ACCOUNT_TYPE, account.type)
.build();
context.getContentResolver().delete(calenderUri, null, null);
}
}