summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorMarc Blank <mblank@google.com>2012-06-28 12:16:59 -0700
committerMarc Blank <mblank@google.com>2012-06-28 12:16:59 -0700
commitc5afb16430a145f20d7c887e45f47b38687054da (patch)
tree131ec20d0b66d2bdd6eec9eb1eafd2f0faa61ff1
parentf419287f22ae44f25e1ba1f757ec33c7941bbfa8 (diff)
downloadandroid_packages_apps_Email-c5afb16430a145f20d7c887e45f47b38687054da.tar.gz
android_packages_apps_Email-c5afb16430a145f20d7c887e45f47b38687054da.tar.bz2
android_packages_apps_Email-c5afb16430a145f20d7c887e45f47b38687054da.zip
Add a bunch of stuff missed earlier
Change-Id: I7f707446a963912fe5786dacb5569e68db572d1c
-rw-r--r--emailsync/Android.mk30
-rw-r--r--emailsync/src/com/android/emailsync/AbstractSyncService.java305
-rw-r--r--emailsync/src/com/android/emailsync/EmailSyncAlarmReceiver.java111
-rw-r--r--emailsync/src/com/android/emailsync/FileLogger.java120
-rw-r--r--emailsync/src/com/android/emailsync/MailboxAlarmReceiver.java42
-rw-r--r--emailsync/src/com/android/emailsync/PartRequest.java51
-rw-r--r--emailsync/src/com/android/emailsync/Request.java36
-rw-r--r--emailsync/src/com/android/emailsync/SyncServiceManager.java2254
-rw-r--r--res/layout-sw600dp/account_type.xml23
-rw-r--r--res/layout/account_type.xml27
-rw-r--r--res/layout/conversation_item_view_normal.xml102
-rw-r--r--res/layout/conversation_item_view_wide.xml100
-rw-r--r--res/layout/quick_response_edit_dialog.xml32
-rw-r--r--res/mipmap-hdpi/ic_launcher_mail.pngbin0 -> 7010 bytes
-rw-r--r--res/mipmap-mdpi/ic_launcher_mail.pngbin0 -> 3846 bytes
-rw-r--r--res/mipmap-xhdpi/ic_launcher_mail.pngbin0 -> 10781 bytes
-rw-r--r--res/values-sw720dp-land/dimensions.xml24
-rw-r--r--res/xml/searchable.xml25
-rw-r--r--res/xml/services.xml104
-rw-r--r--src/com/android/email/activity/EventViewer.java76
-rw-r--r--src/com/android/email/activity/setup/AccountSetupType.java138
-rw-r--r--src/com/android/email/activity/setup/EmailPreferenceFragment.java39
-rw-r--r--src/com/android/email/activity/setup/ForwardingIntent.java30
-rw-r--r--src/com/android/email/activity/setup/PolicyListPreference.java45
-rw-r--r--src/com/android/email/provider/DBHelper.java1292
-rw-r--r--src/com/android/email/provider/Utilities.java150
-rw-r--r--src/com/android/email/service/AuthenticatorService.java159
-rw-r--r--src/com/android/email/service/EmailServiceStub.java561
-rw-r--r--src/com/android/email/service/ImapService.java1396
-rw-r--r--src/com/android/email/service/ImapTempFileLiteral.java127
-rw-r--r--src/com/android/email/service/Pop3Service.java726
-rw-r--r--src/com/android/email2/ui/CreateShortcutActivityEmail.java50
-rw-r--r--src/com/android/email2/ui/MailActivityEmail.java240
-rw-r--r--src/com/android/email2/ui/MailboxSelectionActivityEmail.java23
-rw-r--r--src/com/android/mail/browse/EmailConversationProvider.java32
-rw-r--r--src/com/android/mail/providers/EmailAccountCacheProvider.java52
-rw-r--r--src/com/android/mail/providers/protos/boot/AccountReceiver.java41
-rw-r--r--src/com/android/mail/utils/LogTag.java28
-rw-r--r--tests/oldAndroid.mk34
39 files changed, 8625 insertions, 0 deletions
diff --git a/emailsync/Android.mk b/emailsync/Android.mk
new file mode 100644
index 000000000..0076c5d72
--- /dev/null
+++ b/emailsync/Android.mk
@@ -0,0 +1,30 @@
+# Copyright 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.
+
+LOCAL_PATH := $(call my-dir)
+
+# Build the com.android.emailcommon static library. At the moment, this includes
+# the emailcommon files themselves plus everything under src/org (apache code). All of our
+# AIDL files are also compiled into the static library
+
+include $(CLEAR_VARS)
+
+
+LOCAL_MODULE := com.android.emailsync
+LOCAL_SRC_FILES := $(call all-java-files-under, src/com/android/emailsync)
+LOCAL_STATIC_JAVA_LIBRARIES := com.android.emailcommon2
+
+LOCAL_SDK_VERSION := 14
+
+include $(BUILD_STATIC_JAVA_LIBRARY)
diff --git a/emailsync/src/com/android/emailsync/AbstractSyncService.java b/emailsync/src/com/android/emailsync/AbstractSyncService.java
new file mode 100644
index 000000000..7cbf13ac5
--- /dev/null
+++ b/emailsync/src/com/android/emailsync/AbstractSyncService.java
@@ -0,0 +1,305 @@
+/*
+ * Copyright (C) 2008-2009 Marc Blank
+ * 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.emailsync;
+
+import com.android.emailcommon.provider.Account;
+import com.android.emailcommon.provider.HostAuth;
+import com.android.emailcommon.provider.Mailbox;
+
+import android.content.Context;
+import android.net.ConnectivityManager;
+import android.net.NetworkInfo;
+import android.os.Bundle;
+import android.util.Log;
+
+import java.util.concurrent.LinkedBlockingQueue;
+
+/**
+ * Base class for all protocol services SyncManager (extends Service, implements
+ * Runnable) instantiates subclasses to run a sync (either timed, or push, or
+ * mail placed in outbox, etc.) EasSyncService is currently implemented; my goal
+ * would be to move IMAP to this structure when it comes time to introduce push
+ * functionality.
+ */
+public abstract class AbstractSyncService implements Runnable {
+
+ public String TAG = "AbstractSyncService";
+
+ public static final int SECONDS = 1000;
+ public static final int MINUTES = 60*SECONDS;
+ public static final int HOURS = 60*MINUTES;
+ public static final int DAYS = 24*HOURS;
+
+ public static final int CONNECT_TIMEOUT = 30*SECONDS;
+ public static final int NETWORK_WAIT = 15*SECONDS;
+
+ public static final String EAS_PROTOCOL = "eas";
+ public static final int EXIT_DONE = 0;
+ public static final int EXIT_IO_ERROR = 1;
+ public static final int EXIT_LOGIN_FAILURE = 2;
+ public static final int EXIT_EXCEPTION = 3;
+ public static final int EXIT_SECURITY_FAILURE = 4;
+ public static final int EXIT_ACCESS_DENIED = 5;
+
+ public Mailbox mMailbox;
+ protected long mMailboxId;
+ protected int mExitStatus = EXIT_EXCEPTION;
+ protected String mMailboxName;
+ public Account mAccount;
+ public Context mContext;
+ public int mChangeCount = 0;
+ public volatile int mSyncReason = 0;
+ protected volatile boolean mStop = false;
+ public volatile Thread mThread;
+ protected final Object mSynchronizer = new Object();
+ // Whether or not the sync service is valid (usable)
+ public boolean mIsValid = true;
+
+ public boolean mUserLog = true; // STOPSHIP
+ public boolean mFileLog = false;
+
+ protected volatile long mRequestTime = 0;
+ protected LinkedBlockingQueue<Request> mRequestQueue = new LinkedBlockingQueue<Request>();
+
+ /**
+ * Sent by SyncManager to request that the service stop itself cleanly
+ */
+ public abstract void stop();
+
+ /**
+ * Sent by SyncManager to indicate that an alarm has fired for this service, and that its
+ * pending (network) operation has timed out. The service is NOT automatically stopped,
+ * although the behavior is service dependent.
+ *
+ * @return true if the operation was stopped normally; false if the thread needed to be
+ * interrupted.
+ */
+ public abstract boolean alarm();
+
+ /**
+ * Sent by SyncManager to request that the service reset itself cleanly; the meaning of this
+ * operation is service dependent.
+ */
+ public abstract void reset();
+
+ /**
+ * Called to validate an account; abstract to allow each protocol to do what
+ * is necessary. For consistency with the Email app's original
+ * functionality, success is indicated by a failure to throw an Exception
+ * (ugh). Parameters are self-explanatory
+ *
+ * @param hostAuth
+ * @return a Bundle containing a result code and, depending on the result, a PolicySet or an
+ * error message
+ */
+ public abstract Bundle validateAccount(HostAuth hostAuth, Context context);
+
+ /**
+ * Called to clear the syncKey for the calendar associated with this service; this is necessary
+ * because changes to calendar sync state cause a reset of data.
+ */
+ public abstract void resetCalendarSyncKey();
+
+ public AbstractSyncService(Context _context, Mailbox _mailbox) {
+ mContext = _context;
+ mMailbox = _mailbox;
+ mMailboxId = _mailbox.mId;
+ mMailboxName = _mailbox.mServerId;
+ mAccount = Account.restoreAccountWithId(_context, _mailbox.mAccountKey);
+ }
+
+ // Will be required when subclasses are instantiated by name
+ public AbstractSyncService(String prefix) {
+ }
+
+ /**
+ * The UI can call this static method to perform account validation. This method wraps each
+ * protocol's validateAccount method. Arguments are self-explanatory, except where noted.
+ *
+ * @param klass the protocol class (EasSyncService.class for example)
+ * @param hostAuth
+ * @param context
+ * @return a Bundle containing a result code and, depending on the result, a PolicySet or an
+ * error message
+ */
+ public static Bundle validate(Class<? extends AbstractSyncService> klass,
+ HostAuth hostAuth, Context context) {
+ AbstractSyncService svc;
+ try {
+ svc = klass.newInstance();
+ return svc.validateAccount(hostAuth, context);
+ } catch (IllegalAccessException e) {
+ } catch (InstantiationException e) {
+ }
+ return null;
+ }
+
+ public static class ValidationResult {
+ static final int NO_FAILURE = 0;
+ static final int CONNECTION_FAILURE = 1;
+ static final int VALIDATION_FAILURE = 2;
+ static final int EXCEPTION = 3;
+
+ static final ValidationResult succeeded = new ValidationResult(true, NO_FAILURE, null);
+ boolean success;
+ int failure = NO_FAILURE;
+ String reason = null;
+ Exception exception = null;
+
+ ValidationResult(boolean _success, int _failure, String _reason) {
+ success = _success;
+ failure = _failure;
+ reason = _reason;
+ }
+
+ ValidationResult(boolean _success) {
+ success = _success;
+ }
+
+ ValidationResult(Exception e) {
+ success = false;
+ failure = EXCEPTION;
+ exception = e;
+ }
+
+ public boolean isSuccess() {
+ return success;
+ }
+
+ public String getReason() {
+ return reason;
+ }
+ }
+
+ public boolean isStopped() {
+ return mStop;
+ }
+
+ public Object getSynchronizer() {
+ return mSynchronizer;
+ }
+
+ /**
+ * Convenience methods to do user logging (i.e. connection activity). Saves a bunch of
+ * repetitive code.
+ */
+ public void userLog(String string, int code, String string2) {
+ if (mUserLog) {
+ userLog(string + code + string2);
+ }
+ }
+
+ public void userLog(String string, int code) {
+ if (mUserLog) {
+ userLog(string + code);
+ }
+ }
+
+ public void userLog(String str, Exception e) {
+ if (mUserLog) {
+ Log.e(TAG, str, e);
+ } else {
+ Log.e(TAG, str + e);
+ }
+ if (mFileLog) {
+ FileLogger.log(e);
+ }
+ }
+
+ /**
+ * Standard logging for EAS.
+ * If user logging is active, we concatenate any arguments and log them using Log.d
+ * We also check for file logging, and log appropriately
+ * @param strings strings to concatenate and log
+ */
+ public void userLog(String ...strings) {
+ if (mUserLog) {
+ String logText;
+ if (strings.length == 1) {
+ logText = strings[0];
+ } else {
+ StringBuilder sb = new StringBuilder(64);
+ for (String string: strings) {
+ sb.append(string);
+ }
+ logText = sb.toString();
+ }
+ Log.d(TAG, logText);
+ if (mFileLog) {
+ FileLogger.log(TAG, logText);
+ }
+ }
+ }
+
+ /**
+ * Error log is used for serious issues that should always be logged
+ * @param str the string to log
+ */
+ public void errorLog(String str) {
+ Log.e(TAG, str);
+ if (mFileLog) {
+ FileLogger.log(TAG, str);
+ }
+ }
+
+ /**
+ * Waits for up to 10 seconds for network connectivity; returns whether or not there is
+ * network connectivity.
+ *
+ * @return whether there is network connectivity
+ */
+ public boolean hasConnectivity() {
+ ConnectivityManager cm =
+ (ConnectivityManager)mContext.getSystemService(Context.CONNECTIVITY_SERVICE);
+ int tries = 0;
+ while (tries++ < 1) {
+ // Use the same test as in ExchangeService#waitForConnectivity
+ // TODO: Create common code for this test in emailcommon
+ NetworkInfo info = cm.getActiveNetworkInfo();
+ if (info != null) {
+ return true;
+ }
+ try {
+ Thread.sleep(10*SECONDS);
+ } catch (InterruptedException e) {
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Request handling (common functionality)
+ * Can be overridden if desired
+ */
+
+ public void addRequest(Request req) {
+ mRequestQueue.offer(req);
+ }
+
+ public void removeRequest(Request req) {
+ mRequestQueue.remove(req);
+ }
+
+ public boolean hasPendingRequests() {
+ return !mRequestQueue.isEmpty();
+ }
+
+ public void clearRequests() {
+ mRequestQueue.clear();
+ }
+}
diff --git a/emailsync/src/com/android/emailsync/EmailSyncAlarmReceiver.java b/emailsync/src/com/android/emailsync/EmailSyncAlarmReceiver.java
new file mode 100644
index 000000000..639c9cfc2
--- /dev/null
+++ b/emailsync/src/com/android/emailsync/EmailSyncAlarmReceiver.java
@@ -0,0 +1,111 @@
+/*
+ * Copyright (C) 2008-2009 Marc Blank
+ * 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.emailsync;
+
+import android.content.BroadcastReceiver;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.Intent;
+import android.database.Cursor;
+import android.util.Log;
+
+import com.android.emailcommon.provider.EmailContent.Message;
+import com.android.emailcommon.provider.EmailContent.MessageColumns;
+import com.android.emailcommon.provider.ProviderUnavailableException;
+
+import java.util.ArrayList;
+
+/**
+ * EmailSyncAlarmReceiver (USAR) is used by the SyncManager to start up-syncs of user-modified data
+ * back to the Exchange server.
+ *
+ * Here's how this works for Email, for example:
+ *
+ * 1) User modifies or deletes an email from the UI.
+ * 2) SyncManager, which has a ContentObserver watching the Message class, is alerted to a change
+ * 3) SyncManager sets an alarm (to be received by USAR) for a few seconds in the
+ * future (currently 15), the delay preventing excess syncing (think of it as a debounce mechanism).
+ * 4) ESAR Receiver's onReceive method is called
+ * 5) ESAR goes through all change and deletion records and compiles a list of mailboxes which have
+ * changes to be uploaded.
+ * 6) ESAR calls SyncManager to start syncs of those mailboxes
+ *
+ * If EmailProvider isn't available, the upsyncs will happen the next time ExchangeService starts
+ *
+ */
+public class EmailSyncAlarmReceiver extends BroadcastReceiver {
+ final String[] MAILBOX_DATA_PROJECTION = {MessageColumns.MAILBOX_KEY};
+
+ @Override
+ public void onReceive(final Context context, Intent intent) {
+ new Thread(new Runnable() {
+ public void run() {
+ handleReceive(context);
+ }
+ }).start();
+ }
+
+ private void handleReceive(Context context) {
+ ArrayList<Long> mailboxesToNotify = new ArrayList<Long>();
+ 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();
+
+ try {
+ // Find all of the deletions
+ Cursor c = cr.query(Message.DELETED_CONTENT_URI, MAILBOX_DATA_PROJECTION, selector,
+ null, null);
+ if (c == null) throw new ProviderUnavailableException();
+ try {
+ // Keep track of which mailboxes to notify; we'll only notify each one once
+ while (c.moveToNext()) {
+ long mailboxId = c.getLong(0);
+ if (!mailboxesToNotify.contains(mailboxId)) {
+ mailboxesToNotify.add(mailboxId);
+ }
+ }
+ } finally {
+ c.close();
+ }
+
+ // Now, find changed messages
+ c = cr.query(Message.UPDATED_CONTENT_URI, MAILBOX_DATA_PROJECTION, selector,
+ null, null);
+ if (c == null) throw new ProviderUnavailableException();
+ try {
+ // Keep track of which mailboxes to notify; we'll only notify each one once
+ while (c.moveToNext()) {
+ long mailboxId = c.getLong(0);
+ if (!mailboxesToNotify.contains(mailboxId)) {
+ mailboxesToNotify.add(mailboxId);
+ }
+ }
+ } finally {
+ c.close();
+ }
+
+ // Request service from the mailbox
+ for (Long mailboxId: mailboxesToNotify) {
+ SyncServiceManager.serviceRequest(mailboxId, SyncServiceManager.SYNC_UPSYNC);
+ }
+ } catch (ProviderUnavailableException e) {
+ Log.e("EmailSyncAlarmReceiver", "EmailProvider unavailable; aborting alarm receiver");
+ }
+ }
+}
diff --git a/emailsync/src/com/android/emailsync/FileLogger.java b/emailsync/src/com/android/emailsync/FileLogger.java
new file mode 100644
index 000000000..db8b62605
--- /dev/null
+++ b/emailsync/src/com/android/emailsync/FileLogger.java
@@ -0,0 +1,120 @@
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.emailsync;
+
+import android.content.Context;
+import android.os.Environment;
+
+import java.io.FileWriter;
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.util.Date;
+
+public class FileLogger {
+ private static FileLogger LOGGER = null;
+ private static FileWriter sLogWriter = null;
+ public static String LOG_FILE_NAME =
+ Environment.getExternalStorageDirectory() + "/emaillog.txt";
+
+ public synchronized static FileLogger getLogger (Context c) {
+ LOGGER = new FileLogger();
+ return LOGGER;
+ }
+
+ private FileLogger() {
+ try {
+ sLogWriter = new FileWriter(LOG_FILE_NAME, true);
+ } catch (IOException e) {
+ // Doesn't matter
+ }
+ }
+
+ static public synchronized void close() {
+ if (sLogWriter != null) {
+ try {
+ sLogWriter.close();
+ } catch (IOException e) {
+ // Doesn't matter
+ }
+ sLogWriter = null;
+ }
+ }
+
+ static public synchronized void log(Exception e) {
+ if (sLogWriter != null) {
+ log("Exception", "Stack trace follows...");
+ PrintWriter pw = new PrintWriter(sLogWriter);
+ e.printStackTrace(pw);
+ pw.flush();
+ }
+ }
+
+ @SuppressWarnings("deprecation")
+ static public synchronized void log(String prefix, String str) {
+ if (LOGGER == null) {
+ LOGGER = new FileLogger();
+ log("Logger", "\r\n\r\n --- New Log ---");
+ }
+ Date d = new Date();
+ int hr = d.getHours();
+ int min = d.getMinutes();
+ int sec = d.getSeconds();
+
+ // I don't use DateFormat here because (in my experience), it's much slower
+ StringBuffer sb = new StringBuffer(256);
+ sb.append('[');
+ sb.append(hr);
+ sb.append(':');
+ if (min < 10)
+ sb.append('0');
+ sb.append(min);
+ sb.append(':');
+ if (sec < 10) {
+ sb.append('0');
+ }
+ sb.append(sec);
+ sb.append("] ");
+ if (prefix != null) {
+ sb.append(prefix);
+ sb.append("| ");
+ }
+ sb.append(str);
+ sb.append("\r\n");
+ String s = sb.toString();
+
+ if (sLogWriter != null) {
+ try {
+ sLogWriter.write(s);
+ sLogWriter.flush();
+ } catch (IOException e) {
+ // Something might have happened to the sdcard
+ if (Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState())) {
+ // If the card is mounted and we can create the writer, retry
+ LOGGER = new FileLogger();
+ if (sLogWriter != null) {
+ try {
+ log("FileLogger", "Exception writing log; recreating...");
+ log(prefix, str);
+ } catch (Exception e1) {
+ // Nothing to do at this point
+ }
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/emailsync/src/com/android/emailsync/MailboxAlarmReceiver.java b/emailsync/src/com/android/emailsync/MailboxAlarmReceiver.java
new file mode 100644
index 000000000..17a2590eb
--- /dev/null
+++ b/emailsync/src/com/android/emailsync/MailboxAlarmReceiver.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright (C) 2008-2009 Marc Blank
+ * 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.emailsync;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+
+
+/**
+ * MailboxAlarmReceiver is used to "wake up" the ExchangeService at the appropriate time(s). It may
+ * also be used for individual sync adapters, but this isn't implemented at the present time.
+ *
+ */
+public class MailboxAlarmReceiver extends BroadcastReceiver {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ long mailboxId = intent.getLongExtra("mailbox", SyncServiceManager.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));
+ } else {
+ SyncServiceManager.alert(context, mailboxId);
+ }
+ }
+}
+
diff --git a/emailsync/src/com/android/emailsync/PartRequest.java b/emailsync/src/com/android/emailsync/PartRequest.java
new file mode 100644
index 000000000..955f62d87
--- /dev/null
+++ b/emailsync/src/com/android/emailsync/PartRequest.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright (C) 2008-2009 Marc Blank
+ * 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.emailsync;
+
+import com.android.emailcommon.provider.EmailContent.Attachment;
+
+/**
+ * PartRequest is the EAS 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.
+ */
+public class PartRequest extends Request {
+ public final Attachment mAttachment;
+ public final String mDestination;
+ public final String mContentUriString;
+ public final String mLocation;
+
+ public PartRequest(Attachment _att, String _destination, String _contentUriString) {
+ super(_att.mMessageKey);
+ mAttachment = _att;
+ mLocation = mAttachment.mLocation;
+ mDestination = _destination;
+ mContentUriString = _contentUriString;
+ }
+
+ // PartRequests are unique by their attachment id (i.e. multiple attachments might be queued
+ // for a particular message, but any individual attachment can only be loaded once)
+ public boolean equals(Object o) {
+ if (!(o instanceof PartRequest)) return false;
+ return ((PartRequest)o).mAttachment.mId == mAttachment.mId;
+ }
+
+ public int hashCode() {
+ return (int)mAttachment.mId;
+ }
+}
diff --git a/emailsync/src/com/android/emailsync/Request.java b/emailsync/src/com/android/emailsync/Request.java
new file mode 100644
index 000000000..f686a36cc
--- /dev/null
+++ b/emailsync/src/com/android/emailsync/Request.java
@@ -0,0 +1,36 @@
+/*
+ * 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;
+
+/**
+ * Requests for mailbox actions are handled by subclasses of this abstract class.
+ * Three subclasses are now defined: PartRequest (attachment load), MeetingResponseRequest
+ * (respond to a meeting invitation), and MessageMoveRequest (move a message to another folder)
+ */
+public abstract class Request {
+ public final long mTimeStamp = System.currentTimeMillis();
+ public final long mMessageId;
+
+ public Request(long messageId) {
+ mMessageId = messageId;
+ }
+
+ // Subclasses of Request may have different semantics regarding equality; therefore,
+ // we force them to implement the equals method
+ public abstract boolean equals(Object o);
+ public abstract int hashCode();
+}
diff --git a/emailsync/src/com/android/emailsync/SyncServiceManager.java b/emailsync/src/com/android/emailsync/SyncServiceManager.java
new file mode 100644
index 000000000..4a0efcdce
--- /dev/null
+++ b/emailsync/src/com/android/emailsync/SyncServiceManager.java
@@ -0,0 +1,2254 @@
+/*
+ * Copyright (C) 2008-2009 Marc Blank
+ * 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.emailsync;
+
+import android.app.AlarmManager;
+import android.app.PendingIntent;
+import android.app.Service;
+import android.content.BroadcastReceiver;
+import android.content.ContentResolver;
+import android.content.ContentUris;
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.database.ContentObserver;
+import android.database.Cursor;
+import android.net.ConnectivityManager;
+import android.net.NetworkInfo;
+import android.net.NetworkInfo.State;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.PowerManager;
+import android.os.PowerManager.WakeLock;
+import android.os.Process;
+import android.os.RemoteException;
+import android.provider.CalendarContract;
+import android.provider.CalendarContract.Calendars;
+import android.provider.CalendarContract.Events;
+import android.provider.ContactsContract;
+import android.util.Log;
+
+import com.android.emailcommon.TempDirectory;
+import com.android.emailcommon.provider.Account;
+import com.android.emailcommon.provider.EmailContent;
+import com.android.emailcommon.provider.EmailContent.Body;
+import com.android.emailcommon.provider.EmailContent.BodyColumns;
+import com.android.emailcommon.provider.EmailContent.MailboxColumns;
+import com.android.emailcommon.provider.EmailContent.Message;
+import com.android.emailcommon.provider.EmailContent.MessageColumns;
+import com.android.emailcommon.provider.EmailContent.SyncColumns;
+import com.android.emailcommon.provider.HostAuth;
+import com.android.emailcommon.provider.Mailbox;
+import com.android.emailcommon.provider.Policy;
+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;
+import com.android.emailcommon.utility.EmailClientConnectionManager;
+import com.android.emailcommon.utility.Utility;
+
+import org.apache.http.conn.params.ConnManagerPNames;
+import org.apache.http.conn.params.ConnPerRoute;
+import org.apache.http.conn.routing.HttpRoute;
+import org.apache.http.params.BasicHttpParams;
+import org.apache.http.params.HttpParams;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * The SyncServiceManager handles the lifecycle of various sync adapters used by services that
+ * cannot rely on the system SyncManager
+ *
+ * SyncServiceManager uses ContentObservers to detect changes to accounts, mailboxes, & messages in
+ * order to maintain proper 2-way syncing of data. (More documentation to follow)
+ *
+ */
+public abstract class SyncServiceManager extends Service implements Runnable {
+
+ private static final String TAG = "SyncServiceManager";
+
+ // The SyncServiceManager's mailbox "id"
+ public static final int EXTRA_MAILBOX_ID = -1;
+ public static final int SYNC_SERVICE_MAILBOX_ID = 0;
+
+ private static final int SECONDS = 1000;
+ private static final int MINUTES = 60*SECONDS;
+ private static final int ONE_DAY_MINUTES = 1440;
+
+ private static final int SYNC_SERVICE_HEARTBEAT_TIME = 15*MINUTES;
+ private static final int CONNECTIVITY_WAIT_TIME = 10*MINUTES;
+
+ // Sync hold constants for services with transient errors
+ private static final int HOLD_DELAY_MAXIMUM = 4*MINUTES;
+
+ // Reason codes when SyncServiceManager.kick is called (mainly for debugging)
+ // UI has changed data, requiring an upsync of changes
+ public static final int SYNC_UPSYNC = 0;
+ // A scheduled sync (when not using push)
+ public static final int SYNC_SCHEDULED = 1;
+ // Mailbox was marked push
+ public static final int SYNC_PUSH = 2;
+ // A ping (EAS push signal) was received
+ public static final int SYNC_PING = 3;
+ // Misc.
+ public static final int SYNC_KICK = 4;
+ // A part request (attachment load, for now) was sent to SyncServiceManager
+ public static final int SYNC_SERVICE_PART_REQUEST = 5;
+
+ // Requests >= SYNC_CALLBACK_START generate callbacks to the UI
+ public static final int SYNC_CALLBACK_START = 6;
+ // startSync was requested of SyncServiceManager (other than due to user request)
+ public static final int SYNC_SERVICE_START_SYNC = SYNC_CALLBACK_START + 0;
+ // startSync was requested of SyncServiceManager (due to user request)
+ public static final int SYNC_UI_REQUEST = SYNC_CALLBACK_START + 1;
+
+ protected static final String WHERE_IN_ACCOUNT_AND_PUSHABLE =
+ MailboxColumns.ACCOUNT_KEY + "=? and type in (" + Mailbox.TYPE_INBOX + ','
+ + Mailbox.TYPE_EAS_ACCOUNT_MAILBOX + ',' + Mailbox.TYPE_CONTACTS + ','
+ + Mailbox.TYPE_CALENDAR + ')';
+ protected static final String WHERE_IN_ACCOUNT_AND_TYPE_INBOX =
+ MailboxColumns.ACCOUNT_KEY + "=? and type = " + Mailbox.TYPE_INBOX ;
+ private static final String WHERE_MAILBOX_KEY = Message.MAILBOX_KEY + "=?";
+ private static final String WHERE_NOT_INTERVAL_NEVER_AND_ACCOUNT_KEY_IN =
+ "(" + MailboxColumns.TYPE + '=' + Mailbox.TYPE_OUTBOX
+ + " or " + MailboxColumns.SYNC_INTERVAL + "!=" + Mailbox.CHECK_INTERVAL_NEVER + ')'
+ + " and " + MailboxColumns.ACCOUNT_KEY + " in (";
+
+ public static final int SEND_FAILED = 1;
+ public static final String MAILBOX_KEY_AND_NOT_SEND_FAILED =
+ MessageColumns.MAILBOX_KEY + "=? and (" + SyncColumns.SERVER_ID + " is null or " +
+ SyncColumns.SERVER_ID + "!=" + SEND_FAILED + ')';
+
+ public static final String CALENDAR_SELECTION =
+ Calendars.ACCOUNT_NAME + "=? AND " + Calendars.ACCOUNT_TYPE + "=?";
+ private static final String WHERE_CALENDAR_ID = Events.CALENDAR_ID + "=?";
+
+ // Offsets into the syncStatus data for EAS that indicate type, exit status, and change count
+ // The format is S<type_char>:<exit_char>:<change_count>
+ public static final int STATUS_TYPE_CHAR = 1;
+ public static final int STATUS_EXIT_CHAR = 3;
+ public static final int STATUS_CHANGE_COUNT_OFFSET = 5;
+
+ // Ready for ping
+ public static final int PING_STATUS_OK = 0;
+ // Service already running (can't ping)
+ public static final int PING_STATUS_RUNNING = 1;
+ // Service waiting after I/O error (can't ping)
+ public static final int PING_STATUS_WAITING = 2;
+ // Service had a fatal error; can't run
+ public static final int PING_STATUS_UNABLE = 3;
+ // Service is disabled by user (checkbox)
+ public static final int PING_STATUS_DISABLED = 4;
+
+ private static final int MAX_CLIENT_CONNECTION_MANAGER_SHUTDOWNS = 1;
+
+ // We synchronize on this for all actions affecting the service and error maps
+ private static final Object sSyncLock = new Object();
+ // All threads can use this lock to wait for connectivity
+ public static final Object sConnectivityLock = new Object();
+ public static boolean sConnectivityHold = false;
+
+ // Keeps track of running services (by mailbox id)
+ public final HashMap<Long, AbstractSyncService> mServiceMap =
+ new HashMap<Long, AbstractSyncService>();
+ // Keeps track of services whose last sync ended with an error (by mailbox id)
+ /*package*/ public ConcurrentHashMap<Long, SyncError> mSyncErrorMap =
+ new ConcurrentHashMap<Long, SyncError>();
+ // Keeps track of which services require a wake lock (by mailbox id)
+ private final HashMap<Long, Boolean> mWakeLocks = new HashMap<Long, Boolean>();
+ // Keeps track of PendingIntents for mailbox alarms (by mailbox id)
+ private final HashMap<Long, PendingIntent> mPendingIntents = new HashMap<Long, PendingIntent>();
+ // The actual WakeLock obtained by SyncServiceManager
+ private WakeLock mWakeLock = null;
+ // Keep our cached list of active Accounts here
+ public final AccountList mAccountList = new AccountList();
+
+ // Observers that we use to look for changed mail-related data
+ private final Handler mHandler = new Handler();
+ private AccountObserver mAccountObserver;
+ private MailboxObserver mMailboxObserver;
+ private SyncedMessageObserver mSyncedMessageObserver;
+
+ // Concurrent because CalendarSyncAdapter can modify the map during a wipe
+ private final ConcurrentHashMap<Long, CalendarObserver> mCalendarObservers =
+ new ConcurrentHashMap<Long, CalendarObserver>();
+
+ public ContentResolver mResolver;
+
+ // The singleton SyncServiceManager object, with its thread and stop flag
+ protected static SyncServiceManager INSTANCE;
+ protected static Thread sServiceThread = null;
+ // Cached unique device id
+ protected static String sDeviceId = null;
+ // HashMap of ConnectionManagers that all EAS threads can use (by ssl/port pair)
+ private static HashMap<Integer, EmailClientConnectionManager> sClientConnectionManagers =
+ new HashMap<Integer, EmailClientConnectionManager>();
+ // Count of ClientConnectionManager shutdowns
+ private static volatile int sClientConnectionManagerShutdownCount = 0;
+
+ private static volatile boolean sStartingUp = false;
+ private static volatile boolean sStop = false;
+
+ // The reason for SyncServiceManager's next wakeup call
+ private String mNextWaitReason;
+ // Whether we have an unsatisfied "kick" pending
+ private boolean mKicked = false;
+
+ // Receiver of connectivity broadcasts
+ private ConnectivityReceiver mConnectivityReceiver = null;
+ private ConnectivityReceiver mBackgroundDataSettingReceiver = null;
+ private volatile boolean mBackgroundData = true;
+ // The most current NetworkInfo (from ConnectivityManager)
+ private NetworkInfo mNetworkInfo;
+
+ // For sync logging
+ protected static boolean sUserLog = false;
+ protected static boolean sFileLog = false;
+
+ /**
+ * Return an AccountObserver for this manager; the subclass must implement the newAccount()
+ * method, which is called whenever the observer discovers that a new account has been created.
+ * The subclass should do any housekeeping necessary
+ * @param handler a Handler
+ * @return the AccountObserver
+ */
+ public abstract AccountObserver getAccountObserver(Handler handler);
+
+ /**
+ * Perform any housekeeping necessary upon startup of the manager
+ */
+ public abstract void onStartup();
+
+ /**
+ * Returns a String that can be used as a WHERE clause in SQLite that selects accounts whose
+ * syncs are managed by this manager
+ * @return the account selector String
+ */
+ public abstract String getAccountsSelector();
+
+ /**
+ * Returns an appropriate sync service for the passed in mailbox
+ * @param context the caller's context
+ * @param mailbox the Mailbox to be synced
+ * @return a service that will sync the Mailbox
+ */
+ public abstract AbstractSyncService getServiceForMailbox(Context context, Mailbox mailbox);
+
+ /**
+ * Return a list of all Accounts in EmailProvider. Because the result of this call may be used
+ * in account reconciliation, an exception is thrown if the result cannot be guaranteed accurate
+ * @param context the caller's context
+ * @param accounts a list that Accounts will be added into
+ * @return the list of Accounts
+ * @throws ProviderUnavailableException if the list of Accounts cannot be guaranteed valid
+ */
+ public abstract AccountList collectAccounts(Context context, AccountList accounts);
+
+ /**
+ * Returns the AccountManager type (e.g. com.android.exchange) for this sync service
+ */
+ public abstract String getAccountManagerType();
+
+ /**
+ * Returns the intent action used for this sync service
+ */
+ public abstract String getServiceIntentAction();
+
+ /**
+ * Returns the callback proxy used for communicating back with the Email app
+ */
+ public abstract Stub getCallbackProxy();
+
+ public class AccountList extends ArrayList<Account> {
+ private static final long serialVersionUID = 1L;
+
+ @Override
+ public boolean add(Account account) {
+ // Cache the account manager account
+ account.mAmAccount = new android.accounts.Account(
+ account.mEmailAddress, getAccountManagerType());
+ super.add(account);
+ return true;
+ }
+
+ public boolean contains(long id) {
+ for (Account account : this) {
+ if (account.mId == id) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ public Account getById(long id) {
+ for (Account account : this) {
+ if (account.mId == id) {
+ return account;
+ }
+ }
+ return null;
+ }
+
+ public Account getByName(String accountName) {
+ for (Account account : this) {
+ if (account.mEmailAddress.equalsIgnoreCase(accountName)) {
+ return account;
+ }
+ }
+ return null;
+ }
+ }
+
+ public static void setUserDebug(int state) {
+ sUserLog = (state & EmailServiceProxy.DEBUG_BIT) != 0;
+ sFileLog = (state & EmailServiceProxy.DEBUG_FILE_BIT) != 0;
+ if (sFileLog) {
+ sUserLog = true;
+ }
+ Log.d("Sync Debug", "Logging: " + (sUserLog ? "User " : "") + (sFileLog ? "File" : ""));
+ }
+
+ private boolean onSecurityHold(Account account) {
+ return (account.mFlags & Account.FLAGS_SECURITY_HOLD) != 0;
+ }
+
+ public static String getAccountSelector() {
+ SyncServiceManager ssm = INSTANCE;
+ if (ssm == null) return "";
+ return ssm.getAccountsSelector();
+ }
+
+ public abstract class AccountObserver extends ContentObserver {
+ String mSyncableMailboxSelector = null;
+ String mAccountSelector = null;
+
+ // Runs when SyncServiceManager first starts
+ @SuppressWarnings("deprecation")
+ public AccountObserver(Handler handler) {
+ super(handler);
+ // At startup, we want to see what EAS accounts exist and cache them
+ // TODO: Move database work out of UI thread
+ Context context = getContext();
+ synchronized (mAccountList) {
+ try {
+ collectAccounts(context, mAccountList);
+ } catch (ProviderUnavailableException e) {
+ // Just leave if EmailProvider is unavailable
+ return;
+ }
+ // Create an account mailbox for any account without one
+ for (Account account : mAccountList) {
+ int cnt = Mailbox.count(context, Mailbox.CONTENT_URI, "accountKey="
+ + account.mId, null);
+ if (cnt == 0) {
+ // This case handles a newly created account
+ newAccount(account.mId);
+ }
+ }
+ }
+ // Run through accounts and update account hold information
+ Utility.runAsync(new Runnable() {
+ @Override
+ public void run() {
+ synchronized (mAccountList) {
+ for (Account account : mAccountList) {
+ 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,
+ account, false);
+ log("isActive true; release hold for " + account.mDisplayName);
+ }
+ }
+ }
+ }
+ }});
+ }
+
+ /**
+ * Returns a String suitable for appending to a where clause that selects for all syncable
+ * mailboxes in all eas accounts
+ * @return a complex selection string that is not to be cached
+ */
+ public String getSyncableMailboxWhere() {
+ if (mSyncableMailboxSelector == null) {
+ StringBuilder sb = new StringBuilder(WHERE_NOT_INTERVAL_NEVER_AND_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(')');
+ mSyncableMailboxSelector = sb.toString();
+ }
+ return mSyncableMailboxSelector;
+ }
+
+ private void onAccountChanged() {
+ try {
+ maybeStartSyncServiceManagerThread();
+ Context context = getContext();
+
+ // A change to the list requires us to scan for deletions (stop running syncs)
+ // At startup, we want to see what accounts exist and cache them
+ AccountList currentAccounts = new AccountList();
+ try {
+ collectAccounts(context, currentAccounts);
+ } catch (ProviderUnavailableException e) {
+ // Just leave if EmailProvider is unavailable
+ return;
+ }
+ synchronized (mAccountList) {
+ for (Account account : mAccountList) {
+ boolean accountIncomplete =
+ (account.mFlags & Account.FLAGS_INCOMPLETE) != 0;
+ // If the current list doesn't include this account and the account wasn't
+ // incomplete, then this is a deletion
+ if (!currentAccounts.contains(account.mId) && !accountIncomplete) {
+ // 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);
+ // See if the account is still around
+ Account deletedAccount =
+ Account.restoreAccountWithId(context, account.mId);
+ if (deletedAccount != null) {
+ // It is; add it to our account list
+ alwaysLog("Account still in provider: " + account.mDisplayName);
+ currentAccounts.add(account);
+ } else {
+ // It isn't; stop syncs and clear our selectors
+ alwaysLog("Account deletion confirmed: " + account.mDisplayName);
+ stopAccountSyncs(account.mId, true);
+ mSyncableMailboxSelector = null;
+ mAccountSelector = null;
+ }
+ } else {
+ // Get the newest version of this account
+ Account updatedAccount =
+ Account.restoreAccountWithId(context, account.mId);
+ if (updatedAccount == null) continue;
+ if (account.mSyncInterval != updatedAccount.mSyncInterval
+ || account.mSyncLookback != updatedAccount.mSyncLookback) {
+ // Set the inbox interval to the interval of the Account
+ // This setting should NOT affect other boxes
+ ContentValues cv = new ContentValues();
+ cv.put(MailboxColumns.SYNC_INTERVAL, updatedAccount.mSyncInterval);
+ getContentResolver().update(Mailbox.CONTENT_URI, cv,
+ WHERE_IN_ACCOUNT_AND_TYPE_INBOX, new String[] {
+ Long.toString(account.mId)
+ });
+ // Stop all current syncs; the appropriate ones will restart
+ log("Account " + account.mDisplayName + " changed; stop syncs");
+ stopAccountSyncs(account.mId, true);
+ }
+
+ // See if this account is no longer on security hold
+ if (onSecurityHold(account) && !onSecurityHold(updatedAccount)) {
+ releaseSyncHolds(SyncServiceManager.this,
+ AbstractSyncService.EXIT_SECURITY_FAILURE, account);
+ }
+
+ // Put current values into our cached account
+ account.mSyncInterval = updatedAccount.mSyncInterval;
+ account.mSyncLookback = updatedAccount.mSyncLookback;
+ account.mFlags = updatedAccount.mFlags;
+ }
+ }
+ // Look for new accounts
+ for (Account account : currentAccounts) {
+ if (!mAccountList.contains(account.mId)) {
+ // Don't forget to cache the HostAuth
+ HostAuth ha = HostAuth.restoreHostAuthWithId(getContext(),
+ account.mHostAuthKeyRecv);
+ if (ha == null) continue;
+ account.mHostAuthRecv = ha;
+ // This is an addition; create our magic hidden mailbox...
+ log("Account observer found new account: " + account.mDisplayName);
+ newAccount(account.mId);
+ mAccountList.add(account);
+ mSyncableMailboxSelector = null;
+ mAccountSelector = null;
+ }
+ }
+ // Finally, make sure our account list is up to date
+ mAccountList.clear();
+ mAccountList.addAll(currentAccounts);
+ }
+
+ // See if there's anything to do...
+ kick("account changed");
+ } catch (ProviderUnavailableException e) {
+ alwaysLog("Observer failed; provider unavailable");
+ }
+ }
+
+ @Override
+ public void onChange(boolean selfChange) {
+ new Thread(new Runnable() {
+ @Override
+ public void run() {
+ onAccountChanged();
+ }}, "Account Observer").start();
+ }
+
+ public abstract void newAccount(long acctId);
+ }
+
+ /**
+ * Register a specific Calendar's data observer; we need to recognize when the SYNC_EVENTS
+ * column has changed (when sync has turned off or on)
+ * @param account the Account whose Calendar we're observing
+ */
+ private void registerCalendarObserver(Account account) {
+ // Get a new observer
+ CalendarObserver observer = new CalendarObserver(mHandler, account);
+ if (observer.mCalendarId != 0) {
+ // If we find the Calendar (and we'd better) register it and store it in the map
+ mCalendarObservers.put(account.mId, observer);
+ mResolver.registerContentObserver(
+ ContentUris.withAppendedId(Calendars.CONTENT_URI, observer.mCalendarId), false,
+ observer);
+ }
+ }
+
+ /**
+ * Unregister all CalendarObserver's
+ */
+ static public void unregisterCalendarObservers() {
+ SyncServiceManager ssm = INSTANCE;
+ if (ssm == null) return;
+ ContentResolver resolver = ssm.mResolver;
+ for (CalendarObserver observer: ssm.mCalendarObservers.values()) {
+ resolver.unregisterContentObserver(observer);
+ }
+ ssm.mCalendarObservers.clear();
+ }
+
+ public static Uri asSyncAdapter(Uri uri, String account, String accountType) {
+ return uri.buildUpon().appendQueryParameter(CalendarContract.CALLER_IS_SYNCADAPTER, "true")
+ .appendQueryParameter(Calendars.ACCOUNT_NAME, account)
+ .appendQueryParameter(Calendars.ACCOUNT_TYPE, accountType).build();
+ }
+
+ /**
+ * Return the syncable state of an account's calendar, as determined by the sync_events column
+ * of our Calendar (from CalendarProvider2)
+ * Note that the current state of sync_events is cached in our CalendarObserver
+ * @param accountId the id of the account whose calendar we are checking
+ * @return whether or not syncing of events is enabled
+ */
+ private boolean isCalendarEnabled(long accountId) {
+ CalendarObserver observer = mCalendarObservers.get(accountId);
+ if (observer != null) {
+ return (observer.mSyncEvents == 1);
+ }
+ // If there's no observer, there's no Calendar in CalendarProvider2, so we return true
+ // to allow Calendar creation
+ return true;
+ }
+
+ private class CalendarObserver extends ContentObserver {
+ final long mAccountId;
+ final String mAccountName;
+ long mCalendarId;
+ long mSyncEvents;
+
+ public CalendarObserver(Handler handler, Account account) {
+ super(handler);
+ mAccountId = account.mId;
+ mAccountName = account.mEmailAddress;
+ // Find the Calendar for this account
+ Cursor c = mResolver.query(Calendars.CONTENT_URI,
+ new String[] {Calendars._ID, Calendars.SYNC_EVENTS},
+ CALENDAR_SELECTION,
+ new String[] {account.mEmailAddress, getAccountManagerType()},
+ null);
+ if (c != null) {
+ // Save its id and its sync events status
+ try {
+ if (c.moveToFirst()) {
+ mCalendarId = c.getLong(0);
+ mSyncEvents = c.getLong(1);
+ }
+ } finally {
+ c.close();
+ }
+ }
+ }
+
+ @Override
+ public synchronized void onChange(boolean selfChange) {
+ // See if the user has changed syncing of our calendar
+ if (!selfChange) {
+ new Thread(new Runnable() {
+ @Override
+ public void run() {
+ try {
+ Cursor c = mResolver.query(Calendars.CONTENT_URI,
+ new String[] {Calendars.SYNC_EVENTS}, Calendars._ID + "=?",
+ new String[] {Long.toString(mCalendarId)}, null);
+ if (c == null) return;
+ // Get its sync events; if it's changed, we've got work to do
+ try {
+ if (c.moveToFirst()) {
+ long newSyncEvents = c.getLong(0);
+ if (newSyncEvents != mSyncEvents) {
+ log("_sync_events changed for calendar in " + mAccountName);
+ Mailbox mailbox = Mailbox.restoreMailboxOfType(INSTANCE,
+ mAccountId, Mailbox.TYPE_CALENDAR);
+ // Sanity check for mailbox deletion
+ if (mailbox == null) return;
+ ContentValues cv = new ContentValues();
+ if (newSyncEvents == 0) {
+ // When sync is disabled, we're supposed to delete
+ // all events in the calendar
+ log("Deleting events and setting syncKey to 0 for " +
+ mAccountName);
+ // First, stop any sync that's ongoing
+ stopManualSync(mailbox.mId);
+ // Set the syncKey to 0 (reset)
+ AbstractSyncService service = getServiceForMailbox(
+ INSTANCE, mailbox);
+ service.resetCalendarSyncKey();
+ // Reset the sync key locally and stop syncing
+ cv.put(Mailbox.SYNC_KEY, "0");
+ cv.put(Mailbox.SYNC_INTERVAL,
+ Mailbox.CHECK_INTERVAL_NEVER);
+ mResolver.update(ContentUris.withAppendedId(
+ Mailbox.CONTENT_URI, mailbox.mId), cv, null,
+ null);
+ // Delete all events using the sync adapter
+ // parameter so that the deletion is only local
+ Uri eventsAsSyncAdapter =
+ asSyncAdapter(
+ Events.CONTENT_URI,
+ mAccountName,
+ getAccountManagerType());
+ mResolver.delete(eventsAsSyncAdapter, WHERE_CALENDAR_ID,
+ new String[] {Long.toString(mCalendarId)});
+ } else {
+ // Make this a push mailbox and kick; this will start
+ // a resync of the Calendar; the account mailbox will
+ // ping on this during the next cycle of the ping loop
+ cv.put(Mailbox.SYNC_INTERVAL,
+ Mailbox.CHECK_INTERVAL_PUSH);
+ mResolver.update(ContentUris.withAppendedId(
+ Mailbox.CONTENT_URI, mailbox.mId), cv, null,
+ null);
+ kick("calendar sync changed");
+ }
+
+ // Save away the new value
+ mSyncEvents = newSyncEvents;
+ }
+ }
+ } finally {
+ c.close();
+ }
+ } catch (ProviderUnavailableException e) {
+ Log.w(TAG, "Observer failed; provider unavailable");
+ }
+ }}, "Calendar Observer").start();
+ }
+ }
+ }
+
+ private class MailboxObserver extends ContentObserver {
+ public MailboxObserver(Handler handler) {
+ super(handler);
+ }
+
+ @Override
+ public void onChange(boolean selfChange) {
+ // See if there's anything to do...
+ if (!selfChange) {
+ kick("mailbox changed");
+ }
+ }
+ }
+
+ private class SyncedMessageObserver extends ContentObserver {
+ Intent syncAlarmIntent = new Intent(INSTANCE, EmailSyncAlarmReceiver.class);
+ PendingIntent syncAlarmPendingIntent =
+ PendingIntent.getBroadcast(INSTANCE, 0, syncAlarmIntent, 0);
+ AlarmManager alarmManager = (AlarmManager)INSTANCE.getSystemService(Context.ALARM_SERVICE);
+
+ public SyncedMessageObserver(Handler handler) {
+ super(handler);
+ }
+
+ @Override
+ public void onChange(boolean selfChange) {
+ alarmManager.set(AlarmManager.RTC_WAKEUP,
+ System.currentTimeMillis() + 10*SECONDS, syncAlarmPendingIntent);
+ }
+ }
+
+ static public Account getAccountById(long accountId) {
+ SyncServiceManager ssm = INSTANCE;
+ if (ssm != null) {
+ AccountList accountList = ssm.mAccountList;
+ synchronized (accountList) {
+ return accountList.getById(accountId);
+ }
+ }
+ return null;
+ }
+
+ static public Account getAccountByName(String accountName) {
+ SyncServiceManager ssm = INSTANCE;
+ if (ssm != null) {
+ AccountList accountList = ssm.mAccountList;
+ synchronized (accountList) {
+ return accountList.getByName(accountName);
+ }
+ }
+ return null;
+ }
+
+ public class SyncStatus {
+ static public final int NOT_RUNNING = 0;
+ static public final int DIED = 1;
+ static public final int SYNC = 2;
+ static public final int IDLE = 3;
+ }
+
+ /*package*/ public class SyncError {
+ int reason;
+ public boolean fatal = false;
+ long holdDelay = 15*SECONDS;
+ public long holdEndTime = System.currentTimeMillis() + holdDelay;
+
+ public SyncError(int _reason, boolean _fatal) {
+ reason = _reason;
+ fatal = _fatal;
+ }
+
+ /**
+ * We double the holdDelay from 15 seconds through 4 mins
+ */
+ void escalate() {
+ if (holdDelay < HOLD_DELAY_MAXIMUM) {
+ holdDelay *= 2;
+ }
+ holdEndTime = System.currentTimeMillis() + holdDelay;
+ }
+ }
+
+ private void logSyncHolds() {
+ if (sUserLog) {
+ log("Sync holds:");
+ long time = System.currentTimeMillis();
+ for (long mailboxId : mSyncErrorMap.keySet()) {
+ Mailbox m = Mailbox.restoreMailboxWithId(this, mailboxId);
+ if (m == null) {
+ log("Mailbox " + mailboxId + " no longer exists");
+ } else {
+ SyncError error = mSyncErrorMap.get(mailboxId);
+ if (error != null) {
+ log("Mailbox " + m.mDisplayName + ", error = " + error.reason
+ + ", fatal = " + error.fatal);
+ if (error.holdEndTime > 0) {
+ log("Hold ends in " + ((error.holdEndTime - time) / 1000) + "s");
+ }
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * Release security holds for the specified account
+ * @param account the account whose Mailboxes should be released from security hold
+ */
+ static public void releaseSecurityHold(Account account) {
+ SyncServiceManager ssm = INSTANCE;
+ if (ssm != null) {
+ ssm.releaseSyncHolds(INSTANCE, AbstractSyncService.EXIT_SECURITY_FAILURE,
+ account);
+ }
+ }
+
+ /**
+ * Release a specific type of hold (the reason) for the specified Account; if the account
+ * is null, mailboxes from all accounts with the specified hold will be released
+ * @param reason the reason for the SyncError (AbstractSyncService.EXIT_XXX)
+ * @param account an Account whose mailboxes should be released (or all if null)
+ * @return whether or not any mailboxes were released
+ */
+ public /*package*/ boolean releaseSyncHolds(Context context, int reason, Account account) {
+ boolean holdWasReleased = releaseSyncHoldsImpl(context, reason, account);
+ kick("security release");
+ return holdWasReleased;
+ }
+
+ private boolean releaseSyncHoldsImpl(Context context, int reason, Account account) {
+ boolean holdWasReleased = false;
+ for (long mailboxId: mSyncErrorMap.keySet()) {
+ if (account != null) {
+ Mailbox m = Mailbox.restoreMailboxWithId(context, mailboxId);
+ if (m == null) {
+ mSyncErrorMap.remove(mailboxId);
+ } else if (m.mAccountKey != account.mId) {
+ continue;
+ }
+ }
+ SyncError error = mSyncErrorMap.get(mailboxId);
+ if (error != null && error.reason == reason) {
+ mSyncErrorMap.remove(mailboxId);
+ holdWasReleased = true;
+ }
+ }
+ return holdWasReleased;
+ }
+
+ public static void log(String str) {
+ log(TAG, str);
+ }
+
+ public static void log(String tag, String str) {
+ if (sUserLog) {
+ Log.d(tag, str);
+ if (sFileLog) {
+ FileLogger.log(tag, str);
+ }
+ }
+ }
+
+ public static void alwaysLog(String str) {
+ if (!sUserLog) {
+ Log.d(TAG, str);
+ } else {
+ log(str);
+ }
+ }
+
+ /**
+ * EAS requires a unique device id, so that sync is possible from a variety of different
+ * devices (e.g. the syncKey is specific to a device) If we're on an emulator or some other
+ * device that doesn't provide one, we can create it as "device".
+ * This would work on a real device as well, but it would be better to use the "real" id if
+ * it's available
+ */
+ static public String getDeviceId(Context context) throws IOException {
+ if (sDeviceId == null) {
+ sDeviceId = new AccountServiceProxy(context).getDeviceId();
+ alwaysLog("Received deviceId from Email app: " + sDeviceId);
+ }
+ return sDeviceId;
+ }
+
+ static public ConnPerRoute sConnPerRoute = new ConnPerRoute() {
+ @Override
+ public int getMaxForRoute(HttpRoute route) {
+ return 8;
+ }
+ };
+
+ static public synchronized EmailClientConnectionManager getClientConnectionManager(boolean ssl,
+ int port) {
+ // We'll use a different connection manager for each ssl/port pair
+ int key = (ssl ? 0x10000 : 0) + port;
+ EmailClientConnectionManager mgr = sClientConnectionManagers.get(key);
+ if (mgr == null) {
+ // After two tries, kill the process. Most likely, this will happen in the background
+ // The service will restart itself after about 5 seconds
+ if (sClientConnectionManagerShutdownCount > MAX_CLIENT_CONNECTION_MANAGER_SHUTDOWNS) {
+ alwaysLog("Shutting down process to unblock threads");
+ Process.killProcess(Process.myPid());
+ }
+ HttpParams params = new BasicHttpParams();
+ params.setIntParameter(ConnManagerPNames.MAX_TOTAL_CONNECTIONS, 25);
+ params.setParameter(ConnManagerPNames.MAX_CONNECTIONS_PER_ROUTE, sConnPerRoute);
+ mgr = EmailClientConnectionManager.newInstance(params, ssl, port);
+ log("Creating connection manager for port " + port + ", ssl: " + ssl);
+ sClientConnectionManagers.put(key, mgr);
+ }
+ // Null is a valid return result if we get an exception
+ return mgr;
+ }
+
+ static private synchronized void shutdownConnectionManager() {
+ log("Shutting down ClientConnectionManagers");
+ for (EmailClientConnectionManager mgr: sClientConnectionManagers.values()) {
+ mgr.shutdown();
+ }
+ sClientConnectionManagers.clear();
+ }
+
+ public static void stopAccountSyncs(long acctId) {
+ SyncServiceManager ssm = INSTANCE;
+ if (ssm != null) {
+ ssm.stopAccountSyncs(acctId, true);
+ }
+ }
+
+ public void stopAccountSyncs(long acctId, boolean includeAccountMailbox) {
+ synchronized (sSyncLock) {
+ List<Long> deletedBoxes = new ArrayList<Long>();
+ for (Long mid : mServiceMap.keySet()) {
+ Mailbox box = Mailbox.restoreMailboxWithId(this, mid);
+ if (box != null) {
+ if (box.mAccountKey == acctId) {
+ if (!includeAccountMailbox &&
+ box.mType == Mailbox.TYPE_EAS_ACCOUNT_MAILBOX) {
+ AbstractSyncService svc = mServiceMap.get(mid);
+ if (svc != null) {
+ svc.stop();
+ }
+ continue;
+ }
+ AbstractSyncService svc = mServiceMap.get(mid);
+ if (svc != null) {
+ svc.stop();
+ Thread t = svc.mThread;
+ if (t != null) {
+ t.interrupt();
+ }
+ }
+ deletedBoxes.add(mid);
+ }
+ }
+ }
+ for (Long mid : deletedBoxes) {
+ releaseMailbox(mid);
+ }
+ }
+ }
+
+ /**
+ * Informs SyncServiceManager that an account has a new folder list; as a result, any existing
+ * folder might have become invalid. Therefore, we act as if the account has been deleted, and
+ * then we reinitialize it.
+ *
+ * @param acctId
+ */
+ static public void stopNonAccountMailboxSyncsForAccount(long acctId) {
+ SyncServiceManager ssm = INSTANCE;
+ if (ssm != null) {
+ ssm.stopAccountSyncs(acctId, false);
+ kick("reload folder list");
+ }
+ }
+
+ private void acquireWakeLock(long id) {
+ synchronized (mWakeLocks) {
+ Boolean lock = mWakeLocks.get(id);
+ if (lock == null) {
+ if (mWakeLock == null) {
+ PowerManager pm = (PowerManager)getSystemService(Context.POWER_SERVICE);
+ mWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "MAIL_SERVICE");
+ mWakeLock.acquire();
+ //log("+WAKE LOCK ACQUIRED");
+ }
+ mWakeLocks.put(id, true);
+ }
+ }
+ }
+
+ private void releaseWakeLock(long id) {
+ synchronized (mWakeLocks) {
+ Boolean lock = mWakeLocks.get(id);
+ if (lock != null) {
+ mWakeLocks.remove(id);
+ if (mWakeLocks.isEmpty()) {
+ if (mWakeLock != null) {
+ mWakeLock.release();
+ }
+ mWakeLock = null;
+ //log("+WAKE LOCK RELEASED");
+ } else {
+ }
+ }
+ }
+ }
+
+ static public String alarmOwner(long id) {
+ if (id == EXTRA_MAILBOX_ID) {
+ return TAG;
+ } else {
+ String name = Long.toString(id);
+ if (sUserLog && INSTANCE != null) {
+ Mailbox m = Mailbox.restoreMailboxWithId(INSTANCE, id);
+ if (m != null) {
+ name = m.mDisplayName + '(' + m.mAccountKey + ')';
+ }
+ }
+ return "Mailbox " + name;
+ }
+ }
+
+ private void clearAlarm(long id) {
+ synchronized (mPendingIntents) {
+ PendingIntent pi = mPendingIntents.get(id);
+ if (pi != null) {
+ AlarmManager alarmManager = (AlarmManager)getSystemService(Context.ALARM_SERVICE);
+ alarmManager.cancel(pi);
+ //log("+Alarm cleared for " + alarmOwner(id));
+ mPendingIntents.remove(id);
+ }
+ }
+ }
+
+ private void setAlarm(long id, long millis) {
+ synchronized (mPendingIntents) {
+ PendingIntent pi = mPendingIntents.get(id);
+ if (pi == null) {
+ Intent i = new Intent(this, MailboxAlarmReceiver.class);
+ i.putExtra("mailbox", id);
+ i.setData(Uri.parse("Box" + id));
+ pi = PendingIntent.getBroadcast(this, 0, i, 0);
+ mPendingIntents.put(id, pi);
+
+ AlarmManager alarmManager = (AlarmManager)getSystemService(Context.ALARM_SERVICE);
+ alarmManager.set(AlarmManager.RTC_WAKEUP, System.currentTimeMillis() + millis, pi);
+ //log("+Alarm set for " + alarmOwner(id) + ", " + millis/1000 + "s");
+ }
+ }
+ }
+
+ private void clearAlarms() {
+ AlarmManager alarmManager = (AlarmManager)getSystemService(Context.ALARM_SERVICE);
+ synchronized (mPendingIntents) {
+ for (PendingIntent pi : mPendingIntents.values()) {
+ alarmManager.cancel(pi);
+ }
+ mPendingIntents.clear();
+ }
+ }
+
+ static public void runAwake(long id) {
+ SyncServiceManager ssm = INSTANCE;
+ if (ssm != null) {
+ ssm.acquireWakeLock(id);
+ ssm.clearAlarm(id);
+ }
+ }
+
+ static public void runAsleep(long id, long millis) {
+ SyncServiceManager ssm = INSTANCE;
+ if (ssm != null) {
+ ssm.setAlarm(id, millis);
+ ssm.releaseWakeLock(id);
+ }
+ }
+
+ static public void clearWatchdogAlarm(long id) {
+ SyncServiceManager ssm = INSTANCE;
+ if (ssm != null) {
+ ssm.clearAlarm(id);
+ }
+ }
+
+ static public void setWatchdogAlarm(long id, long millis) {
+ SyncServiceManager ssm = INSTANCE;
+ if (ssm != null) {
+ ssm.setAlarm(id, millis);
+ }
+ }
+
+ static public void alert(Context context, final long id) {
+ final SyncServiceManager ssm = INSTANCE;
+ checkSyncServiceManagerServiceRunning();
+ if (id < 0) {
+ log("SyncServiceManager alert");
+ kick("ping SyncServiceManager");
+ } else if (ssm == null) {
+ context.startService(new Intent(context, SyncServiceManager.class));
+ } else {
+ final AbstractSyncService service = ssm.mServiceMap.get(id);
+ if (service != null) {
+ // Handle alerts in a background thread, as we are typically called from a
+ // broadcast receiver, and are therefore running in the UI thread
+ String threadName = "SyncServiceManager Alert: ";
+ if (service.mMailbox != null) {
+ threadName += service.mMailbox.mDisplayName;
+ }
+ new Thread(new Runnable() {
+ @Override
+ public void run() {
+ Mailbox m = Mailbox.restoreMailboxWithId(ssm, id);
+ if (m != null) {
+ // We ignore drafts completely (doesn't sync). Changes in Outbox are
+ // handled in the checkMailboxes loop, so we can ignore these pings.
+ if (sUserLog) {
+ Log.d(TAG, "Alert for mailbox " + id + " (" + m.mDisplayName + ")");
+ }
+ if (m.mType == Mailbox.TYPE_DRAFTS || m.mType == Mailbox.TYPE_OUTBOX) {
+ String[] args = new String[] {Long.toString(m.mId)};
+ ContentResolver resolver = INSTANCE.mResolver;
+ resolver.delete(Message.DELETED_CONTENT_URI, WHERE_MAILBOX_KEY,
+ args);
+ resolver.delete(Message.UPDATED_CONTENT_URI, WHERE_MAILBOX_KEY,
+ args);
+ return;
+ }
+ service.mAccount = Account.restoreAccountWithId(INSTANCE, m.mAccountKey);
+ service.mMailbox = m;
+ // Send the alarm to the sync service
+ if (!service.alarm()) {
+ // A false return means that we were forced to interrupt the thread
+ // In this case, we release the mailbox so that we can start another
+ // thread to do the work
+ log("Alarm failed; releasing mailbox");
+ synchronized(sSyncLock) {
+ ssm.releaseMailbox(id);
+ }
+ // Shutdown the connection manager; this should close all of our
+ // sockets and generate IOExceptions all around.
+ SyncServiceManager.shutdownConnectionManager();
+ }
+ }
+ }}, threadName).start();
+ }
+ }
+ }
+
+ public class ConnectivityReceiver extends BroadcastReceiver {
+ @SuppressWarnings("deprecation")
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ if (intent.getAction().equals(ConnectivityManager.CONNECTIVITY_ACTION)) {
+ Bundle b = intent.getExtras();
+ if (b != null) {
+ NetworkInfo a = (NetworkInfo)b.get(ConnectivityManager.EXTRA_NETWORK_INFO);
+ String info = "Connectivity alert for " + a.getTypeName();
+ State state = a.getState();
+ if (state == State.CONNECTED) {
+ info += " CONNECTED";
+ log(info);
+ synchronized (sConnectivityLock) {
+ sConnectivityLock.notifyAll();
+ }
+ kick("connected");
+ } else if (state == State.DISCONNECTED) {
+ info += " DISCONNECTED";
+ log(info);
+ kick("disconnected");
+ }
+ }
+ } else if (intent.getAction().equals(
+ ConnectivityManager.ACTION_BACKGROUND_DATA_SETTING_CHANGED)) {
+ ConnectivityManager cm =
+ (ConnectivityManager)getSystemService(Context.CONNECTIVITY_SERVICE);
+ mBackgroundData = cm.getBackgroundDataSetting();
+ // If background data is now on, we want to kick SyncServiceManager
+ if (mBackgroundData) {
+ kick("background data on");
+ log("Background data on; restart syncs");
+ // Otherwise, stop all syncs
+ } else {
+ log("Background data off: stop all syncs");
+ EmailAsyncTask.runAsyncParallel(new Runnable() {
+ @Override
+ public void run() {
+ synchronized (mAccountList) {
+ for (Account account : mAccountList)
+ SyncServiceManager.stopAccountSyncs(account.mId);
+ }
+ }});
+ }
+ }
+ }
+ }
+
+ /**
+ * Starts a service thread and enters it into the service map
+ * This is the point of instantiation of all sync threads
+ * @param service the service to start
+ * @param m the Mailbox on which the service will operate
+ */
+ private void startServiceThread(AbstractSyncService service) {
+ synchronized (sSyncLock) {
+ Mailbox mailbox = service.mMailbox;
+ String mailboxName = mailbox.mDisplayName;
+ String accountName = service.mAccount.mDisplayName;
+ Thread thread = new Thread(service, mailboxName + "[" + accountName + "]");
+ log("Starting thread for " + mailboxName + " in account " + accountName);
+ thread.start();
+ mServiceMap.put(mailbox.mId, service);
+ runAwake(mailbox.mId);
+ if (mailbox.mServerId != null && mailbox.mType != Mailbox.TYPE_EAS_ACCOUNT_MAILBOX) {
+ stopPing(mailbox.mAccountKey);
+ }
+ }
+ }
+
+ /**
+ * Stop any ping in progress for the given account
+ * @param accountId
+ */
+ private void stopPing(long accountId) {
+ // Go through our active mailboxes looking for the right one
+ synchronized (sSyncLock) {
+ for (long mailboxId: mServiceMap.keySet()) {
+ Mailbox m = Mailbox.restoreMailboxWithId(this, mailboxId);
+ if (m != null) {
+ String serverId = m.mServerId;
+ if (m.mAccountKey == accountId && serverId != null &&
+ m.mType == Mailbox.TYPE_EAS_ACCOUNT_MAILBOX) {
+ // Here's our account mailbox; reset him (stopping pings)
+ AbstractSyncService svc = mServiceMap.get(mailboxId);
+ svc.reset();
+ }
+ }
+ }
+ }
+ }
+
+ private void requestSync(Mailbox m, int reason, Request req) {
+ int syncStatus = EmailContent.SYNC_STATUS_BACKGROUND;
+ // Don't sync if there's no connectivity
+ if (sConnectivityHold || (m == null) || sStop) {
+ if (reason >= SYNC_CALLBACK_START) {
+ try {
+ Stub proxy = getCallbackProxy();
+ if (proxy != null) {
+ proxy.syncMailboxStatus(m.mId, EmailServiceStatus.CONNECTION_ERROR, 0);
+ }
+ } catch (RemoteException e) {
+ // We tried...
+ }
+ }
+ return;
+ }
+ synchronized (sSyncLock) {
+ Account acct = Account.restoreAccountWithId(this, m.mAccountKey);
+ if (acct != null) {
+ // Always make sure there's not a running instance of this service
+ AbstractSyncService service = mServiceMap.get(m.mId);
+ if (service == null) {
+ service = getServiceForMailbox(this, m);
+ if (!service.mIsValid) return;
+ service.mSyncReason = reason;
+ if (req != null) {
+ service.addRequest(req);
+ }
+ startServiceThread(service);
+ if (reason >= SYNC_CALLBACK_START) {
+ syncStatus = EmailContent.SYNC_STATUS_USER;
+ }
+ setMailboxSyncStatus(m.mId, syncStatus);
+ }
+ }
+ }
+ }
+
+ private void setMailboxSyncStatus(long id, int status) {
+ ContentValues values = new ContentValues();
+ values.put(Mailbox.UI_SYNC_STATUS, status);
+ mResolver.update(ContentUris.withAppendedId(Mailbox.CONTENT_URI, id), values, null, null);
+ }
+
+ private void setMailboxLastSyncResult(long id, int result) {
+ ContentValues values = new ContentValues();
+ values.put(Mailbox.UI_LAST_SYNC_RESULT, result);
+ mResolver.update(ContentUris.withAppendedId(Mailbox.CONTENT_URI, id), values, null, null);
+ }
+
+ private void stopServiceThreads() {
+ synchronized (sSyncLock) {
+ ArrayList<Long> toStop = new ArrayList<Long>();
+
+ // Keep track of which services to stop
+ for (Long mailboxId : mServiceMap.keySet()) {
+ toStop.add(mailboxId);
+ }
+
+ // Shut down all of those running services
+ for (Long mailboxId : toStop) {
+ AbstractSyncService svc = mServiceMap.get(mailboxId);
+ if (svc != null) {
+ log("Stopping " + svc.mAccount.mDisplayName + '/' + svc.mMailbox.mDisplayName);
+ svc.stop();
+ if (svc.mThread != null) {
+ svc.mThread.interrupt();
+ }
+ }
+ releaseWakeLock(mailboxId);
+ }
+ }
+ }
+
+ private void waitForConnectivity() {
+ boolean waiting = false;
+ ConnectivityManager cm =
+ (ConnectivityManager)getSystemService(Context.CONNECTIVITY_SERVICE);
+ while (!sStop) {
+ NetworkInfo info = cm.getActiveNetworkInfo();
+ if (info != null) {
+ mNetworkInfo = info;
+ // We're done if there's an active network
+ if (waiting) {
+ // If we've been waiting, release any I/O error holds
+ releaseSyncHolds(this, AbstractSyncService.EXIT_IO_ERROR, null);
+ // And log what's still being held
+ logSyncHolds();
+ }
+ return;
+ } else {
+ // If this is our first time through the loop, shut down running service threads
+ if (!waiting) {
+ waiting = true;
+ stopServiceThreads();
+ }
+ // Wait until a network is connected (or 10 mins), but let the device sleep
+ // We'll set an alarm just in case we don't get notified (bugs happen)
+ synchronized (sConnectivityLock) {
+ runAsleep(EXTRA_MAILBOX_ID, CONNECTIVITY_WAIT_TIME+5*SECONDS);
+ try {
+ log("Connectivity lock...");
+ sConnectivityHold = true;
+ sConnectivityLock.wait(CONNECTIVITY_WAIT_TIME);
+ log("Connectivity lock released...");
+ } catch (InterruptedException e) {
+ // This is fine; we just go around the loop again
+ } finally {
+ sConnectivityHold = false;
+ }
+ runAwake(EXTRA_MAILBOX_ID);
+ }
+ }
+ }
+ }
+
+ /**
+ * Note that there are two ways the EAS SyncServiceManager service can be created:
+ *
+ * 1) as a background service instantiated via startService (which happens on boot, when the
+ * first EAS account is created, etc), in which case the service thread is spun up, mailboxes
+ * sync, etc. and
+ * 2) to execute an RPC call from the UI, in which case the background service will already be
+ * running most of the time (unless we're creating a first EAS account)
+ *
+ * If the running background service detects that there are no EAS accounts (on boot, if none
+ * were created, or afterward if the last remaining EAS account is deleted), it will call
+ * stopSelf() to terminate operation.
+ *
+ * The goal is to ensure that the background service is running at all times when there is at
+ * least one EAS account in existence
+ *
+ * Because there are edge cases in which our process can crash (typically, this has been seen
+ * in UI crashes, ANR's, etc.), it's possible for the UI to start up again without the
+ * background service having been started. We explicitly try to start the service in Welcome
+ * (to handle the case of the app having been reloaded). We also start the service on any
+ * startSync call (if it isn't already running)
+ */
+ @SuppressWarnings("deprecation")
+ @Override
+ public void onCreate() {
+ Utility.runAsync(new Runnable() {
+ @Override
+ public void run() {
+ // Quick checks first, before getting the lock
+ if (sStartingUp) return;
+ synchronized (sSyncLock) {
+ alwaysLog("!!! EAS SyncServiceManager, onCreate");
+ // Try to start up properly; we might be coming back from a crash that the Email
+ // application isn't aware of.
+ startService(new Intent(getServiceIntentAction()));
+ if (sStop) {
+ return;
+ }
+ }
+ }});
+ }
+
+ @SuppressWarnings("deprecation")
+ @Override
+ public int onStartCommand(Intent intent, int flags, int startId) {
+ alwaysLog("!!! EAS SyncServiceManager, onStartCommand, startingUp = " + sStartingUp +
+ ", running = " + (INSTANCE != null));
+ if (!sStartingUp && INSTANCE == null) {
+ sStartingUp = true;
+ Utility.runAsync(new Runnable() {
+ @Override
+ public void run() {
+ try {
+ synchronized (sSyncLock) {
+ // SyncServiceManager cannot start unless we connect to AccountService
+ if (!new AccountServiceProxy(SyncServiceManager.this).test()) {
+ alwaysLog("!!! Email application not found; stopping self");
+ stopSelf();
+ }
+ if (sDeviceId == null) {
+ try {
+ String deviceId = getDeviceId(SyncServiceManager.this);
+ if (deviceId != null) {
+ sDeviceId = deviceId;
+ }
+ } catch (IOException e) {
+ }
+ if (sDeviceId == null) {
+ alwaysLog("!!! deviceId unknown; stopping self and retrying");
+ stopSelf();
+ // Try to restart ourselves in a few seconds
+ Utility.runAsync(new Runnable() {
+ @Override
+ public void run() {
+ try {
+ Thread.sleep(5000);
+ } catch (InterruptedException e) {
+ }
+ startService(new Intent(getServiceIntentAction()));
+ }});
+ return;
+ }
+ }
+ // 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);
+ // Update other services depending on final account configuration
+ maybeStartSyncServiceManagerThread();
+ if (sServiceThread == null) {
+ log("!!! EAS SyncServiceManager, stopping self");
+ stopSelf();
+ } else if (sStop) {
+ // If we were trying to stop, attempt a restart in 5 secs
+ setAlarm(SYNC_SERVICE_MAILBOX_ID, 5*SECONDS);
+ }
+ }
+ } finally {
+ sStartingUp = false;
+ }
+ }});
+ }
+ return Service.START_STICKY;
+ }
+
+ public static void reconcileAccounts(Context context) {
+ SyncServiceManager ssm = INSTANCE;
+ if (ssm != null) {
+ ssm.runAccountReconcilerSync(context);
+ }
+ }
+
+ protected abstract void runAccountReconcilerSync(Context context);
+
+ @SuppressWarnings("deprecation")
+ @Override
+ public void onDestroy() {
+ log("!!! EAS SyncServiceManager, onDestroy");
+ // Handle shutting down off the UI thread
+ Utility.runAsync(new Runnable() {
+ @Override
+ public void run() {
+ // Quick checks first, before getting the lock
+ if (INSTANCE == null || sServiceThread == null) return;
+ synchronized(sSyncLock) {
+ // Stop the sync manager thread and return
+ if (sServiceThread != null) {
+ sStop = true;
+ sServiceThread.interrupt();
+ }
+ }
+ }});
+ }
+
+ void maybeStartSyncServiceManagerThread() {
+ // Start our thread...
+ // See if there are any EAS accounts; otherwise, just go away
+ if (sServiceThread == null || !sServiceThread.isAlive()) {
+ AccountList currentAccounts = new AccountList();
+ try {
+ collectAccounts(this, currentAccounts);
+ } catch (ProviderUnavailableException e) {
+ // Just leave if EmailProvider is unavailable
+ return;
+ }
+ if (!currentAccounts.isEmpty()) {
+ log(sServiceThread == null ? "Starting thread..." : "Restarting thread...");
+ sServiceThread = new Thread(this, TAG);
+ INSTANCE = this;
+ sServiceThread.start();
+ }
+ }
+ }
+
+ /**
+ * Start up the SyncServiceManager service if it's not already running
+ * This is a stopgap for cases in which SyncServiceManager died (due to a crash somewhere in
+ * com.android.email) and hasn't been restarted. See the comment for onCreate for details
+ */
+ static void checkSyncServiceManagerServiceRunning() {
+ SyncServiceManager ssm = INSTANCE;
+ if (ssm == null) return;
+ if (sServiceThread == null) {
+ log("!!! checkSyncServiceManagerServiceRunning; starting service...");
+ ssm.startService(new Intent(ssm, SyncServiceManager.class));
+ }
+ }
+
+ @SuppressWarnings("deprecation")
+ @Override
+ public void run() {
+ sStop = false;
+ alwaysLog("SyncServiceManager thread running");
+
+ TempDirectory.setTempDirectory(this);
+
+ // Synchronize here to prevent a shutdown from happening while we initialize our observers
+ // and receivers
+ synchronized (sSyncLock) {
+ if (INSTANCE != null) {
+ mResolver = getContentResolver();
+
+ // Set up our observers; we need them to know when to start/stop various syncs based
+ // on the insert/delete/update of mailboxes and accounts
+ // We also observe synced messages to trigger upsyncs at the appropriate time
+ mAccountObserver = getAccountObserver(mHandler);
+ mResolver.registerContentObserver(Account.NOTIFIER_URI, true, mAccountObserver);
+ mMailboxObserver = new MailboxObserver(mHandler);
+ mResolver.registerContentObserver(Mailbox.CONTENT_URI, false, mMailboxObserver);
+ mSyncedMessageObserver = new SyncedMessageObserver(mHandler);
+ mResolver.registerContentObserver(Message.SYNCED_CONTENT_URI, true,
+ mSyncedMessageObserver);
+
+ // Set up receivers for connectivity and background data setting
+ mConnectivityReceiver = new ConnectivityReceiver();
+ registerReceiver(mConnectivityReceiver, new IntentFilter(
+ ConnectivityManager.CONNECTIVITY_ACTION));
+
+ mBackgroundDataSettingReceiver = new ConnectivityReceiver();
+ registerReceiver(mBackgroundDataSettingReceiver, new IntentFilter(
+ ConnectivityManager.ACTION_BACKGROUND_DATA_SETTING_CHANGED));
+ // Save away the current background data setting; we'll keep track of it with the
+ // receiver we just registered
+ ConnectivityManager cm = (ConnectivityManager)getSystemService(
+ Context.CONNECTIVITY_SERVICE);
+ mBackgroundData = cm.getBackgroundDataSetting();
+
+ onStartup();
+ }
+ }
+
+ try {
+ // Loop indefinitely until we're shut down
+ while (!sStop) {
+ runAwake(EXTRA_MAILBOX_ID);
+ waitForConnectivity();
+ mNextWaitReason = null;
+ long nextWait = checkMailboxes();
+ try {
+ synchronized (this) {
+ if (!mKicked) {
+ if (nextWait < 0) {
+ log("Negative wait? Setting to 1s");
+ nextWait = 1*SECONDS;
+ }
+ if (nextWait > 10*SECONDS) {
+ if (mNextWaitReason != null) {
+ log("Next awake " + nextWait / 1000 + "s: " + mNextWaitReason);
+ }
+ runAsleep(EXTRA_MAILBOX_ID, nextWait + (3*SECONDS));
+ }
+ wait(nextWait);
+ }
+ }
+ } catch (InterruptedException e) {
+ // Needs to be caught, but causes no problem
+ log("SyncServiceManager interrupted");
+ } finally {
+ synchronized (this) {
+ if (mKicked) {
+ //log("Wait deferred due to kick");
+ mKicked = false;
+ }
+ }
+ }
+ }
+ log("Shutdown requested");
+ } catch (ProviderUnavailableException pue) {
+ // Shutdown cleanly in this case
+ // NOTE: Sync adapters will also crash with this error, but that is already handled
+ // in the adapters themselves, i.e. they return cleanly via done(). When the Email
+ // 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));
+ } catch (RuntimeException e) {
+ // Crash; this is a completely unexpected runtime error
+ Log.e(TAG, "RuntimeException in SyncServiceManager", e);
+ throw e;
+ } finally {
+ shutdown();
+ }
+ }
+
+ private void shutdown() {
+ synchronized (sSyncLock) {
+ // If INSTANCE is null, we've already been shut down
+ if (INSTANCE != null) {
+ log("SyncServiceManager shutting down...");
+
+ // Stop our running syncs
+ stopServiceThreads();
+
+ // Stop receivers
+ if (mConnectivityReceiver != null) {
+ unregisterReceiver(mConnectivityReceiver);
+ }
+ if (mBackgroundDataSettingReceiver != null) {
+ unregisterReceiver(mBackgroundDataSettingReceiver);
+ }
+
+ // Unregister observers
+ ContentResolver resolver = getContentResolver();
+ if (mSyncedMessageObserver != null) {
+ resolver.unregisterContentObserver(mSyncedMessageObserver);
+ mSyncedMessageObserver = null;
+ }
+ if (mAccountObserver != null) {
+ resolver.unregisterContentObserver(mAccountObserver);
+ mAccountObserver = null;
+ }
+ if (mMailboxObserver != null) {
+ resolver.unregisterContentObserver(mMailboxObserver);
+ mMailboxObserver = null;
+ }
+ unregisterCalendarObservers();
+
+ // Clear pending alarms and associated Intents
+ clearAlarms();
+
+ // Release our wake lock, if we have one
+ synchronized (mWakeLocks) {
+ if (mWakeLock != null) {
+ mWakeLock.release();
+ mWakeLock = null;
+ }
+ }
+
+ INSTANCE = null;
+ sServiceThread = null;
+ sStop = false;
+ log("Goodbye");
+ }
+ }
+ }
+
+ /**
+ * Release a mailbox from the service map and release its wake lock.
+ * NOTE: This method MUST be called while holding sSyncLock!
+ *
+ * @param mailboxId the id of the mailbox to be released
+ */
+ public void releaseMailbox(long mailboxId) {
+ mServiceMap.remove(mailboxId);
+ releaseWakeLock(mailboxId);
+ }
+
+ /**
+ * Check whether an Outbox (referenced by a Cursor) has any messages that can be sent
+ * @param c the cursor to an Outbox
+ * @return true if there is mail to be sent
+ */
+ private boolean hasSendableMessages(Cursor outboxCursor) {
+ Cursor c = mResolver.query(Message.CONTENT_URI, Message.ID_COLUMN_PROJECTION,
+ MAILBOX_KEY_AND_NOT_SEND_FAILED,
+ new String[] {Long.toString(outboxCursor.getLong(Mailbox.CONTENT_ID_COLUMN))},
+ null);
+ try {
+ while (c.moveToNext()) {
+ if (!Utility.hasUnloadedAttachments(this, c.getLong(Message.CONTENT_ID_COLUMN))) {
+ return true;
+ }
+ }
+ } finally {
+ if (c != null) {
+ c.close();
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Taken from ConnectivityManager using public constants
+ */
+ public static boolean isNetworkTypeMobile(int networkType) {
+ switch (networkType) {
+ case ConnectivityManager.TYPE_MOBILE:
+ case ConnectivityManager.TYPE_MOBILE_MMS:
+ case ConnectivityManager.TYPE_MOBILE_SUPL:
+ case ConnectivityManager.TYPE_MOBILE_DUN:
+ case ConnectivityManager.TYPE_MOBILE_HIPRI:
+ return true;
+ default:
+ return false;
+ }
+ }
+
+ /**
+ * Determine whether the account is allowed to sync automatically, as opposed to manually, based
+ * on whether the "require manual sync when roaming" policy is in force and applicable
+ * @param account the account
+ * @return whether or not the account can sync automatically
+ */
+ /*package*/ public static boolean canAutoSync(Account account) {
+ SyncServiceManager ssm = INSTANCE;
+ if (ssm == null) {
+ return false;
+ }
+ NetworkInfo networkInfo = ssm.mNetworkInfo;
+
+ // Enforce manual sync only while roaming here
+ long policyKey = account.mPolicyKey;
+ // Quick exit from this check
+ if ((policyKey != 0) && (networkInfo != null) &&
+ isNetworkTypeMobile(networkInfo.getType())) {
+ // We'll cache the Policy data here
+ Policy policy = account.mPolicy;
+ if (policy == null) {
+ policy = Policy.restorePolicyWithId(INSTANCE, policyKey);
+ account.mPolicy = policy;
+ if (!PolicyServiceProxy.isActive(ssm, policy)) return false;
+ }
+ if (policy != null && policy.mRequireManualSyncWhenRoaming && networkInfo.isRoaming()) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ /**
+ * Convenience method to determine whether Email sync is enabled for a given account
+ * @param account the Account in question
+ * @return whether Email sync is enabled
+ */
+ private boolean canSyncEmail(android.accounts.Account account) {
+ return ContentResolver.getSyncAutomatically(account, EmailContent.AUTHORITY);
+ }
+
+ /**
+ * Determine whether a mailbox of a given type in a given account can be synced automatically
+ * by SyncServiceManager. This is an increasingly complex determination, taking into account
+ * security policies and user settings (both within the Email application and in the Settings
+ * application)
+ *
+ * @param account the Account that the mailbox is in
+ * @param type the type of the Mailbox
+ * @return whether or not to start a sync
+ */
+ private boolean isMailboxSyncable(Account account, int type) {
+ // This 'if' statement performs checks to see whether or not a mailbox is a
+ // candidate for syncing based on policies, user settings, & other restrictions
+ if (type == Mailbox.TYPE_OUTBOX) {
+ // Outbox is always syncable
+ return true;
+ } else if (type == Mailbox.TYPE_EAS_ACCOUNT_MAILBOX) {
+ // Always sync EAS mailbox unless master sync is off
+ return ContentResolver.getMasterSyncAutomatically();
+ } else if (type == Mailbox.TYPE_CONTACTS || type == Mailbox.TYPE_CALENDAR) {
+ // Contacts/Calendar obey this setting from ContentResolver
+ if (!ContentResolver.getMasterSyncAutomatically()) {
+ return false;
+ }
+ // Get the right authority for the mailbox
+ String authority;
+ if (type == Mailbox.TYPE_CONTACTS) {
+ authority = ContactsContract.AUTHORITY;
+ } else {
+ authority = CalendarContract.AUTHORITY;
+ if (!mCalendarObservers.containsKey(account.mId)){
+ // Make sure we have an observer for this Calendar, as
+ // we need to be able to detect sync state changes, sigh
+ registerCalendarObserver(account);
+ }
+ }
+ // See if "sync automatically" is set; if not, punt
+ if (!ContentResolver.getSyncAutomatically(account.mAmAccount, authority)) {
+ return false;
+ // See if the calendar is enabled from the Calendar app UI; if not, punt
+ } else if ((type == Mailbox.TYPE_CALENDAR) && !isCalendarEnabled(account.mId)) {
+ return false;
+ }
+ // Never automatically sync trash
+ } else if (type == Mailbox.TYPE_TRASH) {
+ return false;
+ // For non-outbox, non-account mail, we do three checks:
+ // 1) are we restricted by policy (i.e. manual sync only),
+ // 2) has the user checked the "Sync Email" box in Account Settings, and
+ // 3) does the user have the master "background data" box checked in Settings
+ } else if (!canAutoSync(account) || !canSyncEmail(account.mAmAccount) || !mBackgroundData) {
+ return false;
+ }
+ return true;
+ }
+
+ private long checkMailboxes () {
+ // First, see if any running mailboxes have been deleted
+ ArrayList<Long> deletedMailboxes = new ArrayList<Long>();
+ synchronized (sSyncLock) {
+ for (long mailboxId: mServiceMap.keySet()) {
+ Mailbox m = Mailbox.restoreMailboxWithId(this, mailboxId);
+ if (m == null) {
+ deletedMailboxes.add(mailboxId);
+ }
+ }
+ // If so, stop them or remove them from the map
+ for (Long mailboxId: deletedMailboxes) {
+ AbstractSyncService svc = mServiceMap.get(mailboxId);
+ if (svc == null || svc.mThread == null) {
+ releaseMailbox(mailboxId);
+ continue;
+ } else {
+ boolean alive = svc.mThread.isAlive();
+ log("Deleted mailbox: " + svc.mMailboxName);
+ if (alive) {
+ stopManualSync(mailboxId);
+ } else {
+ log("Removing from serviceMap");
+ releaseMailbox(mailboxId);
+ }
+ }
+ }
+ }
+
+ long nextWait = SYNC_SERVICE_HEARTBEAT_TIME;
+ long now = System.currentTimeMillis();
+
+ // Start up threads that need it; use a query which finds eas mailboxes where the
+ // the sync interval is not "never". This is the set of mailboxes that we control
+ if (mAccountObserver == null) {
+ log("mAccountObserver null; service died??");
+ return nextWait;
+ }
+
+ Cursor c = getContentResolver().query(Mailbox.CONTENT_URI, Mailbox.CONTENT_PROJECTION,
+ mAccountObserver.getSyncableMailboxWhere(), null, null);
+ if (c == null) throw new ProviderUnavailableException();
+ try {
+ while (c.moveToNext()) {
+ long mailboxId = c.getLong(Mailbox.CONTENT_ID_COLUMN);
+ AbstractSyncService service = null;
+ synchronized (sSyncLock) {
+ service = mServiceMap.get(mailboxId);
+ }
+ if (service == null) {
+ // Get the cached account
+ Account account = getAccountById(c.getInt(Mailbox.CONTENT_ACCOUNT_KEY_COLUMN));
+ if (account == null) continue;
+
+ // We handle a few types of mailboxes specially
+ int mailboxType = c.getInt(Mailbox.CONTENT_TYPE_COLUMN);
+ if (!isMailboxSyncable(account, mailboxType)) {
+ continue;
+ }
+
+ // Check whether we're in a hold (temporary or permanent)
+ SyncError syncError = mSyncErrorMap.get(mailboxId);
+ if (syncError != null) {
+ // Nothing we can do about fatal errors
+ if (syncError.fatal) continue;
+ if (now < syncError.holdEndTime) {
+ // If release time is earlier than next wait time,
+ // move next wait time up to the release time
+ if (syncError.holdEndTime < now + nextWait) {
+ nextWait = syncError.holdEndTime - now;
+ mNextWaitReason = "Release hold";
+ }
+ continue;
+ } else {
+ // Keep the error around, but clear the end time
+ syncError.holdEndTime = 0;
+ }
+ }
+
+ // Otherwise, we use the sync interval
+ long syncInterval = c.getInt(Mailbox.CONTENT_SYNC_INTERVAL_COLUMN);
+ if (syncInterval == Mailbox.CHECK_INTERVAL_PUSH) {
+ Mailbox m = EmailContent.getContent(c, Mailbox.class);
+ requestSync(m, SYNC_PUSH, null);
+ } else if (mailboxType == Mailbox.TYPE_OUTBOX) {
+ if (hasSendableMessages(c)) {
+ Mailbox m = EmailContent.getContent(c, Mailbox.class);
+ startServiceThread(getServiceForMailbox(this, m));
+ }
+ } else if (syncInterval > 0 && syncInterval <= ONE_DAY_MINUTES) {
+ long lastSync = c.getLong(Mailbox.CONTENT_SYNC_TIME_COLUMN);
+ long sinceLastSync = now - lastSync;
+ long toNextSync = syncInterval*MINUTES - sinceLastSync;
+ String name = c.getString(Mailbox.CONTENT_DISPLAY_NAME_COLUMN);
+ if (toNextSync <= 0) {
+ Mailbox m = EmailContent.getContent(c, Mailbox.class);
+ requestSync(m, SYNC_SCHEDULED, null);
+ } else if (toNextSync < nextWait) {
+ nextWait = toNextSync;
+ if (sUserLog) {
+ log("Next sync for " + name + " in " + nextWait/1000 + "s");
+ }
+ mNextWaitReason = "Scheduled sync, " + name;
+ } else if (sUserLog) {
+ log("Next sync for " + name + " in " + toNextSync/1000 + "s");
+ }
+ }
+ } else {
+ Thread thread = service.mThread;
+ // Look for threads that have died and remove them from the map
+ if (thread != null && !thread.isAlive()) {
+ if (sUserLog) {
+ log("Dead thread, mailbox released: " +
+ c.getString(Mailbox.CONTENT_DISPLAY_NAME_COLUMN));
+ }
+ releaseMailbox(mailboxId);
+ // Restart this if necessary
+ if (nextWait > 3*SECONDS) {
+ nextWait = 3*SECONDS;
+ mNextWaitReason = "Clean up dead thread(s)";
+ }
+ } else {
+ long requestTime = service.mRequestTime;
+ if (requestTime > 0) {
+ long timeToRequest = requestTime - now;
+ if (timeToRequest <= 0) {
+ service.mRequestTime = 0;
+ service.alarm();
+ } else if (requestTime > 0 && timeToRequest < nextWait) {
+ if (timeToRequest < 11*MINUTES) {
+ nextWait = timeToRequest < 250 ? 250 : timeToRequest;
+ mNextWaitReason = "Sync data change";
+ } else {
+ log("Illegal timeToRequest: " + timeToRequest);
+ }
+ }
+ }
+ }
+ }
+ }
+ } finally {
+ c.close();
+ }
+ return nextWait;
+ }
+
+ static public void serviceRequest(long mailboxId, int reason) {
+ serviceRequest(mailboxId, 5*SECONDS, reason);
+ }
+
+ /**
+ * Return a boolean indicating whether the mailbox can be synced
+ * @param m the mailbox
+ * @return whether or not the mailbox can be synced
+ */
+ public static boolean isSyncable(Mailbox m) {
+ return m.mType != Mailbox.TYPE_DRAFTS
+ && m.mType != Mailbox.TYPE_OUTBOX
+ && m.mType != Mailbox.TYPE_SEARCH
+ && m.mType < Mailbox.TYPE_NOT_SYNCABLE;
+ }
+
+ static public void serviceRequest(long mailboxId, long ms, int reason) {
+ SyncServiceManager ssm = INSTANCE;
+ if (ssm == null) return;
+ Mailbox m = Mailbox.restoreMailboxWithId(ssm, mailboxId);
+ if (m == null || !isSyncable(m)) return;
+ try {
+ AbstractSyncService service = ssm.mServiceMap.get(mailboxId);
+ if (service != null) {
+ service.mRequestTime = System.currentTimeMillis() + ms;
+ kick("service request");
+ } else {
+ startManualSync(mailboxId, reason, null);
+ }
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ }
+
+ static public void serviceRequestImmediate(long mailboxId) {
+ SyncServiceManager ssm = INSTANCE;
+ if (ssm == null) return;
+ AbstractSyncService service = ssm.mServiceMap.get(mailboxId);
+ if (service != null) {
+ service.mRequestTime = System.currentTimeMillis();
+ Mailbox m = Mailbox.restoreMailboxWithId(ssm, mailboxId);
+ if (m != null) {
+ service.mAccount = Account.restoreAccountWithId(ssm, m.mAccountKey);
+ service.mMailbox = m;
+ kick("service request immediate");
+ }
+ }
+ }
+
+ static public void sendMessageRequest(Request req) {
+ SyncServiceManager ssm = INSTANCE;
+ Message msg = Message.restoreMessageWithId(ssm, req.mMessageId);
+ if (msg == null) return;
+ long mailboxId = msg.mMailboxKey;
+ Mailbox mailbox = Mailbox.restoreMailboxWithId(ssm, mailboxId);
+ if (mailbox == null) return;
+
+ // If we're loading an attachment for Outbox, we want to look at the source message
+ // to find the loading mailbox
+ if (mailbox.mType == Mailbox.TYPE_OUTBOX) {
+ long sourceId = Utility.getFirstRowLong(ssm, Body.CONTENT_URI,
+ new String[] {BodyColumns.SOURCE_MESSAGE_KEY},
+ BodyColumns.MESSAGE_KEY + "=?",
+ new String[] {Long.toString(msg.mId)}, null, 0, -1L);
+ if (sourceId != -1L) {
+ EmailContent.Message sourceMsg =
+ EmailContent.Message.restoreMessageWithId(ssm, sourceId);
+ if (sourceMsg != null) {
+ mailboxId = sourceMsg.mMailboxKey;
+ }
+ }
+ }
+
+ AbstractSyncService service = ssm.mServiceMap.get(mailboxId);
+ if (service == null) {
+ startManualSync(mailboxId, SYNC_SERVICE_PART_REQUEST, req);
+ kick("part request");
+ } else {
+ service.addRequest(req);
+ }
+ }
+
+ /**
+ * Determine whether a given Mailbox can be synced, i.e. is not already syncing and is not in
+ * an error state
+ *
+ * @param mailboxId
+ * @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;
+ if (ssm == null) return PING_STATUS_OK;
+ // Already syncing...
+ if (ssm.mServiceMap.get(mailboxId) != null) {
+ return PING_STATUS_RUNNING;
+ }
+ // No errors or a transient error, don't ping...
+ SyncError error = ssm.mSyncErrorMap.get(mailboxId);
+ if (error != null) {
+ if (error.fatal) {
+ return PING_STATUS_UNABLE;
+ } else if (error.holdEndTime > 0) {
+ return PING_STATUS_WAITING;
+ }
+ }
+ return PING_STATUS_OK;
+ }
+
+ static public void startManualSync(long mailboxId, int reason, Request req) {
+ SyncServiceManager ssm = INSTANCE;
+ if (ssm == null) return;
+ synchronized (sSyncLock) {
+ AbstractSyncService svc = ssm.mServiceMap.get(mailboxId);
+ if (svc == null) {
+ ssm.mSyncErrorMap.remove(mailboxId);
+ Mailbox m = Mailbox.restoreMailboxWithId(ssm, mailboxId);
+ if (m != null) {
+ log("Starting sync for " + m.mDisplayName);
+ ssm.requestSync(m, reason, req);
+ }
+ } else {
+ // If this is a ui request, set the sync reason for the service
+ if (reason >= SYNC_CALLBACK_START) {
+ svc.mSyncReason = reason;
+ }
+ }
+ }
+ }
+
+ // DO NOT CALL THIS IN A LOOP ON THE SERVICEMAP
+ static public void stopManualSync(long mailboxId) {
+ SyncServiceManager ssm = INSTANCE;
+ if (ssm == null) return;
+ synchronized (sSyncLock) {
+ AbstractSyncService svc = ssm.mServiceMap.get(mailboxId);
+ if (svc != null) {
+ log("Stopping sync for " + svc.mMailboxName);
+ svc.stop();
+ svc.mThread.interrupt();
+ ssm.releaseWakeLock(mailboxId);
+ }
+ }
+ }
+
+ /**
+ * Wake up SyncServiceManager to check for mailboxes needing service
+ */
+ static public void kick(String reason) {
+ SyncServiceManager ssm = INSTANCE;
+ if (ssm != null) {
+ synchronized (ssm) {
+ //INSTANCE.log("Kick: " + reason);
+ ssm.mKicked = true;
+ ssm.notify();
+ }
+ }
+ if (sConnectivityLock != null) {
+ synchronized (sConnectivityLock) {
+ sConnectivityLock.notify();
+ }
+ }
+ }
+
+ /**
+ * Tell SyncServiceManager to remove the mailbox from the map of mailboxes with sync errors
+ * @param mailboxId the id of the mailbox
+ */
+ static public void removeFromSyncErrorMap(long mailboxId) {
+ SyncServiceManager ssm = INSTANCE;
+ if (ssm != null) {
+ ssm.mSyncErrorMap.remove(mailboxId);
+ }
+ }
+
+ private boolean isRunningInServiceThread(long mailboxId) {
+ AbstractSyncService syncService = mServiceMap.get(mailboxId);
+ Thread thisThread = Thread.currentThread();
+ return syncService != null && syncService.mThread != null &&
+ thisThread == syncService.mThread;
+ }
+
+ /**
+ * Sent by services indicating that their thread is finished; action depends on the exitStatus
+ * of the service.
+ *
+ * @param svc the service that is finished
+ */
+ static public void done(AbstractSyncService svc) {
+ SyncServiceManager ssm = INSTANCE;
+ if (ssm == null) return;
+ synchronized(sSyncLock) {
+ long mailboxId = svc.mMailboxId;
+ // If we're no longer the syncing thread for the mailbox, just return
+ if (!ssm.isRunningInServiceThread(mailboxId)) {
+ return;
+ }
+ ssm.releaseMailbox(mailboxId);
+ ssm.setMailboxSyncStatus(mailboxId, EmailContent.SYNC_STATUS_NONE);
+
+ ConcurrentHashMap<Long, SyncError> errorMap = ssm.mSyncErrorMap;
+ SyncError syncError = errorMap.get(mailboxId);
+
+ int exitStatus = svc.mExitStatus;
+ Mailbox m = Mailbox.restoreMailboxWithId(ssm, mailboxId);
+ if (m == null) return;
+
+ if (exitStatus != AbstractSyncService.EXIT_LOGIN_FAILURE) {
+ long accountId = m.mAccountKey;
+ Account account = Account.restoreAccountWithId(ssm, accountId);
+ if (account == null) return;
+ if (ssm.releaseSyncHolds(ssm,
+ AbstractSyncService.EXIT_LOGIN_FAILURE, account)) {
+ new AccountServiceProxy(ssm).notifyLoginSucceeded(accountId);
+ }
+ }
+
+ int lastResult = EmailContent.LAST_SYNC_RESULT_SUCCESS;
+ // For error states, whether the error is fatal (won't automatically be retried)
+ boolean errorIsFatal = true;
+ try {
+ switch (exitStatus) {
+ case AbstractSyncService.EXIT_DONE:
+ if (svc.hasPendingRequests()) {
+ // TODO Handle this case
+ }
+ errorMap.remove(mailboxId);
+ // If we've had a successful sync, clear the shutdown count
+ synchronized (SyncServiceManager.class) {
+ sClientConnectionManagerShutdownCount = 0;
+ }
+ // Leave now; other statuses are errors
+ return;
+ // I/O errors get retried at increasing intervals
+ case AbstractSyncService.EXIT_IO_ERROR:
+ if (syncError != null) {
+ syncError.escalate();
+ log(m.mDisplayName + " held for " + syncError.holdDelay + "ms");
+ } else {
+ log(m.mDisplayName + " added to syncErrorMap, hold for 15s");
+ }
+ lastResult = EmailContent.LAST_SYNC_RESULT_CONNECTION_ERROR;
+ errorIsFatal = false;
+ break;
+ // These errors are not retried automatically
+ case AbstractSyncService.EXIT_LOGIN_FAILURE:
+ new AccountServiceProxy(ssm).notifyLoginFailed(m.mAccountKey);
+ lastResult = EmailContent.LAST_SYNC_RESULT_AUTH_ERROR;
+ break;
+ case AbstractSyncService.EXIT_SECURITY_FAILURE:
+ case AbstractSyncService.EXIT_ACCESS_DENIED:
+ lastResult = EmailContent.LAST_SYNC_RESULT_SECURITY_ERROR;
+ break;
+ case AbstractSyncService.EXIT_EXCEPTION:
+ lastResult = EmailContent.LAST_SYNC_RESULT_INTERNAL_ERROR;
+ break;
+ }
+ // Add this box to the error map
+ errorMap.put(mailboxId, ssm.new SyncError(exitStatus, errorIsFatal));
+ } finally {
+ // Always set the last result
+ ssm.setMailboxLastSyncResult(mailboxId, lastResult);
+ kick("sync completed");
+ }
+ }
+ }
+
+ /**
+ * Given the status string from a Mailbox, return the type code for the last sync
+ * @param status the syncStatus column of a Mailbox
+ * @return
+ */
+ static public int getStatusType(String status) {
+ if (status == null) {
+ return -1;
+ } else {
+ return status.charAt(STATUS_TYPE_CHAR) - '0';
+ }
+ }
+
+ /**
+ * Given the status string from a Mailbox, return the change count for the last sync
+ * The change count is the number of adds + deletes + changes in the last sync
+ * @param status the syncStatus column of a Mailbox
+ * @return
+ */
+ static public int getStatusChangeCount(String status) {
+ try {
+ String s = status.substring(STATUS_CHANGE_COUNT_OFFSET);
+ return Integer.parseInt(s);
+ } catch (RuntimeException e) {
+ return -1;
+ }
+ }
+
+ static public Context getContext() {
+ return INSTANCE;
+ }
+}
diff --git a/res/layout-sw600dp/account_type.xml b/res/layout-sw600dp/account_type.xml
new file mode 100644
index 000000000..805f211a4
--- /dev/null
+++ b/res/layout-sw600dp/account_type.xml
@@ -0,0 +1,23 @@
+<?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.
+-->
+
+<!-- small -->
+
+ <Button xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_alignParentLeft="true"
+ android:layout_marginTop="@dimen/setup_buttons_vertical_spacing"
+ android:layout_marginLeft="48dip"
+ style="@style/accountSetupButton" />
diff --git a/res/layout/account_type.xml b/res/layout/account_type.xml
new file mode 100644
index 000000000..1740ee4fa
--- /dev/null
+++ b/res/layout/account_type.xml
@@ -0,0 +1,27 @@
+<?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.
+-->
+
+<!-- small -->
+
+ <Button xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/button"
+ android:text="@string/account_setup_options_mail_window_auto"
+ android:layout_height="wrap_content"
+ android:layout_width="150sp"
+ android:layout_marginTop="25dip"
+ android:minWidth="@dimen/button_minWidth"
+ android:layout_gravity="center_horizontal"
+ />
diff --git a/res/layout/conversation_item_view_normal.xml b/res/layout/conversation_item_view_normal.xml
new file mode 100644
index 000000000..e0652bd87
--- /dev/null
+++ b/res/layout/conversation_item_view_normal.xml
@@ -0,0 +1,102 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2011 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.
+-->
+
+<!-- This layout is used as a template to create custom view CanvasConversationHeaderView
+ in normal mode. To be able to get the correct measurements, every source field should
+ be populated with data here. E.g:
+ - Text View should set text to a random long string (android:text="@string/long_string")
+ - Image View should set source to a specific asset -->
+<LinearLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="@dimen/conversation_item_height"
+ android:orientation="vertical">
+ <RelativeLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:gravity="center_vertical">
+ <ImageView
+ android:id="@+id/reply_state"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="@dimen/replystate_margin_top"
+ android:layout_marginLeft="@dimen/replystate_margin_left"
+ android:layout_marginRight="@dimen/replystate_margin_right"
+ android:src="@drawable/ic_badge_reply_holo_light"
+ />
+ <com.android.mail.browse.SendersView
+ android:id="@+id/senders"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ style="@style/SendersStyle"
+ android:text="@string/long_string"
+ android:textSize="@dimen/senders_font_size"
+ android:lines="1"
+ android:layout_toRightOf="@id/reply_state" />
+ <RelativeLayout
+ android:layout_height="wrap_content"
+ android:layout_width="@dimen/total_conversationbar_width"
+ android:layout_alignParentRight="true">
+ <View android:id="@+id/color_bar"
+ android:layout_alignParentTop="true"
+ android:layout_height="@dimen/color_block_height"
+ android:layout_width="@dimen/total_conversationbar_width" />
+ <LinearLayout
+ android:layout_alignWithParentIfMissing="true"
+ android:layout_alignParentTop="true"
+ android:layout_height="wrap_content"
+ android:layout_width="@dimen/total_conversationbar_width"
+ android:layout_alignParentRight="true">
+ <ImageView
+ android:id="@+id/paperclip"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:src="@drawable/ic_attachment_holo_light"
+ android:layout_marginTop="6sp" />
+ <TextView
+ android:id="@+id/date"
+ android:layout_width="0dip"
+ android:layout_weight="1"
+ android:layout_height="wrap_content"
+ android:text="@string/long_string"
+ android:textSize="@dimen/date_font_size"
+ android:lines="1"
+ android:layout_marginRight="16dip"
+ android:layout_marginTop="10sp" />
+ </LinearLayout>
+ </RelativeLayout>
+ </RelativeLayout>
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content">
+ <ImageView
+ style="@style/CheckmarkStyle"
+ android:id="@+id/checkmark"
+ android:src="@drawable/btn_check_on_normal_holo_light"/>
+ <TextView
+ android:id="@+id/subject"
+ android:layout_width="@dimen/subject_width"
+ style="@style/SubjectStyle"
+ android:text="@string/long_string"
+ android:lines="2"/>
+ <ImageView
+ android:id="@+id/star"
+ style="@style/StarStyle"
+ android:src="@drawable/btn_star_off_normal_email_holo_light" />
+ </LinearLayout>
+</LinearLayout>
diff --git a/res/layout/conversation_item_view_wide.xml b/res/layout/conversation_item_view_wide.xml
new file mode 100644
index 000000000..8033afdec
--- /dev/null
+++ b/res/layout/conversation_item_view_wide.xml
@@ -0,0 +1,100 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2011 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.
+-->
+
+<!-- This layout is used as a template to create custom view CanvasConversationHeaderView
+ in wide mode. To be able to get the correct measurements, every source field should
+ be populated with data here. E.g:
+ - Text View should set text to a random long string (android:text="@string/long_string")
+ - Image View should set source to a specific asset -->
+<LinearLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:minHeight="64sp"
+ android:orientation="horizontal">
+ <ImageView
+ android:id="@+id/checkmark"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginLeft="16dip"
+ android:layout_marginRight="16dip"
+ android:layout_gravity="center_vertical"
+ android:src="@drawable/btn_check_on_normal_holo_light" />
+ <com.android.mail.browse.SendersView
+ android:id="@+id/senders"
+ android:layout_width="224dip"
+ android:layout_height="wrap_content"
+ android:text="@string/long_string"
+ android:textSize="@dimen/wide_senders_font_size"
+ android:layout_gravity="center_vertical"
+ android:maxLines="2"
+ android:layout_marginTop="@dimen/wide_senders_margin_top" />
+ <ImageView
+ android:id="@+id/reply_state"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginLeft="16dip"
+ android:layout_marginRight="16dip"
+ android:layout_gravity="center_vertical"
+ android:src="@drawable/ic_badge_reply_holo_light" />
+ <TextView
+ android:id="@+id/subject"
+ android:layout_width="0dip"
+ android:layout_weight="0.7"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center_vertical"
+ android:text="@string/long_string"
+ android:lines="2"
+ android:textColor="@color/subject_text_color_unread"
+ android:textSize="@dimen/wide_subject_font_size"
+ android:layout_marginTop="2sp"
+ android:layout_marginRight="@dimen/wide_subject_margin_right"/>
+ <LinearLayout
+ android:layout_width="wrap_content"
+ android:layout_height="match_parent"
+ android:orientation="vertical">
+ <LinearLayout
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal"
+ android:layout_gravity="center_vertical">
+ <ImageView
+ android:id="@+id/paperclip"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:src="@drawable/ic_attachment_holo_light"
+ android:layout_gravity="center_vertical"
+ android:layout_marginTop="@dimen/wide_attachment_margin_top"/>
+ <TextView
+ android:id="@+id/date"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center_vertical"
+ android:text="@string/date"
+ android:layout_marginTop="@dimen/wide_date_margin_top" />
+ </LinearLayout>
+ </LinearLayout>
+ <ImageView
+ android:id="@+id/star"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginLeft="16dip"
+ android:layout_marginRight="16dip"
+ android:layout_gravity="center_vertical"
+ android:src="@drawable/btn_star_off_normal_email_holo_light" />
+</LinearLayout>
diff --git a/res/layout/quick_response_edit_dialog.xml b/res/layout/quick_response_edit_dialog.xml
new file mode 100644
index 000000000..cdb8fb79f
--- /dev/null
+++ b/res/layout/quick_response_edit_dialog.xml
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+
+<RelativeLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:focusable="true"
+ android:paddingLeft="4dip"
+ android:paddingTop="6dip"
+ android:paddingRight="4dip"
+ android:paddingBottom="6dip">
+
+ <EditText
+ android:id="@+id/quick_response_text"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"/>
+
+</RelativeLayout> \ No newline at end of file
diff --git a/res/mipmap-hdpi/ic_launcher_mail.png b/res/mipmap-hdpi/ic_launcher_mail.png
new file mode 100644
index 000000000..2616df73a
--- /dev/null
+++ b/res/mipmap-hdpi/ic_launcher_mail.png
Binary files differ
diff --git a/res/mipmap-mdpi/ic_launcher_mail.png b/res/mipmap-mdpi/ic_launcher_mail.png
new file mode 100644
index 000000000..545902704
--- /dev/null
+++ b/res/mipmap-mdpi/ic_launcher_mail.png
Binary files differ
diff --git a/res/mipmap-xhdpi/ic_launcher_mail.png b/res/mipmap-xhdpi/ic_launcher_mail.png
new file mode 100644
index 000000000..91c768e45
--- /dev/null
+++ b/res/mipmap-xhdpi/ic_launcher_mail.png
Binary files differ
diff --git a/res/values-sw720dp-land/dimensions.xml b/res/values-sw720dp-land/dimensions.xml
new file mode 100644
index 000000000..1b775848d
--- /dev/null
+++ b/res/values-sw720dp-land/dimensions.xml
@@ -0,0 +1,24 @@
+<?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.
+-->
+
+<!-- tablet, landscape -->
+<resources>
+ <!-- Account Setup Activities -->
+ <dimen name="setup_padding_top">16dip</dimen>
+ <dimen name="setup_padding_left">128dip</dimen>
+ <dimen name="setup_padding_right">128dip</dimen>
+</resources>
+
diff --git a/res/xml/searchable.xml b/res/xml/searchable.xml
new file mode 100644
index 000000000..434d29974
--- /dev/null
+++ b/res/xml/searchable.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ 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.
+-->
+<searchable xmlns:android="http://schemas.android.com/apk/res/android"
+ android:label="@string/search_title"
+ android:hint="@string/search_hint"
+ android:icon="@drawable/ic_menu_search_holo_light"
+ android:searchSuggestAuthority="com.android.email.suggestionsprovider"
+ android:searchSuggestSelection="query LIKE ?"
+ android:searchSuggestIntentAction="android.intent.action.SEARCH"
+ android:imeOptions="actionSearch" />
diff --git a/res/xml/services.xml b/res/xml/services.xml
new file mode 100644
index 000000000..5cbb1f9a3
--- /dev/null
+++ b/res/xml/services.xml
@@ -0,0 +1,104 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+
+<!--
+ Email services (protocols) are defined here. For the present, these are baked into the Email
+ apk; the goal is for remote services to register themselves into this file.
+
+ The required attributes are as follows (except that EITHER serviceClass or intent is required):
+ protocol: the unique name used to identify the protocol
+ name: the name of the account type option presented to users during account setup
+ accountType: the AccountManager type of accounts created using this service
+ serviceClass: a class implementing IEmailService (or null, if the service is remote)
+ intent: the intent used to connect to a remote IEmailService
+ port: the (default) port used when creating accounts using this service
+ portSsl: as above, when SSL is selected
+ syncIntervalStrings: a reference to an array of sync interval options
+ syncIntervals: a reference to an array of values corresponding to syncIntervalStrings
+ defaultSyncInterval: the default sync interval, selected from enums defined in attrs.xml
+
+ The following optional attributes default to "false":
+ offerTls: whether a TLS option (e.g. STARTTLS) is offered for this service
+ offerCerts: whether or not certificate authentication is an option for this service
+ usesSmtp: whether SMTP is used as the outgoing protocol for this service
+ offerPrefix: whether a "prefix" is offered to the user (for IMAP)
+ offerLocalDeletes: whether an option to delete locally is offered
+ syncChanges: whether non-deletion changes to messages sync back to the server
+ offerAttachmentPreload: whether to offer attachment preloading (pre-caching)
+ usesAutodiscover: whether to attempt using the "autodiscover" API when creating
+ an account
+ offerLookback: whether a sync "lookback" is offered (rather than the POP/IMAP
+ legacy "25 most recent messages synced")
+ defaultLookback: if "lookback" is offered, an enum of possible lookbacks
+ syncCalendar: whether this service is capable of syncing a calendar (offering a checkbox)
+ syncContacts: whether this service is capable of syncing contacts (offering a checkbox)
+-->
+
+<emailservices xmlns:email="http://schemas.android.com/apk/res/com.android.email">
+ <emailservice
+ email:protocol="pop3"
+ email:name="POP3"
+ email:accountType="com.android.email"
+ email:serviceClass="com.android.email.service.ImapService"
+ email:port="110"
+ email:portSsl="995"
+ email:syncIntervalStrings="@array/account_settings_check_frequency_entries"
+ email:syncIntervals="@array/account_settings_check_frequency_values"
+ email:defaultSyncInterval="mins15"
+
+ email:offerTls="true"
+ email:usesSmtp="true"
+ email:offerLocalDeletes="true"
+ />
+ <emailservice
+ email:protocol="imap"
+ email:name="IMAP"
+ email:accountType="com.android.email"
+ email:serviceClass="com.android.email.service.Pop3Service"
+ email:port="143"
+ email:portSsl="993"
+ email:syncIntervalStrings="@array/account_settings_check_frequency_entries"
+ email:syncIntervals="@array/account_settings_check_frequency_values"
+ email:defaultSyncInterval="mins15"
+
+ email:offerTls="true"
+ email:usesSmtp="true"
+ email:offerAttachmentPreload="true"
+ email:offerPrefix="true"
+ email:syncChanges="true"
+ />
+ <emailservice
+ email:protocol="eas"
+ email:name="Exchange"
+ email:accountType="com.android.exchange"
+ email:intent="com.android.email.EXCHANGE_INTENT"
+ email:port="80"
+ email:portSsl="443"
+ email:syncIntervalStrings="@array/account_settings_check_frequency_entries_push"
+ email:syncIntervals="@array/account_settings_check_frequency_values_push"
+ email:defaultSyncInterval="push"
+
+ email:defaultSsl="true"
+ email:offerCerts="true"
+ email:syncChanges="true"
+ email:usesAutodiscover="true"
+ email:offerAttachmentPreload="true"
+ email:offerLookback="true"
+ email:defaultLookback="auto"
+ email:syncContacts="true"
+ email:syncCalendar="true"
+ />
+</emailservices>
diff --git a/src/com/android/email/activity/EventViewer.java b/src/com/android/email/activity/EventViewer.java
new file mode 100644
index 000000000..7eebeef11
--- /dev/null
+++ b/src/com/android/email/activity/EventViewer.java
@@ -0,0 +1,76 @@
+/**
+ * Copyright (c) 2012, Google Inc.
+ *
+ * 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.activity;
+
+import android.app.Activity;
+import android.content.ContentUris;
+import android.content.Intent;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.Bundle;
+import android.provider.CalendarContract;
+
+import com.android.emailcommon.mail.MeetingInfo;
+import com.android.emailcommon.mail.PackedString;
+import com.android.emailcommon.provider.EmailContent.Message;
+import com.android.emailcommon.utility.Utility;
+
+public class EventViewer extends Activity {
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ Uri uri = getIntent().getData();
+ long messageId = Long.parseLong(uri.getLastPathSegment());
+ Message msg = Message.restoreMessageWithId(this, messageId);
+ if (msg == null) {
+ finish();
+ } else {
+ PackedString info = new PackedString(msg.mMeetingInfo);
+ String uid = info.get(MeetingInfo.MEETING_UID);
+ long eventId = -1;
+ if (uid != null) {
+ Cursor c = getContentResolver().query(CalendarContract.Events.CONTENT_URI,
+ new String[] {CalendarContract.Events._ID},
+ CalendarContract.Events.SYNC_DATA2 + "=?",
+ new String[] {uid}, null);
+ if (c != null) {
+ try {
+ if (c.getCount() == 1) {
+ c.moveToFirst();
+ eventId = c.getLong(0);
+ }
+ } finally {
+ c.close();
+ }
+ }
+ }
+ Intent intent = new Intent(Intent.ACTION_VIEW);
+ if (eventId != -1) {
+ uri = ContentUris.withAppendedId(CalendarContract.Events.CONTENT_URI, eventId);
+ } else {
+ long time =
+ Utility.parseEmailDateTimeToMillis(info.get(MeetingInfo.MEETING_DTSTART));
+ uri = Uri.parse("content://com.android.calendar/time/" + time);
+ intent.putExtra("VIEW", "DAY");
+ }
+ intent.setData(uri);
+ intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET);
+ startActivity(intent);
+ finish();
+ }
+ }
+}
diff --git a/src/com/android/email/activity/setup/AccountSetupType.java b/src/com/android/email/activity/setup/AccountSetupType.java
new file mode 100644
index 000000000..ff830945c
--- /dev/null
+++ b/src/com/android/email/activity/setup/AccountSetupType.java
@@ -0,0 +1,138 @@
+/*
+ * 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.
+ */
+
+package com.android.email.activity.setup;
+
+import android.app.Activity;
+import android.content.Intent;
+import android.os.Bundle;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.view.ViewGroup;
+import android.widget.Button;
+import android.widget.RelativeLayout;
+import android.widget.RelativeLayout.LayoutParams;
+
+import com.android.email.R;
+import com.android.email.activity.ActivityHelper;
+import com.android.email.activity.UiUtilities;
+import com.android.email.service.EmailServiceUtils;
+import com.android.email.service.EmailServiceUtils.EmailServiceInfo;
+import com.android.emailcommon.provider.Account;
+import com.android.emailcommon.provider.HostAuth;
+
+/**
+ * Prompts the user to select an account type. The account type, along with the
+ * passed in email address, password and makeDefault are then passed on to the
+ * AccountSetupIncoming activity.
+ */
+public class AccountSetupType extends AccountSetupActivity implements OnClickListener {
+
+ public static void actionSelectAccountType(Activity fromActivity) {
+ Intent i = new ForwardingIntent(fromActivity, AccountSetupType.class);
+ fromActivity.startActivity(i);
+ }
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ ActivityHelper.debugSetWindowFlags(this);
+ int flowMode = SetupData.getFlowMode();
+
+ String accountType = SetupData.getFlowAccountType();
+ // If we're in account setup flow mode, see if there's just one protocol that matches
+ if (flowMode == SetupData.FLOW_MODE_ACCOUNT_MANAGER) {
+ int matches = 0;
+ String protocol = null;
+ for (EmailServiceInfo info: EmailServiceUtils.getServiceInfoList(this)) {
+ if (info.accountType.equals(accountType)) {
+ protocol = info.protocol;
+ matches++;
+ }
+ }
+ // If so, select it...
+ if (matches == 1) {
+ onSelect(protocol);
+ return;
+ }
+ }
+
+ // Otherwise proceed into this screen
+ setContentView(R.layout.account_setup_account_type);
+ ViewGroup parent = UiUtilities.getView(this, R.id.accountTypes);
+ boolean parentRelative = parent instanceof RelativeLayout;
+ View lastView = parent.getChildAt(0);
+ int i = 1;
+ for (EmailServiceInfo info: EmailServiceUtils.getServiceInfoList(this)) {
+ if (EmailServiceUtils.isServiceAvailable(this, info.protocol)) {
+ // If we're looking for a specific account type, reject others
+ if (accountType != null && !accountType.equals(info.accountType)) {
+ continue;
+ }
+ LayoutInflater.from(this).inflate(R.layout.account_type, parent);
+ Button button = (Button)parent.getChildAt(i);
+ if (parentRelative) {
+ LayoutParams params = (LayoutParams)button.getLayoutParams();
+ params.addRule(RelativeLayout.BELOW, lastView.getId());
+ }
+ button.setId(i);
+ button.setTag(info.protocol);
+ button.setText(info.name);
+ button.setOnClickListener(this);
+ lastView = button;
+ i++;
+ // TODO: Remember vendor overlay for exchange name
+ }
+ }
+ final Button previousButton = (Button) findViewById(R.id.previous); // xlarge only
+ if (previousButton != null) previousButton.setOnClickListener(this);
+ }
+
+ /**
+ * The user has selected an exchange account type. Set the mail delete policy here, because
+ * there is no UI (for exchange), and switch the default sync interval to "push".
+ */
+ private void onSelect(String protocol) {
+ Account account = SetupData.getAccount();
+ HostAuth recvAuth = account.getOrCreateHostAuthRecv(this);
+ recvAuth.setConnection(protocol, recvAuth.mAddress, recvAuth.mPort, recvAuth.mFlags);
+ EmailServiceInfo info = EmailServiceUtils.getServiceInfo(this, protocol);
+ if (info.usesAutodiscover) {
+ SetupData.setCheckSettingsMode(SetupData.CHECK_AUTODISCOVER);
+ } else {
+ SetupData.setCheckSettingsMode(
+ SetupData.CHECK_INCOMING | (info.usesSmtp ? SetupData.CHECK_OUTGOING : 0));
+ }
+ recvAuth.mLogin = recvAuth.mLogin + "@" + recvAuth.mAddress;
+ AccountSetupBasics.setDefaultsForProtocol(this, account);
+ AccountSetupIncoming.actionIncomingSettings(this, SetupData.getFlowMode(), account);
+ // Back from the incoming screen returns to AccountSetupBasics
+ finish();
+ }
+
+ @Override
+ public void onClick(View v) {
+ switch (v.getId()) {
+ case R.id.previous:
+ finish();
+ break;
+ default:
+ onSelect((String)v.getTag());
+ break;
+ }
+ }
+}
diff --git a/src/com/android/email/activity/setup/EmailPreferenceFragment.java b/src/com/android/email/activity/setup/EmailPreferenceFragment.java
new file mode 100644
index 000000000..9f379f873
--- /dev/null
+++ b/src/com/android/email/activity/setup/EmailPreferenceFragment.java
@@ -0,0 +1,39 @@
+/*
+ * 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.email.activity.setup;
+
+import android.os.Bundle;
+import android.preference.PreferenceFragment;
+import android.view.Menu;
+import android.view.MenuInflater;
+
+public class EmailPreferenceFragment extends PreferenceFragment {
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setHasOptionsMenu(true);
+ }
+
+ @Override
+ public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
+ //***
+ //if (!UiUtilities.useTwoPane(getActivity())) {
+ // menu.clear();
+ //}
+ }
+}
diff --git a/src/com/android/email/activity/setup/ForwardingIntent.java b/src/com/android/email/activity/setup/ForwardingIntent.java
new file mode 100644
index 000000000..1fc913ef7
--- /dev/null
+++ b/src/com/android/email/activity/setup/ForwardingIntent.java
@@ -0,0 +1,30 @@
+/* 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.activity.setup;
+
+import android.content.Context;
+import android.content.Intent;
+
+/**
+ * An intent that forwards results
+ */
+public class ForwardingIntent extends Intent {
+ public ForwardingIntent(Context activity, Class klass) {
+ super(activity, klass);
+ setFlags(Intent.FLAG_ACTIVITY_FORWARD_RESULT);
+ }
+}
diff --git a/src/com/android/email/activity/setup/PolicyListPreference.java b/src/com/android/email/activity/setup/PolicyListPreference.java
new file mode 100644
index 000000000..3a32438e7
--- /dev/null
+++ b/src/com/android/email/activity/setup/PolicyListPreference.java
@@ -0,0 +1,45 @@
+/*
+ * 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.email.activity.setup;
+
+import android.content.Context;
+import android.preference.Preference;
+import android.util.AttributeSet;
+import android.view.View;
+import android.widget.TextView;
+
+/**
+ * Simple text preference allowing a large number of lines
+ */
+public class PolicyListPreference extends Preference {
+ // Arbitrary, but large number (we don't, and won't, have nearly this many)
+ public static final int MAX_POLICIES = 24;
+
+ public PolicyListPreference(Context ctx, AttributeSet attrs, int defStyle) {
+ super(ctx, attrs, defStyle);
+ }
+
+ public PolicyListPreference(Context ctx, AttributeSet attrs) {
+ super(ctx, attrs);
+ }
+
+ @Override
+ protected void onBindView(View view) {
+ super.onBindView(view);
+ ((TextView)view.findViewById(android.R.id.summary)).setMaxLines(MAX_POLICIES);
+ }
+}
diff --git a/src/com/android/email/provider/DBHelper.java b/src/com/android/email/provider/DBHelper.java
new file mode 100644
index 000000000..8df131ea6
--- /dev/null
+++ b/src/com/android/email/provider/DBHelper.java
@@ -0,0 +1,1292 @@
+/*
+ * 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.email.provider;
+
+import android.accounts.AccountManager;
+import android.content.ContentResolver;
+import android.content.ContentValues;
+import android.content.Context;
+import android.database.Cursor;
+import android.database.SQLException;
+import android.database.sqlite.SQLiteDatabase;
+import android.database.sqlite.SQLiteOpenHelper;
+import android.provider.CalendarContract;
+import android.provider.ContactsContract;
+import android.util.Log;
+
+import com.android.email2.ui.MailActivityEmail;
+import com.android.emailcommon.AccountManagerTypes;
+import com.android.emailcommon.mail.Address;
+import com.android.emailcommon.provider.Account;
+import com.android.emailcommon.provider.EmailContent;
+import com.android.emailcommon.provider.EmailContent.AccountColumns;
+import com.android.emailcommon.provider.EmailContent.Attachment;
+import com.android.emailcommon.provider.EmailContent.AttachmentColumns;
+import com.android.emailcommon.provider.EmailContent.Body;
+import com.android.emailcommon.provider.EmailContent.BodyColumns;
+import com.android.emailcommon.provider.EmailContent.HostAuthColumns;
+import com.android.emailcommon.provider.EmailContent.MailboxColumns;
+import com.android.emailcommon.provider.EmailContent.Message;
+import com.android.emailcommon.provider.EmailContent.MessageColumns;
+import com.android.emailcommon.provider.EmailContent.PolicyColumns;
+import com.android.emailcommon.provider.EmailContent.QuickResponseColumns;
+import com.android.emailcommon.provider.EmailContent.SyncColumns;
+import com.android.emailcommon.provider.HostAuth;
+import com.android.emailcommon.provider.Mailbox;
+import com.android.emailcommon.provider.Policy;
+import com.android.emailcommon.provider.QuickResponse;
+import com.android.emailcommon.service.LegacyPolicySet;
+import com.android.mail.providers.UIProvider;
+import com.google.common.annotations.VisibleForTesting;
+
+public final class DBHelper {
+ private static final String TAG = "EmailProvider";
+
+ private static final String WHERE_ID = EmailContent.RECORD_ID + "=?";
+
+ private static final String TRIGGER_MAILBOX_DELETE =
+ "create trigger mailbox_delete before delete on " + Mailbox.TABLE_NAME +
+ " begin" +
+ " delete from " + Message.TABLE_NAME +
+ " where " + MessageColumns.MAILBOX_KEY + "=old." + EmailContent.RECORD_ID +
+ "; delete from " + Message.UPDATED_TABLE_NAME +
+ " where " + MessageColumns.MAILBOX_KEY + "=old." + EmailContent.RECORD_ID +
+ "; delete from " + Message.DELETED_TABLE_NAME +
+ " where " + MessageColumns.MAILBOX_KEY + "=old." + EmailContent.RECORD_ID +
+ "; end";
+
+ private static final String TRIGGER_ACCOUNT_DELETE =
+ "create trigger account_delete before delete on " + Account.TABLE_NAME +
+ " begin delete from " + Mailbox.TABLE_NAME +
+ " where " + MailboxColumns.ACCOUNT_KEY + "=old." + EmailContent.RECORD_ID +
+ "; delete from " + HostAuth.TABLE_NAME +
+ " where " + EmailContent.RECORD_ID + "=old." + AccountColumns.HOST_AUTH_KEY_RECV +
+ "; delete from " + HostAuth.TABLE_NAME +
+ " where " + EmailContent.RECORD_ID + "=old." + AccountColumns.HOST_AUTH_KEY_SEND +
+ "; delete from " + Policy.TABLE_NAME +
+ " where " + EmailContent.RECORD_ID + "=old." + AccountColumns.POLICY_KEY +
+ "; end";
+
+ // Any changes to the database format *must* include update-in-place code.
+ // Original version: 3
+ // Version 4: Database wipe required; changing AccountManager interface w/Exchange
+ // Version 5: Database wipe required; changing AccountManager interface w/Exchange
+ // Version 6: Adding Message.mServerTimeStamp column
+ // Version 7: Replace the mailbox_delete trigger with a version that removes orphaned messages
+ // from the Message_Deletes and Message_Updates tables
+ // Version 8: Add security flags column to accounts table
+ // Version 9: Add security sync key and signature to accounts table
+ // Version 10: Add meeting info to message table
+ // Version 11: Add content and flags to attachment table
+ // Version 12: Add content_bytes to attachment table. content is deprecated.
+ // Version 13: Add messageCount to Mailbox table.
+ // Version 14: Add snippet to Message table
+ // Version 15: Fix upgrade problem in version 14.
+ // Version 16: Add accountKey to Attachment table
+ // Version 17: Add parentKey to Mailbox table
+ // Version 18: Copy Mailbox.displayName to Mailbox.serverId for all IMAP & POP3 mailboxes.
+ // Column Mailbox.serverId is used for the server-side pathname of a mailbox.
+ // Version 19: Add Policy table; add policyKey to Account table and trigger to delete an
+ // Account's policy when the Account is deleted
+ // Version 20: Add new policies to Policy table
+ // Version 21: Add lastSeenMessageKey column to Mailbox table
+ // Version 22: Upgrade path for IMAP/POP accounts to integrate with AccountManager
+ // Version 23: Add column to mailbox table for time of last access
+ // Version 24: Add column to hostauth table for client cert alias
+ // Version 25: Added QuickResponse table
+ // Version 26: Update IMAP accounts to add FLAG_SUPPORTS_SEARCH flag
+ // Version 27: Add protocolSearchInfo to Message table
+ // Version 28: Add notifiedMessageId and notifiedMessageCount to Account
+ // Version 29: Add protocolPoliciesEnforced and protocolPoliciesUnsupported to Policy
+ // Version 30: Use CSV of RFC822 addresses instead of "packed" values
+ // Version 31: Add columns to mailbox for ui status/last result
+ // Version 32: Add columns to mailbox for last notified message key/count; insure not null
+ // for "notified" columns
+ // Version 33: Add columns to attachment for ui provider columns
+ // Version 34: Add total count to mailbox
+ // Version 35: Set up defaults for lastTouchedCount for drafts and sent
+ // Version 36: mblank intentionally left this space
+ // Version 37: Add flag for settings support in folders
+ // Version 38&39: Add threadTopic to message (for future support)
+ // Version 39 is last Email1 version
+ // Version 100 is first Email2 version
+ // Version 101 SHOULD NOT BE USED
+ // Version 102&103: Add hierarchicalName to Mailbox
+
+ public static final int DATABASE_VERSION = 103;
+
+ // Any changes to the database format *must* include update-in-place code.
+ // Original version: 2
+ // Version 3: Add "sourceKey" column
+ // Version 4: Database wipe required; changing AccountManager interface w/Exchange
+ // Version 5: Database wipe required; changing AccountManager interface w/Exchange
+ // Version 6: Adding Body.mIntroText column
+ // Version 7/8: Adding quoted text start pos
+ // Version 8 is last Email1 version
+ public static final int BODY_DATABASE_VERSION = 100;
+
+ /*
+ * Internal helper method for index creation.
+ * Example:
+ * "create index message_" + MessageColumns.FLAG_READ
+ * + " on " + Message.TABLE_NAME + " (" + MessageColumns.FLAG_READ + ");"
+ */
+ /* package */
+ static String createIndex(String tableName, String columnName) {
+ return "create index " + tableName.toLowerCase() + '_' + columnName
+ + " on " + tableName + " (" + columnName + ");";
+ }
+
+ static void createMessageTable(SQLiteDatabase db) {
+ String messageColumns = MessageColumns.DISPLAY_NAME + " text, "
+ + MessageColumns.TIMESTAMP + " integer, "
+ + MessageColumns.SUBJECT + " text, "
+ + MessageColumns.FLAG_READ + " integer, "
+ + MessageColumns.FLAG_LOADED + " integer, "
+ + MessageColumns.FLAG_FAVORITE + " integer, "
+ + MessageColumns.FLAG_ATTACHMENT + " integer, "
+ + MessageColumns.FLAGS + " integer, "
+ + MessageColumns.DRAFT_INFO + " integer, "
+ + MessageColumns.MESSAGE_ID + " text, "
+ + MessageColumns.MAILBOX_KEY + " integer, "
+ + MessageColumns.ACCOUNT_KEY + " integer, "
+ + MessageColumns.FROM_LIST + " text, "
+ + MessageColumns.TO_LIST + " text, "
+ + MessageColumns.CC_LIST + " text, "
+ + MessageColumns.BCC_LIST + " text, "
+ + MessageColumns.REPLY_TO_LIST + " text, "
+ + MessageColumns.MEETING_INFO + " text, "
+ + MessageColumns.SNIPPET + " text, "
+ + MessageColumns.PROTOCOL_SEARCH_INFO + " text, "
+ + MessageColumns.THREAD_TOPIC + " text"
+ + ");";
+
+ // This String and the following String MUST have the same columns, except for the type
+ // of those columns!
+ String createString = " (" + EmailContent.RECORD_ID + " integer primary key autoincrement, "
+ + SyncColumns.SERVER_ID + " text, "
+ + SyncColumns.SERVER_TIMESTAMP + " integer, "
+ + messageColumns;
+
+ // For the updated and deleted tables, the id is assigned, but we do want to keep track
+ // of the ORDER of updates using an autoincrement primary key. We use the DATA column
+ // at this point; it has no other function
+ String altCreateString = " (" + EmailContent.RECORD_ID + " integer unique, "
+ + SyncColumns.SERVER_ID + " text, "
+ + SyncColumns.SERVER_TIMESTAMP + " integer, "
+ + messageColumns;
+
+ // The three tables have the same schema
+ db.execSQL("create table " + Message.TABLE_NAME + createString);
+ db.execSQL("create table " + Message.UPDATED_TABLE_NAME + altCreateString);
+ db.execSQL("create table " + Message.DELETED_TABLE_NAME + altCreateString);
+
+ String indexColumns[] = {
+ MessageColumns.TIMESTAMP,
+ MessageColumns.FLAG_READ,
+ MessageColumns.FLAG_LOADED,
+ MessageColumns.MAILBOX_KEY,
+ SyncColumns.SERVER_ID
+ };
+
+ for (String columnName : indexColumns) {
+ db.execSQL(createIndex(Message.TABLE_NAME, columnName));
+ }
+
+ // Deleting a Message deletes all associated Attachments
+ // Deleting the associated Body cannot be done in a trigger, because the Body is stored
+ // in a separate database, and trigger cannot operate on attached databases.
+ db.execSQL("create trigger message_delete before delete on " + Message.TABLE_NAME +
+ " begin delete from " + Attachment.TABLE_NAME +
+ " where " + AttachmentColumns.MESSAGE_KEY + "=old." + EmailContent.RECORD_ID +
+ "; end");
+
+ // Add triggers to keep unread count accurate per mailbox
+
+ // NOTE: SQLite's before triggers are not safe when recursive triggers are involved.
+ // Use caution when changing them.
+
+ // Insert a message; if flagRead is zero, add to the unread count of the message's mailbox
+ db.execSQL("create trigger unread_message_insert before insert on " + Message.TABLE_NAME +
+ " when NEW." + MessageColumns.FLAG_READ + "=0" +
+ " begin update " + Mailbox.TABLE_NAME + " set " + MailboxColumns.UNREAD_COUNT +
+ '=' + MailboxColumns.UNREAD_COUNT + "+1" +
+ " where " + EmailContent.RECORD_ID + "=NEW." + MessageColumns.MAILBOX_KEY +
+ "; end");
+
+ // Delete a message; if flagRead is zero, decrement the unread count of the msg's mailbox
+ db.execSQL("create trigger unread_message_delete before delete on " + Message.TABLE_NAME +
+ " when OLD." + MessageColumns.FLAG_READ + "=0" +
+ " begin update " + Mailbox.TABLE_NAME + " set " + MailboxColumns.UNREAD_COUNT +
+ '=' + MailboxColumns.UNREAD_COUNT + "-1" +
+ " where " + EmailContent.RECORD_ID + "=OLD." + MessageColumns.MAILBOX_KEY +
+ "; end");
+
+ // Change a message's mailbox
+ db.execSQL("create trigger unread_message_move before update of " +
+ MessageColumns.MAILBOX_KEY + " on " + Message.TABLE_NAME +
+ " when OLD." + MessageColumns.FLAG_READ + "=0" +
+ " begin update " + Mailbox.TABLE_NAME + " set " + MailboxColumns.UNREAD_COUNT +
+ '=' + MailboxColumns.UNREAD_COUNT + "-1" +
+ " where " + EmailContent.RECORD_ID + "=OLD." + MessageColumns.MAILBOX_KEY +
+ "; update " + Mailbox.TABLE_NAME + " set " + MailboxColumns.UNREAD_COUNT +
+ '=' + MailboxColumns.UNREAD_COUNT + "+1" +
+ " where " + EmailContent.RECORD_ID + "=NEW." + MessageColumns.MAILBOX_KEY +
+ "; end");
+
+ // Change a message's read state
+ db.execSQL("create trigger unread_message_read before update of " +
+ MessageColumns.FLAG_READ + " on " + Message.TABLE_NAME +
+ " when OLD." + MessageColumns.FLAG_READ + "!=NEW." + MessageColumns.FLAG_READ +
+ " begin update " + Mailbox.TABLE_NAME + " set " + MailboxColumns.UNREAD_COUNT +
+ '=' + MailboxColumns.UNREAD_COUNT + "+ case OLD." + MessageColumns.FLAG_READ +
+ " when 0 then -1 else 1 end" +
+ " where " + EmailContent.RECORD_ID + "=OLD." + MessageColumns.MAILBOX_KEY +
+ "; end");
+
+ // Add triggers to update message count per mailbox
+
+ // Insert a message.
+ db.execSQL("create trigger message_count_message_insert after insert on " +
+ Message.TABLE_NAME +
+ " begin update " + Mailbox.TABLE_NAME + " set " + MailboxColumns.MESSAGE_COUNT +
+ '=' + MailboxColumns.MESSAGE_COUNT + "+1" +
+ " where " + EmailContent.RECORD_ID + "=NEW." + MessageColumns.MAILBOX_KEY +
+ "; end");
+
+ // Delete a message; if flagRead is zero, decrement the unread count of the msg's mailbox
+ db.execSQL("create trigger message_count_message_delete after delete on " +
+ Message.TABLE_NAME +
+ " begin update " + Mailbox.TABLE_NAME + " set " + MailboxColumns.MESSAGE_COUNT +
+ '=' + MailboxColumns.MESSAGE_COUNT + "-1" +
+ " where " + EmailContent.RECORD_ID + "=OLD." + MessageColumns.MAILBOX_KEY +
+ "; end");
+
+ // Change a message's mailbox
+ db.execSQL("create trigger message_count_message_move after update of " +
+ MessageColumns.MAILBOX_KEY + " on " + Message.TABLE_NAME +
+ " begin update " + Mailbox.TABLE_NAME + " set " + MailboxColumns.MESSAGE_COUNT +
+ '=' + MailboxColumns.MESSAGE_COUNT + "-1" +
+ " where " + EmailContent.RECORD_ID + "=OLD." + MessageColumns.MAILBOX_KEY +
+ "; update " + Mailbox.TABLE_NAME + " set " + MailboxColumns.MESSAGE_COUNT +
+ '=' + MailboxColumns.MESSAGE_COUNT + "+1" +
+ " where " + EmailContent.RECORD_ID + "=NEW." + MessageColumns.MAILBOX_KEY +
+ "; end");
+ }
+
+ static void resetMessageTable(SQLiteDatabase db, int oldVersion, int newVersion) {
+ try {
+ db.execSQL("drop table " + Message.TABLE_NAME);
+ db.execSQL("drop table " + Message.UPDATED_TABLE_NAME);
+ db.execSQL("drop table " + Message.DELETED_TABLE_NAME);
+ } catch (SQLException e) {
+ }
+ createMessageTable(db);
+ }
+
+ @SuppressWarnings("deprecation")
+ static void createAccountTable(SQLiteDatabase db) {
+ String s = " (" + EmailContent.RECORD_ID + " integer primary key autoincrement, "
+ + AccountColumns.DISPLAY_NAME + " text, "
+ + AccountColumns.EMAIL_ADDRESS + " text, "
+ + AccountColumns.SYNC_KEY + " text, "
+ + AccountColumns.SYNC_LOOKBACK + " integer, "
+ + AccountColumns.SYNC_INTERVAL + " text, "
+ + AccountColumns.HOST_AUTH_KEY_RECV + " integer, "
+ + AccountColumns.HOST_AUTH_KEY_SEND + " integer, "
+ + AccountColumns.FLAGS + " integer, "
+ + AccountColumns.IS_DEFAULT + " integer, "
+ + AccountColumns.COMPATIBILITY_UUID + " text, "
+ + AccountColumns.SENDER_NAME + " text, "
+ + AccountColumns.RINGTONE_URI + " text, "
+ + AccountColumns.PROTOCOL_VERSION + " text, "
+ + AccountColumns.NEW_MESSAGE_COUNT + " integer, "
+ + AccountColumns.SECURITY_FLAGS + " integer, "
+ + AccountColumns.SECURITY_SYNC_KEY + " text, "
+ + AccountColumns.SIGNATURE + " text, "
+ + AccountColumns.POLICY_KEY + " integer"
+ + ");";
+ db.execSQL("create table " + Account.TABLE_NAME + s);
+ // Deleting an account deletes associated Mailboxes and HostAuth's
+ db.execSQL(TRIGGER_ACCOUNT_DELETE);
+ }
+
+ static void resetAccountTable(SQLiteDatabase db, int oldVersion, int newVersion) {
+ try {
+ db.execSQL("drop table " + Account.TABLE_NAME);
+ } catch (SQLException e) {
+ }
+ createAccountTable(db);
+ }
+
+ static void createPolicyTable(SQLiteDatabase db) {
+ String s = " (" + EmailContent.RECORD_ID + " integer primary key autoincrement, "
+ + PolicyColumns.PASSWORD_MODE + " integer, "
+ + PolicyColumns.PASSWORD_MIN_LENGTH + " integer, "
+ + PolicyColumns.PASSWORD_EXPIRATION_DAYS + " integer, "
+ + PolicyColumns.PASSWORD_HISTORY + " integer, "
+ + PolicyColumns.PASSWORD_COMPLEX_CHARS + " integer, "
+ + PolicyColumns.PASSWORD_MAX_FAILS + " integer, "
+ + PolicyColumns.MAX_SCREEN_LOCK_TIME + " integer, "
+ + PolicyColumns.REQUIRE_REMOTE_WIPE + " integer, "
+ + PolicyColumns.REQUIRE_ENCRYPTION + " integer, "
+ + PolicyColumns.REQUIRE_ENCRYPTION_EXTERNAL + " integer, "
+ + PolicyColumns.REQUIRE_MANUAL_SYNC_WHEN_ROAMING + " integer, "
+ + PolicyColumns.DONT_ALLOW_CAMERA + " integer, "
+ + PolicyColumns.DONT_ALLOW_ATTACHMENTS + " integer, "
+ + PolicyColumns.DONT_ALLOW_HTML + " integer, "
+ + PolicyColumns.MAX_ATTACHMENT_SIZE + " integer, "
+ + PolicyColumns.MAX_TEXT_TRUNCATION_SIZE + " integer, "
+ + PolicyColumns.MAX_HTML_TRUNCATION_SIZE + " integer, "
+ + PolicyColumns.MAX_EMAIL_LOOKBACK + " integer, "
+ + PolicyColumns.MAX_CALENDAR_LOOKBACK + " integer, "
+ + PolicyColumns.PASSWORD_RECOVERY_ENABLED + " integer, "
+ + PolicyColumns.PROTOCOL_POLICIES_ENFORCED + " text, "
+ + PolicyColumns.PROTOCOL_POLICIES_UNSUPPORTED + " text"
+ + ");";
+ db.execSQL("create table " + Policy.TABLE_NAME + s);
+ }
+
+ static void createHostAuthTable(SQLiteDatabase db) {
+ String s = " (" + EmailContent.RECORD_ID + " integer primary key autoincrement, "
+ + HostAuthColumns.PROTOCOL + " text, "
+ + HostAuthColumns.ADDRESS + " text, "
+ + HostAuthColumns.PORT + " integer, "
+ + HostAuthColumns.FLAGS + " integer, "
+ + HostAuthColumns.LOGIN + " text, "
+ + HostAuthColumns.PASSWORD + " text, "
+ + HostAuthColumns.DOMAIN + " text, "
+ + HostAuthColumns.ACCOUNT_KEY + " integer,"
+ + HostAuthColumns.CLIENT_CERT_ALIAS + " text"
+ + ");";
+ db.execSQL("create table " + HostAuth.TABLE_NAME + s);
+ }
+
+ static void resetHostAuthTable(SQLiteDatabase db, int oldVersion, int newVersion) {
+ try {
+ db.execSQL("drop table " + HostAuth.TABLE_NAME);
+ } catch (SQLException e) {
+ }
+ createHostAuthTable(db);
+ }
+
+ static void createMailboxTable(SQLiteDatabase db) {
+ String s = " (" + EmailContent.RECORD_ID + " integer primary key autoincrement, "
+ + MailboxColumns.DISPLAY_NAME + " text, "
+ + MailboxColumns.SERVER_ID + " text, "
+ + MailboxColumns.PARENT_SERVER_ID + " text, "
+ + MailboxColumns.PARENT_KEY + " integer, "
+ + MailboxColumns.ACCOUNT_KEY + " integer, "
+ + MailboxColumns.TYPE + " integer, "
+ + MailboxColumns.DELIMITER + " integer, "
+ + MailboxColumns.SYNC_KEY + " text, "
+ + MailboxColumns.SYNC_LOOKBACK + " integer, "
+ + MailboxColumns.SYNC_INTERVAL + " integer, "
+ + MailboxColumns.SYNC_TIME + " integer, "
+ + MailboxColumns.UNREAD_COUNT + " integer, "
+ + MailboxColumns.FLAG_VISIBLE + " integer, "
+ + MailboxColumns.FLAGS + " integer, "
+ + MailboxColumns.VISIBLE_LIMIT + " integer, "
+ + MailboxColumns.SYNC_STATUS + " text, "
+ + MailboxColumns.MESSAGE_COUNT + " integer not null default 0, "
+ + MailboxColumns.LAST_TOUCHED_TIME + " integer default 0, "
+ + MailboxColumns.UI_SYNC_STATUS + " integer default 0, "
+ + MailboxColumns.UI_LAST_SYNC_RESULT + " integer default 0, "
+ + MailboxColumns.LAST_NOTIFIED_MESSAGE_KEY + " integer not null default 0, "
+ + MailboxColumns.LAST_NOTIFIED_MESSAGE_COUNT + " integer not null default 0, "
+ + MailboxColumns.TOTAL_COUNT + " integer, "
+ + MailboxColumns.HIERARCHICAL_NAME + " text"
+ + ");";
+ db.execSQL("create table " + Mailbox.TABLE_NAME + s);
+ db.execSQL("create index mailbox_" + MailboxColumns.SERVER_ID
+ + " on " + Mailbox.TABLE_NAME + " (" + MailboxColumns.SERVER_ID + ")");
+ db.execSQL("create index mailbox_" + MailboxColumns.ACCOUNT_KEY
+ + " on " + Mailbox.TABLE_NAME + " (" + MailboxColumns.ACCOUNT_KEY + ")");
+ // Deleting a Mailbox deletes associated Messages in all three tables
+ db.execSQL(TRIGGER_MAILBOX_DELETE);
+ }
+
+ static void resetMailboxTable(SQLiteDatabase db, int oldVersion, int newVersion) {
+ try {
+ db.execSQL("drop table " + Mailbox.TABLE_NAME);
+ } catch (SQLException e) {
+ }
+ createMailboxTable(db);
+ }
+
+ static void createAttachmentTable(SQLiteDatabase db) {
+ String s = " (" + EmailContent.RECORD_ID + " integer primary key autoincrement, "
+ + AttachmentColumns.FILENAME + " text, "
+ + AttachmentColumns.MIME_TYPE + " text, "
+ + AttachmentColumns.SIZE + " integer, "
+ + AttachmentColumns.CONTENT_ID + " text, "
+ + AttachmentColumns.CONTENT_URI + " text, "
+ + AttachmentColumns.MESSAGE_KEY + " integer, "
+ + AttachmentColumns.LOCATION + " text, "
+ + AttachmentColumns.ENCODING + " text, "
+ + AttachmentColumns.CONTENT + " text, "
+ + AttachmentColumns.FLAGS + " integer, "
+ + AttachmentColumns.CONTENT_BYTES + " blob, "
+ + AttachmentColumns.ACCOUNT_KEY + " integer, "
+ + AttachmentColumns.UI_STATE + " integer, "
+ + AttachmentColumns.UI_DESTINATION + " integer, "
+ + AttachmentColumns.UI_DOWNLOADED_SIZE + " integer"
+ + ");";
+ db.execSQL("create table " + Attachment.TABLE_NAME + s);
+ db.execSQL(createIndex(Attachment.TABLE_NAME, AttachmentColumns.MESSAGE_KEY));
+ }
+
+ static void resetAttachmentTable(SQLiteDatabase db, int oldVersion, int newVersion) {
+ try {
+ db.execSQL("drop table " + Attachment.TABLE_NAME);
+ } catch (SQLException e) {
+ }
+ createAttachmentTable(db);
+ }
+
+ static void createQuickResponseTable(SQLiteDatabase db) {
+ String s = " (" + EmailContent.RECORD_ID + " integer primary key autoincrement, "
+ + QuickResponseColumns.TEXT + " text, "
+ + QuickResponseColumns.ACCOUNT_KEY + " integer"
+ + ");";
+ db.execSQL("create table " + QuickResponse.TABLE_NAME + s);
+ }
+
+ static void createBodyTable(SQLiteDatabase db) {
+ String s = " (" + EmailContent.RECORD_ID + " integer primary key autoincrement, "
+ + BodyColumns.MESSAGE_KEY + " integer, "
+ + BodyColumns.HTML_CONTENT + " text, "
+ + BodyColumns.TEXT_CONTENT + " text, "
+ + BodyColumns.HTML_REPLY + " text, "
+ + BodyColumns.TEXT_REPLY + " text, "
+ + BodyColumns.SOURCE_MESSAGE_KEY + " text, "
+ + BodyColumns.INTRO_TEXT + " text, "
+ + BodyColumns.QUOTED_TEXT_START_POS + " integer"
+ + ");";
+ db.execSQL("create table " + Body.TABLE_NAME + s);
+ db.execSQL(createIndex(Body.TABLE_NAME, BodyColumns.MESSAGE_KEY));
+ }
+
+ static void upgradeBodyTable(SQLiteDatabase db, int oldVersion, int newVersion) {
+ if (oldVersion < 5) {
+ try {
+ db.execSQL("drop table " + Body.TABLE_NAME);
+ createBodyTable(db);
+ oldVersion = 5;
+ } catch (SQLException e) {
+ }
+ }
+ if (oldVersion == 5) {
+ try {
+ db.execSQL("alter table " + Body.TABLE_NAME
+ + " add " + BodyColumns.INTRO_TEXT + " text");
+ } catch (SQLException e) {
+ // Shouldn't be needed unless we're debugging and interrupt the process
+ Log.w(TAG, "Exception upgrading EmailProviderBody.db from v5 to v6", e);
+ }
+ oldVersion = 6;
+ }
+ if (oldVersion == 6 || oldVersion == 7) {
+ try {
+ db.execSQL("alter table " + Body.TABLE_NAME
+ + " add " + BodyColumns.QUOTED_TEXT_START_POS + " integer");
+ } 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);
+ }
+ oldVersion = 8;
+ }
+ if (oldVersion == 8) {
+ // Move to Email2 version
+ oldVersion = 100;
+ }
+ }
+
+ protected static class BodyDatabaseHelper extends SQLiteOpenHelper {
+ BodyDatabaseHelper(Context context, String name) {
+ super(context, name, null, BODY_DATABASE_VERSION);
+ }
+
+ @Override
+ public void onCreate(SQLiteDatabase db) {
+ Log.d(TAG, "Creating EmailProviderBody database");
+ createBodyTable(db);
+ }
+
+ @Override
+ public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
+ upgradeBodyTable(db, oldVersion, newVersion);
+ }
+
+ @Override
+ public void onOpen(SQLiteDatabase db) {
+ }
+ }
+
+ /** Counts the number of messages in each mailbox, and updates the message count column. */
+ @VisibleForTesting
+ static void recalculateMessageCount(SQLiteDatabase db) {
+ db.execSQL("update " + Mailbox.TABLE_NAME + " set " + MailboxColumns.MESSAGE_COUNT +
+ "= (select count(*) from " + Message.TABLE_NAME +
+ " where " + Message.MAILBOX_KEY + " = " +
+ Mailbox.TABLE_NAME + "." + EmailContent.RECORD_ID + ")");
+ }
+
+ protected static class DatabaseHelper extends SQLiteOpenHelper {
+ Context mContext;
+
+ DatabaseHelper(Context context, String name) {
+ super(context, name, null, DATABASE_VERSION);
+ mContext = context;
+ }
+
+ @Override
+ public void onCreate(SQLiteDatabase db) {
+ Log.d(TAG, "Creating EmailProvider database");
+ // Create all tables here; each class has its own method
+ createMessageTable(db);
+ createAttachmentTable(db);
+ createMailboxTable(db);
+ createHostAuthTable(db);
+ createAccountTable(db);
+ createPolicyTable(db);
+ createQuickResponseTable(db);
+ }
+
+ public void onDowngrade(SQLiteDatabase db, int oldVersion, int newVersion) {
+ if (oldVersion == 101 && newVersion == 100) {
+ Log.d(TAG, "Downgrade from v101 to v100");
+ } else {
+ super.onDowngrade(db, oldVersion, newVersion);
+ }
+ }
+
+ @Override
+ @SuppressWarnings("deprecation")
+ public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
+ // For versions prior to 5, delete all data
+ // Versions >= 5 require that data be preserved!
+ if (oldVersion < 5) {
+ android.accounts.Account[] accounts = AccountManager.get(mContext)
+ .getAccountsByType(AccountManagerTypes.TYPE_EXCHANGE);
+ for (android.accounts.Account account: accounts) {
+ AccountManager.get(mContext).removeAccount(account, null, null);
+ }
+ resetMessageTable(db, oldVersion, newVersion);
+ resetAttachmentTable(db, oldVersion, newVersion);
+ resetMailboxTable(db, oldVersion, newVersion);
+ resetHostAuthTable(db, oldVersion, newVersion);
+ resetAccountTable(db, oldVersion, newVersion);
+ return;
+ }
+ if (oldVersion == 5) {
+ // Message Tables: Add SyncColumns.SERVER_TIMESTAMP
+ try {
+ db.execSQL("alter table " + Message.TABLE_NAME
+ + " add column " + SyncColumns.SERVER_TIMESTAMP + " integer" + ";");
+ db.execSQL("alter table " + Message.UPDATED_TABLE_NAME
+ + " add column " + SyncColumns.SERVER_TIMESTAMP + " integer" + ";");
+ db.execSQL("alter table " + Message.DELETED_TABLE_NAME
+ + " add column " + SyncColumns.SERVER_TIMESTAMP + " integer" + ";");
+ } catch (SQLException e) {
+ // Shouldn't be needed unless we're debugging and interrupt the process
+ Log.w(TAG, "Exception upgrading EmailProvider.db from v5 to v6", e);
+ }
+ oldVersion = 6;
+ }
+ if (oldVersion == 6) {
+ // Use the newer mailbox_delete trigger
+ db.execSQL("drop trigger mailbox_delete;");
+ db.execSQL(TRIGGER_MAILBOX_DELETE);
+ oldVersion = 7;
+ }
+ if (oldVersion == 7) {
+ // add the security (provisioning) column
+ try {
+ db.execSQL("alter table " + Account.TABLE_NAME
+ + " add column " + AccountColumns.SECURITY_FLAGS + " integer" + ";");
+ } catch (SQLException e) {
+ // Shouldn't be needed unless we're debugging and interrupt the process
+ Log.w(TAG, "Exception upgrading EmailProvider.db from 7 to 8 " + e);
+ }
+ oldVersion = 8;
+ }
+ if (oldVersion == 8) {
+ // accounts: add security sync key & user signature columns
+ try {
+ db.execSQL("alter table " + Account.TABLE_NAME
+ + " add column " + AccountColumns.SECURITY_SYNC_KEY + " text" + ";");
+ db.execSQL("alter table " + Account.TABLE_NAME
+ + " add column " + AccountColumns.SIGNATURE + " text" + ";");
+ } catch (SQLException e) {
+ // Shouldn't be needed unless we're debugging and interrupt the process
+ Log.w(TAG, "Exception upgrading EmailProvider.db from 8 to 9 " + e);
+ }
+ oldVersion = 9;
+ }
+ if (oldVersion == 9) {
+ // Message: add meeting info column into Message tables
+ try {
+ db.execSQL("alter table " + Message.TABLE_NAME
+ + " add column " + MessageColumns.MEETING_INFO + " text" + ";");
+ db.execSQL("alter table " + Message.UPDATED_TABLE_NAME
+ + " add column " + MessageColumns.MEETING_INFO + " text" + ";");
+ db.execSQL("alter table " + Message.DELETED_TABLE_NAME
+ + " add column " + MessageColumns.MEETING_INFO + " text" + ";");
+ } catch (SQLException e) {
+ // Shouldn't be needed unless we're debugging and interrupt the process
+ Log.w(TAG, "Exception upgrading EmailProvider.db from 9 to 10 " + e);
+ }
+ oldVersion = 10;
+ }
+ if (oldVersion == 10) {
+ // Attachment: add content and flags columns
+ try {
+ db.execSQL("alter table " + Attachment.TABLE_NAME
+ + " add column " + AttachmentColumns.CONTENT + " text" + ";");
+ db.execSQL("alter table " + Attachment.TABLE_NAME
+ + " add column " + AttachmentColumns.FLAGS + " integer" + ";");
+ } catch (SQLException e) {
+ // Shouldn't be needed unless we're debugging and interrupt the process
+ Log.w(TAG, "Exception upgrading EmailProvider.db from 10 to 11 " + e);
+ }
+ oldVersion = 11;
+ }
+ if (oldVersion == 11) {
+ // Attachment: add content_bytes
+ try {
+ db.execSQL("alter table " + Attachment.TABLE_NAME
+ + " add column " + AttachmentColumns.CONTENT_BYTES + " blob" + ";");
+ } catch (SQLException e) {
+ // Shouldn't be needed unless we're debugging and interrupt the process
+ Log.w(TAG, "Exception upgrading EmailProvider.db from 11 to 12 " + e);
+ }
+ oldVersion = 12;
+ }
+ if (oldVersion == 12) {
+ try {
+ db.execSQL("alter table " + Mailbox.TABLE_NAME
+ + " add column " + Mailbox.MESSAGE_COUNT
+ +" integer not null default 0" + ";");
+ recalculateMessageCount(db);
+ } catch (SQLException e) {
+ // Shouldn't be needed unless we're debugging and interrupt the process
+ Log.w(TAG, "Exception upgrading EmailProvider.db from 12 to 13 " + e);
+ }
+ oldVersion = 13;
+ }
+ if (oldVersion == 13) {
+ try {
+ db.execSQL("alter table " + Message.TABLE_NAME
+ + " add column " + Message.SNIPPET
+ +" text" + ";");
+ } catch (SQLException e) {
+ // Shouldn't be needed unless we're debugging and interrupt the process
+ Log.w(TAG, "Exception upgrading EmailProvider.db from 13 to 14 " + e);
+ }
+ oldVersion = 14;
+ }
+ if (oldVersion == 14) {
+ try {
+ db.execSQL("alter table " + Message.DELETED_TABLE_NAME
+ + " add column " + Message.SNIPPET +" text" + ";");
+ db.execSQL("alter table " + Message.UPDATED_TABLE_NAME
+ + " add column " + Message.SNIPPET +" text" + ";");
+ } catch (SQLException e) {
+ // Shouldn't be needed unless we're debugging and interrupt the process
+ Log.w(TAG, "Exception upgrading EmailProvider.db from 14 to 15 " + e);
+ }
+ oldVersion = 15;
+ }
+ if (oldVersion == 15) {
+ try {
+ db.execSQL("alter table " + Attachment.TABLE_NAME
+ + " add column " + Attachment.ACCOUNT_KEY +" integer" + ";");
+ // Update all existing attachments to add the accountKey data
+ db.execSQL("update " + Attachment.TABLE_NAME + " set " +
+ Attachment.ACCOUNT_KEY + "= (SELECT " + Message.TABLE_NAME + "." +
+ Message.ACCOUNT_KEY + " from " + Message.TABLE_NAME + " where " +
+ Message.TABLE_NAME + "." + Message.RECORD_ID + " = " +
+ Attachment.TABLE_NAME + "." + Attachment.MESSAGE_KEY + ")");
+ } catch (SQLException e) {
+ // Shouldn't be needed unless we're debugging and interrupt the process
+ Log.w(TAG, "Exception upgrading EmailProvider.db from 15 to 16 " + e);
+ }
+ oldVersion = 16;
+ }
+ if (oldVersion == 16) {
+ try {
+ db.execSQL("alter table " + Mailbox.TABLE_NAME
+ + " add column " + Mailbox.PARENT_KEY + " integer;");
+ } catch (SQLException e) {
+ // Shouldn't be needed unless we're debugging and interrupt the process
+ Log.w(TAG, "Exception upgrading EmailProvider.db from 16 to 17 " + e);
+ }
+ oldVersion = 17;
+ }
+ if (oldVersion == 17) {
+ upgradeFromVersion17ToVersion18(db);
+ oldVersion = 18;
+ }
+ if (oldVersion == 18) {
+ try {
+ db.execSQL("alter table " + Account.TABLE_NAME
+ + " add column " + Account.POLICY_KEY + " integer;");
+ db.execSQL("drop trigger account_delete;");
+ db.execSQL(TRIGGER_ACCOUNT_DELETE);
+ createPolicyTable(db);
+ convertPolicyFlagsToPolicyTable(db);
+ } catch (SQLException e) {
+ // Shouldn't be needed unless we're debugging and interrupt the process
+ Log.w(TAG, "Exception upgrading EmailProvider.db from 18 to 19 " + e);
+ }
+ oldVersion = 19;
+ }
+ if (oldVersion == 19) {
+ try {
+ db.execSQL("alter table " + Policy.TABLE_NAME
+ + " add column " + PolicyColumns.REQUIRE_MANUAL_SYNC_WHEN_ROAMING +
+ " integer;");
+ db.execSQL("alter table " + Policy.TABLE_NAME
+ + " add column " + PolicyColumns.DONT_ALLOW_CAMERA + " integer;");
+ db.execSQL("alter table " + Policy.TABLE_NAME
+ + " add column " + PolicyColumns.DONT_ALLOW_ATTACHMENTS + " integer;");
+ db.execSQL("alter table " + Policy.TABLE_NAME
+ + " add column " + PolicyColumns.DONT_ALLOW_HTML + " integer;");
+ db.execSQL("alter table " + Policy.TABLE_NAME
+ + " add column " + PolicyColumns.MAX_ATTACHMENT_SIZE + " integer;");
+ db.execSQL("alter table " + Policy.TABLE_NAME
+ + " add column " + PolicyColumns.MAX_TEXT_TRUNCATION_SIZE +
+ " integer;");
+ db.execSQL("alter table " + Policy.TABLE_NAME
+ + " add column " + PolicyColumns.MAX_HTML_TRUNCATION_SIZE +
+ " integer;");
+ db.execSQL("alter table " + Policy.TABLE_NAME
+ + " add column " + PolicyColumns.MAX_EMAIL_LOOKBACK + " integer;");
+ db.execSQL("alter table " + Policy.TABLE_NAME
+ + " add column " + PolicyColumns.MAX_CALENDAR_LOOKBACK + " integer;");
+ db.execSQL("alter table " + Policy.TABLE_NAME
+ + " add column " + PolicyColumns.PASSWORD_RECOVERY_ENABLED +
+ " integer;");
+ } catch (SQLException e) {
+ // Shouldn't be needed unless we're debugging and interrupt the process
+ Log.w(TAG, "Exception upgrading EmailProvider.db from 19 to 20 " + e);
+ }
+ oldVersion = 20;
+ }
+ if (oldVersion == 20) {
+ oldVersion = 21;
+ }
+ if (oldVersion == 21) {
+ upgradeFromVersion21ToVersion22(db, mContext);
+ oldVersion = 22;
+ }
+ if (oldVersion == 22) {
+ upgradeFromVersion22ToVersion23(db);
+ oldVersion = 23;
+ }
+ if (oldVersion == 23) {
+ upgradeFromVersion23ToVersion24(db);
+ oldVersion = 24;
+ }
+ if (oldVersion == 24) {
+ upgradeFromVersion24ToVersion25(db);
+ oldVersion = 25;
+ }
+ if (oldVersion == 25) {
+ upgradeFromVersion25ToVersion26(db);
+ oldVersion = 26;
+ }
+ if (oldVersion == 26) {
+ try {
+ db.execSQL("alter table " + Message.TABLE_NAME
+ + " add column " + Message.PROTOCOL_SEARCH_INFO + " text;");
+ db.execSQL("alter table " + Message.DELETED_TABLE_NAME
+ + " add column " + Message.PROTOCOL_SEARCH_INFO +" text" + ";");
+ db.execSQL("alter table " + Message.UPDATED_TABLE_NAME
+ + " add column " + Message.PROTOCOL_SEARCH_INFO +" text" + ";");
+ } catch (SQLException e) {
+ // Shouldn't be needed unless we're debugging and interrupt the process
+ Log.w(TAG, "Exception upgrading EmailProvider.db from 26 to 27 " + e);
+ }
+ oldVersion = 27;
+ }
+ if (oldVersion == 27) {
+ oldVersion = 28;
+ }
+ if (oldVersion == 28) {
+ try {
+ db.execSQL("alter table " + Policy.TABLE_NAME
+ + " add column " + Policy.PROTOCOL_POLICIES_ENFORCED + " text;");
+ db.execSQL("alter table " + Policy.TABLE_NAME
+ + " add column " + Policy.PROTOCOL_POLICIES_UNSUPPORTED + " text;");
+ } catch (SQLException e) {
+ // Shouldn't be needed unless we're debugging and interrupt the process
+ Log.w(TAG, "Exception upgrading EmailProvider.db from 28 to 29 " + e);
+ }
+ oldVersion = 29;
+ }
+ if (oldVersion == 29) {
+ upgradeFromVersion29ToVersion30(db);
+ oldVersion = 30;
+ }
+ if (oldVersion == 30) {
+ try {
+ db.execSQL("alter table " + Mailbox.TABLE_NAME
+ + " add column " + Mailbox.UI_SYNC_STATUS + " integer;");
+ db.execSQL("alter table " + Mailbox.TABLE_NAME
+ + " add column " + Mailbox.UI_LAST_SYNC_RESULT + " integer;");
+ } catch (SQLException e) {
+ // Shouldn't be needed unless we're debugging and interrupt the process
+ Log.w(TAG, "Exception upgrading EmailProvider.db from 30 to 31 " + e);
+ }
+ oldVersion = 31;
+ }
+ if (oldVersion == 31) {
+ try {
+ db.execSQL("alter table " + Mailbox.TABLE_NAME
+ + " add column " + Mailbox.LAST_NOTIFIED_MESSAGE_KEY + " integer;");
+ db.execSQL("alter table " + Mailbox.TABLE_NAME
+ + " add column " + Mailbox.LAST_NOTIFIED_MESSAGE_COUNT + " integer;");
+ db.execSQL("update Mailbox set " + Mailbox.LAST_NOTIFIED_MESSAGE_KEY +
+ "=0 where " + Mailbox.LAST_NOTIFIED_MESSAGE_KEY + " IS NULL");
+ db.execSQL("update Mailbox set " + Mailbox.LAST_NOTIFIED_MESSAGE_COUNT +
+ "=0 where " + Mailbox.LAST_NOTIFIED_MESSAGE_COUNT + " IS NULL");
+ } catch (SQLException e) {
+ // Shouldn't be needed unless we're debugging and interrupt the process
+ Log.w(TAG, "Exception upgrading EmailProvider.db from 31 to 32 " + e);
+ }
+ oldVersion = 32;
+ }
+ if (oldVersion == 32) {
+ try {
+ db.execSQL("alter table " + Attachment.TABLE_NAME
+ + " add column " + Attachment.UI_STATE + " integer;");
+ db.execSQL("alter table " + Attachment.TABLE_NAME
+ + " add column " + Attachment.UI_DESTINATION + " integer;");
+ db.execSQL("alter table " + Attachment.TABLE_NAME
+ + " add column " + Attachment.UI_DOWNLOADED_SIZE + " integer;");
+ // If we have a contentUri then the attachment is saved
+ // uiDestination of 0 = "cache", so we don't have to set this
+ db.execSQL("update " + Attachment.TABLE_NAME + " set " + Attachment.UI_STATE +
+ "=" + UIProvider.AttachmentState.SAVED + " where " +
+ AttachmentColumns.CONTENT_URI + " is not null;");
+ } catch (SQLException e) {
+ // Shouldn't be needed unless we're debugging and interrupt the process
+ Log.w(TAG, "Exception upgrading EmailProvider.db from 32 to 33 " + e);
+ }
+ oldVersion = 33;
+ }
+ if (oldVersion == 33) {
+ try {
+ db.execSQL("alter table " + Mailbox.TABLE_NAME
+ + " add column " + MailboxColumns.TOTAL_COUNT + " integer;");
+ } catch (SQLException e) {
+ // Shouldn't be needed unless we're debugging and interrupt the process
+ Log.w(TAG, "Exception upgrading EmailProvider.db from 33 to 34 " + e);
+ }
+ oldVersion = 34;
+ }
+ if (oldVersion == 34) {
+ try {
+ db.execSQL("update " + Mailbox.TABLE_NAME + " set " +
+ MailboxColumns.LAST_TOUCHED_TIME + " = " +
+ Mailbox.DRAFTS_DEFAULT_TOUCH_TIME + " WHERE " + MailboxColumns.TYPE +
+ " = " + Mailbox.TYPE_DRAFTS);
+ db.execSQL("update " + Mailbox.TABLE_NAME + " set " +
+ MailboxColumns.LAST_TOUCHED_TIME + " = " +
+ Mailbox.SENT_DEFAULT_TOUCH_TIME + " WHERE " + MailboxColumns.TYPE +
+ " = " + Mailbox.TYPE_SENT);
+ } catch (SQLException e) {
+ // Shouldn't be needed unless we're debugging and interrupt the process
+ Log.w(TAG, "Exception upgrading EmailProvider.db from 34 to 35 " + e);
+ }
+ oldVersion = 35;
+ }
+ if (oldVersion == 35 || oldVersion == 36) {
+ try {
+ // Set "supports settings" for EAS mailboxes
+ db.execSQL("update " + Mailbox.TABLE_NAME + " set " +
+ MailboxColumns.FLAGS + "=" + MailboxColumns.FLAGS + "|" +
+ Mailbox.FLAG_SUPPORTS_SETTINGS + " where (" +
+ MailboxColumns.FLAGS + "&" + Mailbox.FLAG_HOLDS_MAIL + ")!=0 and " +
+ MailboxColumns.ACCOUNT_KEY + " IN (SELECT " + Account.TABLE_NAME +
+ "." + AccountColumns.ID + " from " + Account.TABLE_NAME + "," +
+ HostAuth.TABLE_NAME + " where " + Account.TABLE_NAME + "." +
+ AccountColumns.HOST_AUTH_KEY_RECV + "=" + HostAuth.TABLE_NAME + "." +
+ HostAuthColumns.ID + " and " + HostAuthColumns.PROTOCOL + "='" +
+ HostAuth.LEGACY_SCHEME_EAS + "')");
+ } catch (SQLException e) {
+ // Shouldn't be needed unless we're debugging and interrupt the process
+ Log.w(TAG, "Exception upgrading EmailProvider.db from 35 to 36 " + e);
+ }
+ oldVersion = 37;
+ }
+ if (oldVersion == 37) {
+ try {
+ db.execSQL("alter table " + Message.TABLE_NAME
+ + " add column " + MessageColumns.THREAD_TOPIC + " text;");
+ } catch (SQLException e) {
+ // Shouldn't be needed unless we're debugging and interrupt the process
+ Log.w(TAG, "Exception upgrading EmailProvider.db from 37 to 38 " + e);
+ }
+ oldVersion = 38;
+ }
+ if (oldVersion == 38) {
+ try {
+ db.execSQL("alter table " + Message.DELETED_TABLE_NAME
+ + " add column " + MessageColumns.THREAD_TOPIC + " text;");
+ db.execSQL("alter table " + Message.UPDATED_TABLE_NAME
+ + " add column " + MessageColumns.THREAD_TOPIC + " text;");
+ } catch (SQLException e) {
+ // Shouldn't be needed unless we're debugging and interrupt the process
+ Log.w(TAG, "Exception upgrading EmailProvider.db from 38 to 39 " + e);
+ }
+ oldVersion = 39;
+ }
+ if (oldVersion == 39) {
+ upgradeToEmail2(db);
+ oldVersion = 100;
+ }
+ if (oldVersion >= 100 && oldVersion < 103) {
+ try {
+ db.execSQL("alter table " + Mailbox.TABLE_NAME
+ + " 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);
+ }
+ oldVersion = 103;
+ }
+ }
+
+ @Override
+ public void onOpen(SQLiteDatabase db) {
+ }
+ }
+
+ @VisibleForTesting
+ @SuppressWarnings("deprecation")
+ static void convertPolicyFlagsToPolicyTable(SQLiteDatabase db) {
+ Cursor c = db.query(Account.TABLE_NAME,
+ new String[] {EmailContent.RECORD_ID /*0*/, AccountColumns.SECURITY_FLAGS /*1*/},
+ AccountColumns.SECURITY_FLAGS + ">0", null, null, null, null);
+ ContentValues cv = new ContentValues();
+ String[] args = new String[1];
+ while (c.moveToNext()) {
+ long securityFlags = c.getLong(1 /*SECURITY_FLAGS*/);
+ Policy policy = LegacyPolicySet.flagsToPolicy(securityFlags);
+ long policyId = db.insert(Policy.TABLE_NAME, null, policy.toContentValues());
+ cv.put(AccountColumns.POLICY_KEY, policyId);
+ cv.putNull(AccountColumns.SECURITY_FLAGS);
+ args[0] = Long.toString(c.getLong(0 /*RECORD_ID*/));
+ db.update(Account.TABLE_NAME, cv, EmailContent.RECORD_ID + "=?", args);
+ }
+ }
+
+ /** Upgrades the database from v17 to v18 */
+ @VisibleForTesting
+ static void upgradeFromVersion17ToVersion18(SQLiteDatabase db) {
+ // Copy the displayName column to the serverId column. In v18 of the database,
+ // we use the serverId for IMAP/POP3 mailboxes instead of overloading the
+ // display name.
+ //
+ // For posterity; this is the command we're executing:
+ //sqlite> UPDATE mailbox SET serverid=displayname WHERE mailbox._id in (
+ // ...> SELECT mailbox._id FROM mailbox,account,hostauth WHERE
+ // ...> (mailbox.parentkey isnull OR mailbox.parentkey=0) AND
+ // ...> mailbox.accountkey=account._id AND
+ // ...> account.hostauthkeyrecv=hostauth._id AND
+ // ...> (hostauth.protocol='imap' OR hostauth.protocol='pop3'));
+ try {
+ db.execSQL(
+ "UPDATE " + Mailbox.TABLE_NAME + " SET "
+ + MailboxColumns.SERVER_ID + "=" + MailboxColumns.DISPLAY_NAME
+ + " WHERE "
+ + Mailbox.TABLE_NAME + "." + MailboxColumns.ID + " IN ( SELECT "
+ + Mailbox.TABLE_NAME + "." + MailboxColumns.ID + " FROM "
+ + Mailbox.TABLE_NAME + "," + Account.TABLE_NAME + ","
+ + HostAuth.TABLE_NAME + " WHERE "
+ + "("
+ + Mailbox.TABLE_NAME + "." + MailboxColumns.PARENT_KEY + " isnull OR "
+ + Mailbox.TABLE_NAME + "." + MailboxColumns.PARENT_KEY + "=0 "
+ + ") AND "
+ + Mailbox.TABLE_NAME + "." + MailboxColumns.ACCOUNT_KEY + "="
+ + Account.TABLE_NAME + "." + AccountColumns.ID + " AND "
+ + Account.TABLE_NAME + "." + AccountColumns.HOST_AUTH_KEY_RECV + "="
+ + HostAuth.TABLE_NAME + "." + HostAuthColumns.ID + " AND ( "
+ + HostAuth.TABLE_NAME + "." + HostAuthColumns.PROTOCOL + "='imap' OR "
+ + HostAuth.TABLE_NAME + "." + HostAuthColumns.PROTOCOL + "='pop3' ) )");
+ } catch (SQLException e) {
+ // Shouldn't be needed unless we're debugging and interrupt the process
+ Log.w(TAG, "Exception upgrading EmailProvider.db from 17 to 18 " + e);
+ }
+ ContentCache.invalidateAllCaches();
+ }
+
+ /**
+ * Upgrade the database from v21 to v22
+ * This entails creating AccountManager accounts for all pop3 and imap accounts
+ */
+
+ private static final String[] V21_ACCOUNT_PROJECTION =
+ new String[] {AccountColumns.HOST_AUTH_KEY_RECV, AccountColumns.EMAIL_ADDRESS};
+ private static final int V21_ACCOUNT_RECV = 0;
+ private static final int V21_ACCOUNT_EMAIL = 1;
+
+ private static final String[] V21_HOSTAUTH_PROJECTION =
+ new String[] {HostAuthColumns.PROTOCOL, HostAuthColumns.PASSWORD};
+ private static final int V21_HOSTAUTH_PROTOCOL = 0;
+ private static final int V21_HOSTAUTH_PASSWORD = 1;
+
+ static private void createAccountManagerAccount(Context context, String login,
+ String password) {
+ AccountManager accountManager = AccountManager.get(context);
+ android.accounts.Account amAccount =
+ new android.accounts.Account(login, AccountManagerTypes.TYPE_POP_IMAP);
+ accountManager.addAccountExplicitly(amAccount, password, null);
+ ContentResolver.setIsSyncable(amAccount, EmailContent.AUTHORITY, 1);
+ ContentResolver.setSyncAutomatically(amAccount, EmailContent.AUTHORITY, true);
+ ContentResolver.setIsSyncable(amAccount, ContactsContract.AUTHORITY, 0);
+ ContentResolver.setIsSyncable(amAccount, CalendarContract.AUTHORITY, 0);
+ }
+
+ @VisibleForTesting
+ static void upgradeFromVersion21ToVersion22(SQLiteDatabase db, Context accountManagerContext) {
+ try {
+ // Loop through accounts, looking for pop/imap accounts
+ Cursor accountCursor = db.query(Account.TABLE_NAME, V21_ACCOUNT_PROJECTION, null,
+ null, null, null, null);
+ try {
+ String[] hostAuthArgs = new String[1];
+ while (accountCursor.moveToNext()) {
+ hostAuthArgs[0] = accountCursor.getString(V21_ACCOUNT_RECV);
+ // Get the "receive" HostAuth for this account
+ Cursor hostAuthCursor = db.query(HostAuth.TABLE_NAME,
+ V21_HOSTAUTH_PROJECTION, HostAuth.RECORD_ID + "=?", hostAuthArgs,
+ null, null, null);
+ try {
+ if (hostAuthCursor.moveToFirst()) {
+ String protocol = hostAuthCursor.getString(V21_HOSTAUTH_PROTOCOL);
+ // If this is a pop3 or imap account, create the account manager account
+ if (HostAuth.LEGACY_SCHEME_IMAP.equals(protocol) ||
+ HostAuth.LEGACY_SCHEME_POP3.equals(protocol)) {
+ if (MailActivityEmail.DEBUG) {
+ Log.d(TAG, "Create AccountManager account for " + protocol +
+ "account: " +
+ accountCursor.getString(V21_ACCOUNT_EMAIL));
+ }
+ createAccountManagerAccount(accountManagerContext,
+ accountCursor.getString(V21_ACCOUNT_EMAIL),
+ hostAuthCursor.getString(V21_HOSTAUTH_PASSWORD));
+ // If an EAS account, make Email sync automatically (equivalent of
+ // checking the "Sync Email" box in settings
+ } else if (HostAuth.LEGACY_SCHEME_EAS.equals(protocol)) {
+ android.accounts.Account amAccount =
+ new android.accounts.Account(
+ accountCursor.getString(V21_ACCOUNT_EMAIL),
+ AccountManagerTypes.TYPE_EXCHANGE);
+ ContentResolver.setIsSyncable(amAccount, EmailContent.AUTHORITY, 1);
+ ContentResolver.setSyncAutomatically(amAccount,
+ EmailContent.AUTHORITY, true);
+
+ }
+ }
+ } finally {
+ hostAuthCursor.close();
+ }
+ }
+ } finally {
+ accountCursor.close();
+ }
+ } catch (SQLException e) {
+ // Shouldn't be needed unless we're debugging and interrupt the process
+ Log.w(TAG, "Exception upgrading EmailProvider.db from 20 to 21 " + e);
+ }
+ }
+
+ /** Upgrades the database from v22 to v23 */
+ private static void upgradeFromVersion22ToVersion23(SQLiteDatabase db) {
+ try {
+ db.execSQL("alter table " + Mailbox.TABLE_NAME
+ + " add column " + Mailbox.LAST_TOUCHED_TIME + " integer default 0;");
+ } catch (SQLException e) {
+ // Shouldn't be needed unless we're debugging and interrupt the process
+ Log.w(TAG, "Exception upgrading EmailProvider.db from 22 to 23 " + e);
+ }
+ }
+
+ /** Adds in a column for information about a client certificate to use. */
+ private static void upgradeFromVersion23ToVersion24(SQLiteDatabase db) {
+ try {
+ db.execSQL("alter table " + HostAuth.TABLE_NAME
+ + " add column " + HostAuth.CLIENT_CERT_ALIAS + " text;");
+ } catch (SQLException e) {
+ // Shouldn't be needed unless we're debugging and interrupt the process
+ Log.w(TAG, "Exception upgrading EmailProvider.db from 23 to 24 " + e);
+ }
+ }
+
+ /** Upgrades the database from v24 to v25 by creating table for quick responses */
+ private static void upgradeFromVersion24ToVersion25(SQLiteDatabase db) {
+ try {
+ createQuickResponseTable(db);
+ } catch (SQLException e) {
+ // Shouldn't be needed unless we're debugging and interrupt the process
+ Log.w(TAG, "Exception upgrading EmailProvider.db from 24 to 25 " + e);
+ }
+ }
+
+ private static final String[] V25_ACCOUNT_PROJECTION =
+ new String[] {AccountColumns.ID, AccountColumns.FLAGS, AccountColumns.HOST_AUTH_KEY_RECV};
+ private static final int V25_ACCOUNT_ID = 0;
+ private static final int V25_ACCOUNT_FLAGS = 1;
+ private static final int V25_ACCOUNT_RECV = 2;
+
+ private static final String[] V25_HOSTAUTH_PROJECTION = new String[] {HostAuthColumns.PROTOCOL};
+ private static final int V25_HOSTAUTH_PROTOCOL = 0;
+
+ /** Upgrades the database from v25 to v26 by adding FLAG_SUPPORTS_SEARCH to IMAP accounts */
+ private static void upgradeFromVersion25ToVersion26(SQLiteDatabase db) {
+ try {
+ // Loop through accounts, looking for imap accounts
+ Cursor accountCursor = db.query(Account.TABLE_NAME, V25_ACCOUNT_PROJECTION, null,
+ null, null, null, null);
+ ContentValues cv = new ContentValues();
+ try {
+ String[] hostAuthArgs = new String[1];
+ while (accountCursor.moveToNext()) {
+ hostAuthArgs[0] = accountCursor.getString(V25_ACCOUNT_RECV);
+ // Get the "receive" HostAuth for this account
+ Cursor hostAuthCursor = db.query(HostAuth.TABLE_NAME,
+ V25_HOSTAUTH_PROJECTION, HostAuth.RECORD_ID + "=?", hostAuthArgs,
+ null, null, null);
+ try {
+ if (hostAuthCursor.moveToFirst()) {
+ String protocol = hostAuthCursor.getString(V25_HOSTAUTH_PROTOCOL);
+ // If this is an imap account, add the search flag
+ if (HostAuth.LEGACY_SCHEME_IMAP.equals(protocol)) {
+ String id = accountCursor.getString(V25_ACCOUNT_ID);
+ int flags = accountCursor.getInt(V25_ACCOUNT_FLAGS);
+ cv.put(AccountColumns.FLAGS, flags | Account.FLAGS_SUPPORTS_SEARCH);
+ db.update(Account.TABLE_NAME, cv, Account.RECORD_ID + "=?",
+ new String[] {id});
+ }
+ }
+ } finally {
+ hostAuthCursor.close();
+ }
+ }
+ } finally {
+ accountCursor.close();
+ }
+ } catch (SQLException e) {
+ // Shouldn't be needed unless we're debugging and interrupt the process
+ Log.w(TAG, "Exception upgrading EmailProvider.db from 25 to 26 " + e);
+ }
+ }
+
+ /** Upgrades the database from v29 to v30 by updating all address fields in Message */
+ private static final int[] ADDRESS_COLUMN_INDICES = new int[] {
+ Message.CONTENT_BCC_LIST_COLUMN, Message.CONTENT_CC_LIST_COLUMN,
+ Message.CONTENT_FROM_LIST_COLUMN, Message.CONTENT_REPLY_TO_COLUMN,
+ Message.CONTENT_TO_LIST_COLUMN
+ };
+ private static final String[] ADDRESS_COLUMN_NAMES = new String[] {
+ Message.BCC_LIST, Message.CC_LIST, Message.FROM_LIST, Message.REPLY_TO_LIST, Message.TO_LIST
+ };
+
+ private static void upgradeFromVersion29ToVersion30(SQLiteDatabase db) {
+ try {
+ // Loop through all messages, updating address columns to new format (CSV, RFC822)
+ Cursor messageCursor = db.query(Message.TABLE_NAME, Message.CONTENT_PROJECTION, null,
+ null, null, null, null);
+ ContentValues cv = new ContentValues();
+ String[] whereArgs = new String[1];
+ try {
+ while (messageCursor.moveToNext()) {
+ for (int i = 0; i < ADDRESS_COLUMN_INDICES.length; i++) {
+ Address[] addrs =
+ Address.unpack(messageCursor.getString(ADDRESS_COLUMN_INDICES[i]));
+ cv.put(ADDRESS_COLUMN_NAMES[i], Address.pack(addrs));
+ }
+ whereArgs[0] = messageCursor.getString(Message.CONTENT_ID_COLUMN);
+ db.update(Message.TABLE_NAME, cv, WHERE_ID, whereArgs);
+ }
+ } finally {
+ messageCursor.close();
+ }
+ } catch (SQLException e) {
+ // Shouldn't be needed unless we're debugging and interrupt the process
+ Log.w(TAG, "Exception upgrading EmailProvider.db from 29 to 30 " + e);
+ }
+ }
+
+ private static void upgradeToEmail2(SQLiteDatabase db) {
+ // Perform cleanup operations from Email1 to Email2; Email1 will have added new
+ // data that won't conform to what's expected in Email2
+
+ // From 31->32 upgrade
+ try {
+ db.execSQL("update Mailbox set " + Mailbox.LAST_NOTIFIED_MESSAGE_KEY +
+ "=0 where " + Mailbox.LAST_NOTIFIED_MESSAGE_KEY + " IS NULL");
+ db.execSQL("update Mailbox set " + Mailbox.LAST_NOTIFIED_MESSAGE_COUNT +
+ "=0 where " + Mailbox.LAST_NOTIFIED_MESSAGE_COUNT + " IS NULL");
+ } catch (SQLException e) {
+ Log.w(TAG, "Exception upgrading EmailProvider.db from 31 to 32/100 " + e);
+ }
+
+ // From 32->33 upgrade
+ try {
+ db.execSQL("update " + Attachment.TABLE_NAME + " set " + Attachment.UI_STATE +
+ "=" + UIProvider.AttachmentState.SAVED + " where " +
+ AttachmentColumns.CONTENT_URI + " is not null;");
+ } catch (SQLException e) {
+ Log.w(TAG, "Exception upgrading EmailProvider.db from 32 to 33/100 " + e);
+ }
+
+ // From 34->35 upgrade
+ try {
+ db.execSQL("update " + Mailbox.TABLE_NAME + " set " +
+ MailboxColumns.LAST_TOUCHED_TIME + " = " +
+ Mailbox.DRAFTS_DEFAULT_TOUCH_TIME + " WHERE " + MailboxColumns.TYPE +
+ " = " + Mailbox.TYPE_DRAFTS);
+ db.execSQL("update " + Mailbox.TABLE_NAME + " set " +
+ MailboxColumns.LAST_TOUCHED_TIME + " = " +
+ Mailbox.SENT_DEFAULT_TOUCH_TIME + " WHERE " + MailboxColumns.TYPE +
+ " = " + Mailbox.TYPE_SENT);
+ } catch (SQLException e) {
+ Log.w(TAG, "Exception upgrading EmailProvider.db from 34 to 35/100 " + e);
+ }
+
+ // From 35/36->37
+ try {
+ db.execSQL("update " + Mailbox.TABLE_NAME + " set " +
+ MailboxColumns.FLAGS + "=" + MailboxColumns.FLAGS + "|" +
+ Mailbox.FLAG_SUPPORTS_SETTINGS + " where (" +
+ MailboxColumns.FLAGS + "&" + Mailbox.FLAG_HOLDS_MAIL + ")!=0 and " +
+ MailboxColumns.ACCOUNT_KEY + " IN (SELECT " + Account.TABLE_NAME +
+ "." + AccountColumns.ID + " from " + Account.TABLE_NAME + "," +
+ HostAuth.TABLE_NAME + " where " + Account.TABLE_NAME + "." +
+ AccountColumns.HOST_AUTH_KEY_RECV + "=" + HostAuth.TABLE_NAME + "." +
+ HostAuthColumns.ID + " and " + HostAuthColumns.PROTOCOL + "='" +
+ HostAuth.LEGACY_SCHEME_EAS + "')");
+ } catch (SQLException e) {
+ Log.w(TAG, "Exception upgrading EmailProvider.db from 35/36 to 37/100 " + e);
+ }
+ }
+}
diff --git a/src/com/android/email/provider/Utilities.java b/src/com/android/email/provider/Utilities.java
new file mode 100644
index 000000000..ab175f666
--- /dev/null
+++ b/src/com/android/email/provider/Utilities.java
@@ -0,0 +1,150 @@
+/*
+ * 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.email.provider;
+
+import android.content.ContentUris;
+import android.content.ContentValues;
+import android.content.Context;
+import android.database.Cursor;
+import android.net.Uri;
+import android.util.Log;
+
+import com.android.email.LegacyConversions;
+import com.android.emailcommon.Logging;
+import com.android.emailcommon.internet.MimeUtility;
+import com.android.emailcommon.mail.Message;
+import com.android.emailcommon.mail.MessagingException;
+import com.android.emailcommon.mail.Part;
+import com.android.emailcommon.provider.Account;
+import com.android.emailcommon.provider.EmailContent;
+import com.android.emailcommon.provider.EmailContent.MessageColumns;
+import com.android.emailcommon.provider.EmailContent.SyncColumns;
+import com.android.emailcommon.provider.Mailbox;
+import com.android.emailcommon.utility.ConversionUtilities;
+
+import java.io.IOException;
+import java.util.ArrayList;
+
+public class Utilities {
+ /**
+ * Copy one downloaded message (which may have partially-loaded sections)
+ * into a newly created EmailProvider Message, given the account and mailbox
+ *
+ * @param message the remote message we've just downloaded
+ * @param account the account it will be stored into
+ * @param folder the mailbox it will be stored into
+ * @param loadStatus when complete, the message will be marked with this status (e.g.
+ * EmailContent.Message.LOADED)
+ */
+ public static void copyOneMessageToProvider(Context context, Message message, Account account,
+ Mailbox folder, int loadStatus) {
+ EmailContent.Message localMessage = null;
+ Cursor c = null;
+ try {
+ c = context.getContentResolver().query(
+ EmailContent.Message.CONTENT_URI,
+ EmailContent.Message.CONTENT_PROJECTION,
+ EmailContent.MessageColumns.ACCOUNT_KEY + "=?" +
+ " AND " + MessageColumns.MAILBOX_KEY + "=?" +
+ " AND " + SyncColumns.SERVER_ID + "=?",
+ new String[] {
+ String.valueOf(account.mId),
+ String.valueOf(folder.mId),
+ String.valueOf(message.getUid())
+ },
+ null);
+ if (c.moveToNext()) {
+ localMessage = EmailContent.getContent(c, EmailContent.Message.class);
+ localMessage.mMailboxKey = folder.mId;
+ localMessage.mAccountKey = account.mId;
+ copyOneMessageToProvider(context, message, localMessage, loadStatus);
+ }
+ } finally {
+ if (c != null) {
+ c.close();
+ }
+ }
+ }
+
+ /**
+ * Copy one downloaded message (which may have partially-loaded sections)
+ * into an already-created EmailProvider Message
+ *
+ * @param message the remote message we've just downloaded
+ * @param localMessage the EmailProvider Message, already created
+ * @param loadStatus when complete, the message will be marked with this status (e.g.
+ * EmailContent.Message.LOADED)
+ * @param context the context to be used for EmailProvider
+ */
+ public static void copyOneMessageToProvider(Context context, Message message,
+ EmailContent.Message localMessage, int loadStatus) {
+ try {
+
+ EmailContent.Body body = EmailContent.Body.restoreBodyWithMessageId(context,
+ localMessage.mId);
+ if (body == null) {
+ body = new EmailContent.Body();
+ }
+ try {
+ // Copy the fields that are available into the message object
+ LegacyConversions.updateMessageFields(localMessage, message,
+ localMessage.mAccountKey, localMessage.mMailboxKey);
+
+ // Now process body parts & attachments
+ ArrayList<Part> viewables = new ArrayList<Part>();
+ ArrayList<Part> attachments = new ArrayList<Part>();
+ MimeUtility.collectParts(message, viewables, attachments);
+
+ ConversionUtilities.updateBodyFields(body, localMessage, viewables);
+
+ // Commit the message & body to the local store immediately
+ saveOrUpdate(localMessage, context);
+ saveOrUpdate(body, context);
+
+ // process (and save) attachments
+ LegacyConversions.updateAttachments(context, localMessage, attachments);
+
+ // One last update of message with two updated flags
+ localMessage.mFlagLoaded = loadStatus;
+
+ ContentValues cv = new ContentValues();
+ cv.put(EmailContent.MessageColumns.FLAG_ATTACHMENT, localMessage.mFlagAttachment);
+ cv.put(EmailContent.MessageColumns.FLAG_LOADED, localMessage.mFlagLoaded);
+ Uri uri = ContentUris.withAppendedId(EmailContent.Message.CONTENT_URI,
+ localMessage.mId);
+ context.getContentResolver().update(uri, cv, null, null);
+
+ } catch (MessagingException me) {
+ Log.e(Logging.LOG_TAG, "Error while copying downloaded message." + me);
+ }
+
+ } catch (RuntimeException rte) {
+ Log.e(Logging.LOG_TAG, "Error while storing downloaded message." + rte.toString());
+ } catch (IOException ioe) {
+ Log.e(Logging.LOG_TAG, "Error while storing attachment." + ioe.toString());
+ }
+ }
+
+ public static void saveOrUpdate(EmailContent content, Context context) {
+ if (content.isSaved()) {
+ content.update(context, content.toContentValues());
+ } else {
+ content.save(context);
+ }
+ }
+
+}
diff --git a/src/com/android/email/service/AuthenticatorService.java b/src/com/android/email/service/AuthenticatorService.java
new file mode 100644
index 000000000..2c93c4ba1
--- /dev/null
+++ b/src/com/android/email/service/AuthenticatorService.java
@@ -0,0 +1,159 @@
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.email.service;
+
+import com.android.email.activity.setup.AccountSetupBasics;
+import com.android.emailcommon.provider.EmailContent;
+
+import android.accounts.AbstractAccountAuthenticator;
+import android.accounts.Account;
+import android.accounts.AccountAuthenticatorResponse;
+import android.accounts.AccountManager;
+import android.accounts.NetworkErrorException;
+import android.app.Service;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.Intent;
+import android.os.Bundle;
+import android.os.IBinder;
+import android.provider.CalendarContract;
+import android.provider.ContactsContract;
+
+/**
+ * A very basic authenticator service for EAS. At the moment, it has no UI hooks. When called
+ * with addAccount, it simply adds the account to AccountManager directly with a username and
+ * password.
+ */
+public class AuthenticatorService extends Service {
+ public static final String OPTIONS_USERNAME = "username";
+ public static final String OPTIONS_PASSWORD = "password";
+ public static final String OPTIONS_CONTACTS_SYNC_ENABLED = "contacts";
+ public static final String OPTIONS_CALENDAR_SYNC_ENABLED = "calendar";
+ public static final String OPTIONS_EMAIL_SYNC_ENABLED = "email";
+
+ class Authenticator extends AbstractAccountAuthenticator {
+
+ public Authenticator(Context context) {
+ super(context);
+ }
+
+ @Override
+ public Bundle addAccount(AccountAuthenticatorResponse response, String accountType,
+ String authTokenType, String[] requiredFeatures, Bundle options)
+ throws NetworkErrorException {
+
+ // There are two cases here:
+ // 1) We are called with a username/password; this comes from the traditional email
+ // app UI; we simply create the account and return the proper bundle
+ if (options != null && options.containsKey(OPTIONS_PASSWORD)
+ && options.containsKey(OPTIONS_USERNAME)) {
+ final Account account = new Account(options.getString(OPTIONS_USERNAME),
+ accountType);
+ AccountManager.get(AuthenticatorService.this).addAccountExplicitly(
+ account, options.getString(OPTIONS_PASSWORD), null);
+
+ // Set up contacts syncing, if appropriate
+ if (options.containsKey(OPTIONS_CONTACTS_SYNC_ENABLED)) {
+ boolean syncContacts = options.getBoolean(OPTIONS_CONTACTS_SYNC_ENABLED);
+ ContentResolver.setIsSyncable(account, ContactsContract.AUTHORITY, 1);
+ ContentResolver.setSyncAutomatically(account, ContactsContract.AUTHORITY,
+ syncContacts);
+ }
+
+ // Set up calendar syncing, if appropriate
+ if (options.containsKey(OPTIONS_CALENDAR_SYNC_ENABLED)) {
+ boolean syncCalendar = options.getBoolean(OPTIONS_CALENDAR_SYNC_ENABLED);
+ ContentResolver.setIsSyncable(account, CalendarContract.AUTHORITY, 1);
+ ContentResolver.setSyncAutomatically(account, CalendarContract.AUTHORITY,
+ syncCalendar);
+ }
+
+ // Set up email syncing (it's always syncable, but we respect the user's choice
+ // for whether to enable it now)
+ boolean syncEmail = false;
+ if (options.containsKey(OPTIONS_EMAIL_SYNC_ENABLED) &&
+ options.getBoolean(OPTIONS_EMAIL_SYNC_ENABLED)) {
+ syncEmail = true;
+ }
+ ContentResolver.setIsSyncable(account, EmailContent.AUTHORITY, 1);
+ ContentResolver.setSyncAutomatically(account, EmailContent.AUTHORITY,
+ syncEmail);
+
+ Bundle b = new Bundle();
+ b.putString(AccountManager.KEY_ACCOUNT_NAME, options.getString(OPTIONS_USERNAME));
+ b.putString(AccountManager.KEY_ACCOUNT_TYPE, accountType);
+ return b;
+ // 2) The other case is that we're creating a new account from an Account manager
+ // activity. In this case, we add an intent that will be used to gather the
+ // account information...
+ } else {
+ Bundle b = new Bundle();
+ Intent intent =
+ AccountSetupBasics.actionGetCreateAccountIntent(AuthenticatorService.this,
+ accountType);
+ intent.putExtra(AccountManager.KEY_ACCOUNT_AUTHENTICATOR_RESPONSE, response);
+ b.putParcelable(AccountManager.KEY_INTENT, intent);
+ return b;
+ }
+ }
+
+ @Override
+ public Bundle confirmCredentials(AccountAuthenticatorResponse response, Account account,
+ Bundle options) {
+ return null;
+ }
+
+ @Override
+ public Bundle editProperties(AccountAuthenticatorResponse response, String accountType) {
+ return null;
+ }
+
+ @Override
+ public Bundle getAuthToken(AccountAuthenticatorResponse response, Account account,
+ String authTokenType, Bundle loginOptions) throws NetworkErrorException {
+ return null;
+ }
+
+ @Override
+ public String getAuthTokenLabel(String authTokenType) {
+ // null means we don't have compartmentalized authtoken types
+ return null;
+ }
+
+ @Override
+ public Bundle hasFeatures(AccountAuthenticatorResponse response, Account account,
+ String[] features) throws NetworkErrorException {
+ return null;
+ }
+
+ @Override
+ public Bundle updateCredentials(AccountAuthenticatorResponse response, Account account,
+ String authTokenType, Bundle loginOptions) {
+ return null;
+ }
+
+ }
+
+ @Override
+ public IBinder onBind(Intent intent) {
+ if (AccountManager.ACTION_AUTHENTICATOR_INTENT.equals(intent.getAction())) {
+ return new Authenticator(this).getIBinder();
+ } else {
+ return null;
+ }
+ }
+}
diff --git a/src/com/android/email/service/EmailServiceStub.java b/src/com/android/email/service/EmailServiceStub.java
new file mode 100644
index 000000000..e18e5e1a0
--- /dev/null
+++ b/src/com/android/email/service/EmailServiceStub.java
@@ -0,0 +1,561 @@
+/* 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.email.service;
+
+import android.content.ContentResolver;
+import android.content.ContentUris;
+import android.content.ContentValues;
+import android.content.Context;
+import android.database.Cursor;
+import android.net.TrafficStats;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.RemoteException;
+import android.text.TextUtils;
+import android.util.Log;
+
+import com.android.email.NotificationController;
+import com.android.email.mail.Sender;
+import com.android.email.mail.Store;
+import com.android.email.provider.Utilities;
+import com.android.email2.ui.MailActivityEmail;
+import com.android.emailcommon.AccountManagerTypes;
+import com.android.emailcommon.Api;
+import com.android.emailcommon.Logging;
+import com.android.emailcommon.TrafficFlags;
+import com.android.emailcommon.internet.MimeBodyPart;
+import com.android.emailcommon.internet.MimeHeader;
+import com.android.emailcommon.internet.MimeMultipart;
+import com.android.emailcommon.mail.AuthenticationFailedException;
+import com.android.emailcommon.mail.FetchProfile;
+import com.android.emailcommon.mail.Folder;
+import com.android.emailcommon.mail.Folder.MessageRetrievalListener;
+import com.android.emailcommon.mail.Folder.OpenMode;
+import com.android.emailcommon.mail.Message;
+import com.android.emailcommon.mail.MessagingException;
+import com.android.emailcommon.provider.Account;
+import com.android.emailcommon.provider.EmailContent;
+import com.android.emailcommon.provider.EmailContent.Attachment;
+import com.android.emailcommon.provider.EmailContent.AttachmentColumns;
+import com.android.emailcommon.provider.EmailContent.Body;
+import com.android.emailcommon.provider.EmailContent.BodyColumns;
+import com.android.emailcommon.provider.EmailContent.MailboxColumns;
+import com.android.emailcommon.provider.EmailContent.MessageColumns;
+import com.android.emailcommon.provider.HostAuth;
+import com.android.emailcommon.provider.Mailbox;
+import com.android.emailcommon.service.EmailServiceStatus;
+import com.android.emailcommon.service.IEmailService;
+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.UIProvider;
+
+import java.util.HashSet;
+
+/**
+ * EmailServiceStub is an abstract class representing an EmailService
+ *
+ * This class provides legacy support for a few methods that are common to both
+ * IMAP and POP3, including startSync, loadMore, loadAttachment, and sendMail
+ */
+public abstract class EmailServiceStub extends IEmailService.Stub implements IEmailService {
+
+ private static final int MAILBOX_COLUMN_ID = 0;
+ private static final int MAILBOX_COLUMN_SERVER_ID = 1;
+ private static final int MAILBOX_COLUMN_TYPE = 2;
+
+ public static final String SYNC_EXTRA_MAILBOX_ID = "__mailboxId__";
+
+ /** Small projection for just the columns required for a sync. */
+ private static final String[] MAILBOX_PROJECTION = new String[] {
+ MailboxColumns.ID,
+ MailboxColumns.SERVER_ID,
+ MailboxColumns.TYPE,
+ };
+
+ private Context mContext;
+ private IEmailServiceCallback.Stub mCallback;
+
+ protected void init(Context context, IEmailServiceCallback.Stub callbackProxy) {
+ mContext = context;
+ mCallback = callbackProxy;
+ }
+
+ @Override
+ public Bundle validate(HostAuth hostauth) throws RemoteException {
+ // TODO Auto-generated method stub
+ return null;
+ }
+
+ @Override
+ public void startSync(long mailboxId, boolean userRequest) throws RemoteException {
+ Mailbox mailbox = Mailbox.restoreMailboxWithId(mContext, mailboxId);
+ if (mailbox == null) return;
+ Account account = Account.restoreAccountWithId(mContext, mailbox.mAccountKey);
+ if (account == null) return;
+ android.accounts.Account acct = new android.accounts.Account(account.mEmailAddress,
+ AccountManagerTypes.TYPE_POP_IMAP);
+ Bundle extras = new Bundle();
+ extras.putBoolean(ContentResolver.SYNC_EXTRAS_EXPEDITED, true);
+ extras.putLong(SYNC_EXTRA_MAILBOX_ID, mailboxId);
+ ContentResolver.requestSync(acct, EmailContent.AUTHORITY, extras);
+ }
+
+ @Override
+ public void stopSync(long mailboxId) throws RemoteException {
+ // Not required
+ }
+
+ @Override
+ public void loadMore(long messageId) throws RemoteException {
+ // Load a message for view...
+ try {
+ // 1. Resample the message, in case it disappeared or synced while
+ // this command was in queue
+ EmailContent.Message message =
+ EmailContent.Message.restoreMessageWithId(mContext, messageId);
+ if (message == null) {
+ mCallback.loadMessageStatus(messageId,
+ EmailServiceStatus.MESSAGE_NOT_FOUND, 0);
+ return;
+ }
+ if (message.mFlagLoaded == EmailContent.Message.FLAG_LOADED_COMPLETE) {
+ // We should NEVER get here
+ mCallback.loadMessageStatus(messageId, 0, 100);
+ return;
+ }
+
+ // 2. Open the remote folder.
+ // TODO combine with common code in loadAttachment
+ Account account = Account.restoreAccountWithId(mContext, message.mAccountKey);
+ Mailbox mailbox = Mailbox.restoreMailboxWithId(mContext, message.mMailboxKey);
+ if (account == null || mailbox == null) {
+ //mListeners.loadMessageForViewFailed(messageId, "null account or mailbox");
+ return;
+ }
+ TrafficStats.setThreadStatsTag(TrafficFlags.getSyncFlags(mContext, account));
+
+ Store remoteStore = Store.getInstance(account, mContext);
+ String remoteServerId = mailbox.mServerId;
+ // If this is a search result, use the protocolSearchInfo field to get the
+ // correct remote location
+ if (!TextUtils.isEmpty(message.mProtocolSearchInfo)) {
+ remoteServerId = message.mProtocolSearchInfo;
+ }
+ Folder remoteFolder = remoteStore.getFolder(remoteServerId);
+ remoteFolder.open(OpenMode.READ_WRITE);
+
+ // 3. Set up to download the entire message
+ Message remoteMessage = remoteFolder.getMessage(message.mServerId);
+ FetchProfile fp = new FetchProfile();
+ fp.add(FetchProfile.Item.BODY);
+ remoteFolder.fetch(new Message[] { remoteMessage }, fp, null);
+
+ // 4. Write to provider
+ Utilities.copyOneMessageToProvider(mContext, remoteMessage, account, mailbox,
+ EmailContent.Message.FLAG_LOADED_COMPLETE);
+
+ // 5. Notify UI
+ mCallback.loadMessageStatus(messageId, 0, 100);
+
+ } catch (MessagingException me) {
+ if (Logging.LOGD) Log.v(Logging.LOG_TAG, "", me);
+ mCallback.loadMessageStatus(messageId, EmailServiceStatus.REMOTE_EXCEPTION, 0);
+ } catch (RuntimeException rte) {
+ mCallback.loadMessageStatus(messageId, EmailServiceStatus.REMOTE_EXCEPTION, 0);
+ }
+ }
+
+ private void doProgressCallback(long messageId, long attachmentId, int progress) {
+ try {
+ mCallback.loadAttachmentStatus(messageId, attachmentId,
+ EmailServiceStatus.IN_PROGRESS, progress);
+ } catch (RemoteException e) {
+ // No danger if the client is no longer around
+ }
+ }
+
+ @Override
+ public void loadAttachment(long attachmentId, boolean background) throws RemoteException {
+ try {
+ //1. Check if the attachment is already here and return early in that case
+ Attachment attachment =
+ Attachment.restoreAttachmentWithId(mContext, attachmentId);
+ if (attachment == null) {
+ mCallback.loadAttachmentStatus(0, attachmentId,
+ EmailServiceStatus.ATTACHMENT_NOT_FOUND, 0);
+ return;
+ }
+ long messageId = attachment.mMessageKey;
+
+ EmailContent.Message message =
+ EmailContent.Message.restoreMessageWithId(mContext, attachment.mMessageKey);
+ if (message == null) {
+ mCallback.loadAttachmentStatus(messageId, attachmentId,
+ EmailServiceStatus.MESSAGE_NOT_FOUND, 0);
+ }
+
+ // If the message is loaded, just report that we're finished
+ if (Utility.attachmentExists(mContext, attachment)) {
+ mCallback.loadAttachmentStatus(messageId, attachmentId, EmailServiceStatus.SUCCESS,
+ 0);
+ return;
+ }
+
+ // Say we're starting...
+ doProgressCallback(messageId, attachmentId, 0);
+
+ // 2. Open the remote folder.
+ Account account = Account.restoreAccountWithId(mContext, message.mAccountKey);
+ Mailbox mailbox = Mailbox.restoreMailboxWithId(mContext, message.mMailboxKey);
+
+ if (mailbox.mType == Mailbox.TYPE_OUTBOX) {
+ long sourceId = Utility.getFirstRowLong(mContext, Body.CONTENT_URI,
+ new String[] {BodyColumns.SOURCE_MESSAGE_KEY},
+ BodyColumns.MESSAGE_KEY + "=?",
+ new String[] {Long.toString(messageId)}, null, 0, -1L);
+ if (sourceId != -1 ) {
+ EmailContent.Message sourceMsg =
+ EmailContent.Message.restoreMessageWithId(mContext, sourceId);
+ if (sourceMsg != null) {
+ mailbox = Mailbox.restoreMailboxWithId(mContext, sourceMsg.mMailboxKey);
+ message.mServerId = sourceMsg.mServerId;
+ }
+ }
+ }
+
+ if (account == null || mailbox == null) {
+ // If the account/mailbox are gone, just report success; the UI handles this
+ mCallback.loadAttachmentStatus(messageId, attachmentId,
+ EmailServiceStatus.SUCCESS, 0);
+ return;
+ }
+ TrafficStats.setThreadStatsTag(
+ TrafficFlags.getAttachmentFlags(mContext, account));
+
+ Store remoteStore = Store.getInstance(account, mContext);
+ Folder remoteFolder = remoteStore.getFolder(mailbox.mServerId);
+ remoteFolder.open(OpenMode.READ_WRITE);
+
+ // 3. Generate a shell message in which to retrieve the attachment,
+ // and a shell BodyPart for the attachment. Then glue them together.
+ Message storeMessage = remoteFolder.createMessage(message.mServerId);
+ MimeBodyPart storePart = new MimeBodyPart();
+ storePart.setSize((int)attachment.mSize);
+ storePart.setHeader(MimeHeader.HEADER_ANDROID_ATTACHMENT_STORE_DATA,
+ attachment.mLocation);
+ storePart.setHeader(MimeHeader.HEADER_CONTENT_TYPE,
+ String.format("%s;\n name=\"%s\"",
+ attachment.mMimeType,
+ attachment.mFileName));
+ // TODO is this always true for attachments? I think we dropped the
+ // true encoding along the way
+ storePart.setHeader(MimeHeader.HEADER_CONTENT_TRANSFER_ENCODING, "base64");
+
+ MimeMultipart multipart = new MimeMultipart();
+ multipart.setSubType("mixed");
+ multipart.addBodyPart(storePart);
+
+ storeMessage.setHeader(MimeHeader.HEADER_CONTENT_TYPE, "multipart/mixed");
+ storeMessage.setBody(multipart);
+
+ // 4. Now ask for the attachment to be fetched
+ FetchProfile fp = new FetchProfile();
+ fp.add(storePart);
+ remoteFolder.fetch(new Message[] { storeMessage }, fp,
+ new MessageRetrievalListenerBridge(messageId, attachmentId));
+
+ // If we failed to load the attachment, throw an Exception here, so that
+ // AttachmentDownloadService knows that we failed
+ if (storePart.getBody() == null) {
+ throw new MessagingException("Attachment not loaded.");
+ }
+
+ // Save the attachment to wherever it's going
+ AttachmentUtilities.saveAttachment(mContext, storePart.getBody().getInputStream(),
+ attachment);
+
+ // 6. Report success
+ mCallback.loadAttachmentStatus(messageId, attachmentId, EmailServiceStatus.SUCCESS, 0);
+
+ // Close the connection
+ remoteFolder.close(false);
+ }
+ catch (MessagingException me) {
+ if (Logging.LOGD) Log.v(Logging.LOG_TAG, "", me);
+ // TODO: Fix this up; consider the best approach
+
+ ContentValues cv = new ContentValues();
+ cv.put(AttachmentColumns.UI_STATE, UIProvider.AttachmentState.FAILED);
+ Uri uri = ContentUris.withAppendedId(Attachment.CONTENT_URI, attachmentId);
+ mContext.getContentResolver().update(uri, cv, null, null);
+
+ mCallback.loadAttachmentStatus(0, attachmentId, EmailServiceStatus.CONNECTION_ERROR, 0);
+ }
+
+ }
+
+ /**
+ * Bridge to intercept {@link MessageRetrievalListener#loadAttachmentProgress} and
+ * pass down to {@link Result}.
+ */
+ public class MessageRetrievalListenerBridge implements MessageRetrievalListener {
+ private final long mMessageId;
+ private final long mAttachmentId;
+
+ public MessageRetrievalListenerBridge(long messageId, long attachmentId) {
+ mMessageId = messageId;
+ mAttachmentId = attachmentId;
+ }
+
+ @Override
+ public void loadAttachmentProgress(int progress) {
+ doProgressCallback(mMessageId, mAttachmentId, progress);
+ }
+
+ @Override
+ public void messageRetrieved(com.android.emailcommon.mail.Message message) {
+ }
+ }
+
+ @Override
+ public void updateFolderList(long accountId) throws RemoteException {
+ Account account = Account.restoreAccountWithId(mContext, accountId);
+ if (account == null) return;
+ Mailbox inbox = Mailbox.restoreMailboxOfType(mContext, accountId, Mailbox.TYPE_INBOX);
+ TrafficStats.setThreadStatsTag(TrafficFlags.getSyncFlags(mContext, account));
+ Cursor localFolderCursor = null;
+ try {
+ // Step 1: Get remote mailboxes
+ Store store = Store.getInstance(account, mContext);
+ Folder[] remoteFolders = store.updateFolders();
+ HashSet<String> remoteFolderNames = new HashSet<String>();
+ for (int i = 0, count = remoteFolders.length; i < count; i++) {
+ remoteFolderNames.add(remoteFolders[i].getName());
+ }
+
+ // Step 2: Get local mailboxes
+ localFolderCursor = mContext.getContentResolver().query(
+ Mailbox.CONTENT_URI,
+ MAILBOX_PROJECTION,
+ EmailContent.MailboxColumns.ACCOUNT_KEY + "=?",
+ new String[] { String.valueOf(account.mId) },
+ null);
+
+ // Step 3: Remove any local mailbox not on the remote list
+ while (localFolderCursor.moveToNext()) {
+ String mailboxPath = localFolderCursor.getString(MAILBOX_COLUMN_SERVER_ID);
+ // Short circuit if we have a remote mailbox with the same name
+ if (remoteFolderNames.contains(mailboxPath)) {
+ continue;
+ }
+
+ int mailboxType = localFolderCursor.getInt(MAILBOX_COLUMN_TYPE);
+ long mailboxId = localFolderCursor.getLong(MAILBOX_COLUMN_ID);
+ switch (mailboxType) {
+ case Mailbox.TYPE_INBOX:
+ case Mailbox.TYPE_DRAFTS:
+ case Mailbox.TYPE_OUTBOX:
+ case Mailbox.TYPE_SENT:
+ case Mailbox.TYPE_TRASH:
+ case Mailbox.TYPE_SEARCH:
+ // Never, ever delete special mailboxes
+ break;
+ default:
+ // Drop all attachment files related to this mailbox
+ AttachmentUtilities.deleteAllMailboxAttachmentFiles(
+ mContext, accountId, mailboxId);
+ // Delete the mailbox; database triggers take care of related
+ // Message, Body and Attachment records
+ Uri uri = ContentUris.withAppendedId(
+ Mailbox.CONTENT_URI, mailboxId);
+ mContext.getContentResolver().delete(uri, null, null);
+ break;
+ }
+ }
+ } catch (MessagingException e) {
+ // We'll hope this is temporary
+ } finally {
+ if (localFolderCursor != null) {
+ localFolderCursor.close();
+ }
+ // If this is a first sync, find the inbox and sync it
+ if (inbox == null) {
+ inbox = Mailbox.restoreMailboxOfType(mContext, accountId, Mailbox.TYPE_INBOX);
+ if (inbox != null) {
+ startSync(inbox.mId, true);
+ }
+ }
+ }
+ }
+
+ @Override
+ public boolean createFolder(long accountId, String name) throws RemoteException {
+ // Not required
+ return false;
+ }
+
+ @Override
+ public boolean deleteFolder(long accountId, String name) throws RemoteException {
+ // Not required
+ return false;
+ }
+
+ @Override
+ public boolean renameFolder(long accountId, String oldName, String newName)
+ throws RemoteException {
+ // Not required
+ return false;
+ }
+
+ @Override
+ public void setCallback(IEmailServiceCallback cb) throws RemoteException {
+ // Not required
+ }
+
+ @Override
+ public void setLogging(int on) throws RemoteException {
+ // Not required
+ }
+
+ @Override
+ public void hostChanged(long accountId) throws RemoteException {
+ // Not required
+ }
+
+ @Override
+ public Bundle autoDiscover(String userName, String password) throws RemoteException {
+ // Not required
+ return null;
+ }
+
+ @Override
+ public void sendMeetingResponse(long messageId, int response) throws RemoteException {
+ // Not required
+ }
+
+ @Override
+ public void deleteAccountPIMData(long accountId) throws RemoteException {
+ MailService.reconcilePopImapAccountsSync(mContext);
+ }
+
+ @Override
+ public int getApiLevel() throws RemoteException {
+ return Api.LEVEL;
+ }
+
+ @Override
+ public int searchMessages(long accountId, SearchParams params, long destMailboxId)
+ throws RemoteException {
+ // Not required
+ return 0;
+ }
+
+ @Override
+ public void sendMail(long accountId) throws RemoteException {
+ sendMailImpl(mContext, accountId);
+ }
+
+ public static void sendMailImpl(Context context, long accountId) {
+ Account account = Account.restoreAccountWithId(context, accountId);
+ TrafficStats.setThreadStatsTag(TrafficFlags.getSmtpFlags(context, account));
+ NotificationController nc = NotificationController.getInstance(context);
+ // 1. Loop through all messages in the account's outbox
+ long outboxId = Mailbox.findMailboxOfType(context, account.mId, Mailbox.TYPE_OUTBOX);
+ if (outboxId == Mailbox.NO_MAILBOX) {
+ return;
+ }
+ ContentResolver resolver = context.getContentResolver();
+ Cursor c = resolver.query(EmailContent.Message.CONTENT_URI,
+ EmailContent.Message.ID_COLUMN_PROJECTION,
+ EmailContent.Message.MAILBOX_KEY + "=?", new String[] { Long.toString(outboxId) },
+ null);
+ try {
+ // 2. exit early
+ if (c.getCount() <= 0) {
+ return;
+ }
+ Sender sender = Sender.getInstance(context, account);
+ Store remoteStore = Store.getInstance(account, context);
+ boolean requireMoveMessageToSentFolder = remoteStore.requireCopyMessageToSentFolder();
+ ContentValues moveToSentValues = null;
+ if (requireMoveMessageToSentFolder) {
+ Mailbox sentFolder =
+ Mailbox.restoreMailboxOfType(context, accountId, Mailbox.TYPE_SENT);
+ moveToSentValues = new ContentValues();
+ moveToSentValues.put(MessageColumns.MAILBOX_KEY, sentFolder.mId);
+ }
+
+ // 3. loop through the available messages and send them
+ while (c.moveToNext()) {
+ long messageId = -1;
+ moveToSentValues.remove(EmailContent.MessageColumns.FLAGS);
+ try {
+ messageId = c.getLong(0);
+ // Don't send messages with unloaded attachments
+ if (Utility.hasUnloadedAttachments(context, messageId)) {
+ if (MailActivityEmail.DEBUG) {
+ Log.d(Logging.LOG_TAG, "Can't send #" + messageId +
+ "; unloaded attachments");
+ }
+ continue;
+ }
+ sender.sendMessage(messageId);
+ } catch (MessagingException me) {
+ // report error for this message, but keep trying others
+ if (me instanceof AuthenticationFailedException) {
+ nc.showLoginFailedNotification(account.mId);
+ }
+ continue;
+ }
+ // 4. move to sent, or delete
+ Uri syncedUri =
+ ContentUris.withAppendedId(EmailContent.Message.SYNCED_CONTENT_URI, messageId);
+ if (requireMoveMessageToSentFolder) {
+ // If this is a forwarded message and it has attachments, delete them, as they
+ // duplicate information found elsewhere (on the server). This saves storage.
+ EmailContent.Message msg =
+ EmailContent.Message.restoreMessageWithId(context, messageId);
+ if (msg != null &&
+ ((msg.mFlags & EmailContent.Message.FLAG_TYPE_FORWARD) != 0)) {
+ AttachmentUtilities.deleteAllAttachmentFiles(context, account.mId,
+ messageId);
+ }
+ int flags = msg.mFlags & ~(EmailContent.Message.FLAG_TYPE_REPLY |
+ EmailContent.Message.FLAG_TYPE_FORWARD);
+ moveToSentValues.put(EmailContent.MessageColumns.FLAGS, flags);
+ resolver.update(syncedUri, moveToSentValues, null, null);
+ } else {
+ AttachmentUtilities.deleteAllAttachmentFiles(context, account.mId,
+ messageId);
+ Uri uri =
+ ContentUris.withAppendedId(EmailContent.Message.CONTENT_URI, messageId);
+ resolver.delete(uri, null, null);
+ resolver.delete(syncedUri, null, null);
+ }
+ }
+ nc.cancelLoginFailedNotification(account.mId);
+ } catch (MessagingException me) {
+ if (me instanceof AuthenticationFailedException) {
+ nc.showLoginFailedNotification(account.mId);
+ }
+ } finally {
+ c.close();
+ }
+
+ }
+}
diff --git a/src/com/android/email/service/ImapService.java b/src/com/android/email/service/ImapService.java
new file mode 100644
index 000000000..693f419b0
--- /dev/null
+++ b/src/com/android/email/service/ImapService.java
@@ -0,0 +1,1396 @@
+/*
+ * 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.email.service;
+
+import android.app.Service;
+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.net.TrafficStats;
+import android.net.Uri;
+import android.os.IBinder;
+import android.os.RemoteCallbackList;
+import android.os.RemoteException;
+import android.text.TextUtils;
+import android.util.Log;
+
+import com.android.email.LegacyConversions;
+import com.android.email.NotificationController;
+import com.android.email.mail.Store;
+import com.android.email.provider.Utilities;
+import com.android.email2.ui.MailActivityEmail;
+import com.android.emailcommon.Logging;
+import com.android.emailcommon.TrafficFlags;
+import com.android.emailcommon.internet.MimeUtility;
+import com.android.emailcommon.mail.AuthenticationFailedException;
+import com.android.emailcommon.mail.FetchProfile;
+import com.android.emailcommon.mail.Flag;
+import com.android.emailcommon.mail.Folder;
+import com.android.emailcommon.mail.Folder.FolderType;
+import com.android.emailcommon.mail.Folder.MessageRetrievalListener;
+import com.android.emailcommon.mail.Folder.MessageUpdateCallbacks;
+import com.android.emailcommon.mail.Folder.OpenMode;
+import com.android.emailcommon.mail.Message;
+import com.android.emailcommon.mail.MessagingException;
+import com.android.emailcommon.mail.Part;
+import com.android.emailcommon.provider.Account;
+import com.android.emailcommon.provider.EmailContent;
+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.EmailServiceStatus;
+import com.android.emailcommon.service.IEmailServiceCallback;
+import com.android.emailcommon.service.SearchParams;
+import com.android.emailcommon.utility.AttachmentUtilities;
+import com.android.mail.providers.UIProvider.AccountCapabilities;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.HashSet;
+
+public class ImapService extends Service {
+ private static final String TAG = "ImapService";
+ private static final int MAX_SMALL_MESSAGE_SIZE = (25 * 1024);
+
+ private static final Flag[] FLAG_LIST_SEEN = new Flag[] { Flag.SEEN };
+ private static final Flag[] FLAG_LIST_FLAGGED = new Flag[] { Flag.FLAGGED };
+ private static final Flag[] FLAG_LIST_ANSWERED = new Flag[] { Flag.ANSWERED };
+
+ /**
+ * Simple cache for last search result mailbox by account and serverId, since the most common
+ * case will be repeated use of the same mailbox
+ */
+ private static long mLastSearchAccountKey = Account.NO_ACCOUNT;
+ private static String mLastSearchServerId = null;
+ private static Mailbox mLastSearchRemoteMailbox = null;
+
+ /**
+ * Cache search results by account; this allows for "load more" support without having to
+ * redo the search (which can be quite slow). SortableMessage is a smallish class, so memory
+ * shouldn't be an issue
+ */
+ private static final HashMap<Long, SortableMessage[]> sSearchResults =
+ new HashMap<Long, SortableMessage[]>();
+
+ /**
+ * We write this into the serverId field of messages that will never be upsynced.
+ */
+ private static final String LOCAL_SERVERID_PREFIX = "Local-";
+
+ @Override
+ public int onStartCommand(Intent intent, int flags, int startId) {
+ return Service.START_STICKY;
+ }
+
+ // Callbacks as set up via setCallback
+ 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);
+ }
+ });
+ }
+ };
+
+ /**
+ * Create our EmailService implementation here.
+ */
+ private final EmailServiceStub mBinder = new EmailServiceStub() {
+
+ @Override
+ public void setCallback(IEmailServiceCallback cb) throws RemoteException {
+ mCallbackList.register(cb);
+ }
+
+ @Override
+ public void loadMore(long messageId) throws RemoteException {
+ // We don't do "loadMore" for IMAP messages; the sync should handle this
+ }
+
+ @Override
+ public int searchMessages(long accountId, SearchParams searchParams, long destMailboxId) {
+ try {
+ return searchMailboxImpl(getApplicationContext(), accountId, searchParams,
+ destMailboxId);
+ } catch (MessagingException e) {
+ }
+ return 0;
+ }
+
+ @Override
+ public int getCapabilities(long accountId) throws RemoteException {
+ return AccountCapabilities.SYNCABLE_FOLDERS |
+ AccountCapabilities.FOLDER_SERVER_SEARCH |
+ AccountCapabilities.UNDO;
+ }
+ };
+
+ @Override
+ public IBinder onBind(Intent intent) {
+ mBinder.init(this, sCallbackProxy);
+ return mBinder;
+ }
+
+ private static void sendMailboxStatus(Mailbox mailbox, int status) {
+ try {
+ sCallbackProxy.syncMailboxStatus(mailbox.mId, status, 0);
+ } catch (RemoteException e) {
+ }
+ }
+
+ /**
+ * Start foreground synchronization of the specified folder. This is called by
+ * synchronizeMailbox or checkMail.
+ * TODO this should use ID's instead of fully-restored objects
+ * @param account
+ * @param folder
+ * @throws MessagingException
+ */
+ public static void synchronizeMailboxSynchronous(Context context, final Account account,
+ final Mailbox folder) throws MessagingException {
+ sendMailboxStatus(folder, EmailServiceStatus.IN_PROGRESS);
+
+ TrafficStats.setThreadStatsTag(TrafficFlags.getSyncFlags(context, account));
+ if ((folder.mFlags & Mailbox.FLAG_HOLDS_MAIL) == 0) {
+ sendMailboxStatus(folder, EmailServiceStatus.SUCCESS);
+ }
+ NotificationController nc = NotificationController.getInstance(context);
+ try {
+ processPendingActionsSynchronous(context, account);
+ synchronizeMailboxGeneric(context, account, folder);
+ // Clear authentication notification for this account
+ nc.cancelLoginFailedNotification(account.mId);
+ sendMailboxStatus(folder, EmailServiceStatus.SUCCESS);
+ } catch (MessagingException e) {
+ if (Logging.LOGD) {
+ Log.v(Logging.LOG_TAG, "synchronizeMailbox", e);
+ }
+ if (e instanceof AuthenticationFailedException) {
+ // Generate authentication notification
+ nc.showLoginFailedNotification(account.mId);
+ }
+ sendMailboxStatus(folder, e.getExceptionType());
+ throw e;
+ }
+ }
+
+ /**
+ * Lightweight record for the first pass of message sync, where I'm just seeing if
+ * the local message requires sync. Later (for messages that need syncing) we'll do a full
+ * readout from the DB.
+ */
+ private static class LocalMessageInfo {
+ private static final int COLUMN_ID = 0;
+ private static final int COLUMN_FLAG_READ = 1;
+ private static final int COLUMN_FLAG_FAVORITE = 2;
+ private static final int COLUMN_FLAG_LOADED = 3;
+ private static final int COLUMN_SERVER_ID = 4;
+ private static final int COLUMN_FLAGS = 7;
+ private static final String[] PROJECTION = new String[] {
+ EmailContent.RECORD_ID,
+ MessageColumns.FLAG_READ, MessageColumns.FLAG_FAVORITE, MessageColumns.FLAG_LOADED,
+ SyncColumns.SERVER_ID, MessageColumns.MAILBOX_KEY, MessageColumns.ACCOUNT_KEY,
+ MessageColumns.FLAGS
+ };
+
+ final long mId;
+ final boolean mFlagRead;
+ final boolean mFlagFavorite;
+ final int mFlagLoaded;
+ final String mServerId;
+ final int mFlags;
+
+ public LocalMessageInfo(Cursor c) {
+ mId = c.getLong(COLUMN_ID);
+ mFlagRead = c.getInt(COLUMN_FLAG_READ) != 0;
+ mFlagFavorite = c.getInt(COLUMN_FLAG_FAVORITE) != 0;
+ mFlagLoaded = c.getInt(COLUMN_FLAG_LOADED);
+ mServerId = c.getString(COLUMN_SERVER_ID);
+ mFlags = c.getInt(COLUMN_FLAGS);
+ // Note: mailbox key and account key not needed - they are projected for the SELECT
+ }
+ }
+
+ /**
+ * Load the structure and body of messages not yet synced
+ * @param account the account we're syncing
+ * @param remoteFolder the (open) Folder we're working on
+ * @param unsyncedMessages an array of Message's we've got headers for
+ * @param toMailbox the destination mailbox we're syncing
+ * @throws MessagingException
+ */
+ static void loadUnsyncedMessages(final Context context, final Account account,
+ Folder remoteFolder, ArrayList<Message> messages, final Mailbox toMailbox)
+ throws MessagingException {
+
+ FetchProfile fp = new FetchProfile();
+ fp.add(FetchProfile.Item.STRUCTURE);
+ remoteFolder.fetch(messages.toArray(new Message[messages.size()]), fp, null);
+ for (Message message : messages) {
+ // Build a list of parts we are interested in. Text parts will be downloaded
+ // right now, attachments will be left for later.
+ ArrayList<Part> viewables = new ArrayList<Part>();
+ ArrayList<Part> attachments = new ArrayList<Part>();
+ MimeUtility.collectParts(message, viewables, attachments);
+ // Download the viewables immediately
+ for (Part part : viewables) {
+ fp.clear();
+ fp.add(part);
+ remoteFolder.fetch(new Message[] { message }, fp, null);
+ }
+ // Store the updated message locally and mark it fully loaded
+ Utilities.copyOneMessageToProvider(context, message, account, toMailbox,
+ EmailContent.Message.FLAG_LOADED_COMPLETE);
+ }
+ }
+
+ public static void downloadFlagAndEnvelope(final Context context, final Account account,
+ final Mailbox mailbox, Folder remoteFolder, ArrayList<Message> unsyncedMessages,
+ HashMap<String, LocalMessageInfo> localMessageMap, final ArrayList<Long> unseenMessages)
+ throws MessagingException {
+ FetchProfile fp = new FetchProfile();
+ fp.add(FetchProfile.Item.FLAGS);
+ fp.add(FetchProfile.Item.ENVELOPE);
+
+ final HashMap<String, LocalMessageInfo> localMapCopy;
+ if (localMessageMap != null)
+ localMapCopy = new HashMap<String, LocalMessageInfo>(localMessageMap);
+ else {
+ localMapCopy = new HashMap<String, LocalMessageInfo>();
+ }
+
+ remoteFolder.fetch(unsyncedMessages.toArray(new Message[0]), fp,
+ new MessageRetrievalListener() {
+ @Override
+ public void messageRetrieved(Message message) {
+ try {
+ // Determine if the new message was already known (e.g. partial)
+ // And create or reload the full message info
+ LocalMessageInfo localMessageInfo =
+ localMapCopy.get(message.getUid());
+ EmailContent.Message localMessage = null;
+ if (localMessageInfo == null) {
+ localMessage = new EmailContent.Message();
+ } else {
+ localMessage = EmailContent.Message.restoreMessageWithId(
+ context, localMessageInfo.mId);
+ }
+
+ if (localMessage != null) {
+ try {
+ // Copy the fields that are available into the message
+ LegacyConversions.updateMessageFields(localMessage,
+ message, account.mId, mailbox.mId);
+ // Commit the message to the local store
+ Utilities.saveOrUpdate(localMessage, context);
+ // Track the "new" ness of the downloaded message
+ if (!message.isSet(Flag.SEEN) && unseenMessages != null) {
+ unseenMessages.add(localMessage.mId);
+ }
+ } catch (MessagingException me) {
+ Log.e(Logging.LOG_TAG,
+ "Error while copying downloaded message." + me);
+ }
+
+ }
+ }
+ catch (Exception e) {
+ Log.e(Logging.LOG_TAG,
+ "Error while storing downloaded message." + e.toString());
+ }
+ }
+
+ @Override
+ public void loadAttachmentProgress(int progress) {
+ }
+ });
+
+ }
+
+ /**
+ * Synchronizer for IMAP.
+ *
+ * TODO Break this method up into smaller chunks.
+ *
+ * @param account the account to sync
+ * @param mailbox the mailbox to sync
+ * @return results of the sync pass
+ * @throws MessagingException
+ */
+ private static void synchronizeMailboxGeneric(final Context context,
+ final Account account, final Mailbox mailbox) throws MessagingException {
+
+ /*
+ * A list of IDs for messages that were downloaded and did not have the seen flag set.
+ * This serves as the "true" new message count reported to the user via notification.
+ */
+ final ArrayList<Long> unseenMessages = new ArrayList<Long>();
+
+ ContentResolver resolver = context.getContentResolver();
+
+ // 0. We do not ever sync DRAFTS or OUTBOX (down or up)
+ if (mailbox.mType == Mailbox.TYPE_DRAFTS || mailbox.mType == Mailbox.TYPE_OUTBOX) {
+ return;
+ }
+
+ // 1. Get the message list from the local store and create an index of the uids
+
+ Cursor localUidCursor = null;
+ HashMap<String, LocalMessageInfo> localMessageMap = new HashMap<String, LocalMessageInfo>();
+
+ try {
+ localUidCursor = resolver.query(
+ EmailContent.Message.CONTENT_URI,
+ LocalMessageInfo.PROJECTION,
+ EmailContent.MessageColumns.ACCOUNT_KEY + "=?" +
+ " AND " + MessageColumns.MAILBOX_KEY + "=?",
+ new String[] {
+ String.valueOf(account.mId),
+ String.valueOf(mailbox.mId)
+ },
+ null);
+ while (localUidCursor.moveToNext()) {
+ LocalMessageInfo info = new LocalMessageInfo(localUidCursor);
+ localMessageMap.put(info.mServerId, info);
+ }
+ } finally {
+ if (localUidCursor != null) {
+ localUidCursor.close();
+ }
+ }
+
+ // 2. Open the remote folder and create the remote folder if necessary
+
+ Store remoteStore = Store.getInstance(account, context);
+ // The account might have been deleted
+ if (remoteStore == null) return;
+ Folder remoteFolder = remoteStore.getFolder(mailbox.mServerId);
+
+ /*
+ * If the folder is a "special" folder we need to see if it exists
+ * on the remote server. It if does not exist we'll try to create it. If we
+ * can't create we'll abort. This will happen on every single Pop3 folder as
+ * designed and on Imap folders during error conditions. This allows us
+ * to treat Pop3 and Imap the same in this code.
+ */
+ if (mailbox.mType == Mailbox.TYPE_TRASH || mailbox.mType == Mailbox.TYPE_SENT
+ || mailbox.mType == Mailbox.TYPE_DRAFTS) {
+ if (!remoteFolder.exists()) {
+ if (!remoteFolder.create(FolderType.HOLDS_MESSAGES)) {
+ return;
+ }
+ }
+ }
+
+ // 3, Open the remote folder. This pre-loads certain metadata like message count.
+ remoteFolder.open(OpenMode.READ_WRITE);
+
+ // 4. Trash any remote messages that are marked as trashed locally.
+ // TODO - this comment was here, but no code was here.
+
+ // 5. Get the remote message count.
+ int remoteMessageCount = remoteFolder.getMessageCount();
+ ContentValues values = new ContentValues();
+ values.put(MailboxColumns.TOTAL_COUNT, remoteMessageCount);
+ mailbox.update(context, values);
+
+ // 6. Determine the limit # of messages to download
+ int visibleLimit = mailbox.mVisibleLimit;
+ if (visibleLimit <= 0) {
+ visibleLimit = MailActivityEmail.VISIBLE_LIMIT_DEFAULT;
+ }
+
+ // 7. Create a list of messages to download
+ Message[] remoteMessages = new Message[0];
+ final ArrayList<Message> unsyncedMessages = new ArrayList<Message>();
+ HashMap<String, Message> remoteUidMap = new HashMap<String, Message>();
+
+ if (remoteMessageCount > 0) {
+ /*
+ * Message numbers start at 1.
+ */
+ int remoteStart = Math.max(0, remoteMessageCount - visibleLimit) + 1;
+ int remoteEnd = remoteMessageCount;
+ remoteMessages = remoteFolder.getMessages(remoteStart, remoteEnd, null);
+ // TODO Why are we running through the list twice? Combine w/ for loop below
+ for (Message message : remoteMessages) {
+ remoteUidMap.put(message.getUid(), message);
+ }
+
+ /*
+ * Get a list of the messages that are in the remote list but not on the
+ * local store, or messages that are in the local store but failed to download
+ * on the last sync. These are the new messages that we will download.
+ * Note, we also skip syncing messages which are flagged as "deleted message" sentinels,
+ * because they are locally deleted and we don't need or want the old message from
+ * the server.
+ */
+ for (Message message : remoteMessages) {
+ LocalMessageInfo localMessage = localMessageMap.get(message.getUid());
+ // localMessage == null -> message has never been created (not even headers)
+ // mFlagLoaded = UNLOADED -> message created, but none of body loaded
+ // mFlagLoaded = PARTIAL -> message created, a "sane" amt of body has been loaded
+ // mFlagLoaded = COMPLETE -> message body has been completely loaded
+ // mFlagLoaded = DELETED -> message has been deleted
+ // Only the first two of these are "unsynced", so let's retrieve them
+ if (localMessage == null ||
+ (localMessage.mFlagLoaded == EmailContent.Message.FLAG_LOADED_UNLOADED) ||
+ (localMessage.mFlagLoaded == EmailContent.Message.FLAG_LOADED_PARTIAL)) {
+ unsyncedMessages.add(message);
+ }
+ }
+ }
+
+ // 8. Download basic info about the new/unloaded messages (if any)
+ /*
+ * Fetch the flags and envelope only of the new messages. This is intended to get us
+ * critical data as fast as possible, and then we'll fill in the details.
+ */
+ if (unsyncedMessages.size() > 0) {
+ downloadFlagAndEnvelope(context, account, mailbox, remoteFolder, unsyncedMessages,
+ localMessageMap, unseenMessages);
+ }
+
+ // 9. Refresh the flags for any messages in the local store that we didn't just download.
+ FetchProfile fp = new FetchProfile();
+ fp.add(FetchProfile.Item.FLAGS);
+ remoteFolder.fetch(remoteMessages, fp, null);
+ boolean remoteSupportsSeen = false;
+ boolean remoteSupportsFlagged = false;
+ boolean remoteSupportsAnswered = false;
+ for (Flag flag : remoteFolder.getPermanentFlags()) {
+ if (flag == Flag.SEEN) {
+ remoteSupportsSeen = true;
+ }
+ if (flag == Flag.FLAGGED) {
+ remoteSupportsFlagged = true;
+ }
+ if (flag == Flag.ANSWERED) {
+ remoteSupportsAnswered = true;
+ }
+ }
+ // Update SEEN/FLAGGED/ANSWERED (star) flags (if supported remotely - e.g. not for POP3)
+ if (remoteSupportsSeen || remoteSupportsFlagged || remoteSupportsAnswered) {
+ for (Message remoteMessage : remoteMessages) {
+ LocalMessageInfo localMessageInfo = localMessageMap.get(remoteMessage.getUid());
+ if (localMessageInfo == null) {
+ continue;
+ }
+ boolean localSeen = localMessageInfo.mFlagRead;
+ boolean remoteSeen = remoteMessage.isSet(Flag.SEEN);
+ boolean newSeen = (remoteSupportsSeen && (remoteSeen != localSeen));
+ boolean localFlagged = localMessageInfo.mFlagFavorite;
+ boolean remoteFlagged = remoteMessage.isSet(Flag.FLAGGED);
+ boolean newFlagged = (remoteSupportsFlagged && (localFlagged != remoteFlagged));
+ int localFlags = localMessageInfo.mFlags;
+ boolean localAnswered = (localFlags & EmailContent.Message.FLAG_REPLIED_TO) != 0;
+ boolean remoteAnswered = remoteMessage.isSet(Flag.ANSWERED);
+ boolean newAnswered = (remoteSupportsAnswered && (localAnswered != remoteAnswered));
+ if (newSeen || newFlagged || newAnswered) {
+ Uri uri = ContentUris.withAppendedId(
+ EmailContent.Message.CONTENT_URI, localMessageInfo.mId);
+ ContentValues updateValues = new ContentValues();
+ updateValues.put(MessageColumns.FLAG_READ, remoteSeen);
+ updateValues.put(MessageColumns.FLAG_FAVORITE, remoteFlagged);
+ if (remoteAnswered) {
+ localFlags |= EmailContent.Message.FLAG_REPLIED_TO;
+ } else {
+ localFlags &= ~EmailContent.Message.FLAG_REPLIED_TO;
+ }
+ updateValues.put(MessageColumns.FLAGS, localFlags);
+ resolver.update(uri, updateValues, null, null);
+ }
+ }
+ }
+
+ // 10. Remove any messages that are in the local store but no longer on the remote store.
+ HashSet<String> localUidsToDelete = new HashSet<String>(localMessageMap.keySet());
+ localUidsToDelete.removeAll(remoteUidMap.keySet());
+ for (String uidToDelete : localUidsToDelete) {
+ LocalMessageInfo infoToDelete = localMessageMap.get(uidToDelete);
+
+ // Delete associated data (attachment files)
+ // Attachment & Body records are auto-deleted when we delete the Message record
+ AttachmentUtilities.deleteAllAttachmentFiles(context, account.mId,
+ infoToDelete.mId);
+
+ // Delete the message itself
+ Uri uriToDelete = ContentUris.withAppendedId(
+ EmailContent.Message.CONTENT_URI, infoToDelete.mId);
+ resolver.delete(uriToDelete, null, null);
+
+ // Delete extra rows (e.g. synced or deleted)
+ Uri syncRowToDelete = ContentUris.withAppendedId(
+ EmailContent.Message.UPDATED_CONTENT_URI, infoToDelete.mId);
+ resolver.delete(syncRowToDelete, null, null);
+ Uri deletERowToDelete = ContentUris.withAppendedId(
+ EmailContent.Message.UPDATED_CONTENT_URI, infoToDelete.mId);
+ resolver.delete(deletERowToDelete, null, null);
+ }
+
+ loadUnsyncedMessages(context, account, remoteFolder, unsyncedMessages, mailbox);
+
+ // 14. Clean up and report results
+ remoteFolder.close(false);
+ }
+
+ /**
+ * Find messages in the updated table that need to be written back to server.
+ *
+ * Handles:
+ * Read/Unread
+ * Flagged
+ * Append (upload)
+ * Move To Trash
+ * Empty trash
+ * TODO:
+ * Move
+ *
+ * @param account the account to scan for pending actions
+ * @throws MessagingException
+ */
+ private static void processPendingActionsSynchronous(Context context, Account account)
+ throws MessagingException {
+ TrafficStats.setThreadStatsTag(TrafficFlags.getSyncFlags(context, account));
+ String[] accountIdArgs = new String[] { Long.toString(account.mId) };
+
+ // Handle deletes first, it's always better to get rid of things first
+ processPendingDeletesSynchronous(context, account, accountIdArgs);
+
+ // Handle uploads (currently, only to sent messages)
+ processPendingUploadsSynchronous(context, account, accountIdArgs);
+
+ // Now handle updates / upsyncs
+ processPendingUpdatesSynchronous(context, account, accountIdArgs);
+ }
+
+ /**
+ * Get the mailbox corresponding to the remote location of a message; this will normally be
+ * the mailbox whose _id is mailboxKey, except for search results, where we must look it up
+ * by serverId
+ * @param message the message in question
+ * @return the mailbox in which the message resides on the server
+ */
+ private static Mailbox getRemoteMailboxForMessage(Context context,
+ EmailContent.Message message) {
+ // If this is a search result, use the protocolSearchInfo field to get the server info
+ if (!TextUtils.isEmpty(message.mProtocolSearchInfo)) {
+ long accountKey = message.mAccountKey;
+ String protocolSearchInfo = message.mProtocolSearchInfo;
+ if (accountKey == mLastSearchAccountKey &&
+ protocolSearchInfo.equals(mLastSearchServerId)) {
+ return mLastSearchRemoteMailbox;
+ }
+ Cursor c = context.getContentResolver().query(Mailbox.CONTENT_URI,
+ Mailbox.CONTENT_PROJECTION, Mailbox.PATH_AND_ACCOUNT_SELECTION,
+ new String[] {protocolSearchInfo, Long.toString(accountKey)},
+ null);
+ try {
+ if (c.moveToNext()) {
+ Mailbox mailbox = new Mailbox();
+ mailbox.restore(c);
+ mLastSearchAccountKey = accountKey;
+ mLastSearchServerId = protocolSearchInfo;
+ mLastSearchRemoteMailbox = mailbox;
+ return mailbox;
+ } else {
+ return null;
+ }
+ } finally {
+ c.close();
+ }
+ } else {
+ return Mailbox.restoreMailboxWithId(context, message.mMailboxKey);
+ }
+ }
+
+ /**
+ * Scan for messages that are in the Message_Deletes table, look for differences that
+ * we can deal with, and do the work.
+ *
+ * @param account
+ * @param resolver
+ * @param accountIdArgs
+ */
+ private static void processPendingDeletesSynchronous(Context context, Account account,
+ String[] accountIdArgs) {
+ Cursor deletes = context.getContentResolver().query(
+ EmailContent.Message.DELETED_CONTENT_URI,
+ EmailContent.Message.CONTENT_PROJECTION,
+ EmailContent.MessageColumns.ACCOUNT_KEY + "=?", accountIdArgs,
+ EmailContent.MessageColumns.MAILBOX_KEY);
+ long lastMessageId = -1;
+ try {
+ // Defer setting up the store until we know we need to access it
+ Store remoteStore = null;
+ // loop through messages marked as deleted
+ while (deletes.moveToNext()) {
+ boolean deleteFromTrash = false;
+
+ EmailContent.Message oldMessage =
+ EmailContent.getContent(deletes, EmailContent.Message.class);
+
+ if (oldMessage != null) {
+ lastMessageId = oldMessage.mId;
+
+ Mailbox mailbox = getRemoteMailboxForMessage(context, oldMessage);
+ if (mailbox == null) {
+ continue; // Mailbox removed. Move to the next message.
+ }
+ deleteFromTrash = mailbox.mType == Mailbox.TYPE_TRASH;
+
+ // Load the remote store if it will be needed
+ if (remoteStore == null && deleteFromTrash) {
+ remoteStore = Store.getInstance(account, context);
+ }
+
+ // Dispatch here for specific change types
+ if (deleteFromTrash) {
+ // Move message to trash
+ processPendingDeleteFromTrash(context, remoteStore, account, mailbox,
+ oldMessage);
+ }
+ }
+
+ // Finally, delete the update
+ Uri uri = ContentUris.withAppendedId(EmailContent.Message.DELETED_CONTENT_URI,
+ oldMessage.mId);
+ context.getContentResolver().delete(uri, null, null);
+ }
+ } catch (MessagingException me) {
+ // Presumably an error here is an account connection failure, so there is
+ // no point in continuing through the rest of the pending updates.
+ if (MailActivityEmail.DEBUG) {
+ Log.d(Logging.LOG_TAG, "Unable to process pending delete for id="
+ + lastMessageId + ": " + me);
+ }
+ } finally {
+ deletes.close();
+ }
+ }
+
+ /**
+ * Scan for messages that are in Sent, and are in need of upload,
+ * and send them to the server. "In need of upload" is defined as:
+ * serverId == null (no UID has been assigned)
+ * or
+ * message is in the updated list
+ *
+ * Note we also look for messages that are moving from drafts->outbox->sent. They never
+ * go through "drafts" or "outbox" on the server, so we hang onto these until they can be
+ * uploaded directly to the Sent folder.
+ *
+ * @param account
+ * @param resolver
+ * @param accountIdArgs
+ */
+ private static void processPendingUploadsSynchronous(Context context, Account account,
+ String[] accountIdArgs) {
+ ContentResolver resolver = context.getContentResolver();
+ // Find the Sent folder (since that's all we're uploading for now
+ Cursor mailboxes = resolver.query(Mailbox.CONTENT_URI, Mailbox.ID_PROJECTION,
+ MailboxColumns.ACCOUNT_KEY + "=?"
+ + " and " + MailboxColumns.TYPE + "=" + Mailbox.TYPE_SENT,
+ accountIdArgs, null);
+ long lastMessageId = -1;
+ try {
+ // Defer setting up the store until we know we need to access it
+ Store remoteStore = null;
+ while (mailboxes.moveToNext()) {
+ long mailboxId = mailboxes.getLong(Mailbox.ID_PROJECTION_COLUMN);
+ String[] mailboxKeyArgs = new String[] { Long.toString(mailboxId) };
+ // Demand load mailbox
+ Mailbox mailbox = null;
+
+ // First handle the "new" messages (serverId == null)
+ Cursor upsyncs1 = resolver.query(EmailContent.Message.CONTENT_URI,
+ EmailContent.Message.ID_PROJECTION,
+ EmailContent.Message.MAILBOX_KEY + "=?"
+ + " and (" + EmailContent.Message.SERVER_ID + " is null"
+ + " or " + EmailContent.Message.SERVER_ID + "=''" + ")",
+ mailboxKeyArgs,
+ null);
+ try {
+ while (upsyncs1.moveToNext()) {
+ // Load the remote store if it will be needed
+ if (remoteStore == null) {
+ remoteStore = Store.getInstance(account, context);
+ }
+ // Load the mailbox if it will be needed
+ if (mailbox == null) {
+ mailbox = Mailbox.restoreMailboxWithId(context, mailboxId);
+ if (mailbox == null) {
+ continue; // Mailbox removed. Move to the next message.
+ }
+ }
+ // upsync the message
+ long id = upsyncs1.getLong(EmailContent.Message.ID_PROJECTION_COLUMN);
+ lastMessageId = id;
+ processUploadMessage(context, remoteStore, account, mailbox, id);
+ }
+ } finally {
+ if (upsyncs1 != null) {
+ upsyncs1.close();
+ }
+ }
+
+ // Next, handle any updates (e.g. edited in place, although this shouldn't happen)
+ Cursor upsyncs2 = resolver.query(EmailContent.Message.UPDATED_CONTENT_URI,
+ EmailContent.Message.ID_PROJECTION,
+ EmailContent.MessageColumns.MAILBOX_KEY + "=?", mailboxKeyArgs,
+ null);
+ try {
+ while (upsyncs2.moveToNext()) {
+ // Load the remote store if it will be needed
+ if (remoteStore == null) {
+ remoteStore = Store.getInstance(account, context);
+ }
+ // Load the mailbox if it will be needed
+ if (mailbox == null) {
+ mailbox = Mailbox.restoreMailboxWithId(context, mailboxId);
+ if (mailbox == null) {
+ continue; // Mailbox removed. Move to the next message.
+ }
+ }
+ // upsync the message
+ long id = upsyncs2.getLong(EmailContent.Message.ID_PROJECTION_COLUMN);
+ lastMessageId = id;
+ processUploadMessage(context, remoteStore, account, mailbox, id);
+ }
+ } finally {
+ if (upsyncs2 != null) {
+ upsyncs2.close();
+ }
+ }
+ }
+ } catch (MessagingException me) {
+ // Presumably an error here is an account connection failure, so there is
+ // no point in continuing through the rest of the pending updates.
+ if (MailActivityEmail.DEBUG) {
+ Log.d(Logging.LOG_TAG, "Unable to process pending upsync for id="
+ + lastMessageId + ": " + me);
+ }
+ } finally {
+ if (mailboxes != null) {
+ mailboxes.close();
+ }
+ }
+ }
+
+ /**
+ * Scan for messages that are in the Message_Updates table, look for differences that
+ * we can deal with, and do the work.
+ *
+ * @param account
+ * @param resolver
+ * @param accountIdArgs
+ */
+ private static void processPendingUpdatesSynchronous(Context context, Account account,
+ String[] accountIdArgs) {
+ ContentResolver resolver = context.getContentResolver();
+ Cursor updates = resolver.query(EmailContent.Message.UPDATED_CONTENT_URI,
+ EmailContent.Message.CONTENT_PROJECTION,
+ EmailContent.MessageColumns.ACCOUNT_KEY + "=?", accountIdArgs,
+ EmailContent.MessageColumns.MAILBOX_KEY);
+ long lastMessageId = -1;
+ try {
+ // Defer setting up the store until we know we need to access it
+ Store remoteStore = null;
+ // Demand load mailbox (note order-by to reduce thrashing here)
+ Mailbox mailbox = null;
+ // loop through messages marked as needing updates
+ while (updates.moveToNext()) {
+ boolean changeMoveToTrash = false;
+ boolean changeRead = false;
+ boolean changeFlagged = false;
+ boolean changeMailbox = false;
+ boolean changeAnswered = false;
+
+ EmailContent.Message oldMessage =
+ EmailContent.getContent(updates, EmailContent.Message.class);
+ lastMessageId = oldMessage.mId;
+ EmailContent.Message newMessage =
+ EmailContent.Message.restoreMessageWithId(context, oldMessage.mId);
+ if (newMessage != null) {
+ mailbox = Mailbox.restoreMailboxWithId(context, newMessage.mMailboxKey);
+ if (mailbox == null) {
+ continue; // Mailbox removed. Move to the next message.
+ }
+ if (oldMessage.mMailboxKey != newMessage.mMailboxKey) {
+ if (mailbox.mType == Mailbox.TYPE_TRASH) {
+ changeMoveToTrash = true;
+ } else {
+ changeMailbox = true;
+ }
+ }
+ changeRead = oldMessage.mFlagRead != newMessage.mFlagRead;
+ changeFlagged = oldMessage.mFlagFavorite != newMessage.mFlagFavorite;
+ changeAnswered = (oldMessage.mFlags & EmailContent.Message.FLAG_REPLIED_TO) !=
+ (newMessage.mFlags & EmailContent.Message.FLAG_REPLIED_TO);
+ }
+
+ // Load the remote store if it will be needed
+ if (remoteStore == null &&
+ (changeMoveToTrash || changeRead || changeFlagged || changeMailbox ||
+ changeAnswered)) {
+ remoteStore = Store.getInstance(account, context);
+ }
+
+ // Dispatch here for specific change types
+ if (changeMoveToTrash) {
+ // Move message to trash
+ processPendingMoveToTrash(context, remoteStore, account, mailbox, oldMessage,
+ newMessage);
+ } else if (changeRead || changeFlagged || changeMailbox || changeAnswered) {
+ processPendingDataChange(context, remoteStore, mailbox, changeRead,
+ changeFlagged, changeMailbox, changeAnswered, oldMessage, newMessage);
+ }
+
+ // Finally, delete the update
+ Uri uri = ContentUris.withAppendedId(EmailContent.Message.UPDATED_CONTENT_URI,
+ oldMessage.mId);
+ resolver.delete(uri, null, null);
+ }
+
+ } catch (MessagingException me) {
+ // Presumably an error here is an account connection failure, so there is
+ // no point in continuing through the rest of the pending updates.
+ if (MailActivityEmail.DEBUG) {
+ Log.d(Logging.LOG_TAG, "Unable to process pending update for id="
+ + lastMessageId + ": " + me);
+ }
+ } finally {
+ updates.close();
+ }
+ }
+
+ /**
+ * Upsync an entire message. This must also unwind whatever triggered it (either by
+ * updating the serverId, or by deleting the update record, or it's going to keep happening
+ * over and over again.
+ *
+ * Note: If the message is being uploaded into an unexpected mailbox, we *do not* upload.
+ * This is to avoid unnecessary uploads into the trash. Although the caller attempts to select
+ * only the Drafts and Sent folders, this can happen when the update record and the current
+ * record mismatch. In this case, we let the update record remain, because the filters
+ * in processPendingUpdatesSynchronous() will pick it up as a move and handle it (or drop it)
+ * appropriately.
+ *
+ * @param resolver
+ * @param remoteStore
+ * @param account
+ * @param mailbox the actual mailbox
+ * @param messageId
+ */
+ private static void processUploadMessage(Context context, Store remoteStore,
+ Account account, Mailbox mailbox, long messageId)
+ throws MessagingException {
+ EmailContent.Message newMessage =
+ EmailContent.Message.restoreMessageWithId(context, messageId);
+ boolean deleteUpdate = false;
+ if (newMessage == null) {
+ deleteUpdate = true;
+ Log.d(Logging.LOG_TAG, "Upsync failed for null message, id=" + messageId);
+ } else if (mailbox.mType == Mailbox.TYPE_DRAFTS) {
+ deleteUpdate = false;
+ Log.d(Logging.LOG_TAG, "Upsync skipped for mailbox=drafts, id=" + messageId);
+ } else if (mailbox.mType == Mailbox.TYPE_OUTBOX) {
+ deleteUpdate = false;
+ Log.d(Logging.LOG_TAG, "Upsync skipped for mailbox=outbox, id=" + messageId);
+ } else if (mailbox.mType == Mailbox.TYPE_TRASH) {
+ deleteUpdate = false;
+ Log.d(Logging.LOG_TAG, "Upsync skipped for mailbox=trash, id=" + messageId);
+ } else if (newMessage != null && newMessage.mMailboxKey != mailbox.mId) {
+ deleteUpdate = false;
+ Log.d(Logging.LOG_TAG, "Upsync skipped; mailbox changed, id=" + messageId);
+ } else {
+// Log.d(Logging.LOG_TAG, "Upsyc triggered for message id=" + messageId);
+// deleteUpdate = processPendingAppend(context, remoteStore, account, mailbox,
+ //newMessage);
+ }
+ if (deleteUpdate) {
+ // Finally, delete the update (if any)
+ Uri uri = ContentUris.withAppendedId(
+ EmailContent.Message.UPDATED_CONTENT_URI, messageId);
+ context.getContentResolver().delete(uri, null, null);
+ }
+ }
+
+ /**
+ * Upsync changes to read, flagged, or mailbox
+ *
+ * @param remoteStore the remote store for this mailbox
+ * @param mailbox the mailbox the message is stored in
+ * @param changeRead whether the message's read state has changed
+ * @param changeFlagged whether the message's flagged state has changed
+ * @param changeMailbox whether the message's mailbox has changed
+ * @param oldMessage the message in it's pre-change state
+ * @param newMessage the current version of the message
+ */
+ private static void processPendingDataChange(final Context context, Store remoteStore,
+ Mailbox mailbox, boolean changeRead, boolean changeFlagged, boolean changeMailbox,
+ boolean changeAnswered, EmailContent.Message oldMessage,
+ final EmailContent.Message newMessage) throws MessagingException {
+ // New mailbox is the mailbox this message WILL be in (same as the one it WAS in if it isn't
+ // being moved
+ Mailbox newMailbox = mailbox;
+ // Mailbox is the original remote mailbox (the one we're acting on)
+ mailbox = getRemoteMailboxForMessage(context, oldMessage);
+
+ // 0. No remote update if the message is local-only
+ if (newMessage.mServerId == null || newMessage.mServerId.equals("")
+ || newMessage.mServerId.startsWith(LOCAL_SERVERID_PREFIX) || (mailbox == null)) {
+ return;
+ }
+
+ // 1. No remote update for DRAFTS or OUTBOX
+ if (mailbox.mType == Mailbox.TYPE_DRAFTS || mailbox.mType == Mailbox.TYPE_OUTBOX) {
+ return;
+ }
+
+ // 2. Open the remote store & folder
+ Folder remoteFolder = remoteStore.getFolder(mailbox.mServerId);
+ if (!remoteFolder.exists()) {
+ return;
+ }
+ remoteFolder.open(OpenMode.READ_WRITE);
+ if (remoteFolder.getMode() != OpenMode.READ_WRITE) {
+ return;
+ }
+
+ // 3. Finally, apply the changes to the message
+ Message remoteMessage = remoteFolder.getMessage(newMessage.mServerId);
+ if (remoteMessage == null) {
+ return;
+ }
+ if (MailActivityEmail.DEBUG) {
+ Log.d(Logging.LOG_TAG,
+ "Update for msg id=" + newMessage.mId
+ + " read=" + newMessage.mFlagRead
+ + " flagged=" + newMessage.mFlagFavorite
+ + " answered="
+ + ((newMessage.mFlags & EmailContent.Message.FLAG_REPLIED_TO) != 0)
+ + " new mailbox=" + newMessage.mMailboxKey);
+ }
+ Message[] messages = new Message[] { remoteMessage };
+ if (changeRead) {
+ remoteFolder.setFlags(messages, FLAG_LIST_SEEN, newMessage.mFlagRead);
+ }
+ if (changeFlagged) {
+ remoteFolder.setFlags(messages, FLAG_LIST_FLAGGED, newMessage.mFlagFavorite);
+ }
+ if (changeAnswered) {
+ remoteFolder.setFlags(messages, FLAG_LIST_ANSWERED,
+ (newMessage.mFlags & EmailContent.Message.FLAG_REPLIED_TO) != 0);
+ }
+ if (changeMailbox) {
+ Folder toFolder = remoteStore.getFolder(newMailbox.mServerId);
+ if (!remoteFolder.exists()) {
+ return;
+ }
+ // We may need the message id to search for the message in the destination folder
+ remoteMessage.setMessageId(newMessage.mMessageId);
+ // Copy the message to its new folder
+ remoteFolder.copyMessages(messages, toFolder, new MessageUpdateCallbacks() {
+ @Override
+ public void onMessageUidChange(Message message, String newUid) {
+ ContentValues cv = new ContentValues();
+ cv.put(EmailContent.Message.SERVER_ID, newUid);
+ // We only have one message, so, any updates _must_ be for it. Otherwise,
+ // we'd have to cycle through to find the one with the same server ID.
+ context.getContentResolver().update(ContentUris.withAppendedId(
+ EmailContent.Message.CONTENT_URI, newMessage.mId), cv, null, null);
+ }
+ @Override
+ public void onMessageNotFound(Message message) {
+ }
+ });
+ // Delete the message from the remote source folder
+ remoteMessage.setFlag(Flag.DELETED, true);
+ remoteFolder.expunge();
+ }
+ remoteFolder.close(false);
+ }
+
+ /**
+ * Process a pending trash message command.
+ *
+ * @param remoteStore the remote store we're working in
+ * @param account The account in which we are working
+ * @param newMailbox The local trash mailbox
+ * @param oldMessage The message copy that was saved in the updates shadow table
+ * @param newMessage The message that was moved to the mailbox
+ */
+ private static void processPendingMoveToTrash(final Context context, Store remoteStore,
+ Account account, Mailbox newMailbox, EmailContent.Message oldMessage,
+ final EmailContent.Message newMessage) throws MessagingException {
+
+ // 0. No remote move if the message is local-only
+ if (newMessage.mServerId == null || newMessage.mServerId.equals("")
+ || newMessage.mServerId.startsWith(LOCAL_SERVERID_PREFIX)) {
+ return;
+ }
+
+ // 1. Escape early if we can't find the local mailbox
+ // TODO smaller projection here
+ Mailbox oldMailbox = getRemoteMailboxForMessage(context, oldMessage);
+ if (oldMailbox == null) {
+ // can't find old mailbox, it may have been deleted. just return.
+ return;
+ }
+ // 2. We don't support delete-from-trash here
+ if (oldMailbox.mType == Mailbox.TYPE_TRASH) {
+ return;
+ }
+
+ // The rest of this method handles server-side deletion
+
+ // 4. Find the remote mailbox (that we deleted from), and open it
+ Folder remoteFolder = remoteStore.getFolder(oldMailbox.mServerId);
+ if (!remoteFolder.exists()) {
+ return;
+ }
+
+ remoteFolder.open(OpenMode.READ_WRITE);
+ if (remoteFolder.getMode() != OpenMode.READ_WRITE) {
+ remoteFolder.close(false);
+ return;
+ }
+
+ // 5. Find the remote original message
+ Message remoteMessage = remoteFolder.getMessage(oldMessage.mServerId);
+ if (remoteMessage == null) {
+ remoteFolder.close(false);
+ return;
+ }
+
+ // 6. Find the remote trash folder, and create it if not found
+ Folder remoteTrashFolder = remoteStore.getFolder(newMailbox.mServerId);
+ if (!remoteTrashFolder.exists()) {
+ /*
+ * If the remote trash folder doesn't exist we try to create it.
+ */
+ remoteTrashFolder.create(FolderType.HOLDS_MESSAGES);
+ }
+
+ // 7. Try to copy the message into the remote trash folder
+ // Note, this entire section will be skipped for POP3 because there's no remote trash
+ if (remoteTrashFolder.exists()) {
+ /*
+ * Because remoteTrashFolder may be new, we need to explicitly open it
+ */
+ remoteTrashFolder.open(OpenMode.READ_WRITE);
+ if (remoteTrashFolder.getMode() != OpenMode.READ_WRITE) {
+ remoteFolder.close(false);
+ remoteTrashFolder.close(false);
+ return;
+ }
+
+ remoteFolder.copyMessages(new Message[] { remoteMessage }, remoteTrashFolder,
+ new Folder.MessageUpdateCallbacks() {
+ @Override
+ public void onMessageUidChange(Message message, String newUid) {
+ // update the UID in the local trash folder, because some stores will
+ // have to change it when copying to remoteTrashFolder
+ ContentValues cv = new ContentValues();
+ cv.put(EmailContent.Message.SERVER_ID, newUid);
+ context.getContentResolver().update(newMessage.getUri(), cv, null, null);
+ }
+
+ /**
+ * This will be called if the deleted message doesn't exist and can't be
+ * deleted (e.g. it was already deleted from the server.) In this case,
+ * attempt to delete the local copy as well.
+ */
+ @Override
+ public void onMessageNotFound(Message message) {
+ context.getContentResolver().delete(newMessage.getUri(), null, null);
+ }
+ });
+ remoteTrashFolder.close(false);
+ }
+
+ // 8. Delete the message from the remote source folder
+ remoteMessage.setFlag(Flag.DELETED, true);
+ remoteFolder.expunge();
+ remoteFolder.close(false);
+ }
+
+ /**
+ * Process a pending trash message command.
+ *
+ * @param remoteStore the remote store we're working in
+ * @param account The account in which we are working
+ * @param oldMailbox The local trash mailbox
+ * @param oldMessage The message that was deleted from the trash
+ */
+ private static void processPendingDeleteFromTrash(Context context, Store remoteStore,
+ Account account, Mailbox oldMailbox, EmailContent.Message oldMessage)
+ throws MessagingException {
+
+ // 1. We only support delete-from-trash here
+ if (oldMailbox.mType != Mailbox.TYPE_TRASH) {
+ return;
+ }
+
+ // 2. Find the remote trash folder (that we are deleting from), and open it
+ Folder remoteTrashFolder = remoteStore.getFolder(oldMailbox.mServerId);
+ if (!remoteTrashFolder.exists()) {
+ return;
+ }
+
+ remoteTrashFolder.open(OpenMode.READ_WRITE);
+ if (remoteTrashFolder.getMode() != OpenMode.READ_WRITE) {
+ remoteTrashFolder.close(false);
+ return;
+ }
+
+ // 3. Find the remote original message
+ Message remoteMessage = remoteTrashFolder.getMessage(oldMessage.mServerId);
+ if (remoteMessage == null) {
+ remoteTrashFolder.close(false);
+ return;
+ }
+
+ // 4. Delete the message from the remote trash folder
+ remoteMessage.setFlag(Flag.DELETED, true);
+ remoteTrashFolder.expunge();
+ remoteTrashFolder.close(false);
+ }
+
+ /**
+ * A message and numeric uid that's easily sortable
+ */
+ private static class SortableMessage {
+ private final Message mMessage;
+ private final long mUid;
+
+ SortableMessage(Message message, long uid) {
+ mMessage = message;
+ mUid = uid;
+ }
+ }
+
+ public int searchMailbox(Context context, long accountId, SearchParams searchParams,
+ long destMailboxId) throws MessagingException {
+ try {
+ return searchMailboxImpl(context, accountId, searchParams, destMailboxId);
+ } finally {
+ // Tell UI
+ }
+ }
+
+ private int searchMailboxImpl(final Context context, long accountId, SearchParams searchParams,
+ final long destMailboxId) throws MessagingException {
+ final Account account = Account.restoreAccountWithId(context, accountId);
+ final Mailbox mailbox = Mailbox.restoreMailboxWithId(context, searchParams.mMailboxId);
+ final Mailbox destMailbox = Mailbox.restoreMailboxWithId(context, destMailboxId);
+ if (account == null || mailbox == null || destMailbox == null) {
+ Log.d(Logging.LOG_TAG, "Attempted search for " + searchParams
+ + " but account or mailbox information was missing");
+ return 0;
+ }
+
+ // Tell UI that we're loading messages
+
+ Store remoteStore = Store.getInstance(account, context);
+ Folder remoteFolder = remoteStore.getFolder(mailbox.mServerId);
+ remoteFolder.open(OpenMode.READ_WRITE);
+
+ SortableMessage[] sortableMessages = new SortableMessage[0];
+ if (searchParams.mOffset == 0) {
+ // Get the "bare" messages (basically uid)
+ Message[] remoteMessages = remoteFolder.getMessages(searchParams, null);
+ int remoteCount = remoteMessages.length;
+ if (remoteCount > 0) {
+ sortableMessages = new SortableMessage[remoteCount];
+ int i = 0;
+ for (Message msg : remoteMessages) {
+ sortableMessages[i++] = new SortableMessage(msg, Long.parseLong(msg.getUid()));
+ }
+ // Sort the uid's, most recent first
+ // Note: Not all servers will be nice and return results in the order of request;
+ // those that do will see messages arrive from newest to oldest
+ Arrays.sort(sortableMessages, new Comparator<SortableMessage>() {
+ @Override
+ public int compare(SortableMessage lhs, SortableMessage rhs) {
+ return lhs.mUid > rhs.mUid ? -1 : lhs.mUid < rhs.mUid ? 1 : 0;
+ }
+ });
+ sSearchResults.put(accountId, sortableMessages);
+ }
+ } else {
+ sortableMessages = sSearchResults.get(accountId);
+ }
+
+ final int numSearchResults = sortableMessages.length;
+ final int numToLoad =
+ Math.min(numSearchResults - searchParams.mOffset, searchParams.mLimit);
+ if (numToLoad <= 0) {
+ return 0;
+ }
+
+ final ArrayList<Message> messageList = new ArrayList<Message>();
+ for (int i = searchParams.mOffset; i < numToLoad + searchParams.mOffset; i++) {
+ messageList.add(sortableMessages[i].mMessage);
+ }
+ // Get everything in one pass, rather than two (as in sync); this starts getting us
+ // usable results quickly.
+ FetchProfile fp = new FetchProfile();
+ fp.add(FetchProfile.Item.FLAGS);
+ fp.add(FetchProfile.Item.ENVELOPE);
+ fp.add(FetchProfile.Item.STRUCTURE);
+ fp.add(FetchProfile.Item.BODY_SANE);
+ remoteFolder.fetch(messageList.toArray(new Message[0]), fp,
+ new MessageRetrievalListener() {
+ @Override
+ public void messageRetrieved(Message message) {
+ try {
+ // Determine if the new message was already known (e.g. partial)
+ // And create or reload the full message info
+ EmailContent.Message localMessage = new EmailContent.Message();
+ try {
+ // Copy the fields that are available into the message
+ LegacyConversions.updateMessageFields(localMessage,
+ message, account.mId, mailbox.mId);
+ // Commit the message to the local store
+ Utilities.saveOrUpdate(localMessage, context);
+ localMessage.mMailboxKey = destMailboxId;
+ // We load 50k or so; maybe it's complete, maybe not...
+ int flag = EmailContent.Message.FLAG_LOADED_COMPLETE;
+ // We store the serverId of the source mailbox into protocolSearchInfo
+ // This will be used by loadMessageForView, etc. to use the proper remote
+ // folder
+ localMessage.mProtocolSearchInfo = mailbox.mServerId;
+ if (message.getSize() > Store.FETCH_BODY_SANE_SUGGESTED_SIZE) {
+ flag = EmailContent.Message.FLAG_LOADED_PARTIAL;
+ }
+ Utilities.copyOneMessageToProvider(context, message, localMessage, flag);
+ } catch (MessagingException me) {
+ Log.e(Logging.LOG_TAG,
+ "Error while copying downloaded message." + me);
+ }
+ } catch (Exception e) {
+ Log.e(Logging.LOG_TAG,
+ "Error while storing downloaded message." + e.toString());
+ }
+ }
+
+ @Override
+ public void loadAttachmentProgress(int progress) {
+ }
+ });
+ return numSearchResults;
+ }
+} \ No newline at end of file
diff --git a/src/com/android/email/service/ImapTempFileLiteral.java b/src/com/android/email/service/ImapTempFileLiteral.java
new file mode 100644
index 000000000..cc1dd5410
--- /dev/null
+++ b/src/com/android/email/service/ImapTempFileLiteral.java
@@ -0,0 +1,127 @@
+/*
+ * 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;
+
+import android.util.Log;
+
+import com.android.email.FixedLengthInputStream;
+import com.android.email.mail.store.imap.ImapResponse;
+import com.android.email.mail.store.imap.ImapResponseParser;
+import com.android.email.mail.store.imap.ImapString;
+import com.android.emailcommon.Logging;
+import com.android.emailcommon.TempDirectory;
+import com.android.emailcommon.utility.Utility;
+
+import org.apache.commons.io.IOUtils;
+
+import java.io.ByteArrayInputStream;
+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;
+
+/**
+ * Subclass of {@link ImapString} used for literals backed by a temp file.
+ */
+public class ImapTempFileLiteral extends ImapString {
+ /* package for test */ final File mFile;
+
+ /** Size is purely for toString() */
+ private final int mSize;
+
+ /* package */ ImapTempFileLiteral(FixedLengthInputStream stream) throws IOException {
+ mSize = stream.getLength();
+ mFile = File.createTempFile("imap", ".tmp", TempDirectory.getTempDirectory());
+
+ // Unfortunately, we can't really use deleteOnExit(), because temp filenames are random
+ // so it'd simply cause a memory leak.
+ // deleteOnExit() simply adds filenames to a static list and the list will never shrink.
+ // mFile.deleteOnExit();
+ OutputStream out = new FileOutputStream(mFile);
+ IOUtils.copy(stream, out);
+ out.close();
+ }
+
+ /**
+ * Make sure we delete the temp file.
+ *
+ * We should always be calling {@link ImapResponse#destroy()}, but it's here as a last resort.
+ */
+ @Override
+ protected void finalize() throws Throwable {
+ try {
+ destroy();
+ } finally {
+ super.finalize();
+ }
+ }
+
+ @Override
+ public InputStream getAsStream() {
+ checkNotDestroyed();
+ try {
+ return new FileInputStream(mFile);
+ } catch (FileNotFoundException e) {
+ // It's probably possible if we're low on storage and the system clears the cache dir.
+ Log.w(Logging.LOG_TAG, "ImapTempFileLiteral: Temp file not found");
+
+ // Return 0 byte stream as a dummy...
+ return new ByteArrayInputStream(new byte[0]);
+ }
+ }
+
+ @Override
+ public String getString() {
+ checkNotDestroyed();
+ try {
+ byte[] bytes = IOUtils.toByteArray(getAsStream());
+ // Prevent crash from OOM; we've seen this, but only rarely and not reproducibly
+ if (bytes.length > ImapResponseParser.LITERAL_KEEP_IN_MEMORY_THRESHOLD) {
+ throw new IOException();
+ }
+ return Utility.fromAscii(bytes);
+ } catch (IOException e) {
+ Log.w(Logging.LOG_TAG, "ImapTempFileLiteral: Error while reading temp file", e);
+ return "";
+ }
+ }
+
+ @Override
+ public void destroy() {
+ try {
+ if (!isDestroyed() && mFile.exists()) {
+ mFile.delete();
+ }
+ } catch (RuntimeException re) {
+ // Just log and ignore.
+ Log.w(Logging.LOG_TAG, "Failed to remove temp file: " + re.getMessage());
+ }
+ super.destroy();
+ }
+
+ @Override
+ public String toString() {
+ return String.format("{%d byte literal(file)}", mSize);
+ }
+
+ public boolean tempFileExistsForTest() {
+ return mFile.exists();
+ }
+}
diff --git a/src/com/android/email/service/Pop3Service.java b/src/com/android/email/service/Pop3Service.java
new file mode 100644
index 000000000..404ef8779
--- /dev/null
+++ b/src/com/android/email/service/Pop3Service.java
@@ -0,0 +1,726 @@
+/*
+ * 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.email.service;
+
+import android.app.Service;
+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.net.TrafficStats;
+import android.net.Uri;
+import android.os.IBinder;
+import android.os.RemoteCallbackList;
+import android.os.RemoteException;
+import android.util.Log;
+
+import com.android.email.LegacyConversions;
+import com.android.email.NotificationController;
+import com.android.email.mail.Store;
+import com.android.email.provider.Utilities;
+import com.android.email2.ui.MailActivityEmail;
+import com.android.emailcommon.Logging;
+import com.android.emailcommon.TrafficFlags;
+import com.android.emailcommon.internet.MimeUtility;
+import com.android.emailcommon.mail.AuthenticationFailedException;
+import com.android.emailcommon.mail.FetchProfile;
+import com.android.emailcommon.mail.Flag;
+import com.android.emailcommon.mail.Folder;
+import com.android.emailcommon.mail.Folder.FolderType;
+import com.android.emailcommon.mail.Folder.MessageRetrievalListener;
+import com.android.emailcommon.mail.Folder.OpenMode;
+import com.android.emailcommon.mail.Message;
+import com.android.emailcommon.mail.MessagingException;
+import com.android.emailcommon.mail.Part;
+import com.android.emailcommon.provider.Account;
+import com.android.emailcommon.provider.EmailContent;
+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.EmailServiceStatus;
+import com.android.emailcommon.service.IEmailServiceCallback;
+import com.android.emailcommon.utility.AttachmentUtilities;
+import com.android.mail.providers.UIProvider.AccountCapabilities;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.HashSet;
+
+public class Pop3Service extends Service {
+ private static final String TAG = "Pop3Service";
+ private static final int MAX_SMALL_MESSAGE_SIZE = (25 * 1024);
+
+ @Override
+ public int onStartCommand(Intent intent, int flags, int startId) {
+ return Service.START_STICKY;
+ }
+
+ // Callbacks as set up via setCallback
+ 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);
+ }
+ });
+ }
+ };
+
+ /**
+ * Create our EmailService implementation here.
+ */
+ private final EmailServiceStub mBinder = new EmailServiceStub() {
+
+ @Override
+ public void setCallback(IEmailServiceCallback cb) throws RemoteException {
+ mCallbackList.register(cb);
+ }
+
+ @Override
+ public int getCapabilities(long accountId) throws RemoteException {
+ return AccountCapabilities.UNDO;
+ }
+ };
+
+ @Override
+ public IBinder onBind(Intent intent) {
+ mBinder.init(this, sCallbackProxy);
+ return mBinder;
+ }
+
+ private static void sendMailboxStatus(Mailbox mailbox, int status) {
+ try {
+ sCallbackProxy.syncMailboxStatus(mailbox.mId, status, 0);
+ } catch (RemoteException e) {
+ }
+ }
+
+ /**
+ * Start foreground synchronization of the specified folder. This is called by
+ * synchronizeMailbox or checkMail.
+ * TODO this should use ID's instead of fully-restored objects
+ * @param account
+ * @param folder
+ * @throws MessagingException
+ */
+ public static void synchronizeMailboxSynchronous(Context context, final Account account,
+ final Mailbox folder) throws MessagingException {
+ sendMailboxStatus(folder, EmailServiceStatus.IN_PROGRESS);
+
+ TrafficStats.setThreadStatsTag(TrafficFlags.getSyncFlags(context, account));
+ if ((folder.mFlags & Mailbox.FLAG_HOLDS_MAIL) == 0) {
+ sendMailboxStatus(folder, EmailServiceStatus.SUCCESS);
+ }
+ NotificationController nc = NotificationController.getInstance(context);
+ try {
+ processPendingActionsSynchronous(context, account);
+ synchronizeMailboxGeneric(context, account, folder);
+ // Clear authentication notification for this account
+ nc.cancelLoginFailedNotification(account.mId);
+ sendMailboxStatus(folder, EmailServiceStatus.SUCCESS);
+ } catch (MessagingException e) {
+ if (Logging.LOGD) {
+ Log.v(Logging.LOG_TAG, "synchronizeMailbox", e);
+ }
+ if (e instanceof AuthenticationFailedException) {
+ // Generate authentication notification
+ nc.showLoginFailedNotification(account.mId);
+ }
+ sendMailboxStatus(folder, e.getExceptionType());
+ throw e;
+ }
+ }
+
+ /**
+ * Lightweight record for the first pass of message sync, where I'm just seeing if
+ * the local message requires sync. Later (for messages that need syncing) we'll do a full
+ * readout from the DB.
+ */
+ private static class LocalMessageInfo {
+ private static final int COLUMN_ID = 0;
+ private static final int COLUMN_FLAG_READ = 1;
+ private static final int COLUMN_FLAG_FAVORITE = 2;
+ private static final int COLUMN_FLAG_LOADED = 3;
+ private static final int COLUMN_SERVER_ID = 4;
+ private static final int COLUMN_FLAGS = 7;
+ private static final String[] PROJECTION = new String[] {
+ EmailContent.RECORD_ID,
+ MessageColumns.FLAG_READ, MessageColumns.FLAG_FAVORITE, MessageColumns.FLAG_LOADED,
+ SyncColumns.SERVER_ID, MessageColumns.MAILBOX_KEY, MessageColumns.ACCOUNT_KEY,
+ MessageColumns.FLAGS
+ };
+
+ final long mId;
+ final boolean mFlagRead;
+ final boolean mFlagFavorite;
+ final int mFlagLoaded;
+ final String mServerId;
+ final int mFlags;
+
+ public LocalMessageInfo(Cursor c) {
+ mId = c.getLong(COLUMN_ID);
+ mFlagRead = c.getInt(COLUMN_FLAG_READ) != 0;
+ mFlagFavorite = c.getInt(COLUMN_FLAG_FAVORITE) != 0;
+ mFlagLoaded = c.getInt(COLUMN_FLAG_LOADED);
+ mServerId = c.getString(COLUMN_SERVER_ID);
+ mFlags = c.getInt(COLUMN_FLAGS);
+ // Note: mailbox key and account key not needed - they are projected for the SELECT
+ }
+ }
+
+ private static void saveOrUpdate(EmailContent content, Context context) {
+ if (content.isSaved()) {
+ content.update(context, content.toContentValues());
+ } else {
+ content.save(context);
+ }
+ }
+
+ /**
+ * Load the structure and body of messages not yet synced
+ * @param account the account we're syncing
+ * @param remoteFolder the (open) Folder we're working on
+ * @param unsyncedMessages an array of Message's we've got headers for
+ * @param toMailbox the destination mailbox we're syncing
+ * @throws MessagingException
+ */
+ static void loadUnsyncedMessages(final Context context, final Account account,
+ Folder remoteFolder, ArrayList<Message> unsyncedMessages, final Mailbox toMailbox)
+ throws MessagingException {
+
+ // 1. Divide the unsynced messages into small & large (by size)
+
+ // TODO doing this work here (synchronously) is problematic because it prevents the UI
+ // from affecting the order (e.g. download a message because the user requested it.) Much
+ // of this logic should move out to a different sync loop that attempts to update small
+ // groups of messages at a time, as a background task. However, we can't just return
+ // (yet) because POP messages don't have an envelope yet....
+
+ ArrayList<Message> largeMessages = new ArrayList<Message>();
+ ArrayList<Message> smallMessages = new ArrayList<Message>();
+ for (Message message : unsyncedMessages) {
+ if (message.getSize() > (MAX_SMALL_MESSAGE_SIZE)) {
+ largeMessages.add(message);
+ } else {
+ smallMessages.add(message);
+ }
+ }
+
+ // 2. Download small messages
+
+ // TODO Problems with this implementation. 1. For IMAP, where we get a real envelope,
+ // this is going to be inefficient and duplicate work we've already done. 2. It's going
+ // back to the DB for a local message that we already had (and discarded).
+
+ // For small messages, we specify "body", which returns everything (incl. attachments)
+ FetchProfile fp = new FetchProfile();
+ fp.add(FetchProfile.Item.BODY);
+ remoteFolder.fetch(smallMessages.toArray(new Message[smallMessages.size()]), fp,
+ new MessageRetrievalListener() {
+ @Override
+ public void messageRetrieved(Message message) {
+ // Store the updated message locally and mark it fully loaded
+ Utilities.copyOneMessageToProvider(context, message, account, toMailbox,
+ EmailContent.Message.FLAG_LOADED_COMPLETE);
+ }
+
+ @Override
+ public void loadAttachmentProgress(int progress) {
+ }
+ });
+
+ // 3. Download large messages. We ask the server to give us the message structure,
+ // but not all of the attachments.
+ fp.clear();
+ fp.add(FetchProfile.Item.STRUCTURE);
+ remoteFolder.fetch(largeMessages.toArray(new Message[largeMessages.size()]), fp, null);
+ for (Message message : largeMessages) {
+ if (message.getBody() == null) {
+ // POP doesn't support STRUCTURE mode, so we'll just do a partial download
+ // (hopefully enough to see some/all of the body) and mark the message for
+ // further download.
+ fp.clear();
+ fp.add(FetchProfile.Item.BODY_SANE);
+ // TODO a good optimization here would be to make sure that all Stores set
+ // the proper size after this fetch and compare the before and after size. If
+ // they equal we can mark this SYNCHRONIZED instead of PARTIALLY_SYNCHRONIZED
+ remoteFolder.fetch(new Message[] { message }, fp, null);
+
+ // Store the partially-loaded message and mark it partially loaded
+ Utilities.copyOneMessageToProvider(context, message, account, toMailbox,
+ EmailContent.Message.FLAG_LOADED_PARTIAL);
+ } else {
+ // We have a structure to deal with, from which
+ // we can pull down the parts we want to actually store.
+ // Build a list of parts we are interested in. Text parts will be downloaded
+ // right now, attachments will be left for later.
+ ArrayList<Part> viewables = new ArrayList<Part>();
+ ArrayList<Part> attachments = new ArrayList<Part>();
+ MimeUtility.collectParts(message, viewables, attachments);
+ // Download the viewables immediately
+ for (Part part : viewables) {
+ fp.clear();
+ fp.add(part);
+ // TODO what happens if the network connection dies? We've got partial
+ // messages with incorrect status stored.
+ remoteFolder.fetch(new Message[] { message }, fp, null);
+ }
+ // Store the updated message locally and mark it fully loaded
+ Utilities.copyOneMessageToProvider(context, message, account, toMailbox,
+ EmailContent.Message.FLAG_LOADED_COMPLETE);
+ }
+ }
+
+ }
+
+ public static void downloadFlagAndEnvelope(final Context context, final Account account,
+ final Mailbox mailbox, Folder remoteFolder, ArrayList<Message> unsyncedMessages,
+ HashMap<String, LocalMessageInfo> localMessageMap, final ArrayList<Long> unseenMessages)
+ throws MessagingException {
+ FetchProfile fp = new FetchProfile();
+ fp.add(FetchProfile.Item.FLAGS);
+ fp.add(FetchProfile.Item.ENVELOPE);
+
+ final HashMap<String, LocalMessageInfo> localMapCopy;
+ if (localMessageMap != null)
+ localMapCopy = new HashMap<String, LocalMessageInfo>(localMessageMap);
+ else {
+ localMapCopy = new HashMap<String, LocalMessageInfo>();
+ }
+
+ remoteFolder.fetch(unsyncedMessages.toArray(new Message[0]), fp,
+ new MessageRetrievalListener() {
+ @Override
+ public void messageRetrieved(Message message) {
+ try {
+ // Determine if the new message was already known (e.g. partial)
+ // And create or reload the full message info
+ LocalMessageInfo localMessageInfo =
+ localMapCopy.get(message.getUid());
+ EmailContent.Message localMessage = null;
+ if (localMessageInfo == null) {
+ localMessage = new EmailContent.Message();
+ } else {
+ localMessage = EmailContent.Message.restoreMessageWithId(
+ context, localMessageInfo.mId);
+ }
+
+ if (localMessage != null) {
+ try {
+ // Copy the fields that are available into the message
+ LegacyConversions.updateMessageFields(localMessage,
+ message, account.mId, mailbox.mId);
+ // Commit the message to the local store
+ saveOrUpdate(localMessage, context);
+ // Track the "new" ness of the downloaded message
+ if (!message.isSet(Flag.SEEN) && unseenMessages != null) {
+ unseenMessages.add(localMessage.mId);
+ }
+ } catch (MessagingException me) {
+ Log.e(Logging.LOG_TAG,
+ "Error while copying downloaded message." + me);
+ }
+
+ }
+ }
+ catch (Exception e) {
+ Log.e(Logging.LOG_TAG,
+ "Error while storing downloaded message." + e.toString());
+ }
+ }
+
+ @Override
+ public void loadAttachmentProgress(int progress) {
+ }
+ });
+
+ }
+
+ /**
+ * Synchronizer for IMAP.
+ *
+ * TODO Break this method up into smaller chunks.
+ *
+ * @param account the account to sync
+ * @param mailbox the mailbox to sync
+ * @return results of the sync pass
+ * @throws MessagingException
+ */
+ private static void synchronizeMailboxGeneric(final Context context,
+ final Account account, final Mailbox mailbox) throws MessagingException {
+
+ /*
+ * A list of IDs for messages that were downloaded and did not have the seen flag set.
+ * This serves as the "true" new message count reported to the user via notification.
+ */
+ final ArrayList<Long> unseenMessages = new ArrayList<Long>();
+
+ ContentResolver resolver = context.getContentResolver();
+
+ // 0. We do not ever sync DRAFTS or OUTBOX (down or up)
+ if (mailbox.mType == Mailbox.TYPE_DRAFTS || mailbox.mType == Mailbox.TYPE_OUTBOX) {
+ return;
+ }
+
+ // 1. Get the message list from the local store and create an index of the uids
+
+ Cursor localUidCursor = null;
+ HashMap<String, LocalMessageInfo> localMessageMap = new HashMap<String, LocalMessageInfo>();
+
+ try {
+ localUidCursor = resolver.query(
+ EmailContent.Message.CONTENT_URI,
+ LocalMessageInfo.PROJECTION,
+ EmailContent.MessageColumns.ACCOUNT_KEY + "=?" +
+ " AND " + MessageColumns.MAILBOX_KEY + "=?",
+ new String[] {
+ String.valueOf(account.mId),
+ String.valueOf(mailbox.mId)
+ },
+ null);
+ while (localUidCursor.moveToNext()) {
+ LocalMessageInfo info = new LocalMessageInfo(localUidCursor);
+ localMessageMap.put(info.mServerId, info);
+ }
+ } finally {
+ if (localUidCursor != null) {
+ localUidCursor.close();
+ }
+ }
+
+ // 2. Open the remote folder and create the remote folder if necessary
+
+ Store remoteStore = Store.getInstance(account, context);
+ // The account might have been deleted
+ if (remoteStore == null) return;
+ Folder remoteFolder = remoteStore.getFolder(mailbox.mServerId);
+
+ /*
+ * If the folder is a "special" folder we need to see if it exists
+ * on the remote server. It if does not exist we'll try to create it. If we
+ * can't create we'll abort. This will happen on every single Pop3 folder as
+ * designed and on Imap folders during error conditions. This allows us
+ * to treat Pop3 and Imap the same in this code.
+ */
+ if (mailbox.mType == Mailbox.TYPE_TRASH || mailbox.mType == Mailbox.TYPE_SENT
+ || mailbox.mType == Mailbox.TYPE_DRAFTS) {
+ if (!remoteFolder.exists()) {
+ if (!remoteFolder.create(FolderType.HOLDS_MESSAGES)) {
+ return;
+ }
+ }
+ }
+
+ // 3, Open the remote folder. This pre-loads certain metadata like message count.
+ remoteFolder.open(OpenMode.READ_WRITE);
+
+ // 4. Trash any remote messages that are marked as trashed locally.
+ // TODO - this comment was here, but no code was here.
+
+ // 5. Get the remote message count.
+ int remoteMessageCount = remoteFolder.getMessageCount();
+ ContentValues values = new ContentValues();
+ values.put(MailboxColumns.TOTAL_COUNT, remoteMessageCount);
+ mailbox.update(context, values);
+
+ // 6. Determine the limit # of messages to download
+ int visibleLimit = mailbox.mVisibleLimit;
+ if (visibleLimit <= 0) {
+ visibleLimit = MailActivityEmail.VISIBLE_LIMIT_DEFAULT;
+ }
+
+ // 7. Create a list of messages to download
+ Message[] remoteMessages = new Message[0];
+ final ArrayList<Message> unsyncedMessages = new ArrayList<Message>();
+ HashMap<String, Message> remoteUidMap = new HashMap<String, Message>();
+
+ if (remoteMessageCount > 0) {
+ /*
+ * Message numbers start at 1.
+ */
+ int remoteStart = Math.max(0, remoteMessageCount - visibleLimit) + 1;
+ int remoteEnd = remoteMessageCount;
+ remoteMessages = remoteFolder.getMessages(remoteStart, remoteEnd, null);
+ // TODO Why are we running through the list twice? Combine w/ for loop below
+ for (Message message : remoteMessages) {
+ remoteUidMap.put(message.getUid(), message);
+ }
+
+ /*
+ * Get a list of the messages that are in the remote list but not on the
+ * local store, or messages that are in the local store but failed to download
+ * on the last sync. These are the new messages that we will download.
+ * Note, we also skip syncing messages which are flagged as "deleted message" sentinels,
+ * because they are locally deleted and we don't need or want the old message from
+ * the server.
+ */
+ for (Message message : remoteMessages) {
+ LocalMessageInfo localMessage = localMessageMap.get(message.getUid());
+ // localMessage == null -> message has never been created (not even headers)
+ // mFlagLoaded = UNLOADED -> message created, but none of body loaded
+ // mFlagLoaded = PARTIAL -> message created, a "sane" amt of body has been loaded
+ // mFlagLoaded = COMPLETE -> message body has been completely loaded
+ // mFlagLoaded = DELETED -> message has been deleted
+ // Only the first two of these are "unsynced", so let's retrieve them
+ if (localMessage == null ||
+ (localMessage.mFlagLoaded == EmailContent.Message.FLAG_LOADED_UNLOADED)) {
+ unsyncedMessages.add(message);
+ }
+ }
+ }
+
+ // 8. Download basic info about the new/unloaded messages (if any)
+ /*
+ * Fetch the flags and envelope only of the new messages. This is intended to get us
+ * critical data as fast as possible, and then we'll fill in the details.
+ */
+ if (unsyncedMessages.size() > 0) {
+ downloadFlagAndEnvelope(context, account, mailbox, remoteFolder, unsyncedMessages,
+ localMessageMap, unseenMessages);
+ }
+
+ // 9. Refresh the flags for any messages in the local store that we didn't just download.
+ FetchProfile fp = new FetchProfile();
+ fp.add(FetchProfile.Item.FLAGS);
+ remoteFolder.fetch(remoteMessages, fp, null);
+ boolean remoteSupportsSeen = false;
+ boolean remoteSupportsFlagged = false;
+ boolean remoteSupportsAnswered = false;
+ for (Flag flag : remoteFolder.getPermanentFlags()) {
+ if (flag == Flag.SEEN) {
+ remoteSupportsSeen = true;
+ }
+ if (flag == Flag.FLAGGED) {
+ remoteSupportsFlagged = true;
+ }
+ if (flag == Flag.ANSWERED) {
+ remoteSupportsAnswered = true;
+ }
+ }
+ // Update SEEN/FLAGGED/ANSWERED (star) flags (if supported remotely - e.g. not for POP3)
+ if (remoteSupportsSeen || remoteSupportsFlagged || remoteSupportsAnswered) {
+ for (Message remoteMessage : remoteMessages) {
+ LocalMessageInfo localMessageInfo = localMessageMap.get(remoteMessage.getUid());
+ if (localMessageInfo == null) {
+ continue;
+ }
+ boolean localSeen = localMessageInfo.mFlagRead;
+ boolean remoteSeen = remoteMessage.isSet(Flag.SEEN);
+ boolean newSeen = (remoteSupportsSeen && (remoteSeen != localSeen));
+ boolean localFlagged = localMessageInfo.mFlagFavorite;
+ boolean remoteFlagged = remoteMessage.isSet(Flag.FLAGGED);
+ boolean newFlagged = (remoteSupportsFlagged && (localFlagged != remoteFlagged));
+ int localFlags = localMessageInfo.mFlags;
+ boolean localAnswered = (localFlags & EmailContent.Message.FLAG_REPLIED_TO) != 0;
+ boolean remoteAnswered = remoteMessage.isSet(Flag.ANSWERED);
+ boolean newAnswered = (remoteSupportsAnswered && (localAnswered != remoteAnswered));
+ if (newSeen || newFlagged || newAnswered) {
+ Uri uri = ContentUris.withAppendedId(
+ EmailContent.Message.CONTENT_URI, localMessageInfo.mId);
+ ContentValues updateValues = new ContentValues();
+ updateValues.put(MessageColumns.FLAG_READ, remoteSeen);
+ updateValues.put(MessageColumns.FLAG_FAVORITE, remoteFlagged);
+ if (remoteAnswered) {
+ localFlags |= EmailContent.Message.FLAG_REPLIED_TO;
+ } else {
+ localFlags &= ~EmailContent.Message.FLAG_REPLIED_TO;
+ }
+ updateValues.put(MessageColumns.FLAGS, localFlags);
+ resolver.update(uri, updateValues, null, null);
+ }
+ }
+ }
+
+ // 10. Remove any messages that are in the local store but no longer on the remote store.
+ HashSet<String> localUidsToDelete = new HashSet<String>(localMessageMap.keySet());
+ localUidsToDelete.removeAll(remoteUidMap.keySet());
+ for (String uidToDelete : localUidsToDelete) {
+ LocalMessageInfo infoToDelete = localMessageMap.get(uidToDelete);
+
+ // Delete associated data (attachment files)
+ // Attachment & Body records are auto-deleted when we delete the Message record
+ AttachmentUtilities.deleteAllAttachmentFiles(context, account.mId,
+ infoToDelete.mId);
+
+ // Delete the message itself
+ Uri uriToDelete = ContentUris.withAppendedId(
+ EmailContent.Message.CONTENT_URI, infoToDelete.mId);
+ resolver.delete(uriToDelete, null, null);
+
+ // Delete extra rows (e.g. synced or deleted)
+ Uri syncRowToDelete = ContentUris.withAppendedId(
+ EmailContent.Message.UPDATED_CONTENT_URI, infoToDelete.mId);
+ resolver.delete(syncRowToDelete, null, null);
+ Uri deletERowToDelete = ContentUris.withAppendedId(
+ EmailContent.Message.UPDATED_CONTENT_URI, infoToDelete.mId);
+ resolver.delete(deletERowToDelete, null, null);
+ }
+
+ loadUnsyncedMessages(context, account, remoteFolder, unsyncedMessages, mailbox);
+
+ // 14. Clean up and report results
+ remoteFolder.close(false);
+ }
+
+ /**
+ * Find messages in the updated table that need to be written back to server.
+ *
+ * Handles:
+ * Read/Unread
+ * Flagged
+ * Append (upload)
+ * Move To Trash
+ * Empty trash
+ * TODO:
+ * Move
+ *
+ * @param account the account to scan for pending actions
+ * @throws MessagingException
+ */
+ private static void processPendingActionsSynchronous(Context context, Account account)
+ throws MessagingException {
+ TrafficStats.setThreadStatsTag(TrafficFlags.getSyncFlags(context, account));
+ String[] accountIdArgs = new String[] { Long.toString(account.mId) };
+
+ // Handle deletes first, it's always better to get rid of things first
+ processPendingDeletesSynchronous(context, account, accountIdArgs);
+ }
+
+ /**
+ * Scan for messages that are in the Message_Deletes table, look for differences that
+ * we can deal with, and do the work.
+ *
+ * @param account
+ * @param resolver
+ * @param accountIdArgs
+ */
+ private static void processPendingDeletesSynchronous(Context context, Account account,
+ String[] accountIdArgs) {
+ Cursor deletes = context.getContentResolver().query(
+ EmailContent.Message.DELETED_CONTENT_URI,
+ EmailContent.Message.CONTENT_PROJECTION,
+ EmailContent.MessageColumns.ACCOUNT_KEY + "=?", accountIdArgs,
+ EmailContent.MessageColumns.MAILBOX_KEY);
+ try {
+ // loop through messages marked as deleted
+ while (deletes.moveToNext()) {
+ EmailContent.Message oldMessage =
+ EmailContent.getContent(deletes, EmailContent.Message.class);
+
+ // Finally, delete the update
+ Uri uri = ContentUris.withAppendedId(EmailContent.Message.DELETED_CONTENT_URI,
+ oldMessage.mId);
+ context.getContentResolver().delete(uri, null, null);
+ }
+ } finally {
+ deletes.close();
+ }
+ }
+} \ No newline at end of file
diff --git a/src/com/android/email2/ui/CreateShortcutActivityEmail.java b/src/com/android/email2/ui/CreateShortcutActivityEmail.java
new file mode 100644
index 000000000..0dc08ad47
--- /dev/null
+++ b/src/com/android/email2/ui/CreateShortcutActivityEmail.java
@@ -0,0 +1,50 @@
+/*
+ * 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.email2.ui;
+
+import com.android.mail.providers.Account;
+import com.android.mail.ui.FolderSelectionActivity;
+import com.android.mail.ui.MailboxSelectionActivity;
+import com.android.mail.utils.AccountUtils;
+
+import android.app.Activity;
+import android.content.Intent;
+import android.os.Bundle;
+
+public class CreateShortcutActivityEmail extends Activity {
+
+ @Override
+ public void onCreate(Bundle icicle) {
+ super.onCreate(icicle);
+ Account[] cachedAccounts = AccountUtils.getSyncingAccounts(this);
+ Intent intent = getIntent();
+ if (cachedAccounts != null && cachedAccounts.length == 1) {
+ intent.setClass(this, FolderSelectionActivity.class);
+ intent.setFlags(
+ Intent.FLAG_ACTIVITY_NO_HISTORY | Intent.FLAG_ACTIVITY_FORWARD_RESULT);
+ intent.setAction(Intent.ACTION_CREATE_SHORTCUT);
+ intent.putExtra(FolderSelectionActivity.EXTRA_ACCOUNT_SHORTCUT,
+ cachedAccounts[0]);
+ } else {
+ intent.setClass(this, MailboxSelectionActivity.class);
+ intent.setFlags(Intent.FLAG_ACTIVITY_FORWARD_RESULT);
+ }
+ startActivity(intent);
+ finish();
+ }
+}
diff --git a/src/com/android/email2/ui/MailActivityEmail.java b/src/com/android/email2/ui/MailActivityEmail.java
new file mode 100644
index 000000000..915bf8cd4
--- /dev/null
+++ b/src/com/android/email2/ui/MailActivityEmail.java
@@ -0,0 +1,240 @@
+/*
+ * 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.
+ */
+
+package com.android.email2.ui;
+
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.database.Cursor;
+import android.os.Bundle;
+import android.util.Log;
+
+import com.android.email.NotificationController;
+import com.android.email.Preferences;
+import com.android.email.R;
+import com.android.email.service.AttachmentDownloadService;
+import com.android.email.service.EmailServiceUtils;
+import com.android.email.service.MailService;
+import com.android.emailcommon.Logging;
+import com.android.emailcommon.TempDirectory;
+import com.android.emailcommon.provider.Account;
+import com.android.emailcommon.service.EmailServiceProxy;
+import com.android.emailcommon.utility.EmailAsyncTask;
+import com.android.emailcommon.utility.Utility;
+
+public class MailActivityEmail extends com.android.mail.ui.MailActivity {
+ /**
+ * If this is enabled there will be additional logging information sent to
+ * Log.d, including protocol dumps.
+ *
+ * This should only be used for logs that are useful for debbuging user problems,
+ * not for internal/development logs.
+ *
+ * This can be enabled by typing "debug" in the AccountFolderList activity.
+ * Changing the value to 'true' here will likely have no effect at all!
+ *
+ * TODO: rename this to sUserDebug, and rename LOGD below to DEBUG.
+ */
+ public static boolean DEBUG;
+
+ // Exchange debugging flags (passed to Exchange, when available, via EmailServiceProxy)
+ public static boolean DEBUG_EXCHANGE;
+ public static boolean DEBUG_VERBOSE;
+ public static boolean DEBUG_FILE;
+
+ /**
+ * If true, inhibit hardware graphics acceleration in UI (for a/b testing)
+ */
+ public static boolean sDebugInhibitGraphicsAcceleration = false;
+
+ /**
+ * Specifies how many messages will be shown in a folder by default. This number is set
+ * on each new folder and can be incremented with "Load more messages..." by the
+ * VISIBLE_LIMIT_INCREMENT
+ */
+ public static final int VISIBLE_LIMIT_DEFAULT = 25;
+
+ /**
+ * Number of additional messages to load when a user selects "Load more messages..."
+ */
+ public static final int VISIBLE_LIMIT_INCREMENT = 25;
+
+ /**
+ * This is used to force stacked UI to return to the "welcome" screen any time we change
+ * the accounts list (e.g. deleting accounts in the Account Manager preferences.)
+ */
+ private static boolean sAccountsChangedNotification = false;
+
+ private static String sMessageDecodeErrorString;
+
+ private static Thread sUiThread;
+
+ /**
+ * Asynchronous version of {@link #setServicesEnabledSync(Context)}. Use when calling from
+ * UI thread (or lifecycle entry points.)
+ *
+ * @param context
+ */
+ public static void setServicesEnabledAsync(final Context context) {
+ EmailAsyncTask.runAsyncParallel(new Runnable() {
+ @Override
+ public void run() {
+ setServicesEnabledSync(context);
+ }
+ });
+ }
+
+ /**
+ * Called throughout the application when the number of accounts has changed. This method
+ * enables or disables the Compose activity, the boot receiver and the service based on
+ * whether any accounts are configured.
+ *
+ * Blocking call - do not call from UI/lifecycle threads.
+ *
+ * @param context
+ * @return true if there are any accounts configured.
+ */
+ public static boolean setServicesEnabledSync(Context context) {
+ Cursor c = null;
+ try {
+ c = context.getContentResolver().query(
+ Account.CONTENT_URI,
+ Account.ID_PROJECTION,
+ null, null, null);
+ boolean enable = c.getCount() > 0;
+ setServicesEnabled(context, enable);
+ return enable;
+ } finally {
+ if (c != null) {
+ c.close();
+ }
+ }
+ }
+
+ private static void setServicesEnabled(Context context, boolean enabled) {
+ PackageManager pm = context.getPackageManager();
+ pm.setComponentEnabledSetting(
+ new ComponentName(context, MailService.class),
+ enabled ? PackageManager.COMPONENT_ENABLED_STATE_ENABLED :
+ PackageManager.COMPONENT_ENABLED_STATE_DISABLED,
+ PackageManager.DONT_KILL_APP);
+ pm.setComponentEnabledSetting(
+ new ComponentName(context, AttachmentDownloadService.class),
+ enabled ? PackageManager.COMPONENT_ENABLED_STATE_ENABLED :
+ PackageManager.COMPONENT_ENABLED_STATE_DISABLED,
+ PackageManager.DONT_KILL_APP);
+
+ // Start/stop the various services depending on whether there are any accounts
+ startOrStopService(enabled, context, new Intent(context, AttachmentDownloadService.class));
+ NotificationController.getInstance(context).watchForMessages(enabled);
+ }
+
+ /**
+ * Starts or stops the service as necessary.
+ * @param enabled If {@code true}, the service will be started. Otherwise, it will be stopped.
+ * @param context The context to manage the service with.
+ * @param intent The intent of the service to be managed.
+ */
+ private static void startOrStopService(boolean enabled, Context context, Intent intent) {
+ if (enabled) {
+ context.startService(intent);
+ } else {
+ context.stopService(intent);
+ }
+ }
+
+ @Override
+ public void onCreate(Bundle bundle) {
+ super.onCreate(bundle);
+ sUiThread = Thread.currentThread();
+ Preferences prefs = Preferences.getPreferences(this);
+ DEBUG = prefs.getEnableDebugLogging();
+ sDebugInhibitGraphicsAcceleration = prefs.getInhibitGraphicsAcceleration();
+ enableStrictMode(prefs.getEnableStrictMode());
+ TempDirectory.setTempDirectory(this);
+
+ // Enable logging in the EAS service, so it starts up as early as possible.
+ updateLoggingFlags(this);
+
+ // Get a helper string used deep inside message decoders (which don't have context)
+ sMessageDecodeErrorString = getString(R.string.message_decode_error);
+
+ // Make sure all required services are running when the app is started (can prevent
+ // issues after an adb sync/install)
+ setServicesEnabledAsync(this);
+ }
+
+ /**
+ * Load enabled debug flags from the preferences and update the EAS debug flag.
+ */
+ public static void updateLoggingFlags(Context context) {
+ Preferences prefs = Preferences.getPreferences(context);
+ int debugLogging = prefs.getEnableDebugLogging() ? EmailServiceProxy.DEBUG_BIT : 0;
+ int verboseLogging =
+ prefs.getEnableExchangeLogging() ? EmailServiceProxy.DEBUG_VERBOSE_BIT : 0;
+ int fileLogging =
+ prefs.getEnableExchangeFileLogging() ? EmailServiceProxy.DEBUG_FILE_BIT : 0;
+ int enableStrictMode =
+ prefs.getEnableStrictMode() ? EmailServiceProxy.DEBUG_ENABLE_STRICT_MODE : 0;
+ int debugBits = debugLogging | verboseLogging | fileLogging | enableStrictMode;
+ EmailServiceUtils.setRemoteServicesLogging(context, debugBits);
+ }
+
+ /**
+ * Internal, utility method for logging.
+ * The calls to log() must be guarded with "if (Email.LOGD)" for performance reasons.
+ */
+ public static void log(String message) {
+ Log.d(Logging.LOG_TAG, message);
+ }
+
+ /**
+ * Called by the accounts reconciler to notify that accounts have changed, or by "Welcome"
+ * to clear the flag.
+ * @param setFlag true to set the notification flag, false to clear it
+ */
+ public static synchronized void setNotifyUiAccountsChanged(boolean setFlag) {
+ sAccountsChangedNotification = setFlag;
+ }
+
+ /**
+ * Called from activity onResume() functions to check for an accounts-changed condition, at
+ * which point they should finish() and jump to the Welcome activity.
+ */
+ public static synchronized boolean getNotifyUiAccountsChanged() {
+ return sAccountsChangedNotification;
+ }
+
+ public static void warnIfUiThread() {
+ if (Thread.currentThread().equals(sUiThread)) {
+ Log.w(Logging.LOG_TAG, "Method called on the UI thread", new Exception("STACK TRACE"));
+ }
+ }
+
+ /**
+ * Retrieve a simple string that can be used when message decoders encounter bad data.
+ * This is provided here because the protocol decoders typically don't have mContext.
+ */
+ public static String getMessageDecodeErrorString() {
+ return sMessageDecodeErrorString != null ? sMessageDecodeErrorString : "";
+ }
+
+ public static void enableStrictMode(boolean enabled) {
+ Utility.enableStrictMode(enabled);
+ }
+}
diff --git a/src/com/android/email2/ui/MailboxSelectionActivityEmail.java b/src/com/android/email2/ui/MailboxSelectionActivityEmail.java
new file mode 100644
index 000000000..6831ef193
--- /dev/null
+++ b/src/com/android/email2/ui/MailboxSelectionActivityEmail.java
@@ -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.email2.ui;
+
+ /* An activity that shows the list of all the available accounts and return the
+ * one selected in onResult().
+ */
+public class MailboxSelectionActivityEmail extends com.android.mail.ui.MailboxSelectionActivity {
+
+} \ No newline at end of file
diff --git a/src/com/android/mail/browse/EmailConversationProvider.java b/src/com/android/mail/browse/EmailConversationProvider.java
new file mode 100644
index 000000000..ebff3137c
--- /dev/null
+++ b/src/com/android/mail/browse/EmailConversationProvider.java
@@ -0,0 +1,32 @@
+/*
+ * 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.mail.browse;
+
+import com.android.mail.browse.ConversationCursor.ConversationProvider;
+
+import java.lang.Override;
+
+public class EmailConversationProvider extends ConversationProvider {
+ // The authority of our conversation provider (a forwarding provider)
+ // This string must match the declaration in AndroidManifest.xml
+ private static final String sAuthority = "com.android.email2.conversation.provider";
+
+ @Override
+ protected String getAuthority() {
+ return sAuthority;
+ }
+} \ No newline at end of file
diff --git a/src/com/android/mail/providers/EmailAccountCacheProvider.java b/src/com/android/mail/providers/EmailAccountCacheProvider.java
new file mode 100644
index 000000000..a311e36cb
--- /dev/null
+++ b/src/com/android/mail/providers/EmailAccountCacheProvider.java
@@ -0,0 +1,52 @@
+/*
+ * 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.mail.providers;
+
+import android.content.Context;
+import android.content.Intent;
+import android.net.Uri;
+
+import com.android.email.activity.setup.AccountSettings;
+
+public class EmailAccountCacheProvider extends MailAppProvider {
+ // Content provider for Email
+ private static final String sAuthority = "com.android.email2.accountcache";
+ /**
+ * Authority for the suggestions provider. This is specified in AndroidManifest.xml and
+ * res/xml/searchable.xml.
+ */
+ private static final String sSuggestionsAuthority = "com.android.email.suggestionsprovider";
+
+ @Override
+ protected String getAuthority() {
+ return sAuthority;
+ }
+
+ @Override
+ protected Intent getNoAccountsIntent(Context context) {
+ Intent intent = new Intent();
+ intent.setAction(Intent.ACTION_EDIT);
+ intent.setData(Uri.parse("content://ui.email.android.com/settings"));
+ intent.putExtra(AccountSettings.EXTRA_NO_ACCOUNTS, true);
+ return intent;
+ }
+
+ @Override
+ public String getSuggestionAuthority() {
+ return sSuggestionsAuthority;
+ }
+}
diff --git a/src/com/android/mail/providers/protos/boot/AccountReceiver.java b/src/com/android/mail/providers/protos/boot/AccountReceiver.java
new file mode 100644
index 000000000..16eed27d3
--- /dev/null
+++ b/src/com/android/mail/providers/protos/boot/AccountReceiver.java
@@ -0,0 +1,41 @@
+/*
+ * 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.mail.providers.protos.boot;
+
+import com.android.mail.providers.MailAppProvider;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.net.Uri;
+
+public class AccountReceiver extends BroadcastReceiver {
+ /**
+ * Intent used to notify interested parties that the Mail provider has been created.
+ */
+ public static final String ACTION_PROVIDER_CREATED
+ = "com.android.email2.providers.protos.boot.intent.ACTION_PROVIDER_CREATED";
+
+ private static final Uri ACCOUNTS_URI =
+ Uri.parse("content://com.android.email.provider/uiaccts");
+
+
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ MailAppProvider.addAccountsForUriAsync(ACCOUNTS_URI);
+ }
+}
diff --git a/src/com/android/mail/utils/LogTag.java b/src/com/android/mail/utils/LogTag.java
new file mode 100644
index 000000000..b0c39879f
--- /dev/null
+++ b/src/com/android/mail/utils/LogTag.java
@@ -0,0 +1,28 @@
+/**
+ * Copyright (c) 2012, Google Inc.
+ *
+ * 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.mail.utils;
+
+public class LogTag {
+ private static String LOG_TAG = "Email";
+
+ /**
+ * Get the log tag to apply to logging.
+ */
+ public static String getLogTag() {
+ return LOG_TAG;
+ }
+}
diff --git a/tests/oldAndroid.mk b/tests/oldAndroid.mk
new file mode 100644
index 000000000..29f51720c
--- /dev/null
+++ b/tests/oldAndroid.mk
@@ -0,0 +1,34 @@
+# 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)
+
+# We only want this apk build for tests.
+LOCAL_MODULE_TAGS := tests
+
+LOCAL_JAVA_LIBRARIES := android.test.runner
+
+# Include all test java files.
+LOCAL_SRC_FILES := $(call all-java-files-under, src)
+
+# Notice that we don't have to include the src files of Email because, by
+# running the tests using an instrumentation targeting Email, we
+# automatically get all of its classes loaded into our environment.
+
+LOCAL_PACKAGE_NAME := EmailTests
+
+LOCAL_INSTRUMENTATION_FOR := Email
+
+include $(BUILD_PACKAGE)