From bb68c13afa630cae058eb40d3ce68644f3f3c8b9 Mon Sep 17 00:00:00 2001 From: Paul Westbrook Date: Sun, 7 Sep 2014 13:36:33 -0700 Subject: Changes to support smaller email tombstone apk size This reduces the tombstone down by 100K A follow-on cl will remove the unused resources from the tombstone build Bug: 17414014 Change-Id: I5d38811b17a5273ec726e750ab123e10e36cee04 --- src/com/android/email/AttachmentInfo.java | 249 - src/com/android/email/DebugUtils.java | 48 - src/com/android/email/EmailApplication.java | 8 + .../android/email/EmailConnectivityManager.java | 217 - src/com/android/email/EmailIntentService.java | 45 - .../android/email/EmailNotificationController.java | 855 +++ src/com/android/email/FixedLengthInputStream.java | 80 - src/com/android/email/LegacyConversions.java | 547 -- src/com/android/email/NotificationController.java | 842 --- src/com/android/email/PeekableInputStream.java | 80 - src/com/android/email/Preferences.java | 280 - src/com/android/email/ResourceHelper.java | 87 - src/com/android/email/SecurityPolicy.java | 904 --- .../activity/setup/AccountCreationFragment.java | 3 +- .../email/activity/setup/AccountSecurity.java | 628 -- .../setup/AccountServerSettingsActivity.java | 3 +- .../activity/setup/AccountSettingsFragment.java | 3 +- .../email/activity/setup/AccountSettingsUtils.java | 433 -- .../email/activity/setup/AccountSetupFinal.java | 40 +- .../activity/setup/AccountSetupNamesFragment.java | 9 +- .../activity/setup/EmailPreferenceActivity.java | 3 +- .../email/activity/setup/ForwardingIntent.java | 30 - .../setup/HeadlessAccountSettingsLoader.java | 7 - .../email/activity/setup/SetupDataFragment.java | 11 +- src/com/android/email/mail/Sender.java | 125 - src/com/android/email/mail/Store.java | 226 - .../email/mail/internet/AuthenticationCache.java | 162 - .../email/mail/internet/OAuthAuthenticator.java | 191 - .../android/email/mail/store/ImapConnection.java | 636 -- src/com/android/email/mail/store/ImapFolder.java | 1291 ---- src/com/android/email/mail/store/ImapStore.java | 657 --- src/com/android/email/mail/store/Pop3Store.java | 833 --- src/com/android/email/mail/store/ServiceStore.java | 89 - .../email/mail/store/imap/ImapConstants.java | 104 - .../android/email/mail/store/imap/ImapElement.java | 120 - .../android/email/mail/store/imap/ImapList.java | 235 - .../email/mail/store/imap/ImapMemoryLiteral.java | 71 - .../email/mail/store/imap/ImapResponse.java | 152 - .../email/mail/store/imap/ImapResponseParser.java | 453 -- .../email/mail/store/imap/ImapSimpleString.java | 55 - .../android/email/mail/store/imap/ImapString.java | 186 - .../email/mail/store/imap/ImapTempFileLiteral.java | 123 - .../android/email/mail/store/imap/ImapUtility.java | 126 - .../email/mail/transport/DiscourseLogger.java | 119 - .../email/mail/transport/MailTransport.java | 320 - .../email/provider/AccountBackupRestore.java | 51 - .../android/email/provider/AccountReconciler.java | 306 - .../android/email/provider/AttachmentProvider.java | 338 -- src/com/android/email/provider/ContentCache.java | 822 --- src/com/android/email/provider/DBHelper.java | 1896 ------ .../email/provider/EmailConversationCursor.java | 245 - .../android/email/provider/EmailMessageCursor.java | 124 - src/com/android/email/provider/EmailProvider.java | 6188 -------------------- .../email/provider/FolderPickerActivity.java | 218 - .../email/provider/FolderPickerCallback.java | 25 - .../android/email/provider/FolderPickerDialog.java | 158 - .../provider/FolderPickerSelectorAdapter.java | 46 - .../email/provider/RefreshStatusMonitor.java | 159 - src/com/android/email/provider/Utilities.java | 234 - src/com/android/email/provider/WidgetProvider.java | 191 - src/com/android/email/service/AccountService.java | 85 - .../android/email/service/AttachmentService.java | 1401 ----- .../email/service/AuthenticatorService.java | 165 - .../email/service/EasAuthenticatorService.java | 23 - .../service/EasAuthenticatorServiceAlternate.java | 23 - .../email/service/EasTestAuthenticatorService.java | 123 - .../service/EmailBroadcastProcessorService.java | 371 -- .../email/service/EmailBroadcastReceiver.java | 31 - .../android/email/service/EmailServiceStub.java | 523 -- .../android/email/service/EmailServiceUtils.java | 780 --- .../service/EmailUpgradeBroadcastReceiver.java | 17 - .../email/service/ImapAuthenticatorService.java | 23 - src/com/android/email/service/ImapService.java | 1615 ----- .../android/email/service/ImapTempFileLiteral.java | 126 - .../service/LegacyEasAuthenticatorService.java | 23 - .../service/LegacyEmailAuthenticatorService.java | 23 - .../service/LegacyImapAuthenticatorService.java | 23 - .../service/LegacyImapSyncAdapterService.java | 20 - src/com/android/email/service/PolicyService.java | 94 - .../email/service/Pop3AuthenticatorService.java | 23 - src/com/android/email/service/Pop3Service.java | 475 -- .../email/service/Pop3SyncAdapterService.java | 20 - .../email/service/PopImapSyncAdapterService.java | 261 - src/com/android/email2/ui/MailActivityEmail.java | 81 +- .../mail/providers/EmailAccountCacheProvider.java | 3 +- 85 files changed, 891 insertions(+), 28148 deletions(-) delete mode 100644 src/com/android/email/AttachmentInfo.java delete mode 100644 src/com/android/email/DebugUtils.java delete mode 100644 src/com/android/email/EmailConnectivityManager.java delete mode 100644 src/com/android/email/EmailIntentService.java create mode 100644 src/com/android/email/EmailNotificationController.java delete mode 100644 src/com/android/email/FixedLengthInputStream.java delete mode 100644 src/com/android/email/LegacyConversions.java delete mode 100644 src/com/android/email/NotificationController.java delete mode 100644 src/com/android/email/PeekableInputStream.java delete mode 100644 src/com/android/email/Preferences.java delete mode 100644 src/com/android/email/ResourceHelper.java delete mode 100644 src/com/android/email/SecurityPolicy.java delete mode 100644 src/com/android/email/activity/setup/AccountSecurity.java delete mode 100644 src/com/android/email/activity/setup/AccountSettingsUtils.java delete mode 100644 src/com/android/email/activity/setup/ForwardingIntent.java delete mode 100644 src/com/android/email/mail/Sender.java delete mode 100644 src/com/android/email/mail/Store.java delete mode 100644 src/com/android/email/mail/internet/AuthenticationCache.java delete mode 100644 src/com/android/email/mail/internet/OAuthAuthenticator.java delete mode 100644 src/com/android/email/mail/store/ImapConnection.java delete mode 100644 src/com/android/email/mail/store/ImapFolder.java delete mode 100644 src/com/android/email/mail/store/ImapStore.java delete mode 100644 src/com/android/email/mail/store/Pop3Store.java delete mode 100644 src/com/android/email/mail/store/ServiceStore.java delete mode 100644 src/com/android/email/mail/store/imap/ImapConstants.java delete mode 100644 src/com/android/email/mail/store/imap/ImapElement.java delete mode 100644 src/com/android/email/mail/store/imap/ImapList.java delete mode 100644 src/com/android/email/mail/store/imap/ImapMemoryLiteral.java delete mode 100644 src/com/android/email/mail/store/imap/ImapResponse.java delete mode 100644 src/com/android/email/mail/store/imap/ImapResponseParser.java delete mode 100644 src/com/android/email/mail/store/imap/ImapSimpleString.java delete mode 100644 src/com/android/email/mail/store/imap/ImapString.java delete mode 100644 src/com/android/email/mail/store/imap/ImapTempFileLiteral.java delete mode 100644 src/com/android/email/mail/store/imap/ImapUtility.java delete mode 100644 src/com/android/email/mail/transport/DiscourseLogger.java delete mode 100644 src/com/android/email/mail/transport/MailTransport.java delete mode 100644 src/com/android/email/provider/AccountBackupRestore.java delete mode 100644 src/com/android/email/provider/AccountReconciler.java delete mode 100644 src/com/android/email/provider/AttachmentProvider.java delete mode 100644 src/com/android/email/provider/ContentCache.java delete mode 100644 src/com/android/email/provider/DBHelper.java delete mode 100644 src/com/android/email/provider/EmailConversationCursor.java delete mode 100644 src/com/android/email/provider/EmailMessageCursor.java delete mode 100644 src/com/android/email/provider/EmailProvider.java delete mode 100644 src/com/android/email/provider/FolderPickerActivity.java delete mode 100644 src/com/android/email/provider/FolderPickerCallback.java delete mode 100644 src/com/android/email/provider/FolderPickerDialog.java delete mode 100644 src/com/android/email/provider/FolderPickerSelectorAdapter.java delete mode 100644 src/com/android/email/provider/RefreshStatusMonitor.java delete mode 100644 src/com/android/email/provider/Utilities.java delete mode 100644 src/com/android/email/provider/WidgetProvider.java delete mode 100644 src/com/android/email/service/AccountService.java delete mode 100644 src/com/android/email/service/AttachmentService.java delete mode 100644 src/com/android/email/service/AuthenticatorService.java delete mode 100644 src/com/android/email/service/EasAuthenticatorService.java delete mode 100644 src/com/android/email/service/EasAuthenticatorServiceAlternate.java delete mode 100644 src/com/android/email/service/EasTestAuthenticatorService.java delete mode 100644 src/com/android/email/service/EmailBroadcastProcessorService.java delete mode 100644 src/com/android/email/service/EmailBroadcastReceiver.java delete mode 100644 src/com/android/email/service/EmailServiceStub.java delete mode 100644 src/com/android/email/service/EmailServiceUtils.java delete mode 100644 src/com/android/email/service/EmailUpgradeBroadcastReceiver.java delete mode 100644 src/com/android/email/service/ImapAuthenticatorService.java delete mode 100644 src/com/android/email/service/ImapService.java delete mode 100644 src/com/android/email/service/ImapTempFileLiteral.java delete mode 100644 src/com/android/email/service/LegacyEasAuthenticatorService.java delete mode 100644 src/com/android/email/service/LegacyEmailAuthenticatorService.java delete mode 100644 src/com/android/email/service/LegacyImapAuthenticatorService.java delete mode 100644 src/com/android/email/service/LegacyImapSyncAdapterService.java delete mode 100644 src/com/android/email/service/PolicyService.java delete mode 100644 src/com/android/email/service/Pop3AuthenticatorService.java delete mode 100644 src/com/android/email/service/Pop3Service.java delete mode 100644 src/com/android/email/service/Pop3SyncAdapterService.java delete mode 100644 src/com/android/email/service/PopImapSyncAdapterService.java (limited to 'src') diff --git a/src/com/android/email/AttachmentInfo.java b/src/com/android/email/AttachmentInfo.java deleted file mode 100644 index 13ff068f7..000000000 --- a/src/com/android/email/AttachmentInfo.java +++ /dev/null @@ -1,249 +0,0 @@ -/* - * 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; - -import com.android.emailcommon.internet.MimeUtility; -import com.android.emailcommon.provider.EmailContent.Attachment; -import com.android.emailcommon.provider.EmailContent.AttachmentColumns; -import com.android.emailcommon.utility.AttachmentUtilities; -import com.android.emailcommon.utility.Utility; - -import android.content.Context; -import android.content.Intent; -import android.content.pm.PackageManager; -import android.content.pm.ResolveInfo; -import android.database.Cursor; -import android.net.ConnectivityManager; -import android.net.Uri; -import android.provider.Settings; -import android.text.TextUtils; - -import java.util.List; - -/** - * Encapsulates commonly used attachment information related to suitability for viewing and saving, - * based on the attachment's filename and mimetype. - */ -public class AttachmentInfo { - // Projection which can be used with the constructor taking a Cursor argument - public static final String[] PROJECTION = { - AttachmentColumns._ID, - AttachmentColumns.SIZE, - AttachmentColumns.FILENAME, - AttachmentColumns.MIME_TYPE, - AttachmentColumns.ACCOUNT_KEY, - AttachmentColumns.FLAGS - }; - // Offsets into PROJECTION - public static final int COLUMN_ID = 0; - public static final int COLUMN_SIZE = 1; - public static final int COLUMN_FILENAME = 2; - public static final int COLUMN_MIME_TYPE = 3; - public static final int COLUMN_ACCOUNT_KEY = 4; - public static final int COLUMN_FLAGS = 5; - - /** Attachment not denied */ - public static final int ALLOW = 0x00; - /** Attachment suspected of being malware */ - public static final int DENY_MALWARE = 0x01; - /** Attachment too large; must download over wi-fi */ - public static final int DENY_WIFIONLY = 0x02; - /** No receiving intent to handle attachment type */ - public static final int DENY_NOINTENT = 0x04; - /** Side load of applications is disabled */ - public static final int DENY_NOSIDELOAD = 0x08; - // TODO Remove DENY_APKINSTALL when we can install directly from the Email activity - /** Unable to install any APK */ - public static final int DENY_APKINSTALL = 0x10; - /** Security policy prohibits install */ - public static final int DENY_POLICY = 0x20; - - public final long mId; - public final long mSize; - public final String mName; - public final String mContentType; - public final long mAccountKey; - public final int mFlags; - - /** Whether or not this attachment can be viewed */ - public final boolean mAllowView; - /** Whether or not this attachment can be saved */ - public final boolean mAllowSave; - /** Whether or not this attachment can be installed [only true for APKs] */ - public final boolean mAllowInstall; - /** Reason(s) why this attachment is denied from being viewed */ - public final int mDenyFlags; - - public AttachmentInfo(Context context, Attachment attachment) { - this(context, attachment.mId, attachment.mSize, attachment.mFileName, attachment.mMimeType, - attachment.mAccountKey, attachment.mFlags); - } - - public AttachmentInfo(Context context, Cursor c) { - this(context, c.getLong(COLUMN_ID), c.getLong(COLUMN_SIZE), c.getString(COLUMN_FILENAME), - c.getString(COLUMN_MIME_TYPE), c.getLong(COLUMN_ACCOUNT_KEY), - c.getInt(COLUMN_FLAGS)); - } - - public AttachmentInfo(Context context, AttachmentInfo info) { - this(context, info.mId, info.mSize, info.mName, info.mContentType, info.mAccountKey, - info.mFlags); - } - - public AttachmentInfo(Context context, long id, long size, String fileName, String mimeType, - long accountKey, int flags) { - mSize = size; - mContentType = AttachmentUtilities.inferMimeType(fileName, mimeType); - mName = fileName; - mId = id; - mAccountKey = accountKey; - mFlags = flags; - boolean canView = true; - boolean canSave = true; - boolean canInstall = false; - int denyFlags = ALLOW; - - // Don't enable the "save" button if we've got no place to save the file - if (!Utility.isExternalStorageMounted()) { - canSave = false; - } - - // Check for acceptable / unacceptable attachments by MIME-type - if ((!MimeUtility.mimeTypeMatches(mContentType, - AttachmentUtilities.ACCEPTABLE_ATTACHMENT_VIEW_TYPES)) || - (MimeUtility.mimeTypeMatches(mContentType, - AttachmentUtilities.UNACCEPTABLE_ATTACHMENT_VIEW_TYPES))) { - canView = false; - } - - // Check for unacceptable attachments by filename extension - String extension = AttachmentUtilities.getFilenameExtension(mName); - if (!TextUtils.isEmpty(extension) && - Utility.arrayContains(AttachmentUtilities.UNACCEPTABLE_ATTACHMENT_EXTENSIONS, - extension)) { - canView = false; - canSave = false; - denyFlags |= DENY_MALWARE; - } - - // Check for policy restrictions on download - if ((flags & Attachment.FLAG_POLICY_DISALLOWS_DOWNLOAD) != 0) { - canView = false; - canSave = false; - denyFlags |= DENY_POLICY; - } - - // Check for installable attachments by filename extension - extension = AttachmentUtilities.getFilenameExtension(mName); - if (!TextUtils.isEmpty(extension) && - Utility.arrayContains(AttachmentUtilities.INSTALLABLE_ATTACHMENT_EXTENSIONS, - extension)) { - boolean sideloadEnabled; - sideloadEnabled = Settings.Secure.getInt(context.getContentResolver(), - Settings.Secure.INSTALL_NON_MARKET_APPS, 0 /* sideload disabled */) == 1; - canSave &= sideloadEnabled; - canView = canSave; - canInstall = canSave; - if (!sideloadEnabled) { - denyFlags |= DENY_NOSIDELOAD; - } - } - - // Check for file size exceeded - // The size limit is overridden when on a wifi connection - any size is OK - if (mSize > AttachmentUtilities.MAX_ATTACHMENT_DOWNLOAD_SIZE) { - int networkType = EmailConnectivityManager.getActiveNetworkType(context); - if (networkType != ConnectivityManager.TYPE_WIFI) { - canView = false; - canSave = false; - denyFlags |= DENY_WIFIONLY; - } - } - - // Check to see if any activities can view this attachment; if none, we can't view it - Intent intent = getAttachmentIntent(context, 0); - PackageManager pm = context.getPackageManager(); - List activityList = pm.queryIntentActivities(intent, 0 /*no account*/); - if (activityList.isEmpty()) { - canView = false; - canSave = false; - denyFlags |= DENY_NOINTENT; - } - - mAllowView = canView; - mAllowSave = canSave; - mAllowInstall = canInstall; - mDenyFlags = denyFlags; - } - - /** - * Returns an Intent to load the given attachment. - * @param context the caller's context - * @param accountId the account associated with the attachment (or 0 if we don't need to - * resolve from attachmentUri to contentUri) - * @return an Intent suitable for viewing the attachment - */ - public Intent getAttachmentIntent(Context context, long accountId) { - Uri contentUri = getUriForIntent(context, accountId); - Intent intent = new Intent(Intent.ACTION_VIEW); - intent.setDataAndType(contentUri, mContentType); - intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION - | Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET); - return intent; - } - - protected Uri getUriForIntent(Context context, long accountId) { - Uri contentUri = AttachmentUtilities.getAttachmentUri(accountId, mId); - if (accountId > 0) { - contentUri = AttachmentUtilities.resolveAttachmentIdToContentUri( - context.getContentResolver(), contentUri); - } - - return contentUri; - } - - /** - * An attachment is eligible for download if it can either be viewed or saved (or both) - * @return whether the attachment is eligible for download - */ - public boolean isEligibleForDownload() { - return mAllowView || mAllowSave; - } - - @Override - public int hashCode() { - return (int) (mId ^ (mId >>> 32)); - } - - @Override - public boolean equals(Object o) { - if (o == this) { - return true; - } - - if ((o == null) || (o.getClass() != getClass())) { - return false; - } - - return ((AttachmentInfo) o).mId == mId; - } - - @Override - public String toString() { - return "{Attachment " + mId + ":" + mName + "," + mContentType + "," + mSize + "}"; - } -} diff --git a/src/com/android/email/DebugUtils.java b/src/com/android/email/DebugUtils.java deleted file mode 100644 index 94ce78e75..000000000 --- a/src/com/android/email/DebugUtils.java +++ /dev/null @@ -1,48 +0,0 @@ -package com.android.email; - -import android.content.Context; - -import com.android.email.service.EmailServiceUtils; -import com.android.emailcommon.service.EmailServiceProxy; -import com.android.emailcommon.utility.Utility; -import com.android.mail.utils.LogTag; - -public class DebugUtils { - public static final String LOG_TAG = LogTag.getLogTag(); - - public static boolean DEBUG; - public static boolean DEBUG_EXCHANGE; - public static boolean DEBUG_FILE; - - public static void init(final Context context) { - final Preferences prefs = Preferences.getPreferences(context); - DEBUG = prefs.getEnableDebugLogging(); - DEBUG_EXCHANGE = prefs.getEnableExchangeLogging(); - DEBUG_FILE = prefs.getEnableExchangeFileLogging(); - - // Enable logging in the EAS service, so it starts up as early as possible. - updateLoggingFlags(context); - enableStrictMode(prefs.getEnableStrictMode()); - } - - /** - * 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 exchangeLogging = - prefs.getEnableExchangeLogging() ? EmailServiceProxy.DEBUG_EXCHANGE_BIT: 0; - int fileLogging = - prefs.getEnableExchangeFileLogging() ? EmailServiceProxy.DEBUG_FILE_BIT : 0; - int enableStrictMode = - prefs.getEnableStrictMode() ? EmailServiceProxy.DEBUG_ENABLE_STRICT_MODE : 0; - int debugBits = debugLogging | exchangeLogging | fileLogging | enableStrictMode; - EmailServiceUtils.setRemoteServicesLogging(context, debugBits); - } - - public static void enableStrictMode(final boolean enable) { - Utility.enableStrictMode(enable); - } - -} diff --git a/src/com/android/email/EmailApplication.java b/src/com/android/email/EmailApplication.java index 224ac490c..6e7586435 100644 --- a/src/com/android/email/EmailApplication.java +++ b/src/com/android/email/EmailApplication.java @@ -62,5 +62,13 @@ public class EmailApplication extends Application { }); PublicPreferenceActivity.sPreferenceActivityClass = EmailPreferenceActivity.class; + + NotificationControllerCreatorHolder.setNotificationControllerCreator( + new NotificationControllerCreator() { + @Override + public NotificationController getInstance(Context context){ + return EmailNotificationController.getInstance(context); + } + }); } } diff --git a/src/com/android/email/EmailConnectivityManager.java b/src/com/android/email/EmailConnectivityManager.java deleted file mode 100644 index 90a511f06..000000000 --- a/src/com/android/email/EmailConnectivityManager.java +++ /dev/null @@ -1,217 +0,0 @@ -/* - * 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; - -import android.content.BroadcastReceiver; -import android.content.ContentResolver; -import android.content.Context; -import android.content.Intent; -import android.content.IntentFilter; -import android.net.ConnectivityManager; -import android.net.NetworkInfo; -import android.net.NetworkInfo.State; -import android.os.Bundle; -import android.os.PowerManager; -import android.os.PowerManager.WakeLock; - -import com.android.mail.utils.LogUtils; - -/** - * Encapsulates functionality of ConnectivityManager for use in the Email application. In - * particular, this class provides callbacks for connectivity lost, connectivity restored, and - * background setting changed, as well as providing a method that waits for connectivity - * to be available without holding a wake lock - * - * To use, EmailConnectivityManager mgr = new EmailConnectivityManager(context, "Name"); - * When done, mgr.unregister() to unregister the internal receiver - * - * TODO: Use this class in ExchangeService - */ -public class EmailConnectivityManager extends BroadcastReceiver { - private static final String TAG = "EmailConnectivityMgr"; - - // Loop time while waiting (stopgap in case we don't get a broadcast) - private static final int CONNECTIVITY_WAIT_TIME = 10*60*1000; - - // Sentinel value for "no active network" - public static final int NO_ACTIVE_NETWORK = -1; - - // The name of this manager (used for logging) - private final String mName; - // The monitor lock we use while waiting for connectivity - private final Object mLock = new Object(); - // The instantiator's context - private final Context mContext; - // The wake lock used while running (so we don't fall asleep during execution/callbacks) - private final WakeLock mWakeLock; - private final android.net.ConnectivityManager mConnectivityManager; - - // Set when we abort waitForConnectivity() via stopWait - private boolean mStop = false; - // The thread waiting for connectivity - private Thread mWaitThread; - // Whether or not we're registered with the system connectivity manager - private boolean mRegistered = true; - - public EmailConnectivityManager(Context context, String name) { - mContext = context; - mName = name; - mConnectivityManager = - (ConnectivityManager)context.getSystemService(Context.CONNECTIVITY_SERVICE); - PowerManager pm = (PowerManager)context.getSystemService(Context.POWER_SERVICE); - mWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, name); - mContext.registerReceiver(this, new IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION)); - } - - public boolean isAutoSyncAllowed() { - return ContentResolver.getMasterSyncAutomatically(); - } - - public void stopWait() { - mStop = true; - Thread thread= mWaitThread; - if (thread != null) { - thread.interrupt(); - } - } - - /** - * Called when network connectivity has been restored; this method should be overridden by - * subclasses as necessary. NOTE: CALLED ON UI THREAD - * @param networkType as defined by ConnectivityManager - */ - public void onConnectivityRestored(int networkType) { - } - - /** - * Called when network connectivity has been lost; this method should be overridden by - * subclasses as necessary. NOTE: CALLED ON UI THREAD - * @param networkType as defined by ConnectivityManager - */ - public void onConnectivityLost(int networkType) { - } - - public void unregister() { - try { - mContext.unregisterReceiver(this); - } catch (RuntimeException e) { - // Don't crash if we didn't register - } finally { - mRegistered = false; - } - } - - @Override - public void onReceive(Context context, Intent intent) { - if (intent.getAction().equals(ConnectivityManager.CONNECTIVITY_ACTION)) { - Bundle extras = intent.getExtras(); - if (extras != null) { - NetworkInfo networkInfo = - (NetworkInfo)extras.get(ConnectivityManager.EXTRA_NETWORK_INFO); - if (networkInfo == null) return; - State state = networkInfo.getState(); - if (state == State.CONNECTED) { - synchronized (mLock) { - mLock.notifyAll(); - } - onConnectivityRestored(networkInfo.getType()); - } else if (state == State.DISCONNECTED) { - onConnectivityLost(networkInfo.getType()); - } - } - } - } - - /** - * Request current connectivity status - * @return whether there is connectivity at this time - */ - public boolean hasConnectivity() { - NetworkInfo info = mConnectivityManager.getActiveNetworkInfo(); - return (info != null); - } - - /** - * Get the type of the currently active data network - * @return the type of the active network (or NO_ACTIVE_NETWORK) - */ - public int getActiveNetworkType() { - return getActiveNetworkType(mConnectivityManager); - } - - static public int getActiveNetworkType(Context context) { - ConnectivityManager cm = - (ConnectivityManager)context.getSystemService(Context.CONNECTIVITY_SERVICE); - return getActiveNetworkType(cm); - } - - static public int getActiveNetworkType(ConnectivityManager cm) { - NetworkInfo info = cm.getActiveNetworkInfo(); - if (info == null) return NO_ACTIVE_NETWORK; - return info.getType(); - } - - public void waitForConnectivity() { - // If we're unregistered, throw an exception - if (!mRegistered) { - throw new IllegalStateException("ConnectivityManager not registered"); - } - boolean waiting = false; - mWaitThread = Thread.currentThread(); - // Acquire the wait lock while we work - mWakeLock.acquire(); - try { - while (!mStop) { - NetworkInfo info = mConnectivityManager.getActiveNetworkInfo(); - if (info != null) { - // We're done if there's an active network - if (waiting) { - if (DebugUtils.DEBUG) { - LogUtils.d(TAG, mName + ": Connectivity wait ended"); - } - } - return; - } else { - if (!waiting) { - if (DebugUtils.DEBUG) { - LogUtils.d(TAG, mName + ": Connectivity waiting..."); - } - waiting = true; - } - // Wait until a network is connected (or 10 mins), but let the device sleep - synchronized (mLock) { - // Don't hold a lock during our wait - mWakeLock.release(); - try { - mLock.wait(CONNECTIVITY_WAIT_TIME); - } catch (InterruptedException e) { - // This is fine; we just go around the loop again - } - // Get the lock back and check again for connectivity - mWakeLock.acquire(); - } - } - } - } finally { - // Make sure we always release the wait lock - if (mWakeLock.isHeld()) { - mWakeLock.release(); - } - mWaitThread = null; - } - } -} diff --git a/src/com/android/email/EmailIntentService.java b/src/com/android/email/EmailIntentService.java deleted file mode 100644 index 7bf1b2d20..000000000 --- a/src/com/android/email/EmailIntentService.java +++ /dev/null @@ -1,45 +0,0 @@ -/* - * Copyright (C) 2013 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; - -import android.content.Intent; - -import com.android.mail.MailIntentService; -import com.android.mail.providers.UIProvider; -import com.android.mail.utils.LogTag; -import com.android.mail.utils.LogUtils; - -/** - * A service to handle various intents asynchronously. - */ -public class EmailIntentService extends MailIntentService { - private static final String LOG_TAG = LogTag.getLogTag(); - - public EmailIntentService() { - super("EmailIntentService"); - } - - @Override - protected void onHandleIntent(final Intent intent) { - super.onHandleIntent(intent); - - if (UIProvider.ACTION_UPDATE_NOTIFICATION.equals(intent.getAction())) { - NotificationController.handleUpdateNotificationIntent(this, intent); - } - - LogUtils.v(LOG_TAG, "Handling intent %s", intent); - } -} diff --git a/src/com/android/email/EmailNotificationController.java b/src/com/android/email/EmailNotificationController.java new file mode 100644 index 000000000..e57b41833 --- /dev/null +++ b/src/com/android/email/EmailNotificationController.java @@ -0,0 +1,855 @@ +/* + * 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; + +import android.app.Notification; +import android.app.Notification.Builder; +import android.app.NotificationManager; +import android.app.PendingIntent; +import android.content.ContentResolver; +import android.content.ContentUris; +import android.content.Context; +import android.content.Intent; +import android.database.ContentObserver; +import android.database.Cursor; +import android.graphics.Bitmap; +import android.net.Uri; +import android.os.Handler; +import android.os.Looper; +import android.os.Process; +import android.provider.Settings; +import android.support.v4.app.NotificationCompat; +import android.text.TextUtils; +import android.text.format.DateUtils; + +import com.android.email.activity.setup.AccountSecurity; +import com.android.email.activity.setup.HeadlessAccountSettingsLoader; +import com.android.email.provider.EmailProvider; +import com.android.email.service.EmailServiceUtils; +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.Message; +import com.android.emailcommon.provider.Mailbox; +import com.android.emailcommon.utility.EmailAsyncTask; +import com.android.mail.preferences.FolderPreferences; +import com.android.mail.providers.Folder; +import com.android.mail.providers.UIProvider; +import com.android.mail.utils.Clock; +import com.android.mail.utils.LogTag; +import com.android.mail.utils.LogUtils; +import com.android.mail.utils.NotificationUtils; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +/** + * Class that manages notifications. + */ +public class EmailNotificationController implements NotificationController { + private static final String LOG_TAG = LogTag.getLogTag(); + + private static final int NOTIFICATION_ID_ATTACHMENT_WARNING = 3; + private static final int NOTIFICATION_ID_PASSWORD_EXPIRING = 4; + private static final int NOTIFICATION_ID_PASSWORD_EXPIRED = 5; + + private static final int NOTIFICATION_ID_BASE_MASK = 0xF0000000; + private static final int NOTIFICATION_ID_BASE_LOGIN_WARNING = 0x20000000; + private static final int NOTIFICATION_ID_BASE_SECURITY_NEEDED = 0x30000000; + private static final int NOTIFICATION_ID_BASE_SECURITY_CHANGED = 0x40000000; + + private static NotificationThread sNotificationThread; + private static Handler sNotificationHandler; + private static EmailNotificationController sInstance; + private final Context mContext; + private final NotificationManager mNotificationManager; + private final Clock mClock; + /** Maps account id to its observer */ + private final Map mNotificationMap = + new HashMap(); + private ContentObserver mAccountObserver; + + /** Constructor */ + protected EmailNotificationController(Context context, Clock clock) { + mContext = context.getApplicationContext(); + EmailContent.init(context); + mNotificationManager = (NotificationManager) context.getSystemService( + Context.NOTIFICATION_SERVICE); + mClock = clock; + } + + /** Singleton access */ + public static synchronized EmailNotificationController getInstance(Context context) { + if (sInstance == null) { + sInstance = new EmailNotificationController(context, Clock.INSTANCE); + } + return sInstance; + } + + /** + * Return whether or not a notification, based on the passed-in id, needs to be "ongoing" + * @param notificationId the notification id to check + * @return whether or not the notification must be "ongoing" + */ + private static boolean needsOngoingNotification(int notificationId) { + // "Security needed" must be ongoing so that the user doesn't close it; otherwise, sync will + // be prevented until a reboot. Consider also doing this for password expired. + return (notificationId & NOTIFICATION_ID_BASE_MASK) == NOTIFICATION_ID_BASE_SECURITY_NEEDED; + } + + /** + * Returns a {@link android.support.v4.app.NotificationCompat.Builder} for an event with the + * given account. The account contains specific rules on ring tone usage and these will be used + * to modify the notification behaviour. + * + * @param accountId The id of the account this notification is being built for. + * @param ticker Text displayed when the notification is first shown. May be {@code null}. + * @param title The first line of text. May NOT be {@code null}. + * @param contentText The second line of text. May NOT be {@code null}. + * @param intent The intent to start if the user clicks on the notification. + * @param number A number to display using {@link Builder#setNumber(int)}. May be {@code null}. + * @param enableAudio If {@code false}, do not play any sound. Otherwise, play sound according + * to the settings for the given account. + * @return A {@link Notification} that can be sent to the notification service. + */ + private NotificationCompat.Builder createBaseAccountNotificationBuilder(long accountId, + String ticker, CharSequence title, String contentText, Intent intent, + Integer number, boolean enableAudio, boolean ongoing) { + // Pending Intent + PendingIntent pending = null; + if (intent != null) { + pending = PendingIntent.getActivity( + mContext, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT); + } + + // NOTE: the ticker is not shown for notifications in the Holo UX + final NotificationCompat.Builder builder = new NotificationCompat.Builder(mContext) + .setContentTitle(title) + .setContentText(contentText) + .setContentIntent(pending) + .setNumber(number == null ? 0 : number) + .setSmallIcon(R.drawable.ic_notification_mail_24dp) + .setWhen(mClock.getTime()) + .setTicker(ticker) + .setOngoing(ongoing); + + if (enableAudio) { + Account account = Account.restoreAccountWithId(mContext, accountId); + setupSoundAndVibration(builder, account); + } + + return builder; + } + + /** + * Generic notifier for any account. Uses notification rules from account. + * + * @param accountId The account id this notification is being built for. + * @param ticker Text displayed when the notification is first shown. May be {@code null}. + * @param title The first line of text. May NOT be {@code null}. + * @param contentText The second line of text. May NOT be {@code null}. + * @param intent The intent to start if the user clicks on the notification. + * @param notificationId The ID of the notification to register with the service. + */ + private void showNotification(long accountId, String ticker, String title, + String contentText, Intent intent, int notificationId) { + final NotificationCompat.Builder builder = createBaseAccountNotificationBuilder(accountId, + ticker, title, contentText, intent, null, true, + needsOngoingNotification(notificationId)); + mNotificationManager.notify(notificationId, builder.build()); + } + + /** + * Tells the notification controller if it should be watching for changes to the message table. + * This is the main life cycle method for message notifications. When we stop observing + * database changes, we save the state [e.g. message ID and count] of the most recent + * notification shown to the user. And, when we start observing database changes, we restore + * the saved state. + */ + @Override + public void watchForMessages() { + ensureHandlerExists(); + // Run this on the message notification handler + sNotificationHandler.post(new Runnable() { + @Override + public void run() { + ContentResolver resolver = mContext.getContentResolver(); + + // otherwise, start new observers for all notified accounts + registerMessageNotification(Account.ACCOUNT_ID_COMBINED_VIEW); + // If we're already observing account changes, don't do anything else + if (mAccountObserver == null) { + LogUtils.i(LOG_TAG, "Observing account changes for notifications"); + mAccountObserver = new AccountContentObserver(sNotificationHandler, mContext); + resolver.registerContentObserver(Account.NOTIFIER_URI, true, mAccountObserver); + } + } + }); + } + + /** + * Ensures the notification handler exists and is ready to handle requests. + */ + + /** + * TODO: Notifications jump around too much because we get too many content updates. + * We should try to make the provider generate fewer updates instead. + */ + + private static final int NOTIFICATION_DELAYED_MESSAGE = 0; + private static final long NOTIFICATION_DELAY = 15 * DateUtils.SECOND_IN_MILLIS; + // True if we're coalescing notification updates + private static boolean sNotificationDelayedMessagePending; + // True if accounts have changed and we need to refresh everything + private static boolean sRefreshAllNeeded; + // Set of accounts we need to regenerate notifications for + private static final HashSet sRefreshAccountSet = new HashSet(); + // These should all be accessed on-thread, but just in case... + private static final Object sNotificationDelayedMessageLock = new Object(); + + private static synchronized void ensureHandlerExists() { + if (sNotificationThread == null) { + sNotificationThread = new NotificationThread(); + sNotificationHandler = new Handler(sNotificationThread.getLooper(), + new Handler.Callback() { + @Override + public boolean handleMessage(final android.os.Message message) { + /** + * To reduce spamming the notifications, we quiesce updates for a few + * seconds to batch them up, then handle them here. + */ + LogUtils.d(LOG_TAG, "Delayed notification processing"); + synchronized (sNotificationDelayedMessageLock) { + sNotificationDelayedMessagePending = false; + final Context context = (Context)message.obj; + if (sRefreshAllNeeded) { + sRefreshAllNeeded = false; + refreshAllNotificationsInternal(context); + } + for (final Long accountId : sRefreshAccountSet) { + refreshNotificationsForAccountInternal(context, accountId); + } + sRefreshAccountSet.clear(); + } + return true; + } + }); + } + } + + /** + * Registers an observer for changes to mailboxes in the given account. + * NOTE: This must be called on the notification handler thread. + * @param accountId The ID of the account to register the observer for. May be + * {@link Account#ACCOUNT_ID_COMBINED_VIEW} to register observers for all + * accounts that allow for user notification. + */ + private void registerMessageNotification(final long accountId) { + ContentResolver resolver = mContext.getContentResolver(); + if (accountId == Account.ACCOUNT_ID_COMBINED_VIEW) { + Cursor c = resolver.query( + Account.CONTENT_URI, EmailContent.ID_PROJECTION, + null, null, null); + try { + while (c.moveToNext()) { + long id = c.getLong(EmailContent.ID_PROJECTION_COLUMN); + registerMessageNotification(id); + } + } finally { + c.close(); + } + } else { + ContentObserver obs = mNotificationMap.get(accountId); + if (obs != null) return; // we're already observing; nothing to do + LogUtils.i(LOG_TAG, "Registering for notifications for account " + accountId); + ContentObserver observer = new MessageContentObserver( + sNotificationHandler, mContext, accountId); + resolver.registerContentObserver(Message.NOTIFIER_URI, true, observer); + mNotificationMap.put(accountId, observer); + // Now, ping the observer for any initial notifications + observer.onChange(true); + } + } + + /** + * Unregisters the observer for the given account. If the specified account does not have + * a registered observer, no action is performed. This will not clear any existing notification + * for the specified account. Use {@link NotificationManager#cancel(int)}. + * NOTE: This must be called on the notification handler thread. + * @param accountId The ID of the account to unregister from. To unregister all accounts that + * have observers, specify an ID of {@link Account#ACCOUNT_ID_COMBINED_VIEW}. + */ + private void unregisterMessageNotification(final long accountId) { + ContentResolver resolver = mContext.getContentResolver(); + if (accountId == Account.ACCOUNT_ID_COMBINED_VIEW) { + LogUtils.i(LOG_TAG, "Unregistering notifications for all accounts"); + // cancel all existing message observers + for (ContentObserver observer : mNotificationMap.values()) { + resolver.unregisterContentObserver(observer); + } + mNotificationMap.clear(); + } else { + LogUtils.i(LOG_TAG, "Unregistering notifications for account " + accountId); + ContentObserver observer = mNotificationMap.remove(accountId); + if (observer != null) { + resolver.unregisterContentObserver(observer); + } + } + } + + public static final String EXTRA_ACCOUNT = "account"; + public static final String EXTRA_CONVERSATION = "conversationUri"; + public static final String EXTRA_FOLDER = "folder"; + + /** Sets up the notification's sound and vibration based upon account details. */ + private void setupSoundAndVibration( + NotificationCompat.Builder builder, Account account) { + String ringtoneUri = Settings.System.DEFAULT_NOTIFICATION_URI.toString(); + boolean vibrate = false; + + // Use the Inbox notification preferences + final Cursor accountCursor = mContext.getContentResolver().query(EmailProvider.uiUri( + "uiaccount", account.mId), UIProvider.ACCOUNTS_PROJECTION, null, null, null); + + com.android.mail.providers.Account uiAccount = null; + try { + if (accountCursor.moveToFirst()) { + uiAccount = com.android.mail.providers.Account.builder().buildFrom(accountCursor); + } + } finally { + accountCursor.close(); + } + + if (uiAccount != null) { + final Cursor folderCursor = + mContext.getContentResolver().query(uiAccount.settings.defaultInbox, + UIProvider.FOLDERS_PROJECTION, null, null, null); + + if (folderCursor == null) { + // This can happen when the notification is for the security policy notification + // that happens before the account is setup + LogUtils.w(LOG_TAG, "Null folder cursor for mailbox %s", + uiAccount.settings.defaultInbox); + } else { + Folder folder = null; + try { + if (folderCursor.moveToFirst()) { + folder = new Folder(folderCursor); + } + } finally { + folderCursor.close(); + } + + if (folder != null) { + final FolderPreferences folderPreferences = new FolderPreferences( + mContext, uiAccount.getEmailAddress(), folder, true /* inbox */); + + ringtoneUri = folderPreferences.getNotificationRingtoneUri(); + vibrate = folderPreferences.isNotificationVibrateEnabled(); + } else { + LogUtils.e(LOG_TAG, + "Null folder for mailbox %s", uiAccount.settings.defaultInbox); + } + } + } else { + LogUtils.e(LOG_TAG, "Null uiAccount for account id %d", account.mId); + } + + int defaults = Notification.DEFAULT_LIGHTS; + if (vibrate) { + defaults |= Notification.DEFAULT_VIBRATE; + } + + builder.setSound(TextUtils.isEmpty(ringtoneUri) ? null : Uri.parse(ringtoneUri)) + .setDefaults(defaults); + } + + /** + * Show (or update) a notification that the given attachment could not be forwarded. This + * is a very unusual case, and perhaps we shouldn't even send a notification. For now, + * it's helpful for debugging. + * + * NOTE: DO NOT CALL THIS METHOD FROM THE UI THREAD (DATABASE ACCESS) + */ + @Override + public void showDownloadForwardFailedNotificationSynchronous(Attachment attachment) { + final Message message = Message.restoreMessageWithId(mContext, attachment.mMessageKey); + if (message == null) return; + final Mailbox mailbox = Mailbox.restoreMailboxWithId(mContext, message.mMailboxKey); + showNotification(mailbox.mAccountKey, + mContext.getString(R.string.forward_download_failed_ticker), + mContext.getString(R.string.forward_download_failed_title), + attachment.mFileName, + null, + NOTIFICATION_ID_ATTACHMENT_WARNING); + } + + /** + * Returns a notification ID for login failed notifications for the given account account. + */ + private static int getLoginFailedNotificationId(long accountId) { + return NOTIFICATION_ID_BASE_LOGIN_WARNING + (int)accountId; + } + + /** + * Show (or update) a notification that there was a login failure for the given account. + * + * NOTE: DO NOT CALL THIS METHOD FROM THE UI THREAD (DATABASE ACCESS) + */ + @Override + public void showLoginFailedNotificationSynchronous(long accountId, boolean incoming) { + final Account account = Account.restoreAccountWithId(mContext, accountId); + if (account == null) return; + final Mailbox mailbox = Mailbox.restoreMailboxOfType(mContext, accountId, + Mailbox.TYPE_INBOX); + if (mailbox == null) return; + + final Intent settingsIntent; + if (incoming) { + settingsIntent = new Intent(Intent.ACTION_VIEW, + EmailProvider.getIncomingSettingsUri(accountId)); + } else { + settingsIntent = new Intent(Intent.ACTION_VIEW, + HeadlessAccountSettingsLoader.getOutgoingSettingsUri(accountId)); + } + showNotification(mailbox.mAccountKey, + mContext.getString(R.string.login_failed_ticker, account.mDisplayName), + mContext.getString(R.string.login_failed_title), + account.getDisplayName(), + settingsIntent, + getLoginFailedNotificationId(accountId)); + } + + /** + * Cancels the login failed notification for the given account. + */ + @Override + public void cancelLoginFailedNotification(long accountId) { + mNotificationManager.cancel(getLoginFailedNotificationId(accountId)); + } + + /** + * Show (or update) a notification that the user's password is expiring. The given account + * is used to update the display text, but, all accounts share the same notification ID. + * + * NOTE: DO NOT CALL THIS METHOD FROM THE UI THREAD (DATABASE ACCESS) + */ + @Override + public void showPasswordExpiringNotificationSynchronous(long accountId) { + final Account account = Account.restoreAccountWithId(mContext, accountId); + if (account == null) return; + + final Intent intent = AccountSecurity.actionDevicePasswordExpirationIntent(mContext, + accountId, false); + final String accountName = account.getDisplayName(); + final String ticker = + mContext.getString(R.string.password_expire_warning_ticker_fmt, accountName); + final String title = mContext.getString(R.string.password_expire_warning_content_title); + showNotification(accountId, ticker, title, accountName, intent, + NOTIFICATION_ID_PASSWORD_EXPIRING); + } + + /** + * Show (or update) a notification that the user's password has expired. The given account + * is used to update the display text, but, all accounts share the same notification ID. + * + * NOTE: DO NOT CALL THIS METHOD FROM THE UI THREAD (DATABASE ACCESS) + */ + @Override + public void showPasswordExpiredNotificationSynchronous(long accountId) { + final Account account = Account.restoreAccountWithId(mContext, accountId); + if (account == null) return; + + final Intent intent = AccountSecurity.actionDevicePasswordExpirationIntent(mContext, + accountId, true); + final String accountName = account.getDisplayName(); + final String ticker = mContext.getString(R.string.password_expired_ticker); + final String title = mContext.getString(R.string.password_expired_content_title); + showNotification(accountId, ticker, title, accountName, intent, + NOTIFICATION_ID_PASSWORD_EXPIRED); + } + + /** + * Cancels any password expire notifications [both expired & expiring]. + */ + @Override + public void cancelPasswordExpirationNotifications() { + mNotificationManager.cancel(NOTIFICATION_ID_PASSWORD_EXPIRING); + mNotificationManager.cancel(NOTIFICATION_ID_PASSWORD_EXPIRED); + } + + /** + * Show (or update) a security needed notification. If tapped, the user is taken to a + * dialog asking whether he wants to update his settings. + */ + @Override + public void showSecurityNeededNotification(Account account) { + Intent intent = AccountSecurity.actionUpdateSecurityIntent(mContext, account.mId, true); + String accountName = account.getDisplayName(); + String ticker = + mContext.getString(R.string.security_needed_ticker_fmt, accountName); + String title = mContext.getString(R.string.security_notification_content_update_title); + showNotification(account.mId, ticker, title, accountName, intent, + (int)(NOTIFICATION_ID_BASE_SECURITY_NEEDED + account.mId)); + } + + /** + * Show (or update) a security changed notification. If tapped, the user is taken to the + * account settings screen where he can view the list of enforced policies + */ + @Override + public void showSecurityChangedNotification(Account account) { + final Intent intent = new Intent(Intent.ACTION_VIEW, + EmailProvider.getIncomingSettingsUri(account.getId())); + final String accountName = account.getDisplayName(); + final String ticker = + mContext.getString(R.string.security_changed_ticker_fmt, accountName); + final String title = + mContext.getString(R.string.security_notification_content_change_title); + showNotification(account.mId, ticker, title, accountName, intent, + (int)(NOTIFICATION_ID_BASE_SECURITY_CHANGED + account.mId)); + } + + /** + * Show (or update) a security unsupported notification. If tapped, the user is taken to the + * account settings screen where he can view the list of unsupported policies + */ + @Override + public void showSecurityUnsupportedNotification(Account account) { + final Intent intent = new Intent(Intent.ACTION_VIEW, + EmailProvider.getIncomingSettingsUri(account.getId())); + final String accountName = account.getDisplayName(); + final String ticker = + mContext.getString(R.string.security_unsupported_ticker_fmt, accountName); + final String title = + mContext.getString(R.string.security_notification_content_unsupported_title); + showNotification(account.mId, ticker, title, accountName, intent, + (int)(NOTIFICATION_ID_BASE_SECURITY_NEEDED + account.mId)); + } + + /** + * Cancels all security needed notifications. + */ + @Override + public void cancelSecurityNeededNotification() { + EmailAsyncTask.runAsyncParallel(new Runnable() { + @Override + public void run() { + Cursor c = mContext.getContentResolver().query(Account.CONTENT_URI, + Account.ID_PROJECTION, null, null, null); + try { + while (c.moveToNext()) { + long id = c.getLong(Account.ID_PROJECTION_COLUMN); + mNotificationManager.cancel( + (int)(NOTIFICATION_ID_BASE_SECURITY_NEEDED + id)); + } + } + finally { + c.close(); + } + }}); + } + + /** + * Cancels all notifications for the specified account id. This includes new mail notifications, + * as well as special login/security notifications. + */ + @Override + public void cancelNotifications(final Context context, final Account account) { + final EmailServiceUtils.EmailServiceInfo serviceInfo + = EmailServiceUtils.getServiceInfoForAccount(context, account.mId); + if (serviceInfo == null) { + LogUtils.d(LOG_TAG, "Can't cancel notification for missing account %d", account.mId); + return; + } + final android.accounts.Account notifAccount + = account.getAccountManagerAccount(serviceInfo.accountType); + + NotificationUtils.clearAccountNotifications(context, notifAccount); + + final NotificationManager notificationManager = getInstance(context).mNotificationManager; + + notificationManager.cancel((int) (NOTIFICATION_ID_BASE_LOGIN_WARNING + account.mId)); + notificationManager.cancel((int) (NOTIFICATION_ID_BASE_SECURITY_NEEDED + account.mId)); + notificationManager.cancel((int) (NOTIFICATION_ID_BASE_SECURITY_CHANGED + account.mId)); + } + + private static void refreshNotificationsForAccount(final Context context, + final long accountId) { + synchronized (sNotificationDelayedMessageLock) { + if (sNotificationDelayedMessagePending) { + sRefreshAccountSet.add(accountId); + } else { + ensureHandlerExists(); + sNotificationHandler.sendMessageDelayed( + android.os.Message.obtain(sNotificationHandler, + NOTIFICATION_DELAYED_MESSAGE, context), NOTIFICATION_DELAY); + sNotificationDelayedMessagePending = true; + refreshNotificationsForAccountInternal(context, accountId); + } + } + } + + private static void refreshNotificationsForAccountInternal(final Context context, + final long accountId) { + final Uri accountUri = EmailProvider.uiUri("uiaccount", accountId); + + final ContentResolver contentResolver = context.getContentResolver(); + + final Cursor mailboxCursor = contentResolver.query( + ContentUris.withAppendedId(EmailContent.MAILBOX_NOTIFICATION_URI, accountId), + null, null, null, null); + try { + while (mailboxCursor.moveToNext()) { + final long mailboxId = + mailboxCursor.getLong(EmailContent.NOTIFICATION_MAILBOX_ID_COLUMN); + if (mailboxId == 0) continue; + + final int unseenCount = mailboxCursor.getInt( + EmailContent.NOTIFICATION_MAILBOX_UNSEEN_COUNT_COLUMN); + + final int unreadCount; + // If nothing is unseen, clear the notification + if (unseenCount == 0) { + unreadCount = 0; + } else { + unreadCount = mailboxCursor.getInt( + EmailContent.NOTIFICATION_MAILBOX_UNREAD_COUNT_COLUMN); + } + + final Uri folderUri = EmailProvider.uiUri("uifolder", mailboxId); + + + LogUtils.d(LOG_TAG, "Changes to account " + accountId + ", folder: " + + mailboxId + ", unreadCount: " + unreadCount + ", unseenCount: " + + unseenCount); + + final Intent intent = new Intent(UIProvider.ACTION_UPDATE_NOTIFICATION); + intent.setPackage(context.getPackageName()); + intent.setType(EmailProvider.EMAIL_APP_MIME_TYPE); + + intent.putExtra(UIProvider.UpdateNotificationExtras.EXTRA_ACCOUNT, accountUri); + intent.putExtra(UIProvider.UpdateNotificationExtras.EXTRA_FOLDER, folderUri); + intent.putExtra(UIProvider.UpdateNotificationExtras.EXTRA_UPDATED_UNREAD_COUNT, + unreadCount); + intent.putExtra(UIProvider.UpdateNotificationExtras.EXTRA_UPDATED_UNSEEN_COUNT, + unseenCount); + + context.sendOrderedBroadcast(intent, null); + } + } finally { + mailboxCursor.close(); + } + } + + @Override + public void handleUpdateNotificationIntent(Context context, Intent intent) { + final Uri accountUri = + intent.getParcelableExtra(UIProvider.UpdateNotificationExtras.EXTRA_ACCOUNT); + final Uri folderUri = + intent.getParcelableExtra(UIProvider.UpdateNotificationExtras.EXTRA_FOLDER); + final int unreadCount = intent.getIntExtra( + UIProvider.UpdateNotificationExtras.EXTRA_UPDATED_UNREAD_COUNT, 0); + final int unseenCount = intent.getIntExtra( + UIProvider.UpdateNotificationExtras.EXTRA_UPDATED_UNSEEN_COUNT, 0); + + final ContentResolver contentResolver = context.getContentResolver(); + + final Cursor accountCursor = contentResolver.query(accountUri, + UIProvider.ACCOUNTS_PROJECTION, null, null, null); + + if (accountCursor == null) { + LogUtils.e(LOG_TAG, "Null account cursor for account " + accountUri); + return; + } + + com.android.mail.providers.Account account = null; + try { + if (accountCursor.moveToFirst()) { + account = com.android.mail.providers.Account.builder().buildFrom(accountCursor); + } + } finally { + accountCursor.close(); + } + + if (account == null) { + LogUtils.d(LOG_TAG, "Tried to create a notification for a missing account " + + accountUri); + return; + } + + final Cursor folderCursor = contentResolver.query(folderUri, UIProvider.FOLDERS_PROJECTION, + null, null, null); + + if (folderCursor == null) { + LogUtils.e(LOG_TAG, "Null folder cursor for account " + accountUri + ", mailbox " + + folderUri); + return; + } + + Folder folder = null; + try { + if (folderCursor.moveToFirst()) { + folder = new Folder(folderCursor); + } else { + LogUtils.e(LOG_TAG, "Empty folder cursor for account " + accountUri + ", mailbox " + + folderUri); + return; + } + } finally { + folderCursor.close(); + } + + // TODO: we don't always want getAttention to be true, but we don't necessarily have a + // good heuristic for when it should or shouldn't be. + NotificationUtils.sendSetNewEmailIndicatorIntent(context, unreadCount, unseenCount, + account, folder, true /* getAttention */); + } + + private static void refreshAllNotifications(final Context context) { + synchronized (sNotificationDelayedMessageLock) { + if (sNotificationDelayedMessagePending) { + sRefreshAllNeeded = true; + } else { + ensureHandlerExists(); + sNotificationHandler.sendMessageDelayed( + android.os.Message.obtain(sNotificationHandler, + NOTIFICATION_DELAYED_MESSAGE, context), NOTIFICATION_DELAY); + sNotificationDelayedMessagePending = true; + refreshAllNotificationsInternal(context); + } + } + } + + private static void refreshAllNotificationsInternal(final Context context) { + NotificationUtils.resendNotifications( + context, false, null, null, null /* ContactFetcher */); + } + + /** + * Observer invoked whenever a message we're notifying the user about changes. + */ + private static class MessageContentObserver extends ContentObserver { + private final Context mContext; + private final long mAccountId; + + public MessageContentObserver( + final Handler handler, final Context context, final long accountId) { + super(handler); + mContext = context; + mAccountId = accountId; + } + + @Override + public void onChange(final boolean selfChange) { + refreshNotificationsForAccount(mContext, mAccountId); + } + } + + /** + * Observer invoked whenever an account is modified. This could mean the user changed the + * notification settings. + */ + private static class AccountContentObserver extends ContentObserver { + private final Context mContext; + public AccountContentObserver(final Handler handler, final Context context) { + super(handler); + mContext = context; + } + + @Override + public void onChange(final boolean selfChange) { + final ContentResolver resolver = mContext.getContentResolver(); + final Cursor c = resolver.query(Account.CONTENT_URI, EmailContent.ID_PROJECTION, + null, null, null); + final Set newAccountList = new HashSet(); + final Set removedAccountList = new HashSet(); + if (c == null) { + // Suspender time ... theoretically, this will never happen + LogUtils.wtf(LOG_TAG, "#onChange(); NULL response for account id query"); + return; + } + try { + while (c.moveToNext()) { + long accountId = c.getLong(EmailContent.ID_PROJECTION_COLUMN); + newAccountList.add(accountId); + } + } finally { + c.close(); + } + // NOTE: Looping over three lists is not necessarily the most efficient. However, the + // account lists are going to be very small, so, this will not be necessarily bad. + // Cycle through existing notification list and adjust as necessary + for (final long accountId : sInstance.mNotificationMap.keySet()) { + if (!newAccountList.remove(accountId)) { + // account id not in the current set of notifiable accounts + removedAccountList.add(accountId); + } + } + // A new account was added to the notification list + for (final long accountId : newAccountList) { + sInstance.registerMessageNotification(accountId); + } + // An account was removed from the notification list + for (final long accountId : removedAccountList) { + sInstance.unregisterMessageNotification(accountId); + } + + refreshAllNotifications(mContext); + } + } + + /** + * Thread to handle all notification actions through its own {@link Looper}. + */ + private static class NotificationThread implements Runnable { + /** Lock to ensure proper initialization */ + private final Object mLock = new Object(); + /** The {@link Looper} that handles messages for this thread */ + private Looper mLooper; + + public NotificationThread() { + new Thread(null, this, "EmailNotification").start(); + synchronized (mLock) { + while (mLooper == null) { + try { + mLock.wait(); + } catch (InterruptedException ex) { + // Loop around and wait again + } + } + } + } + + @Override + public void run() { + synchronized (mLock) { + Looper.prepare(); + mLooper = Looper.myLooper(); + mLock.notifyAll(); + } + Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND); + Looper.loop(); + } + + public Looper getLooper() { + return mLooper; + } + } +} diff --git a/src/com/android/email/FixedLengthInputStream.java b/src/com/android/email/FixedLengthInputStream.java deleted file mode 100644 index 753c03181..000000000 --- a/src/com/android/email/FixedLengthInputStream.java +++ /dev/null @@ -1,80 +0,0 @@ -/* - * 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; - -import java.io.IOException; -import java.io.InputStream; - -/** - * A filtering InputStream that stops allowing reads after the given length has been read. This - * is used to allow a client to read directly from an underlying protocol stream without reading - * past where the protocol handler intended the client to read. - */ -public class FixedLengthInputStream extends InputStream { - private final InputStream mIn; - private final int mLength; - private int mCount; - - public FixedLengthInputStream(InputStream in, int length) { - this.mIn = in; - this.mLength = length; - } - - @Override - public int available() throws IOException { - return mLength - mCount; - } - - @Override - public int read() throws IOException { - if (mCount < mLength) { - mCount++; - return mIn.read(); - } else { - return -1; - } - } - - @Override - public int read(byte[] b, int offset, int length) throws IOException { - if (mCount < mLength) { - int d = mIn.read(b, offset, Math.min(mLength - mCount, length)); - if (d == -1) { - return -1; - } else { - mCount += d; - return d; - } - } else { - return -1; - } - } - - @Override - public int read(byte[] b) throws IOException { - return read(b, 0, b.length); - } - - public int getLength() { - return mLength; - } - - @Override - public String toString() { - return String.format("FixedLengthInputStream(in=%s, length=%d)", mIn.toString(), mLength); - } -} diff --git a/src/com/android/email/LegacyConversions.java b/src/com/android/email/LegacyConversions.java deleted file mode 100644 index 3de9b68cc..000000000 --- a/src/com/android/email/LegacyConversions.java +++ /dev/null @@ -1,547 +0,0 @@ -/* - * 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; - -import android.content.ContentUris; -import android.content.ContentValues; -import android.content.Context; -import android.database.Cursor; -import android.net.Uri; -import android.text.TextUtils; - -import com.android.emailcommon.Logging; -import com.android.emailcommon.internet.MimeBodyPart; -import com.android.emailcommon.internet.MimeHeader; -import com.android.emailcommon.internet.MimeMessage; -import com.android.emailcommon.internet.MimeMultipart; -import com.android.emailcommon.internet.MimeUtility; -import com.android.emailcommon.internet.TextBody; -import com.android.emailcommon.mail.Address; -import com.android.emailcommon.mail.Base64Body; -import com.android.emailcommon.mail.Flag; -import com.android.emailcommon.mail.Message; -import com.android.emailcommon.mail.Message.RecipientType; -import com.android.emailcommon.mail.MessagingException; -import com.android.emailcommon.mail.Multipart; -import com.android.emailcommon.mail.Part; -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.Mailbox; -import com.android.emailcommon.utility.AttachmentUtilities; -import com.android.mail.providers.UIProvider; -import com.android.mail.utils.LogUtils; -import com.google.common.annotations.VisibleForTesting; - -import org.apache.commons.io.IOUtils; - -import java.io.ByteArrayInputStream; -import java.io.File; -import java.io.FileNotFoundException; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.util.ArrayList; -import java.util.Date; -import java.util.HashMap; - -public class LegacyConversions { - - /** DO NOT CHECK IN "TRUE" */ - private static final boolean DEBUG_ATTACHMENTS = false; - - /** Used for mapping folder names to type codes (e.g. inbox, drafts, trash) */ - private static final HashMap - sServerMailboxNames = new HashMap(); - - /** - * Copy field-by-field from a "store" message to a "provider" message - * - * @param message The message we've just downloaded (must be a MimeMessage) - * @param localMessage The message we'd like to write into the DB - * @return true if dirty (changes were made) - */ - public static boolean updateMessageFields(final EmailContent.Message localMessage, - final Message message, final long accountId, final long mailboxId) - throws MessagingException { - - final Address[] from = message.getFrom(); - final Address[] to = message.getRecipients(Message.RecipientType.TO); - final Address[] cc = message.getRecipients(Message.RecipientType.CC); - final Address[] bcc = message.getRecipients(Message.RecipientType.BCC); - final Address[] replyTo = message.getReplyTo(); - final String subject = message.getSubject(); - final Date sentDate = message.getSentDate(); - final Date internalDate = message.getInternalDate(); - - if (from != null && from.length > 0) { - localMessage.mDisplayName = from[0].toFriendly(); - } - if (sentDate != null) { - localMessage.mTimeStamp = sentDate.getTime(); - } else if (internalDate != null) { - LogUtils.w(Logging.LOG_TAG, "No sentDate, falling back to internalDate"); - localMessage.mTimeStamp = internalDate.getTime(); - } - if (subject != null) { - localMessage.mSubject = subject; - } - localMessage.mFlagRead = message.isSet(Flag.SEEN); - if (message.isSet(Flag.ANSWERED)) { - localMessage.mFlags |= EmailContent.Message.FLAG_REPLIED_TO; - } - - // Keep the message in the "unloaded" state until it has (at least) a display name. - // This prevents early flickering of empty messages in POP download. - if (localMessage.mFlagLoaded != EmailContent.Message.FLAG_LOADED_COMPLETE) { - if (localMessage.mDisplayName == null || "".equals(localMessage.mDisplayName)) { - localMessage.mFlagLoaded = EmailContent.Message.FLAG_LOADED_UNLOADED; - } else { - localMessage.mFlagLoaded = EmailContent.Message.FLAG_LOADED_PARTIAL; - } - } - localMessage.mFlagFavorite = message.isSet(Flag.FLAGGED); -// public boolean mFlagAttachment = false; -// public int mFlags = 0; - - localMessage.mServerId = message.getUid(); - if (internalDate != null) { - localMessage.mServerTimeStamp = internalDate.getTime(); - } -// public String mClientId; - - // Only replace the local message-id if a new one was found. This is seen in some ISP's - // which may deliver messages w/o a message-id header. - final String messageId = message.getMessageId(); - if (messageId != null) { - localMessage.mMessageId = messageId; - } - -// public long mBodyKey; - localMessage.mMailboxKey = mailboxId; - localMessage.mAccountKey = accountId; - - if (from != null && from.length > 0) { - localMessage.mFrom = Address.toString(from); - } - - localMessage.mTo = Address.toString(to); - localMessage.mCc = Address.toString(cc); - localMessage.mBcc = Address.toString(bcc); - localMessage.mReplyTo = Address.toString(replyTo); - -// public String mText; -// public String mHtml; -// public String mTextReply; -// public String mHtmlReply; - -// // Can be used while building messages, but is NOT saved by the Provider -// transient public ArrayList mAttachments = null; - - return true; - } - - /** - * Copy attachments from MimeMessage to provider Message. - * - * @param context a context for file operations - * @param localMessage the attachments will be built against this message - * @param attachments the attachments to add - */ - public static void updateAttachments(final Context context, - final EmailContent.Message localMessage, final ArrayList attachments) - throws MessagingException, IOException { - localMessage.mAttachments = null; - for (Part attachmentPart : attachments) { - addOneAttachment(context, localMessage, attachmentPart); - } - } - - public static void updateInlineAttachments(final Context context, - final EmailContent.Message localMessage, final ArrayList inlineAttachments) - throws MessagingException, IOException { - for (final Part inlinePart : inlineAttachments) { - final String disposition = MimeUtility.getHeaderParameter( - MimeUtility.unfoldAndDecode(inlinePart.getDisposition()), null); - if (!TextUtils.isEmpty(disposition)) { - // Treat inline parts as attachments - addOneAttachment(context, localMessage, inlinePart); - } - } - } - - /** - * Convert a MIME Part object into an Attachment object. Separated for unit testing. - * - * @param part MIME part object to convert - * @return Populated Account object - * @throws MessagingException - */ - @VisibleForTesting - protected static Attachment mimePartToAttachment(final Part part) throws MessagingException { - // Transfer fields from mime format to provider format - final String contentType = MimeUtility.unfoldAndDecode(part.getContentType()); - - String name = MimeUtility.getHeaderParameter(contentType, "name"); - if (TextUtils.isEmpty(name)) { - final String contentDisposition = MimeUtility.unfoldAndDecode(part.getDisposition()); - name = MimeUtility.getHeaderParameter(contentDisposition, "filename"); - } - - // Incoming attachment: Try to pull size from disposition (if not downloaded yet) - long size = 0; - final String disposition = part.getDisposition(); - if (!TextUtils.isEmpty(disposition)) { - String s = MimeUtility.getHeaderParameter(disposition, "size"); - if (!TextUtils.isEmpty(s)) { - try { - size = Long.parseLong(s); - } catch (final NumberFormatException e) { - LogUtils.d(LogUtils.TAG, e, "Could not decode size \"%s\" from attachment part", - size); - } - } - } - - // Get partId for unloaded IMAP attachments (if any) - // This is only provided (and used) when we have structure but not the actual attachment - final String[] partIds = part.getHeader(MimeHeader.HEADER_ANDROID_ATTACHMENT_STORE_DATA); - final String partId = partIds != null ? partIds[0] : null; - - final Attachment localAttachment = new Attachment(); - - // Run the mime type through inferMimeType in case we have something generic and can do - // better using the filename extension - localAttachment.mMimeType = AttachmentUtilities.inferMimeType(name, part.getMimeType()); - localAttachment.mFileName = name; - localAttachment.mSize = size; - localAttachment.mContentId = part.getContentId(); - localAttachment.setContentUri(null); // Will be rewritten by saveAttachmentBody - localAttachment.mLocation = partId; - localAttachment.mEncoding = "B"; // TODO - convert other known encodings - - return localAttachment; - } - - /** - * Add a single attachment part to the message - * - * This will skip adding attachments if they are already found in the attachments table. - * The heuristic for this will fail (false-positive) if two identical attachments are - * included in a single POP3 message. - * TODO: Fix that, by (elsewhere) simulating an mLocation value based on the attachments - * position within the list of multipart/mixed elements. This would make every POP3 attachment - * unique, and might also simplify the code (since we could just look at the positions, and - * ignore the filename, etc.) - * - * TODO: Take a closer look at encoding and deal with it if necessary. - * - * @param context a context for file operations - * @param localMessage the attachments will be built against this message - * @param part a single attachment part from POP or IMAP - */ - public static void addOneAttachment(final Context context, - final EmailContent.Message localMessage, final Part part) - throws MessagingException, IOException { - final Attachment localAttachment = mimePartToAttachment(part); - localAttachment.mMessageKey = localMessage.mId; - localAttachment.mAccountKey = localMessage.mAccountKey; - - if (DEBUG_ATTACHMENTS) { - LogUtils.d(Logging.LOG_TAG, "Add attachment " + localAttachment); - } - - // To prevent duplication - do we already have a matching attachment? - // The fields we'll check for equality are: - // mFileName, mMimeType, mContentId, mMessageKey, mLocation - // NOTE: This will false-positive if you attach the exact same file, twice, to a POP3 - // message. We can live with that - you'll get one of the copies. - final Uri uri = ContentUris.withAppendedId(Attachment.MESSAGE_ID_URI, localMessage.mId); - final Cursor cursor = context.getContentResolver().query(uri, Attachment.CONTENT_PROJECTION, - null, null, null); - boolean attachmentFoundInDb = false; - try { - while (cursor.moveToNext()) { - final Attachment dbAttachment = new Attachment(); - dbAttachment.restore(cursor); - // We test each of the fields here (instead of in SQL) because they may be - // null, or may be strings. - if (!TextUtils.equals(dbAttachment.mFileName, localAttachment.mFileName) || - !TextUtils.equals(dbAttachment.mMimeType, localAttachment.mMimeType) || - !TextUtils.equals(dbAttachment.mContentId, localAttachment.mContentId) || - !TextUtils.equals(dbAttachment.mLocation, localAttachment.mLocation)) { - continue; - } - // We found a match, so use the existing attachment id, and stop looking/looping - attachmentFoundInDb = true; - localAttachment.mId = dbAttachment.mId; - if (DEBUG_ATTACHMENTS) { - LogUtils.d(Logging.LOG_TAG, "Skipped, found db attachment " + dbAttachment); - } - break; - } - } finally { - cursor.close(); - } - - // Save the attachment (so far) in order to obtain an id - if (!attachmentFoundInDb) { - localAttachment.save(context); - } - - // If an attachment body was actually provided, we need to write the file now - saveAttachmentBody(context, part, localAttachment, localMessage.mAccountKey); - - if (localMessage.mAttachments == null) { - localMessage.mAttachments = new ArrayList(); - } - localMessage.mAttachments.add(localAttachment); - localMessage.mFlagAttachment = true; - } - - /** - * Save the body part of a single attachment, to a file in the attachments directory. - */ - public static void saveAttachmentBody(final Context context, final Part part, - final Attachment localAttachment, long accountId) - throws MessagingException, IOException { - if (part.getBody() != null) { - final long attachmentId = localAttachment.mId; - - final File saveIn = AttachmentUtilities.getAttachmentDirectory(context, accountId); - - if (!saveIn.isDirectory() && !saveIn.mkdirs()) { - throw new IOException("Could not create attachment directory"); - } - final File saveAs = AttachmentUtilities.getAttachmentFilename(context, accountId, - attachmentId); - - InputStream in = null; - FileOutputStream out = null; - final long copySize; - try { - in = part.getBody().getInputStream(); - out = new FileOutputStream(saveAs); - copySize = IOUtils.copyLarge(in, out); - } finally { - if (in != null) { - in.close(); - } - if (out != null) { - out.close(); - } - } - - // update the attachment with the extra information we now know - final String contentUriString = AttachmentUtilities.getAttachmentUri( - accountId, attachmentId).toString(); - - localAttachment.mSize = copySize; - localAttachment.setContentUri(contentUriString); - - // update the attachment in the database as well - final ContentValues cv = new ContentValues(3); - cv.put(AttachmentColumns.SIZE, copySize); - cv.put(AttachmentColumns.CONTENT_URI, contentUriString); - cv.put(AttachmentColumns.UI_STATE, UIProvider.AttachmentState.SAVED); - final Uri uri = ContentUris.withAppendedId(Attachment.CONTENT_URI, attachmentId); - context.getContentResolver().update(uri, cv, null, null); - } - } - - /** - * Read a complete Provider message into a legacy message (for IMAP upload). This - * is basically the equivalent of LocalFolder.getMessages() + LocalFolder.fetch(). - */ - public static Message makeMessage(final Context context, - final EmailContent.Message localMessage) - throws MessagingException { - final MimeMessage message = new MimeMessage(); - - // LocalFolder.getMessages() equivalent: Copy message fields - message.setSubject(localMessage.mSubject == null ? "" : localMessage.mSubject); - final Address[] from = Address.fromHeader(localMessage.mFrom); - if (from.length > 0) { - message.setFrom(from[0]); - } - message.setSentDate(new Date(localMessage.mTimeStamp)); - message.setUid(localMessage.mServerId); - message.setFlag(Flag.DELETED, - localMessage.mFlagLoaded == EmailContent.Message.FLAG_LOADED_DELETED); - message.setFlag(Flag.SEEN, localMessage.mFlagRead); - message.setFlag(Flag.FLAGGED, localMessage.mFlagFavorite); -// message.setFlag(Flag.DRAFT, localMessage.mMailboxKey == draftMailboxKey); - message.setRecipients(RecipientType.TO, Address.fromHeader(localMessage.mTo)); - message.setRecipients(RecipientType.CC, Address.fromHeader(localMessage.mCc)); - message.setRecipients(RecipientType.BCC, Address.fromHeader(localMessage.mBcc)); - message.setReplyTo(Address.fromHeader(localMessage.mReplyTo)); - message.setInternalDate(new Date(localMessage.mServerTimeStamp)); - message.setMessageId(localMessage.mMessageId); - - // LocalFolder.fetch() equivalent: build body parts - message.setHeader(MimeHeader.HEADER_CONTENT_TYPE, "multipart/mixed"); - final MimeMultipart mp = new MimeMultipart(); - mp.setSubType("mixed"); - message.setBody(mp); - - try { - addTextBodyPart(mp, "text/html", - EmailContent.Body.restoreBodyHtmlWithMessageId(context, localMessage.mId)); - } catch (RuntimeException rte) { - LogUtils.d(Logging.LOG_TAG, "Exception while reading html body " + rte.toString()); - } - - try { - addTextBodyPart(mp, "text/plain", - EmailContent.Body.restoreBodyTextWithMessageId(context, localMessage.mId)); - } catch (RuntimeException rte) { - LogUtils.d(Logging.LOG_TAG, "Exception while reading text body " + rte.toString()); - } - - // Attachments - final Uri uri = ContentUris.withAppendedId(Attachment.MESSAGE_ID_URI, localMessage.mId); - final Cursor attachments = - context.getContentResolver().query(uri, Attachment.CONTENT_PROJECTION, - null, null, null); - - try { - while (attachments != null && attachments.moveToNext()) { - final Attachment att = new Attachment(); - att.restore(attachments); - try { - final InputStream content; - if (att.mContentBytes != null) { - // This is generally only the case for synthetic attachments, such as those - // generated by unit tests or calendar invites - content = new ByteArrayInputStream(att.mContentBytes); - } else { - String contentUriString = att.getCachedFileUri(); - if (TextUtils.isEmpty(contentUriString)) { - contentUriString = att.getContentUri(); - } - if (TextUtils.isEmpty(contentUriString)) { - content = null; - } else { - final Uri contentUri = Uri.parse(contentUriString); - content = context.getContentResolver().openInputStream(contentUri); - } - } - final String mimeType = att.mMimeType; - final Long contentSize = att.mSize; - final String contentId = att.mContentId; - final String filename = att.mFileName; - if (content != null) { - addAttachmentPart(mp, mimeType, contentSize, filename, contentId, content); - } else { - LogUtils.e(LogUtils.TAG, "Could not open attachment file for upsync"); - } - } catch (final FileNotFoundException e) { - LogUtils.e(LogUtils.TAG, "File Not Found error on %s while upsyncing message", - att.getCachedFileUri()); - } - } - } finally { - if (attachments != null) { - attachments.close(); - } - } - - return message; - } - - /** - * Helper method to add a body part for a given type of text, if found - * - * @param mp The text body part will be added to this multipart - * @param contentType The content-type of the text being added - * @param partText The text to add. If null, nothing happens - */ - private static void addTextBodyPart(final MimeMultipart mp, final String contentType, - final String partText) - throws MessagingException { - if (partText == null) { - return; - } - final TextBody body = new TextBody(partText); - final MimeBodyPart bp = new MimeBodyPart(body, contentType); - mp.addBodyPart(bp); - } - - /** - * Helper method to add an attachment part - * - * @param mp Multipart message to append attachment part to - * @param contentType Mime type - * @param contentSize Attachment metadata: unencoded file size - * @param filename Attachment metadata: file name - * @param contentId as referenced from cid: uris in the message body (if applicable) - * @param content unencoded bytes - */ - @VisibleForTesting - protected static void addAttachmentPart(final Multipart mp, final String contentType, - final Long contentSize, final String filename, final String contentId, - final InputStream content) throws MessagingException { - final Base64Body body = new Base64Body(content); - final MimeBodyPart bp = new MimeBodyPart(body, contentType); - bp.setHeader(MimeHeader.HEADER_CONTENT_TRANSFER_ENCODING, "base64"); - bp.setHeader(MimeHeader.HEADER_CONTENT_DISPOSITION, "attachment;\n " - + (!TextUtils.isEmpty(filename) ? "filename=\"" + filename + "\";" : "") - + "size=" + contentSize); - if (contentId != null) { - bp.setHeader(MimeHeader.HEADER_CONTENT_ID, contentId); - } - mp.addBodyPart(bp); - } - - /** - * Infer mailbox type from mailbox name. Used by MessagingController (for live folder sync). - * - * Deprecation: this should be configured in the UI, in conjunction with RF6154 support - */ - @Deprecated - public static synchronized int inferMailboxTypeFromName(Context context, String mailboxName) { - if (sServerMailboxNames.size() == 0) { - // preload the hashmap, one time only - sServerMailboxNames.put( - context.getString(R.string.mailbox_name_server_inbox), - Mailbox.TYPE_INBOX); - sServerMailboxNames.put( - context.getString(R.string.mailbox_name_server_outbox), - Mailbox.TYPE_OUTBOX); - sServerMailboxNames.put( - context.getString(R.string.mailbox_name_server_drafts), - Mailbox.TYPE_DRAFTS); - sServerMailboxNames.put( - context.getString(R.string.mailbox_name_server_trash), - Mailbox.TYPE_TRASH); - sServerMailboxNames.put( - context.getString(R.string.mailbox_name_server_sent), - Mailbox.TYPE_SENT); - sServerMailboxNames.put( - context.getString(R.string.mailbox_name_server_junk), - Mailbox.TYPE_JUNK); - } - if (mailboxName == null || mailboxName.length() == 0) { - return Mailbox.TYPE_MAIL; - } - Integer type = sServerMailboxNames.get(mailboxName); - if (type != null) { - return type; - } - return Mailbox.TYPE_MAIL; - } -} diff --git a/src/com/android/email/NotificationController.java b/src/com/android/email/NotificationController.java deleted file mode 100644 index 675140632..000000000 --- a/src/com/android/email/NotificationController.java +++ /dev/null @@ -1,842 +0,0 @@ -/* - * 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; - -import android.app.Notification; -import android.app.Notification.Builder; -import android.app.NotificationManager; -import android.app.PendingIntent; -import android.content.ContentResolver; -import android.content.ContentUris; -import android.content.Context; -import android.content.Intent; -import android.database.ContentObserver; -import android.database.Cursor; -import android.graphics.Bitmap; -import android.net.Uri; -import android.os.Handler; -import android.os.Looper; -import android.os.Process; -import android.provider.Settings; -import android.support.v4.app.NotificationCompat; -import android.text.TextUtils; -import android.text.format.DateUtils; - -import com.android.email.activity.setup.AccountSecurity; -import com.android.email.activity.setup.HeadlessAccountSettingsLoader; -import com.android.email.provider.EmailProvider; -import com.android.email.service.EmailServiceUtils; -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.Message; -import com.android.emailcommon.provider.Mailbox; -import com.android.emailcommon.utility.EmailAsyncTask; -import com.android.mail.preferences.FolderPreferences; -import com.android.mail.providers.Folder; -import com.android.mail.providers.UIProvider; -import com.android.mail.utils.Clock; -import com.android.mail.utils.LogTag; -import com.android.mail.utils.LogUtils; -import com.android.mail.utils.NotificationUtils; - -import java.util.HashMap; -import java.util.HashSet; -import java.util.Map; -import java.util.Set; - -/** - * Class that manages notifications. - */ -public class NotificationController { - private static final String LOG_TAG = LogTag.getLogTag(); - - private static final int NOTIFICATION_ID_ATTACHMENT_WARNING = 3; - private static final int NOTIFICATION_ID_PASSWORD_EXPIRING = 4; - private static final int NOTIFICATION_ID_PASSWORD_EXPIRED = 5; - - private static final int NOTIFICATION_ID_BASE_MASK = 0xF0000000; - private static final int NOTIFICATION_ID_BASE_LOGIN_WARNING = 0x20000000; - private static final int NOTIFICATION_ID_BASE_SECURITY_NEEDED = 0x30000000; - private static final int NOTIFICATION_ID_BASE_SECURITY_CHANGED = 0x40000000; - - private static NotificationThread sNotificationThread; - private static Handler sNotificationHandler; - private static NotificationController sInstance; - private final Context mContext; - private final NotificationManager mNotificationManager; - private final Clock mClock; - /** Maps account id to its observer */ - private final Map mNotificationMap = - new HashMap(); - private ContentObserver mAccountObserver; - - /** Constructor */ - protected NotificationController(Context context, Clock clock) { - mContext = context.getApplicationContext(); - EmailContent.init(context); - mNotificationManager = (NotificationManager) context.getSystemService( - Context.NOTIFICATION_SERVICE); - mClock = clock; - } - - /** Singleton access */ - public static synchronized NotificationController getInstance(Context context) { - if (sInstance == null) { - sInstance = new NotificationController(context, Clock.INSTANCE); - } - return sInstance; - } - - /** - * Return whether or not a notification, based on the passed-in id, needs to be "ongoing" - * @param notificationId the notification id to check - * @return whether or not the notification must be "ongoing" - */ - private static boolean needsOngoingNotification(int notificationId) { - // "Security needed" must be ongoing so that the user doesn't close it; otherwise, sync will - // be prevented until a reboot. Consider also doing this for password expired. - return (notificationId & NOTIFICATION_ID_BASE_MASK) == NOTIFICATION_ID_BASE_SECURITY_NEEDED; - } - - /** - * Returns a {@link android.support.v4.app.NotificationCompat.Builder} for an event with the - * given account. The account contains specific rules on ring tone usage and these will be used - * to modify the notification behaviour. - * - * @param accountId The id of the account this notification is being built for. - * @param ticker Text displayed when the notification is first shown. May be {@code null}. - * @param title The first line of text. May NOT be {@code null}. - * @param contentText The second line of text. May NOT be {@code null}. - * @param intent The intent to start if the user clicks on the notification. - * @param number A number to display using {@link Builder#setNumber(int)}. May be {@code null}. - * @param enableAudio If {@code false}, do not play any sound. Otherwise, play sound according - * to the settings for the given account. - * @return A {@link Notification} that can be sent to the notification service. - */ - private NotificationCompat.Builder createBaseAccountNotificationBuilder(long accountId, - String ticker, CharSequence title, String contentText, Intent intent, - Integer number, boolean enableAudio, boolean ongoing) { - // Pending Intent - PendingIntent pending = null; - if (intent != null) { - pending = PendingIntent.getActivity( - mContext, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT); - } - - // NOTE: the ticker is not shown for notifications in the Holo UX - final NotificationCompat.Builder builder = new NotificationCompat.Builder(mContext) - .setContentTitle(title) - .setContentText(contentText) - .setContentIntent(pending) - .setNumber(number == null ? 0 : number) - .setSmallIcon(R.drawable.ic_notification_mail_24dp) - .setWhen(mClock.getTime()) - .setTicker(ticker) - .setOngoing(ongoing); - - if (enableAudio) { - Account account = Account.restoreAccountWithId(mContext, accountId); - setupSoundAndVibration(builder, account); - } - - return builder; - } - - /** - * Generic notifier for any account. Uses notification rules from account. - * - * @param accountId The account id this notification is being built for. - * @param ticker Text displayed when the notification is first shown. May be {@code null}. - * @param title The first line of text. May NOT be {@code null}. - * @param contentText The second line of text. May NOT be {@code null}. - * @param intent The intent to start if the user clicks on the notification. - * @param notificationId The ID of the notification to register with the service. - */ - private void showNotification(long accountId, String ticker, String title, - String contentText, Intent intent, int notificationId) { - final NotificationCompat.Builder builder = createBaseAccountNotificationBuilder(accountId, - ticker, title, contentText, intent, null, true, - needsOngoingNotification(notificationId)); - mNotificationManager.notify(notificationId, builder.build()); - } - - /** - * Tells the notification controller if it should be watching for changes to the message table. - * This is the main life cycle method for message notifications. When we stop observing - * database changes, we save the state [e.g. message ID and count] of the most recent - * notification shown to the user. And, when we start observing database changes, we restore - * the saved state. - */ - public void watchForMessages() { - ensureHandlerExists(); - // Run this on the message notification handler - sNotificationHandler.post(new Runnable() { - @Override - public void run() { - ContentResolver resolver = mContext.getContentResolver(); - - // otherwise, start new observers for all notified accounts - registerMessageNotification(Account.ACCOUNT_ID_COMBINED_VIEW); - // If we're already observing account changes, don't do anything else - if (mAccountObserver == null) { - LogUtils.i(LOG_TAG, "Observing account changes for notifications"); - mAccountObserver = new AccountContentObserver(sNotificationHandler, mContext); - resolver.registerContentObserver(Account.NOTIFIER_URI, true, mAccountObserver); - } - } - }); - } - - /** - * Ensures the notification handler exists and is ready to handle requests. - */ - - /** - * TODO: Notifications jump around too much because we get too many content updates. - * We should try to make the provider generate fewer updates instead. - */ - - private static final int NOTIFICATION_DELAYED_MESSAGE = 0; - private static final long NOTIFICATION_DELAY = 15 * DateUtils.SECOND_IN_MILLIS; - // True if we're coalescing notification updates - private static boolean sNotificationDelayedMessagePending; - // True if accounts have changed and we need to refresh everything - private static boolean sRefreshAllNeeded; - // Set of accounts we need to regenerate notifications for - private static final HashSet sRefreshAccountSet = new HashSet(); - // These should all be accessed on-thread, but just in case... - private static final Object sNotificationDelayedMessageLock = new Object(); - - private static synchronized void ensureHandlerExists() { - if (sNotificationThread == null) { - sNotificationThread = new NotificationThread(); - sNotificationHandler = new Handler(sNotificationThread.getLooper(), - new Handler.Callback() { - @Override - public boolean handleMessage(final android.os.Message message) { - /** - * To reduce spamming the notifications, we quiesce updates for a few - * seconds to batch them up, then handle them here. - */ - LogUtils.d(LOG_TAG, "Delayed notification processing"); - synchronized (sNotificationDelayedMessageLock) { - sNotificationDelayedMessagePending = false; - final Context context = (Context)message.obj; - if (sRefreshAllNeeded) { - sRefreshAllNeeded = false; - refreshAllNotificationsInternal(context); - } - for (final Long accountId : sRefreshAccountSet) { - refreshNotificationsForAccountInternal(context, accountId); - } - sRefreshAccountSet.clear(); - } - return true; - } - }); - } - } - - /** - * Registers an observer for changes to mailboxes in the given account. - * NOTE: This must be called on the notification handler thread. - * @param accountId The ID of the account to register the observer for. May be - * {@link Account#ACCOUNT_ID_COMBINED_VIEW} to register observers for all - * accounts that allow for user notification. - */ - private void registerMessageNotification(final long accountId) { - ContentResolver resolver = mContext.getContentResolver(); - if (accountId == Account.ACCOUNT_ID_COMBINED_VIEW) { - Cursor c = resolver.query( - Account.CONTENT_URI, EmailContent.ID_PROJECTION, - null, null, null); - try { - while (c.moveToNext()) { - long id = c.getLong(EmailContent.ID_PROJECTION_COLUMN); - registerMessageNotification(id); - } - } finally { - c.close(); - } - } else { - ContentObserver obs = mNotificationMap.get(accountId); - if (obs != null) return; // we're already observing; nothing to do - LogUtils.i(LOG_TAG, "Registering for notifications for account " + accountId); - ContentObserver observer = new MessageContentObserver( - sNotificationHandler, mContext, accountId); - resolver.registerContentObserver(Message.NOTIFIER_URI, true, observer); - mNotificationMap.put(accountId, observer); - // Now, ping the observer for any initial notifications - observer.onChange(true); - } - } - - /** - * Unregisters the observer for the given account. If the specified account does not have - * a registered observer, no action is performed. This will not clear any existing notification - * for the specified account. Use {@link NotificationManager#cancel(int)}. - * NOTE: This must be called on the notification handler thread. - * @param accountId The ID of the account to unregister from. To unregister all accounts that - * have observers, specify an ID of {@link Account#ACCOUNT_ID_COMBINED_VIEW}. - */ - private void unregisterMessageNotification(final long accountId) { - ContentResolver resolver = mContext.getContentResolver(); - if (accountId == Account.ACCOUNT_ID_COMBINED_VIEW) { - LogUtils.i(LOG_TAG, "Unregistering notifications for all accounts"); - // cancel all existing message observers - for (ContentObserver observer : mNotificationMap.values()) { - resolver.unregisterContentObserver(observer); - } - mNotificationMap.clear(); - } else { - LogUtils.i(LOG_TAG, "Unregistering notifications for account " + accountId); - ContentObserver observer = mNotificationMap.remove(accountId); - if (observer != null) { - resolver.unregisterContentObserver(observer); - } - } - } - - public static final String EXTRA_ACCOUNT = "account"; - public static final String EXTRA_CONVERSATION = "conversationUri"; - public static final String EXTRA_FOLDER = "folder"; - - /** Sets up the notification's sound and vibration based upon account details. */ - private void setupSoundAndVibration( - NotificationCompat.Builder builder, Account account) { - String ringtoneUri = Settings.System.DEFAULT_NOTIFICATION_URI.toString(); - boolean vibrate = false; - - // Use the Inbox notification preferences - final Cursor accountCursor = mContext.getContentResolver().query(EmailProvider.uiUri( - "uiaccount", account.mId), UIProvider.ACCOUNTS_PROJECTION, null, null, null); - - com.android.mail.providers.Account uiAccount = null; - try { - if (accountCursor.moveToFirst()) { - uiAccount = com.android.mail.providers.Account.builder().buildFrom(accountCursor); - } - } finally { - accountCursor.close(); - } - - if (uiAccount != null) { - final Cursor folderCursor = - mContext.getContentResolver().query(uiAccount.settings.defaultInbox, - UIProvider.FOLDERS_PROJECTION, null, null, null); - - if (folderCursor == null) { - // This can happen when the notification is for the security policy notification - // that happens before the account is setup - LogUtils.w(LOG_TAG, "Null folder cursor for mailbox %s", - uiAccount.settings.defaultInbox); - } else { - Folder folder = null; - try { - if (folderCursor.moveToFirst()) { - folder = new Folder(folderCursor); - } - } finally { - folderCursor.close(); - } - - if (folder != null) { - final FolderPreferences folderPreferences = new FolderPreferences( - mContext, uiAccount.getEmailAddress(), folder, true /* inbox */); - - ringtoneUri = folderPreferences.getNotificationRingtoneUri(); - vibrate = folderPreferences.isNotificationVibrateEnabled(); - } else { - LogUtils.e(LOG_TAG, - "Null folder for mailbox %s", uiAccount.settings.defaultInbox); - } - } - } else { - LogUtils.e(LOG_TAG, "Null uiAccount for account id %d", account.mId); - } - - int defaults = Notification.DEFAULT_LIGHTS; - if (vibrate) { - defaults |= Notification.DEFAULT_VIBRATE; - } - - builder.setSound(TextUtils.isEmpty(ringtoneUri) ? null : Uri.parse(ringtoneUri)) - .setDefaults(defaults); - } - - /** - * Show (or update) a notification that the given attachment could not be forwarded. This - * is a very unusual case, and perhaps we shouldn't even send a notification. For now, - * it's helpful for debugging. - * - * NOTE: DO NOT CALL THIS METHOD FROM THE UI THREAD (DATABASE ACCESS) - */ - public void showDownloadForwardFailedNotificationSynchronous(Attachment attachment) { - final Message message = Message.restoreMessageWithId(mContext, attachment.mMessageKey); - if (message == null) return; - final Mailbox mailbox = Mailbox.restoreMailboxWithId(mContext, message.mMailboxKey); - showNotification(mailbox.mAccountKey, - mContext.getString(R.string.forward_download_failed_ticker), - mContext.getString(R.string.forward_download_failed_title), - attachment.mFileName, - null, - NOTIFICATION_ID_ATTACHMENT_WARNING); - } - - /** - * Returns a notification ID for login failed notifications for the given account account. - */ - private static int getLoginFailedNotificationId(long accountId) { - return NOTIFICATION_ID_BASE_LOGIN_WARNING + (int)accountId; - } - - /** - * Show (or update) a notification that there was a login failure for the given account. - * - * NOTE: DO NOT CALL THIS METHOD FROM THE UI THREAD (DATABASE ACCESS) - */ - public void showLoginFailedNotificationSynchronous(long accountId, boolean incoming) { - final Account account = Account.restoreAccountWithId(mContext, accountId); - if (account == null) return; - final Mailbox mailbox = Mailbox.restoreMailboxOfType(mContext, accountId, - Mailbox.TYPE_INBOX); - if (mailbox == null) return; - - final Intent settingsIntent; - if (incoming) { - settingsIntent = new Intent(Intent.ACTION_VIEW, - HeadlessAccountSettingsLoader.getIncomingSettingsUri(accountId)); - } else { - settingsIntent = new Intent(Intent.ACTION_VIEW, - HeadlessAccountSettingsLoader.getOutgoingSettingsUri(accountId)); - } - showNotification(mailbox.mAccountKey, - mContext.getString(R.string.login_failed_ticker, account.mDisplayName), - mContext.getString(R.string.login_failed_title), - account.getDisplayName(), - settingsIntent, - getLoginFailedNotificationId(accountId)); - } - - /** - * Cancels the login failed notification for the given account. - */ - public void cancelLoginFailedNotification(long accountId) { - mNotificationManager.cancel(getLoginFailedNotificationId(accountId)); - } - - /** - * Show (or update) a notification that the user's password is expiring. The given account - * is used to update the display text, but, all accounts share the same notification ID. - * - * NOTE: DO NOT CALL THIS METHOD FROM THE UI THREAD (DATABASE ACCESS) - */ - public void showPasswordExpiringNotificationSynchronous(long accountId) { - final Account account = Account.restoreAccountWithId(mContext, accountId); - if (account == null) return; - - final Intent intent = AccountSecurity.actionDevicePasswordExpirationIntent(mContext, - accountId, false); - final String accountName = account.getDisplayName(); - final String ticker = - mContext.getString(R.string.password_expire_warning_ticker_fmt, accountName); - final String title = mContext.getString(R.string.password_expire_warning_content_title); - showNotification(accountId, ticker, title, accountName, intent, - NOTIFICATION_ID_PASSWORD_EXPIRING); - } - - /** - * Show (or update) a notification that the user's password has expired. The given account - * is used to update the display text, but, all accounts share the same notification ID. - * - * NOTE: DO NOT CALL THIS METHOD FROM THE UI THREAD (DATABASE ACCESS) - */ - public void showPasswordExpiredNotificationSynchronous(long accountId) { - final Account account = Account.restoreAccountWithId(mContext, accountId); - if (account == null) return; - - final Intent intent = AccountSecurity.actionDevicePasswordExpirationIntent(mContext, - accountId, true); - final String accountName = account.getDisplayName(); - final String ticker = mContext.getString(R.string.password_expired_ticker); - final String title = mContext.getString(R.string.password_expired_content_title); - showNotification(accountId, ticker, title, accountName, intent, - NOTIFICATION_ID_PASSWORD_EXPIRED); - } - - /** - * Cancels any password expire notifications [both expired & expiring]. - */ - public void cancelPasswordExpirationNotifications() { - mNotificationManager.cancel(NOTIFICATION_ID_PASSWORD_EXPIRING); - mNotificationManager.cancel(NOTIFICATION_ID_PASSWORD_EXPIRED); - } - - /** - * Show (or update) a security needed notification. If tapped, the user is taken to a - * dialog asking whether he wants to update his settings. - */ - public void showSecurityNeededNotification(Account account) { - Intent intent = AccountSecurity.actionUpdateSecurityIntent(mContext, account.mId, true); - String accountName = account.getDisplayName(); - String ticker = - mContext.getString(R.string.security_needed_ticker_fmt, accountName); - String title = mContext.getString(R.string.security_notification_content_update_title); - showNotification(account.mId, ticker, title, accountName, intent, - (int)(NOTIFICATION_ID_BASE_SECURITY_NEEDED + account.mId)); - } - - /** - * Show (or update) a security changed notification. If tapped, the user is taken to the - * account settings screen where he can view the list of enforced policies - */ - public void showSecurityChangedNotification(Account account) { - final Intent intent = new Intent(Intent.ACTION_VIEW, - HeadlessAccountSettingsLoader.getIncomingSettingsUri(account.getId())); - final String accountName = account.getDisplayName(); - final String ticker = - mContext.getString(R.string.security_changed_ticker_fmt, accountName); - final String title = - mContext.getString(R.string.security_notification_content_change_title); - showNotification(account.mId, ticker, title, accountName, intent, - (int)(NOTIFICATION_ID_BASE_SECURITY_CHANGED + account.mId)); - } - - /** - * Show (or update) a security unsupported notification. If tapped, the user is taken to the - * account settings screen where he can view the list of unsupported policies - */ - public void showSecurityUnsupportedNotification(Account account) { - final Intent intent = new Intent(Intent.ACTION_VIEW, - HeadlessAccountSettingsLoader.getIncomingSettingsUri(account.getId())); - final String accountName = account.getDisplayName(); - final String ticker = - mContext.getString(R.string.security_unsupported_ticker_fmt, accountName); - final String title = - mContext.getString(R.string.security_notification_content_unsupported_title); - showNotification(account.mId, ticker, title, accountName, intent, - (int)(NOTIFICATION_ID_BASE_SECURITY_NEEDED + account.mId)); - } - - /** - * Cancels all security needed notifications. - */ - public void cancelSecurityNeededNotification() { - EmailAsyncTask.runAsyncParallel(new Runnable() { - @Override - public void run() { - Cursor c = mContext.getContentResolver().query(Account.CONTENT_URI, - Account.ID_PROJECTION, null, null, null); - try { - while (c.moveToNext()) { - long id = c.getLong(Account.ID_PROJECTION_COLUMN); - mNotificationManager.cancel( - (int)(NOTIFICATION_ID_BASE_SECURITY_NEEDED + id)); - } - } - finally { - c.close(); - } - }}); - } - - /** - * Cancels all notifications for the specified account id. This includes new mail notifications, - * as well as special login/security notifications. - */ - public static void cancelNotifications(final Context context, final Account account) { - final EmailServiceUtils.EmailServiceInfo serviceInfo - = EmailServiceUtils.getServiceInfoForAccount(context, account.mId); - if (serviceInfo == null) { - LogUtils.d(LOG_TAG, "Can't cancel notification for missing account %d", account.mId); - return; - } - final android.accounts.Account notifAccount - = account.getAccountManagerAccount(serviceInfo.accountType); - - NotificationUtils.clearAccountNotifications(context, notifAccount); - - final NotificationManager notificationManager = getInstance(context).mNotificationManager; - - notificationManager.cancel((int) (NOTIFICATION_ID_BASE_LOGIN_WARNING + account.mId)); - notificationManager.cancel((int) (NOTIFICATION_ID_BASE_SECURITY_NEEDED + account.mId)); - notificationManager.cancel((int) (NOTIFICATION_ID_BASE_SECURITY_CHANGED + account.mId)); - } - - private static void refreshNotificationsForAccount(final Context context, - final long accountId) { - synchronized (sNotificationDelayedMessageLock) { - if (sNotificationDelayedMessagePending) { - sRefreshAccountSet.add(accountId); - } else { - ensureHandlerExists(); - sNotificationHandler.sendMessageDelayed( - android.os.Message.obtain(sNotificationHandler, - NOTIFICATION_DELAYED_MESSAGE, context), NOTIFICATION_DELAY); - sNotificationDelayedMessagePending = true; - refreshNotificationsForAccountInternal(context, accountId); - } - } - } - - private static void refreshNotificationsForAccountInternal(final Context context, - final long accountId) { - final Uri accountUri = EmailProvider.uiUri("uiaccount", accountId); - - final ContentResolver contentResolver = context.getContentResolver(); - - final Cursor mailboxCursor = contentResolver.query( - ContentUris.withAppendedId(EmailContent.MAILBOX_NOTIFICATION_URI, accountId), - null, null, null, null); - try { - while (mailboxCursor.moveToNext()) { - final long mailboxId = - mailboxCursor.getLong(EmailContent.NOTIFICATION_MAILBOX_ID_COLUMN); - if (mailboxId == 0) continue; - - final int unseenCount = mailboxCursor.getInt( - EmailContent.NOTIFICATION_MAILBOX_UNSEEN_COUNT_COLUMN); - - final int unreadCount; - // If nothing is unseen, clear the notification - if (unseenCount == 0) { - unreadCount = 0; - } else { - unreadCount = mailboxCursor.getInt( - EmailContent.NOTIFICATION_MAILBOX_UNREAD_COUNT_COLUMN); - } - - final Uri folderUri = EmailProvider.uiUri("uifolder", mailboxId); - - - LogUtils.d(LOG_TAG, "Changes to account " + accountId + ", folder: " - + mailboxId + ", unreadCount: " + unreadCount + ", unseenCount: " - + unseenCount); - - final Intent intent = new Intent(UIProvider.ACTION_UPDATE_NOTIFICATION); - intent.setPackage(context.getPackageName()); - intent.setType(EmailProvider.EMAIL_APP_MIME_TYPE); - - intent.putExtra(UIProvider.UpdateNotificationExtras.EXTRA_ACCOUNT, accountUri); - intent.putExtra(UIProvider.UpdateNotificationExtras.EXTRA_FOLDER, folderUri); - intent.putExtra(UIProvider.UpdateNotificationExtras.EXTRA_UPDATED_UNREAD_COUNT, - unreadCount); - intent.putExtra(UIProvider.UpdateNotificationExtras.EXTRA_UPDATED_UNSEEN_COUNT, - unseenCount); - - context.sendOrderedBroadcast(intent, null); - } - } finally { - mailboxCursor.close(); - } - } - - public static void handleUpdateNotificationIntent(Context context, Intent intent) { - final Uri accountUri = - intent.getParcelableExtra(UIProvider.UpdateNotificationExtras.EXTRA_ACCOUNT); - final Uri folderUri = - intent.getParcelableExtra(UIProvider.UpdateNotificationExtras.EXTRA_FOLDER); - final int unreadCount = intent.getIntExtra( - UIProvider.UpdateNotificationExtras.EXTRA_UPDATED_UNREAD_COUNT, 0); - final int unseenCount = intent.getIntExtra( - UIProvider.UpdateNotificationExtras.EXTRA_UPDATED_UNSEEN_COUNT, 0); - - final ContentResolver contentResolver = context.getContentResolver(); - - final Cursor accountCursor = contentResolver.query(accountUri, - UIProvider.ACCOUNTS_PROJECTION, null, null, null); - - if (accountCursor == null) { - LogUtils.e(LOG_TAG, "Null account cursor for account " + accountUri); - return; - } - - com.android.mail.providers.Account account = null; - try { - if (accountCursor.moveToFirst()) { - account = com.android.mail.providers.Account.builder().buildFrom(accountCursor); - } - } finally { - accountCursor.close(); - } - - if (account == null) { - LogUtils.d(LOG_TAG, "Tried to create a notification for a missing account " - + accountUri); - return; - } - - final Cursor folderCursor = contentResolver.query(folderUri, UIProvider.FOLDERS_PROJECTION, - null, null, null); - - if (folderCursor == null) { - LogUtils.e(LOG_TAG, "Null folder cursor for account " + accountUri + ", mailbox " - + folderUri); - return; - } - - Folder folder = null; - try { - if (folderCursor.moveToFirst()) { - folder = new Folder(folderCursor); - } else { - LogUtils.e(LOG_TAG, "Empty folder cursor for account " + accountUri + ", mailbox " - + folderUri); - return; - } - } finally { - folderCursor.close(); - } - - // TODO: we don't always want getAttention to be true, but we don't necessarily have a - // good heuristic for when it should or shouldn't be. - NotificationUtils.sendSetNewEmailIndicatorIntent(context, unreadCount, unseenCount, - account, folder, true /* getAttention */); - } - - private static void refreshAllNotifications(final Context context) { - synchronized (sNotificationDelayedMessageLock) { - if (sNotificationDelayedMessagePending) { - sRefreshAllNeeded = true; - } else { - ensureHandlerExists(); - sNotificationHandler.sendMessageDelayed( - android.os.Message.obtain(sNotificationHandler, - NOTIFICATION_DELAYED_MESSAGE, context), NOTIFICATION_DELAY); - sNotificationDelayedMessagePending = true; - refreshAllNotificationsInternal(context); - } - } - } - - private static void refreshAllNotificationsInternal(final Context context) { - NotificationUtils.resendNotifications( - context, false, null, null, null /* ContactFetcher */); - } - - /** - * Observer invoked whenever a message we're notifying the user about changes. - */ - private static class MessageContentObserver extends ContentObserver { - private final Context mContext; - private final long mAccountId; - - public MessageContentObserver( - final Handler handler, final Context context, final long accountId) { - super(handler); - mContext = context; - mAccountId = accountId; - } - - @Override - public void onChange(final boolean selfChange) { - refreshNotificationsForAccount(mContext, mAccountId); - } - } - - /** - * Observer invoked whenever an account is modified. This could mean the user changed the - * notification settings. - */ - private static class AccountContentObserver extends ContentObserver { - private final Context mContext; - public AccountContentObserver(final Handler handler, final Context context) { - super(handler); - mContext = context; - } - - @Override - public void onChange(final boolean selfChange) { - final ContentResolver resolver = mContext.getContentResolver(); - final Cursor c = resolver.query(Account.CONTENT_URI, EmailContent.ID_PROJECTION, - null, null, null); - final Set newAccountList = new HashSet(); - final Set removedAccountList = new HashSet(); - if (c == null) { - // Suspender time ... theoretically, this will never happen - LogUtils.wtf(LOG_TAG, "#onChange(); NULL response for account id query"); - return; - } - try { - while (c.moveToNext()) { - long accountId = c.getLong(EmailContent.ID_PROJECTION_COLUMN); - newAccountList.add(accountId); - } - } finally { - c.close(); - } - // NOTE: Looping over three lists is not necessarily the most efficient. However, the - // account lists are going to be very small, so, this will not be necessarily bad. - // Cycle through existing notification list and adjust as necessary - for (final long accountId : sInstance.mNotificationMap.keySet()) { - if (!newAccountList.remove(accountId)) { - // account id not in the current set of notifiable accounts - removedAccountList.add(accountId); - } - } - // A new account was added to the notification list - for (final long accountId : newAccountList) { - sInstance.registerMessageNotification(accountId); - } - // An account was removed from the notification list - for (final long accountId : removedAccountList) { - sInstance.unregisterMessageNotification(accountId); - } - - refreshAllNotifications(mContext); - } - } - - /** - * Thread to handle all notification actions through its own {@link Looper}. - */ - private static class NotificationThread implements Runnable { - /** Lock to ensure proper initialization */ - private final Object mLock = new Object(); - /** The {@link Looper} that handles messages for this thread */ - private Looper mLooper; - - public NotificationThread() { - new Thread(null, this, "EmailNotification").start(); - synchronized (mLock) { - while (mLooper == null) { - try { - mLock.wait(); - } catch (InterruptedException ex) { - // Loop around and wait again - } - } - } - } - - @Override - public void run() { - synchronized (mLock) { - Looper.prepare(); - mLooper = Looper.myLooper(); - mLock.notifyAll(); - } - Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND); - Looper.loop(); - } - - public Looper getLooper() { - return mLooper; - } - } -} diff --git a/src/com/android/email/PeekableInputStream.java b/src/com/android/email/PeekableInputStream.java deleted file mode 100644 index e1c35a2bd..000000000 --- a/src/com/android/email/PeekableInputStream.java +++ /dev/null @@ -1,80 +0,0 @@ -/* - * 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; - -import java.io.IOException; -import java.io.InputStream; - -/** - * A filtering InputStream that allows single byte "peeks" without consuming the byte. The - * client of this stream can call peek() to see the next available byte in the stream - * and a subsequent read will still return the peeked byte. - */ -public class PeekableInputStream extends InputStream { - private final InputStream mIn; - private boolean mPeeked; - private int mPeekedByte; - - public PeekableInputStream(InputStream in) { - this.mIn = in; - } - - @Override - public int read() throws IOException { - if (!mPeeked) { - return mIn.read(); - } else { - mPeeked = false; - return mPeekedByte; - } - } - - public int peek() throws IOException { - if (!mPeeked) { - mPeekedByte = read(); - mPeeked = true; - } - return mPeekedByte; - } - - @Override - public int read(byte[] b, int offset, int length) throws IOException { - if (!mPeeked) { - return mIn.read(b, offset, length); - } else { - b[0] = (byte)mPeekedByte; - mPeeked = false; - int r = mIn.read(b, offset + 1, length - 1); - if (r == -1) { - return 1; - } else { - return r + 1; - } - } - } - - @Override - public int read(byte[] b) throws IOException { - return read(b, 0, b.length); - } - - @Override - public String toString() { - return String.format("PeekableInputStream(in=%s, peeked=%b, peekedByte=%d)", - mIn.toString(), mPeeked, mPeekedByte); - } -} diff --git a/src/com/android/email/Preferences.java b/src/com/android/email/Preferences.java deleted file mode 100644 index 49df5fa33..000000000 --- a/src/com/android/email/Preferences.java +++ /dev/null @@ -1,280 +0,0 @@ -/* - * 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; - -import android.content.Context; -import android.content.SharedPreferences; -import android.text.TextUtils; - -import com.android.emailcommon.Logging; -import com.android.emailcommon.provider.Account; -import com.android.mail.utils.LogUtils; - -import org.json.JSONArray; -import org.json.JSONException; - -import java.util.Collections; -import java.util.HashSet; -import java.util.Set; -import java.util.UUID; - -public class Preferences { - - // Preferences file - public static final String PREFERENCES_FILE = "AndroidMail.Main"; - - // Preferences field names - @Deprecated - private static final String ACCOUNT_UUIDS = "accountUuids"; - private static final String ENABLE_DEBUG_LOGGING = "enableDebugLogging"; - private static final String ENABLE_EXCHANGE_LOGGING = "enableExchangeLogging"; - private static final String ENABLE_EXCHANGE_FILE_LOGGING = "enableExchangeFileLogging"; - private static final String ENABLE_STRICT_MODE = "enableStrictMode"; - private static final String DEVICE_UID = "deviceUID"; - private static final String ONE_TIME_INITIALIZATION_PROGRESS = "oneTimeInitializationProgress"; - private static final String LAST_ACCOUNT_USED = "lastAccountUsed"; - // The following are only used for migration - @Deprecated - private static final String AUTO_ADVANCE_DIRECTION = "autoAdvance"; - @Deprecated - private static final String TRUSTED_SENDERS = "trustedSenders"; - @Deprecated - private static final String CONFIRM_DELETE = "confirm_delete"; - @Deprecated - private static final String CONFIRM_SEND = "confirm_send"; - @Deprecated - private static final String SWIPE_DELETE = "swipe_delete"; - @Deprecated - private static final String CONV_LIST_ICON = "conversation_list_icons"; - @Deprecated - private static final String REPLY_ALL = "reply_all"; - - @Deprecated - public static final int AUTO_ADVANCE_NEWER = 0; - @Deprecated - public static final int AUTO_ADVANCE_OLDER = 1; - @Deprecated - public static final int AUTO_ADVANCE_MESSAGE_LIST = 2; - // "move to older" was the behavior on older versions. - @Deprecated - private static final int AUTO_ADVANCE_DEFAULT = AUTO_ADVANCE_OLDER; - @Deprecated - private static final boolean CONFIRM_DELETE_DEFAULT = false; - @Deprecated - private static final boolean CONFIRM_SEND_DEFAULT = false; - - @Deprecated - public static final String CONV_LIST_ICON_SENDER_IMAGE = "senderimage"; - @Deprecated - public static final String CONV_LIST_ICON_NONE = "none"; - @Deprecated - public static final String CONV_LIST_ICON_DEFAULT = CONV_LIST_ICON_SENDER_IMAGE; - - private static Preferences sPreferences; - - private final SharedPreferences mSharedPreferences; - - private Preferences(Context context) { - mSharedPreferences = context.getSharedPreferences(PREFERENCES_FILE, Context.MODE_PRIVATE); - } - - /** - * TODO need to think about what happens if this gets GCed along with the - * Activity that initialized it. Do we lose ability to read Preferences in - * further Activities? Maybe this should be stored in the Application - * context. - */ - public static synchronized Preferences getPreferences(Context context) { - if (sPreferences == null) { - sPreferences = new Preferences(context); - } - return sPreferences; - } - - public static SharedPreferences getSharedPreferences(Context context) { - return getPreferences(context).mSharedPreferences; - } - - public static String getLegacyBackupPreference(Context context) { - return getPreferences(context).mSharedPreferences.getString(ACCOUNT_UUIDS, null); - } - - public static void clearLegacyBackupPreference(Context context) { - getPreferences(context).mSharedPreferences.edit().remove(ACCOUNT_UUIDS).apply(); - } - - public void setEnableDebugLogging(boolean value) { - mSharedPreferences.edit().putBoolean(ENABLE_DEBUG_LOGGING, value).apply(); - } - - public boolean getEnableDebugLogging() { - return mSharedPreferences.getBoolean(ENABLE_DEBUG_LOGGING, false); - } - - public void setEnableExchangeLogging(boolean value) { - mSharedPreferences.edit().putBoolean(ENABLE_EXCHANGE_LOGGING, value).apply(); - } - - public boolean getEnableExchangeLogging() { - return mSharedPreferences.getBoolean(ENABLE_EXCHANGE_LOGGING, false); - } - - public void setEnableExchangeFileLogging(boolean value) { - mSharedPreferences.edit().putBoolean(ENABLE_EXCHANGE_FILE_LOGGING, value).apply(); - } - - public boolean getEnableExchangeFileLogging() { - return mSharedPreferences.getBoolean(ENABLE_EXCHANGE_FILE_LOGGING, false); - } - - public void setEnableStrictMode(boolean value) { - mSharedPreferences.edit().putBoolean(ENABLE_STRICT_MODE, value).apply(); - } - - public boolean getEnableStrictMode() { - return mSharedPreferences.getBoolean(ENABLE_STRICT_MODE, false); - } - - /** - * Generate a new "device UID". This is local to Email app only, to prevent possibility - * of correlation with any other user activities in any other apps. - * @return a persistent, unique ID - */ - public synchronized String getDeviceUID() { - String result = mSharedPreferences.getString(DEVICE_UID, null); - if (result == null) { - result = UUID.randomUUID().toString(); - mSharedPreferences.edit().putString(DEVICE_UID, result).apply(); - } - return result; - } - - public int getOneTimeInitializationProgress() { - return mSharedPreferences.getInt(ONE_TIME_INITIALIZATION_PROGRESS, 0); - } - - public void setOneTimeInitializationProgress(int progress) { - mSharedPreferences.edit().putInt(ONE_TIME_INITIALIZATION_PROGRESS, progress).apply(); - } - - /** @deprecated Only used for migration */ - @Deprecated - public int getAutoAdvanceDirection() { - return mSharedPreferences.getInt(AUTO_ADVANCE_DIRECTION, AUTO_ADVANCE_DEFAULT); - } - - /** @deprecated Only used for migration */ - @Deprecated - public String getConversationListIcon() { - return mSharedPreferences.getString(CONV_LIST_ICON, CONV_LIST_ICON_SENDER_IMAGE); - } - - /** @deprecated Only used for migration */ - @Deprecated - public boolean getConfirmDelete() { - return mSharedPreferences.getBoolean(CONFIRM_DELETE, CONFIRM_DELETE_DEFAULT); - } - - /** @deprecated Only used for migration */ - @Deprecated - public boolean getConfirmSend() { - return mSharedPreferences.getBoolean(CONFIRM_SEND, CONFIRM_SEND_DEFAULT); - } - - /** @deprecated Only used for migration */ - @Deprecated - public boolean hasSwipeDelete() { - return mSharedPreferences.contains(SWIPE_DELETE); - } - - /** @deprecated Only used for migration */ - @Deprecated - public boolean getSwipeDelete() { - return mSharedPreferences.getBoolean(SWIPE_DELETE, false); - } - - /** @deprecated Only used for migration */ - @Deprecated - public boolean hasReplyAll() { - return mSharedPreferences.contains(REPLY_ALL); - } - - /** @deprecated Only used for migration */ - @Deprecated - public boolean getReplyAll() { - return mSharedPreferences.getBoolean(REPLY_ALL, false); - } - - /** - * @deprecated This has been moved to {@link com.android.mail.preferences.MailPrefs}, and is - * only here for migration. - */ - @Deprecated - public Set getWhitelistedSenderAddresses() { - try { - return parseEmailSet(mSharedPreferences.getString(TRUSTED_SENDERS, "")); - } catch (JSONException e) { - return Collections.emptySet(); - } - } - - HashSet parseEmailSet(String serialized) throws JSONException { - HashSet result = new HashSet(); - if (!TextUtils.isEmpty(serialized)) { - JSONArray arr = new JSONArray(serialized); - for (int i = 0, len = arr.length(); i < len; i++) { - result.add((String) arr.get(i)); - } - } - return result; - } - - /** - * Returns the last used account ID as set by {@link #setLastUsedAccountId}. - * The system makes no attempt to automatically track what is considered a "use" - clients - * are expected to call {@link #setLastUsedAccountId} manually. - * - * Note that the last used account may have been deleted in the background so there is also - * no guarantee that the account exists. - */ - public long getLastUsedAccountId() { - return mSharedPreferences.getLong(LAST_ACCOUNT_USED, Account.NO_ACCOUNT); - } - - /** - * Sets the specified ID of the last account used. Treated as an opaque ID and does not - * validate the value. Value is saved asynchronously. - */ - public void setLastUsedAccountId(long accountId) { - mSharedPreferences - .edit() - .putLong(LAST_ACCOUNT_USED, accountId) - .apply(); - } - - public void clear() { - mSharedPreferences.edit().clear().apply(); - } - - public void dump() { - if (Logging.LOGD) { - for (String key : mSharedPreferences.getAll().keySet()) { - LogUtils.v(Logging.LOG_TAG, key + " = " + mSharedPreferences.getAll().get(key)); - } - } - } -} diff --git a/src/com/android/email/ResourceHelper.java b/src/com/android/email/ResourceHelper.java deleted file mode 100644 index e3c15c12e..000000000 --- a/src/com/android/email/ResourceHelper.java +++ /dev/null @@ -1,87 +0,0 @@ -/* - * 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; - -import android.content.Context; -import android.content.res.Resources; -import android.content.res.TypedArray; -import android.graphics.Paint; - -/** - * Helper class to load resources. - */ -public class ResourceHelper { - public final static int UNDEFINED_RESOURCE_ID = -1; - - private static ResourceHelper sInstance; - private final Context mContext; - private final Resources mResources; - - private final int[] mAccountColors; - private final Paint[] mAccountColorPaints; - private final TypedArray mAccountColorArray; - - private ResourceHelper(Context context) { - mContext = context.getApplicationContext(); - mResources = mContext.getResources(); - - mAccountColorArray = mResources.obtainTypedArray(R.array.combined_view_account_colors); - mAccountColors = mResources.getIntArray(R.array.combined_view_account_colors); - mAccountColorPaints = new Paint[mAccountColors.length]; - for (int i = 0; i < mAccountColors.length; i++) { - Paint p = new Paint(); - p.setColor(mAccountColors[i]); - mAccountColorPaints[i] = p; - } - } - - public static synchronized ResourceHelper getInstance(Context context) { - if (sInstance == null) { - sInstance = new ResourceHelper(context); - } - return sInstance; - } - - /* package */ int getAccountColorIndex(long accountId) { - // The account ID is 1-based, so -1. - // Use abs so that it'd work for -1 as well. - return Math.abs((int) ((accountId - 1) % mAccountColors.length)); - } - - /** - * @return color for an account. - */ - public int getAccountColor(long accountId) { - return mAccountColors[getAccountColorIndex(accountId)]; - } - - /** - * @return The resource ID for an account color. - * Otherwise, {@value #UNDEFINED_RESOURCE_ID} if color was not specified via ID. - */ - public int getAccountColorId(long accountId) { - return mAccountColorArray.getResourceId(getAccountColorIndex(accountId), - UNDEFINED_RESOURCE_ID); - } - - /** - * @return {@link Paint} equivalent to {@link #getAccountColor}. - */ - public Paint getAccountColorPaint(long accountId) { - return mAccountColorPaints[getAccountColorIndex(accountId)]; - } -} diff --git a/src/com/android/email/SecurityPolicy.java b/src/com/android/email/SecurityPolicy.java deleted file mode 100644 index fe9f9b38f..000000000 --- a/src/com/android/email/SecurityPolicy.java +++ /dev/null @@ -1,904 +0,0 @@ -/* - * 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; - -import android.app.admin.DeviceAdminInfo; -import android.app.admin.DeviceAdminReceiver; -import android.app.admin.DevicePolicyManager; -import android.content.ComponentName; -import android.content.ContentProviderOperation; -import android.content.ContentResolver; -import android.content.ContentUris; -import android.content.ContentValues; -import android.content.Context; -import android.content.Intent; -import android.content.OperationApplicationException; -import android.database.Cursor; -import android.net.Uri; -import android.os.Bundle; -import android.os.RemoteException; - -import com.android.email.provider.AccountReconciler; -import com.android.email.provider.EmailProvider; -import com.android.email.service.EmailBroadcastProcessorService; -import com.android.email.service.EmailServiceUtils; -import com.android.emailcommon.Logging; -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.PolicyColumns; -import com.android.emailcommon.provider.Policy; -import com.android.emailcommon.utility.TextUtilities; -import com.android.emailcommon.utility.Utility; -import com.android.mail.utils.LogUtils; -import com.google.common.annotations.VisibleForTesting; - -import java.util.ArrayList; - -/** - * Utility functions to support reading and writing security policies, and handshaking the device - * into and out of various security states. - */ -public class SecurityPolicy { - private static final String TAG = "Email/SecurityPolicy"; - private static SecurityPolicy sInstance = null; - private Context mContext; - private DevicePolicyManager mDPM; - private final ComponentName mAdminName; - private Policy mAggregatePolicy; - - // Messages used for DevicePolicyManager callbacks - private static final int DEVICE_ADMIN_MESSAGE_ENABLED = 1; - private static final int DEVICE_ADMIN_MESSAGE_DISABLED = 2; - private static final int DEVICE_ADMIN_MESSAGE_PASSWORD_CHANGED = 3; - private static final int DEVICE_ADMIN_MESSAGE_PASSWORD_EXPIRING = 4; - - private static final String HAS_PASSWORD_EXPIRATION = - PolicyColumns.PASSWORD_EXPIRATION_DAYS + ">0"; - - /** - * Get the security policy instance - */ - public synchronized static SecurityPolicy getInstance(Context context) { - if (sInstance == null) { - sInstance = new SecurityPolicy(context.getApplicationContext()); - } - return sInstance; - } - - /** - * Private constructor (one time only) - */ - private SecurityPolicy(Context context) { - mContext = context.getApplicationContext(); - mDPM = null; - mAdminName = new ComponentName(context, PolicyAdmin.class); - mAggregatePolicy = null; - } - - /** - * For testing only: Inject context into already-created instance - */ - /* package */ void setContext(Context context) { - mContext = context; - } - - /** - * Compute the aggregate policy for all accounts that require it, and record it. - * - * The business logic is as follows: - * min password length take the max - * password mode take the max (strongest mode) - * max password fails take the min - * max screen lock time take the min - * require remote wipe take the max (logical or) - * password history take the max (strongest mode) - * password expiration take the min (strongest mode) - * password complex chars take the max (strongest mode) - * encryption take the max (logical or) - * - * @return a policy representing the strongest aggregate. If no policy sets are defined, - * a lightweight "nothing required" policy will be returned. Never null. - */ - @VisibleForTesting - Policy computeAggregatePolicy() { - boolean policiesFound = false; - Policy aggregate = new Policy(); - aggregate.mPasswordMinLength = Integer.MIN_VALUE; - aggregate.mPasswordMode = Integer.MIN_VALUE; - aggregate.mPasswordMaxFails = Integer.MAX_VALUE; - aggregate.mPasswordHistory = Integer.MIN_VALUE; - aggregate.mPasswordExpirationDays = Integer.MAX_VALUE; - aggregate.mPasswordComplexChars = Integer.MIN_VALUE; - aggregate.mMaxScreenLockTime = Integer.MAX_VALUE; - aggregate.mRequireRemoteWipe = false; - aggregate.mRequireEncryption = false; - - // This can never be supported at this time. It exists only for historic reasons where - // this was able to be supported prior to the introduction of proper removable storage - // support for external storage. - aggregate.mRequireEncryptionExternal = false; - - Cursor c = mContext.getContentResolver().query(Policy.CONTENT_URI, - Policy.CONTENT_PROJECTION, null, null, null); - Policy policy = new Policy(); - try { - while (c.moveToNext()) { - policy.restore(c); - if (DebugUtils.DEBUG) { - LogUtils.d(TAG, "Aggregate from: " + policy); - } - aggregate.mPasswordMinLength = - Math.max(policy.mPasswordMinLength, aggregate.mPasswordMinLength); - aggregate.mPasswordMode = Math.max(policy.mPasswordMode, aggregate.mPasswordMode); - if (policy.mPasswordMaxFails > 0) { - aggregate.mPasswordMaxFails = - Math.min(policy.mPasswordMaxFails, aggregate.mPasswordMaxFails); - } - if (policy.mMaxScreenLockTime > 0) { - aggregate.mMaxScreenLockTime = Math.min(policy.mMaxScreenLockTime, - aggregate.mMaxScreenLockTime); - } - if (policy.mPasswordHistory > 0) { - aggregate.mPasswordHistory = - Math.max(policy.mPasswordHistory, aggregate.mPasswordHistory); - } - if (policy.mPasswordExpirationDays > 0) { - aggregate.mPasswordExpirationDays = - Math.min(policy.mPasswordExpirationDays, aggregate.mPasswordExpirationDays); - } - if (policy.mPasswordComplexChars > 0) { - aggregate.mPasswordComplexChars = Math.max(policy.mPasswordComplexChars, - aggregate.mPasswordComplexChars); - } - aggregate.mRequireRemoteWipe |= policy.mRequireRemoteWipe; - aggregate.mRequireEncryption |= policy.mRequireEncryption; - aggregate.mDontAllowCamera |= policy.mDontAllowCamera; - policiesFound = true; - } - } finally { - c.close(); - } - if (policiesFound) { - // final cleanup pass converts any untouched min/max values to zero (not specified) - if (aggregate.mPasswordMinLength == Integer.MIN_VALUE) aggregate.mPasswordMinLength = 0; - if (aggregate.mPasswordMode == Integer.MIN_VALUE) aggregate.mPasswordMode = 0; - if (aggregate.mPasswordMaxFails == Integer.MAX_VALUE) aggregate.mPasswordMaxFails = 0; - if (aggregate.mMaxScreenLockTime == Integer.MAX_VALUE) aggregate.mMaxScreenLockTime = 0; - if (aggregate.mPasswordHistory == Integer.MIN_VALUE) aggregate.mPasswordHistory = 0; - if (aggregate.mPasswordExpirationDays == Integer.MAX_VALUE) - aggregate.mPasswordExpirationDays = 0; - if (aggregate.mPasswordComplexChars == Integer.MIN_VALUE) - aggregate.mPasswordComplexChars = 0; - if (DebugUtils.DEBUG) { - LogUtils.d(TAG, "Calculated Aggregate: " + aggregate); - } - return aggregate; - } - if (DebugUtils.DEBUG) { - LogUtils.d(TAG, "Calculated Aggregate: no policy"); - } - return Policy.NO_POLICY; - } - - /** - * Return updated aggregate policy, from cached value if possible - */ - public synchronized Policy getAggregatePolicy() { - if (mAggregatePolicy == null) { - mAggregatePolicy = computeAggregatePolicy(); - } - return mAggregatePolicy; - } - - /** - * Get the dpm. This mainly allows us to make some utility calls without it, for testing. - */ - /* package */ synchronized DevicePolicyManager getDPM() { - if (mDPM == null) { - mDPM = (DevicePolicyManager) mContext.getSystemService(Context.DEVICE_POLICY_SERVICE); - } - return mDPM; - } - - /** - * API: Report that policies may have been updated due to rewriting values in an Account; we - * clear the aggregate policy (so it can be recomputed) and set the policies in the DPM - */ - public synchronized void policiesUpdated() { - mAggregatePolicy = null; - setActivePolicies(); - } - - /** - * API: Report that policies may have been updated *and* the caller vouches that the - * change is a reduction in policies. This forces an immediate change to device state. - * Typically used when deleting accounts, although we may use it for server-side policy - * rollbacks. - */ - public void reducePolicies() { - if (DebugUtils.DEBUG) { - LogUtils.d(TAG, "reducePolicies"); - } - policiesUpdated(); - } - - /** - * API: Query used to determine if a given policy is "active" (the device is operating at - * the required security level). - * - * @param policy the policies requested, or null to check aggregate stored policies - * @return true if the requested policies are active, false if not. - */ - public boolean isActive(Policy policy) { - int reasons = getInactiveReasons(policy); - if (DebugUtils.DEBUG && (reasons != 0)) { - StringBuilder sb = new StringBuilder("isActive for " + policy + ": "); - sb.append("FALSE -> "); - if ((reasons & INACTIVE_NEED_ACTIVATION) != 0) { - sb.append("no_admin "); - } - if ((reasons & INACTIVE_NEED_CONFIGURATION) != 0) { - sb.append("config "); - } - if ((reasons & INACTIVE_NEED_PASSWORD) != 0) { - sb.append("password "); - } - if ((reasons & INACTIVE_NEED_ENCRYPTION) != 0) { - sb.append("encryption "); - } - if ((reasons & INACTIVE_PROTOCOL_POLICIES) != 0) { - sb.append("protocol "); - } - LogUtils.d(TAG, sb.toString()); - } - return reasons == 0; - } - - /** - * Return bits from isActive: Device Policy Manager has not been activated - */ - public final static int INACTIVE_NEED_ACTIVATION = 1; - - /** - * Return bits from isActive: Some required configuration is not correct (no user action). - */ - public final static int INACTIVE_NEED_CONFIGURATION = 2; - - /** - * Return bits from isActive: Password needs to be set or updated - */ - public final static int INACTIVE_NEED_PASSWORD = 4; - - /** - * Return bits from isActive: Encryption has not be enabled - */ - public final static int INACTIVE_NEED_ENCRYPTION = 8; - - /** - * Return bits from isActive: Protocol-specific policies cannot be enforced - */ - public final static int INACTIVE_PROTOCOL_POLICIES = 16; - - /** - * API: Query used to determine if a given policy is "active" (the device is operating at - * the required security level). - * - * This can be used when syncing a specific account, by passing a specific set of policies - * for that account. Or, it can be used at any time to compare the device - * state against the aggregate set of device policies stored in all accounts. - * - * This method is for queries only, and does not trigger any change in device state. - * - * NOTE: If there are multiple accounts with password expiration policies, the device - * password will be set to expire in the shortest required interval (most secure). This method - * will return 'false' as soon as the password expires - irrespective of which account caused - * the expiration. In other words, all accounts (that require expiration) will run/stop - * based on the requirements of the account with the shortest interval. - * - * @param policy the policies requested, or null to check aggregate stored policies - * @return zero if the requested policies are active, non-zero bits indicates that more work - * is needed (typically, by the user) before the required security polices are fully active. - */ - public int getInactiveReasons(Policy policy) { - // select aggregate set if needed - if (policy == null) { - policy = getAggregatePolicy(); - } - // quick check for the "empty set" of no policies - if (policy == Policy.NO_POLICY) { - return 0; - } - int reasons = 0; - DevicePolicyManager dpm = getDPM(); - if (isActiveAdmin()) { - // check each policy explicitly - if (policy.mPasswordMinLength > 0) { - if (dpm.getPasswordMinimumLength(mAdminName) < policy.mPasswordMinLength) { - reasons |= INACTIVE_NEED_PASSWORD; - } - } - if (policy.mPasswordMode > 0) { - if (dpm.getPasswordQuality(mAdminName) < policy.getDPManagerPasswordQuality()) { - reasons |= INACTIVE_NEED_PASSWORD; - } - if (!dpm.isActivePasswordSufficient()) { - reasons |= INACTIVE_NEED_PASSWORD; - } - } - if (policy.mMaxScreenLockTime > 0) { - // Note, we use seconds, dpm uses milliseconds - if (dpm.getMaximumTimeToLock(mAdminName) > policy.mMaxScreenLockTime * 1000) { - reasons |= INACTIVE_NEED_CONFIGURATION; - } - } - if (policy.mPasswordExpirationDays > 0) { - // confirm that expirations are currently set - long currentTimeout = dpm.getPasswordExpirationTimeout(mAdminName); - if (currentTimeout == 0 - || currentTimeout > policy.getDPManagerPasswordExpirationTimeout()) { - reasons |= INACTIVE_NEED_PASSWORD; - } - // confirm that the current password hasn't expired - long expirationDate = dpm.getPasswordExpiration(mAdminName); - long timeUntilExpiration = expirationDate - System.currentTimeMillis(); - boolean expired = timeUntilExpiration < 0; - if (expired) { - reasons |= INACTIVE_NEED_PASSWORD; - } - } - if (policy.mPasswordHistory > 0) { - if (dpm.getPasswordHistoryLength(mAdminName) < policy.mPasswordHistory) { - // There's no user action for changes here; this is just a configuration change - reasons |= INACTIVE_NEED_CONFIGURATION; - } - } - if (policy.mPasswordComplexChars > 0) { - if (dpm.getPasswordMinimumNonLetter(mAdminName) < policy.mPasswordComplexChars) { - reasons |= INACTIVE_NEED_PASSWORD; - } - } - if (policy.mRequireEncryption) { - int encryptionStatus = getDPM().getStorageEncryptionStatus(); - if (encryptionStatus != DevicePolicyManager.ENCRYPTION_STATUS_ACTIVE) { - reasons |= INACTIVE_NEED_ENCRYPTION; - } - } - if (policy.mDontAllowCamera && !dpm.getCameraDisabled(mAdminName)) { - reasons |= INACTIVE_NEED_CONFIGURATION; - } - // password failures are counted locally - no test required here - // no check required for remote wipe (it's supported, if we're the admin) - - if (policy.mProtocolPoliciesUnsupported != null) { - reasons |= INACTIVE_PROTOCOL_POLICIES; - } - - // If we made it all the way, reasons == 0 here. Otherwise it's a list of grievances. - return reasons; - } - // return false, not active - return INACTIVE_NEED_ACTIVATION; - } - - /** - * Set the requested security level based on the aggregate set of requests. - * If the set is empty, we release our device administration. If the set is non-empty, - * we only proceed if we are already active as an admin. - */ - public void setActivePolicies() { - DevicePolicyManager dpm = getDPM(); - // compute aggregate set of policies - Policy aggregatePolicy = getAggregatePolicy(); - // if empty set, detach from policy manager - if (aggregatePolicy == Policy.NO_POLICY) { - if (DebugUtils.DEBUG) { - LogUtils.d(TAG, "setActivePolicies: none, remove admin"); - } - dpm.removeActiveAdmin(mAdminName); - } else if (isActiveAdmin()) { - if (DebugUtils.DEBUG) { - LogUtils.d(TAG, "setActivePolicies: " + aggregatePolicy); - } - // set each policy in the policy manager - // password mode & length - dpm.setPasswordQuality(mAdminName, aggregatePolicy.getDPManagerPasswordQuality()); - dpm.setPasswordMinimumLength(mAdminName, aggregatePolicy.mPasswordMinLength); - // screen lock time - dpm.setMaximumTimeToLock(mAdminName, aggregatePolicy.mMaxScreenLockTime * 1000); - // local wipe (failed passwords limit) - dpm.setMaximumFailedPasswordsForWipe(mAdminName, aggregatePolicy.mPasswordMaxFails); - // password expiration (days until a password expires). API takes mSec. - dpm.setPasswordExpirationTimeout(mAdminName, - aggregatePolicy.getDPManagerPasswordExpirationTimeout()); - // password history length (number of previous passwords that may not be reused) - dpm.setPasswordHistoryLength(mAdminName, aggregatePolicy.mPasswordHistory); - // password minimum complex characters. - // Note, in Exchange, "complex chars" simply means "non alpha", but in the DPM, - // setting the quality to complex also defaults min symbols=1 and min numeric=1. - // We always / safely clear minSymbols & minNumeric to zero (there is no policy - // configuration in which we explicitly require a minimum number of digits or symbols.) - dpm.setPasswordMinimumSymbols(mAdminName, 0); - dpm.setPasswordMinimumNumeric(mAdminName, 0); - dpm.setPasswordMinimumNonLetter(mAdminName, aggregatePolicy.mPasswordComplexChars); - // Device capabilities - dpm.setCameraDisabled(mAdminName, aggregatePolicy.mDontAllowCamera); - - // encryption required - dpm.setStorageEncryption(mAdminName, aggregatePolicy.mRequireEncryption); - } - } - - /** - * Convenience method; see javadoc below - */ - public static void setAccountHoldFlag(Context context, long accountId, boolean newState) { - Account account = Account.restoreAccountWithId(context, accountId); - if (account != null) { - setAccountHoldFlag(context, account, newState); - if (newState) { - // Make sure there's a notification up - NotificationController.getInstance(context).showSecurityNeededNotification(account); - } - } - } - - /** - * API: Set/Clear the "hold" flag in any account. This flag serves a dual purpose: - * Setting it gives us an indication that it was blocked, and clearing it gives EAS a - * signal to try syncing again. - * @param context context - * @param account the account whose hold flag is to be set/cleared - * @param newState true = security hold, false = free to sync - */ - public static void setAccountHoldFlag(Context context, Account account, boolean newState) { - if (newState) { - account.mFlags |= Account.FLAGS_SECURITY_HOLD; - } else { - account.mFlags &= ~Account.FLAGS_SECURITY_HOLD; - } - ContentValues cv = new ContentValues(); - cv.put(AccountColumns.FLAGS, account.mFlags); - account.update(context, cv); - } - - /** - * API: Sync service should call this any time a sync fails due to isActive() returning false. - * This will kick off the notify-acquire-admin-state process and/or increase the security level. - * The caller needs to write the required policies into this account before making this call. - * Should not be called from UI thread - uses DB lookups to prepare new notifications - * - * @param accountId the account for which sync cannot proceed - */ - public void policiesRequired(long accountId) { - Account account = Account.restoreAccountWithId(mContext, accountId); - // In case the account has been deleted, just return - if (account == null) return; - if (account.mPolicyKey == 0) return; - Policy policy = Policy.restorePolicyWithId(mContext, account.mPolicyKey); - if (policy == null) return; - if (DebugUtils.DEBUG) { - LogUtils.d(TAG, "policiesRequired for " + account.mDisplayName + ": " + policy); - } - - // Mark the account as "on hold". - setAccountHoldFlag(mContext, account, true); - - // Put up an appropriate notification - if (policy.mProtocolPoliciesUnsupported == null) { - NotificationController.getInstance(mContext).showSecurityNeededNotification(account); - } else { - NotificationController.getInstance(mContext).showSecurityUnsupportedNotification( - account); - } - } - - public static void clearAccountPolicy(Context context, Account account) { - setAccountPolicy(context, account, null, null); - } - - /** - * Set the policy for an account atomically; this also removes any other policy associated with - * the account and sets the policy key for the account. If policy is null, the policyKey is - * set to 0 and the securitySyncKey to null. Also, update the account object to reflect the - * current policyKey and securitySyncKey - * @param context the caller's context - * @param account the account whose policy is to be set - * @param policy the policy to set, or null if we're clearing the policy - * @param securitySyncKey the security sync key for this account (ignored if policy is null) - */ - public static void setAccountPolicy(Context context, Account account, Policy policy, - String securitySyncKey) { - ArrayList ops = new ArrayList(); - - // Make sure this is a valid policy set - if (policy != null) { - policy.normalize(); - // Add the new policy (no account will yet reference this) - ops.add(ContentProviderOperation.newInsert( - Policy.CONTENT_URI).withValues(policy.toContentValues()).build()); - // Make the policyKey of the account our newly created policy, and set the sync key - ops.add(ContentProviderOperation.newUpdate( - ContentUris.withAppendedId(Account.CONTENT_URI, account.mId)) - .withValueBackReference(AccountColumns.POLICY_KEY, 0) - .withValue(AccountColumns.SECURITY_SYNC_KEY, securitySyncKey) - .build()); - } else { - ops.add(ContentProviderOperation.newUpdate( - ContentUris.withAppendedId(Account.CONTENT_URI, account.mId)) - .withValue(AccountColumns.SECURITY_SYNC_KEY, null) - .withValue(AccountColumns.POLICY_KEY, 0) - .build()); - } - - // Delete the previous policy associated with this account, if any - if (account.mPolicyKey > 0) { - ops.add(ContentProviderOperation.newDelete( - ContentUris.withAppendedId( - Policy.CONTENT_URI, account.mPolicyKey)).build()); - } - - try { - context.getContentResolver().applyBatch(EmailContent.AUTHORITY, ops); - account.refresh(context); - syncAccount(context, account); - } catch (RemoteException e) { - // This is fatal to a remote process - throw new IllegalStateException("Exception setting account policy."); - } catch (OperationApplicationException e) { - // Can't happen; our provider doesn't throw this exception - } - } - - private static void syncAccount(final Context context, final Account account) { - final EmailServiceUtils.EmailServiceInfo info = - EmailServiceUtils.getServiceInfo(context, account.getProtocol(context)); - final android.accounts.Account amAccount = - new android.accounts.Account(account.mEmailAddress, info.accountType); - final Bundle extras = new Bundle(3); - extras.putBoolean(ContentResolver.SYNC_EXTRAS_MANUAL, true); - extras.putBoolean(ContentResolver.SYNC_EXTRAS_DO_NOT_RETRY, true); - extras.putBoolean(ContentResolver.SYNC_EXTRAS_EXPEDITED, true); - ContentResolver.requestSync(amAccount, EmailContent.AUTHORITY, extras); - LogUtils.i(TAG, "requestSync SecurityPolicy syncAccount %s, %s", account.toString(), - extras.toString()); - } - - public void syncAccount(final Account account) { - syncAccount(mContext, account); - } - - public void setAccountPolicy(long accountId, Policy policy, String securityKey, - boolean notify) { - Account account = Account.restoreAccountWithId(mContext, accountId); - // In case the account has been deleted, just return - if (account == null) { - return; - } - Policy oldPolicy = null; - if (account.mPolicyKey > 0) { - oldPolicy = Policy.restorePolicyWithId(mContext, account.mPolicyKey); - } - - // If attachment policies have changed, fix up any affected attachment records - if (oldPolicy != null && securityKey != null) { - if ((oldPolicy.mDontAllowAttachments != policy.mDontAllowAttachments) || - (oldPolicy.mMaxAttachmentSize != policy.mMaxAttachmentSize)) { - Policy.setAttachmentFlagsForNewPolicy(mContext, account, policy); - } - } - - boolean policyChanged = (oldPolicy == null) || !oldPolicy.equals(policy); - if (!policyChanged && (TextUtilities.stringOrNullEquals(securityKey, - account.mSecuritySyncKey))) { - LogUtils.d(Logging.LOG_TAG, "setAccountPolicy; policy unchanged"); - } else { - setAccountPolicy(mContext, account, policy, securityKey); - policiesUpdated(); - } - - boolean setHold = false; - if (policy.mProtocolPoliciesUnsupported != null) { - // We can't support this, reasons in unsupportedRemotePolicies - LogUtils.d(Logging.LOG_TAG, - "Notify policies for " + account.mDisplayName + " not supported."); - setHold = true; - if (notify) { - NotificationController.getInstance(mContext).showSecurityUnsupportedNotification( - account); - } - // Erase data - Uri uri = EmailProvider.uiUri("uiaccountdata", accountId); - mContext.getContentResolver().delete(uri, null, null); - } else if (isActive(policy)) { - if (policyChanged) { - LogUtils.d(Logging.LOG_TAG, "Notify policies for " + account.mDisplayName - + " changed."); - if (notify) { - // Notify that policies changed - NotificationController.getInstance(mContext).showSecurityChangedNotification( - account); - } - } else { - LogUtils.d(Logging.LOG_TAG, "Policy is active and unchanged; do not notify."); - } - } else { - setHold = true; - LogUtils.d(Logging.LOG_TAG, "Notify policies for " + account.mDisplayName + - " are not being enforced."); - if (notify) { - // Put up a notification - NotificationController.getInstance(mContext).showSecurityNeededNotification( - account); - } - } - // Set/clear the account hold. - setAccountHoldFlag(mContext, account, setHold); - } - - /** - * Called from the notification's intent receiver to register that the notification can be - * cleared now. - */ - public void clearNotification() { - NotificationController.getInstance(mContext).cancelSecurityNeededNotification(); - } - - /** - * API: Remote wipe (from server). This is final, there is no confirmation. It will only - * return to the caller if there is an unexpected failure. The wipe includes external storage. - */ - public void remoteWipe() { - DevicePolicyManager dpm = getDPM(); - if (dpm.isAdminActive(mAdminName)) { - dpm.wipeData(DevicePolicyManager.WIPE_EXTERNAL_STORAGE); - } else { - LogUtils.d(Logging.LOG_TAG, "Could not remote wipe because not device admin."); - } - } - /** - * If we are not the active device admin, try to become so. - * - * Also checks for any policies that we have added during the lifetime of this app. - * This catches the case where the user granted an earlier (smaller) set of policies - * but an app upgrade requires that new policies be granted. - * - * @return true if we are already active, false if we are not - */ - public boolean isActiveAdmin() { - DevicePolicyManager dpm = getDPM(); - return dpm.isAdminActive(mAdminName) - && dpm.hasGrantedPolicy(mAdminName, DeviceAdminInfo.USES_POLICY_EXPIRE_PASSWORD) - && dpm.hasGrantedPolicy(mAdminName, DeviceAdminInfo.USES_ENCRYPTED_STORAGE) - && dpm.hasGrantedPolicy(mAdminName, DeviceAdminInfo.USES_POLICY_DISABLE_CAMERA); - } - - /** - * Report admin component name - for making calls into device policy manager - */ - public ComponentName getAdminComponent() { - return mAdminName; - } - - /** - * Delete all accounts whose security flags aren't zero (i.e. they have security enabled). - * This method is synchronous, so it should normally be called within a worker thread (the - * exception being for unit tests) - * - * @param context the caller's context - */ - /*package*/ void deleteSecuredAccounts(Context context) { - ContentResolver cr = context.getContentResolver(); - // Find all accounts with security and delete them - Cursor c = cr.query(Account.CONTENT_URI, EmailContent.ID_PROJECTION, - Account.SECURITY_NONZERO_SELECTION, null, null); - try { - LogUtils.w(TAG, "Email administration disabled; deleting " + c.getCount() + - " secured account(s)"); - while (c.moveToNext()) { - long accountId = c.getLong(EmailContent.ID_PROJECTION_COLUMN); - Uri uri = EmailProvider.uiUri("uiaccount", accountId); - cr.delete(uri, null, null); - } - } finally { - c.close(); - } - policiesUpdated(); - AccountReconciler.reconcileAccounts(context); - } - - /** - * Internal handler for enabled->disabled transitions. Deletes all secured accounts. - * Must call from worker thread, not on UI thread. - */ - /*package*/ void onAdminEnabled(boolean isEnabled) { - if (!isEnabled) { - deleteSecuredAccounts(mContext); - } - } - - /** - * Handle password expiration - if any accounts appear to have triggered this, put up - * warnings, or even shut them down. - * - * NOTE: If there are multiple accounts with password expiration policies, the device - * password will be set to expire in the shortest required interval (most secure). The logic - * in this method operates based on the aggregate setting - irrespective of which account caused - * the expiration. In other words, all accounts (that require expiration) will run/stop - * based on the requirements of the account with the shortest interval. - */ - private void onPasswordExpiring(Context context) { - // 1. Do we have any accounts that matter here? - long nextExpiringAccountId = findShortestExpiration(context); - - // 2. If not, exit immediately - if (nextExpiringAccountId == -1) { - return; - } - - // 3. If yes, are we warning or expired? - long expirationDate = getDPM().getPasswordExpiration(mAdminName); - long timeUntilExpiration = expirationDate - System.currentTimeMillis(); - boolean expired = timeUntilExpiration < 0; - if (!expired) { - // 4. If warning, simply put up a generic notification and report that it came from - // the shortest-expiring account. - NotificationController.getInstance(mContext).showPasswordExpiringNotificationSynchronous( - nextExpiringAccountId); - } else { - // 5. Actually expired - find all accounts that expire passwords, and wipe them - boolean wiped = wipeExpiredAccounts(context); - if (wiped) { - NotificationController.getInstance(mContext).showPasswordExpiredNotificationSynchronous( - nextExpiringAccountId); - } - } - } - - /** - * Find the account with the shortest expiration time. This is always assumed to be - * the account that forces the password to be refreshed. - * @return -1 if no expirations, or accountId if one is found - */ - @VisibleForTesting - /*package*/ static long findShortestExpiration(Context context) { - long policyId = Utility.getFirstRowLong(context, Policy.CONTENT_URI, Policy.ID_PROJECTION, - HAS_PASSWORD_EXPIRATION, null, PolicyColumns.PASSWORD_EXPIRATION_DAYS + " ASC", - EmailContent.ID_PROJECTION_COLUMN, -1L); - if (policyId < 0) return -1L; - return Policy.getAccountIdWithPolicyKey(context, policyId); - } - - /** - * For all accounts that require password expiration, put them in security hold and wipe - * their data. - * @param context context - * @return true if one or more accounts were wiped - */ - @VisibleForTesting - /*package*/ static boolean wipeExpiredAccounts(Context context) { - boolean result = false; - Cursor c = context.getContentResolver().query(Policy.CONTENT_URI, - Policy.ID_PROJECTION, HAS_PASSWORD_EXPIRATION, null, null); - if (c == null) { - return false; - } - try { - while (c.moveToNext()) { - long policyId = c.getLong(Policy.ID_PROJECTION_COLUMN); - long accountId = Policy.getAccountIdWithPolicyKey(context, policyId); - if (accountId < 0) continue; - Account account = Account.restoreAccountWithId(context, accountId); - if (account != null) { - // Mark the account as "on hold". - setAccountHoldFlag(context, account, true); - // Erase data - Uri uri = EmailProvider.uiUri("uiaccountdata", accountId); - context.getContentResolver().delete(uri, null, null); - // Report one or more were found - result = true; - } - } - } finally { - c.close(); - } - return result; - } - - /** - * Callback from EmailBroadcastProcessorService. This provides the workers for the - * DeviceAdminReceiver calls. These should perform the work directly and not use async - * threads for completion. - */ - public static void onDeviceAdminReceiverMessage(Context context, int message) { - SecurityPolicy instance = SecurityPolicy.getInstance(context); - switch (message) { - case DEVICE_ADMIN_MESSAGE_ENABLED: - instance.onAdminEnabled(true); - break; - case DEVICE_ADMIN_MESSAGE_DISABLED: - instance.onAdminEnabled(false); - break; - case DEVICE_ADMIN_MESSAGE_PASSWORD_CHANGED: - // TODO make a small helper for this - // Clear security holds (if any) - Account.clearSecurityHoldOnAllAccounts(context); - // Cancel any active notifications (if any are posted) - NotificationController.getInstance(context).cancelPasswordExpirationNotifications(); - break; - case DEVICE_ADMIN_MESSAGE_PASSWORD_EXPIRING: - instance.onPasswordExpiring(instance.mContext); - break; - } - } - - /** - * Device Policy administrator. This is primarily a listener for device state changes. - * Note: This is instantiated by incoming messages. - * Note: This is actually a BroadcastReceiver and must remain within the guidelines required - * for proper behavior, including avoidance of ANRs. - * Note: We do not implement onPasswordFailed() because the default behavior of the - * DevicePolicyManager - complete local wipe after 'n' failures - is sufficient. - */ - public static class PolicyAdmin extends DeviceAdminReceiver { - - /** - * Called after the administrator is first enabled. - */ - @Override - public void onEnabled(Context context, Intent intent) { - EmailBroadcastProcessorService.processDevicePolicyMessage(context, - DEVICE_ADMIN_MESSAGE_ENABLED); - } - - /** - * Called prior to the administrator being disabled. - */ - @Override - public void onDisabled(Context context, Intent intent) { - EmailBroadcastProcessorService.processDevicePolicyMessage(context, - DEVICE_ADMIN_MESSAGE_DISABLED); - } - - /** - * Called when the user asks to disable administration; we return a warning string that - * will be presented to the user - */ - @Override - public CharSequence onDisableRequested(Context context, Intent intent) { - return context.getString(R.string.disable_admin_warning); - } - - /** - * Called after the user has changed their password. - */ - @Override - public void onPasswordChanged(Context context, Intent intent) { - EmailBroadcastProcessorService.processDevicePolicyMessage(context, - DEVICE_ADMIN_MESSAGE_PASSWORD_CHANGED); - } - - /** - * Called when device password is expiring - */ - @Override - public void onPasswordExpiring(Context context, Intent intent) { - EmailBroadcastProcessorService.processDevicePolicyMessage(context, - DEVICE_ADMIN_MESSAGE_PASSWORD_EXPIRING); - } - } -} diff --git a/src/com/android/email/activity/setup/AccountCreationFragment.java b/src/com/android/email/activity/setup/AccountCreationFragment.java index e5736774c..1f0d685d7 100644 --- a/src/com/android/email/activity/setup/AccountCreationFragment.java +++ b/src/com/android/email/activity/setup/AccountCreationFragment.java @@ -28,6 +28,7 @@ import android.os.Bundle; import android.os.Handler; import android.os.RemoteException; +import com.android.email.provider.EmailProvider; import com.android.email.service.EmailServiceUtils; import com.android.email2.ui.MailActivityEmail; import com.android.emailcommon.provider.Account; @@ -298,7 +299,7 @@ public class AccountCreationFragment extends Fragment { account.mFlags &= ~Account.FLAGS_SECURITY_HOLD; AccountSettingsUtils.commitSettings(mAppContext, account); // Start up services based on new account(s) - MailActivityEmail.setServicesEnabledSync(mAppContext); + EmailProvider.setServicesEnabledSync(mAppContext); EmailServiceUtils .startService(mAppContext, account.mHostAuthRecv.mProtocol); return account; diff --git a/src/com/android/email/activity/setup/AccountSecurity.java b/src/com/android/email/activity/setup/AccountSecurity.java deleted file mode 100644 index c2be45928..000000000 --- a/src/com/android/email/activity/setup/AccountSecurity.java +++ /dev/null @@ -1,628 +0,0 @@ -/* - * 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.activity.setup; - -import android.app.Activity; -import android.app.AlertDialog; -import android.app.Dialog; -import android.app.DialogFragment; -import android.app.FragmentManager; -import android.app.LoaderManager; -import android.app.admin.DevicePolicyManager; -import android.content.Context; -import android.content.DialogInterface; -import android.content.Intent; -import android.content.Loader; -import android.content.res.Resources; -import android.net.Uri; -import android.os.AsyncTask; -import android.os.Bundle; -import android.os.Handler; -import android.text.TextUtils; - -import com.android.email.DebugUtils; -import com.android.email.R; -import com.android.email.SecurityPolicy; -import com.android.emailcommon.provider.Account; -import com.android.emailcommon.provider.EmailContent; -import com.android.emailcommon.provider.HostAuth; -import com.android.emailcommon.provider.Policy; -import com.android.emailcommon.utility.IntentUtilities; -import com.android.mail.ui.MailAsyncTaskLoader; -import com.android.mail.utils.LogUtils; - -/** - * Psuedo-activity (no UI) to bootstrap the user up to a higher desired security level. This - * bootstrap requires the following steps. - * - * 1. Confirm the account of interest has any security policies defined - exit early if not - * 2. If not actively administrating the device, ask Device Policy Manager to start that - * 3. When we are actively administrating, check current policies and see if they're sufficient - * 4. If not, set policies - * 5. If necessary, request for user to update device password - * 6. If necessary, request for user to activate device encryption - */ -public class AccountSecurity extends Activity { - private static final String TAG = "Email/AccountSecurity"; - - private static final boolean DEBUG = false; // Don't ship with this set to true - - private static final String EXTRA_ACCOUNT_ID = "ACCOUNT_ID"; - private static final String EXTRA_SHOW_DIALOG = "SHOW_DIALOG"; - private static final String EXTRA_PASSWORD_EXPIRING = "EXPIRING"; - private static final String EXTRA_PASSWORD_EXPIRED = "EXPIRED"; - - private static final String SAVESTATE_INITIALIZED_TAG = "initialized"; - private static final String SAVESTATE_TRIED_ADD_ADMINISTRATOR_TAG = "triedAddAdministrator"; - private static final String SAVESTATE_TRIED_SET_PASSWORD_TAG = "triedSetpassword"; - private static final String SAVESTATE_TRIED_SET_ENCRYPTION_TAG = "triedSetEncryption"; - private static final String SAVESTATE_ACCOUNT_TAG = "account"; - - private static final int REQUEST_ENABLE = 1; - private static final int REQUEST_PASSWORD = 2; - private static final int REQUEST_ENCRYPTION = 3; - - private boolean mTriedAddAdministrator; - private boolean mTriedSetPassword; - private boolean mTriedSetEncryption; - - private Account mAccount; - - protected boolean mInitialized; - - private Handler mHandler; - private boolean mActivityResumed; - - private static final int ACCOUNT_POLICY_LOADER_ID = 0; - private AccountAndPolicyLoaderCallbacks mAPLoaderCallbacks; - private Bundle mAPLoaderArgs; - - public static Uri getUpdateSecurityUri(final long accountId, final boolean showDialog) { - final Uri.Builder baseUri = Uri.parse("auth://" + EmailContent.EMAIL_PACKAGE_NAME + - ".ACCOUNT_SECURITY/").buildUpon(); - IntentUtilities.setAccountId(baseUri, accountId); - baseUri.appendQueryParameter(EXTRA_SHOW_DIALOG, Boolean.toString(showDialog)); - return baseUri.build(); - } - - /** - * Used for generating intent for this activity (which is intended to be launched - * from a notification.) - * - * @param context Calling context for building the intent - * @param accountId The account of interest - * @param showDialog If true, a simple warning dialog will be shown before kicking off - * the necessary system settings. Should be true anywhere the context of the security settings - * is not clear (e.g. any time after the account has been set up). - * @return an Intent which can be used to view that account - */ - public static Intent actionUpdateSecurityIntent(Context context, long accountId, - boolean showDialog) { - Intent intent = new Intent(context, AccountSecurity.class); - intent.putExtra(EXTRA_ACCOUNT_ID, accountId); - intent.putExtra(EXTRA_SHOW_DIALOG, showDialog); - return intent; - } - - /** - * Used for generating intent for this activity (which is intended to be launched - * from a notification.) This is a special mode of this activity which exists only - * to give the user a dialog (for context) about a device pin/password expiration event. - */ - public static Intent actionDevicePasswordExpirationIntent(Context context, long accountId, - boolean expired) { - Intent intent = new ForwardingIntent(context, AccountSecurity.class); - intent.putExtra(EXTRA_ACCOUNT_ID, accountId); - intent.putExtra(expired ? EXTRA_PASSWORD_EXPIRED : EXTRA_PASSWORD_EXPIRING, true); - return intent; - } - - @Override - public void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - - mHandler = new Handler(); - - final Intent i = getIntent(); - final long accountId; - Bundle extras = i.getExtras(); - if (extras == null) { - // We have been invoked via a uri. We need to get our parameters from the URI instead - // of looking in the intent extras. - extras = new Bundle(); - accountId = IntentUtilities.getAccountIdFromIntent(i); - extras.putLong(EXTRA_ACCOUNT_ID, accountId); - boolean showDialog = false; - final String value = i.getData().getQueryParameter(EXTRA_SHOW_DIALOG); - if (!TextUtils.isEmpty(value)) { - showDialog = Boolean.getBoolean(value); - } - extras.putBoolean(EXTRA_SHOW_DIALOG, showDialog); - } else { - accountId = i.getLongExtra(EXTRA_ACCOUNT_ID, -1); - extras = i.getExtras(); - } - - final SecurityPolicy security = SecurityPolicy.getInstance(this); - security.clearNotification(); - if (accountId == -1) { - finish(); - return; - } - - if (savedInstanceState != null) { - mInitialized = savedInstanceState.getBoolean(SAVESTATE_INITIALIZED_TAG, false); - - mTriedAddAdministrator = - savedInstanceState.getBoolean(SAVESTATE_TRIED_ADD_ADMINISTRATOR_TAG, false); - mTriedSetPassword = - savedInstanceState.getBoolean(SAVESTATE_TRIED_SET_PASSWORD_TAG, false); - mTriedSetEncryption = - savedInstanceState.getBoolean(SAVESTATE_TRIED_SET_ENCRYPTION_TAG, false); - - mAccount = savedInstanceState.getParcelable(SAVESTATE_ACCOUNT_TAG); - } - - if (!mInitialized) { - startAccountAndPolicyLoader(extras); - } - } - - @Override - protected void onSaveInstanceState(final Bundle outState) { - super.onSaveInstanceState(outState); - outState.putBoolean(SAVESTATE_INITIALIZED_TAG, mInitialized); - - outState.putBoolean(SAVESTATE_TRIED_ADD_ADMINISTRATOR_TAG, mTriedAddAdministrator); - outState.putBoolean(SAVESTATE_TRIED_SET_PASSWORD_TAG, mTriedSetPassword); - outState.putBoolean(SAVESTATE_TRIED_SET_ENCRYPTION_TAG, mTriedSetEncryption); - - outState.putParcelable(SAVESTATE_ACCOUNT_TAG, mAccount); - } - - @Override - protected void onPause() { - super.onPause(); - mActivityResumed = false; - } - - @Override - protected void onResume() { - super.onResume(); - mActivityResumed = true; - tickleAccountAndPolicyLoader(); - } - - protected boolean isActivityResumed() { - return mActivityResumed; - } - - private void tickleAccountAndPolicyLoader() { - // If we're already initialized we don't need to tickle. - if (!mInitialized) { - getLoaderManager().initLoader(ACCOUNT_POLICY_LOADER_ID, mAPLoaderArgs, - mAPLoaderCallbacks); - } - } - - private void startAccountAndPolicyLoader(final Bundle args) { - mAPLoaderArgs = args; - mAPLoaderCallbacks = new AccountAndPolicyLoaderCallbacks(); - tickleAccountAndPolicyLoader(); - } - - private class AccountAndPolicyLoaderCallbacks - implements LoaderManager.LoaderCallbacks { - @Override - public Loader onCreateLoader(final int id, final Bundle args) { - final long accountId = args.getLong(EXTRA_ACCOUNT_ID, -1); - final boolean showDialog = args.getBoolean(EXTRA_SHOW_DIALOG, false); - final boolean passwordExpiring = - args.getBoolean(EXTRA_PASSWORD_EXPIRING, false); - final boolean passwordExpired = - args.getBoolean(EXTRA_PASSWORD_EXPIRED, false); - - return new AccountAndPolicyLoader(getApplicationContext(), accountId, - showDialog, passwordExpiring, passwordExpired); - } - - @Override - public void onLoadFinished(final Loader loader, final Account account) { - mHandler.post(new Runnable() { - @Override - public void run() { - final AccountSecurity activity = AccountSecurity.this; - if (!activity.isActivityResumed()) { - return; - } - - if (account == null || (account.mPolicyKey != 0 && account.mPolicy == null)) { - activity.finish(); - LogUtils.d(TAG, "could not load account or policy in AccountSecurity"); - return; - } - - if (!activity.mInitialized) { - activity.mInitialized = true; - - final AccountAndPolicyLoader apLoader = (AccountAndPolicyLoader) loader; - activity.completeCreate(account, apLoader.mShowDialog, - apLoader.mPasswordExpiring, apLoader.mPasswordExpired); - } - } - }); - } - - @Override - public void onLoaderReset(Loader loader) {} - } - - private static class AccountAndPolicyLoader extends MailAsyncTaskLoader { - private final long mAccountId; - public final boolean mShowDialog; - public final boolean mPasswordExpiring; - public final boolean mPasswordExpired; - - private final Context mContext; - - AccountAndPolicyLoader(final Context context, final long accountId, - final boolean showDialog, final boolean passwordExpiring, - final boolean passwordExpired) { - super(context); - mContext = context; - mAccountId = accountId; - mShowDialog = showDialog; - mPasswordExpiring = passwordExpiring; - mPasswordExpired = passwordExpired; - } - - @Override - public Account loadInBackground() { - final Account account = Account.restoreAccountWithId(mContext, mAccountId); - if (account == null) { - return null; - } - - final long policyId = account.mPolicyKey; - if (policyId != 0) { - account.mPolicy = Policy.restorePolicyWithId(mContext, policyId); - } - - account.getOrCreateHostAuthRecv(mContext); - - return account; - } - - @Override - protected void onDiscardResult(Account result) {} - } - - protected void completeCreate(final Account account, final boolean showDialog, - final boolean passwordExpiring, final boolean passwordExpired) { - mAccount = account; - - // Special handling for password expiration events - if (passwordExpiring || passwordExpired) { - FragmentManager fm = getFragmentManager(); - if (fm.findFragmentByTag("password_expiration") == null) { - PasswordExpirationDialog dialog = - PasswordExpirationDialog.newInstance(mAccount.getDisplayName(), - passwordExpired); - if (DebugUtils.DEBUG || DEBUG) { - LogUtils.d(TAG, "Showing password expiration dialog"); - } - dialog.show(fm, "password_expiration"); - } - return; - } - // Otherwise, handle normal security settings flow - if (mAccount.mPolicyKey != 0) { - // This account wants to control security - if (showDialog) { - // Show dialog first, unless already showing (e.g. after rotation) - FragmentManager fm = getFragmentManager(); - if (fm.findFragmentByTag("security_needed") == null) { - SecurityNeededDialog dialog = - SecurityNeededDialog.newInstance(mAccount.getDisplayName()); - if (DebugUtils.DEBUG || DEBUG) { - LogUtils.d(TAG, "Showing security needed dialog"); - } - dialog.show(fm, "security_needed"); - } - } else { - // Go directly to security settings - tryAdvanceSecurity(mAccount); - } - return; - } - finish(); - } - - /** - * After any of the activities return, try to advance to the "next step" - */ - @Override - protected void onActivityResult(int requestCode, int resultCode, Intent data) { - tryAdvanceSecurity(mAccount); - super.onActivityResult(requestCode, resultCode, data); - } - - /** - * Walk the user through the required steps to become an active administrator and with - * the requisite security settings for the given account. - * - * These steps will be repeated each time we return from a given attempt (e.g. asking the - * user to choose a device pin/password). In a typical activation, we may repeat these - * steps a few times. It may go as far as step 5 (password) or step 6 (encryption), but it - * will terminate when step 2 (isActive()) succeeds. - * - * If at any point we do not advance beyond a given user step, (e.g. the user cancels - * instead of setting a password) we simply repost the security notification, and exit. - * We never want to loop here. - */ - private void tryAdvanceSecurity(Account account) { - SecurityPolicy security = SecurityPolicy.getInstance(this); - // Step 1. Check if we are an active device administrator, and stop here to activate - if (!security.isActiveAdmin()) { - if (mTriedAddAdministrator) { - if (DebugUtils.DEBUG || DEBUG) { - LogUtils.d(TAG, "Not active admin: repost notification"); - } - repostNotification(account, security); - finish(); - } else { - mTriedAddAdministrator = true; - // retrieve name of server for the format string - final HostAuth hostAuth = account.mHostAuthRecv; - if (hostAuth == null) { - if (DebugUtils.DEBUG || DEBUG) { - LogUtils.d(TAG, "No HostAuth: repost notification"); - } - repostNotification(account, security); - finish(); - } else { - if (DebugUtils.DEBUG || DEBUG) { - LogUtils.d(TAG, "Not active admin: post initial notification"); - } - // try to become active - must happen here in activity, to get result - Intent intent = new Intent(DevicePolicyManager.ACTION_ADD_DEVICE_ADMIN); - intent.putExtra(DevicePolicyManager.EXTRA_DEVICE_ADMIN, - security.getAdminComponent()); - intent.putExtra(DevicePolicyManager.EXTRA_ADD_EXPLANATION, - this.getString(R.string.account_security_policy_explanation_fmt, - hostAuth.mAddress)); - startActivityForResult(intent, REQUEST_ENABLE); - } - } - return; - } - - // Step 2. Check if the current aggregate security policy is being satisfied by the - // DevicePolicyManager (the current system security level). - if (security.isActive(null)) { - if (DebugUtils.DEBUG || DEBUG) { - LogUtils.d(TAG, "Security active; clear holds"); - } - Account.clearSecurityHoldOnAllAccounts(this); - security.syncAccount(account); - security.clearNotification(); - finish(); - return; - } - - // Step 3. Try to assert the current aggregate security requirements with the system. - security.setActivePolicies(); - - // Step 4. Recheck the security policy, and determine what changes are needed (if any) - // to satisfy the requirements. - int inactiveReasons = security.getInactiveReasons(null); - - // Step 5. If password is needed, try to have the user set it - if ((inactiveReasons & SecurityPolicy.INACTIVE_NEED_PASSWORD) != 0) { - if (mTriedSetPassword) { - if (DebugUtils.DEBUG || DEBUG) { - LogUtils.d(TAG, "Password needed; repost notification"); - } - repostNotification(account, security); - finish(); - } else { - if (DebugUtils.DEBUG || DEBUG) { - LogUtils.d(TAG, "Password needed; request it via DPM"); - } - mTriedSetPassword = true; - // launch the activity to have the user set a new password. - Intent intent = new Intent(DevicePolicyManager.ACTION_SET_NEW_PASSWORD); - startActivityForResult(intent, REQUEST_PASSWORD); - } - return; - } - - // Step 6. If encryption is needed, try to have the user set it - if ((inactiveReasons & SecurityPolicy.INACTIVE_NEED_ENCRYPTION) != 0) { - if (mTriedSetEncryption) { - if (DebugUtils.DEBUG || DEBUG) { - LogUtils.d(TAG, "Encryption needed; repost notification"); - } - repostNotification(account, security); - finish(); - } else { - if (DebugUtils.DEBUG || DEBUG) { - LogUtils.d(TAG, "Encryption needed; request it via DPM"); - } - mTriedSetEncryption = true; - // launch the activity to start up encryption. - Intent intent = new Intent(DevicePolicyManager.ACTION_START_ENCRYPTION); - startActivityForResult(intent, REQUEST_ENCRYPTION); - } - return; - } - - // Step 7. No problems were found, so clear holds and exit - if (DebugUtils.DEBUG || DEBUG) { - LogUtils.d(TAG, "Policies enforced; clear holds"); - } - Account.clearSecurityHoldOnAllAccounts(this); - security.syncAccount(account); - security.clearNotification(); - finish(); - } - - /** - * Mark an account as not-ready-for-sync and post a notification to bring the user back here - * eventually. - */ - private static void repostNotification(final Account account, final SecurityPolicy security) { - if (account == null) return; - new AsyncTask() { - @Override - protected Void doInBackground(Void... params) { - security.policiesRequired(account.mId); - return null; - } - }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); - } - - /** - * Dialog briefly shown in some cases, to indicate the user that a security update is needed. - * If the user clicks OK, we proceed into the "tryAdvanceSecurity" flow. If the user cancels, - * we repost the notification and finish() the activity. - */ - public static class SecurityNeededDialog extends DialogFragment - implements DialogInterface.OnClickListener { - private static final String BUNDLE_KEY_ACCOUNT_NAME = "account_name"; - - // Public no-args constructor needed for fragment re-instantiation - public SecurityNeededDialog() {} - - /** - * Create a new dialog. - */ - public static SecurityNeededDialog newInstance(String accountName) { - final SecurityNeededDialog dialog = new SecurityNeededDialog(); - Bundle b = new Bundle(); - b.putString(BUNDLE_KEY_ACCOUNT_NAME, accountName); - dialog.setArguments(b); - return dialog; - } - - @Override - public Dialog onCreateDialog(Bundle savedInstanceState) { - final String accountName = getArguments().getString(BUNDLE_KEY_ACCOUNT_NAME); - - final Context context = getActivity(); - final Resources res = context.getResources(); - final AlertDialog.Builder b = new AlertDialog.Builder(context); - b.setTitle(R.string.account_security_dialog_title); - b.setIconAttribute(android.R.attr.alertDialogIcon); - b.setMessage(res.getString(R.string.account_security_dialog_content_fmt, accountName)); - b.setPositiveButton(android.R.string.ok, this); - b.setNegativeButton(android.R.string.cancel, this); - if (DebugUtils.DEBUG || DEBUG) { - LogUtils.d(TAG, "Posting security needed dialog"); - } - return b.create(); - } - - @Override - public void onClick(DialogInterface dialog, int which) { - dismiss(); - AccountSecurity activity = (AccountSecurity) getActivity(); - if (activity.mAccount == null) { - // Clicked before activity fully restored - probably just monkey - exit quickly - activity.finish(); - return; - } - switch (which) { - case DialogInterface.BUTTON_POSITIVE: - if (DebugUtils.DEBUG || DEBUG) { - LogUtils.d(TAG, "User accepts; advance to next step"); - } - activity.tryAdvanceSecurity(activity.mAccount); - break; - case DialogInterface.BUTTON_NEGATIVE: - if (DebugUtils.DEBUG || DEBUG) { - LogUtils.d(TAG, "User declines; repost notification"); - } - AccountSecurity.repostNotification( - activity.mAccount, SecurityPolicy.getInstance(activity)); - activity.finish(); - break; - } - } - } - - /** - * Dialog briefly shown in some cases, to indicate the user that the PIN/Password is expiring - * or has expired. If the user clicks OK, we launch the password settings screen. - */ - public static class PasswordExpirationDialog extends DialogFragment - implements DialogInterface.OnClickListener { - private static final String BUNDLE_KEY_ACCOUNT_NAME = "account_name"; - private static final String BUNDLE_KEY_EXPIRED = "expired"; - - /** - * Create a new dialog. - */ - public static PasswordExpirationDialog newInstance(String accountName, boolean expired) { - final PasswordExpirationDialog dialog = new PasswordExpirationDialog(); - Bundle b = new Bundle(); - b.putString(BUNDLE_KEY_ACCOUNT_NAME, accountName); - b.putBoolean(BUNDLE_KEY_EXPIRED, expired); - dialog.setArguments(b); - return dialog; - } - - // Public no-args constructor needed for fragment re-instantiation - public PasswordExpirationDialog() {} - - /** - * Note, this actually creates two slightly different dialogs (for expiring vs. expired) - */ - @Override - public Dialog onCreateDialog(Bundle savedInstanceState) { - final String accountName = getArguments().getString(BUNDLE_KEY_ACCOUNT_NAME); - final boolean expired = getArguments().getBoolean(BUNDLE_KEY_EXPIRED); - final int titleId = expired - ? R.string.password_expired_dialog_title - : R.string.password_expire_warning_dialog_title; - final int contentId = expired - ? R.string.password_expired_dialog_content_fmt - : R.string.password_expire_warning_dialog_content_fmt; - - final Context context = getActivity(); - final Resources res = context.getResources(); - return new AlertDialog.Builder(context) - .setTitle(titleId) - .setIconAttribute(android.R.attr.alertDialogIcon) - .setMessage(res.getString(contentId, accountName)) - .setPositiveButton(android.R.string.ok, this) - .setNegativeButton(android.R.string.cancel, this) - .create(); - } - - @Override - public void onClick(DialogInterface dialog, int which) { - dismiss(); - AccountSecurity activity = (AccountSecurity) getActivity(); - if (which == DialogInterface.BUTTON_POSITIVE) { - Intent intent = new Intent(DevicePolicyManager.ACTION_SET_NEW_PASSWORD); - activity.startActivity(intent); - } - activity.finish(); - } - } -} diff --git a/src/com/android/email/activity/setup/AccountServerSettingsActivity.java b/src/com/android/email/activity/setup/AccountServerSettingsActivity.java index b0782570e..f119219bb 100644 --- a/src/com/android/email/activity/setup/AccountServerSettingsActivity.java +++ b/src/com/android/email/activity/setup/AccountServerSettingsActivity.java @@ -26,6 +26,7 @@ import android.content.Intent; import android.os.Bundle; import com.android.email.R; +import com.android.email.setup.AuthenticatorSetupIntentHelper; import com.android.emailcommon.provider.Account; import com.android.mail.utils.LogUtils; @@ -65,7 +66,7 @@ public class AccountServerSettingsActivity extends AccountSetupActivity implemen public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - mSetupData.setFlowMode(SetupDataFragment.FLOW_MODE_EDIT); + mSetupData.setFlowMode(AuthenticatorSetupIntentHelper.FLOW_MODE_EDIT); setContentView(R.layout.account_server_settings); setFinishOnTouchOutside(false); diff --git a/src/com/android/email/activity/setup/AccountSettingsFragment.java b/src/com/android/email/activity/setup/AccountSettingsFragment.java index f6ea75614..91e14d608 100644 --- a/src/com/android/email/activity/setup/AccountSettingsFragment.java +++ b/src/com/android/email/activity/setup/AccountSettingsFragment.java @@ -52,7 +52,6 @@ import com.android.email.provider.EmailProvider; import com.android.email.provider.FolderPickerActivity; import com.android.email.service.EmailServiceUtils; import com.android.email.service.EmailServiceUtils.EmailServiceInfo; -import com.android.email2.ui.MailActivityEmail; import com.android.emailcommon.provider.Account; import com.android.emailcommon.provider.EmailContent; import com.android.emailcommon.provider.EmailContent.AccountColumns; @@ -402,7 +401,7 @@ public class AccountSettingsFragment extends MailAccountPrefsFragment } if (cv.size() > 0) { new UpdateTask().run(mContext.getContentResolver(), mAccount.getUri(), cv, null, null); - MailActivityEmail.setServicesEnabledAsync(mContext); + EmailProvider.setServicesEnabledAsync(mContext); } return false; } diff --git a/src/com/android/email/activity/setup/AccountSettingsUtils.java b/src/com/android/email/activity/setup/AccountSettingsUtils.java deleted file mode 100644 index dbbd51ee7..000000000 --- a/src/com/android/email/activity/setup/AccountSettingsUtils.java +++ /dev/null @@ -1,433 +0,0 @@ -/* - * 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.activity.setup; - -import android.content.ContentResolver; -import android.content.ContentValues; -import android.content.Context; -import android.content.res.XmlResourceParser; -import android.net.Uri; -import android.text.TextUtils; - -import com.android.email.R; -import com.android.email.provider.AccountBackupRestore; -import com.android.emailcommon.Logging; -import com.android.emailcommon.VendorPolicyLoader; -import com.android.emailcommon.VendorPolicyLoader.OAuthProvider; -import com.android.emailcommon.VendorPolicyLoader.Provider; -import com.android.emailcommon.provider.Account; -import com.android.emailcommon.provider.EmailContent.AccountColumns; -import com.android.emailcommon.provider.QuickResponse; -import com.android.emailcommon.service.PolicyServiceProxy; -import com.android.emailcommon.utility.Utility; -import com.android.mail.utils.LogUtils; -import com.google.common.annotations.VisibleForTesting; - -import java.util.ArrayList; -import java.util.List; - -public class AccountSettingsUtils { - - /** Pattern to match any part of a domain */ - private final static String WILD_STRING = "*"; - /** Will match any, single character */ - private final static char WILD_CHARACTER = '?'; - private final static String DOMAIN_SEPARATOR = "\\."; - - /** - * Commits the UI-related settings of an account to the provider. This is static so that it - * can be used by the various account activities. If the account has never been saved, this - * method saves it; otherwise, it just saves the settings. - * @param context the context of the caller - * @param account the account whose settings will be committed - */ - public static void commitSettings(Context context, Account account) { - if (!account.isSaved()) { - account.save(context); - - if (account.mPolicy != null) { - // TODO: we need better handling for unsupported policies - // For now, just clear the unsupported policies, as the server will (hopefully) - // just reject our sync attempts if it's not happy with half-measures - if (account.mPolicy.mProtocolPoliciesUnsupported != null) { - LogUtils.d(LogUtils.TAG, "Clearing unsupported policies " - + account.mPolicy.mProtocolPoliciesUnsupported); - account.mPolicy.mProtocolPoliciesUnsupported = null; - } - PolicyServiceProxy.setAccountPolicy2(context, - account.getId(), - account.mPolicy, - account.mSecuritySyncKey == null ? "" : account.mSecuritySyncKey, - false /* notify */); - } - - // Set up default quick responses here... - String[] defaultQuickResponses = - context.getResources().getStringArray(R.array.default_quick_responses); - ContentValues cv = new ContentValues(); - cv.put(QuickResponse.ACCOUNT_KEY, account.mId); - ContentResolver resolver = context.getContentResolver(); - for (String quickResponse: defaultQuickResponses) { - // Allow empty entries (some localizations may not want to have the maximum - // number) - if (!TextUtils.isEmpty(quickResponse)) { - cv.put(QuickResponse.TEXT, quickResponse); - resolver.insert(QuickResponse.CONTENT_URI, cv); - } - } - } else { - ContentValues cv = getAccountContentValues(account); - account.update(context, cv); - } - - // Update the backup (side copy) of the accounts - AccountBackupRestore.backup(context); - } - - /** - * Returns a set of content values to commit account changes (not including the foreign keys - * for the two host auth's and policy) to the database. Does not actually commit anything. - */ - public static ContentValues getAccountContentValues(Account account) { - ContentValues cv = new ContentValues(); - cv.put(AccountColumns.DISPLAY_NAME, account.getDisplayName()); - cv.put(AccountColumns.SENDER_NAME, account.getSenderName()); - cv.put(AccountColumns.SIGNATURE, account.getSignature()); - cv.put(AccountColumns.SYNC_INTERVAL, account.mSyncInterval); - cv.put(AccountColumns.FLAGS, account.mFlags); - cv.put(AccountColumns.SYNC_LOOKBACK, account.mSyncLookback); - cv.put(AccountColumns.SECURITY_SYNC_KEY, account.mSecuritySyncKey); - return cv; - } - - /** - * Create the request to get the authorization code. - * - * @param context - * @param provider The OAuth provider to register with - * @param emailAddress Email address to send as a hint to the oauth service. - * @return - */ - public static Uri createOAuthRegistrationRequest(final Context context, - final OAuthProvider provider, final String emailAddress) { - final Uri.Builder b = Uri.parse(provider.authEndpoint).buildUpon(); - b.appendQueryParameter("response_type", provider.responseType); - b.appendQueryParameter("client_id", provider.clientId); - b.appendQueryParameter("redirect_uri", provider.redirectUri); - b.appendQueryParameter("scope", provider.scope); - b.appendQueryParameter("state", provider.state); - b.appendQueryParameter("login_hint", emailAddress); - return b.build(); - } - - /** - * Search for a single resource containing known oauth provider definitions. - * - * @param context - * @param id String Id of the oauth provider. - * @return The OAuthProvider if found, null if not. - */ - public static OAuthProvider findOAuthProvider(final Context context, final String id) { - return findOAuthProvider(context, id, R.xml.oauth); - } - - public static List getAllOAuthProviders(final Context context) { - try { - List providers = new ArrayList(); - final XmlResourceParser xml = context.getResources().getXml(R.xml.oauth); - int xmlEventType; - OAuthProvider provider = null; - while ((xmlEventType = xml.next()) != XmlResourceParser.END_DOCUMENT) { - if (xmlEventType == XmlResourceParser.START_TAG - && "provider".equals(xml.getName())) { - try { - provider = new OAuthProvider(); - provider.id = getXmlAttribute(context, xml, "id"); - provider.label = getXmlAttribute(context, xml, "label"); - provider.authEndpoint = getXmlAttribute(context, xml, "auth_endpoint"); - provider.tokenEndpoint = getXmlAttribute(context, xml, "token_endpoint"); - provider.refreshEndpoint = getXmlAttribute(context, xml, - "refresh_endpoint"); - provider.responseType = getXmlAttribute(context, xml, "response_type"); - provider.redirectUri = getXmlAttribute(context, xml, "redirect_uri"); - provider.scope = getXmlAttribute(context, xml, "scope"); - provider.state = getXmlAttribute(context, xml, "state"); - provider.clientId = getXmlAttribute(context, xml, "client_id"); - provider.clientSecret = getXmlAttribute(context, xml, "client_secret"); - providers.add(provider); - } catch (IllegalArgumentException e) { - LogUtils.w(Logging.LOG_TAG, "providers line: " + xml.getLineNumber() + - "; Domain contains multiple globals"); - } - } - } - return providers; - } catch (Exception e) { - LogUtils.e(Logging.LOG_TAG, "Error while trying to load provider settings.", e); - } - return null; - } - - /** - * Search for a single resource containing known oauth provider definitions. - * - * @param context - * @param id String Id of the oauth provider. - * @param resourceId ResourceId of the xml file to search. - * @return The OAuthProvider if found, null if not. - */ - public static OAuthProvider findOAuthProvider(final Context context, final String id, - final int resourceId) { - // TODO: Consider adding a way to cache this file during new account setup, so that we - // don't need to keep loading the file over and over. - // TODO: need a mechanism to get a list of all supported OAuth providers so that we can - // offer the user a choice of who to authenticate with. - try { - final XmlResourceParser xml = context.getResources().getXml(resourceId); - int xmlEventType; - OAuthProvider provider = null; - while ((xmlEventType = xml.next()) != XmlResourceParser.END_DOCUMENT) { - if (xmlEventType == XmlResourceParser.START_TAG - && "provider".equals(xml.getName())) { - String providerId = getXmlAttribute(context, xml, "id"); - try { - if (TextUtils.equals(id, providerId)) { - provider = new OAuthProvider(); - provider.id = id; - provider.label = getXmlAttribute(context, xml, "label"); - provider.authEndpoint = getXmlAttribute(context, xml, "auth_endpoint"); - provider.tokenEndpoint = getXmlAttribute(context, xml, "token_endpoint"); - provider.refreshEndpoint = getXmlAttribute(context, xml, - "refresh_endpoint"); - provider.responseType = getXmlAttribute(context, xml, "response_type"); - provider.redirectUri = getXmlAttribute(context, xml, "redirect_uri"); - provider.scope = getXmlAttribute(context, xml, "scope"); - provider.state = getXmlAttribute(context, xml, "state"); - provider.clientId = getXmlAttribute(context, xml, "client_id"); - provider.clientSecret = getXmlAttribute(context, xml, "client_secret"); - return provider; - } - } catch (IllegalArgumentException e) { - LogUtils.w(Logging.LOG_TAG, "providers line: " + xml.getLineNumber() + - "; Domain contains multiple globals"); - } - } - } - } catch (Exception e) { - LogUtils.e(Logging.LOG_TAG, "Error while trying to load provider settings.", e); - } - return null; - } - - /** - * Search the list of known Email providers looking for one that matches the user's email - * domain. We check for vendor supplied values first, then we look in providers_product.xml, - * and finally by the entries in platform providers.xml. This provides a nominal override - * capability. - * - * A match is defined as any provider entry for which the "domain" attribute matches. - * - * @param domain The domain portion of the user's email address - * @return suitable Provider definition, or null if no match found - */ - public static Provider findProviderForDomain(Context context, String domain) { - Provider p = VendorPolicyLoader.getInstance(context).findProviderForDomain(domain); - if (p == null) { - p = findProviderForDomain(context, domain, R.xml.providers_product); - } - if (p == null) { - p = findProviderForDomain(context, domain, R.xml.providers); - } - return p; - } - - /** - * Search a single resource containing known Email provider definitions. - * - * @param domain The domain portion of the user's email address - * @param resourceId Id of the provider resource to scan - * @return suitable Provider definition, or null if no match found - */ - /*package*/ static Provider findProviderForDomain( - Context context, String domain, int resourceId) { - try { - XmlResourceParser xml = context.getResources().getXml(resourceId); - int xmlEventType; - Provider provider = null; - while ((xmlEventType = xml.next()) != XmlResourceParser.END_DOCUMENT) { - if (xmlEventType == XmlResourceParser.START_TAG - && "provider".equals(xml.getName())) { - String providerDomain = getXmlAttribute(context, xml, "domain"); - try { - if (matchProvider(domain, providerDomain)) { - provider = new Provider(); - provider.id = getXmlAttribute(context, xml, "id"); - provider.label = getXmlAttribute(context, xml, "label"); - provider.domain = domain.toLowerCase(); - provider.note = getXmlAttribute(context, xml, "note"); - // TODO: Maybe this should actually do a lookup of the OAuth provider - // here, and keep a pointer to it rather than a textual key. - // To do this probably requires caching oauth.xml, otherwise the lookup - // is expensive and likely to happen repeatedly. - provider.oauth = getXmlAttribute(context, xml, "oauth"); - } - } catch (IllegalArgumentException e) { - LogUtils.w(Logging.LOG_TAG, "providers line: " + xml.getLineNumber() + - "; Domain contains multiple globals"); - } - } - else if (xmlEventType == XmlResourceParser.START_TAG - && "incoming".equals(xml.getName()) - && provider != null) { - provider.incomingUriTemplate = getXmlAttribute(context, xml, "uri"); - provider.incomingUsernameTemplate = getXmlAttribute(context, xml, "username"); - } - else if (xmlEventType == XmlResourceParser.START_TAG - && "outgoing".equals(xml.getName()) - && provider != null) { - provider.outgoingUriTemplate = getXmlAttribute(context, xml, "uri"); - provider.outgoingUsernameTemplate = getXmlAttribute(context, xml, "username"); - } - else if (xmlEventType == XmlResourceParser.START_TAG - && "incoming-fallback".equals(xml.getName()) - && provider != null) { - provider.altIncomingUriTemplate = getXmlAttribute(context, xml, "uri"); - provider.altIncomingUsernameTemplate = - getXmlAttribute(context, xml, "username"); - } - else if (xmlEventType == XmlResourceParser.START_TAG - && "outgoing-fallback".equals(xml.getName()) - && provider != null) { - provider.altOutgoingUriTemplate = getXmlAttribute(context, xml, "uri"); - provider.altOutgoingUsernameTemplate = - getXmlAttribute(context, xml, "username"); - } - else if (xmlEventType == XmlResourceParser.END_TAG - && "provider".equals(xml.getName()) - && provider != null) { - return provider; - } - } - } - catch (Exception e) { - LogUtils.e(Logging.LOG_TAG, "Error while trying to load provider settings.", e); - } - return null; - } - - /** - * Returns true if the string s1 matches the string s2. The string - * s2 may contain any number of wildcards -- a '?' character -- and/or asterisk - * characters -- '*'. Wildcards match any single character, while the asterisk matches a domain - * part (i.e. substring demarcated by a period, '.') - */ - @VisibleForTesting - public static boolean matchProvider(String testDomain, String providerDomain) { - String[] testParts = testDomain.split(DOMAIN_SEPARATOR); - String[] providerParts = providerDomain.split(DOMAIN_SEPARATOR); - if (testParts.length != providerParts.length) { - return false; - } - for (int i = 0; i < testParts.length; i++) { - String testPart = testParts[i].toLowerCase(); - String providerPart = providerParts[i].toLowerCase(); - if (!providerPart.equals(WILD_STRING) && - !matchWithWildcards(testPart, providerPart)) { - return false; - } - } - return true; - } - - private static boolean matchWithWildcards(String testPart, String providerPart) { - int providerLength = providerPart.length(); - if (testPart.length() != providerLength){ - return false; - } - for (int i = 0; i < providerLength; i++) { - char testChar = testPart.charAt(i); - char providerChar = providerPart.charAt(i); - if (testChar != providerChar && providerChar != WILD_CHARACTER) { - return false; - } - } - return true; - } - - /** - * Attempts to get the given attribute as a String resource first, and if it fails - * returns the attribute as a simple String value. - * @param xml - * @param name - * @return the requested resource - */ - private static String getXmlAttribute(Context context, XmlResourceParser xml, String name) { - int resId = xml.getAttributeResourceValue(null, name, 0); - if (resId == 0) { - return xml.getAttributeValue(null, name); - } - else { - return context.getString(resId); - } - } - - /** - * Infer potential email server addresses from domain names - * - * Incoming: Prepend "imap" or "pop3" to domain, unless "pop", "pop3", - * "imap", or "mail" are found. - * Outgoing: Prepend "smtp" if domain starts with any in the host prefix array - * - * @param server name as we know it so far - * @param incoming "pop3" or "imap" (or null) - * @param outgoing "smtp" or null - * @return the post-processed name for use in the UI - */ - public static String inferServerName(Context context, String server, String incoming, - String outgoing) { - // Default values cause entire string to be kept, with prepended server string - int keepFirstChar = 0; - int firstDotIndex = server.indexOf('.'); - if (firstDotIndex != -1) { - // look at first word and decide what to do - String firstWord = server.substring(0, firstDotIndex).toLowerCase(); - String[] hostPrefixes = - context.getResources().getStringArray(R.array.smtp_host_prefixes); - boolean canSubstituteSmtp = Utility.arrayContains(hostPrefixes, firstWord); - boolean isMail = "mail".equals(firstWord); - // Now decide what to do - if (incoming != null) { - // For incoming, we leave imap/pop/pop3/mail alone, or prepend incoming - if (canSubstituteSmtp || isMail) { - return server; - } - } else { - // For outgoing, replace imap/pop/pop3 with outgoing, leave mail alone, or - // prepend outgoing - if (canSubstituteSmtp) { - keepFirstChar = firstDotIndex + 1; - } else if (isMail) { - return server; - } else { - // prepend - } - } - } - return ((incoming != null) ? incoming : outgoing) + '.' + server.substring(keepFirstChar); - } - -} diff --git a/src/com/android/email/activity/setup/AccountSetupFinal.java b/src/com/android/email/activity/setup/AccountSetupFinal.java index 85ee24e99..ed33a5f5d 100644 --- a/src/com/android/email/activity/setup/AccountSetupFinal.java +++ b/src/com/android/email/activity/setup/AccountSetupFinal.java @@ -43,6 +43,7 @@ import android.view.inputmethod.InputMethodManager; import android.widget.Toast; import com.android.email.R; +import com.android.email.setup.AuthenticatorSetupIntentHelper; import com.android.email.service.EmailServiceUtils; import com.android.emailcommon.VendorPolicyLoader; import com.android.emailcommon.provider.Account; @@ -97,8 +98,6 @@ public class AccountSetupFinal extends AccountSetupActivity * and the appropriate incoming/outgoing information will be filled in automatically. */ private static String INTENT_FORCE_CREATE_ACCOUNT; - private static final String EXTRA_FLOW_MODE = "FLOW_MODE"; - private static final String EXTRA_FLOW_ACCOUNT_TYPE = "FLOW_ACCOUNT_TYPE"; private static final String EXTRA_CREATE_ACCOUNT_EMAIL = "EMAIL"; private static final String EXTRA_CREATE_ACCOUNT_USER = "USER"; private static final String EXTRA_CREATE_ACCOUNT_PASSWORD = "PASSWORD"; @@ -180,26 +179,6 @@ public class AccountSetupFinal extends AccountSetupActivity private static final int EXISTING_ACCOUNTS_LOADER_ID = 1; private Map mExistingAccountsMap; - public static Intent actionNewAccountIntent(final Context context) { - final Intent i = new Intent(context, AccountSetupFinal.class); - i.putExtra(EXTRA_FLOW_MODE, SetupDataFragment.FLOW_MODE_NORMAL); - return i; - } - - public static Intent actionNewAccountWithResultIntent(final Context context) { - final Intent i = new Intent(context, AccountSetupFinal.class); - i.putExtra(EXTRA_FLOW_MODE, SetupDataFragment.FLOW_MODE_NO_ACCOUNTS); - return i; - } - - public static Intent actionGetCreateAccountIntent(final Context context, - final String accountManagerType) { - final Intent i = new Intent(context, AccountSetupFinal.class); - i.putExtra(EXTRA_FLOW_MODE, SetupDataFragment.FLOW_MODE_ACCOUNT_MANAGER); - i.putExtra(EXTRA_FLOW_ACCOUNT_TYPE, accountManagerType); - return i; - } - @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); @@ -250,11 +229,13 @@ public class AccountSetupFinal extends AccountSetupActivity // Initialize the SetupDataFragment if (INTENT_FORCE_CREATE_ACCOUNT.equals(action)) { - mSetupData.setFlowMode(SetupDataFragment.FLOW_MODE_FORCE_CREATE); + mSetupData.setFlowMode(AuthenticatorSetupIntentHelper.FLOW_MODE_FORCE_CREATE); } else { - final int intentFlowMode = intent.getIntExtra(EXTRA_FLOW_MODE, - SetupDataFragment.FLOW_MODE_UNSPECIFIED); - final String flowAccountType = intent.getStringExtra(EXTRA_FLOW_ACCOUNT_TYPE); + final int intentFlowMode = intent.getIntExtra( + AuthenticatorSetupIntentHelper.EXTRA_FLOW_MODE, + AuthenticatorSetupIntentHelper.FLOW_MODE_UNSPECIFIED); + final String flowAccountType = intent.getStringExtra( + AuthenticatorSetupIntentHelper.EXTRA_FLOW_ACCOUNT_TYPE); mSetupData.setAmProtocol( EmailServiceUtils.getProtocolFromAccountType(this, flowAccountType)); mSetupData.setFlowMode(intentFlowMode); @@ -273,8 +254,8 @@ public class AccountSetupFinal extends AccountSetupActivity mPasswordFailed = false; } - if (!mIsProcessing - && mSetupData.getFlowMode() == SetupDataFragment.FLOW_MODE_FORCE_CREATE) { + if (!mIsProcessing && mSetupData.getFlowMode() == + AuthenticatorSetupIntentHelper.FLOW_MODE_FORCE_CREATE) { /** * To support continuous testing, we allow the forced creation of accounts. * This works in a manner fairly similar to automatic setup, in which the complete @@ -679,7 +660,8 @@ public class AccountSetupFinal extends AccountSetupActivity case STATE_CREATING: mState = STATE_NAMES; updateContentFragment(true /* addToBackstack */); - if (mSetupData.getFlowMode() == SetupDataFragment.FLOW_MODE_FORCE_CREATE) { + if (mSetupData.getFlowMode() == + AuthenticatorSetupIntentHelper.FLOW_MODE_FORCE_CREATE) { getFragmentManager().executePendingTransactions(); initiateAccountFinalize(); } diff --git a/src/com/android/email/activity/setup/AccountSetupNamesFragment.java b/src/com/android/email/activity/setup/AccountSetupNamesFragment.java index 363de9bf6..f9c20dc6a 100644 --- a/src/com/android/email/activity/setup/AccountSetupNamesFragment.java +++ b/src/com/android/email/activity/setup/AccountSetupNamesFragment.java @@ -33,6 +33,7 @@ import android.widget.EditText; import com.android.email.R; import com.android.email.activity.UiUtilities; import com.android.email.service.EmailServiceUtils; +import com.android.email.setup.AuthenticatorSetupIntentHelper; import com.android.emailcommon.provider.Account; public class AccountSetupNamesFragment extends AccountSetupFragment { @@ -81,8 +82,8 @@ public class AccountSetupNamesFragment extends AccountSetupFragment { final Account account = setupData.getAccount(); - if (flowMode != SetupDataFragment.FLOW_MODE_FORCE_CREATE - && flowMode != SetupDataFragment.FLOW_MODE_EDIT) { + if (flowMode != AuthenticatorSetupIntentHelper.FLOW_MODE_FORCE_CREATE + && flowMode != AuthenticatorSetupIntentHelper.FLOW_MODE_EDIT) { final String accountEmail = account.mEmailAddress; mDescription.setText(accountEmail); @@ -99,8 +100,8 @@ public class AccountSetupNamesFragment extends AccountSetupFragment { } else { if (account.getSenderName() != null) { mName.setText(account.getSenderName()); - } else if (flowMode != SetupDataFragment.FLOW_MODE_FORCE_CREATE - && flowMode != SetupDataFragment.FLOW_MODE_EDIT) { + } else if (flowMode != AuthenticatorSetupIntentHelper.FLOW_MODE_FORCE_CREATE + && flowMode != AuthenticatorSetupIntentHelper.FLOW_MODE_EDIT) { // Attempt to prefill the name field from the profile if we don't have it, final Context loaderContext = getActivity().getApplicationContext(); getLoaderManager().initLoader(0, null, new LoaderManager.LoaderCallbacks() { diff --git a/src/com/android/email/activity/setup/EmailPreferenceActivity.java b/src/com/android/email/activity/setup/EmailPreferenceActivity.java index 4a5dc4ed9..cbb47b922 100644 --- a/src/com/android/email/activity/setup/EmailPreferenceActivity.java +++ b/src/com/android/email/activity/setup/EmailPreferenceActivity.java @@ -27,6 +27,7 @@ import android.view.Menu; import android.view.MenuItem; import com.android.email.R; +import com.android.email.setup.AuthenticatorSetupIntentHelper; import com.android.emailcommon.utility.IntentUtilities; import com.android.mail.providers.UIProvider.EditSettingsExtras; import com.android.mail.ui.settings.MailPreferenceActivity; @@ -201,7 +202,7 @@ public class EmailPreferenceActivity extends MailPreferenceActivity { } private void onAddNewAccount() { - final Intent setupIntent = AccountSetupFinal.actionNewAccountIntent(this); + final Intent setupIntent = AuthenticatorSetupIntentHelper.actionNewAccountIntent(this); startActivity(setupIntent); } diff --git a/src/com/android/email/activity/setup/ForwardingIntent.java b/src/com/android/email/activity/setup/ForwardingIntent.java deleted file mode 100644 index 1fc913ef7..000000000 --- a/src/com/android/email/activity/setup/ForwardingIntent.java +++ /dev/null @@ -1,30 +0,0 @@ -/* 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/HeadlessAccountSettingsLoader.java b/src/com/android/email/activity/setup/HeadlessAccountSettingsLoader.java index 09eedd9a5..bca6d5545 100644 --- a/src/com/android/email/activity/setup/HeadlessAccountSettingsLoader.java +++ b/src/com/android/email/activity/setup/HeadlessAccountSettingsLoader.java @@ -21,13 +21,6 @@ import com.android.mail.ui.MailAsyncTaskLoader; */ public class HeadlessAccountSettingsLoader extends Activity { - public static Uri getIncomingSettingsUri(long accountId) { - final Uri.Builder baseUri = Uri.parse("auth://" + EmailContent.EMAIL_PACKAGE_NAME + - ".ACCOUNT_SETTINGS/incoming/").buildUpon(); - IntentUtilities.setAccountId(baseUri, accountId); - return baseUri.build(); - } - public static Uri getOutgoingSettingsUri(long accountId) { final Uri.Builder baseUri = Uri.parse("auth://" + EmailContent.EMAIL_PACKAGE_NAME + ".ACCOUNT_SETTINGS/outgoing/").buildUpon(); diff --git a/src/com/android/email/activity/setup/SetupDataFragment.java b/src/com/android/email/activity/setup/SetupDataFragment.java index 45b7cf1ad..9824e1b34 100644 --- a/src/com/android/email/activity/setup/SetupDataFragment.java +++ b/src/com/android/email/activity/setup/SetupDataFragment.java @@ -7,6 +7,7 @@ import android.os.Parcel; import android.os.Parcelable; import com.android.email.service.EmailServiceUtils; +import com.android.email.setup.AuthenticatorSetupIntentHelper; import com.android.emailcommon.provider.Account; import com.android.emailcommon.provider.HostAuth; import com.android.emailcommon.provider.Policy; @@ -18,20 +19,12 @@ public class SetupDataFragment extends Fragment implements Parcelable { // The "extra" name for the Bundle saved with SetupData public static final String EXTRA_SETUP_DATA = "com.android.email.setupdata"; - // NORMAL is the standard entry from the Email app; EAS and POP_IMAP are used when entering via - // Settings -> Accounts - public static final int FLOW_MODE_UNSPECIFIED = -1; - public static final int FLOW_MODE_NORMAL = 0; - public static final int FLOW_MODE_ACCOUNT_MANAGER = 1; - public static final int FLOW_MODE_EDIT = 3; - public static final int FLOW_MODE_FORCE_CREATE = 4; // The following two modes are used to "pop the stack" and return from the setup flow. We // either return to the caller (if we're in an account type flow) or go to the message list // TODO: figure out if we still care about these public static final int FLOW_MODE_RETURN_TO_CALLER = 5; public static final int FLOW_MODE_RETURN_TO_MESSAGE_LIST = 6; public static final int FLOW_MODE_RETURN_NO_ACCOUNTS_RESULT = 7; - public static final int FLOW_MODE_NO_ACCOUNTS = 8; // Mode bits for AccountSetupCheckSettings, indicating the type of check requested public static final int CHECK_INCOMING = 1; @@ -49,7 +42,7 @@ public class SetupDataFragment extends Fragment implements Parcelable { private static final String SAVESTATE_AM_PROTOCOL = "SetupDataFragment.amProtocol"; // All access will be through getters/setters - private int mFlowMode = FLOW_MODE_NORMAL; + private int mFlowMode = AuthenticatorSetupIntentHelper.FLOW_MODE_NORMAL; private Account mAccount; private String mEmail; private Bundle mCredentialResults; diff --git a/src/com/android/email/mail/Sender.java b/src/com/android/email/mail/Sender.java deleted file mode 100644 index 4e85d70fa..000000000 --- a/src/com/android/email/mail/Sender.java +++ /dev/null @@ -1,125 +0,0 @@ -/* - * 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.mail; - -import android.content.Context; -import android.content.res.XmlResourceParser; - -import com.android.email.R; -import com.android.emailcommon.Logging; -import com.android.emailcommon.mail.MessagingException; -import com.android.emailcommon.provider.Account; -import com.android.emailcommon.provider.HostAuth; -import com.android.mail.utils.LogUtils; - -import org.xmlpull.v1.XmlPullParserException; - -import java.io.IOException; - -public abstract class Sender { - protected static final int SOCKET_CONNECT_TIMEOUT = 10000; - - /** - * Static named constructor. It should be overrode by extending class. - * Because this method will be called through reflection, it can not be protected. - */ - public static Sender newInstance(Account account) throws MessagingException { - throw new MessagingException("Sender.newInstance: Unknown scheme in " - + account.mDisplayName); - } - - private static Sender instantiateSender(Context context, String className, Account account) - throws MessagingException { - Object o = null; - try { - Class c = Class.forName(className); - // and invoke "newInstance" class method and instantiate sender object. - java.lang.reflect.Method m = - c.getMethod("newInstance", Account.class, Context.class); - o = m.invoke(null, account, context); - } catch (Exception e) { - LogUtils.d(Logging.LOG_TAG, String.format( - "exception %s invoking method %s#newInstance(Account, Context) for %s", - e.toString(), className, account.mDisplayName)); - throw new MessagingException("can not instantiate Sender for " + account.mDisplayName); - } - if (!(o instanceof Sender)) { - throw new MessagingException( - account.mDisplayName + ": " + className + " create incompatible object"); - } - return (Sender) o; - } - - /** - * Find Sender implementation consulting with sender.xml file. - */ - private static Sender findSender(Context context, int resourceId, Account account) - throws MessagingException { - Sender sender = null; - try { - XmlResourceParser xml = context.getResources().getXml(resourceId); - int xmlEventType; - HostAuth sendAuth = account.getOrCreateHostAuthSend(context); - // walk through senders.xml file. - while ((xmlEventType = xml.next()) != XmlResourceParser.END_DOCUMENT) { - if (xmlEventType == XmlResourceParser.START_TAG && - "sender".equals(xml.getName())) { - String xmlScheme = xml.getAttributeValue(null, "scheme"); - if (sendAuth.mProtocol != null && sendAuth.mProtocol.startsWith(xmlScheme)) { - // found sender entry whose scheme is matched with uri. - // then load sender class. - String className = xml.getAttributeValue(null, "class"); - sender = instantiateSender(context, className, account); - } - } - } - } catch (XmlPullParserException e) { - // ignore - } catch (IOException e) { - // ignore - } - return sender; - } - - /** - * Get an instance of a mail sender for the given account. The account must be valid (i.e. has - * at least an outgoing server name). - * - * @param context the caller's context - * @param account the account of the sender. - * @return an initialized sender of the appropriate class - * @throws MessagingException If the sender cannot be obtained or if the account is invalid. - */ - public synchronized static Sender getInstance(Context context, Account account) - throws MessagingException { - Context appContext = context.getApplicationContext(); - Sender sender = findSender(appContext, R.xml.senders_product, account); - if (sender == null) { - sender = findSender(appContext, R.xml.senders, account); - } - if (sender == null) { - throw new MessagingException("Cannot find sender for account " + account.mDisplayName); - } - return sender; - } - - public abstract void open() throws MessagingException; - - public abstract void sendMessage(long messageId) throws MessagingException; - - public abstract void close() throws MessagingException; -} diff --git a/src/com/android/email/mail/Store.java b/src/com/android/email/mail/Store.java deleted file mode 100644 index 377b59448..000000000 --- a/src/com/android/email/mail/Store.java +++ /dev/null @@ -1,226 +0,0 @@ -/* - * Copyright (C) 2008 The Android Open Source P-roject - * - * 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.mail; - -import android.content.Context; -import android.os.Bundle; - -import com.android.email.R; -import com.android.email.mail.store.ImapStore; -import com.android.email.mail.store.Pop3Store; -import com.android.email.mail.store.ServiceStore; -import com.android.email.mail.transport.MailTransport; -import com.android.emailcommon.Logging; -import com.android.emailcommon.mail.Folder; -import com.android.emailcommon.mail.MessagingException; -import com.android.emailcommon.provider.Account; -import com.android.emailcommon.provider.EmailContent; -import com.android.emailcommon.provider.HostAuth; -import com.android.emailcommon.provider.Mailbox; -import com.android.mail.utils.LogUtils; -import com.google.common.annotations.VisibleForTesting; - -import java.lang.reflect.Method; -import java.util.HashMap; - -/** - * Store is the legacy equivalent of the Account class - */ -public abstract class Store { - /** - * A global suggestion to Store implementors on how much of the body - * should be returned on FetchProfile.Item.BODY_SANE requests. We'll use 125k now. - */ - public static final int FETCH_BODY_SANE_SUGGESTED_SIZE = (125 * 1024); - - @VisibleForTesting - static final HashMap sStores = new HashMap(); - protected Context mContext; - protected Account mAccount; - protected MailTransport mTransport; - protected String mUsername; - protected String mPassword; - - static final HashMap> sStoreClasses = - new HashMap>(); - - /** - * Static named constructor. It should be overrode by extending class. - * Because this method will be called through reflection, it can not be protected. - */ - static Store newInstance(Account account) throws MessagingException { - throw new MessagingException("Store#newInstance: Unknown scheme in " - + account.mDisplayName); - } - - /** - * Get an instance of a mail store for the given account. The account must be valid (i.e. has - * at least an incoming server name). - * - * NOTE: The internal algorithm used to find a cached store depends upon the account's - * HostAuth row. If this ever changes (e.g. such as the user updating the - * host name or port), we will leak entries. This should not be typical, so, it is not - * a critical problem. However, it is something we should consider fixing. - * - * @param account The account of the store. - * @param context For all the usual context-y stuff - * @return an initialized store of the appropriate class - * @throws MessagingException If the store cannot be obtained or if the account is invalid. - */ - public synchronized static Store getInstance(Account account, Context context) - throws MessagingException { - if (sStores.isEmpty()) { - sStoreClasses.put(context.getString(R.string.protocol_pop3), Pop3Store.class); - sStoreClasses.put(context.getString(R.string.protocol_legacy_imap), ImapStore.class); - } - HostAuth hostAuth = account.getOrCreateHostAuthRecv(context); - // An existing account might have been deleted - if (hostAuth == null) return null; - if (!account.isTemporary()) { - Store store = sStores.get(hostAuth); - if (store == null) { - store = createInstanceInternal(account, context, true); - } else { - // Make sure the account object is up to date (according to the caller, at least) - store.mAccount = account; - } - return store; - } else { - return createInstanceInternal(account, context, false); - } - } - - private synchronized static Store createInstanceInternal(final Account account, - final Context context, final boolean cacheInstance) - throws MessagingException { - Context appContext = context.getApplicationContext(); - final HostAuth hostAuth = account.getOrCreateHostAuthRecv(context); - Class klass = sStoreClasses.get(hostAuth.mProtocol); - if (klass == null) { - klass = ServiceStore.class; - } - final Store store; - try { - // invoke "newInstance" class method - Method m = klass.getMethod("newInstance", Account.class, Context.class); - store = (Store)m.invoke(null, account, appContext); - } catch (Exception e) { - LogUtils.d(Logging.LOG_TAG, String.format( - "exception %s invoking method %s#newInstance(Account, Context) for %s", - e.toString(), klass.getName(), account.mDisplayName)); - throw new MessagingException("Can't instantiate Store for " + account.mDisplayName); - } - // Don't cache this unless it's we've got a saved HostAuth - if (hostAuth.mId != EmailContent.NOT_SAVED && cacheInstance) { - sStores.put(hostAuth, store); - } - return store; - } - - /** - * Delete the mail store associated with the given account. The account must be valid (i.e. has - * at least an incoming server name). - * - * The store should have been notified already by calling delete(), and the caller should - * also take responsibility for deleting the matching LocalStore, etc. - * - * @throws MessagingException If the store cannot be removed or if the account is invalid. - */ - public synchronized static Store removeInstance(Account account, Context context) - throws MessagingException { - return sStores.remove(HostAuth.restoreHostAuthWithId(context, account.mHostAuthKeyRecv)); - } - - /** - * Some protocols require that a sent message be copied (uploaded) into the Sent folder - * while others can take care of it automatically (ideally, on the server). This function - * allows a given store to indicate which mode(s) it supports. - * @return true if the store requires an upload into "sent", false if this happens automatically - * for any sent message. - */ - public boolean requireCopyMessageToSentFolder() { - return true; - } - - public Folder getFolder(String name) throws MessagingException { - return null; - } - - /** - * Updates the local list of mailboxes according to what is located on the remote server. - * Note: This does not perform folder synchronization and it will not remove mailboxes - * that are stored locally but not remotely. - * @return The set of remote folders - * @throws MessagingException If there was a problem connecting to the remote server - */ - public Folder[] updateFolders() throws MessagingException { - return null; - } - - public abstract Bundle checkSettings() throws MessagingException; - - /** - * Handle discovery of account settings using only the user's email address and password - * @param context the context of the caller - * @param emailAddress the email address of the exchange user - * @param password the password of the exchange user - * @return a Bundle containing an error code and a HostAuth (if successful) - * @throws MessagingException - */ - public Bundle autoDiscover(Context context, String emailAddress, String password) - throws MessagingException { - return null; - } - - /** - * Updates the fields within the given mailbox. Only the fields that are important to - * non-EAS accounts are modified. - */ - protected static void updateMailbox(Mailbox mailbox, long accountId, String mailboxPath, - char delimiter, boolean selectable, int type) { - mailbox.mAccountKey = accountId; - mailbox.mDelimiter = delimiter; - String displayPath = mailboxPath; - int pathIndex = mailboxPath.lastIndexOf(delimiter); - if (pathIndex > 0) { - displayPath = mailboxPath.substring(pathIndex + 1); - } - mailbox.mDisplayName = displayPath; - if (selectable) { - mailbox.mFlags = Mailbox.FLAG_HOLDS_MAIL | Mailbox.FLAG_ACCEPTS_MOVED_MAIL; - } - mailbox.mFlagVisible = true; - //mailbox.mParentKey; - //mailbox.mParentServerId; - mailbox.mServerId = mailboxPath; - //mailbox.mServerId; - //mailbox.mSyncFrequency; - //mailbox.mSyncKey; - //mailbox.mSyncLookback; - //mailbox.mSyncTime; - mailbox.mType = type; - //box.mUnreadCount; - } - - public void closeConnections() { - // Base implementation does nothing. - } - - public Account getAccount() { - return mAccount; - } -} diff --git a/src/com/android/email/mail/internet/AuthenticationCache.java b/src/com/android/email/mail/internet/AuthenticationCache.java deleted file mode 100644 index 21508af35..000000000 --- a/src/com/android/email/mail/internet/AuthenticationCache.java +++ /dev/null @@ -1,162 +0,0 @@ -package com.android.email.mail.internet; - -import android.content.Context; -import android.text.format.DateUtils; - -import com.android.email.mail.internet.OAuthAuthenticator.AuthenticationResult; -import com.android.emailcommon.Logging; -import com.android.emailcommon.mail.AuthenticationFailedException; -import com.android.emailcommon.mail.MessagingException; -import com.android.emailcommon.provider.Account; -import com.android.emailcommon.provider.Credential; -import com.android.emailcommon.provider.HostAuth; -import com.android.mail.utils.LogUtils; - -import java.io.IOException; -import java.util.HashMap; -import java.util.Map; - -public class AuthenticationCache { - private static AuthenticationCache sCache; - - // Threshold for refreshing a token. If the token is expected to expire within this amount of - // time, we won't even bother attempting to use it and will simply force a refresh. - private static final long EXPIRATION_THRESHOLD = 5 * DateUtils.MINUTE_IN_MILLIS; - - private final Map mCache; - private final OAuthAuthenticator mAuthenticator; - - private class CacheEntry { - CacheEntry(long accountId, String providerId, String accessToken, String refreshToken, - long expirationTime) { - mAccountId = accountId; - mProviderId = providerId; - mAccessToken = accessToken; - mRefreshToken = refreshToken; - mExpirationTime = expirationTime; - } - - final long mAccountId; - String mProviderId; - String mAccessToken; - String mRefreshToken; - long mExpirationTime; - } - - public static AuthenticationCache getInstance() { - synchronized (AuthenticationCache.class) { - if (sCache == null) { - sCache = new AuthenticationCache(); - } - return sCache; - } - } - - private AuthenticationCache() { - mCache = new HashMap(); - mAuthenticator = new OAuthAuthenticator(); - } - - // Gets an access token for the given account. This may be whatever is currently cached, or - // it may query the server to get a new one if the old one is expired or nearly expired. - public String retrieveAccessToken(Context context, Account account) throws - MessagingException, IOException { - // Currently, we always use the same OAuth info for both sending and receiving. - // If we start to allow different credential objects for sending and receiving, this - // will need to be updated. - CacheEntry entry = null; - synchronized (mCache) { - entry = getEntry(context, account); - } - synchronized (entry) { - final long actualExpiration = entry.mExpirationTime - EXPIRATION_THRESHOLD; - if (System.currentTimeMillis() > actualExpiration) { - // This access token is pretty close to end of life. Don't bother trying to use it, - // it might just time out while we're trying to sync. Go ahead and refresh it - // immediately. - refreshEntry(context, entry); - } - return entry.mAccessToken; - } - } - - public String refreshAccessToken(Context context, Account account) throws - MessagingException, IOException { - CacheEntry entry = getEntry(context, account); - synchronized (entry) { - refreshEntry(context, entry); - return entry.mAccessToken; - } - } - - private CacheEntry getEntry(Context context, Account account) { - CacheEntry entry; - if (account.isSaved() && !account.isTemporary()) { - entry = mCache.get(account.mId); - if (entry == null) { - LogUtils.d(Logging.LOG_TAG, "initializing entry from database"); - final HostAuth hostAuth = account.getOrCreateHostAuthRecv(context); - final Credential credential = hostAuth.getOrCreateCredential(context); - entry = new CacheEntry(account.mId, credential.mProviderId, credential.mAccessToken, - credential.mRefreshToken, credential.mExpiration); - mCache.put(account.mId, entry); - } - } else { - // This account is temporary, just create a temporary entry. Don't store - // it in the cache, it won't be findable because we don't yet have an account Id. - final HostAuth hostAuth = account.getOrCreateHostAuthRecv(context); - final Credential credential = hostAuth.getCredential(context); - entry = new CacheEntry(account.mId, credential.mProviderId, credential.mAccessToken, - credential.mRefreshToken, credential.mExpiration); - } - return entry; - } - - private void refreshEntry(Context context, CacheEntry entry) throws - IOException, MessagingException { - LogUtils.d(Logging.LOG_TAG, "AuthenticationCache refreshEntry %d", entry.mAccountId); - try { - final AuthenticationResult result = mAuthenticator.requestRefresh(context, - entry.mProviderId, entry.mRefreshToken); - // Don't set the refresh token here, it's not returned by the refresh response, - // so setting it here would make it blank. - entry.mAccessToken = result.mAccessToken; - entry.mExpirationTime = result.mExpiresInSeconds * DateUtils.SECOND_IN_MILLIS + - System.currentTimeMillis(); - saveEntry(context, entry); - } catch (AuthenticationFailedException e) { - // This is fatal. Clear the tokens and rethrow the exception. - LogUtils.d(Logging.LOG_TAG, "authentication failed, clearning"); - clearEntry(context, entry); - throw e; - } catch (MessagingException e) { - LogUtils.d(Logging.LOG_TAG, "messaging exception"); - throw e; - } catch (IOException e) { - LogUtils.d(Logging.LOG_TAG, "IO exception"); - throw e; - } - } - - private void saveEntry(Context context, CacheEntry entry) { - LogUtils.d(Logging.LOG_TAG, "saveEntry"); - - final Account account = Account.restoreAccountWithId(context, entry.mAccountId); - final HostAuth hostAuth = account.getOrCreateHostAuthRecv(context); - final Credential cred = hostAuth.getOrCreateCredential(context); - cred.mProviderId = entry.mProviderId; - cred.mAccessToken = entry.mAccessToken; - cred.mRefreshToken = entry.mRefreshToken; - cred.mExpiration = entry.mExpirationTime; - cred.update(context, cred.toContentValues()); - } - - private void clearEntry(Context context, CacheEntry entry) { - LogUtils.d(Logging.LOG_TAG, "clearEntry"); - entry.mAccessToken = ""; - entry.mRefreshToken = ""; - entry.mExpirationTime = 0; - saveEntry(context, entry); - mCache.remove(entry.mAccountId); - } -} diff --git a/src/com/android/email/mail/internet/OAuthAuthenticator.java b/src/com/android/email/mail/internet/OAuthAuthenticator.java deleted file mode 100644 index c3e255b5c..000000000 --- a/src/com/android/email/mail/internet/OAuthAuthenticator.java +++ /dev/null @@ -1,191 +0,0 @@ -package com.android.email.mail.internet; - -import android.content.Context; -import android.text.format.DateUtils; - -import com.android.email.activity.setup.AccountSettingsUtils; -import com.android.emailcommon.Logging; -import com.android.emailcommon.VendorPolicyLoader.OAuthProvider; -import com.android.emailcommon.mail.AuthenticationFailedException; -import com.android.emailcommon.mail.MessagingException; -import com.android.mail.utils.LogUtils; - -import org.apache.http.HttpResponse; -import org.apache.http.HttpStatus; -import org.apache.http.client.HttpClient; -import org.apache.http.client.entity.UrlEncodedFormEntity; -import org.apache.http.client.methods.HttpPost; -import org.apache.http.impl.client.DefaultHttpClient; -import org.apache.http.message.BasicNameValuePair; -import org.apache.http.params.BasicHttpParams; -import org.apache.http.params.HttpConnectionParams; -import org.apache.http.params.HttpParams; -import org.json.JSONException; -import org.json.JSONObject; - -import java.io.BufferedReader; -import java.io.IOException; -import java.io.InputStreamReader; -import java.io.UnsupportedEncodingException; -import java.util.ArrayList; -import java.util.List; - -public class OAuthAuthenticator { - private static final String TAG = Logging.LOG_TAG; - - public static final String OAUTH_REQUEST_CODE = "code"; - public static final String OAUTH_REQUEST_REFRESH_TOKEN = "refresh_token"; - public static final String OAUTH_REQUEST_CLIENT_ID = "client_id"; - public static final String OAUTH_REQUEST_CLIENT_SECRET = "client_secret"; - public static final String OAUTH_REQUEST_REDIRECT_URI = "redirect_uri"; - public static final String OAUTH_REQUEST_GRANT_TYPE = "grant_type"; - - public static final String JSON_ACCESS_TOKEN = "access_token"; - public static final String JSON_REFRESH_TOKEN = "refresh_token"; - public static final String JSON_EXPIRES_IN = "expires_in"; - - - private static final long CONNECTION_TIMEOUT = 20 * DateUtils.SECOND_IN_MILLIS; - private static final long COMMAND_TIMEOUT = 30 * DateUtils.SECOND_IN_MILLIS; - - final HttpClient mClient; - - public static class AuthenticationResult { - public AuthenticationResult(final String accessToken, final String refreshToken, - final int expiresInSeconds) { - mAccessToken = accessToken; - mRefreshToken = refreshToken; - mExpiresInSeconds = expiresInSeconds; - } - - @Override - public String toString() { - return "result access " + (mAccessToken==null?"null":"[REDACTED]") + - " refresh " + (mRefreshToken==null?"null":"[REDACTED]") + - " expiresInSeconds " + mExpiresInSeconds; - } - - public final String mAccessToken; - public final String mRefreshToken; - public final int mExpiresInSeconds; - } - - public OAuthAuthenticator() { - final HttpParams params = new BasicHttpParams(); - HttpConnectionParams.setConnectionTimeout(params, (int)(CONNECTION_TIMEOUT)); - HttpConnectionParams.setSoTimeout(params, (int)(COMMAND_TIMEOUT)); - HttpConnectionParams.setSocketBufferSize(params, 8192); - mClient = new DefaultHttpClient(params); - } - - public AuthenticationResult requestAccess(final Context context, final String providerId, - final String code) throws MessagingException, IOException { - final OAuthProvider provider = AccountSettingsUtils.findOAuthProvider(context, providerId); - if (provider == null) { - LogUtils.e(TAG, "invalid provider %s", providerId); - // This shouldn't happen, but if it does, it's a fatal. Throw an authentication failed - // exception, this will at least give the user a heads up to set up their account again. - throw new AuthenticationFailedException("Invalid provider" + providerId); - } - - final HttpPost post = new HttpPost(provider.tokenEndpoint); - post.setHeader("Content-Type", "application/x-www-form-urlencoded"); - final List nvp = new ArrayList(); - nvp.add(new BasicNameValuePair(OAUTH_REQUEST_CODE, code)); - nvp.add(new BasicNameValuePair(OAUTH_REQUEST_CLIENT_ID, provider.clientId)); - nvp.add(new BasicNameValuePair(OAUTH_REQUEST_CLIENT_SECRET, provider.clientSecret)); - nvp.add(new BasicNameValuePair(OAUTH_REQUEST_REDIRECT_URI, provider.redirectUri)); - nvp.add(new BasicNameValuePair(OAUTH_REQUEST_GRANT_TYPE, "authorization_code")); - try { - post.setEntity(new UrlEncodedFormEntity(nvp)); - } catch (UnsupportedEncodingException e) { - LogUtils.e(TAG, e, "unsupported encoding"); - // This shouldn't happen, but if it does, it's a fatal. Throw an authentication failed - // exception, this will at least give the user a heads up to set up their account again. - throw new AuthenticationFailedException("Unsupported encoding", e); - } - - return doRequest(post); - } - - public AuthenticationResult requestRefresh(final Context context, final String providerId, - final String refreshToken) throws MessagingException, IOException { - final OAuthProvider provider = AccountSettingsUtils.findOAuthProvider(context, providerId); - if (provider == null) { - LogUtils.e(TAG, "invalid provider %s", providerId); - // This shouldn't happen, but if it does, it's a fatal. Throw an authentication failed - // exception, this will at least give the user a heads up to set up their account again. - throw new AuthenticationFailedException("Invalid provider" + providerId); - } - final HttpPost post = new HttpPost(provider.refreshEndpoint); - post.setHeader("Content-Type", "application/x-www-form-urlencoded"); - final List nvp = new ArrayList(); - nvp.add(new BasicNameValuePair(OAUTH_REQUEST_REFRESH_TOKEN, refreshToken)); - nvp.add(new BasicNameValuePair(OAUTH_REQUEST_CLIENT_ID, provider.clientId)); - nvp.add(new BasicNameValuePair(OAUTH_REQUEST_CLIENT_SECRET, provider.clientSecret)); - nvp.add(new BasicNameValuePair(OAUTH_REQUEST_GRANT_TYPE, "refresh_token")); - try { - post.setEntity(new UrlEncodedFormEntity(nvp)); - } catch (UnsupportedEncodingException e) { - LogUtils.e(TAG, e, "unsupported encoding"); - // This shouldn't happen, but if it does, it's a fatal. Throw an authentication failed - // exception, this will at least give the user a heads up to set up their account again. - throw new AuthenticationFailedException("Unsuported encoding", e); - } - - return doRequest(post); - } - - private AuthenticationResult doRequest(HttpPost post) throws MessagingException, - IOException { - final HttpResponse response; - response = mClient.execute(post); - final int status = response.getStatusLine().getStatusCode(); - if (status == HttpStatus.SC_OK) { - return parseResponse(response); - } else if (status == HttpStatus.SC_FORBIDDEN || status == HttpStatus.SC_UNAUTHORIZED || - status == HttpStatus.SC_BAD_REQUEST) { - LogUtils.e(TAG, "HTTP Authentication error getting oauth tokens %d", status); - // This is fatal, and we probably should clear our tokens after this. - throw new AuthenticationFailedException("Auth error getting auth token"); - } else { - LogUtils.e(TAG, "HTTP Error %d getting oauth tokens", status); - // This is probably a transient error, we can try again later. - throw new MessagingException("HTTPError " + status + " getting oauth token"); - } - } - - private AuthenticationResult parseResponse(HttpResponse response) throws IOException, - MessagingException { - final BufferedReader reader = new BufferedReader(new InputStreamReader( - response.getEntity().getContent(), "UTF-8")); - final StringBuilder builder = new StringBuilder(); - for (String line = null; (line = reader.readLine()) != null;) { - builder.append(line).append("\n"); - } - try { - final JSONObject jsonResult = new JSONObject(builder.toString()); - final String accessToken = jsonResult.getString(JSON_ACCESS_TOKEN); - final String expiresIn = jsonResult.getString(JSON_EXPIRES_IN); - final String refreshToken; - if (jsonResult.has(JSON_REFRESH_TOKEN)) { - refreshToken = jsonResult.getString(JSON_REFRESH_TOKEN); - } else { - refreshToken = null; - } - try { - int expiresInSeconds = Integer.valueOf(expiresIn); - return new AuthenticationResult(accessToken, refreshToken, expiresInSeconds); - } catch (NumberFormatException e) { - LogUtils.e(TAG, e, "Invalid expiration %s", expiresIn); - // This indicates a server error, we can try again later. - throw new MessagingException("Invalid number format", e); - } - } catch (JSONException e) { - LogUtils.e(TAG, e, "Invalid JSON"); - // This indicates a server error, we can try again later. - throw new MessagingException("Invalid JSON", e); - } - } -} - diff --git a/src/com/android/email/mail/store/ImapConnection.java b/src/com/android/email/mail/store/ImapConnection.java deleted file mode 100644 index 3e71774fb..000000000 --- a/src/com/android/email/mail/store/ImapConnection.java +++ /dev/null @@ -1,636 +0,0 @@ -/* - * 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.mail.store; - -import android.text.TextUtils; -import android.util.Base64; - -import com.android.email.DebugUtils; -import com.android.email.mail.internet.AuthenticationCache; -import com.android.email.mail.store.ImapStore.ImapException; -import com.android.email.mail.store.imap.ImapConstants; -import com.android.email.mail.store.imap.ImapList; -import com.android.email.mail.store.imap.ImapResponse; -import com.android.email.mail.store.imap.ImapResponseParser; -import com.android.email.mail.store.imap.ImapUtility; -import com.android.email.mail.transport.DiscourseLogger; -import com.android.email.mail.transport.MailTransport; -import com.android.emailcommon.Logging; -import com.android.emailcommon.mail.AuthenticationFailedException; -import com.android.emailcommon.mail.CertificateValidationException; -import com.android.emailcommon.mail.MessagingException; -import com.android.mail.utils.LogUtils; - -import java.io.IOException; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.concurrent.atomic.AtomicInteger; - -import javax.net.ssl.SSLException; - -/** - * A cacheable class that stores the details for a single IMAP connection. - */ -class ImapConnection { - // Always check in FALSE - private static final boolean DEBUG_FORCE_SEND_ID = false; - - /** ID capability per RFC 2971*/ - public static final int CAPABILITY_ID = 1 << 0; - /** NAMESPACE capability per RFC 2342 */ - public static final int CAPABILITY_NAMESPACE = 1 << 1; - /** STARTTLS capability per RFC 3501 */ - public static final int CAPABILITY_STARTTLS = 1 << 2; - /** UIDPLUS capability per RFC 4315 */ - public static final int CAPABILITY_UIDPLUS = 1 << 3; - - /** The capabilities supported; a set of CAPABILITY_* values. */ - private int mCapabilities; - static final String IMAP_REDACTED_LOG = "[IMAP command redacted]"; - MailTransport mTransport; - private ImapResponseParser mParser; - private ImapStore mImapStore; - private String mLoginPhrase; - private String mAccessToken; - private String mIdPhrase = null; - - /** # of command/response lines to log upon crash. */ - private static final int DISCOURSE_LOGGER_SIZE = 64; - private final DiscourseLogger mDiscourse = new DiscourseLogger(DISCOURSE_LOGGER_SIZE); - /** - * Next tag to use. All connections associated to the same ImapStore instance share the same - * counter to make tests simpler. - * (Some of the tests involve multiple connections but only have a single counter to track the - * tag.) - */ - private final AtomicInteger mNextCommandTag = new AtomicInteger(0); - - // Keep others from instantiating directly - ImapConnection(ImapStore store) { - setStore(store); - } - - void setStore(ImapStore store) { - // TODO: maybe we should throw an exception if the connection is not closed here, - // if it's not currently closed, then we won't reopen it, so if the credentials have - // changed, the connection will not be reestablished. - mImapStore = store; - mLoginPhrase = null; - } - - /** - * Generates and returns the phrase to be used for authentication. This will be a LOGIN with - * username and password, or an OAUTH authentication string, with username and access token. - * Currently, these are the only two auth mechanisms supported. - * - * @throws IOException - * @throws AuthenticationFailedException - * @return the login command string to sent to the IMAP server - */ - String getLoginPhrase() throws MessagingException, IOException { - // build the LOGIN string once (instead of over-and-over again.) - if (mImapStore.getUseOAuth()) { - // We'll recreate the login phrase if it's null, or if the access token - // has changed. - final String accessToken = AuthenticationCache.getInstance().retrieveAccessToken( - mImapStore.getContext(), mImapStore.getAccount()); - if (mLoginPhrase == null || !TextUtils.equals(mAccessToken, accessToken)) { - mAccessToken = accessToken; - final String oauthCode = "user=" + mImapStore.getUsername() + '\001' + - "auth=Bearer " + mAccessToken + '\001' + '\001'; - mLoginPhrase = ImapConstants.AUTHENTICATE + " " + ImapConstants.XOAUTH2 + " " + - Base64.encodeToString(oauthCode.getBytes(), Base64.NO_WRAP); - } - } else { - if (mLoginPhrase == null) { - if (mImapStore.getUsername() != null && mImapStore.getPassword() != null) { - // build the LOGIN string once (instead of over-and-over again.) - // apply the quoting here around the built-up password - mLoginPhrase = ImapConstants.LOGIN + " " + mImapStore.getUsername() + " " - + ImapUtility.imapQuoted(mImapStore.getPassword()); - } - } - } - return mLoginPhrase; - } - - void open() throws IOException, MessagingException { - if (mTransport != null && mTransport.isOpen()) { - return; - } - - try { - // copy configuration into a clean transport, if necessary - if (mTransport == null) { - mTransport = mImapStore.cloneTransport(); - } - - mTransport.open(); - - createParser(); - - // BANNER - mParser.readResponse(); - - // CAPABILITY - ImapResponse capabilities = queryCapabilities(); - - boolean hasStartTlsCapability = - capabilities.contains(ImapConstants.STARTTLS); - - // TLS - ImapResponse newCapabilities = doStartTls(hasStartTlsCapability); - if (newCapabilities != null) { - capabilities = newCapabilities; - } - - // NOTE: An IMAP response MUST be processed before issuing any new IMAP - // requests. Subsequent requests may destroy previous response data. As - // such, we save away capability information here for future use. - setCapabilities(capabilities); - String capabilityString = capabilities.flatten(); - - // ID - doSendId(isCapable(CAPABILITY_ID), capabilityString); - - // LOGIN - doLogin(); - - // NAMESPACE (only valid in the Authenticated state) - doGetNamespace(isCapable(CAPABILITY_NAMESPACE)); - - // Gets the path separator from the server - doGetPathSeparator(); - - mImapStore.ensurePrefixIsValid(); - } catch (SSLException e) { - if (DebugUtils.DEBUG) { - LogUtils.d(Logging.LOG_TAG, e, "SSLException"); - } - throw new CertificateValidationException(e.getMessage(), e); - } catch (IOException ioe) { - // NOTE: Unlike similar code in POP3, I'm going to rethrow as-is. There is a lot - // of other code here that catches IOException and I don't want to break it. - // This catch is only here to enhance logging of connection-time issues. - if (DebugUtils.DEBUG) { - LogUtils.d(Logging.LOG_TAG, ioe, "IOException"); - } - throw ioe; - } finally { - destroyResponses(); - } - } - - /** - * Closes the connection and releases all resources. This connection can not be used again - * until {@link #setStore(ImapStore)} is called. - */ - void close() { - if (mTransport != null) { - mTransport.close(); - mTransport = null; - } - destroyResponses(); - mParser = null; - mImapStore = null; - } - - /** - * Returns whether or not the specified capability is supported by the server. - */ - private boolean isCapable(int capability) { - return (mCapabilities & capability) != 0; - } - - /** - * Sets the capability flags according to the response provided by the server. - * Note: We only set the capability flags that we are interested in. There are many IMAP - * capabilities that we do not track. - */ - private void setCapabilities(ImapResponse capabilities) { - if (capabilities.contains(ImapConstants.ID)) { - mCapabilities |= CAPABILITY_ID; - } - if (capabilities.contains(ImapConstants.NAMESPACE)) { - mCapabilities |= CAPABILITY_NAMESPACE; - } - if (capabilities.contains(ImapConstants.UIDPLUS)) { - mCapabilities |= CAPABILITY_UIDPLUS; - } - if (capabilities.contains(ImapConstants.STARTTLS)) { - mCapabilities |= CAPABILITY_STARTTLS; - } - } - - /** - * Create an {@link ImapResponseParser} from {@code mTransport.getInputStream()} and - * set it to {@link #mParser}. - * - * If we already have an {@link ImapResponseParser}, we - * {@link #destroyResponses()} and throw it away. - */ - private void createParser() { - destroyResponses(); - mParser = new ImapResponseParser(mTransport.getInputStream(), mDiscourse); - } - - void destroyResponses() { - if (mParser != null) { - mParser.destroyResponses(); - } - } - - boolean isTransportOpenForTest() { - return mTransport != null && mTransport.isOpen(); - } - - ImapResponse readResponse() throws IOException, MessagingException { - return mParser.readResponse(); - } - - /** - * Send a single command to the server. The command will be preceded by an IMAP command - * tag and followed by \r\n (caller need not supply them). - * - * @param command The command to send to the server - * @param sensitive If true, the command will not be logged - * @return Returns the command tag that was sent - */ - String sendCommand(String command, boolean sensitive) - throws MessagingException, IOException { - LogUtils.d(Logging.LOG_TAG, "sendCommand %s", (sensitive ? IMAP_REDACTED_LOG : command)); - open(); - return sendCommandInternal(command, sensitive); - } - - String sendCommandInternal(String command, boolean sensitive) - throws MessagingException, IOException { - if (mTransport == null) { - throw new IOException("Null transport"); - } - String tag = Integer.toString(mNextCommandTag.incrementAndGet()); - String commandToSend = tag + " " + command; - mTransport.writeLine(commandToSend, sensitive ? IMAP_REDACTED_LOG : null); - mDiscourse.addSentCommand(sensitive ? IMAP_REDACTED_LOG : commandToSend); - return tag; - } - - /** - * Send a single, complex command to the server. The command will be preceded by an IMAP - * command tag and followed by \r\n (caller need not supply them). After each piece of the - * command, a response will be read which MUST be a continuation request. - * - * @param commands An array of Strings comprising the command to be sent to the server - * @return Returns the command tag that was sent - */ - String sendComplexCommand(List commands, boolean sensitive) throws MessagingException, - IOException { - open(); - String tag = Integer.toString(mNextCommandTag.incrementAndGet()); - int len = commands.size(); - for (int i = 0; i < len; i++) { - String commandToSend = commands.get(i); - // The first part of the command gets the tag - if (i == 0) { - commandToSend = tag + " " + commandToSend; - } else { - // Otherwise, read the response from the previous part of the command - ImapResponse response = readResponse(); - // If it isn't a continuation request, that's an error - if (!response.isContinuationRequest()) { - throw new MessagingException("Expected continuation request"); - } - } - // Send the command - mTransport.writeLine(commandToSend, null); - mDiscourse.addSentCommand(sensitive ? IMAP_REDACTED_LOG : commandToSend); - } - return tag; - } - - List executeSimpleCommand(String command) throws IOException, MessagingException { - return executeSimpleCommand(command, false); - } - - /** - * Read and return all of the responses from the most recent command sent to the server - * - * @return a list of ImapResponses - * @throws IOException - * @throws MessagingException - */ - List getCommandResponses() throws IOException, MessagingException { - final List responses = new ArrayList(); - ImapResponse response; - do { - response = mParser.readResponse(); - responses.add(response); - } while (!response.isTagged()); - - if (!response.isOk()) { - final String toString = response.toString(); - final String alert = response.getAlertTextOrEmpty().getString(); - final String responseCode = response.getResponseCodeOrEmpty().getString(); - destroyResponses(); - - // if the response code indicates an error occurred within the server, indicate that - if (ImapConstants.UNAVAILABLE.equals(responseCode)) { - throw new MessagingException(MessagingException.SERVER_ERROR, alert); - } - - throw new ImapException(toString, alert, responseCode); - } - return responses; - } - - /** - * Execute a simple command at the server, a simple command being one that is sent in a single - * line of text - * - * @param command the command to send to the server - * @param sensitive whether the command should be redacted in logs (used for login) - * @return a list of ImapResponses - * @throws IOException - * @throws MessagingException - */ - List executeSimpleCommand(String command, boolean sensitive) - throws IOException, MessagingException { - // TODO: It may be nice to catch IOExceptions and close the connection here. - // Currently, we expect callers to do that, but if they fail to we'll be in a broken state. - sendCommand(command, sensitive); - return getCommandResponses(); - } - - /** - * Execute a complex command at the server, a complex command being one that must be sent in - * multiple lines due to the use of string literals - * - * @param commands a list of strings that comprise the command to be sent to the server - * @param sensitive whether the command should be redacted in logs (used for login) - * @return a list of ImapResponses - * @throws IOException - * @throws MessagingException - */ - List executeComplexCommand(List commands, boolean sensitive) - throws IOException, MessagingException { - sendComplexCommand(commands, sensitive); - return getCommandResponses(); - } - - /** - * Query server for capabilities. - */ - private ImapResponse queryCapabilities() throws IOException, MessagingException { - ImapResponse capabilityResponse = null; - for (ImapResponse r : executeSimpleCommand(ImapConstants.CAPABILITY)) { - if (r.is(0, ImapConstants.CAPABILITY)) { - capabilityResponse = r; - break; - } - } - if (capabilityResponse == null) { - throw new MessagingException("Invalid CAPABILITY response received"); - } - return capabilityResponse; - } - - /** - * Sends client identification information to the IMAP server per RFC 2971. If - * the server does not support the ID command, this will perform no operation. - * - * Interoperability hack: Never send ID to *.secureserver.net, which sends back a - * malformed response that our parser can't deal with. - */ - private void doSendId(boolean hasIdCapability, String capabilities) - throws MessagingException { - if (!hasIdCapability) return; - - // Never send ID to *.secureserver.net - String host = mTransport.getHost(); - if (host.toLowerCase().endsWith(".secureserver.net")) return; - - // Assign user-agent string (for RFC2971 ID command) - String mUserAgent = - ImapStore.getImapId(mImapStore.getContext(), mImapStore.getUsername(), host, - capabilities); - - if (mUserAgent != null) { - mIdPhrase = ImapConstants.ID + " (" + mUserAgent + ")"; - } else if (DEBUG_FORCE_SEND_ID) { - mIdPhrase = ImapConstants.ID + " " + ImapConstants.NIL; - } - // else: mIdPhrase = null, no ID will be emitted - - // Send user-agent in an RFC2971 ID command - if (mIdPhrase != null) { - try { - executeSimpleCommand(mIdPhrase); - } catch (ImapException ie) { - // Log for debugging, but this is not a fatal problem. - if (DebugUtils.DEBUG) { - LogUtils.d(Logging.LOG_TAG, ie, "ImapException"); - } - } catch (IOException ioe) { - // Special case to handle malformed OK responses and ignore them. - // A true IOException will recur on the following login steps - // This can go away after the parser is fixed - see bug 2138981 - } - } - } - - /** - * Gets the user's Personal Namespace from the IMAP server per RFC 2342. If the user - * explicitly sets a namespace (using setup UI) or if the server does not support the - * namespace command, this will perform no operation. - */ - private void doGetNamespace(boolean hasNamespaceCapability) throws MessagingException { - // user did not specify a hard-coded prefix; try to get it from the server - if (hasNamespaceCapability && !mImapStore.isUserPrefixSet()) { - List responseList = Collections.emptyList(); - - try { - responseList = executeSimpleCommand(ImapConstants.NAMESPACE); - } catch (ImapException ie) { - // Log for debugging, but this is not a fatal problem. - if (DebugUtils.DEBUG) { - LogUtils.d(Logging.LOG_TAG, ie, "ImapException"); - } - } catch (IOException ioe) { - // Special case to handle malformed OK responses and ignore them. - } - - for (ImapResponse response: responseList) { - if (response.isDataResponse(0, ImapConstants.NAMESPACE)) { - ImapList namespaceList = response.getListOrEmpty(1); - ImapList namespace = namespaceList.getListOrEmpty(0); - String namespaceString = namespace.getStringOrEmpty(0).getString(); - if (!TextUtils.isEmpty(namespaceString)) { - mImapStore.setPathPrefix(ImapStore.decodeFolderName(namespaceString, null)); - mImapStore.setPathSeparator(namespace.getStringOrEmpty(1).getString()); - } - } - } - } - } - - /** - * Logs into the IMAP server - */ - private void doLogin() throws IOException, MessagingException, AuthenticationFailedException { - try { - if (mImapStore.getUseOAuth()) { - // SASL authentication can take multiple steps. Currently the only SASL - // authentication supported is OAuth. - doSASLAuth(); - } else { - executeSimpleCommand(getLoginPhrase(), true); - } - } catch (ImapException ie) { - if (DebugUtils.DEBUG) { - LogUtils.d(Logging.LOG_TAG, ie, "ImapException"); - } - - final String code = ie.getResponseCode(); - final String alertText = ie.getAlertText(); - - // if the response code indicates expired or bad credentials, throw a special exception - if (ImapConstants.AUTHENTICATIONFAILED.equals(code) || - ImapConstants.EXPIRED.equals(code)) { - throw new AuthenticationFailedException(alertText, ie); - } - - throw new MessagingException(alertText, ie); - } - } - - /** - * Performs an SASL authentication. Currently, the only type of SASL authentication supported - * is OAuth. - * @throws MessagingException - * @throws IOException - */ - private void doSASLAuth() throws MessagingException, IOException { - LogUtils.d(Logging.LOG_TAG, "doSASLAuth"); - ImapResponse response = getOAuthResponse(); - if (!response.isOk()) { - // Failed to authenticate. This may be just due to an expired token. - LogUtils.d(Logging.LOG_TAG, "failed to authenticate, retrying"); - destroyResponses(); - // Clear the login phrase, this will force us to refresh the auth token. - mLoginPhrase = null; - // Close the transport so that we'll retry the authentication. - if (mTransport != null) { - mTransport.close(); - mTransport = null; - } - response = getOAuthResponse(); - if (!response.isOk()) { - LogUtils.d(Logging.LOG_TAG, "failed to authenticate, giving up"); - destroyResponses(); - throw new AuthenticationFailedException("OAuth failed after refresh"); - } - } - } - - private ImapResponse getOAuthResponse() throws IOException, MessagingException { - ImapResponse response; - sendCommandInternal(getLoginPhrase(), true); - do { - response = mParser.readResponse(); - } while (!response.isTagged() && !response.isContinuationRequest()); - - if (response.isContinuationRequest()) { - // SASL allows for a challenge/response type authentication, so if it doesn't yet have - // enough info, it will send back a continuation request. - // Currently, the only type of authentication we support is OAuth. The only case where - // it will send a continuation request is when we fail to authenticate. We need to - // reply with a CR/LF, and it will then return with a NO response. - sendCommandInternal("", true); - response = readResponse(); - } - - // if the response code indicates an error occurred within the server, indicate that - final String responseCode = response.getResponseCodeOrEmpty().getString(); - if (ImapConstants.UNAVAILABLE.equals(responseCode)) { - final String alert = response.getAlertTextOrEmpty().getString(); - throw new MessagingException(MessagingException.SERVER_ERROR, alert); - } - - return response; - } - - /** - * Gets the path separator per the LIST command in RFC 3501. If the path separator - * was obtained while obtaining the namespace or there is no prefix defined, this - * will perform no operation. - */ - private void doGetPathSeparator() throws MessagingException { - // user did not specify a hard-coded prefix; try to get it from the server - if (mImapStore.isUserPrefixSet()) { - List responseList = Collections.emptyList(); - - try { - responseList = executeSimpleCommand(ImapConstants.LIST + " \"\" \"\""); - } catch (ImapException ie) { - // Log for debugging, but this is not a fatal problem. - if (DebugUtils.DEBUG) { - LogUtils.d(Logging.LOG_TAG, ie, "ImapException"); - } - } catch (IOException ioe) { - // Special case to handle malformed OK responses and ignore them. - } - - for (ImapResponse response: responseList) { - if (response.isDataResponse(0, ImapConstants.LIST)) { - mImapStore.setPathSeparator(response.getStringOrEmpty(2).getString()); - } - } - } - } - - /** - * Starts a TLS session with the IMAP server per RFC 3501. If the user has not opted - * to use TLS or the server does not support the TLS capability, this will perform - * no operation. - */ - private ImapResponse doStartTls(boolean hasStartTlsCapability) - throws IOException, MessagingException { - if (mTransport.canTryTlsSecurity()) { - if (hasStartTlsCapability) { - // STARTTLS - executeSimpleCommand(ImapConstants.STARTTLS); - - mTransport.reopenTls(); - createParser(); - // Per RFC requirement (3501-6.2.1) gather new capabilities - return(queryCapabilities()); - } else { - if (DebugUtils.DEBUG) { - LogUtils.d(Logging.LOG_TAG, "TLS not supported but required"); - } - throw new MessagingException(MessagingException.TLS_REQUIRED); - } - } - return null; - } - - /** @see DiscourseLogger#logLastDiscourse() */ - void logLastDiscourse() { - mDiscourse.logLastDiscourse(); - } -} diff --git a/src/com/android/email/mail/store/ImapFolder.java b/src/com/android/email/mail/store/ImapFolder.java deleted file mode 100644 index 3a9081131..000000000 --- a/src/com/android/email/mail/store/ImapFolder.java +++ /dev/null @@ -1,1291 +0,0 @@ -/* - * 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.mail.store; - -import android.content.Context; -import android.text.TextUtils; -import android.util.Base64DataException; - -import com.android.email.DebugUtils; -import com.android.email.mail.store.ImapStore.ImapException; -import com.android.email.mail.store.ImapStore.ImapMessage; -import com.android.email.mail.store.imap.ImapConstants; -import com.android.email.mail.store.imap.ImapElement; -import com.android.email.mail.store.imap.ImapList; -import com.android.email.mail.store.imap.ImapResponse; -import com.android.email.mail.store.imap.ImapString; -import com.android.email.mail.store.imap.ImapUtility; -import com.android.email.service.ImapService; -import com.android.emailcommon.Logging; -import com.android.emailcommon.internet.BinaryTempFileBody; -import com.android.emailcommon.internet.MimeBodyPart; -import com.android.emailcommon.internet.MimeHeader; -import com.android.emailcommon.internet.MimeMultipart; -import com.android.emailcommon.internet.MimeUtility; -import com.android.emailcommon.mail.AuthenticationFailedException; -import com.android.emailcommon.mail.Body; -import com.android.emailcommon.mail.FetchProfile; -import com.android.emailcommon.mail.Flag; -import com.android.emailcommon.mail.Folder; -import com.android.emailcommon.mail.Message; -import com.android.emailcommon.mail.MessagingException; -import com.android.emailcommon.mail.Part; -import com.android.emailcommon.provider.Mailbox; -import com.android.emailcommon.service.SearchParams; -import com.android.emailcommon.utility.CountingOutputStream; -import com.android.emailcommon.utility.EOLConvertingOutputStream; -import com.android.emailcommon.utility.Utility; -import com.android.mail.utils.LogUtils; -import com.google.common.annotations.VisibleForTesting; - -import org.apache.commons.io.IOUtils; - -import java.io.File; -import java.io.FileInputStream; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.text.SimpleDateFormat; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Date; -import java.util.HashMap; -import java.util.LinkedHashSet; -import java.util.List; -import java.util.Locale; -import java.util.TimeZone; - -class ImapFolder extends Folder { - private final static Flag[] PERMANENT_FLAGS = - { Flag.DELETED, Flag.SEEN, Flag.FLAGGED, Flag.ANSWERED }; - private static final int COPY_BUFFER_SIZE = 16*1024; - - private final ImapStore mStore; - private final String mName; - private int mMessageCount = -1; - private ImapConnection mConnection; - private OpenMode mMode; - private boolean mExists; - /** The local mailbox associated with this remote folder */ - Mailbox mMailbox; - /** A set of hashes that can be used to track dirtiness */ - Object mHash[]; - - /*package*/ ImapFolder(ImapStore store, String name) { - mStore = store; - mName = name; - } - - private void destroyResponses() { - if (mConnection != null) { - mConnection.destroyResponses(); - } - } - - @Override - public void open(OpenMode mode) - throws MessagingException { - try { - if (isOpen()) { - if (mMode == mode) { - // Make sure the connection is valid. - // If it's not we'll close it down and continue on to get a new one. - try { - mConnection.executeSimpleCommand(ImapConstants.NOOP); - return; - - } catch (IOException ioe) { - ioExceptionHandler(mConnection, ioe); - } finally { - destroyResponses(); - } - } else { - // Return the connection to the pool, if exists. - close(false); - } - } - synchronized (this) { - mConnection = mStore.getConnection(); - } - // * FLAGS (\Answered \Flagged \Deleted \Seen \Draft NonJunk - // $MDNSent) - // * OK [PERMANENTFLAGS (\Answered \Flagged \Deleted \Seen \Draft - // NonJunk $MDNSent \*)] Flags permitted. - // * 23 EXISTS - // * 0 RECENT - // * OK [UIDVALIDITY 1125022061] UIDs valid - // * OK [UIDNEXT 57576] Predicted next UID - // 2 OK [READ-WRITE] Select completed. - try { - doSelect(); - } catch (IOException ioe) { - throw ioExceptionHandler(mConnection, ioe); - } finally { - destroyResponses(); - } - } catch (AuthenticationFailedException e) { - // Don't cache this connection, so we're forced to try connecting/login again - mConnection = null; - close(false); - throw e; - } catch (MessagingException e) { - mExists = false; - close(false); - throw e; - } - } - - @Override - @VisibleForTesting - public boolean isOpen() { - return mExists && mConnection != null; - } - - @Override - public OpenMode getMode() { - return mMode; - } - - @Override - public void close(boolean expunge) { - // TODO implement expunge - mMessageCount = -1; - synchronized (this) { - mStore.poolConnection(mConnection); - mConnection = null; - } - } - - @Override - public String getName() { - return mName; - } - - @Override - public boolean exists() throws MessagingException { - if (mExists) { - return true; - } - /* - * This method needs to operate in the unselected mode as well as the selected mode - * so we must get the connection ourselves if it's not there. We are specifically - * not calling checkOpen() since we don't care if the folder is open. - */ - ImapConnection connection = null; - synchronized(this) { - if (mConnection == null) { - connection = mStore.getConnection(); - } else { - connection = mConnection; - } - } - try { - connection.executeSimpleCommand(String.format(Locale.US, - ImapConstants.STATUS + " \"%s\" (" + ImapConstants.UIDVALIDITY + ")", - ImapStore.encodeFolderName(mName, mStore.mPathPrefix))); - mExists = true; - return true; - - } catch (MessagingException me) { - // Treat IOERROR messaging exception as IOException - if (me.getExceptionType() == MessagingException.IOERROR) { - throw me; - } - return false; - - } catch (IOException ioe) { - throw ioExceptionHandler(connection, ioe); - - } finally { - connection.destroyResponses(); - if (mConnection == null) { - mStore.poolConnection(connection); - } - } - } - - // IMAP supports folder creation - @Override - public boolean canCreate(FolderType type) { - return true; - } - - @Override - public boolean create(FolderType type) throws MessagingException { - /* - * This method needs to operate in the unselected mode as well as the selected mode - * so we must get the connection ourselves if it's not there. We are specifically - * not calling checkOpen() since we don't care if the folder is open. - */ - ImapConnection connection = null; - synchronized(this) { - if (mConnection == null) { - connection = mStore.getConnection(); - } else { - connection = mConnection; - } - } - try { - connection.executeSimpleCommand(String.format(Locale.US, - ImapConstants.CREATE + " \"%s\"", - ImapStore.encodeFolderName(mName, mStore.mPathPrefix))); - return true; - - } catch (MessagingException me) { - return false; - - } catch (IOException ioe) { - throw ioExceptionHandler(connection, ioe); - - } finally { - connection.destroyResponses(); - if (mConnection == null) { - mStore.poolConnection(connection); - } - } - } - - @Override - public void copyMessages(Message[] messages, Folder folder, - MessageUpdateCallbacks callbacks) throws MessagingException { - checkOpen(); - try { - List responseList = mConnection.executeSimpleCommand( - String.format(Locale.US, ImapConstants.UID_COPY + " %s \"%s\"", - ImapStore.joinMessageUids(messages), - ImapStore.encodeFolderName(folder.getName(), mStore.mPathPrefix))); - // Build a message map for faster UID matching - HashMap messageMap = new HashMap(); - boolean handledUidPlus = false; - for (Message m : messages) { - messageMap.put(m.getUid(), m); - } - // Process response to get the new UIDs - for (ImapResponse response : responseList) { - // All "BAD" responses are bad. Only "NO", tagged responses are bad. - if (response.isBad() || (response.isNo() && response.isTagged())) { - String responseText = response.getStatusResponseTextOrEmpty().getString(); - throw new MessagingException(responseText); - } - // Skip untagged responses; they're just status - if (!response.isTagged()) { - continue; - } - // No callback provided to report of UID changes; nothing more to do here - // NOTE: We check this here to catch any server errors - if (callbacks == null) { - continue; - } - ImapList copyResponse = response.getListOrEmpty(1); - String responseCode = copyResponse.getStringOrEmpty(0).getString(); - if (ImapConstants.COPYUID.equals(responseCode)) { - handledUidPlus = true; - String origIdSet = copyResponse.getStringOrEmpty(2).getString(); - String newIdSet = copyResponse.getStringOrEmpty(3).getString(); - String[] origIdArray = ImapUtility.getImapSequenceValues(origIdSet); - String[] newIdArray = ImapUtility.getImapSequenceValues(newIdSet); - // There has to be a 1:1 mapping between old and new IDs - if (origIdArray.length != newIdArray.length) { - throw new MessagingException("Set length mis-match; orig IDs \"" + - origIdSet + "\" new IDs \"" + newIdSet + "\""); - } - for (int i = 0; i < origIdArray.length; i++) { - final String id = origIdArray[i]; - final Message m = messageMap.get(id); - if (m != null) { - callbacks.onMessageUidChange(m, newIdArray[i]); - } - } - } - } - // If the server doesn't support UIDPLUS, try a different way to get the new UID(s) - if (callbacks != null && !handledUidPlus) { - final ImapFolder newFolder = (ImapFolder)folder; - try { - // Temporarily select the destination folder - newFolder.open(OpenMode.READ_WRITE); - // Do the search(es) ... - for (Message m : messages) { - final String searchString = - "HEADER Message-Id \"" + m.getMessageId() + "\""; - final String[] newIdArray = newFolder.searchForUids(searchString); - if (newIdArray.length == 1) { - callbacks.onMessageUidChange(m, newIdArray[0]); - } - } - } catch (MessagingException e) { - // Log, but, don't abort; failures here don't need to be propagated - LogUtils.d(Logging.LOG_TAG, "Failed to find message", e); - } finally { - newFolder.close(false); - } - // Re-select the original folder - doSelect(); - } - } catch (IOException ioe) { - throw ioExceptionHandler(mConnection, ioe); - } finally { - destroyResponses(); - } - } - - @Override - public int getMessageCount() { - return mMessageCount; - } - - @Override - public int getUnreadMessageCount() throws MessagingException { - checkOpen(); - try { - int unreadMessageCount = 0; - final List responses = mConnection.executeSimpleCommand( - String.format(Locale.US, - ImapConstants.STATUS + " \"%s\" (" + ImapConstants.UNSEEN + ")", - ImapStore.encodeFolderName(mName, mStore.mPathPrefix))); - // S: * STATUS mboxname (MESSAGES 231 UIDNEXT 44292) - for (ImapResponse response : responses) { - if (response.isDataResponse(0, ImapConstants.STATUS)) { - unreadMessageCount = response.getListOrEmpty(2) - .getKeyedStringOrEmpty(ImapConstants.UNSEEN).getNumberOrZero(); - } - } - return unreadMessageCount; - } catch (IOException ioe) { - throw ioExceptionHandler(mConnection, ioe); - } finally { - destroyResponses(); - } - } - - @Override - public void delete(boolean recurse) { - throw new Error("ImapStore.delete() not yet implemented"); - } - - String[] getSearchUids(List responses) { - // S: * SEARCH 2 3 6 - final ArrayList uids = new ArrayList(); - for (ImapResponse response : responses) { - if (!response.isDataResponse(0, ImapConstants.SEARCH)) { - continue; - } - // Found SEARCH response data - for (int i = 1; i < response.size(); i++) { - ImapString s = response.getStringOrEmpty(i); - if (s.isString()) { - uids.add(s.getString()); - } - } - } - return uids.toArray(Utility.EMPTY_STRINGS); - } - - String[] searchForUids(String searchCriteria) throws MessagingException { - return searchForUids(searchCriteria, true); - } - - /** - * I'm not a fan of having a parameter that determines whether to throw exceptions or - * consume them, but getMessage() for a date range needs to differentiate between - * a failure and just a legitimate empty result. - * See b/11183568. - * TODO: - * Either figure out how to make getMessage() with a date range work without this - * exception information, or make all users of searchForUids() handle the ImapException. - * It's too late in the release cycle to add this risk right now. - */ - @VisibleForTesting - String[] searchForUids(String searchCriteria, boolean swallowException) - throws MessagingException { - checkOpen(); - try { - try { - final String command = ImapConstants.UID_SEARCH + " " + searchCriteria; - final String[] result = getSearchUids(mConnection.executeSimpleCommand(command)); - LogUtils.d(Logging.LOG_TAG, "searchForUids '" + searchCriteria + "' results: " + - result.length); - return result; - } catch (ImapException me) { - LogUtils.d(Logging.LOG_TAG, me, "ImapException in search: " + searchCriteria); - if (swallowException) { - return Utility.EMPTY_STRINGS; // Not found - } else { - throw me; - } - } catch (IOException ioe) { - LogUtils.d(Logging.LOG_TAG, ioe, "IOException in search: " + searchCriteria); - throw ioExceptionHandler(mConnection, ioe); - } - } finally { - destroyResponses(); - } - } - - @Override - @VisibleForTesting - public Message getMessage(String uid) throws MessagingException { - checkOpen(); - - final String[] uids = searchForUids(ImapConstants.UID + " " + uid); - for (int i = 0; i < uids.length; i++) { - if (uids[i].equals(uid)) { - return new ImapMessage(uid, this); - } - } - return null; - } - - @VisibleForTesting - protected static boolean isAsciiString(String str) { - int len = str.length(); - for (int i = 0; i < len; i++) { - char c = str.charAt(i); - if (c >= 128) return false; - } - return true; - } - - /** - * Retrieve messages based on search parameters. We search FROM, TO, CC, SUBJECT, and BODY - * We send: SEARCH OR FROM "foo" (OR TO "foo" (OR CC "foo" (OR SUBJECT "foo" BODY "foo"))), but - * with the additional CHARSET argument and sending "foo" as a literal (e.g. {3}foo} - */ - @Override - @VisibleForTesting - public Message[] getMessages(SearchParams params, MessageRetrievalListener listener) - throws MessagingException { - List commands = new ArrayList(); - final String filter = params.mFilter; - // All servers MUST accept US-ASCII, so we'll send this as the CHARSET unless we're really - // dealing with a string that contains non-ascii characters - String charset = "US-ASCII"; - if (!isAsciiString(filter)) { - charset = "UTF-8"; - } - // This is the length of the string in octets (bytes), formatted as a string literal {n} - final String octetLength = "{" + filter.getBytes().length + "}"; - // Break the command up into pieces ending with the string literal length - commands.add(ImapConstants.UID_SEARCH + " CHARSET " + charset + " OR FROM " + octetLength); - commands.add(filter + " (OR TO " + octetLength); - commands.add(filter + " (OR CC " + octetLength); - commands.add(filter + " (OR SUBJECT " + octetLength); - commands.add(filter + " BODY " + octetLength); - commands.add(filter + ")))"); - return getMessagesInternal(complexSearchForUids(commands), listener); - } - - /* package */ String[] complexSearchForUids(List commands) throws MessagingException { - checkOpen(); - try { - try { - return getSearchUids(mConnection.executeComplexCommand(commands, false)); - } catch (ImapException e) { - return Utility.EMPTY_STRINGS; // not found; - } catch (IOException ioe) { - throw ioExceptionHandler(mConnection, ioe); - } - } finally { - destroyResponses(); - } - } - - @Override - @VisibleForTesting - public Message[] getMessages(int start, int end, MessageRetrievalListener listener) - throws MessagingException { - if (start < 1 || end < 1 || end < start) { - throw new MessagingException(String.format("Invalid range: %d %d", start, end)); - } - LogUtils.d(Logging.LOG_TAG, "getMessages number " + start + " - " + end); - return getMessagesInternal( - searchForUids(String.format(Locale.US, "%d:%d NOT DELETED", start, end)), listener); - } - - private String generateDateRangeCommand(final long startDate, final long endDate, - boolean useQuotes) - throws MessagingException { - // Dates must be formatted like: 7-Feb-1994. Time info within a date is not - // universally supported. - // XXX can I limit the maximum number of results? - final SimpleDateFormat formatter = new SimpleDateFormat("dd-LLL-yyyy", Locale.US); - formatter.setTimeZone(TimeZone.getTimeZone("UTC")); - final String sinceDateStr = formatter.format(endDate); - - StringBuilder queryParam = new StringBuilder(); - queryParam.append( "1:* "); - // If the caller requests a startDate of zero, then ignore the BEFORE parameter. - // This makes sure that we can always query for the newest messages, even if our - // time is different from the imap server's time. - if (startDate != 0) { - final String beforeDateStr = formatter.format(startDate); - if (startDate < endDate) { - throw new MessagingException(String.format("Invalid date range: %s - %s", - sinceDateStr, beforeDateStr)); - } - queryParam.append("BEFORE "); - if (useQuotes) queryParam.append('\"'); - queryParam.append(beforeDateStr); - if (useQuotes) queryParam.append('\"'); - queryParam.append(" "); - } - queryParam.append("SINCE "); - if (useQuotes) queryParam.append('\"'); - queryParam.append(sinceDateStr); - if (useQuotes) queryParam.append('\"'); - - return queryParam.toString(); - } - - @Override - @VisibleForTesting - public Message[] getMessages(long startDate, long endDate, MessageRetrievalListener listener) - throws MessagingException { - String [] uids = null; - String command = generateDateRangeCommand(startDate, endDate, false); - LogUtils.d(Logging.LOG_TAG, "getMessages dateRange " + command.toString()); - - try { - uids = searchForUids(command.toString(), false); - } catch (ImapException e) { - // TODO: This is a last minute hack to make certain servers work. Some servers - // demand that the date in the date range be surrounded by double quotes, other - // servers won't accept that. So if we can an ImapException using one method, - // try the other. - // See b/11183568 - LogUtils.d(Logging.LOG_TAG, e, "query failed %s, trying alternate", - command.toString()); - command = generateDateRangeCommand(startDate, endDate, true); - try { - uids = searchForUids(command, true); - } catch (ImapException e2) { - LogUtils.w(Logging.LOG_TAG, e2, "query failed %s, fatal", command); - uids = null; - } - } - return getMessagesInternal(uids, listener); - } - - @Override - @VisibleForTesting - public Message[] getMessages(String[] uids, MessageRetrievalListener listener) - throws MessagingException { - if (uids == null) { - uids = searchForUids("1:* NOT DELETED"); - } - return getMessagesInternal(uids, listener); - } - - public Message[] getMessagesInternal(String[] uids, MessageRetrievalListener listener) { - final ArrayList messages = new ArrayList(uids.length); - for (int i = 0; i < uids.length; i++) { - final String uid = uids[i]; - final ImapMessage message = new ImapMessage(uid, this); - messages.add(message); - if (listener != null) { - listener.messageRetrieved(message); - } - } - return messages.toArray(Message.EMPTY_ARRAY); - } - - @Override - public void fetch(Message[] messages, FetchProfile fp, MessageRetrievalListener listener) - throws MessagingException { - try { - fetchInternal(messages, fp, listener); - } catch (RuntimeException e) { // Probably a parser error. - LogUtils.w(Logging.LOG_TAG, "Exception detected: " + e.getMessage()); - if (mConnection != null) { - mConnection.logLastDiscourse(); - } - throw e; - } - } - - public void fetchInternal(Message[] messages, FetchProfile fp, - MessageRetrievalListener listener) throws MessagingException { - if (messages.length == 0) { - return; - } - checkOpen(); - HashMap messageMap = new HashMap(); - for (Message m : messages) { - messageMap.put(m.getUid(), m); - } - - /* - * Figure out what command we are going to run: - * FLAGS - UID FETCH (FLAGS) - * ENVELOPE - UID FETCH (INTERNALDATE UID RFC822.SIZE FLAGS BODY.PEEK[ - * HEADER.FIELDS (date subject from content-type to cc)]) - * STRUCTURE - UID FETCH (BODYSTRUCTURE) - * BODY_SANE - UID FETCH (BODY.PEEK[]<0.N>) where N = max bytes returned - * BODY - UID FETCH (BODY.PEEK[]) - * Part - UID FETCH (BODY.PEEK[ID]) where ID = mime part ID - */ - - final LinkedHashSet fetchFields = new LinkedHashSet(); - - fetchFields.add(ImapConstants.UID); - if (fp.contains(FetchProfile.Item.FLAGS)) { - fetchFields.add(ImapConstants.FLAGS); - } - if (fp.contains(FetchProfile.Item.ENVELOPE)) { - fetchFields.add(ImapConstants.INTERNALDATE); - fetchFields.add(ImapConstants.RFC822_SIZE); - fetchFields.add(ImapConstants.FETCH_FIELD_HEADERS); - } - if (fp.contains(FetchProfile.Item.STRUCTURE)) { - fetchFields.add(ImapConstants.BODYSTRUCTURE); - } - - if (fp.contains(FetchProfile.Item.BODY_SANE)) { - fetchFields.add(ImapConstants.FETCH_FIELD_BODY_PEEK_SANE); - } - if (fp.contains(FetchProfile.Item.BODY)) { - fetchFields.add(ImapConstants.FETCH_FIELD_BODY_PEEK); - } - - // TODO Why are we only fetching the first part given? - final Part fetchPart = fp.getFirstPart(); - if (fetchPart != null) { - final String[] partIds = - fetchPart.getHeader(MimeHeader.HEADER_ANDROID_ATTACHMENT_STORE_DATA); - // TODO Why can a single part have more than one Id? And why should we only fetch - // the first id if there are more than one? - if (partIds != null) { - fetchFields.add(ImapConstants.FETCH_FIELD_BODY_PEEK_BARE - + "[" + partIds[0] + "]"); - } - } - - try { - mConnection.sendCommand(String.format(Locale.US, - ImapConstants.UID_FETCH + " %s (%s)", ImapStore.joinMessageUids(messages), - Utility.combine(fetchFields.toArray(new String[fetchFields.size()]), ' ') - ), false); - ImapResponse response; - do { - response = null; - try { - response = mConnection.readResponse(); - - if (!response.isDataResponse(1, ImapConstants.FETCH)) { - continue; // Ignore - } - final ImapList fetchList = response.getListOrEmpty(2); - final String uid = fetchList.getKeyedStringOrEmpty(ImapConstants.UID) - .getString(); - if (TextUtils.isEmpty(uid)) continue; - - ImapMessage message = (ImapMessage) messageMap.get(uid); - if (message == null) continue; - - if (fp.contains(FetchProfile.Item.FLAGS)) { - final ImapList flags = - fetchList.getKeyedListOrEmpty(ImapConstants.FLAGS); - for (int i = 0, count = flags.size(); i < count; i++) { - final ImapString flag = flags.getStringOrEmpty(i); - if (flag.is(ImapConstants.FLAG_DELETED)) { - message.setFlagInternal(Flag.DELETED, true); - } else if (flag.is(ImapConstants.FLAG_ANSWERED)) { - message.setFlagInternal(Flag.ANSWERED, true); - } else if (flag.is(ImapConstants.FLAG_SEEN)) { - message.setFlagInternal(Flag.SEEN, true); - } else if (flag.is(ImapConstants.FLAG_FLAGGED)) { - message.setFlagInternal(Flag.FLAGGED, true); - } - } - } - if (fp.contains(FetchProfile.Item.ENVELOPE)) { - final Date internalDate = fetchList.getKeyedStringOrEmpty( - ImapConstants.INTERNALDATE).getDateOrNull(); - final int size = fetchList.getKeyedStringOrEmpty( - ImapConstants.RFC822_SIZE).getNumberOrZero(); - final String header = fetchList.getKeyedStringOrEmpty( - ImapConstants.BODY_BRACKET_HEADER, true).getString(); - - message.setInternalDate(internalDate); - message.setSize(size); - message.parse(Utility.streamFromAsciiString(header)); - } - if (fp.contains(FetchProfile.Item.STRUCTURE)) { - ImapList bs = fetchList.getKeyedListOrEmpty( - ImapConstants.BODYSTRUCTURE); - if (!bs.isEmpty()) { - try { - parseBodyStructure(bs, message, ImapConstants.TEXT); - } catch (MessagingException e) { - if (Logging.LOGD) { - LogUtils.v(Logging.LOG_TAG, e, "Error handling message"); - } - message.setBody(null); - } - } - } - if (fp.contains(FetchProfile.Item.BODY) - || fp.contains(FetchProfile.Item.BODY_SANE)) { - // Body is keyed by "BODY[]...". - // Previously used "BODY[..." but this can be confused with "BODY[HEADER..." - // TODO Should we accept "RFC822" as well?? - ImapString body = fetchList.getKeyedStringOrEmpty("BODY[]", true); - InputStream bodyStream = body.getAsStream(); - message.parse(bodyStream); - } - if (fetchPart != null) { - InputStream bodyStream = - fetchList.getKeyedStringOrEmpty("BODY[", true).getAsStream(); - String encodings[] = fetchPart.getHeader( - MimeHeader.HEADER_CONTENT_TRANSFER_ENCODING); - - String contentTransferEncoding = null; - if (encodings != null && encodings.length > 0) { - contentTransferEncoding = encodings[0]; - } else { - // According to http://tools.ietf.org/html/rfc2045#section-6.1 - // "7bit" is the default. - contentTransferEncoding = "7bit"; - } - - try { - // TODO Don't create 2 temp files. - // decodeBody creates BinaryTempFileBody, but we could avoid this - // if we implement ImapStringBody. - // (We'll need to share a temp file. Protect it with a ref-count.) - fetchPart.setBody(decodeBody(bodyStream, contentTransferEncoding, - fetchPart.getSize(), listener)); - } catch(Exception e) { - // TODO: Figure out what kinds of exceptions might actually be thrown - // from here. This blanket catch-all is because we're not sure what to - // do if we don't have a contentTransferEncoding, and we don't have - // time to figure out what exceptions might be thrown. - LogUtils.e(Logging.LOG_TAG, "Error fetching body %s", e); - } - } - - if (listener != null) { - listener.messageRetrieved(message); - } - } finally { - destroyResponses(); - } - } while (!response.isTagged()); - } catch (IOException ioe) { - throw ioExceptionHandler(mConnection, ioe); - } - } - - /** - * Removes any content transfer encoding from the stream and returns a Body. - * This code is taken/condensed from MimeUtility.decodeBody - */ - private static Body decodeBody(InputStream in, String contentTransferEncoding, int size, - MessageRetrievalListener listener) throws IOException { - // Get a properly wrapped input stream - in = MimeUtility.getInputStreamForContentTransferEncoding(in, contentTransferEncoding); - BinaryTempFileBody tempBody = new BinaryTempFileBody(); - OutputStream out = tempBody.getOutputStream(); - try { - byte[] buffer = new byte[COPY_BUFFER_SIZE]; - int n = 0; - int count = 0; - while (-1 != (n = in.read(buffer))) { - out.write(buffer, 0, n); - count += n; - if (listener != null) { - if (size == 0) { - // We don't know how big the file is, so just fake it. - listener.loadAttachmentProgress((int)Math.ceil(100 * (1-1.0/count))); - } else { - listener.loadAttachmentProgress(count * 100 / size); - } - } - } - } catch (Base64DataException bde) { - String warning = "\n\n" + ImapService.getMessageDecodeErrorString(); - out.write(warning.getBytes()); - } finally { - out.close(); - } - return tempBody; - } - - @Override - public Flag[] getPermanentFlags() { - return PERMANENT_FLAGS; - } - - /** - * Handle any untagged responses that the caller doesn't care to handle themselves. - * @param responses - */ - private void handleUntaggedResponses(List responses) { - for (ImapResponse response : responses) { - handleUntaggedResponse(response); - } - } - - /** - * Handle an untagged response that the caller doesn't care to handle themselves. - * @param response - */ - private void handleUntaggedResponse(ImapResponse response) { - if (response.isDataResponse(1, ImapConstants.EXISTS)) { - mMessageCount = response.getStringOrEmpty(0).getNumberOrZero(); - } - } - - private static void parseBodyStructure(ImapList bs, Part part, String id) - throws MessagingException { - if (bs.getElementOrNone(0).isList()) { - /* - * This is a multipart/* - */ - MimeMultipart mp = new MimeMultipart(); - for (int i = 0, count = bs.size(); i < count; i++) { - ImapElement e = bs.getElementOrNone(i); - if (e.isList()) { - /* - * For each part in the message we're going to add a new BodyPart and parse - * into it. - */ - MimeBodyPart bp = new MimeBodyPart(); - if (id.equals(ImapConstants.TEXT)) { - parseBodyStructure(bs.getListOrEmpty(i), bp, Integer.toString(i + 1)); - - } else { - parseBodyStructure(bs.getListOrEmpty(i), bp, id + "." + (i + 1)); - } - mp.addBodyPart(bp); - - } else { - if (e.isString()) { - mp.setSubType(bs.getStringOrEmpty(i).getString().toLowerCase(Locale.US)); - } - break; // Ignore the rest of the list. - } - } - part.setBody(mp); - } else { - /* - * This is a body. We need to add as much information as we can find out about - * it to the Part. - */ - - /* - body type - body subtype - body parameter parenthesized list - body id - body description - body encoding - body size - */ - - final ImapString type = bs.getStringOrEmpty(0); - final ImapString subType = bs.getStringOrEmpty(1); - final String mimeType = - (type.getString() + "/" + subType.getString()).toLowerCase(Locale.US); - - final ImapList bodyParams = bs.getListOrEmpty(2); - final ImapString cid = bs.getStringOrEmpty(3); - final ImapString encoding = bs.getStringOrEmpty(5); - final int size = bs.getStringOrEmpty(6).getNumberOrZero(); - - if (MimeUtility.mimeTypeMatches(mimeType, MimeUtility.MIME_TYPE_RFC822)) { - // A body type of type MESSAGE and subtype RFC822 - // contains, immediately after the basic fields, the - // envelope structure, body structure, and size in - // text lines of the encapsulated message. - // [MESSAGE, RFC822, [NAME, filename.eml], NIL, NIL, 7BIT, 5974, NIL, - // [INLINE, [FILENAME*0, Fwd: Xxx..., FILENAME*1, filename.eml]], NIL] - /* - * This will be caught by fetch and handled appropriately. - */ - throw new MessagingException("BODYSTRUCTURE " + MimeUtility.MIME_TYPE_RFC822 - + " not yet supported."); - } - - /* - * Set the content type with as much information as we know right now. - */ - final StringBuilder contentType = new StringBuilder(mimeType); - - /* - * If there are body params we might be able to get some more information out - * of them. - */ - for (int i = 1, count = bodyParams.size(); i < count; i += 2) { - - // TODO We need to convert " into %22, but - // because MimeUtility.getHeaderParameter doesn't recognize it, - // we can't fix it for now. - contentType.append(String.format(";\n %s=\"%s\"", - bodyParams.getStringOrEmpty(i - 1).getString(), - bodyParams.getStringOrEmpty(i).getString())); - } - - part.setHeader(MimeHeader.HEADER_CONTENT_TYPE, contentType.toString()); - - // Extension items - final ImapList bodyDisposition; - - if (type.is(ImapConstants.TEXT) && bs.getElementOrNone(9).isList()) { - // If media-type is TEXT, 9th element might be: [body-fld-lines] := number - // So, if it's not a list, use 10th element. - // (Couldn't find evidence in the RFC if it's ALWAYS 10th element.) - bodyDisposition = bs.getListOrEmpty(9); - } else { - bodyDisposition = bs.getListOrEmpty(8); - } - - final StringBuilder contentDisposition = new StringBuilder(); - - if (bodyDisposition.size() > 0) { - final String bodyDisposition0Str = - bodyDisposition.getStringOrEmpty(0).getString().toLowerCase(Locale.US); - if (!TextUtils.isEmpty(bodyDisposition0Str)) { - contentDisposition.append(bodyDisposition0Str); - } - - final ImapList bodyDispositionParams = bodyDisposition.getListOrEmpty(1); - if (!bodyDispositionParams.isEmpty()) { - /* - * If there is body disposition information we can pull some more - * information about the attachment out. - */ - for (int i = 1, count = bodyDispositionParams.size(); i < count; i += 2) { - - // TODO We need to convert " into %22. See above. - contentDisposition.append(String.format(Locale.US, ";\n %s=\"%s\"", - bodyDispositionParams.getStringOrEmpty(i - 1) - .getString().toLowerCase(Locale.US), - bodyDispositionParams.getStringOrEmpty(i).getString())); - } - } - } - - if ((size > 0) - && (MimeUtility.getHeaderParameter(contentDisposition.toString(), "size") - == null)) { - contentDisposition.append(String.format(Locale.US, ";\n size=%d", size)); - } - - if (contentDisposition.length() > 0) { - /* - * Set the content disposition containing at least the size. Attachment - * handling code will use this down the road. - */ - part.setHeader(MimeHeader.HEADER_CONTENT_DISPOSITION, - contentDisposition.toString()); - } - - /* - * Set the Content-Transfer-Encoding header. Attachment code will use this - * to parse the body. - */ - if (!encoding.isEmpty()) { - part.setHeader(MimeHeader.HEADER_CONTENT_TRANSFER_ENCODING, - encoding.getString()); - } - - /* - * Set the Content-ID header. - */ - if (!cid.isEmpty()) { - part.setHeader(MimeHeader.HEADER_CONTENT_ID, cid.getString()); - } - - if (size > 0) { - if (part instanceof ImapMessage) { - ((ImapMessage) part).setSize(size); - } else if (part instanceof MimeBodyPart) { - ((MimeBodyPart) part).setSize(size); - } else { - throw new MessagingException("Unknown part type " + part.toString()); - } - } - part.setHeader(MimeHeader.HEADER_ANDROID_ATTACHMENT_STORE_DATA, id); - } - - } - - /** - * Appends the given messages to the selected folder. This implementation also determines - * the new UID of the given message on the IMAP server and sets the Message's UID to the - * new server UID. - * @param message Message - * @param noTimeout Set to true on manual syncs, disables the timeout after sending the message - * content to the server - */ - @Override - public void appendMessage(final Context context, final Message message, final boolean noTimeout) - throws MessagingException { - checkOpen(); - try { - // Create temp file - /** - * We need to know the encoded message size before we upload it, and encoding - * attachments as Base64, possibly reading from a slow provider, is a non-trivial - * operation. So we write the contents to a temp file while measuring the size, - * and then use that temp file and size to do the actual upsync. - * For context, most classic email clients would store the message in RFC822 format - * internally, and so would not need to do this on-the-fly. - */ - final File tempDir = context.getExternalCacheDir(); - final File tempFile = File.createTempFile("IMAPupsync", ".eml", tempDir); - // Delete here so we don't leave the file lingering. We've got a handle to it so we - // can still use it. - final boolean deleteSuccessful = tempFile.delete(); - if (!deleteSuccessful) { - LogUtils.w(LogUtils.TAG, "Could not delete temp file %s", - tempFile.getAbsolutePath()); - } - final OutputStream tempOut = new FileOutputStream(tempFile); - // Create output count while writing temp file - final CountingOutputStream out = new CountingOutputStream(tempOut); - final EOLConvertingOutputStream eolOut = new EOLConvertingOutputStream(out); - message.writeTo(eolOut); - eolOut.flush(); - // Create flag list (most often this will be "\SEEN") - String flagList = ""; - Flag[] flags = message.getFlags(); - if (flags.length > 0) { - StringBuilder sb = new StringBuilder(); - for (final Flag flag : flags) { - if (flag == Flag.SEEN) { - sb.append(" " + ImapConstants.FLAG_SEEN); - } else if (flag == Flag.FLAGGED) { - sb.append(" " + ImapConstants.FLAG_FLAGGED); - } - } - if (sb.length() > 0) { - flagList = sb.substring(1); - } - } - - mConnection.sendCommand( - String.format(Locale.US, ImapConstants.APPEND + " \"%s\" (%s) {%d}", - ImapStore.encodeFolderName(mName, mStore.mPathPrefix), - flagList, - out.getCount()), false); - ImapResponse response; - do { - final int socketTimeout = mConnection.mTransport.getSoTimeout(); - try { - // Need to set the timeout to unlimited since we might be upsyncing a pretty - // big attachment so who knows how long it'll take. It would sure be nice - // if this only timed out after the send buffer drained but welp. - if (noTimeout) { - // For now, only unset the timeout if we're doing a manual sync - mConnection.mTransport.setSoTimeout(0); - } - response = mConnection.readResponse(); - if (response.isContinuationRequest()) { - final OutputStream transportOutputStream = - mConnection.mTransport.getOutputStream(); - IOUtils.copyLarge(new FileInputStream(tempFile), transportOutputStream); - transportOutputStream.write('\r'); - transportOutputStream.write('\n'); - transportOutputStream.flush(); - } else if (!response.isTagged()) { - handleUntaggedResponse(response); - } - } finally { - mConnection.mTransport.setSoTimeout(socketTimeout); - } - } while (!response.isTagged()); - - // TODO Why not check the response? - - /* - * Try to recover the UID of the message from an APPENDUID response. - * e.g. 11 OK [APPENDUID 2 238268] APPEND completed - */ - final ImapList appendList = response.getListOrEmpty(1); - if ((appendList.size() >= 3) && appendList.is(0, ImapConstants.APPENDUID)) { - String serverUid = appendList.getStringOrEmpty(2).getString(); - if (!TextUtils.isEmpty(serverUid)) { - message.setUid(serverUid); - return; - } - } - - /* - * Try to find the UID of the message we just appended using the - * Message-ID header. If there are more than one response, take the - * last one, as it's most likely the newest (the one we just uploaded). - */ - final String messageId = message.getMessageId(); - if (messageId == null || messageId.length() == 0) { - return; - } - // Most servers don't care about parenthesis in the search query [and, some - // fail to work if they are used] - String[] uids = searchForUids( - String.format(Locale.US, "HEADER MESSAGE-ID %s", messageId)); - if (uids.length > 0) { - message.setUid(uids[0]); - } - // However, there's at least one server [AOL] that fails to work unless there - // are parenthesis, so, try this as a last resort - uids = searchForUids(String.format(Locale.US, "(HEADER MESSAGE-ID %s)", messageId)); - if (uids.length > 0) { - message.setUid(uids[0]); - } - } catch (IOException ioe) { - throw ioExceptionHandler(mConnection, ioe); - } finally { - destroyResponses(); - } - } - - @Override - public Message[] expunge() throws MessagingException { - checkOpen(); - try { - handleUntaggedResponses(mConnection.executeSimpleCommand(ImapConstants.EXPUNGE)); - } catch (IOException ioe) { - throw ioExceptionHandler(mConnection, ioe); - } finally { - destroyResponses(); - } - return null; - } - - @Override - public void setFlags(Message[] messages, Flag[] flags, boolean value) - throws MessagingException { - checkOpen(); - - String allFlags = ""; - if (flags.length > 0) { - StringBuilder flagList = new StringBuilder(); - for (int i = 0, count = flags.length; i < count; i++) { - Flag flag = flags[i]; - if (flag == Flag.SEEN) { - flagList.append(" " + ImapConstants.FLAG_SEEN); - } else if (flag == Flag.DELETED) { - flagList.append(" " + ImapConstants.FLAG_DELETED); - } else if (flag == Flag.FLAGGED) { - flagList.append(" " + ImapConstants.FLAG_FLAGGED); - } else if (flag == Flag.ANSWERED) { - flagList.append(" " + ImapConstants.FLAG_ANSWERED); - } - } - allFlags = flagList.substring(1); - } - try { - mConnection.executeSimpleCommand(String.format(Locale.US, - ImapConstants.UID_STORE + " %s %s" + ImapConstants.FLAGS_SILENT + " (%s)", - ImapStore.joinMessageUids(messages), - value ? "+" : "-", - allFlags)); - - } catch (IOException ioe) { - throw ioExceptionHandler(mConnection, ioe); - } finally { - destroyResponses(); - } - } - - /** - * Persists this folder. We will always perform the proper database operation (e.g. - * 'save' or 'update'). As an optimization, if a folder has not been modified, no - * database operations are performed. - */ - void save(Context context) { - final Mailbox mailbox = mMailbox; - if (!mailbox.isSaved()) { - mailbox.save(context); - mHash = mailbox.getHashes(); - } else { - Object[] hash = mailbox.getHashes(); - if (!Arrays.equals(mHash, hash)) { - mailbox.update(context, mailbox.toContentValues()); - mHash = hash; // Save updated hash - } - } - } - - /** - * Selects the folder for use. Before performing any operations on this folder, it - * must be selected. - */ - private void doSelect() throws IOException, MessagingException { - final List responses = mConnection.executeSimpleCommand( - String.format(Locale.US, ImapConstants.SELECT + " \"%s\"", - ImapStore.encodeFolderName(mName, mStore.mPathPrefix))); - - // Assume the folder is opened read-write; unless we are notified otherwise - mMode = OpenMode.READ_WRITE; - int messageCount = -1; - for (ImapResponse response : responses) { - if (response.isDataResponse(1, ImapConstants.EXISTS)) { - messageCount = response.getStringOrEmpty(0).getNumberOrZero(); - } else if (response.isOk()) { - final ImapString responseCode = response.getResponseCodeOrEmpty(); - if (responseCode.is(ImapConstants.READ_ONLY)) { - mMode = OpenMode.READ_ONLY; - } else if (responseCode.is(ImapConstants.READ_WRITE)) { - mMode = OpenMode.READ_WRITE; - } - } else if (response.isTagged()) { // Not OK - throw new MessagingException("Can't open mailbox: " - + response.getStatusResponseTextOrEmpty()); - } - } - if (messageCount == -1) { - throw new MessagingException("Did not find message count during select"); - } - mMessageCount = messageCount; - mExists = true; - } - - private void checkOpen() throws MessagingException { - if (!isOpen()) { - throw new MessagingException("Folder " + mName + " is not open."); - } - } - - private MessagingException ioExceptionHandler(ImapConnection connection, IOException ioe) { - if (DebugUtils.DEBUG) { - LogUtils.d(Logging.LOG_TAG, "IO Exception detected: ", ioe); - } - connection.close(); - if (connection == mConnection) { - mConnection = null; // To prevent close() from returning the connection to the pool. - close(false); - } - return new MessagingException(MessagingException.IOERROR, "IO Error", ioe); - } - - @Override - public boolean equals(Object o) { - if (o instanceof ImapFolder) { - return ((ImapFolder)o).mName.equals(mName); - } - return super.equals(o); - } - - @Override - public Message createMessage(String uid) { - return new ImapMessage(uid, this); - } -} diff --git a/src/com/android/email/mail/store/ImapStore.java b/src/com/android/email/mail/store/ImapStore.java deleted file mode 100644 index d45188837..000000000 --- a/src/com/android/email/mail/store/ImapStore.java +++ /dev/null @@ -1,657 +0,0 @@ -/* - * 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.mail.store; - -import android.content.Context; -import android.os.Build; -import android.os.Bundle; -import android.telephony.TelephonyManager; -import android.text.TextUtils; -import android.util.Base64; - -import com.android.email.LegacyConversions; -import com.android.email.Preferences; -import com.android.email.mail.Store; -import com.android.email.mail.store.imap.ImapConstants; -import com.android.email.mail.store.imap.ImapResponse; -import com.android.email.mail.store.imap.ImapString; -import com.android.email.mail.transport.MailTransport; -import com.android.emailcommon.Logging; -import com.android.emailcommon.VendorPolicyLoader; -import com.android.emailcommon.internet.MimeMessage; -import com.android.emailcommon.mail.AuthenticationFailedException; -import com.android.emailcommon.mail.Flag; -import com.android.emailcommon.mail.Folder; -import com.android.emailcommon.mail.Message; -import com.android.emailcommon.mail.MessagingException; -import com.android.emailcommon.provider.Account; -import com.android.emailcommon.provider.Credential; -import com.android.emailcommon.provider.HostAuth; -import com.android.emailcommon.provider.Mailbox; -import com.android.emailcommon.service.EmailServiceProxy; -import com.android.emailcommon.utility.Utility; -import com.android.mail.utils.LogUtils; -import com.beetstra.jutf7.CharsetProvider; -import com.google.common.annotations.VisibleForTesting; - -import java.io.IOException; -import java.io.InputStream; -import java.nio.ByteBuffer; -import java.nio.charset.Charset; -import java.security.MessageDigest; -import java.security.NoSuchAlgorithmException; -import java.util.Collection; -import java.util.HashMap; -import java.util.List; -import java.util.Set; -import java.util.concurrent.ConcurrentLinkedQueue; -import java.util.regex.Pattern; - - -/** - *
- * TODO Need to start keeping track of UIDVALIDITY
- * TODO Need a default response handler for things like folder updates
- * TODO In fetch(), if we need a ImapMessage and were given
- *      something else we can try to do a pre-fetch first.
- * TODO Collect ALERT messages and show them to users.
- *
- * ftp://ftp.isi.edu/in-notes/rfc2683.txt When a client asks for
- * certain information in a FETCH command, the server may return the requested
- * information in any order, not necessarily in the order that it was requested.
- * Further, the server may return the information in separate FETCH responses
- * and may also return information that was not explicitly requested (to reflect
- * to the client changes in the state of the subject message).
- * 
- */ -public class ImapStore extends Store { - /** Charset used for converting folder names to and from UTF-7 as defined by RFC 3501. */ - private static final Charset MODIFIED_UTF_7_CHARSET = - new CharsetProvider().charsetForName("X-RFC-3501"); - - @VisibleForTesting static String sImapId = null; - @VisibleForTesting String mPathPrefix; - @VisibleForTesting String mPathSeparator; - - private boolean mUseOAuth; - - private final ConcurrentLinkedQueue mConnectionPool = - new ConcurrentLinkedQueue(); - - /** - * Static named constructor. - */ - public static Store newInstance(Account account, Context context) throws MessagingException { - return new ImapStore(context, account); - } - - /** - * Creates a new store for the given account. Always use - * {@link #newInstance(Account, Context)} to create an IMAP store. - */ - private ImapStore(Context context, Account account) throws MessagingException { - mContext = context; - mAccount = account; - - HostAuth recvAuth = account.getOrCreateHostAuthRecv(context); - if (recvAuth == null) { - throw new MessagingException("No HostAuth in ImapStore?"); - } - mTransport = new MailTransport(context, "IMAP", recvAuth); - - String[] userInfo = recvAuth.getLogin(); - mUsername = userInfo[0]; - mPassword = userInfo[1]; - final Credential cred = recvAuth.getCredential(context); - mUseOAuth = (cred != null); - mPathPrefix = recvAuth.mDomain; - } - - boolean getUseOAuth() { - return mUseOAuth; - } - - String getUsername() { - return mUsername; - } - - String getPassword() { - return mPassword; - } - - @VisibleForTesting - Collection getConnectionPoolForTest() { - return mConnectionPool; - } - - /** - * For testing only. Injects a different root transport (it will be copied using - * newInstanceWithConfiguration() each time IMAP sets up a new channel). The transport - * should already be set up and ready to use. Do not use for real code. - * @param testTransport The Transport to inject and use for all future communication. - */ - @VisibleForTesting - void setTransportForTest(MailTransport testTransport) { - mTransport = testTransport; - } - - /** - * Return, or create and return, an string suitable for use in an IMAP ID message. - * This is constructed similarly to the way the browser sets up its user-agent strings. - * See RFC 2971 for more details. The output of this command will be a series of key-value - * pairs delimited by spaces (there is no point in returning a structured result because - * this will be sent as-is to the IMAP server). No tokens, parenthesis or "ID" are included, - * because some connections may append additional values. - * - * The following IMAP ID keys may be included: - * name Android package name of the program - * os "android" - * os-version "version; model; build-id" - * vendor Vendor of the client/server - * x-android-device-model Model (only revealed if release build) - * x-android-net-operator Mobile network operator (if known) - * AGUID A device+account UID - * - * In addition, a vendor policy .apk can append key/value pairs. - * - * @param userName the username of the account - * @param host the host (server) of the account - * @param capabilities a list of the capabilities from the server - * @return a String for use in an IMAP ID message. - */ - public static String getImapId(Context context, String userName, String host, - String capabilities) { - // The first section is global to all IMAP connections, and generates the fixed - // values in any IMAP ID message - synchronized (ImapStore.class) { - if (sImapId == null) { - TelephonyManager tm = - (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE); - String networkOperator = tm.getNetworkOperatorName(); - if (networkOperator == null) networkOperator = ""; - - sImapId = makeCommonImapId(context.getPackageName(), Build.VERSION.RELEASE, - Build.VERSION.CODENAME, Build.MODEL, Build.ID, Build.MANUFACTURER, - networkOperator); - } - } - - // This section is per Store, and adds in a dynamic elements like UID's. - // We don't cache the result of this work, because the caller does anyway. - StringBuilder id = new StringBuilder(sImapId); - - // Optionally add any vendor-supplied id keys - String vendorId = VendorPolicyLoader.getInstance(context).getImapIdValues(userName, host, - capabilities); - if (vendorId != null) { - id.append(' '); - id.append(vendorId); - } - - // Generate a UID that mixes a "stable" device UID with the email address - try { - String devUID = Preferences.getPreferences(context).getDeviceUID(); - MessageDigest messageDigest; - messageDigest = MessageDigest.getInstance("SHA-1"); - messageDigest.update(userName.getBytes()); - messageDigest.update(devUID.getBytes()); - byte[] uid = messageDigest.digest(); - String hexUid = Base64.encodeToString(uid, Base64.NO_WRAP); - id.append(" \"AGUID\" \""); - id.append(hexUid); - id.append('\"'); - } catch (NoSuchAlgorithmException e) { - LogUtils.d(Logging.LOG_TAG, "couldn't obtain SHA-1 hash for device UID"); - } - return id.toString(); - } - - /** - * Helper function that actually builds the static part of the IMAP ID string. This is - * separated from getImapId for testability. There is no escaping or encoding in IMAP ID so - * any rogue chars must be filtered here. - * - * @param packageName context.getPackageName() - * @param version Build.VERSION.RELEASE - * @param codeName Build.VERSION.CODENAME - * @param model Build.MODEL - * @param id Build.ID - * @param vendor Build.MANUFACTURER - * @param networkOperator TelephonyManager.getNetworkOperatorName() - * @return the static (never changes) portion of the IMAP ID - */ - @VisibleForTesting - static String makeCommonImapId(String packageName, String version, - String codeName, String model, String id, String vendor, String networkOperator) { - - // Before building up IMAP ID string, pre-filter the input strings for "legal" chars - // This is using a fairly arbitrary char set intended to pass through most reasonable - // version, model, and vendor strings: a-z A-Z 0-9 - _ + = ; : . , / - // The most important thing is *not* to pass parens, quotes, or CRLF, which would break - // the format of the IMAP ID list. - Pattern p = Pattern.compile("[^a-zA-Z0-9-_\\+=;:\\.,/ ]"); - packageName = p.matcher(packageName).replaceAll(""); - version = p.matcher(version).replaceAll(""); - codeName = p.matcher(codeName).replaceAll(""); - model = p.matcher(model).replaceAll(""); - id = p.matcher(id).replaceAll(""); - vendor = p.matcher(vendor).replaceAll(""); - networkOperator = p.matcher(networkOperator).replaceAll(""); - - // "name" "com.android.email" - StringBuilder sb = new StringBuilder("\"name\" \""); - sb.append(packageName); - sb.append("\""); - - // "os" "android" - sb.append(" \"os\" \"android\""); - - // "os-version" "version; build-id" - sb.append(" \"os-version\" \""); - if (version.length() > 0) { - sb.append(version); - } else { - // default to "1.0" - sb.append("1.0"); - } - // add the build ID or build # - if (id.length() > 0) { - sb.append("; "); - sb.append(id); - } - sb.append("\""); - - // "vendor" "the vendor" - if (vendor.length() > 0) { - sb.append(" \"vendor\" \""); - sb.append(vendor); - sb.append("\""); - } - - // "x-android-device-model" the device model (on release builds only) - if ("REL".equals(codeName)) { - if (model.length() > 0) { - sb.append(" \"x-android-device-model\" \""); - sb.append(model); - sb.append("\""); - } - } - - // "x-android-mobile-net-operator" "name of network operator" - if (networkOperator.length() > 0) { - sb.append(" \"x-android-mobile-net-operator\" \""); - sb.append(networkOperator); - sb.append("\""); - } - - return sb.toString(); - } - - - @Override - public Folder getFolder(String name) { - return new ImapFolder(this, name); - } - - /** - * Creates a mailbox hierarchy out of the flat data provided by the server. - */ - @VisibleForTesting - static void createHierarchy(HashMap mailboxes) { - Set pathnames = mailboxes.keySet(); - for (String path : pathnames) { - final ImapFolder folder = mailboxes.get(path); - final Mailbox mailbox = folder.mMailbox; - int delimiterIdx = mailbox.mServerId.lastIndexOf(mailbox.mDelimiter); - long parentKey = Mailbox.NO_MAILBOX; - String parentPath = null; - if (delimiterIdx != -1) { - parentPath = path.substring(0, delimiterIdx); - final ImapFolder parentFolder = mailboxes.get(parentPath); - final Mailbox parentMailbox = (parentFolder == null) ? null : parentFolder.mMailbox; - if (parentMailbox != null) { - parentKey = parentMailbox.mId; - parentMailbox.mFlags - |= (Mailbox.FLAG_HAS_CHILDREN | Mailbox.FLAG_CHILDREN_VISIBLE); - } - } - mailbox.mParentKey = parentKey; - mailbox.mParentServerId = parentPath; - } - } - - /** - * Creates a {@link Folder} and associated {@link Mailbox}. If the folder does not already - * exist in the local database, a new row will immediately be created in the mailbox table. - * Otherwise, the existing row will be used. Any changes to existing rows, will not be stored - * to the database immediately. - * @param accountId The ID of the account the mailbox is to be associated with - * @param mailboxPath The path of the mailbox to add - * @param delimiter A path delimiter. May be {@code null} if there is no delimiter. - * @param selectable If {@code true}, the mailbox can be selected and used to store messages. - * @param mailbox If not null, mailbox is used instead of querying for the Mailbox. - */ - private ImapFolder addMailbox(Context context, long accountId, String mailboxPath, - char delimiter, boolean selectable, Mailbox mailbox) { - // TODO: pass in the mailbox type, or do a proper lookup here - final int mailboxType; - if (mailbox == null) { - mailboxType = LegacyConversions.inferMailboxTypeFromName(context, mailboxPath); - mailbox = Mailbox.getMailboxForPath(context, accountId, mailboxPath); - } else { - mailboxType = mailbox.mType; - } - final ImapFolder folder = (ImapFolder) getFolder(mailboxPath); - if (mailbox.isSaved()) { - // existing mailbox - // mailbox retrieved from database; save hash _before_ updating fields - folder.mHash = mailbox.getHashes(); - } - updateMailbox(mailbox, accountId, mailboxPath, delimiter, selectable, mailboxType); - if (folder.mHash == null) { - // new mailbox - // save hash after updating. allows tracking changes if the mailbox is saved - // outside of #saveMailboxList() - folder.mHash = mailbox.getHashes(); - // We must save this here to make sure we have a valid ID for later - mailbox.save(mContext); - } - folder.mMailbox = mailbox; - return folder; - } - - /** - * Persists the folders in the given list. - */ - private static void saveMailboxList(Context context, HashMap folderMap) { - for (ImapFolder imapFolder : folderMap.values()) { - imapFolder.save(context); - } - } - - @Override - public Folder[] updateFolders() throws MessagingException { - // TODO: There is nothing that ever closes this connection. Trouble is, it's not exactly - // clear when we should close it, we'd like to keep it open until we're really done - // using it. - ImapConnection connection = getConnection(); - try { - final HashMap mailboxes = new HashMap(); - // Establish a connection to the IMAP server; if necessary - // This ensures a valid prefix if the prefix is automatically set by the server - connection.executeSimpleCommand(ImapConstants.NOOP); - String imapCommand = ImapConstants.LIST + " \"\" \"*\""; - if (mPathPrefix != null) { - imapCommand = ImapConstants.LIST + " \"\" \"" + mPathPrefix + "*\""; - } - List responses = connection.executeSimpleCommand(imapCommand); - for (ImapResponse response : responses) { - // S: * LIST (\Noselect) "/" ~/Mail/foo - if (response.isDataResponse(0, ImapConstants.LIST)) { - // Get folder name. - ImapString encodedFolder = response.getStringOrEmpty(3); - if (encodedFolder.isEmpty()) continue; - - String folderName = decodeFolderName(encodedFolder.getString(), mPathPrefix); - - if (ImapConstants.INBOX.equalsIgnoreCase(folderName)) continue; - - // Parse attributes. - boolean selectable = - !response.getListOrEmpty(1).contains(ImapConstants.FLAG_NO_SELECT); - String delimiter = response.getStringOrEmpty(2).getString(); - char delimiterChar = '\0'; - if (!TextUtils.isEmpty(delimiter)) { - delimiterChar = delimiter.charAt(0); - } - ImapFolder folder = addMailbox( - mContext, mAccount.mId, folderName, delimiterChar, selectable, null); - mailboxes.put(folderName, folder); - } - } - - // In order to properly map INBOX -> Inbox, handle it as a special case. - final Mailbox inbox = - Mailbox.restoreMailboxOfType(mContext, mAccount.mId, Mailbox.TYPE_INBOX); - final ImapFolder newFolder = addMailbox( - mContext, mAccount.mId, inbox.mServerId, '\0', true /*selectable*/, inbox); - mailboxes.put(ImapConstants.INBOX, newFolder); - - createHierarchy(mailboxes); - saveMailboxList(mContext, mailboxes); - return mailboxes.values().toArray(new Folder[mailboxes.size()]); - } catch (IOException ioe) { - connection.close(); - throw new MessagingException("Unable to get folder list", ioe); - } catch (AuthenticationFailedException afe) { - // We do NOT want this connection pooled, or we will continue to send NOOP and SELECT - // commands to the server - connection.destroyResponses(); - connection = null; - throw afe; - } finally { - if (connection != null) { - // We keep our connection out of the pool as long as we are using it, then - // put it back into the pool so it can be reused. - poolConnection(connection); - } - } - } - - @Override - public Bundle checkSettings() throws MessagingException { - int result = MessagingException.NO_ERROR; - Bundle bundle = new Bundle(); - // TODO: why doesn't this use getConnection()? I guess this is only done during setup, - // so there's need to look for a pooled connection? - // But then why doesn't it use poolConnection() after it's done? - ImapConnection connection = new ImapConnection(this); - try { - connection.open(); - connection.close(); - } catch (IOException ioe) { - bundle.putString(EmailServiceProxy.VALIDATE_BUNDLE_ERROR_MESSAGE, ioe.getMessage()); - result = MessagingException.IOERROR; - } finally { - connection.destroyResponses(); - } - bundle.putInt(EmailServiceProxy.VALIDATE_BUNDLE_RESULT_CODE, result); - return bundle; - } - - /** - * Returns whether or not the prefix has been set by the user. This can be determined by - * the fact that the prefix is set, but, the path separator is not set. - */ - boolean isUserPrefixSet() { - return TextUtils.isEmpty(mPathSeparator) && !TextUtils.isEmpty(mPathPrefix); - } - - /** Sets the path separator */ - void setPathSeparator(String pathSeparator) { - mPathSeparator = pathSeparator; - } - - /** Sets the prefix */ - void setPathPrefix(String pathPrefix) { - mPathPrefix = pathPrefix; - } - - /** Gets the context for this store */ - Context getContext() { - return mContext; - } - - /** Returns a clone of the transport associated with this store. */ - MailTransport cloneTransport() { - return mTransport.clone(); - } - - /** - * Fixes the path prefix, if necessary. The path prefix must always end with the - * path separator. - */ - void ensurePrefixIsValid() { - // Make sure the path prefix ends with the path separator - if (!TextUtils.isEmpty(mPathPrefix) && !TextUtils.isEmpty(mPathSeparator)) { - if (!mPathPrefix.endsWith(mPathSeparator)) { - mPathPrefix = mPathPrefix + mPathSeparator; - } - } - } - - /** - * Gets a connection if one is available from the pool, or creates a new one if not. - */ - ImapConnection getConnection() { - // TODO Why would we ever have (or need to have) more than one active connection? - // TODO We set new username/password each time, but we don't actually close the transport - // when we do this. So if that information has changed, this connection will fail. - ImapConnection connection; - while ((connection = mConnectionPool.poll()) != null) { - try { - connection.setStore(this); - connection.executeSimpleCommand(ImapConstants.NOOP); - break; - } catch (MessagingException e) { - // Fall through - } catch (IOException e) { - // Fall through - } - connection.close(); - } - - if (connection == null) { - connection = new ImapConnection(this); - } - return connection; - } - - /** - * Save a {@link ImapConnection} in the pool for reuse. Any responses associated with the - * connection are destroyed before adding the connection to the pool. - */ - void poolConnection(ImapConnection connection) { - if (connection != null) { - connection.destroyResponses(); - mConnectionPool.add(connection); - } - } - - /** - * Prepends the folder name with the given prefix and UTF-7 encodes it. - */ - static String encodeFolderName(String name, String prefix) { - // do NOT add the prefix to the special name "INBOX" - if (ImapConstants.INBOX.equalsIgnoreCase(name)) return name; - - // Prepend prefix - if (prefix != null) { - name = prefix + name; - } - - // TODO bypass the conversion if name doesn't have special char. - ByteBuffer bb = MODIFIED_UTF_7_CHARSET.encode(name); - byte[] b = new byte[bb.limit()]; - bb.get(b); - - return Utility.fromAscii(b); - } - - /** - * UTF-7 decodes the folder name and removes the given path prefix. - */ - static String decodeFolderName(String name, String prefix) { - // TODO bypass the conversion if name doesn't have special char. - String folder; - folder = MODIFIED_UTF_7_CHARSET.decode(ByteBuffer.wrap(Utility.toAscii(name))).toString(); - if ((prefix != null) && folder.startsWith(prefix)) { - folder = folder.substring(prefix.length()); - } - return folder; - } - - /** - * Returns UIDs of Messages joined with "," as the separator. - */ - static String joinMessageUids(Message[] messages) { - StringBuilder sb = new StringBuilder(); - boolean notFirst = false; - for (Message m : messages) { - if (notFirst) { - sb.append(','); - } - sb.append(m.getUid()); - notFirst = true; - } - return sb.toString(); - } - - static class ImapMessage extends MimeMessage { - ImapMessage(String uid, ImapFolder folder) { - mUid = uid; - mFolder = folder; - } - - public void setSize(int size) { - mSize = size; - } - - @Override - public void parse(InputStream in) throws IOException, MessagingException { - super.parse(in); - } - - public void setFlagInternal(Flag flag, boolean set) throws MessagingException { - super.setFlag(flag, set); - } - - @Override - public void setFlag(Flag flag, boolean set) throws MessagingException { - super.setFlag(flag, set); - mFolder.setFlags(new Message[] { this }, new Flag[] { flag }, set); - } - } - - static class ImapException extends MessagingException { - private static final long serialVersionUID = 1L; - - private final String mAlertText; - private final String mResponseCode; - - public ImapException(String message, String alertText, String responseCode) { - super(message); - mAlertText = alertText; - mResponseCode = responseCode; - } - - public String getAlertText() { - return mAlertText; - } - - public String getResponseCode() { - return mResponseCode; - } - } - - public void closeConnections() { - ImapConnection connection; - while ((connection = mConnectionPool.poll()) != null) { - connection.close(); - } - } -} diff --git a/src/com/android/email/mail/store/Pop3Store.java b/src/com/android/email/mail/store/Pop3Store.java deleted file mode 100644 index 4ea75ccf3..000000000 --- a/src/com/android/email/mail/store/Pop3Store.java +++ /dev/null @@ -1,833 +0,0 @@ -/* - * 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.mail.store; - -import android.content.Context; -import android.os.Bundle; - -import com.android.email.DebugUtils; -import com.android.email.mail.Store; -import com.android.email.mail.transport.MailTransport; -import com.android.emailcommon.Logging; -import com.android.emailcommon.internet.MimeMessage; -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.OpenMode; -import com.android.emailcommon.mail.Message; -import com.android.emailcommon.mail.MessagingException; -import com.android.emailcommon.provider.Account; -import com.android.emailcommon.provider.HostAuth; -import com.android.emailcommon.provider.Mailbox; -import com.android.emailcommon.service.EmailServiceProxy; -import com.android.emailcommon.service.SearchParams; -import com.android.emailcommon.utility.LoggingInputStream; -import com.android.emailcommon.utility.Utility; -import com.android.mail.utils.LogUtils; -import com.google.common.annotations.VisibleForTesting; - -import org.apache.james.mime4j.EOLConvertingInputStream; - -import java.io.IOException; -import java.io.InputStream; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.Locale; - -public class Pop3Store extends Store { - // All flags defining debug or development code settings must be FALSE - // when code is checked in or released. - private static boolean DEBUG_FORCE_SINGLE_LINE_UIDL = false; - private static boolean DEBUG_LOG_RAW_STREAM = false; - - private static final Flag[] PERMANENT_FLAGS = { Flag.DELETED }; - /** The name of the only mailbox available to POP3 accounts */ - private static final String POP3_MAILBOX_NAME = "INBOX"; - private final HashMap mFolders = new HashMap(); - private final Message[] mOneMessage = new Message[1]; - - /** - * Static named constructor. - */ - public static Store newInstance(Account account, Context context) throws MessagingException { - return new Pop3Store(context, account); - } - - /** - * Creates a new store for the given account. - */ - private Pop3Store(Context context, Account account) throws MessagingException { - mContext = context; - mAccount = account; - - HostAuth recvAuth = account.getOrCreateHostAuthRecv(context); - mTransport = new MailTransport(context, "POP3", recvAuth); - String[] userInfoParts = recvAuth.getLogin(); - mUsername = userInfoParts[0]; - mPassword = userInfoParts[1]; - } - - /** - * For testing only. Injects a different transport. The transport should already be set - * up and ready to use. Do not use for real code. - * @param testTransport The Transport to inject and use for all future communication. - */ - /* package */ void setTransport(MailTransport testTransport) { - mTransport = testTransport; - } - - @Override - public Folder getFolder(String name) { - Folder folder = mFolders.get(name); - if (folder == null) { - folder = new Pop3Folder(name); - mFolders.put(folder.getName(), folder); - } - return folder; - } - - @Override - public Folder[] updateFolders() { - Mailbox mailbox = Mailbox.restoreMailboxOfType(mContext, mAccount.mId, Mailbox.TYPE_INBOX); - if (mailbox == null) { - mailbox = Mailbox.newSystemMailbox(mContext, mAccount.mId, Mailbox.TYPE_INBOX); - } - if (mailbox.isSaved()) { - mailbox.update(mContext, mailbox.toContentValues()); - } else { - mailbox.save(mContext); - } - return new Folder[] { getFolder(mailbox.mServerId) }; - } - - /** - * Used by account setup to test if an account's settings are appropriate. The definition - * of "checked" here is simply, can you log into the account and does it meet some minimum set - * of feature requirements? - * - * @throws MessagingException if there was some problem with the account - */ - @Override - public Bundle checkSettings() throws MessagingException { - Pop3Folder folder = new Pop3Folder(POP3_MAILBOX_NAME); - Bundle bundle = null; - // Close any open or half-open connections - checkSettings should always be "fresh" - if (mTransport.isOpen()) { - folder.close(false); - } - try { - folder.open(OpenMode.READ_WRITE); - bundle = folder.checkSettings(); - } finally { - folder.close(false); // false == don't expunge anything - } - return bundle; - } - - public class Pop3Folder extends Folder { - private final HashMap mUidToMsgMap - = new HashMap(); - private final HashMap mMsgNumToMsgMap - = new HashMap(); - private final HashMap mUidToMsgNumMap = new HashMap(); - private final String mName; - private int mMessageCount; - private Pop3Capabilities mCapabilities; - - public Pop3Folder(String name) { - if (name.equalsIgnoreCase(POP3_MAILBOX_NAME)) { - mName = POP3_MAILBOX_NAME; - } else { - mName = name; - } - } - - /** - * Used by account setup to test if an account's settings are appropriate. Here, we run - * an additional test to see if UIDL is supported on the server. If it's not we - * can't service this account. - * - * @return Bundle containing validation data (code and, if appropriate, error message) - * @throws MessagingException if the account is not going to be useable - */ - public Bundle checkSettings() throws MessagingException { - Bundle bundle = new Bundle(); - int result = MessagingException.NO_ERROR; - try { - UidlParser parser = new UidlParser(); - executeSimpleCommand("UIDL"); - // drain the entire output, so additional communications don't get confused. - String response; - while ((response = mTransport.readLine(false)) != null) { - parser.parseMultiLine(response); - if (parser.mEndOfMessage) { - break; - } - } - } catch (IOException ioe) { - mTransport.close(); - result = MessagingException.IOERROR; - bundle.putString(EmailServiceProxy.VALIDATE_BUNDLE_ERROR_MESSAGE, - ioe.getMessage()); - } - bundle.putInt(EmailServiceProxy.VALIDATE_BUNDLE_RESULT_CODE, result); - return bundle; - } - - @Override - public synchronized void open(OpenMode mode) throws MessagingException { - if (mTransport.isOpen()) { - return; - } - - if (!mName.equalsIgnoreCase(POP3_MAILBOX_NAME)) { - throw new MessagingException("Folder does not exist"); - } - - try { - mTransport.open(); - - // Eat the banner - executeSimpleCommand(null); - - mCapabilities = getCapabilities(); - - if (mTransport.canTryTlsSecurity()) { - if (mCapabilities.stls) { - executeSimpleCommand("STLS"); - mTransport.reopenTls(); - } else { - if (DebugUtils.DEBUG) { - LogUtils.d(Logging.LOG_TAG, "TLS not supported but required"); - } - throw new MessagingException(MessagingException.TLS_REQUIRED); - } - } - - try { - executeSensitiveCommand("USER " + mUsername, "USER /redacted/"); - executeSensitiveCommand("PASS " + mPassword, "PASS /redacted/"); - } catch (MessagingException me) { - if (DebugUtils.DEBUG) { - LogUtils.d(Logging.LOG_TAG, me.toString()); - } - throw new AuthenticationFailedException(null, me); - } - } catch (IOException ioe) { - mTransport.close(); - if (DebugUtils.DEBUG) { - LogUtils.d(Logging.LOG_TAG, ioe.toString()); - } - throw new MessagingException(MessagingException.IOERROR, ioe.toString()); - } - - Exception statException = null; - try { - String response = executeSimpleCommand("STAT"); - String[] parts = response.split(" "); - if (parts.length < 2) { - statException = new IOException(); - } else { - mMessageCount = Integer.parseInt(parts[1]); - } - } catch (MessagingException me) { - statException = me; - } catch (IOException ioe) { - statException = ioe; - } catch (NumberFormatException nfe) { - statException = nfe; - } - if (statException != null) { - mTransport.close(); - if (DebugUtils.DEBUG) { - LogUtils.d(Logging.LOG_TAG, statException.toString()); - } - throw new MessagingException("POP3 STAT", statException); - } - mUidToMsgMap.clear(); - mMsgNumToMsgMap.clear(); - mUidToMsgNumMap.clear(); - } - - @Override - public OpenMode getMode() { - return OpenMode.READ_WRITE; - } - - /** - * Close the folder (and the transport below it). - * - * MUST NOT return any exceptions. - * - * @param expunge If true all deleted messages will be expunged (TODO - not implemented) - */ - @Override - public void close(boolean expunge) { - try { - executeSimpleCommand("QUIT"); - } - catch (Exception e) { - // ignore any problems here - just continue closing - } - mTransport.close(); - } - - @Override - public String getName() { - return mName; - } - - // POP3 does not folder creation - @Override - public boolean canCreate(FolderType type) { - return false; - } - - @Override - public boolean create(FolderType type) { - return false; - } - - @Override - public boolean exists() { - return mName.equalsIgnoreCase(POP3_MAILBOX_NAME); - } - - @Override - public int getMessageCount() { - return mMessageCount; - } - - @Override - public int getUnreadMessageCount() { - return -1; - } - - @Override - public Message getMessage(String uid) throws MessagingException { - if (mUidToMsgNumMap.size() == 0) { - try { - indexMsgNums(1, mMessageCount); - } catch (IOException ioe) { - mTransport.close(); - if (DebugUtils.DEBUG) { - LogUtils.d(Logging.LOG_TAG, "Unable to index during getMessage " + ioe); - } - throw new MessagingException("getMessages", ioe); - } - } - Pop3Message message = mUidToMsgMap.get(uid); - return message; - } - - @Override - public Pop3Message[] getMessages(int start, int end, MessageRetrievalListener listener) - throws MessagingException { - return null; - } - - @Override - public Pop3Message[] getMessages(long startDate, long endDate, - MessageRetrievalListener listener) throws MessagingException { - return null; - } - - public Pop3Message[] getMessages(int end, final int limit) - throws MessagingException { - try { - indexMsgNums(1, end); - } catch (IOException ioe) { - mTransport.close(); - if (DebugUtils.DEBUG) { - LogUtils.d(Logging.LOG_TAG, ioe.toString()); - } - throw new MessagingException("getMessages", ioe); - } - ArrayList messages = new ArrayList(); - for (int msgNum = end; msgNum > 0 && (messages.size() < limit); msgNum--) { - Pop3Message message = mMsgNumToMsgMap.get(msgNum); - if (message != null) { - messages.add(message); - } - } - return messages.toArray(new Pop3Message[messages.size()]); - } - - /** - * Ensures that the given message set (from start to end inclusive) - * has been queried so that uids are available in the local cache. - * @param start - * @param end - * @throws MessagingException - * @throws IOException - */ - private void indexMsgNums(int start, int end) - throws MessagingException, IOException { - if (!mMsgNumToMsgMap.isEmpty()) { - return; - } - UidlParser parser = new UidlParser(); - if (DEBUG_FORCE_SINGLE_LINE_UIDL || (mMessageCount > 5000)) { - /* - * In extreme cases we'll do a UIDL command per message instead of a bulk - * download. - */ - for (int msgNum = start; msgNum <= end; msgNum++) { - Pop3Message message = mMsgNumToMsgMap.get(msgNum); - if (message == null) { - String response = executeSimpleCommand("UIDL " + msgNum); - if (!parser.parseSingleLine(response)) { - throw new IOException(); - } - message = new Pop3Message(parser.mUniqueId, this); - indexMessage(msgNum, message); - } - } - } else { - String response = executeSimpleCommand("UIDL"); - while ((response = mTransport.readLine(false)) != null) { - if (!parser.parseMultiLine(response)) { - throw new IOException(); - } - if (parser.mEndOfMessage) { - break; - } - int msgNum = parser.mMessageNumber; - if (msgNum >= start && msgNum <= end) { - Pop3Message message = mMsgNumToMsgMap.get(msgNum); - if (message == null) { - message = new Pop3Message(parser.mUniqueId, this); - indexMessage(msgNum, message); - } - } - } - } - } - - /** - * Simple parser class for UIDL messages. - * - *

NOTE: In variance with RFC 1939, we allow multiple whitespace between the - * message-number and unique-id fields. This provides greater compatibility with some - * non-compliant POP3 servers, e.g. mail.comcast.net. - */ - /* package */ class UidlParser { - - /** - * Caller can read back message-number from this field - */ - public int mMessageNumber; - /** - * Caller can read back unique-id from this field - */ - public String mUniqueId; - /** - * True if the response was "end-of-message" - */ - public boolean mEndOfMessage; - /** - * True if an error was reported - */ - public boolean mErr; - - /** - * Construct & Initialize - */ - public UidlParser() { - mErr = true; - } - - /** - * Parse a single-line response. This is returned from a command of the form - * "UIDL msg-num" and will be formatted as: "+OK msg-num unique-id" or - * "-ERR diagnostic text" - * - * @param response The string returned from the server - * @return true if the string parsed as expected (e.g. no syntax problems) - */ - public boolean parseSingleLine(String response) { - mErr = false; - if (response == null || response.length() == 0) { - return false; - } - char first = response.charAt(0); - if (first == '+') { - String[] uidParts = response.split(" +"); - if (uidParts.length >= 3) { - try { - mMessageNumber = Integer.parseInt(uidParts[1]); - } catch (NumberFormatException nfe) { - return false; - } - mUniqueId = uidParts[2]; - mEndOfMessage = true; - return true; - } - } else if (first == '-') { - mErr = true; - return true; - } - return false; - } - - /** - * Parse a multi-line response. This is returned from a command of the form - * "UIDL" and will be formatted as: "." or "msg-num unique-id". - * - * @param response The string returned from the server - * @return true if the string parsed as expected (e.g. no syntax problems) - */ - public boolean parseMultiLine(String response) { - mErr = false; - if (response == null || response.length() == 0) { - return false; - } - char first = response.charAt(0); - if (first == '.') { - mEndOfMessage = true; - return true; - } else { - String[] uidParts = response.split(" +"); - if (uidParts.length >= 2) { - try { - mMessageNumber = Integer.parseInt(uidParts[0]); - } catch (NumberFormatException nfe) { - return false; - } - mUniqueId = uidParts[1]; - mEndOfMessage = false; - return true; - } - } - return false; - } - } - - private void indexMessage(int msgNum, Pop3Message message) { - mMsgNumToMsgMap.put(msgNum, message); - mUidToMsgMap.put(message.getUid(), message); - mUidToMsgNumMap.put(message.getUid(), msgNum); - } - - @Override - public Message[] getMessages(String[] uids, MessageRetrievalListener listener) { - throw new UnsupportedOperationException( - "Pop3Folder.getMessage(MessageRetrievalListener)"); - } - - /** - * Fetch the items contained in the FetchProfile into the given set of - * Messages in as efficient a manner as possible. - * @param messages - * @param fp - * @throws MessagingException - */ - @Override - public void fetch(Message[] messages, FetchProfile fp, MessageRetrievalListener listener) - throws MessagingException { - throw new UnsupportedOperationException( - "Pop3Folder.fetch(Message[], FetchProfile, MessageRetrievalListener)"); - } - - /** - * Fetches the body of the given message, limiting the stored data - * to the specified number of lines. If lines is -1 the entire message - * is fetched. This is implemented with RETR for lines = -1 or TOP - * for any other value. If the server does not support TOP it is - * emulated with RETR and extra lines are thrown away. - * - * @param message - * @param lines - * @param callback optional callback that reports progress of the fetch - */ - public void fetchBody(Pop3Message message, int lines, - EOLConvertingInputStream.Callback callback) throws IOException, MessagingException { - String response = null; - int messageId = mUidToMsgNumMap.get(message.getUid()); - if (lines == -1) { - // Fetch entire message - response = executeSimpleCommand(String.format(Locale.US, "RETR %d", messageId)); - } else { - // Fetch partial message. Try "TOP", and fall back to slower "RETR" if necessary - try { - response = executeSimpleCommand( - String.format(Locale.US, "TOP %d %d", messageId, lines)); - } catch (MessagingException me) { - try { - response = executeSimpleCommand( - String.format(Locale.US, "RETR %d", messageId)); - } catch (MessagingException e) { - LogUtils.w(Logging.LOG_TAG, "Can't read message " + messageId); - } - } - } - if (response != null) { - try { - int ok = response.indexOf("OK"); - if (ok > 0) { - try { - int start = ok + 3; - if (start > response.length()) { - // No length was supplied, this is a protocol error. - LogUtils.e(Logging.LOG_TAG, "No body length supplied"); - message.setSize(0); - } else { - int end = response.indexOf(" ", start); - final String intString; - if (end > 0) { - intString = response.substring(start, end); - } else { - intString = response.substring(start); - } - message.setSize(Integer.parseInt(intString)); - } - } catch (NumberFormatException e) { - // We tried - } - } - InputStream in = mTransport.getInputStream(); - if (DEBUG_LOG_RAW_STREAM && DebugUtils.DEBUG) { - in = new LoggingInputStream(in); - } - message.parse(new Pop3ResponseInputStream(in), callback); - } - catch (MessagingException me) { - /* - * If we're only downloading headers it's possible - * we'll get a broken MIME message which we're not - * real worried about. If we've downloaded the body - * and can't parse it we need to let the user know. - */ - if (lines == -1) { - throw me; - } - } - } - } - - @Override - public Flag[] getPermanentFlags() { - return PERMANENT_FLAGS; - } - - @Override - public void appendMessage(Context context, Message message, boolean noTimeout) { - } - - @Override - public void delete(boolean recurse) { - } - - @Override - public Message[] expunge() { - return null; - } - - public void deleteMessage(Message message) throws MessagingException { - mOneMessage[0] = message; - setFlags(mOneMessage, PERMANENT_FLAGS, true); - } - - @Override - public void setFlags(Message[] messages, Flag[] flags, boolean value) - throws MessagingException { - if (!value || !Utility.arrayContains(flags, Flag.DELETED)) { - /* - * The only flagging we support is setting the Deleted flag. - */ - return; - } - try { - for (Message message : messages) { - try { - String uid = message.getUid(); - int msgNum = mUidToMsgNumMap.get(uid); - executeSimpleCommand(String.format(Locale.US, "DELE %s", msgNum)); - // Remove from the maps - mMsgNumToMsgMap.remove(msgNum); - mUidToMsgNumMap.remove(uid); - } catch (MessagingException e) { - // A failed deletion isn't a problem - } - } - } - catch (IOException ioe) { - mTransport.close(); - if (DebugUtils.DEBUG) { - LogUtils.d(Logging.LOG_TAG, ioe.toString()); - } - throw new MessagingException("setFlags()", ioe); - } - } - - @Override - public void copyMessages(Message[] msgs, Folder folder, MessageUpdateCallbacks callbacks) { - throw new UnsupportedOperationException("copyMessages is not supported in POP3"); - } - - private Pop3Capabilities getCapabilities() throws IOException { - Pop3Capabilities capabilities = new Pop3Capabilities(); - try { - String response = executeSimpleCommand("CAPA"); - while ((response = mTransport.readLine(true)) != null) { - if (response.equals(".")) { - break; - } else if (response.equalsIgnoreCase("STLS")){ - capabilities.stls = true; - } - } - } - catch (MessagingException me) { - /* - * The server may not support the CAPA command, so we just eat this Exception - * and allow the empty capabilities object to be returned. - */ - } - return capabilities; - } - - /** - * Send a single command and wait for a single line response. Reopens the connection, - * if it is closed. Leaves the connection open. - * - * @param command The command string to send to the server. - * @return Returns the response string from the server. - */ - private String executeSimpleCommand(String command) throws IOException, MessagingException { - return executeSensitiveCommand(command, null); - } - - /** - * Send a single command and wait for a single line response. Reopens the connection, - * if it is closed. Leaves the connection open. - * - * @param command The command string to send to the server. - * @param sensitiveReplacement If the command includes sensitive data (e.g. authentication) - * please pass a replacement string here (for logging). - * @return Returns the response string from the server. - */ - private String executeSensitiveCommand(String command, String sensitiveReplacement) - throws IOException, MessagingException { - open(OpenMode.READ_WRITE); - - if (command != null) { - mTransport.writeLine(command, sensitiveReplacement); - } - - String response = mTransport.readLine(true); - - if (response.length() > 1 && response.charAt(0) == '-') { - throw new MessagingException(response); - } - - return response; - } - - @Override - public boolean equals(Object o) { - if (o instanceof Pop3Folder) { - return ((Pop3Folder) o).mName.equals(mName); - } - return super.equals(o); - } - - @Override - @VisibleForTesting - public boolean isOpen() { - return mTransport.isOpen(); - } - - @Override - public Message createMessage(String uid) { - return new Pop3Message(uid, this); - } - - @Override - public Message[] getMessages(SearchParams params, MessageRetrievalListener listener) { - return null; - } - } - - public static class Pop3Message extends MimeMessage { - public Pop3Message(String uid, Pop3Folder folder) { - mUid = uid; - mFolder = folder; - mSize = -1; - } - - public void setSize(int size) { - mSize = size; - } - - @Override - public void parse(InputStream in) throws IOException, MessagingException { - super.parse(in); - } - - @Override - public void setFlag(Flag flag, boolean set) throws MessagingException { - super.setFlag(flag, set); - mFolder.setFlags(new Message[] { this }, new Flag[] { flag }, set); - } - } - - /** - * POP3 Capabilities as defined in RFC 2449. This is not a complete list of CAPA - * responses - just those that we use in this client. - */ - class Pop3Capabilities { - /** The STLS (start TLS) command is supported */ - public boolean stls; - - @Override - public String toString() { - return String.format("STLS %b", stls); - } - } - - // TODO figure out what is special about this and merge it into MailTransport - class Pop3ResponseInputStream extends InputStream { - private final InputStream mIn; - private boolean mStartOfLine = true; - private boolean mFinished; - - public Pop3ResponseInputStream(InputStream in) { - mIn = in; - } - - @Override - public int read() throws IOException { - if (mFinished) { - return -1; - } - int d = mIn.read(); - if (mStartOfLine && d == '.') { - d = mIn.read(); - if (d == '\r') { - mFinished = true; - mIn.read(); - return -1; - } - } - - mStartOfLine = (d == '\n'); - - return d; - } - } -} diff --git a/src/com/android/email/mail/store/ServiceStore.java b/src/com/android/email/mail/store/ServiceStore.java deleted file mode 100644 index ae568f516..000000000 --- a/src/com/android/email/mail/store/ServiceStore.java +++ /dev/null @@ -1,89 +0,0 @@ -/* - * 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.mail.store; - -import android.content.Context; -import android.os.Bundle; -import android.os.RemoteException; - -import com.android.email.mail.Store; -import com.android.email.service.EmailServiceUtils; -import com.android.emailcommon.mail.MessagingException; -import com.android.emailcommon.provider.Account; -import com.android.emailcommon.provider.HostAuth; -import com.android.emailcommon.service.EmailServiceProxy; -import com.android.emailcommon.service.HostAuthCompat; -import com.android.emailcommon.service.IEmailService; - -/** - * Base class for service-based stores - */ -public class ServiceStore extends Store { - protected final HostAuth mHostAuth; - - /** - * Creates a new store for the given account. - */ - public ServiceStore(Account account, Context context) throws MessagingException { - mContext = context; - mHostAuth = account.getOrCreateHostAuthRecv(mContext); - } - - /** - * Static named constructor. - */ - public static Store newInstance(Account account, Context context) throws MessagingException { - return new ServiceStore(account, context); - } - - private IEmailService getService() { - return EmailServiceUtils.getService(mContext, mHostAuth.mProtocol); - } - - @Override - public Bundle checkSettings() throws MessagingException { - /** - * Here's where we check the settings - * @throws MessagingException if we can't authenticate the account - */ - try { - IEmailService svc = getService(); - // Use a longer timeout for the validate command. Note that the instanceof check - // shouldn't be necessary; we'll do it anyway, just to be safe - if (svc instanceof EmailServiceProxy) { - ((EmailServiceProxy)svc).setTimeout(90); - } - HostAuthCompat hostAuthCom = new HostAuthCompat(mHostAuth); - return svc.validate(hostAuthCom); - } catch (RemoteException e) { - throw new MessagingException("Call to validate generated an exception", e); - } - } - - /** - * We handle AutoDiscover here, wrapping the EmailService call. The service call returns a - * HostAuth and we return null if there was a service issue - */ - @Override - public Bundle autoDiscover(Context context, String username, String password) { - try { - return getService().autoDiscover(username, password); - } catch (RemoteException e) { - return null; - } - } -} diff --git a/src/com/android/email/mail/store/imap/ImapConstants.java b/src/com/android/email/mail/store/imap/ImapConstants.java deleted file mode 100644 index 9f4d59290..000000000 --- a/src/com/android/email/mail/store/imap/ImapConstants.java +++ /dev/null @@ -1,104 +0,0 @@ -/* - * 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.mail.store.imap; - -import com.android.email.mail.Store; - -import java.util.Locale; - -public final class ImapConstants { - private ImapConstants() {} - - public static final String FETCH_FIELD_BODY_PEEK_BARE = "BODY.PEEK"; - public static final String FETCH_FIELD_BODY_PEEK = FETCH_FIELD_BODY_PEEK_BARE + "[]"; - public static final String FETCH_FIELD_BODY_PEEK_SANE - = String.format(Locale.US, "BODY.PEEK[]<0.%d>", Store.FETCH_BODY_SANE_SUGGESTED_SIZE); - public static final String FETCH_FIELD_HEADERS = - "BODY.PEEK[HEADER.FIELDS (date subject from content-type to cc message-id)]"; - - public static final String ALERT = "ALERT"; - public static final String APPEND = "APPEND"; - public static final String AUTHENTICATE = "AUTHENTICATE"; - public static final String BAD = "BAD"; - public static final String BADCHARSET = "BADCHARSET"; - public static final String BODY = "BODY"; - public static final String BODY_BRACKET_HEADER = "BODY[HEADER"; - public static final String BODYSTRUCTURE = "BODYSTRUCTURE"; - public static final String BYE = "BYE"; - public static final String CAPABILITY = "CAPABILITY"; - public static final String CHECK = "CHECK"; - public static final String CLOSE = "CLOSE"; - public static final String COPY = "COPY"; - public static final String COPYUID = "COPYUID"; - public static final String CREATE = "CREATE"; - public static final String DELETE = "DELETE"; - public static final String EXAMINE = "EXAMINE"; - public static final String EXISTS = "EXISTS"; - public static final String EXPUNGE = "EXPUNGE"; - public static final String FETCH = "FETCH"; - public static final String FLAG_ANSWERED = "\\ANSWERED"; - public static final String FLAG_DELETED = "\\DELETED"; - public static final String FLAG_FLAGGED = "\\FLAGGED"; - public static final String FLAG_NO_SELECT = "\\NOSELECT"; - public static final String FLAG_SEEN = "\\SEEN"; - public static final String FLAGS = "FLAGS"; - public static final String FLAGS_SILENT = "FLAGS.SILENT"; - public static final String ID = "ID"; - public static final String INBOX = "INBOX"; - public static final String INTERNALDATE = "INTERNALDATE"; - public static final String LIST = "LIST"; - public static final String LOGIN = "LOGIN"; - public static final String LOGOUT = "LOGOUT"; - public static final String LSUB = "LSUB"; - public static final String NAMESPACE = "NAMESPACE"; - public static final String NO = "NO"; - public static final String NOOP = "NOOP"; - public static final String OK = "OK"; - public static final String PARSE = "PARSE"; - public static final String PERMANENTFLAGS = "PERMANENTFLAGS"; - public static final String PREAUTH = "PREAUTH"; - public static final String READ_ONLY = "READ-ONLY"; - public static final String READ_WRITE = "READ-WRITE"; - public static final String RENAME = "RENAME"; - public static final String RFC822_SIZE = "RFC822.SIZE"; - public static final String SEARCH = "SEARCH"; - public static final String SELECT = "SELECT"; - public static final String STARTTLS = "STARTTLS"; - public static final String STATUS = "STATUS"; - public static final String STORE = "STORE"; - public static final String SUBSCRIBE = "SUBSCRIBE"; - public static final String TEXT = "TEXT"; - public static final String TRYCREATE = "TRYCREATE"; - public static final String UID = "UID"; - public static final String UID_COPY = "UID COPY"; - public static final String UID_FETCH = "UID FETCH"; - public static final String UID_SEARCH = "UID SEARCH"; - public static final String UID_STORE = "UID STORE"; - public static final String UIDNEXT = "UIDNEXT"; - public static final String UIDPLUS = "UIDPLUS"; - public static final String UIDVALIDITY = "UIDVALIDITY"; - public static final String UNSEEN = "UNSEEN"; - public static final String UNSUBSCRIBE = "UNSUBSCRIBE"; - public static final String XOAUTH2 = "XOAUTH2"; - public static final String APPENDUID = "APPENDUID"; - public static final String NIL = "NIL"; - - /** response codes within IMAP responses */ - public static final String EXPIRED = "EXPIRED"; - public static final String AUTHENTICATIONFAILED = "AUTHENTICATIONFAILED"; - public static final String UNAVAILABLE = "UNAVAILABLE"; -} diff --git a/src/com/android/email/mail/store/imap/ImapElement.java b/src/com/android/email/mail/store/imap/ImapElement.java deleted file mode 100644 index 80bb6cd99..000000000 --- a/src/com/android/email/mail/store/imap/ImapElement.java +++ /dev/null @@ -1,120 +0,0 @@ -/* - * 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.mail.store.imap; - -/** - * Class representing "element"s in IMAP responses. - * - *

Class hierarchy: - *

- * ImapElement
- *   |
- *   |-- ImapElement.NONE (for 'index out of range')
- *   |
- *   |-- ImapList (isList() == true)
- *   |   |
- *   |   |-- ImapList.EMPTY
- *   |   |
- *   |   --- ImapResponse
- *   |
- *   --- ImapString (isString() == true)
- *       |
- *       |-- ImapString.EMPTY
- *       |
- *       |-- ImapSimpleString
- *       |
- *       |-- ImapMemoryLiteral
- *       |
- *       --- ImapTempFileLiteral
- * 
- */ -public abstract class ImapElement { - /** - * An element that is returned by {@link ImapList#getElementOrNone} to indicate an index - * is out of range. - */ - public static final ImapElement NONE = new ImapElement() { - @Override public void destroy() { - // Don't call super.destroy(). - // It's a shared object. We don't want the mDestroyed to be set on this. - } - - @Override public boolean isList() { - return false; - } - - @Override public boolean isString() { - return false; - } - - @Override public String toString() { - return "[NO ELEMENT]"; - } - - @Override - public boolean equalsForTest(ImapElement that) { - return super.equalsForTest(that); - } - }; - - private boolean mDestroyed = false; - - public abstract boolean isList(); - - public abstract boolean isString(); - - protected boolean isDestroyed() { - return mDestroyed; - } - - /** - * Clean up the resources used by the instance. - * It's for removing a temp file used by {@link ImapTempFileLiteral}. - */ - public void destroy() { - mDestroyed = true; - } - - /** - * Throws {@link RuntimeException} if it's already destroyed. - */ - protected final void checkNotDestroyed() { - if (mDestroyed) { - throw new RuntimeException("Already destroyed"); - } - } - - /** - * Return a string that represents this object; it's purely for the debug purpose. Don't - * mistake it for {@link ImapString#getString}. - * - * Abstract to force subclasses to implement it. - */ - @Override - public abstract String toString(); - - /** - * The equals implementation that is intended to be used only for unit testing. - * (Because it may be heavy and has a special sense of "equal" for testing.) - */ - public boolean equalsForTest(ImapElement that) { - if (that == null) { - return false; - } - return this.getClass() == that.getClass(); // Has to be the same class. - } -} diff --git a/src/com/android/email/mail/store/imap/ImapList.java b/src/com/android/email/mail/store/imap/ImapList.java deleted file mode 100644 index e28355989..000000000 --- a/src/com/android/email/mail/store/imap/ImapList.java +++ /dev/null @@ -1,235 +0,0 @@ -/* - * 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.mail.store.imap; - -import java.util.ArrayList; - -/** - * Class represents an IMAP list. - */ -public class ImapList extends ImapElement { - /** - * {@link ImapList} representing an empty list. - */ - public static final ImapList EMPTY = new ImapList() { - @Override public void destroy() { - // Don't call super.destroy(). - // It's a shared object. We don't want the mDestroyed to be set on this. - } - - @Override void add(ImapElement e) { - throw new RuntimeException(); - } - }; - - private ArrayList mList = new ArrayList(); - - /* package */ void add(ImapElement e) { - if (e == null) { - throw new RuntimeException("Can't add null"); - } - mList.add(e); - } - - @Override - public final boolean isString() { - return false; - } - - @Override - public final boolean isList() { - return true; - } - - public final int size() { - return mList.size(); - } - - public final boolean isEmpty() { - return size() == 0; - } - - /** - * Return true if the element at {@code index} exists, is string, and equals to {@code s}. - * (case insensitive) - */ - public final boolean is(int index, String s) { - return is(index, s, false); - } - - /** - * Same as {@link #is(int, String)}, but does the prefix match if {@code prefixMatch}. - */ - public final boolean is(int index, String s, boolean prefixMatch) { - if (!prefixMatch) { - return getStringOrEmpty(index).is(s); - } else { - return getStringOrEmpty(index).startsWith(s); - } - } - - /** - * Return the element at {@code index}. - * If {@code index} is out of range, returns {@link ImapElement#NONE}. - */ - public final ImapElement getElementOrNone(int index) { - return (index >= mList.size()) ? ImapElement.NONE : mList.get(index); - } - - /** - * Return the element at {@code index} if it's a list. - * If {@code index} is out of range or not a list, returns {@link ImapList#EMPTY}. - */ - public final ImapList getListOrEmpty(int index) { - ImapElement el = getElementOrNone(index); - return el.isList() ? (ImapList) el : EMPTY; - } - - /** - * Return the element at {@code index} if it's a string. - * If {@code index} is out of range or not a string, returns {@link ImapString#EMPTY}. - */ - public final ImapString getStringOrEmpty(int index) { - ImapElement el = getElementOrNone(index); - return el.isString() ? (ImapString) el : ImapString.EMPTY; - } - - /** - * Return an element keyed by {@code key}. Return null if not found. {@code key} has to be - * at an even index. - */ - /* package */ final ImapElement getKeyedElementOrNull(String key, boolean prefixMatch) { - for (int i = 1; i < size(); i += 2) { - if (is(i-1, key, prefixMatch)) { - return mList.get(i); - } - } - return null; - } - - /** - * Return an {@link ImapList} keyed by {@code key}. - * Return {@link ImapList#EMPTY} if not found. - */ - public final ImapList getKeyedListOrEmpty(String key) { - return getKeyedListOrEmpty(key, false); - } - - /** - * Return an {@link ImapList} keyed by {@code key}. - * Return {@link ImapList#EMPTY} if not found. - */ - public final ImapList getKeyedListOrEmpty(String key, boolean prefixMatch) { - ImapElement e = getKeyedElementOrNull(key, prefixMatch); - return (e != null) ? ((ImapList) e) : ImapList.EMPTY; - } - - /** - * Return an {@link ImapString} keyed by {@code key}. - * Return {@link ImapString#EMPTY} if not found. - */ - public final ImapString getKeyedStringOrEmpty(String key) { - return getKeyedStringOrEmpty(key, false); - } - - /** - * Return an {@link ImapString} keyed by {@code key}. - * Return {@link ImapString#EMPTY} if not found. - */ - public final ImapString getKeyedStringOrEmpty(String key, boolean prefixMatch) { - ImapElement e = getKeyedElementOrNull(key, prefixMatch); - return (e != null) ? ((ImapString) e) : ImapString.EMPTY; - } - - /** - * Return true if it contains {@code s}. - */ - public final boolean contains(String s) { - for (int i = 0; i < size(); i++) { - if (getStringOrEmpty(i).is(s)) { - return true; - } - } - return false; - } - - @Override - public void destroy() { - if (mList != null) { - for (ImapElement e : mList) { - e.destroy(); - } - mList = null; - } - super.destroy(); - } - - @Override - public String toString() { - return mList.toString(); - } - - /** - * Return the text representations of the contents concatenated with ",". - */ - public final String flatten() { - return flatten(new StringBuilder()).toString(); - } - - /** - * Returns text representations (i.e. getString()) of contents joined together with - * "," as the separator. - * - * Only used for building the capability string passed to vendor policies. - * - * We can't use toString(), because it's for debugging (meaning the format may change any time), - * and it won't expand literals. - */ - private final StringBuilder flatten(StringBuilder sb) { - sb.append('['); - for (int i = 0; i < mList.size(); i++) { - if (i > 0) { - sb.append(','); - } - final ImapElement e = getElementOrNone(i); - if (e.isList()) { - getListOrEmpty(i).flatten(sb); - } else if (e.isString()) { - sb.append(getStringOrEmpty(i).getString()); - } - } - sb.append(']'); - return sb; - } - - @Override - public boolean equalsForTest(ImapElement that) { - if (!super.equalsForTest(that)) { - return false; - } - ImapList thatList = (ImapList) that; - if (size() != thatList.size()) { - return false; - } - for (int i = 0; i < size(); i++) { - if (!mList.get(i).equalsForTest(thatList.getElementOrNone(i))) { - return false; - } - } - return true; - } -} diff --git a/src/com/android/email/mail/store/imap/ImapMemoryLiteral.java b/src/com/android/email/mail/store/imap/ImapMemoryLiteral.java deleted file mode 100644 index 26f5e6c9c..000000000 --- a/src/com/android/email/mail/store/imap/ImapMemoryLiteral.java +++ /dev/null @@ -1,71 +0,0 @@ -/* - * 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.mail.store.imap; - -import com.android.email.FixedLengthInputStream; -import com.android.emailcommon.Logging; -import com.android.emailcommon.utility.Utility; -import com.android.mail.utils.LogUtils; - -import java.io.ByteArrayInputStream; -import java.io.IOException; -import java.io.InputStream; - -/** - * Subclass of {@link ImapString} used for literals backed by an in-memory byte array. - */ -public class ImapMemoryLiteral extends ImapString { - private byte[] mData; - - /* package */ ImapMemoryLiteral(FixedLengthInputStream in) throws IOException { - // We could use ByteArrayOutputStream and IOUtils.copy, but it'd perform an unnecessary - // copy.... - mData = new byte[in.getLength()]; - int pos = 0; - while (pos < mData.length) { - int read = in.read(mData, pos, mData.length - pos); - if (read < 0) { - break; - } - pos += read; - } - if (pos != mData.length) { - LogUtils.w(Logging.LOG_TAG, ""); - } - } - - @Override - public void destroy() { - mData = null; - super.destroy(); - } - - @Override - public String getString() { - return Utility.fromAscii(mData); - } - - @Override - public InputStream getAsStream() { - return new ByteArrayInputStream(mData); - } - - @Override - public String toString() { - return String.format("{%d byte literal(memory)}", mData.length); - } -} diff --git a/src/com/android/email/mail/store/imap/ImapResponse.java b/src/com/android/email/mail/store/imap/ImapResponse.java deleted file mode 100644 index 05bf594e6..000000000 --- a/src/com/android/email/mail/store/imap/ImapResponse.java +++ /dev/null @@ -1,152 +0,0 @@ -/* - * 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.mail.store.imap; - - -/** - * Class represents an IMAP response. - */ -public class ImapResponse extends ImapList { - private final String mTag; - private final boolean mIsContinuationRequest; - - /* package */ ImapResponse(String tag, boolean isContinuationRequest) { - mTag = tag; - mIsContinuationRequest = isContinuationRequest; - } - - /* package */ static boolean isStatusResponse(String symbol) { - return ImapConstants.OK.equalsIgnoreCase(symbol) - || ImapConstants.NO.equalsIgnoreCase(symbol) - || ImapConstants.BAD.equalsIgnoreCase(symbol) - || ImapConstants.PREAUTH.equalsIgnoreCase(symbol) - || ImapConstants.BYE.equalsIgnoreCase(symbol); - } - - /** - * @return whether it's a tagged response. - */ - public boolean isTagged() { - return mTag != null; - } - - /** - * @return whether it's a continuation request. - */ - public boolean isContinuationRequest() { - return mIsContinuationRequest; - } - - public boolean isStatusResponse() { - return isStatusResponse(getStringOrEmpty(0).getString()); - } - - /** - * @return whether it's an OK response. - */ - public boolean isOk() { - return is(0, ImapConstants.OK); - } - - /** - * @return whether it's an BAD response. - */ - public boolean isBad() { - return is(0, ImapConstants.BAD); - } - - /** - * @return whether it's an NO response. - */ - public boolean isNo() { - return is(0, ImapConstants.NO); - } - - /** - * @return whether it's an {@code responseType} data response. (i.e. not tagged). - * @param index where {@code responseType} should appear. e.g. 1 for "FETCH" - * @param responseType e.g. "FETCH" - */ - public final boolean isDataResponse(int index, String responseType) { - return !isTagged() && getStringOrEmpty(index).is(responseType); - } - - /** - * @return Response code (RFC 3501 7.1) if it's a status response. - * - * e.g. "ALERT" for "* OK [ALERT] System shutdown in 10 minutes" - */ - public ImapString getResponseCodeOrEmpty() { - if (!isStatusResponse()) { - return ImapString.EMPTY; // Not a status response. - } - return getListOrEmpty(1).getStringOrEmpty(0); - } - - /** - * @return Alert message it it has ALERT response code. - * - * e.g. "System shutdown in 10 minutes" for "* OK [ALERT] System shutdown in 10 minutes" - */ - public ImapString getAlertTextOrEmpty() { - if (!getResponseCodeOrEmpty().is(ImapConstants.ALERT)) { - return ImapString.EMPTY; // Not an ALERT - } - // The 3rd element contains all the rest of line. - return getStringOrEmpty(2); - } - - /** - * @return Response text in a status response. - */ - public ImapString getStatusResponseTextOrEmpty() { - if (!isStatusResponse()) { - return ImapString.EMPTY; - } - return getStringOrEmpty(getElementOrNone(1).isList() ? 2 : 1); - } - - @Override - public String toString() { - String tag = mTag; - if (isContinuationRequest()) { - tag = "+"; - } - return "#" + tag + "# " + super.toString(); - } - - @Override - public boolean equalsForTest(ImapElement that) { - if (!super.equalsForTest(that)) { - return false; - } - final ImapResponse thatResponse = (ImapResponse) that; - if (mTag == null) { - if (thatResponse.mTag != null) { - return false; - } - } else { - if (!mTag.equals(thatResponse.mTag)) { - return false; - } - } - if (mIsContinuationRequest != thatResponse.mIsContinuationRequest) { - return false; - } - return true; - } -} diff --git a/src/com/android/email/mail/store/imap/ImapResponseParser.java b/src/com/android/email/mail/store/imap/ImapResponseParser.java deleted file mode 100644 index 8dd1cf610..000000000 --- a/src/com/android/email/mail/store/imap/ImapResponseParser.java +++ /dev/null @@ -1,453 +0,0 @@ -/* - * 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.mail.store.imap; - -import android.text.TextUtils; - -import com.android.email.DebugUtils; -import com.android.email.FixedLengthInputStream; -import com.android.email.PeekableInputStream; -import com.android.email.mail.transport.DiscourseLogger; -import com.android.emailcommon.Logging; -import com.android.emailcommon.mail.MessagingException; -import com.android.emailcommon.utility.LoggingInputStream; -import com.android.mail.utils.LogUtils; - -import java.io.IOException; -import java.io.InputStream; -import java.util.ArrayList; - -/** - * IMAP response parser. - */ -public class ImapResponseParser { - private static final boolean DEBUG_LOG_RAW_STREAM = false; // DO NOT RELEASE AS 'TRUE' - - /** - * Literal larger than this will be stored in temp file. - */ - public static final int LITERAL_KEEP_IN_MEMORY_THRESHOLD = 2 * 1024 * 1024; - - /** Input stream */ - private final PeekableInputStream mIn; - - /** - * To log network activities when the parser crashes. - * - *

We log all bytes received from the server, except for the part sent as literals. - */ - private final DiscourseLogger mDiscourseLogger; - - private final int mLiteralKeepInMemoryThreshold; - - /** StringBuilder used by readUntil() */ - private final StringBuilder mBufferReadUntil = new StringBuilder(); - - /** StringBuilder used by parseBareString() */ - private final StringBuilder mParseBareString = new StringBuilder(); - - /** - * We store all {@link ImapResponse} in it. {@link #destroyResponses()} must be called from - * time to time to destroy them and clear it. - */ - private final ArrayList mResponsesToDestroy = new ArrayList(); - - /** - * Exception thrown when we receive BYE. It derives from IOException, so it'll be treated - * in the same way EOF does. - */ - public static class ByeException extends IOException { - public static final String MESSAGE = "Received BYE"; - public ByeException() { - super(MESSAGE); - } - } - - /** - * Public constructor for normal use. - */ - public ImapResponseParser(InputStream in, DiscourseLogger discourseLogger) { - this(in, discourseLogger, LITERAL_KEEP_IN_MEMORY_THRESHOLD); - } - - /** - * Constructor for testing to override the literal size threshold. - */ - /* package for test */ ImapResponseParser(InputStream in, DiscourseLogger discourseLogger, - int literalKeepInMemoryThreshold) { - if (DEBUG_LOG_RAW_STREAM && DebugUtils.DEBUG) { - in = new LoggingInputStream(in); - } - mIn = new PeekableInputStream(in); - mDiscourseLogger = discourseLogger; - mLiteralKeepInMemoryThreshold = literalKeepInMemoryThreshold; - } - - private static IOException newEOSException() { - final String message = "End of stream reached"; - if (DebugUtils.DEBUG) { - LogUtils.d(Logging.LOG_TAG, message); - } - return new IOException(message); - } - - /** - * Peek next one byte. - * - * Throws IOException() if reaches EOF. As long as logical response lines end with \r\n, - * we shouldn't see EOF during parsing. - */ - private int peek() throws IOException { - final int next = mIn.peek(); - if (next == -1) { - throw newEOSException(); - } - return next; - } - - /** - * Read and return one byte from {@link #mIn}, and put it in {@link #mDiscourseLogger}. - * - * Throws IOException() if reaches EOF. As long as logical response lines end with \r\n, - * we shouldn't see EOF during parsing. - */ - private int readByte() throws IOException { - int next = mIn.read(); - if (next == -1) { - throw newEOSException(); - } - mDiscourseLogger.addReceivedByte(next); - return next; - } - - /** - * Destroy all the {@link ImapResponse}s stored in the internal storage and clear it. - * - * @see #readResponse() - */ - public void destroyResponses() { - for (ImapResponse r : mResponsesToDestroy) { - r.destroy(); - } - mResponsesToDestroy.clear(); - } - - /** - * Reads the next response available on the stream and returns an - * {@link ImapResponse} object that represents it. - * - *

When this method successfully returns an {@link ImapResponse}, the {@link ImapResponse} - * is stored in the internal storage. When the {@link ImapResponse} is no longer used - * {@link #destroyResponses} should be called to destroy all the responses in the array. - * - * @return the parsed {@link ImapResponse} object. - * @exception ByeException when detects BYE. - */ - public ImapResponse readResponse() throws IOException, MessagingException { - ImapResponse response = null; - try { - response = parseResponse(); - if (DebugUtils.DEBUG) { - LogUtils.d(Logging.LOG_TAG, "<<< " + response.toString()); - } - - } catch (RuntimeException e) { - // Parser crash -- log network activities. - onParseError(e); - throw e; - } catch (IOException e) { - // Network error, or received an unexpected char. - onParseError(e); - throw e; - } - - // Handle this outside of try-catch. We don't have to dump protocol log when getting BYE. - if (response.is(0, ImapConstants.BYE)) { - LogUtils.w(Logging.LOG_TAG, ByeException.MESSAGE); - response.destroy(); - throw new ByeException(); - } - mResponsesToDestroy.add(response); - return response; - } - - private void onParseError(Exception e) { - // Read a few more bytes, so that the log will contain some more context, even if the parser - // crashes in the middle of a response. - // This also makes sure the byte in question will be logged, no matter where it crashes. - // e.g. when parseAtom() peeks and finds at an unexpected char, it throws an exception - // before actually reading it. - // However, we don't want to read too much, because then it may get into an email message. - try { - for (int i = 0; i < 4; i++) { - int b = readByte(); - if (b == -1 || b == '\n') { - break; - } - } - } catch (IOException ignore) { - } - LogUtils.w(Logging.LOG_TAG, "Exception detected: " + e.getMessage()); - mDiscourseLogger.logLastDiscourse(); - } - - /** - * Read next byte from stream and throw it away. If the byte is different from {@code expected} - * throw {@link MessagingException}. - */ - /* package for test */ void expect(char expected) throws IOException { - final int next = readByte(); - if (expected != next) { - throw new IOException(String.format("Expected %04x (%c) but got %04x (%c)", - (int) expected, expected, next, (char) next)); - } - } - - /** - * Read bytes until we find {@code end}, and return all as string. - * The {@code end} will be read (rather than peeked) and won't be included in the result. - */ - /* package for test */ String readUntil(char end) throws IOException { - mBufferReadUntil.setLength(0); - for (;;) { - final int ch = readByte(); - if (ch != end) { - mBufferReadUntil.append((char) ch); - } else { - return mBufferReadUntil.toString(); - } - } - } - - /** - * Read all bytes until \r\n. - */ - /* package */ String readUntilEol() throws IOException { - String ret = readUntil('\r'); - expect('\n'); // TODO Should this really be error? - return ret; - } - - /** - * Parse and return the response line. - */ - private ImapResponse parseResponse() throws IOException, MessagingException { - // We need to destroy the response if we get an exception. - // So, we first store the response that's being built in responseToDestroy, until it's - // completely built, at which point we copy it into responseToReturn and null out - // responseToDestroyt. - // If responseToDestroy is not null in finally, we destroy it because that means - // we got an exception somewhere. - ImapResponse responseToDestroy = null; - final ImapResponse responseToReturn; - - try { - final int ch = peek(); - if (ch == '+') { // Continuation request - readByte(); // skip + - expect(' '); - responseToDestroy = new ImapResponse(null, true); - - // If it's continuation request, we don't really care what's in it. - responseToDestroy.add(new ImapSimpleString(readUntilEol())); - - // Response has successfully been built. Let's return it. - responseToReturn = responseToDestroy; - responseToDestroy = null; - } else { - // Status response or response data - final String tag; - if (ch == '*') { - tag = null; - readByte(); // skip * - expect(' '); - } else { - tag = readUntil(' '); - } - responseToDestroy = new ImapResponse(tag, false); - - final ImapString firstString = parseBareString(); - responseToDestroy.add(firstString); - - // parseBareString won't eat a space after the string, so we need to skip it, - // if exists. - // If the next char is not ' ', it should be EOL. - if (peek() == ' ') { - readByte(); // skip ' ' - - if (responseToDestroy.isStatusResponse()) { // It's a status response - - // Is there a response code? - final int next = peek(); - if (next == '[') { - responseToDestroy.add(parseList('[', ']')); - if (peek() == ' ') { // Skip following space - readByte(); - } - } - - String rest = readUntilEol(); - if (!TextUtils.isEmpty(rest)) { - // The rest is free-form text. - responseToDestroy.add(new ImapSimpleString(rest)); - } - } else { // It's a response data. - parseElements(responseToDestroy, '\0'); - } - } else { - expect('\r'); - expect('\n'); - } - - // Response has successfully been built. Let's return it. - responseToReturn = responseToDestroy; - responseToDestroy = null; - } - } finally { - if (responseToDestroy != null) { - // We get an exception. - responseToDestroy.destroy(); - } - } - - return responseToReturn; - } - - private ImapElement parseElement() throws IOException, MessagingException { - final int next = peek(); - switch (next) { - case '(': - return parseList('(', ')'); - case '[': - return parseList('[', ']'); - case '"': - readByte(); // Skip " - return new ImapSimpleString(readUntil('"')); - case '{': - return parseLiteral(); - case '\r': // CR - readByte(); // Consume \r - expect('\n'); // Should be followed by LF. - return null; - case '\n': // LF // There shouldn't be a bare LF, but just in case. - readByte(); // Consume \n - return null; - default: - return parseBareString(); - } - } - - /** - * Parses an atom. - * - * Special case: If an atom contains '[', everything until the next ']' will be considered - * a part of the atom. - * (e.g. "BODY[HEADER.FIELDS ("DATE" ...)]" will become a single ImapString) - * - * If the value is "NIL", returns an empty string. - */ - private ImapString parseBareString() throws IOException, MessagingException { - mParseBareString.setLength(0); - for (;;) { - final int ch = peek(); - - // TODO Can we clean this up? (This condition is from the old parser.) - if (ch == '(' || ch == ')' || ch == '{' || ch == ' ' || - // ']' is not part of atom (it's in resp-specials) - ch == ']' || - // docs claim that flags are \ atom but atom isn't supposed to - // contain - // * and some flags contain * - // ch == '%' || ch == '*' || - ch == '%' || - // TODO probably should not allow \ and should recognize - // it as a flag instead - // ch == '"' || ch == '\' || - ch == '"' || (0x00 <= ch && ch <= 0x1f) || ch == 0x7f) { - if (mParseBareString.length() == 0) { - throw new MessagingException("Expected string, none found."); - } - String s = mParseBareString.toString(); - - // NIL will be always converted into the empty string. - if (ImapConstants.NIL.equalsIgnoreCase(s)) { - return ImapString.EMPTY; - } - return new ImapSimpleString(s); - } else if (ch == '[') { - // Eat all until next ']' - mParseBareString.append((char) readByte()); - mParseBareString.append(readUntil(']')); - mParseBareString.append(']'); // readUntil won't include the end char. - } else { - mParseBareString.append((char) readByte()); - } - } - } - - private void parseElements(ImapList list, char end) - throws IOException, MessagingException { - for (;;) { - for (;;) { - final int next = peek(); - if (next == end) { - return; - } - if (next != ' ') { - break; - } - // Skip space - readByte(); - } - final ImapElement el = parseElement(); - if (el == null) { // EOL - return; - } - list.add(el); - } - } - - private ImapList parseList(char opening, char closing) - throws IOException, MessagingException { - expect(opening); - final ImapList list = new ImapList(); - parseElements(list, closing); - expect(closing); - return list; - } - - private ImapString parseLiteral() throws IOException, MessagingException { - expect('{'); - final int size; - try { - size = Integer.parseInt(readUntil('}')); - } catch (NumberFormatException nfe) { - throw new MessagingException("Invalid length in literal"); - } - if (size < 0) { - throw new MessagingException("Invalid negative length in literal"); - } - expect('\r'); - expect('\n'); - FixedLengthInputStream in = new FixedLengthInputStream(mIn, size); - if (size > mLiteralKeepInMemoryThreshold) { - return new ImapTempFileLiteral(in); - } else { - return new ImapMemoryLiteral(in); - } - } -} diff --git a/src/com/android/email/mail/store/imap/ImapSimpleString.java b/src/com/android/email/mail/store/imap/ImapSimpleString.java deleted file mode 100644 index 190c5237f..000000000 --- a/src/com/android/email/mail/store/imap/ImapSimpleString.java +++ /dev/null @@ -1,55 +0,0 @@ -/* - * 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.mail.store.imap; - -import com.android.emailcommon.utility.Utility; - -import java.io.ByteArrayInputStream; -import java.io.InputStream; - -/** - * Subclass of {@link ImapString} used for non literals. - */ -public class ImapSimpleString extends ImapString { - private String mString; - - /* package */ ImapSimpleString(String string) { - mString = (string != null) ? string : ""; - } - - @Override - public void destroy() { - mString = null; - super.destroy(); - } - - @Override - public String getString() { - return mString; - } - - @Override - public InputStream getAsStream() { - return new ByteArrayInputStream(Utility.toAscii(mString)); - } - - @Override - public String toString() { - // Purposefully not return just mString, in order to prevent using it instead of getString. - return "\"" + mString + "\""; - } -} diff --git a/src/com/android/email/mail/store/imap/ImapString.java b/src/com/android/email/mail/store/imap/ImapString.java deleted file mode 100644 index d74a7cf0e..000000000 --- a/src/com/android/email/mail/store/imap/ImapString.java +++ /dev/null @@ -1,186 +0,0 @@ -/* - * 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.mail.store.imap; - -import com.android.emailcommon.Logging; -import com.android.mail.utils.LogUtils; - -import java.io.ByteArrayInputStream; -import java.io.InputStream; -import java.text.ParseException; -import java.text.SimpleDateFormat; -import java.util.Date; -import java.util.Locale; - -/** - * Class represents an IMAP "element" that is not a list. - * - * An atom, quoted string, literal, are all represented by this. Values like OK, STATUS are too. - * Also, this class class may contain more arbitrary value like "BODY[HEADER.FIELDS ("DATE")]". - * See {@link ImapResponseParser}. - */ -public abstract class ImapString extends ImapElement { - private static final byte[] EMPTY_BYTES = new byte[0]; - - public static final ImapString EMPTY = new ImapString() { - @Override public void destroy() { - // Don't call super.destroy(). - // It's a shared object. We don't want the mDestroyed to be set on this. - } - - @Override public String getString() { - return ""; - } - - @Override public InputStream getAsStream() { - return new ByteArrayInputStream(EMPTY_BYTES); - } - - @Override public String toString() { - return ""; - } - }; - - // This is used only for parsing IMAP's FETCH ENVELOPE command, in which - // en_US-like date format is used like "01-Jan-2009 11:20:39 -0800", so this should be - // handled by Locale.US - private final static SimpleDateFormat DATE_TIME_FORMAT = - new SimpleDateFormat("dd-MMM-yyyy HH:mm:ss Z", Locale.US); - - private boolean mIsInteger; - private int mParsedInteger; - private Date mParsedDate; - - @Override - public final boolean isList() { - return false; - } - - @Override - public final boolean isString() { - return true; - } - - /** - * @return true if and only if the length of the string is larger than 0. - * - * Note: IMAP NIL is considered an empty string. See {@link ImapResponseParser - * #parseBareString}. - * On the other hand, a quoted/literal string with value NIL (i.e. "NIL" and {3}\r\nNIL) is - * treated literally. - */ - public final boolean isEmpty() { - return getString().length() == 0; - } - - public abstract String getString(); - - public abstract InputStream getAsStream(); - - /** - * @return whether it can be parsed as a number. - */ - public final boolean isNumber() { - if (mIsInteger) { - return true; - } - try { - mParsedInteger = Integer.parseInt(getString()); - mIsInteger = true; - return true; - } catch (NumberFormatException e) { - return false; - } - } - - /** - * @return value parsed as a number. - */ - public final int getNumberOrZero() { - if (!isNumber()) { - return 0; - } - return mParsedInteger; - } - - /** - * @return whether it can be parsed as a date using {@link #DATE_TIME_FORMAT}. - */ - public final boolean isDate() { - if (mParsedDate != null) { - return true; - } - if (isEmpty()) { - return false; - } - try { - mParsedDate = DATE_TIME_FORMAT.parse(getString()); - return true; - } catch (ParseException e) { - LogUtils.w(Logging.LOG_TAG, getString() + " can't be parsed as a date."); - return false; - } - } - - /** - * @return value it can be parsed as a {@link Date}, or null otherwise. - */ - public final Date getDateOrNull() { - if (!isDate()) { - return null; - } - return mParsedDate; - } - - /** - * @return whether the value case-insensitively equals to {@code s}. - */ - public final boolean is(String s) { - if (s == null) { - return false; - } - return getString().equalsIgnoreCase(s); - } - - - /** - * @return whether the value case-insensitively starts with {@code s}. - */ - public final boolean startsWith(String prefix) { - if (prefix == null) { - return false; - } - final String me = this.getString(); - if (me.length() < prefix.length()) { - return false; - } - return me.substring(0, prefix.length()).equalsIgnoreCase(prefix); - } - - // To force subclasses to implement it. - @Override - public abstract String toString(); - - @Override - public final boolean equalsForTest(ImapElement that) { - if (!super.equalsForTest(that)) { - return false; - } - ImapString thatString = (ImapString) that; - return getString().equals(thatString.getString()); - } -} diff --git a/src/com/android/email/mail/store/imap/ImapTempFileLiteral.java b/src/com/android/email/mail/store/imap/ImapTempFileLiteral.java deleted file mode 100644 index 4feccc760..000000000 --- a/src/com/android/email/mail/store/imap/ImapTempFileLiteral.java +++ /dev/null @@ -1,123 +0,0 @@ -/* - * 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.mail.store.imap; - -import com.android.email.FixedLengthInputStream; -import com.android.emailcommon.Logging; -import com.android.emailcommon.TempDirectory; -import com.android.emailcommon.utility.Utility; -import com.android.mail.utils.LogUtils; - -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. - LogUtils.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) { - LogUtils.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. - LogUtils.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/mail/store/imap/ImapUtility.java b/src/com/android/email/mail/store/imap/ImapUtility.java deleted file mode 100644 index dc024cce7..000000000 --- a/src/com/android/email/mail/store/imap/ImapUtility.java +++ /dev/null @@ -1,126 +0,0 @@ -/* - * 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.mail.store.imap; - -import com.android.emailcommon.Logging; -import com.android.mail.utils.LogUtils; - -import java.util.ArrayList; - -/** - * Utility methods for use with IMAP. - */ -public class ImapUtility { - /** - * Apply quoting rules per IMAP RFC, - * quoted = DQUOTE *QUOTED-CHAR DQUOTE - * QUOTED-CHAR = / "\" quoted-specials - * quoted-specials = DQUOTE / "\" - * - * This is used primarily for IMAP login, but might be useful elsewhere. - * - * NOTE: Not very efficient - you may wish to preflight this, or perhaps it should check - * for trouble chars before calling the replace functions. - * - * @param s The string to be quoted. - * @return A copy of the string, having undergone quoting as described above - */ - public static String imapQuoted(String s) { - - // First, quote any backslashes by replacing \ with \\ - // regex Pattern: \\ (Java string const = \\\\) - // Substitute: \\\\ (Java string const = \\\\\\\\) - String result = s.replaceAll("\\\\", "\\\\\\\\"); - - // Then, quote any double-quotes by replacing " with \" - // regex Pattern: " (Java string const = \") - // Substitute: \\" (Java string const = \\\\\") - result = result.replaceAll("\"", "\\\\\""); - - // return string with quotes around it - return "\"" + result + "\""; - } - - /** - * Gets all of the values in a sequence set per RFC 3501. Any ranges are expanded into a - * list of individual numbers. If the set is invalid, an empty array is returned. - *

-     * sequence-number = nz-number / "*"
-     * sequence-range  = sequence-number ":" sequence-number
-     * sequence-set    = (sequence-number / sequence-range) *("," sequence-set)
-     * 
- */ - public static String[] getImapSequenceValues(String set) { - ArrayList list = new ArrayList(); - if (set != null) { - String[] setItems = set.split(","); - for (String item : setItems) { - if (item.indexOf(':') == -1) { - // simple item - try { - Integer.parseInt(item); // Don't need the value; just ensure it's valid - list.add(item); - } catch (NumberFormatException e) { - LogUtils.d(Logging.LOG_TAG, "Invalid UID value", e); - } - } else { - // range - for (String rangeItem : getImapRangeValues(item)) { - list.add(rangeItem); - } - } - } - } - String[] stringList = new String[list.size()]; - return list.toArray(stringList); - } - - /** - * Expand the given number range into a list of individual numbers. If the range is not valid, - * an empty array is returned. - *
-     * sequence-number = nz-number / "*"
-     * sequence-range  = sequence-number ":" sequence-number
-     * sequence-set    = (sequence-number / sequence-range) *("," sequence-set)
-     * 
- */ - public static String[] getImapRangeValues(String range) { - ArrayList list = new ArrayList(); - try { - if (range != null) { - int colonPos = range.indexOf(':'); - if (colonPos > 0) { - int first = Integer.parseInt(range.substring(0, colonPos)); - int second = Integer.parseInt(range.substring(colonPos + 1)); - if (first < second) { - for (int i = first; i <= second; i++) { - list.add(Integer.toString(i)); - } - } else { - for (int i = first; i >= second; i--) { - list.add(Integer.toString(i)); - } - } - } - } - } catch (NumberFormatException e) { - LogUtils.d(Logging.LOG_TAG, "Invalid range value", e); - } - String[] stringList = new String[list.size()]; - return list.toArray(stringList); - } -} diff --git a/src/com/android/email/mail/transport/DiscourseLogger.java b/src/com/android/email/mail/transport/DiscourseLogger.java deleted file mode 100644 index 67f4e115b..000000000 --- a/src/com/android/email/mail/transport/DiscourseLogger.java +++ /dev/null @@ -1,119 +0,0 @@ -/* - * 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.mail.transport; - -import com.android.emailcommon.Logging; -import com.android.mail.utils.LogUtils; - -import java.util.ArrayList; - -/** - * A class to keep last N of lines sent to the server and responses received from the server. - * They are sent to logcat when {@link #logLastDiscourse} is called. - * - *

This class is used to log the recent network activities when a response parser crashes. - */ -public class DiscourseLogger { - private final int mBufferSize; - private String[] mBuffer; - private int mPos; - private final StringBuilder mReceivingLine = new StringBuilder(100); - - public DiscourseLogger(int bufferSize) { - mBufferSize = bufferSize; - initBuffer(); - } - - private void initBuffer() { - mBuffer = new String[mBufferSize]; - } - - /** Add a single line to {@link #mBuffer}. */ - private void addLine(String s) { - mBuffer[mPos] = s; - mPos++; - if (mPos >= mBufferSize) { - mPos = 0; - } - } - - private void addReceivingLineToBuffer() { - if (mReceivingLine.length() > 0) { - addLine(mReceivingLine.toString()); - mReceivingLine.delete(0, Integer.MAX_VALUE); - } - } - - /** - * Store a single byte received from the server in {@link #mReceivingLine}. When LF is - * received, the content of {@link #mReceivingLine} is added to {@link #mBuffer}. - */ - public void addReceivedByte(int b) { - if (0x20 <= b && b <= 0x7e) { // Append only printable ASCII chars. - mReceivingLine.append((char) b); - } else if (b == '\n') { // LF - addReceivingLineToBuffer(); - } else if (b == '\r') { // CR - } else { - final String hex = "00" + Integer.toHexString(b); - mReceivingLine.append("\\x" + hex.substring(hex.length() - 2, hex.length())); - } - } - - /** Add a line sent to the server to {@link #mBuffer}. */ - public void addSentCommand(String command) { - addLine(command); - } - - /** @return the contents of {@link #mBuffer} as a String array. */ - /* package for testing */ String[] getLines() { - addReceivingLineToBuffer(); - - ArrayList list = new ArrayList(); - - final int start = mPos; - int pos = mPos; - do { - String s = mBuffer[pos]; - if (s != null) { - list.add(s); - } - pos = (pos + 1) % mBufferSize; - } while (pos != start); - - String[] ret = new String[list.size()]; - list.toArray(ret); - return ret; - } - - /** - * Log the contents of the {@link mBuffer}, and clears it out. (So it's okay to call this - * method successively more than once. There will be no duplicate log.) - */ - public void logLastDiscourse() { - String[] lines = getLines(); - if (lines.length == 0) { - return; - } - - LogUtils.w(Logging.LOG_TAG, "Last network activities:"); - for (String r : getLines()) { - LogUtils.w(Logging.LOG_TAG, "%s", r); - } - initBuffer(); - } -} diff --git a/src/com/android/email/mail/transport/MailTransport.java b/src/com/android/email/mail/transport/MailTransport.java deleted file mode 100644 index 213fbfc99..000000000 --- a/src/com/android/email/mail/transport/MailTransport.java +++ /dev/null @@ -1,320 +0,0 @@ -/* - * 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.mail.transport; - -import android.content.Context; - -import com.android.email.DebugUtils; -import com.android.emailcommon.Logging; -import com.android.emailcommon.mail.CertificateValidationException; -import com.android.emailcommon.mail.MessagingException; -import com.android.emailcommon.provider.HostAuth; -import com.android.emailcommon.utility.SSLUtils; -import com.android.mail.utils.LogUtils; - -import java.io.BufferedInputStream; -import java.io.BufferedOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.net.InetAddress; -import java.net.InetSocketAddress; -import java.net.Socket; -import java.net.SocketAddress; -import java.net.SocketException; - -import javax.net.ssl.HostnameVerifier; -import javax.net.ssl.HttpsURLConnection; -import javax.net.ssl.SSLException; -import javax.net.ssl.SSLPeerUnverifiedException; -import javax.net.ssl.SSLSession; -import javax.net.ssl.SSLSocket; - -public class MailTransport { - - // TODO protected eventually - /*protected*/ public static final int SOCKET_CONNECT_TIMEOUT = 10000; - /*protected*/ public static final int SOCKET_READ_TIMEOUT = 60000; - - private static final HostnameVerifier HOSTNAME_VERIFIER = - HttpsURLConnection.getDefaultHostnameVerifier(); - - private final String mDebugLabel; - private final Context mContext; - protected final HostAuth mHostAuth; - - private Socket mSocket; - private InputStream mIn; - private OutputStream mOut; - - public MailTransport(Context context, String debugLabel, HostAuth hostAuth) { - super(); - mContext = context; - mDebugLabel = debugLabel; - mHostAuth = hostAuth; - } - - /** - * Returns a new transport, using the current transport as a model. The new transport is - * configured identically (as if {@link #setSecurity(int, boolean)}, {@link #setPort(int)} - * and {@link #setHost(String)} were invoked), but not opened or connected in any way. - */ - @Override - public MailTransport clone() { - return new MailTransport(mContext, mDebugLabel, mHostAuth); - } - - public String getHost() { - return mHostAuth.mAddress; - } - - public int getPort() { - return mHostAuth.mPort; - } - - public boolean canTrySslSecurity() { - return (mHostAuth.mFlags & HostAuth.FLAG_SSL) != 0; - } - - public boolean canTryTlsSecurity() { - return (mHostAuth.mFlags & HostAuth.FLAG_TLS) != 0; - } - - public boolean canTrustAllCertificates() { - return (mHostAuth.mFlags & HostAuth.FLAG_TRUST_ALL) != 0; - } - - /** - * Attempts to open a connection using the Uri supplied for connection parameters. Will attempt - * an SSL connection if indicated. - */ - public void open() throws MessagingException, CertificateValidationException { - if (DebugUtils.DEBUG) { - LogUtils.d(Logging.LOG_TAG, "*** " + mDebugLabel + " open " + - getHost() + ":" + String.valueOf(getPort())); - } - - try { - SocketAddress socketAddress = new InetSocketAddress(getHost(), getPort()); - if (canTrySslSecurity()) { - mSocket = SSLUtils.getSSLSocketFactory( - mContext, mHostAuth, canTrustAllCertificates()).createSocket(); - } else { - mSocket = new Socket(); - } - mSocket.connect(socketAddress, SOCKET_CONNECT_TIMEOUT); - // After the socket connects to an SSL server, confirm that the hostname is as expected - if (canTrySslSecurity() && !canTrustAllCertificates()) { - verifyHostname(mSocket, getHost()); - } - mIn = new BufferedInputStream(mSocket.getInputStream(), 1024); - mOut = new BufferedOutputStream(mSocket.getOutputStream(), 512); - mSocket.setSoTimeout(SOCKET_READ_TIMEOUT); - } catch (SSLException e) { - if (DebugUtils.DEBUG) { - LogUtils.d(Logging.LOG_TAG, e.toString()); - } - throw new CertificateValidationException(e.getMessage(), e); - } catch (IOException ioe) { - if (DebugUtils.DEBUG) { - LogUtils.d(Logging.LOG_TAG, ioe.toString()); - } - throw new MessagingException(MessagingException.IOERROR, ioe.toString()); - } catch (IllegalArgumentException iae) { - if (DebugUtils.DEBUG) { - LogUtils.d(Logging.LOG_TAG, iae.toString()); - } - throw new MessagingException(MessagingException.UNSPECIFIED_EXCEPTION, iae.toString()); - } - } - - /** - * Attempts to reopen a TLS connection using the Uri supplied for connection parameters. - * - * NOTE: No explicit hostname verification is required here, because it's handled automatically - * by the call to createSocket(). - * - * TODO should we explicitly close the old socket? This seems funky to abandon it. - */ - public void reopenTls() throws MessagingException { - try { - mSocket = SSLUtils.getSSLSocketFactory(mContext, mHostAuth, canTrustAllCertificates()) - .createSocket(mSocket, getHost(), getPort(), true); - mSocket.setSoTimeout(SOCKET_READ_TIMEOUT); - mIn = new BufferedInputStream(mSocket.getInputStream(), 1024); - mOut = new BufferedOutputStream(mSocket.getOutputStream(), 512); - - } catch (SSLException e) { - if (DebugUtils.DEBUG) { - LogUtils.d(Logging.LOG_TAG, e.toString()); - } - throw new CertificateValidationException(e.getMessage(), e); - } catch (IOException ioe) { - if (DebugUtils.DEBUG) { - LogUtils.d(Logging.LOG_TAG, ioe.toString()); - } - throw new MessagingException(MessagingException.IOERROR, ioe.toString()); - } - } - - /** - * Lightweight version of SSLCertificateSocketFactory.verifyHostname, which provides this - * service but is not in the public API. - * - * Verify the hostname of the certificate used by the other end of a - * connected socket. You MUST call this if you did not supply a hostname - * to SSLCertificateSocketFactory.createSocket(). It is harmless to call this method - * redundantly if the hostname has already been verified. - * - *

Wildcard certificates are allowed to verify any matching hostname, - * so "foo.bar.example.com" is verified if the peer has a certificate - * for "*.example.com". - * - * @param socket An SSL socket which has been connected to a server - * @param hostname The expected hostname of the remote server - * @throws IOException if something goes wrong handshaking with the server - * @throws SSLPeerUnverifiedException if the server cannot prove its identity - */ - private static void verifyHostname(Socket socket, String hostname) throws IOException { - // The code at the start of OpenSSLSocketImpl.startHandshake() - // ensures that the call is idempotent, so we can safely call it. - SSLSocket ssl = (SSLSocket) socket; - ssl.startHandshake(); - - SSLSession session = ssl.getSession(); - if (session == null) { - throw new SSLException("Cannot verify SSL socket without session"); - } - // TODO: Instead of reporting the name of the server we think we're connecting to, - // we should be reporting the bad name in the certificate. Unfortunately this is buried - // in the verifier code and is not available in the verifier API, and extracting the - // CN & alts is beyond the scope of this patch. - if (!HOSTNAME_VERIFIER.verify(hostname, session)) { - throw new SSLPeerUnverifiedException( - "Certificate hostname not useable for server: " + hostname); - } - } - - /** - * Get the socket timeout. - * @return the read timeout value in milliseconds - * @throws SocketException - */ - public int getSoTimeout() throws SocketException { - return mSocket.getSoTimeout(); - } - - /** - * Set the socket timeout. - * @param timeoutMilliseconds the read timeout value if greater than {@code 0}, or - * {@code 0} for an infinite timeout. - */ - public void setSoTimeout(int timeoutMilliseconds) throws SocketException { - mSocket.setSoTimeout(timeoutMilliseconds); - } - - public boolean isOpen() { - return (mIn != null && mOut != null && - mSocket != null && mSocket.isConnected() && !mSocket.isClosed()); - } - - /** - * Close the connection. MUST NOT return any exceptions - must be "best effort" and safe. - */ - public void close() { - try { - mIn.close(); - } catch (Exception e) { - // May fail if the connection is already closed. - } - try { - mOut.close(); - } catch (Exception e) { - // May fail if the connection is already closed. - } - try { - mSocket.close(); - } catch (Exception e) { - // May fail if the connection is already closed. - } - mIn = null; - mOut = null; - mSocket = null; - } - - public InputStream getInputStream() { - return mIn; - } - - public OutputStream getOutputStream() { - return mOut; - } - - /** - * Writes a single line to the server using \r\n termination. - */ - public void writeLine(String s, String sensitiveReplacement) throws IOException { - if (DebugUtils.DEBUG) { - if (sensitiveReplacement != null && !Logging.DEBUG_SENSITIVE) { - LogUtils.d(Logging.LOG_TAG, ">>> " + sensitiveReplacement); - } else { - LogUtils.d(Logging.LOG_TAG, ">>> " + s); - } - } - - OutputStream out = getOutputStream(); - out.write(s.getBytes()); - out.write('\r'); - out.write('\n'); - out.flush(); - } - - /** - * Reads a single line from the server, using either \r\n or \n as the delimiter. The - * delimiter char(s) are not included in the result. - */ - public String readLine(boolean loggable) throws IOException { - StringBuffer sb = new StringBuffer(); - InputStream in = getInputStream(); - int d; - while ((d = in.read()) != -1) { - if (((char)d) == '\r') { - continue; - } else if (((char)d) == '\n') { - break; - } else { - sb.append((char)d); - } - } - if (d == -1 && DebugUtils.DEBUG) { - LogUtils.d(Logging.LOG_TAG, "End of stream reached while trying to read line."); - } - String ret = sb.toString(); - if (loggable && DebugUtils.DEBUG) { - LogUtils.d(Logging.LOG_TAG, "<<< " + ret); - } - return ret; - } - - public InetAddress getLocalAddress() { - if (isOpen()) { - return mSocket.getLocalAddress(); - } else { - return null; - } - } -} diff --git a/src/com/android/email/provider/AccountBackupRestore.java b/src/com/android/email/provider/AccountBackupRestore.java deleted file mode 100644 index cb615618a..000000000 --- a/src/com/android/email/provider/AccountBackupRestore.java +++ /dev/null @@ -1,51 +0,0 @@ -/* - * 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.provider; - -import android.content.ContentResolver; -import android.content.Context; - -/** - * Helper class to facilitate EmailProvider's account backup/restore facility. - * - * Account backup/restore was implemented entirely for the purpose of recovering from database - * corruption errors that were/are sporadic and of undetermined cause (though the prevailing wisdom - * is that this is due to some kind of memory issue). Rather than have the offending database get - * deleted by SQLiteDatabase and forcing the user to recreate his accounts from scratch, it was - * decided to backup accounts when created/modified and then restore them if 1) there are no - * accounts in the database and 2) there are backup accounts. This, at least, would cause user's - * email data for IMAP/EAS to be re-synced and prevent the worst outcomes from occurring. - * - * To accomplish backup/restore, we use the facility now built in to EmailProvider to store a - * backup version of the Account and HostAuth tables in a second database (EmailProviderBackup.db) - * - * TODO: We might look into having our own DatabaseErrorHandler that tries to be clever about - * determining whether or not a "corrupt" database is truly corrupt; the problem here is that it - * has proven impossible to reproduce the bug, and therefore any "solution" of this kind of utterly - * impossible to test in the wild. - */ -public class AccountBackupRestore { - /** - * Backup user Account and HostAuth data into our backup database - * - * TODO Make EmailProvider do this automatically. - */ - public static void backup(Context context) { - ContentResolver resolver = context.getContentResolver(); - resolver.update(EmailProvider.ACCOUNT_BACKUP_URI, null, null, null); - } -} diff --git a/src/com/android/email/provider/AccountReconciler.java b/src/com/android/email/provider/AccountReconciler.java deleted file mode 100644 index 251c59a37..000000000 --- a/src/com/android/email/provider/AccountReconciler.java +++ /dev/null @@ -1,306 +0,0 @@ -/* - * 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.provider; - -import android.accounts.AccountManager; -import android.accounts.AccountManagerFuture; -import android.accounts.AuthenticatorException; -import android.accounts.OperationCanceledException; -import android.content.ComponentName; -import android.content.ContentResolver; -import android.content.Context; -import android.content.pm.PackageManager; -import android.database.Cursor; -import android.provider.CalendarContract; -import android.provider.ContactsContract; -import android.text.TextUtils; - -import com.android.email.NotificationController; -import com.android.email.R; -import com.android.email.SecurityPolicy; -import com.android.email.service.EmailServiceUtils; -import com.android.email.service.EmailServiceUtils.EmailServiceInfo; -import com.android.emailcommon.Logging; -import com.android.emailcommon.provider.Account; -import com.android.emailcommon.provider.HostAuth; -import com.android.emailcommon.utility.MigrationUtils; -import com.android.mail.utils.LogUtils; -import com.google.common.collect.ImmutableList; - -import java.io.IOException; -import java.util.Collections; -import java.util.LinkedHashSet; -import java.util.List; - -public class AccountReconciler { - /** - * Get all AccountManager accounts for all email types. - * @param context Our {@link Context}. - * @return A list of all {@link android.accounts.Account}s created by our app. - */ - private static List getAllAmAccounts(final Context context) { - final AccountManager am = AccountManager.get(context); - - // TODO: Consider getting the types programmatically, in case we add more types. - // Some Accounts types can be identical, the set de-duplicates. - final LinkedHashSet accountTypes = new LinkedHashSet(); - accountTypes.add(context.getString(R.string.account_manager_type_legacy_imap)); - accountTypes.add(context.getString(R.string.account_manager_type_pop3)); - accountTypes.add(context.getString(R.string.account_manager_type_exchange)); - - final ImmutableList.Builder builder = ImmutableList.builder(); - for (final String type : accountTypes) { - final android.accounts.Account[] accounts = am.getAccountsByType(type); - builder.add(accounts); - } - return builder.build(); - } - - /** - * Get a all {@link Account} objects from the {@link EmailProvider}. - * @param context Our {@link Context}. - * @return A list of all {@link Account}s from the {@link EmailProvider}. - */ - private static List getAllEmailProviderAccounts(final Context context) { - final Cursor c = context.getContentResolver().query(Account.CONTENT_URI, - Account.CONTENT_PROJECTION, null, null, null); - if (c == null) { - return Collections.emptyList(); - } - - final ImmutableList.Builder builder = ImmutableList.builder(); - try { - while (c.moveToNext()) { - final Account account = new Account(); - account.restore(c); - builder.add(account); - } - } finally { - c.close(); - } - return builder.build(); - } - - /** - * Compare our account list (obtained from EmailProvider) with the account list owned by - * AccountManager. If there are any orphans (an account in one list without a corresponding - * account in the other list), delete the orphan, as these must remain in sync. - * - * Note that the duplication of account information is caused by the Email application's - * incomplete integration with AccountManager. - * - * This function may not be called from the main/UI thread, because it makes blocking calls - * into the account manager. - * - * @param context The context in which to operate - */ - public static synchronized void reconcileAccounts(final Context context) { - final List amAccounts = getAllAmAccounts(context); - final List providerAccounts = getAllEmailProviderAccounts(context); - reconcileAccountsInternal(context, providerAccounts, amAccounts, true); - } - - /** - * Check if the AccountManager accounts list contains a specific account. - * @param accounts The list of {@link android.accounts.Account} objects. - * @param name The name of the account to find. - * @return Whether the account is in the list. - */ - private static boolean hasAmAccount(final List accounts, - final String name, final String type) { - for (final android.accounts.Account account : accounts) { - if (account.name.equalsIgnoreCase(name) && account.type.equalsIgnoreCase(type)) { - return true; - } - } - return false; - } - - /** - * Check if the EmailProvider accounts list contains a specific account. - * @param accounts The list of {@link Account} objects. - * @param name The name of the account to find. - * @return Whether the account is in the list. - */ - private static boolean hasEpAccount(final List accounts, final String name) { - for (final Account account : accounts) { - if (account.mEmailAddress.equalsIgnoreCase(name)) { - return true; - } - } - return false; - } - - /** - * Internal method to actually perform reconciliation, or simply check that it needs to be done - * and avoid doing any heavy work, depending on the value of the passed in - * {@code performReconciliation}. - */ - private static boolean reconcileAccountsInternal( - final Context context, - final List emailProviderAccounts, - final List accountManagerAccounts, - final boolean performReconciliation) { - boolean needsReconciling = false; - int accountsDeleted = 0; - boolean exchangeAccountDeleted = false; - - LogUtils.d(Logging.LOG_TAG, "reconcileAccountsInternal"); - - if (MigrationUtils.migrationInProgress()) { - LogUtils.d(Logging.LOG_TAG, "deferring reconciliation, migration in progress"); - return false; - } - - // See if we should have the Eas authenticators enabled. - if (!EmailServiceUtils.isServiceAvailable(context, - context.getString(R.string.protocol_eas))) { - EmailServiceUtils.disableExchangeComponents(context); - } else { - EmailServiceUtils.enableExchangeComponent(context); - } - // First, look through our EmailProvider accounts to make sure there's a corresponding - // AccountManager account - for (final Account providerAccount : emailProviderAccounts) { - final String providerAccountName = providerAccount.mEmailAddress; - final EmailServiceUtils.EmailServiceInfo infoForAccount = EmailServiceUtils - .getServiceInfoForAccount(context, providerAccount.mId); - - // We want to delete the account if there is no matching Account Manager account for it - // unless it is flagged as incomplete. We also want to delete it if we can't find - // an accountInfo object for it. - if (infoForAccount == null || !hasAmAccount( - accountManagerAccounts, providerAccountName, infoForAccount.accountType)) { - if (infoForAccount != null && - (providerAccount.mFlags & Account.FLAGS_INCOMPLETE) != 0) { - LogUtils.w(Logging.LOG_TAG, - "Account reconciler noticed incomplete account; ignoring"); - continue; - } - - needsReconciling = true; - if (performReconciliation) { - // This account has been deleted in the AccountManager! - LogUtils.d(Logging.LOG_TAG, - "Account deleted in AccountManager; deleting from provider: " + - providerAccountName); - // See if this is an exchange account - final HostAuth auth = providerAccount.getOrCreateHostAuthRecv(context); - LogUtils.d(Logging.LOG_TAG, "deleted account with hostAuth " + auth); - if (auth != null && TextUtils.equals(auth.mProtocol, - context.getString(R.string.protocol_eas))) { - exchangeAccountDeleted = true; - } - // Cancel all notifications for this account - NotificationController.cancelNotifications(context, providerAccount); - - context.getContentResolver().delete( - EmailProvider.uiUri("uiaccount", providerAccount.mId), null, null); - - accountsDeleted++; - - } - } - } - // Now, look through AccountManager accounts to make sure we have a corresponding cached EAS - // account from EmailProvider - boolean needsPolicyUpdate = false; - for (final android.accounts.Account accountManagerAccount : accountManagerAccounts) { - final String accountManagerAccountName = accountManagerAccount.name; - if (!hasEpAccount(emailProviderAccounts, accountManagerAccountName)) { - // This account has been deleted from the EmailProvider database - needsReconciling = true; - - if (performReconciliation) { - LogUtils.d(Logging.LOG_TAG, - "Account deleted from provider; deleting from AccountManager: " + - accountManagerAccountName); - // Delete the account - AccountManagerFuture blockingResult = AccountManager.get(context) - .removeAccount(accountManagerAccount, null, null); - try { - // Note: All of the potential errors from removeAccount() are simply logged - // here, as there is nothing to actually do about them. - blockingResult.getResult(); - } catch (OperationCanceledException e) { - LogUtils.w(Logging.LOG_TAG, e.toString()); - } catch (AuthenticatorException e) { - LogUtils.w(Logging.LOG_TAG, e.toString()); - } catch (IOException e) { - LogUtils.w(Logging.LOG_TAG, e.toString()); - } - // Just set a flag that our policies need to be updated with device - // So we can do the update, one time, at a later point in time. - needsPolicyUpdate = true; - } - } else { - // Fix up the Calendar and Contacts syncing. It used to be possible for IMAP and - // POP accounts to get calendar and contacts syncing enabled. - // See b/11818312 - final String accountType = accountManagerAccount.type; - final String protocol = EmailServiceUtils.getProtocolFromAccountType( - context, accountType); - final EmailServiceInfo info = EmailServiceUtils.getServiceInfo(context, protocol); - if (info == null || !info.syncCalendar) { - ContentResolver.setIsSyncable(accountManagerAccount, - CalendarContract.AUTHORITY, 0); - } - if (info == null || !info.syncContacts) { - ContentResolver.setIsSyncable(accountManagerAccount, - ContactsContract.AUTHORITY, 0); - } - } - } - - if (needsPolicyUpdate) { - // We have removed accounts from the AccountManager, let's make sure that - // our policies are up to date. - SecurityPolicy.getInstance(context).policiesUpdated(); - } - - final String composeActivityName = - context.getString(R.string.reconciliation_compose_activity_name); - if (!TextUtils.isEmpty(composeActivityName)) { - // If there are no accounts remaining after reconciliation, disable the compose activity - final boolean enableCompose = emailProviderAccounts.size() - accountsDeleted > 0; - final ComponentName componentName = new ComponentName(context, composeActivityName); - context.getPackageManager().setComponentEnabledSetting(componentName, - enableCompose ? PackageManager.COMPONENT_ENABLED_STATE_ENABLED : - PackageManager.COMPONENT_ENABLED_STATE_DISABLED, - PackageManager.DONT_KILL_APP); - LogUtils.d(LogUtils.TAG, "Setting compose activity to " - + (enableCompose ? "enabled" : "disabled")); - } - - - // If an account has been deleted, the simplest thing is just to kill our process. - // Otherwise we might have a service running trying to do something for the account - // which has been deleted, which can get NPEs. It's not as clean is it could be, but - // it still works pretty well because there is nowhere in the email app to delete the - // account. You have to go to Settings, so it's not user visible that the Email app - // has been killed. - if (accountsDeleted > 0) { - LogUtils.i(Logging.LOG_TAG, "Restarting because account deleted"); - if (exchangeAccountDeleted) { - EmailServiceUtils.killService(context, context.getString(R.string.protocol_eas)); - } - System.exit(-1); - } - - return needsReconciling; - } -} diff --git a/src/com/android/email/provider/AttachmentProvider.java b/src/com/android/email/provider/AttachmentProvider.java deleted file mode 100644 index c64fb4e4c..000000000 --- a/src/com/android/email/provider/AttachmentProvider.java +++ /dev/null @@ -1,338 +0,0 @@ -/* - * 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.provider; - -import android.content.ContentProvider; -import android.content.ContentUris; -import android.content.ContentValues; -import android.content.Context; -import android.content.pm.PackageManager; -import android.database.Cursor; -import android.database.MatrixCursor; -import android.graphics.Bitmap; -import android.graphics.BitmapFactory; -import android.net.Uri; -import android.os.Binder; -import android.os.ParcelFileDescriptor; - -import com.android.emailcommon.Logging; -import com.android.emailcommon.internet.MimeUtility; -import com.android.emailcommon.provider.EmailContent; -import com.android.emailcommon.provider.EmailContent.Attachment; -import com.android.emailcommon.provider.EmailContent.AttachmentColumns; -import com.android.emailcommon.utility.AttachmentUtilities; -import com.android.emailcommon.utility.AttachmentUtilities.Columns; -import com.android.mail.utils.LogUtils; -import com.android.mail.utils.MatrixCursorWithCachedColumns; - -import java.io.File; -import java.io.FileNotFoundException; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.util.List; - -/* - * A simple ContentProvider that allows file access to Email's attachments. - * - * The URI scheme is as follows. For raw file access: - * content://com.android.mail.attachmentprovider/acct#/attach#/RAW - * - * And for access to thumbnails: - * content://com.android.mail.attachmentprovider/acct#/attach#/THUMBNAIL/width#/height# - * - * The on-disk (storage) schema is as follows. - * - * Attachments are stored at: /account#.db_att/item# - * Thumbnails are stored at: /thmb_account#_item# - * - * Using the standard application context, account #10 and attachment # 20, this would be: - * /data/data/com.android.email/databases/10.db_att/20 - * /data/data/com.android.email/cache/thmb_10_20 - */ -public class AttachmentProvider extends ContentProvider { - - private static final String[] MIME_TYPE_PROJECTION = new String[] { - AttachmentColumns.MIME_TYPE, AttachmentColumns.FILENAME }; - private static final int MIME_TYPE_COLUMN_MIME_TYPE = 0; - private static final int MIME_TYPE_COLUMN_FILENAME = 1; - - private static final String[] PROJECTION_QUERY = new String[] { AttachmentColumns.FILENAME, - AttachmentColumns.SIZE, AttachmentColumns.CONTENT_URI }; - - @Override - public boolean onCreate() { - /* - * We use the cache dir as a temporary directory (since Android doesn't give us one) so - * on startup we'll clean up any .tmp files from the last run. - */ - - final File[] files = getContext().getCacheDir().listFiles(); - if (files != null) { - for (File file : files) { - final String filename = file.getName(); - if (filename.endsWith(".tmp") || filename.startsWith("thmb_")) { - file.delete(); - } - } - } - return true; - } - - /** - * Returns the mime type for a given attachment. There are three possible results: - * - If thumbnail Uri, always returns "image/png" (even if there's no attachment) - * - If the attachment does not exist, returns null - * - Returns the mime type of the attachment - */ - @Override - public String getType(Uri uri) { - long callingId = Binder.clearCallingIdentity(); - try { - List segments = uri.getPathSegments(); - String id = segments.get(1); - String format = segments.get(2); - if (AttachmentUtilities.FORMAT_THUMBNAIL.equals(format)) { - return "image/png"; - } else { - uri = ContentUris.withAppendedId(Attachment.CONTENT_URI, Long.parseLong(id)); - Cursor c = getContext().getContentResolver().query(uri, MIME_TYPE_PROJECTION, null, - null, null); - try { - if (c.moveToFirst()) { - String mimeType = c.getString(MIME_TYPE_COLUMN_MIME_TYPE); - String fileName = c.getString(MIME_TYPE_COLUMN_FILENAME); - mimeType = AttachmentUtilities.inferMimeType(fileName, mimeType); - return mimeType; - } - } finally { - c.close(); - } - return null; - } - } finally { - Binder.restoreCallingIdentity(callingId); - } - } - - /** - * Open an attachment file. There are two "formats" - "raw", which returns an actual file, - * and "thumbnail", which attempts to generate a thumbnail image. - * - * Thumbnails are cached for easy space recovery and cleanup. - * - * TODO: The thumbnail format returns null for its failure cases, instead of throwing - * FileNotFoundException, and should be fixed for consistency. - * - * @throws FileNotFoundException - */ - @Override - public ParcelFileDescriptor openFile(Uri uri, String mode) throws FileNotFoundException { - // If this is a write, the caller must have the EmailProvider permission, which is - // based on signature only - if (mode.equals("w")) { - Context context = getContext(); - if (context.checkCallingOrSelfPermission(EmailContent.PROVIDER_PERMISSION) - != PackageManager.PERMISSION_GRANTED) { - throw new FileNotFoundException(); - } - List segments = uri.getPathSegments(); - String accountId = segments.get(0); - String id = segments.get(1); - File saveIn = - AttachmentUtilities.getAttachmentDirectory(context, Long.parseLong(accountId)); - if (!saveIn.exists()) { - saveIn.mkdirs(); - } - File newFile = new File(saveIn, id); - return ParcelFileDescriptor.open( - newFile, ParcelFileDescriptor.MODE_READ_WRITE | - ParcelFileDescriptor.MODE_CREATE | ParcelFileDescriptor.MODE_TRUNCATE); - } - long callingId = Binder.clearCallingIdentity(); - try { - List segments = uri.getPathSegments(); - String accountId = segments.get(0); - String id = segments.get(1); - String format = segments.get(2); - if (AttachmentUtilities.FORMAT_THUMBNAIL.equals(format)) { - int width = Integer.parseInt(segments.get(3)); - int height = Integer.parseInt(segments.get(4)); - String filename = "thmb_" + accountId + "_" + id; - File dir = getContext().getCacheDir(); - File file = new File(dir, filename); - if (!file.exists()) { - Uri attachmentUri = AttachmentUtilities. - getAttachmentUri(Long.parseLong(accountId), Long.parseLong(id)); - Cursor c = query(attachmentUri, - new String[] { Columns.DATA }, null, null, null); - if (c != null) { - try { - if (c.moveToFirst()) { - attachmentUri = Uri.parse(c.getString(0)); - } else { - return null; - } - } finally { - c.close(); - } - } - String type = getContext().getContentResolver().getType(attachmentUri); - try { - InputStream in = - getContext().getContentResolver().openInputStream(attachmentUri); - Bitmap thumbnail = createThumbnail(type, in); - if (thumbnail == null) { - return null; - } - thumbnail = Bitmap.createScaledBitmap(thumbnail, width, height, true); - FileOutputStream out = new FileOutputStream(file); - thumbnail.compress(Bitmap.CompressFormat.PNG, 100, out); - out.close(); - in.close(); - } catch (IOException ioe) { - LogUtils.d(Logging.LOG_TAG, "openFile/thumbnail failed with " + - ioe.getMessage()); - return null; - } catch (OutOfMemoryError oome) { - LogUtils.d(Logging.LOG_TAG, "openFile/thumbnail failed with " + - oome.getMessage()); - return null; - } - } - return ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY); - } - else { - return ParcelFileDescriptor.open( - new File(getContext().getDatabasePath(accountId + ".db_att"), id), - ParcelFileDescriptor.MODE_READ_ONLY); - } - } finally { - Binder.restoreCallingIdentity(callingId); - } - } - - @Override - public int delete(Uri uri, String arg1, String[] arg2) { - return 0; - } - - @Override - public Uri insert(Uri uri, ContentValues values) { - return null; - } - - /** - * Returns a cursor based on the data in the attachments table, or null if the attachment - * is not recorded in the table. - * - * Supports REST Uri only, for a single row - selection, selection args, and sortOrder are - * ignored (non-null values should probably throw an exception....) - */ - @Override - public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, - String sortOrder) { - long callingId = Binder.clearCallingIdentity(); - try { - if (projection == null) { - projection = - new String[] { - Columns._ID, - Columns.DATA, - }; - } - - List segments = uri.getPathSegments(); - String accountId = segments.get(0); - String id = segments.get(1); - String format = segments.get(2); - String name = null; - int size = -1; - String contentUri = null; - - uri = ContentUris.withAppendedId(Attachment.CONTENT_URI, Long.parseLong(id)); - Cursor c = getContext().getContentResolver().query(uri, PROJECTION_QUERY, - null, null, null); - try { - if (c.moveToFirst()) { - name = c.getString(0); - size = c.getInt(1); - contentUri = c.getString(2); - } else { - return null; - } - } finally { - c.close(); - } - - MatrixCursor ret = new MatrixCursorWithCachedColumns(projection); - Object[] values = new Object[projection.length]; - for (int i = 0, count = projection.length; i < count; i++) { - String column = projection[i]; - if (Columns._ID.equals(column)) { - values[i] = id; - } - else if (Columns.DATA.equals(column)) { - values[i] = contentUri; - } - else if (Columns.DISPLAY_NAME.equals(column)) { - values[i] = name; - } - else if (Columns.SIZE.equals(column)) { - values[i] = size; - } - } - ret.addRow(values); - return ret; - } finally { - Binder.restoreCallingIdentity(callingId); - } - } - - @Override - public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) { - return 0; - } - - private static Bitmap createThumbnail(String type, InputStream data) { - if(MimeUtility.mimeTypeMatches(type, "image/*")) { - return createImageThumbnail(data); - } - return null; - } - - private static Bitmap createImageThumbnail(InputStream data) { - try { - Bitmap bitmap = BitmapFactory.decodeStream(data); - return bitmap; - } catch (OutOfMemoryError oome) { - LogUtils.d(Logging.LOG_TAG, "createImageThumbnail failed with " + oome.getMessage()); - return null; - } catch (Exception e) { - LogUtils.d(Logging.LOG_TAG, "createImageThumbnail failed with " + e.getMessage()); - return null; - } - } - - /** - * Need this to suppress warning in unit tests. - */ - @Override - public void shutdown() { - // Don't call super.shutdown(), which emits a warning... - } -} diff --git a/src/com/android/email/provider/ContentCache.java b/src/com/android/email/provider/ContentCache.java deleted file mode 100644 index 65021faba..000000000 --- a/src/com/android/email/provider/ContentCache.java +++ /dev/null @@ -1,822 +0,0 @@ -/* - * 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.provider; - -import android.content.ContentValues; -import android.database.CrossProcessCursor; -import android.database.Cursor; -import android.database.CursorWindow; -import android.database.CursorWrapper; -import android.database.MatrixCursor; -import android.net.Uri; -import android.util.LruCache; - -import com.android.email.DebugUtils; -import com.android.mail.utils.LogUtils; -import com.android.mail.utils.MatrixCursorWithCachedColumns; -import com.google.common.annotations.VisibleForTesting; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.HashMap; -import java.util.Map; -import java.util.Set; - -/** - * An LRU cache for EmailContent (Account, HostAuth, Mailbox, and Message, thus far). The intended - * user of this cache is EmailProvider itself; caching is entirely transparent to users of the - * provider. - * - * Usage examples; id is a String representation of a row id (_id), as it might be retrieved from - * a uri via getPathSegment - * - * To create a cache: - * ContentCache cache = new ContentCache(name, projection, max); - * - * To (try to) get a cursor from a cache: - * Cursor cursor = cache.getCursor(id, projection); - * - * To read from a table and cache the resulting cursor: - * 1. Get a CacheToken: CacheToken token = cache.getToken(id); - * 2. Get a cursor from the database: Cursor cursor = db.query(....); - * 3. Put the cursor in the cache: cache.putCursor(cursor, id, token); - * Only cursors with the projection given in the definition of the cache can be cached - * - * To delete one or more rows or update multiple rows from a table that uses cached data: - * 1. Lock the row in the cache: cache.lock(id); - * 2. Delete/update the row(s): db.delete(...); - * 3. Invalidate any other caches that might be affected by the delete/update: - * The entire cache: affectedCache.invalidate()* - * A specific row in a cache: affectedCache.invalidate(rowId) - * 4. Unlock the row in the cache: cache.unlock(id); - * - * To update a single row from a table that uses cached data: - * 1. Lock the row in the cache: cache.lock(id); - * 2. Update the row: db.update(...); - * 3. Unlock the row in the cache, passing in the new values: cache.unlock(id, values); - * - * Synchronization note: All of the public methods in ContentCache are synchronized (i.e. on the - * cache itself) except for methods that are solely used for debugging and do not modify the cache. - * All references to ContentCache that are external to the ContentCache class MUST synchronize on - * the ContentCache instance (e.g. CachedCursor.close()) - */ -public final class ContentCache { - private static final boolean DEBUG_CACHE = false; // DO NOT CHECK IN TRUE - private static final boolean DEBUG_TOKENS = false; // DO NOT CHECK IN TRUE - private static final boolean DEBUG_NOT_CACHEABLE = false; // DO NOT CHECK IN TRUE - private static final boolean DEBUG_STATISTICS = false; // DO NOT CHECK THIS IN TRUE - - // If false, reads will not use the cache; this is intended for debugging only - private static final boolean READ_CACHE_ENABLED = true; // DO NOT CHECK IN FALSE - - // Count of non-cacheable queries (debug only) - private static int sNotCacheable = 0; - // A map of queries that aren't cacheable (debug only) - private static final CounterMap sNotCacheableMap = new CounterMap(); - - private final LruCache mLruCache; - - // All defined caches - private static final ArrayList sContentCaches = new ArrayList(); - // A set of all unclosed, cached cursors; this will typically be a very small set, as cursors - // tend to be closed quickly after use. The value, for each cursor, is its reference count - /*package*/ static final CounterMap sActiveCursors = new CounterMap(24); - - // A set of locked content id's - private final CounterMap mLockMap = new CounterMap(4); - // A set of active tokens - /*package*/ TokenList mTokenList; - - // The name of the cache (used for logging) - private final String mName; - // The base projection (only queries in which all columns exist in this projection will be - // able to avoid a cache miss) - private final String[] mBaseProjection; - // The tag used for logging - private final String mLogTag; - // Cache statistics - private final Statistics mStats; - /** If {@code true}, lock the cache for all writes */ - private static boolean sLockCache; - - /** - * A synchronized reference counter for arbitrary objects - */ - /*package*/ static class CounterMap { - private HashMap mMap; - - /*package*/ CounterMap(int maxSize) { - mMap = new HashMap(maxSize); - } - - /*package*/ CounterMap() { - mMap = new HashMap(); - } - - /*package*/ synchronized int subtract(T object) { - Integer refCount = mMap.get(object); - int newCount; - if (refCount == null || refCount.intValue() == 0) { - throw new IllegalStateException(); - } - if (refCount > 1) { - newCount = refCount - 1; - mMap.put(object, newCount); - } else { - newCount = 0; - mMap.remove(object); - } - return newCount; - } - - /*package*/ synchronized void add(T object) { - Integer refCount = mMap.get(object); - if (refCount == null) { - mMap.put(object, 1); - } else { - mMap.put(object, refCount + 1); - } - } - - /*package*/ synchronized boolean contains(T object) { - return mMap.containsKey(object); - } - - /*package*/ synchronized int getCount(T object) { - Integer refCount = mMap.get(object); - return (refCount == null) ? 0 : refCount.intValue(); - } - - synchronized int size() { - return mMap.size(); - } - - /** - * For Debugging Only - not efficient - */ - synchronized Set> entrySet() { - return mMap.entrySet(); - } - } - - /** - * A list of tokens that are in use at any moment; there can be more than one token for an id - */ - /*package*/ static class TokenList extends ArrayList { - private static final long serialVersionUID = 1L; - private final String mLogTag; - - /*package*/ TokenList(String name) { - mLogTag = "TokenList-" + name; - } - - /*package*/ int invalidateTokens(String id) { - if (DebugUtils.DEBUG && DEBUG_TOKENS) { - LogUtils.d(mLogTag, "============ Invalidate tokens for: " + id); - } - ArrayList removeList = new ArrayList(); - int count = 0; - for (CacheToken token: this) { - if (token.getId().equals(id)) { - token.invalidate(); - removeList.add(token); - count++; - } - } - for (CacheToken token: removeList) { - remove(token); - } - return count; - } - - /*package*/ void invalidate() { - if (DebugUtils.DEBUG && DEBUG_TOKENS) { - LogUtils.d(mLogTag, "============ List invalidated"); - } - for (CacheToken token: this) { - token.invalidate(); - } - clear(); - } - - /*package*/ boolean remove(CacheToken token) { - boolean result = super.remove(token); - if (DebugUtils.DEBUG && DEBUG_TOKENS) { - if (result) { - LogUtils.d(mLogTag, "============ Removing token for: " + token.mId); - } else { - LogUtils.d(mLogTag, "============ No token found for: " + token.mId); - } - } - return result; - } - - public CacheToken add(String id) { - CacheToken token = new CacheToken(id); - super.add(token); - if (DebugUtils.DEBUG && DEBUG_TOKENS) { - LogUtils.d(mLogTag, "============ Taking token for: " + token.mId); - } - return token; - } - } - - /** - * A CacheToken is an opaque object that must be passed into putCursor in order to attempt to - * write into the cache. The token becomes invalidated by any intervening write to the cached - * record. - */ - public static final class CacheToken { - private final String mId; - private boolean mIsValid = READ_CACHE_ENABLED; - - /*package*/ CacheToken(String id) { - mId = id; - } - - /*package*/ String getId() { - return mId; - } - - /*package*/ boolean isValid() { - return mIsValid; - } - - /*package*/ void invalidate() { - mIsValid = false; - } - - @Override - public boolean equals(Object token) { - return ((token instanceof CacheToken) && ((CacheToken)token).mId.equals(mId)); - } - - @Override - public int hashCode() { - return mId.hashCode(); - } - } - - /** - * The cached cursor is simply a CursorWrapper whose underlying cursor contains zero or one - * rows. We handle simple movement (moveToFirst(), moveToNext(), etc.), and override close() - * to keep the underlying cursor alive (unless it's no longer cached due to an invalidation). - * Multiple CachedCursor's can use the same underlying cursor, so we override the various - * moveX methods such that each CachedCursor can have its own position information - */ - public static final class CachedCursor extends CursorWrapper implements CrossProcessCursor { - // The cursor we're wrapping - private final Cursor mCursor; - // The cache which generated this cursor - private final ContentCache mCache; - private final String mId; - // The current position of the cursor (can only be 0 or 1) - private int mPosition = -1; - // The number of rows in this cursor (-1 = not determined) - private int mCount = -1; - private boolean isClosed = false; - - public CachedCursor(Cursor cursor, ContentCache cache, String id) { - super(cursor); - mCursor = cursor; - mCache = cache; - mId = id; - // Add this to our set of active cursors - sActiveCursors.add(cursor); - } - - /** - * Close this cursor; if the cursor's cache no longer contains the underlying cursor, and - * there are no other users of that cursor, we'll close it here. In any event, - * we'll remove the cursor from our set of active cursors. - */ - @Override - public void close() { - synchronized(mCache) { - int count = sActiveCursors.subtract(mCursor); - if ((count == 0) && mCache.mLruCache.get(mId) != (mCursor)) { - super.close(); - } - } - isClosed = true; - } - - @Override - public boolean isClosed() { - return isClosed; - } - - @Override - public int getCount() { - if (mCount < 0) { - mCount = super.getCount(); - } - return mCount; - } - - /** - * We'll be happy to move to position 0 or -1 - */ - @Override - public boolean moveToPosition(int pos) { - if (pos >= getCount() || pos < -1) { - return false; - } - mPosition = pos; - return true; - } - - @Override - public boolean moveToFirst() { - return moveToPosition(0); - } - - @Override - public boolean moveToNext() { - return moveToPosition(mPosition + 1); - } - - @Override - public boolean moveToPrevious() { - return moveToPosition(mPosition - 1); - } - - @Override - public int getPosition() { - return mPosition; - } - - @Override - public final boolean move(int offset) { - return moveToPosition(mPosition + offset); - } - - @Override - public final boolean moveToLast() { - return moveToPosition(getCount() - 1); - } - - @Override - public final boolean isLast() { - return mPosition == (getCount() - 1); - } - - @Override - public final boolean isBeforeFirst() { - return mPosition == -1; - } - - @Override - public final boolean isAfterLast() { - return mPosition == 1; - } - - @Override - public CursorWindow getWindow() { - return ((CrossProcessCursor)mCursor).getWindow(); - } - - @Override - public void fillWindow(int pos, CursorWindow window) { - ((CrossProcessCursor)mCursor).fillWindow(pos, window); - } - - @Override - public boolean onMove(int oldPosition, int newPosition) { - return true; - } - } - - /** - * Public constructor - * @param name the name of the cache (used for logging) - * @param baseProjection the projection used for cached cursors; queries whose columns are not - * included in baseProjection will always generate a cache miss - * @param maxSize the maximum number of content cursors to cache - */ - public ContentCache(String name, String[] baseProjection, int maxSize) { - mName = name; - mLruCache = new LruCache(maxSize) { - @Override - protected void entryRemoved( - boolean evicted, String key, Cursor oldValue, Cursor newValue) { - // Close this cursor if it's no longer being used - if (evicted && !sActiveCursors.contains(oldValue)) { - oldValue.close(); - } - } - }; - mBaseProjection = baseProjection; - mLogTag = "ContentCache-" + name; - sContentCaches.add(this); - mTokenList = new TokenList(mName); - mStats = new Statistics(this); - } - - /** - * Return the base projection for cached rows - * Get the projection used for cached rows (typically, the largest possible projection) - * @return - */ - public String[] getProjection() { - return mBaseProjection; - } - - - /** - * Get a CacheToken for a row as specified by its id (_id column) - * @param id the id of the record - * @return a CacheToken needed in order to write data for the record back to the cache - */ - public synchronized CacheToken getCacheToken(String id) { - // If another thread is already writing the data, return an invalid token - CacheToken token = mTokenList.add(id); - if (mLockMap.contains(id)) { - token.invalidate(); - } - return token; - } - - public int size() { - return mLruCache.size(); - } - - @VisibleForTesting - Cursor get(String id) { - return mLruCache.get(id); - } - - protected Map getSnapshot() { - return mLruCache.snapshot(); - } - /** - * Try to cache a cursor for the given id and projection; returns a valid cursor, either a - * cached cursor (if caching was successful) or the original cursor - * - * @param c the cursor to be cached - * @param id the record id (_id) of the content - * @param projection the projection represented by the cursor - * @return whether or not the cursor was cached - */ - public Cursor putCursor(Cursor c, String id, String[] projection, CacheToken token) { - // Make sure the underlying cursor is at the first row, and do this without synchronizing, - // to prevent deadlock with a writing thread (which might, for example, be calling into - // CachedCursor.invalidate) - c.moveToPosition(0); - return putCursorImpl(c, id, projection, token); - } - public synchronized Cursor putCursorImpl(Cursor c, String id, String[] projection, - CacheToken token) { - try { - if (!token.isValid()) { - if (DebugUtils.DEBUG && DEBUG_CACHE) { - LogUtils.d(mLogTag, "============ Stale token for " + id); - } - mStats.mStaleCount++; - return c; - } - if (c != null && Arrays.equals(projection, mBaseProjection) && !sLockCache) { - if (DebugUtils.DEBUG && DEBUG_CACHE) { - LogUtils.d(mLogTag, "============ Caching cursor for: " + id); - } - // If we've already cached this cursor, invalidate the older one - Cursor existingCursor = get(id); - if (existingCursor != null) { - unlockImpl(id, null, false); - } - mLruCache.put(id, c); - return new CachedCursor(c, this, id); - } - return c; - } finally { - mTokenList.remove(token); - } - } - - /** - * Find and, if found, return a cursor, based on cached values, for the supplied id - * @param id the _id column of the desired row - * @param projection the requested projection for a query - * @return a cursor based on cached values, or null if the row is not cached - */ - public synchronized Cursor getCachedCursor(String id, String[] projection) { - if (DebugUtils.DEBUG && DEBUG_STATISTICS) { - // Every 200 calls to getCursor, report cache statistics - dumpOnCount(200); - } - if (projection == mBaseProjection) { - return getCachedCursorImpl(id); - } else { - return getMatrixCursor(id, projection); - } - } - - private CachedCursor getCachedCursorImpl(String id) { - Cursor c = get(id); - if (c != null) { - mStats.mHitCount++; - return new CachedCursor(c, this, id); - } - mStats.mMissCount++; - return null; - } - - private MatrixCursor getMatrixCursor(String id, String[] projection) { - return getMatrixCursor(id, projection, null); - } - - private MatrixCursor getMatrixCursor(String id, String[] projection, - ContentValues values) { - Cursor c = get(id); - if (c != null) { - // Make a new MatrixCursor with the requested columns - MatrixCursor mc = new MatrixCursorWithCachedColumns(projection, 1); - if (c.getCount() == 0) { - return mc; - } - Object[] row = new Object[projection.length]; - if (values != null) { - // Make a copy; we don't want to change the original - values = new ContentValues(values); - } - int i = 0; - for (String column: projection) { - int columnIndex = c.getColumnIndex(column); - if (columnIndex < 0) { - mStats.mProjectionMissCount++; - return null; - } else { - String value; - if (values != null && values.containsKey(column)) { - Object val = values.get(column); - if (val instanceof Boolean) { - value = (val == Boolean.TRUE) ? "1" : "0"; - } else { - value = values.getAsString(column); - } - values.remove(column); - } else { - value = c.getString(columnIndex); - } - row[i++] = value; - } - } - if (values != null && values.size() != 0) { - return null; - } - mc.addRow(row); - mStats.mHitCount++; - return mc; - } - mStats.mMissCount++; - return null; - } - - /** - * Lock a given row, such that no new valid CacheTokens can be created for the passed-in id. - * @param id the id of the row to lock - */ - public synchronized void lock(String id) { - // Prevent new valid tokens from being created - mLockMap.add(id); - // Invalidate current tokens - int count = mTokenList.invalidateTokens(id); - if (DebugUtils.DEBUG && DEBUG_TOKENS) { - LogUtils.d(mTokenList.mLogTag, "============ Lock invalidated " + count + - " tokens for: " + id); - } - } - - /** - * Unlock a given row, allowing new valid CacheTokens to be created for the passed-in id. - * @param id the id of the item whose cursor is cached - */ - public synchronized void unlock(String id) { - unlockImpl(id, null, true); - } - - /** - * If the row with id is currently cached, replaces the cached values with the supplied - * ContentValues. Then, unlock the row, so that new valid CacheTokens can be created. - * - * @param id the id of the item whose cursor is cached - * @param values updated values for this row - */ - public synchronized void unlock(String id, ContentValues values) { - unlockImpl(id, values, true); - } - - /** - * If values are passed in, replaces any cached cursor with one containing new values, and - * then closes the previously cached one (if any, and if not in use) - * If values are not passed in, removes the row from cache - * If the row was locked, unlock it - * @param id the id of the row - * @param values new ContentValues for the row (or null if row should simply be removed) - * @param wasLocked whether or not the row was locked; if so, the lock will be removed - */ - private void unlockImpl(String id, ContentValues values, boolean wasLocked) { - Cursor c = get(id); - if (c != null) { - if (DebugUtils.DEBUG && DEBUG_CACHE) { - LogUtils.d(mLogTag, "=========== Unlocking cache for: " + id); - } - if (values != null && !sLockCache) { - MatrixCursor cursor = getMatrixCursor(id, mBaseProjection, values); - if (cursor != null) { - if (DebugUtils.DEBUG && DEBUG_CACHE) { - LogUtils.d(mLogTag, "=========== Recaching with new values: " + id); - } - cursor.moveToFirst(); - mLruCache.put(id, cursor); - } else { - mLruCache.remove(id); - } - } else { - mLruCache.remove(id); - } - // If there are no cursors using the old cached cursor, close it - if (!sActiveCursors.contains(c)) { - c.close(); - } - } - if (wasLocked) { - mLockMap.subtract(id); - } - } - - /** - * Invalidate the entire cache, without logging - */ - public synchronized void invalidate() { - invalidate(null, null, null); - } - - /** - * Invalidate the entire cache; the arguments are used for logging only, and indicate the - * write operation that caused the invalidation - * - * @param operation a string describing the operation causing the invalidate (or null) - * @param uri the uri causing the invalidate (or null) - * @param selection the selection used with the uri (or null) - */ - public synchronized void invalidate(String operation, Uri uri, String selection) { - if (DEBUG_CACHE && (operation != null)) { - LogUtils.d(mLogTag, "============ INVALIDATED BY " + operation + ": " + uri + - ", SELECTION: " + selection); - } - mStats.mInvalidateCount++; - // Close all cached cursors that are no longer in use - mLruCache.evictAll(); - // Invalidate all current tokens - mTokenList.invalidate(); - } - - // Debugging code below - - private void dumpOnCount(int num) { - mStats.mOpCount++; - if ((mStats.mOpCount % num) == 0) { - dumpStats(); - } - } - - /*package*/ void recordQueryTime(Cursor c, long nanoTime) { - if (c instanceof CachedCursor) { - mStats.hitTimes += nanoTime; - mStats.hits++; - } else { - if (c.getCount() == 1) { - mStats.missTimes += nanoTime; - mStats.miss++; - } - } - } - - public static synchronized void notCacheable(Uri uri, String selection) { - if (DEBUG_NOT_CACHEABLE) { - sNotCacheable++; - String str = uri.toString() + "$" + selection; - sNotCacheableMap.add(str); - } - } - - // For use with unit tests - public static void invalidateAllCaches() { - for (ContentCache cache: sContentCaches) { - cache.invalidate(); - } - } - - /** Sets the cache lock. If the lock is {@code true}, also invalidates all cached items. */ - public static void setLockCacheForTest(boolean lock) { - sLockCache = lock; - if (sLockCache) { - invalidateAllCaches(); - } - } - - static class Statistics { - private final ContentCache mCache; - private final String mName; - - // Cache statistics - // The item is in the cache AND is used to create a cursor - private int mHitCount = 0; - // Basic cache miss (the item is not cached) - private int mMissCount = 0; - // Incremented when a cachePut is invalid due to an intervening write - private int mStaleCount = 0; - // A projection miss occurs when the item is cached, but not all requested columns are - // available in the base projection - private int mProjectionMissCount = 0; - // Incremented whenever the entire cache is invalidated - private int mInvalidateCount = 0; - // Count of operations put/get - private int mOpCount = 0; - // The following are for timing statistics - private long hits = 0; - private long hitTimes = 0; - private long miss = 0; - private long missTimes = 0; - - // Used in toString() and addCacheStatistics() - private int mCursorCount = 0; - private int mTokenCount = 0; - - Statistics(ContentCache cache) { - mCache = cache; - mName = mCache.mName; - } - - Statistics(String name) { - mCache = null; - mName = name; - } - - private void addCacheStatistics(ContentCache cache) { - if (cache != null) { - mHitCount += cache.mStats.mHitCount; - mMissCount += cache.mStats.mMissCount; - mProjectionMissCount += cache.mStats.mProjectionMissCount; - mStaleCount += cache.mStats.mStaleCount; - hitTimes += cache.mStats.hitTimes; - missTimes += cache.mStats.missTimes; - hits += cache.mStats.hits; - miss += cache.mStats.miss; - mCursorCount += cache.size(); - mTokenCount += cache.mTokenList.size(); - } - } - - private static void append(StringBuilder sb, String name, Object value) { - sb.append(", "); - sb.append(name); - sb.append(": "); - sb.append(value); - } - - @Override - public String toString() { - if (mHitCount + mMissCount == 0) return "No cache"; - int totalTries = mMissCount + mProjectionMissCount + mHitCount; - StringBuilder sb = new StringBuilder(); - sb.append("Cache " + mName); - append(sb, "Cursors", mCache == null ? mCursorCount : mCache.size()); - append(sb, "Hits", mHitCount); - append(sb, "Misses", mMissCount + mProjectionMissCount); - append(sb, "Inval", mInvalidateCount); - append(sb, "Tokens", mCache == null ? mTokenCount : mCache.mTokenList.size()); - append(sb, "Hit%", mHitCount * 100 / totalTries); - append(sb, "\nHit time", hitTimes / 1000000.0 / hits); - append(sb, "Miss time", missTimes / 1000000.0 / miss); - return sb.toString(); - } - } - - public static void dumpStats() { - Statistics totals = new Statistics("Totals"); - - for (ContentCache cache: sContentCaches) { - if (cache != null) { - LogUtils.d(cache.mName, cache.mStats.toString()); - totals.addCacheStatistics(cache); - } - } - LogUtils.d(totals.mName, totals.toString()); - } -} diff --git a/src/com/android/email/provider/DBHelper.java b/src/com/android/email/provider/DBHelper.java deleted file mode 100644 index dbeca637c..000000000 --- a/src/com/android/email/provider/DBHelper.java +++ /dev/null @@ -1,1896 +0,0 @@ -/* - * 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.SQLiteDoneException; -import android.database.sqlite.SQLiteOpenHelper; -import android.database.sqlite.SQLiteStatement; -import android.provider.BaseColumns; -import android.provider.CalendarContract; -import android.provider.ContactsContract; -import android.text.TextUtils; - -import com.android.email.DebugUtils; -import com.android.email.R; -import com.android.emailcommon.mail.Address; -import com.android.emailcommon.provider.Account; -import com.android.emailcommon.provider.Credential; -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.MessageChangeLogTable; -import com.android.emailcommon.provider.MessageMove; -import com.android.emailcommon.provider.MessageStateChange; -import com.android.emailcommon.provider.Policy; -import com.android.emailcommon.provider.QuickResponse; -import com.android.emailcommon.service.LegacyPolicySet; -import com.android.emailcommon.service.SyncWindow; -import com.android.mail.providers.UIProvider; -import com.android.mail.utils.LogUtils; -import com.google.common.annotations.VisibleForTesting; -import com.google.common.collect.ImmutableMap; - -import java.io.File; -import java.io.FileWriter; -import java.io.IOException; -import java.util.Map; - -public final class DBHelper { - private static final String TAG = "EmailProvider"; - - private static final String LEGACY_SCHEME_IMAP = "imap"; - private static final String LEGACY_SCHEME_POP3 = "pop3"; - private static final String LEGACY_SCHEME_EAS = "eas"; - - - private static final String WHERE_ID = BaseColumns._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." + BaseColumns._ID + - "; delete from " + Message.UPDATED_TABLE_NAME + - " where " + MessageColumns.MAILBOX_KEY + "=old." + BaseColumns._ID + - "; delete from " + Message.DELETED_TABLE_NAME + - " where " + MessageColumns.MAILBOX_KEY + "=old." + BaseColumns._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." + BaseColumns._ID + - "; delete from " + HostAuth.TABLE_NAME + - " where " + BaseColumns._ID + "=old." + AccountColumns.HOST_AUTH_KEY_RECV + - "; delete from " + HostAuth.TABLE_NAME + - " where " + BaseColumns._ID + "=old." + AccountColumns.HOST_AUTH_KEY_SEND + - "; delete from " + Policy.TABLE_NAME + - " where " + BaseColumns._ID + "=old." + AccountColumns.POLICY_KEY + - "; end"; - - private static final String TRIGGER_HOST_AUTH_DELETE = - "create trigger host_auth_delete after delete on " + HostAuth.TABLE_NAME + - " begin delete from " + Credential.TABLE_NAME + - " where " + Credential._ID + "=old." + HostAuthColumns.CREDENTIAL_KEY + - " and (select count(*) from " + HostAuth.TABLE_NAME + " where " + - HostAuthColumns.CREDENTIAL_KEY + "=old." + HostAuthColumns.CREDENTIAL_KEY + ")=0" + - "; 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 - // Version 104&105: add syncData to Message - // Version 106: Add certificate to HostAuth - // Version 107: Add a SEEN column to the message table - // Version 108: Add a cachedFile column to the attachments table - // Version 109: Migrate the account so they have the correct account manager types - // Version 110: Stop updating message_count, don't use auto lookback, and don't use - // ping/push_hold sync states. Note that message_count updating is restored in 113. - // Version 111: Delete Exchange account mailboxes. - // Version 112: Convert Mailbox syncInterval to a boolean (whether or not this mailbox - // syncs along with the account). - // Version 113: Restore message_count to being useful. - // Version 114: Add lastFullSyncTime column - // Version 115: Add pingDuration column - // Version 116: Add MessageMove & MessageStateChange tables. - // Version 117: Add trigger to delete duplicate messages on sync. - // Version 118: Set syncInterval to 0 for all IMAP mailboxes - // Version 119: Disable syncing of DRAFTS type folders. - // Version 120: Changed duplicateMessage deletion trigger to ignore search mailboxes. - // Version 121: Add mainMailboxKey, which will be set for messages that are in the fake - // "search_results" folder to reflect the mailbox that the server considers - // the message to be in. Also, wipe out any stale search_result folders. - // Version 122: Need to update Message_Updates and Message_Deletes to match previous. - // Version 123: Changed the duplicateMesage deletion trigger to ignore accounts that aren't - // exchange accounts. - // Version 124: Added MAX_ATTACHMENT_SIZE to the account table - // Version 125: Add credentials table for OAuth. - // Version 126: Decode address lists for To, From, Cc, Bcc and Reply-To columns in Message. - // Version 127: Force mFlags to contain the correct flags for EAS accounts given a protocol - // version above 12.0 - public static final int DATABASE_VERSION = 127; - - // 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 - // Version 100 is the first Email2 version - // Version 101: Move body contents to external files - public static final int BODY_DATABASE_VERSION = 101; - - /* - * 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 createMessageCountTriggers(final SQLiteDatabase db) { - // 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 " + BaseColumns._ID + "=NEW." + MessageColumns.MAILBOX_KEY + - "; end"); - - // Delete a message. - 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 " + BaseColumns._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 " + BaseColumns._ID + "=OLD." + MessageColumns.MAILBOX_KEY + - "; update " + Mailbox.TABLE_NAME + " set " + MailboxColumns.MESSAGE_COUNT + - '=' + MailboxColumns.MESSAGE_COUNT + "+1" + - " where " + BaseColumns._ID + "=NEW." + MessageColumns.MAILBOX_KEY + - "; end"); - } - - static void createCredentialsTable(SQLiteDatabase db) { - String s = " (" + Credential._ID + " integer primary key autoincrement, " - + Credential.PROVIDER_COLUMN + " text," - + Credential.ACCESS_TOKEN_COLUMN + " text," - + Credential.REFRESH_TOKEN_COLUMN + " text," - + Credential.EXPIRATION_COLUMN + " integer" - + ");"; - db.execSQL("create table " + Credential.TABLE_NAME + s); - db.execSQL(TRIGGER_HOST_AUTH_DELETE); - } - - static void dropDeleteDuplicateMessagesTrigger(final SQLiteDatabase db) { - db.execSQL("drop trigger message_delete_duplicates_on_insert"); - } - - /** - * Add a trigger to delete duplicate server side messages before insertion. - * This should delete any messages older messages that have the same serverId and account as - * the new message, if: - * Neither message is in a SEARCH type mailbox, and - * The new message's mailbox's account is an exchange account. - * - * Here is the plain text of this sql: - * create trigger message_delete_duplicates_on_insert before insert on - * Message for each row when new.syncServerId is not null and - * (select type from Mailbox where _id=new.mailboxKey) != 8 and - * (select HostAuth.protocol from HostAuth, Account where - * new.accountKey=account._id and account.hostAuthKeyRecv=hostAuth._id) = 'gEas' - * begin delete from Message where new.syncServerId=syncSeverId and - * new.accountKey=accountKey and - * (select Mailbox.type from Mailbox where _id=mailboxKey) != 8; end - */ - static void createDeleteDuplicateMessagesTrigger(final Context context, - final SQLiteDatabase db) { - db.execSQL("create trigger message_delete_duplicates_on_insert before insert on " - + Message.TABLE_NAME + " for each row when new." + SyncColumns.SERVER_ID - + " is not null and " - + "(select " + MailboxColumns.TYPE + " from " + Mailbox.TABLE_NAME - + " where " + MailboxColumns._ID + "=new." - + MessageColumns.MAILBOX_KEY + ")!=" + Mailbox.TYPE_SEARCH - + " and (select " - + HostAuth.TABLE_NAME + "." + HostAuthColumns.PROTOCOL + " from " - + HostAuth.TABLE_NAME + "," + Account.TABLE_NAME - + " where new." + MessageColumns.ACCOUNT_KEY - + "=" + Account.TABLE_NAME + "." + AccountColumns._ID - + " and " + Account.TABLE_NAME + "." + AccountColumns.HOST_AUTH_KEY_RECV - + "=" + HostAuth.TABLE_NAME + "." + HostAuthColumns._ID - + ")='" + context.getString(R.string.protocol_eas) + "'" - + " begin delete from " + Message.TABLE_NAME + " where new." - + SyncColumns.SERVER_ID + "=" + SyncColumns.SERVER_ID + " and new." - + MessageColumns.ACCOUNT_KEY + "=" + MessageColumns.ACCOUNT_KEY - + " and (select " + Mailbox.TABLE_NAME + "." + MailboxColumns.TYPE + " from " - + Mailbox.TABLE_NAME + " where " + MailboxColumns._ID + "=" - + MessageColumns.MAILBOX_KEY + ")!=" + Mailbox.TYPE_SEARCH +"; end"); - } - - static void createMessageTable(Context context, 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, " - + MessageColumns.SYNC_DATA + " text, " - + MessageColumns.FLAG_SEEN + " integer, " - + MessageColumns.MAIN_MAILBOX_KEY + " integer" - + ");"; - - // This String and the following String MUST have the same columns, except for the type - // of those columns! - String createString = " (" + BaseColumns._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 = " (" + BaseColumns._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." + BaseColumns._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 " + BaseColumns._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 " + BaseColumns._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 " + BaseColumns._ID + "=OLD." + MessageColumns.MAILBOX_KEY + - "; update " + Mailbox.TABLE_NAME + " set " + MailboxColumns.UNREAD_COUNT + - '=' + MailboxColumns.UNREAD_COUNT + "+1" + - " where " + BaseColumns._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 " + BaseColumns._ID + "=OLD." + MessageColumns.MAILBOX_KEY + - "; end"); - - // Add triggers to maintain message_count. - createMessageCountTriggers(db); - createDeleteDuplicateMessagesTrigger(context, db); - } - - static void resetMessageTable(Context context, 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(context, db); - } - - /** - * Common columns for all {@link MessageChangeLogTable} tables. - */ - private static String MESSAGE_CHANGE_LOG_COLUMNS = - MessageChangeLogTable.ID + " integer primary key autoincrement, " - + MessageChangeLogTable.MESSAGE_KEY + " integer, " - + MessageChangeLogTable.SERVER_ID + " text, " - + MessageChangeLogTable.ACCOUNT_KEY + " integer, " - + MessageChangeLogTable.STATUS + " integer, "; - - /** - * Create indices common to all {@link MessageChangeLogTable} tables. - * @param db The {@link SQLiteDatabase}. - * @param tableName The name of this particular table. - */ - private static void createMessageChangeLogTableIndices(final SQLiteDatabase db, - final String tableName) { - db.execSQL(createIndex(tableName, MessageChangeLogTable.MESSAGE_KEY)); - db.execSQL(createIndex(tableName, MessageChangeLogTable.ACCOUNT_KEY)); - } - - /** - * Create triggers common to all {@link MessageChangeLogTable} tables. - * @param db The {@link SQLiteDatabase}. - * @param tableName The name of this particular table. - */ - private static void createMessageChangeLogTableTriggers(final SQLiteDatabase db, - final String tableName) { - // Trigger to delete from the change log when a message is deleted. - db.execSQL("create trigger " + tableName + "_delete_message before delete on " - + Message.TABLE_NAME + " for each row begin delete from " + tableName - + " where " + MessageChangeLogTable.MESSAGE_KEY + "=old." + MessageColumns._ID - + "; end"); - - // Trigger to delete from the change log when an account is deleted. - db.execSQL("create trigger " + tableName + "_delete_account before delete on " - + Account.TABLE_NAME + " for each row begin delete from " + tableName - + " where " + MessageChangeLogTable.ACCOUNT_KEY + "=old." + AccountColumns._ID - + "; end"); - } - - /** - * Create the MessageMove table. - * @param db The {@link SQLiteDatabase}. - */ - private static void createMessageMoveTable(final SQLiteDatabase db) { - db.execSQL("create table " + MessageMove.TABLE_NAME + " (" - + MESSAGE_CHANGE_LOG_COLUMNS - + MessageMove.SRC_FOLDER_KEY + " integer, " - + MessageMove.DST_FOLDER_KEY + " integer, " - + MessageMove.SRC_FOLDER_SERVER_ID + " text, " - + MessageMove.DST_FOLDER_SERVER_ID + " text);"); - - createMessageChangeLogTableIndices(db, MessageMove.TABLE_NAME); - createMessageChangeLogTableTriggers(db, MessageMove.TABLE_NAME); - } - - /** - * Create the MessageStateChange table. - * @param db The {@link SQLiteDatabase}. - */ - private static void createMessageStateChangeTable(final SQLiteDatabase db) { - db.execSQL("create table " + MessageStateChange.TABLE_NAME + " (" - + MESSAGE_CHANGE_LOG_COLUMNS - + MessageStateChange.OLD_FLAG_READ + " integer, " - + MessageStateChange.NEW_FLAG_READ + " integer, " - + MessageStateChange.OLD_FLAG_FAVORITE + " integer, " - + MessageStateChange.NEW_FLAG_FAVORITE + " integer);"); - - createMessageChangeLogTableIndices(db, MessageStateChange.TABLE_NAME); - createMessageChangeLogTableTriggers(db, MessageStateChange.TABLE_NAME); - } - - @SuppressWarnings("deprecation") - static void createAccountTable(SQLiteDatabase db) { - String s = " (" + AccountColumns._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, " - + AccountColumns.MAX_ATTACHMENT_SIZE + " integer, " - + AccountColumns.PING_DURATION + " 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 = " (" + PolicyColumns._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 = " (" + HostAuthColumns._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," - + HostAuthColumns.SERVER_CERT + " blob," - + HostAuthColumns.CREDENTIAL_KEY + " integer" - + ");"; - 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); - } - - @SuppressWarnings("deprecation") - static void createMailboxTable(SQLiteDatabase db) { - String s = " (" + MailboxColumns._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, " - + MailboxColumns.LAST_FULL_SYNC_TIME + " integer" - + ");"; - 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 = " (" + AttachmentColumns._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, " - + AttachmentColumns.CACHED_FILE + " text" - + ");"; - 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 = " (" + QuickResponseColumns._ID + " integer primary key autoincrement, " - + QuickResponseColumns.TEXT + " text, " - + QuickResponseColumns.ACCOUNT_KEY + " integer" - + ");"; - db.execSQL("create table " + QuickResponse.TABLE_NAME + s); - } - - @SuppressWarnings("deprecation") - static void createBodyTable(SQLiteDatabase db) { - String s = " (" + BodyColumns._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)); - } - - private static void upgradeBodyToVersion5(final SQLiteDatabase db) { - try { - db.execSQL("drop table " + Body.TABLE_NAME); - createBodyTable(db); - } catch (final SQLException e) { - // Shouldn't be needed unless we're debugging and interrupt the process - LogUtils.w(TAG, e, "Exception upgrading EmailProviderBody.db from = 5 require that data be preserved! - if (oldVersion < 5) { - android.accounts.Account[] accounts = AccountManager.get(mContext) - .getAccountsByType(LEGACY_SCHEME_EAS); - for (android.accounts.Account account: accounts) { - AccountManager.get(mContext).removeAccount(account, null, null); - } - resetMessageTable(mContext, 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 - LogUtils.w(TAG, "Exception upgrading EmailProvider.db from v5 to v6", e); - } - } - // TODO: Change all these to strict inequalities - if (oldVersion <= 6) { - // Use the newer mailbox_delete trigger - db.execSQL("drop trigger mailbox_delete;"); - db.execSQL(TRIGGER_MAILBOX_DELETE); - } - 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 - LogUtils.w(TAG, "Exception upgrading EmailProvider.db from 7 to 8 " + e); - } - } - 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 - LogUtils.w(TAG, "Exception upgrading EmailProvider.db from 8 to 9 " + e); - } - } - 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 - LogUtils.w(TAG, "Exception upgrading EmailProvider.db from 9 to 10 " + e); - } - } - 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 - LogUtils.w(TAG, "Exception upgrading EmailProvider.db from 10 to 11 " + e); - } - } - 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 - LogUtils.w(TAG, "Exception upgrading EmailProvider.db from 11 to 12 " + e); - } - } - 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 - LogUtils.w(TAG, "Exception upgrading EmailProvider.db from 12 to 13 " + e); - } - } - if (oldVersion <= 13) { - try { - db.execSQL("alter table " + Message.TABLE_NAME - + " add column " + MessageColumns.SNIPPET - +" text" + ";"); - } catch (SQLException e) { - // Shouldn't be needed unless we're debugging and interrupt the process - LogUtils.w(TAG, "Exception upgrading EmailProvider.db from 13 to 14 " + e); - } - } - if (oldVersion <= 14) { - try { - db.execSQL("alter table " + Message.DELETED_TABLE_NAME - + " add column " + MessageColumns.SNIPPET +" text" + ";"); - db.execSQL("alter table " + Message.UPDATED_TABLE_NAME - + " add column " + MessageColumns.SNIPPET +" text" + ";"); - } catch (SQLException e) { - // Shouldn't be needed unless we're debugging and interrupt the process - LogUtils.w(TAG, "Exception upgrading EmailProvider.db from 14 to 15 " + e); - } - } - if (oldVersion <= 15) { - try { - db.execSQL("alter table " + Attachment.TABLE_NAME - + " add column " + AttachmentColumns.ACCOUNT_KEY +" integer" + ";"); - // Update all existing attachments to add the accountKey data - db.execSQL("update " + Attachment.TABLE_NAME + " set " + - AttachmentColumns.ACCOUNT_KEY + "= (SELECT " + Message.TABLE_NAME + - "." + MessageColumns.ACCOUNT_KEY + " from " + Message.TABLE_NAME + - " where " + Message.TABLE_NAME + "." + MessageColumns._ID + " = " + - Attachment.TABLE_NAME + "." + AttachmentColumns.MESSAGE_KEY + ")"); - } catch (SQLException e) { - // Shouldn't be needed unless we're debugging and interrupt the process - LogUtils.w(TAG, "Exception upgrading EmailProvider.db from 15 to 16 " + e); - } - } - 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 - LogUtils.w(TAG, "Exception upgrading EmailProvider.db from 16 to 17 " + e); - } - } - if (oldVersion <= 17) { - upgradeFromVersion17ToVersion18(db); - } - if (oldVersion <= 18) { - try { - db.execSQL("alter table " + Account.TABLE_NAME - + " add column " + AccountColumns.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 - LogUtils.w(TAG, "Exception upgrading EmailProvider.db from 18 to 19 " + e); - } - } - 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 - LogUtils.w(TAG, "Exception upgrading EmailProvider.db from 19 to 20 " + e); - } - } - if (oldVersion <= 21) { - upgradeFromVersion21ToVersion22(db, mContext); - oldVersion = 22; - } - if (oldVersion <= 22) { - upgradeFromVersion22ToVersion23(db); - } - if (oldVersion <= 23) { - upgradeFromVersion23ToVersion24(db); - } - if (oldVersion <= 24) { - upgradeFromVersion24ToVersion25(db); - } - if (oldVersion <= 25) { - upgradeFromVersion25ToVersion26(db); - } - if (oldVersion <= 26) { - try { - db.execSQL("alter table " + Message.TABLE_NAME - + " add column " + MessageColumns.PROTOCOL_SEARCH_INFO + " text;"); - db.execSQL("alter table " + Message.DELETED_TABLE_NAME - + " add column " + MessageColumns.PROTOCOL_SEARCH_INFO +" text" + ";"); - db.execSQL("alter table " + Message.UPDATED_TABLE_NAME - + " add column " + MessageColumns.PROTOCOL_SEARCH_INFO +" text" + ";"); - } catch (SQLException e) { - // Shouldn't be needed unless we're debugging and interrupt the process - LogUtils.w(TAG, "Exception upgrading EmailProvider.db from 26 to 27 " + e); - } - } - 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 - LogUtils.w(TAG, "Exception upgrading EmailProvider.db from 28 to 29 " + e); - } - } - if (oldVersion <= 29) { - upgradeFromVersion29ToVersion30(db); - } - 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 - LogUtils.w(TAG, "Exception upgrading EmailProvider.db from 30 to 31 " + e); - } - } - 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 - LogUtils.w(TAG, "Exception upgrading EmailProvider.db from 31 to 32 " + e); - } - } - if (oldVersion <= 32) { - try { - db.execSQL("alter table " + Attachment.TABLE_NAME - + " add column " + AttachmentColumns.UI_STATE + " integer;"); - db.execSQL("alter table " + Attachment.TABLE_NAME - + " add column " + AttachmentColumns.UI_DESTINATION + " integer;"); - db.execSQL("alter table " + Attachment.TABLE_NAME - + " add column " + AttachmentColumns.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 " + - AttachmentColumns.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 - LogUtils.w(TAG, "Exception upgrading EmailProvider.db from 32 to 33 " + e); - } - } - 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 - LogUtils.w(TAG, "Exception upgrading EmailProvider.db from 33 to 34 " + e); - } - } - 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 - LogUtils.w(TAG, "Exception upgrading EmailProvider.db from 34 to 35 " + e); - } - } - if (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 + "='" + - LEGACY_SCHEME_EAS + "')"); - } catch (SQLException e) { - // Shouldn't be needed unless we're debugging and interrupt the process - LogUtils.w(TAG, "Exception upgrading EmailProvider.db from 35 to 36 " + e); - } - } - 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 - LogUtils.w(TAG, "Exception upgrading EmailProvider.db from 37 to 38 " + e); - } - } - 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 - LogUtils.w(TAG, "Exception upgrading EmailProvider.db from 38 to 39 " + e); - } - } - if (oldVersion <= 39) { - upgradeToEmail2(db); - } - if (oldVersion <= 102) { - 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 - LogUtils.w(TAG, "Exception upgrading EmailProvider.db from v10x to v103", e); - } - } - if (oldVersion <= 103) { - try { - db.execSQL("alter table " + Message.TABLE_NAME - + " add " + MessageColumns.SYNC_DATA + " text"); - } catch (SQLException e) { - // Shouldn't be needed unless we're debugging and interrupt the process - LogUtils.w(TAG, "Exception upgrading EmailProvider.db from v103 to v104", e); - } - } - if (oldVersion <= 104) { - try { - db.execSQL("alter table " + Message.UPDATED_TABLE_NAME - + " add " + MessageColumns.SYNC_DATA + " text"); - db.execSQL("alter table " + Message.DELETED_TABLE_NAME - + " add " + MessageColumns.SYNC_DATA + " text"); - } catch (SQLException e) { - // Shouldn't be needed unless we're debugging and interrupt the process - LogUtils.w(TAG, "Exception upgrading EmailProvider.db from v104 to v105", e); - } - } - if (oldVersion <= 105) { - try { - db.execSQL("alter table " + HostAuth.TABLE_NAME - + " add " + HostAuthColumns.SERVER_CERT + " blob"); - } catch (SQLException e) { - // Shouldn't be needed unless we're debugging and interrupt the process - LogUtils.w(TAG, "Exception upgrading EmailProvider.db from v105 to v106", e); - } - } - if (oldVersion <= 106) { - try { - db.execSQL("alter table " + Message.TABLE_NAME - + " add " + MessageColumns.FLAG_SEEN + " integer"); - db.execSQL("alter table " + Message.UPDATED_TABLE_NAME - + " add " + MessageColumns.FLAG_SEEN + " integer"); - db.execSQL("alter table " + Message.DELETED_TABLE_NAME - + " add " + MessageColumns.FLAG_SEEN + " integer"); - } catch (SQLException e) { - // Shouldn't be needed unless we're debugging and interrupt the process - LogUtils.w(TAG, "Exception upgrading EmailProvider.db from v106 to v107", e); - } - } - if (oldVersion <= 107) { - try { - db.execSQL("alter table " + Attachment.TABLE_NAME - + " add column " + AttachmentColumns.CACHED_FILE +" text" + ";"); - } catch (SQLException e) { - // Shouldn't be needed unless we're debugging and interrupt the process - LogUtils.w(TAG, "Exception upgrading EmailProvider.db from v107 to v108", e); - } - } - if (oldVersion <= 108) { - // Migrate the accounts with the correct account type - migrateLegacyAccounts(db, mContext); - } - if (oldVersion <= 109) { - // Fix any mailboxes that have ping or push_hold states. - db.execSQL("update " + Mailbox.TABLE_NAME + " set " + MailboxColumns.SYNC_INTERVAL - + "=" + Mailbox.CHECK_INTERVAL_PUSH + " where " - + MailboxColumns.SYNC_INTERVAL + "<" + Mailbox.CHECK_INTERVAL_PUSH); - - // Fix invalid syncLookback values. - db.execSQL("update " + Account.TABLE_NAME + " set " + AccountColumns.SYNC_LOOKBACK - + "=" + SyncWindow.SYNC_WINDOW_1_WEEK + " where " - + AccountColumns.SYNC_LOOKBACK + " is null or " - + AccountColumns.SYNC_LOOKBACK + "<" + SyncWindow.SYNC_WINDOW_1_DAY + " or " - + AccountColumns.SYNC_LOOKBACK + ">" + SyncWindow.SYNC_WINDOW_ALL); - - db.execSQL("update " + Mailbox.TABLE_NAME + " set " + MailboxColumns.SYNC_LOOKBACK - + "=" + SyncWindow.SYNC_WINDOW_ACCOUNT + " where " - + MailboxColumns.SYNC_LOOKBACK + " is null or " - + MailboxColumns.SYNC_LOOKBACK + "<" + SyncWindow.SYNC_WINDOW_1_DAY + " or " - + MailboxColumns.SYNC_LOOKBACK + ">" + SyncWindow.SYNC_WINDOW_ALL); - } - if (oldVersion <= 110) { - // Delete account mailboxes. - db.execSQL("delete from " + Mailbox.TABLE_NAME + " where " + MailboxColumns.TYPE - + "=" +Mailbox.TYPE_EAS_ACCOUNT_MAILBOX); - } - if (oldVersion <= 111) { - // Mailbox sync interval now indicates whether this mailbox syncs with the rest - // of the account. Anyone who was syncing at all, plus outboxes, are set to 1, - // everyone else is 0. - db.execSQL("update " + Mailbox.TABLE_NAME + " set " + MailboxColumns.SYNC_INTERVAL - + "=case when " + MailboxColumns.SYNC_INTERVAL + "=" - + Mailbox.CHECK_INTERVAL_NEVER + " then 0 else 1 end"); - } - if (oldVersion >= 110 && oldVersion <= 112) { - // v110 had dropped these triggers, but starting with v113 we restored them - // (and altered the 109 -> 110 upgrade code to stop dropping them). - // We therefore only add them back for the versions in between. We also need to - // compute the correct value at this point as well. - recalculateMessageCount(db); - createMessageCountTriggers(db); - } - - if (oldVersion <= 113) { - try { - db.execSQL("alter table " + Mailbox.TABLE_NAME - + " add column " + MailboxColumns.LAST_FULL_SYNC_TIME +" integer" + ";"); - final ContentValues cv = new ContentValues(1); - cv.put(MailboxColumns.LAST_FULL_SYNC_TIME, 0); - db.update(Mailbox.TABLE_NAME, cv, null, null); - } catch (final SQLException e) { - // Shouldn't be needed unless we're debugging and interrupt the process - LogUtils.w(TAG, "Exception upgrading EmailProvider.db from v113 to v114", e); - } - } - - if (oldVersion <= 114) { - try { - db.execSQL("alter table " + Account.TABLE_NAME - + " add column " + AccountColumns.PING_DURATION +" integer" + ";"); - final ContentValues cv = new ContentValues(1); - cv.put(AccountColumns.PING_DURATION, 0); - db.update(Account.TABLE_NAME, cv, null, null); - } catch (final SQLException e) { - // Shouldn't be needed unless we're debugging and interrupt the process - LogUtils.w(TAG, "Exception upgrading EmailProvider.db from v113 to v114", e); - } - } - - if (oldVersion <= 115) { - createMessageMoveTable(db); - createMessageStateChangeTable(db); - } - - /** - * Originally, at 116, we added a trigger to delete duplicate messages. - * But we needed to change that trigger for version 120, so when we get - * there, we'll drop the trigger if it exists and create a new version. - */ - - /** - * This statement changes the syncInterval column to 0 for all IMAP mailboxes. - * It does this by matching mailboxes against all account IDs whose receive auth is - * either R.string.protocol_legacy_imap, R.string.protocol_imap or "imap" - */ - if (oldVersion <= 117) { - db.execSQL("update " + Mailbox.TABLE_NAME + " set " + MailboxColumns.SYNC_INTERVAL - + "=0 where " + MailboxColumns.ACCOUNT_KEY + " in (select " - + Account.TABLE_NAME + "." + AccountColumns._ID + " from " - + Account.TABLE_NAME + " join " + HostAuth.TABLE_NAME + " where " - + HostAuth.TABLE_NAME + "." + HostAuthColumns._ID + "=" - + Account.TABLE_NAME + "." + AccountColumns.HOST_AUTH_KEY_RECV - + " and (" + HostAuth.TABLE_NAME + "." - + HostAuthColumns.PROTOCOL + "='" - + mContext.getString(R.string.protocol_legacy_imap) + "' or " - + HostAuth.TABLE_NAME + "." + HostAuthColumns.PROTOCOL + "='" - + mContext.getString(R.string.protocol_imap) + "' or " - + HostAuth.TABLE_NAME + "." + HostAuthColumns.PROTOCOL + "='imap'));"); - } - - /** - * This statement changes the sync interval column to 0 for all DRAFTS type mailboxes, - * and deletes any messages that are: - * * synced from the server, and - * * in an exchange account draft folder - * - * This is primary for Exchange (b/11158759) but we don't sync draft folders for any - * other account type anyway. - * This will only affect people who used intermediate builds between email1 and email2, - * it should be a no-op for most users. - */ - if (oldVersion <= 118) { - db.execSQL("update " + Mailbox.TABLE_NAME + " set " + MailboxColumns.SYNC_INTERVAL - + "=0 where " + MailboxColumns.TYPE + "=" + Mailbox.TYPE_DRAFTS); - - db.execSQL("delete from " + Message.TABLE_NAME + " where " - + "(" + SyncColumns.SERVER_ID + " not null and " - + SyncColumns.SERVER_ID + "!='') and " - + MessageColumns.MAILBOX_KEY + " in (select " - + MailboxColumns._ID + " from " + Mailbox.TABLE_NAME + " where " - + MailboxColumns.TYPE + "=" + Mailbox.TYPE_DRAFTS + ")"); - } - - // We originally dropped and recreated the deleteDuplicateMessagesTrigger here at - // version 120. We needed to update it again at version 123, so there's no reason - // to do it twice. - - // Add the mainMailboxKey column, and get rid of any messages in the search_results - // folder. - if (oldVersion <= 120) { - db.execSQL("alter table " + Message.TABLE_NAME - + " add " + MessageColumns.MAIN_MAILBOX_KEY + " integer"); - - // Delete all TYPE_SEARCH mailboxes. These will be for stale queries anyway, and - // the messages in them will not have the mainMailboxKey column correctly populated. - // We have a trigger (See TRIGGER_MAILBOX_DELETE) that will delete any messages - // in the deleted mailboxes. - db.execSQL("delete from " + Mailbox.TABLE_NAME + " where " - + Mailbox.TYPE + "=" + Mailbox.TYPE_SEARCH); - } - - if (oldVersion <= 121) { - // The previous update omitted making these changes to the Message_Updates and - // Message_Deletes tables. The app will actually crash in between these versions! - db.execSQL("alter table " + Message.UPDATED_TABLE_NAME - + " add " + MessageColumns.MAIN_MAILBOX_KEY + " integer"); - db.execSQL("alter table " + Message.DELETED_TABLE_NAME - + " add " + MessageColumns.MAIN_MAILBOX_KEY + " integer"); - } - - if (oldVersion <= 122) { - if (oldVersion >= 117) { - /** - * This trigger was originally created at version 117, but we needed to change - * it for version 122. So if our oldVersion is 117 or more, we know we have that - * trigger and must drop it before re creating it. - */ - dropDeleteDuplicateMessagesTrigger(db); - } - createDeleteDuplicateMessagesTrigger(mContext, db); - } - - if (oldVersion <= 123) { - try { - db.execSQL("alter table " + Account.TABLE_NAME - + " add column " + AccountColumns.MAX_ATTACHMENT_SIZE +" integer" + ";"); - final ContentValues cv = new ContentValues(1); - cv.put(AccountColumns.MAX_ATTACHMENT_SIZE, 0); - db.update(Account.TABLE_NAME, cv, null, null); - } catch (final SQLException e) { - // Shouldn't be needed unless we're debugging and interrupt the process - LogUtils.w(TAG, "Exception upgrading EmailProvider.db from v123 to v124", e); - } - } - - if (oldVersion <= 124) { - createCredentialsTable(db); - // Add the credentialKey column, and set it to -1 for all pre-existing hostAuths. - db.execSQL("alter table " + HostAuth.TABLE_NAME - + " add " + HostAuthColumns.CREDENTIAL_KEY + " integer"); - db.execSQL("update " + HostAuth.TABLE_NAME + " set " - + HostAuthColumns.CREDENTIAL_KEY + "=-1"); - } - - if (oldVersion <= 125) { - upgradeFromVersion125ToVersion126(db); - } - - if (oldVersion <= 126) { - upgradeFromVersion126ToVersion127(mContext, db); - } - } - - @Override - public void onOpen(SQLiteDatabase db) { - try { - // Cleanup some nasty records - db.execSQL("DELETE FROM " + Account.TABLE_NAME - + " WHERE " + AccountColumns.DISPLAY_NAME + " ISNULL;"); - db.execSQL("DELETE FROM " + HostAuth.TABLE_NAME - + " WHERE " + HostAuthColumns.PROTOCOL + " ISNULL;"); - } catch (SQLException e) { - // Shouldn't be needed unless we're debugging and interrupt the process - LogUtils.e(TAG, e, "Exception cleaning EmailProvider.db"); - } - } - } - - @VisibleForTesting - @SuppressWarnings("deprecation") - static void convertPolicyFlagsToPolicyTable(SQLiteDatabase db) { - Cursor c = db.query(Account.TABLE_NAME, - new String[] {BaseColumns._ID /*0*/, AccountColumns.SECURITY_FLAGS /*1*/}, - AccountColumns.SECURITY_FLAGS + ">0", null, null, null, null); - try { - 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 /*_ID*/)); - db.update(Account.TABLE_NAME, cv, BaseColumns._ID + "=?", args); - } - } finally { - c.close(); - } - } - - /** 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 - LogUtils.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; - - private static void createAccountManagerAccount(Context context, String login, String type, - String password) { - final AccountManager accountManager = AccountManager.get(context); - - if (isAccountPresent(accountManager, login, type)) { - // The account already exists,just return - return; - } - LogUtils.v("Email", "Creating account %s %s", login, type); - final android.accounts.Account amAccount = new android.accounts.Account(login, type); - 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); - } - - private static boolean isAccountPresent(AccountManager accountManager, String name, - String type) { - final android.accounts.Account[] amAccounts = accountManager.getAccountsByType(type); - if (amAccounts != null) { - for (android.accounts.Account account : amAccounts) { - if (TextUtils.equals(account.name, name) && TextUtils.equals(account.type, type)) { - return true; - } - } - } - return false; - } - - @VisibleForTesting - static void upgradeFromVersion21ToVersion22(SQLiteDatabase db, Context accountManagerContext) { - migrateLegacyAccounts(db, accountManagerContext); - } - - private static void migrateLegacyAccounts(SQLiteDatabase db, Context accountManagerContext) { - final Map legacyToNewTypeMap = new ImmutableMap.Builder() - .put(LEGACY_SCHEME_POP3, - accountManagerContext.getString(R.string.account_manager_type_pop3)) - .put(LEGACY_SCHEME_IMAP, - accountManagerContext.getString(R.string.account_manager_type_legacy_imap)) - .put(LEGACY_SCHEME_EAS, - accountManagerContext.getString(R.string.account_manager_type_exchange)) - .build(); - try { - // Loop through accounts, looking for pop/imap accounts - final Cursor accountCursor = db.query(Account.TABLE_NAME, V21_ACCOUNT_PROJECTION, null, - null, null, null, null); - try { - final String[] hostAuthArgs = new String[1]; - while (accountCursor.moveToNext()) { - hostAuthArgs[0] = accountCursor.getString(V21_ACCOUNT_RECV); - // Get the "receive" HostAuth for this account - final Cursor hostAuthCursor = db.query(HostAuth.TABLE_NAME, - V21_HOSTAUTH_PROJECTION, HostAuthColumns._ID + "=?", hostAuthArgs, - null, null, null); - try { - if (hostAuthCursor.moveToFirst()) { - final String protocol = hostAuthCursor.getString(V21_HOSTAUTH_PROTOCOL); - // If this is a pop3 or imap account, create the account manager account - if (LEGACY_SCHEME_IMAP.equals(protocol) || - LEGACY_SCHEME_POP3.equals(protocol)) { - // If this is a pop3 or imap account, create the account manager - // account - if (DebugUtils.DEBUG) { - LogUtils.d(TAG, "Create AccountManager account for " + protocol - + "account: " - + accountCursor.getString(V21_ACCOUNT_EMAIL)); - } - createAccountManagerAccount(accountManagerContext, - accountCursor.getString(V21_ACCOUNT_EMAIL), - legacyToNewTypeMap.get(protocol), - hostAuthCursor.getString(V21_HOSTAUTH_PASSWORD)); - } else if (LEGACY_SCHEME_EAS.equals(protocol)) { - // If an EAS account, make Email sync automatically (equivalent of - // checking the "Sync Email" box in settings - - android.accounts.Account amAccount = new android.accounts.Account( - accountCursor.getString(V21_ACCOUNT_EMAIL), - legacyToNewTypeMap.get(protocol)); - 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 - LogUtils.w(TAG, "Exception while migrating accounts " + 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 - LogUtils.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 " + HostAuthColumns.CLIENT_CERT_ALIAS + " text;"); - } catch (SQLException e) { - // Shouldn't be needed unless we're debugging and interrupt the process - LogUtils.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 - LogUtils.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, HostAuthColumns._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 (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, AccountColumns._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 - LogUtils.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 = { - 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 = { - MessageColumns.BCC_LIST, - MessageColumns.CC_LIST, - MessageColumns.FROM_LIST, - MessageColumns.REPLY_TO_LIST, - MessageColumns.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.fromHeader(messageCursor.getString(ADDRESS_COLUMN_INDICES[i])); - cv.put(ADDRESS_COLUMN_NAMES[i], Address.toHeader(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 - LogUtils.w(TAG, "Exception upgrading EmailProvider.db from 29 to 30 " + e); - } - } - - private static void upgradeFromVersion125ToVersion126(SQLiteDatabase db) { - try { - // Loop through all messages, updating address columns to their decoded form - 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.fromHeader(messageCursor.getString(ADDRESS_COLUMN_INDICES[i])); - cv.put(ADDRESS_COLUMN_NAMES[i], Address.toString(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 - LogUtils.w(TAG, "Exception upgrading EmailProvider.db from 125 to 126 " + e); - } - } - - /** - * Update all accounts that are EAS v12.0 or greater with SmartForward and search flags - */ - private static void upgradeFromVersion126ToVersion127(final Context context, - final SQLiteDatabase db) { - try { - // These are the flags that we want to add to the Account table for the - // appropriate rows. - final long newFlags = Account.FLAGS_SUPPORTS_GLOBAL_SEARCH + - Account.FLAGS_SUPPORTS_SEARCH + Account.FLAGS_SUPPORTS_SMART_FORWARD; - - // For posterity; this is the command we're executing: - // UPDATE Account SET flags=flags|[new flags] WHERE _id IN (SELECT t1._id FROM Account - // t1 INNER JOIN HostAuth t2 ON t1.hostAuthKeyRecv=t2._id WHERE t2.protocol='gEas' AND - // CAST(t1.protocolVersion AS REAL)>=12.0) - db.execSQL( - "UPDATE " + Account.TABLE_NAME + " SET " + AccountColumns.FLAGS + "=" + - AccountColumns.FLAGS + "|" + Long.toString(newFlags) + " WHERE " + - AccountColumns._ID + " IN (SELECT t1." + AccountColumns._ID + " FROM " + - Account.TABLE_NAME + " t1 INNER JOIN " + HostAuth.TABLE_NAME + - " t2 ON t1." + AccountColumns.HOST_AUTH_KEY_RECV + "=t2._id WHERE t2." + - HostAuthColumns.PROTOCOL + "='" + - context.getString(R.string.protocol_eas) + "' AND CAST(t1." + - AccountColumns.PROTOCOL_VERSION + " AS REAL)>=12.0)"); - } catch (SQLException e) { - LogUtils.w(TAG, "Exception upgrading EmailProvider.db from 126 to 127 " + 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) { - LogUtils.w(TAG, "Exception upgrading EmailProvider.db from 31 to 32/100 " + e); - } - - // From 32->33 upgrade - try { - db.execSQL("update " + Attachment.TABLE_NAME + " set " + AttachmentColumns.UI_STATE + - "=" + UIProvider.AttachmentState.SAVED + " where " + - AttachmentColumns.CONTENT_URI + " is not null;"); - } catch (SQLException e) { - LogUtils.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) { - LogUtils.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 + "='" + - LEGACY_SCHEME_EAS + "')"); - } catch (SQLException e) { - LogUtils.w(TAG, "Exception upgrading EmailProvider.db from 35/36 to 37/100 " + e); - } - } -} diff --git a/src/com/android/email/provider/EmailConversationCursor.java b/src/com/android/email/provider/EmailConversationCursor.java deleted file mode 100644 index 4a49caa9d..000000000 --- a/src/com/android/email/provider/EmailConversationCursor.java +++ /dev/null @@ -1,245 +0,0 @@ -/* - * Copyright (C) 2014 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.ContentResolver; -import android.content.ContentValues; -import android.content.Context; -import android.database.Cursor; -import android.database.CursorWrapper; -import android.net.Uri; -import android.os.Bundle; -import android.text.TextUtils; -import android.text.format.DateUtils; -import android.text.util.Rfc822Token; -import android.text.util.Rfc822Tokenizer; - -import com.android.emailcommon.Logging; -import com.android.emailcommon.mail.Address; -import com.android.emailcommon.provider.EmailContent; -import com.android.emailcommon.provider.Mailbox; -import com.android.mail.browse.ConversationCursorOperationListener; -import com.android.mail.providers.ConversationInfo; -import com.android.mail.providers.Folder; -import com.android.mail.providers.FolderList; -import com.android.mail.providers.ParticipantInfo; -import com.android.mail.providers.UIProvider; -import com.android.mail.providers.UIProvider.ConversationColumns; -import com.android.mail.utils.LogUtils; -import com.google.common.collect.Lists; - -/** - * Wrapper that handles the visibility feature (i.e. the conversation list is visible, so - * any pending notifications for the corresponding mailbox should be canceled). We also handle - * getExtras() to provide a snapshot of the mailbox's status - */ -public class EmailConversationCursor extends CursorWrapper implements - ConversationCursorOperationListener { - private final long mMailboxId; - private final int mMailboxTypeId; - private final Context mContext; - private final FolderList mFolderList; - private final Bundle mExtras = new Bundle(); - - /** - * When showing a folder, if it's been at least this long since the last sync, - * force a folder refresh. - */ - private static final long AUTO_REFRESH_INTERVAL_MS = 5 * DateUtils.MINUTE_IN_MILLIS; - - public EmailConversationCursor(final Context context, final Cursor cursor, - final Folder folder, final long mailboxId) { - super(cursor); - mMailboxId = mailboxId; - mContext = context; - mFolderList = FolderList.copyOf(Lists.newArrayList(folder)); - Mailbox mailbox = Mailbox.restoreMailboxWithId(context, mailboxId); - - if (mailbox != null) { - mMailboxTypeId = mailbox.mType; - - mExtras.putInt(UIProvider.CursorExtraKeys.EXTRA_ERROR, - mailbox.mUiLastSyncResult); - mExtras.putInt(UIProvider.CursorExtraKeys.EXTRA_TOTAL_COUNT, mailbox.mTotalCount); - if (mailbox.mUiSyncStatus == EmailContent.SYNC_STATUS_BACKGROUND - || mailbox.mUiSyncStatus == EmailContent.SYNC_STATUS_USER - || mailbox.mUiSyncStatus == EmailContent.SYNC_STATUS_LIVE) { - mExtras.putInt(UIProvider.CursorExtraKeys.EXTRA_STATUS, - UIProvider.CursorStatus.LOADING); - } else if (mailbox.mUiSyncStatus == EmailContent.SYNC_STATUS_NONE) { - if (mailbox.mSyncInterval == 0 - && (Mailbox.isSyncableType(mailbox.mType) - || mailbox.mType == Mailbox.TYPE_SEARCH) - && !TextUtils.isEmpty(mailbox.mServerId) && - // TODO: There's potentially a race condition here. - // Consider merging this check with the auto-sync code in respond. - System.currentTimeMillis() - mailbox.mSyncTime - > AUTO_REFRESH_INTERVAL_MS) { - // This will be syncing momentarily - mExtras.putInt(UIProvider.CursorExtraKeys.EXTRA_STATUS, - UIProvider.CursorStatus.LOADING); - } else { - mExtras.putInt(UIProvider.CursorExtraKeys.EXTRA_STATUS, - UIProvider.CursorStatus.COMPLETE); - } - } else { - LogUtils.d(Logging.LOG_TAG, - "Unknown mailbox sync status" + mailbox.mUiSyncStatus); - mExtras.putInt(UIProvider.CursorExtraKeys.EXTRA_STATUS, - UIProvider.CursorStatus.COMPLETE); - } - } else { - mMailboxTypeId = -1; - // TODO for virtual mailboxes, we may want to do something besides just fake it - mExtras.putInt(UIProvider.CursorExtraKeys.EXTRA_ERROR, - UIProvider.LastSyncResult.SUCCESS); - mExtras.putInt(UIProvider.CursorExtraKeys.EXTRA_TOTAL_COUNT, - cursor != null ? cursor.getCount() : 0); - mExtras.putInt(UIProvider.CursorExtraKeys.EXTRA_STATUS, - UIProvider.CursorStatus.COMPLETE); - } - } - - @Override - public Bundle getExtras() { - return mExtras; - } - - @Override - public Bundle respond(Bundle params) { - final String setVisibilityKey = - UIProvider.ConversationCursorCommand.COMMAND_KEY_SET_VISIBILITY; - if (params.containsKey(setVisibilityKey)) { - final boolean visible = params.getBoolean(setVisibilityKey); - if (visible) { - // Mark all messages as seen - markContentsSeen(); - if (params.containsKey( - UIProvider.ConversationCursorCommand.COMMAND_KEY_ENTERED_FOLDER)) { - Mailbox mailbox = Mailbox.restoreMailboxWithId(mContext, mMailboxId); - if (mailbox != null) { - // For non-push mailboxes, if it's stale (i.e. last sync was a while - // ago), force a sync. - // TODO: Fix the check for whether we're non-push? Right now it checks - // whether we are participating in account sync rules. - if (mailbox.mSyncInterval == 0) { - final long timeSinceLastSync = - System.currentTimeMillis() - mailbox.mSyncTime; - if (timeSinceLastSync > AUTO_REFRESH_INTERVAL_MS) { - final ContentResolver resolver = mContext.getContentResolver(); - final Uri refreshUri = Uri.parse(EmailContent.CONTENT_URI + - "/" + EmailProvider.QUERY_UIREFRESH + "/" + mailbox.mId); - resolver.query(refreshUri, null, null, null, null); - } - } - } - } - } - } - // Return success - final Bundle response = new Bundle(2); - - response.putString(setVisibilityKey, - UIProvider.ConversationCursorCommand.COMMAND_RESPONSE_OK); - - final String rawFoldersKey = - UIProvider.ConversationCursorCommand.COMMAND_GET_RAW_FOLDERS; - if (params.containsKey(rawFoldersKey)) { - response.putParcelable(rawFoldersKey, mFolderList); - } - - final String convInfoKey = - UIProvider.ConversationCursorCommand.COMMAND_GET_CONVERSATION_INFO; - if (params.containsKey(convInfoKey)) { - response.putParcelable(convInfoKey, generateConversationInfo()); - } - - return response; - } - - private ConversationInfo generateConversationInfo() { - final int numMessages = getInt(getColumnIndex(ConversationColumns.NUM_MESSAGES)); - final ConversationInfo conversationInfo = new ConversationInfo(numMessages); - - conversationInfo.firstSnippet = getString(getColumnIndex(ConversationColumns.SNIPPET)); - conversationInfo.lastSnippet = conversationInfo.firstSnippet; - conversationInfo.firstUnreadSnippet = conversationInfo.firstSnippet; - - final boolean isRead = getInt(getColumnIndex(ConversationColumns.READ)) != 0; - final String senderString = getString(getColumnIndex(EmailContent.MessageColumns.DISPLAY_NAME)); - - final String fromString = getString(getColumnIndex(EmailContent.MessageColumns.FROM_LIST)); - final String senderEmail; - - if (fromString != null) { - final Rfc822Token[] tokens = Rfc822Tokenizer.tokenize(fromString); - if (tokens.length > 0) { - senderEmail = tokens[0].getAddress(); - } else { - LogUtils.d(LogUtils.TAG, "Couldn't parse sender email address"); - senderEmail = fromString; - } - } else { - senderEmail = null; - } - - // we *intentionally* report no participants for Draft emails so that the UI always - // displays the single word "Draft" as per b/13304929 - if (mMailboxTypeId == Mailbox.TYPE_DRAFTS) { - // the UI displays "Draft" in the conversation list based on this count - conversationInfo.draftCount = 1; - } else if (mMailboxTypeId == Mailbox.TYPE_SENT || - mMailboxTypeId == Mailbox.TYPE_OUTBOX) { - // for conversations in outgoing mail mailboxes return a list of recipients - final String recipientsString = getString(getColumnIndex( - EmailContent.MessageColumns.TO_LIST)); - final Address[] recipientAddresses = Address.parse(recipientsString); - for (Address recipientAddress : recipientAddresses) { - final String name = recipientAddress.getSimplifiedName(); - final String email = recipientAddress.getAddress(); - - // all recipients are said to have read all messages in the conversation - conversationInfo.addParticipant(new ParticipantInfo(name, email, 0, isRead)); - } - } else { - // for conversations in incoming mail mailboxes return the sender - conversationInfo.addParticipant(new ParticipantInfo(senderString, senderEmail, 0, - isRead)); - } - - return conversationInfo; - } - - @Override - public void markContentsSeen() { - final ContentResolver resolver = mContext.getContentResolver(); - final ContentValues contentValues = new ContentValues(1); - contentValues.put(EmailContent.MessageColumns.FLAG_SEEN, true); - final Uri uri = EmailContent.Message.CONTENT_URI; - final String where = EmailContent.MessageColumns.MAILBOX_KEY + " = ? AND " + - EmailContent.MessageColumns.FLAG_SEEN + " != ?"; - final String[] selectionArgs = {String.valueOf(mMailboxId), "1"}; - resolver.update(uri, contentValues, where, selectionArgs); - } - - @Override - public void emptyFolder() { - final ContentResolver resolver = mContext.getContentResolver(); - final Uri purgeUri = EmailProvider.uiUri("uipurgefolder", mMailboxId); - resolver.delete(purgeUri, null, null); - } -} diff --git a/src/com/android/email/provider/EmailMessageCursor.java b/src/com/android/email/provider/EmailMessageCursor.java deleted file mode 100644 index d57fb1e34..000000000 --- a/src/com/android/email/provider/EmailMessageCursor.java +++ /dev/null @@ -1,124 +0,0 @@ -/* - * Copyright (C) 2014 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.ContentResolver; -import android.content.Context; -import android.database.Cursor; -import android.database.CursorWrapper; -import android.net.Uri; -import android.provider.BaseColumns; -import android.util.SparseArray; - -import com.android.emailcommon.provider.EmailContent.Body; -import com.android.mail.utils.HtmlSanitizer; -import com.android.mail.utils.LogUtils; - -import org.apache.commons.io.IOUtils; - -import java.io.IOException; -import java.io.InputStream; - -/** - * This class wraps a cursor for the purpose of bypassing the CursorWindow object for the - * potentially over-sized body content fields. The CursorWindow has a hard limit of 2MB and so a - * large email message can exceed that limit and cause the cursor to fail to load. - * - * To get around this, we load null values in those columns, and then in this wrapper we directly - * load the content from the provider, skipping the cursor window. - * - * This will still potentially blow up if this cursor gets wrapped in a CrossProcessCursorWrapper - * which uses a CursorWindow to shuffle results between processes. Since we're only using this for - * passing a cursor back to UnifiedEmail this shouldn't be an issue. - */ -public class EmailMessageCursor extends CursorWrapper { - - private final SparseArray mTextParts; - private final SparseArray mHtmlParts; - private final int mTextColumnIndex; - private final int mHtmlColumnIndex; - - public EmailMessageCursor(final Context c, final Cursor cursor, final String htmlColumn, - final String textColumn) { - super(cursor); - mHtmlColumnIndex = cursor.getColumnIndex(htmlColumn); - mTextColumnIndex = cursor.getColumnIndex(textColumn); - final int cursorSize = cursor.getCount(); - mHtmlParts = new SparseArray(cursorSize); - mTextParts = new SparseArray(cursorSize); - - final ContentResolver cr = c.getContentResolver(); - - while (cursor.moveToNext()) { - final int position = cursor.getPosition(); - final long messageId = cursor.getLong(cursor.getColumnIndex(BaseColumns._ID)); - try { - if (mHtmlColumnIndex != -1) { - final Uri htmlUri = Body.getBodyHtmlUriForMessageWithId(messageId); - final InputStream in = cr.openInputStream(htmlUri); - final String underlyingHtmlString; - try { - underlyingHtmlString = IOUtils.toString(in); - } finally { - in.close(); - } - final String sanitizedHtml = HtmlSanitizer.sanitizeHtml(underlyingHtmlString); - mHtmlParts.put(position, sanitizedHtml); - } - } catch (final IOException e) { - LogUtils.v(LogUtils.TAG, e, "Did not find html body for message %d", messageId); - } - try { - if (mTextColumnIndex != -1) { - final Uri textUri = Body.getBodyTextUriForMessageWithId(messageId); - final InputStream in = cr.openInputStream(textUri); - final String underlyingTextString; - try { - underlyingTextString = IOUtils.toString(in); - } finally { - in.close(); - } - mTextParts.put(position, underlyingTextString); - } - } catch (final IOException e) { - LogUtils.v(LogUtils.TAG, e, "Did not find text body for message %d", messageId); - } - } - cursor.moveToPosition(-1); - } - - @Override - public String getString(final int columnIndex) { - if (columnIndex == mHtmlColumnIndex) { - return mHtmlParts.get(getPosition()); - } else if (columnIndex == mTextColumnIndex) { - return mTextParts.get(getPosition()); - } - return super.getString(columnIndex); - } - - @Override - public int getType(int columnIndex) { - if (columnIndex == mHtmlColumnIndex || columnIndex == mTextColumnIndex) { - // Need to force this, otherwise we might fall through to some other get*() method - // instead of getString() if the underlying cursor has other ideas about this content - return FIELD_TYPE_STRING; - } else { - return super.getType(columnIndex); - } - } -} diff --git a/src/com/android/email/provider/EmailProvider.java b/src/com/android/email/provider/EmailProvider.java deleted file mode 100644 index 893fecc4d..000000000 --- a/src/com/android/email/provider/EmailProvider.java +++ /dev/null @@ -1,6188 +0,0 @@ -/* - * 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.provider; - -import android.accounts.AccountManager; -import android.appwidget.AppWidgetManager; -import android.content.ComponentCallbacks; -import android.content.ComponentName; -import android.content.ContentProvider; -import android.content.ContentProviderOperation; -import android.content.ContentProviderResult; -import android.content.ContentResolver; -import android.content.ContentUris; -import android.content.ContentValues; -import android.content.Context; -import android.content.Intent; -import android.content.OperationApplicationException; -import android.content.PeriodicSync; -import android.content.SharedPreferences; -import android.content.UriMatcher; -import android.content.pm.ActivityInfo; -import android.content.res.Configuration; -import android.content.res.Resources; -import android.database.ContentObserver; -import android.database.Cursor; -import android.database.CursorWrapper; -import android.database.DatabaseUtils; -import android.database.MatrixCursor; -import android.database.MergeCursor; -import android.database.sqlite.SQLiteDatabase; -import android.database.sqlite.SQLiteException; -import android.database.sqlite.SQLiteStatement; -import android.net.Uri; -import android.os.AsyncTask; -import android.os.Binder; -import android.os.Build; -import android.os.Bundle; -import android.os.Handler; -import android.os.Handler.Callback; -import android.os.Looper; -import android.os.Parcel; -import android.os.ParcelFileDescriptor; -import android.os.RemoteException; -import android.provider.BaseColumns; -import android.text.TextUtils; -import android.text.format.DateUtils; -import android.util.Base64; -import android.util.Log; -import android.util.SparseArray; - -import com.android.common.content.ProjectionMap; -import com.android.email.DebugUtils; -import com.android.email.Preferences; -import com.android.email.R; -import com.android.email.SecurityPolicy; -import com.android.email.activity.setup.AccountSecurity; -import com.android.email.activity.setup.AccountSettingsFragment; -import com.android.email.activity.setup.AccountSettingsUtils; -import com.android.email.activity.setup.HeadlessAccountSettingsLoader; -import com.android.email.service.AttachmentService; -import com.android.email.service.EmailServiceUtils; -import com.android.email.service.EmailServiceUtils.EmailServiceInfo; -import com.android.email2.ui.MailActivityEmail; -import com.android.emailcommon.Logging; -import com.android.emailcommon.mail.Address; -import com.android.emailcommon.provider.Account; -import com.android.emailcommon.provider.Credential; -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.MailboxUtilities; -import com.android.emailcommon.provider.MessageChangeLogTable; -import com.android.emailcommon.provider.MessageMove; -import com.android.emailcommon.provider.MessageStateChange; -import com.android.emailcommon.provider.Policy; -import com.android.emailcommon.provider.QuickResponse; -import com.android.emailcommon.service.EmailServiceProxy; -import com.android.emailcommon.service.EmailServiceStatus; -import com.android.emailcommon.service.IEmailService; -import com.android.emailcommon.service.SearchParams; -import com.android.emailcommon.utility.AttachmentUtilities; -import com.android.emailcommon.utility.EmailAsyncTask; -import com.android.emailcommon.utility.Utility; -import com.android.ex.photo.provider.PhotoContract; -import com.android.mail.preferences.MailPrefs; -import com.android.mail.preferences.MailPrefs.PreferenceKeys; -import com.android.mail.providers.Folder; -import com.android.mail.providers.FolderList; -import com.android.mail.providers.Settings; -import com.android.mail.providers.UIProvider; -import com.android.mail.providers.UIProvider.AccountCapabilities; -import com.android.mail.providers.UIProvider.AccountColumns.SettingsColumns; -import com.android.mail.providers.UIProvider.AccountCursorExtraKeys; -import com.android.mail.providers.UIProvider.ConversationPriority; -import com.android.mail.providers.UIProvider.ConversationSendingState; -import com.android.mail.providers.UIProvider.DraftType; -import com.android.mail.utils.AttachmentUtils; -import com.android.mail.utils.LogTag; -import com.android.mail.utils.LogUtils; -import com.android.mail.utils.MatrixCursorWithCachedColumns; -import com.android.mail.utils.MatrixCursorWithExtra; -import com.android.mail.utils.MimeType; -import com.android.mail.utils.Utils; -import com.android.mail.widget.BaseWidgetProvider; -import com.google.common.collect.ImmutableMap; -import com.google.common.collect.ImmutableSet; -import com.google.common.collect.Sets; - -import java.io.File; -import java.io.FileDescriptor; -import java.io.FileNotFoundException; -import java.io.FileWriter; -import java.io.IOException; -import java.io.PrintWriter; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; -import java.util.HashSet; -import java.util.List; -import java.util.Locale; -import java.util.Map; -import java.util.Set; -import java.util.regex.Pattern; - -public class EmailProvider extends ContentProvider - implements SharedPreferences.OnSharedPreferenceChangeListener { - - private static final String TAG = LogTag.getLogTag(); - - // Time to delay upsync requests. - public static final long SYNC_DELAY_MILLIS = 30 * DateUtils.SECOND_IN_MILLIS; - - public static String EMAIL_APP_MIME_TYPE; - - // exposed for testing - public static final String DATABASE_NAME = "EmailProvider.db"; - public static final String BODY_DATABASE_NAME = "EmailProviderBody.db"; - - // We don't back up to the backup database anymore, just keep this constant here so we can - // delete the old backups and trigger a new backup to the account manager - @Deprecated - private static final String BACKUP_DATABASE_NAME = "EmailProviderBackup.db"; - private static final String ACCOUNT_MANAGER_JSON_TAG = "accountJson"; - - /** - * Notifies that changes happened. Certain UI components, e.g., widgets, can register for this - * {@link android.content.Intent} and update accordingly. However, this can be very broad and - * is NOT the preferred way of getting notification. - */ - private static final String ACTION_NOTIFY_MESSAGE_LIST_DATASET_CHANGED = - "com.android.email.MESSAGE_LIST_DATASET_CHANGED"; - - private static final String EMAIL_MESSAGE_MIME_TYPE = - "vnd.android.cursor.item/email-message"; - private static final String EMAIL_ATTACHMENT_MIME_TYPE = - "vnd.android.cursor.item/email-attachment"; - - /** Appended to the notification URI for delete operations */ - private static final String NOTIFICATION_OP_DELETE = "delete"; - /** Appended to the notification URI for insert operations */ - private static final String NOTIFICATION_OP_INSERT = "insert"; - /** Appended to the notification URI for update operations */ - private static final String NOTIFICATION_OP_UPDATE = "update"; - - /** The query string to trigger a folder refresh. */ - protected static String QUERY_UIREFRESH = "uirefresh"; - - // Definitions for our queries looking for orphaned messages - private static final String[] ORPHANS_PROJECTION - = new String[] {MessageColumns._ID, MessageColumns.MAILBOX_KEY}; - private static final int ORPHANS_ID = 0; - private static final int ORPHANS_MAILBOX_KEY = 1; - - private static final String WHERE_ID = BaseColumns._ID + "=?"; - - private static final int ACCOUNT_BASE = 0; - private static final int ACCOUNT = ACCOUNT_BASE; - private static final int ACCOUNT_ID = ACCOUNT_BASE + 1; - private static final int ACCOUNT_CHECK = ACCOUNT_BASE + 2; - private static final int ACCOUNT_PICK_TRASH_FOLDER = ACCOUNT_BASE + 3; - private static final int ACCOUNT_PICK_SENT_FOLDER = ACCOUNT_BASE + 4; - - private static final int MAILBOX_BASE = 0x1000; - private static final int MAILBOX = MAILBOX_BASE; - private static final int MAILBOX_ID = MAILBOX_BASE + 1; - private static final int MAILBOX_NOTIFICATION = MAILBOX_BASE + 2; - private static final int MAILBOX_MOST_RECENT_MESSAGE = MAILBOX_BASE + 3; - private static final int MAILBOX_MESSAGE_COUNT = MAILBOX_BASE + 4; - - private static final int MESSAGE_BASE = 0x2000; - private static final int MESSAGE = MESSAGE_BASE; - private static final int MESSAGE_ID = MESSAGE_BASE + 1; - private static final int SYNCED_MESSAGE_ID = MESSAGE_BASE + 2; - private static final int MESSAGE_SELECTION = MESSAGE_BASE + 3; - private static final int MESSAGE_MOVE = MESSAGE_BASE + 4; - private static final int MESSAGE_STATE_CHANGE = MESSAGE_BASE + 5; - - private static final int ATTACHMENT_BASE = 0x3000; - private static final int ATTACHMENT = ATTACHMENT_BASE; - private static final int ATTACHMENT_ID = ATTACHMENT_BASE + 1; - private static final int ATTACHMENTS_MESSAGE_ID = ATTACHMENT_BASE + 2; - private static final int ATTACHMENTS_CACHED_FILE_ACCESS = ATTACHMENT_BASE + 3; - - private static final int HOSTAUTH_BASE = 0x4000; - private static final int HOSTAUTH = HOSTAUTH_BASE; - private static final int HOSTAUTH_ID = HOSTAUTH_BASE + 1; - - private static final int UPDATED_MESSAGE_BASE = 0x5000; - private static final int UPDATED_MESSAGE = UPDATED_MESSAGE_BASE; - private static final int UPDATED_MESSAGE_ID = UPDATED_MESSAGE_BASE + 1; - - private static final int DELETED_MESSAGE_BASE = 0x6000; - private static final int DELETED_MESSAGE = DELETED_MESSAGE_BASE; - private static final int DELETED_MESSAGE_ID = DELETED_MESSAGE_BASE + 1; - - private static final int POLICY_BASE = 0x7000; - private static final int POLICY = POLICY_BASE; - private static final int POLICY_ID = POLICY_BASE + 1; - - private static final int QUICK_RESPONSE_BASE = 0x8000; - private static final int QUICK_RESPONSE = QUICK_RESPONSE_BASE; - private static final int QUICK_RESPONSE_ID = QUICK_RESPONSE_BASE + 1; - private static final int QUICK_RESPONSE_ACCOUNT_ID = QUICK_RESPONSE_BASE + 2; - - private static final int UI_BASE = 0x9000; - private static final int UI_FOLDERS = UI_BASE; - private static final int UI_SUBFOLDERS = UI_BASE + 1; - private static final int UI_MESSAGES = UI_BASE + 2; - private static final int UI_MESSAGE = UI_BASE + 3; - private static final int UI_UNDO = UI_BASE + 4; - private static final int UI_FOLDER_REFRESH = UI_BASE + 5; - private static final int UI_FOLDER = UI_BASE + 6; - private static final int UI_ACCOUNT = UI_BASE + 7; - private static final int UI_ACCTS = UI_BASE + 8; - private static final int UI_ATTACHMENTS = UI_BASE + 9; - private static final int UI_ATTACHMENT = UI_BASE + 10; - private static final int UI_ATTACHMENT_BY_CID = UI_BASE + 11; - private static final int UI_SEARCH = UI_BASE + 12; - private static final int UI_ACCOUNT_DATA = UI_BASE + 13; - private static final int UI_FOLDER_LOAD_MORE = UI_BASE + 14; - private static final int UI_CONVERSATION = UI_BASE + 15; - private static final int UI_RECENT_FOLDERS = UI_BASE + 16; - private static final int UI_DEFAULT_RECENT_FOLDERS = UI_BASE + 17; - private static final int UI_FULL_FOLDERS = UI_BASE + 18; - private static final int UI_ALL_FOLDERS = UI_BASE + 19; - private static final int UI_PURGE_FOLDER = UI_BASE + 20; - private static final int UI_INBOX = UI_BASE + 21; - private static final int UI_ACCTSETTINGS = UI_BASE + 22; - - private static final int BODY_BASE = 0xA000; - private static final int BODY = BODY_BASE; - private static final int BODY_ID = BODY_BASE + 1; - private static final int BODY_HTML = BODY_BASE + 2; - private static final int BODY_TEXT = BODY_BASE + 3; - - private static final int CREDENTIAL_BASE = 0xB000; - private static final int CREDENTIAL = CREDENTIAL_BASE; - private static final int CREDENTIAL_ID = CREDENTIAL_BASE + 1; - - private static final int BASE_SHIFT = 12; // 12 bits to the base type: 0, 0x1000, 0x2000, etc. - - private static final SparseArray TABLE_NAMES; - static { - SparseArray array = new SparseArray(11); - array.put(ACCOUNT_BASE >> BASE_SHIFT, Account.TABLE_NAME); - array.put(MAILBOX_BASE >> BASE_SHIFT, Mailbox.TABLE_NAME); - array.put(MESSAGE_BASE >> BASE_SHIFT, Message.TABLE_NAME); - array.put(ATTACHMENT_BASE >> BASE_SHIFT, Attachment.TABLE_NAME); - array.put(HOSTAUTH_BASE >> BASE_SHIFT, HostAuth.TABLE_NAME); - array.put(UPDATED_MESSAGE_BASE >> BASE_SHIFT, Message.UPDATED_TABLE_NAME); - array.put(DELETED_MESSAGE_BASE >> BASE_SHIFT, Message.DELETED_TABLE_NAME); - array.put(POLICY_BASE >> BASE_SHIFT, Policy.TABLE_NAME); - array.put(QUICK_RESPONSE_BASE >> BASE_SHIFT, QuickResponse.TABLE_NAME); - array.put(UI_BASE >> BASE_SHIFT, null); - array.put(BODY_BASE >> BASE_SHIFT, Body.TABLE_NAME); - array.put(CREDENTIAL_BASE >> BASE_SHIFT, Credential.TABLE_NAME); - TABLE_NAMES = array; - } - - private static final UriMatcher sURIMatcher = new UriMatcher(UriMatcher.NO_MATCH); - - /** - * Functions which manipulate the database connection or files synchronize on this. - * It's static because there can be multiple provider objects. - * TODO: Do we actually need to synchronize across all DB access, not just connection creation? - */ - private static final Object sDatabaseLock = new Object(); - - /** - * Let's only generate these SQL strings once, as they are used frequently - * Note that this isn't relevant for table creation strings, since they are used only once - */ - private static final String UPDATED_MESSAGE_INSERT = "insert or ignore into " + - Message.UPDATED_TABLE_NAME + " select * from " + Message.TABLE_NAME + " where " + - BaseColumns._ID + '='; - - private static final String UPDATED_MESSAGE_DELETE = "delete from " + - Message.UPDATED_TABLE_NAME + " where " + BaseColumns._ID + '='; - - private static final String DELETED_MESSAGE_INSERT = "insert or replace into " + - Message.DELETED_TABLE_NAME + " select * from " + Message.TABLE_NAME + " where " + - BaseColumns._ID + '='; - - private static final String ORPHAN_BODY_MESSAGE_ID_SELECT = - "select " + BodyColumns.MESSAGE_KEY + " from " + Body.TABLE_NAME + - " except select " + BaseColumns._ID + " from " + Message.TABLE_NAME; - - private static final String DELETE_ORPHAN_BODIES = "delete from " + Body.TABLE_NAME + - " where " + BodyColumns.MESSAGE_KEY + " in " + '(' + ORPHAN_BODY_MESSAGE_ID_SELECT + ')'; - - private static final String DELETE_BODY = "delete from " + Body.TABLE_NAME + - " where " + BodyColumns.MESSAGE_KEY + '='; - - private static final ContentValues EMPTY_CONTENT_VALUES = new ContentValues(); - - private static final String MESSAGE_URI_PARAMETER_MAILBOX_ID = "mailboxId"; - - // For undo handling - private int mLastSequence = -1; - private final ArrayList mLastSequenceOps = - new ArrayList(); - - // Query parameter indicating the command came from UIProvider - private static final String IS_UIPROVIDER = "is_uiprovider"; - - private static final String SYNC_STATUS_CALLBACK_METHOD = "sync_status"; - - /** - * Wrap the UriMatcher call so we can throw a runtime exception if an unknown Uri is passed in - * @param uri the Uri to match - * @return the match value - */ - private static int findMatch(Uri uri, String methodName) { - int match = sURIMatcher.match(uri); - if (match < 0) { - throw new IllegalArgumentException("Unknown uri: " + uri); - } else if (Logging.LOGD) { - LogUtils.v(TAG, methodName + ": uri=" + uri + ", match is " + match); - } - return match; - } - - // exposed for testing - public static Uri INTEGRITY_CHECK_URI; - - public static Uri ACCOUNT_BACKUP_URI; - private static Uri FOLDER_STATUS_URI; - - private SQLiteDatabase mDatabase; - private SQLiteDatabase mBodyDatabase; - - private Handler mDelayedSyncHandler; - private final Set mDelayedSyncRequests = new HashSet(); - - private static void reconcileAccountsAsync(final Context context) { - if (context.getResources().getBoolean(R.bool.reconcile_accounts)) { - EmailAsyncTask.runAsyncParallel(new Runnable() { - @Override - public void run() { - AccountReconciler.reconcileAccounts(context); - } - }); - } - } - - public static Uri uiUri(String type, long id) { - return Uri.parse(uiUriString(type, id)); - } - - /** - * Creates a URI string from a database ID (guaranteed to be unique). - * @param type of the resource: uifolder, message, etc. - * @param id the id of the resource. - * @return uri string - */ - public static String uiUriString(String type, long id) { - return "content://" + EmailContent.AUTHORITY + "/" + type + ((id == -1) ? "" : ("/" + id)); - } - - /** - * Orphan record deletion utility. Generates a sqlite statement like: - * delete from where not in (select from ) - * Exposed for testing. - * @param db the EmailProvider database - * @param table the table whose orphans are to be removed - * @param column the column deletion will be based on - * @param foreignColumn the column in the foreign table whose absence will trigger the deletion - * @param foreignTable the foreign table - */ - public static void deleteUnlinked(SQLiteDatabase db, String table, String column, - String foreignColumn, String foreignTable) { - int count = db.delete(table, column + " not in (select " + foreignColumn + " from " + - foreignTable + ")", null); - if (count > 0) { - LogUtils.w(TAG, "Found " + count + " orphaned row(s) in " + table); - } - } - - - /** - * Make sure that parentKeys match with parentServerId. - * When we sync folders, we do two passes: First to create the mailbox rows, and second - * to set the parentKeys. Two passes are needed because we won't know the parent's Id - * until that row is inserted, and the order in which the rows are given is arbitrary. - * If we crash while this operation is in progress, the parent keys can be left uninitialized. - * @param db SQLiteDatabase to modify - */ - private void fixParentKeys(SQLiteDatabase db) { - LogUtils.d(TAG, "Fixing parent keys"); - - // Update the parentKey for each mailbox row to match the _id of the row whose - // serverId matches our parentServerId. This will leave parentKey blank for any - // row that does not have a parentServerId - - // This is kind of a confusing sql statement, so here's the actual text of it, - // for reference: - // - // update mailbox set parentKey = (select _id from mailbox as b where - // mailbox.parentServerId=b.serverId and mailbox.parentServerId not null and - // mailbox.accountKey=b.accountKey) - db.execSQL("update " + Mailbox.TABLE_NAME + " set " + MailboxColumns.PARENT_KEY + "=" - + "(select " + Mailbox._ID + " from " + Mailbox.TABLE_NAME + " as b where " - + Mailbox.TABLE_NAME + "." + MailboxColumns.PARENT_SERVER_ID + "=" - + "b." + MailboxColumns.SERVER_ID + " and " - + Mailbox.TABLE_NAME + "." + MailboxColumns.PARENT_SERVER_ID + " not null and " - + Mailbox.TABLE_NAME + "." + MailboxColumns.ACCOUNT_KEY - + "=b." + Mailbox.ACCOUNT_KEY + ")"); - - // Top level folders can still have uninitialized parent keys. Update these - // to indicate that the parent is -1. - // - // update mailbox set parentKey = -1 where parentKey=0 or parentKey is null; - db.execSQL("update " + Mailbox.TABLE_NAME + " set " + MailboxColumns.PARENT_KEY - + "=" + Mailbox.NO_MAILBOX + " where " + MailboxColumns.PARENT_KEY - + "=" + Mailbox.PARENT_KEY_UNINITIALIZED + " or " + MailboxColumns.PARENT_KEY - + " is null"); - - } - - // exposed for testing - public SQLiteDatabase getDatabase(Context context) { - synchronized (sDatabaseLock) { - // Always return the cached database, if we've got one - if (mDatabase != null) { - return mDatabase; - } - - // Whenever we create or re-cache the databases, make sure that we haven't lost one - // to corruption - checkDatabases(); - - DBHelper.DatabaseHelper helper = new DBHelper.DatabaseHelper(context, DATABASE_NAME); - mDatabase = helper.getWritableDatabase(); - DBHelper.BodyDatabaseHelper bodyHelper = - new DBHelper.BodyDatabaseHelper(context, BODY_DATABASE_NAME); - mBodyDatabase = bodyHelper.getWritableDatabase(); - if (mBodyDatabase != null) { - String bodyFileName = mBodyDatabase.getPath(); - mDatabase.execSQL("attach \"" + bodyFileName + "\" as BodyDatabase"); - } - - // Restore accounts if the database is corrupted... - restoreIfNeeded(context, mDatabase); - // Check for any orphaned Messages in the updated/deleted tables - deleteMessageOrphans(mDatabase, Message.UPDATED_TABLE_NAME); - deleteMessageOrphans(mDatabase, Message.DELETED_TABLE_NAME); - // Delete orphaned mailboxes/messages/policies (account no longer exists) - deleteUnlinked(mDatabase, Mailbox.TABLE_NAME, MailboxColumns.ACCOUNT_KEY, - AccountColumns._ID, Account.TABLE_NAME); - deleteUnlinked(mDatabase, Message.TABLE_NAME, MessageColumns.ACCOUNT_KEY, - AccountColumns._ID, Account.TABLE_NAME); - deleteUnlinked(mDatabase, Policy.TABLE_NAME, PolicyColumns._ID, - AccountColumns.POLICY_KEY, Account.TABLE_NAME); - fixParentKeys(mDatabase); - initUiProvider(); - return mDatabase; - } - } - - /** - * Perform startup actions related to UI - */ - private void initUiProvider() { - // Clear mailbox sync status - mDatabase.execSQL("update " + Mailbox.TABLE_NAME + " set " + MailboxColumns.UI_SYNC_STATUS + - "=" + UIProvider.SyncStatus.NO_SYNC); - } - - /** - * Restore user Account and HostAuth data from our backup database - */ - private static void restoreIfNeeded(Context context, SQLiteDatabase mainDatabase) { - if (DebugUtils.DEBUG) { - LogUtils.w(TAG, "restoreIfNeeded..."); - } - // Check for legacy backup - String legacyBackup = Preferences.getLegacyBackupPreference(context); - // If there's a legacy backup, create a new-style backup and delete the legacy backup - // In the 1:1000000000 chance that the user gets an app update just as his database becomes - // corrupt, oh well... - if (!TextUtils.isEmpty(legacyBackup)) { - backupAccounts(context, mainDatabase); - Preferences.clearLegacyBackupPreference(context); - LogUtils.w(TAG, "Created new EmailProvider backup database"); - return; - } - - // If there's a backup database (old style) delete it and trigger an account manager backup. - // Roughly the same comment as above applies - final File backupDb = context.getDatabasePath(BACKUP_DATABASE_NAME); - if (backupDb.exists()) { - backupAccounts(context, mainDatabase); - context.deleteDatabase(BACKUP_DATABASE_NAME); - LogUtils.w(TAG, "Migrated from backup database to account manager"); - return; - } - - // If we have accounts, we're done - if (DatabaseUtils.longForQuery(mainDatabase, - "SELECT EXISTS (SELECT ? FROM " + Account.TABLE_NAME + " )", - EmailContent.ID_PROJECTION) > 0) { - if (DebugUtils.DEBUG) { - LogUtils.w(TAG, "restoreIfNeeded: Account exists."); - } - return; - } - - restoreAccounts(context); - } - - /** {@inheritDoc} */ - @Override - public void shutdown() { - if (mDatabase != null) { - mDatabase.close(); - mDatabase = null; - } - if (mBodyDatabase != null) { - mBodyDatabase.close(); - mBodyDatabase = null; - } - } - - // exposed for testing - public static void deleteMessageOrphans(SQLiteDatabase database, String tableName) { - if (database != null) { - // We'll look at all of the items in the table; there won't be many typically - Cursor c = database.query(tableName, ORPHANS_PROJECTION, null, null, null, null, null); - // Usually, there will be nothing in these tables, so make a quick check - try { - if (c.getCount() == 0) return; - ArrayList foundMailboxes = new ArrayList(); - ArrayList notFoundMailboxes = new ArrayList(); - ArrayList deleteList = new ArrayList(); - String[] bindArray = new String[1]; - while (c.moveToNext()) { - // Get the mailbox key and see if we've already found this mailbox - // If so, we're fine - long mailboxId = c.getLong(ORPHANS_MAILBOX_KEY); - // If we already know this mailbox doesn't exist, mark the message for deletion - if (notFoundMailboxes.contains(mailboxId)) { - deleteList.add(c.getLong(ORPHANS_ID)); - // If we don't know about this mailbox, we'll try to find it - } else if (!foundMailboxes.contains(mailboxId)) { - bindArray[0] = Long.toString(mailboxId); - Cursor boxCursor = database.query(Mailbox.TABLE_NAME, - Mailbox.ID_PROJECTION, WHERE_ID, bindArray, null, null, null); - try { - // If it exists, we'll add it to the "found" mailboxes - if (boxCursor.moveToFirst()) { - foundMailboxes.add(mailboxId); - // Otherwise, we'll add to "not found" and mark the message for deletion - } else { - notFoundMailboxes.add(mailboxId); - deleteList.add(c.getLong(ORPHANS_ID)); - } - } finally { - boxCursor.close(); - } - } - } - // Now, delete the orphan messages - for (long messageId: deleteList) { - bindArray[0] = Long.toString(messageId); - database.delete(tableName, WHERE_ID, bindArray); - } - } finally { - c.close(); - } - } - } - - @Override - public int delete(Uri uri, String selection, String[] selectionArgs) { - Log.d(TAG, "Delete: " + uri); - final int match = findMatch(uri, "delete"); - final Context context = getContext(); - // Pick the correct database for this operation - // If we're in a transaction already (which would happen during applyBatch), then the - // body database is already attached to the email database and any attempt to use the - // body database directly will result in a SQLiteException (the database is locked) - final SQLiteDatabase db = getDatabase(context); - final int table = match >> BASE_SHIFT; - String id = "0"; - boolean messageDeletion = false; - - final String tableName = TABLE_NAMES.valueAt(table); - int result = -1; - - try { - if (match == MESSAGE_ID || match == SYNCED_MESSAGE_ID) { - if (!uri.getBooleanQueryParameter(IS_UIPROVIDER, false)) { - notifyUIConversation(uri); - } - } - switch (match) { - case UI_MESSAGE: - return uiDeleteMessage(uri); - case UI_ACCOUNT_DATA: - return uiDeleteAccountData(uri); - case UI_ACCOUNT: - return uiDeleteAccount(uri); - case UI_PURGE_FOLDER: - return uiPurgeFolder(uri); - case MESSAGE_SELECTION: - Cursor findCursor = db.query(tableName, Message.ID_COLUMN_PROJECTION, selection, - selectionArgs, null, null, null); - try { - if (findCursor.moveToFirst()) { - return delete(ContentUris.withAppendedId( - Message.CONTENT_URI, - findCursor.getLong(Message.ID_COLUMNS_ID_COLUMN)), - null, null); - } else { - return 0; - } - } finally { - findCursor.close(); - } - // These are cases in which one or more Messages might get deleted, either by - // cascade or explicitly - case MAILBOX_ID: - case MAILBOX: - case ACCOUNT_ID: - case ACCOUNT: - case MESSAGE: - case SYNCED_MESSAGE_ID: - case MESSAGE_ID: - // Handle lost Body records here, since this cannot be done in a trigger - // The process is: - // 1) Begin a transaction, ensuring that both databases are affected atomically - // 2) Do the requested deletion, with cascading deletions handled in triggers - // 3) End the transaction, committing all changes atomically - // - // Bodies are auto-deleted here; Attachments are auto-deleted via trigger - messageDeletion = true; - db.beginTransaction(); - break; - } - switch (match) { - case BODY_ID: - case DELETED_MESSAGE_ID: - case SYNCED_MESSAGE_ID: - case MESSAGE_ID: - case UPDATED_MESSAGE_ID: - case ATTACHMENT_ID: - case MAILBOX_ID: - case ACCOUNT_ID: - case HOSTAUTH_ID: - case POLICY_ID: - case QUICK_RESPONSE_ID: - case CREDENTIAL_ID: - id = uri.getPathSegments().get(1); - if (match == SYNCED_MESSAGE_ID) { - // For synced messages, first copy the old message to the deleted table and - // delete it from the updated table (in case it was updated first) - // Note that this is all within a transaction, for atomicity - db.execSQL(DELETED_MESSAGE_INSERT + id); - db.execSQL(UPDATED_MESSAGE_DELETE + id); - } - - final long accountId; - if (match == MAILBOX_ID) { - accountId = Mailbox.getAccountIdForMailbox(context, id); - } else { - accountId = Account.NO_ACCOUNT; - } - - result = db.delete(tableName, whereWithId(id, selection), selectionArgs); - - if (match == ACCOUNT_ID) { - notifyUI(UIPROVIDER_ACCOUNT_NOTIFIER, id); - notifyUI(UIPROVIDER_ALL_ACCOUNTS_NOTIFIER, null); - } else if (match == MAILBOX_ID) { - notifyUIFolder(id, accountId); - } else if (match == ATTACHMENT_ID) { - notifyUI(UIPROVIDER_ATTACHMENT_NOTIFIER, id); - } - break; - case ATTACHMENTS_MESSAGE_ID: - // All attachments for the given message - id = uri.getPathSegments().get(2); - result = db.delete(tableName, - whereWith(AttachmentColumns.MESSAGE_KEY + "=" + id, selection), - selectionArgs); - break; - - case BODY: - case MESSAGE: - case DELETED_MESSAGE: - case UPDATED_MESSAGE: - case ATTACHMENT: - case MAILBOX: - case ACCOUNT: - case HOSTAUTH: - case POLICY: - result = db.delete(tableName, selection, selectionArgs); - break; - case MESSAGE_MOVE: - db.delete(MessageMove.TABLE_NAME, selection, selectionArgs); - break; - case MESSAGE_STATE_CHANGE: - db.delete(MessageStateChange.TABLE_NAME, selection, selectionArgs); - break; - default: - throw new IllegalArgumentException("Unknown URI " + uri); - } - if (messageDeletion) { - if (match == MESSAGE_ID) { - // Delete the Body record associated with the deleted message - final ContentValues emptyValues = new ContentValues(2); - emptyValues.putNull(BodyColumns.HTML_CONTENT); - emptyValues.putNull(BodyColumns.TEXT_CONTENT); - final long messageId = Long.valueOf(id); - try { - writeBodyFiles(context, messageId, emptyValues); - } catch (final IllegalStateException e) { - LogUtils.v(LogUtils.TAG, e, "Exception while deleting bodies"); - } - db.execSQL(DELETE_BODY + id); - } else { - // Delete any orphaned Body records - final Cursor orphans = db.rawQuery(ORPHAN_BODY_MESSAGE_ID_SELECT, null); - try { - final ContentValues emptyValues = new ContentValues(2); - emptyValues.putNull(BodyColumns.HTML_CONTENT); - emptyValues.putNull(BodyColumns.TEXT_CONTENT); - while (orphans.moveToNext()) { - final long messageId = orphans.getLong(0); - try { - writeBodyFiles(context, messageId, emptyValues); - } catch (final IllegalStateException e) { - LogUtils.v(LogUtils.TAG, e, "Exception while deleting bodies"); - } - } - } finally { - orphans.close(); - } - db.execSQL(DELETE_ORPHAN_BODIES); - } - db.setTransactionSuccessful(); - } - } catch (SQLiteException e) { - checkDatabases(); - throw e; - } finally { - if (messageDeletion) { - db.endTransaction(); - } - } - - // Notify all notifier cursors - sendNotifierChange(getBaseNotificationUri(match), NOTIFICATION_OP_DELETE, id); - - // Notify all email content cursors - notifyUI(EmailContent.CONTENT_URI, null); - return result; - } - - @Override - // Use the email- prefix because message, mailbox, and account are so generic (e.g. SMS, IM) - public String getType(Uri uri) { - int match = findMatch(uri, "getType"); - switch (match) { - case BODY_ID: - return "vnd.android.cursor.item/email-body"; - case BODY: - return "vnd.android.cursor.dir/email-body"; - case UPDATED_MESSAGE_ID: - case MESSAGE_ID: - // NOTE: According to the framework folks, we're supposed to invent mime types as - // a way of passing information to drag & drop recipients. - // If there's a mailboxId parameter in the url, we respond with a mime type that - // has -n appended, where n is the mailboxId of the message. The drag & drop code - // uses this information to know not to allow dragging the item to its own mailbox - String mimeType = EMAIL_MESSAGE_MIME_TYPE; - String mailboxId = uri.getQueryParameter(MESSAGE_URI_PARAMETER_MAILBOX_ID); - if (mailboxId != null) { - mimeType += "-" + mailboxId; - } - return mimeType; - case UPDATED_MESSAGE: - case MESSAGE: - return "vnd.android.cursor.dir/email-message"; - case MAILBOX: - return "vnd.android.cursor.dir/email-mailbox"; - case MAILBOX_ID: - return "vnd.android.cursor.item/email-mailbox"; - case ACCOUNT: - return "vnd.android.cursor.dir/email-account"; - case ACCOUNT_ID: - return "vnd.android.cursor.item/email-account"; - case ATTACHMENTS_MESSAGE_ID: - case ATTACHMENT: - return "vnd.android.cursor.dir/email-attachment"; - case ATTACHMENT_ID: - return EMAIL_ATTACHMENT_MIME_TYPE; - case HOSTAUTH: - return "vnd.android.cursor.dir/email-hostauth"; - case HOSTAUTH_ID: - return "vnd.android.cursor.item/email-hostauth"; - default: - return null; - } - } - - // These URIs are used for specific UI notifications. We don't use EmailContent.CONTENT_URI - // as the base because that gets spammed. - // These can't be statically initialized because they depend on EmailContent.AUTHORITY - private static Uri UIPROVIDER_CONVERSATION_NOTIFIER; - private static Uri UIPROVIDER_FOLDER_NOTIFIER; - private static Uri UIPROVIDER_FOLDERLIST_NOTIFIER; - private static Uri UIPROVIDER_ACCOUNT_NOTIFIER; - // Not currently used - //public static Uri UIPROVIDER_SETTINGS_NOTIFIER; - private static Uri UIPROVIDER_ATTACHMENT_NOTIFIER; - private static Uri UIPROVIDER_ATTACHMENTS_NOTIFIER; - private static Uri UIPROVIDER_ALL_ACCOUNTS_NOTIFIER; - private static Uri UIPROVIDER_MESSAGE_NOTIFIER; - private static Uri UIPROVIDER_RECENT_FOLDERS_NOTIFIER; - - @Override - public Uri insert(Uri uri, ContentValues values) { - Log.d(TAG, "Insert: " + uri); - final int match = findMatch(uri, "insert"); - final Context context = getContext(); - - // See the comment at delete(), above - final SQLiteDatabase db = getDatabase(context); - final int table = match >> BASE_SHIFT; - String id = "0"; - long longId; - - // We do NOT allow setting of unreadCount/messageCount via the provider - // These columns are maintained via triggers - if (match == MAILBOX_ID || match == MAILBOX) { - values.put(MailboxColumns.UNREAD_COUNT, 0); - values.put(MailboxColumns.MESSAGE_COUNT, 0); - } - - final Uri resultUri; - - try { - switch (match) { - case BODY: - final ContentValues dbValues = new ContentValues(values); - // Prune out the content we don't want in the DB - dbValues.remove(BodyColumns.HTML_CONTENT); - dbValues.remove(BodyColumns.TEXT_CONTENT); - // TODO: move this to the message table - longId = db.insert(Body.TABLE_NAME, "foo", dbValues); - resultUri = ContentUris.withAppendedId(uri, longId); - // Write content to the filesystem where appropriate - // This will look less ugly once the body table is folded into the message table - // and we can just use longId instead - if (!values.containsKey(BodyColumns.MESSAGE_KEY)) { - throw new IllegalArgumentException( - "Cannot insert body without MESSAGE_KEY"); - } - final long messageId = values.getAsLong(BodyColumns.MESSAGE_KEY); - writeBodyFiles(getContext(), messageId, values); - break; - // NOTE: It is NOT legal for production code to insert directly into UPDATED_MESSAGE - // or DELETED_MESSAGE; see the comment below for details - case UPDATED_MESSAGE: - case DELETED_MESSAGE: - case MESSAGE: - decodeEmailAddresses(values); - case ATTACHMENT: - case MAILBOX: - case ACCOUNT: - case HOSTAUTH: - case CREDENTIAL: - case POLICY: - case QUICK_RESPONSE: - longId = db.insert(TABLE_NAMES.valueAt(table), "foo", values); - resultUri = ContentUris.withAppendedId(uri, longId); - switch(match) { - case MESSAGE: - final long mailboxId = values.getAsLong(MessageColumns.MAILBOX_KEY); - if (!uri.getBooleanQueryParameter(IS_UIPROVIDER, false)) { - notifyUIConversationMailbox(mailboxId); - } - notifyUIFolder(mailboxId, values.getAsLong(MessageColumns.ACCOUNT_KEY)); - break; - case MAILBOX: - if (values.containsKey(MailboxColumns.TYPE)) { - if (values.getAsInteger(MailboxColumns.TYPE) < - Mailbox.TYPE_NOT_EMAIL) { - // Notify the account when a new mailbox is added - final Long accountId = - values.getAsLong(MailboxColumns.ACCOUNT_KEY); - if (accountId != null && accountId > 0) { - notifyUI(UIPROVIDER_ACCOUNT_NOTIFIER, accountId); - notifyUI(UIPROVIDER_FOLDERLIST_NOTIFIER, accountId); - } - } - } - break; - case ACCOUNT: - updateAccountSyncInterval(longId, values); - if (!uri.getBooleanQueryParameter(IS_UIPROVIDER, false)) { - notifyUIAccount(longId); - } - notifyUI(UIPROVIDER_ALL_ACCOUNTS_NOTIFIER, null); - break; - case UPDATED_MESSAGE: - case DELETED_MESSAGE: - throw new IllegalArgumentException("Unknown URL " + uri); - case ATTACHMENT: - int flags = 0; - if (values.containsKey(AttachmentColumns.FLAGS)) { - flags = values.getAsInteger(AttachmentColumns.FLAGS); - } - // Report all new attachments to the download service - if (TextUtils.isEmpty(values.getAsString(AttachmentColumns.LOCATION))) { - LogUtils.w(TAG, new Throwable(), "attachment with blank location"); - } - mAttachmentService.attachmentChanged(getContext(), longId, flags); - break; - } - break; - case QUICK_RESPONSE_ACCOUNT_ID: - longId = Long.parseLong(uri.getPathSegments().get(2)); - values.put(QuickResponseColumns.ACCOUNT_KEY, longId); - return insert(QuickResponse.CONTENT_URI, values); - case MAILBOX_ID: - // This implies adding a message to a mailbox - // Hmm, a problem here is that we can't link the account as well, so it must be - // already in the values... - longId = Long.parseLong(uri.getPathSegments().get(1)); - values.put(MessageColumns.MAILBOX_KEY, longId); - return insert(Message.CONTENT_URI, values); // Recurse - case MESSAGE_ID: - // This implies adding an attachment to a message. - id = uri.getPathSegments().get(1); - longId = Long.parseLong(id); - values.put(AttachmentColumns.MESSAGE_KEY, longId); - return insert(Attachment.CONTENT_URI, values); // Recurse - case ACCOUNT_ID: - // This implies adding a mailbox to an account. - longId = Long.parseLong(uri.getPathSegments().get(1)); - values.put(MailboxColumns.ACCOUNT_KEY, longId); - return insert(Mailbox.CONTENT_URI, values); // Recurse - case ATTACHMENTS_MESSAGE_ID: - longId = db.insert(TABLE_NAMES.valueAt(table), "foo", values); - resultUri = ContentUris.withAppendedId(Attachment.CONTENT_URI, longId); - break; - default: - throw new IllegalArgumentException("Unknown URL " + uri); - } - } catch (SQLiteException e) { - checkDatabases(); - throw e; - } - - // Notify all notifier cursors - sendNotifierChange(getBaseNotificationUri(match), NOTIFICATION_OP_INSERT, id); - - // Notify all existing cursors. - notifyUI(EmailContent.CONTENT_URI, null); - return resultUri; - } - - @Override - public boolean onCreate() { - Context context = getContext(); - EmailContent.init(context); - init(context); - DebugUtils.init(context); - // Do this last, so that EmailContent/EmailProvider are initialized - MailActivityEmail.setServicesEnabledAsync(context); - reconcileAccountsAsync(context); - - // Update widgets - final Intent updateAllWidgetsIntent = - new Intent(com.android.mail.utils.Utils.ACTION_NOTIFY_DATASET_CHANGED); - updateAllWidgetsIntent.putExtra(BaseWidgetProvider.EXTRA_UPDATE_ALL_WIDGETS, true); - updateAllWidgetsIntent.setType(context.getString(R.string.application_mime_type)); - context.sendBroadcast(updateAllWidgetsIntent); - - // The combined account name changes on locale changes - final Configuration oldConfiguration = - new Configuration(context.getResources().getConfiguration()); - context.registerComponentCallbacks(new ComponentCallbacks() { - @Override - public void onConfigurationChanged(Configuration configuration) { - int delta = oldConfiguration.updateFrom(configuration); - if (Configuration.needNewResources(delta, ActivityInfo.CONFIG_LOCALE)) { - notifyUIAccount(COMBINED_ACCOUNT_ID); - } - } - - @Override - public void onLowMemory() {} - }); - - MailPrefs.get(context).registerOnSharedPreferenceChangeListener(this); - - return false; - } - - private static void init(final Context context) { - // Synchronize on the matcher rather than the class object to minimize risk of contention - // & deadlock. - synchronized (sURIMatcher) { - // We use the existence of this variable as indicative of whether this function has - // already run. - if (INTEGRITY_CHECK_URI != null) { - return; - } - INTEGRITY_CHECK_URI = Uri.parse("content://" + EmailContent.AUTHORITY + - "/integrityCheck"); - ACCOUNT_BACKUP_URI = - Uri.parse("content://" + EmailContent.AUTHORITY + "/accountBackup"); - FOLDER_STATUS_URI = - Uri.parse("content://" + EmailContent.AUTHORITY + "/status"); - EMAIL_APP_MIME_TYPE = context.getString(R.string.application_mime_type); - - final String uiNotificationAuthority = - EmailContent.EMAIL_PACKAGE_NAME + ".uinotifications"; - UIPROVIDER_CONVERSATION_NOTIFIER = - Uri.parse("content://" + uiNotificationAuthority + "/uimessages"); - UIPROVIDER_FOLDER_NOTIFIER = - Uri.parse("content://" + uiNotificationAuthority + "/uifolder"); - UIPROVIDER_FOLDERLIST_NOTIFIER = - Uri.parse("content://" + uiNotificationAuthority + "/uifolders"); - UIPROVIDER_ACCOUNT_NOTIFIER = - Uri.parse("content://" + uiNotificationAuthority + "/uiaccount"); - // Not currently used - /* UIPROVIDER_SETTINGS_NOTIFIER = - Uri.parse("content://" + uiNotificationAuthority + "/uisettings");*/ - UIPROVIDER_ATTACHMENT_NOTIFIER = - Uri.parse("content://" + uiNotificationAuthority + "/uiattachment"); - UIPROVIDER_ATTACHMENTS_NOTIFIER = - Uri.parse("content://" + uiNotificationAuthority + "/uiattachments"); - UIPROVIDER_ALL_ACCOUNTS_NOTIFIER = - Uri.parse("content://" + uiNotificationAuthority + "/uiaccts"); - UIPROVIDER_MESSAGE_NOTIFIER = - Uri.parse("content://" + uiNotificationAuthority + "/uimessage"); - UIPROVIDER_RECENT_FOLDERS_NOTIFIER = - Uri.parse("content://" + uiNotificationAuthority + "/uirecentfolders"); - - // All accounts - sURIMatcher.addURI(EmailContent.AUTHORITY, "account", ACCOUNT); - // A specific account - // insert into this URI causes a mailbox to be added to the account - sURIMatcher.addURI(EmailContent.AUTHORITY, "account/#", ACCOUNT_ID); - sURIMatcher.addURI(EmailContent.AUTHORITY, "accountCheck/#", ACCOUNT_CHECK); - - // All mailboxes - sURIMatcher.addURI(EmailContent.AUTHORITY, "mailbox", MAILBOX); - // A specific mailbox - // insert into this URI causes a message to be added to the mailbox - // ** NOTE For now, the accountKey must be set manually in the values! - sURIMatcher.addURI(EmailContent.AUTHORITY, "mailbox/*", MAILBOX_ID); - sURIMatcher.addURI(EmailContent.AUTHORITY, "mailboxNotification/#", - MAILBOX_NOTIFICATION); - sURIMatcher.addURI(EmailContent.AUTHORITY, "mailboxMostRecentMessage/#", - MAILBOX_MOST_RECENT_MESSAGE); - sURIMatcher.addURI(EmailContent.AUTHORITY, "mailboxCount/#", MAILBOX_MESSAGE_COUNT); - - // All messages - sURIMatcher.addURI(EmailContent.AUTHORITY, "message", MESSAGE); - // A specific message - // insert into this URI causes an attachment to be added to the message - sURIMatcher.addURI(EmailContent.AUTHORITY, "message/#", MESSAGE_ID); - - // A specific attachment - sURIMatcher.addURI(EmailContent.AUTHORITY, "attachment", ATTACHMENT); - // A specific attachment (the header information) - sURIMatcher.addURI(EmailContent.AUTHORITY, "attachment/#", ATTACHMENT_ID); - // The attachments of a specific message (query only) (insert & delete TBD) - sURIMatcher.addURI(EmailContent.AUTHORITY, "attachment/message/#", - ATTACHMENTS_MESSAGE_ID); - sURIMatcher.addURI(EmailContent.AUTHORITY, "attachment/cachedFile", - ATTACHMENTS_CACHED_FILE_ACCESS); - - // All mail bodies - sURIMatcher.addURI(EmailContent.AUTHORITY, "body", BODY); - // A specific mail body - sURIMatcher.addURI(EmailContent.AUTHORITY, "body/#", BODY_ID); - // A specific HTML body part, for openFile - sURIMatcher.addURI(EmailContent.AUTHORITY, "bodyHtml/#", BODY_HTML); - // A specific text body part, for openFile - sURIMatcher.addURI(EmailContent.AUTHORITY, "bodyText/#", BODY_TEXT); - - // All hostauth records - sURIMatcher.addURI(EmailContent.AUTHORITY, "hostauth", HOSTAUTH); - // A specific hostauth - sURIMatcher.addURI(EmailContent.AUTHORITY, "hostauth/*", HOSTAUTH_ID); - - // All credential records - sURIMatcher.addURI(EmailContent.AUTHORITY, "credential", CREDENTIAL); - // A specific credential - sURIMatcher.addURI(EmailContent.AUTHORITY, "credential/*", CREDENTIAL_ID); - - /** - * THIS URI HAS SPECIAL SEMANTICS - * ITS USE IS INTENDED FOR THE UI TO MARK CHANGES THAT NEED TO BE SYNCED BACK - * TO A SERVER VIA A SYNC ADAPTER - */ - sURIMatcher.addURI(EmailContent.AUTHORITY, "syncedMessage/#", SYNCED_MESSAGE_ID); - sURIMatcher.addURI(EmailContent.AUTHORITY, "messageBySelection", MESSAGE_SELECTION); - - sURIMatcher.addURI(EmailContent.AUTHORITY, MessageMove.PATH, MESSAGE_MOVE); - sURIMatcher.addURI(EmailContent.AUTHORITY, MessageStateChange.PATH, - MESSAGE_STATE_CHANGE); - - /** - * THE URIs BELOW THIS POINT ARE INTENDED TO BE USED BY SYNC ADAPTERS ONLY - * THEY REFER TO DATA CREATED AND MAINTAINED BY CALLS TO THE SYNCED_MESSAGE_ID URI - * BY THE UI APPLICATION - */ - // All deleted messages - sURIMatcher.addURI(EmailContent.AUTHORITY, "deletedMessage", DELETED_MESSAGE); - // A specific deleted message - sURIMatcher.addURI(EmailContent.AUTHORITY, "deletedMessage/#", DELETED_MESSAGE_ID); - - // All updated messages - sURIMatcher.addURI(EmailContent.AUTHORITY, "updatedMessage", UPDATED_MESSAGE); - // A specific updated message - sURIMatcher.addURI(EmailContent.AUTHORITY, "updatedMessage/#", UPDATED_MESSAGE_ID); - - sURIMatcher.addURI(EmailContent.AUTHORITY, "policy", POLICY); - sURIMatcher.addURI(EmailContent.AUTHORITY, "policy/#", POLICY_ID); - - // All quick responses - sURIMatcher.addURI(EmailContent.AUTHORITY, "quickresponse", QUICK_RESPONSE); - // A specific quick response - sURIMatcher.addURI(EmailContent.AUTHORITY, "quickresponse/#", QUICK_RESPONSE_ID); - // All quick responses associated with a particular account id - sURIMatcher.addURI(EmailContent.AUTHORITY, "quickresponse/account/#", - QUICK_RESPONSE_ACCOUNT_ID); - - sURIMatcher.addURI(EmailContent.AUTHORITY, "uifolders/#", UI_FOLDERS); - sURIMatcher.addURI(EmailContent.AUTHORITY, "uifullfolders/#", UI_FULL_FOLDERS); - sURIMatcher.addURI(EmailContent.AUTHORITY, "uiallfolders/#", UI_ALL_FOLDERS); - sURIMatcher.addURI(EmailContent.AUTHORITY, "uisubfolders/#", UI_SUBFOLDERS); - sURIMatcher.addURI(EmailContent.AUTHORITY, "uimessages/#", UI_MESSAGES); - sURIMatcher.addURI(EmailContent.AUTHORITY, "uimessage/#", UI_MESSAGE); - sURIMatcher.addURI(EmailContent.AUTHORITY, "uiundo", UI_UNDO); - sURIMatcher.addURI(EmailContent.AUTHORITY, QUERY_UIREFRESH + "/#", UI_FOLDER_REFRESH); - // We listen to everything trailing uifolder/ since there might be an appVersion - // as in Utils.appendVersionQueryParameter(). - sURIMatcher.addURI(EmailContent.AUTHORITY, "uifolder/*", UI_FOLDER); - sURIMatcher.addURI(EmailContent.AUTHORITY, "uiinbox/#", UI_INBOX); - sURIMatcher.addURI(EmailContent.AUTHORITY, "uiaccount/#", UI_ACCOUNT); - sURIMatcher.addURI(EmailContent.AUTHORITY, "uiaccts", UI_ACCTS); - sURIMatcher.addURI(EmailContent.AUTHORITY, "uiacctsettings", UI_ACCTSETTINGS); - sURIMatcher.addURI(EmailContent.AUTHORITY, "uiattachments/#", UI_ATTACHMENTS); - sURIMatcher.addURI(EmailContent.AUTHORITY, "uiattachment/#", UI_ATTACHMENT); - sURIMatcher.addURI(EmailContent.AUTHORITY, "uiattachmentbycid/#/*", - UI_ATTACHMENT_BY_CID); - sURIMatcher.addURI(EmailContent.AUTHORITY, "uisearch/#", UI_SEARCH); - sURIMatcher.addURI(EmailContent.AUTHORITY, "uiaccountdata/#", UI_ACCOUNT_DATA); - sURIMatcher.addURI(EmailContent.AUTHORITY, "uiloadmore/#", UI_FOLDER_LOAD_MORE); - sURIMatcher.addURI(EmailContent.AUTHORITY, "uiconversation/#", UI_CONVERSATION); - sURIMatcher.addURI(EmailContent.AUTHORITY, "uirecentfolders/#", UI_RECENT_FOLDERS); - sURIMatcher.addURI(EmailContent.AUTHORITY, "uidefaultrecentfolders/#", - UI_DEFAULT_RECENT_FOLDERS); - sURIMatcher.addURI(EmailContent.AUTHORITY, "pickTrashFolder/#", - ACCOUNT_PICK_TRASH_FOLDER); - sURIMatcher.addURI(EmailContent.AUTHORITY, "pickSentFolder/#", - ACCOUNT_PICK_SENT_FOLDER); - sURIMatcher.addURI(EmailContent.AUTHORITY, "uipurgefolder/#", UI_PURGE_FOLDER); - } - } - - /** - * The idea here is that the two databases (EmailProvider.db and EmailProviderBody.db must - * always be in sync (i.e. there are two database or NO databases). This code will delete - * any "orphan" database, so that both will be created together. Note that an "orphan" database - * will exist after either of the individual databases is deleted due to data corruption. - */ - public void checkDatabases() { - synchronized (sDatabaseLock) { - // Uncache the databases - if (mDatabase != null) { - mDatabase = null; - } - if (mBodyDatabase != null) { - mBodyDatabase = null; - } - // Look for orphans, and delete as necessary; these must always be in sync - final File databaseFile = getContext().getDatabasePath(DATABASE_NAME); - final File bodyFile = getContext().getDatabasePath(BODY_DATABASE_NAME); - - // TODO Make sure attachments are deleted - if (databaseFile.exists() && !bodyFile.exists()) { - LogUtils.w(TAG, "Deleting orphaned EmailProvider database..."); - getContext().deleteDatabase(DATABASE_NAME); - } else if (bodyFile.exists() && !databaseFile.exists()) { - LogUtils.w(TAG, "Deleting orphaned EmailProviderBody database..."); - getContext().deleteDatabase(BODY_DATABASE_NAME); - } - } - } - - @Override - public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, - String sortOrder) { - Cursor c = null; - int match; - try { - match = findMatch(uri, "query"); - } catch (IllegalArgumentException e) { - String uriString = uri.toString(); - // If we were passed an illegal uri, see if it ends in /-1 - // if so, and if substituting 0 for -1 results in a valid uri, return an empty cursor - if (uriString != null && uriString.endsWith("/-1")) { - uri = Uri.parse(uriString.substring(0, uriString.length() - 2) + "0"); - match = findMatch(uri, "query"); - switch (match) { - case BODY_ID: - case MESSAGE_ID: - case DELETED_MESSAGE_ID: - case UPDATED_MESSAGE_ID: - case ATTACHMENT_ID: - case MAILBOX_ID: - case ACCOUNT_ID: - case HOSTAUTH_ID: - case CREDENTIAL_ID: - case POLICY_ID: - return new MatrixCursorWithCachedColumns(projection, 0); - } - } - throw e; - } - Context context = getContext(); - // See the comment at delete(), above - SQLiteDatabase db = getDatabase(context); - int table = match >> BASE_SHIFT; - String limit = uri.getQueryParameter(EmailContent.PARAMETER_LIMIT); - String id; - - String tableName = TABLE_NAMES.valueAt(table); - - try { - switch (match) { - // First, dispatch queries from UnifiedEmail - case UI_SEARCH: - c = uiSearch(uri, projection); - return c; - case UI_ACCTS: - final String suppressParam = - uri.getQueryParameter(EmailContent.SUPPRESS_COMBINED_ACCOUNT_PARAM); - final boolean suppressCombined = - suppressParam != null && Boolean.parseBoolean(suppressParam); - c = uiAccounts(projection, suppressCombined); - return c; - case UI_UNDO: - return uiUndo(projection); - case UI_SUBFOLDERS: - case UI_MESSAGES: - case UI_MESSAGE: - case UI_FOLDER: - case UI_INBOX: - case UI_ACCOUNT: - case UI_ATTACHMENT: - case UI_ATTACHMENTS: - case UI_ATTACHMENT_BY_CID: - case UI_CONVERSATION: - case UI_RECENT_FOLDERS: - case UI_FULL_FOLDERS: - case UI_ALL_FOLDERS: - // For now, we don't allow selection criteria within these queries - if (selection != null || selectionArgs != null) { - throw new IllegalArgumentException("UI queries can't have selection/args"); - } - - final String seenParam = uri.getQueryParameter(UIProvider.SEEN_QUERY_PARAMETER); - final boolean unseenOnly = - seenParam != null && Boolean.FALSE.toString().equals(seenParam); - - c = uiQuery(match, uri, projection, unseenOnly); - return c; - case UI_FOLDERS: - c = uiFolders(uri, projection); - return c; - case UI_FOLDER_LOAD_MORE: - c = uiFolderLoadMore(getMailbox(uri)); - return c; - case UI_FOLDER_REFRESH: - c = uiFolderRefresh(getMailbox(uri), 0); - return c; - case MAILBOX_NOTIFICATION: - c = notificationQuery(uri); - return c; - case MAILBOX_MOST_RECENT_MESSAGE: - c = mostRecentMessageQuery(uri); - return c; - case MAILBOX_MESSAGE_COUNT: - c = getMailboxMessageCount(uri); - return c; - case MESSAGE_MOVE: - return db.query(MessageMove.TABLE_NAME, projection, selection, selectionArgs, - null, null, sortOrder, limit); - case MESSAGE_STATE_CHANGE: - return db.query(MessageStateChange.TABLE_NAME, projection, selection, - selectionArgs, null, null, sortOrder, limit); - case MESSAGE: - case UPDATED_MESSAGE: - case DELETED_MESSAGE: - case ATTACHMENT: - case MAILBOX: - case ACCOUNT: - case HOSTAUTH: - case CREDENTIAL: - case POLICY: - c = db.query(tableName, projection, - selection, selectionArgs, null, null, sortOrder, limit); - break; - case QUICK_RESPONSE: - c = uiQuickResponse(projection); - break; - case BODY: - case BODY_ID: { - final ProjectionMap map = new ProjectionMap.Builder() - .addAll(projection) - .build(); - if (map.containsKey(BodyColumns.HTML_CONTENT) || - map.containsKey(BodyColumns.TEXT_CONTENT)) { - throw new IllegalArgumentException( - "Body content cannot be returned in the cursor"); - } - - final ContentValues cv = new ContentValues(2); - cv.put(BodyColumns.HTML_CONTENT_URI, "@" + uriWithColumn("bodyHtml", - BodyColumns.MESSAGE_KEY)); - cv.put(BodyColumns.TEXT_CONTENT_URI, "@" + uriWithColumn("bodyText", - BodyColumns.MESSAGE_KEY)); - - final StringBuilder sb = genSelect(map, projection, cv); - sb.append(" FROM ").append(Body.TABLE_NAME); - if (match == BODY_ID) { - id = uri.getPathSegments().get(1); - sb.append(" WHERE ").append(whereWithId(id, selection)); - } else if (!TextUtils.isEmpty(selection)) { - sb.append(" WHERE ").append(selection); - } - if (!TextUtils.isEmpty(sortOrder)) { - sb.append(" ORDER BY ").append(sortOrder); - } - if (!TextUtils.isEmpty(limit)) { - sb.append(" LIMIT ").append(limit); - } - c = db.rawQuery(sb.toString(), selectionArgs); - break; - } - case MESSAGE_ID: - case DELETED_MESSAGE_ID: - case UPDATED_MESSAGE_ID: - case ATTACHMENT_ID: - case MAILBOX_ID: - case ACCOUNT_ID: - case HOSTAUTH_ID: - case CREDENTIAL_ID: - case POLICY_ID: - id = uri.getPathSegments().get(1); - c = db.query(tableName, projection, whereWithId(id, selection), - selectionArgs, null, null, sortOrder, limit); - break; - case QUICK_RESPONSE_ID: - id = uri.getPathSegments().get(1); - c = uiQuickResponseId(projection, id); - break; - case ATTACHMENTS_MESSAGE_ID: - // All attachments for the given message - id = uri.getPathSegments().get(2); - c = db.query(Attachment.TABLE_NAME, projection, - whereWith(AttachmentColumns.MESSAGE_KEY + "=" + id, selection), - selectionArgs, null, null, sortOrder, limit); - break; - case QUICK_RESPONSE_ACCOUNT_ID: - // All quick responses for the given account - id = uri.getPathSegments().get(2); - c = uiQuickResponseAccount(projection, id); - break; - default: - throw new IllegalArgumentException("Unknown URI " + uri); - } - } catch (SQLiteException e) { - checkDatabases(); - throw e; - } catch (RuntimeException e) { - checkDatabases(); - e.printStackTrace(); - throw e; - } finally { - if (c == null) { - // This should never happen, but let's be sure to log it... - // TODO: There are actually cases where c == null is expected, for example - // UI_FOLDER_LOAD_MORE. - // Demoting this to a warning for now until we figure out what to do with it. - LogUtils.w(TAG, "Query returning null for uri: %s selection: %s", uri, selection); - } - } - - if ((c != null) && !isTemporary()) { - c.setNotificationUri(getContext().getContentResolver(), uri); - } - return c; - } - - private static String whereWithId(String id, String selection) { - StringBuilder sb = new StringBuilder(256); - sb.append("_id="); - sb.append(id); - if (selection != null) { - sb.append(" AND ("); - sb.append(selection); - sb.append(')'); - } - return sb.toString(); - } - - /** - * Combine a locally-generated selection with a user-provided selection - * - * This introduces risk that the local selection might insert incorrect chars - * into the SQL, so use caution. - * - * @param where locally-generated selection, must not be null - * @param selection user-provided selection, may be null - * @return a single selection string - */ - private static String whereWith(String where, String selection) { - if (selection == null) { - return where; - } - return where + " AND (" + selection + ")"; - } - - /** - * Restore a HostAuth from a database, given its unique id - * @param db the database - * @param id the unique id (_id) of the row - * @return a fully populated HostAuth or null if the row does not exist - */ - private static HostAuth restoreHostAuth(SQLiteDatabase db, long id) { - Cursor c = db.query(HostAuth.TABLE_NAME, HostAuth.CONTENT_PROJECTION, - HostAuthColumns._ID + "=?", new String[] {Long.toString(id)}, null, null, null); - try { - if (c.moveToFirst()) { - HostAuth hostAuth = new HostAuth(); - hostAuth.restore(c); - return hostAuth; - } - return null; - } finally { - c.close(); - } - } - - /** - * Copy the Account and HostAuth tables from one database to another - * @param fromDatabase the source database - * @param toDatabase the destination database - * @return the number of accounts copied, or -1 if an error occurred - */ - private static int copyAccountTables(SQLiteDatabase fromDatabase, SQLiteDatabase toDatabase) { - if (fromDatabase == null || toDatabase == null) return -1; - - // Lock both databases; for the "from" database, we don't want anyone changing it from - // under us; for the "to" database, we want to make the operation atomic - int copyCount = 0; - fromDatabase.beginTransaction(); - try { - toDatabase.beginTransaction(); - try { - // Delete anything hanging around here - toDatabase.delete(Account.TABLE_NAME, null, null); - toDatabase.delete(HostAuth.TABLE_NAME, null, null); - - // Get our account cursor - Cursor c = fromDatabase.query(Account.TABLE_NAME, Account.CONTENT_PROJECTION, - null, null, null, null, null); - if (c == null) return 0; - LogUtils.d(TAG, "fromDatabase accounts: " + c.getCount()); - try { - // Loop through accounts, copying them and associated host auth's - while (c.moveToNext()) { - Account account = new Account(); - account.restore(c); - - // Clear security sync key and sync key, as these were specific to the - // state of the account, and we've reset that... - // Clear policy key so that we can re-establish policies from the server - // TODO This is pretty EAS specific, but there's a lot of that around - account.mSecuritySyncKey = null; - account.mSyncKey = null; - account.mPolicyKey = 0; - - // Copy host auth's and update foreign keys - HostAuth hostAuth = restoreHostAuth(fromDatabase, - account.mHostAuthKeyRecv); - - // The account might have gone away, though very unlikely - if (hostAuth == null) continue; - account.mHostAuthKeyRecv = toDatabase.insert(HostAuth.TABLE_NAME, null, - hostAuth.toContentValues()); - - // EAS accounts have no send HostAuth - if (account.mHostAuthKeySend > 0) { - hostAuth = restoreHostAuth(fromDatabase, account.mHostAuthKeySend); - // Belt and suspenders; I can't imagine that this is possible, - // since we checked the validity of the account above, and the - // database is now locked - if (hostAuth == null) continue; - account.mHostAuthKeySend = toDatabase.insert( - HostAuth.TABLE_NAME, null, hostAuth.toContentValues()); - } - - // Now, create the account in the "to" database - toDatabase.insert(Account.TABLE_NAME, null, account.toContentValues()); - copyCount++; - } - } finally { - c.close(); - } - - // Say it's ok to commit - toDatabase.setTransactionSuccessful(); - } finally { - toDatabase.endTransaction(); - } - } catch (SQLiteException ex) { - LogUtils.w(TAG, "Exception while copying account tables", ex); - copyCount = -1; - } finally { - fromDatabase.endTransaction(); - } - return copyCount; - } - - /** - * Backup account data, returning the number of accounts backed up - */ - private static int backupAccounts(final Context context, final SQLiteDatabase db) { - final AccountManager am = AccountManager.get(context); - final Cursor accountCursor = db.query(Account.TABLE_NAME, Account.CONTENT_PROJECTION, - null, null, null, null, null); - int updatedCount = 0; - try { - while (accountCursor.moveToNext()) { - final Account account = new Account(); - account.restore(accountCursor); - EmailServiceInfo serviceInfo = - EmailServiceUtils.getServiceInfo(context, account.getProtocol(context)); - if (serviceInfo == null) { - LogUtils.d(LogUtils.TAG, "Could not find service info for account"); - continue; - } - final String jsonString = account.toJsonString(context); - final android.accounts.Account amAccount = - account.getAccountManagerAccount(serviceInfo.accountType); - am.setUserData(amAccount, ACCOUNT_MANAGER_JSON_TAG, jsonString); - updatedCount++; - } - } finally { - accountCursor.close(); - } - return updatedCount; - } - - /** - * Restore account data, returning the number of accounts restored - */ - private static int restoreAccounts(final Context context) { - final Collection infos = EmailServiceUtils.getServiceInfoList(context); - // Find all possible account types - final Set accountTypes = new HashSet(3); - for (final EmailServiceInfo info : infos) { - if (!TextUtils.isEmpty(info.accountType)) { - // accountType will be empty for the gmail stub entry - accountTypes.add(info.accountType); - } - } - // Find all accounts we own - final List amAccounts = new ArrayList(); - final AccountManager am = AccountManager.get(context); - for (final String accountType : accountTypes) { - amAccounts.addAll(Arrays.asList(am.getAccountsByType(accountType))); - } - // Try to restore them from saved JSON - int restoredCount = 0; - for (final android.accounts.Account amAccount : amAccounts) { - final String jsonString = am.getUserData(amAccount, ACCOUNT_MANAGER_JSON_TAG); - if (TextUtils.isEmpty(jsonString)) { - continue; - } - final Account account = Account.fromJsonString(jsonString); - if (account != null) { - AccountSettingsUtils.commitSettings(context, account); - final Bundle extras = new Bundle(3); - extras.putBoolean(ContentResolver.SYNC_EXTRAS_MANUAL, true); - extras.putBoolean(ContentResolver.SYNC_EXTRAS_DO_NOT_RETRY, true); - extras.putBoolean(ContentResolver.SYNC_EXTRAS_EXPEDITED, true); - ContentResolver.requestSync(amAccount, EmailContent.AUTHORITY, extras); - restoredCount++; - } - } - return restoredCount; - } - - private static final String MESSAGE_CHANGE_LOG_TABLE_INSERT_PREFIX = "insert into %s (" - + MessageChangeLogTable.MESSAGE_KEY + "," + MessageChangeLogTable.SERVER_ID + "," - + MessageChangeLogTable.ACCOUNT_KEY + "," + MessageChangeLogTable.STATUS + ","; - - private static final String MESSAGE_CHANGE_LOG_TABLE_VALUES_PREFIX = ") values (%s, " - + "(select " + MessageColumns.SERVER_ID + " from " + - Message.TABLE_NAME + " where _id=%s)," - + "(select " + MessageColumns.ACCOUNT_KEY + " from " + - Message.TABLE_NAME + " where _id=%s)," - + MessageMove.STATUS_NONE_STRING + ","; - - /** - * Formatting string to generate the SQL statement for inserting into MessageMove. - * The formatting parameters are: - * table name, message id x 4, destination folder id, message id, destination folder id. - * Duplications are needed for sub-selects. - */ - private static final String MESSAGE_MOVE_INSERT = MESSAGE_CHANGE_LOG_TABLE_INSERT_PREFIX - + MessageMove.SRC_FOLDER_KEY + "," + MessageMove.DST_FOLDER_KEY + "," - + MessageMove.SRC_FOLDER_SERVER_ID + "," + MessageMove.DST_FOLDER_SERVER_ID - + MESSAGE_CHANGE_LOG_TABLE_VALUES_PREFIX - + "(select " + MessageColumns.MAILBOX_KEY + - " from " + Message.TABLE_NAME + " where _id=%s)," + "%d," - + "(select " + Mailbox.SERVER_ID + " from " + Mailbox.TABLE_NAME + " where _id=(select " - + MessageColumns.MAILBOX_KEY + " from " + Message.TABLE_NAME + " where _id=%s))," - + "(select " + Mailbox.SERVER_ID + " from " + Mailbox.TABLE_NAME + " where _id=%d))"; - - /** - * Insert a row into the MessageMove table when that message is moved. - * @param db The {@link SQLiteDatabase}. - * @param messageId The id of the message being moved. - * @param dstFolderKey The folder to which the message is being moved. - */ - private void addToMessageMove(final SQLiteDatabase db, final String messageId, - final long dstFolderKey) { - db.execSQL(String.format(Locale.US, MESSAGE_MOVE_INSERT, MessageMove.TABLE_NAME, - messageId, messageId, messageId, messageId, dstFolderKey, messageId, dstFolderKey)); - } - - /** - * Formatting string to generate the SQL statement for inserting into MessageStateChange. - * The formatting parameters are: - * table name, message id x 4, new flag read, message id, new flag favorite. - * Duplications are needed for sub-selects. - */ - private static final String MESSAGE_STATE_CHANGE_INSERT = MESSAGE_CHANGE_LOG_TABLE_INSERT_PREFIX - + MessageStateChange.OLD_FLAG_READ + "," + MessageStateChange.NEW_FLAG_READ + "," - + MessageStateChange.OLD_FLAG_FAVORITE + "," + MessageStateChange.NEW_FLAG_FAVORITE - + MESSAGE_CHANGE_LOG_TABLE_VALUES_PREFIX - + "(select " + MessageColumns.FLAG_READ + - " from " + Message.TABLE_NAME + " where _id=%s)," + "%d," - + "(select " + MessageColumns.FLAG_FAVORITE + - " from " + Message.TABLE_NAME + " where _id=%s)," + "%d)"; - - private void addToMessageStateChange(final SQLiteDatabase db, final String messageId, - final int newFlagRead, final int newFlagFavorite) { - db.execSQL(String.format(Locale.US, MESSAGE_STATE_CHANGE_INSERT, - MessageStateChange.TABLE_NAME, messageId, messageId, messageId, messageId, - newFlagRead, messageId, newFlagFavorite)); - } - - // select count(*) from (select count(*) as dupes from Mailbox where accountKey=? - // group by serverId) where dupes > 1; - private static final String ACCOUNT_INTEGRITY_SQL = - "select count(*) from (select count(*) as dupes from " + Mailbox.TABLE_NAME + - " where accountKey=? group by " + MailboxColumns.SERVER_ID + ") where dupes > 1"; - - - // Query to get the protocol for a message. Temporary to switch between new and old upsync - // behavior; should go away when IMAP gets converted. - private static final String GET_MESSAGE_DETAILS = "SELECT" - + " h." + HostAuthColumns.PROTOCOL + "," - + " m." + MessageColumns.MAILBOX_KEY + "," - + " a." + AccountColumns._ID - + " FROM " + Message.TABLE_NAME + " AS m" - + " INNER JOIN " + Account.TABLE_NAME + " AS a" - + " ON m." + MessageColumns.ACCOUNT_KEY + "=a." + AccountColumns._ID - + " INNER JOIN " + HostAuth.TABLE_NAME + " AS h" - + " ON a." + AccountColumns.HOST_AUTH_KEY_RECV + "=h." + HostAuthColumns._ID - + " WHERE m." + MessageColumns._ID + "=?"; - private static final int INDEX_PROTOCOL = 0; - private static final int INDEX_MAILBOX_KEY = 1; - private static final int INDEX_ACCOUNT_KEY = 2; - - /** - * Query to get the protocol and email address for an account. Note that this uses - * {@link #INDEX_PROTOCOL} and {@link #INDEX_EMAIL_ADDRESS} for its columns. - */ - private static final String GET_ACCOUNT_DETAILS = "SELECT" - + " h." + HostAuthColumns.PROTOCOL + "," - + " a." + AccountColumns.EMAIL_ADDRESS + "," - + " a." + AccountColumns.SYNC_KEY - + " FROM " + Account.TABLE_NAME + " AS a" - + " INNER JOIN " + HostAuth.TABLE_NAME + " AS h" - + " ON a." + AccountColumns.HOST_AUTH_KEY_RECV + "=h." + HostAuthColumns._ID - + " WHERE a." + AccountColumns._ID + "=?"; - private static final int INDEX_EMAIL_ADDRESS = 1; - private static final int INDEX_SYNC_KEY = 2; - - /** - * Restart push if we need it (currently only for Exchange accounts). - * @param context A {@link Context}. - * @param db The {@link SQLiteDatabase}. - * @param id The id of the thing we're looking for. - * @return Whether or not we sent a request to restart the push. - */ - private static boolean restartPush(final Context context, final SQLiteDatabase db, - final String id) { - final Cursor c = db.rawQuery(GET_ACCOUNT_DETAILS, new String[] {id}); - if (c != null) { - try { - if (c.moveToFirst()) { - final String protocol = c.getString(INDEX_PROTOCOL); - // Only restart push for EAS accounts that have completed initial sync. - if (context.getString(R.string.protocol_eas).equals(protocol) && - !EmailContent.isInitialSyncKey(c.getString(INDEX_SYNC_KEY))) { - final String emailAddress = c.getString(INDEX_EMAIL_ADDRESS); - final android.accounts.Account account = - getAccountManagerAccount(context, emailAddress, protocol); - if (account != null) { - restartPush(account); - return true; - } - } - } - } finally { - c.close(); - } - } - return false; - } - - /** - * Restart push if a mailbox's settings change in a way that requires it. - * @param context A {@link Context}. - * @param db The {@link SQLiteDatabase}. - * @param values The {@link ContentValues} that were updated for the mailbox. - * @param accountId The id of the account for this mailbox. - * @return Whether or not the push was restarted. - */ - private static boolean restartPushForMailbox(final Context context, final SQLiteDatabase db, - final ContentValues values, final String accountId) { - if (values.containsKey(MailboxColumns.SYNC_LOOKBACK) || - values.containsKey(MailboxColumns.SYNC_INTERVAL)) { - return restartPush(context, db, accountId); - } - return false; - } - - /** - * Restart push if an account's settings change in a way that requires it. - * @param context A {@link Context}. - * @param db The {@link SQLiteDatabase}. - * @param values The {@link ContentValues} that were updated for the account. - * @param accountId The id of the account. - * @return Whether or not the push was restarted. - */ - private static boolean restartPushForAccount(final Context context, final SQLiteDatabase db, - final ContentValues values, final String accountId) { - if (values.containsKey(AccountColumns.SYNC_LOOKBACK) || - values.containsKey(AccountColumns.SYNC_INTERVAL)) { - return restartPush(context, db, accountId); - } - return false; - } - - @Override - public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) { - LogUtils.d(TAG, "Update: " + uri); - // Handle this special case the fastest possible way - if (INTEGRITY_CHECK_URI.equals(uri)) { - checkDatabases(); - return 0; - } else if (ACCOUNT_BACKUP_URI.equals(uri)) { - return backupAccounts(getContext(), getDatabase(getContext())); - } - - // Notify all existing cursors, except for ACCOUNT_RESET_NEW_COUNT(_ID) - Uri notificationUri = EmailContent.CONTENT_URI; - - final int match = findMatch(uri, "update"); - final Context context = getContext(); - // See the comment at delete(), above - final SQLiteDatabase db = getDatabase(context); - final int table = match >> BASE_SHIFT; - int result; - - // We do NOT allow setting of unreadCount/messageCount via the provider - // These columns are maintained via triggers - if (match == MAILBOX_ID || match == MAILBOX) { - values.remove(MailboxColumns.UNREAD_COUNT); - values.remove(MailboxColumns.MESSAGE_COUNT); - } - - final String tableName = TABLE_NAMES.valueAt(table); - String id = "0"; - - try { - switch (match) { - case ACCOUNT_PICK_TRASH_FOLDER: - return pickTrashFolder(uri); - case ACCOUNT_PICK_SENT_FOLDER: - return pickSentFolder(uri); - case UI_ACCTSETTINGS: - return uiUpdateSettings(context, values); - case UI_FOLDER: - return uiUpdateFolder(context, uri, values); - case UI_RECENT_FOLDERS: - return uiUpdateRecentFolders(uri, values); - case UI_DEFAULT_RECENT_FOLDERS: - return uiPopulateRecentFolders(uri); - case UI_ATTACHMENT: - return uiUpdateAttachment(uri, values); - case UI_MESSAGE: - return uiUpdateMessage(uri, values); - case ACCOUNT_CHECK: - id = uri.getLastPathSegment(); - // With any error, return 1 (a failure) - int res = 1; - Cursor ic = null; - try { - ic = db.rawQuery(ACCOUNT_INTEGRITY_SQL, new String[] {id}); - if (ic.moveToFirst()) { - res = ic.getInt(0); - } - } finally { - if (ic != null) { - ic.close(); - } - } - // Count of duplicated mailboxes - return res; - case MESSAGE_SELECTION: - Cursor findCursor = db.query(tableName, Message.ID_COLUMN_PROJECTION, selection, - selectionArgs, null, null, null); - try { - if (findCursor.moveToFirst()) { - return update(ContentUris.withAppendedId( - Message.CONTENT_URI, - findCursor.getLong(Message.ID_COLUMNS_ID_COLUMN)), - values, null, null); - } else { - return 0; - } - } finally { - findCursor.close(); - } - case SYNCED_MESSAGE_ID: - case UPDATED_MESSAGE_ID: - case MESSAGE_ID: - case ATTACHMENT_ID: - case MAILBOX_ID: - case ACCOUNT_ID: - case HOSTAUTH_ID: - case CREDENTIAL_ID: - case QUICK_RESPONSE_ID: - case POLICY_ID: - id = uri.getPathSegments().get(1); - if (match == SYNCED_MESSAGE_ID) { - // TODO: Migrate IMAP to use MessageMove/MessageStateChange as well. - boolean isEas = false; - long mailboxId = -1; - long accountId = -1; - final Cursor c = db.rawQuery(GET_MESSAGE_DETAILS, new String[] {id}); - if (c != null) { - try { - if (c.moveToFirst()) { - final String protocol = c.getString(INDEX_PROTOCOL); - isEas = context.getString(R.string.protocol_eas) - .equals(protocol); - mailboxId = c.getLong(INDEX_MAILBOX_KEY); - accountId = c.getLong(INDEX_ACCOUNT_KEY); - } - } finally { - c.close(); - } - } - - if (isEas) { - // EAS uses the new upsync classes. - Long dstFolderId = values.getAsLong(MessageColumns.MAILBOX_KEY); - if (dstFolderId != null) { - addToMessageMove(db, id, dstFolderId); - } - Integer flagRead = values.getAsInteger(MessageColumns.FLAG_READ); - Integer flagFavorite = values.getAsInteger(MessageColumns.FLAG_FAVORITE); - int flagReadValue = (flagRead != null) ? - flagRead : MessageStateChange.VALUE_UNCHANGED; - int flagFavoriteValue = (flagFavorite != null) ? - flagFavorite : MessageStateChange.VALUE_UNCHANGED; - if (flagRead != null || flagFavorite != null) { - addToMessageStateChange(db, id, flagReadValue, flagFavoriteValue); - } - - // Request a sync for the messages mailbox so the update will upsync. - // This is normally done with ContentResolver.notifyUpdate() but doesn't - // work for Exchange because the Sync Adapter is declared as - // android:supportsUploading="false". Changing it to true is not trivial - // because that would require us to protect all calls to notifyUpdate() - // with syncToServer=false except in cases where we actually want to - // upsync. - // TODO: Look into making Exchange Sync Adapter supportsUploading=true - // Since we can't use the Sync Manager "delayed-sync" feature which - // applies only to UPLOAD syncs, we need to do this ourselves. The - // purpose of this is not to spam syncs when making frequent - // modifications. - final Handler handler = getDelayedSyncHandler(); - final android.accounts.Account amAccount = - getAccountManagerAccount(accountId); - if (amAccount != null) { - final SyncRequestMessage request = new SyncRequestMessage( - uri.getAuthority(), amAccount, mailboxId); - synchronized (mDelayedSyncRequests) { - if (!mDelayedSyncRequests.contains(request)) { - mDelayedSyncRequests.add(request); - final android.os.Message message = - handler.obtainMessage(0, request); - handler.sendMessageDelayed(message, SYNC_DELAY_MILLIS); - } - } - } else { - LogUtils.d(TAG, - "Attempted to start delayed sync for invalid account %d", - accountId); - } - } else { - // Old way of doing upsync. - // For synced messages, first copy the old message to the updated table - // Note the insert or ignore semantics, guaranteeing that only the first - // update will be reflected in the updated message table; therefore this - // row will always have the "original" data - db.execSQL(UPDATED_MESSAGE_INSERT + id); - } - } else if (match == MESSAGE_ID) { - db.execSQL(UPDATED_MESSAGE_DELETE + id); - } - result = db.update(tableName, values, whereWithId(id, selection), - selectionArgs); - if (match == MESSAGE_ID || match == SYNCED_MESSAGE_ID) { - handleMessageUpdateNotifications(uri, id, values); - } else if (match == ATTACHMENT_ID) { - long attId = Integer.parseInt(id); - if (values.containsKey(AttachmentColumns.FLAGS)) { - int flags = values.getAsInteger(AttachmentColumns.FLAGS); - mAttachmentService.attachmentChanged(context, attId, flags); - } - // Notify UI if necessary; there are only two columns we can change that - // would be worth a notification - if (values.containsKey(AttachmentColumns.UI_STATE) || - values.containsKey(AttachmentColumns.UI_DOWNLOADED_SIZE)) { - // Notify on individual attachment - notifyUI(UIPROVIDER_ATTACHMENT_NOTIFIER, id); - Attachment att = Attachment.restoreAttachmentWithId(context, attId); - if (att != null) { - // And on owning Message - notifyUI(UIPROVIDER_ATTACHMENTS_NOTIFIER, att.mMessageKey); - } - } - } else if (match == MAILBOX_ID) { - final long accountId = Mailbox.getAccountIdForMailbox(context, id); - notifyUIFolder(id, accountId); - restartPushForMailbox(context, db, values, Long.toString(accountId)); - } else if (match == ACCOUNT_ID) { - updateAccountSyncInterval(Long.parseLong(id), values); - // Notify individual account and "all accounts" - notifyUI(UIPROVIDER_ACCOUNT_NOTIFIER, id); - notifyUI(UIPROVIDER_ALL_ACCOUNTS_NOTIFIER, null); - restartPushForAccount(context, db, values, id); - } - break; - case BODY_ID: { - final ContentValues updateValues = new ContentValues(values); - updateValues.remove(BodyColumns.HTML_CONTENT); - updateValues.remove(BodyColumns.TEXT_CONTENT); - - result = db.update(tableName, updateValues, whereWithId(id, selection), - selectionArgs); - - if (values.containsKey(BodyColumns.HTML_CONTENT) || - values.containsKey(BodyColumns.TEXT_CONTENT)) { - final long messageId; - if (values.containsKey(BodyColumns.MESSAGE_KEY)) { - messageId = values.getAsLong(BodyColumns.MESSAGE_KEY); - } else { - final long bodyId = Long.parseLong(id); - final SQLiteStatement sql = db.compileStatement( - "select " + BodyColumns.MESSAGE_KEY + - " from " + Body.TABLE_NAME + - " where " + BodyColumns._ID + "=" + Long - .toString(bodyId) - ); - messageId = sql.simpleQueryForLong(); - } - writeBodyFiles(context, messageId, values); - } - break; - } - case BODY: { - final ContentValues updateValues = new ContentValues(values); - updateValues.remove(BodyColumns.HTML_CONTENT); - updateValues.remove(BodyColumns.TEXT_CONTENT); - - result = db.update(tableName, updateValues, selection, selectionArgs); - - if (result == 0 && selection.equals(Body.SELECTION_BY_MESSAGE_KEY)) { - // TODO: This is a hack. Notably, the selection equality test above - // is hokey at best. - LogUtils.i(TAG, "Body Update to non-existent row, morphing to insert"); - final ContentValues insertValues = new ContentValues(values); - insertValues.put(BodyColumns.MESSAGE_KEY, selectionArgs[0]); - insert(Body.CONTENT_URI, insertValues); - } else { - // possibly need to write new body values - if (values.containsKey(BodyColumns.HTML_CONTENT) || - values.containsKey(BodyColumns.TEXT_CONTENT)) { - final long messageIds[]; - if (values.containsKey(BodyColumns.MESSAGE_KEY)) { - messageIds = new long[] {values.getAsLong(BodyColumns.MESSAGE_KEY)}; - } else if (values.containsKey(BodyColumns._ID)) { - final long bodyId = values.getAsLong(BodyColumns._ID); - final SQLiteStatement sql = db.compileStatement( - "select " + BodyColumns.MESSAGE_KEY + - " from " + Body.TABLE_NAME + - " where " + BodyColumns._ID + "=" + Long - .toString(bodyId) - ); - messageIds = new long[] {sql.simpleQueryForLong()}; - } else { - final String proj[] = {BodyColumns.MESSAGE_KEY}; - final Cursor c = db.query(Body.TABLE_NAME, proj, - selection, selectionArgs, - null, null, null); - try { - final int count = c.getCount(); - if (count == 0) { - throw new IllegalStateException("Can't find body record"); - } - messageIds = new long[count]; - int i = 0; - while (c.moveToNext()) { - messageIds[i++] = c.getLong(0); - } - } finally { - c.close(); - } - } - // This is probably overkill - for (int i = 0; i < messageIds.length; i++) { - final long messageId = messageIds[i]; - writeBodyFiles(context, messageId, values); - } - } - } - break; - } - case MESSAGE: - decodeEmailAddresses(values); - case UPDATED_MESSAGE: - case ATTACHMENT: - case MAILBOX: - case ACCOUNT: - case HOSTAUTH: - case CREDENTIAL: - case POLICY: - if (match == ATTACHMENT) { - if (values.containsKey(AttachmentColumns.LOCATION) && - TextUtils.isEmpty(values.getAsString(AttachmentColumns.LOCATION))) { - LogUtils.w(TAG, new Throwable(), "attachment with blank location"); - } - } - result = db.update(tableName, values, selection, selectionArgs); - break; - case MESSAGE_MOVE: - result = db.update(MessageMove.TABLE_NAME, values, selection, selectionArgs); - break; - case MESSAGE_STATE_CHANGE: - result = db.update(MessageStateChange.TABLE_NAME, values, selection, - selectionArgs); - break; - default: - throw new IllegalArgumentException("Unknown URI " + uri); - } - } catch (SQLiteException e) { - checkDatabases(); - throw e; - } - - // Notify all notifier cursors if some records where changed in the database - if (result > 0) { - sendNotifierChange(getBaseNotificationUri(match), NOTIFICATION_OP_UPDATE, id); - notifyUI(notificationUri, null); - } - return result; - } - - private void updateSyncStatus(final Bundle extras) { - final long id = extras.getLong(EmailServiceStatus.SYNC_STATUS_ID); - final int statusCode = extras.getInt(EmailServiceStatus.SYNC_STATUS_CODE); - final Uri uri = ContentUris.withAppendedId(FOLDER_STATUS_URI, id); - notifyUI(uri, null); - final boolean inProgress = statusCode == EmailServiceStatus.IN_PROGRESS; - if (inProgress) { - RefreshStatusMonitor.getInstance(getContext()).setSyncStarted(id); - } else { - final int result = extras.getInt(EmailServiceStatus.SYNC_RESULT); - final ContentValues values = new ContentValues(); - values.put(Mailbox.UI_LAST_SYNC_RESULT, result); - mDatabase.update( - Mailbox.TABLE_NAME, - values, - WHERE_ID, - new String[] { String.valueOf(id) }); - } - } - - @Override - public Bundle call(String method, String arg, Bundle extras) { - LogUtils.d(TAG, "EmailProvider#call(%s, %s)", method, arg); - - // Handle queries for the device friendly name. - // TODO: This should eventually be a device property, not defined by the app. - if (TextUtils.equals(method, EmailContent.DEVICE_FRIENDLY_NAME)) { - final Bundle bundle = new Bundle(1); - // TODO: For now, just use the model name since we don't yet have a user-supplied name. - bundle.putString(EmailContent.DEVICE_FRIENDLY_NAME, Build.MODEL); - return bundle; - } - - // Handle sync status callbacks. - if (TextUtils.equals(method, SYNC_STATUS_CALLBACK_METHOD)) { - updateSyncStatus(extras); - return null; - } - if (TextUtils.equals(method, MailboxUtilities.FIX_PARENT_KEYS_METHOD)) { - fixParentKeys(getDatabase(getContext())); - return null; - } - - // Handle send & save. - final Uri accountUri = Uri.parse(arg); - final long accountId = Long.parseLong(accountUri.getPathSegments().get(1)); - - Uri messageUri = null; - - if (TextUtils.equals(method, UIProvider.AccountCallMethods.SEND_MESSAGE)) { - messageUri = uiSendDraftMessage(accountId, extras); - Preferences.getPreferences(getContext()).setLastUsedAccountId(accountId); - } else if (TextUtils.equals(method, UIProvider.AccountCallMethods.SAVE_MESSAGE)) { - messageUri = uiSaveDraftMessage(accountId, extras); - } else if (TextUtils.equals(method, UIProvider.AccountCallMethods.SET_CURRENT_ACCOUNT)) { - LogUtils.d(TAG, "Unhandled (but expected) Content provider method: %s", method); - } else { - LogUtils.wtf(TAG, "Unexpected Content provider method: %s", method); - } - - final Bundle result; - if (messageUri != null) { - result = new Bundle(1); - result.putParcelable(UIProvider.MessageColumns.URI, messageUri); - } else { - result = null; - } - - return result; - } - - /** - * Writes message bodies to disk, read from a set of ContentValues - * - * @param c Context for finding files - * @param messageId id of message to write body for - * @param cv {@link ContentValues} containing {@link BodyColumns#HTML_CONTENT} and/or - * {@link BodyColumns#TEXT_CONTENT}. Inserting a null or empty value will delete the - * associated text or html body file - * @throws IllegalStateException - */ - private static void writeBodyFiles(final Context c, final long messageId, - final ContentValues cv) throws IllegalStateException { - if (cv.containsKey(BodyColumns.HTML_CONTENT)) { - final String htmlContent = cv.getAsString(BodyColumns.HTML_CONTENT); - try { - writeBodyFile(c, messageId, "html", htmlContent); - } catch (final IOException e) { - throw new IllegalStateException("IOException while writing html body " + - "for message id " + Long.toString(messageId), e); - } - } - if (cv.containsKey(BodyColumns.TEXT_CONTENT)) { - final String textContent = cv.getAsString(BodyColumns.TEXT_CONTENT); - try { - writeBodyFile(c, messageId, "txt", textContent); - } catch (final IOException e) { - throw new IllegalStateException("IOException while writing text body " + - "for message id " + Long.toString(messageId), e); - } - } - } - - /** - * Writes a message body file to disk - * - * @param c Context for finding files dir - * @param messageId id of message to write body for - * @param ext "html" or "txt" - * @param content Body content to write to file, or null/empty to delete file - * @throws IOException - */ - private static void writeBodyFile(final Context c, final long messageId, final String ext, - final String content) throws IOException { - final File textFile = getBodyFile(c, messageId, ext); - if (TextUtils.isEmpty(content)) { - if (!textFile.delete()) { - LogUtils.v(LogUtils.TAG, "did not delete text body for %d", messageId); - } - } else { - final FileWriter w = new FileWriter(textFile); - try { - w.write(content); - } finally { - w.close(); - } - } - } - - /** - * Returns a {@link java.io.File} object pointing to the body content file for the message - * - * @param c Context for finding files dir - * @param messageId id of message to locate - * @param ext "html" or "txt" - * @return File ready for operating upon - */ - protected static File getBodyFile(final Context c, final long messageId, final String ext) - throws FileNotFoundException { - if (!TextUtils.equals(ext, "html") && !TextUtils.equals(ext, "txt")) { - throw new IllegalArgumentException("ext must be one of 'html' or 'txt'"); - } - long l1 = messageId / 100 % 100; - long l2 = messageId % 100; - final File dir = new File(c.getFilesDir(), - "body/" + Long.toString(l1) + "/" + Long.toString(l2) + "/"); - if (!dir.isDirectory() && !dir.mkdirs()) { - throw new FileNotFoundException("Could not create directory for body file"); - } - return new File(dir, Long.toString(messageId) + "." + ext); - } - - @Override - public ParcelFileDescriptor openFile(final Uri uri, final String mode) - throws FileNotFoundException { - if (LogUtils.isLoggable(TAG, LogUtils.DEBUG)) { - LogUtils.d(TAG, "EmailProvider.openFile: %s", LogUtils.contentUriToString(TAG, uri)); - } - - final int match = findMatch(uri, "openFile"); - switch (match) { - case ATTACHMENTS_CACHED_FILE_ACCESS: - // Parse the cache file path out from the uri - final String cachedFilePath = - uri.getQueryParameter(Attachment.CACHED_FILE_QUERY_PARAM); - - if (cachedFilePath != null) { - // clearCallingIdentity means that the download manager will - // check our permissions rather than the permissions of whatever - // code is calling us. - long binderToken = Binder.clearCallingIdentity(); - try { - LogUtils.d(TAG, "Opening attachment %s", cachedFilePath); - return ParcelFileDescriptor.open( - new File(cachedFilePath), ParcelFileDescriptor.MODE_READ_ONLY); - } finally { - Binder.restoreCallingIdentity(binderToken); - } - } - break; - case BODY_HTML: { - final long messageKey = Long.valueOf(uri.getLastPathSegment()); - return ParcelFileDescriptor.open(getBodyFile(getContext(), messageKey, "html"), - Utilities.parseMode(mode)); - } - case BODY_TEXT:{ - final long messageKey = Long.valueOf(uri.getLastPathSegment()); - return ParcelFileDescriptor.open(getBodyFile(getContext(), messageKey, "txt"), - Utilities.parseMode(mode)); - } - } - - throw new FileNotFoundException("unable to open file"); - } - - - /** - * Returns the base notification URI for the given content type. - * - * @param match The type of content that was modified. - */ - private static Uri getBaseNotificationUri(int match) { - Uri baseUri = null; - switch (match) { - case MESSAGE: - case MESSAGE_ID: - case SYNCED_MESSAGE_ID: - baseUri = Message.NOTIFIER_URI; - break; - case ACCOUNT: - case ACCOUNT_ID: - baseUri = Account.NOTIFIER_URI; - break; - } - return baseUri; - } - - /** - * Sends a change notification to any cursors observers of the given base URI. The final - * notification URI is dynamically built to contain the specified information. It will be - * of the format <>/<>/<>; where <> and <> are optional depending - * upon the given values. - * NOTE: If <> is specified, notifications for <>/<> will NOT be invoked. - * If this is necessary, it can be added. However, due to the implementation of - * {@link ContentObserver}, observers of <> will receive multiple notifications. - * - * @param baseUri The base URI to send notifications to. Must be able to take appended IDs. - * @param op Optional operation to be appended to the URI. - * @param id If a positive value, the ID to append to the base URI. Otherwise, no ID will be - * appended to the base URI. - */ - private void sendNotifierChange(Uri baseUri, String op, String id) { - if (baseUri == null) return; - - // Append the operation, if specified - if (op != null) { - baseUri = baseUri.buildUpon().appendEncodedPath(op).build(); - } - - long longId = 0L; - try { - longId = Long.valueOf(id); - } catch (NumberFormatException ignore) {} - if (longId > 0) { - notifyUI(baseUri, id); - } else { - notifyUI(baseUri, null); - } - - // We want to send the message list changed notification if baseUri is Message.NOTIFIER_URI. - if (baseUri.equals(Message.NOTIFIER_URI)) { - sendMessageListDataChangedNotification(); - } - } - - private void sendMessageListDataChangedNotification() { - final Context context = getContext(); - final Intent intent = new Intent(ACTION_NOTIFY_MESSAGE_LIST_DATASET_CHANGED); - // Ideally this intent would contain information about which account changed, to limit the - // updates to that particular account. Unfortunately, that information is not available in - // sendNotifierChange(). - context.sendBroadcast(intent); - } - - // We might have more than one thread trying to make its way through applyBatch() so the - // notification coalescing needs to be thread-local to work correctly. - private final ThreadLocal> mTLBatchNotifications = - new ThreadLocal>(); - - private Set getBatchNotificationsSet() { - return mTLBatchNotifications.get(); - } - - private void setBatchNotificationsSet(Set batchNotifications) { - mTLBatchNotifications.set(batchNotifications); - } - - @Override - public ContentProviderResult[] applyBatch(ArrayList operations) - throws OperationApplicationException { - /** - * Collect notification URIs to notify at the end of batch processing. - * These are populated by calls to notifyUI() by way of update(), insert() and delete() - * calls made in super.applyBatch() - */ - setBatchNotificationsSet(Sets.newHashSet()); - Context context = getContext(); - SQLiteDatabase db = getDatabase(context); - db.beginTransaction(); - try { - ContentProviderResult[] results = super.applyBatch(operations); - db.setTransactionSuccessful(); - return results; - } finally { - db.endTransaction(); - final Set notifications = getBatchNotificationsSet(); - setBatchNotificationsSet(null); - for (final Uri uri : notifications) { - context.getContentResolver().notifyChange(uri, null); - } - } - } - - public static interface EmailAttachmentService { - /** - * Notify the service that an attachment has changed. - */ - void attachmentChanged(final Context context, final long id, final int flags); - } - - private final EmailAttachmentService DEFAULT_ATTACHMENT_SERVICE = new EmailAttachmentService() { - @Override - public void attachmentChanged(final Context context, final long id, final int flags) { - // The default implementation delegates to the real service. - AttachmentService.attachmentChanged(context, id, flags); - } - }; - private EmailAttachmentService mAttachmentService = DEFAULT_ATTACHMENT_SERVICE; - - // exposed for testing - public void injectAttachmentService(final EmailAttachmentService attachmentService) { - mAttachmentService = - attachmentService == null ? DEFAULT_ATTACHMENT_SERVICE : attachmentService; - } - - private Cursor notificationQuery(final Uri uri) { - final SQLiteDatabase db = getDatabase(getContext()); - final String accountId = uri.getLastPathSegment(); - - final String sql = "SELECT " + MessageColumns.MAILBOX_KEY + ", " + - "SUM(CASE " + MessageColumns.FLAG_READ + " WHEN 0 THEN 1 ELSE 0 END), " + - "SUM(CASE " + MessageColumns.FLAG_SEEN + " WHEN 0 THEN 1 ELSE 0 END)\n" + - "FROM " + Message.TABLE_NAME + "\n" + - "WHERE " + MessageColumns.ACCOUNT_KEY + " = ?\n" + - "GROUP BY " + MessageColumns.MAILBOX_KEY; - - final String[] selectionArgs = {accountId}; - - return db.rawQuery(sql, selectionArgs); - } - - public Cursor mostRecentMessageQuery(Uri uri) { - SQLiteDatabase db = getDatabase(getContext()); - String mailboxId = uri.getLastPathSegment(); - return db.rawQuery("select max(_id) from Message where mailboxKey=?", - new String[] {mailboxId}); - } - - private Cursor getMailboxMessageCount(Uri uri) { - SQLiteDatabase db = getDatabase(getContext()); - String mailboxId = uri.getLastPathSegment(); - return db.rawQuery("select count(*) from Message where mailboxKey=?", - new String[] {mailboxId}); - } - - /** - * Support for UnifiedEmail below - */ - - private static final String NOT_A_DRAFT_STRING = - Integer.toString(UIProvider.DraftType.NOT_A_DRAFT); - - private static final String CONVERSATION_FLAGS = - "CASE WHEN (" + MessageColumns.FLAGS + "&" + Message.FLAG_INCOMING_MEETING_INVITE + - ") !=0 THEN " + UIProvider.ConversationFlags.CALENDAR_INVITE + - " ELSE 0 END + " + - "CASE WHEN (" + MessageColumns.FLAGS + "&" + Message.FLAG_FORWARDED + - ") !=0 THEN " + UIProvider.ConversationFlags.FORWARDED + - " ELSE 0 END + " + - "CASE WHEN (" + MessageColumns.FLAGS + "&" + Message.FLAG_REPLIED_TO + - ") !=0 THEN " + UIProvider.ConversationFlags.REPLIED + - " ELSE 0 END"; - - /** - * Array of pre-defined account colors (legacy colors from old email app) - */ - private static final int[] ACCOUNT_COLORS = new int[] { - 0xff71aea7, 0xff621919, 0xff18462f, 0xffbf8e52, 0xff001f79, - 0xffa8afc2, 0xff6b64c4, 0xff738359, 0xff9d50a4 - }; - - private static final String CONVERSATION_COLOR = - "@CASE (" + MessageColumns.ACCOUNT_KEY + " - 1) % " + ACCOUNT_COLORS.length + - " WHEN 0 THEN " + ACCOUNT_COLORS[0] + - " WHEN 1 THEN " + ACCOUNT_COLORS[1] + - " WHEN 2 THEN " + ACCOUNT_COLORS[2] + - " WHEN 3 THEN " + ACCOUNT_COLORS[3] + - " WHEN 4 THEN " + ACCOUNT_COLORS[4] + - " WHEN 5 THEN " + ACCOUNT_COLORS[5] + - " WHEN 6 THEN " + ACCOUNT_COLORS[6] + - " WHEN 7 THEN " + ACCOUNT_COLORS[7] + - " WHEN 8 THEN " + ACCOUNT_COLORS[8] + - " END"; - - private static final String ACCOUNT_COLOR = - "@CASE (" + AccountColumns._ID + " - 1) % " + ACCOUNT_COLORS.length + - " WHEN 0 THEN " + ACCOUNT_COLORS[0] + - " WHEN 1 THEN " + ACCOUNT_COLORS[1] + - " WHEN 2 THEN " + ACCOUNT_COLORS[2] + - " WHEN 3 THEN " + ACCOUNT_COLORS[3] + - " WHEN 4 THEN " + ACCOUNT_COLORS[4] + - " WHEN 5 THEN " + ACCOUNT_COLORS[5] + - " WHEN 6 THEN " + ACCOUNT_COLORS[6] + - " WHEN 7 THEN " + ACCOUNT_COLORS[7] + - " WHEN 8 THEN " + ACCOUNT_COLORS[8] + - " END"; - - /** - * Mapping of UIProvider columns to EmailProvider columns for the message list (called the - * conversation list in UnifiedEmail) - */ - private static ProjectionMap getMessageListMap() { - if (sMessageListMap == null) { - sMessageListMap = ProjectionMap.builder() - .add(BaseColumns._ID, MessageColumns._ID) - .add(UIProvider.ConversationColumns.URI, uriWithId("uimessage")) - .add(UIProvider.ConversationColumns.MESSAGE_LIST_URI, uriWithId("uimessage")) - .add(UIProvider.ConversationColumns.SUBJECT, MessageColumns.SUBJECT) - .add(UIProvider.ConversationColumns.SNIPPET, MessageColumns.SNIPPET) - .add(UIProvider.ConversationColumns.CONVERSATION_INFO, null) - .add(UIProvider.ConversationColumns.DATE_RECEIVED_MS, MessageColumns.TIMESTAMP) - .add(UIProvider.ConversationColumns.HAS_ATTACHMENTS, MessageColumns.FLAG_ATTACHMENT) - .add(UIProvider.ConversationColumns.NUM_MESSAGES, "1") - .add(UIProvider.ConversationColumns.NUM_DRAFTS, "0") - .add(UIProvider.ConversationColumns.SENDING_STATE, - Integer.toString(ConversationSendingState.OTHER)) - .add(UIProvider.ConversationColumns.PRIORITY, - Integer.toString(ConversationPriority.LOW)) - .add(UIProvider.ConversationColumns.READ, MessageColumns.FLAG_READ) - .add(UIProvider.ConversationColumns.SEEN, MessageColumns.FLAG_SEEN) - .add(UIProvider.ConversationColumns.STARRED, MessageColumns.FLAG_FAVORITE) - .add(UIProvider.ConversationColumns.FLAGS, CONVERSATION_FLAGS) - .add(UIProvider.ConversationColumns.ACCOUNT_URI, - uriWithColumn("uiaccount", MessageColumns.ACCOUNT_KEY)) - .add(UIProvider.ConversationColumns.SENDER_INFO, MessageColumns.FROM_LIST) - .add(UIProvider.ConversationColumns.ORDER_KEY, MessageColumns.TIMESTAMP) - .build(); - } - return sMessageListMap; - } - private static ProjectionMap sMessageListMap; - - /** - * Generate UIProvider draft type; note the test for "reply all" must come before "reply" - */ - private static final String MESSAGE_DRAFT_TYPE = - "CASE WHEN (" + MessageColumns.FLAGS + "&" + Message.FLAG_TYPE_ORIGINAL + - ") !=0 THEN " + UIProvider.DraftType.COMPOSE + - " WHEN (" + MessageColumns.FLAGS + "&" + Message.FLAG_TYPE_REPLY_ALL + - ") !=0 THEN " + UIProvider.DraftType.REPLY_ALL + - " WHEN (" + MessageColumns.FLAGS + "&" + Message.FLAG_TYPE_REPLY + - ") !=0 THEN " + UIProvider.DraftType.REPLY + - " WHEN (" + MessageColumns.FLAGS + "&" + Message.FLAG_TYPE_FORWARD + - ") !=0 THEN " + UIProvider.DraftType.FORWARD + - " ELSE " + UIProvider.DraftType.NOT_A_DRAFT + " END"; - - private static final String MESSAGE_FLAGS = - "CASE WHEN (" + MessageColumns.FLAGS + "&" + Message.FLAG_INCOMING_MEETING_INVITE + - ") !=0 THEN " + UIProvider.MessageFlags.CALENDAR_INVITE + - " ELSE 0 END"; - - /** - * Mapping of UIProvider columns to EmailProvider columns for a detailed message view in - * UnifiedEmail - */ - private static ProjectionMap getMessageViewMap() { - if (sMessageViewMap == null) { - sMessageViewMap = ProjectionMap.builder() - .add(BaseColumns._ID, Message.TABLE_NAME + "." + MessageColumns._ID) - .add(UIProvider.MessageColumns.SERVER_ID, SyncColumns.SERVER_ID) - .add(UIProvider.MessageColumns.URI, uriWithFQId("uimessage", Message.TABLE_NAME)) - .add(UIProvider.MessageColumns.CONVERSATION_ID, - uriWithFQId("uimessage", Message.TABLE_NAME)) - .add(UIProvider.MessageColumns.SUBJECT, MessageColumns.SUBJECT) - .add(UIProvider.MessageColumns.SNIPPET, MessageColumns.SNIPPET) - .add(UIProvider.MessageColumns.FROM, MessageColumns.FROM_LIST) - .add(UIProvider.MessageColumns.TO, MessageColumns.TO_LIST) - .add(UIProvider.MessageColumns.CC, MessageColumns.CC_LIST) - .add(UIProvider.MessageColumns.BCC, MessageColumns.BCC_LIST) - .add(UIProvider.MessageColumns.REPLY_TO, MessageColumns.REPLY_TO_LIST) - .add(UIProvider.MessageColumns.DATE_RECEIVED_MS, MessageColumns.TIMESTAMP) - .add(UIProvider.MessageColumns.BODY_HTML, null) // Loaded in EmailMessageCursor - .add(UIProvider.MessageColumns.BODY_TEXT, null) // Loaded in EmailMessageCursor - .add(UIProvider.MessageColumns.REF_MESSAGE_ID, "0") - .add(UIProvider.MessageColumns.DRAFT_TYPE, NOT_A_DRAFT_STRING) - .add(UIProvider.MessageColumns.APPEND_REF_MESSAGE_CONTENT, "0") - .add(UIProvider.MessageColumns.HAS_ATTACHMENTS, MessageColumns.FLAG_ATTACHMENT) - .add(UIProvider.MessageColumns.ATTACHMENT_LIST_URI, - uriWithFQId("uiattachments", Message.TABLE_NAME)) - .add(UIProvider.MessageColumns.ATTACHMENT_BY_CID_URI, - uriWithFQId("uiattachmentbycid", Message.TABLE_NAME)) - .add(UIProvider.MessageColumns.MESSAGE_FLAGS, MESSAGE_FLAGS) - .add(UIProvider.MessageColumns.DRAFT_TYPE, MESSAGE_DRAFT_TYPE) - .add(UIProvider.MessageColumns.MESSAGE_ACCOUNT_URI, - uriWithColumn("uiaccount", MessageColumns.ACCOUNT_KEY)) - .add(UIProvider.MessageColumns.STARRED, MessageColumns.FLAG_FAVORITE) - .add(UIProvider.MessageColumns.READ, MessageColumns.FLAG_READ) - .add(UIProvider.MessageColumns.SEEN, MessageColumns.FLAG_SEEN) - .add(UIProvider.MessageColumns.SPAM_WARNING_STRING, null) - .add(UIProvider.MessageColumns.SPAM_WARNING_LEVEL, - Integer.toString(UIProvider.SpamWarningLevel.NO_WARNING)) - .add(UIProvider.MessageColumns.SPAM_WARNING_LINK_TYPE, - Integer.toString(UIProvider.SpamWarningLinkType.NO_LINK)) - .add(UIProvider.MessageColumns.VIA_DOMAIN, null) - .add(UIProvider.MessageColumns.CLIPPED, "0") - .add(UIProvider.MessageColumns.PERMALINK, null) - .build(); - } - return sMessageViewMap; - } - private static ProjectionMap sMessageViewMap; - - /** - * Generate UIProvider folder capabilities from mailbox flags - */ - private static final String FOLDER_CAPABILITIES = - "CASE WHEN (" + MailboxColumns.FLAGS + "&" + Mailbox.FLAG_ACCEPTS_MOVED_MAIL + - ") !=0 THEN " + UIProvider.FolderCapabilities.CAN_ACCEPT_MOVED_MESSAGES + - " ELSE 0 END"; - - /** - * Convert EmailProvider type to UIProvider type - */ - private static final String FOLDER_TYPE = "CASE " + MailboxColumns.TYPE - + " WHEN " + Mailbox.TYPE_INBOX + " THEN " + UIProvider.FolderType.INBOX - + " WHEN " + Mailbox.TYPE_DRAFTS + " THEN " + UIProvider.FolderType.DRAFT - + " WHEN " + Mailbox.TYPE_OUTBOX + " THEN " + UIProvider.FolderType.OUTBOX - + " WHEN " + Mailbox.TYPE_SENT + " THEN " + UIProvider.FolderType.SENT - + " WHEN " + Mailbox.TYPE_TRASH + " THEN " + UIProvider.FolderType.TRASH - + " WHEN " + Mailbox.TYPE_JUNK + " THEN " + UIProvider.FolderType.SPAM - + " WHEN " + Mailbox.TYPE_STARRED + " THEN " + UIProvider.FolderType.STARRED - + " WHEN " + Mailbox.TYPE_UNREAD + " THEN " + UIProvider.FolderType.UNREAD - + " WHEN " + Mailbox.TYPE_SEARCH + " THEN " - + getFolderTypeFromMailboxType(Mailbox.TYPE_SEARCH) - + " ELSE " + UIProvider.FolderType.DEFAULT + " END"; - - private static final String FOLDER_ICON = "CASE " + MailboxColumns.TYPE - + " WHEN " + Mailbox.TYPE_INBOX + " THEN " + R.drawable.ic_drawer_inbox_24dp - + " WHEN " + Mailbox.TYPE_DRAFTS + " THEN " + R.drawable.ic_drawer_drafts_24dp - + " WHEN " + Mailbox.TYPE_OUTBOX + " THEN " + R.drawable.ic_drawer_outbox_24dp - + " WHEN " + Mailbox.TYPE_SENT + " THEN " + R.drawable.ic_drawer_sent_24dp - + " WHEN " + Mailbox.TYPE_TRASH + " THEN " + R.drawable.ic_drawer_trash_24dp - + " WHEN " + Mailbox.TYPE_STARRED + " THEN " + R.drawable.ic_drawer_starred_24dp - + " ELSE " + R.drawable.ic_drawer_folder_24dp + " END"; - - /** - * Local-only folders set totalCount < 0; such folders should substitute message count for - * total count. - * TODO: IMAP and POP don't adhere to this convention yet so for now we force a few types. - */ - private static final String TOTAL_COUNT = "CASE WHEN " - + MailboxColumns.TOTAL_COUNT + "<0 OR " - + MailboxColumns.TYPE + "=" + Mailbox.TYPE_DRAFTS + " OR " - + MailboxColumns.TYPE + "=" + Mailbox.TYPE_OUTBOX + " OR " - + MailboxColumns.TYPE + "=" + Mailbox.TYPE_TRASH - + " THEN " + MailboxColumns.MESSAGE_COUNT - + " ELSE " + MailboxColumns.TOTAL_COUNT + " END"; - - private static ProjectionMap getFolderListMap() { - if (sFolderListMap == null) { - sFolderListMap = ProjectionMap.builder() - .add(BaseColumns._ID, MailboxColumns._ID) - .add(UIProvider.FolderColumns.PERSISTENT_ID, MailboxColumns.SERVER_ID) - .add(UIProvider.FolderColumns.URI, uriWithId("uifolder")) - .add(UIProvider.FolderColumns.NAME, "displayName") - .add(UIProvider.FolderColumns.HAS_CHILDREN, - MailboxColumns.FLAGS + "&" + Mailbox.FLAG_HAS_CHILDREN) - .add(UIProvider.FolderColumns.CAPABILITIES, FOLDER_CAPABILITIES) - .add(UIProvider.FolderColumns.SYNC_WINDOW, "3") - .add(UIProvider.FolderColumns.CONVERSATION_LIST_URI, uriWithId("uimessages")) - .add(UIProvider.FolderColumns.CHILD_FOLDERS_LIST_URI, uriWithId("uisubfolders")) - .add(UIProvider.FolderColumns.UNREAD_COUNT, MailboxColumns.UNREAD_COUNT) - .add(UIProvider.FolderColumns.TOTAL_COUNT, TOTAL_COUNT) - .add(UIProvider.FolderColumns.REFRESH_URI, uriWithId(QUERY_UIREFRESH)) - .add(UIProvider.FolderColumns.SYNC_STATUS, MailboxColumns.UI_SYNC_STATUS) - .add(UIProvider.FolderColumns.LAST_SYNC_RESULT, MailboxColumns.UI_LAST_SYNC_RESULT) - .add(UIProvider.FolderColumns.TYPE, FOLDER_TYPE) - .add(UIProvider.FolderColumns.ICON_RES_ID, FOLDER_ICON) - .add(UIProvider.FolderColumns.LOAD_MORE_URI, uriWithId("uiloadmore")) - .add(UIProvider.FolderColumns.HIERARCHICAL_DESC, MailboxColumns.HIERARCHICAL_NAME) - .add(UIProvider.FolderColumns.PARENT_URI, "case when " + MailboxColumns.PARENT_KEY - + "=" + Mailbox.NO_MAILBOX + " then NULL else " + - uriWithColumn("uifolder", MailboxColumns.PARENT_KEY) + " end") - /** - * SELECT group_concat(fromList) FROM - * (SELECT fromList FROM message WHERE mailboxKey=? AND flagRead=0 - * GROUP BY fromList ORDER BY timestamp DESC) - */ - .add(UIProvider.FolderColumns.UNREAD_SENDERS, - "(SELECT group_concat(" + MessageColumns.FROM_LIST + ") FROM " + - "(SELECT " + MessageColumns.FROM_LIST + " FROM " + Message.TABLE_NAME + - " WHERE " + MessageColumns.MAILBOX_KEY + "=" + Mailbox.TABLE_NAME + "." + - MailboxColumns._ID + " AND " + MessageColumns.FLAG_READ + "=0" + - " GROUP BY " + MessageColumns.FROM_LIST + " ORDER BY " + - MessageColumns.TIMESTAMP + " DESC))") - .build(); - } - return sFolderListMap; - } - private static ProjectionMap sFolderListMap; - - /** - * Constructs the map of default entries for accounts. These values can be overridden in - * {@link #genQueryAccount(String[], String)}. - */ - private static ProjectionMap getAccountListMap(Context context) { - if (sAccountListMap == null) { - final ProjectionMap.Builder builder = ProjectionMap.builder() - .add(BaseColumns._ID, AccountColumns._ID) - .add(UIProvider.AccountColumns.FOLDER_LIST_URI, uriWithId("uifolders")) - .add(UIProvider.AccountColumns.FULL_FOLDER_LIST_URI, uriWithId("uifullfolders")) - .add(UIProvider.AccountColumns.ALL_FOLDER_LIST_URI, uriWithId("uiallfolders")) - .add(UIProvider.AccountColumns.NAME, AccountColumns.DISPLAY_NAME) - .add(UIProvider.AccountColumns.ACCOUNT_MANAGER_NAME, - AccountColumns.EMAIL_ADDRESS) - .add(UIProvider.AccountColumns.ACCOUNT_ID, - AccountColumns.EMAIL_ADDRESS) - .add(UIProvider.AccountColumns.SENDER_NAME, - AccountColumns.SENDER_NAME) - .add(UIProvider.AccountColumns.UNDO_URI, - ("'content://" + EmailContent.AUTHORITY + "/uiundo'")) - .add(UIProvider.AccountColumns.URI, uriWithId("uiaccount")) - .add(UIProvider.AccountColumns.SEARCH_URI, uriWithId("uisearch")) - // TODO: Is provider version used? - .add(UIProvider.AccountColumns.PROVIDER_VERSION, "1") - .add(UIProvider.AccountColumns.SYNC_STATUS, "0") - .add(UIProvider.AccountColumns.RECENT_FOLDER_LIST_URI, - uriWithId("uirecentfolders")) - .add(UIProvider.AccountColumns.DEFAULT_RECENT_FOLDER_LIST_URI, - uriWithId("uidefaultrecentfolders")) - .add(UIProvider.AccountColumns.SettingsColumns.SIGNATURE, - AccountColumns.SIGNATURE) - .add(UIProvider.AccountColumns.SettingsColumns.SNAP_HEADERS, - Integer.toString(UIProvider.SnapHeaderValue.ALWAYS)) - .add(UIProvider.AccountColumns.SettingsColumns.CONFIRM_ARCHIVE, "0") - .add(UIProvider.AccountColumns.SettingsColumns.CONVERSATION_VIEW_MODE, - Integer.toString(UIProvider.ConversationViewMode.UNDEFINED)) - .add(UIProvider.AccountColumns.SettingsColumns.VEILED_ADDRESS_PATTERN, null); - - final String feedbackUri = context.getString(R.string.email_feedback_uri); - if (!TextUtils.isEmpty(feedbackUri)) { - // This string needs to be in single quotes, as it will be used as a constant - // in a sql expression - builder.add(UIProvider.AccountColumns.SEND_FEEDBACK_INTENT_URI, - "'" + feedbackUri + "'"); - } - - final String helpUri = context.getString(R.string.help_uri); - if (!TextUtils.isEmpty(helpUri)) { - // This string needs to be in single quotes, as it will be used as a constant - // in a sql expression - builder.add(UIProvider.AccountColumns.HELP_INTENT_URI, - "'" + helpUri + "'"); - } - - sAccountListMap = builder.build(); - } - return sAccountListMap; - } - private static ProjectionMap sAccountListMap; - - private static ProjectionMap getQuickResponseMap() { - if (sQuickResponseMap == null) { - sQuickResponseMap = ProjectionMap.builder() - .add(UIProvider.QuickResponseColumns.TEXT, QuickResponseColumns.TEXT) - .add(UIProvider.QuickResponseColumns.URI, - "'" + combinedUriString("quickresponse", "") + "'||" - + QuickResponseColumns._ID) - .build(); - } - return sQuickResponseMap; - } - private static ProjectionMap sQuickResponseMap; - - /** - * The "ORDER BY" clause for top level folders - */ - private static final String MAILBOX_ORDER_BY = "CASE " + MailboxColumns.TYPE - + " WHEN " + Mailbox.TYPE_INBOX + " THEN 0" - + " WHEN " + Mailbox.TYPE_DRAFTS + " THEN 1" - + " WHEN " + Mailbox.TYPE_OUTBOX + " THEN 2" - + " WHEN " + Mailbox.TYPE_SENT + " THEN 3" - + " WHEN " + Mailbox.TYPE_TRASH + " THEN 4" - + " WHEN " + Mailbox.TYPE_JUNK + " THEN 5" - // Other mailboxes (i.e. of Mailbox.TYPE_MAIL) are shown in alphabetical order. - + " ELSE 10 END" - + " ," + MailboxColumns.DISPLAY_NAME + " COLLATE LOCALIZED ASC"; - - /** - * Mapping of UIProvider columns to EmailProvider columns for a message's attachments - */ - private static ProjectionMap getAttachmentMap() { - if (sAttachmentMap == null) { - sAttachmentMap = ProjectionMap.builder() - .add(UIProvider.AttachmentColumns.NAME, AttachmentColumns.FILENAME) - .add(UIProvider.AttachmentColumns.SIZE, AttachmentColumns.SIZE) - .add(UIProvider.AttachmentColumns.URI, uriWithId("uiattachment")) - .add(UIProvider.AttachmentColumns.CONTENT_TYPE, AttachmentColumns.MIME_TYPE) - .add(UIProvider.AttachmentColumns.STATE, AttachmentColumns.UI_STATE) - .add(UIProvider.AttachmentColumns.DESTINATION, AttachmentColumns.UI_DESTINATION) - .add(UIProvider.AttachmentColumns.DOWNLOADED_SIZE, - AttachmentColumns.UI_DOWNLOADED_SIZE) - .add(UIProvider.AttachmentColumns.CONTENT_URI, AttachmentColumns.CONTENT_URI) - .add(UIProvider.AttachmentColumns.FLAGS, AttachmentColumns.FLAGS) - .build(); - } - return sAttachmentMap; - } - private static ProjectionMap sAttachmentMap; - - /** - * Generate the SELECT clause using a specified mapping and the original UI projection - * @param map the ProjectionMap to use for this projection - * @param projection the projection as sent by UnifiedEmail - * @return a StringBuilder containing the SELECT expression for a SQLite query - */ - private static StringBuilder genSelect(ProjectionMap map, String[] projection) { - return genSelect(map, projection, EMPTY_CONTENT_VALUES); - } - - private static StringBuilder genSelect(ProjectionMap map, String[] projection, - ContentValues values) { - final StringBuilder sb = new StringBuilder("SELECT "); - boolean first = true; - for (final String column: projection) { - if (first) { - first = false; - } else { - sb.append(','); - } - final String val; - // First look at values; this is an override of default behavior - if (values.containsKey(column)) { - final String value = values.getAsString(column); - if (value == null) { - val = "NULL AS " + column; - } else if (value.startsWith("@")) { - val = value.substring(1) + " AS " + column; - } else { - val = DatabaseUtils.sqlEscapeString(value) + " AS " + column; - } - } else { - // Now, get the standard value for the column from our projection map - final String mapVal = map.get(column); - // If we don't have the column, return "NULL AS ", and warn - if (mapVal == null) { - val = "NULL AS " + column; - // Apparently there's a lot of these, so don't spam the log with warnings - // LogUtils.w(TAG, "column " + column + " missing from projection map"); - } else { - val = mapVal; - } - } - sb.append(val); - } - return sb; - } - - /** - * Convenience method to create a Uri string given the "type" of query; we append the type - * of the query and the id column name (_id) - * - * @param type the "type" of the query, as defined by our UriMatcher definitions - * @return a Uri string - */ - private static String uriWithId(String type) { - return uriWithColumn(type, BaseColumns._ID); - } - - /** - * Convenience method to create a Uri string given the "type" of query; we append the type - * of the query and the passed in column name - * - * @param type the "type" of the query, as defined by our UriMatcher definitions - * @param columnName the column in the table being queried - * @return a Uri string - */ - private static String uriWithColumn(String type, String columnName) { - return "'content://" + EmailContent.AUTHORITY + "/" + type + "/' || " + columnName; - } - - /** - * Convenience method to create a Uri string given the "type" of query and the table name to - * which it applies; we append the type of the query and the fully qualified (FQ) id column - * (i.e. including the table name); we need this for join queries where _id would otherwise - * be ambiguous - * - * @param type the "type" of the query, as defined by our UriMatcher definitions - * @param tableName the name of the table whose _id is referred to - * @return a Uri string - */ - private static String uriWithFQId(String type, String tableName) { - return "'content://" + EmailContent.AUTHORITY + "/" + type + "/' || " + tableName + "._id"; - } - - // Regex that matches start of img tag. '<(?i)img\s+'. - private static final Pattern IMG_TAG_START_REGEX = Pattern.compile("<(?i)img\\s+"); - - /** - * Class that holds the sqlite query and the attachment (JSON) value (which might be null) - */ - private static class MessageQuery { - final String query; - final String attachmentJson; - - MessageQuery(String _query, String _attachmentJson) { - query = _query; - attachmentJson = _attachmentJson; - } - } - - /** - * Generate the "view message" SQLite query, given a projection from UnifiedEmail - * - * @param uiProjection as passed from UnifiedEmail - * @return the SQLite query to be executed on the EmailProvider database - */ - private MessageQuery genQueryViewMessage(String[] uiProjection, String id) { - Context context = getContext(); - long messageId = Long.parseLong(id); - Message msg = Message.restoreMessageWithId(context, messageId); - ContentValues values = new ContentValues(); - String attachmentJson = null; - if (msg != null) { - Body body = Body.restoreBodyWithMessageId(context, messageId); - if (body != null) { - if (body.mHtmlContent != null) { - if (IMG_TAG_START_REGEX.matcher(body.mHtmlContent).find()) { - values.put(UIProvider.MessageColumns.EMBEDS_EXTERNAL_RESOURCES, 1); - } - } - } - Address[] fromList = Address.fromHeader(msg.mFrom); - int autoShowImages = 0; - final MailPrefs mailPrefs = MailPrefs.get(context); - for (Address sender : fromList) { - final String email = sender.getAddress(); - if (mailPrefs.getDisplayImagesFromSender(email)) { - autoShowImages = 1; - break; - } - } - values.put(UIProvider.MessageColumns.ALWAYS_SHOW_IMAGES, autoShowImages); - // Add attachments... - Attachment[] atts = Attachment.restoreAttachmentsWithMessageId(context, messageId); - if (atts.length > 0) { - ArrayList uiAtts = - new ArrayList(); - for (Attachment att : atts) { - // TODO: This code is intended to strip out any inlined attachments (which - // would have a non-null contentId) so that they will not display at the bottom - // along with the non-inlined attachments. - // The problem is that the UI_ATTACHMENTS query does not behave the same way, - // which causes crazy formatting. - // There is an open question here, should attachments that are inlined - // ALSO appear in the list of attachments at the bottom with the non-inlined - // attachments? - // Either way, the two queries need to behave the same way. - // As of now, they will. If we decide to stop this, then we need to enable - // the code below, and then also make the UI_ATTACHMENTS query behave - // the same way. -// -// if (att.mContentId != null && att.getContentUri() != null) { -// continue; -// } - com.android.mail.providers.Attachment uiAtt = - new com.android.mail.providers.Attachment(); - uiAtt.setName(att.mFileName); - uiAtt.setContentType(att.mMimeType); - uiAtt.size = (int) att.mSize; - uiAtt.uri = uiUri("uiattachment", att.mId); - uiAtt.flags = att.mFlags; - uiAtts.add(uiAtt); - } - values.put(UIProvider.MessageColumns.ATTACHMENTS, "@?"); // @ for literal - attachmentJson = com.android.mail.providers.Attachment.toJSONArray(uiAtts); - } - if (msg.mDraftInfo != 0) { - values.put(UIProvider.MessageColumns.APPEND_REF_MESSAGE_CONTENT, - (msg.mDraftInfo & Message.DRAFT_INFO_APPEND_REF_MESSAGE) != 0 ? 1 : 0); - values.put(UIProvider.MessageColumns.QUOTE_START_POS, - msg.mDraftInfo & Message.DRAFT_INFO_QUOTE_POS_MASK); - } - if ((msg.mFlags & Message.FLAG_INCOMING_MEETING_INVITE) != 0) { - values.put(UIProvider.MessageColumns.EVENT_INTENT_URI, - "content://ui.email2.android.com/event/" + msg.mId); - } - /** - * HACK: override the attachment uri to contain a query parameter - * This forces the message footer to reload the attachment display when the message is - * fully loaded. - */ - final Uri attachmentListUri = uiUri("uiattachments", messageId).buildUpon() - .appendQueryParameter("MessageLoaded", - msg.mFlagLoaded == Message.FLAG_LOADED_COMPLETE ? "true" : "false") - .build(); - values.put(UIProvider.MessageColumns.ATTACHMENT_LIST_URI, attachmentListUri.toString()); - } - StringBuilder sb = genSelect(getMessageViewMap(), uiProjection, values); - sb.append(" FROM " + Message.TABLE_NAME + " LEFT JOIN " + Body.TABLE_NAME + - " ON " + BodyColumns.MESSAGE_KEY + "=" + Message.TABLE_NAME + "." + - MessageColumns._ID + - " WHERE " + Message.TABLE_NAME + "." + MessageColumns._ID + "=?"); - String sql = sb.toString(); - return new MessageQuery(sql, attachmentJson); - } - - private static void appendConversationInfoColumns(final StringBuilder stringBuilder) { - // TODO(skennedy) These columns are needed for the respond call for ConversationInfo :( - // There may be a better way to do this, but since the projection is specified by the - // unified UI code, it can't ask for these columns. - stringBuilder.append(',').append(MessageColumns.DISPLAY_NAME) - .append(',').append(MessageColumns.FROM_LIST) - .append(',').append(MessageColumns.TO_LIST); - } - - /** - * Generate the "message list" SQLite query, given a projection from UnifiedEmail - * - * @param uiProjection as passed from UnifiedEmail - * @param unseenOnly true to only return unseen messages - * @return the SQLite query to be executed on the EmailProvider database - */ - private static String genQueryMailboxMessages(String[] uiProjection, final boolean unseenOnly) { - StringBuilder sb = genSelect(getMessageListMap(), uiProjection); - appendConversationInfoColumns(sb); - sb.append(" FROM " + Message.TABLE_NAME + " WHERE " + - Message.FLAG_LOADED_SELECTION + " AND " + - MessageColumns.MAILBOX_KEY + "=? "); - if (unseenOnly) { - sb.append("AND ").append(MessageColumns.FLAG_SEEN).append(" = 0 "); - sb.append("AND ").append(MessageColumns.FLAG_READ).append(" = 0 "); - } - sb.append("ORDER BY " + MessageColumns.TIMESTAMP + " DESC "); - sb.append("LIMIT " + UIProvider.CONVERSATION_PROJECTION_QUERY_CURSOR_WINDOW_LIMIT); - return sb.toString(); - } - - /** - * Generate various virtual mailbox SQLite queries, given a projection from UnifiedEmail - * - * @param uiProjection as passed from UnifiedEmail - * @param mailboxId the id of the virtual mailbox - * @param unseenOnly true to only return unseen messages - * @return the SQLite query to be executed on the EmailProvider database - */ - private static Cursor getVirtualMailboxMessagesCursor(SQLiteDatabase db, String[] uiProjection, - long mailboxId, final boolean unseenOnly) { - ContentValues values = new ContentValues(); - values.put(UIProvider.ConversationColumns.COLOR, CONVERSATION_COLOR); - final int virtualMailboxId = getVirtualMailboxType(mailboxId); - final String[] selectionArgs; - StringBuilder sb = genSelect(getMessageListMap(), uiProjection, values); - appendConversationInfoColumns(sb); - sb.append(" FROM " + Message.TABLE_NAME + " WHERE " + - Message.FLAG_LOADED_SELECTION + " AND "); - if (isCombinedMailbox(mailboxId)) { - if (unseenOnly) { - sb.append(MessageColumns.FLAG_SEEN).append("=0 AND "); - sb.append(MessageColumns.FLAG_READ).append("=0 AND "); - } - selectionArgs = null; - } else { - if (virtualMailboxId == Mailbox.TYPE_INBOX) { - throw new IllegalArgumentException("No virtual mailbox for: " + mailboxId); - } - sb.append(MessageColumns.ACCOUNT_KEY).append("=? AND "); - selectionArgs = new String[]{getVirtualMailboxAccountIdString(mailboxId)}; - } - switch (getVirtualMailboxType(mailboxId)) { - case Mailbox.TYPE_INBOX: - sb.append(MessageColumns.MAILBOX_KEY + " IN (SELECT " + MailboxColumns._ID + - " FROM " + Mailbox.TABLE_NAME + " WHERE " + MailboxColumns.TYPE + - "=" + Mailbox.TYPE_INBOX + ")"); - break; - case Mailbox.TYPE_STARRED: - sb.append(MessageColumns.FLAG_FAVORITE + "=1"); - break; - case Mailbox.TYPE_UNREAD: - sb.append(MessageColumns.FLAG_READ + "=0 AND " + MessageColumns.MAILBOX_KEY + - " NOT IN (SELECT " + MailboxColumns._ID + " FROM " + Mailbox.TABLE_NAME + - " WHERE " + MailboxColumns.TYPE + "=" + Mailbox.TYPE_TRASH + ")"); - break; - default: - throw new IllegalArgumentException("No virtual mailbox for: " + mailboxId); - } - sb.append(" ORDER BY " + MessageColumns.TIMESTAMP + " DESC"); - return db.rawQuery(sb.toString(), selectionArgs); - } - - /** - * Generate the "message list" SQLite query, given a projection from UnifiedEmail - * - * @param uiProjection as passed from UnifiedEmail - * @return the SQLite query to be executed on the EmailProvider database - */ - private static String genQueryConversation(String[] uiProjection) { - StringBuilder sb = genSelect(getMessageListMap(), uiProjection); - sb.append(" FROM " + Message.TABLE_NAME + " WHERE " + MessageColumns._ID + "=?"); - return sb.toString(); - } - - /** - * Generate the "top level folder list" SQLite query, given a projection from UnifiedEmail - * - * @param uiProjection as passed from UnifiedEmail - * @return the SQLite query to be executed on the EmailProvider database - */ - private static String genQueryAccountMailboxes(String[] uiProjection) { - StringBuilder sb = genSelect(getFolderListMap(), uiProjection); - sb.append(" FROM " + Mailbox.TABLE_NAME + " WHERE " + MailboxColumns.ACCOUNT_KEY + - "=? AND " + MailboxColumns.TYPE + " < " + Mailbox.TYPE_NOT_EMAIL + - " AND " + MailboxColumns.TYPE + " != " + Mailbox.TYPE_SEARCH + - " AND " + MailboxColumns.PARENT_KEY + " < 0 ORDER BY "); - sb.append(MAILBOX_ORDER_BY); - return sb.toString(); - } - - /** - * Generate the "all folders" SQLite query, given a projection from UnifiedEmail. The list is - * sorted by the name as it appears in a hierarchical listing - * - * @param uiProjection as passed from UnifiedEmail - * @return the SQLite query to be executed on the EmailProvider database - */ - private static String genQueryAccountAllMailboxes(String[] uiProjection) { - StringBuilder sb = genSelect(getFolderListMap(), uiProjection); - // Use a derived column to choose either hierarchicalName or displayName - sb.append(", case when " + MailboxColumns.HIERARCHICAL_NAME + " is null then " + - MailboxColumns.DISPLAY_NAME + " else " + MailboxColumns.HIERARCHICAL_NAME + - " end as h_name"); - // Order by the derived column - sb.append(" FROM " + Mailbox.TABLE_NAME + " WHERE " + MailboxColumns.ACCOUNT_KEY + - "=? AND " + MailboxColumns.TYPE + " < " + Mailbox.TYPE_NOT_EMAIL + - " AND " + MailboxColumns.TYPE + " != " + Mailbox.TYPE_SEARCH + - " ORDER BY h_name"); - return sb.toString(); - } - - /** - * Generate the "recent folder list" SQLite query, given a projection from UnifiedEmail - * - * @param uiProjection as passed from UnifiedEmail - * @return the SQLite query to be executed on the EmailProvider database - */ - private static String genQueryRecentMailboxes(String[] uiProjection) { - StringBuilder sb = genSelect(getFolderListMap(), uiProjection); - sb.append(" FROM " + Mailbox.TABLE_NAME + " WHERE " + MailboxColumns.ACCOUNT_KEY + - "=? AND " + MailboxColumns.TYPE + " < " + Mailbox.TYPE_NOT_EMAIL + - " AND " + MailboxColumns.TYPE + " != " + Mailbox.TYPE_SEARCH + - " AND " + MailboxColumns.PARENT_KEY + " < 0 AND " + - MailboxColumns.LAST_TOUCHED_TIME + " > 0 ORDER BY " + - MailboxColumns.LAST_TOUCHED_TIME + " DESC"); - return sb.toString(); - } - - private int getFolderCapabilities(EmailServiceInfo info, int mailboxType, long mailboxId) { - // Special case for Search folders: only permit delete, do not try to give any other caps. - if (mailboxType == Mailbox.TYPE_SEARCH) { - return UIProvider.FolderCapabilities.DELETE; - } - - // All folders support delete, except drafts. - int caps = 0; - if (mailboxType != Mailbox.TYPE_DRAFTS) { - caps = UIProvider.FolderCapabilities.DELETE; - } - if (info != null && info.offerLookback) { - // Protocols supporting lookback support settings - caps |= UIProvider.FolderCapabilities.SUPPORTS_SETTINGS; - } - - if (mailboxType == Mailbox.TYPE_MAIL || mailboxType == Mailbox.TYPE_TRASH || - mailboxType == Mailbox.TYPE_JUNK || mailboxType == Mailbox.TYPE_INBOX) { - // If the mailbox can accept moved mail, report that as well - caps |= UIProvider.FolderCapabilities.CAN_ACCEPT_MOVED_MESSAGES; - caps |= UIProvider.FolderCapabilities.ALLOWS_REMOVE_CONVERSATION; - } - - // For trash, we don't allow undo - if (mailboxType == Mailbox.TYPE_TRASH) { - caps = UIProvider.FolderCapabilities.CAN_ACCEPT_MOVED_MESSAGES | - UIProvider.FolderCapabilities.ALLOWS_REMOVE_CONVERSATION | - UIProvider.FolderCapabilities.DELETE | - UIProvider.FolderCapabilities.DELETE_ACTION_FINAL; - } - if (isVirtualMailbox(mailboxId)) { - caps |= UIProvider.FolderCapabilities.IS_VIRTUAL; - } - - // If we don't know the protocol or the protocol doesn't support it, don't allow moving - // messages - if (info == null || !info.offerMoveTo) { - caps &= ~UIProvider.FolderCapabilities.CAN_ACCEPT_MOVED_MESSAGES & - ~UIProvider.FolderCapabilities.ALLOWS_REMOVE_CONVERSATION & - ~UIProvider.FolderCapabilities.ALLOWS_MOVE_TO_INBOX; - } - - // If the mailbox stores outgoing mail, show recipients instead of senders - // (however the Drafts folder shows neither senders nor recipients... just the word "Draft") - if (mailboxType == Mailbox.TYPE_OUTBOX || mailboxType == Mailbox.TYPE_SENT) { - caps |= UIProvider.FolderCapabilities.SHOW_RECIPIENTS; - } - - return caps; - } - - /** - * Generate a "single mailbox" SQLite query, given a projection from UnifiedEmail - * - * @param uiProjection as passed from UnifiedEmail - * @return the SQLite query to be executed on the EmailProvider database - */ - private String genQueryMailbox(String[] uiProjection, String id) { - long mailboxId = Long.parseLong(id); - ContentValues values = new ContentValues(3); - if (mSearchParams != null && mailboxId == mSearchParams.mSearchMailboxId) { - // "load more" is valid for search results - values.put(UIProvider.FolderColumns.LOAD_MORE_URI, - uiUriString("uiloadmore", mailboxId)); - values.put(UIProvider.FolderColumns.CAPABILITIES, UIProvider.FolderCapabilities.DELETE); - } else { - Context context = getContext(); - Mailbox mailbox = Mailbox.restoreMailboxWithId(context, mailboxId); - // Make sure we can't get NPE if mailbox has disappeared (the result will end up moot) - if (mailbox != null) { - String protocol = Account.getProtocol(context, mailbox.mAccountKey); - EmailServiceInfo info = EmailServiceUtils.getServiceInfo(context, protocol); - // All folders support delete - if (info != null && info.offerLoadMore) { - // "load more" is valid for protocols not supporting "lookback" - values.put(UIProvider.FolderColumns.LOAD_MORE_URI, - uiUriString("uiloadmore", mailboxId)); - } - values.put(UIProvider.FolderColumns.CAPABILITIES, - getFolderCapabilities(info, mailbox.mType, mailboxId)); - // The persistent id is used to form a filename, so we must ensure that it doesn't - // include illegal characters (such as '/'). Only perform the encoding if this - // query wants the persistent id. - boolean shouldEncodePersistentId = false; - if (uiProjection == null) { - shouldEncodePersistentId = true; - } else { - for (final String column : uiProjection) { - if (TextUtils.equals(column, UIProvider.FolderColumns.PERSISTENT_ID)) { - shouldEncodePersistentId = true; - break; - } - } - } - if (shouldEncodePersistentId) { - values.put(UIProvider.FolderColumns.PERSISTENT_ID, - Base64.encodeToString(mailbox.mServerId.getBytes(), - Base64.URL_SAFE | Base64.NO_WRAP | Base64.NO_PADDING)); - } - } - } - StringBuilder sb = genSelect(getFolderListMap(), uiProjection, values); - sb.append(" FROM " + Mailbox.TABLE_NAME + " WHERE " + MailboxColumns._ID + "=?"); - return sb.toString(); - } - - public static final String LEGACY_AUTHORITY = "ui.email.android.com"; - private static final Uri BASE_EXTERNAL_URI = Uri.parse("content://" + LEGACY_AUTHORITY); - - private static final Uri BASE_EXTERAL_URI2 = Uri.parse("content://ui.email2.android.com"); - - private static String getExternalUriString(String segment, String account) { - return BASE_EXTERNAL_URI.buildUpon().appendPath(segment) - .appendQueryParameter("account", account).build().toString(); - } - - private static String getExternalUriStringEmail2(String segment, String account) { - return BASE_EXTERAL_URI2.buildUpon().appendPath(segment) - .appendQueryParameter("account", account).build().toString(); - } - - private static String getBits(int bitField) { - StringBuilder sb = new StringBuilder(" "); - for (int i = 0; i < 32; i++, bitField >>= 1) { - if ((bitField & 1) != 0) { - sb.append(i) - .append(" "); - } - } - return sb.toString(); - } - - private static int getCapabilities(Context context, final Account account) { - if (account == null) { - return 0; - } - // Account capabilities are based on protocol -- different protocols (and, for EAS, - // different protocol versions) support different feature sets. - final String protocol = account.getProtocol(context); - int capabilities; - if (TextUtils.equals(context.getString(R.string.protocol_imap), protocol) || - TextUtils.equals(context.getString(R.string.protocol_legacy_imap), protocol)) { - capabilities = AccountCapabilities.SYNCABLE_FOLDERS | - AccountCapabilities.SERVER_SEARCH | - AccountCapabilities.FOLDER_SERVER_SEARCH | - AccountCapabilities.UNDO | - AccountCapabilities.DISCARD_CONVERSATION_DRAFTS; - } else if (TextUtils.equals(context.getString(R.string.protocol_pop3), protocol)) { - capabilities = AccountCapabilities.UNDO | - AccountCapabilities.DISCARD_CONVERSATION_DRAFTS; - } else if (TextUtils.equals(context.getString(R.string.protocol_eas), protocol)) { - final String easVersion = account.mProtocolVersion; - double easVersionDouble = 2.5D; - if (easVersion != null) { - try { - easVersionDouble = Double.parseDouble(easVersion); - } catch (final NumberFormatException e) { - // Use the default (lowest) set of capabilities. - } - } - if (easVersionDouble >= 12.0D) { - capabilities = AccountCapabilities.SYNCABLE_FOLDERS | - AccountCapabilities.SERVER_SEARCH | - AccountCapabilities.FOLDER_SERVER_SEARCH | - AccountCapabilities.SMART_REPLY | - AccountCapabilities.UNDO | - AccountCapabilities.DISCARD_CONVERSATION_DRAFTS; - } else { - capabilities = AccountCapabilities.SYNCABLE_FOLDERS | - AccountCapabilities.SMART_REPLY | - AccountCapabilities.UNDO | - AccountCapabilities.DISCARD_CONVERSATION_DRAFTS; - } - } else { - LogUtils.w(TAG, "Unknown protocol for account %d", account.getId()); - return 0; - } - LogUtils.d(TAG, "getCapabilities() for %d (protocol %s): 0x%x %s", account.getId(), protocol, - capabilities, getBits(capabilities)); - - // If the configuration states that feedback is supported, add that capability - final Resources res = context.getResources(); - if (res.getBoolean(R.bool.feedback_supported)) { - capabilities |= AccountCapabilities.SEND_FEEDBACK; - } - - // If we can find a help URL then add the Help capability - if (!TextUtils.isEmpty(context.getResources().getString(R.string.help_uri))) { - capabilities |= AccountCapabilities.HELP_CONTENT; - } - - capabilities |= AccountCapabilities.EMPTY_TRASH; - - // TODO: Should this be stored per-account, or some other mechanism? - capabilities |= AccountCapabilities.NESTED_FOLDERS; - - // the client is permitted to sanitize HTML emails for all Email accounts - capabilities |= AccountCapabilities.CLIENT_SANITIZED_HTML; - - return capabilities; - } - - /** - * Generate a "single account" SQLite query, given a projection from UnifiedEmail - * - * @param uiProjection as passed from UnifiedEmail - * @param id account row ID - * @return the SQLite query to be executed on the EmailProvider database - */ - private String genQueryAccount(String[] uiProjection, String id) { - final ContentValues values = new ContentValues(); - final long accountId = Long.parseLong(id); - final Context context = getContext(); - - EmailServiceInfo info = null; - - // TODO: If uiProjection is null, this will NPE. We should do everything here if it's null. - final Set projectionColumns = ImmutableSet.copyOf(uiProjection); - - final Account account = Account.restoreAccountWithId(context, accountId); - if (account == null) { - LogUtils.d(TAG, "Account %d not found during genQueryAccount", accountId); - } - if (projectionColumns.contains(UIProvider.AccountColumns.CAPABILITIES)) { - // Get account capabilities from the service - values.put(UIProvider.AccountColumns.CAPABILITIES, - (account == null ? 0 : getCapabilities(context, account))); - } - if (projectionColumns.contains(UIProvider.AccountColumns.SETTINGS_INTENT_URI)) { - values.put(UIProvider.AccountColumns.SETTINGS_INTENT_URI, - getExternalUriString("settings", id)); - } - if (projectionColumns.contains(UIProvider.AccountColumns.COMPOSE_URI)) { - values.put(UIProvider.AccountColumns.COMPOSE_URI, - getExternalUriStringEmail2("compose", id)); - } - if (projectionColumns.contains(UIProvider.AccountColumns.REAUTHENTICATION_INTENT_URI)) { - values.put(UIProvider.AccountColumns.REAUTHENTICATION_INTENT_URI, - HeadlessAccountSettingsLoader.getIncomingSettingsUri(accountId) - .toString()); - } - if (projectionColumns.contains(UIProvider.AccountColumns.MIME_TYPE)) { - values.put(UIProvider.AccountColumns.MIME_TYPE, EMAIL_APP_MIME_TYPE); - } - if (projectionColumns.contains(UIProvider.AccountColumns.COLOR)) { - values.put(UIProvider.AccountColumns.COLOR, ACCOUNT_COLOR); - } - - // TODO: if we're getting the values out of MailPrefs then we don't need to be passing the - // values this way - final MailPrefs mailPrefs = MailPrefs.get(getContext()); - if (projectionColumns.contains(UIProvider.AccountColumns.SettingsColumns.CONFIRM_DELETE)) { - values.put(UIProvider.AccountColumns.SettingsColumns.CONFIRM_DELETE, - mailPrefs.getConfirmDelete() ? "1" : "0"); - } - if (projectionColumns.contains(UIProvider.AccountColumns.SettingsColumns.CONFIRM_SEND)) { - values.put(UIProvider.AccountColumns.SettingsColumns.CONFIRM_SEND, - mailPrefs.getConfirmSend() ? "1" : "0"); - } - if (projectionColumns.contains(UIProvider.AccountColumns.SettingsColumns.SWIPE)) { - values.put(UIProvider.AccountColumns.SettingsColumns.SWIPE, - mailPrefs.getConversationListSwipeActionInteger(false)); - } - if (projectionColumns.contains( - UIProvider.AccountColumns.SettingsColumns.CONV_LIST_ICON)) { - values.put(UIProvider.AccountColumns.SettingsColumns.CONV_LIST_ICON, - getConversationListIcon(mailPrefs)); - } - if (projectionColumns.contains(UIProvider.AccountColumns.SettingsColumns.AUTO_ADVANCE)) { - values.put(UIProvider.AccountColumns.SettingsColumns.AUTO_ADVANCE, - Integer.toString(mailPrefs.getAutoAdvanceMode())); - } - // Set default inbox, if we've got an inbox; otherwise, say initial sync needed - final long inboxMailboxId = - Mailbox.findMailboxOfType(context, accountId, Mailbox.TYPE_INBOX); - if (projectionColumns.contains(UIProvider.AccountColumns.SettingsColumns.DEFAULT_INBOX) && - inboxMailboxId != Mailbox.NO_MAILBOX) { - values.put(UIProvider.AccountColumns.SettingsColumns.DEFAULT_INBOX, - uiUriString("uifolder", inboxMailboxId)); - } else { - values.put(UIProvider.AccountColumns.SettingsColumns.DEFAULT_INBOX, - uiUriString("uiinbox", accountId)); - } - if (projectionColumns.contains( - UIProvider.AccountColumns.SettingsColumns.DEFAULT_INBOX_NAME) && - inboxMailboxId != Mailbox.NO_MAILBOX) { - values.put(UIProvider.AccountColumns.SettingsColumns.DEFAULT_INBOX_NAME, - Mailbox.getDisplayName(context, inboxMailboxId)); - } - if (projectionColumns.contains(UIProvider.AccountColumns.SYNC_STATUS)) { - if (inboxMailboxId != Mailbox.NO_MAILBOX) { - values.put(UIProvider.AccountColumns.SYNC_STATUS, UIProvider.SyncStatus.NO_SYNC); - } else { - values.put(UIProvider.AccountColumns.SYNC_STATUS, - UIProvider.SyncStatus.INITIAL_SYNC_NEEDED); - } - } - if (projectionColumns.contains(UIProvider.AccountColumns.UPDATE_SETTINGS_URI)) { - values.put(UIProvider.AccountColumns.UPDATE_SETTINGS_URI, - uiUriString("uiacctsettings", -1)); - } - if (projectionColumns.contains(UIProvider.AccountColumns.ENABLE_MESSAGE_TRANSFORMS)) { - // Email is now sanitized, which grants the ability to inject beautifying javascript. - values.put(UIProvider.AccountColumns.ENABLE_MESSAGE_TRANSFORMS, 1); - } - if (projectionColumns.contains(UIProvider.AccountColumns.SECURITY_HOLD)) { - final int hold = ((account != null && - ((account.getFlags() & Account.FLAGS_SECURITY_HOLD) == 0)) ? 0 : 1); - values.put(UIProvider.AccountColumns.SECURITY_HOLD, hold); - } - if (projectionColumns.contains(UIProvider.AccountColumns.ACCOUNT_SECURITY_URI)) { - values.put(UIProvider.AccountColumns.ACCOUNT_SECURITY_URI, - (account == null ? "" : AccountSecurity.getUpdateSecurityUri( - account.getId(), true).toString())); - } - if (projectionColumns.contains( - UIProvider.AccountColumns.SettingsColumns.IMPORTANCE_MARKERS_ENABLED)) { - // Email doesn't support priority inbox, so always state importance markers disabled. - values.put(UIProvider.AccountColumns.SettingsColumns.IMPORTANCE_MARKERS_ENABLED, "0"); - } - if (projectionColumns.contains( - UIProvider.AccountColumns.SettingsColumns.SHOW_CHEVRONS_ENABLED)) { - // Email doesn't support priority inbox, so always state show chevrons disabled. - values.put(UIProvider.AccountColumns.SettingsColumns.SHOW_CHEVRONS_ENABLED, "0"); - } - if (projectionColumns.contains( - UIProvider.AccountColumns.SettingsColumns.SETUP_INTENT_URI)) { - // Set the setup intent if needed - // TODO We should clarify/document the trash/setup relationship - long trashId = Mailbox.findMailboxOfType(context, accountId, Mailbox.TYPE_TRASH); - if (trashId == Mailbox.NO_MAILBOX) { - info = EmailServiceUtils.getServiceInfoForAccount(context, accountId); - if (info != null && info.requiresSetup) { - values.put(UIProvider.AccountColumns.SettingsColumns.SETUP_INTENT_URI, - getExternalUriString("setup", id)); - } - } - } - if (projectionColumns.contains(UIProvider.AccountColumns.TYPE)) { - final String type; - if (info == null) { - info = EmailServiceUtils.getServiceInfoForAccount(context, accountId); - } - if (info != null) { - type = info.accountType; - } else { - type = "unknown"; - } - - values.put(UIProvider.AccountColumns.TYPE, type); - } - if (projectionColumns.contains(UIProvider.AccountColumns.SettingsColumns.MOVE_TO_INBOX) && - inboxMailboxId != Mailbox.NO_MAILBOX) { - values.put(UIProvider.AccountColumns.SettingsColumns.MOVE_TO_INBOX, - uiUriString("uifolder", inboxMailboxId)); - } - if (projectionColumns.contains(UIProvider.AccountColumns.SYNC_AUTHORITY)) { - values.put(UIProvider.AccountColumns.SYNC_AUTHORITY, EmailContent.AUTHORITY); - } - if (projectionColumns.contains(UIProvider.AccountColumns.QUICK_RESPONSE_URI)) { - values.put(UIProvider.AccountColumns.QUICK_RESPONSE_URI, - combinedUriString("quickresponse/account", id)); - } - if (projectionColumns.contains(UIProvider.AccountColumns.SETTINGS_FRAGMENT_CLASS)) { - values.put(UIProvider.AccountColumns.SETTINGS_FRAGMENT_CLASS, - AccountSettingsFragment.class.getName()); - } - if (projectionColumns.contains(UIProvider.AccountColumns.SettingsColumns.REPLY_BEHAVIOR)) { - values.put(UIProvider.AccountColumns.SettingsColumns.REPLY_BEHAVIOR, - mailPrefs.getDefaultReplyAll() - ? UIProvider.DefaultReplyBehavior.REPLY_ALL - : UIProvider.DefaultReplyBehavior.REPLY); - } - if (projectionColumns.contains(UIProvider.AccountColumns.SettingsColumns.SHOW_IMAGES)) { - values.put(UIProvider.AccountColumns.SettingsColumns.SHOW_IMAGES, - Settings.ShowImages.ASK_FIRST); - } - - final StringBuilder sb = genSelect(getAccountListMap(getContext()), uiProjection, values); - sb.append(" FROM " + Account.TABLE_NAME + " WHERE " + AccountColumns._ID + "=?"); - return sb.toString(); - } - - /** - * Generate a Uri string for a combined mailbox uri - * @param type the uri command type (e.g. "uimessages") - * @param id the id of the item (e.g. an account, mailbox, or message id) - * @return a Uri string - */ - private static String combinedUriString(String type, String id) { - return "content://" + EmailContent.AUTHORITY + "/" + type + "/" + id; - } - - public static final long COMBINED_ACCOUNT_ID = 0x10000000; - - /** - * Generate an id for a combined mailbox of a given type - * @param type the mailbox type for the combined mailbox - * @return the id, as a String - */ - private static String combinedMailboxId(int type) { - return Long.toString(Account.ACCOUNT_ID_COMBINED_VIEW + type); - } - - public static long getVirtualMailboxId(long accountId, int type) { - return (accountId << 32) + type; - } - - private static boolean isVirtualMailbox(long mailboxId) { - return mailboxId >= 0x100000000L; - } - - private static boolean isCombinedMailbox(long mailboxId) { - return (mailboxId >> 32) == COMBINED_ACCOUNT_ID; - } - - private static long getVirtualMailboxAccountId(long mailboxId) { - return mailboxId >> 32; - } - - private static String getVirtualMailboxAccountIdString(long mailboxId) { - return Long.toString(mailboxId >> 32); - } - - private static int getVirtualMailboxType(long mailboxId) { - return (int)(mailboxId & 0xF); - } - - private void addCombinedAccountRow(MatrixCursor mc) { - final long lastUsedAccountId = - Preferences.getPreferences(getContext()).getLastUsedAccountId(); - final long id = Account.getDefaultAccountId(getContext(), lastUsedAccountId); - if (id == Account.NO_ACCOUNT) return; - - // Build a map of the requested columns to the appropriate positions - final ImmutableMap.Builder builder = - new ImmutableMap.Builder(); - final String[] columnNames = mc.getColumnNames(); - for (int i = 0; i < columnNames.length; i++) { - builder.put(columnNames[i], i); - } - final Map colPosMap = builder.build(); - - final MailPrefs mailPrefs = MailPrefs.get(getContext()); - final Object[] values = new Object[columnNames.length]; - if (colPosMap.containsKey(BaseColumns._ID)) { - values[colPosMap.get(BaseColumns._ID)] = 0; - } - if (colPosMap.containsKey(UIProvider.AccountColumns.CAPABILITIES)) { - values[colPosMap.get(UIProvider.AccountColumns.CAPABILITIES)] = - AccountCapabilities.UNDO | AccountCapabilities.VIRTUAL_ACCOUNT; - } - if (colPosMap.containsKey(UIProvider.AccountColumns.FOLDER_LIST_URI)) { - values[colPosMap.get(UIProvider.AccountColumns.FOLDER_LIST_URI)] = - combinedUriString("uifolders", COMBINED_ACCOUNT_ID_STRING); - } - if (colPosMap.containsKey(UIProvider.AccountColumns.NAME)) { - values[colPosMap.get(UIProvider.AccountColumns.NAME)] = getContext().getString( - R.string.mailbox_list_account_selector_combined_view); - } - if (colPosMap.containsKey(UIProvider.AccountColumns.ACCOUNT_MANAGER_NAME)) { - values[colPosMap.get(UIProvider.AccountColumns.ACCOUNT_MANAGER_NAME)] = - getContext().getString(R.string.mailbox_list_account_selector_combined_view); - } - if (colPosMap.containsKey(UIProvider.AccountColumns.ACCOUNT_ID)) { - values[colPosMap.get(UIProvider.AccountColumns.ACCOUNT_ID)] = "Account Id"; - } - if (colPosMap.containsKey(UIProvider.AccountColumns.TYPE)) { - values[colPosMap.get(UIProvider.AccountColumns.TYPE)] = "unknown"; - } - if (colPosMap.containsKey(UIProvider.AccountColumns.UNDO_URI)) { - values[colPosMap.get(UIProvider.AccountColumns.UNDO_URI)] = - "'content://" + EmailContent.AUTHORITY + "/uiundo'"; - } - if (colPosMap.containsKey(UIProvider.AccountColumns.URI)) { - values[colPosMap.get(UIProvider.AccountColumns.URI)] = - combinedUriString("uiaccount", COMBINED_ACCOUNT_ID_STRING); - } - if (colPosMap.containsKey(UIProvider.AccountColumns.MIME_TYPE)) { - values[colPosMap.get(UIProvider.AccountColumns.MIME_TYPE)] = - EMAIL_APP_MIME_TYPE; - } - if (colPosMap.containsKey(UIProvider.AccountColumns.SECURITY_HOLD)) { - values[colPosMap.get(UIProvider.AccountColumns.SECURITY_HOLD)] = 0; - } - if (colPosMap.containsKey(UIProvider.AccountColumns.ACCOUNT_SECURITY_URI)) { - values[colPosMap.get(UIProvider.AccountColumns.ACCOUNT_SECURITY_URI)] = ""; - } - if (colPosMap.containsKey(UIProvider.AccountColumns.SETTINGS_INTENT_URI)) { - values[colPosMap.get(UIProvider.AccountColumns.SETTINGS_INTENT_URI)] = - getExternalUriString("settings", COMBINED_ACCOUNT_ID_STRING); - } - if (colPosMap.containsKey(UIProvider.AccountColumns.COMPOSE_URI)) { - values[colPosMap.get(UIProvider.AccountColumns.COMPOSE_URI)] = - getExternalUriStringEmail2("compose", Long.toString(id)); - } - if (colPosMap.containsKey(UIProvider.AccountColumns.UPDATE_SETTINGS_URI)) { - values[colPosMap.get(UIProvider.AccountColumns.UPDATE_SETTINGS_URI)] = - uiUriString("uiacctsettings", -1); - } - - if (colPosMap.containsKey(UIProvider.AccountColumns.SettingsColumns.AUTO_ADVANCE)) { - values[colPosMap.get(UIProvider.AccountColumns.SettingsColumns.AUTO_ADVANCE)] = - Integer.toString(mailPrefs.getAutoAdvanceMode()); - } - if (colPosMap.containsKey(UIProvider.AccountColumns.SettingsColumns.SNAP_HEADERS)) { - values[colPosMap.get(UIProvider.AccountColumns.SettingsColumns.SNAP_HEADERS)] = - Integer.toString(UIProvider.SnapHeaderValue.ALWAYS); - } - //.add(UIProvider.SettingsColumns.SIGNATURE, AccountColumns.SIGNATURE) - if (colPosMap.containsKey(UIProvider.AccountColumns.SettingsColumns.REPLY_BEHAVIOR)) { - values[colPosMap.get(UIProvider.AccountColumns.SettingsColumns.REPLY_BEHAVIOR)] = - Integer.toString(mailPrefs.getDefaultReplyAll() - ? UIProvider.DefaultReplyBehavior.REPLY_ALL - : UIProvider.DefaultReplyBehavior.REPLY); - } - if (colPosMap.containsKey(UIProvider.AccountColumns.SettingsColumns.CONV_LIST_ICON)) { - values[colPosMap.get(UIProvider.AccountColumns.SettingsColumns.CONV_LIST_ICON)] = - getConversationListIcon(mailPrefs); - } - if (colPosMap.containsKey(UIProvider.AccountColumns.SettingsColumns.CONFIRM_DELETE)) { - values[colPosMap.get(UIProvider.AccountColumns.SettingsColumns.CONFIRM_DELETE)] = - mailPrefs.getConfirmDelete() ? 1 : 0; - } - if (colPosMap.containsKey(UIProvider.AccountColumns.SettingsColumns.CONFIRM_ARCHIVE)) { - values[colPosMap.get( - UIProvider.AccountColumns.SettingsColumns.CONFIRM_ARCHIVE)] = 0; - } - if (colPosMap.containsKey(UIProvider.AccountColumns.SettingsColumns.CONFIRM_SEND)) { - values[colPosMap.get(UIProvider.AccountColumns.SettingsColumns.CONFIRM_SEND)] = - mailPrefs.getConfirmSend() ? 1 : 0; - } - if (colPosMap.containsKey(UIProvider.AccountColumns.SettingsColumns.DEFAULT_INBOX)) { - values[colPosMap.get(UIProvider.AccountColumns.SettingsColumns.DEFAULT_INBOX)] = - combinedUriString("uifolder", combinedMailboxId(Mailbox.TYPE_INBOX)); - } - if (colPosMap.containsKey(UIProvider.AccountColumns.SettingsColumns.MOVE_TO_INBOX)) { - values[colPosMap.get(UIProvider.AccountColumns.SettingsColumns.MOVE_TO_INBOX)] = - combinedUriString("uifolder", combinedMailboxId(Mailbox.TYPE_INBOX)); - } - if (colPosMap.containsKey(UIProvider.AccountColumns.SettingsColumns.SHOW_IMAGES)) { - values[colPosMap.get(UIProvider.AccountColumns.SettingsColumns.SHOW_IMAGES)] = - Settings.ShowImages.ASK_FIRST; - } - - mc.addRow(values); - } - - private static int getConversationListIcon(MailPrefs mailPrefs) { - return mailPrefs.getShowSenderImages() ? - UIProvider.ConversationListIcon.SENDER_IMAGE : - UIProvider.ConversationListIcon.NONE; - } - - private Cursor getVirtualMailboxCursor(long mailboxId, String[] projection) { - MatrixCursor mc = new MatrixCursorWithCachedColumns(projection, 1); - mc.addRow(getVirtualMailboxRow(getVirtualMailboxAccountId(mailboxId), - getVirtualMailboxType(mailboxId), projection)); - return mc; - } - - private Object[] getVirtualMailboxRow(long accountId, int mailboxType, String[] projection) { - final long id = getVirtualMailboxId(accountId, mailboxType); - final String idString = Long.toString(id); - Object[] values = new Object[projection.length]; - // Not all column values are filled in here, as some are not applicable to virtual mailboxes - // The remainder are left null - for (int i = 0; i < projection.length; i++) { - final String column = projection[i]; - if (column.equals(UIProvider.FolderColumns._ID)) { - values[i] = id; - } else if (column.equals(UIProvider.FolderColumns.URI)) { - values[i] = combinedUriString("uifolder", idString); - } else if (column.equals(UIProvider.FolderColumns.NAME)) { - // default empty string since all of these should use resource strings - values[i] = getFolderDisplayName(getFolderTypeFromMailboxType(mailboxType), ""); - } else if (column.equals(UIProvider.FolderColumns.HAS_CHILDREN)) { - values[i] = 0; - } else if (column.equals(UIProvider.FolderColumns.CAPABILITIES)) { - values[i] = UIProvider.FolderCapabilities.DELETE - | UIProvider.FolderCapabilities.IS_VIRTUAL; - } else if (column.equals(UIProvider.FolderColumns.CONVERSATION_LIST_URI)) { - values[i] = combinedUriString("uimessages", idString); - } else if (column.equals(UIProvider.FolderColumns.UNREAD_COUNT)) { - if (mailboxType == Mailbox.TYPE_INBOX && accountId == COMBINED_ACCOUNT_ID) { - final int unreadCount = EmailContent.count(getContext(), Message.CONTENT_URI, - MessageColumns.MAILBOX_KEY + " IN (SELECT " + MailboxColumns._ID - + " FROM " + Mailbox.TABLE_NAME + " WHERE " + MailboxColumns.TYPE - + "=" + Mailbox.TYPE_INBOX + ") AND " + MessageColumns.FLAG_READ + "=0", - null); - values[i] = unreadCount; - } else if (mailboxType == Mailbox.TYPE_UNREAD) { - final String accountKeyClause; - final String[] whereArgs; - if (accountId == COMBINED_ACCOUNT_ID) { - accountKeyClause = ""; - whereArgs = null; - } else { - accountKeyClause = MessageColumns.ACCOUNT_KEY + "= ? AND "; - whereArgs = new String[] { Long.toString(accountId) }; - } - final int unreadCount = EmailContent.count(getContext(), Message.CONTENT_URI, - accountKeyClause + MessageColumns.FLAG_READ + "=0 AND " - + MessageColumns.MAILBOX_KEY + " NOT IN (SELECT " + MailboxColumns._ID - + " FROM " + Mailbox.TABLE_NAME + " WHERE " + MailboxColumns.TYPE + "=" - + Mailbox.TYPE_TRASH + ")", whereArgs); - values[i] = unreadCount; - } else if (mailboxType == Mailbox.TYPE_STARRED) { - final String accountKeyClause; - final String[] whereArgs; - if (accountId == COMBINED_ACCOUNT_ID) { - accountKeyClause = ""; - whereArgs = null; - } else { - accountKeyClause = MessageColumns.ACCOUNT_KEY + "= ? AND "; - whereArgs = new String[] { Long.toString(accountId) }; - } - final int starredCount = EmailContent.count(getContext(), Message.CONTENT_URI, - accountKeyClause + MessageColumns.FLAG_FAVORITE + "=1", whereArgs); - values[i] = starredCount; - } - } else if (column.equals(UIProvider.FolderColumns.ICON_RES_ID)) { - if (mailboxType == Mailbox.TYPE_INBOX) { - values[i] = R.drawable.ic_drawer_inbox_24dp; - } else if (mailboxType == Mailbox.TYPE_UNREAD) { - values[i] = R.drawable.ic_drawer_unread_24dp; - } else if (mailboxType == Mailbox.TYPE_STARRED) { - values[i] = R.drawable.ic_drawer_starred_24dp; - } - } - } - return values; - } - - private Cursor uiAccounts(String[] uiProjection, boolean suppressCombined) { - final Context context = getContext(); - final SQLiteDatabase db = getDatabase(context); - final Cursor accountIdCursor = - db.rawQuery("select _id from " + Account.TABLE_NAME, new String[0]); - final MatrixCursor mc; - try { - boolean combinedAccount = false; - if (!suppressCombined && accountIdCursor.getCount() > 1) { - combinedAccount = true; - } - final Bundle extras = new Bundle(); - // Email always returns the accurate number of accounts - extras.putInt(AccountCursorExtraKeys.ACCOUNTS_LOADED, 1); - mc = new MatrixCursorWithExtra(uiProjection, accountIdCursor.getCount(), extras); - final Object[] values = new Object[uiProjection.length]; - while (accountIdCursor.moveToNext()) { - final String id = accountIdCursor.getString(0); - final Cursor accountCursor = - db.rawQuery(genQueryAccount(uiProjection, id), new String[] {id}); - try { - if (accountCursor.moveToNext()) { - for (int i = 0; i < uiProjection.length; i++) { - values[i] = accountCursor.getString(i); - } - mc.addRow(values); - } - } finally { - accountCursor.close(); - } - } - if (combinedAccount) { - addCombinedAccountRow(mc); - } - } finally { - accountIdCursor.close(); - } - mc.setNotificationUri(context.getContentResolver(), UIPROVIDER_ALL_ACCOUNTS_NOTIFIER); - - return mc; - } - - private Cursor uiQuickResponseAccount(String[] uiProjection, String account) { - final Context context = getContext(); - final SQLiteDatabase db = getDatabase(context); - final StringBuilder sb = genSelect(getQuickResponseMap(), uiProjection); - sb.append(" FROM " + QuickResponse.TABLE_NAME); - sb.append(" WHERE " + QuickResponse.ACCOUNT_KEY + "=?"); - final String query = sb.toString(); - return db.rawQuery(query, new String[] {account}); - } - - private Cursor uiQuickResponseId(String[] uiProjection, String id) { - final Context context = getContext(); - final SQLiteDatabase db = getDatabase(context); - final StringBuilder sb = genSelect(getQuickResponseMap(), uiProjection); - sb.append(" FROM " + QuickResponse.TABLE_NAME); - sb.append(" WHERE " + QuickResponse._ID + "=?"); - final String query = sb.toString(); - return db.rawQuery(query, new String[] {id}); - } - - private Cursor uiQuickResponse(String[] uiProjection) { - final Context context = getContext(); - final SQLiteDatabase db = getDatabase(context); - final StringBuilder sb = genSelect(getQuickResponseMap(), uiProjection); - sb.append(" FROM " + QuickResponse.TABLE_NAME); - final String query = sb.toString(); - return db.rawQuery(query, new String[0]); - } - - /** - * Generate the "attachment list" SQLite query, given a projection from UnifiedEmail - * - * @param uiProjection as passed from UnifiedEmail - * @param contentTypeQueryParameters list of mimeTypes, used as a filter for the attachments - * or null if there are no query parameters - * @return the SQLite query to be executed on the EmailProvider database - */ - private static String genQueryAttachments(String[] uiProjection, - List contentTypeQueryParameters) { - // MAKE SURE THESE VALUES STAY IN SYNC WITH GEN QUERY ATTACHMENT - ContentValues values = new ContentValues(1); - values.put(UIProvider.AttachmentColumns.SUPPORTS_DOWNLOAD_AGAIN, 1); - StringBuilder sb = genSelect(getAttachmentMap(), uiProjection, values); - sb.append(" FROM ") - .append(Attachment.TABLE_NAME) - .append(" WHERE ") - .append(AttachmentColumns.MESSAGE_KEY) - .append(" =? "); - - // Filter for certain content types. - // The filter works by adding LIKE operators for each - // content type you wish to request. Content types - // are filtered by performing a case-insensitive "starts with" - // filter. IE, "image/" would return "image/png" as well as "image/jpeg". - if (contentTypeQueryParameters != null && !contentTypeQueryParameters.isEmpty()) { - final int size = contentTypeQueryParameters.size(); - sb.append("AND ("); - for (int i = 0; i < size; i++) { - final String contentType = contentTypeQueryParameters.get(i); - sb.append(AttachmentColumns.MIME_TYPE) - .append(" LIKE '") - .append(contentType) - .append("%'"); - - if (i != size - 1) { - sb.append(" OR "); - } - } - sb.append(")"); - } - return sb.toString(); - } - - /** - * Generate the "single attachment" SQLite query, given a projection from UnifiedEmail - * - * @param uiProjection as passed from UnifiedEmail - * @return the SQLite query to be executed on the EmailProvider database - */ - private String genQueryAttachment(String[] uiProjection) { - // MAKE SURE THESE VALUES STAY IN SYNC WITH GEN QUERY ATTACHMENTS - final ContentValues values = new ContentValues(2); - values.put(AttachmentColumns.CONTENT_URI, createAttachmentUriColumnSQL()); - values.put(UIProvider.AttachmentColumns.SUPPORTS_DOWNLOAD_AGAIN, 1); - - return genSelect(getAttachmentMap(), uiProjection, values) - .append(" FROM ").append(Attachment.TABLE_NAME) - .append(" WHERE ") - .append(AttachmentColumns._ID).append(" =? ") - .toString(); - } - - /** - * Generate the "single attachment by Content ID" SQLite query, given a projection from - * UnifiedEmail - * - * @param uiProjection as passed from UnifiedEmail - * @return the SQLite query to be executed on the EmailProvider database - */ - private String genQueryAttachmentByMessageIDAndCid(String[] uiProjection) { - final ContentValues values = new ContentValues(2); - values.put(AttachmentColumns.CONTENT_URI, createAttachmentUriColumnSQL()); - values.put(UIProvider.AttachmentColumns.SUPPORTS_DOWNLOAD_AGAIN, 1); - - return genSelect(getAttachmentMap(), uiProjection, values) - .append(" FROM ").append(Attachment.TABLE_NAME) - .append(" WHERE ") - .append(AttachmentColumns.MESSAGE_KEY).append(" =? ") - .append(" AND ") - .append(AttachmentColumns.CONTENT_ID).append(" =? ") - .toString(); - } - - /** - * @return a fragment of SQL that is the expression which, when evaluated for a particular - * Attachment row, produces the Content URI for the attachment - */ - private static String createAttachmentUriColumnSQL() { - final String uriPrefix = Attachment.ATTACHMENT_PROVIDER_URI_PREFIX; - final String accountKey = AttachmentColumns.ACCOUNT_KEY; - final String id = AttachmentColumns._ID; - final String raw = AttachmentUtilities.FORMAT_RAW; - final String contentUri = String.format("%s/' || %s || '/' || %s || '/%s", uriPrefix, - accountKey, id, raw); - - return "@CASE " + - "WHEN contentUri IS NULL THEN '" + contentUri + "' " + - "WHEN contentUri IS NOT NULL THEN contentUri " + - "END"; - } - - /** - * Generate the "subfolder list" SQLite query, given a projection from UnifiedEmail - * - * @param uiProjection as passed from UnifiedEmail - * @return the SQLite query to be executed on the EmailProvider database - */ - private static String genQuerySubfolders(String[] uiProjection) { - StringBuilder sb = genSelect(getFolderListMap(), uiProjection); - sb.append(" FROM " + Mailbox.TABLE_NAME + " WHERE " + MailboxColumns.PARENT_KEY + - " =? ORDER BY "); - sb.append(MAILBOX_ORDER_BY); - return sb.toString(); - } - - private static final String COMBINED_ACCOUNT_ID_STRING = Long.toString(COMBINED_ACCOUNT_ID); - - /** - * Returns a cursor over all the folders for a specific URI which corresponds to a single - * account. - * @param uri uri to query - * @param uiProjection projection - * @return query result cursor - */ - private Cursor uiFolders(final Uri uri, final String[] uiProjection) { - final Context context = getContext(); - final SQLiteDatabase db = getDatabase(context); - final String id = uri.getPathSegments().get(1); - - final Uri notifyUri = - UIPROVIDER_FOLDERLIST_NOTIFIER.buildUpon().appendEncodedPath(id).build(); - - final Cursor vc = uiVirtualMailboxes(id, uiProjection); - vc.setNotificationUri(context.getContentResolver(), notifyUri); - if (id.equals(COMBINED_ACCOUNT_ID_STRING)) { - return vc; - } else { - Cursor c = db.rawQuery(genQueryAccountMailboxes(UIProvider.FOLDERS_PROJECTION), - new String[] {id}); - c = getFolderListCursor(c, Long.valueOf(id), uiProjection); - c.setNotificationUri(context.getContentResolver(), notifyUri); - if (c.getCount() > 0) { - Cursor[] cursors = new Cursor[]{vc, c}; - return new MergeCursor(cursors); - } else { - return c; - } - } - } - - private Cursor uiVirtualMailboxes(final String id, final String[] uiProjection) { - final MatrixCursor mc = new MatrixCursorWithCachedColumns(uiProjection); - - if (id.equals(COMBINED_ACCOUNT_ID_STRING)) { - mc.addRow(getVirtualMailboxRow(COMBINED_ACCOUNT_ID, Mailbox.TYPE_INBOX, uiProjection)); - mc.addRow( - getVirtualMailboxRow(COMBINED_ACCOUNT_ID, Mailbox.TYPE_STARRED, uiProjection)); - mc.addRow(getVirtualMailboxRow(COMBINED_ACCOUNT_ID, Mailbox.TYPE_UNREAD, uiProjection)); - } else { - final long acctId = Long.parseLong(id); - mc.addRow(getVirtualMailboxRow(acctId, Mailbox.TYPE_STARRED, uiProjection)); - mc.addRow(getVirtualMailboxRow(acctId, Mailbox.TYPE_UNREAD, uiProjection)); - } - - return mc; - } - - /** - * Returns an array of the default recent folders for a given URI which is unique for an - * account. Some accounts might not have default recent folders, in which case an empty array - * is returned. - * @param id account id - * @return array of URIs - */ - private Uri[] defaultRecentFolders(final String id) { - Uri[] recentFolders = new Uri[0]; - final SQLiteDatabase db = getDatabase(getContext()); - if (id.equals(COMBINED_ACCOUNT_ID_STRING)) { - // We don't have default recents for the combined view. - return recentFolders; - } - // We search for the types we want, and find corresponding IDs. - final String[] idAndType = { BaseColumns._ID, UIProvider.FolderColumns.TYPE }; - - // Sent, Drafts, and Starred are the default recents. - final StringBuilder sb = genSelect(getFolderListMap(), idAndType); - sb.append(" FROM ") - .append(Mailbox.TABLE_NAME) - .append(" WHERE ") - .append(MailboxColumns.ACCOUNT_KEY) - .append(" = ") - .append(id) - .append(" AND ") - .append(MailboxColumns.TYPE) - .append(" IN (") - .append(Mailbox.TYPE_SENT) - .append(", ") - .append(Mailbox.TYPE_DRAFTS) - .append(", ") - .append(Mailbox.TYPE_STARRED) - .append(")"); - LogUtils.d(TAG, "defaultRecentFolders: Query is %s", sb); - final Cursor c = db.rawQuery(sb.toString(), null); - try { - if (c == null || c.getCount() <= 0 || !c.moveToFirst()) { - return recentFolders; - } - // Read all the IDs of the mailboxes, and turn them into URIs. - recentFolders = new Uri[c.getCount()]; - int i = 0; - do { - final long folderId = c.getLong(0); - recentFolders[i] = uiUri("uifolder", folderId); - LogUtils.d(TAG, "Default recent folder: %d, with uri %s", folderId, - recentFolders[i]); - ++i; - } while (c.moveToNext()); - } finally { - if (c != null) { - c.close(); - } - } - return recentFolders; - } - - /** - * Convenience method to create a {@link Folder} - * @param context to get a {@link ContentResolver} - * @param mailboxId id of the {@link Mailbox} that we want - * @return the {@link Folder} or null - */ - public static Folder getFolder(Context context, long mailboxId) { - final ContentResolver resolver = context.getContentResolver(); - final Cursor fc = resolver.query(EmailProvider.uiUri("uifolder", mailboxId), - UIProvider.FOLDERS_PROJECTION, null, null, null); - - if (fc == null) { - LogUtils.e(TAG, "Null folder cursor for mailboxId %d", mailboxId); - return null; - } - - Folder uiFolder = null; - try { - if (fc.moveToFirst()) { - uiFolder = new Folder(fc); - } - } finally { - fc.close(); - } - return uiFolder; - } - - static class AttachmentsCursor extends CursorWrapper { - private final int mContentUriIndex; - private final int mUriIndex; - private final Context mContext; - private final String[] mContentUriStrings; - - public AttachmentsCursor(Context context, Cursor cursor) { - super(cursor); - mContentUriIndex = cursor.getColumnIndex(UIProvider.AttachmentColumns.CONTENT_URI); - mUriIndex = cursor.getColumnIndex(UIProvider.AttachmentColumns.URI); - mContext = context; - mContentUriStrings = new String[cursor.getCount()]; - if (mContentUriIndex == -1) { - // Nothing to do here, move along - return; - } - while (cursor.moveToNext()) { - final int index = cursor.getPosition(); - final Uri uri = Uri.parse(getString(mUriIndex)); - final long id = Long.parseLong(uri.getLastPathSegment()); - final Attachment att = Attachment.restoreAttachmentWithId(mContext, id); - - if (att == null) { - mContentUriStrings[index] = ""; - continue; - } - - if (!TextUtils.isEmpty(att.getCachedFileUri())) { - mContentUriStrings[index] = att.getCachedFileUri(); - continue; - } - - final String contentUri; - // Until the package installer can handle opening apks from a content:// uri, for - // any apk that was successfully saved in external storage, return the - // content uri from the attachment - if (att.mUiDestination == UIProvider.AttachmentDestination.EXTERNAL && - att.mUiState == UIProvider.AttachmentState.SAVED && - TextUtils.equals(att.mMimeType, MimeType.ANDROID_ARCHIVE)) { - contentUri = att.getContentUri(); - } else { - final String attUriString = att.getContentUri(); - final String authority; - if (!TextUtils.isEmpty(attUriString)) { - authority = Uri.parse(attUriString).getAuthority(); - } else { - authority = null; - } - if (TextUtils.equals(authority, Attachment.ATTACHMENT_PROVIDER_AUTHORITY)) { - contentUri = attUriString; - } else { - contentUri = AttachmentUtilities.getAttachmentUri(att.mAccountKey, id) - .toString(); - } - } - mContentUriStrings[index] = contentUri; - - } - cursor.moveToPosition(-1); - } - - @Override - public String getString(int column) { - if (column == mContentUriIndex) { - return mContentUriStrings[getPosition()]; - } else { - return super.getString(column); - } - } - } - - /** - * For debugging purposes; shouldn't be used in production code - */ - @SuppressWarnings("unused") - static class CloseDetectingCursor extends CursorWrapper { - - public CloseDetectingCursor(Cursor cursor) { - super(cursor); - } - - @Override - public void close() { - super.close(); - LogUtils.d(TAG, "Closing cursor", new Error()); - } - } - - /** - * Converts a mailbox in a row of the mailboxCursor into a row - * in the supplied {@link MatrixCursor} in the format required for {@link Folder}. - * As a convenience, the modified {@link MatrixCursor} is also returned. - * @param mc the {@link MatrixCursor} into which the mailbox data will be converted - * @param projectionLength the length of the projection for this Cursor - * @param mailboxCursor the cursor supplying the mailbox data - * @param nameColumn column in the cursor containing the folder name value - * @param typeColumn column in the cursor containing the folder type value - * @return the {@link MatrixCursor} containing the transformed data. - */ - private Cursor getUiFolderCursorRowFromMailboxCursorRow( - MatrixCursor mc, int projectionLength, Cursor mailboxCursor, - int nameColumn, int typeColumn) { - final MatrixCursor.RowBuilder builder = mc.newRow(); - for (int i = 0; i < projectionLength; i++) { - // If we are at the name column, get the type - // and use it to use a properly translated string - // from resources instead of the display name. - // This ignores display names for system mailboxes. - if (nameColumn == i) { - // We implicitly assume that if name is requested, - // type has also been requested. If not, this will - // error in unknown ways. - final int type = mailboxCursor.getInt(typeColumn); - builder.add(getFolderDisplayName(type, mailboxCursor.getString(i))); - } else { - builder.add(mailboxCursor.getString(i)); - } - } - return mc; - } - - /** - * Takes a uifolder cursor (that was generated with a full projection) and remaps values for - * columns that are difficult to generate in the SQL query. This currently includes: - * - Folder name (due to system folder localization). - * - Capabilities (due to this varying by account protocol). - * - Persistent id (due to needing to base64 encode it). - * - Load more uri (due to this varying by account protocol). - * TODO: This would be better as a CursorWrapper, rather than doing a copy. - * @param inputCursor A cursor containing all columns of {@link UIProvider.FolderColumns}. - * Strictly speaking doesn't need all, but simpler if we assume that. - * @param outputCursor A MatrixCursor which this function will populate. - * @param accountId The account id for the mailboxes in this query. - * @param uiProjection The projection specified by the query. - */ - private void remapFolderCursor(final Cursor inputCursor, final MatrixCursor outputCursor, - final long accountId, final String[] uiProjection) { - // Return early if our input cursor is empty. - if (inputCursor == null || inputCursor.getCount() == 0) { - return; - } - // Get the column indices for the columns we need during remapping. - // While we currently could assume the column indices for UIProvider.FOLDERS_PROJECTION - // and therefore avoid the calls to getColumnIndex, this at least tries to future-proof a - // bit. - // Note that id and type MUST be present for this function to work correctly. - final int idColumn = inputCursor.getColumnIndex(BaseColumns._ID); - final int typeColumn = inputCursor.getColumnIndex(UIProvider.FolderColumns.TYPE); - final int nameColumn = inputCursor.getColumnIndex(UIProvider.FolderColumns.NAME); - final int capabilitiesColumn = - inputCursor.getColumnIndex(UIProvider.FolderColumns.CAPABILITIES); - final int persistentIdColumn = - inputCursor.getColumnIndex(UIProvider.FolderColumns.PERSISTENT_ID); - final int loadMoreUriColumn = - inputCursor.getColumnIndex(UIProvider.FolderColumns.LOAD_MORE_URI); - - // Get the EmailServiceInfo for the current account. - final Context context = getContext(); - final String protocol = Account.getProtocol(context, accountId); - final EmailServiceInfo info = EmailServiceUtils.getServiceInfo(context, protocol); - - // Build the return cursor. We iterate over all rows of the input cursor and construct - // a row in the output using the columns in uiProjection. - while (inputCursor.moveToNext()) { - final MatrixCursor.RowBuilder builder = outputCursor.newRow(); - final int folderType = inputCursor.getInt(typeColumn); - for (int i = 0; i < uiProjection.length; i++) { - // Find the index in the input cursor corresponding the column requested in the - // output projection. - final int index = inputCursor.getColumnIndex(uiProjection[i]); - if (index == -1) { - // We don't have this value, so put a blank in the output and move on. - builder.add(null); - continue; - } - final String value = inputCursor.getString(index); - // remapped indicates whether we've written a value to the output for this column. - final boolean remapped; - if (nameColumn == index) { - // Remap folder name for system folders. - builder.add(getFolderDisplayName(folderType, value)); - remapped = true; - } else if (capabilitiesColumn == index) { - // Get the correct capabilities for this folder. - final long mailboxID = inputCursor.getLong(idColumn); - final int mailboxType = getMailboxTypeFromFolderType(folderType); - builder.add(getFolderCapabilities(info, mailboxType, mailboxID)); - remapped = true; - } else if (persistentIdColumn == index) { - // Hash the persistent id. - builder.add(Base64.encodeToString(value.getBytes(), - Base64.URL_SAFE | Base64.NO_WRAP | Base64.NO_PADDING)); - remapped = true; - } else if (loadMoreUriColumn == index && folderType != Mailbox.TYPE_SEARCH && - (info == null || !info.offerLoadMore)) { - // Blank the load more uri for account types that don't offer it. - // Note that all account types permit load more for search results. - builder.add(null); - remapped = true; - } else { - remapped = false; - } - // If the above logic didn't write some other value to the output, use the value - // from the input cursor. - if (!remapped) { - builder.add(value); - } - } - } - } - - private Cursor getFolderListCursor(final Cursor inputCursor, final long accountId, - final String[] uiProjection) { - final MatrixCursor mc = new MatrixCursorWithCachedColumns(uiProjection); - if (inputCursor != null) { - try { - remapFolderCursor(inputCursor, mc, accountId, uiProjection); - } finally { - inputCursor.close(); - } - } - return mc; - } - - /** - * Returns a {@link String} from Resources corresponding - * to the {@link UIProvider.FolderType} requested. - * @param folderType {@link UIProvider.FolderType} value for the folder - * @param defaultName a {@link String} to use in case the {@link UIProvider.FolderType} - * provided is not a system folder. - * @return a {@link String} to use as the display name for the folder - */ - private String getFolderDisplayName(int folderType, String defaultName) { - final int resId; - switch (folderType) { - case UIProvider.FolderType.INBOX: - resId = R.string.mailbox_name_display_inbox; - break; - case UIProvider.FolderType.OUTBOX: - resId = R.string.mailbox_name_display_outbox; - break; - case UIProvider.FolderType.DRAFT: - resId = R.string.mailbox_name_display_drafts; - break; - case UIProvider.FolderType.TRASH: - resId = R.string.mailbox_name_display_trash; - break; - case UIProvider.FolderType.SENT: - resId = R.string.mailbox_name_display_sent; - break; - case UIProvider.FolderType.SPAM: - resId = R.string.mailbox_name_display_junk; - break; - case UIProvider.FolderType.STARRED: - resId = R.string.mailbox_name_display_starred; - break; - case UIProvider.FolderType.UNREAD: - resId = R.string.mailbox_name_display_unread; - break; - default: - return defaultName; - } - return getContext().getString(resId); - } - - /** - * Converts a {@link Mailbox} type value to its {@link UIProvider.FolderType} - * equivalent. - * @param mailboxType a {@link Mailbox} type - * @return a {@link UIProvider.FolderType} value - */ - private static int getFolderTypeFromMailboxType(int mailboxType) { - switch (mailboxType) { - case Mailbox.TYPE_INBOX: - return UIProvider.FolderType.INBOX; - case Mailbox.TYPE_OUTBOX: - return UIProvider.FolderType.OUTBOX; - case Mailbox.TYPE_DRAFTS: - return UIProvider.FolderType.DRAFT; - case Mailbox.TYPE_TRASH: - return UIProvider.FolderType.TRASH; - case Mailbox.TYPE_SENT: - return UIProvider.FolderType.SENT; - case Mailbox.TYPE_JUNK: - return UIProvider.FolderType.SPAM; - case Mailbox.TYPE_STARRED: - return UIProvider.FolderType.STARRED; - case Mailbox.TYPE_UNREAD: - return UIProvider.FolderType.UNREAD; - case Mailbox.TYPE_SEARCH: - // TODO Can the DEFAULT type be removed from SEARCH folders? - return UIProvider.FolderType.DEFAULT | UIProvider.FolderType.SEARCH; - default: - return UIProvider.FolderType.DEFAULT; - } - } - - /** - * Converts a {@link UIProvider.FolderType} type value to its {@link Mailbox} equivalent. - * @param folderType a {@link UIProvider.FolderType} type - * @return a {@link Mailbox} value - */ - private static int getMailboxTypeFromFolderType(int folderType) { - switch (folderType) { - case UIProvider.FolderType.DEFAULT: - return Mailbox.TYPE_MAIL; - case UIProvider.FolderType.INBOX: - return Mailbox.TYPE_INBOX; - case UIProvider.FolderType.OUTBOX: - return Mailbox.TYPE_OUTBOX; - case UIProvider.FolderType.DRAFT: - return Mailbox.TYPE_DRAFTS; - case UIProvider.FolderType.TRASH: - return Mailbox.TYPE_TRASH; - case UIProvider.FolderType.SENT: - return Mailbox.TYPE_SENT; - case UIProvider.FolderType.SPAM: - return Mailbox.TYPE_JUNK; - case UIProvider.FolderType.STARRED: - return Mailbox.TYPE_STARRED; - case UIProvider.FolderType.UNREAD: - return Mailbox.TYPE_UNREAD; - case UIProvider.FolderType.DEFAULT | UIProvider.FolderType.SEARCH: - // TODO Can the DEFAULT type be removed from SEARCH folders? - return Mailbox.TYPE_SEARCH; - default: - throw new IllegalArgumentException("Unable to map folder type: " + folderType); - } - } - - /** - * We need a reasonably full projection for getFolderListCursor to work, but don't always want - * to do the subquery needed for FolderColumns.UNREAD_SENDERS - * @param uiProjection The projection we actually want - * @return Full projection, possibly with or without FolderColumns.UNREAD_SENDERS - */ - private String[] folderProjectionFromUiProjection(final String[] uiProjection) { - final Set columns = ImmutableSet.copyOf(uiProjection); - if (columns.contains(UIProvider.FolderColumns.UNREAD_SENDERS)) { - return UIProvider.FOLDERS_PROJECTION_WITH_UNREAD_SENDERS; - } else { - return UIProvider.FOLDERS_PROJECTION; - } - } - - /** - * Handle UnifiedEmail queries here (dispatched from query()) - * - * @param match the UriMatcher match for the original uri passed in from UnifiedEmail - * @param uri the original uri passed in from UnifiedEmail - * @param uiProjection the projection passed in from UnifiedEmail - * @param unseenOnly true to only return unseen messages (where supported) - * @return the result Cursor - */ - private Cursor uiQuery(int match, Uri uri, String[] uiProjection, final boolean unseenOnly) { - Context context = getContext(); - ContentResolver resolver = context.getContentResolver(); - SQLiteDatabase db = getDatabase(context); - // Should we ever return null, or throw an exception?? - Cursor c = null; - String id = uri.getPathSegments().get(1); - Uri notifyUri = null; - switch(match) { - case UI_ALL_FOLDERS: - notifyUri = - UIPROVIDER_FOLDERLIST_NOTIFIER.buildUpon().appendEncodedPath(id).build(); - final Cursor vc = uiVirtualMailboxes(id, uiProjection); - if (id.equals(COMBINED_ACCOUNT_ID_STRING)) { - // There's no real mailboxes, so just return the virtual ones - c = vc; - } else { - // Return real and virtual mailboxes alike - final Cursor rawc = db.rawQuery(genQueryAccountAllMailboxes(uiProjection), - new String[] {id}); - rawc.setNotificationUri(context.getContentResolver(), notifyUri); - vc.setNotificationUri(context.getContentResolver(), notifyUri); - if (rawc.getCount() > 0) { - c = new MergeCursor(new Cursor[]{rawc, vc}); - } else { - c = rawc; - } - } - break; - case UI_FULL_FOLDERS: { - // We need a full projection for getFolderListCursor - final String[] folderProjection = folderProjectionFromUiProjection(uiProjection); - c = db.rawQuery(genQueryAccountAllMailboxes(folderProjection), new String[] {id}); - c = getFolderListCursor(c, Long.valueOf(id), uiProjection); - notifyUri = - UIPROVIDER_FOLDERLIST_NOTIFIER.buildUpon().appendEncodedPath(id).build(); - break; - } - case UI_RECENT_FOLDERS: - c = db.rawQuery(genQueryRecentMailboxes(uiProjection), new String[] {id}); - notifyUri = UIPROVIDER_RECENT_FOLDERS_NOTIFIER.buildUpon().appendPath(id).build(); - break; - case UI_SUBFOLDERS: { - // We need a full projection for getFolderListCursor - final String[] folderProjection = folderProjectionFromUiProjection(uiProjection); - c = db.rawQuery(genQuerySubfolders(folderProjection), new String[] {id}); - c = getFolderListCursor(c, Mailbox.getAccountIdForMailbox(context, id), - uiProjection); - // Get notifications for any folder changes on this account. This is broader than - // we need but otherwise we'd need for every folder change to notify on all relevant - // subtrees. For now we opt for simplicity. - final long accountId = Mailbox.getAccountIdForMailbox(context, id); - notifyUri = ContentUris.withAppendedId(UIPROVIDER_FOLDERLIST_NOTIFIER, accountId); - break; - } - case UI_MESSAGES: - long mailboxId = Long.parseLong(id); - final Folder folder = getFolder(context, mailboxId); - if (folder == null) { - // This mailboxId is bogus. Return an empty cursor - // TODO: Make callers of this query handle null cursors instead b/10819309 - return new MatrixCursor(uiProjection); - } - if (isVirtualMailbox(mailboxId)) { - c = getVirtualMailboxMessagesCursor(db, uiProjection, mailboxId, unseenOnly); - } else { - c = db.rawQuery( - genQueryMailboxMessages(uiProjection, unseenOnly), new String[] {id}); - } - notifyUri = UIPROVIDER_CONVERSATION_NOTIFIER.buildUpon().appendPath(id).build(); - c = new EmailConversationCursor(context, c, folder, mailboxId); - break; - case UI_MESSAGE: - MessageQuery qq = genQueryViewMessage(uiProjection, id); - String sql = qq.query; - String attJson = qq.attachmentJson; - // With attachments, we have another argument to bind - if (attJson != null) { - c = db.rawQuery(sql, new String[] {attJson, id}); - } else { - c = db.rawQuery(sql, new String[] {id}); - } - if (c != null) { - c = new EmailMessageCursor(getContext(), c, UIProvider.MessageColumns.BODY_HTML, - UIProvider.MessageColumns.BODY_TEXT); - } - notifyUri = UIPROVIDER_MESSAGE_NOTIFIER.buildUpon().appendPath(id).build(); - break; - case UI_ATTACHMENTS: - final List contentTypeQueryParameters = - uri.getQueryParameters(PhotoContract.ContentTypeParameters.CONTENT_TYPE); - c = db.rawQuery(genQueryAttachments(uiProjection, contentTypeQueryParameters), - new String[] {id}); - c = new AttachmentsCursor(context, c); - notifyUri = UIPROVIDER_ATTACHMENTS_NOTIFIER.buildUpon().appendPath(id).build(); - break; - case UI_ATTACHMENT: - c = db.rawQuery(genQueryAttachment(uiProjection), new String[] {id}); - notifyUri = UIPROVIDER_ATTACHMENT_NOTIFIER.buildUpon().appendPath(id).build(); - break; - case UI_ATTACHMENT_BY_CID: - final String cid = uri.getPathSegments().get(2); - final String[] selectionArgs = {id, cid}; - c = db.rawQuery(genQueryAttachmentByMessageIDAndCid(uiProjection), selectionArgs); - - // we don't have easy access to the attachment ID (which is buried in the cursor - // being returned), so we notify on the parent message object - notifyUri = UIPROVIDER_ATTACHMENTS_NOTIFIER.buildUpon().appendPath(id).build(); - break; - case UI_FOLDER: - case UI_INBOX: - if (match == UI_INBOX) { - mailboxId = Mailbox.findMailboxOfType(context, Long.parseLong(id), - Mailbox.TYPE_INBOX); - if (mailboxId == Mailbox.NO_MAILBOX) { - LogUtils.d(LogUtils.TAG, "No inbox found for account %s", id); - return null; - } - LogUtils.d(LogUtils.TAG, "Found inbox id %d", mailboxId); - } else { - mailboxId = Long.parseLong(id); - } - final String mailboxIdString = Long.toString(mailboxId); - if (isVirtualMailbox(mailboxId)) { - c = getVirtualMailboxCursor(mailboxId, uiProjection); - notifyUri = UIPROVIDER_FOLDER_NOTIFIER.buildUpon().appendPath(mailboxIdString) - .build(); - } else { - c = db.rawQuery(genQueryMailbox(uiProjection, mailboxIdString), - new String[]{mailboxIdString}); - final List projectionList = Arrays.asList(uiProjection); - final int nameColumn = projectionList.indexOf(UIProvider.FolderColumns.NAME); - final int typeColumn = projectionList.indexOf(UIProvider.FolderColumns.TYPE); - if (c.moveToFirst()) { - final Cursor closeThis = c; - try { - c = getUiFolderCursorRowFromMailboxCursorRow( - new MatrixCursorWithCachedColumns(uiProjection), - uiProjection.length, c, nameColumn, typeColumn); - } finally { - closeThis.close(); - } - } - notifyUri = UIPROVIDER_FOLDER_NOTIFIER.buildUpon().appendPath(mailboxIdString) - .build(); - } - break; - case UI_ACCOUNT: - if (id.equals(COMBINED_ACCOUNT_ID_STRING)) { - MatrixCursor mc = new MatrixCursorWithCachedColumns(uiProjection, 1); - addCombinedAccountRow(mc); - c = mc; - } else { - c = db.rawQuery(genQueryAccount(uiProjection, id), new String[] {id}); - } - notifyUri = UIPROVIDER_ACCOUNT_NOTIFIER.buildUpon().appendPath(id).build(); - break; - case UI_CONVERSATION: - c = db.rawQuery(genQueryConversation(uiProjection), new String[] {id}); - break; - } - if (notifyUri != null) { - c.setNotificationUri(resolver, notifyUri); - } - return c; - } - - /** - * Convert a UIProvider attachment to an EmailProvider attachment (for sending); we only need - * a few of the fields - * @param uiAtt the UIProvider attachment to convert - * @param cachedFile the path to the cached file to - * @return the EmailProvider attachment - */ - // TODO(pwestbro): once the Attachment contains the cached uri, the second parameter can be - // removed - // TODO(mhibdon): if the UI Attachment contained the account key, the third parameter could - // be removed. - private static Attachment convertUiAttachmentToAttachment( - com.android.mail.providers.Attachment uiAtt, String cachedFile, long accountKey) { - final Attachment att = new Attachment(); - - att.setContentUri(uiAtt.contentUri.toString()); - - if (!TextUtils.isEmpty(cachedFile)) { - // Generate the content provider uri for this cached file - final Uri.Builder cachedFileBuilder = Uri.parse( - "content://" + EmailContent.AUTHORITY + "/attachment/cachedFile").buildUpon(); - cachedFileBuilder.appendQueryParameter(Attachment.CACHED_FILE_QUERY_PARAM, cachedFile); - att.setCachedFileUri(cachedFileBuilder.build().toString()); - } - att.mAccountKey = accountKey; - att.mFileName = uiAtt.getName(); - att.mMimeType = uiAtt.getContentType(); - att.mSize = uiAtt.size; - return att; - } - - /** - * Create a mailbox given the account and mailboxType. - */ - private Mailbox createMailbox(long accountId, int mailboxType) { - Context context = getContext(); - Mailbox box = Mailbox.newSystemMailbox(context, accountId, mailboxType); - // Make sure drafts and save will show up in recents... - // If these already exist (from old Email app), they will have touch times - switch (mailboxType) { - case Mailbox.TYPE_DRAFTS: - box.mLastTouchedTime = Mailbox.DRAFTS_DEFAULT_TOUCH_TIME; - break; - case Mailbox.TYPE_SENT: - box.mLastTouchedTime = Mailbox.SENT_DEFAULT_TOUCH_TIME; - break; - } - box.save(context); - return box; - } - - /** - * Given an account name and a mailbox type, return that mailbox, creating it if necessary - * @param accountId the account id to use - * @param mailboxType the type of mailbox we're trying to find - * @return the mailbox of the given type for the account in the uri, or null if not found - */ - private Mailbox getMailboxByAccountIdAndType(final long accountId, final int mailboxType) { - Mailbox mailbox = Mailbox.restoreMailboxOfType(getContext(), accountId, mailboxType); - if (mailbox == null) { - mailbox = createMailbox(accountId, mailboxType); - } - return mailbox; - } - - /** - * Given a mailbox and the content values for a message, create/save the message in the mailbox - * @param mailbox the mailbox to use - * @param extras the bundle containing the message fields - * @return the uri of the newly created message - * TODO(yph): The following fields are available in extras but unused, verify whether they - * should be respected: - * - UIProvider.MessageColumns.SNIPPET - * - UIProvider.MessageColumns.REPLY_TO - * - UIProvider.MessageColumns.FROM - */ - private Uri uiSaveMessage(Message msg, Mailbox mailbox, Bundle extras) { - final Context context = getContext(); - // Fill in the message - final Account account = Account.restoreAccountWithId(context, mailbox.mAccountKey); - if (account == null) return null; - final String customFromAddress = - extras.getString(UIProvider.MessageColumns.CUSTOM_FROM_ADDRESS); - if (!TextUtils.isEmpty(customFromAddress)) { - msg.mFrom = customFromAddress; - } else { - msg.mFrom = account.getEmailAddress(); - } - msg.mTimeStamp = System.currentTimeMillis(); - msg.mTo = extras.getString(UIProvider.MessageColumns.TO); - msg.mCc = extras.getString(UIProvider.MessageColumns.CC); - msg.mBcc = extras.getString(UIProvider.MessageColumns.BCC); - msg.mSubject = extras.getString(UIProvider.MessageColumns.SUBJECT); - msg.mText = extras.getString(UIProvider.MessageColumns.BODY_TEXT); - msg.mHtml = extras.getString(UIProvider.MessageColumns.BODY_HTML); - msg.mMailboxKey = mailbox.mId; - msg.mAccountKey = mailbox.mAccountKey; - msg.mDisplayName = msg.mTo; - msg.mFlagLoaded = Message.FLAG_LOADED_COMPLETE; - msg.mFlagRead = true; - msg.mFlagSeen = true; - msg.mQuotedTextStartPos = extras.getInt(UIProvider.MessageColumns.QUOTE_START_POS, 0); - int flags = 0; - final int draftType = extras.getInt(UIProvider.MessageColumns.DRAFT_TYPE); - switch(draftType) { - case DraftType.FORWARD: - flags |= Message.FLAG_TYPE_FORWARD; - break; - case DraftType.REPLY_ALL: - flags |= Message.FLAG_TYPE_REPLY_ALL; - //$FALL-THROUGH$ - case DraftType.REPLY: - flags |= Message.FLAG_TYPE_REPLY; - break; - case DraftType.COMPOSE: - flags |= Message.FLAG_TYPE_ORIGINAL; - break; - } - int draftInfo = 0; - if (extras.containsKey(UIProvider.MessageColumns.QUOTE_START_POS)) { - draftInfo = extras.getInt(UIProvider.MessageColumns.QUOTE_START_POS); - if (extras.getInt(UIProvider.MessageColumns.APPEND_REF_MESSAGE_CONTENT) != 0) { - draftInfo |= Message.DRAFT_INFO_APPEND_REF_MESSAGE; - } - } - if (!extras.containsKey(UIProvider.MessageColumns.APPEND_REF_MESSAGE_CONTENT)) { - flags |= Message.FLAG_NOT_INCLUDE_QUOTED_TEXT; - } - msg.mDraftInfo = draftInfo; - msg.mFlags = flags; - - final String ref = extras.getString(UIProvider.MessageColumns.REF_MESSAGE_ID); - if (ref != null && msg.mQuotedTextStartPos >= 0) { - String refId = Uri.parse(ref).getLastPathSegment(); - try { - msg.mSourceKey = Long.parseLong(refId); - } catch (NumberFormatException e) { - // This will be zero; the default - } - } - - // Get attachments from the ContentValues - final List uiAtts = - com.android.mail.providers.Attachment.fromJSONArray( - extras.getString(UIProvider.MessageColumns.ATTACHMENTS)); - final ArrayList atts = new ArrayList(); - boolean hasUnloadedAttachments = false; - Bundle attachmentFds = - extras.getParcelable(UIProvider.SendOrSaveMethodParamKeys.OPENED_FD_MAP); - for (com.android.mail.providers.Attachment uiAtt: uiAtts) { - final Uri attUri = uiAtt.uri; - if (attUri != null && attUri.getAuthority().equals(EmailContent.AUTHORITY)) { - // If it's one of ours, retrieve the attachment and add it to the list - final long attId = Long.parseLong(attUri.getLastPathSegment()); - final Attachment att = Attachment.restoreAttachmentWithId(context, attId); - if (att != null) { - // We must clone the attachment into a new one for this message; easiest to - // use a parcel here - final Parcel p = Parcel.obtain(); - att.writeToParcel(p, 0); - p.setDataPosition(0); - final Attachment attClone = new Attachment(p); - p.recycle(); - // Clear the messageKey (this is going to be a new attachment) - attClone.mMessageKey = 0; - // If we're sending this, it's not loaded, and we're not smart forwarding - // add the download flag, so that ADS will start up - if (mailbox.mType == Mailbox.TYPE_OUTBOX && att.getContentUri() == null && - ((account.mFlags & Account.FLAGS_SUPPORTS_SMART_FORWARD) == 0)) { - attClone.mFlags |= Attachment.FLAG_DOWNLOAD_FORWARD; - hasUnloadedAttachments = true; - } - atts.add(attClone); - } - } else { - // Cache the attachment. This will allow us to send it, if the permissions are - // revoked. - final String cachedFileUri = - AttachmentUtils.cacheAttachmentUri(context, uiAtt, attachmentFds); - - // Convert external attachment to one of ours and add to the list - atts.add(convertUiAttachmentToAttachment(uiAtt, cachedFileUri, msg.mAccountKey)); - } - } - if (!atts.isEmpty()) { - msg.mAttachments = atts; - msg.mFlagAttachment = true; - if (hasUnloadedAttachments) { - Utility.showToast(context, R.string.message_view_attachment_background_load); - } - } - // Save it or update it... - if (!msg.isSaved()) { - msg.save(context); - } else { - // This is tricky due to how messages/attachments are saved; rather than putz with - // what's changed, we'll delete/re-add them - final ArrayList ops = - new ArrayList(); - // Delete all existing attachments - ops.add(ContentProviderOperation.newDelete( - ContentUris.withAppendedId(Attachment.MESSAGE_ID_URI, msg.mId)) - .build()); - // Delete the body - ops.add(ContentProviderOperation.newDelete(Body.CONTENT_URI) - .withSelection(BodyColumns.MESSAGE_KEY + "=?", - new String[] {Long.toString(msg.mId)}) - .build()); - // Add the ops for the message, atts, and body - msg.addSaveOps(ops); - // Do it! - try { - applyBatch(ops); - } catch (OperationApplicationException e) { - LogUtils.d(TAG, "applyBatch exception"); - } - } - notifyUIMessage(msg.mId); - - if (mailbox.mType == Mailbox.TYPE_OUTBOX) { - startSync(mailbox, 0); - final long originalMsgId = msg.mSourceKey; - if (originalMsgId != 0) { - final Message originalMsg = Message.restoreMessageWithId(context, originalMsgId); - // If the original message exists, set its forwarded/replied to flags - if (originalMsg != null) { - final ContentValues cv = new ContentValues(); - flags = originalMsg.mFlags; - switch(draftType) { - case DraftType.FORWARD: - flags |= Message.FLAG_FORWARDED; - break; - case DraftType.REPLY_ALL: - case DraftType.REPLY: - flags |= Message.FLAG_REPLIED_TO; - break; - } - cv.put(MessageColumns.FLAGS, flags); - context.getContentResolver().update(ContentUris.withAppendedId( - Message.CONTENT_URI, originalMsgId), cv, null, null); - } - } - } - return uiUri("uimessage", msg.mId); - } - - private Uri uiSaveDraftMessage(final long accountId, final Bundle extras) { - final Mailbox mailbox = - getMailboxByAccountIdAndType(accountId, Mailbox.TYPE_DRAFTS); - if (mailbox == null) return null; - Message msg = null; - if (extras.containsKey(BaseColumns._ID)) { - final long messageId = extras.getLong(BaseColumns._ID); - msg = Message.restoreMessageWithId(getContext(), messageId); - } - if (msg == null) { - msg = new Message(); - } - return uiSaveMessage(msg, mailbox, extras); - } - - private Uri uiSendDraftMessage(final long accountId, final Bundle extras) { - final Message msg; - if (extras.containsKey(BaseColumns._ID)) { - final long messageId = extras.getLong(BaseColumns._ID); - msg = Message.restoreMessageWithId(getContext(), messageId); - } else { - msg = new Message(); - } - - if (msg == null) return null; - final Mailbox mailbox = getMailboxByAccountIdAndType(accountId, Mailbox.TYPE_OUTBOX); - if (mailbox == null) return null; - // Make sure the sent mailbox exists, since it will be necessary soon. - // TODO(yph): move system mailbox creation to somewhere sane. - final Mailbox sentMailbox = getMailboxByAccountIdAndType(accountId, Mailbox.TYPE_SENT); - if (sentMailbox == null) return null; - final Uri messageUri = uiSaveMessage(msg, mailbox, extras); - // Kick observers - notifyUI(Mailbox.CONTENT_URI, null); - return messageUri; - } - - private static void putIntegerLongOrBoolean(ContentValues values, String columnName, - Object value) { - if (value instanceof Integer) { - Integer intValue = (Integer)value; - values.put(columnName, intValue); - } else if (value instanceof Boolean) { - Boolean boolValue = (Boolean)value; - values.put(columnName, boolValue ? 1 : 0); - } else if (value instanceof Long) { - Long longValue = (Long)value; - values.put(columnName, longValue); - } - } - - /** - * Update the timestamps for the folders specified and notifies on the recent folder URI. - * @param folders array of folder Uris to update - * @return number of folders updated - */ - private int updateTimestamp(final Context context, String id, Uri[] folders){ - int updated = 0; - final long now = System.currentTimeMillis(); - final ContentResolver resolver = context.getContentResolver(); - final ContentValues touchValues = new ContentValues(1); - for (final Uri folder : folders) { - touchValues.put(MailboxColumns.LAST_TOUCHED_TIME, now); - LogUtils.d(TAG, "updateStamp: %s updated", folder); - updated += resolver.update(folder, touchValues, null, null); - } - final Uri toNotify = - UIPROVIDER_RECENT_FOLDERS_NOTIFIER.buildUpon().appendPath(id).build(); - LogUtils.d(TAG, "updateTimestamp: Notifying on %s", toNotify); - notifyUI(toNotify, null); - return updated; - } - - /** - * Updates the recent folders. The values to be updated are specified as ContentValues pairs - * of (Folder URI, access timestamp). Returns nonzero if successful, always. - * @param uri provider query uri - * @param values uri, timestamp pairs - * @return nonzero value always. - */ - private int uiUpdateRecentFolders(Uri uri, ContentValues values) { - final int numFolders = values.size(); - final String id = uri.getPathSegments().get(1); - final Uri[] folders = new Uri[numFolders]; - final Context context = getContext(); - int i = 0; - for (final String uriString : values.keySet()) { - folders[i] = Uri.parse(uriString); - } - return updateTimestamp(context, id, folders); - } - - /** - * Populates the recent folders according to the design. - * @param uri provider query uri - * @return the number of recent folders were populated. - */ - private int uiPopulateRecentFolders(Uri uri) { - final Context context = getContext(); - final String id = uri.getLastPathSegment(); - final Uri[] recentFolders = defaultRecentFolders(id); - final int numFolders = recentFolders.length; - if (numFolders <= 0) { - return 0; - } - final int rowsUpdated = updateTimestamp(context, id, recentFolders); - LogUtils.d(TAG, "uiPopulateRecentFolders: %d folders changed", rowsUpdated); - return rowsUpdated; - } - - private int uiUpdateAttachment(Uri uri, ContentValues uiValues) { - int result = 0; - Integer stateValue = uiValues.getAsInteger(UIProvider.AttachmentColumns.STATE); - if (stateValue != null) { - // This is a command from UIProvider - long attachmentId = Long.parseLong(uri.getLastPathSegment()); - Context context = getContext(); - Attachment attachment = - Attachment.restoreAttachmentWithId(context, attachmentId); - if (attachment == null) { - // Went away; ah, well... - return result; - } - int state = stateValue; - ContentValues values = new ContentValues(); - if (state == UIProvider.AttachmentState.NOT_SAVED - || state == UIProvider.AttachmentState.REDOWNLOADING) { - // Set state, try to cancel request - values.put(AttachmentColumns.UI_STATE, UIProvider.AttachmentState.NOT_SAVED); - values.put(AttachmentColumns.FLAGS, - attachment.mFlags &= ~Attachment.FLAG_DOWNLOAD_USER_REQUEST); - attachment.update(context, values); - result = 1; - } - if (state == UIProvider.AttachmentState.DOWNLOADING - || state == UIProvider.AttachmentState.REDOWNLOADING) { - // Set state and destination; request download - values.put(AttachmentColumns.UI_STATE, UIProvider.AttachmentState.DOWNLOADING); - Integer destinationValue = - uiValues.getAsInteger(UIProvider.AttachmentColumns.DESTINATION); - values.put(AttachmentColumns.UI_DESTINATION, - destinationValue == null ? 0 : destinationValue); - values.put(AttachmentColumns.FLAGS, - attachment.mFlags | Attachment.FLAG_DOWNLOAD_USER_REQUEST); - - if (values.containsKey(AttachmentColumns.LOCATION) && - TextUtils.isEmpty(values.getAsString(AttachmentColumns.LOCATION))) { - LogUtils.w(TAG, new Throwable(), "attachment with blank location"); - } - - attachment.update(context, values); - result = 1; - } - if (state == UIProvider.AttachmentState.SAVED) { - // If this is an inline attachment, notify message has changed - if (!TextUtils.isEmpty(attachment.mContentId)) { - notifyUI(UIPROVIDER_MESSAGE_NOTIFIER, attachment.mMessageKey); - } - result = 1; - } - } - return result; - } - - private int uiUpdateFolder(final Context context, Uri uri, ContentValues uiValues) { - // We need to mark seen separately - if (uiValues.containsKey(UIProvider.ConversationColumns.SEEN)) { - final int seenValue = uiValues.getAsInteger(UIProvider.ConversationColumns.SEEN); - - if (seenValue == 1) { - final String mailboxId = uri.getLastPathSegment(); - final int rows = markAllSeen(context, mailboxId); - - if (uiValues.size() == 1) { - // Nothing else to do, so return this value - return rows; - } - } - } - - final Uri ourUri = convertToEmailProviderUri(uri, Mailbox.CONTENT_URI, true); - if (ourUri == null) return 0; - ContentValues ourValues = new ContentValues(); - // This should only be called via update to "recent folders" - for (String columnName: uiValues.keySet()) { - if (columnName.equals(MailboxColumns.LAST_TOUCHED_TIME)) { - ourValues.put(MailboxColumns.LAST_TOUCHED_TIME, uiValues.getAsLong(columnName)); - } - } - return update(ourUri, ourValues, null, null); - } - - private int uiUpdateSettings(final Context c, final ContentValues uiValues) { - final MailPrefs mailPrefs = MailPrefs.get(c); - - if (uiValues.containsKey(SettingsColumns.AUTO_ADVANCE)) { - mailPrefs.setAutoAdvanceMode(uiValues.getAsInteger(SettingsColumns.AUTO_ADVANCE)); - } - if (uiValues.containsKey(SettingsColumns.CONVERSATION_VIEW_MODE)) { - final int value = uiValues.getAsInteger(SettingsColumns.CONVERSATION_VIEW_MODE); - final boolean overviewMode = value == UIProvider.ConversationViewMode.OVERVIEW; - mailPrefs.setConversationOverviewMode(overviewMode); - } - - c.getContentResolver().notifyChange(UIPROVIDER_ALL_ACCOUNTS_NOTIFIER, null, false); - - return 1; - } - - private int markAllSeen(final Context context, final String mailboxId) { - final SQLiteDatabase db = getDatabase(context); - final String table = Message.TABLE_NAME; - final ContentValues values = new ContentValues(1); - values.put(MessageColumns.FLAG_SEEN, 1); - final String whereClause = MessageColumns.MAILBOX_KEY + " = ?"; - final String[] whereArgs = new String[] {mailboxId}; - - return db.update(table, values, whereClause, whereArgs); - } - - private ContentValues convertUiMessageValues(Message message, ContentValues values) { - final ContentValues ourValues = new ContentValues(); - for (String columnName : values.keySet()) { - final Object val = values.get(columnName); - if (columnName.equals(UIProvider.ConversationColumns.STARRED)) { - putIntegerLongOrBoolean(ourValues, MessageColumns.FLAG_FAVORITE, val); - } else if (columnName.equals(UIProvider.ConversationColumns.READ)) { - putIntegerLongOrBoolean(ourValues, MessageColumns.FLAG_READ, val); - } else if (columnName.equals(UIProvider.ConversationColumns.SEEN)) { - putIntegerLongOrBoolean(ourValues, MessageColumns.FLAG_SEEN, val); - } else if (columnName.equals(MessageColumns.MAILBOX_KEY)) { - putIntegerLongOrBoolean(ourValues, MessageColumns.MAILBOX_KEY, val); - } else if (columnName.equals(UIProvider.ConversationOperations.FOLDERS_UPDATED)) { - // Skip this column, as the folders will also be specified the RAW_FOLDERS column - } else if (columnName.equals(UIProvider.ConversationColumns.RAW_FOLDERS)) { - // Convert from folder list uri to mailbox key - final FolderList flist = FolderList.fromBlob(values.getAsByteArray(columnName)); - if (flist.folders.size() != 1) { - LogUtils.e(TAG, - "Incorrect number of folders for this message: Message is %s", - message.mId); - } else { - final Folder f = flist.folders.get(0); - final Uri uri = f.folderUri.fullUri; - final Long mailboxId = Long.parseLong(uri.getLastPathSegment()); - putIntegerLongOrBoolean(ourValues, MessageColumns.MAILBOX_KEY, mailboxId); - } - } else if (columnName.equals(UIProvider.MessageColumns.ALWAYS_SHOW_IMAGES)) { - Address[] fromList = Address.fromHeader(message.mFrom); - final MailPrefs mailPrefs = MailPrefs.get(getContext()); - for (Address sender : fromList) { - final String email = sender.getAddress(); - mailPrefs.setDisplayImagesFromSender(email, null); - } - } else if (columnName.equals(UIProvider.ConversationColumns.VIEWED) || - columnName.equals(UIProvider.ConversationOperations.Parameters.SUPPRESS_UNDO)) { - // Ignore for now - } else if (UIProvider.ConversationColumns.CONVERSATION_INFO.equals(columnName)) { - // Email's conversation info is generated, not stored, so just ignore this update - } else { - throw new IllegalArgumentException("Can't update " + columnName + " in message"); - } - } - return ourValues; - } - - private static Uri convertToEmailProviderUri(Uri uri, Uri newBaseUri, boolean asProvider) { - final String idString = uri.getLastPathSegment(); - try { - final long id = Long.parseLong(idString); - Uri ourUri = ContentUris.withAppendedId(newBaseUri, id); - if (asProvider) { - ourUri = ourUri.buildUpon().appendQueryParameter(IS_UIPROVIDER, "true").build(); - } - return ourUri; - } catch (NumberFormatException e) { - return null; - } - } - - private Message getMessageFromLastSegment(Uri uri) { - long messageId = Long.parseLong(uri.getLastPathSegment()); - return Message.restoreMessageWithId(getContext(), messageId); - } - - /** - * Add an undo operation for the current sequence; if the sequence is newer than what we've had, - * clear out the undo list and start over - * @param uri the uri we're working on - * @param op the ContentProviderOperation to perform upon undo - */ - private void addToSequence(Uri uri, ContentProviderOperation op) { - String sequenceString = uri.getQueryParameter(UIProvider.SEQUENCE_QUERY_PARAMETER); - if (sequenceString != null) { - int sequence = Integer.parseInt(sequenceString); - if (sequence > mLastSequence) { - // Reset sequence - mLastSequenceOps.clear(); - mLastSequence = sequence; - } - // TODO: Need something to indicate a change isn't ready (undoable) - mLastSequenceOps.add(op); - } - } - - // TODO: This should depend on flags on the mailbox... - private static boolean uploadsToServer(Context context, Mailbox m) { - if (m.mType == Mailbox.TYPE_DRAFTS || m.mType == Mailbox.TYPE_OUTBOX || - m.mType == Mailbox.TYPE_SEARCH) { - return false; - } - String protocol = Account.getProtocol(context, m.mAccountKey); - EmailServiceInfo info = EmailServiceUtils.getServiceInfo(context, protocol); - return (info != null && info.syncChanges); - } - - private int uiUpdateMessage(Uri uri, ContentValues values) { - return uiUpdateMessage(uri, values, false); - } - - private int uiUpdateMessage(Uri uri, ContentValues values, boolean forceSync) { - Context context = getContext(); - Message msg = getMessageFromLastSegment(uri); - if (msg == null) return 0; - Mailbox mailbox = Mailbox.restoreMailboxWithId(context, msg.mMailboxKey); - if (mailbox == null) return 0; - Uri ourBaseUri = - (forceSync || uploadsToServer(context, mailbox)) ? Message.SYNCED_CONTENT_URI : - Message.CONTENT_URI; - Uri ourUri = convertToEmailProviderUri(uri, ourBaseUri, true); - if (ourUri == null) return 0; - - // Special case - meeting response - if (values.containsKey(UIProvider.MessageOperations.RESPOND_COLUMN)) { - final EmailServiceProxy service = - EmailServiceUtils.getServiceForAccount(context, mailbox.mAccountKey); - try { - service.sendMeetingResponse(msg.mId, - values.getAsInteger(UIProvider.MessageOperations.RESPOND_COLUMN)); - // Delete the message immediately - uiDeleteMessage(uri); - Utility.showToast(context, R.string.confirm_response); - // Notify box has changed so the deletion is reflected in the UI - notifyUIConversationMailbox(mailbox.mId); - } catch (RemoteException e) { - LogUtils.d(TAG, "Remote exception while sending meeting response"); - } - return 1; - } - - // Another special case - deleting a draft. - final String operation = values.getAsString( - UIProvider.ConversationOperations.OPERATION_KEY); - // TODO: for now let's just default to delete for MOVE_FAILED_TO_DRAFT operation - if (UIProvider.ConversationOperations.DISCARD_DRAFTS.equals(operation) || - UIProvider.ConversationOperations.MOVE_FAILED_TO_DRAFTS.equals(operation)) { - uiDeleteMessage(uri); - return 1; - } - - ContentValues undoValues = new ContentValues(); - ContentValues ourValues = convertUiMessageValues(msg, values); - for (String columnName: ourValues.keySet()) { - if (columnName.equals(MessageColumns.MAILBOX_KEY)) { - undoValues.put(MessageColumns.MAILBOX_KEY, msg.mMailboxKey); - } else if (columnName.equals(MessageColumns.FLAG_READ)) { - undoValues.put(MessageColumns.FLAG_READ, msg.mFlagRead); - } else if (columnName.equals(MessageColumns.FLAG_SEEN)) { - undoValues.put(MessageColumns.FLAG_SEEN, msg.mFlagSeen); - } else if (columnName.equals(MessageColumns.FLAG_FAVORITE)) { - undoValues.put(MessageColumns.FLAG_FAVORITE, msg.mFlagFavorite); - } - } - if (undoValues.size() == 0) { - return -1; - } - final Boolean suppressUndo = - values.getAsBoolean(UIProvider.ConversationOperations.Parameters.SUPPRESS_UNDO); - if (suppressUndo == null || !suppressUndo) { - final ContentProviderOperation op = - ContentProviderOperation.newUpdate(convertToEmailProviderUri( - uri, ourBaseUri, false)) - .withValues(undoValues) - .build(); - addToSequence(uri, op); - } - - return update(ourUri, ourValues, null, null); - } - - /** - * Projection for use with getting mailbox & account keys for a message. - */ - private static final String[] MESSAGE_KEYS_PROJECTION = - { MessageColumns.MAILBOX_KEY, MessageColumns.ACCOUNT_KEY }; - private static final int MESSAGE_KEYS_MAILBOX_KEY_COLUMN = 0; - private static final int MESSAGE_KEYS_ACCOUNT_KEY_COLUMN = 1; - - /** - * Notify necessary UI components in response to a message update. - * @param uri The {@link Uri} for this message update. - * @param messageId The id of the message that's been updated. - * @param values The {@link ContentValues} that were updated in the message. - */ - private void handleMessageUpdateNotifications(final Uri uri, final String messageId, - final ContentValues values) { - if (!uri.getBooleanQueryParameter(IS_UIPROVIDER, false)) { - notifyUIConversation(uri); - } - notifyUIMessage(messageId); - // TODO: Ideally, also test that the values actually changed. - if (values.containsKey(MessageColumns.FLAG_READ) || - values.containsKey(MessageColumns.MAILBOX_KEY)) { - final Cursor c = query( - Message.CONTENT_URI.buildUpon().appendEncodedPath(messageId).build(), - MESSAGE_KEYS_PROJECTION, null, null, null); - if (c != null) { - try { - if (c.moveToFirst()) { - notifyUIFolder(c.getLong(MESSAGE_KEYS_MAILBOX_KEY_COLUMN), - c.getLong(MESSAGE_KEYS_ACCOUNT_KEY_COLUMN)); - } - } finally { - c.close(); - } - } - } - } - - /** - * Perform a "Delete" operation - * @param uri message to delete - * @return number of rows affected - */ - private int uiDeleteMessage(Uri uri) { - final Context context = getContext(); - Message msg = getMessageFromLastSegment(uri); - if (msg == null) return 0; - Mailbox mailbox = Mailbox.restoreMailboxWithId(context, msg.mMailboxKey); - if (mailbox == null) return 0; - if (mailbox.mType == Mailbox.TYPE_TRASH || mailbox.mType == Mailbox.TYPE_DRAFTS) { - // We actually delete these, including attachments - AttachmentUtilities.deleteAllAttachmentFiles(context, msg.mAccountKey, msg.mId); - final int r = context.getContentResolver().delete( - ContentUris.withAppendedId(Message.SYNCED_CONTENT_URI, msg.mId), null, null); - notifyUIFolder(mailbox.mId, mailbox.mAccountKey); - notifyUIMessage(msg.mId); - return r; - } - Mailbox trashMailbox = - Mailbox.restoreMailboxOfType(context, msg.mAccountKey, Mailbox.TYPE_TRASH); - if (trashMailbox == null) { - return 0; - } - ContentValues values = new ContentValues(); - values.put(MessageColumns.MAILBOX_KEY, trashMailbox.mId); - final int r = uiUpdateMessage(uri, values, true); - notifyUIFolder(mailbox.mId, mailbox.mAccountKey); - notifyUIMessage(msg.mId); - return r; - } - - /** - * Hard delete all synced messages in a particular mailbox - * @param uri Mailbox to empty (Trash, or maybe Spam/Junk later) - * @return number of rows affected - */ - private int uiPurgeFolder(Uri uri) { - final Context context = getContext(); - final long mailboxId = Long.parseLong(uri.getLastPathSegment()); - final SQLiteDatabase db = getDatabase(context); - - // Find the account ID (needed in a few calls) - final Cursor mailboxCursor = db.query( - Mailbox.TABLE_NAME, new String[] { MailboxColumns.ACCOUNT_KEY }, - Mailbox._ID + "=" + mailboxId, null, null, null, null); - if (mailboxCursor == null || !mailboxCursor.moveToFirst()) { - LogUtils.wtf(LogUtils.TAG, "Null or empty cursor when trying to purge mailbox %d", - mailboxId); - return 0; - } - final long accountId = mailboxCursor.getLong(mailboxCursor.getColumnIndex( - MailboxColumns.ACCOUNT_KEY)); - - // Find all the messages in the mailbox - final String[] messageProjection = - new String[] { MessageColumns._ID }; - final String messageWhere = MessageColumns.MAILBOX_KEY + "=" + mailboxId; - final Cursor messageCursor = db.query(Message.TABLE_NAME, messageProjection, messageWhere, - null, null, null, null); - int deletedCount = 0; - - // Kill them with fire - while (messageCursor != null && messageCursor.moveToNext()) { - final long messageId = messageCursor.getLong(messageCursor.getColumnIndex( - MessageColumns._ID)); - AttachmentUtilities.deleteAllAttachmentFiles(context, accountId, messageId); - deletedCount += context.getContentResolver().delete( - ContentUris.withAppendedId(Message.SYNCED_CONTENT_URI, messageId), null, null); - notifyUIMessage(messageId); - } - - notifyUIFolder(mailboxId, accountId); - return deletedCount; - } - - public static final String PICKER_UI_ACCOUNT = "picker_ui_account"; - public static final String PICKER_MAILBOX_TYPE = "picker_mailbox_type"; - // Currently unused - //public static final String PICKER_MESSAGE_ID = "picker_message_id"; - public static final String PICKER_HEADER_ID = "picker_header_id"; - - private int pickFolder(Uri uri, int type, int headerId) { - Context context = getContext(); - Long acctId = Long.parseLong(uri.getLastPathSegment()); - // For push imap, for example, we want the user to select the trash mailbox - Cursor ac = query(uiUri("uiaccount", acctId), UIProvider.ACCOUNTS_PROJECTION, - null, null, null); - try { - if (ac.moveToFirst()) { - final com.android.mail.providers.Account uiAccount = - com.android.mail.providers.Account.builder().buildFrom(ac); - Intent intent = new Intent(context, FolderPickerActivity.class); - intent.putExtra(PICKER_UI_ACCOUNT, uiAccount); - intent.putExtra(PICKER_MAILBOX_TYPE, type); - intent.putExtra(PICKER_HEADER_ID, headerId); - intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - context.startActivity(intent); - return 1; - } - return 0; - } finally { - ac.close(); - } - } - - private int pickTrashFolder(Uri uri) { - return pickFolder(uri, Mailbox.TYPE_TRASH, R.string.trash_folder_selection_title); - } - - private int pickSentFolder(Uri uri) { - return pickFolder(uri, Mailbox.TYPE_SENT, R.string.sent_folder_selection_title); - } - - private Cursor uiUndo(String[] projection) { - // First see if we have any operations saved - // TODO: Make sure seq matches - if (!mLastSequenceOps.isEmpty()) { - try { - // TODO Always use this projection? Or what's passed in? - // Not sure if UI wants it, but I'm making a cursor of convo uri's - MatrixCursor c = new MatrixCursorWithCachedColumns( - new String[] {UIProvider.ConversationColumns.URI}, - mLastSequenceOps.size()); - for (ContentProviderOperation op: mLastSequenceOps) { - c.addRow(new String[] {op.getUri().toString()}); - } - // Just apply the batch and we're done! - applyBatch(mLastSequenceOps); - // But clear the operations - mLastSequenceOps.clear(); - return c; - } catch (OperationApplicationException e) { - LogUtils.d(TAG, "applyBatch exception"); - } - } - return new MatrixCursorWithCachedColumns(projection, 0); - } - - private void notifyUIConversation(Uri uri) { - String id = uri.getLastPathSegment(); - Message msg = Message.restoreMessageWithId(getContext(), Long.parseLong(id)); - if (msg != null) { - notifyUIConversationMailbox(msg.mMailboxKey); - } - } - - /** - * Notify about the Mailbox id passed in - * @param id the Mailbox id to be notified - */ - private void notifyUIConversationMailbox(long id) { - notifyUI(UIPROVIDER_CONVERSATION_NOTIFIER, Long.toString(id)); - Mailbox mailbox = Mailbox.restoreMailboxWithId(getContext(), id); - if (mailbox == null) { - LogUtils.w(TAG, "No mailbox for notification: " + id); - return; - } - // Notify combined inbox... - if (mailbox.mType == Mailbox.TYPE_INBOX) { - notifyUI(UIPROVIDER_CONVERSATION_NOTIFIER, - EmailProvider.combinedMailboxId(Mailbox.TYPE_INBOX)); - } - notifyWidgets(id); - } - - /** - * Notify about the message id passed in - * @param id the message id to be notified - */ - private void notifyUIMessage(long id) { - notifyUI(UIPROVIDER_MESSAGE_NOTIFIER, id); - } - - /** - * Notify about the message id passed in - * @param id the message id to be notified - */ - private void notifyUIMessage(String id) { - notifyUI(UIPROVIDER_MESSAGE_NOTIFIER, id); - } - - /** - * Notify about the Account id passed in - * @param id the Account id to be notified - */ - private void notifyUIAccount(long id) { - // Notify on the specific account - notifyUI(UIPROVIDER_ACCOUNT_NOTIFIER, Long.toString(id)); - - // Notify on the all accounts list - notifyUI(UIPROVIDER_ALL_ACCOUNTS_NOTIFIER, null); - } - - // TODO: temporary workaround for ConversationCursor - @Deprecated - private static final int NOTIFY_FOLDER_LOOP_MESSAGE_ID = 0; - @Deprecated - private Handler mFolderNotifierHandler; - - /** - * Notify about a folder update. Because folder changes can affect the conversation cursor's - * extras, the conversation must also be notified here. - * @param folderId the folder id to be notified - * @param accountId the account id to be notified (for folder list notification). - */ - private void notifyUIFolder(final String folderId, final long accountId) { - notifyUI(UIPROVIDER_CONVERSATION_NOTIFIER, folderId); - notifyUI(UIPROVIDER_FOLDER_NOTIFIER, folderId); - if (accountId != Account.NO_ACCOUNT) { - notifyUI(UIPROVIDER_FOLDERLIST_NOTIFIER, accountId); - } - - // Notify for combined account too - // TODO: might be nice to only notify when an inbox changes - notifyUI(UIPROVIDER_FOLDER_NOTIFIER, - getVirtualMailboxId(COMBINED_ACCOUNT_ID, Mailbox.TYPE_INBOX)); - notifyUI(UIPROVIDER_FOLDERLIST_NOTIFIER, COMBINED_ACCOUNT_ID); - - // TODO: temporary workaround for ConversationCursor - synchronized (this) { - if (mFolderNotifierHandler == null) { - mFolderNotifierHandler = new Handler(Looper.getMainLooper(), - new Callback() { - @Override - public boolean handleMessage(final android.os.Message message) { - final String folderId = (String) message.obj; - LogUtils.d(TAG, "Notifying conversation Uri %s twice", folderId); - notifyUI(UIPROVIDER_CONVERSATION_NOTIFIER, folderId); - return true; - } - }); - } - } - mFolderNotifierHandler.removeMessages(NOTIFY_FOLDER_LOOP_MESSAGE_ID); - android.os.Message message = android.os.Message.obtain(mFolderNotifierHandler, - NOTIFY_FOLDER_LOOP_MESSAGE_ID); - message.obj = folderId; - mFolderNotifierHandler.sendMessageDelayed(message, 2000); - } - - private void notifyUIFolder(final long folderId, final long accountId) { - notifyUIFolder(Long.toString(folderId), accountId); - } - - private void notifyUI(final Uri uri, final String id) { - final Uri notifyUri = (id != null) ? uri.buildUpon().appendPath(id).build() : uri; - final Set batchNotifications = getBatchNotificationsSet(); - if (batchNotifications != null) { - batchNotifications.add(notifyUri); - } else { - getContext().getContentResolver().notifyChange(notifyUri, null); - } - } - - private void notifyUI(Uri uri, long id) { - notifyUI(uri, Long.toString(id)); - } - - private Mailbox getMailbox(final Uri uri) { - final long id = Long.parseLong(uri.getLastPathSegment()); - return Mailbox.restoreMailboxWithId(getContext(), id); - } - - /** - * Create an android.accounts.Account object for this account. - * @param accountId id of account to load. - * @return an android.accounts.Account for this account, or null if we can't load it. - */ - private android.accounts.Account getAccountManagerAccount(final long accountId) { - final Context context = getContext(); - final Account account = Account.restoreAccountWithId(context, accountId); - if (account == null) return null; - return getAccountManagerAccount(context, account.mEmailAddress, - account.getProtocol(context)); - } - - /** - * Create an android.accounts.Account object for an emailAddress/protocol pair. - * @param context A {@link Context}. - * @param emailAddress The email address we're interested in. - * @param protocol The protocol we're intereted in. - * @return an {@link android.accounts.Account} for this info. - */ - private static android.accounts.Account getAccountManagerAccount(final Context context, - final String emailAddress, final String protocol) { - final EmailServiceInfo info = EmailServiceUtils.getServiceInfo(context, protocol); - if (info == null) { - return null; - } - return new android.accounts.Account(emailAddress, info.accountType); - } - - /** - * Update an account's periodic sync if the sync interval has changed. - * @param accountId id for the account to update. - * @param values the ContentValues for this update to the account. - */ - private void updateAccountSyncInterval(final long accountId, final ContentValues values) { - final Integer syncInterval = values.getAsInteger(AccountColumns.SYNC_INTERVAL); - if (syncInterval == null) { - // No change to the sync interval. - return; - } - final android.accounts.Account account = getAccountManagerAccount(accountId); - if (account == null) { - // Unable to load the account, or unknown protocol. - return; - } - - LogUtils.d(TAG, "Setting sync interval for account %s to %d minutes", - accountId, syncInterval); - - // First remove all existing periodic syncs. - final List syncs = - ContentResolver.getPeriodicSyncs(account, EmailContent.AUTHORITY); - for (final PeriodicSync sync : syncs) { - ContentResolver.removePeriodicSync(account, EmailContent.AUTHORITY, sync.extras); - } - - // Only positive values of sync interval indicate periodic syncs. The value is in minutes, - // while addPeriodicSync expects its time in seconds. - if (syncInterval > 0) { - ContentResolver.addPeriodicSync(account, EmailContent.AUTHORITY, Bundle.EMPTY, - syncInterval * DateUtils.MINUTE_IN_MILLIS / DateUtils.SECOND_IN_MILLIS); - } - } - - /** - * Request a sync. - * @param account The {@link android.accounts.Account} we want to sync. - * @param mailboxId The mailbox id we want to sync (or one of the special constants in - * {@link Mailbox}). - * @param deltaMessageCount If we're requesting a load more, the number of additional messages - * to sync. - */ - private static void startSync(final android.accounts.Account account, final long mailboxId, - final int deltaMessageCount) { - final Bundle extras = Mailbox.createSyncBundle(mailboxId); - extras.putBoolean(ContentResolver.SYNC_EXTRAS_MANUAL, true); - extras.putBoolean(ContentResolver.SYNC_EXTRAS_DO_NOT_RETRY, true); - extras.putBoolean(ContentResolver.SYNC_EXTRAS_EXPEDITED, true); - if (deltaMessageCount != 0) { - extras.putInt(Mailbox.SYNC_EXTRA_DELTA_MESSAGE_COUNT, deltaMessageCount); - } - extras.putString(EmailServiceStatus.SYNC_EXTRAS_CALLBACK_URI, - EmailContent.CONTENT_URI.toString()); - extras.putString(EmailServiceStatus.SYNC_EXTRAS_CALLBACK_METHOD, - SYNC_STATUS_CALLBACK_METHOD); - ContentResolver.requestSync(account, EmailContent.AUTHORITY, extras); - LogUtils.i(TAG, "requestSync EmailProvider startSync %s, %s", account.toString(), - extras.toString()); - } - - /** - * Request a sync. - * @param mailbox The {@link Mailbox} we want to sync. - * @param deltaMessageCount If we're requesting a load more, the number of additional messages - * to sync. - */ - private void startSync(final Mailbox mailbox, final int deltaMessageCount) { - final android.accounts.Account account = getAccountManagerAccount(mailbox.mAccountKey); - if (account != null) { - startSync(account, mailbox.mId, deltaMessageCount); - } - } - - /** - * Restart any push operations for an account. - * @param account The {@link android.accounts.Account} we're interested in. - */ - private static void restartPush(final android.accounts.Account account) { - final Bundle extras = new Bundle(); - extras.putBoolean(ContentResolver.SYNC_EXTRAS_MANUAL, true); - extras.putBoolean(ContentResolver.SYNC_EXTRAS_DO_NOT_RETRY, true); - extras.putBoolean(ContentResolver.SYNC_EXTRAS_EXPEDITED, true); - extras.putBoolean(Mailbox.SYNC_EXTRA_PUSH_ONLY, true); - extras.putString(EmailServiceStatus.SYNC_EXTRAS_CALLBACK_URI, - EmailContent.CONTENT_URI.toString()); - extras.putString(EmailServiceStatus.SYNC_EXTRAS_CALLBACK_METHOD, - SYNC_STATUS_CALLBACK_METHOD); - ContentResolver.requestSync(account, EmailContent.AUTHORITY, extras); - LogUtils.i(TAG, "requestSync EmailProvider startSync %s, %s", account.toString(), - extras.toString()); - } - - private Cursor uiFolderRefresh(final Mailbox mailbox, final int deltaMessageCount) { - if (mailbox != null) { - RefreshStatusMonitor.getInstance(getContext()) - .monitorRefreshStatus(mailbox.mId, new RefreshStatusMonitor.Callback() { - @Override - public void onRefreshCompleted(long mailboxId, int result) { - final ContentValues values = new ContentValues(); - values.put(Mailbox.UI_SYNC_STATUS, UIProvider.SyncStatus.NO_SYNC); - values.put(Mailbox.UI_LAST_SYNC_RESULT, result); - mDatabase.update( - Mailbox.TABLE_NAME, - values, - WHERE_ID, - new String[] { String.valueOf(mailboxId) }); - notifyUIFolder(mailbox.mId, mailbox.mAccountKey); - } - - @Override - public void onTimeout(long mailboxId) { - // todo - } - }); - startSync(mailbox, deltaMessageCount); - } - return null; - } - - //Number of additional messages to load when a user selects "Load more..." in POP/IMAP boxes - public static final int VISIBLE_LIMIT_INCREMENT = 10; - //Number of additional messages to load when a user selects "Load more..." in a search - public static final int SEARCH_MORE_INCREMENT = 10; - - private Cursor uiFolderLoadMore(final Mailbox mailbox) { - if (mailbox == null) return null; - if (mailbox.mType == Mailbox.TYPE_SEARCH) { - // Ask for 10 more messages - mSearchParams.mOffset += SEARCH_MORE_INCREMENT; - runSearchQuery(getContext(), mailbox.mAccountKey, mailbox.mId); - } else { - uiFolderRefresh(mailbox, VISIBLE_LIMIT_INCREMENT); - } - return null; - } - - private static final String SEARCH_MAILBOX_SERVER_ID = "__search_mailbox__"; - private SearchParams mSearchParams; - - /** - * Returns the search mailbox for the specified account, creating one if necessary - * @return the search mailbox for the passed in account - */ - private Mailbox getSearchMailbox(long accountId) { - Context context = getContext(); - Mailbox m = Mailbox.restoreMailboxOfType(context, accountId, Mailbox.TYPE_SEARCH); - if (m == null) { - m = new Mailbox(); - m.mAccountKey = accountId; - m.mServerId = SEARCH_MAILBOX_SERVER_ID; - m.mFlagVisible = false; - m.mDisplayName = SEARCH_MAILBOX_SERVER_ID; - m.mSyncInterval = 0; - m.mType = Mailbox.TYPE_SEARCH; - m.mFlags = Mailbox.FLAG_HOLDS_MAIL; - m.mParentKey = Mailbox.NO_MAILBOX; - m.save(context); - } - return m; - } - - private void runSearchQuery(final Context context, final long accountId, - final long searchMailboxId) { - LogUtils.d(TAG, "runSearchQuery. account: %d mailbox id: %d", - accountId, searchMailboxId); - - // Start the search running in the background - new AsyncTask() { - @Override - public Void doInBackground(Void... params) { - final EmailServiceProxy service = - EmailServiceUtils.getServiceForAccount(context, accountId); - if (service != null) { - try { - final int totalCount = - service.searchMessages(accountId, mSearchParams, searchMailboxId); - - // Save away the total count - final ContentValues cv = new ContentValues(1); - cv.put(MailboxColumns.TOTAL_COUNT, totalCount); - update(ContentUris.withAppendedId(Mailbox.CONTENT_URI, searchMailboxId), cv, - null, null); - LogUtils.d(TAG, "EmailProvider#runSearchQuery. TotalCount to UI: %d", - totalCount); - } catch (RemoteException e) { - LogUtils.e("searchMessages", "RemoteException", e); - } - } - return null; - } - }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); - } - - // This handles an initial search query. More results are loaded using uiFolderLoadMore. - private Cursor uiSearch(Uri uri, String[] projection) { - LogUtils.d(TAG, "runSearchQuery in search %s", uri); - final long accountId = Long.parseLong(uri.getLastPathSegment()); - - // TODO: Check the actual mailbox - Mailbox inbox = Mailbox.restoreMailboxOfType(getContext(), accountId, Mailbox.TYPE_INBOX); - if (inbox == null) { - LogUtils.w(Logging.LOG_TAG, "In uiSearch, inbox doesn't exist for account " - + accountId); - - return null; - } - - String filter = uri.getQueryParameter(UIProvider.SearchQueryParameters.QUERY); - if (filter == null) { - throw new IllegalArgumentException("No query parameter in search query"); - } - - // Find/create our search mailbox - Mailbox searchMailbox = getSearchMailbox(accountId); - final long searchMailboxId = searchMailbox.mId; - - mSearchParams = new SearchParams(inbox.mId, filter, searchMailboxId); - - final Context context = getContext(); - if (mSearchParams.mOffset == 0) { - // TODO: This conditional is unnecessary, just two lines earlier we created - // mSearchParams using a constructor that never sets mOffset. - LogUtils.d(TAG, "deleting existing search results."); - - // Delete existing contents of search mailbox - ContentResolver resolver = context.getContentResolver(); - resolver.delete(Message.CONTENT_URI, MessageColumns.MAILBOX_KEY + "=" + searchMailboxId, - null); - final ContentValues cv = new ContentValues(1); - // For now, use the actual query as the name of the mailbox - cv.put(Mailbox.DISPLAY_NAME, mSearchParams.mFilter); - resolver.update(ContentUris.withAppendedId(Mailbox.CONTENT_URI, searchMailboxId), - cv, null, null); - } - - // Start the search running in the background - runSearchQuery(context, accountId, searchMailboxId); - - // This will look just like a "normal" folder - return uiQuery(UI_FOLDER, ContentUris.withAppendedId(Mailbox.CONTENT_URI, - searchMailbox.mId), projection, false); - } - - private static final String MAILBOXES_FOR_ACCOUNT_SELECTION = MailboxColumns.ACCOUNT_KEY + "=?"; - - /** - * Delete an account and clean it up - */ - private int uiDeleteAccount(Uri uri) { - Context context = getContext(); - long accountId = Long.parseLong(uri.getLastPathSegment()); - try { - // Get the account URI. - final Account account = Account.restoreAccountWithId(context, accountId); - if (account == null) { - return 0; // Already deleted? - } - - deleteAccountData(context, accountId); - - // Now delete the account itself - uri = ContentUris.withAppendedId(Account.CONTENT_URI, accountId); - context.getContentResolver().delete(uri, null, null); - - // Clean up - AccountBackupRestore.backup(context); - SecurityPolicy.getInstance(context).reducePolicies(); - MailActivityEmail.setServicesEnabledSync(context); - // TODO: We ought to reconcile accounts here, but some callers do this in a loop, - // which would be a problem when the first account reconciliation shuts us down. - return 1; - } catch (Exception e) { - LogUtils.w(Logging.LOG_TAG, "Exception while deleting account", e); - } - return 0; - } - - private int uiDeleteAccountData(Uri uri) { - Context context = getContext(); - long accountId = Long.parseLong(uri.getLastPathSegment()); - // Get the account URI. - final Account account = Account.restoreAccountWithId(context, accountId); - if (account == null) { - return 0; // Already deleted? - } - deleteAccountData(context, accountId); - return 1; - } - - /** - * The method will no longer be needed after platform L releases. As emails are received from - * various protocols the email addresses are decoded and intended to be stored in the database - * in decoded form. The problem is that Exchange is a separate .apk and the old Exchange .apk - * still attempts to store encoded email addresses. So, we decode here at the - * Provider before writing to the database to ensure the addresses are written in decoded form. - * - * @param values the values to be written into the Message table - */ - private static void decodeEmailAddresses(ContentValues values) { - if (values.containsKey(Message.MessageColumns.TO_LIST)) { - final String to = values.getAsString(Message.MessageColumns.TO_LIST); - values.put(Message.MessageColumns.TO_LIST, Address.fromHeaderToString(to)); - } - - if (values.containsKey(Message.MessageColumns.FROM_LIST)) { - final String from = values.getAsString(Message.MessageColumns.FROM_LIST); - values.put(Message.MessageColumns.FROM_LIST, Address.fromHeaderToString(from)); - } - - if (values.containsKey(Message.MessageColumns.CC_LIST)) { - final String cc = values.getAsString(Message.MessageColumns.CC_LIST); - values.put(Message.MessageColumns.CC_LIST, Address.fromHeaderToString(cc)); - } - - if (values.containsKey(Message.MessageColumns.BCC_LIST)) { - final String bcc = values.getAsString(Message.MessageColumns.BCC_LIST); - values.put(Message.MessageColumns.BCC_LIST, Address.fromHeaderToString(bcc)); - } - - if (values.containsKey(Message.MessageColumns.REPLY_TO_LIST)) { - final String replyTo = values.getAsString(Message.MessageColumns.REPLY_TO_LIST); - values.put(Message.MessageColumns.REPLY_TO_LIST, - Address.fromHeaderToString(replyTo)); - } - } - - /** Projection used for getting email address for an account. */ - private static final String[] ACCOUNT_EMAIL_PROJECTION = { AccountColumns.EMAIL_ADDRESS }; - - private static void deleteAccountData(Context context, long accountId) { - // We will delete PIM data, but by the time the asynchronous call to do that happens, - // the account may have been deleted from the DB. Therefore we have to get the email - // address now and send that, rather than the account id. - final String emailAddress = Utility.getFirstRowString(context, Account.CONTENT_URI, - ACCOUNT_EMAIL_PROJECTION, Account.ID_SELECTION, - new String[] {Long.toString(accountId)}, null, 0); - if (emailAddress == null) { - LogUtils.e(TAG, "Could not find email address for account %d", accountId); - } - - // Delete synced attachments - AttachmentUtilities.deleteAllAccountAttachmentFiles(context, accountId); - - // Delete all mailboxes. - ContentResolver resolver = context.getContentResolver(); - String[] accountIdArgs = new String[] { Long.toString(accountId) }; - resolver.delete(Mailbox.CONTENT_URI, MAILBOXES_FOR_ACCOUNT_SELECTION, accountIdArgs); - - // Delete account sync key. - final ContentValues cv = new ContentValues(); - cv.putNull(AccountColumns.SYNC_KEY); - resolver.update(Account.CONTENT_URI, cv, Account.ID_SELECTION, accountIdArgs); - - // Delete PIM data (contacts, calendar), stop syncs, etc. if applicable - if (emailAddress != null) { - final IEmailService service = - EmailServiceUtils.getServiceForAccount(context, accountId); - if (service != null) { - try { - service.deleteExternalAccountPIMData(emailAddress); - } catch (final RemoteException e) { - // Can't do anything about this - } - } - } - } - - private int[] mSavedWidgetIds = new int[0]; - private final ArrayList mWidgetNotifyMailboxes = new ArrayList(); - private AppWidgetManager mAppWidgetManager; - private ComponentName mEmailComponent; - - private void notifyWidgets(long mailboxId) { - Context context = getContext(); - // Lazily initialize these - if (mAppWidgetManager == null) { - mAppWidgetManager = AppWidgetManager.getInstance(context); - mEmailComponent = new ComponentName(context, WidgetProvider.getProviderName(context)); - } - - // See if we have to populate our array of mailboxes used in widgets - int[] widgetIds = mAppWidgetManager.getAppWidgetIds(mEmailComponent); - if (!Arrays.equals(widgetIds, mSavedWidgetIds)) { - mSavedWidgetIds = widgetIds; - String[][] widgetInfos = BaseWidgetProvider.getWidgetInfo(context, widgetIds); - // widgetInfo now has pairs of account uri/folder uri - mWidgetNotifyMailboxes.clear(); - for (String[] widgetInfo: widgetInfos) { - try { - if (widgetInfo == null || TextUtils.isEmpty(widgetInfo[1])) continue; - long id = Long.parseLong(Uri.parse(widgetInfo[1]).getLastPathSegment()); - if (!isCombinedMailbox(id)) { - // For a regular mailbox, just add it to the list - if (!mWidgetNotifyMailboxes.contains(id)) { - mWidgetNotifyMailboxes.add(id); - } - } else { - switch (getVirtualMailboxType(id)) { - // We only handle the combined inbox in widgets - case Mailbox.TYPE_INBOX: - Cursor c = query(Mailbox.CONTENT_URI, Mailbox.ID_PROJECTION, - MailboxColumns.TYPE + "=?", - new String[] {Integer.toString(Mailbox.TYPE_INBOX)}, null); - try { - while (c.moveToNext()) { - mWidgetNotifyMailboxes.add( - c.getLong(Mailbox.ID_PROJECTION_COLUMN)); - } - } finally { - c.close(); - } - break; - } - } - } catch (NumberFormatException e) { - // Move along - } - } - } - - // If our mailbox needs to be notified, do so... - if (mWidgetNotifyMailboxes.contains(mailboxId)) { - Intent intent = new Intent(Utils.ACTION_NOTIFY_DATASET_CHANGED); - intent.putExtra(Utils.EXTRA_FOLDER_URI, uiUri("uifolder", mailboxId)); - intent.setType(EMAIL_APP_MIME_TYPE); - context.sendBroadcast(intent); - } - } - - @Override - public void dump(FileDescriptor fd, PrintWriter writer, String[] args) { - Context context = getContext(); - writer.println("Installed services:"); - for (EmailServiceInfo info: EmailServiceUtils.getServiceInfoList(context)) { - writer.println(" " + info); - } - writer.println(); - writer.println("Accounts: "); - Cursor cursor = query(Account.CONTENT_URI, Account.CONTENT_PROJECTION, null, null, null); - if (cursor.getCount() == 0) { - writer.println(" None"); - } - try { - while (cursor.moveToNext()) { - Account account = new Account(); - account.restore(cursor); - writer.println(" Account " + account.mDisplayName); - HostAuth hostAuth = - HostAuth.restoreHostAuthWithId(context, account.mHostAuthKeyRecv); - if (hostAuth != null) { - writer.println(" Protocol = " + hostAuth.mProtocol + - (TextUtils.isEmpty(account.mProtocolVersion) ? "" : " version " + - account.mProtocolVersion)); - } - } - } finally { - cursor.close(); - } - } - - synchronized public Handler getDelayedSyncHandler() { - if (mDelayedSyncHandler == null) { - mDelayedSyncHandler = new Handler(getContext().getMainLooper(), new Callback() { - @Override - public boolean handleMessage(android.os.Message msg) { - synchronized (mDelayedSyncRequests) { - final SyncRequestMessage request = (SyncRequestMessage) msg.obj; - // TODO: It's possible that the account is deleted by the time we get here - // It would be nice if we could validate it before trying to sync - final android.accounts.Account account = request.mAccount; - final Bundle extras = Mailbox.createSyncBundle(request.mMailboxId); - ContentResolver.requestSync(account, request.mAuthority, extras); - LogUtils.i(TAG, "requestSync getDelayedSyncHandler %s, %s", - account.toString(), extras.toString()); - mDelayedSyncRequests.remove(request); - return true; - } - } - }); - } - return mDelayedSyncHandler; - } - - private class SyncRequestMessage { - private final String mAuthority; - private final android.accounts.Account mAccount; - private final long mMailboxId; - - private SyncRequestMessage(final String authority, final android.accounts.Account account, - final long mailboxId) { - mAuthority = authority; - mAccount = account; - mMailboxId = mailboxId; - } - - @Override - public boolean equals(Object o) { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - - SyncRequestMessage that = (SyncRequestMessage) o; - - return mAccount.equals(that.mAccount) - && mMailboxId == that.mMailboxId - && mAuthority.equals(that.mAuthority); - } - - @Override - public int hashCode() { - int result = mAuthority.hashCode(); - result = 31 * result + mAccount.hashCode(); - result = 31 * result + (int) (mMailboxId ^ (mMailboxId >>> 32)); - return result; - } - } - - @Override - public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) { - if (PreferenceKeys.REMOVAL_ACTION.equals(key) || - PreferenceKeys.CONVERSATION_LIST_SWIPE.equals(key) || - PreferenceKeys.SHOW_SENDER_IMAGES.equals(key) || - PreferenceKeys.DEFAULT_REPLY_ALL.equals(key) || - PreferenceKeys.CONVERSATION_OVERVIEW_MODE.equals(key) || - PreferenceKeys.AUTO_ADVANCE_MODE.equals(key) || - PreferenceKeys.SNAP_HEADER_MODE.equals(key) || - PreferenceKeys.CONFIRM_DELETE.equals(key) || - PreferenceKeys.CONFIRM_ARCHIVE.equals(key) || - PreferenceKeys.CONFIRM_SEND.equals(key)) { - notifyUI(UIPROVIDER_ALL_ACCOUNTS_NOTIFIER, null); - } - } -} diff --git a/src/com/android/email/provider/FolderPickerActivity.java b/src/com/android/email/provider/FolderPickerActivity.java deleted file mode 100644 index 8e0598b24..000000000 --- a/src/com/android/email/provider/FolderPickerActivity.java +++ /dev/null @@ -1,218 +0,0 @@ -/* - * Copyright (C) 2012 Google Inc. - * Licensed to The Android Open Source Project. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.email.provider; - -import android.app.Activity; -import android.app.ProgressDialog; -import android.content.ContentUris; -import android.content.ContentValues; -import android.content.Context; -import android.content.Intent; -import android.database.ContentObserver; -import android.net.Uri; -import android.os.Bundle; -import android.os.Handler; - -import com.android.email.R; -import com.android.emailcommon.provider.Account; -import com.android.emailcommon.provider.EmailContent; -import com.android.emailcommon.provider.Mailbox; -import com.android.emailcommon.provider.EmailContent.AccountColumns; -import com.android.emailcommon.provider.EmailContent.MailboxColumns; -import com.android.mail.providers.Folder; -import com.android.mail.utils.LogUtils; - -public class FolderPickerActivity extends Activity implements FolderPickerCallback { - private static final String TAG = "FolderPickerActivity"; - public static final String MAILBOX_TYPE_EXTRA = "mailbox_type"; - - private long mAccountId; - private int mMailboxType; - private AccountObserver mAccountObserver; - private String mAccountName; - private boolean mInSetup = true; - - @Override - public void onCreate(Bundle bundle) { - super.onCreate(bundle); - Intent i = getIntent(); - Uri uri = i.getData(); - int headerId; - final com.android.mail.providers.Account uiAccount; - // If we've gotten a Uri, then this is a call from the UI in response to setupIntentUri - // in an account (meaning the account requires setup) - if (uri != null) { - String id = uri.getQueryParameter("account"); - if (id == null) { - LogUtils.w(TAG, "No account # in Uri?"); - finish(); - return; - } - try { - mAccountId = Long.parseLong(id); - } catch (NumberFormatException e) { - LogUtils.w(TAG, "Invalid account # in Uri?"); - finish(); - return; - } - // We act a bit differently if we're coming to set up the trash after account creation - mInSetup = !i.hasExtra(MAILBOX_TYPE_EXTRA); - mMailboxType = i.getIntExtra(MAILBOX_TYPE_EXTRA, Mailbox.TYPE_TRASH); - long trashMailboxId = Mailbox.findMailboxOfType(this, mAccountId, Mailbox.TYPE_TRASH); - // If we already have a trash mailbox, we're done (if in setup; a race?) - if (trashMailboxId != Mailbox.NO_MAILBOX && mInSetup) { - LogUtils.w(TAG, "Trash folder already exists"); - finish(); - return; - } - Account account = Account.restoreAccountWithId(this, mAccountId); - if (account == null) { - LogUtils.w(TAG, "No account?"); - finish(); - } else { - mAccountName = account.mDisplayName; - // Two possibilities here; either we have our folder list, or we don't - if ((account.mFlags & Account.FLAGS_INITIAL_FOLDER_LIST_LOADED) != 0) { - // If we've got them, just start up the picker dialog - startPickerForAccount(); - } else { - // Otherwise, wait for the folders to show up - waitForFolders(); - } - } - } else { - // In this case, we're coming from Settings - uiAccount = i.getParcelableExtra(EmailProvider.PICKER_UI_ACCOUNT); - mAccountName = uiAccount.getDisplayName(); - mAccountId = Long.parseLong(uiAccount.uri.getLastPathSegment()); - mMailboxType = i.getIntExtra(EmailProvider.PICKER_MAILBOX_TYPE, -1); - headerId = i.getIntExtra(EmailProvider.PICKER_HEADER_ID, 0); - if (headerId == 0) { - finish(); - return; - } - startPicker(uiAccount.folderListUri, headerId); - } - } - - @Override - public void onDestroy() { - super.onDestroy(); - // Clean up - if (mAccountObserver != null) { - getContentResolver().unregisterContentObserver(mAccountObserver); - mAccountObserver = null; - } - if (mWaitingForFoldersDialog != null) { - mWaitingForFoldersDialog.dismiss(); - mWaitingForFoldersDialog = null; - } - } - - private class AccountObserver extends ContentObserver { - private final Context mContext; - - public AccountObserver(Context context, Handler handler) { - super(handler); - mContext = context; - } - - @Override - public void onChange(boolean selfChange) { - Account account = Account.restoreAccountWithId(mContext, mAccountId); - // All we care about is whether the folder list is now loaded - if ((account.mFlags & Account.FLAGS_INITIAL_FOLDER_LIST_LOADED) != 0 && - (mAccountObserver != null)) { - mContext.getContentResolver().unregisterContentObserver(mAccountObserver); - mAccountObserver = null; - // Bring down the ProgressDialog and show the picker - if (mWaitingForFoldersDialog != null) { - mWaitingForFoldersDialog.dismiss(); - mWaitingForFoldersDialog = null; - } - startPickerForAccount(); - } - } - } - - private ProgressDialog mWaitingForFoldersDialog; - - private void waitForFolders() { - /// Show "Waiting for folders..." dialog - mWaitingForFoldersDialog = new ProgressDialog(this); - mWaitingForFoldersDialog.setIndeterminate(true); - mWaitingForFoldersDialog.setMessage(getString(R.string.account_waiting_for_folders_msg)); - mWaitingForFoldersDialog.show(); - - // Listen for account changes - mAccountObserver = new AccountObserver(this, new Handler()); - Uri uri = ContentUris.withAppendedId(Account.CONTENT_URI, mAccountId); - getContentResolver().registerContentObserver(uri, false, mAccountObserver); - } - - private void startPickerForAccount() { - int headerId = R.string.trash_folder_selection_title; - Uri uri = Uri.parse("content://" + EmailContent.AUTHORITY + "/uifullfolders/" + mAccountId); - startPicker(uri, headerId); - } - - private void startPicker(Uri uri, int headerId) { - String header = getString(headerId, mAccountName); - FolderPickerDialog dialog = - new FolderPickerDialog(this, uri, this, header, !mInSetup); - dialog.show(); - } - - @Override - public void select(Folder folder) { - String folderId = folder.folderUri.fullUri.getLastPathSegment(); - Long id = Long.parseLong(folderId); - ContentValues values = new ContentValues(); - - // If we already have a mailbox of this type, change it back to generic mail type - Mailbox ofType = Mailbox.restoreMailboxOfType(this, mAccountId, mMailboxType); - if (ofType != null) { - values.put(MailboxColumns.TYPE, Mailbox.TYPE_MAIL); - getContentResolver().update( - ContentUris.withAppendedId(Mailbox.CONTENT_URI, ofType.mId), values, - null, null); - } - - // Change this mailbox to be of the desired type - Mailbox mailbox = Mailbox.restoreMailboxWithId(this, id); - if (mailbox != null) { - values.put(MailboxColumns.TYPE, mMailboxType); - getContentResolver().update( - ContentUris.withAppendedId(Mailbox.CONTENT_URI, mailbox.mId), values, - null, null); - values.clear(); - // Touch the account so that UI won't bring up this picker again - Account account = Account.restoreAccountWithId(this, mAccountId); - values.put(AccountColumns.FLAGS, account.mFlags); - getContentResolver().update( - ContentUris.withAppendedId(Account.CONTENT_URI, account.mId), values, - null, null); - } - finish(); - } - - @Override - public void cancel() { - finish(); - } -} diff --git a/src/com/android/email/provider/FolderPickerCallback.java b/src/com/android/email/provider/FolderPickerCallback.java deleted file mode 100644 index a1e127641..000000000 --- a/src/com/android/email/provider/FolderPickerCallback.java +++ /dev/null @@ -1,25 +0,0 @@ -/* - * Copyright (C) 2012 Google Inc. - * Licensed to The Android Open Source Project. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.email.provider; - -import com.android.mail.providers.Folder; - -public interface FolderPickerCallback { - public void select(Folder folder); - public void cancel(); -} diff --git a/src/com/android/email/provider/FolderPickerDialog.java b/src/com/android/email/provider/FolderPickerDialog.java deleted file mode 100644 index 37a7ed71f..000000000 --- a/src/com/android/email/provider/FolderPickerDialog.java +++ /dev/null @@ -1,158 +0,0 @@ -/* - * Copyright (C) 2012 Google Inc. - * Licensed to The Android Open Source Project. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.email.provider; - -import android.app.AlertDialog; -import android.content.Context; -import android.content.DialogInterface; -import android.content.DialogInterface.OnCancelListener; -import android.content.DialogInterface.OnClickListener; -import android.content.DialogInterface.OnMultiChoiceClickListener; -import android.database.Cursor; -import android.net.Uri; -import android.view.View; -import android.widget.AdapterView; -import android.widget.Button; - -import com.android.mail.R; -import com.android.mail.providers.Folder; -import com.android.mail.providers.UIProvider; -import com.android.mail.ui.FolderSelectorAdapter; -import com.android.mail.ui.FolderSelectorAdapter.FolderRow; -import com.android.mail.ui.SeparatedFolderListAdapter; - -import java.util.HashMap; -import java.util.HashSet; -import java.util.Map.Entry; - -public class FolderPickerDialog implements OnClickListener, OnMultiChoiceClickListener, - OnCancelListener { - private final AlertDialog mDialog; - private final HashMap mCheckedState; - private final SeparatedFolderListAdapter mAdapter; - private final FolderPickerCallback mCallback; - - public FolderPickerDialog(final Context context, Uri uri, - FolderPickerCallback callback, String header, boolean cancelable) { - mCallback = callback; - // Mapping of a folder's uri to its checked state - mCheckedState = new HashMap(); - AlertDialog.Builder builder = new AlertDialog.Builder(context); - builder.setTitle(header); - builder.setPositiveButton(R.string.ok, this); - builder.setCancelable(cancelable); - builder.setOnCancelListener(this); - // TODO: Do this on a background thread - final Cursor foldersCursor = context.getContentResolver().query( - uri, UIProvider.FOLDERS_PROJECTION, null, null, null); - try { - mAdapter = new SeparatedFolderListAdapter(); - mAdapter.addSection(new FolderPickerSelectorAdapter(context, foldersCursor, - new HashSet(), R.layout.multi_folders_view)); - builder.setAdapter(mAdapter, this); - } finally { - foldersCursor.close(); - } - mDialog = builder.create(); - } - - public void show() { - mDialog.show(); - mDialog.getListView().setOnItemClickListener(new AdapterView.OnItemClickListener() { - @Override - public void onItemClick(AdapterView parent, View view, int position, long id) { - final Object item = mAdapter.getItem(position); - if (item instanceof FolderRow) { - update((FolderRow) item); - } - } - }); - - final Button button = mDialog.getButton(AlertDialog.BUTTON_POSITIVE); - if (mCheckedState.size() == 0) { - // No items are selected, so disable the OK button. - button.setEnabled(false); - } - } - - /** - * Call this to update the state of folders as a result of them being - * selected / de-selected. - * - * @param row The item being updated. - */ - public void update(FolderSelectorAdapter.FolderRow row) { - // Update the UI - final boolean add = !row.isSelected(); - if (!add) { - // This would remove the check on a single radio button, so just - // return. - return; - } - // Clear any other checked items. - mAdapter.getCount(); - for (int i = 0; i < mAdapter.getCount(); i++) { - Object item = mAdapter.getItem(i); - if (item instanceof FolderRow) { - ((FolderRow)item).setIsSelected(false); - } - } - mCheckedState.clear(); - row.setIsSelected(add); - mAdapter.notifyDataSetChanged(); - mCheckedState.put(row.getFolder(), add); - - // Since we know that an item is selected in the list, enable the OK button - final Button button = mDialog.getButton(AlertDialog.BUTTON_POSITIVE); - button.setEnabled(true); - } - - @Override - public void onClick(DialogInterface dialog, int which) { - switch (which) { - case DialogInterface.BUTTON_POSITIVE: - Folder folder = null; - for (Entry entry : mCheckedState.entrySet()) { - if (entry.getValue()) { - folder = entry.getKey(); - break; - } - } - mCallback.select(folder); - break; - default: - onClick(dialog, which, true); - break; - } - } - - @Override - public void onClick(DialogInterface dialog, int which, boolean isChecked) { - final FolderRow row = (FolderRow) mAdapter.getItem(which); - // Clear any other checked items. - mCheckedState.clear(); - isChecked = true; - mCheckedState.put(row.getFolder(), isChecked); - mDialog.getListView().setItemChecked(which, false); - } - - @Override - public void onCancel(DialogInterface dialog) { - mCallback.cancel(); - } -} diff --git a/src/com/android/email/provider/FolderPickerSelectorAdapter.java b/src/com/android/email/provider/FolderPickerSelectorAdapter.java deleted file mode 100644 index e832b4579..000000000 --- a/src/com/android/email/provider/FolderPickerSelectorAdapter.java +++ /dev/null @@ -1,46 +0,0 @@ -/******************************************************************************* - * Copyright (C) 2012 Google Inc. - * Licensed to The Android Open Source Project. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - *******************************************************************************/ - -package com.android.email.provider; - -import android.content.Context; -import android.database.Cursor; - -import com.android.mail.providers.Folder; -import com.android.mail.providers.UIProvider.FolderCapabilities; -import com.android.mail.ui.HierarchicalFolderSelectorAdapter; - -import java.util.Set; - -public class FolderPickerSelectorAdapter extends HierarchicalFolderSelectorAdapter { - - public FolderPickerSelectorAdapter(Context context, Cursor folders, - Set initiallySelected, int layout) { - super(context, folders, initiallySelected, layout); - } - - /** - * Return whether the supplied folder meets the requirements to be displayed - * in the folder list. - */ - @Override - protected boolean meetsRequirements(Folder folder) { - // We only want to show the non-Trash folders that can accept moved messages - return folder.supportsCapability(FolderCapabilities.CAN_ACCEPT_MOVED_MESSAGES) - || folder.isTrash(); - } -} diff --git a/src/com/android/email/provider/RefreshStatusMonitor.java b/src/com/android/email/provider/RefreshStatusMonitor.java deleted file mode 100644 index 604d5c1dc..000000000 --- a/src/com/android/email/provider/RefreshStatusMonitor.java +++ /dev/null @@ -1,159 +0,0 @@ -package com.android.email.provider; - -import com.android.mail.providers.UIProvider; -import com.android.mail.utils.LogTag; -import com.android.mail.utils.LogUtils; -import com.android.mail.utils.StorageLowState; - -import android.content.Context; -import android.net.ConnectivityManager; -import android.net.NetworkInfo; -import android.os.Handler; -import android.text.format.DateUtils; - -import java.util.HashMap; -import java.util.Map; - -/** - * This class implements a singleton that monitors a mailbox refresh activated by the user. - * The refresh requests a sync but sometimes the sync doesn't happen till much later. This class - * checks if a sync has been started for a specific mailbox. It checks for no network connectivity - * and low storage conditions which prevent a sync and notifies the the caller using a callback. - * If no sync is started after a certain timeout, it gives up and notifies the caller. - */ -public class RefreshStatusMonitor { - private static final String TAG = LogTag.getLogTag(); - - private static final int REMOVE_REFRESH_STATUS_DELAY_MS = 250; - public static final long REMOVE_REFRESH_TIMEOUT_MS = DateUtils.MINUTE_IN_MILLIS; - private static final int MAX_RETRY = - (int) (REMOVE_REFRESH_TIMEOUT_MS / REMOVE_REFRESH_STATUS_DELAY_MS); - - private static RefreshStatusMonitor sInstance = null; - private final Handler mHandler; - private boolean mIsStorageLow = false; - private final Map mMailboxSync = new HashMap(); - - private final Context mContext; - - public static RefreshStatusMonitor getInstance(Context context) { - synchronized (RefreshStatusMonitor.class) { - if (sInstance == null) { - sInstance = new RefreshStatusMonitor(context.getApplicationContext()); - } - } - return sInstance; - } - - private RefreshStatusMonitor(Context context) { - mContext = context; - mHandler = new Handler(mContext.getMainLooper()); - StorageLowState.registerHandler(new StorageLowState - .LowStorageHandler() { - @Override - public void onStorageLow() { - mIsStorageLow = true; - } - - @Override - public void onStorageOk() { - mIsStorageLow = false; - } - }); - } - - public void monitorRefreshStatus(long mailboxId, Callback callback) { - synchronized (mMailboxSync) { - if (!mMailboxSync.containsKey(mailboxId)) - mMailboxSync.put(mailboxId, false); - mHandler.postDelayed( - new RemoveRefreshStatusRunnable(mailboxId, callback), - REMOVE_REFRESH_STATUS_DELAY_MS); - } - } - - public void setSyncStarted(long mailboxId) { - synchronized (mMailboxSync) { - // only if we're tracking this mailbox - if (mMailboxSync.containsKey(mailboxId)) { - LogUtils.d(TAG, "RefreshStatusMonitor: setSyncStarted: mailboxId=%d", mailboxId); - mMailboxSync.put(mailboxId, true); - } - } - } - - private boolean isConnected() { - final ConnectivityManager connectivityManager = - ((ConnectivityManager) mContext.getSystemService(Context.CONNECTIVITY_SERVICE)); - final NetworkInfo networkInfo = connectivityManager.getActiveNetworkInfo(); - return (networkInfo != null) && networkInfo.isConnected(); - } - - private class RemoveRefreshStatusRunnable implements Runnable { - private final long mMailboxId; - private final Callback mCallback; - - private int mNumRetries = 0; - - - RemoveRefreshStatusRunnable(long mailboxId, Callback callback) { - mMailboxId = mailboxId; - mCallback = callback; - } - - @Override - public void run() { - synchronized (mMailboxSync) { - final Boolean isSyncRunning = mMailboxSync.get(mMailboxId); - if (Boolean.FALSE.equals(isSyncRunning)) { - if (mIsStorageLow) { - LogUtils.d(TAG, "RefreshStatusMonitor: mailboxId=%d LOW STORAGE", - mMailboxId); - // The device storage is low and sync will never succeed. - mCallback.onRefreshCompleted( - mMailboxId, UIProvider.LastSyncResult.STORAGE_ERROR); - mMailboxSync.remove(mMailboxId); - } else if (!isConnected()) { - LogUtils.d(TAG, "RefreshStatusMonitor: mailboxId=%d NOT CONNECTED", - mMailboxId); - // The device is not connected to the Internet. A sync will never succeed. - mCallback.onRefreshCompleted( - mMailboxId, UIProvider.LastSyncResult.CONNECTION_ERROR); - mMailboxSync.remove(mMailboxId); - } else { - // The device is connected to the Internet. It might take a short while for - // the sync manager to initiate our sync, so let's post this runnable again - // and hope that we have started syncing by then. - mNumRetries++; - LogUtils.d(TAG, "RefreshStatusMonitor: mailboxId=%d Retry %d", - mMailboxId, mNumRetries); - if (mNumRetries > MAX_RETRY) { - LogUtils.d(TAG, "RefreshStatusMonitor: mailboxId=%d TIMEOUT", - mMailboxId); - // Hide the sync status bar if it's been a while since sync was - // requested and still hasn't started. - mMailboxSync.remove(mMailboxId); - mCallback.onTimeout(mMailboxId); - // TODO: Displaying a user friendly message in addition. - } else { - mHandler.postDelayed(this, REMOVE_REFRESH_STATUS_DELAY_MS); - } - } - } else { - // Some sync is currently in progress. We're done - LogUtils.d(TAG, "RefreshStatusMonitor: mailboxId=%d SYNC DETECTED", mMailboxId); - // it's not quite a success yet, the sync just started but we need to clear the - // error so the retry bar goes away. - mCallback.onRefreshCompleted( - mMailboxId, UIProvider.LastSyncResult.SUCCESS); - mMailboxSync.remove(mMailboxId); - } - } - } - } - - public interface Callback { - void onRefreshCompleted(long mailboxId, int result); - void onTimeout(long mailboxId); - } -} diff --git a/src/com/android/email/provider/Utilities.java b/src/com/android/email/provider/Utilities.java deleted file mode 100644 index c3b7ec93a..000000000 --- a/src/com/android/email/provider/Utilities.java +++ /dev/null @@ -1,234 +0,0 @@ -/* - * 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.annotation.TargetApi; -import android.content.ContentUris; -import android.content.ContentValues; -import android.content.Context; -import android.database.Cursor; -import android.net.Uri; -import android.os.ParcelFileDescriptor; - -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.Attachment; -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 com.android.mail.utils.LogUtils; -import com.android.mail.utils.Utils; - -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 == null) { - return; - } else if (c.moveToNext()) { - localMessage = EmailContent.getContent(context, c, EmailContent.Message.class); - } else { - localMessage = new EmailContent.Message(); - } - 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 = null; - if (localMessage.mId != EmailContent.Message.NO_MESSAGE) { - 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 viewables = new ArrayList(); - ArrayList attachments = new ArrayList(); - MimeUtility.collectParts(message, viewables, attachments); - - final ConversionUtilities.BodyFieldData data = - ConversionUtilities.parseBodyFields(viewables); - - // set body and local message values - localMessage.setFlags(data.isQuotedReply, data.isQuotedForward); - localMessage.mSnippet = data.snippet; - body.mTextContent = data.textContent; - body.mHtmlContent = data.htmlContent; - - // Commit the message & body to the local store immediately - saveOrUpdate(localMessage, context); - body.mMessageKey = localMessage.mId; - saveOrUpdate(body, context); - - // process (and save) attachments - if (loadStatus != EmailContent.Message.FLAG_LOADED_PARTIAL - && loadStatus != EmailContent.Message.FLAG_LOADED_UNKNOWN) { - // TODO(pwestbro): What should happen with unknown status? - LegacyConversions.updateAttachments(context, localMessage, attachments); - LegacyConversions.updateInlineAttachments(context, localMessage, viewables); - } else { - EmailContent.Attachment att = new EmailContent.Attachment(); - // Since we haven't actually loaded the attachment, we're just putting - // a dummy placeholder here. When the user taps on it, we'll load the attachment - // for real. - // TODO: This is not a great way to model this. What we're saying is, we don't - // have the complete message, without paying any attention to what we do have. - // Did the main body exceed the maximum initial size? If so, we really might - // not have any attachments at all, and we just need a button somewhere that - // says "load the rest of the message". - // Or, what if we were able to load some, but not all of the attachments? - // Then we should ideally not be dropping the data we have on the floor. - // Also, what behavior we have here really should be based on what protocol - // we're dealing with. If it's POP, then we don't actually know how many - // attachments we have until we've loaded the complete message. - // If it's IMAP, we should know that, and we should load all attachment - // metadata we can get, regardless of whether or not we have the complete - // message body. - att.mFileName = ""; - att.mSize = message.getSize(); - att.mMimeType = "text/plain"; - att.mMessageKey = localMessage.mId; - att.mAccountKey = localMessage.mAccountKey; - att.mFlags = Attachment.FLAG_DUMMY_ATTACHMENT; - att.save(context); - localMessage.mFlagAttachment = true; - } - - // 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) { - LogUtils.e(Logging.LOG_TAG, "Error while copying downloaded message." + me); - } - - } catch (RuntimeException rte) { - LogUtils.e(Logging.LOG_TAG, "Error while storing downloaded message." + rte.toString()); - } catch (IOException ioe) { - LogUtils.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); - } - } - - /** - * Converts a string representing a file mode, such as "rw", into a bitmask suitable for use - * with {@link android.os.ParcelFileDescriptor#open}. - *

- * @param mode The string representation of the file mode. - * @return A bitmask representing the given file mode. - * @throws IllegalArgumentException if the given string does not match a known file mode. - */ - @TargetApi(19) - public static int parseMode(String mode) { - if (Utils.isRunningKitkatOrLater()) { - return ParcelFileDescriptor.parseMode(mode); - } - final int modeBits; - if ("r".equals(mode)) { - modeBits = ParcelFileDescriptor.MODE_READ_ONLY; - } else if ("w".equals(mode) || "wt".equals(mode)) { - modeBits = ParcelFileDescriptor.MODE_WRITE_ONLY - | ParcelFileDescriptor.MODE_CREATE - | ParcelFileDescriptor.MODE_TRUNCATE; - } else if ("wa".equals(mode)) { - modeBits = ParcelFileDescriptor.MODE_WRITE_ONLY - | ParcelFileDescriptor.MODE_CREATE - | ParcelFileDescriptor.MODE_APPEND; - } else if ("rw".equals(mode)) { - modeBits = ParcelFileDescriptor.MODE_READ_WRITE - | ParcelFileDescriptor.MODE_CREATE; - } else if ("rwt".equals(mode)) { - modeBits = ParcelFileDescriptor.MODE_READ_WRITE - | ParcelFileDescriptor.MODE_CREATE - | ParcelFileDescriptor.MODE_TRUNCATE; - } else { - throw new IllegalArgumentException("Bad mode '" + mode + "'"); - } - return modeBits; - } -} diff --git a/src/com/android/email/provider/WidgetProvider.java b/src/com/android/email/provider/WidgetProvider.java deleted file mode 100644 index 6909e8902..000000000 --- a/src/com/android/email/provider/WidgetProvider.java +++ /dev/null @@ -1,191 +0,0 @@ -/* - * 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.ContentResolver; -import android.content.Context; -import android.content.SharedPreferences; -import android.database.Cursor; -import android.net.Uri; - -import com.android.emailcommon.provider.Account; -import com.android.emailcommon.provider.Mailbox; -import com.android.mail.providers.Folder; -import com.android.mail.providers.UIProvider; -import com.android.mail.utils.LogTag; -import com.android.mail.utils.LogUtils; -import com.android.mail.widget.BaseWidgetProvider; -import com.android.mail.widget.WidgetService; - -public class WidgetProvider extends BaseWidgetProvider { - private static final String LEGACY_PREFS_NAME = "com.android.email.widget.WidgetManager"; - private static final String LEGACY_ACCOUNT_ID_PREFIX = "accountId_"; - private static final String LEGACY_MAILBOX_ID_PREFIX = "mailboxId_"; - - private static final String LOG_TAG = LogTag.getLogTag(); - - /** - * Remove preferences when deleting widget - */ - @Override - public void onDeleted(Context context, int[] appWidgetIds) { - super.onDeleted(context, appWidgetIds); - - // Remove any legacy Email widget information - final SharedPreferences prefs = context.getSharedPreferences(LEGACY_PREFS_NAME, 0); - final SharedPreferences.Editor editor = prefs.edit(); - for (int widgetId : appWidgetIds) { - // Remove the account in the preference - editor.remove(LEGACY_ACCOUNT_ID_PREFIX + widgetId); - editor.remove(LEGACY_MAILBOX_ID_PREFIX + widgetId); - } - editor.apply(); - } - - @Override - protected com.android.mail.providers.Account getAccountObject( - Context context, String accountUri) { - final ContentResolver resolver = context.getContentResolver(); - final Cursor accountCursor = resolver.query(Uri.parse(accountUri), - UIProvider.ACCOUNTS_PROJECTION_NO_CAPABILITIES, null, null, null); - - return getPopulatedAccountObject(accountCursor); - } - - - @Override - protected boolean isAccountValid(Context context, com.android.mail.providers.Account account) { - if (account != null) { - final ContentResolver resolver = context.getContentResolver(); - final Cursor accountCursor = resolver.query(account.uri, - UIProvider.ACCOUNTS_PROJECTION_NO_CAPABILITIES, null, null, null); - if (accountCursor != null) { - try { - return accountCursor.getCount() > 0; - } finally { - accountCursor.close(); - } - } - } - return false; - } - - @Override - protected void migrateLegacyWidgetInformation(Context context, int widgetId) { - final SharedPreferences prefs = context.getSharedPreferences(LEGACY_PREFS_NAME, 0); - final SharedPreferences.Editor editor = prefs.edit(); - - long accountId = loadAccountIdPref(context, widgetId); - long mailboxId = loadMailboxIdPref(context, widgetId); - // Legacy support; if preferences haven't been saved for this widget, load something - if (accountId == Account.NO_ACCOUNT || mailboxId == Mailbox.NO_MAILBOX) { - LogUtils.d(LOG_TAG, "Couldn't load account or mailbox. accountId: %d" + - " mailboxId: %d widgetId %d", accountId, mailboxId, widgetId); - return; - } - - accountId = migrateLegacyWidgetAccountId(accountId); - mailboxId = migrateLegacyWidgetMailboxId(mailboxId, accountId); - - // Get Account and folder objects for the account id and mailbox id - final com.android.mail.providers.Account uiAccount = getAccount(context, accountId); - final Folder uiFolder = EmailProvider.getFolder(context, mailboxId); - - if (uiAccount != null && uiFolder != null) { - WidgetService.saveWidgetInformation(context, widgetId, uiAccount, - uiFolder.folderUri.fullUri.toString()); - - updateWidgetInternal(context, widgetId, uiAccount, uiFolder.type, uiFolder.capabilities, - uiFolder.folderUri.fullUri, uiFolder.conversationListUri, uiFolder.name); - - // Now remove the old legacy preference value - editor.remove(LEGACY_ACCOUNT_ID_PREFIX + widgetId); - editor.remove(LEGACY_MAILBOX_ID_PREFIX + widgetId); - } - editor.apply(); - } - - private static long migrateLegacyWidgetAccountId(long accountId) { - if (accountId == Account.ACCOUNT_ID_COMBINED_VIEW) { - return EmailProvider.COMBINED_ACCOUNT_ID; - } - return accountId; - } - - /** - * @param accountId The migrated accountId - * @return - */ - private static long migrateLegacyWidgetMailboxId(long mailboxId, long accountId) { - if (mailboxId == Mailbox.QUERY_ALL_INBOXES) { - return EmailProvider.getVirtualMailboxId(accountId, Mailbox.TYPE_INBOX); - } else if (mailboxId == Mailbox.QUERY_ALL_UNREAD) { - return EmailProvider.getVirtualMailboxId(accountId, Mailbox.TYPE_UNREAD); - } - return mailboxId; - } - - private static com.android.mail.providers.Account getAccount(Context context, long accountId) { - final ContentResolver resolver = context.getContentResolver(); - final Cursor ac = resolver.query(EmailProvider.uiUri("uiaccount", accountId), - UIProvider.ACCOUNTS_PROJECTION_NO_CAPABILITIES, null, null, null); - - com.android.mail.providers.Account uiAccount = getPopulatedAccountObject(ac); - - return uiAccount; - } - - private static com.android.mail.providers.Account getPopulatedAccountObject( - final Cursor accountCursor) { - if (accountCursor == null) { - LogUtils.e(LOG_TAG, "Null account cursor"); - return null; - } - - com.android.mail.providers.Account uiAccount = null; - try { - if (accountCursor.moveToFirst()) { - uiAccount = com.android.mail.providers.Account.builder().buildFrom(accountCursor); - } - } finally { - accountCursor.close(); - } - return uiAccount; - } - - /** - * Returns the saved account ID for the given widget. Otherwise, - * {@link com.android.emailcommon.provider.Account#NO_ACCOUNT} if - * the account ID was not previously saved. - */ - static long loadAccountIdPref(Context context, int appWidgetId) { - final SharedPreferences prefs = context.getSharedPreferences(LEGACY_PREFS_NAME, 0); - long accountId = prefs.getLong(LEGACY_ACCOUNT_ID_PREFIX + appWidgetId, Account.NO_ACCOUNT); - return accountId; - } - - /** - * Returns the saved mailbox ID for the given widget. Otherwise, - * {@link com.android.emailcommon.provider.Mailbox#NO_MAILBOX} if - * the mailbox ID was not previously saved. - */ - static long loadMailboxIdPref(Context context, int appWidgetId) { - final SharedPreferences prefs = context.getSharedPreferences(LEGACY_PREFS_NAME, 0); - long mailboxId = prefs.getLong(LEGACY_MAILBOX_ID_PREFIX + appWidgetId, Mailbox.NO_MAILBOX); - return mailboxId; - } -} diff --git a/src/com/android/email/service/AccountService.java b/src/com/android/email/service/AccountService.java deleted file mode 100644 index c8d94a19a..000000000 --- a/src/com/android/email/service/AccountService.java +++ /dev/null @@ -1,85 +0,0 @@ -/* - * 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.service; - -import android.app.Service; -import android.content.Context; -import android.content.Intent; -import android.os.Bundle; -import android.os.IBinder; - -import com.android.email.DebugUtils; -import com.android.email.ResourceHelper; -import com.android.emailcommon.Configuration; -import com.android.emailcommon.Device; -import com.android.emailcommon.VendorPolicyLoader; -import com.android.emailcommon.service.IAccountService; -import com.android.emailcommon.utility.EmailAsyncTask; - -import java.io.IOException; - -public class AccountService extends Service { - - // Save context - private Context mContext; - - private final IAccountService.Stub mBinder = new IAccountService.Stub() { - - @Override - public int getAccountColor(long accountId) { - return ResourceHelper.getInstance(mContext).getAccountColor(accountId); - } - - @Override - public Bundle getConfigurationData(String accountType) { - Bundle bundle = new Bundle(); - bundle.putBoolean(Configuration.EXCHANGE_CONFIGURATION_USE_ALTERNATE_STRINGS, - VendorPolicyLoader.getInstance(mContext).useAlternateExchangeStrings()); - return bundle; - } - - @Override - public String getDeviceId() { - try { - EmailAsyncTask.runAsyncSerial(new Runnable() { - @Override - public void run() { - // Make sure remote services are running (re: lifecycle) - EmailServiceUtils.startRemoteServices(mContext); - // Send current logging flags - DebugUtils.updateLoggingFlags(mContext); - }}); - return Device.getDeviceId(mContext); - } catch (IOException e) { - return null; - } - } - }; - - @Override - public IBinder onBind(Intent intent) { - if (mContext == null) { - mContext = this; - } - // Make sure we have a valid deviceId (just retrieves a static String except first time) - try { - Device.getDeviceId(this); - } catch (IOException e) { - } - return mBinder; - } -} diff --git a/src/com/android/email/service/AttachmentService.java b/src/com/android/email/service/AttachmentService.java deleted file mode 100644 index f8871688a..000000000 --- a/src/com/android/email/service/AttachmentService.java +++ /dev/null @@ -1,1401 +0,0 @@ -/* - * Copyright (C) 2014 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.accounts.AccountManager; -import android.app.AlarmManager; -import android.app.PendingIntent; -import android.app.Service; -import android.content.BroadcastReceiver; -import android.content.ContentValues; -import android.content.Context; -import android.content.Intent; -import android.database.Cursor; -import android.net.ConnectivityManager; -import android.net.Uri; -import android.os.IBinder; -import android.os.RemoteException; -import android.os.SystemClock; -import android.text.format.DateUtils; - -import com.android.email.AttachmentInfo; -import com.android.email.EmailConnectivityManager; -import com.android.email.NotificationController; -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.Message; -import com.android.emailcommon.service.EmailServiceProxy; -import com.android.emailcommon.service.EmailServiceStatus; -import com.android.emailcommon.service.IEmailServiceCallback; -import com.android.emailcommon.utility.AttachmentUtilities; -import com.android.emailcommon.utility.Utility; -import com.android.mail.providers.UIProvider.AttachmentState; -import com.android.mail.utils.LogUtils; -import com.google.common.annotations.VisibleForTesting; - -import java.io.File; -import java.io.FileDescriptor; -import java.io.PrintWriter; -import java.util.Collection; -import java.util.Comparator; -import java.util.HashMap; -import java.util.PriorityQueue; -import java.util.Queue; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.ConcurrentLinkedQueue; - -public class AttachmentService extends Service implements Runnable { - // For logging. - public static final String LOG_TAG = "AttachmentService"; - - // STOPSHIP Set this to 0 before shipping. - private static final int ENABLE_ATTACHMENT_SERVICE_DEBUG = 0; - - // Minimum wait time before retrying a download that failed due to connection error - private static final long CONNECTION_ERROR_RETRY_MILLIS = 10 * DateUtils.SECOND_IN_MILLIS; - // Number of retries before we start delaying between - private static final long CONNECTION_ERROR_DELAY_THRESHOLD = 5; - // Maximum time to retry for connection errors. - private static final long CONNECTION_ERROR_MAX_RETRIES = 10; - - // Our idle time, waiting for notifications; this is something of a failsafe - private static final int PROCESS_QUEUE_WAIT_TIME = 30 * ((int)DateUtils.MINUTE_IN_MILLIS); - // How long we'll wait for a callback before canceling a download and retrying - private static final int CALLBACK_TIMEOUT = 30 * ((int)DateUtils.SECOND_IN_MILLIS); - // Try to download an attachment in the background this many times before giving up - private static final int MAX_DOWNLOAD_RETRIES = 5; - - static final int PRIORITY_NONE = -1; - // High priority is for user requests - static final int PRIORITY_FOREGROUND = 0; - static final int PRIORITY_HIGHEST = PRIORITY_FOREGROUND; - // Normal priority is for forwarded downloads in outgoing mail - static final int PRIORITY_SEND_MAIL = 1; - // Low priority will be used for opportunistic downloads - static final int PRIORITY_BACKGROUND = 2; - static final int PRIORITY_LOWEST = PRIORITY_BACKGROUND; - - // Minimum free storage in order to perform prefetch (25% of total memory) - private static final float PREFETCH_MINIMUM_STORAGE_AVAILABLE = 0.25F; - // Maximum prefetch storage (also 25% of total memory) - private static final float PREFETCH_MAXIMUM_ATTACHMENT_STORAGE = 0.25F; - - // We can try various values here; I think 2 is completely reasonable as a first pass - private static final int MAX_SIMULTANEOUS_DOWNLOADS = 2; - // Limit on the number of simultaneous downloads per account - // Note that a limit of 1 is currently enforced by both Services (MailService and Controller) - private static final int MAX_SIMULTANEOUS_DOWNLOADS_PER_ACCOUNT = 1; - // Limit on the number of attachments we'll check for background download - private static final int MAX_ATTACHMENTS_TO_CHECK = 25; - - private static final String EXTRA_ATTACHMENT_ID = - "com.android.email.AttachmentService.attachment_id"; - private static final String EXTRA_ATTACHMENT_FLAGS = - "com.android.email.AttachmentService.attachment_flags"; - - // This callback is invoked by the various service implementations to give us download progress - // since those modules are responsible for the actual download. - final ServiceCallback mServiceCallback = new ServiceCallback(); - - // sRunningService is only set in the UI thread; it's visibility elsewhere is guaranteed - // by the use of "volatile" - static volatile AttachmentService sRunningService = null; - - // Signify that we are being shut down & destroyed. - private volatile boolean mStop = false; - - EmailConnectivityManager mConnectivityManager; - - // Helper class that keeps track of in progress downloads to make sure that they - // are progressing well. - final AttachmentWatchdog mWatchdog = new AttachmentWatchdog(); - - private final Object mLock = new Object(); - - // A map of attachment storage used per account as we have account based maximums to follow. - // NOTE: This map is not kept current in terms of deletions (i.e. it stores the last calculated - // amount plus the size of any new attachments loaded). If and when we reach the per-account - // limit, we recalculate the actual usage - final ConcurrentHashMap mAttachmentStorageMap = new ConcurrentHashMap(); - - // A map of attachment ids to the number of failed attempts to download the attachment - // NOTE: We do not want to persist this. This allows us to retry background downloading - // if any transient network errors are fixed & and the app is restarted - final ConcurrentHashMap mAttachmentFailureMap = new ConcurrentHashMap(); - - // Keeps tracks of downloads in progress based on an attachment ID to DownloadRequest mapping. - final ConcurrentHashMap mDownloadsInProgress = - new ConcurrentHashMap(); - - final DownloadQueue mDownloadQueue = new DownloadQueue(); - - // The queue entries here are entries of the form {id, flags}, with the values passed in to - // attachmentChanged(). Entries in the queue are picked off in processQueue(). - private static final Queue sAttachmentChangedQueue = - new ConcurrentLinkedQueue(); - - // Extra layer of control over debug logging that should only be enabled when - // we need to take an extra deep dive at debugging the workflow in this class. - static private void debugTrace(final String format, final Object... args) { - if (ENABLE_ATTACHMENT_SERVICE_DEBUG > 0) { - LogUtils.d(LOG_TAG, String.format(format, args)); - } - } - - /** - * This class is used to contain the details and state of a particular request to download - * an attachment. These objects are constructed and either placed in the {@link DownloadQueue} - * or in the in-progress map used to keep track of downloads that are currently happening - * in the system - */ - static class DownloadRequest { - // Details of the request. - final int mPriority; - final long mCreatedTime; - final long mAttachmentId; - final long mMessageId; - final long mAccountId; - - // Status of the request. - boolean mInProgress = false; - int mLastStatusCode; - int mLastProgress; - long mLastCallbackTime; - long mStartTime; - long mRetryCount; - long mRetryStartTime; - - /** - * This constructor is mainly used for tests - * @param attPriority The priority of this attachment - * @param attId The id of the row in the attachment table. - */ - @VisibleForTesting - DownloadRequest(final int attPriority, final long attId) { - // This constructor should only be used for unit tests. - mCreatedTime = SystemClock.elapsedRealtime(); - mPriority = attPriority; - mAttachmentId = attId; - mAccountId = -1; - mMessageId = -1; - } - - private DownloadRequest(final Context context, final Attachment attachment) { - mAttachmentId = attachment.mId; - final Message msg = Message.restoreMessageWithId(context, attachment.mMessageKey); - if (msg != null) { - mAccountId = msg.mAccountKey; - mMessageId = msg.mId; - } else { - mAccountId = mMessageId = -1; - } - mPriority = getAttachmentPriority(attachment); - mCreatedTime = SystemClock.elapsedRealtime(); - } - - private DownloadRequest(final DownloadRequest orig, final long newTime) { - mPriority = orig.mPriority; - mAttachmentId = orig.mAttachmentId; - mMessageId = orig.mMessageId; - mAccountId = orig.mAccountId; - mCreatedTime = newTime; - mInProgress = orig.mInProgress; - mLastStatusCode = orig.mLastStatusCode; - mLastProgress = orig.mLastProgress; - mLastCallbackTime = orig.mLastCallbackTime; - mStartTime = orig.mStartTime; - mRetryCount = orig.mRetryCount; - mRetryStartTime = orig.mRetryStartTime; - } - - @Override - public int hashCode() { - return (int)mAttachmentId; - } - - /** - * Two download requests are equals if their attachment id's are equals - */ - @Override - public boolean equals(final Object object) { - if (!(object instanceof DownloadRequest)) return false; - final DownloadRequest req = (DownloadRequest)object; - return req.mAttachmentId == mAttachmentId; - } - } - - /** - * This class is used to organize the various download requests that are pending. - * We need a class that allows us to prioritize a collection of {@link DownloadRequest} objects - * while being able to pull off request with the highest priority but we also need - * to be able to find a particular {@link DownloadRequest} by id or by reference for retrieval. - * Bonus points for an implementation that does not require an iterator to accomplish its tasks - * as we can avoid pesky ConcurrentModificationException when one thread has the iterator - * and another thread modifies the collection. - */ - static class DownloadQueue { - private final int DEFAULT_SIZE = 10; - - // For synchronization - private final Object mLock = new Object(); - - /** - * Comparator class for the download set; we first compare by priority. Requests with equal - * priority are compared by the time the request was created (older requests come first) - */ - private static class DownloadComparator implements Comparator { - @Override - public int compare(DownloadRequest req1, DownloadRequest req2) { - int res; - if (req1.mPriority != req2.mPriority) { - res = (req1.mPriority < req2.mPriority) ? -1 : 1; - } else { - if (req1.mCreatedTime == req2.mCreatedTime) { - res = 0; - } else { - res = (req1.mCreatedTime < req2.mCreatedTime) ? -1 : 1; - } - } - return res; - } - } - - // For prioritization of DownloadRequests. - final PriorityQueue mRequestQueue = - new PriorityQueue(DEFAULT_SIZE, new DownloadComparator()); - - // Secondary collection to quickly find objects w/o the help of an iterator. - // This class should be kept in lock step with the priority queue. - final ConcurrentHashMap mRequestMap = - new ConcurrentHashMap(); - - /** - * This function will add the request to our collections if it does not already - * exist. If it does exist, the function will silently succeed. - * @param request The {@link DownloadRequest} that should be added to our queue - * @return true if it was added (or already exists), false otherwise - */ - public boolean addRequest(final DownloadRequest request) - throws NullPointerException { - // It is key to keep the map and queue in lock step - if (request == null) { - // We can't add a null entry into the queue so let's throw what the underlying - // data structure would throw. - throw new NullPointerException(); - } - final long requestId = request.mAttachmentId; - if (requestId < 0) { - // Invalid request - LogUtils.d(LOG_TAG, "Not adding a DownloadRequest with an invalid attachment id"); - return false; - } - debugTrace("Queuing DownloadRequest #%d", requestId); - synchronized (mLock) { - // Check to see if this request is is already in the queue - final boolean exists = mRequestMap.containsKey(requestId); - if (!exists) { - mRequestQueue.offer(request); - mRequestMap.put(requestId, request); - } else { - debugTrace("DownloadRequest #%d was already in the queue"); - } - } - return true; - } - - /** - * This function will remove the specified request from the internal collections. - * @param request The {@link DownloadRequest} that should be removed from our queue - * @return true if it was removed or the request was invalid (meaning that the request - * is not in our queue), false otherwise. - */ - public boolean removeRequest(final DownloadRequest request) { - if (request == null) { - // If it is invalid, its not in the queue. - return true; - } - debugTrace("Removing DownloadRequest #%d", request.mAttachmentId); - final boolean result; - synchronized (mLock) { - // It is key to keep the map and queue in lock step - result = mRequestQueue.remove(request); - if (result) { - mRequestMap.remove(request.mAttachmentId); - } - return result; - } - } - - /** - * Return the next request from our queue. - * @return The next {@link DownloadRequest} object or null if the queue is empty - */ - public DownloadRequest getNextRequest() { - // It is key to keep the map and queue in lock step - final DownloadRequest returnRequest; - synchronized (mLock) { - returnRequest = mRequestQueue.poll(); - if (returnRequest != null) { - final long requestId = returnRequest.mAttachmentId; - mRequestMap.remove(requestId); - } - } - if (returnRequest != null) { - debugTrace("Retrieved DownloadRequest #%d", returnRequest.mAttachmentId); - } - return returnRequest; - } - - /** - * Return the {@link DownloadRequest} with the given ID (attachment ID) - * @param requestId The ID of the request in question - * @return The associated {@link DownloadRequest} object or null if it does not exist - */ - public DownloadRequest findRequestById(final long requestId) { - if (requestId < 0) { - return null; - } - synchronized (mLock) { - return mRequestMap.get(requestId); - } - } - - public int getSize() { - synchronized (mLock) { - return mRequestMap.size(); - } - } - - public boolean isEmpty() { - synchronized (mLock) { - return mRequestMap.isEmpty(); - } - } - } - - /** - * Watchdog alarm receiver; responsible for making sure that downloads in progress are not - * stalled, as determined by the timing of the most recent service callback - */ - public static class AttachmentWatchdog extends BroadcastReceiver { - // How often our watchdog checks for callback timeouts - private static final int WATCHDOG_CHECK_INTERVAL = 20 * ((int)DateUtils.SECOND_IN_MILLIS); - public static final String EXTRA_CALLBACK_TIMEOUT = "callback_timeout"; - private PendingIntent mWatchdogPendingIntent; - - public void setWatchdogAlarm(final Context context, final long delay, - final int callbackTimeout) { - // Lazily initialize the pending intent - if (mWatchdogPendingIntent == null) { - Intent intent = new Intent(context, AttachmentWatchdog.class); - intent.putExtra(EXTRA_CALLBACK_TIMEOUT, callbackTimeout); - mWatchdogPendingIntent = - PendingIntent.getBroadcast(context, 0, intent, 0); - } - // Set the alarm - final AlarmManager am = (AlarmManager)context.getSystemService(Context.ALARM_SERVICE); - am.set(AlarmManager.RTC_WAKEUP, System.currentTimeMillis() + delay, - mWatchdogPendingIntent); - debugTrace("Set up a watchdog for %d millis in the future", delay); - } - - public void setWatchdogAlarm(final Context context) { - // Call the real function with default values. - setWatchdogAlarm(context, WATCHDOG_CHECK_INTERVAL, CALLBACK_TIMEOUT); - } - - @Override - public void onReceive(final Context context, final Intent intent) { - final int callbackTimeout = intent.getIntExtra(EXTRA_CALLBACK_TIMEOUT, - CALLBACK_TIMEOUT); - new Thread(new Runnable() { - @Override - public void run() { - // TODO: Really don't like hard coding the AttachmentService reference here - // as it makes testing harder if we are trying to mock out the service - // We should change this with some sort of getter that returns the - // static (or test) AttachmentService instance to use. - final AttachmentService service = AttachmentService.sRunningService; - if (service != null) { - // If our service instance is gone, just leave - if (service.mStop) { - return; - } - // Get the timeout time from the intent. - watchdogAlarm(service, callbackTimeout); - } - } - }, "AttachmentService AttachmentWatchdog").start(); - } - - boolean validateDownloadRequest(final DownloadRequest dr, final int callbackTimeout, - final long now) { - // Check how long it's been since receiving a callback - final long timeSinceCallback = now - dr.mLastCallbackTime; - if (timeSinceCallback > callbackTimeout) { - LogUtils.d(LOG_TAG, "Timeout for DownloadRequest #%d ", dr.mAttachmentId); - return true; - } - return false; - } - - /** - * Watchdog for downloads; we use this in case we are hanging on a download, which might - * have failed silently (the connection dropped, for example) - */ - void watchdogAlarm(final AttachmentService service, final int callbackTimeout) { - debugTrace("Received a timer callback in the watchdog"); - - // We want to iterate on each of the downloads that are currently in progress and - // cancel the ones that seem to be taking too long. - final Collection inProgressRequests = - service.mDownloadsInProgress.values(); - for (DownloadRequest req: inProgressRequests) { - debugTrace("Checking in-progress request with id: %d", req.mAttachmentId); - final boolean shouldCancelDownload = validateDownloadRequest(req, callbackTimeout, - System.currentTimeMillis()); - if (shouldCancelDownload) { - LogUtils.w(LOG_TAG, "Cancelling DownloadRequest #%d", req.mAttachmentId); - service.cancelDownload(req); - // TODO: Should we also mark the attachment as failed at this point in time? - } - } - // Check whether we can start new downloads... - if (service.isConnected()) { - service.processQueue(); - } - issueNextWatchdogAlarm(service); - } - - void issueNextWatchdogAlarm(final AttachmentService service) { - if (!service.mDownloadsInProgress.isEmpty()) { - debugTrace("Rescheduling watchdog..."); - setWatchdogAlarm(service); - } - } - } - - /** - * We use an EmailServiceCallback to keep track of the progress of downloads. These callbacks - * come from either Controller (IMAP/POP) or ExchangeService (EAS). Note that we only - * implement the single callback that's defined by the EmailServiceCallback interface. - */ - class ServiceCallback extends IEmailServiceCallback.Stub { - - /** - * Simple routine to generate updated status values for the Attachment based on the - * service callback. Right now it is very simple but factoring out this code allows us - * to test easier and very easy to expand in the future. - */ - ContentValues getAttachmentUpdateValues(final Attachment attachment, - final int statusCode, final int progress) { - final ContentValues values = new ContentValues(); - if (attachment != null) { - if (statusCode == EmailServiceStatus.IN_PROGRESS) { - // TODO: What else do we want to expose about this in-progress download through - // the provider? If there is more, make sure that the service implementation - // reports it and make sure that we add it here. - values.put(AttachmentColumns.UI_STATE, AttachmentState.DOWNLOADING); - values.put(AttachmentColumns.UI_DOWNLOADED_SIZE, - attachment.mSize * progress / 100); - } - } - return values; - } - - @Override - public void loadAttachmentStatus(final long messageId, final long attachmentId, - final int statusCode, final int progress) { - debugTrace(LOG_TAG, "ServiceCallback for attachment #%d", attachmentId); - - // Record status and progress - final DownloadRequest req = mDownloadsInProgress.get(attachmentId); - if (req != null) { - final long now = System.currentTimeMillis(); - debugTrace("ServiceCallback: status code changing from %d to %d", - req.mLastStatusCode, statusCode); - debugTrace("ServiceCallback: progress changing from %d to %d", - req.mLastProgress,progress); - debugTrace("ServiceCallback: last callback time changing from %d to %d", - req.mLastCallbackTime, now); - - // Update some state to keep track of the progress of the download - req.mLastStatusCode = statusCode; - req.mLastProgress = progress; - req.mLastCallbackTime = now; - - // Update the attachment status in the provider. - final Attachment attachment = - Attachment.restoreAttachmentWithId(AttachmentService.this, attachmentId); - final ContentValues values = getAttachmentUpdateValues(attachment, statusCode, - progress); - if (values.size() > 0) { - attachment.update(AttachmentService.this, values); - } - - switch (statusCode) { - case EmailServiceStatus.IN_PROGRESS: - break; - default: - // It is assumed that any other error is either a success or an error - // Either way, the final updates to the DownloadRequest and attachment - // objects will be handed there. - LogUtils.d(LOG_TAG, "Attachment #%d is done", attachmentId); - endDownload(attachmentId, statusCode); - break; - } - } else { - // The only way that we can get a callback from the service implementation for - // an attachment that doesn't exist is if it was cancelled due to the - // AttachmentWatchdog. This is a valid scenario and the Watchdog should have already - // marked this attachment as failed/cancelled. - } - } - } - - /** - * Called directly by EmailProvider whenever an attachment is inserted or changed. Since this - * call is being invoked on the UI thread, we need to make sure that the downloads are - * happening in the background. - * @param context the caller's context - * @param id the attachment's id - * @param flags the new flags for the attachment - */ - public static void attachmentChanged(final Context context, final long id, final int flags) { - LogUtils.d(LOG_TAG, "Attachment with id: %d will potentially be queued for download", id); - // Throw this info into an intent and send it to the attachment service. - final Intent intent = new Intent(context, AttachmentService.class); - debugTrace("Calling startService with extras %d & %d", id, flags); - intent.putExtra(EXTRA_ATTACHMENT_ID, id); - intent.putExtra(EXTRA_ATTACHMENT_FLAGS, flags); - context.startService(intent); - } - - /** - * The main entry point for this service, the attachment to download can be identified - * by the EXTRA_ATTACHMENT extra in the intent. - */ - @Override - public int onStartCommand(final Intent intent, final int flags, final int startId) { - if (sRunningService == null) { - sRunningService = this; - } - if (intent != null) { - // Let's add this id/flags combo to the list of potential attachments to process. - final long attachment_id = intent.getLongExtra(EXTRA_ATTACHMENT_ID, -1); - final int attachment_flags = intent.getIntExtra(EXTRA_ATTACHMENT_FLAGS, -1); - if ((attachment_id >= 0) && (attachment_flags >= 0)) { - sAttachmentChangedQueue.add(new long[]{attachment_id, attachment_flags}); - // Process the queue if we're in a wait - kick(); - } else { - debugTrace("Received an invalid intent w/o the required extras %d & %d", - attachment_id, attachment_flags); - } - } else { - debugTrace("Received a null intent in onStartCommand"); - } - return Service.START_STICKY; - } - - /** - * Most of the leg work is done by our service thread that is created when this - * service is created. - */ - @Override - public void onCreate() { - // Start up our service thread. - new Thread(this, "AttachmentService").start(); - } - - @Override - public IBinder onBind(final Intent intent) { - return null; - } - - @Override - public void onDestroy() { - debugTrace("Destroying AttachmentService object"); - dumpInProgressDownloads(); - - // Mark this instance of the service as stopped. Our main loop for the AttachmentService - // checks for this flag along with the AttachmentWatchdog. - mStop = true; - if (sRunningService != null) { - // Kick it awake to get it to realize that we are stopping. - kick(); - sRunningService = null; - } - if (mConnectivityManager != null) { - mConnectivityManager.unregister(); - mConnectivityManager.stopWait(); - mConnectivityManager = null; - } - } - - /** - * The main routine for our AttachmentService service thread. - */ - @Override - public void run() { - // These fields are only used within the service thread - mConnectivityManager = new EmailConnectivityManager(this, LOG_TAG); - mAccountManagerStub = new AccountManagerStub(this); - - // Run through all attachments in the database that require download and add them to - // the queue. This is the case where a previous AttachmentService may have been notified - // to stop before processing everything in its queue. - final int mask = Attachment.FLAG_DOWNLOAD_FORWARD | Attachment.FLAG_DOWNLOAD_USER_REQUEST; - final Cursor c = getContentResolver().query(Attachment.CONTENT_URI, - EmailContent.ID_PROJECTION, "(" + AttachmentColumns.FLAGS + " & ?) != 0", - new String[] {Integer.toString(mask)}, null); - try { - LogUtils.d(LOG_TAG, - "Count of previous downloads to resume (from db): %d", c.getCount()); - while (c.moveToNext()) { - final Attachment attachment = Attachment.restoreAttachmentWithId( - this, c.getLong(EmailContent.ID_PROJECTION_COLUMN)); - if (attachment != null) { - debugTrace("Attempting to download attachment #%d again.", attachment.mId); - onChange(this, attachment); - } - } - } catch (Exception e) { - e.printStackTrace(); - } finally { - c.close(); - } - - // Loop until stopped, with a 30 minute wait loop - while (!mStop) { - // Here's where we run our attachment loading logic... - // Make a local copy of the variable so we don't null-crash on service shutdown - final EmailConnectivityManager ecm = mConnectivityManager; - if (ecm != null) { - ecm.waitForConnectivity(); - } - if (mStop) { - // We might be bailing out here due to the service shutting down - LogUtils.d(LOG_TAG, "AttachmentService has been instructed to stop"); - break; - } - - // In advanced debug mode, let's look at the state of all in-progress downloads - // after processQueue() runs. - debugTrace("Downloads Map before processQueue"); - dumpInProgressDownloads(); - processQueue(); - debugTrace("Downloads Map after processQueue"); - dumpInProgressDownloads(); - - if (mDownloadQueue.isEmpty() && (mDownloadsInProgress.size() < 1)) { - LogUtils.d(LOG_TAG, "Shutting down service. No in-progress or pending downloads."); - stopSelf(); - break; - } - debugTrace("Run() wait for mLock"); - synchronized(mLock) { - try { - mLock.wait(PROCESS_QUEUE_WAIT_TIME); - } catch (InterruptedException e) { - // That's ok; we'll just keep looping - } - } - debugTrace("Run() got mLock"); - } - - // Unregister now that we're done - // Make a local copy of the variable so we don't null-crash on service shutdown - final EmailConnectivityManager ecm = mConnectivityManager; - if (ecm != null) { - ecm.unregister(); - } - } - - /* - * Function that kicks the service into action as it may be waiting for this object - * as it processed the last round of attachments. - */ - private void kick() { - synchronized(mLock) { - mLock.notify(); - } - } - - /** - * onChange is called by the AttachmentReceiver upon receipt of a valid notification from - * EmailProvider that an attachment has been inserted or modified. It's not strictly - * necessary that we detect a deleted attachment, as the code always checks for the - * existence of an attachment before acting on it. - */ - public synchronized void onChange(final Context context, final Attachment att) { - debugTrace("onChange() for Attachment: #%d", att.mId); - DownloadRequest req = mDownloadQueue.findRequestById(att.mId); - final long priority = getAttachmentPriority(att); - if (priority == PRIORITY_NONE) { - LogUtils.d(LOG_TAG, "Attachment #%d has no priority and will not be downloaded", - att.mId); - // In this case, there is no download priority for this attachment - if (req != null) { - // If it exists in the map, remove it - // NOTE: We don't yet support deleting downloads in progress - mDownloadQueue.removeRequest(req); - } - } else { - // Ignore changes that occur during download - if (mDownloadsInProgress.containsKey(att.mId)) { - debugTrace("Attachment #%d was already in the queue", att.mId); - return; - } - // If this is new, add the request to the queue - if (req == null) { - LogUtils.d(LOG_TAG, "Attachment #%d is a new download request", att.mId); - req = new DownloadRequest(context, att); - final AttachmentInfo attachInfo = new AttachmentInfo(context, att); - if (!attachInfo.isEligibleForDownload()) { - LogUtils.w(LOG_TAG, "Attachment #%d is not eligible for download", att.mId); - // We can't download this file due to policy, depending on what type - // of request we received, we handle the response differently. - if (((att.mFlags & Attachment.FLAG_DOWNLOAD_USER_REQUEST) != 0) || - ((att.mFlags & Attachment.FLAG_POLICY_DISALLOWS_DOWNLOAD) != 0)) { - LogUtils.w(LOG_TAG, "Attachment #%d cannot be downloaded ever", att.mId); - // There are a couple of situations where we will not even allow this - // request to go in the queue because we can already process it as a - // failure. - // 1. The user explicitly wants to download this attachment from the - // email view but they should not be able to...either because there is - // no app to view it or because its been marked as a policy violation. - // 2. The user is forwarding an email and the attachment has been - // marked as a policy violation. If the attachment is non viewable - // that is OK for forwarding a message so we'll let it pass through - markAttachmentAsFailed(att); - return; - } - // If we get this far it a forward of an attachment that is only - // ineligible because we can't view it or process it. Not because we - // can't download it for policy reasons. Let's let this go through because - // the final recipient of this forward email might be able to process it. - } - mDownloadQueue.addRequest(req); - } - // TODO: If the request already existed, we'll update the priority (so that the time is - // up-to-date); otherwise, create a new request - LogUtils.d(LOG_TAG, - "Attachment #%d queued for download, priority: %d, created time: %d", - att.mId, req.mPriority, req.mCreatedTime); - } - // Process the queue if we're in a wait - kick(); - } - - /** - * Set the bits in the provider to mark this download as failed. - * @param att The attachment that failed to download. - */ - void markAttachmentAsFailed(final Attachment att) { - final ContentValues cv = new ContentValues(); - final int flags = Attachment.FLAG_DOWNLOAD_FORWARD | Attachment.FLAG_DOWNLOAD_USER_REQUEST; - cv.put(AttachmentColumns.FLAGS, att.mFlags &= ~flags); - cv.put(AttachmentColumns.UI_STATE, AttachmentState.FAILED); - att.update(this, cv); - } - - /** - * Set the bits in the provider to mark this download as completed. - * @param att The attachment that was downloaded. - */ - void markAttachmentAsCompleted(final Attachment att) { - final ContentValues cv = new ContentValues(); - final int flags = Attachment.FLAG_DOWNLOAD_FORWARD | Attachment.FLAG_DOWNLOAD_USER_REQUEST; - cv.put(AttachmentColumns.FLAGS, att.mFlags &= ~flags); - cv.put(AttachmentColumns.UI_STATE, AttachmentState.SAVED); - att.update(this, cv); - } - - /** - * Run through the AttachmentMap and find DownloadRequests that can be executed, enforcing - * the limit on maximum downloads - */ - synchronized void processQueue() { - debugTrace("Processing changed queue, num entries: %d", sAttachmentChangedQueue.size()); - - // First thing we need to do is process the list of "potential downloads" that we - // added to sAttachmentChangedQueue - long[] change = sAttachmentChangedQueue.poll(); - while (change != null) { - // Process this change - final long id = change[0]; - final long flags = change[1]; - final Attachment attachment = Attachment.restoreAttachmentWithId(this, id); - if (attachment == null) { - LogUtils.w(LOG_TAG, "Could not restore attachment #%d", id); - continue; - } - attachment.mFlags = (int) flags; - onChange(this, attachment); - change = sAttachmentChangedQueue.poll(); - } - - debugTrace("Processing download queue, num entries: %d", mDownloadQueue.getSize()); - - while (mDownloadsInProgress.size() < MAX_SIMULTANEOUS_DOWNLOADS) { - final DownloadRequest req = mDownloadQueue.getNextRequest(); - if (req == null) { - // No more queued requests? We are done for now. - break; - } - // Enforce per-account limit here - if (getDownloadsForAccount(req.mAccountId) >= MAX_SIMULTANEOUS_DOWNLOADS_PER_ACCOUNT) { - LogUtils.w(LOG_TAG, "Skipping #%d; maxed for acct %d", - req.mAttachmentId, req.mAccountId); - continue; - } - if (Attachment.restoreAttachmentWithId(this, req.mAttachmentId) == null) { - LogUtils.e(LOG_TAG, "Could not load attachment: #%d", req.mAttachmentId); - continue; - } - if (!req.mInProgress) { - final long currentTime = SystemClock.elapsedRealtime(); - if (req.mRetryCount > 0 && req.mRetryStartTime > currentTime) { - debugTrace("Need to wait before retrying attachment #%d", req.mAttachmentId); - mWatchdog.setWatchdogAlarm(this, CONNECTION_ERROR_RETRY_MILLIS, - CALLBACK_TIMEOUT); - continue; - } - // TODO: We try to gate ineligible downloads from entering the queue but its - // always possible that they made it in here regardless in the future. In a - // perfect world, we would make it bullet proof with a check for eligibility - // here instead/also. - tryStartDownload(req); - } - } - - // Check our ability to be opportunistic regarding background downloads. - final EmailConnectivityManager ecm = mConnectivityManager; - if ((ecm == null) || !ecm.isAutoSyncAllowed() || - (ecm.getActiveNetworkType() != ConnectivityManager.TYPE_WIFI)) { - // Only prefetch if it if connectivity is available, prefetch is enabled - // and we are on WIFI - LogUtils.d(LOG_TAG, "Skipping opportunistic downloads since WIFI is not available"); - return; - } - - // Then, try opportunistic download of appropriate attachments - final int availableBackgroundThreads = - MAX_SIMULTANEOUS_DOWNLOADS - mDownloadsInProgress.size() - 1; - if (availableBackgroundThreads < 1) { - // We want to leave one spot open for a user requested download that we haven't - // started processing yet. - LogUtils.d(LOG_TAG, "Skipping opportunistic downloads, %d threads available", - availableBackgroundThreads); - return; - } - - debugTrace("Launching up to %d opportunistic downloads", availableBackgroundThreads); - - // We'll load up the newest 25 attachments that aren't loaded or queued - // TODO: We are always looking for MAX_ATTACHMENTS_TO_CHECK, shouldn't this be - // backgroundDownloads instead? We should fix and test this. - final Uri lookupUri = EmailContent.uriWithLimit(Attachment.CONTENT_URI, - MAX_ATTACHMENTS_TO_CHECK); - final Cursor c = this.getContentResolver().query(lookupUri, - Attachment.CONTENT_PROJECTION, - EmailContent.Attachment.PRECACHE_INBOX_SELECTION, - null, AttachmentColumns._ID + " DESC"); - File cacheDir = this.getCacheDir(); - try { - while (c.moveToNext()) { - final Attachment att = new Attachment(); - att.restore(c); - final Account account = Account.restoreAccountWithId(this, att.mAccountKey); - if (account == null) { - // Clean up this orphaned attachment; there's no point in keeping it - // around; then try to find another one - debugTrace("Found orphaned attachment #%d", att.mId); - EmailContent.delete(this, Attachment.CONTENT_URI, att.mId); - } else { - // Check that the attachment meets system requirements for download - // Note that there couple be policy that does not allow this attachment - // to be downloaded. - final AttachmentInfo info = new AttachmentInfo(this, att); - if (info.isEligibleForDownload()) { - // Either the account must be able to prefetch or this must be - // an inline attachment. - if (att.mContentId != null || canPrefetchForAccount(account, cacheDir)) { - final Integer tryCount = mAttachmentFailureMap.get(att.mId); - if (tryCount != null && tryCount > MAX_DOWNLOAD_RETRIES) { - // move onto the next attachment - LogUtils.w(LOG_TAG, - "Too many failed attempts for attachment #%d ", att.mId); - continue; - } - // Start this download and we're done - final DownloadRequest req = new DownloadRequest(this, att); - tryStartDownload(req); - break; - } - } else { - // If this attachment was ineligible for download - // because of policy related issues, its flags would be set to - // FLAG_POLICY_DISALLOWS_DOWNLOAD and would not show up in the - // query results. We are most likely here for other reasons such - // as the inability to view the attachment. In that case, let's just - // skip it for now. - LogUtils.w(LOG_TAG, "Skipping attachment #%d, it is ineligible", att.mId); - } - } - } - } finally { - c.close(); - } - } - - /** - * Attempt to execute the DownloadRequest, enforcing the maximum downloads per account - * parameter - * @param req the DownloadRequest - * @return whether or not the download was started - */ - synchronized boolean tryStartDownload(final DownloadRequest req) { - final EmailServiceProxy service = EmailServiceUtils.getServiceForAccount( - AttachmentService.this, req.mAccountId); - - // Do not download the same attachment multiple times - boolean alreadyInProgress = mDownloadsInProgress.get(req.mAttachmentId) != null; - if (alreadyInProgress) { - debugTrace("This attachment #%d is already in progress", req.mAttachmentId); - return false; - } - - try { - startDownload(service, req); - } catch (RemoteException e) { - // TODO: Consider whether we need to do more in this case... - // For now, fix up our data to reflect the failure - cancelDownload(req); - } - return true; - } - - /** - * Do the work of starting an attachment download using the EmailService interface, and - * set our watchdog alarm - * - * @param service the service handling the download - * @param req the DownloadRequest - * @throws RemoteException - */ - private void startDownload(final EmailServiceProxy service, final DownloadRequest req) - throws RemoteException { - LogUtils.d(LOG_TAG, "Starting download for Attachment #%d", req.mAttachmentId); - req.mStartTime = System.currentTimeMillis(); - req.mInProgress = true; - mDownloadsInProgress.put(req.mAttachmentId, req); - service.loadAttachment(mServiceCallback, req.mAccountId, req.mAttachmentId, - req.mPriority != PRIORITY_FOREGROUND); - mWatchdog.setWatchdogAlarm(this); - } - - synchronized void cancelDownload(final DownloadRequest req) { - LogUtils.d(LOG_TAG, "Cancelling download for Attachment #%d", req.mAttachmentId); - req.mInProgress = false; - mDownloadsInProgress.remove(req.mAttachmentId); - // Remove the download from our queue, and then decide whether or not to add it back. - mDownloadQueue.removeRequest(req); - req.mRetryCount++; - if (req.mRetryCount > CONNECTION_ERROR_MAX_RETRIES) { - LogUtils.w(LOG_TAG, "Too many failures giving up on Attachment #%d", req.mAttachmentId); - } else { - debugTrace("Moving to end of queue, will retry #%d", req.mAttachmentId); - // The time field of DownloadRequest is final, because it's unsafe to change it - // as long as the DownloadRequest is in the DownloadSet. It's needed for the - // comparator, so changing time would make the request unfindable. - // Instead, we'll create a new DownloadRequest with an updated time. - // This will sort at the end of the set. - final DownloadRequest newReq = new DownloadRequest(req, SystemClock.elapsedRealtime()); - mDownloadQueue.addRequest(newReq); - } - } - - /** - * Called when a download is finished; we get notified of this via our EmailServiceCallback - * @param attachmentId the id of the attachment whose download is finished - * @param statusCode the EmailServiceStatus code returned by the Service - */ - synchronized void endDownload(final long attachmentId, final int statusCode) { - LogUtils.d(LOG_TAG, "Finishing download #%d", attachmentId); - - // Say we're no longer downloading this - mDownloadsInProgress.remove(attachmentId); - - // TODO: This code is conservative and treats connection issues as failures. - // Since we have no mechanism to throttle reconnection attempts, it makes - // sense to be cautious here. Once logic is in place to prevent connecting - // in a tight loop, we can exclude counting connection issues as "failures". - - // Update the attachment failure list if needed - Integer downloadCount; - downloadCount = mAttachmentFailureMap.remove(attachmentId); - if (statusCode != EmailServiceStatus.SUCCESS) { - if (downloadCount == null) { - downloadCount = 0; - } - downloadCount += 1; - LogUtils.w(LOG_TAG, "This attachment failed, adding #%d to failure map", attachmentId); - mAttachmentFailureMap.put(attachmentId, downloadCount); - } - - final DownloadRequest req = mDownloadQueue.findRequestById(attachmentId); - if (statusCode == EmailServiceStatus.CONNECTION_ERROR) { - // If this needs to be retried, just process the queue again - if (req != null) { - req.mRetryCount++; - if (req.mRetryCount > CONNECTION_ERROR_MAX_RETRIES) { - // We are done, we maxed out our total number of tries. - // Not that we do not flag this attachment with any special flags so the - // AttachmentService will try to download this attachment again the next time - // that it starts up. - LogUtils.w(LOG_TAG, "Too many tried for connection errors, giving up #%d", - attachmentId); - mDownloadQueue.removeRequest(req); - // Note that we are not doing anything with the attachment right now - // We will annotate it later in this function if needed. - } else if (req.mRetryCount > CONNECTION_ERROR_DELAY_THRESHOLD) { - // TODO: I'm not sure this is a great retry/backoff policy, but we're - // afraid of changing behavior too much in case something relies upon it. - // So now, for the first five errors, we'll retry immediately. For the next - // five tries, we'll add a ten second delay between each. After that, we'll - // give up. - LogUtils.w(LOG_TAG, "ConnectionError #%d, retried %d times, adding delay", - attachmentId, req.mRetryCount); - req.mInProgress = false; - req.mRetryStartTime = SystemClock.elapsedRealtime() + - CONNECTION_ERROR_RETRY_MILLIS; - mWatchdog.setWatchdogAlarm(this, CONNECTION_ERROR_RETRY_MILLIS, - CALLBACK_TIMEOUT); - } else { - LogUtils.w(LOG_TAG, "ConnectionError for #%d, retried %d times, adding delay", - attachmentId, req.mRetryCount); - req.mInProgress = false; - req.mRetryStartTime = 0; - kick(); - } - } - return; - } - - // If the request is still in the queue, remove it - if (req != null) { - mDownloadQueue.removeRequest(req); - } - - if (ENABLE_ATTACHMENT_SERVICE_DEBUG > 0) { - long secs = 0; - if (req != null) { - secs = (System.currentTimeMillis() - req.mCreatedTime) / 1000; - } - final String status = (statusCode == EmailServiceStatus.SUCCESS) ? "Success" : - "Error " + statusCode; - debugTrace("Download finished for attachment #%d; %d seconds from request, status: %s", - attachmentId, secs, status); - } - - final Attachment attachment = Attachment.restoreAttachmentWithId(this, attachmentId); - if (attachment != null) { - final long accountId = attachment.mAccountKey; - // Update our attachment storage for this account - Long currentStorage = mAttachmentStorageMap.get(accountId); - if (currentStorage == null) { - currentStorage = 0L; - } - mAttachmentStorageMap.put(accountId, currentStorage + attachment.mSize); - boolean deleted = false; - if ((attachment.mFlags & Attachment.FLAG_DOWNLOAD_FORWARD) != 0) { - if (statusCode == EmailServiceStatus.ATTACHMENT_NOT_FOUND) { - // If this is a forwarding download, and the attachment doesn't exist (or - // can't be downloaded) delete it from the outgoing message, lest that - // message never get sent - EmailContent.delete(this, Attachment.CONTENT_URI, attachment.mId); - // TODO: Talk to UX about whether this is even worth doing - NotificationController nc = NotificationController.getInstance(this); - nc.showDownloadForwardFailedNotificationSynchronous(attachment); - deleted = true; - LogUtils.w(LOG_TAG, "Deleting forwarded attachment #%d for message #%d", - attachmentId, attachment.mMessageKey); - } - // If we're an attachment on forwarded mail, and if we're not still blocked, - // try to send pending mail now (as mediated by MailService) - if ((req != null) && - !Utility.hasUnloadedAttachments(this, attachment.mMessageKey)) { - debugTrace("Downloads finished for outgoing msg #%d", req.mMessageId); - EmailServiceProxy service = EmailServiceUtils.getServiceForAccount( - this, accountId); - try { - service.sendMail(accountId); - } catch (RemoteException e) { - LogUtils.e(LOG_TAG, "RemoteException while trying to send message: #%d, %s", - req.mMessageId, e.toString()); - } - } - } - if (statusCode == EmailServiceStatus.MESSAGE_NOT_FOUND) { - Message msg = Message.restoreMessageWithId(this, attachment.mMessageKey); - if (msg == null) { - LogUtils.w(LOG_TAG, "Deleting attachment #%d with no associated message #%d", - attachment.mId, attachment.mMessageKey); - // If there's no associated message, delete the attachment - EmailContent.delete(this, Attachment.CONTENT_URI, attachment.mId); - } else { - // If there really is a message, retry - // TODO: How will this get retried? It's still marked as inProgress? - LogUtils.w(LOG_TAG, "Retrying attachment #%d with associated message #%d", - attachment.mId, attachment.mMessageKey); - kick(); - return; - } - } else if (!deleted) { - // Clear the download flags, since we're done for now. Note that this happens - // only for non-recoverable errors. When these occur for forwarded mail, we can - // ignore it and continue; otherwise, it was either 1) a user request, in which - // case the user can retry manually or 2) an opportunistic download, in which - // case the download wasn't critical - LogUtils.d(LOG_TAG, "Attachment #%d successfully downloaded!", attachment.mId); - markAttachmentAsCompleted(attachment); - } - } - // Process the queue - kick(); - } - - /** - * Count the number of running downloads in progress for this account - * @param accountId the id of the account - * @return the count of running downloads - */ - synchronized int getDownloadsForAccount(final long accountId) { - int count = 0; - for (final DownloadRequest req: mDownloadsInProgress.values()) { - if (req.mAccountId == accountId) { - count++; - } - } - return count; - } - - /** - * Calculate the download priority of an Attachment. A priority of zero means that the - * attachment is not marked for download. - * @param att the Attachment - * @return the priority key of the Attachment - */ - private static int getAttachmentPriority(final Attachment att) { - int priorityClass = PRIORITY_NONE; - final int flags = att.mFlags; - if ((flags & Attachment.FLAG_DOWNLOAD_FORWARD) != 0) { - priorityClass = PRIORITY_SEND_MAIL; - } else if ((flags & Attachment.FLAG_DOWNLOAD_USER_REQUEST) != 0) { - priorityClass = PRIORITY_FOREGROUND; - } - return priorityClass; - } - - /** - * Determine whether an attachment can be prefetched for the given account based on - * total download size restrictions tied to the account. - * @return true if download is allowed, false otherwise - */ - public boolean canPrefetchForAccount(final Account account, final File dir) { - // Check account, just in case - if (account == null) return false; - - // First, check preference and quickly return if prefetch isn't allowed - if ((account.mFlags & Account.FLAGS_BACKGROUND_ATTACHMENTS) == 0) { - debugTrace("Prefetch is not allowed for this account: %d", account.getId()); - return false; - } - - final long totalStorage = dir.getTotalSpace(); - final long usableStorage = dir.getUsableSpace(); - final long minAvailable = (long)(totalStorage * PREFETCH_MINIMUM_STORAGE_AVAILABLE); - - // If there's not enough overall storage available, stop now - if (usableStorage < minAvailable) { - debugTrace("Not enough physical storage for prefetch"); - return false; - } - - final int numberOfAccounts = mAccountManagerStub.getNumberOfAccounts(); - // Calculate an even per-account storage although it would make a lot of sense to not - // do this as you may assign more storage to your corporate account rather than a personal - // account. - final long perAccountMaxStorage = - (long)(totalStorage * PREFETCH_MAXIMUM_ATTACHMENT_STORAGE / numberOfAccounts); - - // Retrieve our idea of currently used attachment storage; since we don't track deletions, - // this number is the "worst case". If the number is greater than what's allowed per - // account, we walk the directory to determine the actual number. - Long accountStorage = mAttachmentStorageMap.get(account.mId); - if (accountStorage == null || (accountStorage > perAccountMaxStorage)) { - // Calculate the exact figure for attachment storage for this account - accountStorage = 0L; - File[] files = dir.listFiles(); - if (files != null) { - for (File file : files) { - accountStorage += file.length(); - } - } - // Cache the value. No locking here since this is a concurrent collection object. - mAttachmentStorageMap.put(account.mId, accountStorage); - } - - // Return true if we're using less than the maximum per account - if (accountStorage >= perAccountMaxStorage) { - debugTrace("Prefetch not allowed for account %d; used: %d, limit %d", - account.mId, accountStorage, perAccountMaxStorage); - return false; - } - return true; - } - - boolean isConnected() { - if (mConnectivityManager != null) { - return mConnectivityManager.hasConnectivity(); - } - return false; - } - - // For Debugging. - synchronized public void dumpInProgressDownloads() { - if (ENABLE_ATTACHMENT_SERVICE_DEBUG < 1) { - LogUtils.d(LOG_TAG, "Advanced logging not configured."); - } - for (final DownloadRequest req : mDownloadsInProgress.values()) { - LogUtils.d(LOG_TAG, "--BEGIN DownloadRequest DUMP--"); - LogUtils.d(LOG_TAG, "Account: #%d", req.mAccountId); - LogUtils.d(LOG_TAG, "Message: #%d", req.mMessageId); - LogUtils.d(LOG_TAG, "Attachment: #%d", req.mAttachmentId); - LogUtils.d(LOG_TAG, "Created Time: %d", req.mCreatedTime); - LogUtils.d(LOG_TAG, "Priority: %d", req.mPriority); - if (req.mInProgress == true) { - LogUtils.d(LOG_TAG, "This download is in progress"); - } else { - LogUtils.d(LOG_TAG, "This download is not in progress"); - } - LogUtils.d(LOG_TAG, "Start Time: %d", req.mStartTime); - LogUtils.d(LOG_TAG, "Retry Count: %d", req.mRetryCount); - LogUtils.d(LOG_TAG, "Retry Start Tiome: %d", req.mRetryStartTime); - LogUtils.d(LOG_TAG, "Last Status Code: %d", req.mLastStatusCode); - LogUtils.d(LOG_TAG, "Last Progress: %d", req.mLastProgress); - LogUtils.d(LOG_TAG, "Last Callback Time: %d", req.mLastCallbackTime); - LogUtils.d(LOG_TAG, "------------------------------"); - } - } - - - @Override - public void dump(final FileDescriptor fd, final PrintWriter pw, final String[] args) { - pw.println("AttachmentService"); - final long time = System.currentTimeMillis(); - synchronized(mDownloadQueue) { - pw.println(" Queue, " + mDownloadQueue.getSize() + " entries"); - // If you iterate over the queue either via iterator or collection, they are not - // returned in any particular order. With all things being equal its better to go with - // a collection to avoid any potential ConcurrentModificationExceptions. - // If we really want this sorted, we can sort it manually since performance isn't a big - // concern with this debug method. - for (final DownloadRequest req : mDownloadQueue.mRequestMap.values()) { - pw.println(" Account: " + req.mAccountId + ", Attachment: " + req.mAttachmentId); - pw.println(" Priority: " + req.mPriority + ", Time: " + req.mCreatedTime + - (req.mInProgress ? " [In progress]" : "")); - final Attachment att = Attachment.restoreAttachmentWithId(this, req.mAttachmentId); - if (att == null) { - pw.println(" Attachment not in database?"); - } else if (att.mFileName != null) { - final String fileName = att.mFileName; - final String suffix; - final int lastDot = fileName.lastIndexOf('.'); - if (lastDot >= 0) { - suffix = fileName.substring(lastDot); - } else { - suffix = "[none]"; - } - pw.print(" Suffix: " + suffix); - if (att.getContentUri() != null) { - pw.print(" ContentUri: " + att.getContentUri()); - } - pw.print(" Mime: "); - if (att.mMimeType != null) { - pw.print(att.mMimeType); - } else { - pw.print(AttachmentUtilities.inferMimeType(fileName, null)); - pw.print(" [inferred]"); - } - pw.println(" Size: " + att.mSize); - } - if (req.mInProgress) { - pw.println(" Status: " + req.mLastStatusCode + ", Progress: " + - req.mLastProgress); - pw.println(" Started: " + req.mStartTime + ", Callback: " + - req.mLastCallbackTime); - pw.println(" Elapsed: " + ((time - req.mStartTime) / 1000L) + "s"); - if (req.mLastCallbackTime > 0) { - pw.println(" CB: " + ((time - req.mLastCallbackTime) / 1000L) + "s"); - } - } - } - } - } - - // For Testing - AccountManagerStub mAccountManagerStub; - private final HashMap mAccountServiceMap = new HashMap(); - - void addServiceIntentForTest(final long accountId, final Intent intent) { - mAccountServiceMap.put(accountId, intent); - } - - /** - * We only use the getAccounts() call from AccountManager, so this class wraps that call and - * allows us to build a mock account manager stub in the unit tests - */ - static class AccountManagerStub { - private int mNumberOfAccounts; - private final AccountManager mAccountManager; - - AccountManagerStub(final Context context) { - if (context != null) { - mAccountManager = AccountManager.get(context); - } else { - mAccountManager = null; - } - } - - int getNumberOfAccounts() { - if (mAccountManager != null) { - return mAccountManager.getAccounts().length; - } else { - return mNumberOfAccounts; - } - } - - void setNumberOfAccounts(final int numberOfAccounts) { - mNumberOfAccounts = numberOfAccounts; - } - } -} diff --git a/src/com/android/email/service/AuthenticatorService.java b/src/com/android/email/service/AuthenticatorService.java deleted file mode 100644 index c69bb93e6..000000000 --- a/src/com/android/email/service/AuthenticatorService.java +++ /dev/null @@ -1,165 +0,0 @@ -/* - * 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.AccountSetupFinal; -import com.android.email.service.EmailServiceUtils.EmailServiceInfo; -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 { - - final String protocol = EmailServiceUtils.getProtocolFromAccountType( - AuthenticatorService.this, accountType); - final EmailServiceInfo info = EmailServiceUtils.getServiceInfo( - AuthenticatorService.this, protocol); - - // 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 (info != null && info.syncContacts) { - boolean syncContacts = options.getBoolean(OPTIONS_CONTACTS_SYNC_ENABLED, false); - ContentResolver.setIsSyncable(account, ContactsContract.AUTHORITY, 1); - ContentResolver.setSyncAutomatically(account, ContactsContract.AUTHORITY, - syncContacts); - } - - // Set up calendar syncing, if appropriate - if (info != null && info.syncCalendar) { - boolean syncCalendar = options.getBoolean(OPTIONS_CALENDAR_SYNC_ENABLED, false); - 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 = - AccountSetupFinal.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/EasAuthenticatorService.java b/src/com/android/email/service/EasAuthenticatorService.java deleted file mode 100644 index 4dbca7f0c..000000000 --- a/src/com/android/email/service/EasAuthenticatorService.java +++ /dev/null @@ -1,23 +0,0 @@ -/* - * 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; - -/** - * This service needs to be declared separately from the base service - */ -public class EasAuthenticatorService extends AuthenticatorService { -} diff --git a/src/com/android/email/service/EasAuthenticatorServiceAlternate.java b/src/com/android/email/service/EasAuthenticatorServiceAlternate.java deleted file mode 100644 index 28d8fb72f..000000000 --- a/src/com/android/email/service/EasAuthenticatorServiceAlternate.java +++ /dev/null @@ -1,23 +0,0 @@ -/* - * Copyright (C) 2010 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.email.service; - -/** - * This service needs to be declared separately from the base service - */ -public class EasAuthenticatorServiceAlternate extends AuthenticatorService { -} diff --git a/src/com/android/email/service/EasTestAuthenticatorService.java b/src/com/android/email/service/EasTestAuthenticatorService.java deleted file mode 100644 index c8d853b93..000000000 --- a/src/com/android/email/service/EasTestAuthenticatorService.java +++ /dev/null @@ -1,123 +0,0 @@ -/* - * 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.service; - -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.Context; -import android.content.Intent; -import android.os.Bundle; -import android.os.IBinder; - -import com.android.email.activity.setup.AccountSetupFinal; - -/** - * Anauthenticator service for reconciliation tests; it simply adds the account to AccountManager - * directly with a username and password. - */ -public class EasTestAuthenticatorService extends Service { - public static final String OPTIONS_USERNAME = "username"; - public static final String OPTIONS_PASSWORD = "password"; - private static final String TEST_ACCOUNT_TYPE = "com.android.test_exchange"; - - class EasAuthenticator extends AbstractAccountAuthenticator { - - public EasAuthenticator(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), - TEST_ACCOUNT_TYPE); - AccountManager.get(EasTestAuthenticatorService.this).addAccountExplicitly( - account, options.getString(OPTIONS_PASSWORD), null); - - Bundle b = new Bundle(); - b.putString(AccountManager.KEY_ACCOUNT_NAME, TEST_ACCOUNT_TYPE); - 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 = - AccountSetupFinal.actionGetCreateAccountIntent( - EasTestAuthenticatorService.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 EasAuthenticator(this).getIBinder(); - } else { - return null; - } - } -} diff --git a/src/com/android/email/service/EmailBroadcastProcessorService.java b/src/com/android/email/service/EmailBroadcastProcessorService.java deleted file mode 100644 index 3b15904d7..000000000 --- a/src/com/android/email/service/EmailBroadcastProcessorService.java +++ /dev/null @@ -1,371 +0,0 @@ -/* - * 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.accounts.AccountManager; -import android.app.IntentService; -import android.content.ComponentName; -import android.content.ContentResolver; -import android.content.ContentUris; -import android.content.ContentValues; -import android.content.Context; -import android.content.Intent; -import android.content.PeriodicSync; -import android.content.pm.PackageManager; -import android.database.Cursor; -import android.net.Uri; -import android.os.Bundle; -import android.provider.CalendarContract; -import android.provider.ContactsContract; -import android.text.TextUtils; -import android.text.format.DateUtils; - -import com.android.email.EmailIntentService; -import com.android.email.Preferences; -import com.android.email.R; -import com.android.email.SecurityPolicy; -import com.android.email.provider.AccountReconciler; -import com.android.emailcommon.Logging; -import com.android.emailcommon.VendorPolicyLoader; -import com.android.emailcommon.provider.Account; -import com.android.emailcommon.provider.EmailContent; -import com.android.emailcommon.provider.EmailContent.AccountColumns; -import com.android.emailcommon.provider.HostAuth; -import com.android.mail.providers.UIProvider; -import com.android.mail.utils.LogUtils; -import com.google.common.annotations.VisibleForTesting; -import com.google.common.collect.Maps; - -import java.util.Collections; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; - -/** - * The service that really handles broadcast intents on a worker thread. - * - * We make it a service, because: - *

    - *
  • So that it's less likely for the process to get killed. - *
  • Even if it does, the Intent that have started it will be re-delivered by the system, - * and we can start the process again. (Using {@link #setIntentRedelivery}). - *
- * - * This also handles the DeviceAdminReceiver in SecurityPolicy, because it is also - * a BroadcastReceiver and requires the same processing semantics. - */ -public class EmailBroadcastProcessorService extends IntentService { - // Action used for BroadcastReceiver entry point - private static final String ACTION_BROADCAST = "broadcast_receiver"; - - // This is a helper used to process DeviceAdminReceiver messages - private static final String ACTION_DEVICE_POLICY_ADMIN = "com.android.email.devicepolicy"; - private static final String EXTRA_DEVICE_POLICY_ADMIN = "message_code"; - - // Action used for EmailUpgradeBroadcastReceiver. - private static final String ACTION_UPGRADE_BROADCAST = "upgrade_broadcast_receiver"; - - public EmailBroadcastProcessorService() { - // Class name will be the thread name. - super(EmailBroadcastProcessorService.class.getName()); - - // Intent should be redelivered if the process gets killed before completing the job. - setIntentRedelivery(true); - } - - /** - * Entry point for {@link EmailBroadcastReceiver}. - */ - public static void processBroadcastIntent(Context context, Intent broadcastIntent) { - Intent i = new Intent(context, EmailBroadcastProcessorService.class); - i.setAction(ACTION_BROADCAST); - i.putExtra(Intent.EXTRA_INTENT, broadcastIntent); - context.startService(i); - } - - public static void processUpgradeBroadcastIntent(final Context context) { - final Intent i = new Intent(context, EmailBroadcastProcessorService.class); - i.setAction(ACTION_UPGRADE_BROADCAST); - context.startService(i); - } - - /** - * Entry point for {@link com.android.email.SecurityPolicy.PolicyAdmin}. These will - * simply callback to {@link - * com.android.email.SecurityPolicy#onDeviceAdminReceiverMessage(Context, int)}. - */ - public static void processDevicePolicyMessage(Context context, int message) { - Intent i = new Intent(context, EmailBroadcastProcessorService.class); - i.setAction(ACTION_DEVICE_POLICY_ADMIN); - i.putExtra(EXTRA_DEVICE_POLICY_ADMIN, message); - context.startService(i); - } - - @Override - protected void onHandleIntent(Intent intent) { - // This method is called on a worker thread. - - // Dispatch from entry point - final String action = intent.getAction(); - if (ACTION_BROADCAST.equals(action)) { - final Intent broadcastIntent = intent.getParcelableExtra(Intent.EXTRA_INTENT); - final String broadcastAction = broadcastIntent.getAction(); - - if (Intent.ACTION_BOOT_COMPLETED.equals(broadcastAction)) { - onBootCompleted(); - } else if (AccountManager.LOGIN_ACCOUNTS_CHANGED_ACTION.equals(broadcastAction)) { - onSystemAccountChanged(); - } else if (Intent.ACTION_LOCALE_CHANGED.equals(broadcastAction) || - UIProvider.ACTION_UPDATE_NOTIFICATION.equals((broadcastAction))) { - broadcastIntent.setClass(this, EmailIntentService.class); - startService(broadcastIntent); - } - } else if (ACTION_DEVICE_POLICY_ADMIN.equals(action)) { - int message = intent.getIntExtra(EXTRA_DEVICE_POLICY_ADMIN, -1); - SecurityPolicy.onDeviceAdminReceiverMessage(this, message); - } else if (ACTION_UPGRADE_BROADCAST.equals(action)) { - onAppUpgrade(); - } - } - - private void disableComponent(final Class klass) { - getPackageManager().setComponentEnabledSetting(new ComponentName(this, klass), - PackageManager.COMPONENT_ENABLED_STATE_DISABLED, PackageManager.DONT_KILL_APP); - } - - private boolean isComponentDisabled(final Class klass) { - return getPackageManager().getComponentEnabledSetting(new ComponentName(this, klass)) - == PackageManager.COMPONENT_ENABLED_STATE_DISABLED; - } - - private void updateAccountManagerAccountsOfType(final String amAccountType, - final Map protocolMap) { - final android.accounts.Account[] amAccounts = - AccountManager.get(this).getAccountsByType(amAccountType); - - for (android.accounts.Account amAccount: amAccounts) { - EmailServiceUtils.updateAccountManagerType(this, amAccount, protocolMap); - } - } - - /** - * Delete all periodic syncs for an account. - * @param amAccount The account for which to disable syncs. - * @param authority The authority for which to disable syncs. - */ - private static void removePeriodicSyncs(final android.accounts.Account amAccount, - final String authority) { - final List syncs = - ContentResolver.getPeriodicSyncs(amAccount, authority); - for (final PeriodicSync sync : syncs) { - ContentResolver.removePeriodicSync(amAccount, authority, sync.extras); - } - } - - /** - * Remove all existing periodic syncs for an account type, and add the necessary syncs. - * @param amAccountType The account type to handle. - * @param syncIntervals The map of all account addresses to sync intervals in the DB. - */ - private void fixPeriodicSyncs(final String amAccountType, - final Map syncIntervals) { - final android.accounts.Account[] amAccounts = - AccountManager.get(this).getAccountsByType(amAccountType); - for (android.accounts.Account amAccount : amAccounts) { - // First delete existing periodic syncs. - removePeriodicSyncs(amAccount, EmailContent.AUTHORITY); - removePeriodicSyncs(amAccount, CalendarContract.AUTHORITY); - removePeriodicSyncs(amAccount, ContactsContract.AUTHORITY); - - // Add back a sync for this account if necessary (i.e. the account has a positive - // sync interval in the DB). This assumes that the email app requires unique email - // addresses for each account, which is currently the case. - final Integer syncInterval = syncIntervals.get(amAccount.name); - if (syncInterval != null && syncInterval > 0) { - // Sync interval is stored in minutes in DB, but we want the value in seconds. - ContentResolver.addPeriodicSync(amAccount, EmailContent.AUTHORITY, Bundle.EMPTY, - syncInterval * DateUtils.MINUTE_IN_MILLIS / DateUtils.SECOND_IN_MILLIS); - } - } - } - - /** Projection used for getting sync intervals for all accounts. */ - private static final String[] ACCOUNT_SYNC_INTERVAL_PROJECTION = - { AccountColumns.EMAIL_ADDRESS, AccountColumns.SYNC_INTERVAL }; - private static final int ACCOUNT_SYNC_INTERVAL_ADDRESS_COLUMN = 0; - private static final int ACCOUNT_SYNC_INTERVAL_INTERVAL_COLUMN = 1; - - /** - * Get the sync interval for all accounts, as stored in the DB. - * @return The map of all sync intervals by account email address. - */ - private Map getSyncIntervals() { - final Cursor c = getContentResolver().query(Account.CONTENT_URI, - ACCOUNT_SYNC_INTERVAL_PROJECTION, null, null, null); - if (c != null) { - final Map periodicSyncs = - Maps.newHashMapWithExpectedSize(c.getCount()); - try { - while (c.moveToNext()) { - periodicSyncs.put(c.getString(ACCOUNT_SYNC_INTERVAL_ADDRESS_COLUMN), - c.getInt(ACCOUNT_SYNC_INTERVAL_INTERVAL_COLUMN)); - } - } finally { - c.close(); - } - return periodicSyncs; - } - return Collections.emptyMap(); - } - - @VisibleForTesting - protected static void removeNoopUpgrades(final Map protocolMap) { - final Set keySet = new HashSet(protocolMap.keySet()); - for (final String key : keySet) { - if (TextUtils.equals(key, protocolMap.get(key))) { - protocolMap.remove(key); - } - } - } - - private void onAppUpgrade() { - if (isComponentDisabled(EmailUpgradeBroadcastReceiver.class)) { - return; - } - // When upgrading to a version that changes the protocol strings, we need to essentially - // rename the account manager type for all existing accounts, so we add new ones and delete - // the old. - // We specify the translations in this map. We map from old protocol name to new protocol - // name, and from protocol name + "_type" to new account manager type name. (Email1 did - // not use distinct account manager types for POP and IMAP, but Email2 does, hence this - // weird mapping.) - final Map protocolMap = Maps.newHashMapWithExpectedSize(4); - protocolMap.put("imap", getString(R.string.protocol_legacy_imap)); - protocolMap.put("pop3", getString(R.string.protocol_pop3)); - removeNoopUpgrades(protocolMap); - if (!protocolMap.isEmpty()) { - protocolMap.put("imap_type", getString(R.string.account_manager_type_legacy_imap)); - protocolMap.put("pop3_type", getString(R.string.account_manager_type_pop3)); - updateAccountManagerAccountsOfType("com.android.email", protocolMap); - } - - protocolMap.clear(); - protocolMap.put("eas", getString(R.string.protocol_eas)); - removeNoopUpgrades(protocolMap); - if (!protocolMap.isEmpty()) { - protocolMap.put("eas_type", getString(R.string.account_manager_type_exchange)); - updateAccountManagerAccountsOfType("com.android.exchange", protocolMap); - } - - // Disable the old authenticators. - disableComponent(LegacyEmailAuthenticatorService.class); - disableComponent(LegacyEasAuthenticatorService.class); - - // Fix periodic syncs. - final Map syncIntervals = getSyncIntervals(); - for (final EmailServiceUtils.EmailServiceInfo service - : EmailServiceUtils.getServiceInfoList(this)) { - fixPeriodicSyncs(service.accountType, syncIntervals); - } - - // Disable the upgrade broadcast receiver now that we're fully upgraded. - disableComponent(EmailUpgradeBroadcastReceiver.class); - } - - /** - * Handles {@link Intent#ACTION_BOOT_COMPLETED}. Called on a worker thread. - */ - private void onBootCompleted() { - performOneTimeInitialization(); - reconcileAndStartServices(); - } - - private void reconcileAndStartServices() { - /** - * We can get here before the ACTION_UPGRADE_BROADCAST is received, so make sure the - * accounts are converted otherwise terrible, horrible things will happen. - */ - onAppUpgrade(); - // Reconcile accounts - AccountReconciler.reconcileAccounts(this); - // Starts remote services, if any - EmailServiceUtils.startRemoteServices(this); - } - - private void performOneTimeInitialization() { - final Preferences pref = Preferences.getPreferences(this); - int progress = pref.getOneTimeInitializationProgress(); - final int initialProgress = progress; - - if (progress < 1) { - LogUtils.i(Logging.LOG_TAG, "Onetime initialization: 1"); - progress = 1; - EmailServiceUtils.enableExchangeComponent(this); - } - - if (progress < 2) { - LogUtils.i(Logging.LOG_TAG, "Onetime initialization: 2"); - progress = 2; - setImapDeletePolicy(this); - } - - // Add your initialization steps here. - // Use "progress" to skip the initializations that's already done before. - // Using this preference also makes it safe when a user skips an upgrade. (i.e. upgrading - // version N to version N+2) - - if (progress != initialProgress) { - pref.setOneTimeInitializationProgress(progress); - LogUtils.i(Logging.LOG_TAG, "Onetime initialization: completed."); - } - } - - /** - * Sets the delete policy to the correct value for all IMAP accounts. This will have no - * effect on either EAS or POP3 accounts. - */ - /*package*/ static void setImapDeletePolicy(Context context) { - ContentResolver resolver = context.getContentResolver(); - Cursor c = resolver.query(Account.CONTENT_URI, Account.CONTENT_PROJECTION, - null, null, null); - try { - while (c.moveToNext()) { - long recvAuthKey = c.getLong(Account.CONTENT_HOST_AUTH_KEY_RECV_COLUMN); - HostAuth recvAuth = HostAuth.restoreHostAuthWithId(context, recvAuthKey); - String legacyImapProtocol = context.getString(R.string.protocol_legacy_imap); - if (legacyImapProtocol.equals(recvAuth.mProtocol)) { - int flags = c.getInt(Account.CONTENT_FLAGS_COLUMN); - flags &= ~Account.FLAGS_DELETE_POLICY_MASK; - flags |= Account.DELETE_POLICY_ON_DELETE << Account.FLAGS_DELETE_POLICY_SHIFT; - ContentValues cv = new ContentValues(); - cv.put(AccountColumns.FLAGS, flags); - long accountId = c.getLong(Account.CONTENT_ID_COLUMN); - Uri uri = ContentUris.withAppendedId(Account.CONTENT_URI, accountId); - resolver.update(uri, cv, null, null); - } - } - } finally { - c.close(); - } - } - - private void onSystemAccountChanged() { - LogUtils.i(Logging.LOG_TAG, "System accounts updated."); - reconcileAndStartServices(); - } -} diff --git a/src/com/android/email/service/EmailBroadcastReceiver.java b/src/com/android/email/service/EmailBroadcastReceiver.java deleted file mode 100644 index ce7221043..000000000 --- a/src/com/android/email/service/EmailBroadcastReceiver.java +++ /dev/null @@ -1,31 +0,0 @@ -/* - * 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.content.BroadcastReceiver; -import android.content.Context; -import android.content.Intent; - -/** - * The broadcast receiver. The actual job is done in EmailBroadcastProcessor on a worker thread. - */ -public class EmailBroadcastReceiver extends BroadcastReceiver { - @Override - public void onReceive(Context context, Intent intent) { - EmailBroadcastProcessorService.processBroadcastIntent(context, intent); - } -} diff --git a/src/com/android/email/service/EmailServiceStub.java b/src/com/android/email/service/EmailServiceStub.java deleted file mode 100644 index 595d524de..000000000 --- a/src/com/android/email/service/EmailServiceStub.java +++ /dev/null @@ -1,523 +0,0 @@ -/* 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 com.android.email.DebugUtils; -import com.android.email.NotificationController; -import com.android.email.mail.Sender; -import com.android.email.mail.Store; -import com.android.email.service.EmailServiceUtils.EmailServiceInfo; -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.Mailbox; -import com.android.emailcommon.service.EmailServiceStatus; -import com.android.emailcommon.service.EmailServiceVersion; -import com.android.emailcommon.service.HostAuthCompat; -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 com.android.mail.utils.LogUtils; - -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; - - /** Small projection for just the columns required for a sync. */ - private static final String[] MAILBOX_PROJECTION = { - MailboxColumns._ID, - MailboxColumns.SERVER_ID, - MailboxColumns.TYPE, - }; - - protected Context mContext; - - protected void init(Context context) { - mContext = context; - } - - @Override - public Bundle validate(HostAuthCompat hostAuthCom) throws RemoteException { - // TODO Auto-generated method stub - return null; - } - - protected void requestSync(long mailboxId, boolean userRequest, int deltaMessageCount) { - final Mailbox mailbox = Mailbox.restoreMailboxWithId(mContext, mailboxId); - if (mailbox == null) return; - final Account account = Account.restoreAccountWithId(mContext, mailbox.mAccountKey); - if (account == null) return; - final EmailServiceInfo info = - EmailServiceUtils.getServiceInfoForAccount(mContext, account.mId); - final android.accounts.Account acct = new android.accounts.Account(account.mEmailAddress, - info.accountType); - final Bundle extras = Mailbox.createSyncBundle(mailboxId); - if (userRequest) { - extras.putBoolean(ContentResolver.SYNC_EXTRAS_MANUAL, true); - extras.putBoolean(ContentResolver.SYNC_EXTRAS_DO_NOT_RETRY, true); - extras.putBoolean(ContentResolver.SYNC_EXTRAS_EXPEDITED, true); - } - if (deltaMessageCount != 0) { - extras.putInt(Mailbox.SYNC_EXTRA_DELTA_MESSAGE_COUNT, deltaMessageCount); - } - ContentResolver.requestSync(acct, EmailContent.AUTHORITY, extras); - LogUtils.i(Logging.LOG_TAG, "requestSync EmailServiceStub startSync %s, %s", - account.toString(), extras.toString()); - } - - @Override - public void loadAttachment(final IEmailServiceCallback cb, final long accountId, - final long attachmentId, final boolean background) throws RemoteException { - Folder remoteFolder = null; - try { - //1. Check if the attachment is already here and return early in that case - Attachment attachment = - Attachment.restoreAttachmentWithId(mContext, attachmentId); - if (attachment == null) { - cb.loadAttachmentStatus(0, attachmentId, - EmailServiceStatus.ATTACHMENT_NOT_FOUND, 0); - return; - } - final long messageId = attachment.mMessageKey; - - final EmailContent.Message message = - EmailContent.Message.restoreMessageWithId(mContext, attachment.mMessageKey); - if (message == null) { - cb.loadAttachmentStatus(messageId, attachmentId, - EmailServiceStatus.MESSAGE_NOT_FOUND, 0); - return; - } - - // If the message is loaded, just report that we're finished - if (Utility.attachmentExists(mContext, attachment) - && attachment.mUiState == UIProvider.AttachmentState.SAVED) { - cb.loadAttachmentStatus(messageId, attachmentId, EmailServiceStatus.SUCCESS, - 0); - return; - } - - // Say we're starting... - cb.loadAttachmentStatus(messageId, attachmentId, EmailServiceStatus.IN_PROGRESS, 0); - - // 2. Open the remote folder. - final Account account = Account.restoreAccountWithId(mContext, message.mAccountKey); - Mailbox mailbox = Mailbox.restoreMailboxWithId(mContext, message.mMailboxKey); - if (mailbox == null) { - // This could be null if the account is deleted at just the wrong time. - return; - } - 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; - } - } - } else if (mailbox.mType == Mailbox.TYPE_SEARCH && message.mMainMailboxKey != 0) { - mailbox = Mailbox.restoreMailboxWithId(mContext, message.mMainMailboxKey); - } - - if (account == null || mailbox == null) { - // If the account/mailbox are gone, just report success; the UI handles this - cb.loadAttachmentStatus(messageId, attachmentId, - EmailServiceStatus.SUCCESS, 0); - return; - } - TrafficStats.setThreadStatsTag( - TrafficFlags.getAttachmentFlags(mContext, account)); - - final Store remoteStore = Store.getInstance(account, mContext); - 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. - final Message storeMessage = remoteFolder.createMessage(message.mServerId); - final 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"); - - final 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 - final FetchProfile fp = new FetchProfile(); - fp.add(storePart); - remoteFolder.fetch(new Message[] { storeMessage }, fp, - new MessageRetrievalListenerBridge(messageId, attachmentId, cb)); - - // If we failed to load the attachment, throw an Exception here, so that - // AttachmentService 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 - cb.loadAttachmentStatus(messageId, attachmentId, EmailServiceStatus.SUCCESS, 0); - - } catch (MessagingException me) { - LogUtils.i(Logging.LOG_TAG, me, "Error loading attachment"); - - final ContentValues cv = new ContentValues(1); - cv.put(AttachmentColumns.UI_STATE, UIProvider.AttachmentState.FAILED); - final Uri uri = ContentUris.withAppendedId(Attachment.CONTENT_URI, attachmentId); - mContext.getContentResolver().update(uri, cv, null, null); - - cb.loadAttachmentStatus(0, attachmentId, EmailServiceStatus.CONNECTION_ERROR, 0); - } finally { - if (remoteFolder != null) { - remoteFolder.close(false); - } - } - - } - - /** - * Bridge to intercept {@link MessageRetrievalListener#loadAttachmentProgress} and - * pass down to {@link IEmailServiceCallback}. - */ - public class MessageRetrievalListenerBridge implements MessageRetrievalListener { - private final long mMessageId; - private final long mAttachmentId; - private final IEmailServiceCallback mCallback; - - - public MessageRetrievalListenerBridge(final long messageId, final long attachmentId, - final IEmailServiceCallback callback) { - mMessageId = messageId; - mAttachmentId = attachmentId; - mCallback = callback; - } - - @Override - public void loadAttachmentProgress(int progress) { - try { - mCallback.loadAttachmentStatus(mMessageId, mAttachmentId, - EmailServiceStatus.IN_PROGRESS, progress); - } catch (final RemoteException e) { - // No danger if the client is no longer around - } - } - - @Override - public void messageRetrieved(com.android.emailcommon.mail.Message message) { - } - } - - @Override - public void updateFolderList(final long accountId) throws RemoteException { - final Account account = Account.restoreAccountWithId(mContext, accountId); - if (account == null) { - LogUtils.e(LogUtils.TAG, "Account %d not found in updateFolderList", accountId); - return; - }; - long inboxId = -1; - TrafficStats.setThreadStatsTag(TrafficFlags.getSyncFlags(mContext, account)); - Cursor localFolderCursor = null; - Store store = null; - try { - // Step 0: Make sure the default system mailboxes exist. - for (final int type : Mailbox.REQUIRED_FOLDER_TYPES) { - if (Mailbox.findMailboxOfType(mContext, accountId, type) == Mailbox.NO_MAILBOX) { - final Mailbox mailbox = Mailbox.newSystemMailbox(mContext, accountId, type); - mailbox.save(mContext); - if (type == Mailbox.TYPE_INBOX) { - inboxId = mailbox.mId; - } - } - } - - // Step 1: Get remote mailboxes - store = Store.getInstance(account, mContext); - final Folder[] remoteFolders = store.updateFolders(); - final HashSet remoteFolderNames = new HashSet(); - for (final Folder remoteFolder : remoteFolders) { - remoteFolderNames.add(remoteFolder.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()) { - final 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; - } - - final int mailboxType = localFolderCursor.getInt(MAILBOX_COLUMN_TYPE); - final 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 me) { - LogUtils.i(Logging.LOG_TAG, me, "Error in updateFolderList"); - // We'll hope this is temporary - // TODO: Figure out what type of messaging exception it was and return an appropriate - // result. If we start doing this from sync, it's important to let the sync manager - // know if the failure was due to IO error or authentication errors. - } finally { - if (localFolderCursor != null) { - localFolderCursor.close(); - } - if (store != null) { - store.closeConnections(); - } - // If we just created the inbox, sync it - if (inboxId != -1) { - requestSync(inboxId, true, 0); - } - } - } - - @Override - public void setLogging(final int flags) throws RemoteException { - // Not required - } - - @Override - public Bundle autoDiscover(final String userName, final String password) - throws RemoteException { - // Not required - return null; - } - - @Override - public void sendMeetingResponse(final long messageId, final int response) - throws RemoteException { - // Not required - } - - @Override - public void deleteExternalAccountPIMData(final String emailAddress) throws RemoteException { - // No need to do anything here, for IMAP and POP accounts none of our data is external. - } - - @Override - public int searchMessages(final long accountId, final SearchParams params, - final long destMailboxId) - throws RemoteException { - // Not required - return EmailServiceStatus.SUCCESS; - } - - @Override - public void pushModify(final long accountId) throws RemoteException { - LogUtils.e(Logging.LOG_TAG, "pushModify invalid for account type for %d", accountId); - } - - @Override - public int sync(final long accountId, final Bundle syncExtras) { - return EmailServiceStatus.SUCCESS; - - } - - @Override - public void sendMail(final long accountId) throws RemoteException { - sendMailImpl(mContext, accountId); - } - - public static void sendMailImpl(final Context context, final long accountId) { - final Account account = Account.restoreAccountWithId(context, accountId); - if (account == null) { - LogUtils.e(LogUtils.TAG, "account %d not found in sendMailImpl", accountId); - return; - } - TrafficStats.setThreadStatsTag(TrafficFlags.getSmtpFlags(context, account)); - final NotificationController nc = NotificationController.getInstance(context); - // 1. Loop through all messages in the account's outbox - final long outboxId = Mailbox.findMailboxOfType(context, account.mId, Mailbox.TYPE_OUTBOX); - if (outboxId == Mailbox.NO_MAILBOX) { - return; - } - final ContentResolver resolver = context.getContentResolver(); - final Cursor c = resolver.query(EmailContent.Message.CONTENT_URI, - EmailContent.Message.ID_COLUMN_PROJECTION, - MessageColumns.MAILBOX_KEY + "=?", new String[] { Long.toString(outboxId)}, - null); - try { - // 2. exit early - if (c.getCount() <= 0) { - return; - } - final Sender sender = Sender.getInstance(context, account); - final Store remoteStore = Store.getInstance(account, context); - final ContentValues moveToSentValues; - if (remoteStore.requireCopyMessageToSentFolder()) { - Mailbox sentFolder = - Mailbox.restoreMailboxOfType(context, accountId, Mailbox.TYPE_SENT); - moveToSentValues = new ContentValues(); - moveToSentValues.put(MessageColumns.MAILBOX_KEY, sentFolder.mId); - } else { - moveToSentValues = null; - } - - // 3. loop through the available messages and send them - while (c.moveToNext()) { - final long messageId; - if (moveToSentValues != null) { - moveToSentValues.remove(EmailContent.MessageColumns.FLAGS); - } - try { - messageId = c.getLong(0); - // Don't send messages with unloaded attachments - if (Utility.hasUnloadedAttachments(context, messageId)) { - if (DebugUtils.DEBUG) { - LogUtils.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.showLoginFailedNotificationSynchronous(account.mId, - false /* incoming */); - } - continue; - } - // 4. move to sent, or delete - final Uri syncedUri = - ContentUris.withAppendedId(EmailContent.Message.SYNCED_CONTENT_URI, messageId); - // Delete all cached files - AttachmentUtilities.deleteAllCachedAttachmentFiles(context, account.mId, messageId); - if (moveToSentValues != null) { - // If this is a forwarded message and it has attachments, delete them, as they - // duplicate information found elsewhere (on the server). This saves storage. - final EmailContent.Message msg = - EmailContent.Message.restoreMessageWithId(context, messageId); - if ((msg.mFlags & EmailContent.Message.FLAG_TYPE_FORWARD) != 0) { - AttachmentUtilities.deleteAllAttachmentFiles(context, account.mId, - messageId); - } - final int flags = msg.mFlags & ~(EmailContent.Message.FLAG_TYPE_REPLY | - EmailContent.Message.FLAG_TYPE_FORWARD | - EmailContent.Message.FLAG_TYPE_REPLY_ALL | - EmailContent.Message.FLAG_TYPE_ORIGINAL); - - moveToSentValues.put(EmailContent.MessageColumns.FLAGS, flags); - resolver.update(syncedUri, moveToSentValues, null, null); - } else { - AttachmentUtilities.deleteAllAttachmentFiles(context, account.mId, - messageId); - final 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.showLoginFailedNotificationSynchronous(account.mId, false /* incoming */); - } - } finally { - c.close(); - } - } - - public int getApiVersion() { - return EmailServiceVersion.CURRENT; - } -} diff --git a/src/com/android/email/service/EmailServiceUtils.java b/src/com/android/email/service/EmailServiceUtils.java deleted file mode 100644 index 3532689c4..000000000 --- a/src/com/android/email/service/EmailServiceUtils.java +++ /dev/null @@ -1,780 +0,0 @@ -/* - * 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.accounts.AccountManager; -import android.accounts.AccountManagerCallback; -import android.accounts.AccountManagerFuture; -import android.accounts.AuthenticatorException; -import android.accounts.OperationCanceledException; -import android.app.Service; -import android.content.ComponentName; -import android.content.ContentProviderClient; -import android.content.ContentResolver; -import android.content.ContentUris; -import android.content.ContentValues; -import android.content.Context; -import android.content.Intent; -import android.content.pm.ActivityInfo; -import android.content.pm.PackageManager; -import android.content.res.Configuration; -import android.content.res.Resources; -import android.content.res.TypedArray; -import android.content.res.XmlResourceParser; -import android.database.Cursor; -import android.net.Uri; -import android.os.Bundle; -import android.os.IBinder; -import android.os.RemoteException; -import android.provider.CalendarContract; -import android.provider.CalendarContract.Calendars; -import android.provider.CalendarContract.SyncState; -import android.provider.ContactsContract; -import android.provider.ContactsContract.RawContacts; -import android.provider.SyncStateContract; -import android.support.annotation.Nullable; -import android.text.TextUtils; - -import com.android.email.R; -import com.android.emailcommon.VendorPolicyLoader; -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.HostAuthColumns; -import com.android.emailcommon.provider.HostAuth; -import com.android.emailcommon.service.EmailServiceProxy; -import com.android.emailcommon.service.EmailServiceStatus; -import com.android.emailcommon.service.EmailServiceVersion; -import com.android.emailcommon.service.HostAuthCompat; -import com.android.emailcommon.service.IEmailService; -import com.android.emailcommon.service.IEmailServiceCallback; -import com.android.emailcommon.service.SearchParams; -import com.android.emailcommon.service.ServiceProxy; -import com.android.emailcommon.service.SyncWindow; -import com.android.mail.utils.LogUtils; -import com.google.common.collect.ImmutableMap; - -import org.xmlpull.v1.XmlPullParserException; - -import java.io.IOException; -import java.util.Collection; -import java.util.Map; - -/** - * Utility functions for EmailService support. - */ -public class EmailServiceUtils { - /** - * Ask a service to kill its process. This is used when an account is deleted so that - * no background thread that happens to be running will continue, possibly hitting an - * NPE or other error when trying to operate on an account that no longer exists. - * TODO: This is kind of a hack, it's only needed because we fail so badly if an account - * is deleted out from under us while a sync or other operation is in progress. It would - * be a lot cleaner if our background services could handle this without crashing. - */ - public static void killService(Context context, String protocol) { - EmailServiceInfo info = getServiceInfo(context, protocol); - if (info != null && info.intentAction != null) { - final Intent serviceIntent = getServiceIntent(info); - serviceIntent.putExtra(ServiceProxy.EXTRA_FORCE_SHUTDOWN, true); - context.startService(serviceIntent); - } - } - - /** - * Starts an EmailService by protocol - */ - public static void startService(Context context, String protocol) { - EmailServiceInfo info = getServiceInfo(context, protocol); - if (info != null && info.intentAction != null) { - final Intent serviceIntent = getServiceIntent(info); - context.startService(serviceIntent); - } - } - - /** - * Starts all remote services - */ - public static void startRemoteServices(Context context) { - for (EmailServiceInfo info: getServiceInfoList(context)) { - if (info.intentAction != null) { - final Intent serviceIntent = getServiceIntent(info); - context.startService(serviceIntent); - } - } - } - - /** - * Returns whether or not remote services are present on device - */ - public static boolean areRemoteServicesInstalled(Context context) { - for (EmailServiceInfo info: getServiceInfoList(context)) { - if (info.intentAction != null) { - return true; - } - } - return false; - } - - /** - * Starts all remote services - */ - public static void setRemoteServicesLogging(Context context, int debugBits) { - for (EmailServiceInfo info: getServiceInfoList(context)) { - if (info.intentAction != null) { - EmailServiceProxy service = - EmailServiceUtils.getService(context, info.protocol); - if (service != null) { - try { - service.setLogging(debugBits); - } catch (RemoteException e) { - // Move along, nothing to see - } - } - } - } - } - - /** - * Determine if the EmailService is available - */ - public static boolean isServiceAvailable(Context context, String protocol) { - EmailServiceInfo info = getServiceInfo(context, protocol); - if (info == null) return false; - if (info.klass != null) return true; - final Intent serviceIntent = getServiceIntent(info); - return new EmailServiceProxy(context, serviceIntent).test(); - } - - private static Intent getServiceIntent(EmailServiceInfo info) { - final Intent serviceIntent = new Intent(info.intentAction); - serviceIntent.setPackage(info.intentPackage); - return serviceIntent; - } - - /** - * For a given account id, return a service proxy if applicable, or null. - * - * @param accountId the message of interest - * @return service proxy, or null if n/a - */ - public static EmailServiceProxy getServiceForAccount(Context context, long accountId) { - return getService(context, Account.getProtocol(context, accountId)); - } - - /** - * Holder of service information (currently just name and class/intent); if there is a class - * member, this is a (local, i.e. same process) service; otherwise, this is a remote service - */ - public static class EmailServiceInfo { - public String protocol; - public String name; - public String accountType; - Class klass; - String intentAction; - String intentPackage; - public int port; - public int portSsl; - public boolean defaultSsl; - public boolean offerTls; - public boolean offerCerts; - public boolean offerOAuth; - public boolean usesSmtp; - public boolean offerLocalDeletes; - public int defaultLocalDeletes; - public boolean offerPrefix; - public boolean usesAutodiscover; - public boolean offerLookback; - public int defaultLookback; - public boolean syncChanges; - public boolean syncContacts; - public boolean syncCalendar; - public boolean offerAttachmentPreload; - public CharSequence[] syncIntervalStrings; - public CharSequence[] syncIntervals; - public int defaultSyncInterval; - public String inferPrefix; - public boolean offerLoadMore; - public boolean offerMoveTo; - public boolean requiresSetup; - public boolean hide; - public boolean isGmailStub; - - @Override - public String toString() { - StringBuilder sb = new StringBuilder("Protocol: "); - sb.append(protocol); - sb.append(", "); - sb.append(klass != null ? "Local" : "Remote"); - sb.append(" , Account Type: "); - sb.append(accountType); - return sb.toString(); - } - } - - public static EmailServiceProxy getService(Context context, String protocol) { - EmailServiceInfo info = null; - // Handle the degenerate case here (account might have been deleted) - if (protocol != null) { - info = getServiceInfo(context, protocol); - } - if (info == null) { - LogUtils.w(LogUtils.TAG, "Returning NullService for %s", protocol); - return new EmailServiceProxy(context, NullService.class); - } else { - return getServiceFromInfo(context, info); - } - } - - public static EmailServiceProxy getServiceFromInfo(Context context, EmailServiceInfo info) { - if (info.klass != null) { - return new EmailServiceProxy(context, info.klass); - } else { - final Intent serviceIntent = getServiceIntent(info); - return new EmailServiceProxy(context, serviceIntent); - } - } - - public static EmailServiceInfo getServiceInfoForAccount(Context context, long accountId) { - String protocol = Account.getProtocol(context, accountId); - return getServiceInfo(context, protocol); - } - - public static EmailServiceInfo getServiceInfo(Context context, String protocol) { - return getServiceMap(context).get(protocol); - } - - public static Collection getServiceInfoList(Context context) { - return getServiceMap(context).values(); - } - - private static void finishAccountManagerBlocker(AccountManagerFuture future) { - try { - // Note: All of the potential errors are simply logged - // here, as there is nothing to actually do about them. - future.getResult(); - } catch (OperationCanceledException e) { - LogUtils.w(LogUtils.TAG, e, "finishAccountManagerBlocker"); - } catch (AuthenticatorException e) { - LogUtils.w(LogUtils.TAG, e, "finishAccountManagerBlocker"); - } catch (IOException e) { - LogUtils.w(LogUtils.TAG, e, "finishAccountManagerBlocker"); - } - } - - /** - * Add an account to the AccountManager. - * @param context Our {@link Context}. - * @param account The {@link Account} we're adding. - * @param email Whether the user wants to sync email on this account. - * @param calendar Whether the user wants to sync calendar on this account. - * @param contacts Whether the user wants to sync contacts on this account. - * @param callback A callback for when the AccountManager is done. - * @return The result of {@link AccountManager#addAccount}. - */ - public static AccountManagerFuture setupAccountManagerAccount(final Context context, - final Account account, final boolean email, final boolean calendar, - final boolean contacts, final AccountManagerCallback callback) { - final HostAuth hostAuthRecv = - HostAuth.restoreHostAuthWithId(context, account.mHostAuthKeyRecv); - return setupAccountManagerAccount(context, account, email, calendar, contacts, - hostAuthRecv, callback); - } - - /** - * Add an account to the AccountManager. - * @param context Our {@link Context}. - * @param account The {@link Account} we're adding. - * @param email Whether the user wants to sync email on this account. - * @param calendar Whether the user wants to sync calendar on this account. - * @param contacts Whether the user wants to sync contacts on this account. - * @param hostAuth HostAuth that identifies the protocol and password for this account. - * @param callback A callback for when the AccountManager is done. - * @return The result of {@link AccountManager#addAccount}. - */ - public static AccountManagerFuture setupAccountManagerAccount(final Context context, - final Account account, final boolean email, final boolean calendar, - final boolean contacts, final HostAuth hostAuth, - final AccountManagerCallback callback) { - if (hostAuth == null) { - return null; - } - // Set up username/password - final Bundle options = new Bundle(5); - options.putString(EasAuthenticatorService.OPTIONS_USERNAME, account.mEmailAddress); - options.putString(EasAuthenticatorService.OPTIONS_PASSWORD, hostAuth.mPassword); - options.putBoolean(EasAuthenticatorService.OPTIONS_CONTACTS_SYNC_ENABLED, contacts); - options.putBoolean(EasAuthenticatorService.OPTIONS_CALENDAR_SYNC_ENABLED, calendar); - options.putBoolean(EasAuthenticatorService.OPTIONS_EMAIL_SYNC_ENABLED, email); - final EmailServiceInfo info = getServiceInfo(context, hostAuth.mProtocol); - return AccountManager.get(context).addAccount(info.accountType, null, null, options, null, - callback, null); - } - - public static void updateAccountManagerType(Context context, - android.accounts.Account amAccount, final Map protocolMap) { - final ContentResolver resolver = context.getContentResolver(); - final Cursor c = resolver.query(Account.CONTENT_URI, Account.CONTENT_PROJECTION, - AccountColumns.EMAIL_ADDRESS + "=?", new String[] { amAccount.name }, null); - // That's odd, isn't it? - if (c == null) return; - try { - if (c.moveToNext()) { - // Get the EmailProvider Account/HostAuth - final Account account = new Account(); - account.restore(c); - final HostAuth hostAuth = - HostAuth.restoreHostAuthWithId(context, account.mHostAuthKeyRecv); - if (hostAuth == null) { - return; - } - - final String newProtocol = protocolMap.get(hostAuth.mProtocol); - if (newProtocol == null) { - // This account doesn't need updating. - return; - } - - LogUtils.w(LogUtils.TAG, "Converting %s to %s", amAccount.name, newProtocol); - - final ContentValues accountValues = new ContentValues(); - int oldFlags = account.mFlags; - - // Mark the provider account incomplete so it can't get reconciled away - account.mFlags |= Account.FLAGS_INCOMPLETE; - accountValues.put(AccountColumns.FLAGS, account.mFlags); - final Uri accountUri = ContentUris.withAppendedId(Account.CONTENT_URI, account.mId); - resolver.update(accountUri, accountValues, null, null); - - // Change the HostAuth to reference the new protocol; this has to be done before - // trying to create the AccountManager account (below) - final ContentValues hostValues = new ContentValues(); - hostValues.put(HostAuthColumns.PROTOCOL, newProtocol); - resolver.update(ContentUris.withAppendedId(HostAuth.CONTENT_URI, hostAuth.mId), - hostValues, null, null); - LogUtils.w(LogUtils.TAG, "Updated HostAuths"); - - try { - // Get current settings for the existing AccountManager account - boolean email = ContentResolver.getSyncAutomatically(amAccount, - EmailContent.AUTHORITY); - if (!email) { - // Try our old provider name - email = ContentResolver.getSyncAutomatically(amAccount, - "com.android.email.provider"); - } - final boolean contacts = ContentResolver.getSyncAutomatically(amAccount, - ContactsContract.AUTHORITY); - final boolean calendar = ContentResolver.getSyncAutomatically(amAccount, - CalendarContract.AUTHORITY); - LogUtils.w(LogUtils.TAG, "Email: %s, Contacts: %s Calendar: %s", - email, contacts, calendar); - - // Get sync keys for calendar/contacts - final String amName = amAccount.name; - final String oldType = amAccount.type; - ContentProviderClient client = context.getContentResolver() - .acquireContentProviderClient(CalendarContract.CONTENT_URI); - byte[] calendarSyncKey = null; - try { - calendarSyncKey = SyncStateContract.Helpers.get(client, - asCalendarSyncAdapter(SyncState.CONTENT_URI, amName, oldType), - new android.accounts.Account(amName, oldType)); - } catch (RemoteException e) { - LogUtils.w(LogUtils.TAG, "Get calendar key FAILED"); - } finally { - client.release(); - } - client = context.getContentResolver() - .acquireContentProviderClient(ContactsContract.AUTHORITY_URI); - byte[] contactsSyncKey = null; - try { - contactsSyncKey = SyncStateContract.Helpers.get(client, - ContactsContract.SyncState.CONTENT_URI, - new android.accounts.Account(amName, oldType)); - } catch (RemoteException e) { - LogUtils.w(LogUtils.TAG, "Get contacts key FAILED"); - } finally { - client.release(); - } - if (calendarSyncKey != null) { - LogUtils.w(LogUtils.TAG, "Got calendar key: %s", - new String(calendarSyncKey)); - } - if (contactsSyncKey != null) { - LogUtils.w(LogUtils.TAG, "Got contacts key: %s", - new String(contactsSyncKey)); - } - - // Set up a new AccountManager account with new type and old settings - AccountManagerFuture amFuture = setupAccountManagerAccount(context, account, - email, calendar, contacts, null); - finishAccountManagerBlocker(amFuture); - LogUtils.w(LogUtils.TAG, "Created new AccountManager account"); - - // TODO: Clean up how we determine the type. - final String accountType = protocolMap.get(hostAuth.mProtocol + "_type"); - // Move calendar and contacts data from the old account to the new one. - // We must do this before deleting the old account or the data is lost. - moveCalendarData(context.getContentResolver(), amName, oldType, accountType); - moveContactsData(context.getContentResolver(), amName, oldType, accountType); - - // Delete the AccountManager account - amFuture = AccountManager.get(context) - .removeAccount(amAccount, null, null); - finishAccountManagerBlocker(amFuture); - LogUtils.w(LogUtils.TAG, "Deleted old AccountManager account"); - - // Restore sync keys for contacts/calendar - - if (accountType != null && - calendarSyncKey != null && calendarSyncKey.length != 0) { - client = context.getContentResolver() - .acquireContentProviderClient(CalendarContract.CONTENT_URI); - try { - SyncStateContract.Helpers.set(client, - asCalendarSyncAdapter(SyncState.CONTENT_URI, amName, - accountType), - new android.accounts.Account(amName, accountType), - calendarSyncKey); - LogUtils.w(LogUtils.TAG, "Set calendar key..."); - } catch (RemoteException e) { - LogUtils.w(LogUtils.TAG, "Set calendar key FAILED"); - } finally { - client.release(); - } - } - if (accountType != null && - contactsSyncKey != null && contactsSyncKey.length != 0) { - client = context.getContentResolver() - .acquireContentProviderClient(ContactsContract.AUTHORITY_URI); - try { - SyncStateContract.Helpers.set(client, - ContactsContract.SyncState.CONTENT_URI, - new android.accounts.Account(amName, accountType), - contactsSyncKey); - LogUtils.w(LogUtils.TAG, "Set contacts key..."); - } catch (RemoteException e) { - LogUtils.w(LogUtils.TAG, "Set contacts key FAILED"); - } - } - - // That's all folks! - LogUtils.w(LogUtils.TAG, "Account update completed."); - } finally { - // Clear the incomplete flag on the provider account - accountValues.put(AccountColumns.FLAGS, oldFlags); - resolver.update(accountUri, accountValues, null, null); - LogUtils.w(LogUtils.TAG, "[Incomplete flag cleared]"); - } - } - } finally { - c.close(); - } - } - - private static void moveCalendarData(final ContentResolver resolver, final String name, - final String oldType, final String newType) { - final Uri oldCalendars = Calendars.CONTENT_URI.buildUpon() - .appendQueryParameter(CalendarContract.CALLER_IS_SYNCADAPTER, "true") - .appendQueryParameter(Calendars.ACCOUNT_NAME, name) - .appendQueryParameter(Calendars.ACCOUNT_TYPE, oldType) - .build(); - - // Update this calendar to have the new account type. - final ContentValues values = new ContentValues(); - values.put(CalendarContract.Calendars.ACCOUNT_TYPE, newType); - resolver.update(oldCalendars, values, - Calendars.ACCOUNT_NAME + "=? AND " + Calendars.ACCOUNT_TYPE + "=?", - new String[] {name, oldType}); - } - - private static void moveContactsData(final ContentResolver resolver, final String name, - final String oldType, final String newType) { - final Uri oldContacts = RawContacts.CONTENT_URI.buildUpon() - .appendQueryParameter(ContactsContract.CALLER_IS_SYNCADAPTER, "true") - .appendQueryParameter(RawContacts.ACCOUNT_NAME, name) - .appendQueryParameter(RawContacts.ACCOUNT_TYPE, oldType) - .build(); - - // Update this calendar to have the new account type. - final ContentValues values = new ContentValues(); - values.put(CalendarContract.Calendars.ACCOUNT_TYPE, newType); - resolver.update(oldContacts, values, null, null); - } - - private static final Configuration sOldConfiguration = new Configuration(); - private static Map sServiceMap = null; - private static final Object sServiceMapLock = new Object(); - - /** - * Parse services.xml file to find our available email services - */ - private static Map getServiceMap(final Context context) { - synchronized (sServiceMapLock) { - /** - * We cache localized strings here, so make sure to regenerate the service map if - * the locale changes - */ - if (sServiceMap == null) { - sOldConfiguration.setTo(context.getResources().getConfiguration()); - } - - final int delta = - sOldConfiguration.updateFrom(context.getResources().getConfiguration()); - - if (sServiceMap != null - && !Configuration.needNewResources(delta, ActivityInfo.CONFIG_LOCALE)) { - return sServiceMap; - } - - final ImmutableMap.Builder builder = ImmutableMap.builder(); - if (!context.getResources().getBoolean(R.bool.enable_services)) { - // Return an empty map if services have been disabled because this is the Email - // Tombstone app. - sServiceMap = builder.build(); - return sServiceMap; - } - - try { - final Resources res = context.getResources(); - final XmlResourceParser xml = res.getXml(R.xml.services); - int xmlEventType; - // walk through senders.xml file. - while ((xmlEventType = xml.next()) != XmlResourceParser.END_DOCUMENT) { - if (xmlEventType == XmlResourceParser.START_TAG && - "emailservice".equals(xml.getName())) { - final EmailServiceInfo info = new EmailServiceInfo(); - final TypedArray ta = - res.obtainAttributes(xml, R.styleable.EmailServiceInfo); - info.protocol = ta.getString(R.styleable.EmailServiceInfo_protocol); - info.accountType = ta.getString(R.styleable.EmailServiceInfo_accountType); - info.name = ta.getString(R.styleable.EmailServiceInfo_name); - info.hide = ta.getBoolean(R.styleable.EmailServiceInfo_hide, false); - final String klass = - ta.getString(R.styleable.EmailServiceInfo_serviceClass); - info.intentAction = ta.getString(R.styleable.EmailServiceInfo_intent); - info.intentPackage = - ta.getString(R.styleable.EmailServiceInfo_intentPackage); - info.defaultSsl = - ta.getBoolean(R.styleable.EmailServiceInfo_defaultSsl, false); - info.port = ta.getInteger(R.styleable.EmailServiceInfo_port, 0); - info.portSsl = ta.getInteger(R.styleable.EmailServiceInfo_portSsl, 0); - info.offerTls = ta.getBoolean(R.styleable.EmailServiceInfo_offerTls, false); - info.offerCerts = - ta.getBoolean(R.styleable.EmailServiceInfo_offerCerts, false); - info.offerOAuth = - ta.getBoolean(R.styleable.EmailServiceInfo_offerOAuth, false); - info.offerLocalDeletes = - ta.getBoolean(R.styleable.EmailServiceInfo_offerLocalDeletes, false); - info.defaultLocalDeletes = - ta.getInteger(R.styleable.EmailServiceInfo_defaultLocalDeletes, - Account.DELETE_POLICY_ON_DELETE); - info.offerPrefix = - ta.getBoolean(R.styleable.EmailServiceInfo_offerPrefix, false); - info.usesSmtp = ta.getBoolean(R.styleable.EmailServiceInfo_usesSmtp, false); - info.usesAutodiscover = - ta.getBoolean(R.styleable.EmailServiceInfo_usesAutodiscover, false); - info.offerLookback = - ta.getBoolean(R.styleable.EmailServiceInfo_offerLookback, false); - info.defaultLookback = - ta.getInteger(R.styleable.EmailServiceInfo_defaultLookback, - SyncWindow.SYNC_WINDOW_3_DAYS); - info.syncChanges = - ta.getBoolean(R.styleable.EmailServiceInfo_syncChanges, false); - info.syncContacts = - ta.getBoolean(R.styleable.EmailServiceInfo_syncContacts, false); - info.syncCalendar = - ta.getBoolean(R.styleable.EmailServiceInfo_syncCalendar, false); - info.offerAttachmentPreload = - ta.getBoolean(R.styleable.EmailServiceInfo_offerAttachmentPreload, - false); - info.syncIntervalStrings = - ta.getTextArray(R.styleable.EmailServiceInfo_syncIntervalStrings); - info.syncIntervals = - ta.getTextArray(R.styleable.EmailServiceInfo_syncIntervals); - info.defaultSyncInterval = - ta.getInteger(R.styleable.EmailServiceInfo_defaultSyncInterval, 15); - info.inferPrefix = ta.getString(R.styleable.EmailServiceInfo_inferPrefix); - info.offerLoadMore = - ta.getBoolean(R.styleable.EmailServiceInfo_offerLoadMore, false); - info.offerMoveTo = - ta.getBoolean(R.styleable.EmailServiceInfo_offerMoveTo, false); - info.requiresSetup = - ta.getBoolean(R.styleable.EmailServiceInfo_requiresSetup, false); - info.isGmailStub = - ta.getBoolean(R.styleable.EmailServiceInfo_isGmailStub, false); - - // Must have either "class" (local) or "intent" (remote) - if (klass != null) { - try { - // noinspection unchecked - info.klass = (Class) Class.forName(klass); - } catch (ClassNotFoundException e) { - throw new IllegalStateException( - "Class not found in service descriptor: " + klass); - } - } - if (info.klass == null && - info.intentAction == null && - !info.isGmailStub) { - throw new IllegalStateException( - "No class or intent action specified in service descriptor"); - } - if (info.klass != null && info.intentAction != null) { - throw new IllegalStateException( - "Both class and intent action specified in service descriptor"); - } - builder.put(info.protocol, info); - } - } - } catch (XmlPullParserException e) { - // ignore - } catch (IOException e) { - // ignore - } - sServiceMap = builder.build(); - return sServiceMap; - } - } - - /** - * Resolves a service name into a protocol name, or null if ambiguous - * @param context for loading service map - * @param accountType sync adapter service name - * @return protocol name or null - */ - public static @Nullable String getProtocolFromAccountType(final Context context, - final String accountType) { - if (TextUtils.isEmpty(accountType)) { - return null; - } - final Map serviceInfoMap = getServiceMap(context); - String protocol = null; - for (final EmailServiceInfo info : serviceInfoMap.values()) { - if (TextUtils.equals(accountType, info.accountType)) { - if (!TextUtils.isEmpty(protocol) && !TextUtils.equals(protocol, info.protocol)) { - // More than one protocol matches - return null; - } - protocol = info.protocol; - } - } - return protocol; - } - - private static Uri asCalendarSyncAdapter(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(); - } - - /** - * A no-op service that can be returned for non-existent/null protocols - */ - class NullService implements IEmailService { - @Override - public IBinder asBinder() { - return null; - } - - @Override - public Bundle validate(HostAuthCompat hostauth) throws RemoteException { - return null; - } - - @Override - public void loadAttachment(final IEmailServiceCallback cb, final long accountId, - final long attachmentId, final boolean background) throws RemoteException { - } - - @Override - public void updateFolderList(long accountId) throws RemoteException {} - - @Override - public void setLogging(int flags) throws RemoteException { - } - - @Override - public Bundle autoDiscover(String userName, String password) throws RemoteException { - return null; - } - - @Override - public void sendMeetingResponse(long messageId, int response) throws RemoteException { - } - - @Override - public void deleteExternalAccountPIMData(final String emailAddress) throws RemoteException { - } - - @Override - public int searchMessages(long accountId, SearchParams params, long destMailboxId) - throws RemoteException { - return 0; - } - - @Override - public void sendMail(long accountId) throws RemoteException { - } - - @Override - public void pushModify(long accountId) throws RemoteException { - } - - @Override - public int sync(final long accountId, final Bundle syncExtras) { - return EmailServiceStatus.SUCCESS; - } - - public int getApiVersion() { - return EmailServiceVersion.CURRENT; - } - } - - public static void setComponentStatus(final Context context, Class clazz, boolean enabled) { - final ComponentName c = new ComponentName(context, clazz.getName()); - context.getPackageManager().setComponentEnabledSetting(c, - enabled ? PackageManager.COMPONENT_ENABLED_STATE_ENABLED - : PackageManager.COMPONENT_ENABLED_STATE_DISABLED, - PackageManager.DONT_KILL_APP); - } - - /** - * This is a helper function that enables the proper Exchange component and disables - * the other Exchange component ensuring that only one is enabled at a time. - */ - public static void enableExchangeComponent(final Context context) { - if (VendorPolicyLoader.getInstance(context).useAlternateExchangeStrings()) { - LogUtils.d(LogUtils.TAG, "Enabling alternate EAS authenticator"); - setComponentStatus(context, EasAuthenticatorServiceAlternate.class, true); - setComponentStatus(context, EasAuthenticatorService.class, false); - } else { - LogUtils.d(LogUtils.TAG, "Enabling EAS authenticator"); - setComponentStatus(context, EasAuthenticatorService.class, true); - setComponentStatus(context, - EasAuthenticatorServiceAlternate.class, false); - } - } - - public static void disableExchangeComponents(final Context context) { - LogUtils.d(LogUtils.TAG, "Disabling EAS authenticators"); - setComponentStatus(context, EasAuthenticatorServiceAlternate.class, false); - setComponentStatus(context, EasAuthenticatorService.class, false); - } - -} diff --git a/src/com/android/email/service/EmailUpgradeBroadcastReceiver.java b/src/com/android/email/service/EmailUpgradeBroadcastReceiver.java deleted file mode 100644 index fe48e33a6..000000000 --- a/src/com/android/email/service/EmailUpgradeBroadcastReceiver.java +++ /dev/null @@ -1,17 +0,0 @@ -package com.android.email.service; - -import android.content.BroadcastReceiver; -import android.content.Context; -import android.content.Intent; - -/** - * {@link BroadcastReceiver} for app upgrade. This listens to package replacement (for unbundled - * upgrade) and reboot (for OTA upgrade). The code in the {@link EmailBroadcastProcessorService} - * disables this receiver after it runs. - */ -public class EmailUpgradeBroadcastReceiver extends BroadcastReceiver { - @Override - public void onReceive(final Context context, final Intent intent) { - EmailBroadcastProcessorService.processUpgradeBroadcastIntent(context); - } -} diff --git a/src/com/android/email/service/ImapAuthenticatorService.java b/src/com/android/email/service/ImapAuthenticatorService.java deleted file mode 100644 index 975583da3..000000000 --- a/src/com/android/email/service/ImapAuthenticatorService.java +++ /dev/null @@ -1,23 +0,0 @@ -/* - * Copyright (C) 2010 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.email.service; - -/** - * This service needs to be declared separately from the base service - */ -public class ImapAuthenticatorService extends AuthenticatorService { -} diff --git a/src/com/android/email/service/ImapService.java b/src/com/android/email/service/ImapService.java deleted file mode 100644 index f7f2517d0..000000000 --- a/src/com/android/email/service/ImapService.java +++ /dev/null @@ -1,1615 +0,0 @@ -/* - * 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.SystemClock; -import android.text.TextUtils; -import android.text.format.DateUtils; - -import com.android.email.DebugUtils; -import com.android.email.LegacyConversions; -import com.android.email.NotificationController; -import com.android.email.R; -import com.android.email.mail.Store; -import com.android.email.provider.Utilities; -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.SearchParams; -import com.android.emailcommon.service.SyncWindow; -import com.android.emailcommon.utility.AttachmentUtilities; -import com.android.mail.providers.UIProvider; -import com.android.mail.utils.LogUtils; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Comparator; -import java.util.Date; -import java.util.HashMap; -import java.util.List; - -public class ImapService extends Service { - // TODO get these from configurations or settings. - private static final long QUICK_SYNC_WINDOW_MILLIS = DateUtils.DAY_IN_MILLIS; - private static final long FULL_SYNC_WINDOW_MILLIS = 7 * DateUtils.DAY_IN_MILLIS; - private static final long FULL_SYNC_INTERVAL_MILLIS = 4 * DateUtils.HOUR_IN_MILLIS; - - // The maximum number of messages to fetch in a single command. - private static final int MAX_MESSAGES_TO_FETCH = 500; - private static final int MINIMUM_MESSAGES_TO_SYNC = 10; - private static final int LOAD_MORE_MIN_INCREMENT = 10; - private static final int LOAD_MORE_MAX_INCREMENT = 20; - private static final long INITIAL_WINDOW_SIZE_INCREASE = 24 * 60 * 60 * 1000; - - 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 sSearchResults = - new HashMap(); - - /** - * We write this into the serverId field of messages that will never be upsynced. - */ - private static final String LOCAL_SERVERID_PREFIX = "Local-"; - - private static String sMessageDecodeErrorString; - - /** - * Used in ImapFolder for base64 errors. Cached here because ImapFolder does not have access - * to a Context object. - * @return Error string or empty string - */ - public static String getMessageDecodeErrorString() { - return sMessageDecodeErrorString == null ? "" : sMessageDecodeErrorString; - } - - @Override - public void onCreate() { - super.onCreate(); - - sMessageDecodeErrorString = getString(R.string.message_decode_error); - } - - @Override - public int onStartCommand(Intent intent, int flags, int startId) { - return Service.START_STICKY; - } - - /** - * Create our EmailService implementation here. - */ - private final EmailServiceStub mBinder = new EmailServiceStub() { - @Override - public int searchMessages(long accountId, SearchParams searchParams, long destMailboxId) { - try { - return searchMailboxImpl(getApplicationContext(), accountId, searchParams, - destMailboxId); - } catch (MessagingException e) { - // Ignore - } - return 0; - } - }; - - @Override - public IBinder onBind(Intent intent) { - mBinder.init(this); - return mBinder; - } - - /** - * 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 - * @return The status code for whether this operation succeeded. - * @throws MessagingException - */ - public static synchronized int synchronizeMailboxSynchronous(Context context, - final Account account, final Mailbox folder, final boolean loadMore, - final boolean uiRefresh) throws MessagingException { - TrafficStats.setThreadStatsTag(TrafficFlags.getSyncFlags(context, account)); - NotificationController nc = NotificationController.getInstance(context); - Store remoteStore = null; - try { - remoteStore = Store.getInstance(account, context); - processPendingActionsSynchronous(context, account, remoteStore, uiRefresh); - synchronizeMailboxGeneric(context, account, remoteStore, folder, loadMore, uiRefresh); - // Clear authentication notification for this account - nc.cancelLoginFailedNotification(account.mId); - } catch (MessagingException e) { - if (Logging.LOGD) { - LogUtils.d(Logging.LOG_TAG, "synchronizeMailboxSynchronous", e); - } - if (e instanceof AuthenticationFailedException) { - // Generate authentication notification - nc.showLoginFailedNotificationSynchronous(account.mId, true /* incoming */); - } - throw e; - } finally { - if (remoteStore != null) { - remoteStore.closeConnections(); - } - } - // TODO: Rather than use exceptions as logic above, return the status and handle it - // correctly in caller. - return EmailServiceStatus.SUCCESS; - } - - /** - * 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 = 5; - private static final int COLUMN_TIMESTAMP = 6; - private static final String[] PROJECTION = { - MessageColumns._ID, - MessageColumns.FLAG_READ, - MessageColumns.FLAG_FAVORITE, - MessageColumns.FLAG_LOADED, - SyncColumns.SERVER_ID, - MessageColumns.FLAGS, - MessageColumns.TIMESTAMP - }; - - final long mId; - final boolean mFlagRead; - final boolean mFlagFavorite; - final int mFlagLoaded; - final String mServerId; - final int mFlags; - final long mTimestamp; - - 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); - mTimestamp = c.getLong(COLUMN_TIMESTAMP); - // Note: mailbox key and account key not needed - they are projected for the SELECT - } - } - - private static class OldestTimestampInfo { - private static final int COLUMN_OLDEST_TIMESTAMP = 0; - private static final String[] PROJECTION = new String[] { - "MIN(" + MessageColumns.TIMESTAMP + ")" - }; - } - - /** - * 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 messages an array of Messages 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 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); - Message [] oneMessageArray = new Message[1]; - 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 viewables = new ArrayList(); - ArrayList attachments = new ArrayList(); - MimeUtility.collectParts(message, viewables, attachments); - // Download the viewables immediately - oneMessageArray[0] = message; - for (Part part : viewables) { - fp.clear(); - fp.add(part); - remoteFolder.fetch(oneMessageArray, 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 unsyncedMessages, - HashMap localMessageMap, final ArrayList unseenMessages) - throws MessagingException { - FetchProfile fp = new FetchProfile(); - fp.add(FetchProfile.Item.FLAGS); - fp.add(FetchProfile.Item.ENVELOPE); - - final HashMap localMapCopy; - if (localMessageMap != null) - localMapCopy = new HashMap(localMessageMap); - else { - localMapCopy = new HashMap(); - } - - remoteFolder.fetch(unsyncedMessages.toArray(new Message[unsyncedMessages.size()]), 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 - final LocalMessageInfo localMessageInfo = - localMapCopy.get(message.getUid()); - final boolean localExists = localMessageInfo != null; - - if (!localExists && message.isSet(Flag.DELETED)) { - // This is a deleted message that we don't have locally, so don't - // create it - return; - } - - final EmailContent.Message localMessage; - if (!localExists) { - 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) { - LogUtils.e(Logging.LOG_TAG, - "Error while copying downloaded message." + me); - } - } - } - catch (Exception e) { - LogUtils.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 - * @param loadMore whether we should be loading more older messages - * @param uiRefresh whether this request is in response to a user action - * @throws MessagingException - */ - private synchronized static void synchronizeMailboxGeneric(final Context context, - final Account account, Store remoteStore, final Mailbox mailbox, final boolean loadMore, - final boolean uiRefresh) - throws MessagingException { - - LogUtils.d(Logging.LOG_TAG, "synchronizeMailboxGeneric " + account + " " + mailbox + " " - + loadMore + " " + uiRefresh); - - final ArrayList unseenMessages = new ArrayList(); - - 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. Figure out what our sync window should be. - long endDate; - - // We will do a full sync if the user has actively requested a sync, or if it has been - // too long since the last full sync. - // If we have rebooted since the last full sync, then we may get a negative - // timeSinceLastFullSync. In this case, we don't know how long it's been since the last - // full sync so we should perform the full sync. - final long timeSinceLastFullSync = SystemClock.elapsedRealtime() - - mailbox.mLastFullSyncTime; - final boolean fullSync = (uiRefresh || loadMore || - timeSinceLastFullSync >= FULL_SYNC_INTERVAL_MILLIS || timeSinceLastFullSync < 0); - - if (account.mSyncLookback == SyncWindow.SYNC_WINDOW_ALL) { - // This is really for testing. There is no UI that allows setting the sync window for - // IMAP, but it can be set by sending a special intent to AccountSetupFinal activity. - endDate = 0; - } else if (fullSync) { - // Find the oldest message in the local store. We need our time window to include - // all messages that are currently present locally. - endDate = System.currentTimeMillis() - FULL_SYNC_WINDOW_MILLIS; - Cursor localOldestCursor = null; - try { - // b/11520812 Ignore message with timestamp = 0 (which includes NULL) - localOldestCursor = resolver.query(EmailContent.Message.CONTENT_URI, - OldestTimestampInfo.PROJECTION, - EmailContent.MessageColumns.ACCOUNT_KEY + "=?" + " AND " + - MessageColumns.MAILBOX_KEY + "=? AND " + - MessageColumns.TIMESTAMP + "!=0", - new String[] {String.valueOf(account.mId), String.valueOf(mailbox.mId)}, - null); - if (localOldestCursor != null && localOldestCursor.moveToFirst()) { - long oldestLocalMessageDate = localOldestCursor.getLong( - OldestTimestampInfo.COLUMN_OLDEST_TIMESTAMP); - if (oldestLocalMessageDate > 0) { - endDate = Math.min(endDate, oldestLocalMessageDate); - LogUtils.d( - Logging.LOG_TAG, "oldest local message " + oldestLocalMessageDate); - } - } - } finally { - if (localOldestCursor != null) { - localOldestCursor.close(); - } - } - LogUtils.d(Logging.LOG_TAG, "full sync: original window: now - " + endDate); - } else { - // We are doing a frequent, quick sync. This only syncs a small time window, so that - // we wil get any new messages, but not spend a lot of bandwidth downloading - // messageIds that we most likely already have. - endDate = System.currentTimeMillis() - QUICK_SYNC_WINDOW_MILLIS; - LogUtils.d(Logging.LOG_TAG, "quick sync: original window: now - " + endDate); - } - - // 2. Open the remote folder and create the remote folder if necessary - // The account might have been deleted - if (remoteStore == null) { - LogUtils.d(Logging.LOG_TAG, "account is apparently deleted"); - return; - } - final 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) { - if (!remoteFolder.exists()) { - if (!remoteFolder.create(FolderType.HOLDS_MESSAGES)) { - LogUtils.w(Logging.LOG_TAG, "could not create remote folder type %d", - mailbox.mType); - return; - } - } - } - remoteFolder.open(OpenMode.READ_WRITE); - - // 3. Trash any remote messages that are marked as trashed locally. - // TODO - this comment was here, but no code was here. - - // 4. Get the number of messages on the server. - // TODO: this value includes deleted but unpurged messages, and so slightly mismatches - // the contents of our DB since we drop deleted messages. Figure out what to do about this. - final int remoteMessageCount = remoteFolder.getMessageCount(); - - // 5. Save folder message count locally. - mailbox.updateMessageCount(context, remoteMessageCount); - - // 6. Get all message Ids in our sync window: - Message[] remoteMessages; - remoteMessages = remoteFolder.getMessages(0, endDate, null); - LogUtils.d(Logging.LOG_TAG, "received " + remoteMessages.length + " messages"); - - // 7. See if we need any additional messages beyond our date query range results. - // If we do, keep increasing the size of our query window until we have - // enough, or until we have all messages in the mailbox. - int totalCountNeeded; - if (loadMore) { - totalCountNeeded = remoteMessages.length + LOAD_MORE_MIN_INCREMENT; - } else { - totalCountNeeded = remoteMessages.length; - if (fullSync && totalCountNeeded < MINIMUM_MESSAGES_TO_SYNC) { - totalCountNeeded = MINIMUM_MESSAGES_TO_SYNC; - } - } - LogUtils.d(Logging.LOG_TAG, "need " + totalCountNeeded + " total"); - - final int additionalMessagesNeeded = totalCountNeeded - remoteMessages.length; - if (additionalMessagesNeeded > 0) { - LogUtils.d(Logging.LOG_TAG, "trying to get " + additionalMessagesNeeded + " more"); - long startDate = endDate - 1; - Message[] additionalMessages = new Message[0]; - long windowIncreaseSize = INITIAL_WINDOW_SIZE_INCREASE; - while (additionalMessages.length < additionalMessagesNeeded && endDate > 0) { - endDate = endDate - windowIncreaseSize; - if (endDate < 0) { - LogUtils.d(Logging.LOG_TAG, "window size too large, this is the last attempt"); - endDate = 0; - } - LogUtils.d(Logging.LOG_TAG, - "requesting additional messages from range " + startDate + " - " + endDate); - additionalMessages = remoteFolder.getMessages(startDate, endDate, null); - - // If don't get enough messages with the first window size expansion, - // we need to accelerate rate at which the window expands. Otherwise, - // if there were no messages for several weeks, we'd always end up - // performing dozens of queries. - windowIncreaseSize *= 2; - } - - LogUtils.d(Logging.LOG_TAG, "additionalMessages " + additionalMessages.length); - if (additionalMessages.length < additionalMessagesNeeded) { - // We have attempted to load a window that goes all the way back to time zero, - // but we still don't have as many messages as the server says are in the inbox. - // This is not expected to happen. - LogUtils.e(Logging.LOG_TAG, "expected to find " + additionalMessagesNeeded - + " more messages, only got " + additionalMessages.length); - } - int additionalToKeep = additionalMessages.length; - if (additionalMessages.length > LOAD_MORE_MAX_INCREMENT) { - // We have way more additional messages than intended, drop some of them. - // The last messages are the most recent, so those are the ones we need to keep. - additionalToKeep = LOAD_MORE_MAX_INCREMENT; - } - - // Copy the messages into one array. - Message[] allMessages = new Message[remoteMessages.length + additionalToKeep]; - System.arraycopy(remoteMessages, 0, allMessages, 0, remoteMessages.length); - // additionalMessages may have more than we need, only copy the last - // several. These are the most recent messages in that set because - // of the way IMAP server returns messages. - System.arraycopy(additionalMessages, additionalMessages.length - additionalToKeep, - allMessages, remoteMessages.length, additionalToKeep); - remoteMessages = allMessages; - } - - // 8. Get the all of the local messages within the sync window, and create - // an index of the uids. - // The IMAP query for messages ignores time, and only looks at the date part of the endDate. - // So if we query for messages since Aug 11 at 3:00 PM, we can get messages from any time - // on Aug 11. Our IMAP query results can include messages up to 24 hours older than endDate, - // or up to 25 hours older at a daylight savings transition. - // It is important that we have the Id of any local message that could potentially be - // returned by the IMAP query, or we will create duplicate copies of the same messages. - // So we will increase our local query range by this much. - // Note that this complicates deletion: It's not okay to delete anything that is in the - // localMessageMap but not in the remote result, because we know that we may be getting - // Ids of local messages that are outside the IMAP query window. - Cursor localUidCursor = null; - HashMap localMessageMap = new HashMap(); - try { - // FLAG: There is a problem that causes us to store the wrong date on some messages, - // so messages get a date of zero. If we filter these messages out and don't put them - // in our localMessageMap, then we'll end up loading the same message again. - // See b/10508861 -// final long queryEndDate = endDate - DateUtils.DAY_IN_MILLIS - DateUtils.HOUR_IN_MILLIS; - final long queryEndDate = 0; - localUidCursor = resolver.query( - EmailContent.Message.CONTENT_URI, - LocalMessageInfo.PROJECTION, - EmailContent.MessageColumns.ACCOUNT_KEY + "=?" - + " AND " + MessageColumns.MAILBOX_KEY + "=?" - + " AND " + MessageColumns.TIMESTAMP + ">=?", - new String[] { - String.valueOf(account.mId), - String.valueOf(mailbox.mId), - String.valueOf(queryEndDate) }, - null); - while (localUidCursor.moveToNext()) { - LocalMessageInfo info = new LocalMessageInfo(localUidCursor); - // If the message has no server id, it's local only. This should only happen for - // mail created on the client that has failed to upsync. We want to ignore such - // mail during synchronization (i.e. leave it as-is and let the next sync try again - // to upsync). - if (!TextUtils.isEmpty(info.mServerId)) { - localMessageMap.put(info.mServerId, info); - } - } - } finally { - if (localUidCursor != null) { - localUidCursor.close(); - } - } - - // 9. 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. - final ArrayList unsyncedMessages = new ArrayList(); - final HashMap remoteUidMap = new HashMap(); - // Process the messages in the reverse order we received them in. This means that - // we load the most recent one first, which gives a better user experience. - for (int i = remoteMessages.length - 1; i >= 0; i--) { - Message message = remoteMessages[i]; - LogUtils.d(Logging.LOG_TAG, "remote message " + message.getUid()); - remoteUidMap.put(message.getUid(), message); - - 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); - } - } - - // 10. 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); - } - - // 11. Refresh the flags for any messages in the local store that we didn't just download. - // TODO This is a bit wasteful because we're also updating any messages we already did get - // the flags and envelope for previously. - // TODO: the fetch() function, and others, should take List<>s of messages, not - // arrays of messages. - FetchProfile fp = new FetchProfile(); - fp.add(FetchProfile.Item.FLAGS); - if (remoteMessages.length > MAX_MESSAGES_TO_FETCH) { - List remoteMessageList = Arrays.asList(remoteMessages); - for (int start = 0; start < remoteMessageList.size(); start += MAX_MESSAGES_TO_FETCH) { - int end = start + MAX_MESSAGES_TO_FETCH; - if (end >= remoteMessageList.size()) { - end = remoteMessageList.size() - 1; - } - List chunk = remoteMessageList.subList(start, end); - final Message[] partialArray = chunk.toArray(new Message[chunk.size()]); - // Fetch this one chunk of messages - remoteFolder.fetch(partialArray, fp, null); - } - } else { - 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; - } - } - - // 12. 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); - } - } - } - - // 12.5 Remove messages that are marked as deleted so that we drop them from the DB in the - // next step - for (final Message remoteMessage : remoteMessages) { - if (remoteMessage.isSet(Flag.DELETED)) { - remoteUidMap.remove(remoteMessage.getUid()); - unsyncedMessages.remove(remoteMessage); - } - } - - // 13. Remove messages that are in the local store and in the current sync window, - // but no longer on the remote store. Note that localMessageMap can contain messages - // that are not actually in our sync window. We need to check the timestamp to ensure - // that it is before deleting. - for (final LocalMessageInfo info : localMessageMap.values()) { - // If this message is inside our sync window, and we cannot find it in our list - // of remote messages, then we know it's been deleted from the server. - if (info.mTimestamp >= endDate && !remoteUidMap.containsKey(info.mServerId)) { - // Delete associated data (attachment files) - // Attachment & Body records are auto-deleted when we delete the Message record - AttachmentUtilities.deleteAllAttachmentFiles(context, account.mId, info.mId); - - // Delete the message itself - final Uri uriToDelete = ContentUris.withAppendedId( - EmailContent.Message.CONTENT_URI, info.mId); - resolver.delete(uriToDelete, null, null); - - // Delete extra rows (e.g. updated or deleted) - final Uri updateRowToDelete = ContentUris.withAppendedId( - EmailContent.Message.UPDATED_CONTENT_URI, info.mId); - resolver.delete(updateRowToDelete, null, null); - final Uri deleteRowToDelete = ContentUris.withAppendedId( - EmailContent.Message.DELETED_CONTENT_URI, info.mId); - resolver.delete(deleteRowToDelete, null, null); - } - } - - loadUnsyncedMessages(context, account, remoteFolder, unsyncedMessages, mailbox); - - if (fullSync) { - mailbox.updateLastFullSyncTime(context, SystemClock.elapsedRealtime()); - } - - // 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, - Store remoteStore, boolean manualSync) - 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, remoteStore, accountIdArgs); - - // Handle uploads (currently, only to sent messages) - processPendingUploadsSynchronous(context, account, remoteStore, accountIdArgs, manualSync); - - // Now handle updates / upsyncs - processPendingUpdatesSynchronous(context, account, remoteStore, 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. - */ - private static void processPendingDeletesSynchronous(Context context, Account account, - Store remoteStore, 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 { - // loop through messages marked as deleted - while (deletes.moveToNext()) { - EmailContent.Message oldMessage = - EmailContent.getContent(context, 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. - } - final boolean deleteFromTrash = mailbox.mType == Mailbox.TYPE_TRASH; - - // Dispatch here for specific change types - if (deleteFromTrash) { - // Move message to trash - processPendingDeleteFromTrash(remoteStore, 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 (DebugUtils.DEBUG) { - LogUtils.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. - */ - private static void processPendingUploadsSynchronous(Context context, Account account, - Store remoteStore, String[] accountIdArgs, boolean manualSync) { - ContentResolver resolver = context.getContentResolver(); - // Find the Sent folder (since that's all we're uploading for now - // TODO: Upsync for all folders? (In case a user moves mail from Sent before it is - // handled. Also, this would generically solve allowing drafts to upload.) - 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 { - 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, - MessageColumns.MAILBOX_KEY + "=?" - + " and (" + MessageColumns.SERVER_ID + " is null" - + " or " + MessageColumns.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, mailbox, id, manualSync); - } - } finally { - if (upsyncs1 != null) { - upsyncs1.close(); - } - if (remoteStore != null) { - remoteStore.closeConnections(); - } - } - } - } 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 (DebugUtils.DEBUG) { - LogUtils.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. - */ - private static void processPendingUpdatesSynchronous(Context context, Account account, - Store remoteStore, 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 { - // 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(context, 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, 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 (DebugUtils.DEBUG) { - LogUtils.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 mailbox the actual mailbox - */ - private static void processUploadMessage(Context context, Store remoteStore, Mailbox mailbox, - long messageId, boolean manualSync) - throws MessagingException { - EmailContent.Message newMessage = - EmailContent.Message.restoreMessageWithId(context, messageId); - final boolean deleteUpdate; - if (newMessage == null) { - deleteUpdate = true; - LogUtils.d(Logging.LOG_TAG, "Upsync failed for null message, id=" + messageId); - } else if (mailbox.mType == Mailbox.TYPE_DRAFTS) { - deleteUpdate = false; - LogUtils.d(Logging.LOG_TAG, "Upsync skipped for mailbox=drafts, id=" + messageId); - } else if (mailbox.mType == Mailbox.TYPE_OUTBOX) { - deleteUpdate = false; - LogUtils.d(Logging.LOG_TAG, "Upsync skipped for mailbox=outbox, id=" + messageId); - } else if (mailbox.mType == Mailbox.TYPE_TRASH) { - deleteUpdate = false; - LogUtils.d(Logging.LOG_TAG, "Upsync skipped for mailbox=trash, id=" + messageId); - } else if (newMessage.mMailboxKey != mailbox.mId) { - deleteUpdate = false; - LogUtils.d(Logging.LOG_TAG, "Upsync skipped; mailbox changed, id=" + messageId); - } else { - LogUtils.d(Logging.LOG_TAG, "Upsync triggered for message id=" + messageId); - deleteUpdate = - processPendingAppend(context, remoteStore, mailbox, newMessage, manualSync); - } - 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 (DebugUtils.DEBUG) { - LogUtils.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(MessageColumns.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 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, - 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(MessageColumns.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 oldMailbox The local trash mailbox - * @param oldMessage The message that was deleted from the trash - */ - private static void processPendingDeleteFromTrash(Store remoteStore, - 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); - } - - /** - * Process a pending append message command. This command uploads a local message to the - * server, first checking to be sure that the server message is not newer than - * the local message. - * - * @param remoteStore the remote store we're working in - * @param mailbox The mailbox we're appending to - * @param message The message we're appending - * @param manualSync True if this is a manual sync (changes upsync behavior) - * @return true if successfully uploaded - */ - private static boolean processPendingAppend(Context context, Store remoteStore, Mailbox mailbox, - EmailContent.Message message, boolean manualSync) - throws MessagingException { - boolean updateInternalDate = false; - boolean updateMessage = false; - boolean deleteMessage = false; - - // 1. Find the remote folder that we're appending to and create and/or open it - Folder remoteFolder = remoteStore.getFolder(mailbox.mServerId); - if (!remoteFolder.exists()) { - if (!remoteFolder.create(FolderType.HOLDS_MESSAGES)) { - // This is a (hopefully) transient error and we return false to try again later - return false; - } - } - remoteFolder.open(OpenMode.READ_WRITE); - if (remoteFolder.getMode() != OpenMode.READ_WRITE) { - return false; - } - - // 2. If possible, load a remote message with the matching UID - Message remoteMessage = null; - if (message.mServerId != null && message.mServerId.length() > 0) { - remoteMessage = remoteFolder.getMessage(message.mServerId); - } - - // 3. If a remote message could not be found, upload our local message - if (remoteMessage == null) { - // TODO: - // if we have a serverId and remoteMessage is still null, then probably the message - // has been deleted and we should delete locally. - // 3a. Create a legacy message to upload - Message localMessage = LegacyConversions.makeMessage(context, message); - // 3b. Upload it - //FetchProfile fp = new FetchProfile(); - //fp.add(FetchProfile.Item.BODY); - // Note that this operation will assign the Uid to localMessage - remoteFolder.appendMessage(context, localMessage, manualSync /* no timeout */); - - // 3b. And record the UID from the server - message.mServerId = localMessage.getUid(); - updateInternalDate = true; - updateMessage = true; - } else { - // 4. If the remote message exists we need to determine which copy to keep. - // TODO: - // I don't see a good reason we should be here. If the message already has a serverId, - // then we should be handling it in processPendingUpdates(), - // not processPendingUploads() - FetchProfile fp = new FetchProfile(); - fp.add(FetchProfile.Item.ENVELOPE); - remoteFolder.fetch(new Message[] { remoteMessage }, fp, null); - Date localDate = new Date(message.mServerTimeStamp); - Date remoteDate = remoteMessage.getInternalDate(); - if (remoteDate != null && remoteDate.compareTo(localDate) > 0) { - // 4a. If the remote message is newer than ours we'll just - // delete ours and move on. A sync will get the server message - // if we need to be able to see it. - deleteMessage = true; - } else { - // 4b. Otherwise we'll upload our message and then delete the remote message. - - // Create a legacy message to upload - // TODO: This strategy has a problem: This will create a second message, - // so that at least temporarily, we will have two messages for what the - // user would think of as one. - Message localMessage = LegacyConversions.makeMessage(context, message); - - // 4c. Upload it - fp.clear(); - fp = new FetchProfile(); - fp.add(FetchProfile.Item.BODY); - remoteFolder.appendMessage(context, localMessage, manualSync /* no timeout */); - - // 4d. Record the UID and new internalDate from the server - message.mServerId = localMessage.getUid(); - updateInternalDate = true; - updateMessage = true; - - // 4e. And delete the old copy of the message from the server. - remoteMessage.setFlag(Flag.DELETED, true); - } - } - - // 5. If requested, Best-effort to capture new "internaldate" from the server - if (updateInternalDate && message.mServerId != null) { - try { - Message remoteMessage2 = remoteFolder.getMessage(message.mServerId); - if (remoteMessage2 != null) { - FetchProfile fp2 = new FetchProfile(); - fp2.add(FetchProfile.Item.ENVELOPE); - remoteFolder.fetch(new Message[] { remoteMessage2 }, fp2, null); - final Date remoteDate = remoteMessage2.getInternalDate(); - if (remoteDate != null) { - message.mServerTimeStamp = remoteMessage2.getInternalDate().getTime(); - updateMessage = true; - } - } - } catch (MessagingException me) { - // skip it - we can live without this - } - } - - // 6. Perform required edits to local copy of message - if (deleteMessage || updateMessage) { - Uri uri = ContentUris.withAppendedId(EmailContent.Message.CONTENT_URI, message.mId); - ContentResolver resolver = context.getContentResolver(); - if (deleteMessage) { - resolver.delete(uri, null, null); - } else if (updateMessage) { - ContentValues cv = new ContentValues(); - cv.put(MessageColumns.SERVER_ID, message.mServerId); - cv.put(MessageColumns.SERVER_TIMESTAMP, message.mServerTimeStamp); - resolver.update(uri, cv, null, null); - } - } - - return true; - } - - /** - * 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; - } - } - - private static int searchMailboxImpl(final Context context, final long accountId, - final 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) { - LogUtils.d(Logging.LOG_TAG, "Attempted search for %s " - + "but account or mailbox information was missing", searchParams); - return 0; - } - - // Tell UI that we're loading messages - final ContentValues statusValues = new ContentValues(2); - statusValues.put(Mailbox.UI_SYNC_STATUS, UIProvider.SyncStatus.LIVE_QUERY); - destMailbox.update(context, statusValues); - - Store remoteStore = null; - int numSearchResults = 0; - try { - remoteStore = Store.getInstance(account, context); - final 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) - final Message[] remoteMessages = remoteFolder.getMessages(searchParams, null); - final 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() { - @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 { - // It seems odd for this to happen, but if the previous query returned zero results, - // but the UI somehow still attempted to load more, then sSearchResults will have - // a null value for this account. We need to handle this below. - sortableMessages = sSearchResults.get(accountId); - } - - numSearchResults = (sortableMessages != null ? sortableMessages.length : 0); - final int numToLoad = - Math.min(numSearchResults - searchParams.mOffset, searchParams.mLimit); - destMailbox.updateMessageCount(context, numSearchResults); - if (numToLoad <= 0) { - return 0; - } - - final ArrayList messageList = new ArrayList<>(); - for (int i = searchParams.mOffset; i < numToLoad + searchParams.mOffset; i++) { - messageList.add(sortableMessages[i].mMessage); - } - // First fetch FLAGS and ENVELOPE. In a second pass, we'll fetch STRUCTURE and - // the first body part. - final FetchProfile fp = new FetchProfile(); - fp.add(FetchProfile.Item.FLAGS); - fp.add(FetchProfile.Item.ENVELOPE); - - Message[] messageArray = messageList.toArray(new Message[messageList.size()]); - - // TODO: We are purposely processing messages with a MessageRetrievalListener here, - // rather than just walking the messageArray after the operation completes. This is so - // that we can immediately update the database so the user can see something useful - // happening, even if the message body has not yet been fetched. - // There are some issues with this approach: - // 1. It means that we have a single thread doing both network and database operations, - // and either can block the other. The database updates could slow down the network - // reads, keeping our network connection open longer than is really necessary. - // 2. We still load all of this data into messageArray, even though it's not used. - // It would be nicer if we had one thread doing the network operation, and a separate - // thread consuming that data and performing the appropriate database work, then - // discarding the data as soon as it is no longer needed. This would reduce our memory - // footprint and potentially allow our network operation to complete faster. - remoteFolder.fetch(messageArray, fp, new MessageRetrievalListener() { - @Override - public void messageRetrieved(Message message) { - try { - EmailContent.Message localMessage = new EmailContent.Message(); - - // Copy the fields that are available into the message - LegacyConversions.updateMessageFields(localMessage, - message, account.mId, mailbox.mId); - // Save off the mailbox that this message *really* belongs in. - // We need this information if we need to do more lookups - // (like loading attachments) for this message. See b/11294681 - localMessage.mMainMailboxKey = localMessage.mMailboxKey; - 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; - // Commit the message to the local store - Utilities.saveOrUpdate(localMessage, context); - } catch (MessagingException me) { - LogUtils.e(Logging.LOG_TAG, me, - "Error while copying downloaded message."); - } catch (Exception e) { - LogUtils.e(Logging.LOG_TAG, e, - "Error while storing downloaded message."); - } - } - - @Override - public void loadAttachmentProgress(int progress) { - } - }); - - // Now load the structure for all of the messages: - fp.clear(); - fp.add(FetchProfile.Item.STRUCTURE); - remoteFolder.fetch(messageArray, fp, null); - - // Finally, load the first body part (i.e. message text). - // This means attachment contents are not yet loaded, but that's okay, - // we'll load them as needed, same as in synced messages. - Message[] oneMessageArray = new Message[1]; - for (Message message : messageArray) { - // Build a list of parts we are interested in. Text parts will be downloaded - // right now, attachments will be left for later. - ArrayList viewables = new ArrayList<>(); - ArrayList attachments = new ArrayList<>(); - MimeUtility.collectParts(message, viewables, attachments); - // Download the viewables immediately - oneMessageArray[0] = message; - for (Part part : viewables) { - fp.clear(); - fp.add(part); - remoteFolder.fetch(oneMessageArray, fp, null); - } - // Store the updated message locally and mark it fully loaded - Utilities.copyOneMessageToProvider(context, message, account, destMailbox, - EmailContent.Message.FLAG_LOADED_COMPLETE); - } - - } finally { - if (remoteStore != null) { - remoteStore.closeConnections(); - } - // Tell UI that we're done loading messages - statusValues.put(Mailbox.SYNC_TIME, System.currentTimeMillis()); - statusValues.put(Mailbox.UI_SYNC_STATUS, UIProvider.SyncStatus.NO_SYNC); - destMailbox.update(context, statusValues); - } - - return numSearchResults; - } -} diff --git a/src/com/android/email/service/ImapTempFileLiteral.java b/src/com/android/email/service/ImapTempFileLiteral.java deleted file mode 100644 index b521036f3..000000000 --- a/src/com/android/email/service/ImapTempFileLiteral.java +++ /dev/null @@ -1,126 +0,0 @@ -/* - * 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 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 com.android.mail.utils.LogUtils; - -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. - LogUtils.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) { - LogUtils.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. - LogUtils.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/LegacyEasAuthenticatorService.java b/src/com/android/email/service/LegacyEasAuthenticatorService.java deleted file mode 100644 index d90497c4c..000000000 --- a/src/com/android/email/service/LegacyEasAuthenticatorService.java +++ /dev/null @@ -1,23 +0,0 @@ -/* - * Copyright (C) 2010 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.email.service; - -/** - * This service needs to be declared separately from the base service - */ -public class LegacyEasAuthenticatorService extends AuthenticatorService { -} diff --git a/src/com/android/email/service/LegacyEmailAuthenticatorService.java b/src/com/android/email/service/LegacyEmailAuthenticatorService.java deleted file mode 100644 index c5b56444d..000000000 --- a/src/com/android/email/service/LegacyEmailAuthenticatorService.java +++ /dev/null @@ -1,23 +0,0 @@ -/* - * Copyright (C) 2010 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.email.service; - -/** - * This service needs to be declared separately from the base service - */ -public class LegacyEmailAuthenticatorService extends AuthenticatorService { -} diff --git a/src/com/android/email/service/LegacyImapAuthenticatorService.java b/src/com/android/email/service/LegacyImapAuthenticatorService.java deleted file mode 100644 index 8480d1e8d..000000000 --- a/src/com/android/email/service/LegacyImapAuthenticatorService.java +++ /dev/null @@ -1,23 +0,0 @@ -/* - * Copyright (C) 2010 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.email.service; - -/** - * This service needs to be declared separately from the base service - */ -public class LegacyImapAuthenticatorService extends AuthenticatorService { -} diff --git a/src/com/android/email/service/LegacyImapSyncAdapterService.java b/src/com/android/email/service/LegacyImapSyncAdapterService.java deleted file mode 100644 index 1f6b6195e..000000000 --- a/src/com/android/email/service/LegacyImapSyncAdapterService.java +++ /dev/null @@ -1,20 +0,0 @@ -/* - * 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; - -public class LegacyImapSyncAdapterService extends PopImapSyncAdapterService { -} \ No newline at end of file diff --git a/src/com/android/email/service/PolicyService.java b/src/com/android/email/service/PolicyService.java deleted file mode 100644 index c045fabab..000000000 --- a/src/com/android/email/service/PolicyService.java +++ /dev/null @@ -1,94 +0,0 @@ -/* - * 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.service; - -import android.app.Service; -import android.content.Context; -import android.content.Intent; -import android.os.IBinder; -import android.os.RemoteException; - -import com.android.email.SecurityPolicy; -import com.android.emailcommon.provider.Policy; -import com.android.emailcommon.service.IPolicyService; -import com.android.mail.utils.LogTag; -import com.android.mail.utils.LogUtils; - -public class PolicyService extends Service { - private static final String LOG_TAG = LogTag.getLogTag(); - - private SecurityPolicy mSecurityPolicy; - private Context mContext; - - private final IPolicyService.Stub mBinder = new IPolicyService.Stub() { - @Override - public boolean isActive(Policy policy) { - try { - return mSecurityPolicy.isActive(policy); - } catch (RuntimeException e) { - // Catch, log and rethrow the exception, as otherwise when the exception is - // ultimately handled, the complete stack trace is losk - LogUtils.e(LOG_TAG, e, "Exception thrown during call to SecurityPolicy#isActive"); - throw e; - } - } - - @Override - public void setAccountHoldFlag(long accountId, boolean newState) { - SecurityPolicy.setAccountHoldFlag(mContext, accountId, newState); - } - - @Override - public void remoteWipe() { - try { - mSecurityPolicy.remoteWipe(); - } catch (RuntimeException e) { - // Catch, log and rethrow the exception, as otherwise when the exception is - // ultimately handled, the complete stack trace is losk - LogUtils.e(LOG_TAG, e, "Exception thrown during call to SecurityPolicy#remoteWipe"); - throw e; - } - } - - @Override - public void setAccountPolicy(long accountId, Policy policy, String securityKey) { - setAccountPolicy2(accountId, policy, securityKey, true /* notify */); - } - - @Override - public void setAccountPolicy2(long accountId, Policy policy, String securityKey, - boolean notify) { - try { - mSecurityPolicy.setAccountPolicy(accountId, policy, securityKey, notify); - } catch (RuntimeException e) { - // Catch, log and rethrow the exception, as otherwise when the exception is - // ultimately handled, the complete stack trace is losk - LogUtils.e(LOG_TAG, e, - "Exception thrown from call to SecurityPolicy#setAccountPolicy"); - throw e; - } - } - }; - - @Override - public IBinder onBind(Intent intent) { - // When we bind this service, save the context and SecurityPolicy singleton - mContext = this; - mSecurityPolicy = SecurityPolicy.getInstance(this); - return mBinder; - } -} \ No newline at end of file diff --git a/src/com/android/email/service/Pop3AuthenticatorService.java b/src/com/android/email/service/Pop3AuthenticatorService.java deleted file mode 100644 index f3076ee6b..000000000 --- a/src/com/android/email/service/Pop3AuthenticatorService.java +++ /dev/null @@ -1,23 +0,0 @@ -/* - * Copyright (C) 2010 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.email.service; - -/** - * This service needs to be declared separately from the base service - */ -public class Pop3AuthenticatorService extends AuthenticatorService { -} diff --git a/src/com/android/email/service/Pop3Service.java b/src/com/android/email/service/Pop3Service.java deleted file mode 100644 index 40b3ca695..000000000 --- a/src/com/android/email/service/Pop3Service.java +++ /dev/null @@ -1,475 +0,0 @@ -/* - * 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.RemoteException; - -import com.android.email.DebugUtils; -import com.android.email.NotificationController; -import com.android.email.mail.Store; -import com.android.email.mail.store.Pop3Store; -import com.android.email.mail.store.Pop3Store.Pop3Folder; -import com.android.email.mail.store.Pop3Store.Pop3Message; -import com.android.email.provider.Utilities; -import com.android.emailcommon.Logging; -import com.android.emailcommon.TrafficFlags; -import com.android.emailcommon.mail.AuthenticationFailedException; -import com.android.emailcommon.mail.Folder.OpenMode; -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.Message; -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; -import com.android.mail.providers.UIProvider.AttachmentState; -import com.android.mail.utils.LogUtils; - -import org.apache.james.mime4j.EOLConvertingInputStream; - -import java.io.IOException; -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 DEFAULT_SYNC_COUNT = 100; - - @Override - public int onStartCommand(Intent intent, int flags, int startId) { - return Service.START_STICKY; - } - - /** - * Create our EmailService implementation here. - */ - private final EmailServiceStub mBinder = new EmailServiceStub() { - @Override - public void loadAttachment(final IEmailServiceCallback callback, final long accountId, - final long attachmentId, final boolean background) throws RemoteException { - Attachment att = Attachment.restoreAttachmentWithId(mContext, attachmentId); - if (att == null || att.mUiState != AttachmentState.DOWNLOADING) return; - long inboxId = Mailbox.findMailboxOfType(mContext, att.mAccountKey, Mailbox.TYPE_INBOX); - if (inboxId == Mailbox.NO_MAILBOX) return; - // We load attachments during a sync - requestSync(inboxId, true, 0); - } - }; - - @Override - public IBinder onBind(Intent intent) { - mBinder.init(this); - return mBinder; - } - - /** - * 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 - * @param deltaMessageCount the requested change in number of messages to sync. - * @return The status code for whether this operation succeeded. - * @throws MessagingException - */ - public static int synchronizeMailboxSynchronous(Context context, final Account account, - final Mailbox folder, final int deltaMessageCount) throws MessagingException { - TrafficStats.setThreadStatsTag(TrafficFlags.getSyncFlags(context, account)); - NotificationController nc = NotificationController.getInstance(context); - try { - synchronizePop3Mailbox(context, account, folder, deltaMessageCount); - // Clear authentication notification for this account - nc.cancelLoginFailedNotification(account.mId); - } catch (MessagingException e) { - if (Logging.LOGD) { - LogUtils.v(Logging.LOG_TAG, "synchronizeMailbox", e); - } - if (e instanceof AuthenticationFailedException) { - // Generate authentication notification - nc.showLoginFailedNotificationSynchronous(account.mId, true /* incoming */); - } - throw e; - } - // TODO: Rather than use exceptions as logic aobve, return the status and handle it - // correctly in caller. - return EmailServiceStatus.SUCCESS; - } - - /** - * 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_LOADED = 1; - private static final int COLUMN_SERVER_ID = 2; - private static final String[] PROJECTION = new String[] { - EmailContent.RECORD_ID, MessageColumns.FLAG_LOADED, SyncColumns.SERVER_ID - }; - - final long mId; - final int mFlagLoaded; - final String mServerId; - - public LocalMessageInfo(Cursor c) { - mId = c.getLong(COLUMN_ID); - mFlagLoaded = c.getInt(COLUMN_FLAG_LOADED); - mServerId = c.getString(COLUMN_SERVER_ID); - // 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, - Pop3Folder remoteFolder, ArrayList unsyncedMessages, - final Mailbox toMailbox) throws MessagingException { - - if (DebugUtils.DEBUG) { - LogUtils.d(TAG, "Loading " + unsyncedMessages.size() + " unsynced messages"); - } - - try { - int cnt = unsyncedMessages.size(); - // They are in most recent to least recent order, process them that way. - for (int i = 0; i < cnt; i++) { - final Pop3Message message = unsyncedMessages.get(i); - remoteFolder.fetchBody(message, Pop3Store.FETCH_BODY_SANE_SUGGESTED_SIZE / 76, - null); - int flag = EmailContent.Message.FLAG_LOADED_COMPLETE; - if (!message.isComplete()) { - // TODO: when the message is not complete, this should mark the message as - // partial. When that change is made, we need to make sure that: - // 1) Partial messages are shown in the conversation list - // 2) We are able to download the rest of the message/attachment when the - // user requests it. - flag = EmailContent.Message.FLAG_LOADED_PARTIAL; - } - if (DebugUtils.DEBUG) { - LogUtils.d(TAG, "Message is " + (message.isComplete() ? "" : "NOT ") - + "complete"); - } - // If message is incomplete, create a "fake" attachment - Utilities.copyOneMessageToProvider(context, message, account, toMailbox, flag); - } - } catch (IOException e) { - throw new MessagingException(MessagingException.IOERROR); - } - } - - private static class FetchCallback implements EOLConvertingInputStream.Callback { - private final ContentResolver mResolver; - private final Uri mAttachmentUri; - private final ContentValues mContentValues = new ContentValues(); - - FetchCallback(ContentResolver resolver, Uri attachmentUri) { - mResolver = resolver; - mAttachmentUri = attachmentUri; - } - - @Override - public void report(int bytesRead) { - mContentValues.put(AttachmentColumns.UI_DOWNLOADED_SIZE, bytesRead); - mResolver.update(mAttachmentUri, mContentValues, null, null); - } - } - - /** - * Synchronizer - * - * @param account the account to sync - * @param mailbox the mailbox to sync - * @param deltaMessageCount the requested change to number of messages to sync - * @throws MessagingException - */ - private synchronized static void synchronizePop3Mailbox(final Context context, final Account account, - final Mailbox mailbox, final int deltaMessageCount) throws MessagingException { - // TODO Break this into smaller pieces - ContentResolver resolver = context.getContentResolver(); - - // We only sync Inbox - if (mailbox.mType != Mailbox.TYPE_INBOX) { - return; - } - - // Get the message list from EmailProvider and create an index of the uids - - Cursor localUidCursor = null; - HashMap localMessageMap = new HashMap(); - - try { - localUidCursor = resolver.query( - EmailContent.Message.CONTENT_URI, - LocalMessageInfo.PROJECTION, - MessageColumns.MAILBOX_KEY + "=?", - new String[] { - String.valueOf(mailbox.mId) - }, - null); - while (localUidCursor.moveToNext()) { - LocalMessageInfo info = new LocalMessageInfo(localUidCursor); - localMessageMap.put(info.mServerId, info); - } - } finally { - if (localUidCursor != null) { - localUidCursor.close(); - } - } - - // Open the remote folder and create the remote folder if necessary - - Pop3Store remoteStore = (Pop3Store)Store.getInstance(account, context); - // The account might have been deleted - if (remoteStore == null) - return; - Pop3Folder remoteFolder = (Pop3Folder)remoteStore.getFolder(mailbox.mServerId); - - // Open the remote folder. This pre-loads certain metadata like message - // count. - remoteFolder.open(OpenMode.READ_WRITE); - - String[] accountIdArgs = new String[] { Long.toString(account.mId) }; - long trashMailboxId = Mailbox.findMailboxOfType(context, account.mId, Mailbox.TYPE_TRASH); - Cursor updates = resolver.query( - EmailContent.Message.UPDATED_CONTENT_URI, - EmailContent.Message.ID_COLUMN_PROJECTION, - EmailContent.MessageColumns.ACCOUNT_KEY + "=?", accountIdArgs, - null); - try { - // loop through messages marked as deleted - while (updates.moveToNext()) { - long id = updates.getLong(Message.ID_COLUMNS_ID_COLUMN); - EmailContent.Message currentMsg = - EmailContent.Message.restoreMessageWithId(context, id); - if (currentMsg.mMailboxKey == trashMailboxId) { - // Delete this on the server - Pop3Message popMessage = - (Pop3Message)remoteFolder.getMessage(currentMsg.mServerId); - if (popMessage != null) { - remoteFolder.deleteMessage(popMessage); - } - } - // Finally, delete the update - Uri uri = ContentUris.withAppendedId(EmailContent.Message.UPDATED_CONTENT_URI, id); - context.getContentResolver().delete(uri, null, null); - } - } finally { - updates.close(); - } - - // Get the remote message count. - final int remoteMessageCount = remoteFolder.getMessageCount(); - - // Save the folder message count. - mailbox.updateMessageCount(context, remoteMessageCount); - - // Create a list of messages to download - Pop3Message[] remoteMessages = new Pop3Message[0]; - final ArrayList unsyncedMessages = new ArrayList(); - HashMap remoteUidMap = new HashMap(); - - if (remoteMessageCount > 0) { - /* - * Get all messageIds in the mailbox. - * We don't necessarily need to sync all of them. - */ - remoteMessages = remoteFolder.getMessages(remoteMessageCount, remoteMessageCount); - LogUtils.d(Logging.LOG_TAG, "remoteMessageCount " + remoteMessageCount); - - /* - * TODO: It would be nicer if the default sync window were time based rather than - * count based, but POP3 does not support time based queries, and the UIDL command - * does not report timestamps. To handle this, we would need to load a block of - * Ids, sync those messages to get the timestamps, and then load more Ids until we - * have filled out our window. - */ - int count = 0; - int countNeeded = DEFAULT_SYNC_COUNT; - for (final Pop3Message message : remoteMessages) { - final String uid = message.getUid(); - remoteUidMap.put(uid, message); - } - - /* - * Figure out which messages we need to sync. Start at the most recent ones, and keep - * going until we hit one of four end conditions: - * 1. We currently have zero local messages. In this case, we will sync the most recent - * DEFAULT_SYNC_COUNT, then stop. - * 2. We have some local messages, and after encountering them, we find some older - * messages that do not yet exist locally. In this case, we will load whichever came - * before the ones we already had locally, and also deltaMessageCount additional - * older messages. - * 3. We have some local messages, but after examining the most recent - * DEFAULT_SYNC_COUNT remote messages, we still have not encountered any that exist - * locally. In this case, we'll stop adding new messages to sync, leaving a gap between - * the ones we've just loaded and the ones we already had. - * 4. We examine all of the remote messages before running into any of our count - * limitations. - */ - for (final Pop3Message message : remoteMessages) { - final String uid = message.getUid(); - final LocalMessageInfo localMessage = localMessageMap.get(uid); - if (localMessage == null) { - count++; - } else { - // We have found a message that already exists locally. We may or may not - // need to keep looking, depending on what deltaMessageCount is. - LogUtils.d(Logging.LOG_TAG, "found a local message, need " + - deltaMessageCount + " more remote messages"); - countNeeded = deltaMessageCount; - count = 0; - } - - // localMessage == null -> message has never been created (not even headers) - // mFlagLoaded != FLAG_LOADED_COMPLETE -> message failed to sync completely - if (localMessage == null || - (localMessage.mFlagLoaded != EmailContent.Message.FLAG_LOADED_COMPLETE && - localMessage.mFlagLoaded != Message.FLAG_LOADED_PARTIAL)) { - LogUtils.d(Logging.LOG_TAG, "need to sync " + uid); - unsyncedMessages.add(message); - } else { - LogUtils.d(Logging.LOG_TAG, "don't need to sync " + uid); - } - - if (count >= countNeeded) { - LogUtils.d(Logging.LOG_TAG, "loaded " + count + " messages, stopping"); - break; - } - } - } else { - if (DebugUtils.DEBUG) { - LogUtils.d(TAG, "*** Message count is zero??"); - } - remoteFolder.close(false); - return; - } - - // Get "attachments" to be loaded - Cursor c = resolver.query(Attachment.CONTENT_URI, Attachment.CONTENT_PROJECTION, - AttachmentColumns.ACCOUNT_KEY + "=? AND " + - AttachmentColumns.UI_STATE + "=" + AttachmentState.DOWNLOADING, - new String[] {Long.toString(account.mId)}, null); - try { - final ContentValues values = new ContentValues(); - while (c.moveToNext()) { - values.put(AttachmentColumns.UI_STATE, UIProvider.AttachmentState.SAVED); - Attachment att = new Attachment(); - att.restore(c); - Message msg = Message.restoreMessageWithId(context, att.mMessageKey); - if (msg == null || (msg.mFlagLoaded == Message.FLAG_LOADED_COMPLETE)) { - values.put(AttachmentColumns.UI_DOWNLOADED_SIZE, att.mSize); - resolver.update(ContentUris.withAppendedId(Attachment.CONTENT_URI, att.mId), - values, null, null); - continue; - } else { - String uid = msg.mServerId; - Pop3Message popMessage = remoteUidMap.get(uid); - if (popMessage != null) { - Uri attUri = ContentUris.withAppendedId(Attachment.CONTENT_URI, att.mId); - try { - remoteFolder.fetchBody(popMessage, -1, - new FetchCallback(resolver, attUri)); - } catch (IOException e) { - throw new MessagingException(MessagingException.IOERROR); - } - - // Say we've downloaded the attachment - values.put(AttachmentColumns.UI_STATE, AttachmentState.SAVED); - resolver.update(attUri, values, null, null); - - int flag = EmailContent.Message.FLAG_LOADED_COMPLETE; - if (!popMessage.isComplete()) { - LogUtils.e(TAG, "How is this possible?"); - } - Utilities.copyOneMessageToProvider( - context, popMessage, account, mailbox, flag); - // Get rid of the temporary attachment - resolver.delete(attUri, null, null); - - } else { - // TODO: Should we mark this attachment as failed so we don't - // keep trying to download? - LogUtils.e(TAG, "Could not find message for attachment " + uid); - } - } - } - } finally { - c.close(); - } - - // Remove any messages that are in the local store but no longer on the remote store. - HashSet localUidsToDelete = new HashSet(localMessageMap.keySet()); - localUidsToDelete.removeAll(remoteUidMap.keySet()); - for (String uidToDelete : localUidsToDelete) { - LogUtils.d(Logging.LOG_TAG, "need to delete " + uidToDelete); - 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 updateRowToDelete = ContentUris.withAppendedId( - EmailContent.Message.UPDATED_CONTENT_URI, infoToDelete.mId); - resolver.delete(updateRowToDelete, null, null); - Uri deleteRowToDelete = ContentUris.withAppendedId( - EmailContent.Message.DELETED_CONTENT_URI, infoToDelete.mId); - resolver.delete(deleteRowToDelete, null, null); - } - - LogUtils.d(TAG, "loadUnsynchedMessages " + unsyncedMessages.size()); - // Load messages we need to sync - loadUnsyncedMessages(context, account, remoteFolder, unsyncedMessages, mailbox); - - // Clean up and report results - remoteFolder.close(false); - } -} diff --git a/src/com/android/email/service/Pop3SyncAdapterService.java b/src/com/android/email/service/Pop3SyncAdapterService.java deleted file mode 100644 index a939f41f9..000000000 --- a/src/com/android/email/service/Pop3SyncAdapterService.java +++ /dev/null @@ -1,20 +0,0 @@ -/* - * 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; - -public class Pop3SyncAdapterService extends PopImapSyncAdapterService { -} \ No newline at end of file diff --git a/src/com/android/email/service/PopImapSyncAdapterService.java b/src/com/android/email/service/PopImapSyncAdapterService.java deleted file mode 100644 index 4fae4d0a9..000000000 --- a/src/com/android/email/service/PopImapSyncAdapterService.java +++ /dev/null @@ -1,261 +0,0 @@ -/* - * 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.app.Service; -import android.content.AbstractThreadedSyncAdapter; -import android.content.ContentProviderClient; -import android.content.ContentResolver; -import android.content.ContentUris; -import android.content.ContentValues; -import android.content.Context; -import android.content.Intent; -import android.content.SyncResult; -import android.database.Cursor; -import android.net.Uri; -import android.os.Bundle; -import android.os.IBinder; - -import com.android.email.R; -import com.android.emailcommon.TempDirectory; -import com.android.emailcommon.mail.MessagingException; -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.Message; -import com.android.emailcommon.provider.EmailContent.MessageColumns; -import com.android.emailcommon.provider.Mailbox; -import com.android.emailcommon.service.EmailServiceProxy; -import com.android.emailcommon.service.EmailServiceStatus; -import com.android.mail.providers.UIProvider; -import com.android.mail.utils.LogUtils; - -import java.util.ArrayList; - -public class PopImapSyncAdapterService extends Service { - private static final String TAG = "PopImapSyncService"; - private SyncAdapterImpl mSyncAdapter = null; - - public PopImapSyncAdapterService() { - super(); - } - - private static class SyncAdapterImpl extends AbstractThreadedSyncAdapter { - public SyncAdapterImpl(Context context) { - super(context, true /* autoInitialize */); - } - - @Override - public void onPerformSync(android.accounts.Account account, Bundle extras, - String authority, ContentProviderClient provider, SyncResult syncResult) { - PopImapSyncAdapterService.performSync(getContext(), account, extras, provider, - syncResult); - } - } - - @Override - public void onCreate() { - super.onCreate(); - mSyncAdapter = new SyncAdapterImpl(getApplicationContext()); - } - - @Override - public IBinder onBind(Intent intent) { - return mSyncAdapter.getSyncAdapterBinder(); - } - - /** - * @return whether or not this mailbox retrieves its data from the server (as opposed to just - * a local mailbox that is never synced). - */ - private static boolean loadsFromServer(Context context, Mailbox m, String protocol) { - String legacyImapProtocol = context.getString(R.string.protocol_legacy_imap); - String pop3Protocol = context.getString(R.string.protocol_pop3); - if (legacyImapProtocol.equals(protocol)) { - // TODO: actually use a sync flag when creating the mailboxes. Right now we use an - // approximation for IMAP. - return m.mType != Mailbox.TYPE_DRAFTS - && m.mType != Mailbox.TYPE_OUTBOX - && m.mType != Mailbox.TYPE_SEARCH; - - } else if (pop3Protocol.equals(protocol)) { - return Mailbox.TYPE_INBOX == m.mType; - } - - return false; - } - - private static void sync(final Context context, final long mailboxId, - final Bundle extras, final SyncResult syncResult, final boolean uiRefresh, - final int deltaMessageCount) { - TempDirectory.setTempDirectory(context); - Mailbox mailbox = Mailbox.restoreMailboxWithId(context, mailboxId); - if (mailbox == null) return; - Account account = Account.restoreAccountWithId(context, mailbox.mAccountKey); - if (account == null) return; - ContentResolver resolver = context.getContentResolver(); - String protocol = account.getProtocol(context); - if ((mailbox.mType != Mailbox.TYPE_OUTBOX) && - !loadsFromServer(context, mailbox, protocol)) { - // This is an update to a message in a non-syncing mailbox; delete this from the - // updates table and return - resolver.delete(Message.UPDATED_CONTENT_URI, MessageColumns.MAILBOX_KEY + "=?", - new String[] {Long.toString(mailbox.mId)}); - return; - } - LogUtils.d(TAG, "About to sync mailbox: " + mailbox.mDisplayName); - - Uri mailboxUri = ContentUris.withAppendedId(Mailbox.CONTENT_URI, mailboxId); - ContentValues values = new ContentValues(); - // Set mailbox sync state - values.put(Mailbox.UI_SYNC_STATUS, - uiRefresh ? EmailContent.SYNC_STATUS_USER : EmailContent.SYNC_STATUS_BACKGROUND); - resolver.update(mailboxUri, values, null, null); - try { - try { - String legacyImapProtocol = context.getString(R.string.protocol_legacy_imap); - if (mailbox.mType == Mailbox.TYPE_OUTBOX) { - EmailServiceStub.sendMailImpl(context, account.mId); - } else { - EmailServiceStatus.syncMailboxStatus(resolver, extras, mailboxId, - EmailServiceStatus.IN_PROGRESS, 0, UIProvider.LastSyncResult.SUCCESS); - final int status; - if (protocol.equals(legacyImapProtocol)) { - status = ImapService.synchronizeMailboxSynchronous(context, account, - mailbox, deltaMessageCount != 0, uiRefresh); - } else { - status = Pop3Service.synchronizeMailboxSynchronous(context, account, - mailbox, deltaMessageCount); - } - EmailServiceStatus.syncMailboxStatus(resolver, extras, mailboxId, status, 0, - UIProvider.LastSyncResult.SUCCESS); - } - } catch (MessagingException e) { - final int type = e.getExceptionType(); - // type must be translated into the domain of values used by EmailServiceStatus - switch(type) { - case MessagingException.IOERROR: - EmailServiceStatus.syncMailboxStatus(resolver, extras, mailboxId, type, 0, - UIProvider.LastSyncResult.CONNECTION_ERROR); - syncResult.stats.numIoExceptions++; - break; - case MessagingException.AUTHENTICATION_FAILED: - EmailServiceStatus.syncMailboxStatus(resolver, extras, mailboxId, type, 0, - UIProvider.LastSyncResult.AUTH_ERROR); - syncResult.stats.numAuthExceptions++; - break; - case MessagingException.SERVER_ERROR: - EmailServiceStatus.syncMailboxStatus(resolver, extras, mailboxId, type, 0, - UIProvider.LastSyncResult.SERVER_ERROR); - break; - - default: - EmailServiceStatus.syncMailboxStatus(resolver, extras, mailboxId, type, 0, - UIProvider.LastSyncResult.INTERNAL_ERROR); - } - } - } finally { - // Always clear our sync state and update sync time. - values.put(Mailbox.UI_SYNC_STATUS, EmailContent.SYNC_STATUS_NONE); - values.put(Mailbox.SYNC_TIME, System.currentTimeMillis()); - resolver.update(mailboxUri, values, null, null); - } - } - - /** - * Partial integration with system SyncManager; we initiate manual syncs upon request - */ - private static void performSync(Context context, android.accounts.Account account, - Bundle extras, ContentProviderClient provider, SyncResult syncResult) { - // Find an EmailProvider account with the Account's email address - Cursor c = null; - try { - c = provider.query(com.android.emailcommon.provider.Account.CONTENT_URI, - Account.CONTENT_PROJECTION, AccountColumns.EMAIL_ADDRESS + "=?", - new String[] {account.name}, null); - if (c != null && c.moveToNext()) { - Account acct = new Account(); - acct.restore(c); - if (extras.getBoolean(ContentResolver.SYNC_EXTRAS_UPLOAD)) { - LogUtils.d(TAG, "Upload sync request for " + acct.mDisplayName); - // See if any boxes have mail... - ArrayList mailboxesToUpdate; - Cursor updatesCursor = provider.query(Message.UPDATED_CONTENT_URI, - new String[] {MessageColumns.MAILBOX_KEY}, - MessageColumns.ACCOUNT_KEY + "=?", - new String[] {Long.toString(acct.mId)}, - null); - try { - if ((updatesCursor == null) || (updatesCursor.getCount() == 0)) return; - mailboxesToUpdate = new ArrayList(); - while (updatesCursor.moveToNext()) { - Long mailboxId = updatesCursor.getLong(0); - if (!mailboxesToUpdate.contains(mailboxId)) { - mailboxesToUpdate.add(mailboxId); - } - } - } finally { - if (updatesCursor != null) { - updatesCursor.close(); - } - } - for (long mailboxId: mailboxesToUpdate) { - sync(context, mailboxId, extras, syncResult, false, 0); - } - } else { - LogUtils.d(TAG, "Sync request for " + acct.mDisplayName); - LogUtils.d(TAG, extras.toString()); - - // We update our folder structure on every sync. - final EmailServiceProxy service = - EmailServiceUtils.getServiceForAccount(context, acct.mId); - service.updateFolderList(acct.mId); - - // Get the id for the mailbox we want to sync. - long [] mailboxIds = Mailbox.getMailboxIdsFromBundle(extras); - if (mailboxIds == null || mailboxIds.length == 0) { - // No mailbox specified, just sync the inbox. - // TODO: IMAP may eventually want to allow multiple auto-sync mailboxes. - final long inboxId = Mailbox.findMailboxOfType(context, acct.mId, - Mailbox.TYPE_INBOX); - if (inboxId != Mailbox.NO_MAILBOX) { - mailboxIds = new long[1]; - mailboxIds[0] = inboxId; - } - } - - if (mailboxIds != null) { - boolean uiRefresh = - extras.getBoolean(ContentResolver.SYNC_EXTRAS_EXPEDITED, false); - int deltaMessageCount = - extras.getInt(Mailbox.SYNC_EXTRA_DELTA_MESSAGE_COUNT, 0); - for (long mailboxId : mailboxIds) { - sync(context, mailboxId, extras, syncResult, uiRefresh, - deltaMessageCount); - } - } - } - } - } catch (Exception e) { - e.printStackTrace(); - } finally { - if (c != null) { - c.close(); - } - } - } -} diff --git a/src/com/android/email2/ui/MailActivityEmail.java b/src/com/android/email2/ui/MailActivityEmail.java index b2403de06..23b3facea 100644 --- a/src/com/android/email2/ui/MailActivityEmail.java +++ b/src/com/android/email2/ui/MailActivityEmail.java @@ -26,21 +26,14 @@ import android.database.Cursor; import android.net.Uri; import android.os.Bundle; -import com.android.email.NotificationController; import com.android.email.Preferences; -import com.android.email.R; import com.android.email.provider.EmailProvider; import com.android.email.service.AttachmentService; import com.android.email.service.EmailServiceUtils; import com.android.emailcommon.Logging; import com.android.emailcommon.TempDirectory; -import com.android.emailcommon.provider.Account; -import com.android.emailcommon.provider.EmailContent; import com.android.emailcommon.provider.Mailbox; -import com.android.emailcommon.service.EmailServiceProxy; -import com.android.emailcommon.utility.EmailAsyncTask; import com.android.emailcommon.utility.IntentUtilities; -import com.android.emailcommon.utility.Utility; import com.android.mail.providers.Folder; import com.android.mail.providers.UIProvider; import com.android.mail.utils.LogTag; @@ -62,78 +55,6 @@ public class MailActivityEmail extends com.android.mail.ui.MailActivity { } - /** - * Asynchronous version of {@link #setServicesEnabledSync(Context)}. Use when calling from - * UI thread (or lifecycle entry points.) - */ - public static void setServicesEnabledAsync(final Context context) { - if (context.getResources().getBoolean(R.bool.enable_services)) { - 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. - * - * @return true if there are any accounts configured. - */ - public static boolean setServicesEnabledSync(Context context) { - // Make sure we're initialized - EmailContent.init(context); - Cursor c = null; - try { - c = context.getContentResolver().query( - Account.CONTENT_URI, - Account.ID_PROJECTION, - null, null, null); - boolean enable = c != null && 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, AttachmentService.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 - // TODO: Make sure that the AttachmentService responds to this request as it - // expects a particular set of data in the intents that it receives or it ignores. - startOrStopService(enabled, context, new Intent(context, AttachmentService.class)); - NotificationController.getInstance(context).watchForMessages(); - } - - /** - * 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) { final Intent intent = getIntent(); @@ -163,7 +84,7 @@ public class MailActivityEmail extends com.android.mail.ui.MailActivity { // Make sure all required services are running when the app is started (can prevent // issues after an adb sync/install) - setServicesEnabledAsync(this); + EmailProvider.setServicesEnabledAsync(this); } /** diff --git a/src/com/android/mail/providers/EmailAccountCacheProvider.java b/src/com/android/mail/providers/EmailAccountCacheProvider.java index 118e31dfa..1d25966ee 100644 --- a/src/com/android/mail/providers/EmailAccountCacheProvider.java +++ b/src/com/android/mail/providers/EmailAccountCacheProvider.java @@ -22,6 +22,7 @@ import android.net.Uri; import com.android.email.R; import com.android.email.activity.setup.AccountSetupFinal; +import com.android.email.setup.AuthenticatorSetupIntentHelper; public class EmailAccountCacheProvider extends MailAppProvider { // Content provider for Email @@ -36,6 +37,6 @@ public class EmailAccountCacheProvider extends MailAppProvider { @Override protected Intent getNoAccountsIntent(Context context) { - return AccountSetupFinal.actionNewAccountWithResultIntent(context); + return AuthenticatorSetupIntentHelper.actionNewAccountWithResultIntent(context); } } -- cgit v1.2.3