Implement Exchange calendar sync support
What should be working: * Events sync down from server and appear in calendar * Recurrences and exceptions appear in calendar * Changed events on server should be reflected in calendar * Deletions on server should be reflected in calendar * Push of new/changed/deleted events should work * Changes on device are NOT synced back to server * New, single events on device are synced back to server (no time zone, attendee, or recurrence support) * Checkbox for syncing calendar added to setup flow * System sync glue in manifest, etc. * Bugs are to be expected * A few unit tests; needs more Change-Id: I7ca262eaba562ccb9d1af5b0cd948c6bac30e5dd
This commit is contained in:
parent
b4e7a85eaa
commit
f3fcb8929e
|
@ -31,6 +31,8 @@
|
|||
|
||||
<!-- For EAS purposes; could be removed when EAS has a permanent home -->
|
||||
<uses-permission android:name="android.permission.WRITE_CONTACTS"/>
|
||||
<uses-permission android:name="android.permission.WRITE_CALENDAR"/>
|
||||
<uses-permission android:name="android.permission.READ_CALENDAR"/>
|
||||
|
||||
<!-- Only required if a store implements push mail and needs to keep network open -->
|
||||
<uses-permission android:name="android.permission.WAKE_LOCK"/>
|
||||
|
@ -208,6 +210,17 @@
|
|||
android:resource="@xml/syncadapter_contacts" />
|
||||
</service>
|
||||
|
||||
<!--Required stanza to register the CalendarSyncAdapterService with SyncManager -->
|
||||
<service
|
||||
android:name="com.android.exchange.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_calendar" />
|
||||
</service>
|
||||
|
||||
<!-- Add android:process=":remote" below to enable SyncManager as a separate process -->
|
||||
<service
|
||||
android:name="com.android.exchange.SyncManager"
|
||||
|
|
|
@ -70,6 +70,12 @@
|
|||
android:layout_width="match_parent"
|
||||
android:text="@string/account_setup_options_sync_contacts_label"
|
||||
android:visibility="gone" />
|
||||
<CheckBox
|
||||
android:id="@+id/account_sync_calendar"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_width="match_parent"
|
||||
android:text="@string/account_setup_options_sync_calendar_label"
|
||||
android:visibility="gone" />
|
||||
</LinearLayout>
|
||||
|
||||
<RelativeLayout
|
||||
|
|
|
@ -454,7 +454,11 @@
|
|||
<!-- In Account setup options & Account Settings screens, check box for new-mail notification -->
|
||||
<string name="account_setup_options_notify_label">Notify me when email arrives.</string>
|
||||
<!-- In Account setup options screen, optional check box to also sync contacts -->
|
||||
<string name="account_setup_options_sync_contacts_label">Sync contacts from this account.</string>
|
||||
<string name="account_setup_options_sync_contacts_label">Sync contacts from this account.
|
||||
</string>
|
||||
<!-- In Account setup options screen, optional check box to also sync contacts -->
|
||||
<string name="account_setup_options_sync_calendar_label">Sync calendar from this account.
|
||||
</string>
|
||||
<!-- Dialog title when "setup" could not finish -->
|
||||
<string name="account_setup_failed_dlg_title">Setup could not finish</string>
|
||||
<!-- In Account setup options screen, label for email check frequency selector -->
|
||||
|
|
|
@ -0,0 +1,26 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
/**
|
||||
* Copyright (c) 2010, 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="com.android.calendar"
|
||||
android:accountType="com.android.exchange"
|
||||
/>
|
|
@ -49,6 +49,7 @@ public class Account {
|
|||
public static final int BACKUP_FLAGS_IS_BACKUP = 1;
|
||||
public static final int BACKUP_FLAGS_SYNC_CONTACTS = 2;
|
||||
public static final int BACKUP_FLAGS_IS_DEFAULT = 4;
|
||||
public static final int BACKUP_FLAGS_SYNC_CALENDAR = 8;
|
||||
|
||||
// transient values - do not serialize
|
||||
private transient Preferences mPreferences;
|
||||
|
|
|
@ -26,6 +26,7 @@ import android.content.Context;
|
|||
import android.content.Intent;
|
||||
import android.database.Cursor;
|
||||
import android.os.Bundle;
|
||||
import android.provider.Calendar;
|
||||
import android.provider.ContactsContract;
|
||||
import android.util.Log;
|
||||
|
||||
|
@ -115,6 +116,11 @@ public class AccountBackupRestore {
|
|||
if (syncContacts) {
|
||||
toAccount.mBackupFlags |= Account.BACKUP_FLAGS_SYNC_CONTACTS;
|
||||
}
|
||||
boolean syncCalendar = ContentResolver.getSyncAutomatically(acct,
|
||||
Calendar.AUTHORITY);
|
||||
if (syncCalendar) {
|
||||
toAccount.mBackupFlags |= Account.BACKUP_FLAGS_SYNC_CALENDAR;
|
||||
}
|
||||
}
|
||||
|
||||
// If this is the default account, mark it as such
|
||||
|
@ -176,9 +182,11 @@ public class AccountBackupRestore {
|
|||
// For exchange accounts, handle system account first, then save in provider
|
||||
if (toAccount.mHostAuthRecv.mProtocol.equals("eas")) {
|
||||
// Recreate entry in Account Manager as well, if needed
|
||||
// Set "sync contacts" mode as well, if needed
|
||||
// Set "sync contacts/calendar" mode as well, if needed
|
||||
boolean alsoSyncContacts =
|
||||
(backupAccount.mBackupFlags & Account.BACKUP_FLAGS_SYNC_CONTACTS) != 0;
|
||||
boolean alsoSyncCalendar =
|
||||
(backupAccount.mBackupFlags & Account.BACKUP_FLAGS_SYNC_CALENDAR) != 0;
|
||||
|
||||
// Use delete-then-add semantic to simplify handling of update-in-place
|
||||
// AccountManagerFuture<Boolean> removeResult = ExchangeStore.removeSystemAccount(
|
||||
|
@ -197,8 +205,9 @@ public class AccountBackupRestore {
|
|||
// NOTE: We must use the Application here, rather than the current context, because
|
||||
// all future references to AccountManager will use the context passed in here
|
||||
// TODO: Need to implement overwrite semantics for an already-installed account
|
||||
AccountManagerFuture<Bundle> addAccountResult = ExchangeStore.addSystemAccount(
|
||||
context.getApplicationContext(), toAccount, alsoSyncContacts, null);
|
||||
AccountManagerFuture<Bundle> addAccountResult =
|
||||
ExchangeStore.addSystemAccount(context.getApplicationContext(), toAccount,
|
||||
alsoSyncContacts, alsoSyncCalendar, null);
|
||||
// try {
|
||||
// // This call blocks until addSystemAccount completes. Result is not used.
|
||||
// addAccountResult.getResult();
|
||||
|
|
|
@ -53,6 +53,7 @@ public class AccountSetupOptions extends Activity implements OnClickListener {
|
|||
private CheckBox mDefaultView;
|
||||
private CheckBox mNotifyView;
|
||||
private CheckBox mSyncContactsView;
|
||||
private CheckBox mSyncCalendarView;
|
||||
private EmailContent.Account mAccount;
|
||||
private boolean mEasFlowMode;
|
||||
private Handler mHandler = new Handler();
|
||||
|
@ -76,6 +77,7 @@ public class AccountSetupOptions extends Activity implements OnClickListener {
|
|||
mDefaultView = (CheckBox)findViewById(R.id.account_default);
|
||||
mNotifyView = (CheckBox)findViewById(R.id.account_notify);
|
||||
mSyncContactsView = (CheckBox) findViewById(R.id.account_sync_contacts);
|
||||
mSyncCalendarView = (CheckBox) findViewById(R.id.account_sync_calendar);
|
||||
|
||||
findViewById(R.id.next).setOnClickListener(this);
|
||||
|
||||
|
@ -129,6 +131,8 @@ public class AccountSetupOptions extends Activity implements OnClickListener {
|
|||
// "also sync contacts" == "true"
|
||||
mSyncContactsView.setVisibility(View.VISIBLE);
|
||||
mSyncContactsView.setChecked(true);
|
||||
mSyncCalendarView.setVisibility(View.VISIBLE);
|
||||
mSyncCalendarView.setChecked(true);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -206,11 +210,12 @@ public class AccountSetupOptions extends Activity implements OnClickListener {
|
|||
&& mAccount.mHostAuthRecv != null
|
||||
&& mAccount.mHostAuthRecv.mProtocol.equals("eas")) {
|
||||
boolean alsoSyncContacts = mSyncContactsView.isChecked();
|
||||
boolean alsoSyncCalendar = mSyncCalendarView.isChecked();
|
||||
// Set the incomplete flag here to avoid reconciliation issues in SyncManager (EAS)
|
||||
mAccount.mFlags |= Account.FLAGS_INCOMPLETE;
|
||||
AccountSettingsUtils.commitSettings(this, mAccount);
|
||||
ExchangeStore.addSystemAccount(getApplication(), mAccount,
|
||||
alsoSyncContacts, mAccountManagerCallback);
|
||||
alsoSyncContacts, alsoSyncCalendar, mAccountManagerCallback);
|
||||
} else {
|
||||
finishOnDone();
|
||||
}
|
||||
|
|
|
@ -81,12 +81,13 @@ public class ExchangeStore extends Store {
|
|||
}
|
||||
|
||||
static public AccountManagerFuture<Bundle> addSystemAccount(Context context, Account acct,
|
||||
boolean syncContacts, AccountManagerCallback<Bundle> callback) {
|
||||
boolean syncContacts, boolean syncCalendar, AccountManagerCallback<Bundle> callback) {
|
||||
// Create a description of the new account
|
||||
Bundle options = new Bundle();
|
||||
options.putString(EasAuthenticatorService.OPTIONS_USERNAME, acct.mEmailAddress);
|
||||
options.putString(EasAuthenticatorService.OPTIONS_PASSWORD, acct.mHostAuthRecv.mPassword);
|
||||
options.putBoolean(EasAuthenticatorService.OPTIONS_CONTACTS_SYNC_ENABLED, syncContacts);
|
||||
options.putBoolean(EasAuthenticatorService.OPTIONS_CALENDAR_SYNC_ENABLED, syncCalendar);
|
||||
|
||||
// Here's where we tell AccountManager about the new account. The addAccount
|
||||
// method in AccountManager calls the addAccount method in our authenticator
|
||||
|
|
|
@ -30,6 +30,7 @@ import android.content.Context;
|
|||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
import android.os.IBinder;
|
||||
import android.provider.Calendar;
|
||||
import android.provider.ContactsContract;
|
||||
|
||||
/**
|
||||
|
@ -41,6 +42,7 @@ public class EasAuthenticatorService extends Service {
|
|||
public static final String OPTIONS_USERNAME = "username";
|
||||
public static final String OPTIONS_PASSWORD = "password";
|
||||
public static final String OPTIONS_CONTACTS_SYNC_ENABLED = "contacts";
|
||||
public static final String OPTIONS_CALENDAR_SYNC_ENABLED = "calendar";
|
||||
|
||||
class EasAuthenticator extends AbstractAccountAuthenticator {
|
||||
public EasAuthenticator(Context context) {
|
||||
|
@ -68,10 +70,18 @@ public class EasAuthenticatorService extends Service {
|
|||
options.getBoolean(OPTIONS_CONTACTS_SYNC_ENABLED)) {
|
||||
syncContacts = true;
|
||||
}
|
||||
ContentResolver.setIsSyncable(account,
|
||||
ContactsContract.AUTHORITY, 1);
|
||||
ContentResolver.setSyncAutomatically(account,
|
||||
ContactsContract.AUTHORITY, syncContacts);
|
||||
ContentResolver.setIsSyncable(account, ContactsContract.AUTHORITY, 1);
|
||||
ContentResolver.setSyncAutomatically(account, ContactsContract.AUTHORITY,
|
||||
syncContacts);
|
||||
|
||||
// Set up calendar syncing, as above
|
||||
boolean syncCalendar = false;
|
||||
if (options.containsKey(OPTIONS_CALENDAR_SYNC_ENABLED) &&
|
||||
options.getBoolean(OPTIONS_CALENDAR_SYNC_ENABLED)) {
|
||||
syncCalendar = true;
|
||||
}
|
||||
ContentResolver.setIsSyncable(account, Calendar.AUTHORITY, 1);
|
||||
ContentResolver.setSyncAutomatically(account, Calendar.AUTHORITY, syncCalendar);
|
||||
|
||||
Bundle b = new Bundle();
|
||||
b.putString(AccountManager.KEY_ACCOUNT_NAME, options.getString(OPTIONS_USERNAME));
|
||||
|
|
|
@ -0,0 +1,142 @@
|
|||
/*
|
||||
* Copyright (C) 2010 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.exchange;
|
||||
|
||||
import com.android.email.provider.EmailContent;
|
||||
import com.android.email.provider.EmailContent.AccountColumns;
|
||||
import com.android.email.provider.EmailContent.Mailbox;
|
||||
import com.android.email.provider.EmailContent.MailboxColumns;
|
||||
|
||||
import android.accounts.Account;
|
||||
import android.accounts.OperationCanceledException;
|
||||
import android.app.Service;
|
||||
import android.content.AbstractThreadedSyncAdapter;
|
||||
import android.content.ContentProviderClient;
|
||||
import android.content.ContentResolver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.SyncResult;
|
||||
import android.database.Cursor;
|
||||
import android.os.Bundle;
|
||||
import android.os.IBinder;
|
||||
import android.provider.Calendar.Events;
|
||||
import android.util.Log;
|
||||
|
||||
public class CalendarSyncAdapterService extends Service {
|
||||
private static final String TAG = "EAS CalendarSyncAdapterService";
|
||||
private static SyncAdapterImpl sSyncAdapter = null;
|
||||
private static final Object sSyncAdapterLock = new Object();
|
||||
|
||||
private static final String ACCOUNT_AND_TYPE_CALENDAR =
|
||||
MailboxColumns.ACCOUNT_KEY + "=? AND " + MailboxColumns.TYPE + '=' + Mailbox.TYPE_CALENDAR;
|
||||
|
||||
public CalendarSyncAdapterService() {
|
||||
super();
|
||||
}
|
||||
|
||||
private static class SyncAdapterImpl extends AbstractThreadedSyncAdapter {
|
||||
private Context mContext;
|
||||
|
||||
public SyncAdapterImpl(Context context) {
|
||||
super(context, true /* autoInitialize */);
|
||||
mContext = context;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPerformSync(Account account, Bundle extras,
|
||||
String authority, ContentProviderClient provider, SyncResult syncResult) {
|
||||
try {
|
||||
CalendarSyncAdapterService.performSync(mContext, account, extras,
|
||||
authority, provider, syncResult);
|
||||
} catch (OperationCanceledException e) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate() {
|
||||
super.onCreate();
|
||||
synchronized (sSyncAdapterLock) {
|
||||
if (sSyncAdapter == null) {
|
||||
sSyncAdapter = new SyncAdapterImpl(getApplicationContext());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public IBinder onBind(Intent intent) {
|
||||
return sSyncAdapter.getSyncAdapterBinder();
|
||||
}
|
||||
|
||||
/**
|
||||
* Partial integration with system SyncManager; we tell our EAS SyncManager to start a calendar
|
||||
* sync when we get the signal from the system SyncManager.
|
||||
* The missing piece at this point is integration with the push/ping mechanism in EAS; this will
|
||||
* be put in place at a later time.
|
||||
*/
|
||||
private static void performSync(Context context, Account account, Bundle extras,
|
||||
String authority, ContentProviderClient provider, SyncResult syncResult)
|
||||
throws OperationCanceledException {
|
||||
ContentResolver cr = context.getContentResolver();
|
||||
boolean logging = Eas.USER_LOG;
|
||||
if (logging) {
|
||||
Log.d(TAG, "performSync");
|
||||
}
|
||||
if (extras.getBoolean(ContentResolver.SYNC_EXTRAS_UPLOAD)) {
|
||||
Cursor c = cr.query(Events.CONTENT_URI,
|
||||
new String[] {Events._ID}, Events._SYNC_ID+ " ISNULL", null, null);
|
||||
try {
|
||||
if (!c.moveToFirst()) {
|
||||
if (logging) {
|
||||
Log.d(TAG, "Upload sync; no changes");
|
||||
}
|
||||
return;
|
||||
}
|
||||
} finally {
|
||||
c.close();
|
||||
}
|
||||
}
|
||||
|
||||
// Find the (EmailProvider) account associated with this email address
|
||||
Cursor accountCursor =
|
||||
cr.query(EmailContent.Account.CONTENT_URI,
|
||||
EmailContent.ID_PROJECTION, AccountColumns.EMAIL_ADDRESS + "=?",
|
||||
new String[] {account.name}, null);
|
||||
try {
|
||||
if (accountCursor.moveToFirst()) {
|
||||
long accountId = accountCursor.getLong(0);
|
||||
// Now, find the calendar mailbox associated with the account
|
||||
Cursor mailboxCursor = cr.query(Mailbox.CONTENT_URI, EmailContent.ID_PROJECTION,
|
||||
ACCOUNT_AND_TYPE_CALENDAR, new String[] {Long.toString(accountId)}, null);
|
||||
try {
|
||||
if (mailboxCursor.moveToFirst()) {
|
||||
if (logging) {
|
||||
Log.d(TAG, "Calendar sync requested for " + account.name);
|
||||
}
|
||||
// Ask for a sync from our sync manager
|
||||
SyncManager.serviceRequest(mailboxCursor.getLong(0),
|
||||
SyncManager.SYNC_UPSYNC);
|
||||
}
|
||||
} finally {
|
||||
mailboxCursor.close();
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
accountCursor.close();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -31,6 +31,7 @@ import com.android.email.provider.EmailContent.Message;
|
|||
import com.android.email.service.EmailServiceProxy;
|
||||
import com.android.exchange.adapter.AbstractSyncAdapter;
|
||||
import com.android.exchange.adapter.AccountSyncAdapter;
|
||||
import com.android.exchange.adapter.CalendarSyncAdapter;
|
||||
import com.android.exchange.adapter.ContactsSyncAdapter;
|
||||
import com.android.exchange.adapter.EmailSyncAdapter;
|
||||
import com.android.exchange.adapter.FolderSyncParser;
|
||||
|
@ -1306,7 +1307,7 @@ public class EasSyncService extends AbstractSyncService {
|
|||
return pp.getSyncStatus();
|
||||
}
|
||||
|
||||
private String getFilterType() {
|
||||
private String getEmailFilter() {
|
||||
String filter = Eas.FILTER_1_WEEK;
|
||||
switch (mAccount.mSyncLookback) {
|
||||
case com.android.email.Account.SYNC_WINDOW_1_DAY: {
|
||||
|
@ -1403,8 +1404,11 @@ public class EasSyncService extends AbstractSyncService {
|
|||
// Handle options
|
||||
s.start(Tags.SYNC_OPTIONS);
|
||||
// Set the lookback appropriately (EAS calls this a "filter") for all but Contacts
|
||||
if (!className.equals("Contacts")) {
|
||||
s.data(Tags.SYNC_FILTER_TYPE, getFilterType());
|
||||
if (className.equals("Email")) {
|
||||
s.data(Tags.SYNC_FILTER_TYPE, getEmailFilter());
|
||||
} else if (className.equals("Calendar")) {
|
||||
// TODO Force one month for calendar until we can set this!
|
||||
s.data(Tags.SYNC_FILTER_TYPE, Eas.FILTER_1_MONTH);
|
||||
}
|
||||
// Set the truncation amount for all classes
|
||||
if (mProtocolVersionDouble >= 12.0) {
|
||||
|
@ -1495,6 +1499,8 @@ public class EasSyncService extends AbstractSyncService {
|
|||
AbstractSyncAdapter target;
|
||||
if (mMailbox.mType == Mailbox.TYPE_CONTACTS) {
|
||||
target = new ContactsSyncAdapter(mMailbox, this);
|
||||
} else if (mMailbox.mType == Mailbox.TYPE_CALENDAR) {
|
||||
target = new CalendarSyncAdapter(mMailbox, this);
|
||||
} else {
|
||||
target = new EmailSyncAdapter(mMailbox, this);
|
||||
}
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -33,7 +33,10 @@ import android.content.ContentUris;
|
|||
import android.content.ContentValues;
|
||||
import android.content.OperationApplicationException;
|
||||
import android.database.Cursor;
|
||||
import android.net.Uri;
|
||||
import android.os.RemoteException;
|
||||
import android.provider.Calendar.Calendars;
|
||||
import android.text.format.Time;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
|
@ -239,7 +242,28 @@ public class FolderSyncParser extends AbstractSyncParser {
|
|||
break;
|
||||
case CALENDAR_TYPE:
|
||||
m.mType = Mailbox.TYPE_CALENDAR;
|
||||
// For now, no sync, since it's not yet implemented
|
||||
m.mSyncInterval = mAccount.mSyncInterval;
|
||||
|
||||
// Create a Calendar object
|
||||
ContentValues cv = new ContentValues();
|
||||
// TODO How will this change if the user changes his account display name?
|
||||
cv.put(Calendars.DISPLAY_NAME, mAccount.mDisplayName);
|
||||
cv.put(Calendars._SYNC_ACCOUNT, mAccount.mEmailAddress);
|
||||
cv.put(Calendars._SYNC_ACCOUNT_TYPE, Eas.ACCOUNT_MANAGER_TYPE);
|
||||
cv.put(Calendars.SYNC_EVENTS, 1);
|
||||
cv.put(Calendars.SELECTED, 1);
|
||||
cv.put(Calendars.HIDDEN, 0);
|
||||
// TODO Find out how to set color!!
|
||||
cv.put(Calendars.COLOR, -14069085 /* blue */);
|
||||
cv.put(Calendars.TIMEZONE, Time.getCurrentTimezone());
|
||||
cv.put(Calendars.ACCESS_LEVEL, Calendars.OWNER_ACCESS);
|
||||
cv.put(Calendars.OWNER_ACCOUNT, mAccount.mEmailAddress);
|
||||
|
||||
Uri uri = mService.mContentResolver.insert(Calendars.CONTENT_URI, cv);
|
||||
// We save the id of the calendar into mSyncStatus
|
||||
if (uri != null) {
|
||||
m.mSyncStatus = uri.getPathSegments().get(1);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
|
|
|
@ -38,8 +38,6 @@ import java.util.ArrayList;
|
|||
*/
|
||||
public abstract class Parser {
|
||||
|
||||
private static final String TAG = "EasParser";
|
||||
|
||||
// The following constants are Wbxml standard
|
||||
public static final int START_DOCUMENT = 0;
|
||||
public static final int DONE = 1;
|
||||
|
@ -52,6 +50,7 @@ public abstract class Parser {
|
|||
private static final int EOF_BYTE = -1;
|
||||
private boolean logging = false;
|
||||
private boolean capture = false;
|
||||
private String logTag = "EAS Parser";
|
||||
|
||||
private ArrayList<Integer> captureArray;
|
||||
|
||||
|
@ -157,6 +156,16 @@ public abstract class Parser {
|
|||
logging = val;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the tag used for logging. When debugging is on, every token is logged (Log.v) to
|
||||
* the console.
|
||||
*
|
||||
* @param val the logging tag
|
||||
*/
|
||||
public void setLoggingTag(String val) {
|
||||
logTag = val;
|
||||
}
|
||||
|
||||
/**
|
||||
* Turns on data capture; this is used to create test streams that represent "live" data and
|
||||
* can be used against the various parsers.
|
||||
|
@ -313,9 +322,9 @@ public abstract class Parser {
|
|||
if (cr > 0) {
|
||||
str = str.substring(0, cr);
|
||||
}
|
||||
Log.v(TAG, str);
|
||||
Log.v(logTag, str);
|
||||
if (Eas.FILE_LOG) {
|
||||
FileLogger.log(TAG, str);
|
||||
FileLogger.log(logTag, str);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,429 @@
|
|||
/*
|
||||
* Copyright (C) 2010 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.exchange.utility;
|
||||
|
||||
import com.android.exchange.Eas;
|
||||
|
||||
import org.bouncycastle.util.encoders.Base64;
|
||||
|
||||
import android.util.Log;
|
||||
|
||||
import java.util.Calendar;
|
||||
import java.util.Date;
|
||||
import java.util.GregorianCalendar;
|
||||
import java.util.HashMap;
|
||||
import java.util.TimeZone;
|
||||
|
||||
public class CalendarUtilities {
|
||||
// NOTE: Most definitions in this class are have package visibility for testing purposes
|
||||
private static final String TAG = "CalendarUtility";
|
||||
|
||||
// Time related convenience constants, in milliseconds
|
||||
static final int SECONDS = 1000;
|
||||
static final int MINUTES = SECONDS*60;
|
||||
static final int HOURS = MINUTES*60;
|
||||
|
||||
// NOTE All Microsoft data structures are little endian
|
||||
|
||||
// The following constants relate to standard Microsoft data sizes
|
||||
// For documentation, see http://msdn.microsoft.com/en-us/library/aa505945.aspx
|
||||
static final int MSFT_LONG_SIZE = 4;
|
||||
static final int MSFT_WCHAR_SIZE = 2;
|
||||
static final int MSFT_WORD_SIZE = 2;
|
||||
|
||||
// The following constants relate to Microsoft's SYSTEMTIME structure
|
||||
// For documentation, see: http://msdn.microsoft.com/en-us/library/ms724950(VS.85).aspx?ppud=4
|
||||
|
||||
static final int MSFT_SYSTEMTIME_YEAR = 0 * MSFT_WORD_SIZE;
|
||||
static final int MSFT_SYSTEMTIME_MONTH = 1 * MSFT_WORD_SIZE;
|
||||
static final int MSFT_SYSTEMTIME_DAY_OF_WEEK = 2 * MSFT_WORD_SIZE;
|
||||
static final int MSFT_SYSTEMTIME_DAY = 3 * MSFT_WORD_SIZE;
|
||||
static final int MSFT_SYSTEMTIME_HOUR = 4 * MSFT_WORD_SIZE;
|
||||
static final int MSFT_SYSTEMTIME_MINUTE = 5 * MSFT_WORD_SIZE;
|
||||
//static final int MSFT_SYSTEMTIME_SECONDS = 6 * MSFT_WORD_SIZE;
|
||||
//static final int MSFT_SYSTEMTIME_MILLIS = 7 * MSFT_WORD_SIZE;
|
||||
static final int MSFT_SYSTEMTIME_SIZE = 8*MSFT_WORD_SIZE;
|
||||
|
||||
// The following constants relate to Microsoft's TIME_ZONE_INFORMATION structure
|
||||
// For documentation, see http://msdn.microsoft.com/en-us/library/ms725481(VS.85).aspx
|
||||
static final int MSFT_TIME_ZONE_BIAS_OFFSET = 0;
|
||||
static final int MSFT_TIME_ZONE_STANDARD_NAME_OFFSET =
|
||||
MSFT_TIME_ZONE_BIAS_OFFSET + MSFT_LONG_SIZE;
|
||||
static final int MSFT_TIME_ZONE_STANDARD_DATE_OFFSET =
|
||||
MSFT_TIME_ZONE_STANDARD_NAME_OFFSET + (MSFT_WCHAR_SIZE*32);
|
||||
static final int MSFT_TIME_ZONE_STANDARD_BIAS_OFFSET =
|
||||
MSFT_TIME_ZONE_STANDARD_DATE_OFFSET + MSFT_SYSTEMTIME_SIZE;
|
||||
static final int MSFT_TIME_ZONE_DAYLIGHT_NAME_OFFSET =
|
||||
MSFT_TIME_ZONE_STANDARD_BIAS_OFFSET + MSFT_LONG_SIZE;
|
||||
static final int MSFT_TIME_ZONE_DAYLIGHT_DATE_OFFSET =
|
||||
MSFT_TIME_ZONE_DAYLIGHT_NAME_OFFSET + (MSFT_WCHAR_SIZE*32);
|
||||
static final int MSFT_TIME_ZONE_DAYLIGHT_BIAS_OFFSET =
|
||||
MSFT_TIME_ZONE_DAYLIGHT_DATE_OFFSET + MSFT_SYSTEMTIME_SIZE;
|
||||
static final int MSFT_TIME_ZONE_SIZE =
|
||||
MSFT_TIME_ZONE_DAYLIGHT_BIAS_OFFSET + MSFT_LONG_SIZE;
|
||||
|
||||
// TimeZone cache; we parse/decode as little as possible, because the process is quite slow
|
||||
private static HashMap<String, TimeZone> sTimeZoneCache = new HashMap<String, TimeZone>();
|
||||
|
||||
// There is no type 4 (thus, the "")
|
||||
static final String[] sTypeToFreq =
|
||||
new String[] {"DAILY", "WEEKLY", "MONTHLY", "MONTHLY", "", "YEARLY", "YEARLY"};
|
||||
|
||||
static final String[] sDayTokens =
|
||||
new String[] {"SU", "MO", "TU", "WE", "TH", "FR", "SA"};
|
||||
|
||||
static final String[] sTwoCharacterNumbers =
|
||||
new String[] {"00", "01", "02", "03", "04", "05", "06", "07", "08", "09", "10", "11", "12"};
|
||||
|
||||
// Return a 4-byte long from a byte array (little endian)
|
||||
static int getLong(byte[] bytes, int offset) {
|
||||
return (bytes[offset++] & 0xFF) | ((bytes[offset++] & 0xFF) << 8) |
|
||||
((bytes[offset++] & 0xFF) << 16) | ((bytes[offset] & 0xFF) << 24);
|
||||
}
|
||||
|
||||
// Put a 4-byte long into a byte array (little endian)
|
||||
static void setLong(byte[] bytes, int offset, int value) {
|
||||
bytes[offset++] = (byte) (value & 0xFF);
|
||||
bytes[offset++] = (byte) ((value >> 8) & 0xFF);
|
||||
bytes[offset++] = (byte) ((value >> 16) & 0xFF);
|
||||
bytes[offset] = (byte) ((value >> 24) & 0xFF);
|
||||
}
|
||||
|
||||
// Return a 2-byte word from a byte array (little endian)
|
||||
static int getWord(byte[] bytes, int offset) {
|
||||
return (bytes[offset++] & 0xFF) | ((bytes[offset] & 0xFF) << 8);
|
||||
}
|
||||
|
||||
// Put a 2-byte word into a byte array (little endian)
|
||||
static void setWord(byte[] bytes, int offset, int value) {
|
||||
bytes[offset++] = (byte) (value & 0xFF);
|
||||
bytes[offset] = (byte) ((value >> 8) & 0xFF);
|
||||
}
|
||||
|
||||
// Internal structure for storing a time zone date from a SYSTEMTIME structure
|
||||
// This date represents either the start or the end time for DST
|
||||
static class TimeZoneDate {
|
||||
String year;
|
||||
int month;
|
||||
int dayOfWeek;
|
||||
int day;
|
||||
int time;
|
||||
int hour;
|
||||
int minute;
|
||||
}
|
||||
|
||||
// Build a TimeZoneDate structure from a SYSTEMTIME within a byte array at a given offset
|
||||
static TimeZoneDate getTimeZoneDateFromSystemTime(byte[] bytes, int offset) {
|
||||
TimeZoneDate tzd = new TimeZoneDate();
|
||||
|
||||
// MSFT year is an int; TimeZone is a String
|
||||
int num = getWord(bytes, offset + MSFT_SYSTEMTIME_YEAR);
|
||||
tzd.year = Integer.toString(num);
|
||||
|
||||
// MSFT month = 0 means no daylight time
|
||||
// MSFT months are 1 based; TimeZone is 0 based
|
||||
num = getWord(bytes, offset + MSFT_SYSTEMTIME_MONTH);
|
||||
if (num == 0) {
|
||||
return null;
|
||||
} else {
|
||||
tzd.month = num -1;
|
||||
}
|
||||
|
||||
// MSFT day of week starts w/ Sunday = 0; TimeZone starts w/ Sunday = 1
|
||||
tzd.dayOfWeek = getWord(bytes, offset + MSFT_SYSTEMTIME_DAY_OF_WEEK) + 1;
|
||||
|
||||
// Get the "day" in TimeZone format
|
||||
num = getWord(bytes, offset + MSFT_SYSTEMTIME_DAY);
|
||||
// 5 means "last" in MSFT land; for TimeZone, it's -1
|
||||
if (num == 5) {
|
||||
tzd.day = -1;
|
||||
} else {
|
||||
tzd.day = num;
|
||||
}
|
||||
|
||||
// Turn hours/minutes into ms from midnight (per TimeZone)
|
||||
int hour = getWord(bytes, offset + MSFT_SYSTEMTIME_HOUR);
|
||||
tzd.hour = hour;
|
||||
int minute = getWord(bytes, offset + MSFT_SYSTEMTIME_MINUTE);
|
||||
tzd.minute = minute;
|
||||
tzd.time = (hour*HOURS) + (minute*MINUTES);
|
||||
|
||||
return tzd;
|
||||
}
|
||||
|
||||
// Return a String from within a byte array at the given offset with max characters
|
||||
// Unused for now, but might be helpful for debugging
|
||||
// String getString(byte[] bytes, int offset, int max) {
|
||||
// StringBuilder sb = new StringBuilder();
|
||||
// while (max-- > 0) {
|
||||
// int b = bytes[offset];
|
||||
// if (b == 0) break;
|
||||
// sb.append((char)b);
|
||||
// offset += 2;
|
||||
// }
|
||||
// return sb.toString();
|
||||
// }
|
||||
|
||||
/**
|
||||
* Build a GregorianCalendar, based on a time zone and TimeZoneDate.
|
||||
* @param timeZone the time zone we're checking
|
||||
* @param tzd the TimeZoneDate we're interested in
|
||||
* @return a GregorianCalendar with the given time zone and date
|
||||
*/
|
||||
static GregorianCalendar getCheckCalendar(TimeZone timeZone, TimeZoneDate tzd) {
|
||||
GregorianCalendar testCalendar = new GregorianCalendar(timeZone);
|
||||
testCalendar.set(GregorianCalendar.YEAR, 2009);
|
||||
testCalendar.set(GregorianCalendar.MONTH, tzd.month);
|
||||
testCalendar.set(GregorianCalendar.DAY_OF_WEEK, tzd.dayOfWeek);
|
||||
testCalendar.set(GregorianCalendar.DAY_OF_WEEK_IN_MONTH, tzd.day);
|
||||
testCalendar.set(GregorianCalendar.HOUR_OF_DAY, tzd.hour);
|
||||
testCalendar.set(GregorianCalendar.MINUTE, tzd.minute);
|
||||
return testCalendar;
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a String as directly read from EAS, returns a TimeZone corresponding to that String
|
||||
* @param timeZoneString the String read from the server
|
||||
* @return the TimeZone, or TimeZone.getDefault() if not found
|
||||
*/
|
||||
static public TimeZone parseTimeZone(String timeZoneString) {
|
||||
// If we have this time zone cached, use that value and return
|
||||
TimeZone timeZone = sTimeZoneCache.get(timeZoneString);
|
||||
if (timeZone != null) {
|
||||
if (Eas.USER_LOG) {
|
||||
Log.d(TAG, "TimeZone " + timeZone.getID() + " in cache: " + timeZone.getDisplayName());
|
||||
}
|
||||
return timeZone;
|
||||
}
|
||||
|
||||
// First, we need to decode the base64 string
|
||||
byte[] timeZoneBytes = Base64.decode(timeZoneString);
|
||||
|
||||
// Then, we get the bias (similar to a rawOffset); for TimeZone, we need ms
|
||||
// but EAS gives us minutes, so do the conversion. Note that EAS is the bias that's added
|
||||
// to the time zone to reach UTC; our library uses the time from UTC to our time zone, so
|
||||
// we need to change the sign
|
||||
int bias = -1 * getLong(timeZoneBytes, MSFT_TIME_ZONE_BIAS_OFFSET) * MINUTES;
|
||||
|
||||
// Get all of the time zones with the bias as a rawOffset; if there aren't any, we return
|
||||
// the default time zone
|
||||
String[] zoneIds = TimeZone.getAvailableIDs(bias);
|
||||
if (zoneIds.length > 0) {
|
||||
// Try to find an existing TimeZone from the data provided by EAS
|
||||
// We start by pulling out the date that standard time begins
|
||||
TimeZoneDate dstEnd =
|
||||
getTimeZoneDateFromSystemTime(timeZoneBytes, MSFT_TIME_ZONE_STANDARD_DATE_OFFSET);
|
||||
if (dstEnd == null) {
|
||||
// In this case, there is no daylight savings time, so the only interesting data
|
||||
// is the offset, and we know that all of the zoneId's match; we'll take the first
|
||||
timeZone = TimeZone.getTimeZone(zoneIds[0]);
|
||||
String dn = timeZone.getDisplayName();
|
||||
sTimeZoneCache.put(timeZoneString, timeZone);
|
||||
if (Eas.USER_LOG) {
|
||||
Log.d(TAG, "TimeZone without DST found by offset: " + dn);
|
||||
}
|
||||
return timeZone;
|
||||
} else {
|
||||
TimeZoneDate dstStart = getTimeZoneDateFromSystemTime(timeZoneBytes,
|
||||
MSFT_TIME_ZONE_DAYLIGHT_DATE_OFFSET);
|
||||
// See comment above for bias...
|
||||
long dstSavings =
|
||||
-1 * getLong(timeZoneBytes, MSFT_TIME_ZONE_DAYLIGHT_BIAS_OFFSET) * 60*SECONDS;
|
||||
|
||||
// We'll go through each time zone to find one with the same DST transitions and
|
||||
// savings length
|
||||
for (String zoneId: zoneIds) {
|
||||
// Get the TimeZone using the zoneId
|
||||
timeZone = TimeZone.getTimeZone(zoneId);
|
||||
|
||||
// Our strategy here is to check just before and just after the transitions
|
||||
// and see whether the check for daylight time matches the expectation
|
||||
// If both transitions match, then we have a match for the offset and start/end
|
||||
// of dst. That's the best we can do for now, since there's no other info
|
||||
// provided by EAS (i.e. we can't get dynamic transitions, etc.)
|
||||
|
||||
// Check start DST transition
|
||||
GregorianCalendar testCalendar = getCheckCalendar(timeZone, dstStart);
|
||||
testCalendar.add(GregorianCalendar.MINUTE, -1);
|
||||
Date before = testCalendar.getTime();
|
||||
testCalendar.add(GregorianCalendar.MINUTE, 2);
|
||||
Date after = testCalendar.getTime();
|
||||
if (timeZone.inDaylightTime(before)) continue;
|
||||
if (!timeZone.inDaylightTime(after)) continue;
|
||||
|
||||
// Check end DST transition
|
||||
testCalendar = getCheckCalendar(timeZone, dstEnd);
|
||||
testCalendar.add(GregorianCalendar.HOUR, -2);
|
||||
before = testCalendar.getTime();
|
||||
testCalendar.add(GregorianCalendar.HOUR, 2);
|
||||
after = testCalendar.getTime();
|
||||
if (!timeZone.inDaylightTime(before)) continue;
|
||||
if (timeZone.inDaylightTime(after)) continue;
|
||||
|
||||
// Check that the savings are the same
|
||||
if (dstSavings != timeZone.getDSTSavings()) continue;
|
||||
|
||||
// If we're here, it's the right time zone, modulo dynamic DST
|
||||
String dn = timeZone.getDisplayName();
|
||||
sTimeZoneCache.put(timeZoneString, timeZone);
|
||||
if (Eas.USER_LOG) {
|
||||
Log.d(TAG, "TimeZone found by rules: " + dn);
|
||||
}
|
||||
return timeZone;
|
||||
}
|
||||
}
|
||||
}
|
||||
// If we don't find a match, we just return the current TimeZone. In theory, this
|
||||
// shouldn't be happening...
|
||||
Log.w(TAG, "TimeZone not found with bias = " + bias + ", using default.");
|
||||
return TimeZone.getDefault();
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a Base64 representation of a MSFT TIME_ZONE_INFORMATION structure from a TimeZone
|
||||
* ID that might be found in an Event. For now, we'll just use the standard bias, and we'll
|
||||
* tackle DST later
|
||||
* @param name the name of the TimeZone
|
||||
* @return the Base64 String representing a Microsoft TIME_ZONE_INFORMATION element
|
||||
*/
|
||||
static public String timeZoneToTZIString(String name) {
|
||||
// TODO Handle DST (ugh)
|
||||
TimeZone tz = TimeZone.getTimeZone(name);
|
||||
byte[] tziBytes = new byte[MSFT_TIME_ZONE_SIZE];
|
||||
|
||||
int standardBias = - tz.getRawOffset();
|
||||
standardBias /= 60*SECONDS;
|
||||
setLong(tziBytes, MSFT_TIME_ZONE_BIAS_OFFSET, standardBias);
|
||||
|
||||
byte[] tziEncodedBytes = Base64.encode(tziBytes);
|
||||
return new String(tziEncodedBytes);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a time in milliseconds from a date string that represents a date/time in GMT
|
||||
* @param DateTime string from Exchange server
|
||||
* @return the time in milliseconds (since Jan 1, 1970)
|
||||
*/
|
||||
static public long parseDateTime(String date) {
|
||||
// Format for calendar date strings is 20090211T180303Z
|
||||
GregorianCalendar cal = new GregorianCalendar(Integer.parseInt(date.substring(0, 4)),
|
||||
Integer.parseInt(date.substring(4, 6)) - 1, Integer.parseInt(date.substring(6, 8)),
|
||||
Integer.parseInt(date.substring(9, 11)), Integer.parseInt(date.substring(11, 13)),
|
||||
Integer.parseInt(date.substring(13, 15)));
|
||||
cal.setTimeZone(TimeZone.getTimeZone("GMT"));
|
||||
return cal.getTimeInMillis();
|
||||
}
|
||||
|
||||
static String formatTwo(int num) {
|
||||
if (num <= 12) {
|
||||
return sTwoCharacterNumbers[num];
|
||||
} else
|
||||
return Integer.toString(num);
|
||||
}
|
||||
|
||||
static public String millisToEasDateTime(long millis) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
GregorianCalendar cal = new GregorianCalendar(TimeZone.getTimeZone("GMT"));
|
||||
cal.setTimeInMillis(millis);
|
||||
sb.append(cal.get(Calendar.YEAR));
|
||||
sb.append(formatTwo(cal.get(Calendar.MONTH) + 1));
|
||||
sb.append(formatTwo(cal.get(Calendar.DAY_OF_MONTH)));
|
||||
sb.append('T');
|
||||
sb.append(formatTwo(cal.get(Calendar.HOUR_OF_DAY)));
|
||||
sb.append(formatTwo(cal.get(Calendar.MINUTE)));
|
||||
sb.append(formatTwo(cal.get(Calendar.SECOND)));
|
||||
sb.append('Z');
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
static void addByDay(StringBuilder rrule, int dow, int wom) {
|
||||
rrule.append(";BYDAY=");
|
||||
boolean addComma = false;
|
||||
for (int i = 0; i < 7; i++) {
|
||||
if ((dow & 1) == 1) {
|
||||
if (addComma) {
|
||||
rrule.append(',');
|
||||
}
|
||||
if (wom > 0) {
|
||||
// 5 = last week -> -1
|
||||
// So -1SU = last sunday
|
||||
rrule.append(wom == 5 ? -1 : wom);
|
||||
}
|
||||
rrule.append(sDayTokens[i]);
|
||||
addComma = true;
|
||||
}
|
||||
dow >>= 1;
|
||||
}
|
||||
}
|
||||
|
||||
static void addByMonthDay(StringBuilder rrule, int dom) {
|
||||
// 127 means last day of the month
|
||||
if (dom == 127) {
|
||||
dom = -1;
|
||||
}
|
||||
rrule.append(";BYMONTHDAY=" + dom);
|
||||
}
|
||||
|
||||
static public String rruleFromRecurrence(int type, int occurrences, int interval, int dow,
|
||||
int dom, int wom, int moy, String until) {
|
||||
StringBuilder rrule = new StringBuilder("FREQ=" + sTypeToFreq[type]);
|
||||
|
||||
// INTERVAL and COUNT
|
||||
if (interval > 0) {
|
||||
rrule.append(";INTERVAL=" + interval);
|
||||
}
|
||||
if (occurrences > 0) {
|
||||
rrule.append(";COUNT=" + occurrences);
|
||||
}
|
||||
|
||||
// Days, weeks, months, etc.
|
||||
switch(type) {
|
||||
case 0: // DAILY
|
||||
case 1: // WEEKLY
|
||||
if (dow > 0) addByDay(rrule, dow, -1);
|
||||
break;
|
||||
case 2: // MONTHLY
|
||||
if (dom > 0) addByMonthDay(rrule, dom);
|
||||
break;
|
||||
case 3: // MONTHLY (on the nth day)
|
||||
if (dow > 0) addByDay(rrule, dow, wom);
|
||||
break;
|
||||
case 5: // YEARLY
|
||||
if (dom > 0) addByMonthDay(rrule, dom);
|
||||
if (moy > 0) {
|
||||
// TODO MAKE SURE WE'RE 1 BASED
|
||||
rrule.append(";BYMONTH=" + moy);
|
||||
}
|
||||
break;
|
||||
case 6: // YEARLY (on the nth day)
|
||||
if (dow > 0) addByDay(rrule, dow, wom);
|
||||
if (moy > 0) addByMonthDay(rrule, dow);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
// UNTIL comes last
|
||||
// TODO Add UNTIL code
|
||||
if (until != null) {
|
||||
// *** until probably needs reformatting
|
||||
//rrule.append(";UNTIL=" + until);
|
||||
}
|
||||
|
||||
return rrule.toString();
|
||||
}
|
||||
}
|
|
@ -73,6 +73,4 @@ public class EasSyncServiceTests extends AndroidTestCase {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
|
@ -0,0 +1,24 @@
|
|||
/*
|
||||
* Copyright (C) 2010 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.exchange.adapter;
|
||||
|
||||
public class CalendarSyncAdapterTests extends SyncAdapterTestCase {
|
||||
|
||||
public CalendarSyncAdapterTests() {
|
||||
super();
|
||||
}
|
||||
}
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright (C) 2009 The Android Open Source Project
|
||||
* Copyright (C) 2010 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.
|
||||
|
@ -32,16 +32,14 @@ import android.content.ContentResolver;
|
|||
import android.content.ContentUris;
|
||||
import android.content.ContentValues;
|
||||
import android.content.Context;
|
||||
import android.test.ProviderTestCase2;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.ArrayList;
|
||||
import java.util.GregorianCalendar;
|
||||
import java.util.TimeZone;
|
||||
|
||||
public class EmailSyncAdapterTests extends ProviderTestCase2<EmailProvider> {
|
||||
public class EmailSyncAdapterTests extends SyncAdapterTestCase {
|
||||
|
||||
EmailProvider mProvider;
|
||||
Context mMockContext;
|
||||
|
@ -52,54 +50,7 @@ public class EmailSyncAdapterTests extends ProviderTestCase2<EmailProvider> {
|
|||
EasEmailSyncParser mSyncParser;
|
||||
|
||||
public EmailSyncAdapterTests() {
|
||||
super(EmailProvider.class, EmailProvider.EMAIL_AUTHORITY);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setUp() throws Exception {
|
||||
super.setUp();
|
||||
mMockContext = getMockContext();
|
||||
mMockResolver = mMockContext.getContentResolver();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void tearDown() throws Exception {
|
||||
super.tearDown();
|
||||
}
|
||||
|
||||
/**
|
||||
* Create and return a short, simple InputStream that has at least four bytes, which is all
|
||||
* that's required to initialize an EasParser (the parent class of EasEmailSyncParser)
|
||||
* @return the InputStream
|
||||
*/
|
||||
public InputStream getTestInputStream() {
|
||||
return new ByteArrayInputStream(new byte[] {0, 0, 0, 0, 0});
|
||||
}
|
||||
|
||||
EasSyncService getTestService() {
|
||||
Account account = new Account();
|
||||
account.mId = -1;
|
||||
Mailbox mailbox = new Mailbox();
|
||||
mailbox.mId = -1;
|
||||
EasSyncService service = new EasSyncService();
|
||||
service.mContext = mMockContext;
|
||||
service.mMailbox = mailbox;
|
||||
service.mAccount = account;
|
||||
return service;
|
||||
}
|
||||
|
||||
EasSyncService getTestService(Account account, Mailbox mailbox) {
|
||||
EasSyncService service = new EasSyncService();
|
||||
service.mContext = mMockContext;
|
||||
service.mMailbox = mailbox;
|
||||
service.mAccount = account;
|
||||
return service;
|
||||
}
|
||||
|
||||
EmailSyncAdapter getTestSyncAdapter() {
|
||||
EasSyncService service = getTestService();
|
||||
EmailSyncAdapter adapter = new EmailSyncAdapter(service.mMailbox, service);
|
||||
return adapter;
|
||||
super();
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -0,0 +1,93 @@
|
|||
/*
|
||||
* Copyright (C) 2010 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.exchange.adapter;
|
||||
|
||||
import com.android.email.provider.EmailProvider;
|
||||
import com.android.email.provider.EmailContent.Account;
|
||||
import com.android.email.provider.EmailContent.Mailbox;
|
||||
import com.android.exchange.EasSyncService;
|
||||
import com.android.exchange.adapter.EmailSyncAdapter.EasEmailSyncParser;
|
||||
|
||||
import android.content.ContentResolver;
|
||||
import android.content.Context;
|
||||
import android.test.ProviderTestCase2;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.InputStream;
|
||||
|
||||
public class SyncAdapterTestCase extends ProviderTestCase2<EmailProvider> {
|
||||
|
||||
EmailProvider mProvider;
|
||||
Context mMockContext;
|
||||
ContentResolver mMockResolver;
|
||||
Mailbox mMailbox;
|
||||
Account mAccount;
|
||||
EmailSyncAdapter mSyncAdapter;
|
||||
EasEmailSyncParser mSyncParser;
|
||||
|
||||
public SyncAdapterTestCase() {
|
||||
super(EmailProvider.class, EmailProvider.EMAIL_AUTHORITY);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setUp() throws Exception {
|
||||
super.setUp();
|
||||
mMockContext = getMockContext();
|
||||
mMockResolver = mMockContext.getContentResolver();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void tearDown() throws Exception {
|
||||
super.tearDown();
|
||||
}
|
||||
|
||||
/**
|
||||
* Create and return a short, simple InputStream that has at least four bytes, which is all
|
||||
* that's required to initialize an EasParser (the parent class of EasEmailSyncParser)
|
||||
* @return the InputStream
|
||||
*/
|
||||
public InputStream getTestInputStream() {
|
||||
return new ByteArrayInputStream(new byte[] {0, 0, 0, 0, 0});
|
||||
}
|
||||
|
||||
EasSyncService getTestService() {
|
||||
Account account = new Account();
|
||||
account.mId = -1;
|
||||
Mailbox mailbox = new Mailbox();
|
||||
mailbox.mId = -1;
|
||||
EasSyncService service = new EasSyncService();
|
||||
service.mContext = mMockContext;
|
||||
service.mMailbox = mailbox;
|
||||
service.mAccount = account;
|
||||
return service;
|
||||
}
|
||||
|
||||
EasSyncService getTestService(Account account, Mailbox mailbox) {
|
||||
EasSyncService service = new EasSyncService();
|
||||
service.mContext = mMockContext;
|
||||
service.mMailbox = mailbox;
|
||||
service.mAccount = account;
|
||||
return service;
|
||||
}
|
||||
|
||||
EmailSyncAdapter getTestSyncAdapter() {
|
||||
EasSyncService service = getTestService();
|
||||
EmailSyncAdapter adapter = new EmailSyncAdapter(service.mMailbox, service);
|
||||
return adapter;
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,85 @@
|
|||
/*
|
||||
* Copyright (C) 2010 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.exchange.utility;
|
||||
|
||||
import android.test.AndroidTestCase;
|
||||
|
||||
import java.util.TimeZone;
|
||||
|
||||
public class CalendarUtilitiesTests extends AndroidTestCase {
|
||||
|
||||
// Some prebuilt time zones, Base64 encoded (as they arrive from EAS)
|
||||
private static final String ISRAEL_STANDARD_TIME =
|
||||
"iP///ygARwBNAFQAKwAwADIAOgAwADAAKQAgAEoAZQByAHUAcwBhAGwAZQBtAAAAAAAAAAAAAAAAAAAAAAAA" +
|
||||
"AAAAAAAAAAkAAAAFAAIAAAAAAAAAAAAAACgARwBNAFQAKwAwADIAOgAwADAAKQAgAEoAZQByAHUAcwBhAGwA" +
|
||||
"ZQBtAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMABQAFAAIAAAAAAAAAxP///w==";
|
||||
private static final String INDIA_STANDARD_TIME =
|
||||
"tv7//0kAbgBkAGkAYQAgAFMAdABhAG4AZABhAHIAZAAgAFQAaQBtAGUAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" +
|
||||
"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEkAbgBkAGkAYQAgAEQAYQB5AGwAaQBnAGgAdAAgAFQAaQBtAGUA" +
|
||||
"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==";
|
||||
private static final String PACIFIC_STANDARD_TIME =
|
||||
"4AEAAFAAYQBjAGkAZgBpAGMAIABTAHQAYQBuAGQAYQByAGQAIABUAGkAbQBlAAAAAAAAAAAAAAAAAAAAAAAA" +
|
||||
"AAAAAAAAAAsAAAABAAIAAAAAAAAAAAAAAFAAYQBjAGkAZgBpAGMAIABEAGEAeQBsAGkAZwBoAHQAIABUAGkA" +
|
||||
"bQBlAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMAAAACAAIAAAAAAAAAxP///w==";
|
||||
|
||||
public void testGetSet() {
|
||||
byte[] bytes = new byte[] {0, 1, 2, 3, 4, 5, 6, 7};
|
||||
|
||||
// First, check that getWord/Long are properly little endian
|
||||
assertEquals(0x0100, CalendarUtilities.getWord(bytes, 0));
|
||||
assertEquals(0x03020100, CalendarUtilities.getLong(bytes, 0));
|
||||
assertEquals(0x07060504, CalendarUtilities.getLong(bytes, 4));
|
||||
|
||||
// Set some words and longs
|
||||
CalendarUtilities.setWord(bytes, 0, 0xDEAD);
|
||||
CalendarUtilities.setLong(bytes, 2, 0xBEEFBEEF);
|
||||
CalendarUtilities.setWord(bytes, 6, 0xCEDE);
|
||||
|
||||
// Retrieve them
|
||||
assertEquals(0xDEAD, CalendarUtilities.getWord(bytes, 0));
|
||||
assertEquals(0xBEEFBEEF, CalendarUtilities.getLong(bytes, 2));
|
||||
assertEquals(0xCEDE, CalendarUtilities.getWord(bytes, 6));
|
||||
}
|
||||
|
||||
public void testParseTimeZoneEndToEnd() {
|
||||
TimeZone tz = CalendarUtilities.parseTimeZone(PACIFIC_STANDARD_TIME);
|
||||
assertEquals("Pacific Standard Time", tz.getDisplayName());
|
||||
tz = CalendarUtilities.parseTimeZone(INDIA_STANDARD_TIME);
|
||||
assertEquals("India Standard Time", tz.getDisplayName());
|
||||
tz = CalendarUtilities.parseTimeZone(ISRAEL_STANDARD_TIME);
|
||||
assertEquals("Israel Standard Time", tz.getDisplayName());
|
||||
}
|
||||
|
||||
// TODO In progress
|
||||
// public void testParseTimeZone() {
|
||||
// GregorianCalendar cal = getTestCalendar(parsedTimeZone, dstStart);
|
||||
// cal.add(GregorianCalendar.MINUTE, -1);
|
||||
// Date b = cal.getTime();
|
||||
// cal.add(GregorianCalendar.MINUTE, 2);
|
||||
// Date a = cal.getTime();
|
||||
// if (parsedTimeZone.inDaylightTime(b) || !parsedTimeZone.inDaylightTime(a)) {
|
||||
// userLog("ERROR IN TIME ZONE CONTROL!");
|
||||
// }
|
||||
// cal = getTestCalendar(parsedTimeZone, dstEnd);
|
||||
// cal.add(GregorianCalendar.HOUR, -2);
|
||||
// b = cal.getTime();
|
||||
// cal.add(GregorianCalendar.HOUR, 2);
|
||||
// a = cal.getTime();
|
||||
// if (!parsedTimeZone.inDaylightTime(b)) userLog("ERROR IN TIME ZONE CONTROL");
|
||||
// if (parsedTimeZone.inDaylightTime(a)) userLog("ERROR IN TIME ZONE CONTROL!");
|
||||
// }
|
||||
}
|
Loading…
Reference in New Issue