Initial Imap2 implementation

This CL includes the following:
* New Imap2.apk generation (not included in builds)
* "Push IMAP" option for accounts when Imap2.apk present
* Account creation/setup
* 2-way sync of messages, deletions, flag updates
* Push (messages, flags)
* Folder list hierarchy handling
* Message text (one plain or html part)
* Picker UI for trash folder (placeholder)
* Capabilities handling/UI command

Major Imap2 new features:
* Push
* Multiple folder sync
* Sync window (like EAS)

TODO:
* Picker UI for sent folder
* Upload of sent messages to server
* Search
* Multiple viewable parts
* Probably lots more, incl. unit tests

Change-Id: Ia5d74073d9c307e0bdae72a7f76b27140dde7d14
This commit is contained in:
Marc Blank 2012-06-29 09:42:05 -07:00
parent ab076fa633
commit c6089bc01f
47 changed files with 4651 additions and 248 deletions

View File

@ -198,6 +198,12 @@
<meta-data android:name="android.app.searchable" android:resource="@xml/searchable" />
</activity>
<activity
android:name=".provider.FolderPickerActivity"
android:label="@string/folder_picker_title"
>
</activity>
<!-- Must be exported in order for the AccountManager to launch it -->
<!-- Also available for continuous test systems to force account creation -->
<activity
@ -508,7 +514,20 @@
android:resource="@xml/authenticator_alternate"
/>
</service>
<service
android:name=".service.Imap2AuthenticatorService"
android:exported="true"
android:enabled="true"
>
<intent-filter>
<action
android:name="android.accounts.AccountAuthenticator" />
</intent-filter>
<meta-data
android:name="android.accounts.AccountAuthenticator"
android:resource="@xml/imap2_authenticator"
/>
</service>
<provider
android:name=".provider.AttachmentProvider"
android:authorities="com.android.email.attachmentprovider"

View File

@ -14,10 +14,7 @@
* limitations under the License.
*/
package com.android.email;
import com.android.email.activity.setup.AccountSettingsUtils.Provider;
import com.android.emailcommon.Logging;
package com.android.emailcommon;
import android.content.Context;
import android.content.pm.ApplicationInfo;
@ -25,6 +22,7 @@ import android.content.pm.PackageManager.NameNotFoundException;
import android.os.Bundle;
import android.util.Log;
import java.io.Serializable;
import java.lang.reflect.Method;
/**
@ -193,6 +191,50 @@ public class VendorPolicyLoader {
return result;
}
public static class Provider implements Serializable {
private static final long serialVersionUID = 8511656164616538989L;
public String id;
public String label;
public String domain;
public String incomingUriTemplate;
public String incomingUsernameTemplate;
public String outgoingUriTemplate;
public String outgoingUsernameTemplate;
public String incomingUri;
public String incomingUsername;
public String outgoingUri;
public String outgoingUsername;
public String note;
/**
* Expands templates in all of the provider fields that support them. Currently,
* templates are used in 4 fields -- incoming and outgoing URI and user name.
* @param email user-specified data used to replace template values
*/
public void expandTemplates(String email) {
String[] emailParts = email.split("@");
String user = emailParts[0];
incomingUri = expandTemplate(incomingUriTemplate, email, user);
incomingUsername = expandTemplate(incomingUsernameTemplate, email, user);
outgoingUri = expandTemplate(outgoingUriTemplate, email, user);
outgoingUsername = expandTemplate(outgoingUsernameTemplate, email, user);
}
/**
* Replaces all parameterized values in the given template. The values replaced are
* $domain, $user and $email.
*/
private String expandTemplate(String template, String email, String user) {
String returnString = template;
returnString = returnString.replaceAll("\\$email", email);
returnString = returnString.replaceAll("\\$user", user);
returnString = returnString.replaceAll("\\$domain", domain);
return returnString;
}
}
/**
* Returns provider setup information for a given email address
*

View File

@ -71,6 +71,9 @@ public abstract class EmailContent {
public static final Uri CONTENT_NOTIFIER_URI = Uri.parse("content://" + NOTIFIER_AUTHORITY);
public static final Uri PICK_TRASH_FOLDER_URI =
Uri.parse("content://" + EmailContent.AUTHORITY + "/pickTrashFolder");
public static final Uri MAILBOX_NOTIFICATION_URI =
Uri.parse("content://" + EmailContent.AUTHORITY + "/mailboxNotification");
public static final String[] NOTIFICATION_PROJECTION =
@ -543,6 +546,8 @@ public abstract class EmailContent {
public static final String PROTOCOL_SEARCH_INFO = "protocolSearchInfo";
// Simple thread topic
public static final String THREAD_TOPIC = "threadTopic";
// For sync adapter use
public static final String SYNC_DATA = "syncData";
}
public static final class Message extends EmailContent implements SyncColumns, MessageColumns {
@ -551,11 +556,12 @@ public abstract class EmailContent {
public static final String DELETED_TABLE_NAME = "Message_Deletes";
// To refer to a specific message, use ContentUris.withAppendedId(CONTENT_URI, id)
@SuppressWarnings("hiding")
public static final Uri CONTENT_URI = Uri.parse(EmailContent.CONTENT_URI + "/message");
public static final Uri CONTENT_URI_LIMIT_1 = uriWithLimit(CONTENT_URI, 1);
public static final Uri SYNCED_CONTENT_URI =
Uri.parse(EmailContent.CONTENT_URI + "/syncedMessage");
Uri.parse(EmailContent.CONTENT_URI + "/syncedMessage");
public static final Uri SYNCED_SELECTION_CONTENT_URI =
Uri.parse(EmailContent.CONTENT_URI + "/syncedMessageSelection");
public static final Uri DELETED_CONTENT_URI =
Uri.parse(EmailContent.CONTENT_URI + "/deletedMessage");
public static final Uri UPDATED_CONTENT_URI =
@ -589,6 +595,7 @@ public abstract class EmailContent {
public static final int CONTENT_SNIPPET_COLUMN = 21;
public static final int CONTENT_PROTOCOL_SEARCH_INFO_COLUMN = 22;
public static final int CONTENT_THREAD_TOPIC_COLUMN = 23;
public static final int CONTENT_SYNC_DATA_COLUMN = 24;
public static final String[] CONTENT_PROJECTION = new String[] {
RECORD_ID,
@ -603,7 +610,7 @@ public abstract class EmailContent {
MessageColumns.BCC_LIST, MessageColumns.REPLY_TO_LIST,
SyncColumns.SERVER_TIMESTAMP, MessageColumns.MEETING_INFO,
MessageColumns.SNIPPET, MessageColumns.PROTOCOL_SEARCH_INFO,
MessageColumns.THREAD_TOPIC
MessageColumns.THREAD_TOPIC, MessageColumns.SYNC_DATA
};
public static final int LIST_ID_COLUMN = 0;
@ -740,6 +747,8 @@ public abstract class EmailContent {
public String mThreadTopic;
public String mSyncData;
/**
* Base64-encoded representation of the byte array provided by servers for identifying
* messages belonging to the same conversation thread. Currently unsupported and not
@ -832,28 +841,22 @@ public abstract class EmailContent {
values.put(MessageColumns.FLAG_FAVORITE, mFlagFavorite);
values.put(MessageColumns.FLAG_ATTACHMENT, mFlagAttachment);
values.put(MessageColumns.FLAGS, mFlags);
values.put(SyncColumns.SERVER_ID, mServerId);
values.put(SyncColumns.SERVER_TIMESTAMP, mServerTimeStamp);
values.put(MessageColumns.DRAFT_INFO, mDraftInfo);
values.put(MessageColumns.MESSAGE_ID, mMessageId);
values.put(MessageColumns.MAILBOX_KEY, mMailboxKey);
values.put(MessageColumns.ACCOUNT_KEY, mAccountKey);
values.put(MessageColumns.FROM_LIST, mFrom);
values.put(MessageColumns.TO_LIST, mTo);
values.put(MessageColumns.CC_LIST, mCc);
values.put(MessageColumns.BCC_LIST, mBcc);
values.put(MessageColumns.REPLY_TO_LIST, mReplyTo);
values.put(MessageColumns.MEETING_INFO, mMeetingInfo);
values.put(MessageColumns.SNIPPET, mSnippet);
values.put(MessageColumns.PROTOCOL_SEARCH_INFO, mProtocolSearchInfo);
values.put(MessageColumns.THREAD_TOPIC, mThreadTopic);
values.put(MessageColumns.SYNC_DATA, mSyncData);
return values;
}
@ -889,6 +892,7 @@ public abstract class EmailContent {
mSnippet = cursor.getString(CONTENT_SNIPPET_COLUMN);
mProtocolSearchInfo = cursor.getString(CONTENT_PROTOCOL_SEARCH_INFO_COLUMN);
mThreadTopic = cursor.getString(CONTENT_THREAD_TOPIC_COLUMN);
mSyncData = cursor.getString(CONTENT_SYNC_DATA_COLUMN);
}
public boolean update() {
@ -991,22 +995,25 @@ public abstract class EmailContent {
if (mQuotedTextStartPos != 0) {
cv.put(Body.QUOTED_TEXT_START_POS, mQuotedTextStartPos);
}
b = ContentProviderOperation.newInsert(Body.CONTENT_URI);
// Put our message id in the Body
if (!isNew) {
cv.put(Body.MESSAGE_KEY, mId);
}
b.withValues(cv);
// We'll need this if we're new
int messageBackValue = ops.size() - 1;
// If we're new, create a back value entry
if (isNew) {
ContentValues backValues = new ContentValues();
backValues.put(Body.MESSAGE_KEY, messageBackValue);
b.withValueBackReferences(backValues);
// Only create a body if we've got some data
if (!cv.keySet().isEmpty()) {
b = ContentProviderOperation.newInsert(Body.CONTENT_URI);
// Put our message id in the Body
if (!isNew) {
cv.put(Body.MESSAGE_KEY, mId);
}
b.withValues(cv);
// If we're new, create a back value entry
if (isNew) {
ContentValues backValues = new ContentValues();
backValues.put(Body.MESSAGE_KEY, messageBackValue);
b.withValueBackReferences(backValues);
}
// And add the Body operation
ops.add(b.build());
}
// And add the Body operation
ops.add(b.build());
// Create the attaachments, if any
if (mAttachments != null) {

View File

@ -0,0 +1,274 @@
/*
* Copyright (C) 2011 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.emailcommon.provider;
import android.content.ContentResolver;
import android.content.ContentUris;
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.os.Debug;
import android.text.TextUtils;
import android.util.Log;
import com.android.emailcommon.Logging;
import com.android.emailcommon.provider.Account;
import com.android.emailcommon.provider.EmailContent.MailboxColumns;
import com.android.emailcommon.provider.Mailbox;
import java.util.HashMap;
public class MailboxUtilities {
public static final String WHERE_PARENT_KEY_UNINITIALIZED =
"(" + MailboxColumns.PARENT_KEY + " isnull OR " + MailboxColumns.PARENT_KEY + "=" +
Mailbox.PARENT_KEY_UNINITIALIZED + ")";
// The flag we use in Account to indicate a mailbox change in progress
private static final int ACCOUNT_MAILBOX_CHANGE_FLAG = Account.FLAGS_SYNC_ADAPTER;
/**
* Recalculate a mailbox's flags and the parent key of any children
* @param context the caller's context
* @param parentCursor a cursor to a mailbox that requires fixup
*/
public static void setFlagsAndChildrensParentKey(Context context, Cursor parentCursor,
String accountSelector) {
ContentResolver resolver = context.getContentResolver();
String[] selectionArgs = new String[1];
ContentValues parentValues = new ContentValues();
// Get the data we need first
long parentId = parentCursor.getLong(Mailbox.CONTENT_ID_COLUMN);
int parentFlags = 0;
int parentType = parentCursor.getInt(Mailbox.CONTENT_TYPE_COLUMN);
String parentServerId = parentCursor.getString(Mailbox.CONTENT_SERVER_ID_COLUMN);
// All email-type boxes hold mail
if (parentType <= Mailbox.TYPE_NOT_EMAIL) {
parentFlags |= Mailbox.FLAG_HOLDS_MAIL + Mailbox.FLAG_SUPPORTS_SETTINGS;
}
// Outbox, Drafts, and Sent don't allow mail to be moved to them
if (parentType == Mailbox.TYPE_MAIL || parentType == Mailbox.TYPE_TRASH ||
parentType == Mailbox.TYPE_JUNK || parentType == Mailbox.TYPE_INBOX) {
parentFlags |= Mailbox.FLAG_ACCEPTS_MOVED_MAIL;
}
// There's no concept of "append" in EAS so FLAG_ACCEPTS_APPENDED_MAIL is never used
// Mark parent mailboxes as parents & add parent key to children
// An example of a mailbox with a null serverId would be an Outbox that we create locally
// for hotmail accounts (which don't have a server-based Outbox)
if (parentServerId != null) {
selectionArgs[0] = parentServerId;
Cursor childCursor = resolver.query(Mailbox.CONTENT_URI,
Mailbox.ID_PROJECTION, MailboxColumns.PARENT_SERVER_ID + "=? AND " +
accountSelector, selectionArgs, null);
if (childCursor == null) return;
try {
while (childCursor.moveToNext()) {
parentFlags |= Mailbox.FLAG_HAS_CHILDREN | Mailbox.FLAG_CHILDREN_VISIBLE;
ContentValues childValues = new ContentValues();
childValues.put(Mailbox.PARENT_KEY, parentId);
long childId = childCursor.getLong(Mailbox.ID_PROJECTION_COLUMN);
resolver.update(ContentUris.withAppendedId(Mailbox.CONTENT_URI, childId),
childValues, null, null);
}
} finally {
childCursor.close();
}
} else {
// Mark this is having no parent, so that we don't examine this mailbox again
parentValues.put(Mailbox.PARENT_KEY, Mailbox.NO_MAILBOX);
Log.w(Logging.LOG_TAG, "Mailbox with null serverId: " +
parentCursor.getString(Mailbox.CONTENT_DISPLAY_NAME_COLUMN) + ", type: " +
parentType);
}
// Save away updated flags and parent key (if any)
parentValues.put(Mailbox.FLAGS, parentFlags);
resolver.update(ContentUris.withAppendedId(Mailbox.CONTENT_URI, parentId),
parentValues, null, null);
}
/**
* Recalculate a mailbox's flags and the parent key of any children
* @param context the caller's context
* @param accountSelector (see description below in fixupUninitializedParentKeys)
* @param serverId the server id of an individual mailbox
*/
public static void setFlagsAndChildrensParentKey(Context context, String accountSelector,
String serverId) {
Cursor cursor = context.getContentResolver().query(Mailbox.CONTENT_URI,
Mailbox.CONTENT_PROJECTION, MailboxColumns.SERVER_ID + "=? AND " + accountSelector,
new String[] {serverId}, null);
if (cursor == null) return;
try {
if (cursor.moveToFirst()) {
setFlagsAndChildrensParentKey(context, cursor, accountSelector);
}
} finally {
cursor.close();
}
}
/**
* Given an account selector, specifying the account(s) on which to work, create the parentKey
* and flags for each mailbox in the account(s) that is uninitialized (parentKey = 0 or null)
*
* @param accountSelector a sqlite WHERE clause expression to be used in determining the
* mailboxes to be acted upon, e.g. accountKey IN (1, 2), accountKey = 12, etc.
*/
public static void fixupUninitializedParentKeys(Context context, String accountSelector) {
// Sanity check first on our arguments
if (accountSelector == null) throw new IllegalArgumentException();
// The selection we'll use to find uninitialized parent key mailboxes
String noParentKeySelection = WHERE_PARENT_KEY_UNINITIALIZED + " AND " + accountSelector;
// We'll loop through mailboxes with an uninitialized parent key
ContentResolver resolver = context.getContentResolver();
Cursor noParentKeyMailboxCursor =
resolver.query(Mailbox.CONTENT_URI, Mailbox.CONTENT_PROJECTION,
noParentKeySelection, null, null);
if (noParentKeyMailboxCursor == null) return;
try {
while (noParentKeyMailboxCursor.moveToNext()) {
setFlagsAndChildrensParentKey(context, noParentKeyMailboxCursor, accountSelector);
String parentServerId =
noParentKeyMailboxCursor.getString(Mailbox.CONTENT_PARENT_SERVER_ID_COLUMN);
// Fixup the parent so that the children's parentKey is updated
if (parentServerId != null) {
setFlagsAndChildrensParentKey(context, accountSelector, parentServerId);
}
}
} finally {
noParentKeyMailboxCursor.close();
}
// Any mailboxes without a parent key should have parentKey set to -1 (no parent)
ContentValues values = new ContentValues();
values.put(Mailbox.PARENT_KEY, Mailbox.NO_MAILBOX);
resolver.update(Mailbox.CONTENT_URI, values, noParentKeySelection, null);
}
private static void setAccountSyncAdapterFlag(Context context, long accountId, boolean start) {
Account account = Account.restoreAccountWithId(context, accountId);
if (account == null) return;
// Set temporary flag indicating state of update of mailbox list
ContentValues cv = new ContentValues();
cv.put(Account.FLAGS, start ? (account.mFlags | ACCOUNT_MAILBOX_CHANGE_FLAG) :
account.mFlags & ~ACCOUNT_MAILBOX_CHANGE_FLAG);
context.getContentResolver().update(
ContentUris.withAppendedId(Account.CONTENT_URI, account.mId), cv, null, null);
}
/**
* Indicate that the specified account is starting the process of changing its mailbox list
* @param context the caller's context
* @param accountId the account that is starting to change its mailbox list
*/
public static void startMailboxChanges(Context context, long accountId) {
setAccountSyncAdapterFlag(context, accountId, true);
}
/**
* Indicate that the specified account is ending the process of changing its mailbox list
* @param context the caller's context
* @param accountId the account that is finished with changes to its mailbox list
*/
public static void endMailboxChanges(Context context, long accountId) {
setAccountSyncAdapterFlag(context, accountId, false);
}
/**
* Check that we didn't leave the account's mailboxes in a (possibly) inconsistent state
* If we did, make them consistent again
* @param context the caller's context
* @param accountId the account whose mailboxes are to be checked
*/
public static void checkMailboxConsistency(Context context, long accountId) {
// If our temporary flag is set, we were interrupted during an update
// First, make sure we're current (really fast w/ caching)
Account account = Account.restoreAccountWithId(context, accountId);
if (account == null) return;
if ((account.mFlags & ACCOUNT_MAILBOX_CHANGE_FLAG) != 0) {
Log.w(Logging.LOG_TAG, "Account " + account.mDisplayName +
" has inconsistent mailbox data; fixing up...");
// Set all account mailboxes to uninitialized parent key
ContentValues values = new ContentValues();
values.put(Mailbox.PARENT_KEY, Mailbox.PARENT_KEY_UNINITIALIZED);
String accountSelector = Mailbox.ACCOUNT_KEY + "=" + account.mId;
ContentResolver resolver = context.getContentResolver();
resolver.update(Mailbox.CONTENT_URI, values, accountSelector, null);
// Fix up keys and flags
MailboxUtilities.fixupUninitializedParentKeys(context, accountSelector);
// Clear the temporary flag
endMailboxChanges(context, accountId);
}
}
private static final String[] HIERARCHY_PROJECTION = new String[] {
MailboxColumns.ID, MailboxColumns.DISPLAY_NAME, MailboxColumns.PARENT_KEY,
MailboxColumns.HIERARCHICAL_NAME
};
private static final int HIERARCHY_ID = 0;
private static final int HIERARCHY_NAME = 1;
private static final int HIERARCHY_PARENT_KEY = 2;
private static final int HIERARCHY_HIERARCHICAL_NAME = 3;
private static String getHierarchicalName(Context context, long id, HashMap<Long, String> map,
String name, long parentId) {
String hierarchicalName;
if (map.containsKey(id)) {
return map.get(id);
} else if (parentId == Mailbox.NO_MAILBOX) {
hierarchicalName = name;
} else {
Mailbox parent = Mailbox.restoreMailboxWithId(context, parentId);
if (parent == null) return name + "/" + "??";
hierarchicalName = getHierarchicalName(context, parentId, map, parent.mDisplayName,
parent.mParentKey) + "/" + name;
}
map.put(id, hierarchicalName);
return hierarchicalName;
}
public static void setupHierarchicalNames(Context context, long accountId) {
Account account = Account.restoreAccountWithId(context, accountId);
if (account == null) return;
// Start by clearing all names
ContentValues values = new ContentValues();
String accountSelector = Mailbox.ACCOUNT_KEY + "=" + account.mId;
ContentResolver resolver = context.getContentResolver();
HashMap<Long, String> nameMap = new HashMap<Long, String>();
Cursor c = resolver.query(Mailbox.CONTENT_URI, HIERARCHY_PROJECTION, accountSelector,
null, null);
try {
while(c.moveToNext()) {
long id = c.getLong(HIERARCHY_ID);
String displayName = c.getString(HIERARCHY_NAME);
String name = getHierarchicalName(context, id, nameMap, displayName,
c.getLong(HIERARCHY_PARENT_KEY));
String oldHierarchicalName = c.getString(HIERARCHY_HIERARCHICAL_NAME);
// Don't write the name unless it has changed or we don't need one (it's top-level)
if (name.equals(oldHierarchicalName) ||
((name.equals(displayName)) && TextUtils.isEmpty(oldHierarchicalName))) {
continue;
}
// If the name has changed, update it
values.put(MailboxColumns.HIERARCHICAL_NAME, name);
resolver.update(ContentUris.withAppendedId(Mailbox.CONTENT_URI, id), values, null,
null);
}
} finally {
c.close();
}
}
}

View File

@ -0,0 +1,117 @@
/* Copyright (C) 2012 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.emailcommon.service;
import android.os.RemoteCallbackList;
import android.os.RemoteException;
import android.util.Log;
import com.android.emailcommon.service.IEmailServiceCallback.Stub;
public class EmailServiceCallback extends Stub {
private final RemoteCallbackList<IEmailServiceCallback> mCallbackList;
public EmailServiceCallback(RemoteCallbackList<IEmailServiceCallback> callbackList) {
mCallbackList = callbackList;
}
/**
* Broadcast a callback to the everyone that's registered
*
* @param wrapper the ServiceCallbackWrapper used in the broadcast
*/
private synchronized void broadcastCallback(ServiceCallbackWrapper wrapper) {
RemoteCallbackList<IEmailServiceCallback> callbackList = mCallbackList;
if (callbackList != null) {
// Call everyone on our callback list
int count = callbackList.beginBroadcast();
try {
for (int i = 0; i < count; i++) {
try {
wrapper.call(callbackList.getBroadcastItem(i));
} catch (RemoteException e) {
// Safe to ignore
} catch (RuntimeException e) {
// We don't want an exception in one call to prevent other calls, so
// we'll just log this and continue
Log.e("EmailServiceCallback", "Caught RuntimeException in broadcast", e);
}
}
} finally {
// No matter what, we need to finish the broadcast
callbackList.finishBroadcast();
}
}
}
@Override
public void loadAttachmentStatus(final long messageId, final long attachmentId,
final int status, final int progress) {
broadcastCallback(new ServiceCallbackWrapper() {
@Override
public void call(IEmailServiceCallback cb) throws RemoteException {
cb.loadAttachmentStatus(messageId, attachmentId, status, progress);
}
});
}
@Override
public void loadMessageStatus(final long messageId, final int status, final int progress) {
broadcastCallback(new ServiceCallbackWrapper() {
@Override
public void call(IEmailServiceCallback cb) throws RemoteException {
cb.loadMessageStatus(messageId, status, progress);
}
});
}
@Override
public void sendMessageStatus(final long messageId, final String subject, final int status,
final int progress) {
broadcastCallback(new ServiceCallbackWrapper() {
@Override
public void call(IEmailServiceCallback cb) throws RemoteException {
cb.sendMessageStatus(messageId, subject, status, progress);
}
});
}
@Override
public void syncMailboxListStatus(final long accountId, final int status,
final int progress) {
broadcastCallback(new ServiceCallbackWrapper() {
@Override
public void call(IEmailServiceCallback cb) throws RemoteException {
cb.syncMailboxListStatus(accountId, status, progress);
}
});
}
@Override
public void syncMailboxStatus(final long mailboxId, final int status,
final int progress) {
broadcastCallback(new ServiceCallbackWrapper() {
@Override
public void call(IEmailServiceCallback cb) throws RemoteException {
cb.syncMailboxStatus(mailboxId, status, progress);
}
});
}
private interface ServiceCallbackWrapper {
public void call(IEmailServiceCallback cb) throws RemoteException;
}
}

View File

@ -26,4 +26,25 @@ public class SyncWindow {
public static final int SYNC_WINDOW_2_WEEKS = 4;
public static final int SYNC_WINDOW_1_MONTH = 5;
public static final int SYNC_WINDOW_ALL = 6;
public static int toDays(int window) {
switch(window) {
case SYNC_WINDOW_1_DAY:
return 1;
case SYNC_WINDOW_3_DAYS:
return 3;
case SYNC_WINDOW_1_WEEK:
return 7;
case SYNC_WINDOW_2_WEEKS:
return 14;
case SYNC_WINDOW_1_MONTH:
return 30;
case SYNC_WINDOW_ALL:
return 365*10;
case SYNC_WINDOW_UNKNOWN:
case SYNC_WINDOW_AUTO:
default:
return 14;
}
}
}

View File

@ -65,7 +65,7 @@ public class EmailSyncAlarmReceiver extends BroadcastReceiver {
ContentResolver cr = context.getContentResolver();
// Get a selector for EAS accounts (we don't want to sync on changes to POP/IMAP messages)
String selector = SyncServiceManager.getAccountSelector();
String selector = SyncManager.getAccountSelector();
try {
// Find all of the deletions
@ -102,7 +102,7 @@ public class EmailSyncAlarmReceiver extends BroadcastReceiver {
// Request service from the mailbox
for (Long mailboxId: mailboxesToNotify) {
SyncServiceManager.serviceRequest(mailboxId, SyncServiceManager.SYNC_UPSYNC);
SyncManager.serviceRequest(mailboxId, SyncManager.SYNC_UPSYNC);
}
} catch (ProviderUnavailableException e) {
Log.e("EmailSyncAlarmReceiver", "EmailProvider unavailable; aborting alarm receiver");

View File

@ -30,12 +30,12 @@ import android.content.Intent;
public class MailboxAlarmReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
long mailboxId = intent.getLongExtra("mailbox", SyncServiceManager.EXTRA_MAILBOX_ID);
long mailboxId = intent.getLongExtra("mailbox", SyncManager.EXTRA_MAILBOX_ID);
// EXCHANGE_SERVICE_MAILBOX_ID tells us that the service is asking to be started
if (mailboxId == SyncServiceManager.SYNC_SERVICE_MAILBOX_ID) {
context.startService(new Intent(context, SyncServiceManager.class));
if (mailboxId == SyncManager.SYNC_SERVICE_MAILBOX_ID) {
context.startService(new Intent(context, SyncManager.class));
} else {
SyncServiceManager.alert(context, mailboxId);
SyncManager.alert(context, mailboxId);
}
}
}

View File

@ -0,0 +1,42 @@
/*
* 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.emailsync;
import com.android.emailsync.Request;
/**
* MessageMoveRequest is the EAS wrapper for requesting a "move to folder"
*/
public class MessageMoveRequest extends Request {
public final long mMailboxId;
public MessageMoveRequest(long messageId, long mailboxId) {
super(messageId);
mMailboxId = mailboxId;
}
// MessageMoveRequests are unique by their message id (i.e. it's meaningless to have two
// separate message moves queued at the same time)
public boolean equals(Object o) {
if (!(o instanceof MessageMoveRequest)) return false;
return ((MessageMoveRequest)o).mMessageId == mMessageId;
}
public int hashCode() {
return (int)mMessageId;
}
}

View File

@ -20,7 +20,7 @@ package com.android.emailsync;
import com.android.emailcommon.provider.EmailContent.Attachment;
/**
* PartRequest is the EAS wrapper for attachment loading requests. In addition to information about
* PartRequest is the wrapper for attachment loading requests. In addition to information about
* the attachment to be loaded, it also contains the callback to be used for status/progress
* updates to the UI.
*/

View File

@ -61,7 +61,6 @@ import com.android.emailcommon.provider.ProviderUnavailableException;
import com.android.emailcommon.service.AccountServiceProxy;
import com.android.emailcommon.service.EmailServiceProxy;
import com.android.emailcommon.service.EmailServiceStatus;
import com.android.emailcommon.service.IEmailServiceCallback;
import com.android.emailcommon.service.IEmailServiceCallback.Stub;
import com.android.emailcommon.service.PolicyServiceProxy;
import com.android.emailcommon.utility.EmailAsyncTask;
@ -88,7 +87,7 @@ import java.util.concurrent.ConcurrentHashMap;
* order to maintain proper 2-way syncing of data. (More documentation to follow)
*
*/
public abstract class SyncServiceManager extends Service implements Runnable {
public abstract class SyncManager extends Service implements Runnable {
private static final String TAG = "SyncServiceManager";
@ -201,7 +200,7 @@ public abstract class SyncServiceManager extends Service implements Runnable {
public ContentResolver mResolver;
// The singleton SyncServiceManager object, with its thread and stop flag
protected static SyncServiceManager INSTANCE;
protected static SyncManager INSTANCE;
protected static Thread sServiceThread = null;
// Cached unique device id
protected static String sDeviceId = null;
@ -338,7 +337,7 @@ public abstract class SyncServiceManager extends Service implements Runnable {
}
public static String getAccountSelector() {
SyncServiceManager ssm = INSTANCE;
SyncManager ssm = INSTANCE;
if (ssm == null) return "";
return ssm.getAccountsSelector();
}
@ -380,8 +379,8 @@ public abstract class SyncServiceManager extends Service implements Runnable {
if (onSecurityHold(account)) {
// If we're in a security hold, and our policies are active, release
// the hold
if (PolicyServiceProxy.isActive(SyncServiceManager.this, null)) {
PolicyServiceProxy.setAccountHoldFlag(SyncServiceManager.this,
if (PolicyServiceProxy.isActive(SyncManager.this, null)) {
PolicyServiceProxy.setAccountHoldFlag(SyncManager.this,
account, false);
log("isActive true; release hold for " + account.mDisplayName);
}
@ -440,7 +439,7 @@ public abstract class SyncServiceManager extends Service implements Runnable {
// The implication is that the account has been deleted; let's find out
alwaysLog("Observer found deleted account: " + account.mDisplayName);
// Run the reconciler (the reconciliation itself runs in the Email app)
runAccountReconcilerSync(SyncServiceManager.this);
runAccountReconcilerSync(SyncManager.this);
// See if the account is still around
Account deletedAccount =
Account.restoreAccountWithId(context, account.mId);
@ -477,7 +476,7 @@ public abstract class SyncServiceManager extends Service implements Runnable {
// See if this account is no longer on security hold
if (onSecurityHold(account) && !onSecurityHold(updatedAccount)) {
releaseSyncHolds(SyncServiceManager.this,
releaseSyncHolds(SyncManager.this,
AbstractSyncService.EXIT_SECURITY_FAILURE, account);
}
@ -548,7 +547,7 @@ public abstract class SyncServiceManager extends Service implements Runnable {
* Unregister all CalendarObserver's
*/
static public void unregisterCalendarObservers() {
SyncServiceManager ssm = INSTANCE;
SyncManager ssm = INSTANCE;
if (ssm == null) return;
ContentResolver resolver = ssm.mResolver;
for (CalendarObserver observer: ssm.mCalendarObservers.values()) {
@ -718,7 +717,7 @@ public abstract class SyncServiceManager extends Service implements Runnable {
}
static public Account getAccountById(long accountId) {
SyncServiceManager ssm = INSTANCE;
SyncManager ssm = INSTANCE;
if (ssm != null) {
AccountList accountList = ssm.mAccountList;
synchronized (accountList) {
@ -729,7 +728,7 @@ public abstract class SyncServiceManager extends Service implements Runnable {
}
static public Account getAccountByName(String accountName) {
SyncServiceManager ssm = INSTANCE;
SyncManager ssm = INSTANCE;
if (ssm != null) {
AccountList accountList = ssm.mAccountList;
synchronized (accountList) {
@ -795,7 +794,7 @@ public abstract class SyncServiceManager extends Service implements Runnable {
* @param account the account whose Mailboxes should be released from security hold
*/
static public void releaseSecurityHold(Account account) {
SyncServiceManager ssm = INSTANCE;
SyncManager ssm = INSTANCE;
if (ssm != null) {
ssm.releaseSyncHolds(INSTANCE, AbstractSyncService.EXIT_SECURITY_FAILURE,
account);
@ -910,7 +909,7 @@ public abstract class SyncServiceManager extends Service implements Runnable {
}
public static void stopAccountSyncs(long acctId) {
SyncServiceManager ssm = INSTANCE;
SyncManager ssm = INSTANCE;
if (ssm != null) {
ssm.stopAccountSyncs(acctId, true);
}
@ -957,7 +956,7 @@ public abstract class SyncServiceManager extends Service implements Runnable {
* @param acctId
*/
static public void stopNonAccountMailboxSyncsForAccount(long acctId) {
SyncServiceManager ssm = INSTANCE;
SyncManager ssm = INSTANCE;
if (ssm != null) {
ssm.stopAccountSyncs(acctId, false);
kick("reload folder list");
@ -1051,7 +1050,7 @@ public abstract class SyncServiceManager extends Service implements Runnable {
}
static public void runAwake(long id) {
SyncServiceManager ssm = INSTANCE;
SyncManager ssm = INSTANCE;
if (ssm != null) {
ssm.acquireWakeLock(id);
ssm.clearAlarm(id);
@ -1059,7 +1058,7 @@ public abstract class SyncServiceManager extends Service implements Runnable {
}
static public void runAsleep(long id, long millis) {
SyncServiceManager ssm = INSTANCE;
SyncManager ssm = INSTANCE;
if (ssm != null) {
ssm.setAlarm(id, millis);
ssm.releaseWakeLock(id);
@ -1067,27 +1066,27 @@ public abstract class SyncServiceManager extends Service implements Runnable {
}
static public void clearWatchdogAlarm(long id) {
SyncServiceManager ssm = INSTANCE;
SyncManager ssm = INSTANCE;
if (ssm != null) {
ssm.clearAlarm(id);
}
}
static public void setWatchdogAlarm(long id, long millis) {
SyncServiceManager ssm = INSTANCE;
SyncManager ssm = INSTANCE;
if (ssm != null) {
ssm.setAlarm(id, millis);
}
}
static public void alert(Context context, final long id) {
final SyncServiceManager ssm = INSTANCE;
final SyncManager ssm = INSTANCE;
checkSyncServiceManagerServiceRunning();
if (id < 0) {
log("SyncServiceManager alert");
kick("ping SyncServiceManager");
} else if (ssm == null) {
context.startService(new Intent(context, SyncServiceManager.class));
context.startService(new Intent(context, SyncManager.class));
} else {
final AbstractSyncService service = ssm.mServiceMap.get(id);
if (service != null) {
@ -1129,7 +1128,7 @@ public abstract class SyncServiceManager extends Service implements Runnable {
}
// Shutdown the connection manager; this should close all of our
// sockets and generate IOExceptions all around.
SyncServiceManager.shutdownConnectionManager();
SyncManager.shutdownConnectionManager();
}
}
}}, threadName).start();
@ -1177,7 +1176,7 @@ public abstract class SyncServiceManager extends Service implements Runnable {
public void run() {
synchronized (mAccountList) {
for (Account account : mAccountList)
SyncServiceManager.stopAccountSyncs(account.mId);
SyncManager.stopAccountSyncs(account.mId);
}
}});
}
@ -1400,13 +1399,13 @@ public abstract class SyncServiceManager extends Service implements Runnable {
try {
synchronized (sSyncLock) {
// SyncServiceManager cannot start unless we connect to AccountService
if (!new AccountServiceProxy(SyncServiceManager.this).test()) {
if (!new AccountServiceProxy(SyncManager.this).test()) {
alwaysLog("!!! Email application not found; stopping self");
stopSelf();
}
if (sDeviceId == null) {
try {
String deviceId = getDeviceId(SyncServiceManager.this);
String deviceId = getDeviceId(SyncManager.this);
if (deviceId != null) {
sDeviceId = deviceId;
}
@ -1430,7 +1429,7 @@ public abstract class SyncServiceManager extends Service implements Runnable {
}
// Run the reconciler and clean up mismatched accounts - if we weren't
// running when accounts were deleted, it won't have been called.
runAccountReconcilerSync(SyncServiceManager.this);
runAccountReconcilerSync(SyncManager.this);
// Update other services depending on final account configuration
maybeStartSyncServiceManagerThread();
if (sServiceThread == null) {
@ -1450,7 +1449,7 @@ public abstract class SyncServiceManager extends Service implements Runnable {
}
public static void reconcileAccounts(Context context) {
SyncServiceManager ssm = INSTANCE;
SyncManager ssm = INSTANCE;
if (ssm != null) {
ssm.runAccountReconcilerSync(context);
}
@ -1504,11 +1503,11 @@ public abstract class SyncServiceManager extends Service implements Runnable {
* com.android.email) and hasn't been restarted. See the comment for onCreate for details
*/
static void checkSyncServiceManagerServiceRunning() {
SyncServiceManager ssm = INSTANCE;
SyncManager ssm = INSTANCE;
if (ssm == null) return;
if (sServiceThread == null) {
log("!!! checkSyncServiceManagerServiceRunning; starting service...");
ssm.startService(new Intent(ssm, SyncServiceManager.class));
ssm.startService(new Intent(ssm, SyncManager.class));
}
}
@ -1598,7 +1597,7 @@ public abstract class SyncServiceManager extends Service implements Runnable {
// process starts running again, remote processes will be started again in due course
Log.e(TAG, "EmailProvider unavailable; shutting down");
// Ask for our service to be restarted; this should kick-start the Email process as well
startService(new Intent(this, SyncServiceManager.class));
startService(new Intent(this, SyncManager.class));
} catch (RuntimeException e) {
// Crash; this is a completely unexpected runtime error
Log.e(TAG, "RuntimeException in SyncServiceManager", e);
@ -1718,7 +1717,7 @@ public abstract class SyncServiceManager extends Service implements Runnable {
* @return whether or not the account can sync automatically
*/
/*package*/ public static boolean canAutoSync(Account account) {
SyncServiceManager ssm = INSTANCE;
SyncManager ssm = INSTANCE;
if (ssm == null) {
return false;
}
@ -1971,7 +1970,7 @@ public abstract class SyncServiceManager extends Service implements Runnable {
}
static public void serviceRequest(long mailboxId, long ms, int reason) {
SyncServiceManager ssm = INSTANCE;
SyncManager ssm = INSTANCE;
if (ssm == null) return;
Mailbox m = Mailbox.restoreMailboxWithId(ssm, mailboxId);
if (m == null || !isSyncable(m)) return;
@ -1989,7 +1988,7 @@ public abstract class SyncServiceManager extends Service implements Runnable {
}
static public void serviceRequestImmediate(long mailboxId) {
SyncServiceManager ssm = INSTANCE;
SyncManager ssm = INSTANCE;
if (ssm == null) return;
AbstractSyncService service = ssm.mServiceMap.get(mailboxId);
if (service != null) {
@ -2004,7 +2003,7 @@ public abstract class SyncServiceManager extends Service implements Runnable {
}
static public void sendMessageRequest(Request req) {
SyncServiceManager ssm = INSTANCE;
SyncManager ssm = INSTANCE;
Message msg = Message.restoreMessageWithId(ssm, req.mMessageId);
if (msg == null) return;
long mailboxId = msg.mMailboxKey;
@ -2044,7 +2043,7 @@ public abstract class SyncServiceManager extends Service implements Runnable {
* @return whether or not the Mailbox is available for syncing (i.e. is a valid push target)
*/
static public int pingStatus(long mailboxId) {
SyncServiceManager ssm = INSTANCE;
SyncManager ssm = INSTANCE;
if (ssm == null) return PING_STATUS_OK;
// Already syncing...
if (ssm.mServiceMap.get(mailboxId) != null) {
@ -2063,7 +2062,7 @@ public abstract class SyncServiceManager extends Service implements Runnable {
}
static public void startManualSync(long mailboxId, int reason, Request req) {
SyncServiceManager ssm = INSTANCE;
SyncManager ssm = INSTANCE;
if (ssm == null) return;
synchronized (sSyncLock) {
AbstractSyncService svc = ssm.mServiceMap.get(mailboxId);
@ -2085,7 +2084,7 @@ public abstract class SyncServiceManager extends Service implements Runnable {
// DO NOT CALL THIS IN A LOOP ON THE SERVICEMAP
static public void stopManualSync(long mailboxId) {
SyncServiceManager ssm = INSTANCE;
SyncManager ssm = INSTANCE;
if (ssm == null) return;
synchronized (sSyncLock) {
AbstractSyncService svc = ssm.mServiceMap.get(mailboxId);
@ -2102,7 +2101,7 @@ public abstract class SyncServiceManager extends Service implements Runnable {
* Wake up SyncServiceManager to check for mailboxes needing service
*/
static public void kick(String reason) {
SyncServiceManager ssm = INSTANCE;
SyncManager ssm = INSTANCE;
if (ssm != null) {
synchronized (ssm) {
//INSTANCE.log("Kick: " + reason);
@ -2122,7 +2121,7 @@ public abstract class SyncServiceManager extends Service implements Runnable {
* @param mailboxId the id of the mailbox
*/
static public void removeFromSyncErrorMap(long mailboxId) {
SyncServiceManager ssm = INSTANCE;
SyncManager ssm = INSTANCE;
if (ssm != null) {
ssm.mSyncErrorMap.remove(mailboxId);
}
@ -2142,7 +2141,7 @@ public abstract class SyncServiceManager extends Service implements Runnable {
* @param svc the service that is finished
*/
static public void done(AbstractSyncService svc) {
SyncServiceManager ssm = INSTANCE;
SyncManager ssm = INSTANCE;
if (ssm == null) return;
synchronized(sSyncLock) {
long mailboxId = svc.mMailboxId;
@ -2181,7 +2180,7 @@ public abstract class SyncServiceManager extends Service implements Runnable {
}
errorMap.remove(mailboxId);
// If we've had a successful sync, clear the shutdown count
synchronized (SyncServiceManager.class) {
synchronized (SyncManager.class) {
sClientConnectionManagerShutdownCount = 0;
}
// Leave now; other statuses are errors

36
imap2/Android.mk Normal file
View File

@ -0,0 +1,36 @@
# Copyright 2008, 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.
LOCAL_PATH:= $(call my-dir)
include $(CLEAR_VARS)
#
# Imap2
#
LOCAL_MODULE_TAGS := optional
LOCAL_SRC_FILES := $(call all-java-files-under, src)
LOCAL_SRC_FILES += $(call all-java-files-under, ../src/com/beetstra)
LOCAL_STATIC_JAVA_LIBRARIES := android-common com.android.emailcommon2 com.android.emailsync
LOCAL_PACKAGE_NAME := Imap2
#LOCAL_PROGUARD_FLAG_FILES := proguard.flags
LOCAL_SDK_VERSION := 15
include $(BUILD_PACKAGE)
# additionally, build unit tests in a separate .apk
include $(call all-makefiles-under,$(LOCAL_PATH))

113
imap2/AndroidManifest.xml Normal file
View File

@ -0,0 +1,113 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (C) 2008 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.
-->
<manifest
xmlns:android="http://schemas.android.com/apk/res/android"
package="com.android.imap2"
android:versionCode="000000"
android:versionName="0.1"
>
<uses-permission
android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
<uses-permission
android:name="android.permission.ACCESS_NETWORK_STATE"/>
<uses-permission
android:name="android.permission.INTERNET"/>
<uses-permission
android:name="android.permission.VIBRATE"/>
<uses-permission
android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission
android:name="android.permission.GET_ACCOUNTS" />
<uses-permission
android:name="android.permission.MANAGE_ACCOUNTS" />
<uses-permission
android:name="android.permission.AUTHENTICATE_ACCOUNTS" />
<uses-permission
android:name="android.permission.READ_SYNC_SETTINGS" />
<uses-permission
android:name="android.permission.WRITE_SYNC_SETTINGS" />
<uses-permission
android:name="android.permission.USE_CREDENTIALS"/>
<!-- Only required if a store implements push mail and needs to keep network open -->
<uses-permission
android:name="android.permission.WAKE_LOCK"/>
<uses-permission
android:name="android.permission.READ_PHONE_STATE"/>
<uses-permission
android:name="com.android.email.permission.READ_ATTACHMENT"/>
<uses-permission
android:name="com.android.email.permission.ACCESS_PROVIDER"/>
<application
android:icon="@mipmap/icon"
android:label="@string/app_name"
android:name="Imap2"
android:theme="@android:style/Theme.Holo.Light"
>
<receiver
android:name="com.android.emailsync.EmailSyncAlarmReceiver"/>
<receiver
android:name="com.android.emailsync.MailboxAlarmReceiver"/>
<receiver
android:name=".Imap2BroadcastReceiver"
android:enabled="true">
<intent-filter>
<action
android:name="android.intent.action.BOOT_COMPLETED" />
<action
android:name="android.intent.action.DEVICE_STORAGE_LOW" />
<action
android:name="android.intent.action.DEVICE_STORAGE_OK" />
<action
android:name="android.accounts.LOGIN_ACCOUNTS_CHANGED" />
</intent-filter>
</receiver>
<service
android:name=".BroadcastProcessorService" />
<!--Required stanza to register the EAS EmailSyncAdapterService with SyncManager -->
<service
android:name=".EmailSyncAdapterService"
android:exported="true">
<intent-filter>
<action
android:name="android.content.SyncAdapter" />
</intent-filter>
<meta-data android:name="android.content.SyncAdapter"
android:resource="@xml/syncadapter_email" />
</service>
<service
android:name="com.android.imap2.Imap2SyncManager"
android:enabled="true"
android:permission="com.android.email.permission.ACCESS_PROVIDER"
>
<intent-filter>
<action
android:name="com.android.email.IMAP2_INTENT" />
</intent-filter>
</service>
</application>
</manifest>

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

View File

@ -0,0 +1,37 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (C) 2008 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.
-->
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<!-- Name of application on Home screen -->
<string name="app_name">Imap2</string>
<!-- The next set of strings are used server-side and must not be localized. -->
<!-- Do Not Translate. This is the name of the "inbox" folder, on the server. -->
<string name="mailbox_name_server_inbox" translatable="false">Inbox</string>
<!-- Do Not Translate. This is the name of the "outbox" folder, on the server. -->
<string name="mailbox_name_server_outbox" translatable="false">Outbox</string>
<!-- Do Not Translate. This is the name of the "drafts" folder, on the server. -->
<string name="mailbox_name_server_drafts" translatable="false">Drafts</string>
<!-- Do Not Translate. This is the name of the "trash" folder, on the server. -->
<string name="mailbox_name_server_trash" translatable="false">Trash</string>
<!-- Do Not Translate. This is the name of the "sent" folder, on the server. -->
<string name="mailbox_name_server_sent" translatable="false">Sent</string>
<!-- Do Not Translate. This is the name of the "junk" folder, on the server. -->
<string name="mailbox_name_server_junk" translatable="false">Junk</string>
</resources>

View File

@ -0,0 +1,27 @@
<?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.email.provider"
android:accountType="com.android.imap2"
android:supportsUploading="true"
/>

View File

@ -0,0 +1,197 @@
/* Copyright (C) 2012 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.imap2;
import android.content.Context;
import android.os.RemoteException;
import com.android.emailcommon.provider.EmailContent.Attachment;
import com.android.emailcommon.provider.EmailContent.Message;
import com.android.emailcommon.service.EmailServiceStatus;
import com.android.emailsync.PartRequest;
import com.android.emailcommon.utility.AttachmentUtilities;
import com.android.imap2.Imap2SyncService.Connection;
import com.android.mail.providers.UIProvider;
import java.io.Closeable;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
/**
* Handle IMAP2 attachment loading
*/
public class AttachmentLoader {
static private final int CHUNK_SIZE = 16*1024;
private final Context mContext;
private final Attachment mAttachment;
private final long mAttachmentId;
private final long mMessageId;
private final Message mMessage;
private final Imap2SyncService mService;
public AttachmentLoader(Imap2SyncService service, PartRequest req) {
mService = service;
mContext = service.mContext;
mAttachment = req.mAttachment;
mAttachmentId = mAttachment.mId;
mMessageId = mAttachment.mMessageKey;
mMessage = Message.restoreMessageWithId(mContext, mMessageId);
}
private void doStatusCallback(int status) {
try {
Imap2SyncManager.callback().loadAttachmentStatus(mMessageId, mAttachmentId, status, 0);
} catch (RemoteException e) {
// No danger if the client is no longer around
}
}
private void doProgressCallback(int progress) {
try {
Imap2SyncManager.callback().loadAttachmentStatus(mMessageId, mAttachmentId,
EmailServiceStatus.IN_PROGRESS, progress);
} catch (RemoteException e) {
// No danger if the client is no longer around
}
}
/**
* Close, ignoring errors (as during cleanup)
* @param c a Closeable
*/
private void close(Closeable c) {
try {
c.close();
} catch (IOException e) {
}
}
/**
* Save away the contentUri for this Attachment and notify listeners
* @throws IOException
*/
private void finishLoadAttachment(File file, OutputStream os) throws IOException {
InputStream in = null;
try {
in = new FileInputStream(file);
AttachmentUtilities.saveAttachment(mContext, in, mAttachment);
doStatusCallback(EmailServiceStatus.SUCCESS);
} catch (FileNotFoundException e) {
// Not bloody likely, as we just created it successfully
throw new IOException("Attachment file not found?");
} finally {
close(in);
}
}
private void readPart (ImapInputStream in, String tag, OutputStream out) throws IOException {
String res = in.readLine();
int bstart = res.indexOf("body[");
if (bstart < 0)
bstart = res.indexOf("BODY[");
if (bstart < 0)
return;
int bend = res.indexOf(']', bstart);
if (bend < 0)
return;
int br = res.indexOf('{');
if (br > 0) {
Parser p = new Parser(res, br + 1);
int expectedLength = p.parseInteger();
int remainingLength = expectedLength;
int totalRead = 0;
byte[] buf = new byte[CHUNK_SIZE];
int lastCallbackPct = -1;
int lastCallbackTotalRead = 0;
while (remainingLength > 0) {
int rdlen = (remainingLength > CHUNK_SIZE ? CHUNK_SIZE : remainingLength);
int bytesRead = in.read(buf, 0, rdlen);
totalRead += bytesRead;
out.write(buf, 0, bytesRead);
remainingLength -= bytesRead;
int pct = (totalRead * 100) / expectedLength;
// Callback only if we've read at least 1% more and have read more than CHUNK_SIZE
// We don't want to spam the Email app
if ((pct > lastCallbackPct) && (totalRead > (lastCallbackTotalRead + CHUNK_SIZE))) {
// Report progress back to the UI
doProgressCallback(pct);
lastCallbackTotalRead = totalRead;
lastCallbackPct = pct;
}
}
out.close();
String line = in.readLine();
if (!line.endsWith(")")) {
mService.errorLog("Bad part?");
throw new IOException();
}
line = in.readLine();
if (!line.startsWith(tag)) {
mService.userLog("Bad part?");
throw new IOException();
}
}
}
/**
* Loads an attachment, based on the PartRequest passed in the constructor
* @throws IOException
*/
public void loadAttachment(Connection conn) throws IOException {
if (mMessage == null) {
doStatusCallback(EmailServiceStatus.MESSAGE_NOT_FOUND);
return;
}
if (mAttachment.mUiState == UIProvider.AttachmentState.SAVED) {
return;
}
// Say we've started loading the attachment
doProgressCallback(0);
try {
OutputStream os = null;
File tmpFile = null;
try {
tmpFile = File.createTempFile("imap2_", "tmp", mContext.getCacheDir());
os = new FileOutputStream(tmpFile);
String tag = mService.writeCommand(conn.writer, "uid fetch " + mMessage.mServerId +
" body[" + mAttachment.mLocation + ']');
readPart(conn.reader, tag, os);
finishLoadAttachment(tmpFile, os);
return;
} catch (FileNotFoundException e) {
mService.errorLog("Can't get attachment; write file not found?");
doStatusCallback(EmailServiceStatus.ATTACHMENT_NOT_FOUND);
} finally {
close(os);
if (tmpFile != null) {
tmpFile.delete();
}
}
} catch (IOException e) {
// Report the error, but also report back to the service
doStatusCallback(EmailServiceStatus.CONNECTION_ERROR);
throw e;
} finally {
}
}
}

View File

@ -0,0 +1,82 @@
/*
* Copyright (C) 2011 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.imap2;
import android.accounts.AccountManager;
import android.app.IntentService;
import android.content.Context;
import android.content.Intent;
import android.util.Log;
import com.android.emailcommon.Logging;
import com.android.emailsync.SyncManager;
/**
* The service that really handles broadcast intents on a worker thread.
*
* We make it a service, because:
* <ul>
* <li>So that it's less likely for the process to get killed.
* <li>Even if it does, the Intent that have started it will be re-delivered by the system,
* and we can start the process again. (Using {@link #setIntentRedelivery}).
* </ul>
*/
public class BroadcastProcessorService extends IntentService {
// Action used for BroadcastReceiver entry point
private static final String ACTION_BROADCAST = "broadcast_receiver";
public BroadcastProcessorService() {
// Class name will be the thread name.
super(BroadcastProcessorService.class.getName());
// Intent should be redelivered if the process gets killed before completing the job.
setIntentRedelivery(true);
}
/**
* Entry point for {@link Imap2BroadcastReceiver}.
*/
public static void processBroadcastIntent(Context context, Intent broadcastIntent) {
Intent i = new Intent(context, BroadcastProcessorService.class);
i.setAction(ACTION_BROADCAST);
i.putExtra(Intent.EXTRA_INTENT, broadcastIntent);
context.startService(i);
}
@Override
protected void onHandleIntent(Intent intent) {
// Dispatch from entry point
final String action = intent.getAction();
if (ACTION_BROADCAST.equals(action)) {
final Intent broadcastIntent = intent.getParcelableExtra(Intent.EXTRA_INTENT);
final String broadcastAction = broadcastIntent.getAction();
if (Intent.ACTION_BOOT_COMPLETED.equals(broadcastAction)) {
onBootCompleted();
} else if (AccountManager.LOGIN_ACCOUNTS_CHANGED_ACTION.equals(broadcastAction)) {
Log.d(Logging.LOG_TAG, "Login accounts changed; reconciling...");
SyncManager.reconcileAccounts(this);
}
}
}
/**
* Handles {@link Intent#ACTION_BOOT_COMPLETED}. Called on a worker thread.
*/
private void onBootCompleted() {
startService(new Intent(this, Imap2SyncManager.class));
}
}

View File

@ -0,0 +1,122 @@
/*
* Copyright (C) 2012 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.imap2;
import com.android.emailcommon.provider.EmailContent;
import com.android.emailcommon.provider.EmailContent.AccountColumns;
import com.android.emailcommon.provider.EmailContent.MailboxColumns;
import com.android.emailcommon.provider.Mailbox;
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.util.Log;
public class EmailSyncAdapterService extends Service {
private static final String TAG = "Imap2 EmailSyncAdapterService";
private static SyncAdapterImpl sSyncAdapter = null;
private static final Object sSyncAdapterLock = new Object();
private static final String[] ID_PROJECTION = new String[] {EmailContent.RECORD_ID};
private static final String ACCOUNT_AND_TYPE_INBOX =
MailboxColumns.ACCOUNT_KEY + "=? AND " + MailboxColumns.TYPE + '=' + Mailbox.TYPE_INBOX;
public EmailSyncAdapterService() {
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 {
EmailSyncAdapterService.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 ExchangeService to start an
* inbox sync when we get the signal from the system SyncManager.
*/
private static void performSync(Context context, Account account, Bundle extras,
String authority, ContentProviderClient provider, SyncResult syncResult)
throws OperationCanceledException {
ContentResolver cr = context.getContentResolver();
Log.i(TAG, "performSync");
// Find the (EmailProvider) account associated with this email address
Cursor accountCursor =
cr.query(com.android.emailcommon.provider.Account.CONTENT_URI,
ID_PROJECTION, AccountColumns.EMAIL_ADDRESS + "=?", new String[] {account.name},
null);
try {
if (accountCursor.moveToFirst()) {
long accountId = accountCursor.getLong(0);
// Now, find the inbox associated with the account
Cursor mailboxCursor = cr.query(Mailbox.CONTENT_URI, ID_PROJECTION,
ACCOUNT_AND_TYPE_INBOX, new String[] {Long.toString(accountId)}, null);
try {
if (mailboxCursor.moveToFirst()) {
Log.i(TAG, "Mail sync requested for " + account.name);
// Ask for a sync from our sync manager
//***
//SyncServiceManager.serviceRequest(mailboxCursor.getLong(0),
// SyncServiceManager.SYNC_KICK);
}
} finally {
mailboxCursor.close();
}
}
} finally {
accountCursor.close();
}
}
}

View File

@ -0,0 +1,23 @@
/*
* Copyright (C) 2012 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.imap2;
import android.app.Application;
public class Imap2 extends Application {
// TODO Investigate whether this class is needed
}

View File

@ -0,0 +1,31 @@
/*
* Copyright (C) 2012 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.imap2;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
/**
* The broadcast receiver. The actual job is done in EmailBroadcastProcessor on a worker thread.
*/
public class Imap2BroadcastReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
BroadcastProcessorService.processBroadcastIntent(context, intent);
}
}

View File

@ -0,0 +1,325 @@
/*
* Copyright (C) 2012 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.imap2;
import android.os.Bundle;
import android.os.Debug;
import android.os.Handler;
import android.os.IBinder;
import android.os.RemoteCallbackList;
import android.os.RemoteException;
import android.content.ContentResolver;
import android.content.ContentUris;
import android.content.ContentValues;
import android.content.Context;
import android.content.Intent;
import android.database.Cursor;
import android.util.Log;
import com.android.emailcommon.Api;
import com.android.emailcommon.provider.EmailContent;
import com.android.emailcommon.provider.EmailContent.Attachment;
import com.android.emailcommon.provider.EmailContent.MailboxColumns;
import com.android.emailcommon.provider.Account;
import com.android.emailcommon.provider.HostAuth;
import com.android.emailcommon.provider.Mailbox;
import com.android.emailcommon.provider.ProviderUnavailableException;
import com.android.emailcommon.service.AccountServiceProxy;
import com.android.emailcommon.service.EmailServiceCallback;
import com.android.emailcommon.service.IEmailService;
import com.android.emailcommon.service.IEmailServiceCallback;
import com.android.emailcommon.service.IEmailServiceCallback.Stub;
import com.android.emailcommon.service.SearchParams;
import com.android.emailsync.AbstractSyncService;
import com.android.emailsync.PartRequest;
import com.android.emailsync.SyncManager;
import com.android.mail.providers.UIProvider.AccountCapabilities;
import java.io.IOException;
import java.util.concurrent.ConcurrentHashMap;
public class Imap2SyncManager extends SyncManager {
// Callbacks as set up via setCallback
private static final RemoteCallbackList<IEmailServiceCallback> mCallbackList =
new RemoteCallbackList<IEmailServiceCallback>();
private static final EmailServiceCallback sCallbackProxy =
new EmailServiceCallback(mCallbackList);
/**
* Create our EmailService implementation here.
*/
private final IEmailService.Stub mBinder = new IEmailService.Stub() {
@Override
public int getApiLevel() {
return Api.LEVEL;
}
@Override
public Bundle validate(HostAuth hostAuth) throws RemoteException {
return new Imap2SyncService(Imap2SyncManager.this,
new Mailbox()).validateAccount(hostAuth, Imap2SyncManager.this);
}
@Override
public Bundle autoDiscover(String userName, String password) throws RemoteException {
return null;
}
@Override
public void startSync(long mailboxId, boolean userRequest) throws RemoteException {
SyncManager imapService = INSTANCE;
if (imapService == null) return;
Imap2SyncService svc = (Imap2SyncService) imapService.mServiceMap.get(mailboxId);
if (svc == null) {
startManualSync(mailboxId, userRequest ? SYNC_UI_REQUEST : SYNC_SERVICE_START_SYNC,
null);
} else {
svc.ping();
}
}
@Override
public void stopSync(long mailboxId) throws RemoteException {
stopManualSync(mailboxId);
}
@Override
public void loadAttachment(long attachmentId, boolean background) throws RemoteException {
Attachment att = Attachment.restoreAttachmentWithId(Imap2SyncManager.this, attachmentId);
log("loadAttachment " + attachmentId + ": " + att.mFileName);
sendMessageRequest(new PartRequest(att, null, null));
}
@Override
public void updateFolderList(long accountId) throws RemoteException {
//***
//reloadFolderList(ImapService.this, accountId, false);
}
@Override
public void hostChanged(long accountId) throws RemoteException {
SyncManager exchangeService = INSTANCE;
if (exchangeService == null) return;
ConcurrentHashMap<Long, SyncError> syncErrorMap = exchangeService.mSyncErrorMap;
// Go through the various error mailboxes
for (long mailboxId: syncErrorMap.keySet()) {
SyncError error = syncErrorMap.get(mailboxId);
// If it's a login failure, look a little harder
Mailbox m = Mailbox.restoreMailboxWithId(exchangeService, mailboxId);
// If it's for the account whose host has changed, clear the error
// If the mailbox is no longer around, remove the entry in the map
if (m == null) {
syncErrorMap.remove(mailboxId);
} else if (error != null && m.mAccountKey == accountId) {
error.fatal = false;
error.holdEndTime = 0;
}
}
// Stop any running syncs
exchangeService.stopAccountSyncs(accountId, true);
// Kick ExchangeService
kick("host changed");
}
@Override
public void setLogging(int flags) throws RemoteException {
// Protocol logging
//Eas.setUserDebug(flags);
// Sync logging
setUserDebug(flags);
}
@Override
public void sendMeetingResponse(long messageId, int response) throws RemoteException {
// Not used in IMAP
}
@Override
public void loadMore(long messageId) throws RemoteException {
}
// The following three methods are not implemented in this version
@Override
public boolean createFolder(long accountId, String name) throws RemoteException {
return false;
}
@Override
public boolean deleteFolder(long accountId, String name) throws RemoteException {
return false;
}
@Override
public boolean renameFolder(long accountId, String oldName, String newName)
throws RemoteException {
return false;
}
@Override
public void setCallback(IEmailServiceCallback cb) throws RemoteException {
mCallbackList.register(cb);
}
@Override
public void deleteAccountPIMData(long accountId) throws RemoteException {
// Not required for IMAP
}
@Override
public int searchMessages(long accountId, SearchParams params, long destMailboxId)
throws RemoteException {
// TODO Auto-generated method stub
return 0;
}
@Override
public void sendMail(long accountId) throws RemoteException {
// Not required for IMAP
}
@Override
public int getCapabilities(long accountId) throws RemoteException {
return AccountCapabilities.SYNCABLE_FOLDERS |
AccountCapabilities.FOLDER_SERVER_SEARCH |
AccountCapabilities.UNDO;
}
};
static public IEmailServiceCallback callback() {
return sCallbackProxy;
}
@Override
public AccountObserver getAccountObserver(Handler handler) {
return new AccountObserver(handler) {
@Override
public void newAccount(long acctId) {
// Create the Inbox for the account
Account acct = Account.restoreAccountWithId(getContext(), acctId);
Mailbox inbox = new Mailbox();
inbox.mDisplayName = "Inbox"; // Localize
inbox.mServerId = "Inbox";
inbox.mAccountKey = acct.mId;
inbox.mType = Mailbox.TYPE_INBOX;
inbox.mSyncInterval = acct.mSyncInterval;
inbox.save(getContext());
log("Creating inbox for account: " + acct.mDisplayName);
Imap2SyncManager.kick("New account");
// Need to sync folder list first; sigh
Imap2SyncService svc = new Imap2SyncService(Imap2SyncManager.this, acct);
try {
svc.loadFolderList();
mResolver.update(
ContentUris.withAppendedId(EmailContent.PICK_TRASH_FOLDER_URI, acctId),
new ContentValues(), null, null);
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
};
}
@Override
public void onStartup() {
// No special behavior
}
private static final String ACCOUNT_KEY_IN = MailboxColumns.ACCOUNT_KEY + " in (";
private String mAccountSelector;
@Override
public String getAccountsSelector() {
if (mAccountSelector == null) {
StringBuilder sb = new StringBuilder(ACCOUNT_KEY_IN);
boolean first = true;
synchronized (mAccountList) {
for (Account account : mAccountList) {
if (!first) {
sb.append(',');
} else {
first = false;
}
sb.append(account.mId);
}
}
sb.append(')');
mAccountSelector = sb.toString();
}
return mAccountSelector;
}
@Override
public AbstractSyncService getServiceForMailbox(Context context, Mailbox mailbox) {
return new Imap2SyncService(context, mailbox);
}
@Override
public AccountList collectAccounts(Context context, AccountList accounts) {
ContentResolver resolver = context.getContentResolver();
Cursor c = resolver.query(Account.CONTENT_URI, Account.CONTENT_PROJECTION, null, null,
null);
// We must throw here; callers might use the information we provide for reconciliation, etc.
if (c == null) throw new ProviderUnavailableException();
try {
while (c.moveToNext()) {
long hostAuthId = c.getLong(Account.CONTENT_HOST_AUTH_KEY_RECV_COLUMN);
if (hostAuthId > 0) {
HostAuth ha = HostAuth.restoreHostAuthWithId(context, hostAuthId);
if (ha != null && ha.mProtocol.equals("imap2")) {
Account account = new Account();
account.restore(c);
account.mHostAuthRecv = ha;
accounts.add(account);
}
}
}
} finally {
c.close();
}
return accounts;
}
@Override
public String getAccountManagerType() {
return "com.android.imap2";
}
@Override
public String getServiceIntentAction() {
return "com.android.email.IMAP2_INTENT";
}
@Override
public Stub getCallbackProxy() {
return sCallbackProxy;
}
@Override
protected void runAccountReconcilerSync(Context context) {
alwaysLog("Reconciling accounts...");
new AccountServiceProxy(context).reconcileAccounts("imap2", getAccountManagerType());
}
@Override
public IBinder onBind(Intent intent) {
return mBinder;
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,189 @@
/*
* Copyright (C) 2012 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.imap2;
import android.content.Context;
import android.os.Build;
import android.telephony.TelephonyManager;
import android.util.Base64;
import android.util.Log;
import com.android.emailcommon.Device;
import com.android.emailcommon.Logging;
import com.android.emailcommon.VendorPolicyLoader;
import com.google.common.annotations.VisibleForTesting;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.regex.Pattern;
public class ImapId {
private static String sImapId;
/**
* Return, or create and return, an string suitable for use in an IMAP ID message.
* This is constructed similarly to the way the browser sets up its user-agent strings.
* See RFC 2971 for more details. The output of this command will be a series of key-value
* pairs delimited by spaces (there is no point in returning a structured result because
* this will be sent as-is to the IMAP server). No tokens, parenthesis or "ID" are included,
* because some connections may append additional values.
*
* The following IMAP ID keys may be included:
* name Android package name of the program
* os "android"
* os-version "version; model; build-id"
* vendor Vendor of the client/server
* x-android-device-model Model (only revealed if release build)
* x-android-net-operator Mobile network operator (if known)
* AGUID A device+account UID
*
* In addition, a vendor policy .apk can append key/value pairs.
*
* @param userName the username of the account
* @param host the host (server) of the account
* @param capabilities a list of the capabilities from the server
* @return a String for use in an IMAP ID message.
*/
public static String getImapId(Context context, String userName, String host,
String capabilities) {
// The first section is global to all IMAP connections, and generates the fixed
// values in any IMAP ID message
synchronized (ImapId.class) {
if (sImapId == null) {
TelephonyManager tm =
(TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE);
String networkOperator = tm.getNetworkOperatorName();
if (networkOperator == null) networkOperator = "";
sImapId = makeCommonImapId(context.getPackageName(), Build.VERSION.RELEASE,
Build.VERSION.CODENAME, Build.MODEL, Build.ID, Build.MANUFACTURER,
networkOperator);
}
}
// This section is per Store, and adds in a dynamic elements like UID's.
// We don't cache the result of this work, because the caller does anyway.
StringBuilder id = new StringBuilder(sImapId);
// Optionally add any vendor-supplied id keys
String vendorId = VendorPolicyLoader.getInstance(context).getImapIdValues(userName, host,
capabilities);
if (vendorId != null) {
id.append(' ');
id.append(vendorId);
}
// Generate a UID that mixes a "stable" device UID with the email address
try {
String devUID = Device.getConsistentDeviceId(context);
MessageDigest messageDigest;
messageDigest = MessageDigest.getInstance("SHA-1");
messageDigest.update(userName.getBytes());
messageDigest.update(devUID.getBytes());
byte[] uid = messageDigest.digest();
String hexUid = Base64.encodeToString(uid, Base64.NO_WRAP);
id.append(" \"AGUID\" \"");
id.append(hexUid);
id.append('\"');
} catch (NoSuchAlgorithmException e) {
Log.d(Logging.LOG_TAG, "couldn't obtain SHA-1 hash for device UID");
}
return id.toString();
}
/**
* Helper function that actually builds the static part of the IMAP ID string. This is
* separated from getImapId for testability. There is no escaping or encoding in IMAP ID so
* any rogue chars must be filtered here.
*
* @param packageName context.getPackageName()
* @param version Build.VERSION.RELEASE
* @param codeName Build.VERSION.CODENAME
* @param model Build.MODEL
* @param id Build.ID
* @param vendor Build.MANUFACTURER
* @param networkOperator TelephonyManager.getNetworkOperatorName()
* @return the static (never changes) portion of the IMAP ID
*/
@VisibleForTesting
static String makeCommonImapId(String packageName, String version,
String codeName, String model, String id, String vendor, String networkOperator) {
// Before building up IMAP ID string, pre-filter the input strings for "legal" chars
// This is using a fairly arbitrary char set intended to pass through most reasonable
// version, model, and vendor strings: a-z A-Z 0-9 - _ + = ; : . , / <space>
// The most important thing is *not* to pass parens, quotes, or CRLF, which would break
// the format of the IMAP ID list.
Pattern p = Pattern.compile("[^a-zA-Z0-9-_\\+=;:\\.,/ ]");
packageName = p.matcher(packageName).replaceAll("");
version = p.matcher(version).replaceAll("");
codeName = p.matcher(codeName).replaceAll("");
model = p.matcher(model).replaceAll("");
id = p.matcher(id).replaceAll("");
vendor = p.matcher(vendor).replaceAll("");
networkOperator = p.matcher(networkOperator).replaceAll("");
// "name" "com.android.email"
StringBuffer sb = new StringBuffer("\"name\" \"");
sb.append(packageName);
sb.append("\"");
// "os" "android"
sb.append(" \"os\" \"android\"");
// "os-version" "version; build-id"
sb.append(" \"os-version\" \"");
if (version.length() > 0) {
sb.append(version);
} else {
// default to "1.0"
sb.append("1.0");
}
// add the build ID or build #
if (id.length() > 0) {
sb.append("; ");
sb.append(id);
}
sb.append("\"");
// "vendor" "the vendor"
if (vendor.length() > 0) {
sb.append(" \"vendor\" \"");
sb.append(vendor);
sb.append("\"");
}
// "x-android-device-model" the device model (on release builds only)
if ("REL".equals(codeName)) {
if (model.length() > 0) {
sb.append(" \"x-android-device-model\" \"");
sb.append(model);
sb.append("\"");
}
}
// "x-android-mobile-net-operator" "name of network operator"
if (networkOperator.length() > 0) {
sb.append(" \"x-android-mobile-net-operator\" \"");
sb.append(networkOperator);
sb.append("\"");
}
return sb.toString();
}
}

View File

@ -0,0 +1,48 @@
/*
* Copyright (C) 2012 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.imap2;
import java.io.FilterInputStream;
import java.io.IOException;
import java.io.InputStream;
public class ImapInputStream extends FilterInputStream {
public ImapInputStream(InputStream in) {
super(in);
}
public String readLine () throws IOException {
StringBuilder sb = new StringBuilder();
while (true) {
int b = read();
// Line ends with \n; ignore \r
// I'm not sure this is the right thing with a raw \r (no \n following)
if (b < 0)
throw new IOException("Socket closed in readLine");
if (b == '\n')
return sb.toString();
else if (b != '\r') {
sb.append((char)b);
}
}
}
public boolean ready () throws IOException {
return this.available() > 0;
}
}

View File

@ -0,0 +1,202 @@
/*
* Copyright (C) 2012 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.imap2;
public class Parser {
String str;
int pos;
int len;
static final String white = "\r\n \t";
public Parser (String _str) {
str = _str;
pos = 0;
len = str.length();
}
public Parser (String _str, int start) {
str = _str;
pos = start;
len = str.length();
}
public void skipWhite () {
while ((pos < len) && white.indexOf(str.charAt(pos)) >= 0)
pos++;
}
public String parseAtom () {
skipWhite();
int start = pos;
while ((pos < len) && white.indexOf(str.charAt(pos)) < 0)
pos++;
if (pos > start)
return str.substring(start, pos);
return null;
}
public char nextChar () {
if (pos >= len)
return 0;
else
return str.charAt(pos++);
}
public char peekChar () {
if (pos >= len)
return 0;
else
return str.charAt(pos);
}
public String parseString () {
return parseString(false);
}
public String parseStringOrAtom () {
return parseString(true);
}
public String parseString (boolean orAtom) {
skipWhite();
char c = nextChar();
if (c != '\"') {
if (c == '{') {
int cnt = parseInteger();
c = nextChar();
if (c != '}')
return null;
int start = pos + 2;
int end = start + cnt;
String s = str.substring(start, end);
pos = end;
return s;
} else if (orAtom) {
backChar();
return parseAtom();
} else if (c == 'n' || c == 'N') {
parseAtom();
return null;
} else
return null;
}
int start = pos;
boolean quote = false;
while (true) {
c = nextChar();
if (c == 0)
return null;
else if (quote)
quote = false;
else if (c == '\\')
quote = true;
else if (c == '\"')
break;
}
return str.substring(start, pos - 1);
}
public void backChar () {
if (pos > 0)
pos--;
}
public String parseListOrNil () {
String list = parseList();
if (list == null) {
parseAtom();
list = "";
}
return list;
}
public String parseList () {
skipWhite();
if (nextChar() != '(') {
backChar();
return null;
}
int start = pos;
int level = 0;
boolean quote = false;
boolean string = false;
while (true) {
char c = nextChar();
if (c == 0)
return null;
else if (quote)
quote = false;
else if (c == '\\' && string)
quote = true;
else if (c == '\"')
string = !string;
else if (c == '(' && !string)
level++;
else if (c == ')' && !string) {
if (level-- == 0)
break;
}
}
return str.substring(start, pos - 1);
}
public Integer parseInteger () {
skipWhite();
int start = pos;
while (pos < len) {
char c = str.charAt(pos);
if (c >= '0' && c <= '9')
pos++;
else
break;
}
if (pos > start) {
try {
Integer i = Integer.parseInt(str.substring(start, pos));
return i;
} catch (NumberFormatException e) {
return -1;
}
} else
return -1;
}
public int[] gatherInts () {
int[] list = new int[128];
int size = 128;
int offs = 0;
while (true) {
// TODO Slow; handle this inline rather than calling the method
Integer i = parseInteger();
if (i >= 0) {
if (offs == size) {
// Double the size of the array as necessary
size <<= 1;
int[] tmp = new int[size];
System.arraycopy(list, 0, tmp, 0, offs);
list = tmp;
}
list[offs++] = i;
}
else
break;
}
int[] res = new int[offs];
System.arraycopy(list, 0, res, 0, offs);
return res;
}
}

View File

@ -0,0 +1,121 @@
/*
* Copyright (C) 2012 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.imap2;
public class QuotedPrintable {
static public String toString (String str) {
int len = str.length();
// Make sure we don't get an index out of bounds error with the = character
int max = len - 2;
StringBuilder sb = new StringBuilder(len);
try {
for (int i = 0; i < len; i++) {
char c = str.charAt(i);
if (c == '=') {
if (i < max) {
char n = str.charAt(++i);
if (n == '\r') {
n = str.charAt(++i);
if (n == '\n')
continue;
else
System.err.println("Not valid QP");
} else {
// Must be less than 0x80, right?
int a;
if (n >= '0' && n <= '9')
a = (n - '0') << 4;
else
a = (10 + (n - 'A')) << 4;
n = str.charAt(++i);
if (n >= '0' && n <= '9')
c = (char) (a + (n - '0'));
else
c = (char) (a + 10 + (n - 'A'));
}
} if (i + 1 == len)
continue;
}
sb.append(c);
}
} catch (IndexOutOfBoundsException e) {
}
String ret = sb.toString();
return ret;
}
static public String encode (String str) {
int len = str.length();
StringBuffer sb = new StringBuffer(len + len>>2);
int i = 0;
while (i < len) {
char c = str.charAt(i++);
if (c < 0x80) {
sb.append(c);
} else {
sb.append('&');
sb.append('#');
sb.append((int)c);
sb.append(';');
}
}
return sb.toString();
}
static public int decode (byte[] bytes, int len) {
// Make sure we don't get an index out of bounds error with the = character
int max = len - 2;
int pos = 0;
try {
for (int i = 0; i < len; i++) {
char c = (char)bytes[i];
if (c == '=') {
if (i < max) {
char n = (char)bytes[++i];
if (n == '\r') {
n = (char)bytes[++i];
if (n == '\n')
continue;
else
System.err.println("Not valid QP");
} else {
// Must be less than 0x80, right?
int a;
if (n >= '0' && n <= '9')
a = (n - '0') << 4;
else
a = (10 + (n - 'A')) << 4;
n = (char)bytes[++i];
if (n >= '0' && n <= '9')
c = (char) (a + (n - '0'));
else
c = (char) (a + 10 + (n - 'A'));
}
} if (i + 1 > len)
continue;
}
bytes[pos++] = (byte)c;
}
} catch (IndexOutOfBoundsException e) {
}
return pos;
}
}

View File

@ -1300,5 +1300,13 @@ as <xliff:g id="filename">%s</xliff:g>.</string>
<!-- Displayed in the middle of the screen when the inbox is empty [CHAR LIMIT 100]-->
<string name="no_conversations">No messages.</string>
<!-- Temporary; used by AccountManager -->
<string name="imap2_name">Push IMAP</string>
<string name="folder_picker_title">Picky, picky, picky!</string>
<string name="trash_folder_selection_title">Select trash folder</string>
<string name="create_new_folder">Create folder</string>
</resources>

View File

@ -0,0 +1,29 @@
<?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 Account Manager. -->
<account-authenticator xmlns:android="http://schemas.android.com/apk/res/android"
android:accountType="com.android.imap2"
android:icon="@mipmap/ic_launcher_mail"
android:smallIcon="@drawable/stat_notify_email_generic"
android:label="@string/imap2_name"
android:accountPreferences="@xml/account_preferences"
/>

View File

@ -146,6 +146,11 @@
<!-- USA -->
<provider id="eon" label="EON" domain="nextobject.com">
<incoming uri="imap2://imap.everyone.net" username="$email" />
<outgoing uri="smtp://smtp.everyone.net" username="$email" />
</provider>
<!-- AOL and variants -->
<!-- Note, AOL appears to support SSL on both sides, as well -->
<provider id="aim" label="AIM" domain="aim.com">

View File

@ -101,4 +101,21 @@
email:syncContacts="true"
email:syncCalendar="true"
/>
<emailservice
email:protocol="imap2"
email:name="Push IMAP"
email:accountType="com.android.imap2"
email:intent="com.android.email.IMAP2_INTENT"
email:port="143"
email:portSsl="993"
email:syncIntervalStrings="@array/account_settings_check_frequency_entries_push"
email:syncIntervals="@array/account_settings_check_frequency_values_push"
email:defaultSyncInterval="push"
email:offerTls="true"
email:usesSmtp="true"
email:offerAttachmentPreload="true"
email:offerPrefix="true"
email:syncChanges="true"
/>
</emailservices>

View File

@ -26,9 +26,10 @@ import android.util.Log;
import android.widget.EditText;
import com.android.email.R;
import com.android.email.VendorPolicyLoader;
import com.android.email.provider.AccountBackupRestore;
import com.android.emailcommon.Logging;
import com.android.emailcommon.VendorPolicyLoader;
import com.android.emailcommon.VendorPolicyLoader.Provider;
import com.android.emailcommon.provider.Account;
import com.android.emailcommon.provider.HostAuth;
import com.android.emailcommon.provider.EmailContent.AccountColumns;
@ -231,50 +232,6 @@ public class AccountSettingsUtils {
}
}
public static class Provider implements Serializable {
private static final long serialVersionUID = 8511656164616538989L;
public String id;
public String label;
public String domain;
public String incomingUriTemplate;
public String incomingUsernameTemplate;
public String outgoingUriTemplate;
public String outgoingUsernameTemplate;
public String incomingUri;
public String incomingUsername;
public String outgoingUri;
public String outgoingUsername;
public String note;
/**
* Expands templates in all of the provider fields that support them. Currently,
* templates are used in 4 fields -- incoming and outgoing URI and user name.
* @param email user-specified data used to replace template values
*/
public void expandTemplates(String email) {
String[] emailParts = email.split("@");
String user = emailParts[0];
incomingUri = expandTemplate(incomingUriTemplate, email, user);
incomingUsername = expandTemplate(incomingUsernameTemplate, email, user);
outgoingUri = expandTemplate(outgoingUriTemplate, email, user);
outgoingUsername = expandTemplate(outgoingUsernameTemplate, email, user);
}
/**
* Replaces all parameterized values in the given template. The values replaced are
* $domain, $user and $email.
*/
private String expandTemplate(String template, String email, String user) {
String returnString = template;
returnString = returnString.replaceAll("\\$email", email);
returnString = returnString.replaceAll("\\$user", user);
returnString = returnString.replaceAll("\\$domain", domain);
return returnString;
}
}
/**
* Infer potential email server addresses from domain names
*

View File

@ -45,10 +45,10 @@ import com.android.email.EmailAddressValidator;
import com.android.email.R;
import com.android.email.activity.ActivityHelper;
import com.android.email.activity.UiUtilities;
import com.android.email.activity.setup.AccountSettingsUtils.Provider;
import com.android.email.service.EmailServiceUtils;
import com.android.email.service.EmailServiceUtils.EmailServiceInfo;
import com.android.emailcommon.Logging;
import com.android.emailcommon.VendorPolicyLoader.Provider;
import com.android.emailcommon.provider.Account;
import com.android.emailcommon.provider.EmailContent;
import com.android.emailcommon.provider.HostAuth;

View File

@ -44,7 +44,6 @@ import com.android.email.service.MailService;
import com.android.email2.ui.MailActivityEmail;
import com.android.emailcommon.Logging;
import com.android.emailcommon.provider.Account;
import com.android.emailcommon.provider.HostAuth;
import com.android.emailcommon.provider.Policy;
import com.android.emailcommon.service.SyncWindow;
import com.android.emailcommon.utility.Utility;
@ -244,6 +243,7 @@ public class AccountSetupOptions extends AccountSetupActivity implements OnClick
final boolean email2 = email;
final boolean calendar2 = calendar;
final boolean contacts2 = contacts;
Utility.runAsync(new Runnable() {
@Override
public void run() {

View File

@ -27,7 +27,6 @@ import android.util.Log;
import com.android.email.LegacyConversions;
import com.android.email.Preferences;
import com.android.email.R;
import com.android.email.VendorPolicyLoader;
import com.android.email.mail.Store;
import com.android.email.mail.Transport;
import com.android.email.mail.store.imap.ImapConstants;
@ -35,6 +34,7 @@ import com.android.email.mail.store.imap.ImapResponse;
import com.android.email.mail.store.imap.ImapString;
import com.android.email.mail.transport.MailTransport;
import com.android.emailcommon.Logging;
import com.android.emailcommon.VendorPolicyLoader;
import com.android.emailcommon.internet.MimeMessage;
import com.android.emailcommon.mail.AuthenticationFailedException;
import com.android.emailcommon.mail.Flag;
@ -182,8 +182,8 @@ public class ImapStore extends Store {
* @param capabilities a list of the capabilities from the server
* @return a String for use in an IMAP ID message.
*/
@VisibleForTesting
static String getImapId(Context context, String userName, String host, String capabilities) {
public static String getImapId(Context context, String userName, String host,
String capabilities) {
// The first section is global to all IMAP connections, and generates the fixed
// values in any IMAP ID message
synchronized (ImapStore.class) {

View File

@ -126,8 +126,9 @@ public final class DBHelper {
// Version 100 is first Email2 version
// Version 101 SHOULD NOT BE USED
// Version 102&103: Add hierarchicalName to Mailbox
// Version 104&105: add syncData to Message
public static final int DATABASE_VERSION = 103;
public static final int DATABASE_VERSION = 105;
// Any changes to the database format *must* include update-in-place code.
// Original version: 2
@ -172,7 +173,8 @@ public final class DBHelper {
+ MessageColumns.MEETING_INFO + " text, "
+ MessageColumns.SNIPPET + " text, "
+ MessageColumns.PROTOCOL_SEARCH_INFO + " text, "
+ MessageColumns.THREAD_TOPIC + " text"
+ MessageColumns.THREAD_TOPIC + " text, "
+ MessageColumns.SYNC_DATA + " text"
+ ");";
// This String and the following String MUST have the same columns, except for the type
@ -968,10 +970,32 @@ public final class DBHelper {
+ " add " + MailboxColumns.HIERARCHICAL_NAME + " text");
} catch (SQLException e) {
// Shouldn't be needed unless we're debugging and interrupt the process
Log.w(TAG, "Exception upgrading EmailProviderBody.db from v6 to v8", e);
Log.w(TAG, "Exception upgrading EmailProviderBody.db from v10x to v103", e);
}
oldVersion = 103;
}
if (oldVersion == 103) {
try {
db.execSQL("alter table " + Message.TABLE_NAME
+ " add " + MessageColumns.SYNC_DATA + " text");
} catch (SQLException e) {
// Shouldn't be needed unless we're debugging and interrupt the process
Log.w(TAG, "Exception upgrading EmailProviderBody.db from v103 to v104", e);
}
oldVersion = 104;
}
if (oldVersion == 104) {
try {
db.execSQL("alter table " + Message.UPDATED_TABLE_NAME
+ " add " + MessageColumns.SYNC_DATA + " text");
db.execSQL("alter table " + Message.DELETED_TABLE_NAME
+ " add " + MessageColumns.SYNC_DATA + " text");
} catch (SQLException e) {
// Shouldn't be needed unless we're debugging and interrupt the process
Log.w(TAG, "Exception upgrading EmailProviderBody.db from v104 to v105", e);
}
oldVersion = 105;
}
}
@Override

View File

@ -77,12 +77,17 @@ import com.android.emailcommon.service.IEmailServiceCallback;
import com.android.emailcommon.service.SearchParams;
import com.android.emailcommon.utility.AttachmentUtilities;
import com.android.emailcommon.utility.Utility;
import com.android.mail.providers.Conversation;
import com.android.mail.providers.Folder;
import com.android.mail.providers.UIProvider;
import com.android.mail.providers.UIProvider.AccountCapabilities;
import com.android.mail.providers.UIProvider.AccountCursorExtraKeys;
import com.android.mail.providers.UIProvider.ConversationPriority;
import com.android.mail.providers.UIProvider.ConversationSendingState;
import com.android.mail.providers.UIProvider.DraftType;
import com.android.mail.ui.ConversationUpdater;
import com.android.mail.ui.DestructiveAction;
import com.android.mail.ui.FoldersSelectionDialog;
import com.android.mail.utils.LogUtils;
import com.android.mail.utils.MatrixCursorWithExtra;
import com.android.mail.utils.Utils;
@ -181,6 +186,7 @@ public class EmailProvider extends ContentProvider {
private static final int ACCOUNT_RESET_NEW_COUNT_ID = ACCOUNT_BASE + 4;
private static final int ACCOUNT_DEFAULT_ID = ACCOUNT_BASE + 5;
private static final int ACCOUNT_CHECK = ACCOUNT_BASE + 6;
private static final int ACCOUNT_PICK_TRASH_FOLDER = ACCOUNT_BASE + 7;
private static final int MAILBOX_BASE = 0x1000;
private static final int MAILBOX = MAILBOX_BASE;
@ -194,6 +200,7 @@ public class EmailProvider extends ContentProvider {
private static final int MESSAGE = MESSAGE_BASE;
private static final int MESSAGE_ID = MESSAGE_BASE + 1;
private static final int SYNCED_MESSAGE_ID = MESSAGE_BASE + 2;
private static final int SYNCED_MESSAGE_SELECTION = MESSAGE_BASE + 3;
private static final int ATTACHMENT_BASE = 0x3000;
private static final int ATTACHMENT = ATTACHMENT_BASE;
@ -412,6 +419,8 @@ public class EmailProvider extends ContentProvider {
* TO A SERVER VIA A SYNC ADAPTER
*/
matcher.addURI(EmailContent.AUTHORITY, "syncedMessage/#", SYNCED_MESSAGE_ID);
matcher.addURI(EmailContent.AUTHORITY, "syncedMessageSelection",
SYNCED_MESSAGE_SELECTION);
/**
* THE URIs BELOW THIS POINT ARE INTENDED TO BE USED BY SYNC ADAPTERS ONLY
@ -465,6 +474,7 @@ public class EmailProvider extends ContentProvider {
matcher.addURI(EmailContent.AUTHORITY, "uirecentfolders/#", UI_RECENT_FOLDERS);
matcher.addURI(EmailContent.AUTHORITY, "uidefaultrecentfolders/#",
UI_DEFAULT_RECENT_FOLDERS);
matcher.addURI(EmailContent.AUTHORITY, "pickTrashFolder/#", ACCOUNT_PICK_TRASH_FOLDER);
}
/**
@ -784,6 +794,21 @@ public class EmailProvider extends ContentProvider {
return uiDeleteAccountData(uri);
case UI_ACCOUNT:
return uiDeleteAccount(uri);
case SYNCED_MESSAGE_SELECTION:
Cursor findCursor = db.query(tableName, Message.ID_COLUMN_PROJECTION, selection,
selectionArgs, null, null, null);
try {
if (findCursor.moveToFirst()) {
return delete(ContentUris.withAppendedId(
Message.SYNCED_CONTENT_URI,
findCursor.getLong(Message.ID_COLUMNS_ID_COLUMN)),
null, null);
} else {
return 0;
}
} finally {
findCursor.close();
}
// These are cases in which one or more Messages might get deleted, either by
// cascade or explicitly
case MAILBOX_ID:
@ -1627,13 +1652,10 @@ public class EmailProvider extends ContentProvider {
String id = "0";
try {
if (match == MESSAGE_ID || match == SYNCED_MESSAGE_ID) {
if (!uri.getBooleanQueryParameter(IS_UIPROVIDER, false)) {
notifyUIConversation(uri);
}
}
outer:
switch (match) {
case ACCOUNT_PICK_TRASH_FOLDER:
return pickTrashFolder(uri);
case UI_FOLDER:
return uiUpdateFolder(uri, values);
case UI_RECENT_FOLDERS:
@ -1706,6 +1728,21 @@ outer:
}
}
break;
case SYNCED_MESSAGE_SELECTION:
Cursor findCursor = db.query(tableName, Message.ID_COLUMN_PROJECTION, selection,
selectionArgs, null, null, null);
try {
if (findCursor.moveToFirst()) {
return update(ContentUris.withAppendedId(
Message.SYNCED_CONTENT_URI,
findCursor.getLong(Message.ID_COLUMNS_ID_COLUMN)),
values, null, null);
} else {
return 0;
}
} finally {
findCursor.close();
}
case SYNCED_MESSAGE_ID:
case UPDATED_MESSAGE_ID:
case MESSAGE_ID:
@ -1741,7 +1778,11 @@ outer:
cache.unlock(id, values);
}
}
if (match == ATTACHMENT_ID) {
if (match == MESSAGE_ID || match == SYNCED_MESSAGE_ID) {
if (!uri.getBooleanQueryParameter(IS_UIPROVIDER, false)) {
notifyUIConversation(uri);
}
} else if (match == ATTACHMENT_ID) {
long attId = Integer.parseInt(id);
if (values.containsKey(Attachment.FLAGS)) {
int flags = values.getAsInteger(Attachment.FLAGS);
@ -3698,8 +3739,12 @@ outer:
return update(ourUri, ourValues, null, null);
}
public static final String PICKER_UI_ACCOUNT = "picker_ui_account";
public static final String PICKER_MAILBOX_TYPE = "picker_mailbox_type";
public static final String PICKER_MESSAGE_ID = "picker_message_id";
private int uiDeleteMessage(Uri uri) {
Context context = getContext();
final Context context = getContext();
Message msg = getMessageFromLastSegment(uri);
if (msg == null) return 0;
Mailbox mailbox = Mailbox.restoreMailboxWithId(context, msg.mMailboxKey);
@ -3709,17 +3754,42 @@ outer:
AttachmentUtilities.deleteAllAttachmentFiles(context, msg.mAccountKey, msg.mId);
notifyUI(UIPROVIDER_FOLDER_NOTIFIER, mailbox.mId);
return context.getContentResolver().delete(
ContentUris.withAppendedId(Message.CONTENT_URI, msg.mId), null, null);
ContentUris.withAppendedId(Message.SYNCED_CONTENT_URI, msg.mId), null, null);
}
Mailbox trashMailbox =
Mailbox.restoreMailboxOfType(context, msg.mAccountKey, Mailbox.TYPE_TRASH);
if (trashMailbox == null) return 0;
if (trashMailbox == null) {
return 0;
}
ContentValues values = new ContentValues();
values.put(MessageColumns.MAILBOX_KEY, trashMailbox.mId);
notifyUI(UIPROVIDER_FOLDER_NOTIFIER, mailbox.mId);
return uiUpdateMessage(uri, values);
}
private int pickTrashFolder(Uri uri) {
Context context = getContext();
Long acctId = Long.parseLong(uri.getLastPathSegment());
// For push imap, for example, we want the user to select the trash mailbox
Cursor ac = query(uiUri("uiaccount", acctId), UIProvider.ACCOUNTS_PROJECTION,
null, null, null);
try {
if (ac.moveToFirst()) {
final com.android.mail.providers.Account uiAccount =
new com.android.mail.providers.Account(ac);
Intent intent = new Intent(context, FolderPickerActivity.class);
intent.putExtra(PICKER_UI_ACCOUNT, uiAccount);
intent.putExtra(PICKER_MAILBOX_TYPE, Mailbox.TYPE_TRASH);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
context.startActivity(intent);
return 1;
}
return 0;
} finally {
ac.close();
}
}
private Cursor uiUndo(String[] projection) {
// First see if we have any operations saved
// TODO: Make sure seq matches

View File

@ -0,0 +1,75 @@
/*
* Copyright (C) 2012 Google Inc.
* Licensed to 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.email.provider;
import android.app.Activity;
import android.content.ContentUris;
import android.content.ContentValues;
import android.content.Intent;
import android.os.Bundle;
import com.android.emailcommon.provider.Mailbox;
import com.android.emailcommon.provider.EmailContent.MailboxColumns;
import com.android.mail.providers.Folder;
public class FolderPickerActivity extends Activity implements FolderPickerCallback {
private long mAccountId;
private int mMailboxType;
public void onCreate(Bundle bundle) {
super.onCreate(bundle);
Intent i = getIntent();
com.android.mail.providers.Account account =
i.getParcelableExtra(EmailProvider.PICKER_UI_ACCOUNT);
mAccountId = Long.parseLong(account.uri.getLastPathSegment());
mMailboxType = i.getIntExtra(EmailProvider.PICKER_MAILBOX_TYPE, -1);
new FolderSelectionDialog(this, account, this).show();
}
@Override
public void select(Folder folder) {
String folderId = folder.uri.getLastPathSegment();
Long id = Long.parseLong(folderId);
ContentValues values = new ContentValues();
// If we already have a mailbox of this type, change it back to generic mail type
Mailbox ofType = Mailbox.restoreMailboxOfType(this, mAccountId, mMailboxType);
if (ofType != null) {
values.put(MailboxColumns.TYPE, Mailbox.TYPE_MAIL);
getContentResolver().update(
ContentUris.withAppendedId(Mailbox.CONTENT_URI, ofType.mId), values,
null, null);
}
// Change this mailbox to be of the desired type
Mailbox mailbox = Mailbox.restoreMailboxWithId(this, id);
if (mailbox != null) {
values.put(MailboxColumns.TYPE, mMailboxType);
getContentResolver().update(
ContentUris.withAppendedId(Mailbox.CONTENT_URI, mailbox.mId), values,
null, null);
}
finish();
}
@Override
public void create() {
// TODO: Not sure about this...
finish();
}
}

View File

@ -0,0 +1,25 @@
/*
* Copyright (C) 2012 Google Inc.
* Licensed to 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.email.provider;
import com.android.mail.providers.Folder;
public interface FolderPickerCallback {
public void select(Folder folder);
public void create();
}

View File

@ -0,0 +1,145 @@
/*
* Copyright (C) 2012 Google Inc.
* Licensed to 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.email.provider;
import android.app.AlertDialog;
import android.content.Context;
import android.content.DialogInterface;
import android.content.DialogInterface.OnClickListener;
import android.content.DialogInterface.OnMultiChoiceClickListener;
import android.database.Cursor;
import android.view.View;
import android.widget.AdapterView;
import com.android.mail.R;
import com.android.mail.providers.Account;
import com.android.mail.providers.Folder;
import com.android.mail.providers.UIProvider;
import com.android.mail.ui.FolderSelectorAdapter;
import com.android.mail.ui.FolderSelectorAdapter.FolderRow;
import com.android.mail.ui.HierarchicalFolderSelectorAdapter;
import com.android.mail.ui.SeparatedFolderListAdapter;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map.Entry;
public class FolderSelectionDialog implements OnClickListener, OnMultiChoiceClickListener {
private AlertDialog mDialog;
private HashMap<Folder, Boolean> mCheckedState;
private SeparatedFolderListAdapter mAdapter;
final private FolderPickerCallback mCallback;
public FolderSelectionDialog(final Context context, Account account,
FolderPickerCallback callback) {
mCallback = callback;
// Mapping of a folder's uri to its checked state
mCheckedState = new HashMap<Folder, Boolean>();
AlertDialog.Builder builder = new AlertDialog.Builder(context);
builder.setTitle(R.string.trash_folder_selection_title);
builder.setPositiveButton(R.string.ok, this);
builder.setNegativeButton(R.string.create_new_folder, this);
final Cursor foldersCursor = context.getContentResolver().query(
account.fullFolderListUri != null ? account.fullFolderListUri
: account.folderListUri, UIProvider.FOLDERS_PROJECTION, null, null, null);
try {
mAdapter = new SeparatedFolderListAdapter(context);
String[] headers = context.getResources()
.getStringArray(R.array.moveto_folder_sections);
mAdapter.addSection(headers[2], new HierarchicalFolderSelectorAdapter(context,
foldersCursor, new HashSet<String>(), true));
builder.setAdapter(mAdapter, this);
} finally {
foldersCursor.close();
}
mDialog = builder.create();
}
public void show() {
mDialog.show();
mDialog.getListView().setOnItemClickListener(new AdapterView.OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
Object item = mAdapter.getItem(position);
if (item instanceof FolderRow) {
update((FolderRow) item);
}
}
});
}
/**
* Call this to update the state of folders as a result of them being
* selected / de-selected.
*
* @param row The item being updated.
*/
public void update(FolderSelectorAdapter.FolderRow row) {
// Update the UI
final boolean add = !row.isPresent();
if (!add) {
// This would remove the check on a single radio button, so just
// return.
return;
}
// Clear any other checked items.
mAdapter.getCount();
for (int i = 0; i < mAdapter.getCount(); i++) {
Object item = mAdapter.getItem(i);
if (item instanceof FolderRow) {
((FolderRow)item).setIsPresent(false);
}
}
mCheckedState.clear();
row.setIsPresent(add);
mAdapter.notifyDataSetChanged();
mCheckedState.put(row.getFolder(), add);
}
@Override
public void onClick(DialogInterface dialog, int which) {
switch (which) {
case DialogInterface.BUTTON_POSITIVE:
Folder folder = null;
for (Entry<Folder, Boolean> entry : mCheckedState.entrySet()) {
if (entry.getValue()) {
folder = entry.getKey();
break;
}
}
mCallback.select(folder);
break;
case DialogInterface.BUTTON_NEGATIVE:
mCallback.create();
break;
default:
onClick(dialog, which, true);
break;
}
}
@Override
public void onClick(DialogInterface dialog, int which, boolean isChecked) {
final FolderRow row = (FolderRow) mAdapter.getItem(which);
// Clear any other checked items.
mCheckedState.clear();
isChecked = true;
mCheckedState.put(row.getFolder(), isChecked);
mDialog.getListView().setItemChecked(which, false);
}
}

View File

@ -26,11 +26,11 @@ import android.os.IBinder;
import com.android.email.NotificationController;
import com.android.email.ResourceHelper;
import com.android.email.VendorPolicyLoader;
import com.android.email.provider.AccountReconciler;
import com.android.email2.ui.MailActivityEmail;
import com.android.emailcommon.Configuration;
import com.android.emailcommon.Device;
import com.android.emailcommon.VendorPolicyLoader;
import com.android.emailcommon.provider.Account;
import com.android.emailcommon.provider.HostAuth;
import com.android.emailcommon.service.IAccountService;

View File

@ -32,9 +32,9 @@ import android.util.Log;
import com.android.email.NotificationController;
import com.android.email.Preferences;
import com.android.email.SecurityPolicy;
import com.android.email.VendorPolicyLoader;
import com.android.email.activity.setup.AccountSettings;
import com.android.emailcommon.Logging;
import com.android.emailcommon.VendorPolicyLoader;
import com.android.emailcommon.provider.Account;
import com.android.emailcommon.provider.EmailContent.AccountColumns;
import com.android.emailcommon.provider.HostAuth;

View File

@ -0,0 +1,23 @@
/*
* 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.email.service;
/**
* This service needs to be declared separately from the base service
*/
public class Imap2AuthenticatorService extends AuthenticatorService {
}

View File

@ -56,6 +56,7 @@ import com.android.emailcommon.provider.EmailContent.MailboxColumns;
import com.android.emailcommon.provider.EmailContent.MessageColumns;
import com.android.emailcommon.provider.EmailContent.SyncColumns;
import com.android.emailcommon.provider.Mailbox;
import com.android.emailcommon.service.EmailServiceCallback;
import com.android.emailcommon.service.EmailServiceStatus;
import com.android.emailcommon.service.IEmailServiceCallback;
import com.android.emailcommon.service.SearchParams;
@ -106,102 +107,8 @@ public class ImapService extends Service {
private static final RemoteCallbackList<IEmailServiceCallback> mCallbackList =
new RemoteCallbackList<IEmailServiceCallback>();
private interface ServiceCallbackWrapper {
public void call(IEmailServiceCallback cb) throws RemoteException;
}
/**
* Proxy that can be used by various sync adapters to tie into ExchangeService's callback system
* Used this way: ExchangeService.callback().callbackMethod(args...);
* The proxy wraps checking for existence of a ExchangeService instance
* Failures of these callbacks can be safely ignored.
*/
static private final IEmailServiceCallback.Stub sCallbackProxy =
new IEmailServiceCallback.Stub() {
/**
* Broadcast a callback to the everyone that's registered
*
* @param wrapper the ServiceCallbackWrapper used in the broadcast
*/
private synchronized void broadcastCallback(ServiceCallbackWrapper wrapper) {
RemoteCallbackList<IEmailServiceCallback> callbackList = mCallbackList;
if (callbackList != null) {
// Call everyone on our callback list
int count = callbackList.beginBroadcast();
try {
for (int i = 0; i < count; i++) {
try {
wrapper.call(callbackList.getBroadcastItem(i));
} catch (RemoteException e) {
// Safe to ignore
} catch (RuntimeException e) {
// We don't want an exception in one call to prevent other calls, so
// we'll just log this and continue
Log.e(TAG, "Caught RuntimeException in broadcast", e);
}
}
} finally {
// No matter what, we need to finish the broadcast
callbackList.finishBroadcast();
}
}
}
@Override
public void loadAttachmentStatus(final long messageId, final long attachmentId,
final int status, final int progress) {
broadcastCallback(new ServiceCallbackWrapper() {
@Override
public void call(IEmailServiceCallback cb) throws RemoteException {
cb.loadAttachmentStatus(messageId, attachmentId, status, progress);
}
});
}
@Override
public void loadMessageStatus(final long messageId, final int status, final int progress) {
broadcastCallback(new ServiceCallbackWrapper() {
@Override
public void call(IEmailServiceCallback cb) throws RemoteException {
cb.loadMessageStatus(messageId, status, progress);
}
});
}
@Override
public void sendMessageStatus(final long messageId, final String subject, final int status,
final int progress) {
broadcastCallback(new ServiceCallbackWrapper() {
@Override
public void call(IEmailServiceCallback cb) throws RemoteException {
cb.sendMessageStatus(messageId, subject, status, progress);
}
});
}
@Override
public void syncMailboxListStatus(final long accountId, final int status,
final int progress) {
broadcastCallback(new ServiceCallbackWrapper() {
@Override
public void call(IEmailServiceCallback cb) throws RemoteException {
cb.syncMailboxListStatus(accountId, status, progress);
}
});
}
@Override
public void syncMailboxStatus(final long mailboxId, final int status,
final int progress) {
broadcastCallback(new ServiceCallbackWrapper() {
@Override
public void call(IEmailServiceCallback cb) throws RemoteException {
cb.syncMailboxStatus(mailboxId, status, progress);
}
});
}
};
private static final EmailServiceCallback sCallbackProxy =
new EmailServiceCallback(mCallbackList);
/**
* Create our EmailService implementation here.
@ -243,10 +150,7 @@ public class ImapService extends Service {
}
private static void sendMailboxStatus(Mailbox mailbox, int status) {
try {
sCallbackProxy.syncMailboxStatus(mailbox.mId, status, 0);
} catch (RemoteException e) {
}
sCallbackProxy.syncMailboxStatus(mailbox.mId, status, 0);
}
/**

View File

@ -1,9 +0,0 @@
# Copyright 2010 Google Inc. All Rights Reserved.
#Fri Jul 16 10:03:09 PDT 2010
currentVersion=1.0.0
version=1.0.0
isNative=false
name=utf7_support
keywords=utf7 support
onDevice=true
homepage=http\://sourceforge.net/projects/jutf7/