summaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
authorPaul Westbrook <pwestbro@google.com>2014-09-07 13:36:33 -0700
committerPaul Westbrook <pwestbro@google.com>2014-09-16 13:59:07 -0700
commitbb68c13afa630cae058eb40d3ce68644f3f3c8b9 (patch)
tree9a4d9d57f4d419a28b9f2576ef9c3783aa6e1f3a /src
parent88929fd2bce947475d7a494961b017b98c5e9b82 (diff)
downloadandroid_packages_apps_Email-bb68c13afa630cae058eb40d3ce68644f3f3c8b9.tar.gz
android_packages_apps_Email-bb68c13afa630cae058eb40d3ce68644f3f3c8b9.tar.bz2
android_packages_apps_Email-bb68c13afa630cae058eb40d3ce68644f3f3c8b9.zip
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
Diffstat (limited to 'src')
-rw-r--r--src/com/android/email/AttachmentInfo.java249
-rw-r--r--src/com/android/email/DebugUtils.java48
-rw-r--r--src/com/android/email/EmailApplication.java8
-rw-r--r--src/com/android/email/EmailConnectivityManager.java217
-rw-r--r--src/com/android/email/EmailIntentService.java45
-rw-r--r--src/com/android/email/EmailNotificationController.java (renamed from src/com/android/email/NotificationController.java)33
-rw-r--r--src/com/android/email/FixedLengthInputStream.java80
-rw-r--r--src/com/android/email/LegacyConversions.java547
-rw-r--r--src/com/android/email/PeekableInputStream.java80
-rw-r--r--src/com/android/email/Preferences.java280
-rw-r--r--src/com/android/email/ResourceHelper.java87
-rw-r--r--src/com/android/email/SecurityPolicy.java904
-rw-r--r--src/com/android/email/activity/setup/AccountCreationFragment.java3
-rw-r--r--src/com/android/email/activity/setup/AccountSecurity.java628
-rw-r--r--src/com/android/email/activity/setup/AccountServerSettingsActivity.java3
-rw-r--r--src/com/android/email/activity/setup/AccountSettingsFragment.java3
-rw-r--r--src/com/android/email/activity/setup/AccountSettingsUtils.java433
-rw-r--r--src/com/android/email/activity/setup/AccountSetupFinal.java40
-rw-r--r--src/com/android/email/activity/setup/AccountSetupNamesFragment.java9
-rw-r--r--src/com/android/email/activity/setup/EmailPreferenceActivity.java3
-rw-r--r--src/com/android/email/activity/setup/ForwardingIntent.java30
-rw-r--r--src/com/android/email/activity/setup/HeadlessAccountSettingsLoader.java7
-rw-r--r--src/com/android/email/activity/setup/SetupDataFragment.java11
-rw-r--r--src/com/android/email/mail/Sender.java125
-rw-r--r--src/com/android/email/mail/Store.java226
-rw-r--r--src/com/android/email/mail/internet/AuthenticationCache.java162
-rw-r--r--src/com/android/email/mail/internet/OAuthAuthenticator.java191
-rw-r--r--src/com/android/email/mail/store/ImapConnection.java636
-rw-r--r--src/com/android/email/mail/store/ImapFolder.java1291
-rw-r--r--src/com/android/email/mail/store/ImapStore.java657
-rw-r--r--src/com/android/email/mail/store/Pop3Store.java833
-rw-r--r--src/com/android/email/mail/store/ServiceStore.java89
-rw-r--r--src/com/android/email/mail/store/imap/ImapConstants.java104
-rw-r--r--src/com/android/email/mail/store/imap/ImapElement.java120
-rw-r--r--src/com/android/email/mail/store/imap/ImapList.java235
-rw-r--r--src/com/android/email/mail/store/imap/ImapMemoryLiteral.java71
-rw-r--r--src/com/android/email/mail/store/imap/ImapResponse.java152
-rw-r--r--src/com/android/email/mail/store/imap/ImapResponseParser.java453
-rw-r--r--src/com/android/email/mail/store/imap/ImapSimpleString.java55
-rw-r--r--src/com/android/email/mail/store/imap/ImapString.java186
-rw-r--r--src/com/android/email/mail/store/imap/ImapTempFileLiteral.java123
-rw-r--r--src/com/android/email/mail/store/imap/ImapUtility.java126
-rw-r--r--src/com/android/email/mail/transport/DiscourseLogger.java119
-rw-r--r--src/com/android/email/mail/transport/MailTransport.java320
-rw-r--r--src/com/android/email/provider/AccountBackupRestore.java51
-rw-r--r--src/com/android/email/provider/AccountReconciler.java306
-rw-r--r--src/com/android/email/provider/AttachmentProvider.java338
-rw-r--r--src/com/android/email/provider/ContentCache.java822
-rw-r--r--src/com/android/email/provider/DBHelper.java1896
-rw-r--r--src/com/android/email/provider/EmailConversationCursor.java245
-rw-r--r--src/com/android/email/provider/EmailMessageCursor.java124
-rw-r--r--src/com/android/email/provider/EmailProvider.java6188
-rw-r--r--src/com/android/email/provider/FolderPickerActivity.java218
-rw-r--r--src/com/android/email/provider/FolderPickerCallback.java25
-rw-r--r--src/com/android/email/provider/FolderPickerDialog.java158
-rw-r--r--src/com/android/email/provider/FolderPickerSelectorAdapter.java46
-rw-r--r--src/com/android/email/provider/RefreshStatusMonitor.java159
-rw-r--r--src/com/android/email/provider/Utilities.java234
-rw-r--r--src/com/android/email/provider/WidgetProvider.java191
-rw-r--r--src/com/android/email/service/AccountService.java85
-rw-r--r--src/com/android/email/service/AttachmentService.java1401
-rw-r--r--src/com/android/email/service/AuthenticatorService.java165
-rw-r--r--src/com/android/email/service/EasAuthenticatorService.java23
-rw-r--r--src/com/android/email/service/EasAuthenticatorServiceAlternate.java23
-rw-r--r--src/com/android/email/service/EasTestAuthenticatorService.java123
-rw-r--r--src/com/android/email/service/EmailBroadcastProcessorService.java371
-rw-r--r--src/com/android/email/service/EmailBroadcastReceiver.java31
-rw-r--r--src/com/android/email/service/EmailServiceStub.java523
-rw-r--r--src/com/android/email/service/EmailServiceUtils.java780
-rw-r--r--src/com/android/email/service/EmailUpgradeBroadcastReceiver.java17
-rw-r--r--src/com/android/email/service/ImapAuthenticatorService.java23
-rw-r--r--src/com/android/email/service/ImapService.java1615
-rw-r--r--src/com/android/email/service/ImapTempFileLiteral.java126
-rw-r--r--src/com/android/email/service/LegacyEasAuthenticatorService.java23
-rw-r--r--src/com/android/email/service/LegacyEmailAuthenticatorService.java23
-rw-r--r--src/com/android/email/service/LegacyImapAuthenticatorService.java23
-rw-r--r--src/com/android/email/service/LegacyImapSyncAdapterService.java20
-rw-r--r--src/com/android/email/service/PolicyService.java94
-rw-r--r--src/com/android/email/service/Pop3AuthenticatorService.java23
-rw-r--r--src/com/android/email/service/Pop3Service.java475
-rw-r--r--src/com/android/email/service/Pop3SyncAdapterService.java20
-rw-r--r--src/com/android/email/service/PopImapSyncAdapterService.java261
-rw-r--r--src/com/android/email2/ui/MailActivityEmail.java81
-rw-r--r--src/com/android/mail/providers/EmailAccountCacheProvider.java3
84 files changed, 59 insertions, 27316 deletions
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<ResolveInfo> 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 <code>Intent</code> 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/NotificationController.java b/src/com/android/email/EmailNotificationController.java
index 675140632..e57b41833 100644
--- a/src/com/android/email/NotificationController.java
+++ b/src/com/android/email/EmailNotificationController.java
@@ -62,7 +62,7 @@ import java.util.Set;
/**
* Class that manages notifications.
*/
-public class NotificationController {
+public class EmailNotificationController implements NotificationController {
private static final String LOG_TAG = LogTag.getLogTag();
private static final int NOTIFICATION_ID_ATTACHMENT_WARNING = 3;
@@ -76,7 +76,7 @@ public class NotificationController {
private static NotificationThread sNotificationThread;
private static Handler sNotificationHandler;
- private static NotificationController sInstance;
+ private static EmailNotificationController sInstance;
private final Context mContext;
private final NotificationManager mNotificationManager;
private final Clock mClock;
@@ -86,7 +86,7 @@ public class NotificationController {
private ContentObserver mAccountObserver;
/** Constructor */
- protected NotificationController(Context context, Clock clock) {
+ protected EmailNotificationController(Context context, Clock clock) {
mContext = context.getApplicationContext();
EmailContent.init(context);
mNotificationManager = (NotificationManager) context.getSystemService(
@@ -95,9 +95,9 @@ public class NotificationController {
}
/** Singleton access */
- public static synchronized NotificationController getInstance(Context context) {
+ public static synchronized EmailNotificationController getInstance(Context context) {
if (sInstance == null) {
- sInstance = new NotificationController(context, Clock.INSTANCE);
+ sInstance = new EmailNotificationController(context, Clock.INSTANCE);
}
return sInstance;
}
@@ -182,6 +182,7 @@ public class NotificationController {
* 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
@@ -386,6 +387,7 @@ public class NotificationController {
*
* 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;
@@ -410,6 +412,7 @@ public class NotificationController {
*
* 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;
@@ -420,7 +423,7 @@ public class NotificationController {
final Intent settingsIntent;
if (incoming) {
settingsIntent = new Intent(Intent.ACTION_VIEW,
- HeadlessAccountSettingsLoader.getIncomingSettingsUri(accountId));
+ EmailProvider.getIncomingSettingsUri(accountId));
} else {
settingsIntent = new Intent(Intent.ACTION_VIEW,
HeadlessAccountSettingsLoader.getOutgoingSettingsUri(accountId));
@@ -436,6 +439,7 @@ public class NotificationController {
/**
* Cancels the login failed notification for the given account.
*/
+ @Override
public void cancelLoginFailedNotification(long accountId) {
mNotificationManager.cancel(getLoginFailedNotificationId(accountId));
}
@@ -446,6 +450,7 @@ public class NotificationController {
*
* 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;
@@ -466,6 +471,7 @@ public class NotificationController {
*
* 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;
@@ -482,6 +488,7 @@ public class NotificationController {
/**
* Cancels any password expire notifications [both expired & expiring].
*/
+ @Override
public void cancelPasswordExpirationNotifications() {
mNotificationManager.cancel(NOTIFICATION_ID_PASSWORD_EXPIRING);
mNotificationManager.cancel(NOTIFICATION_ID_PASSWORD_EXPIRED);
@@ -491,6 +498,7 @@ public class NotificationController {
* 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();
@@ -505,9 +513,10 @@ public class NotificationController {
* 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,
- HeadlessAccountSettingsLoader.getIncomingSettingsUri(account.getId()));
+ EmailProvider.getIncomingSettingsUri(account.getId()));
final String accountName = account.getDisplayName();
final String ticker =
mContext.getString(R.string.security_changed_ticker_fmt, accountName);
@@ -521,9 +530,10 @@ public class NotificationController {
* 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,
- HeadlessAccountSettingsLoader.getIncomingSettingsUri(account.getId()));
+ EmailProvider.getIncomingSettingsUri(account.getId()));
final String accountName = account.getDisplayName();
final String ticker =
mContext.getString(R.string.security_unsupported_ticker_fmt, accountName);
@@ -536,6 +546,7 @@ public class NotificationController {
/**
* Cancels all security needed notifications.
*/
+ @Override
public void cancelSecurityNeededNotification() {
EmailAsyncTask.runAsyncParallel(new Runnable() {
@Override
@@ -559,7 +570,8 @@ public class NotificationController {
* 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) {
+ @Override
+ public void cancelNotifications(final Context context, final Account account) {
final EmailServiceUtils.EmailServiceInfo serviceInfo
= EmailServiceUtils.getServiceInfoForAccount(context, account.mId);
if (serviceInfo == null) {
@@ -646,7 +658,8 @@ public class NotificationController {
}
}
- public static void handleUpdateNotificationIntent(Context context, Intent intent) {
+ @Override
+ public void handleUpdateNotificationIntent(Context context, Intent intent) {
final Uri accountUri =
intent.getParcelableExtra(UIProvider.UpdateNotificationExtras.EXTRA_ACCOUNT);
final Uri folderUri =
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<String, Integer>
- sServerMailboxNames = new HashMap<String, Integer>();
-
- /**
- * 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<Attachment> 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<Part> 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<Part> 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<Attachment>();
- }
- 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/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<String> getWhitelistedSenderAddresses() {
- try {
- return parseEmailSet(mSharedPreferences.getString(TRUSTED_SENDERS, ""));
- } catch (JSONException e) {
- return Collections.emptySet();
- }
- }
-
- HashSet<String> parseEmailSet(String serialized) throws JSONException {
- HashSet<String> result = new HashSet<String>();
- 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<ContentProviderOperation> ops = new ArrayList<ContentProviderOperation>();
-
- // 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<Account> {
- @Override
- public Loader<Account> 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<Account> 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<Account> loader) {}
- }
-
- private static class AccountAndPolicyLoader extends MailAsyncTaskLoader<Account> {
- 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<Void, Void, Void>() {
- @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<OAuthProvider> getAllOAuthProviders(final Context context) {
- try {
- List<OAuthProvider> providers = new ArrayList<OAuthProvider>();
- 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 <code>s1</code> matches the string <code>s2</code>. The string
- * <code>s2</code> 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<String, String> 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<Cursor>() {
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<HostAuth, Store> sStores = new HashMap<HostAuth, Store>();
- protected Context mContext;
- protected Account mAccount;
- protected MailTransport mTransport;
- protected String mUsername;
- protected String mPassword;
-
- static final HashMap<String, Class<? extends Store>> sStoreClasses =
- new HashMap<String, Class<? extends Store>>();
-
- /**
- * 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<? extends Store> 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.
- * <em>Note: This does not perform folder synchronization and it will not remove mailboxes
- * that are stored locally but not remotely.</em>
- * @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<Long, CacheEntry> 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<Long, CacheEntry>();
- 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<BasicNameValuePair> nvp = new ArrayList<BasicNameValuePair>();
- 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<BasicNameValuePair> nvp = new ArrayList<BasicNameValuePair>();
- 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<String> 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<ImapResponse> 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<ImapResponse> getCommandResponses() throws IOException, MessagingException {
- final List<ImapResponse> responses = new ArrayList<ImapResponse>();
- 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<ImapResponse> 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<ImapResponse> executeComplexCommand(List<String> 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<ImapResponse> 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<ImapResponse> 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<ImapResponse> 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<String, Message> messageMap = new HashMap<String, Message>();
- 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<ImapResponse> 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<ImapResponse> responses) {
- // S: * SEARCH 2 3 6
- final ArrayList<String> uids = new ArrayList<String>();
- 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}<CRLF>foo}
- */
- @Override
- @VisibleForTesting
- public Message[] getMessages(SearchParams params, MessageRetrievalListener listener)
- throws MessagingException {
- List<String> commands = new ArrayList<String>();
- 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<String> 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<Message> messages = new ArrayList<Message>(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<String, Message> messageMap = new HashMap<String, Message>();
- 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<String> fetchFields = new LinkedHashSet<String>();
-
- 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<ImapResponse> 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<ImapResponse> 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;
-
-
-/**
- * <pre>
- * 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).
- * </pre>
- */
-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<ImapConnection> mConnectionPool =
- new ConcurrentLinkedQueue<ImapConnection>();
-
- /**
- * 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<ImapConnection> 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 - _ + = ; : . , / <space>
- // The most important thing is *not* to pass parens, quotes, or CRLF, which would break
- // the format of the IMAP ID list.
- Pattern p = Pattern.compile("[^a-zA-Z0-9-_\\+=;:\\.,/ ]");
- packageName = p.matcher(packageName).replaceAll("");
- version = p.matcher(version).replaceAll("");
- codeName = p.matcher(codeName).replaceAll("");
- model = p.matcher(model).replaceAll("");
- id = p.matcher(id).replaceAll("");
- vendor = p.matcher(vendor).replaceAll("");
- networkOperator = p.matcher(networkOperator).replaceAll("");
-
- // "name" "com.android.email"
- 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<String, ImapFolder> mailboxes) {
- Set<String> 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<String, ImapFolder> 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<String, ImapFolder> mailboxes = new HashMap<String, ImapFolder>();
- // 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<ImapResponse> 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<String, Folder> mFolders = new HashMap<String, Folder>();
- 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<String, Pop3Message> mUidToMsgMap
- = new HashMap<String, Pop3Message>();
- private final HashMap<Integer, Pop3Message> mMsgNumToMsgMap
- = new HashMap<Integer, Pop3Message>();
- private final HashMap<String, Integer> mUidToMsgNumMap = new HashMap<String, Integer>();
- 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<Message> messages = new ArrayList<Message>();
- 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.
- *
- * <p>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.
- *
- * <p>Class hierarchy:
- * <pre>
- * ImapElement
- * |
- * |-- ImapElement.NONE (for 'index out of range')
- * |
- * |-- ImapList (isList() == true)
- * | |
- * | |-- ImapList.EMPTY
- * | |
- * | --- ImapResponse
- * |
- * --- ImapString (isString() == true)
- * |
- * |-- ImapString.EMPTY
- * |
- * |-- ImapSimpleString
- * |
- * |-- ImapMemoryLiteral
- * |
- * --- ImapTempFileLiteral
- * </pre>
- */
-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<ImapElement> mList = new ArrayList<ImapElement>();
-
- /* 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.
- *
- * <p>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<ImapResponse> mResponsesToDestroy = new ArrayList<ImapResponse>();
-
- /**
- * 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.
- *
- * <p>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 = <any TEXT-CHAR except quoted-specials> / "\" 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.
- * <pre>
- * sequence-number = nz-number / "*"
- * sequence-range = sequence-number ":" sequence-number
- * sequence-set = (sequence-number / sequence-range) *("," sequence-set)
- * </pre>
- */
- public static String[] getImapSequenceValues(String set) {
- ArrayList<String> list = new ArrayList<String>();
- 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.
- * <pre>
- * sequence-number = nz-number / "*"
- * sequence-range = sequence-number ":" sequence-number
- * sequence-set = (sequence-number / sequence-range) *("," sequence-set)
- * </pre>
- */
- public static String[] getImapRangeValues(String range) {
- ArrayList<String> list = new ArrayList<String>();
- 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.
- *
- * <p>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<String> list = new ArrayList<String>();
-
- 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.
- *
- * <p>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<android.accounts.Account> 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<String> accountTypes = new LinkedHashSet<String>();
- 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<android.accounts.Account> 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<Account> 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<Account> 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<android.accounts.Account> amAccounts = getAllAmAccounts(context);
- final List<Account> 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<android.accounts.Account> 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<Account> 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<Account> emailProviderAccounts,
- final List<android.accounts.Account> 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<Boolean> 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: <database-path>/account#.db_att/item#
- * Thumbnails are stored at: <cache-path>/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<String> 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<String> 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<String> 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<String> 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<String> sNotCacheableMap = new CounterMap<String>();
-
- private final LruCache<String, Cursor> mLruCache;
-
- // All defined caches
- private static final ArrayList<ContentCache> sContentCaches = new ArrayList<ContentCache>();
- // 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<Cursor> sActiveCursors = new CounterMap<Cursor>(24);
-
- // A set of locked content id's
- private final CounterMap<String> mLockMap = new CounterMap<String>(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<T> {
- private HashMap<T, Integer> mMap;
-
- /*package*/ CounterMap(int maxSize) {
- mMap = new HashMap<T, Integer>(maxSize);
- }
-
- /*package*/ CounterMap() {
- mMap = new HashMap<T, Integer>();
- }
-
- /*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<Map.Entry<T, Integer>> 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<CacheToken> {
- 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<CacheToken> removeList = new ArrayList<CacheToken>();
- 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<String, Cursor>(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<String, Cursor> 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 <v5");
- }
- }
-
- @SuppressWarnings("deprecation")
- private static void upgradeBodyFromVersion5ToVersion6(final SQLiteDatabase db) {
- try {
- db.execSQL("alter table " + Body.TABLE_NAME
- + " add " + BodyColumns.INTRO_TEXT + " text");
- } 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 v5 to v6");
- }
- }
-
- private static void upgradeBodyFromVersion6ToVersion8(final SQLiteDatabase db) {
- try {
- db.execSQL("alter table " + Body.TABLE_NAME
- + " add " + BodyColumns.QUOTED_TEXT_START_POS + " integer");
- } 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 v6 to v8");
- }
- }
-
- /**
- * This upgrade migrates email bodies out of the database and into individual files.
- */
- private static void upgradeBodyFromVersion100ToVersion101(final Context context,
- final SQLiteDatabase db) {
- try {
- // We can't read the body parts through the cursor because they might be over 2MB
- final String projection[] = { BodyColumns.MESSAGE_KEY };
- final Cursor cursor = db.query(Body.TABLE_NAME, projection,
- null, null, null, null, null);
- if (cursor == null) {
- throw new IllegalStateException("Could not read body table for upgrade");
- }
-
- final SQLiteStatement htmlSql = db.compileStatement(
- "SELECT " + BodyColumns.HTML_CONTENT +
- " FROM " + Body.TABLE_NAME +
- " WHERE " + BodyColumns.MESSAGE_KEY + "=?"
- );
-
- final SQLiteStatement textSql = db.compileStatement(
- "SELECT " + BodyColumns.TEXT_CONTENT +
- " FROM " + Body.TABLE_NAME +
- " WHERE " + BodyColumns.MESSAGE_KEY + "=?"
- );
-
- while (cursor.moveToNext()) {
- final long messageId = cursor.getLong(0);
- htmlSql.bindLong(1, messageId);
- try {
- final String htmlString = htmlSql.simpleQueryForString();
- if (!TextUtils.isEmpty(htmlString)) {
- final File htmlFile = EmailProvider.getBodyFile(context, messageId, "html");
- final FileWriter w = new FileWriter(htmlFile);
- try {
- w.write(htmlString);
- } finally {
- w.close();
- }
- }
- } catch (final SQLiteDoneException e) {
- LogUtils.v(LogUtils.TAG, e, "Done with the HTML column");
- }
- textSql.bindLong(1, messageId);
- try {
- final String textString = textSql.simpleQueryForString();
- if (!TextUtils.isEmpty(textString)) {
- final File textFile = EmailProvider.getBodyFile(context, messageId, "txt");
- final FileWriter w = new FileWriter(textFile);
- try {
- w.write(textString);
- } finally {
- w.close();
- }
- }
- } catch (final SQLiteDoneException e) {
- LogUtils.v(LogUtils.TAG, e, "Done with the text column");
- }
- }
-
- db.execSQL("update " + Body.TABLE_NAME +
- " set " + BodyColumns.HTML_CONTENT + "=NULL,"
- + BodyColumns.TEXT_CONTENT + "=NULL");
- } 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 v100 to v101");
- } catch (final IOException e) {
- throw new RuntimeException(e);
- }
- }
-
-
- protected static class BodyDatabaseHelper extends SQLiteOpenHelper {
- final Context mContext;
-
- BodyDatabaseHelper(Context context, String name) {
- super(context, name, null, BODY_DATABASE_VERSION);
- mContext = context;
- }
-
- @Override
- public void onCreate(SQLiteDatabase db) {
- LogUtils.d(TAG, "Creating EmailProviderBody database");
- createBodyTable(db);
- }
-
- @Override
- public void onUpgrade(final SQLiteDatabase db, final int oldVersion, final int newVersion) {
- if (oldVersion < 5) {
- upgradeBodyToVersion5(db);
- }
- if (oldVersion < 6) {
- upgradeBodyFromVersion5ToVersion6(db);
- }
- if (oldVersion < 8) {
- upgradeBodyFromVersion6ToVersion8(db);
- }
- if (oldVersion < 101) {
- upgradeBodyFromVersion100ToVersion101(mContext, db);
- }
- }
-
- @Override
- public void onOpen(SQLiteDatabase db) {
- }
- }
-
- /** Counts the number of messages in each mailbox, and updates the message count column. */
- @VisibleForTesting
- static void recalculateMessageCount(SQLiteDatabase db) {
- db.execSQL("update " + Mailbox.TABLE_NAME + " set " + MailboxColumns.MESSAGE_COUNT +
- "= (select count(*) from " + Message.TABLE_NAME +
- " where " + MessageColumns.MAILBOX_KEY + " = " +
- Mailbox.TABLE_NAME + "." + MailboxColumns._ID + ")");
- }
-
- protected static class DatabaseHelper extends SQLiteOpenHelper {
- final Context mContext;
-
- DatabaseHelper(Context context, String name) {
- super(context, name, null, DATABASE_VERSION);
- mContext = context;
- }
-
- @Override
- public void onCreate(SQLiteDatabase db) {
- LogUtils.d(TAG, "Creating EmailProvider database");
- // Create all tables here; each class has its own method
- createMessageTable(mContext, db);
- createAttachmentTable(db);
- createMailboxTable(db);
- createHostAuthTable(db);
- createAccountTable(db);
- createMessageMoveTable(db);
- createMessageStateChangeTable(db);
- createPolicyTable(db);
- createQuickResponseTable(db);
- createCredentialsTable(db);
- }
-
- @Override
- public void onDowngrade(SQLiteDatabase db, int oldVersion, int newVersion) {
- if (oldVersion == 101 && newVersion == 100) {
- LogUtils.d(TAG, "Downgrade from v101 to v100");
- } else {
- super.onDowngrade(db, oldVersion, newVersion);
- }
- }
-
- @Override
- @SuppressWarnings("deprecation")
- public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
- // For versions prior to 5, delete all data
- // Versions >= 5 require that data be preserved!
- if (oldVersion < 5) {
- android.accounts.Account[] accounts = AccountManager.get(mContext)
- .getAccountsByType(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<String, String> legacyToNewTypeMap = new ImmutableMap.Builder<String, String>()
- .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<String> mTextParts;
- private final SparseArray<String> 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<String>(cursorSize);
- mTextParts = new SparseArray<String>(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<String> TABLE_NAMES;
- static {
- SparseArray<String> array = new SparseArray<String>(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<ContentProviderOperation> mLastSequenceOps =
- new ArrayList<ContentProviderOperation>();
-
- // 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<SyncRequestMessage> mDelayedSyncRequests = new HashSet<SyncRequestMessage>();
-
- 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 <table> where <column> not in (select <foreignColumn> from <foreignTable>)
- * 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<Long> foundMailboxes = new ArrayList<Long>();
- ArrayList<Long> notFoundMailboxes = new ArrayList<Long>();
- ArrayList<Long> deleteList = new ArrayList<Long>();
- 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<EmailServiceInfo> infos = EmailServiceUtils.getServiceInfoList(context);
- // Find all possible account types
- final Set<String> accountTypes = new HashSet<String>(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<android.accounts.Account> amAccounts = new ArrayList<android.accounts.Account>();
- 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 <<baseURI>>/<<op>>/<<id>>; where <<op>> and <<id>> are optional depending
- * upon the given values.
- * NOTE: If <<op>> is specified, notifications for <<baseURI>>/<<id>> will NOT be invoked.
- * If this is necessary, it can be added. However, due to the implementation of
- * {@link ContentObserver}, observers of <<baseURI>> 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<Set<Uri>> mTLBatchNotifications =
- new ThreadLocal<Set<Uri>>();
-
- private Set<Uri> getBatchNotificationsSet() {
- return mTLBatchNotifications.get();
- }
-
- private void setBatchNotificationsSet(Set<Uri> batchNotifications) {
- mTLBatchNotifications.set(batchNotifications);
- }
-
- @Override
- public ContentProviderResult[] applyBatch(ArrayList<ContentProviderOperation> 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.<Uri>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<Uri> 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 <column>", 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<com.android.mail.providers.Attachment> uiAtts =
- new ArrayList<com.android.mail.providers.Attachment>();
- 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 <code>true</code> 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 <code>true</code> 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<String> 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<String, Integer> builder =
- new ImmutableMap.Builder<String, Integer>();
- final String[] columnNames = mc.getColumnNames();
- for (int i = 0; i < columnNames.length; i++) {
- builder.put(columnNames[i], i);
- }
- final Map<String, Integer> 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<String> 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<String> 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 <code>true</code> 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<String> 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<String> 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<com.android.mail.providers.Attachment> uiAtts =
- com.android.mail.providers.Attachment.fromJSONArray(
- extras.getString(UIProvider.MessageColumns.ATTACHMENTS));
- final ArrayList<Attachment> atts = new ArrayList<Attachment>();
- 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<ContentProviderOperation> ops =
- new ArrayList<ContentProviderOperation>();
- // 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<Uri> 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<PeriodicSync> 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<Void, Void, Void>() {
- @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 <strong>encoded</strong> 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<Long> mWidgetNotifyMailboxes = new ArrayList<Long>();
- 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<Folder, Boolean> 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<Folder, Boolean>();
- 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<String>(), 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<Folder, Boolean> 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<String> 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<Long, Boolean> mMailboxSync = new HashMap<Long, Boolean>();
-
- 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<Part> viewables = new ArrayList<Part>();
- ArrayList<Part> attachments = new ArrayList<Part>();
- 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}.
- * <p>
- * @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<Long, Long> mAttachmentStorageMap = new ConcurrentHashMap<Long, Long>();
-
- // 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<Long, Integer> mAttachmentFailureMap = new ConcurrentHashMap<Long, Integer>();
-
- // Keeps tracks of downloads in progress based on an attachment ID to DownloadRequest mapping.
- final ConcurrentHashMap<Long, DownloadRequest> mDownloadsInProgress =
- new ConcurrentHashMap<Long, DownloadRequest>();
-
- 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<long[]> sAttachmentChangedQueue =
- new ConcurrentLinkedQueue<long[]>();
-
- // 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<DownloadRequest> {
- @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<DownloadRequest> mRequestQueue =
- new PriorityQueue<DownloadRequest>(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<Long, DownloadRequest> mRequestMap =
- new ConcurrentHashMap<Long, DownloadRequest>();
-
- /**
- * 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<DownloadRequest> 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<Long, Intent> mAccountServiceMap = new HashMap<Long, Intent>();
-
- 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:
- * <ul>
- * <li>So that it's less likely for the process to get killed.
- * <li>Even if it does, the Intent that have started it will be re-delivered by the system,
- * and we can start the process again. (Using {@link #setIntentRedelivery}).
- * </ul>
- *
- * 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<String, String> 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<PeriodicSync> 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<String, Integer> 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<String, Integer> getSyncIntervals() {
- final Cursor c = getContentResolver().query(Account.CONTENT_URI,
- ACCOUNT_SYNC_INTERVAL_PROJECTION, null, null, null);
- if (c != null) {
- final Map<String, Integer> 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<String, String> protocolMap) {
- final Set<String> keySet = new HashSet<String>(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<String, String> 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<String, Integer> 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<String> remoteFolderNames = new HashSet<String>();
- 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<? extends Service> 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<EmailServiceInfo> 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<Bundle> setupAccountManagerAccount(final Context context,
- final Account account, final boolean email, final boolean calendar,
- final boolean contacts, final AccountManagerCallback<Bundle> 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<Bundle> setupAccountManagerAccount(final Context context,
- final Account account, final boolean email, final boolean calendar,
- final boolean contacts, final HostAuth hostAuth,
- final AccountManagerCallback<Bundle> 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<String, String> 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<String, EmailServiceInfo> sServiceMap = null;
- private static final Object sServiceMapLock = new Object();
-
- /**
- * Parse services.xml file to find our available email services
- */
- private static Map<String, EmailServiceInfo> 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<String, EmailServiceInfo> 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<? extends Service>) 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 <String, EmailServiceInfo> 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<Long, SortableMessage[]> sSearchResults =
- new HashMap<Long, SortableMessage[]>();
-
- /**
- * We write this into the serverId field of messages that will never be upsynced.
- */
- private static final String LOCAL_SERVERID_PREFIX = "Local-";
-
- 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<Message> messages, final Mailbox toMailbox)
- throws MessagingException {
-
- FetchProfile fp = new FetchProfile();
- fp.add(FetchProfile.Item.STRUCTURE);
- remoteFolder.fetch(messages.toArray(new Message[messages.size()]), fp, null);
- 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<Part> viewables = new ArrayList<Part>();
- ArrayList<Part> attachments = new ArrayList<Part>();
- 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<Message> unsyncedMessages,
- HashMap<String, LocalMessageInfo> localMessageMap, final ArrayList<Long> unseenMessages)
- throws MessagingException {
- FetchProfile fp = new FetchProfile();
- fp.add(FetchProfile.Item.FLAGS);
- fp.add(FetchProfile.Item.ENVELOPE);
-
- final HashMap<String, LocalMessageInfo> localMapCopy;
- if (localMessageMap != null)
- localMapCopy = new HashMap<String, LocalMessageInfo>(localMessageMap);
- else {
- localMapCopy = new HashMap<String, LocalMessageInfo>();
- }
-
- remoteFolder.fetch(unsyncedMessages.toArray(new Message[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<Long> unseenMessages = new ArrayList<Long>();
-
- ContentResolver resolver = context.getContentResolver();
-
- // 0. We do not ever sync DRAFTS or OUTBOX (down or up)
- if (mailbox.mType == Mailbox.TYPE_DRAFTS || mailbox.mType == Mailbox.TYPE_OUTBOX) {
- return;
- }
-
- // 1. 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<String, LocalMessageInfo> localMessageMap = new HashMap<String, LocalMessageInfo>();
- 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<Message> unsyncedMessages = new ArrayList<Message>();
- final HashMap<String, Message> remoteUidMap = new HashMap<String, Message>();
- // 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<Message> 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<Message> 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<SortableMessage>() {
- @Override
- public int compare(SortableMessage lhs, SortableMessage rhs) {
- return lhs.mUid > rhs.mUid ? -1 : lhs.mUid < rhs.mUid ? 1 : 0;
- }
- });
- sSearchResults.put(accountId, sortableMessages);
- }
- } else {
- // 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<Message> 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<Part> viewables = new ArrayList<>();
- ArrayList<Part> 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<Pop3Message> 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<String, LocalMessageInfo> localMessageMap = new HashMap<String, LocalMessageInfo>();
-
- 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<Pop3Message> unsyncedMessages = new ArrayList<Pop3Message>();
- HashMap<String, Pop3Message> remoteUidMap = new HashMap<String, Pop3Message>();
-
- 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<String> localUidsToDelete = new HashSet<String>(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<Long> 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<Long>();
- 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);
}
}