/*
* ____.____ __.____ ___ _____
* | | |/ _| | \ / _ \ ______ ______
* | | < | | / / /_\ \\____ \\____ \
* /\__| | | \| | / / | \ |_> > |_> >
* \________|____|__ \______/ \____|__ / __/| __/
* \/ \/|__| |__|
*
* Copyright (c) 2014-2015 Paul "Marunjar" Pretsch
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
*/
package org.voidsink.anewjkuapp.calendar;
import android.Manifest;
import android.accounts.Account;
import android.content.ContentProviderClient;
import android.content.ContentProviderOperation;
import android.content.ContentResolver;
import android.content.ContentValues;
import android.content.Context;
import android.content.OperationApplicationException;
import android.content.pm.PackageManager;
import android.database.Cursor;
import android.net.Uri;
import android.os.Build;
import android.os.RemoteException;
import android.provider.CalendarContract;
import android.support.v4.content.ContextCompat;
import android.text.TextUtils;
import android.text.format.DateUtils;
import android.util.Log;
import net.fortuna.ical4j.data.CalendarBuilder;
import net.fortuna.ical4j.data.CalendarParserFactory;
import net.fortuna.ical4j.extensions.groupwise.ShowAs;
import net.fortuna.ical4j.model.ParameterFactoryRegistry;
import net.fortuna.ical4j.model.PropertyFactoryRegistry;
import net.fortuna.ical4j.model.TimeZoneRegistryFactory;
import org.voidsink.anewjkuapp.KusssAuthenticator;
import org.voidsink.anewjkuapp.PreferenceWrapper;
import org.voidsink.anewjkuapp.R;
import org.voidsink.anewjkuapp.analytics.Analytics;
import org.voidsink.anewjkuapp.utils.AppUtils;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public final class CalendarUtils {
public static final String ARG_CALENDAR_EXAM = "ARG_EXAM_CALENDAR";
public static final String ARG_CALENDAR_COURSE = "ARG_LVA_CALENDAR";
// Constants representing column positions from PROJECTION.
public static final String[] CALENDAR_PROJECTION = new String[]{
CalendarContractWrapper.Calendars._ID(),
CalendarContractWrapper.Calendars.NAME(),
CalendarContractWrapper.Calendars.CALENDAR_DISPLAY_NAME(),
CalendarContractWrapper.Calendars.ACCOUNT_NAME(),
CalendarContractWrapper.Calendars.ACCOUNT_TYPE(),
CalendarContractWrapper.Calendars.CALENDAR_ACCESS_LEVEL()};
public static final int COLUMN_CAL_ID = 0;
public static final int COLUMN_CAL_NAME = 1;
public static final int COLUMN_CAL_DISPLAY_NAME = 2;
public static final int COLUMN_CAL_ACCOUNT_NAME = 3;
public static final int COLUMN_CAL_ACCOUNT_TYPE = 4;
public static final int COLUMN_CAL_ACCESS_LEVEL = 5;
public static final String[] EVENT_PROJECTION = new String[]{
CalendarContractWrapper.Events._ID(), //
CalendarContractWrapper.Events.EVENT_LOCATION(), // VEvent.getLocation()
CalendarContractWrapper.Events.TITLE(), // VEvent.getSummary()
CalendarContractWrapper.Events.DESCRIPTION(), // VEvent.getDescription()
CalendarContractWrapper.Events.DTSTART(), // VEvent.getStartDate()
CalendarContractWrapper.Events.DTEND(), // VEvent.getEndDate()
CalendarContractWrapper.Events.SYNC_ID_CUSTOM(), // VEvent.getUID()
CalendarContractWrapper.Events.DIRTY(),
CalendarContractWrapper.Events.DELETED(),
CalendarContractWrapper.Events.CALENDAR_ID(),
CalendarContractWrapper.Events._SYNC_ID(),
CalendarContractWrapper.Events.ALL_DAY()};
public static final int COLUMN_EVENT_ID = 0;
public static final int COLUMN_EVENT_LOCATION = 1;
public static final int COLUMN_EVENT_TITLE = 2;
public static final int COLUMN_EVENT_DESCRIPTION = 3;
public static final int COLUMN_EVENT_DTSTART = 4;
public static final int COLUMN_EVENT_DTEND = 5;
public static final int COLUMN_EVENT_KUSSS_ID = 6;
public static final int COLUMN_EVENT_DIRTY = 7;
public static final int COLUMN_EVENT_DELETED = 8;
public static final int COLUMN_EVENT_CAL_ID = 9;
public static final int COLUMN_EVENT_KUSSS_ID_LEGACY = 10;
public static final int COLUMN_EVENT_ALL_DAY = 11;
public static final String[] EXTENDED_PROPERTIES_PROJECTION = new String[]{
CalendarContract.ExtendedProperties.EVENT_ID,
CalendarContract.ExtendedProperties.NAME,
CalendarContract.ExtendedProperties.VALUE
};
public static final String EXTENDED_PROPERTY_NAME_KUSSS_ID = "kusssId";
public static final String EXTENDED_PROPERTY_LOCATION_EXTRA = "locationExtra";
private static final String TAG = CalendarUtils.class.getSimpleName();
private CalendarUtils() {
}
private static Uri createCalendar(Context context, Account account,
String name, int color) {
if (context == null || account == null) {
return null;
}
try {
String accountName = account.name;
String accountType = account.type;
String displayName = getCalendarName(context, name);
Uri target = KusssAuthenticator.asCalendarSyncAdapter(
CalendarContractWrapper.Calendars.CONTENT_URI(), accountName,
accountType);
ContentValues values = new ContentValues();
values.put(CalendarContractWrapper.Calendars.OWNER_ACCOUNT(),
accountName);
values.put(CalendarContractWrapper.Calendars.ACCOUNT_NAME(),
accountName);
values.put(CalendarContractWrapper.Calendars.ACCOUNT_TYPE(),
accountType);
values.put(CalendarContractWrapper.Calendars.NAME(), name);
values.put(CalendarContractWrapper.Calendars.CALENDAR_DISPLAY_NAME(),
displayName);
values.put(CalendarContractWrapper.Calendars.CALENDAR_COLOR(), color);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
values.put(CalendarContractWrapper.Calendars.CALENDAR_ACCESS_LEVEL(),
CalendarContractWrapper.Calendars.CAL_ACCESS_OWNER());
} else {
values.put(CalendarContractWrapper.Calendars.CALENDAR_ACCESS_LEVEL(),
CalendarContractWrapper.Calendars.CAL_ACCESS_READ());
}
values.put(CalendarContractWrapper.Calendars.SYNC_EVENTS(), 1);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
values.put(CalendarContractWrapper.Calendars.VISIBLE(), 1);
values.put(
CalendarContractWrapper.Calendars.CAN_PARTIALLY_UPDATE(), 0);
values.put(
CalendarContractWrapper.Calendars.ALLOWED_ATTENDEE_TYPES(),
CalendarContractWrapper.Attendees.TYPE_NONE());
}
return context.getContentResolver().insert(target, values);
} catch (Exception e) {
Analytics.sendException(context, e, true, name);
return null;
}
}
public static boolean removeCalendar(Context context, String name) {
Account account = AppUtils.getAccount(context);
if (account == null) {
return true;
}
String id = getCalIDByName(context, account, name, false);
if (id == null) {
return true;
}
final ContentResolver resolver = context.getContentResolver();
resolver.delete(
KusssAuthenticator.asCalendarSyncAdapter(CalendarContractWrapper.Calendars.CONTENT_URI(),
account.name,
account.type),
CalendarContractWrapper.Calendars._ID() + "=?", new String[]{id});
Log.i(TAG, String.format("calendar %s (id=%s) removed", name, id));
return true;
}
private static boolean createCalendarIfNecessary(Context context,
Account account, String name, int color) {
String calId = getCalIDByName(context, account, name, false);
if (calId == null) {
createCalendar(context, account, name, color);
if (getCalIDByName(context, account, name, false) != null) {
Log.d(TAG, String.format("calendar '%s' created", name));
} else {
Log.d(TAG, String.format("can't create calendar '%s'", name));
return false;
}
}
return true;
}
public static CalendarBuilder newCalendarBuilder() {
PropertyFactoryRegistry propertyFactoryRegistry = new PropertyFactoryRegistry();
propertyFactoryRegistry.register(ShowAs.PROPERTY_NAME, ShowAs.FACTORY);
return new CalendarBuilder(
CalendarParserFactory.getInstance().createParser(),
propertyFactoryRegistry,
new ParameterFactoryRegistry(),
TimeZoneRegistryFactory.getInstance().createRegistry());
}
public static boolean createCalendarsIfNecessary(Context context,
Account account) {
boolean calendarCreated = true;
if (!createCalendarIfNecessary(context, account, ARG_CALENDAR_EXAM,
AppUtils.getRandomColor())) {
calendarCreated = false;
}
if (!createCalendarIfNecessary(context, account, ARG_CALENDAR_COURSE,
AppUtils.getRandomColor())) {
calendarCreated = false;
}
return calendarCreated;
}
private static Map<String, String> getCalIDs(Context context,
Account account) {
// get map with calendar ids and names for specific account
HashMap<String, String> ids = new HashMap<>();
// nothing to do if there's no account
if (context == null || account == null) {
return ids;
}
// nothing to do if there's no permission
if (ContextCompat.checkSelfPermission(context, Manifest.permission.READ_CALENDAR) != PackageManager.PERMISSION_GRANTED) {
return ids;
}
ContentResolver cr = context.getContentResolver();
// todo: add selection
Cursor c = cr.query(CalendarContractWrapper.Calendars.CONTENT_URI(),
CALENDAR_PROJECTION, null, null, null);
if (c != null) {
while (c.moveToNext()) {
if (account.name.equals(c.getString(COLUMN_CAL_ACCOUNT_NAME))
&& account.type.equals(c.getString(COLUMN_CAL_ACCOUNT_TYPE))) {
ids.put(c.getString(COLUMN_CAL_NAME),
c.getString(COLUMN_CAL_ID));
}
}
c.close();
}
return ids;
}
public static String getCalIDByName(Context context, Account account,
String name, boolean usePreferences) {
if (ContextCompat.checkSelfPermission(context, Manifest.permission.READ_CALENDAR) != PackageManager.PERMISSION_GRANTED) {
Log.w(TAG, String.format("no id for '%s' found, no permission", name));
return null;
}
String id = null;
// get id from preferences
if (usePreferences) {
switch (name) {
case ARG_CALENDAR_EXAM: {
id = PreferenceWrapper.getExamCalendarId(context);
break;
}
case ARG_CALENDAR_COURSE: {
id = PreferenceWrapper.getLvaCalendarId(context);
break;
}
}
// check id from preferences
if (id != null) {
CalendarList calendars = getCalendars(context, false);
if (!calendars.getIds().contains(Integer.parseInt(id))) {
id = null;
}
}
}
// get default calendar ids
if (id == null) {
id = getCalIDs(context, account).get(name);
}
if (id == null) {
Log.w(TAG, String.format("no id for '%s' found", name));
} else {
Log.d(TAG, String.format("id for '%s' found: %s", name, id));
}
return id;
}
public static String getCalendarName(Context context, String name) {
switch (name) {
case CalendarUtils.ARG_CALENDAR_EXAM:
return context.getString(R.string.calendar_title_exam);
case CalendarUtils.ARG_CALENDAR_COURSE:
return context.getString(R.string.calendar_title_lva);
default: {
CalendarList calendars = getCalendars(context, false);
String displayName = calendars.getDisplayName(name);
if (TextUtils.isEmpty(displayName)) {
displayName = context.getString(R.string.calendar_title_unknown);
}
return displayName;
}
}
}
public static CalendarList getCalendars(Context context, boolean onlyWritable) {
List<Integer> ids = new ArrayList<>();
List<String> names = new ArrayList<>();
List<String> displayNames = new ArrayList<>();
List<String> accountNames = new ArrayList<>();
ContentResolver cr = context.getContentResolver();
Cursor c = null;
try {
c = cr.query(CalendarContractWrapper.Calendars.CONTENT_URI(),
CALENDAR_PROJECTION, null, null, null);
if (c != null) {
while (c.moveToNext()) {
if (!onlyWritable || CalendarUtils.isWriteable(c.getInt(COLUMN_CAL_ACCESS_LEVEL))) {
int id = c.getInt(COLUMN_CAL_ID);
String name = c.getString(COLUMN_CAL_NAME);
String displayName = c.getString(COLUMN_CAL_DISPLAY_NAME);
String accountName = c.getString(COLUMN_CAL_ACCOUNT_NAME);
ids.add(id);
names.add(name);
displayNames.add(displayName);
accountNames.add(accountName);
}
}
}
} catch (Exception e) {
Analytics.sendException(context, e, false);
} finally {
if (c != null) c.close();
}
return new CalendarList(ids, names, displayNames, accountNames);
}
private static boolean isReadable(int accessLevel) {
return accessLevel == CalendarContractWrapper.Calendars.CAL_ACCESS_CONTRIBUTOR() ||
accessLevel == CalendarContractWrapper.Calendars.CAL_ACCESS_EDITOR() ||
accessLevel == CalendarContractWrapper.Calendars.CAL_ACCESS_OWNER() ||
accessLevel == CalendarContractWrapper.Calendars.CAL_ACCESS_READ() ||
accessLevel == CalendarContractWrapper.Calendars.CAL_ACCESS_ROOT();
}
private static boolean isWriteable(int accessLevel) {
return accessLevel == CalendarContractWrapper.Calendars.CAL_ACCESS_CONTRIBUTOR() ||
accessLevel == CalendarContractWrapper.Calendars.CAL_ACCESS_EDITOR() ||
accessLevel == CalendarContractWrapper.Calendars.CAL_ACCESS_OWNER() ||
accessLevel == CalendarContractWrapper.Calendars.CAL_ACCESS_ROOT();
}
public static boolean getSyncCalendar(Context context, String name) {
if (context == null) return false;
switch (name) {
case ARG_CALENDAR_EXAM:
return PreferenceWrapper.getSyncCalendarExam(context);
case ARG_CALENDAR_COURSE:
return PreferenceWrapper.getSyncCalendarLva(context);
default:
return true;
}
}
public static boolean deleteKusssEvents(Context context, Account account) {
boolean done = true;
if (!deleteKusssEvents(context, getCalIDByName(context, account, ARG_CALENDAR_COURSE, false))) {
done = false;
}
if (!deleteKusssEvents(context, PreferenceWrapper.getLvaCalendarId(context))) {
done = false;
}
if (!deleteKusssEvents(context, getCalIDByName(context, account, ARG_CALENDAR_EXAM, false))) {
done = false;
}
if (!deleteKusssEvents(context, PreferenceWrapper.getExamCalendarId(context))) {
done = false;
}
return done;
}
private static boolean deleteKusssEvents(Context context, String calId) {
if (calId != null) {
ContentProviderClient provider = context.getContentResolver()
.acquireContentProviderClient(
CalendarContractWrapper.Events.CONTENT_URI());
if (provider == null) {
return false;
}
try {
Uri calUri = CalendarContractWrapper.Events
.CONTENT_URI();
Cursor c = loadEvent(provider, calUri, calId);
if (c != null) {
try {
ArrayList<ContentProviderOperation> batch = new ArrayList<>();
long deleteFrom = new Date().getTime() - DateUtils.DAY_IN_MILLIS;
while (c.moveToNext()) {
long eventDTStart = c.getLong(CalendarUtils.COLUMN_EVENT_DTSTART);
if (eventDTStart > deleteFrom) {
String eventId = c.getString(COLUMN_EVENT_ID);
// Log.d(TAG, "---------");
String eventKusssId = null;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
// get kusssId from extended properties
Cursor c2 = provider.query(CalendarContract.ExtendedProperties.CONTENT_URI, CalendarUtils.EXTENDED_PROPERTIES_PROJECTION,
CalendarContract.ExtendedProperties.EVENT_ID + " = ?",
new String[]{eventId},
null);
if (c2 != null) {
while (c2.moveToNext()) {
if (c2.getString(1).contains(EXTENDED_PROPERTY_NAME_KUSSS_ID)) {
eventKusssId = c2.getString(2);
}
}
c2.close();
}
} else {
eventKusssId = c.getString(COLUMN_EVENT_KUSSS_ID);
}
if (TextUtils.isEmpty(eventKusssId)) {
eventKusssId = c.getString(COLUMN_EVENT_KUSSS_ID_LEGACY);
}
if (!TextUtils.isEmpty(eventKusssId)) {
Uri deleteUri = calUri.buildUpon()
.appendPath(eventId)
.build();
Log.d(TAG, "Scheduling delete: " + deleteUri);
batch.add(ContentProviderOperation
.newDelete(deleteUri)
.build());
}
}
}
if (batch.size() > 0) {
Log.d(TAG, "Applying batch update");
provider.applyBatch(batch);
Log.d(TAG, "Notify resolver");
} else {
Log.w(TAG,
"No batch operations found! Do nothing");
}
} catch (RemoteException | OperationApplicationException e) {
Analytics.sendException(context, e, true);
return false;
}
}
} finally {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
provider.close();
} else {
provider.release();
}
}
return false;
}
return true;
}
public static Cursor loadEvent(ContentProviderClient mProvider, Uri calUri, String calendarId) {
// The ID of the recurring event whose instances you are
// searching
// for in the Instances table
String selection = CalendarContractWrapper.Events
.CALENDAR_ID() + " = ?";
String[] selectionArgs = new String[]{calendarId};
try {
return mProvider.query(calUri, EVENT_PROJECTION,
selection, selectionArgs, null);
} catch (RemoteException e) {
return null;
}
}
public static class CalendarList {
private final List<Integer> mIds;
private final List<String> mNames;
private final List<String> mDisplayNames;
private final List<String> mAccountNames;
public CalendarList(List<Integer> ids, List<String> names, List<String> displayNames, List<String> accountNames) {
this.mIds = ids;
this.mNames = names;
this.mDisplayNames = displayNames;
this.mAccountNames = accountNames;
}
public List<String> getNames() {
return mNames;
}
public List<String> getDisplayNames() {
return mDisplayNames;
}
public List<String> getAccountNames() {
return mAccountNames;
}
public List<Integer> getIds() {
return mIds;
}
public String getDisplayName(String name) {
for (int i = 0; i < mNames.size(); i++) {
if (mNames.get(i).equals(name)) {
return mDisplayNames.get(i);
}
}
return null;
}
public String[] getIdsAsStrings() {
String[] ret = new String[mIds.size()];
for (int i = 0; i < mIds.size(); i++)
ret[i] = mIds.get(i).toString();
return ret;
}
}
public static boolean isSameDay(Date date1, Date date2) {
if (date1 == null || date2 == null) {
throw new IllegalArgumentException("The date must not be null");
}
Calendar cal1 = Calendar.getInstance();
cal1.setTime(date1);
Calendar cal2 = Calendar.getInstance();
cal2.setTime(date2);
return isSameDay(cal1, cal2);
}
public static boolean isSameDay(Calendar cal1, Calendar cal2) {
if (cal1 == null || cal2 == null) {
throw new IllegalArgumentException("The date must not be null");
}
return (cal1.get(Calendar.ERA) == cal2.get(Calendar.ERA) &&
cal1.get(Calendar.YEAR) == cal2.get(Calendar.YEAR) &&
cal1.get(Calendar.DAY_OF_YEAR) == cal2.get(Calendar.DAY_OF_YEAR));
}
}