Java tutorial
/** * Copyright (c) 2012-2013, Gerald Garcia, David Wiesner, Timo Berger * * This file is part of Andoid Caldav Sync Adapter Free. * * Andoid Caldav Sync Adapter Free 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. * * Andoid Caldav Sync Adapter Free 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 Andoid Caldav Sync Adapter Free. * If not, see <http://www.gnu.org/licenses/>. * */ package org.gege.caldavsyncadapter.syncadapter; import java.io.IOException; import java.net.URI; //import java.net.MalformedURLException; import java.net.URISyntaxException; import java.util.ArrayList; //import java.security.GeneralSecurityException; import javax.xml.parsers.ParserConfigurationException; import net.fortuna.ical4j.data.ParserException; import org.apache.http.ParseException; import org.apache.http.client.ClientProtocolException; import org.gege.caldavsyncadapter.Event; import org.gege.caldavsyncadapter.android.entities.AndroidEvent; import org.gege.caldavsyncadapter.authenticator.AuthenticatorActivity; import org.gege.caldavsyncadapter.caldav.CaldavFacade; import org.gege.caldavsyncadapter.caldav.CaldavProtocolException; import org.gege.caldavsyncadapter.caldav.entities.DavCalendar; import org.gege.caldavsyncadapter.caldav.entities.CalendarEvent; import org.gege.caldavsyncadapter.caldav.entities.CalendarList; import org.gege.caldavsyncadapter.caldav.entities.DavCalendar.CalendarSource; import org.gege.caldavsyncadapter.syncadapter.notifications.NotificationsHelper; import org.xml.sax.SAXException; import android.accounts.Account; import android.accounts.AccountManager; import android.content.AbstractThreadedSyncAdapter; import android.content.ContentProviderClient; import android.content.ContentUris; import android.content.ContentValues; import android.content.Context; import android.content.SyncResult; import android.content.SyncStats; import android.content.pm.PackageManager.NameNotFoundException; import android.database.Cursor; import android.net.Uri; import android.os.Bundle; import android.os.RemoteException; import android.provider.CalendarContract.Attendees; import android.provider.CalendarContract.Calendars; import android.provider.CalendarContract.Events; import android.provider.CalendarContract.Reminders; import android.util.Log; public class SyncAdapter extends AbstractThreadedSyncAdapter { private static final String TAG = "SyncAdapter"; private AccountManager mAccountManager; private String mVersion = ""; private int mCountPerformSync = 0; private int mCountSyncCanceled = 0; private int mCountProviderFailed = 0; private int mCountProviderFailedMax = 3; // private Context mContext; /* private static final String[] CALENDAR_PROJECTION = new String[] { Calendars._ID, // 0 Calendars.ACCOUNT_NAME, // 1 Calendars.CALENDAR_DISPLAY_NAME, // 2 Calendars.OWNER_ACCOUNT, // 3 Calendar.CTAG // 4 };*/ /* // The indices for the projection array above. private static final int PROJECTION_ID_INDEX = 0; private static final int PROJECTION_ACCOUNT_NAME_INDEX = 1; private static final int PROJECTION_DISPLAY_NAME_INDEX = 2; private static final int PROJECTION_OWNER_ACCOUNT_INDEX = 3; */ /* private static final String[] EVENT_PROJECTION = new String[] { Events._ID, Events._SYNC_ID, Events.SYNC_DATA1, Events.CALENDAR_ID }; */ // ignore same CTag //private static final boolean FORCE_SYNCHRONIZE = false; // drop all calendar before synchro //private static final boolean DROP_CALENDAR_EVENTS = false; public SyncAdapter(Context context, boolean autoInitialize) { super(context, autoInitialize); //android.os.Debug.waitForDebugger(); mAccountManager = AccountManager.get(context); try { mVersion = context.getPackageManager().getPackageInfo(context.getPackageName(), 0).versionName; } catch (NameNotFoundException e) { e.printStackTrace(); } // mContext = context; } @Override public void onPerformSync(Account account, Bundle extras, String authority, ContentProviderClient provider, SyncResult syncResult) { boolean bolError = false; String url = mAccountManager.getUserData(account, AuthenticatorActivity.USER_DATA_URL_KEY); this.mCountPerformSync += 1; Log.v(TAG, "onPerformSync() count:" + String.valueOf(this.mCountPerformSync) + " on " + account.name + " with URL " + url); CalendarList serverCalList; CalendarList androidCalList = new CalendarList(account, provider, CalendarSource.Android, url); androidCalList.readCalendarFromClient(); ArrayList<Uri> notifyList = new ArrayList<Uri>(); try { String Username = ""; String UserDataVersion = mAccountManager.getUserData(account, AuthenticatorActivity.USER_DATA_VERSION); if (UserDataVersion == null) { Username = account.name; } else { Username = mAccountManager.getUserData(account, AuthenticatorActivity.USER_DATA_USERNAME); } CaldavFacade facade = new CaldavFacade(Username, mAccountManager.getPassword(account), url); facade.setAccount(account); facade.setProvider(provider); facade.setVersion(mVersion); serverCalList = facade.getCalendarList(this.getContext()); //String davProperties = facade.getLastDav(); Log.i(TAG, String.valueOf(androidCalList.getCalendarList().size()) + " calendars found at android"); for (DavCalendar serverCalendar : serverCalList.getCalendarList()) { Log.i(TAG, "Detected calendar name=" + serverCalendar.getCalendarDisplayName() + " URI=" + serverCalendar.getURI()); Uri androidCalendarUri = serverCalendar.checkAndroidCalendarList(androidCalList, this.getContext()); // check if the adapter was able to get an existing calendar or create a new one if (androidCalendarUri != null) { // the provider seems to work correct, reset the counter mCountProviderFailed = 0; DavCalendar androidCalendar = androidCalList.getCalendarByAndroidUri(androidCalendarUri); //if ((FORCE_SYNCHRONIZE) || (androidCalendar.getcTag() == null) || (!androidCalendar.getcTag().equals(serverCalendar.getcTag()))) { if ((androidCalendar.getcTag() == null) || (!androidCalendar.getcTag().equals(serverCalendar.getcTag()))) { Log.d(TAG, "CTag has changed, something to synchronise"); if (serverCalendar.readCalendarEvents(facade)) { this.synchroniseEvents(androidCalendar, serverCalendar, syncResult.stats, notifyList); Log.d(TAG, "Updating stored CTag"); //serverCalendar.updateAndroidCalendar(androidCalendarUri, Calendar.CTAG, serverCalendar.getcTag()); androidCalendar.setCTag(serverCalendar.getcTag(), true); } else { Log.d(TAG, "unable to read events from server calendar"); } } else { Log.d(TAG, "CTag has not changed, nothing to do"); /* this is unnecessary. "SkippedEntries" are: * Counter for tracking how many entries, either from the server or the local store, * were ignored during the sync operation. This could happen if the SyncAdapter detected * some unparsable data but decided to skip it and move on rather than failing immediately. */ /*long CalendarID = ContentUris.parseId(androidCalendarUri); String selection = "(" + Events.CALENDAR_ID + " = ?)"; String[] selectionArgs = new String[] {String.valueOf(CalendarID)}; Cursor countCursor = provider.query(Events.CONTENT_URI, new String[] {"count(*) AS count"}, selection, selectionArgs, null); countCursor.moveToFirst(); int count = countCursor.getInt(0); syncResult.stats.numSkippedEntries += count; countCursor.close();*/ } this.checkDirtyAndroidEvents(provider, account, androidCalendarUri, facade, serverCalendar.getURI(), syncResult.stats, notifyList); } else { // this happens if the data provider failes to get an existing or create a new calendar mCountProviderFailed += 1; Log.e(TAG, "failed to get an existing or create a new calendar"); syncResult.stats.numIoExceptions += 1; if (mCountProviderFailed >= mCountProviderFailedMax) { // see issue #96 NotificationsHelper.signalSyncErrors(this.getContext(), "Caldav sync error (provider failed)", "are you using CyanogenMod in Incognito Mode?"); } else { NotificationsHelper.signalSyncErrors(this.getContext(), "Caldav sync error (provider failed)", "the provider failed to get an existing or create a new calendar"); } bolError = true; } } if (!bolError) { // check whether a calendar is not synced -> delete it at android androidCalList.deleteCalendarOnClientSideOnly(this.getContext()); } // notify the ContentResolver for (Uri uri : androidCalList.getNotifyList()) { this.getContext().getContentResolver().notifyChange(uri, null); } for (Uri uri : serverCalList.getNotifyList()) { this.getContext().getContentResolver().notifyChange(uri, null); } for (Uri uri : notifyList) { this.getContext().getContentResolver().notifyChange(uri, null); } //Log.i(TAG,"Statistiks for Calendar: " + serverCalendar.getURI().toString()); //Log.i(TAG,"Statistiks for AndroidCalendar: " + androidCalendar.getAndroidCalendarUri().toString()); Log.i(TAG, "Entries: " + String.valueOf(syncResult.stats.numEntries)); Log.i(TAG, "Rows inserted: " + String.valueOf(syncResult.stats.numInserts)); Log.i(TAG, "Rows updated: " + String.valueOf(syncResult.stats.numUpdates)); Log.i(TAG, "Rows deleted: " + String.valueOf(syncResult.stats.numDeletes)); Log.i(TAG, "Rows skipped: " + String.valueOf(syncResult.stats.numSkippedEntries)); Log.i(TAG, "Io Exceptions: " + String.valueOf(syncResult.stats.numIoExceptions)); Log.i(TAG, "Parse Exceptions: " + String.valueOf(syncResult.stats.numParseExceptions)); Log.i(TAG, "Auth Exceptions: " + String.valueOf(syncResult.stats.numAuthExceptions)); Log.i(TAG, "Conflict Detected Exceptions: " + String.valueOf(syncResult.stats.numConflictDetectedExceptions)); /*} catch (final AuthenticatorException e) { syncResult.stats.numParseExceptions++; Log.e(TAG, "AuthenticatorException", e);*/ /*} catch (final OperationCanceledException e) { Log.e(TAG, "OperationCanceledExcetpion", e);*/ } catch (final IOException e) { Log.e(TAG, "IOException", e); syncResult.stats.numIoExceptions++; NotificationsHelper.signalSyncErrors(this.getContext(), "Caldav sync error (IO)", e.getMessage()); //NotificationsHelper.getCurrentSyncLog().addException(e); /*} catch (final AuthenticationException e) { //mAccountManager.invalidateAuthToken(Constants.ACCOUNT_TYPE, authtoken); syncResult.stats.numAuthExceptions++; Log.e(TAG, "AuthenticationException", e);*/ } catch (final ParseException e) { syncResult.stats.numParseExceptions++; Log.e(TAG, "ParseException", e); NotificationsHelper.signalSyncErrors(this.getContext(), "Caldav sync error (parsing)", e.getMessage()); //NotificationsHelper.getCurrentSyncLog().addException(e); /*} catch (final JSONException e) { syncResult.stats.numParseExceptions++; Log.e(TAG, "JSONException", e);*/ } catch (Exception e) { Log.e(TAG, "Updating calendar exception " + e.getClass().getName(), e); syncResult.stats.numParseExceptions++; NotificationsHelper.signalSyncErrors(this.getContext(), "Caldav sync error (general)", e.getMessage()); //NotificationsHelper.getCurrentSyncLog().addException(e); //throw new RuntimeException(e); } } public void onSyncCanceled() { //TODO: implement SyncCanceled this.mCountSyncCanceled += 1; Log.v(TAG, "onSyncCanceled() count:" + String.valueOf(this.mCountSyncCanceled)); } /** * both calender event and android event have been found. * server wins always at the moment. * @param androidCalendar * @param serverCalendar * @param stats * @param notifyList * @throws ClientProtocolException * @throws URISyntaxException * @throws IOException * @throws ParserConfigurationException * @throws SAXException * @throws RemoteException * @throws CaldavProtocolException * @throws ParserException * @see SyncAdapter#updateAndroidEvent(ContentProviderClient, Account, AndroidEvent, CalendarEvent) * @see SyncAdapter#tagAndroidEvent(ContentProviderClient, Account, AndroidEvent) * @see SyncAdapter#untagAndroidEvents(ContentProviderClient, Account, Uri) * @see SyncAdapter#deleteUntaggedEvents(ContentProviderClient, Account, Uri) */ private void synchroniseEvents(DavCalendar androidCalendar, DavCalendar serverCalendar, SyncStats stats, ArrayList<Uri> notifyList) throws ClientProtocolException, URISyntaxException, IOException, ParserConfigurationException, SAXException, RemoteException, CaldavProtocolException, ParserException { /*if (DROP_CALENDAR_EVENTS) { dropAllEvents(account, provider, androidCalendar.getAndroidCalendarUri()); }*/ int rowInsert = 0; int rowUpdate = 0; int rowTag = 0; int rowDelete = 0; int rowUntag = 0; int rowSkip = 0; for (CalendarEvent calendarEvent : serverCalendar.getCalendarEvents()) { try { AndroidEvent androidEvent = calendarEvent.getAndroidEvent(androidCalendar); Log.i(TAG, "Event " + calendarEvent.getUri().toString() + " androidUri=" + androidEvent); if (androidEvent == null) { /* new android event */ if (calendarEvent.createAndroidEvent(androidCalendar)) { rowInsert += 1; androidEvent = calendarEvent.getAndroidEvent(androidCalendar); notifyList.add(androidEvent.getUri()); } else { rowSkip += 1; } } else { /* the android exists */ String androidETag = androidEvent.getETag(); if (androidETag == null) androidETag = ""; Log.d(TAG, "Event compare: " + androidETag + " <> " + calendarEvent.getETag().toString()); if ((androidEvent.getETag() == null) || (!androidETag.equals(calendarEvent.getETag()))) { /* the android event is getting updated */ if (calendarEvent.updateAndroidEvent(androidEvent)) { rowUpdate += 1; notifyList.add(androidEvent.getUri()); } else { rowSkip += 1; } } } if (androidEvent != null) if (androidEvent.tagAndroidEvent()) rowTag += 1; } catch (ParserException ex) { Log.e(TAG, "Parser exception", ex); stats.numParseExceptions++; NotificationsHelper.signalSyncErrors(getContext(), "Caldav sync error (parsing)", ex.getMessage()); //NotificationsHelper.getCurrentSyncLog().addException(ex); } catch (CaldavProtocolException ex) { Log.e(TAG, "Caldav exception", ex); stats.numParseExceptions++; NotificationsHelper.signalSyncErrors(getContext(), "Caldav sync error (caldav)", ex.getMessage()); //NotificationsHelper.getCurrentSyncLog().addException(ex); } } rowDelete = androidCalendar.deleteUntaggedEvents(); rowUntag = androidCalendar.untagAndroidEvents(); /*Log.i(TAG,"Statistiks for Calendar: " + serverCalendar.getURI().toString()); Log.i(TAG,"Statistiks for AndroidCalendar: " + androidCalendar.getAndroidCalendarUri().toString()); Log.i(TAG,"Rows inserted: " + String.valueOf(rowInsert)); Log.i(TAG,"Rows updated: " + String.valueOf(rowUpdate)); Log.i(TAG,"Rows deleted: " + String.valueOf(rowDelete)); Log.i(TAG,"Rows skipped: " + String.valueOf(rowSkip));*/ Log.i(TAG, "Rows tagged: " + String.valueOf(rowTag)); Log.i(TAG, "Rows untagged: " + String.valueOf(rowUntag)); stats.numInserts += rowInsert; stats.numUpdates += rowUpdate; stats.numDeletes += rowDelete; stats.numSkippedEntries += rowSkip; stats.numEntries += rowInsert + rowUpdate + rowDelete; } /** * checks the android events for the dirty flag. * the flag is set by android when the event has been changed. * the dirty flag is removed when an android event has been updated from calendar event * @param provider * @param account * @param calendarUri * @param facade * @param caldavCalendarUri * @param stats * @param notifyList * @return count of dirty events */ private int checkDirtyAndroidEvents(ContentProviderClient provider, Account account, Uri calendarUri, CaldavFacade facade, URI caldavCalendarUri, SyncStats stats, ArrayList<Uri> notifyList) { Cursor curEvent = null; Cursor curAttendee = null; Cursor curReminder = null; Long EventID; Long CalendarID; AndroidEvent androidEvent = null; int rowDirty = 0; int rowInsert = 0; int rowUpdate = 0; int rowDelete = 0; try { CalendarID = ContentUris.parseId(calendarUri); String selection = "(" + Events.DIRTY + " = ?) AND (" + Events.CALENDAR_ID + " = ?)"; String[] selectionArgs = new String[] { "1", CalendarID.toString() }; curEvent = provider.query(Events.CONTENT_URI, null, selection, selectionArgs, null); while (curEvent.moveToNext()) { EventID = curEvent.getLong(curEvent.getColumnIndex(Events._ID)); Uri returnedUri = ContentUris.withAppendedId(Events.CONTENT_URI, EventID); androidEvent = new AndroidEvent(account, provider, returnedUri, calendarUri); androidEvent.readContentValues(curEvent); selection = "(" + Attendees.EVENT_ID + " = ?)"; selectionArgs = new String[] { String.valueOf(EventID) }; curAttendee = provider.query(Attendees.CONTENT_URI, null, selection, selectionArgs, null); selection = "(" + Reminders.EVENT_ID + " = ?)"; selectionArgs = new String[] { String.valueOf(EventID) }; curReminder = provider.query(Reminders.CONTENT_URI, null, selection, selectionArgs, null); androidEvent.readAttendees(curAttendee); androidEvent.readReminder(curReminder); curAttendee.close(); curReminder.close(); String SyncID = androidEvent.ContentValues.getAsString(Events._SYNC_ID); boolean Deleted = false; int intDeleted = 0; intDeleted = curEvent.getInt(curEvent.getColumnIndex(Events.DELETED)); Deleted = (intDeleted == 1); if (SyncID == null) { // new Android event String newGUID = java.util.UUID.randomUUID().toString() + "-caldavsyncadapter"; String calendarPath = caldavCalendarUri.getPath(); if (!calendarPath.endsWith("/")) calendarPath += "/"; SyncID = calendarPath + newGUID + ".ics"; androidEvent.createIcs(newGUID); if (facade.createEvent(URI.create(SyncID), androidEvent.getIcsEvent().toString())) { //HINT: bugfix for google calendar if (SyncID.contains("@")) SyncID = SyncID.replace("@", "%40"); ContentValues values = new ContentValues(); values.put(Events._SYNC_ID, SyncID); //google doesn't send the etag after creation //HINT: my SabreDAV send always the same etag after putting a new event //String LastETag = facade.getLastETag(); //if (!LastETag.equals("")) { // values.put(Event.ETAG, LastETag); //} else { //so get the etag with a new REPORT CalendarEvent calendarEvent = new CalendarEvent(account, provider); calendarEvent.calendarURL = caldavCalendarUri.toURL(); URI SyncURI = new URI(SyncID); calendarEvent.setUri(SyncURI); CaldavFacade.getEvent(calendarEvent); values.put(Event.ETAG, calendarEvent.getETag()); //} values.put(Event.UID, newGUID); values.put(Events.DIRTY, 0); values.put(Event.RAWDATA, androidEvent.getIcsEvent().toString()); int rowCount = provider.update( asSyncAdapter(androidEvent.getUri(), account.name, account.type), values, null, null); if (rowCount == 1) { rowInsert += 1; notifyList.add(androidEvent.getUri()); } } } else if (Deleted) { // deleted Android event if (facade.deleteEvent(URI.create(SyncID), androidEvent.getETag())) { String mSelectionClause = "(" + Events._ID + "= ?)"; String[] mSelectionArgs = { String.valueOf(EventID) }; int countDeleted = provider.delete( asSyncAdapter(Events.CONTENT_URI, account.name, account.type), mSelectionClause, mSelectionArgs); if (countDeleted == 1) { rowDelete += 1; notifyList.add(androidEvent.getUri()); } } } else { //update the android event to the server String uid = androidEvent.getUID(); if ((uid == null) || (uid.equals(""))) { //COMPAT: this is needed because in the past, the UID was not stored in the android event CalendarEvent calendarEvent = new CalendarEvent(account, provider); URI syncURI = new URI(SyncID); calendarEvent.setUri(syncURI); calendarEvent.calendarURL = caldavCalendarUri.toURL(); if (calendarEvent.fetchBody()) { calendarEvent.readContentValues(); uid = calendarEvent.getUID(); } } if (uid != null) { androidEvent.createIcs(uid); if (facade.updateEvent(URI.create(SyncID), androidEvent.getIcsEvent().toString(), androidEvent.getETag())) { selection = "(" + Events._ID + "= ?)"; selectionArgs = new String[] { EventID.toString() }; androidEvent.ContentValues.put(Events.DIRTY, 0); //google doesn't send the etag after update String LastETag = facade.getLastETag(); if (!LastETag.equals("")) { androidEvent.ContentValues.put(Event.ETAG, LastETag); } else { //so get the etag with a new REPORT CalendarEvent calendarEvent = new CalendarEvent(account, provider); calendarEvent.calendarURL = caldavCalendarUri.toURL(); URI SyncURI = new URI(SyncID); calendarEvent.setUri(SyncURI); CaldavFacade.getEvent(calendarEvent); androidEvent.ContentValues.put(Event.ETAG, calendarEvent.getETag()); } androidEvent.ContentValues.put(Event.RAWDATA, androidEvent.getIcsEvent().toString()); int RowCount = provider.update( asSyncAdapter(androidEvent.getUri(), account.name, account.type), androidEvent.ContentValues, null, null); if (RowCount == 1) { rowUpdate += 1; notifyList.add(androidEvent.getUri()); } } else { rowDirty += 1; } } else { rowDirty += 1; } } } curEvent.close(); /*if ((rowInsert > 0) || (rowUpdate > 0) || (rowDelete > 0) || (rowDirty > 0)) { Log.i(TAG,"Android Rows inserted: " + String.valueOf(rowInsert)); Log.i(TAG,"Android Rows updated: " + String.valueOf(rowUpdate)); Log.i(TAG,"Android Rows deleted: " + String.valueOf(rowDelete)); Log.i(TAG,"Android Rows dirty: " + String.valueOf(rowDirty)); }*/ stats.numInserts += rowInsert; stats.numUpdates += rowUpdate; stats.numDeletes += rowDelete; stats.numSkippedEntries += rowDirty; stats.numEntries += rowInsert + rowUpdate + rowDelete; } catch (RemoteException e) { e.printStackTrace(); } catch (URISyntaxException e) { // TODO Automatisch generierter Erfassungsblock e.printStackTrace(); } catch (ClientProtocolException e) { // TODO Automatisch generierter Erfassungsblock e.printStackTrace(); } catch (IOException e) { // TODO Automatisch generierter Erfassungsblock e.printStackTrace(); } catch (CaldavProtocolException e) { // TODO Automatisch generierter Erfassungsblock e.printStackTrace(); } catch (ParserException e) { // TODO Automatisch generierter Erfassungsblock e.printStackTrace(); } return rowDirty; } /* private Account UpgradeAccount(Account OldAccount) { String Username = OldAccount.name; String Type = OldAccount.type; String Password = this.mAccountManager.getPassword(OldAccount); String Url = this.mAccountManager.getUserData(OldAccount, AuthenticatorActivity.USER_DATA_URL_KEY); Account NewAccount = new Account(Username + AuthenticatorActivity.ACCOUNT_NAME_SPLITTER + Url, Type); if (this.mAccountManager.addAccountExplicitly(NewAccount, Password, null)) { this.mAccountManager.setUserData(NewAccount, AuthenticatorActivity.USER_DATA_URL_KEY, Url); this.mAccountManager.setUserData(NewAccount, AuthenticatorActivity.USER_DATA_USERNAME, Username); } this.mAccountManager.removeAccount(OldAccount, null, null); return NewAccount; }*/ /* private void dropAllEvents(Account account, ContentProviderClient provider, Uri calendarUri) throws RemoteException { Log.i(TAG, "Deleting all calendar events for "+calendarUri); String selection = "(" + Events.CALENDAR_ID + " = ?)"; String[] selectionArgs = new String[] {Long.toString(ContentUris.parseId(calendarUri))}; provider.delete(asSyncAdapter(Events.CONTENT_URI, account.name, account.type), selection, selectionArgs); }*/ private static Uri asSyncAdapter(Uri uri, String account, String accountType) { return uri.buildUpon().appendQueryParameter(android.provider.CalendarContract.CALLER_IS_SYNCADAPTER, "true") .appendQueryParameter(Calendars.ACCOUNT_NAME, account) .appendQueryParameter(Calendars.ACCOUNT_TYPE, accountType).build(); } }