am b55344dd: Added Calendar Sync tests support for sync suite
Merge commit 'b55344ddfebfccbdb07ed7a209e91819a5e9925d'
* commit 'b55344ddfebfccbdb07ed7a209e91819a5e9925d':
Added Calendar Sync tests support for sync suite
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index c23ac0e..e2aa1a5 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -41,6 +41,13 @@
android:syncable="true" android:multiprocess="false"
android:readPermission="android.permission.READ_CALENDAR"
android:writePermission="android.permission.WRITE_CALENDAR" />
+ <service android:name="CalendarSyncAdapterService" android:exported="true">
+ <intent-filter>
+ <action android:name="android.content.SyncAdapter" />
+ </intent-filter>
+ <meta-data android:name="android.content.SyncAdapter"
+ android:resource="@xml/syncadapter" />
+ </service>
<activity android:name="CalendarContentProviderTests" android:label="Calendar Content Provider">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
@@ -67,5 +74,19 @@
</intent-filter>
</receiver>
<service android:name=".CalendarAppWidgetService" />
+ <activity android:name="CalendarDebug" android:label="@string/calendar_info">
+ <intent-filter>
+ <action android:name="android.intent.action.MAIN" />
+ <category android:name="android.intent.category.DEFAULT" />
+ </intent-filter>
+ </activity>
+ <receiver android:name="CalendarDebugReceiver">
+ <intent-filter>
+ <action android:name="android.provider.Telephony.SECRET_CODE" />
+ <data android:scheme="android_secret_code" android:host="225" />
+ </intent-filter>
+ </receiver>
+
+
</application>
</manifest>
diff --git a/res/values/strings.xml b/res/values/strings.xml
index 58450cb..07a7c9f 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -36,4 +36,19 @@
<!-- Caption to show on gadget when there are no upcoming calendar events -->
<string name="gadget_no_events">No upcoming calendar events</string>
+ <!-- Caption on secret calendar info -->
+ <string name="calendar_info">Calendar info</string>
+
+ <!-- Error message for secret calendar info -->
+ <string name="calendar_info_error">Error</string>
+
+ <!-- No calendars message for secret calendar info -->
+ <string name="calendar_info_no_calendars">No calendars</string>
+
+ <!-- Event count message for secret calendar info -->
+ <string name="calendar_info_events">Events: <xliff:g id = "events">%1$d</xliff:g></string>
+
+ <!-- Event and dirty event count message for secret calendar info -->
+ <string name="calendar_info_events_dirty">Events: <xliff:g id = "events">%1$d</xliff:g>, Unsaved: <xliff:g id = "dirty events">%2$d</xliff:g></string>
+
</resources>
diff --git a/res/xml/syncadapter.xml b/res/xml/syncadapter.xml
new file mode 100644
index 0000000..b5f89cc
--- /dev/null
+++ b/res/xml/syncadapter.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/**
+ * Copyright (c) 2009, The Android Open Source Project
+ *
+ * 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.
+ */
+-->
+
+<!-- The attributes in this XML file provide configuration information -->
+<!-- for the SyncAdapter. -->
+
+<sync-adapter xmlns:android="http://schemas.android.com/apk/res/android"
+ android:contentAuthority="calendar"
+ android:accountType="com.google.GAIA"
+/>
diff --git a/src/com/android/providers/calendar/CalendarDebug.java b/src/com/android/providers/calendar/CalendarDebug.java
new file mode 100644
index 0000000..7ce687b
--- /dev/null
+++ b/src/com/android/providers/calendar/CalendarDebug.java
@@ -0,0 +1,188 @@
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * 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.android.providers.calendar;
+
+import android.app.ListActivity;
+import android.content.ContentResolver;
+import android.database.Cursor;
+import android.os.AsyncTask;
+import android.os.Bundle;
+import android.os.Handler;
+import android.provider.Calendar;
+import android.widget.ListAdapter;
+import android.widget.SimpleAdapter;
+import android.view.Window;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Displays info about all the user's calendars, for debugging.
+ *
+ * The info is displayed as a ListActivity, where each entry has the calendar name
+ * followed by information about the calendar.
+ */
+public class CalendarDebug extends ListActivity {
+ private static final String[] CALENDARS_PROJECTION = new String[]{
+ Calendar.Calendars._ID,
+ Calendar.Calendars.DISPLAY_NAME,
+ };
+ private static final int INDEX_ID = 0;
+ private static final int INDEX_DISPLAY_NAME = 1;
+
+ private static final String[] EVENTS_PROJECTION = new String[]{
+ Calendar.Events._ID,
+ };
+ private static final String KEY_TITLE = "title";
+ private static final String KEY_TEXT = "text";
+
+ private ContentResolver mContentResolver;
+ private ListActivity mActivity;
+
+ /**
+ * Task to fetch info from the database and display as a ListActivity.
+ */
+ private class FetchInfoTask extends AsyncTask<Void, Void, List<Map<String, String>>> {
+ /**
+ * Starts spinner while task is running.
+ *
+ * @see #onPostExecute
+ * @see #doInBackground
+ */
+ @Override
+ protected void onPreExecute() {
+ setProgressBarIndeterminateVisibility(true);
+ }
+
+ /**
+ * Fetches debugging info from the database
+ * @param params Void
+ * @return a Map for each calendar
+ */
+ protected List<Map<String, String>> doInBackground(Void... params) {
+ Cursor cursor = null;
+ // items is the list of items to display in the list.
+ List<Map<String, String>> items = new ArrayList<Map<String, String>>();
+ try {
+ cursor = mContentResolver.query(Calendar.Calendars.CONTENT_URI,
+ CALENDARS_PROJECTION,
+ null, null /* selectionArgs */,
+ Calendar.Calendars.DEFAULT_SORT_ORDER);
+ if (cursor == null) {
+ addItem(items, mActivity.getString(R.string.calendar_info_error), "");
+ } else {
+ while (cursor.moveToNext()) {
+ // Process each calendar
+ int id = cursor.getInt(INDEX_ID);
+ int eventCount = -1;
+ int dirtyCount = -1;
+ String displayName = cursor.getString(INDEX_DISPLAY_NAME);
+
+ // Compute number of events in the calendar
+ String where = Calendar.EventsColumns.CALENDAR_ID + "=" + id;
+ Cursor eventCursor = Calendar.Events.query(mContentResolver,
+ EVENTS_PROJECTION, where, null);
+ try {
+ eventCount = eventCursor.getCount();
+ } finally {
+ eventCursor.close();
+ }
+
+ // Compute number of dirty events in the calendar
+ String dirtyWhere = Calendar.EventsColumns.CALENDAR_ID + "=" + id
+ + " AND Events." + Calendar.Events._SYNC_DIRTY + "=1";
+ Cursor dirtyCursor = Calendar.Events.query(mContentResolver,
+ EVENTS_PROJECTION, dirtyWhere, null);
+ try {
+ dirtyCount = dirtyCursor.getCount();
+ } finally {
+ dirtyCursor.close();
+ }
+
+ // Format the output
+ String text;
+ if (dirtyCount == 0) {
+ text = mActivity.getString(R.string.calendar_info_events,
+ eventCount);
+ } else {
+ text = mActivity.getString(R.string.calendar_info_events_dirty,
+ eventCount, dirtyCount);
+ }
+
+ addItem(items, displayName, text);
+ }
+ }
+ } catch (Exception e) {
+ // Want to catch all exceptions. The point of this code is to debug
+ // when something bad is happening.
+ addItem(items, mActivity.getString(R.string.calendar_info_error), e.toString());
+ } finally {
+ if (cursor != null) {
+ cursor.close();
+ }
+ }
+
+ if (items.size() == 0) {
+ addItem(items, mActivity.getString(R.string.calendar_info_no_calendars), "");
+ }
+ return items;
+ }
+
+ /**
+ * Runs on the UI thread to display the debugging info.
+ *
+ * @param items The info items to display.
+ * @see #onPreExecute
+ * @see #doInBackground
+ */
+ @Override
+ protected void onPostExecute(List<Map<String, String>> items) {
+ setProgressBarIndeterminateVisibility(false);
+ ListAdapter adapter = new SimpleAdapter(mActivity, items,
+ android.R.layout.simple_list_item_2, new String[]{KEY_TITLE, KEY_TEXT},
+ new int[]{android.R.id.text1, android.R.id.text2});
+
+ // Bind to our new adapter.
+ setListAdapter(adapter);
+ }
+ }
+
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ requestWindowFeature(Window.FEATURE_INDETERMINATE_PROGRESS);
+ mActivity = this;
+ mContentResolver = getContentResolver();
+ getListView(); // Instantiate, for spinner
+ new FetchInfoTask().execute();
+
+ }
+
+ /**
+ * Adds an item to the item map
+ * @param items The item map to update
+ * @param title Title of the item
+ * @param text Text of the item
+ */
+ protected void addItem(List<Map<String, String>> items, String title, String text) {
+ Map<String, String> itemMap = new HashMap<String, String>();
+ itemMap.put(KEY_TITLE, title);
+ itemMap.put(KEY_TEXT, text);
+ items.add(itemMap);
+ }
+}
diff --git a/src/com/android/providers/calendar/CalendarDebugReceiver.java b/src/com/android/providers/calendar/CalendarDebugReceiver.java
new file mode 100644
index 0000000..2d468a9
--- /dev/null
+++ b/src/com/android/providers/calendar/CalendarDebugReceiver.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * 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.android.providers.calendar;
+
+import static android.provider.Telephony.Intents.SECRET_CODE_ACTION;
+
+import android.content.Context;
+import android.content.Intent;
+import android.content.BroadcastReceiver;
+
+
+public class CalendarDebugReceiver extends BroadcastReceiver {
+
+ public CalendarDebugReceiver() {
+ }
+
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ if (intent.getAction().equals(SECRET_CODE_ACTION)) {
+ Intent i = new Intent(Intent.ACTION_MAIN);
+ i.setClass(context, CalendarDebug.class);
+ i.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ context.startActivity(i);
+ }
+ }
+}
diff --git a/src/com/android/providers/calendar/CalendarProvider.java b/src/com/android/providers/calendar/CalendarProvider.java
index 59cc896..34f3e4b 100644
--- a/src/com/android/providers/calendar/CalendarProvider.java
+++ b/src/com/android/providers/calendar/CalendarProvider.java
@@ -17,22 +17,6 @@
package com.android.providers.calendar;
-import com.google.android.collect.Sets;
-import com.google.android.gdata.client.AndroidGDataClient;
-import com.google.android.gdata.client.AndroidXmlParserFactory;
-import com.google.android.googlelogin.GoogleLoginServiceBlockingHelper;
-import com.google.android.googlelogin.GoogleLoginServiceNotFoundException;
-import com.google.android.providers.AbstractGDataSyncAdapter;
-import com.google.android.providers.AbstractGDataSyncAdapter.GDataSyncData;
-import com.google.wireless.gdata.calendar.client.CalendarClient;
-import com.google.wireless.gdata.calendar.data.CalendarEntry;
-import com.google.wireless.gdata.calendar.data.CalendarsFeed;
-import com.google.wireless.gdata.calendar.parser.xml.XmlCalendarGDataParserFactory;
-import com.google.wireless.gdata.client.HttpException;
-import com.google.wireless.gdata.data.Entry;
-import com.google.wireless.gdata.parser.GDataParser;
-import com.google.wireless.gdata.parser.ParseException;
-
import android.app.AlarmManager;
import android.app.PendingIntent;
import android.content.AbstractSyncableContentProvider;
@@ -45,7 +29,6 @@
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
-import android.content.SyncAdapter;
import android.content.SyncContext;
import android.content.UriMatcher;
import android.database.Cursor;
@@ -53,7 +36,6 @@
import android.database.SQLException;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteQueryBuilder;
-import android.graphics.Color;
import android.net.Uri;
import android.os.Bundle;
import android.os.Debug;
@@ -69,34 +51,41 @@
import android.provider.Calendar.ExtendedProperties;
import android.provider.Calendar.Instances;
import android.provider.Calendar.Reminders;
-import android.provider.SyncConstValue;
import android.text.TextUtils;
import android.text.format.Time;
import android.util.Config;
import android.util.Log;
import android.util.TimeFormatException;
+import android.accounts.Account;
+import com.google.android.collect.Sets;
+import com.google.android.collect.Maps;
+import com.google.android.gdata.client.AndroidGDataClient;
+import com.google.android.gdata.client.AndroidXmlParserFactory;
+import com.google.android.providers.AbstractGDataSyncAdapter;
+import com.google.android.providers.AbstractGDataSyncAdapter.GDataSyncData;
+import com.google.wireless.gdata.calendar.client.CalendarClient;
+import com.google.wireless.gdata.calendar.parser.xml.XmlCalendarGDataParserFactory;
-import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
-import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.TimeZone;
public class CalendarProvider extends AbstractSyncableContentProvider {
-
private static final boolean PROFILE = false;
private static final boolean MULTIPLE_ATTENDEES_PER_EVENT = false;
- private static final String[] ACCOUNTS_PROJECTION = new String[] { Calendars._SYNC_ACCOUNT};
+ private static final String[] ACCOUNTS_PROJECTION =
+ new String[] {Calendars._SYNC_ACCOUNT, Calendars._SYNC_ACCOUNT_TYPE};
private static final String[] EVENTS_PROJECTION = new String[] {
Events._SYNC_ID,
Events._SYNC_VERSION,
Events._SYNC_ACCOUNT,
+ Events._SYNC_ACCOUNT_TYPE,
Events.CALENDAR_ID,
Events.RRULE,
Events.RDATE,
@@ -104,11 +93,12 @@
};
private static final int EVENTS_SYNC_ID_INDEX = 0;
private static final int EVENTS_SYNC_VERSION_INDEX = 1;
- private static final int EVENTS_SYNC_ACCOUNT_INDEX = 2;
- private static final int EVENTS_CALENDAR_ID_INDEX = 3;
- private static final int EVENTS_RRULE_INDEX = 4;
- private static final int EVENTS_RDATE_INDEX = 5;
- private static final int EVENTS_ORIGINAL_EVENT_INDEX = 6;
+ private static final int EVENTS_SYNC_ACCOUNT_NAME_INDEX = 2;
+ private static final int EVENTS_SYNC_ACCOUNT_TYPE_INDEX = 3;
+ private static final int EVENTS_CALENDAR_ID_INDEX = 4;
+ private static final int EVENTS_RRULE_INDEX = 5;
+ private static final int EVENTS_RDATE_INDEX = 6;
+ private static final int EVENTS_ORIGINAL_EVENT_INDEX = 7;
private DatabaseUtils.InsertHelper mCalendarsInserter;
private DatabaseUtils.InsertHelper mEventsInserter;
@@ -235,13 +225,7 @@
// Note: if you update the version number, you must also update the code
// in upgradeDatabase() to modify the database (gracefully, if possible).
- private static final int DATABASE_VERSION = 54;
-
- private static final String EXPECTED_PROJECTION = "/full";
-
- private static final String DESIRED_PROJECTION = "/full-selfattendance";
-
- private static final String FEEDS_SUBSTRING = "/feeds/";
+ private static final int DATABASE_VERSION = 55;
// Make sure we load at least two months worth of data.
// Client apps can load more data in a background thread.
@@ -275,7 +259,6 @@
private AlarmManager mAlarmManager;
- private CalendarSyncAdapter mSyncAdapter;
private CalendarAppWidgetProvider mAppWidgetProvider = CalendarAppWidgetProvider.getInstance();
/**
@@ -309,6 +292,8 @@
public boolean onCreate() {
super.onCreate();
+ setTempProviderSyncAdapter(new CalendarSyncAdapter(getContext(), this));
+
// Register for Intent broadcasts
IntentFilter filter = new IntentFilter();
@@ -566,6 +551,27 @@
oldVersion += 1;
}
+ if (oldVersion == 54) {
+ db.execSQL("ALTER TABLE Calendars ADD COLUMN _sync_account_type TEXT;");
+ db.execSQL("ALTER TABLE Events ADD COLUMN _sync_account_type TEXT;");
+ db.execSQL("ALTER TABLE DeletedEvents ADD COLUMN _sync_account_type TEXT;");
+ db.execSQL("UPDATE Calendars"
+ + " SET _sync_account_type='com.google.GAIA'"
+ + " WHERE _sync_account IS NOT NULL");
+ db.execSQL("UPDATE Events"
+ + " SET _sync_account_type='com.google.GAIA'"
+ + " WHERE _sync_account IS NOT NULL");
+ db.execSQL("UPDATE DeletedEvents"
+ + " SET _sync_account_type='com.google.GAIA'"
+ + " WHERE _sync_account IS NOT NULL");
+ Log.w(TAG, "re-creating eventSyncAccountAndIdIndex");
+ db.execSQL("DROP INDEX eventSyncAccountAndIdIndex");
+ db.execSQL("CREATE INDEX eventSyncAccountAndIdIndex ON Events ("
+ + Events._SYNC_ACCOUNT_TYPE + ", " + Events._SYNC_ACCOUNT + ", "
+ + Events._SYNC_ID + ");");
+ oldVersion += 1;
+ }
+
return true; // this was lossless
}
@@ -589,6 +595,7 @@
db.execSQL("CREATE TABLE Calendars (" +
"_id INTEGER PRIMARY KEY," +
"_sync_account TEXT," +
+ "_sync_account_type TEXT," +
"_sync_id TEXT," +
"_sync_version TEXT," +
"_sync_time TEXT," + // UTC
@@ -618,6 +625,7 @@
db.execSQL("CREATE TABLE Events (" +
"_id INTEGER PRIMARY KEY," +
"_sync_account TEXT," +
+ "_sync_account_type TEXT," +
"_sync_id TEXT," +
"_sync_version TEXT," +
"_sync_time TEXT," + // UTC
@@ -652,7 +660,8 @@
");");
db.execSQL("CREATE INDEX eventSyncAccountAndIdIndex ON Events ("
- + Events._SYNC_ACCOUNT + ", " + Events._SYNC_ID + ");");
+ + Events._SYNC_ACCOUNT_TYPE + ", " + Events._SYNC_ACCOUNT + ", "
+ + Events._SYNC_ID + ");");
db.execSQL("CREATE INDEX eventsCalendarIdIndex ON Events (" +
Events.CALENDAR_ID +
@@ -675,6 +684,7 @@
"_sync_id TEXT," +
"_sync_version TEXT," +
"_sync_account TEXT," +
+ "_sync_account_type TEXT," +
(isTemporary() ? "_sync_local_id INTEGER," : "") + // Used while syncing,
"_sync_mark INTEGER," + // To filter out new rows
"calendar_id INTEGER" +
@@ -831,18 +841,17 @@
* syncable.
*/
@Override
- protected void onAccountsChanged(String[] accountsArray) {
+ protected void onAccountsChanged(Account[] accountsArray) {
super.onAccountsChanged(accountsArray);
- Map<String, Boolean> accounts = new HashMap<String, Boolean>();
- for (String account : accountsArray) {
+ Map<Account, Boolean> accounts = Maps.newHashMap();
+ for (Account account : accountsArray) {
accounts.put(account, false);
}
mDb.beginTransaction();
try {
- deleteRowsForRemovedAccounts(accounts, "Calendars",
- SyncConstValue._SYNC_ACCOUNT);
+ deleteRowsForRemovedAccounts(accounts, "Calendars");
mDb.setTransactionSuccessful();
} finally {
mDb.endTransaction();
@@ -856,28 +865,28 @@
// If there are no calendars at all for a given account, add the
// default calendar.
- for (Map.Entry<String, Boolean> entry : accounts.entrySet()) {
+ for (Map.Entry<Account, Boolean> entry : accounts.entrySet()) {
entry.setValue(false);
// TODO: remove this break when Calendar supports multiple accounts. Until then
// pretend that only the first account exists.
break;
}
- Set<String> handledAccounts = Sets.newHashSet();
- ContentResolver cr = getContext().getContentResolver();
+ Set<Account> handledAccounts = Sets.newHashSet();
if (Config.LOGV) Log.v(TAG, "querying calendars");
- Cursor c = cr.query(Calendars.CONTENT_URI, ACCOUNTS_PROJECTION, null, null, null);
+ Cursor c = queryInternal(Calendars.CONTENT_URI, ACCOUNTS_PROJECTION, null, null, null);
try {
while (c.moveToNext()) {
- String account = c.getString(0);
+ final String accountName = c.getString(0);
+ final String accountType = c.getString(1);
+ final Account account = new Account(accountName, accountType);
if (handledAccounts.contains(account)) {
continue;
}
handledAccounts.add(account);
if (accounts.containsKey(account)) {
if (Config.LOGV) {
- Log.v(TAG, "calendars for account " + account
- + " exist");
+ Log.v(TAG, "calendars for account " + account + " exist");
}
accounts.put(account, true /* hasCalendar */);
}
@@ -890,8 +899,8 @@
if (Config.LOGV) {
Log.v(TAG, "scanning over " + accounts.size() + " account(s)");
}
- for (Map.Entry<String, Boolean> entry : accounts.entrySet()) {
- String account = entry.getKey();
+ for (Map.Entry<Account, Boolean> entry : accounts.entrySet()) {
+ final Account account = entry.getKey();
boolean hasCalendar = entry.getValue();
if (hasCalendar) {
if (Config.LOGV) {
@@ -900,14 +909,15 @@
}
continue;
}
- String feedUrl = mCalendarClient.getDefaultCalendarUrl(account,
+ String feedUrl = mCalendarClient.getDefaultCalendarUrl(account.mName,
CalendarClient.PROJECTION_PRIVATE_SELF_ATTENDANCE, null/* query params */);
feedUrl = CalendarSyncAdapter.rewriteUrlforAccount(account, feedUrl);
if (Config.LOGV) {
Log.v(TAG, "adding default calendar for account " + account);
}
ContentValues values = new ContentValues();
- values.put(Calendars._SYNC_ACCOUNT, account);
+ values.put(Calendars._SYNC_ACCOUNT, account.mName);
+ values.put(Calendars._SYNC_ACCOUNT_TYPE, account.mType);
values.put(Calendars.URL, feedUrl);
values.put(Calendars.DISPLAY_NAME, "Default");
values.put(Calendars.SYNC_EVENTS, 1);
@@ -918,7 +928,7 @@
// when the user does a sync.
values.put(Calendars.TIMEZONE, Time.getCurrentTimezone());
values.put(Calendars.ACCESS_LEVEL, Calendars.OWNER_ACCESS);
- cr.insert(Calendars.CONTENT_URI, values);
+ insertInternal(Calendars.CONTENT_URI, values);
scheduleSync(account, false /* do a full sync */, null /* no url */);
}
@@ -957,14 +967,6 @@
}
case CALENDARS:
qb.setTables("Calendars");
- // see if we want to update the list of calendars from the
- // server.
- String update = null;
- update = url.getQueryParameter("update");
- if ("1".equals(update)) {
- fetchCalendarsFromServer();
- }
-
break;
case CALENDARS_ID:
qb.setTables("Calendars");
@@ -1075,279 +1077,6 @@
return ret;
}
- private void fetchCalendarsFromServer() {
- if (mCalendarClient == null) {
- Log.w(TAG, "Cannot fetch calendars -- calendar url defined.");
- return;
- }
-
- GoogleLoginServiceBlockingHelper loginHelper = null;
- String username = null;
- String authToken = null;
-
- try {
- loginHelper = new GoogleLoginServiceBlockingHelper(getContext());
-
- // TODO: allow caller to specify which account's feeds should be updated
- username = loginHelper.getAccount(false);
- if (TextUtils.isEmpty(username)) {
- Log.w(TAG, "Unable to update calendars from server -- "
- + "no users configured.");
- return;
- }
-
- try {
- authToken = loginHelper.getAuthToken(username,
- mCalendarClient.getServiceName());
- } catch (GoogleLoginServiceBlockingHelper.AuthenticationException e) {
- Log.w(TAG, "Unable to update calendars from server -- could not "
- + "authenticate user " + username, e);
- return;
- }
- } catch (GoogleLoginServiceNotFoundException e) {
- Log.e(TAG, "Could not find Google login service", e);
- return;
- } finally {
- if (loginHelper != null) {
- loginHelper.close();
- }
- }
-
- // get the current set of calendars. we'll need to pay attention to
- // which calendars we get back from the server, so we can delete
- // calendars that have been deleted from the server.
- Set<Long> existingCalendarIds = new HashSet<Long>();
-
- final SQLiteDatabase db = getDatabase();
- db.beginTransaction();
- try {
- getCurrentCalendars(existingCalendarIds);
-
- // get and process the calendars meta feed
- GDataParser parser = null;
- try {
- String feedUrl = mCalendarClient.getUserCalendarsUrl(username);
- feedUrl = CalendarSyncAdapter.rewriteUrlforAccount(username, feedUrl);
- parser = mCalendarClient.getParserForUserCalendars(feedUrl, authToken);
- // process the calendars
- processCalendars(username, parser, existingCalendarIds);
- } catch (ParseException pe) {
- Log.w(TAG, "Unable to process calendars from server -- could not "
- + "parse calendar feed.", pe);
- return;
- } catch (IOException ioe) {
- Log.w(TAG, "Unable to process calendars from server -- encountered "
- + "i/o error", ioe);
- return;
- } catch (HttpException e) {
- switch (e.getStatusCode()) {
- case HttpException.SC_UNAUTHORIZED:
- Log.w(TAG, "Unable to process calendars from server -- could not "
- + "authenticate user.", e);
- return;
- case HttpException.SC_GONE:
- Log.w(TAG, "Unable to process calendars from server -- encountered "
- + "an AllDeletedUnavailableException, this should never happen", e);
- return;
- default:
- Log.w(TAG, "Unable to process calendars from server -- error", e);
- return;
- }
- } finally {
- if (parser != null) {
- parser.close();
- }
- }
-
- // delete calendars that are no longer sent from the server.
- final Uri calendarContentUri = Calendars.CONTENT_URI;
- for (long calId : existingCalendarIds) {
- // NOTE: triggers delete all events, instances for this calendar.
- delete(ContentUris.withAppendedId(calendarContentUri, calId),
- null /* where */, null /* selectionArgs */);
- }
- db.setTransactionSuccessful();
- } finally {
- db.endTransaction();
- }
- }
-
- private void getCurrentCalendars(Set<Long> calendarIds) {
- Cursor cursor = query(Calendars.CONTENT_URI,
- new String[] { Calendars._ID },
- null /* selection */,
- null /* selectionArgs */,
- null /* sort */);
- if (cursor != null) {
- try {
- while (cursor.moveToNext()) {
- calendarIds.add(cursor.getLong(0));
- }
- } finally {
- cursor.close();
- }
- }
- }
-
- private void processCalendars(String username,
- GDataParser parser,
- Set<Long> existingCalendarIds)
- throws ParseException, IOException {
- CalendarsFeed feed = (CalendarsFeed) parser.init();
- Entry entry = null;
- ContentValues map = new ContentValues();
- final Uri calendarContentUri = Calendars.CONTENT_URI;
- while (parser.hasMoreData()) {
- entry = parser.readNextEntry(entry);
- if (Config.LOGV) Log.v(TAG, "Read entry: " + entry.toString());
- CalendarEntry calendarEntry = (CalendarEntry) entry;
- String feedUrl = calendarEntryToContentValues(username, feed, calendarEntry, map);
- if (TextUtils.isEmpty(feedUrl)) {
- continue;
- }
- long calId = -1;
-
- Cursor c = query(calendarContentUri,
- new String[] { Calendars._ID },
- Calendars.URL + "='"
- + feedUrl + '\'' /* selection */,
- null /* selectionArgs */,
- null /* sort */);
- if (c != null) {
- try {
- if (c.moveToFirst()) {
- calId = c.getLong(0);
- existingCalendarIds.remove(calId);
- }
- } finally {
- c.close();
- }
- }
-
- if (calId != -1) {
- if (Config.LOGV) Log.v(TAG, "Updating calendar " + map);
- // don't override the existing "selected" or "hidden" settings.
- map.remove(Calendars.SELECTED);
- map.remove(Calendars.HIDDEN);
- // write to db directly, so we don't send a notification.
- updateInternal(ContentUris.withAppendedId(calendarContentUri, calId), map,
- null /* where */, null /* selectionArgs */);
- } else {
- // Select this calendar for syncing and display if it is
- // selected and not hidden.
- int syncAndDisplay = 0;
- if (calendarEntry.isSelected() && !calendarEntry.isHidden()) {
- syncAndDisplay = 1;
- }
- map.put(Calendars.SYNC_EVENTS, syncAndDisplay);
- map.put(Calendars.SELECTED, syncAndDisplay);
- map.put(Calendars.HIDDEN, 0);
- map.put(Calendars._SYNC_ACCOUNT, username);
- if (Config.LOGV) Log.v(TAG, "Adding calendar " + map);
- // write to db directly, so we don't send a notification.
- Uri row = insertInternal(calendarContentUri, map);
- }
- }
- }
-
- // TODO: unit test.
- protected static final String convertCalendarIdToFeedUrl(String url) {
- // id: http://www.google.com/calendar/feeds/<username>/<cal id>
- // desired feed:
- // http://www.google.com/calendar/feeds/<cal id>/<projection>
- int start = url.indexOf(FEEDS_SUBSTRING);
- if (start != -1) {
- // strip out the */ in /feeds/*/
- start += FEEDS_SUBSTRING.length();
- int end = url.indexOf('/', start);
- if (end != -1) {
- url = url.replace(url.substring(start, end + 1), "");
- }
- url = url + "/private" + DESIRED_PROJECTION;
- }
- return url;
- }
-
- /**
- * Convert the CalenderEntry to a Bundle that can be inserted/updated into the
- * Calendars table.
- */
- private String calendarEntryToContentValues(String account, CalendarsFeed feed,
- CalendarEntry entry,
- ContentValues map) {
- map.clear();
-
- String url = entry.getAlternateLink();
-
- // we only want to fetch the full-selfattendance calendar feeds
- if (!TextUtils.isEmpty(url)) {
- if (url.endsWith(EXPECTED_PROJECTION)) {
- url = url.replace(EXPECTED_PROJECTION, DESIRED_PROJECTION);
- }
- } else {
- // yuck. the alternate link was not available. we should
- // reconstruct from the id.
- url = entry.getId();
- if (!TextUtils.isEmpty(url)) {
- url = convertCalendarIdToFeedUrl(url);
- } else {
- if (Config.LOGV) {
- Log.v(TAG, "Cannot generate url for calendar feed.");
- }
- return null;
- }
- }
-
- url = CalendarSyncAdapter.rewriteUrlforAccount(account, url);
-
- map.put(Calendars.URL, url);
- map.put(Calendars.NAME, entry.getTitle());
-
- // TODO:
- map.put(Calendars.DISPLAY_NAME, entry.getTitle());
-
- map.put(Calendars.TIMEZONE, entry.getTimezone());
-
- String colorStr = entry.getColor();
- if (!TextUtils.isEmpty(colorStr)) {
- int color = Color.parseColor(colorStr);
- // Ensure the alpha is set to max
- color |= 0xff000000;
- map.put(Calendars.COLOR, color);
- }
-
- map.put(Calendars.SELECTED, entry.isSelected() ? 1 : 0);
-
- map.put(Calendars.HIDDEN, entry.isHidden() ? 1 : 0);
-
- int accesslevel;
- switch (entry.getAccessLevel()) {
- case CalendarEntry.ACCESS_NONE:
- accesslevel = Calendars.NO_ACCESS;
- break;
- case CalendarEntry.ACCESS_READ:
- accesslevel = Calendars.READ_ACCESS;
- break;
- case CalendarEntry.ACCESS_FREEBUSY:
- accesslevel = Calendars.FREEBUSY_ACCESS;
- break;
- case CalendarEntry.ACCESS_EDITOR:
- accesslevel = Calendars.EDITOR_ACCESS;
- break;
- case CalendarEntry.ACCESS_OWNER:
- accesslevel = Calendars.OWNER_ACCESS;
- break;
- default:
- accesslevel = Calendars.NO_ACCESS;
- }
- map.put(Calendars.ACCESS_LEVEL, accesslevel);
- // TODO: use the update time, when calendar actually supports this.
- // right now, calendar modifies the update time frequently.
- map.put(Calendars._SYNC_TIME, System.currentTimeMillis());
-
- return url;
- }
-
/*
* Fills the Instances table, if necessary, for the given range and then
* queries the Instances table.
@@ -2472,7 +2201,10 @@
if (!isTemporary()) {
Integer syncEvents = initialValues.getAsInteger(Calendars.SYNC_EVENTS);
if (syncEvents != null && syncEvents == 1) {
- String account = initialValues.getAsString(Calendars._SYNC_ACCOUNT);
+ String accountName = initialValues.getAsString(Calendars._SYNC_ACCOUNT);
+ String accountType = initialValues.getAsString(
+ Calendars._SYNC_ACCOUNT_TYPE);
+ final Account account = new Account(accountName, accountType);
String calendarUrl = initialValues.getAsString(Calendars.URL);
scheduleSync(account, false /* two-way sync */, calendarUrl);
}
@@ -3083,13 +2815,17 @@
String syncId = cursor.getString(EVENTS_SYNC_ID_INDEX);
if (!TextUtils.isEmpty(syncId)) {
String syncVersion = cursor.getString(EVENTS_SYNC_VERSION_INDEX);
- String syncAccount = cursor.getString(EVENTS_SYNC_ACCOUNT_INDEX);
+ String syncAccountName =
+ cursor.getString(EVENTS_SYNC_ACCOUNT_NAME_INDEX);
+ String syncAccountType =
+ cursor.getString(EVENTS_SYNC_ACCOUNT_TYPE_INDEX);
Long calId = cursor.getLong(EVENTS_CALENDAR_ID_INDEX);
ContentValues values = new ContentValues();
values.put(Events._SYNC_ID, syncId);
values.put(Events._SYNC_VERSION, syncVersion);
- values.put(Events._SYNC_ACCOUNT, syncAccount);
+ values.put(Events._SYNC_ACCOUNT, syncAccountName);
+ values.put(Events._SYNC_ACCOUNT_TYPE, syncAccountType);
values.put(Events.CALENDAR_ID, calId);
mDeletedEventsInserter.insert(values);
@@ -3332,46 +3068,46 @@
* Schedule a calendar sync for the account.
* @param account the account for which to schedule a sync
* @param uploadChangesOnly if set, specify that the sync should only send
- * up local changes
+ * up local changes
* @param url the url feed for the calendar to sync (may be null)
*/
- private void scheduleSync(String account, boolean uploadChangesOnly, String url) {
+ private void scheduleSync(Account account, boolean uploadChangesOnly, String url) {
Bundle extras = new Bundle();
- extras.putString(ContentResolver.SYNC_EXTRAS_ACCOUNT, account);
extras.putBoolean(ContentResolver.SYNC_EXTRAS_UPLOAD, uploadChangesOnly);
if (url != null) {
extras.putString("feed", url);
- extras.putBoolean(ContentResolver.SYNC_EXTRAS_FORCE, true);
+ extras.putBoolean(ContentResolver.SYNC_EXTRAS_MANUAL, true);
}
- getContext().getContentResolver().startSync(android.provider.Calendar.CONTENT_URI, extras);
+ ContentResolver.requestSync(account, Calendars.CONTENT_URI.getAuthority(), extras);
}
private void modifyCalendarSubscription(long id, boolean syncEvents) {
// get the account, url, and current selected state
// for this calendar.
Cursor cursor = query(ContentUris.withAppendedId(Calendars.CONTENT_URI, id),
- new String[] { Calendars._SYNC_ACCOUNT,
- Calendars.URL,
- Calendars.SYNC_EVENTS},
+ new String[] {Calendars._SYNC_ACCOUNT, Calendars._SYNC_ACCOUNT_TYPE,
+ Calendars.URL, Calendars.SYNC_EVENTS},
null /* selection */,
null /* selectionArgs */,
null /* sort */);
- String account = null;
+ Account account = null;
String calendarUrl = null;
boolean oldSyncEvents = false;
if (cursor != null) {
try {
cursor.moveToFirst();
- account = cursor.getString(0);
- calendarUrl = cursor.getString(1);
- oldSyncEvents = (cursor.getInt(2) != 0);
+ final String accountName = cursor.getString(0);
+ final String accountType = cursor.getString(1);
+ account = new Account(accountName, accountType);
+ calendarUrl = cursor.getString(2);
+ oldSyncEvents = (cursor.getInt(3) != 0);
} finally {
cursor.close();
}
}
- if (TextUtils.isEmpty(account) || TextUtils.isEmpty(calendarUrl)) {
+ if (account == null || TextUtils.isEmpty(calendarUrl)) {
// should not happen?
Log.w(TAG, "Cannot update subscription because account "
+ "or calendar url empty -- should not happen.");
@@ -3422,14 +3158,6 @@
}
@Override
- public synchronized SyncAdapter getSyncAdapter() {
- if (mSyncAdapter == null) {
- mSyncAdapter = new CalendarSyncAdapter(getContext(), this);
- }
- return mSyncAdapter;
- }
-
- @Override
public void onSyncStop(SyncContext context, boolean success) {
super.onSyncStop(context, success);
if (Log.isLoggable(TAG, Log.DEBUG)) {
@@ -4031,6 +3759,8 @@
DatabaseUtils.cursorStringToContentValues(diffsCursor, Events._SYNC_VERSION, values);
DatabaseUtils.cursorStringToContentValues(diffsCursor, Events._SYNC_DIRTY, values);
DatabaseUtils.cursorStringToContentValues(diffsCursor, Events._SYNC_ACCOUNT, values);
+ DatabaseUtils.cursorStringToContentValues(diffsCursor,
+ Events._SYNC_ACCOUNT_TYPE, values);
DatabaseUtils.cursorStringToContentValues(diffsCursor, Events.HTML_URI, values);
DatabaseUtils.cursorStringToContentValues(diffsCursor, Events.TITLE, values);
DatabaseUtils.cursorStringToContentValues(diffsCursor, Events.EVENT_LOCATION, values);
@@ -4151,6 +3881,8 @@
sEventsProjectionMap.put(Events._SYNC_LOCAL_ID, "Events._sync_local_id AS _sync_local_id");
sEventsProjectionMap.put(Events._SYNC_DIRTY, "Events._sync_dirty AS _sync_dirty");
sEventsProjectionMap.put(Events._SYNC_ACCOUNT, "Events._sync_account AS _sync_account");
+ sEventsProjectionMap.put(Events._SYNC_ACCOUNT_TYPE,
+ "Events._sync_account_type AS _sync_account_type");
// Instances columns
sInstancesProjectionMap.put(Instances.BEGIN, "begin");
diff --git a/src/com/android/providers/calendar/CalendarSyncAdapter.java b/src/com/android/providers/calendar/CalendarSyncAdapter.java
index 0c53c6a..e2695e4 100644
--- a/src/com/android/providers/calendar/CalendarSyncAdapter.java
+++ b/src/com/android/providers/calendar/CalendarSyncAdapter.java
@@ -17,23 +17,11 @@
package com.android.providers.calendar;
-import com.google.android.gdata.client.AndroidGDataClient;
-import com.google.android.gdata.client.AndroidXmlParserFactory;
-import com.google.android.providers.AbstractGDataSyncAdapter;
-import com.google.wireless.gdata.calendar.client.CalendarClient;
-import com.google.wireless.gdata.calendar.data.EventEntry;
-import com.google.wireless.gdata.calendar.data.EventsFeed;
-import com.google.wireless.gdata.calendar.data.Reminder;
-import com.google.wireless.gdata.calendar.data.When;
-import com.google.wireless.gdata.calendar.data.Who;
-import com.google.wireless.gdata.calendar.parser.xml.XmlCalendarGDataParserFactory;
-import com.google.wireless.gdata.client.GDataServiceClient;
-import com.google.wireless.gdata.client.QueryParams;
-import com.google.wireless.gdata.data.Entry;
-import com.google.wireless.gdata.data.Feed;
-import com.google.wireless.gdata.data.StringUtils;
-import com.google.wireless.gdata.parser.ParseException;
-
+import android.accounts.Account;
+import android.accounts.AccountManager;
+import android.accounts.AuthenticatorException;
+import android.accounts.Constants;
+import android.accounts.OperationCanceledException;
import android.content.ContentProvider;
import android.content.ContentResolver;
import android.content.ContentUris;
@@ -44,24 +32,50 @@
import android.content.SyncableContentProvider;
import android.database.Cursor;
import android.database.CursorJoiner;
+import android.graphics.Color;
import android.net.Uri;
import android.os.Bundle;
import android.os.SystemProperties;
import android.pim.ICalendar;
import android.pim.RecurrenceSet;
import android.provider.Calendar;
-import android.provider.SubscribedFeeds;
-import android.provider.SyncConstValue;
import android.provider.Calendar.Calendars;
import android.provider.Calendar.Events;
+import android.provider.SubscribedFeeds;
+import android.provider.SyncConstValue;
import android.text.TextUtils;
import android.text.format.Time;
import android.util.Config;
import android.util.Log;
+import com.google.android.gdata.client.AndroidGDataClient;
+import com.google.android.gdata.client.AndroidXmlParserFactory;
+import com.google.android.googlelogin.GoogleLoginServiceConstants;
+import com.google.android.providers.AbstractGDataSyncAdapter;
+import com.google.wireless.gdata.calendar.client.CalendarClient;
+import com.google.wireless.gdata.calendar.data.CalendarEntry;
+import com.google.wireless.gdata.calendar.data.CalendarsFeed;
+import com.google.wireless.gdata.calendar.data.EventEntry;
+import com.google.wireless.gdata.calendar.data.EventsFeed;
+import com.google.wireless.gdata.calendar.data.Reminder;
+import com.google.wireless.gdata.calendar.data.When;
+import com.google.wireless.gdata.calendar.data.Who;
+import com.google.wireless.gdata.calendar.parser.xml.XmlCalendarGDataParserFactory;
+import com.google.wireless.gdata.client.GDataServiceClient;
+import com.google.wireless.gdata.client.HttpException;
+import com.google.wireless.gdata.client.QueryParams;
+import com.google.wireless.gdata.data.Entry;
+import com.google.wireless.gdata.data.Feed;
+import com.google.wireless.gdata.data.StringUtils;
+import com.google.wireless.gdata.parser.GDataParser;
+import com.google.wireless.gdata.parser.ParseException;
+import java.io.IOException;
+import java.util.ArrayList;
import java.util.Enumeration;
+import java.util.HashSet;
import java.util.Hashtable;
import java.util.List;
+import java.util.Set;
import java.util.TimeZone;
import java.util.Vector;
@@ -74,18 +88,27 @@
/* package */ static final String USER_AGENT_APP_VERSION = "Android-GData-Calendar/1.1";
- private static final String SELECT_BY_ACCOUNT = Calendars._SYNC_ACCOUNT + "=?";
+ private static final String SELECT_BY_ACCOUNT =
+ Calendars._SYNC_ACCOUNT + "=? AND " + Calendars._SYNC_ACCOUNT_TYPE + "=?";
private static final String SELECT_BY_ACCOUNT_AND_FEED =
SELECT_BY_ACCOUNT + " AND " + Calendars.URL + "=?";
private static final String[] CALENDAR_KEY_COLUMNS =
- new String[]{Calendars._SYNC_ACCOUNT, Calendars.URL};
+ new String[]{Calendars._SYNC_ACCOUNT, Calendars._SYNC_ACCOUNT_TYPE, Calendars.URL};
private static final String CALENDAR_KEY_SORT_ORDER =
- Calendars._SYNC_ACCOUNT + "," + Calendars.URL;
+ Calendars._SYNC_ACCOUNT + "," + Calendars._SYNC_ACCOUNT_TYPE + "," + Calendars.URL;
private static final String[] FEEDS_KEY_COLUMNS =
- new String[]{SubscribedFeeds.Feeds._SYNC_ACCOUNT, SubscribedFeeds.Feeds.FEED};
+ new String[]{SubscribedFeeds.Feeds._SYNC_ACCOUNT,
+ SubscribedFeeds.Feeds._SYNC_ACCOUNT_TYPE, SubscribedFeeds.Feeds.FEED};
private static final String FEEDS_KEY_SORT_ORDER =
- SubscribedFeeds.Feeds._SYNC_ACCOUNT + ", " + SubscribedFeeds.Feeds.FEED;
+ SubscribedFeeds.Feeds._SYNC_ACCOUNT + ", " + SubscribedFeeds.Feeds._SYNC_ACCOUNT_TYPE
+ + ", " + SubscribedFeeds.Feeds.FEED;
+
+ private static final String EXPECTED_PROJECTION = "/full";
+
+ private static final String DESIRED_PROJECTION = "/full-selfattendance";
+
+ private static final String FEEDS_SUBSTRING = "/feeds/";
public static class SyncInfo {
// public String feedUrl;
@@ -519,19 +542,22 @@
protected boolean handleAllDeletedUnavailable(GDataSyncData syncData, String feed) {
syncData.feedData.remove(feed);
+ final Account account = getAccount();
getContext().getContentResolver().delete(Calendar.Calendars.CONTENT_URI,
- Calendar.Calendars._SYNC_ACCOUNT + "=? AND " + Calendar.Calendars.URL + "=?",
- new String[]{getAccount(), feed});
+ Calendar.Calendars._SYNC_ACCOUNT + "=? AND "
+ + Calendar.Calendars._SYNC_ACCOUNT_TYPE + "=? AND "
+ + Calendar.Calendars.URL + "=?",
+ new String[]{account.mName, account.mType, feed});
return true;
}
@Override
- public void onSyncStarting(SyncContext context, String account, boolean forced,
+ public void onSyncStarting(SyncContext context, Account account, boolean manualSync,
SyncResult result) {
mContentResolver = getContext().getContentResolver();
mServerDiffs = 0;
mRefresh = 0;
- super.onSyncStarting(context, account, forced, result);
+ super.onSyncStarting(context, account, manualSync, result);
}
private void deletedEntryToContentValues(Long syncLocalId, EventEntry event,
@@ -598,10 +624,11 @@
// Base sync info
map.put(Events._SYNC_ID, event.getId());
String version = event.getEditUri();
+ final Account account = getAccount();
if (!StringUtils.isEmpty(version)) {
// Always rewrite the edit URL to https for dasher account to avoid
// redirection.
- map.put(Events._SYNC_VERSION, rewriteUrlforAccount(getAccount(), version));
+ map.put(Events._SYNC_VERSION, rewriteUrlforAccount(account, version));
}
// see if this is an exception to an existing event/recurrence.
@@ -804,7 +831,8 @@
return ENTRY_INVALID;
}
- map.put(SyncConstValue._SYNC_ACCOUNT, getAccount());
+ map.put(SyncConstValue._SYNC_ACCOUNT, account.mName);
+ map.put(SyncConstValue._SYNC_ACCOUNT_TYPE, account.mType);
return ENTRY_OK;
}
@@ -1030,22 +1058,43 @@
final ContentResolver cr = getContext().getContentResolver();
mServerDiffs++;
final boolean syncingSingleFeed = (extras != null) && extras.containsKey("feed");
+ final boolean syncingMetafeedOnly = (extras != null) && extras.containsKey("metafeedonly");
+
if (syncingSingleFeed) {
+ if (syncingMetafeedOnly) {
+ Log.d(TAG, "metafeedonly and feed both set.");
+ return;
+ }
String feedUrl = extras.getString("feed");
getServerDiffsForFeed(context, baseSyncData, tempProvider, feedUrl,
baseSyncInfo, syncResult);
return;
}
+ // At this point, either metafeed sync or poll.
+ // For the poll (or metafeed sync), refresh the list of calendars.
+ // we can move away from this when we move to the new allcalendars feed, which is
+ // syncable. until then, we'll rely on the daily poll to keep the list of calendars
+ // up to date.
+
+ mRefresh++;
+ context.setStatusText("Fetching list of calendars");
+ fetchCalendarsFromServer();
+
+ if (syncingMetafeedOnly) {
+ // If not polling, nothing more to do.
+ return;
+ }
+
// select the set of calendars for this account.
+ final Account account = getAccount();
+ final String[] accountSelectionArgs = new String[]{account.mName, account.mType};
Cursor cursor = cr.query(Calendar.Calendars.CONTENT_URI,
CALENDARS_PROJECTION, SELECT_BY_ACCOUNT,
- new String[] { getAccount() }, null /* sort order */);
+ accountSelectionArgs, null /* sort order */);
Bundle syncExtras = new Bundle();
- boolean refreshCalendars = true;
-
try {
while (cursor.moveToNext()) {
boolean syncEvents = (cursor.getInt(6) == 1);
@@ -1055,29 +1104,12 @@
continue;
}
- // since this is a poll (no specific feed selected), refresh the list of calendars.
- // we can move away from this when we move to the new allcalendars feed, which is
- // syncable. until then, we'll rely on the daily poll to keep the list of calendars
- // up to date.
- if (refreshCalendars) {
- mRefresh++;
- context.setStatusText("Fetching list of calendars");
- // get rid of the current cursor and fetch from the server.
- cursor.close();
- final String[] accountSelectionArgs = new String[]{getAccount()};
- cursor = cr.query(
- Calendar.Calendars.LIVE_CONTENT_URI, CALENDARS_PROJECTION,
- SELECT_BY_ACCOUNT, accountSelectionArgs, null /* sort order */);
- // start over with the loop
- refreshCalendars = false;
- continue;
- }
-
// schedule syncs for each of these feeds.
syncExtras.clear();
syncExtras.putAll(extras);
syncExtras.putString("feed", feedUrl);
- cr.startSync(Calendar.CONTENT_URI, syncExtras);
+ ContentResolver.requestSync(account,
+ Calendar.Calendars.CONTENT_URI.getAuthority(), syncExtras);
}
} finally {
cursor.close();
@@ -1091,9 +1123,10 @@
final SyncInfo syncInfo = (SyncInfo) baseSyncInfo;
final GDataSyncData syncData = (GDataSyncData) baseSyncData;
+ final Account account = getAccount();
Cursor cursor = getContext().getContentResolver().query(Calendar.Calendars.CONTENT_URI,
CALENDARS_PROJECTION, SELECT_BY_ACCOUNT_AND_FEED,
- new String[] { getAccount(), feed }, null /* sort order */);
+ new String[] { account.mName, account.mType, feed }, null /* sort order */);
ContentValues map = new ContentValues();
int maxResults = getMaxEntriesPerSync();
@@ -1154,10 +1187,12 @@
// populate temp provider with calendar ids, so joins work.
ContentValues map = new ContentValues();
+ final Account account = getAccount();
Cursor c = getContext().getContentResolver().query(
Calendar.Calendars.CONTENT_URI,
CALENDARS_PROJECTION,
- SELECT_BY_ACCOUNT, new String[]{getAccount()}, null /* sort order */);
+ SELECT_BY_ACCOUNT, new String[]{account.mName, account.mType},
+ null /* sort order */);
final int idIndex = c.getColumnIndexOrThrow(Calendars._ID);
final int urlIndex = c.getColumnIndexOrThrow(Calendars.URL);
final int timezoneIndex = c.getColumnIndexOrThrow(Calendars.TIMEZONE);
@@ -1171,7 +1206,7 @@
c.close();
}
- public void onAccountsChanged(String[] accountsArray) {
+ public void onAccountsChanged(Account[] accountsArray) {
if (!"yes".equals(SystemProperties.get("ro.config.sync"))) {
return;
}
@@ -1188,12 +1223,17 @@
cursorA = Calendar.Calendars.query(cr, null /* projection */,
Calendar.Calendars.SELECTED + "=1", CALENDAR_KEY_SORT_ORDER);
int urlIndexA = cursorA.getColumnIndexOrThrow(Calendar.Calendars.URL);
- int accountIndexA = cursorA.getColumnIndexOrThrow(Calendar.Calendars._SYNC_ACCOUNT);
+ int accountNameIndexA = cursorA.getColumnIndexOrThrow(Calendar.Calendars._SYNC_ACCOUNT);
+ int accountTypeIndexA =
+ cursorA.getColumnIndexOrThrow(Calendar.Calendars._SYNC_ACCOUNT_TYPE);
cursorB = SubscribedFeeds.Feeds.query(cr, FEEDS_KEY_COLUMNS,
SubscribedFeeds.Feeds.AUTHORITY + "=?", new String[]{Calendar.AUTHORITY},
FEEDS_KEY_SORT_ORDER);
int urlIndexB = cursorB.getColumnIndexOrThrow(SubscribedFeeds.Feeds.FEED);
- int accountIndexB = cursorB.getColumnIndexOrThrow(SubscribedFeeds.Feeds._SYNC_ACCOUNT);
+ int accountNameIndexB =
+ cursorB.getColumnIndexOrThrow(SubscribedFeeds.Feeds._SYNC_ACCOUNT);
+ int accountTypeIndexB =
+ cursorB.getColumnIndexOrThrow(SubscribedFeeds.Feeds._SYNC_ACCOUNT_TYPE);
for (CursorJoiner.Result joinerResult :
new CursorJoiner(cursorA, CALENDAR_KEY_COLUMNS, cursorB, FEEDS_KEY_COLUMNS)) {
switch (joinerResult) {
@@ -1201,7 +1241,8 @@
SubscribedFeeds.addFeed(
cr,
cursorA.getString(urlIndexA),
- cursorA.getString(accountIndexA),
+ new Account(cursorA.getString(accountNameIndexA),
+ cursorA.getString(accountTypeIndexA)),
Calendar.AUTHORITY,
CalendarClient.SERVICE);
break;
@@ -1209,7 +1250,8 @@
SubscribedFeeds.deleteFeed(
cr,
cursorB.getString(urlIndexB),
- cursorB.getString(accountIndexB),
+ new Account(cursorB.getString(accountNameIndexB),
+ cursorB.getString(accountTypeIndexB)),
Calendar.AUTHORITY);
break;
case BOTH:
@@ -1229,7 +1271,7 @@
* to/from the device, and thus is determined and passed around as a local variable, where
* appropriate.
*/
- protected String getFeedUrl(String account) {
+ protected String getFeedUrl(Account account) {
throw new UnsupportedOperationException("getFeedUrl() should not get called.");
}
@@ -1279,4 +1321,286 @@
sb.append("s").append(mServerDiffs);
}
}
+
+ private void fetchCalendarsFromServer() {
+ if (mCalendarClient == null) {
+ Log.w(TAG, "Cannot fetch calendars -- calendar url defined.");
+ return;
+ }
+
+ Account account = null;
+ String authToken = null;
+
+
+ try {
+ // TODO: allow caller to specify which account's feeds should be updated
+ Account[] accounts = AccountManager.get(getContext())
+ .blockingGetAccountsWithTypeAndFeatures(
+ GoogleLoginServiceConstants.ACCOUNT_TYPE,
+ new String[]{GoogleLoginServiceConstants.FEATURE_GOOGLE_OR_DASHER});
+ if (accounts.length == 0) {
+ Log.w(TAG, "Unable to update calendars from server -- no users configured.");
+ return;
+ }
+
+ account = accounts[0];
+
+ Bundle bundle = AccountManager.get(getContext()).getAuthToken(
+ account, mCalendarClient.getServiceName(),
+ true /* notifyAuthFailure */, null /* callback */, null /* handler */)
+ .getResult();
+ authToken = bundle.getString(Constants.AUTHTOKEN_KEY);
+ if (authToken == null) {
+ Log.w(TAG, "Unable to update calendars from server -- could not "
+ + "authenticate user " + account);
+ return;
+ }
+ } catch (IOException e) {
+ Log.w(TAG, "Unable to update calendars from server -- could not "
+ + "authenticate user " + account, e);
+ return;
+ } catch (AuthenticatorException e) {
+ Log.w(TAG, "Unable to update calendars from server -- could not "
+ + "authenticate user " + account, e);
+ return;
+ } catch (OperationCanceledException e) {
+ Log.w(TAG, "Unable to update calendars from server -- could not "
+ + "authenticate user " + account, e);
+ return;
+ }
+
+ // get the current set of calendars. we'll need to pay attention to
+ // which calendars we get back from the server, so we can delete
+ // calendars that have been deleted from the server.
+ Set<Long> existingCalendarIds = new HashSet<Long>();
+
+ getCurrentCalendars(existingCalendarIds);
+
+ // get and process the calendars meta feed
+ GDataParser parser = null;
+ try {
+ String feedUrl = mCalendarClient.getUserCalendarsUrl(account.mName);
+ feedUrl = CalendarSyncAdapter.rewriteUrlforAccount(account, feedUrl);
+ parser = mCalendarClient.getParserForUserCalendars(feedUrl, authToken);
+ // process the calendars
+ processCalendars(account, parser, existingCalendarIds);
+ } catch (ParseException pe) {
+ Log.w(TAG, "Unable to process calendars from server -- could not "
+ + "parse calendar feed.", pe);
+ return;
+ } catch (IOException ioe) {
+ Log.w(TAG, "Unable to process calendars from server -- encountered "
+ + "i/o error", ioe);
+ return;
+ } catch (HttpException e) {
+ switch (e.getStatusCode()) {
+ case HttpException.SC_UNAUTHORIZED:
+ Log.w(TAG, "Unable to process calendars from server -- could not "
+ + "authenticate user.", e);
+ return;
+ case HttpException.SC_GONE:
+ Log.w(TAG, "Unable to process calendars from server -- encountered "
+ + "an AllDeletedUnavailableException, this should never happen", e);
+ return;
+ default:
+ Log.w(TAG, "Unable to process calendars from server -- error", e);
+ return;
+ }
+ } finally {
+ if (parser != null) {
+ parser.close();
+ }
+ }
+
+ // delete calendars that are no longer sent from the server.
+ final Uri calendarContentUri = Calendars.CONTENT_URI;
+ final ContentResolver cr = getContext().getContentResolver();
+ for (long calId : existingCalendarIds) {
+ // NOTE: triggers delete all events, instances for this calendar.
+ cr.delete(ContentUris.withAppendedId(calendarContentUri, calId),
+ null /* where */, null /* selectionArgs */);
+ }
+ }
+
+ private void getCurrentCalendars(Set<Long> calendarIds) {
+ final ContentResolver cr = getContext().getContentResolver();
+ Cursor cursor = cr.query(Calendars.CONTENT_URI,
+ new String[] { Calendars._ID },
+ null /* selection */,
+ null /* selectionArgs */,
+ null /* sort */);
+ if (cursor != null) {
+ try {
+ while (cursor.moveToNext()) {
+ calendarIds.add(cursor.getLong(0));
+ }
+ } finally {
+ cursor.close();
+ }
+ }
+ }
+
+ private void processCalendars(Account account,
+ GDataParser parser,
+ Set<Long> existingCalendarIds)
+ throws ParseException, IOException {
+ final ContentResolver cr = getContext().getContentResolver();
+ CalendarsFeed feed = (CalendarsFeed) parser.init();
+ Entry entry = null;
+ final Uri calendarContentUri = Calendars.CONTENT_URI;
+ ArrayList<ContentValues> inserts = new ArrayList<ContentValues>();
+ while (parser.hasMoreData()) {
+ entry = parser.readNextEntry(entry);
+ if (Config.LOGV) Log.v(TAG, "Read entry: " + entry.toString());
+ CalendarEntry calendarEntry = (CalendarEntry) entry;
+ ContentValues map = new ContentValues();
+ String feedUrl = calendarEntryToContentValues(account, feed, calendarEntry, map);
+ if (TextUtils.isEmpty(feedUrl)) {
+ continue;
+ }
+ long calId = -1;
+
+ Cursor c = cr.query(calendarContentUri,
+ new String[] { Calendars._ID },
+ Calendars.URL + "='"
+ + feedUrl + '\'' /* selection */,
+ null /* selectionArgs */,
+ null /* sort */);
+ if (c != null) {
+ try {
+ if (c.moveToFirst()) {
+ calId = c.getLong(0);
+ existingCalendarIds.remove(calId);
+ }
+ } finally {
+ c.close();
+ }
+ }
+
+ if (calId != -1) {
+ if (Config.LOGV) Log.v(TAG, "Updating calendar " + map);
+ // don't override the existing "selected" or "hidden" settings.
+ map.remove(Calendars.SELECTED);
+ map.remove(Calendars.HIDDEN);
+ cr.update(ContentUris.withAppendedId(calendarContentUri, calId), map,
+ null /* where */, null /* selectionArgs */);
+ } else {
+ // Select this calendar for syncing and display if it is
+ // selected and not hidden.
+ int syncAndDisplay = 0;
+ if (calendarEntry.isSelected() && !calendarEntry.isHidden()) {
+ syncAndDisplay = 1;
+ }
+ map.put(Calendars.SYNC_EVENTS, syncAndDisplay);
+ map.put(Calendars.SELECTED, syncAndDisplay);
+ map.put(Calendars.HIDDEN, 0);
+ map.put(Calendars._SYNC_ACCOUNT, account.mName);
+ map.put(Calendars._SYNC_ACCOUNT_TYPE, account.mType);
+ if (Config.LOGV) Log.v(TAG, "Adding calendar " + map);
+ inserts.add(map);
+ }
+ }
+ if (!inserts.isEmpty()) {
+ if (Config.LOGV) Log.v(TAG, "Bulk updating calendar list.");
+ cr.bulkInsert(calendarContentUri, inserts.toArray(new ContentValues[inserts.size()]));
+ }
+ }
+
+ /**
+ * Convert the CalenderEntry to a Bundle that can be inserted/updated into the
+ * Calendars table.
+ */
+ private String calendarEntryToContentValues(Account account, CalendarsFeed feed,
+ CalendarEntry entry,
+ ContentValues map) {
+ map.clear();
+
+ String url = entry.getAlternateLink();
+
+ // we only want to fetch the full-selfattendance calendar feeds
+ if (!TextUtils.isEmpty(url)) {
+ if (url.endsWith(EXPECTED_PROJECTION)) {
+ url = url.replace(EXPECTED_PROJECTION, DESIRED_PROJECTION);
+ }
+ } else {
+ // yuck. the alternate link was not available. we should
+ // reconstruct from the id.
+ url = entry.getId();
+ if (!TextUtils.isEmpty(url)) {
+ url = convertCalendarIdToFeedUrl(url);
+ } else {
+ if (Config.LOGV) {
+ Log.v(TAG, "Cannot generate url for calendar feed.");
+ }
+ return null;
+ }
+ }
+
+ url = rewriteUrlforAccount(account, url);
+
+ map.put(Calendars.URL, url);
+ map.put(Calendars.NAME, entry.getTitle());
+
+ // TODO:
+ map.put(Calendars.DISPLAY_NAME, entry.getTitle());
+
+ map.put(Calendars.TIMEZONE, entry.getTimezone());
+
+ String colorStr = entry.getColor();
+ if (!TextUtils.isEmpty(colorStr)) {
+ int color = Color.parseColor(colorStr);
+ // Ensure the alpha is set to max
+ color |= 0xff000000;
+ map.put(Calendars.COLOR, color);
+ }
+
+ map.put(Calendars.SELECTED, entry.isSelected() ? 1 : 0);
+
+ map.put(Calendars.HIDDEN, entry.isHidden() ? 1 : 0);
+
+ int accesslevel;
+ switch (entry.getAccessLevel()) {
+ case CalendarEntry.ACCESS_NONE:
+ accesslevel = Calendars.NO_ACCESS;
+ break;
+ case CalendarEntry.ACCESS_READ:
+ accesslevel = Calendars.READ_ACCESS;
+ break;
+ case CalendarEntry.ACCESS_FREEBUSY:
+ accesslevel = Calendars.FREEBUSY_ACCESS;
+ break;
+ case CalendarEntry.ACCESS_EDITOR:
+ accesslevel = Calendars.EDITOR_ACCESS;
+ break;
+ case CalendarEntry.ACCESS_OWNER:
+ accesslevel = Calendars.OWNER_ACCESS;
+ break;
+ default:
+ accesslevel = Calendars.NO_ACCESS;
+ }
+ map.put(Calendars.ACCESS_LEVEL, accesslevel);
+ // TODO: use the update time, when calendar actually supports this.
+ // right now, calendar modifies the update time frequently.
+ map.put(Calendars._SYNC_TIME, System.currentTimeMillis());
+
+ return url;
+ }
+
+ // TODO: unit test.
+ protected static final String convertCalendarIdToFeedUrl(String url) {
+ // id: http://www.google.com/calendar/feeds/<username>/<cal id>
+ // desired feed:
+ // http://www.google.com/calendar/feeds/<cal id>/<projection>
+ int start = url.indexOf(FEEDS_SUBSTRING);
+ if (start != -1) {
+ // strip out the */ in /feeds/*/
+ start += FEEDS_SUBSTRING.length();
+ int end = url.indexOf('/', start);
+ if (end != -1) {
+ url = url.replace(url.substring(start, end + 1), "");
+ }
+ url = url + "/private" + DESIRED_PROJECTION;
+ }
+ return url;
+ }
}
diff --git a/src/com/android/providers/calendar/CalendarSyncAdapterService.java b/src/com/android/providers/calendar/CalendarSyncAdapterService.java
new file mode 100644
index 0000000..7634f2c
--- /dev/null
+++ b/src/com/android/providers/calendar/CalendarSyncAdapterService.java
@@ -0,0 +1,29 @@
+package com.android.providers.calendar;
+
+import android.app.Service;
+import android.os.IBinder;
+import android.content.Intent;
+import android.content.ContentProviderClient;
+import android.content.ContentProvider;
+import android.content.SyncableContentProvider;
+import android.provider.Calendar;
+
+public class CalendarSyncAdapterService extends Service {
+ private ContentProviderClient mContentProviderClient = null;
+
+ public void onCreate() {
+ mContentProviderClient =
+ getContentResolver().acquireContentProviderClient(Calendar.CONTENT_URI);
+ }
+
+ public void onDestroy() {
+ mContentProviderClient.release();
+ }
+
+ public IBinder onBind(Intent intent) {
+ ContentProvider contentProvider = mContentProviderClient.getLocalContentProvider();
+ if (contentProvider == null) throw new IllegalStateException();
+ SyncableContentProvider syncableContentProvider = (SyncableContentProvider)contentProvider;
+ return syncableContentProvider.getTempProviderSyncAdapter().getISyncAdapter().asBinder();
+ }
+}
diff --git a/tests/src/com/android/providers/calendar/TestCalendarSyncAdapter.java b/tests/src/com/android/providers/calendar/TestCalendarSyncAdapter.java
index 136b382..bcc1c73 100644
--- a/tests/src/com/android/providers/calendar/TestCalendarSyncAdapter.java
+++ b/tests/src/com/android/providers/calendar/TestCalendarSyncAdapter.java
@@ -26,6 +26,7 @@
import android.net.Uri;
import android.os.Bundle;
import android.text.format.DateUtils;
+import android.accounts.Account;
import java.util.Calendar;
import java.util.TimeZone;
@@ -40,7 +41,7 @@
}
@Override
- public void onSyncStarting(SyncContext context, String account, boolean forced,
+ public void onSyncStarting(SyncContext context, Account account, boolean manualSync,
SyncResult result)
{
}
@@ -162,7 +163,7 @@
throw new UnsupportedOperationException("not implemented");
}
- public void onAccountsChanged(String[] accounts) {
+ public void onAccountsChanged(Account[] accounts) {
}
}