diff options
| author | Marc Blank <mblank@google.com> | 2012-06-28 12:16:59 -0700 |
|---|---|---|
| committer | Marc Blank <mblank@google.com> | 2012-06-28 12:16:59 -0700 |
| commit | c5afb16430a145f20d7c887e45f47b38687054da (patch) | |
| tree | 131ec20d0b66d2bdd6eec9eb1eafd2f0faa61ff1 | |
| parent | f419287f22ae44f25e1ba1f757ec33c7941bbfa8 (diff) | |
| download | android_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
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 Binary files differnew file mode 100644 index 000000000..2616df73a --- /dev/null +++ b/res/mipmap-hdpi/ic_launcher_mail.png diff --git a/res/mipmap-mdpi/ic_launcher_mail.png b/res/mipmap-mdpi/ic_launcher_mail.png Binary files differnew file mode 100644 index 000000000..545902704 --- /dev/null +++ b/res/mipmap-mdpi/ic_launcher_mail.png diff --git a/res/mipmap-xhdpi/ic_launcher_mail.png b/res/mipmap-xhdpi/ic_launcher_mail.png Binary files differnew file mode 100644 index 000000000..91c768e45 --- /dev/null +++ b/res/mipmap-xhdpi/ic_launcher_mail.png 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) |
