Copy emailcommon to email2
* Also, fix Exchange logging
* Also, get notifications working
* Refactor former Email class into MailActivityEmail
Change-Id: Id726f4178134485f4a3ec1ee317861d984d659a0
diff --git a/email2/emailcommon/Android.mk b/email2/emailcommon/Android.mk
new file mode 100644
index 0000000..cd70309
--- /dev/null
+++ b/email2/emailcommon/Android.mk
@@ -0,0 +1,44 @@
+# Copyright 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.
+
+LOCAL_PATH := $(call my-dir)
+
+# Build the com.android.emailcommon static library. At the moment, this includes
+# the emailcommon files themselves plus everything under src/org (apache code). All of our
+# AIDL files are also compiled into the static library
+
+include $(CLEAR_VARS)
+
+unified_email_src_dir := ../../../UnifiedEmail/src
+apache_src_dir := ../../../UnifiedEmail/src/org
+
+imported_unified_email_files := \
+ $(unified_email_src_dir)/com/android/mail/utils/LogUtils.java \
+ $(unified_email_src_dir)/com/android/mail/utils/LoggingInputStream.java \
+ $(unified_email_src_dir)/com/android/mail/providers/UIProvider.java
+
+LOCAL_MODULE := com.android.emailcommon
+LOCAL_STATIC_JAVA_LIBRARIES := guava android-common
+LOCAL_SRC_FILES := $(call all-java-files-under, src/com/android/emailcommon)
+LOCAL_SRC_FILES += \
+ src/com/android/emailcommon/service/IEmailService.aidl \
+ src/com/android/emailcommon/service/IEmailServiceCallback.aidl \
+ src/com/android/emailcommon/service/IPolicyService.aidl \
+ src/com/android/emailcommon/service/IAccountService.aidl
+LOCAL_SRC_FILES += $(call all-java-files-under, $(apache_src_dir))
+LOCAL_SRC_FILES += $(imported_unified_email_files)
+
+LOCAL_SDK_VERSION := current
+
+include $(BUILD_STATIC_JAVA_LIBRARY)
diff --git a/email2/emailcommon/src/com/android/emailcommon/AccountManagerTypes.java b/email2/emailcommon/src/com/android/emailcommon/AccountManagerTypes.java
new file mode 100644
index 0000000..4ccd480
--- /dev/null
+++ b/email2/emailcommon/src/com/android/emailcommon/AccountManagerTypes.java
@@ -0,0 +1,23 @@
+/*
+ /*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.emailcommon;
+
+public class AccountManagerTypes {
+ public static final String TYPE_EXCHANGE = "com.android.exchange";
+ public static final String TYPE_POP_IMAP = "com.android.email";
+}
diff --git a/email2/emailcommon/src/com/android/emailcommon/Api.java b/email2/emailcommon/src/com/android/emailcommon/Api.java
new file mode 100644
index 0000000..2b1e89b
--- /dev/null
+++ b/email2/emailcommon/src/com/android/emailcommon/Api.java
@@ -0,0 +1,28 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.emailcommon;
+
+/**
+ * This class will be used for API-related definitions; for now, just the api "level"
+ *
+ * Level 1: As shipped in HC/MR1
+ * Level 2: Adds searchMessages to EmailService
+ *
+ */
+public class Api {
+ public static final int LEVEL = 2;
+}
diff --git a/email2/emailcommon/src/com/android/emailcommon/CalendarProviderStub.java b/email2/emailcommon/src/com/android/emailcommon/CalendarProviderStub.java
new file mode 100644
index 0000000..eac371a
--- /dev/null
+++ b/email2/emailcommon/src/com/android/emailcommon/CalendarProviderStub.java
@@ -0,0 +1,25 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.emailcommon;
+
+/**
+ * This is the only non-SDK reference in the com.android.email project, referencing the once and
+ * future CalendarProvider authority name.
+ */
+public class CalendarProviderStub {
+ public static final String AUTHORITY = "com.android.calendar";
+}
diff --git a/email2/emailcommon/src/com/android/emailcommon/Configuration.java b/email2/emailcommon/src/com/android/emailcommon/Configuration.java
new file mode 100644
index 0000000..d9469d9
--- /dev/null
+++ b/email2/emailcommon/src/com/android/emailcommon/Configuration.java
@@ -0,0 +1,23 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.emailcommon;
+
+public class Configuration {
+ // Bundle key for Exchange configuration (boolean value)
+ public static final String EXCHANGE_CONFIGURATION_USE_ALTERNATE_STRINGS =
+ "com.android.email.EXCHANGE_CONFIGURATION_USE_ALTERNATE_STRINGS";
+}
diff --git a/email2/emailcommon/src/com/android/emailcommon/Device.java b/email2/emailcommon/src/com/android/emailcommon/Device.java
new file mode 100644
index 0000000..ba93062
--- /dev/null
+++ b/email2/emailcommon/src/com/android/emailcommon/Device.java
@@ -0,0 +1,112 @@
+/*
+ /*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.emailcommon;
+
+import android.content.Context;
+import android.telephony.TelephonyManager;
+import android.util.Log;
+
+import com.android.emailcommon.utility.Utility;
+
+import java.io.BufferedReader;
+import java.io.BufferedWriter;
+import java.io.File;
+import java.io.FileReader;
+import java.io.FileWriter;
+import java.io.IOException;
+
+public class Device {
+ private static String sDeviceId = null;
+
+ /**
+ * EAS requires a unique device id, so that sync is possible from a variety of different
+ * devices (e.g. the syncKey is specific to a device) If we're on an emulator or some other
+ * device that doesn't provide one, we can create it as android<n> where <n> is system time.
+ * This would work on a real device as well, but it would be better to use the "real" id if
+ * it's available
+ */
+ static public synchronized String getDeviceId(Context context) throws IOException {
+ if (sDeviceId == null) {
+ sDeviceId = getDeviceIdInternal(context);
+ }
+ return sDeviceId;
+ }
+
+ static private String getDeviceIdInternal(Context context) throws IOException {
+ if (context == null) {
+ throw new IllegalStateException("getDeviceId requires a Context");
+ }
+ File f = context.getFileStreamPath("deviceName");
+ BufferedReader rdr = null;
+ String id;
+ if (f.exists()) {
+ if (f.canRead()) {
+ rdr = new BufferedReader(new FileReader(f), 128);
+ id = rdr.readLine();
+ rdr.close();
+ if (id == null) {
+ // It's very bad if we read a null device id; let's delete that file
+ if (!f.delete()) {
+ Log.e(Logging.LOG_TAG, "Can't delete null deviceName file; try overwrite.");
+ }
+ } else {
+ return id;
+ }
+ } else {
+ Log.w(Logging.LOG_TAG, f.getAbsolutePath() + ": File exists, but can't read?" +
+ " Trying to remove.");
+ if (!f.delete()) {
+ Log.w(Logging.LOG_TAG, "Remove failed. Tring to overwrite.");
+ }
+ }
+ }
+ BufferedWriter w = new BufferedWriter(new FileWriter(f), 128);
+ final String consistentDeviceId = getConsistentDeviceId(context);
+ if (consistentDeviceId != null) {
+ // Use different prefix from random IDs.
+ id = "androidc" + consistentDeviceId;
+ } else {
+ id = "android" + System.currentTimeMillis();
+ }
+ w.write(id);
+ w.close();
+ return id;
+ }
+
+ /**
+ * @return Device's unique ID if available. null if the device has no unique ID.
+ */
+ public static String getConsistentDeviceId(Context context) {
+ final String deviceId;
+ try {
+ TelephonyManager tm =
+ (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE);
+ if (tm == null) {
+ return null;
+ }
+ deviceId = tm.getDeviceId();
+ if (deviceId == null) {
+ return null;
+ }
+ } catch (Exception e) {
+ Log.d(Logging.LOG_TAG, "Error in TelephonyManager.getDeviceId(): " + e.getMessage());
+ return null;
+ }
+ return Utility.getSmallHash(deviceId);
+ }
+}
diff --git a/email2/emailcommon/src/com/android/emailcommon/Logging.java b/email2/emailcommon/src/com/android/emailcommon/Logging.java
new file mode 100644
index 0000000..1fae76f
--- /dev/null
+++ b/email2/emailcommon/src/com/android/emailcommon/Logging.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.emailcommon;
+
+public class Logging {
+ public static final String LOG_TAG = "Email";
+
+ /**
+ * Set this to 'true' to enable as much Email logging as possible.
+ */
+ public static final boolean LOGD;
+
+ /**
+ * If this is enabled then logging that normally hides sensitive information
+ * like passwords will show that information.
+ */
+ public static final boolean DEBUG_SENSITIVE;
+
+ /**
+ * If true, logging regarding UI (such as activity/fragment lifecycle) will be enabled.
+ *
+ * TODO rename it to DEBUG_UI.
+ */
+ public static final boolean DEBUG_LIFECYCLE;
+
+ static {
+ // Declare values here to avoid dead code warnings; it means we have some extra
+ // "if" statements in the byte code that always evaluate to "if (false)"
+ LOGD = false; // DO NOT CHECK IN WITH TRUE
+ DEBUG_SENSITIVE = false; // DO NOT CHECK IN WITH TRUE
+ DEBUG_LIFECYCLE = false; // DO NOT CHECK IN WITH TRUE
+ }
+}
diff --git a/email2/emailcommon/src/com/android/emailcommon/TempDirectory.java b/email2/emailcommon/src/com/android/emailcommon/TempDirectory.java
new file mode 100644
index 0000000..252488c
--- /dev/null
+++ b/email2/emailcommon/src/com/android/emailcommon/TempDirectory.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.emailcommon;
+
+import android.content.Context;
+
+import java.io.File;
+
+/**
+ * TempDirectory caches the directory used for caching file. It is set up during application
+ * initialization.
+ */
+public class TempDirectory {
+ private static File sTempDirectory = null;
+
+ public static void setTempDirectory(Context context) {
+ sTempDirectory = context.getCacheDir();
+ }
+
+ public static File getTempDirectory() {
+ if (sTempDirectory == null) {
+ throw new RuntimeException(
+ "TempDirectory not set. " +
+ "If in a unit test, call Email.setTempDirectory(context) in setUp().");
+ }
+ return sTempDirectory;
+ }
+}
diff --git a/email2/emailcommon/src/com/android/emailcommon/TrafficFlags.java b/email2/emailcommon/src/com/android/emailcommon/TrafficFlags.java
new file mode 100644
index 0000000..c8c4e03
--- /dev/null
+++ b/email2/emailcommon/src/com/android/emailcommon/TrafficFlags.java
@@ -0,0 +1,113 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.emailcommon;
+
+import android.content.Context;
+
+import com.android.emailcommon.provider.Account;
+import com.android.emailcommon.provider.HostAuth;
+import com.android.emailcommon.utility.Utility;
+
+/**
+ * Constants for tagging threads for traffic stats, and associated utilities
+ *
+ * Example usage:
+ * TrafficStats.setThreadStatsTag(accountId | PROTOCOL_IMAP | DATA_EMAIL | REASON_SYNC);
+ */
+public class TrafficFlags {
+ // Bits 0->15, account id
+ private static final int ACCOUNT_MASK = 0x0000FFFF;
+
+ // Bits 16&17, protocol (0 = POP3)
+ private static final int PROTOCOL_SHIFT = 16;
+ private static final int PROTOCOL_MASK = 3 << PROTOCOL_SHIFT;
+ public static final int PROTOCOL_POP3 = 0 << PROTOCOL_SHIFT;
+ public static final int PROTOCOL_IMAP = 1 << PROTOCOL_SHIFT;
+ public static final int PROTOCOL_EAS = 2 << PROTOCOL_SHIFT;
+ public static final int PROTOCOL_SMTP = 3 << PROTOCOL_SHIFT;
+ private static final String[] PROTOCOLS = new String[] {HostAuth.SCHEME_POP3,
+ HostAuth.SCHEME_IMAP, HostAuth.SCHEME_EAS, HostAuth.SCHEME_SMTP};
+
+ // Bits 18&19, type (0 = EMAIL)
+ private static final int DATA_SHIFT = 18;
+ private static final int DATA_MASK = 3 << DATA_SHIFT;
+ public static final int DATA_EMAIL = 0 << DATA_SHIFT;
+ public static final int DATA_CONTACTS = 1 << DATA_SHIFT;
+ public static final int DATA_CALENDAR = 2 << DATA_SHIFT;
+
+ // Bits 20&21, reason (if protocol != SMTP)
+ private static final int REASON_SHIFT = 20;
+ private static final int REASON_MASK = 3 << REASON_SHIFT;
+ public static final int REASON_SYNC = 0 << REASON_SHIFT;
+ public static final int REASON_ATTACHMENT_USER = 1 << REASON_SHIFT;
+ // Note: We don't yet use the PRECACHE reason (it's complicated...)
+ public static final int REASON_ATTACHMENT_PRECACHE = 2 << REASON_SHIFT;
+ private static final String[] REASONS = new String[] {"sync", "attachment", "precache"};
+
+ /**
+ * Get flags indicating sync of the passed-in account; note that, by default, these flags
+ * indicate an email sync; to change the type of sync, simply "or" in DATA_CONTACTS or
+ * DATA_CALENDAR (since DATA_EMAIL = 0)
+ *
+ * @param context the caller's context
+ * @param account the account being used
+ * @return flags for syncing this account
+ */
+ public static int getSyncFlags(Context context, Account account) {
+ int protocolIndex = Utility.arrayIndex(PROTOCOLS, account.getProtocol(context));
+ return (int)account.mId | REASON_SYNC | (protocolIndex << PROTOCOL_SHIFT);
+ }
+
+ /**
+ * Get flags indicating attachment loading from the passed-in account
+ *
+ * @param context the caller's context
+ * @param account the account being used
+ * @return flags for loading an attachment in this account
+ */
+ public static int getAttachmentFlags(Context context, Account account) {
+ int protocolIndex = Utility.arrayIndex(PROTOCOLS, account.getProtocol(context));
+ return (int)account.mId | REASON_ATTACHMENT_USER | (protocolIndex << PROTOCOL_SHIFT);
+ }
+
+ /**
+ * Get flags indicating sending SMTP email from the passed-in account
+ *
+ * @param context the caller's context
+ * @param account the account being used
+ * @return flags for sending SMTP email from this account
+ */
+ public static int getSmtpFlags(Context context, Account account) {
+ return (int)account.mId | REASON_SYNC | PROTOCOL_SMTP;
+ }
+
+ public static String toString(int flags) {
+ StringBuilder sb = new StringBuilder();
+ sb.append("account ");
+ sb.append(flags & ACCOUNT_MASK);
+ sb.append(',');
+ sb.append(REASONS[(flags & REASON_MASK) >> REASON_SHIFT]);
+ sb.append(',');
+ sb.append(PROTOCOLS[(flags & PROTOCOL_MASK) >> PROTOCOL_SHIFT]);
+ int maskedData = flags & DATA_MASK;
+ if (maskedData != 0) {
+ sb.append(',');
+ sb.append(maskedData == DATA_CALENDAR ? "calendar" : "contacts");
+ }
+ return sb.toString();
+ }
+}
diff --git a/email2/emailcommon/src/com/android/emailcommon/internet/BinaryTempFileBody.java b/email2/emailcommon/src/com/android/emailcommon/internet/BinaryTempFileBody.java
new file mode 100644
index 0000000..f0821ed
--- /dev/null
+++ b/email2/emailcommon/src/com/android/emailcommon/internet/BinaryTempFileBody.java
@@ -0,0 +1,89 @@
+/*
+ * 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.emailcommon.internet;
+
+import com.android.emailcommon.TempDirectory;
+import com.android.emailcommon.mail.Body;
+import com.android.emailcommon.mail.MessagingException;
+
+import org.apache.commons.io.IOUtils;
+
+import android.util.Base64;
+import android.util.Base64OutputStream;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.FilterInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+
+/**
+ * A Body that is backed by a temp file. The Body exposes a getOutputStream method that allows
+ * the user to write to the temp file. After the write the body is available via getInputStream
+ * and writeTo one time. After writeTo is called, or the InputStream returned from
+ * getInputStream is closed the file is deleted and the Body should be considered disposed of.
+ */
+public class BinaryTempFileBody implements Body {
+ private File mFile;
+
+ /**
+ * An alternate way to put data into a BinaryTempFileBody is to simply supply an already-
+ * created file. Note that this file will be deleted after it is read.
+ * @param filePath The file containing the data to be stored on disk temporarily
+ */
+ public void setFile(String filePath) {
+ mFile = new File(filePath);
+ }
+
+ public OutputStream getOutputStream() throws IOException {
+ mFile = File.createTempFile("body", null, TempDirectory.getTempDirectory());
+ mFile.deleteOnExit();
+ return new FileOutputStream(mFile);
+ }
+
+ public InputStream getInputStream() throws MessagingException {
+ try {
+ return new BinaryTempFileBodyInputStream(new FileInputStream(mFile));
+ }
+ catch (IOException ioe) {
+ throw new MessagingException("Unable to open body", ioe);
+ }
+ }
+
+ public void writeTo(OutputStream out) throws IOException, MessagingException {
+ InputStream in = getInputStream();
+ Base64OutputStream base64Out = new Base64OutputStream(
+ out, Base64.CRLF | Base64.NO_CLOSE);
+ IOUtils.copy(in, base64Out);
+ base64Out.close();
+ mFile.delete();
+ }
+
+ class BinaryTempFileBodyInputStream extends FilterInputStream {
+ public BinaryTempFileBodyInputStream(InputStream in) {
+ super(in);
+ }
+
+ @Override
+ public void close() throws IOException {
+ super.close();
+ mFile.delete();
+ }
+ }
+}
diff --git a/email2/emailcommon/src/com/android/emailcommon/internet/MimeBodyPart.java b/email2/emailcommon/src/com/android/emailcommon/internet/MimeBodyPart.java
new file mode 100644
index 0000000..01efd55
--- /dev/null
+++ b/email2/emailcommon/src/com/android/emailcommon/internet/MimeBodyPart.java
@@ -0,0 +1,193 @@
+/*
+ * 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.emailcommon.internet;
+
+import com.android.emailcommon.mail.Body;
+import com.android.emailcommon.mail.BodyPart;
+import com.android.emailcommon.mail.MessagingException;
+
+import java.io.BufferedWriter;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.OutputStreamWriter;
+import java.util.regex.Pattern;
+
+/**
+ * TODO this is a close approximation of Message, need to update along with
+ * Message.
+ */
+public class MimeBodyPart extends BodyPart {
+ protected MimeHeader mHeader = new MimeHeader();
+ protected MimeHeader mExtendedHeader;
+ protected Body mBody;
+ protected int mSize;
+
+ // regex that matches content id surrounded by "<>" optionally.
+ private static final Pattern REMOVE_OPTIONAL_BRACKETS = Pattern.compile("^<?([^>]+)>?$");
+ // regex that matches end of line.
+ private static final Pattern END_OF_LINE = Pattern.compile("\r?\n");
+
+ public MimeBodyPart() throws MessagingException {
+ this(null);
+ }
+
+ public MimeBodyPart(Body body) throws MessagingException {
+ this(body, null);
+ }
+
+ public MimeBodyPart(Body body, String mimeType) throws MessagingException {
+ if (mimeType != null) {
+ setHeader(MimeHeader.HEADER_CONTENT_TYPE, mimeType);
+ }
+ setBody(body);
+ }
+
+ protected String getFirstHeader(String name) throws MessagingException {
+ return mHeader.getFirstHeader(name);
+ }
+
+ public void addHeader(String name, String value) throws MessagingException {
+ mHeader.addHeader(name, value);
+ }
+
+ public void setHeader(String name, String value) throws MessagingException {
+ mHeader.setHeader(name, value);
+ }
+
+ public String[] getHeader(String name) throws MessagingException {
+ return mHeader.getHeader(name);
+ }
+
+ public void removeHeader(String name) throws MessagingException {
+ mHeader.removeHeader(name);
+ }
+
+ public Body getBody() throws MessagingException {
+ return mBody;
+ }
+
+ public void setBody(Body body) throws MessagingException {
+ this.mBody = body;
+ if (body instanceof com.android.emailcommon.mail.Multipart) {
+ com.android.emailcommon.mail.Multipart multipart =
+ ((com.android.emailcommon.mail.Multipart)body);
+ multipart.setParent(this);
+ setHeader(MimeHeader.HEADER_CONTENT_TYPE, multipart.getContentType());
+ }
+ else if (body instanceof TextBody) {
+ String contentType = String.format("%s;\n charset=utf-8", getMimeType());
+ String name = MimeUtility.getHeaderParameter(getContentType(), "name");
+ if (name != null) {
+ contentType += String.format(";\n name=\"%s\"", name);
+ }
+ setHeader(MimeHeader.HEADER_CONTENT_TYPE, contentType);
+ setHeader(MimeHeader.HEADER_CONTENT_TRANSFER_ENCODING, "base64");
+ }
+ }
+
+ public String getContentType() throws MessagingException {
+ String contentType = getFirstHeader(MimeHeader.HEADER_CONTENT_TYPE);
+ if (contentType == null) {
+ return "text/plain";
+ } else {
+ return contentType;
+ }
+ }
+
+ public String getDisposition() throws MessagingException {
+ String contentDisposition = getFirstHeader(MimeHeader.HEADER_CONTENT_DISPOSITION);
+ if (contentDisposition == null) {
+ return null;
+ } else {
+ return contentDisposition;
+ }
+ }
+
+ public String getContentId() throws MessagingException {
+ String contentId = getFirstHeader(MimeHeader.HEADER_CONTENT_ID);
+ if (contentId == null) {
+ return null;
+ } else {
+ // remove optionally surrounding brackets.
+ return REMOVE_OPTIONAL_BRACKETS.matcher(contentId).replaceAll("$1");
+ }
+ }
+
+ public String getMimeType() throws MessagingException {
+ return MimeUtility.getHeaderParameter(getContentType(), null);
+ }
+
+ public boolean isMimeType(String mimeType) throws MessagingException {
+ return getMimeType().equals(mimeType);
+ }
+
+ public void setSize(int size) {
+ this.mSize = size;
+ }
+
+ public int getSize() throws MessagingException {
+ return mSize;
+ }
+
+ /**
+ * Set extended header
+ *
+ * @param name Extended header name
+ * @param value header value - flattened by removing CR-NL if any
+ * remove header if value is null
+ * @throws MessagingException
+ */
+ public void setExtendedHeader(String name, String value) throws MessagingException {
+ if (value == null) {
+ if (mExtendedHeader != null) {
+ mExtendedHeader.removeHeader(name);
+ }
+ return;
+ }
+ if (mExtendedHeader == null) {
+ mExtendedHeader = new MimeHeader();
+ }
+ mExtendedHeader.setHeader(name, END_OF_LINE.matcher(value).replaceAll(""));
+ }
+
+ /**
+ * Get extended header
+ *
+ * @param name Extended header name
+ * @return header value - null if header does not exist
+ * @throws MessagingException
+ */
+ public String getExtendedHeader(String name) throws MessagingException {
+ if (mExtendedHeader == null) {
+ return null;
+ }
+ return mExtendedHeader.getFirstHeader(name);
+ }
+
+ /**
+ * Write the MimeMessage out in MIME format.
+ */
+ public void writeTo(OutputStream out) throws IOException, MessagingException {
+ BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(out), 1024);
+ mHeader.writeTo(out);
+ writer.write("\r\n");
+ writer.flush();
+ if (mBody != null) {
+ mBody.writeTo(out);
+ }
+ }
+}
diff --git a/email2/emailcommon/src/com/android/emailcommon/internet/MimeHeader.java b/email2/emailcommon/src/com/android/emailcommon/internet/MimeHeader.java
new file mode 100644
index 0000000..b0ad777
--- /dev/null
+++ b/email2/emailcommon/src/com/android/emailcommon/internet/MimeHeader.java
@@ -0,0 +1,153 @@
+/*
+ * 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.emailcommon.internet;
+
+import com.android.emailcommon.mail.MessagingException;
+import com.android.emailcommon.utility.Utility;
+
+import java.io.BufferedWriter;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.OutputStreamWriter;
+import java.util.ArrayList;
+
+public class MimeHeader {
+ /**
+ * Application specific header that contains Store specific information about an attachment.
+ * In IMAP this contains the IMAP BODYSTRUCTURE part id so that the ImapStore can later
+ * retrieve the attachment at will from the server.
+ * The info is recorded from this header on LocalStore.appendMessages and is put back
+ * into the MIME data by LocalStore.fetch.
+ */
+ public static final String HEADER_ANDROID_ATTACHMENT_STORE_DATA = "X-Android-Attachment-StoreData";
+ /**
+ * Application specific header that is used to tag body parts for quoted/forwarded messages.
+ */
+ public static final String HEADER_ANDROID_BODY_QUOTED_PART = "X-Android-Body-Quoted-Part";
+
+ public static final String HEADER_CONTENT_TYPE = "Content-Type";
+ public static final String HEADER_CONTENT_TRANSFER_ENCODING = "Content-Transfer-Encoding";
+ public static final String HEADER_CONTENT_DISPOSITION = "Content-Disposition";
+ public static final String HEADER_CONTENT_ID = "Content-ID";
+
+ /**
+ * Fields that should be omitted when writing the header using writeTo()
+ */
+ private static final String[] WRITE_OMIT_FIELDS = {
+// HEADER_ANDROID_ATTACHMENT_DOWNLOADED,
+// HEADER_ANDROID_ATTACHMENT_ID,
+ HEADER_ANDROID_ATTACHMENT_STORE_DATA
+ };
+
+ protected final ArrayList<Field> mFields = new ArrayList<Field>();
+
+ public void clear() {
+ mFields.clear();
+ }
+
+ public String getFirstHeader(String name) throws MessagingException {
+ String[] header = getHeader(name);
+ if (header == null) {
+ return null;
+ }
+ return header[0];
+ }
+
+ public void addHeader(String name, String value) throws MessagingException {
+ mFields.add(new Field(name, value));
+ }
+
+ public void setHeader(String name, String value) throws MessagingException {
+ if (name == null || value == null) {
+ return;
+ }
+ removeHeader(name);
+ addHeader(name, value);
+ }
+
+ public String[] getHeader(String name) throws MessagingException {
+ ArrayList<String> values = new ArrayList<String>();
+ for (Field field : mFields) {
+ if (field.name.equalsIgnoreCase(name)) {
+ values.add(field.value);
+ }
+ }
+ if (values.size() == 0) {
+ return null;
+ }
+ return values.toArray(new String[] {});
+ }
+
+ public void removeHeader(String name) throws MessagingException {
+ ArrayList<Field> removeFields = new ArrayList<Field>();
+ for (Field field : mFields) {
+ if (field.name.equalsIgnoreCase(name)) {
+ removeFields.add(field);
+ }
+ }
+ mFields.removeAll(removeFields);
+ }
+
+ /**
+ * Write header into String
+ *
+ * @return CR-NL separated header string except the headers in writeOmitFields
+ * null if header is empty
+ */
+ public String writeToString() {
+ if (mFields.size() == 0) {
+ return null;
+ }
+ StringBuilder builder = new StringBuilder();
+ for (Field field : mFields) {
+ if (!Utility.arrayContains(WRITE_OMIT_FIELDS, field.name)) {
+ builder.append(field.name + ": " + field.value + "\r\n");
+ }
+ }
+ return builder.toString();
+ }
+
+ public void writeTo(OutputStream out) throws IOException, MessagingException {
+ BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(out), 1024);
+ for (Field field : mFields) {
+ if (!Utility.arrayContains(WRITE_OMIT_FIELDS, field.name)) {
+ writer.write(field.name + ": " + field.value + "\r\n");
+ }
+ }
+ writer.flush();
+ }
+
+ private static class Field {
+ final String name;
+ final String value;
+
+ public Field(String name, String value) {
+ this.name = name;
+ this.value = value;
+ }
+
+ @Override
+ public String toString() {
+ return name + "=" + value;
+ }
+ }
+
+ @Override
+ public String toString() {
+ return (mFields == null) ? null : mFields.toString();
+ }
+}
diff --git a/email2/emailcommon/src/com/android/emailcommon/internet/MimeMessage.java b/email2/emailcommon/src/com/android/emailcommon/internet/MimeMessage.java
new file mode 100644
index 0000000..412092d
--- /dev/null
+++ b/email2/emailcommon/src/com/android/emailcommon/internet/MimeMessage.java
@@ -0,0 +1,626 @@
+/*
+ * 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.emailcommon.internet;
+
+import com.android.emailcommon.mail.Address;
+import com.android.emailcommon.mail.Body;
+import com.android.emailcommon.mail.BodyPart;
+import com.android.emailcommon.mail.Message;
+import com.android.emailcommon.mail.MessagingException;
+import com.android.emailcommon.mail.Multipart;
+import com.android.emailcommon.mail.Part;
+
+import org.apache.james.mime4j.BodyDescriptor;
+import org.apache.james.mime4j.ContentHandler;
+import org.apache.james.mime4j.EOLConvertingInputStream;
+import org.apache.james.mime4j.MimeStreamParser;
+import org.apache.james.mime4j.field.DateTimeField;
+import org.apache.james.mime4j.field.Field;
+
+import android.text.TextUtils;
+
+import java.io.BufferedWriter;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.io.OutputStreamWriter;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.Locale;
+import java.util.Stack;
+import java.util.regex.Pattern;
+
+/**
+ * An implementation of Message that stores all of its metadata in RFC 822 and
+ * RFC 2045 style headers.
+ *
+ * NOTE: Automatic generation of a local message-id is becoming unwieldy and should be removed.
+ * It would be better to simply do it explicitly on local creation of new outgoing messages.
+ */
+public class MimeMessage extends Message {
+ private MimeHeader mHeader;
+ private MimeHeader mExtendedHeader;
+
+ // NOTE: The fields here are transcribed out of headers, and values stored here will supercede
+ // the values found in the headers. Use caution to prevent any out-of-phase errors. In
+ // particular, any adds/changes/deletes here must be echoed by changes in the parse() function.
+ private Address[] mFrom;
+ private Address[] mTo;
+ private Address[] mCc;
+ private Address[] mBcc;
+ private Address[] mReplyTo;
+ private Date mSentDate;
+ private Body mBody;
+ protected int mSize;
+ private boolean mInhibitLocalMessageId = false;
+
+ // Shared random source for generating local message-id values
+ private static final java.util.Random sRandom = new java.util.Random();
+
+ // In MIME, en_US-like date format should be used. In other words "MMM" should be encoded to
+ // "Jan", not the other localized format like "Ene" (meaning January in locale es).
+ // This conversion is used when generating outgoing MIME messages. Incoming MIME date
+ // headers are parsed by org.apache.james.mime4j.field.DateTimeField which does not have any
+ // localization code.
+ private static final SimpleDateFormat DATE_FORMAT =
+ new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss Z", Locale.US);
+
+ // regex that matches content id surrounded by "<>" optionally.
+ private static final Pattern REMOVE_OPTIONAL_BRACKETS = Pattern.compile("^<?([^>]+)>?$");
+ // regex that matches end of line.
+ private static final Pattern END_OF_LINE = Pattern.compile("\r?\n");
+
+ public MimeMessage() {
+ mHeader = null;
+ }
+
+ /**
+ * Generate a local message id. This is only used when none has been assigned, and is
+ * installed lazily. Any remote (typically server-assigned) message id takes precedence.
+ * @return a long, locally-generated message-ID value
+ */
+ private String generateMessageId() {
+ StringBuffer sb = new StringBuffer();
+ sb.append("<");
+ for (int i = 0; i < 24; i++) {
+ // We'll use a 5-bit range (0..31)
+ int value = sRandom.nextInt() & 31;
+ char c = "0123456789abcdefghijklmnopqrstuv".charAt(value);
+ sb.append(c);
+ }
+ sb.append(".");
+ sb.append(Long.toString(System.currentTimeMillis()));
+ sb.append("@email.android.com>");
+ return sb.toString();
+ }
+
+ /**
+ * Parse the given InputStream using Apache Mime4J to build a MimeMessage.
+ *
+ * @param in
+ * @throws IOException
+ * @throws MessagingException
+ */
+ public MimeMessage(InputStream in) throws IOException, MessagingException {
+ parse(in);
+ }
+
+ protected void parse(InputStream in) throws IOException, MessagingException {
+ // Before parsing the input stream, clear all local fields that may be superceded by
+ // the new incoming message.
+ getMimeHeaders().clear();
+ mInhibitLocalMessageId = true;
+ mFrom = null;
+ mTo = null;
+ mCc = null;
+ mBcc = null;
+ mReplyTo = null;
+ mSentDate = null;
+ mBody = null;
+
+ MimeStreamParser parser = new MimeStreamParser();
+ parser.setContentHandler(new MimeMessageBuilder());
+ parser.parse(new EOLConvertingInputStream(in));
+ }
+
+ /**
+ * Return the internal mHeader value, with very lazy initialization.
+ * The goal is to save memory by not creating the headers until needed.
+ */
+ private MimeHeader getMimeHeaders() {
+ if (mHeader == null) {
+ mHeader = new MimeHeader();
+ }
+ return mHeader;
+ }
+
+ @Override
+ public Date getReceivedDate() throws MessagingException {
+ return null;
+ }
+
+ @Override
+ public Date getSentDate() throws MessagingException {
+ if (mSentDate == null) {
+ try {
+ DateTimeField field = (DateTimeField)Field.parse("Date: "
+ + MimeUtility.unfoldAndDecode(getFirstHeader("Date")));
+ mSentDate = field.getDate();
+ } catch (Exception e) {
+
+ }
+ }
+ return mSentDate;
+ }
+
+ @Override
+ public void setSentDate(Date sentDate) throws MessagingException {
+ setHeader("Date", DATE_FORMAT.format(sentDate));
+ this.mSentDate = sentDate;
+ }
+
+ @Override
+ public String getContentType() throws MessagingException {
+ String contentType = getFirstHeader(MimeHeader.HEADER_CONTENT_TYPE);
+ if (contentType == null) {
+ return "text/plain";
+ } else {
+ return contentType;
+ }
+ }
+
+ public String getDisposition() throws MessagingException {
+ String contentDisposition = getFirstHeader(MimeHeader.HEADER_CONTENT_DISPOSITION);
+ if (contentDisposition == null) {
+ return null;
+ } else {
+ return contentDisposition;
+ }
+ }
+
+ public String getContentId() throws MessagingException {
+ String contentId = getFirstHeader(MimeHeader.HEADER_CONTENT_ID);
+ if (contentId == null) {
+ return null;
+ } else {
+ // remove optionally surrounding brackets.
+ return REMOVE_OPTIONAL_BRACKETS.matcher(contentId).replaceAll("$1");
+ }
+ }
+
+ public String getMimeType() throws MessagingException {
+ return MimeUtility.getHeaderParameter(getContentType(), null);
+ }
+
+ public int getSize() throws MessagingException {
+ return mSize;
+ }
+
+ /**
+ * Returns a list of the given recipient type from this message. If no addresses are
+ * found the method returns an empty array.
+ */
+ @Override
+ public Address[] getRecipients(RecipientType type) throws MessagingException {
+ if (type == RecipientType.TO) {
+ if (mTo == null) {
+ mTo = Address.parse(MimeUtility.unfold(getFirstHeader("To")));
+ }
+ return mTo;
+ } else if (type == RecipientType.CC) {
+ if (mCc == null) {
+ mCc = Address.parse(MimeUtility.unfold(getFirstHeader("CC")));
+ }
+ return mCc;
+ } else if (type == RecipientType.BCC) {
+ if (mBcc == null) {
+ mBcc = Address.parse(MimeUtility.unfold(getFirstHeader("BCC")));
+ }
+ return mBcc;
+ } else {
+ throw new MessagingException("Unrecognized recipient type.");
+ }
+ }
+
+ @Override
+ public void setRecipients(RecipientType type, Address[] addresses) throws MessagingException {
+ final int TO_LENGTH = 4; // "To: "
+ final int CC_LENGTH = 4; // "Cc: "
+ final int BCC_LENGTH = 5; // "Bcc: "
+ if (type == RecipientType.TO) {
+ if (addresses == null || addresses.length == 0) {
+ removeHeader("To");
+ this.mTo = null;
+ } else {
+ setHeader("To", MimeUtility.fold(Address.toHeader(addresses), TO_LENGTH));
+ this.mTo = addresses;
+ }
+ } else if (type == RecipientType.CC) {
+ if (addresses == null || addresses.length == 0) {
+ removeHeader("CC");
+ this.mCc = null;
+ } else {
+ setHeader("CC", MimeUtility.fold(Address.toHeader(addresses), CC_LENGTH));
+ this.mCc = addresses;
+ }
+ } else if (type == RecipientType.BCC) {
+ if (addresses == null || addresses.length == 0) {
+ removeHeader("BCC");
+ this.mBcc = null;
+ } else {
+ setHeader("BCC", MimeUtility.fold(Address.toHeader(addresses), BCC_LENGTH));
+ this.mBcc = addresses;
+ }
+ } else {
+ throw new MessagingException("Unrecognized recipient type.");
+ }
+ }
+
+ /**
+ * Returns the unfolded, decoded value of the Subject header.
+ */
+ @Override
+ public String getSubject() throws MessagingException {
+ return MimeUtility.unfoldAndDecode(getFirstHeader("Subject"));
+ }
+
+ @Override
+ public void setSubject(String subject) throws MessagingException {
+ final int HEADER_NAME_LENGTH = 9; // "Subject: "
+ setHeader("Subject", MimeUtility.foldAndEncode2(subject, HEADER_NAME_LENGTH));
+ }
+
+ @Override
+ public Address[] getFrom() throws MessagingException {
+ if (mFrom == null) {
+ String list = MimeUtility.unfold(getFirstHeader("From"));
+ if (list == null || list.length() == 0) {
+ list = MimeUtility.unfold(getFirstHeader("Sender"));
+ }
+ mFrom = Address.parse(list);
+ }
+ return mFrom;
+ }
+
+ @Override
+ public void setFrom(Address from) throws MessagingException {
+ final int FROM_LENGTH = 6; // "From: "
+ if (from != null) {
+ setHeader("From", MimeUtility.fold(from.toHeader(), FROM_LENGTH));
+ this.mFrom = new Address[] {
+ from
+ };
+ } else {
+ this.mFrom = null;
+ }
+ }
+
+ @Override
+ public Address[] getReplyTo() throws MessagingException {
+ if (mReplyTo == null) {
+ mReplyTo = Address.parse(MimeUtility.unfold(getFirstHeader("Reply-to")));
+ }
+ return mReplyTo;
+ }
+
+ @Override
+ public void setReplyTo(Address[] replyTo) throws MessagingException {
+ final int REPLY_TO_LENGTH = 10; // "Reply-to: "
+ if (replyTo == null || replyTo.length == 0) {
+ removeHeader("Reply-to");
+ mReplyTo = null;
+ } else {
+ setHeader("Reply-to", MimeUtility.fold(Address.toHeader(replyTo), REPLY_TO_LENGTH));
+ mReplyTo = replyTo;
+ }
+ }
+
+ /**
+ * Set the mime "Message-ID" header
+ * @param messageId the new Message-ID value
+ * @throws MessagingException
+ */
+ @Override
+ public void setMessageId(String messageId) throws MessagingException {
+ setHeader("Message-ID", messageId);
+ }
+
+ /**
+ * Get the mime "Message-ID" header. This value will be preloaded with a locally-generated
+ * random ID, if the value has not previously been set. Local generation can be inhibited/
+ * overridden by explicitly clearing the headers, removing the message-id header, etc.
+ * @return the Message-ID header string, or null if explicitly has been set to null
+ */
+ @Override
+ public String getMessageId() throws MessagingException {
+ String messageId = getFirstHeader("Message-ID");
+ if (messageId == null && !mInhibitLocalMessageId) {
+ messageId = generateMessageId();
+ setMessageId(messageId);
+ }
+ return messageId;
+ }
+
+ @Override
+ public void saveChanges() throws MessagingException {
+ throw new MessagingException("saveChanges not yet implemented");
+ }
+
+ @Override
+ public Body getBody() throws MessagingException {
+ return mBody;
+ }
+
+ @Override
+ public void setBody(Body body) throws MessagingException {
+ this.mBody = body;
+ if (body instanceof Multipart) {
+ Multipart multipart = ((Multipart)body);
+ multipart.setParent(this);
+ setHeader(MimeHeader.HEADER_CONTENT_TYPE, multipart.getContentType());
+ setHeader("MIME-Version", "1.0");
+ }
+ else if (body instanceof TextBody) {
+ setHeader(MimeHeader.HEADER_CONTENT_TYPE, String.format("%s;\n charset=utf-8",
+ getMimeType()));
+ setHeader(MimeHeader.HEADER_CONTENT_TRANSFER_ENCODING, "base64");
+ }
+ }
+
+ protected String getFirstHeader(String name) throws MessagingException {
+ return getMimeHeaders().getFirstHeader(name);
+ }
+
+ @Override
+ public void addHeader(String name, String value) throws MessagingException {
+ getMimeHeaders().addHeader(name, value);
+ }
+
+ @Override
+ public void setHeader(String name, String value) throws MessagingException {
+ getMimeHeaders().setHeader(name, value);
+ }
+
+ @Override
+ public String[] getHeader(String name) throws MessagingException {
+ return getMimeHeaders().getHeader(name);
+ }
+
+ @Override
+ public void removeHeader(String name) throws MessagingException {
+ getMimeHeaders().removeHeader(name);
+ if ("Message-ID".equalsIgnoreCase(name)) {
+ mInhibitLocalMessageId = true;
+ }
+ }
+
+ /**
+ * Set extended header
+ *
+ * @param name Extended header name
+ * @param value header value - flattened by removing CR-NL if any
+ * remove header if value is null
+ * @throws MessagingException
+ */
+ public void setExtendedHeader(String name, String value) throws MessagingException {
+ if (value == null) {
+ if (mExtendedHeader != null) {
+ mExtendedHeader.removeHeader(name);
+ }
+ return;
+ }
+ if (mExtendedHeader == null) {
+ mExtendedHeader = new MimeHeader();
+ }
+ mExtendedHeader.setHeader(name, END_OF_LINE.matcher(value).replaceAll(""));
+ }
+
+ /**
+ * Get extended header
+ *
+ * @param name Extended header name
+ * @return header value - null if header does not exist
+ * @throws MessagingException
+ */
+ public String getExtendedHeader(String name) throws MessagingException {
+ if (mExtendedHeader == null) {
+ return null;
+ }
+ return mExtendedHeader.getFirstHeader(name);
+ }
+
+ /**
+ * Set entire extended headers from String
+ *
+ * @param headers Extended header and its value - "CR-NL-separated pairs
+ * if null or empty, remove entire extended headers
+ * @throws MessagingException
+ */
+ public void setExtendedHeaders(String headers) throws MessagingException {
+ if (TextUtils.isEmpty(headers)) {
+ mExtendedHeader = null;
+ } else {
+ mExtendedHeader = new MimeHeader();
+ for (String header : END_OF_LINE.split(headers)) {
+ String[] tokens = header.split(":", 2);
+ if (tokens.length != 2) {
+ throw new MessagingException("Illegal extended headers: " + headers);
+ }
+ mExtendedHeader.setHeader(tokens[0].trim(), tokens[1].trim());
+ }
+ }
+ }
+
+ /**
+ * Get entire extended headers as String
+ *
+ * @return "CR-NL-separated extended headers - null if extended header does not exist
+ */
+ public String getExtendedHeaders() {
+ if (mExtendedHeader != null) {
+ return mExtendedHeader.writeToString();
+ }
+ return null;
+ }
+
+ /**
+ * Write message header and body to output stream
+ *
+ * @param out Output steam to write message header and body.
+ */
+ public void writeTo(OutputStream out) throws IOException, MessagingException {
+ BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(out), 1024);
+ // Force creation of local message-id
+ getMessageId();
+ getMimeHeaders().writeTo(out);
+ // mExtendedHeader will not be write out to external output stream,
+ // because it is intended to internal use.
+ writer.write("\r\n");
+ writer.flush();
+ if (mBody != null) {
+ mBody.writeTo(out);
+ }
+ }
+
+ public InputStream getInputStream() throws MessagingException {
+ return null;
+ }
+
+ class MimeMessageBuilder implements ContentHandler {
+ private Stack<Object> stack = new Stack<Object>();
+
+ public MimeMessageBuilder() {
+ }
+
+ private void expect(Class c) {
+ if (!c.isInstance(stack.peek())) {
+ throw new IllegalStateException("Internal stack error: " + "Expected '"
+ + c.getName() + "' found '" + stack.peek().getClass().getName() + "'");
+ }
+ }
+
+ public void startMessage() {
+ if (stack.isEmpty()) {
+ stack.push(MimeMessage.this);
+ } else {
+ expect(Part.class);
+ try {
+ MimeMessage m = new MimeMessage();
+ ((Part)stack.peek()).setBody(m);
+ stack.push(m);
+ } catch (MessagingException me) {
+ throw new Error(me);
+ }
+ }
+ }
+
+ public void endMessage() {
+ expect(MimeMessage.class);
+ stack.pop();
+ }
+
+ public void startHeader() {
+ expect(Part.class);
+ }
+
+ public void field(String fieldData) {
+ expect(Part.class);
+ try {
+ String[] tokens = fieldData.split(":", 2);
+ ((Part)stack.peek()).addHeader(tokens[0], tokens[1].trim());
+ } catch (MessagingException me) {
+ throw new Error(me);
+ }
+ }
+
+ public void endHeader() {
+ expect(Part.class);
+ }
+
+ public void startMultipart(BodyDescriptor bd) {
+ expect(Part.class);
+
+ Part e = (Part)stack.peek();
+ try {
+ MimeMultipart multiPart = new MimeMultipart(e.getContentType());
+ e.setBody(multiPart);
+ stack.push(multiPart);
+ } catch (MessagingException me) {
+ throw new Error(me);
+ }
+ }
+
+ public void body(BodyDescriptor bd, InputStream in) throws IOException {
+ expect(Part.class);
+ Body body = MimeUtility.decodeBody(in, bd.getTransferEncoding());
+ try {
+ ((Part)stack.peek()).setBody(body);
+ } catch (MessagingException me) {
+ throw new Error(me);
+ }
+ }
+
+ public void endMultipart() {
+ stack.pop();
+ }
+
+ public void startBodyPart() {
+ expect(MimeMultipart.class);
+
+ try {
+ MimeBodyPart bodyPart = new MimeBodyPart();
+ ((MimeMultipart)stack.peek()).addBodyPart(bodyPart);
+ stack.push(bodyPart);
+ } catch (MessagingException me) {
+ throw new Error(me);
+ }
+ }
+
+ public void endBodyPart() {
+ expect(BodyPart.class);
+ stack.pop();
+ }
+
+ public void epilogue(InputStream is) throws IOException {
+ expect(MimeMultipart.class);
+ StringBuffer sb = new StringBuffer();
+ int b;
+ while ((b = is.read()) != -1) {
+ sb.append((char)b);
+ }
+ // ((Multipart) stack.peek()).setEpilogue(sb.toString());
+ }
+
+ public void preamble(InputStream is) throws IOException {
+ expect(MimeMultipart.class);
+ StringBuffer sb = new StringBuffer();
+ int b;
+ while ((b = is.read()) != -1) {
+ sb.append((char)b);
+ }
+ try {
+ ((MimeMultipart)stack.peek()).setPreamble(sb.toString());
+ } catch (MessagingException me) {
+ throw new Error(me);
+ }
+ }
+
+ public void raw(InputStream is) throws IOException {
+ throw new UnsupportedOperationException("Not supported");
+ }
+ }
+}
diff --git a/email2/emailcommon/src/com/android/emailcommon/internet/MimeMultipart.java b/email2/emailcommon/src/com/android/emailcommon/internet/MimeMultipart.java
new file mode 100644
index 0000000..e6977ee
--- /dev/null
+++ b/email2/emailcommon/src/com/android/emailcommon/internet/MimeMultipart.java
@@ -0,0 +1,111 @@
+/*
+ * 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.emailcommon.internet;
+
+import com.android.emailcommon.mail.BodyPart;
+import com.android.emailcommon.mail.MessagingException;
+import com.android.emailcommon.mail.Multipart;
+
+import java.io.BufferedWriter;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.io.OutputStreamWriter;
+
+public class MimeMultipart extends Multipart {
+ protected String mPreamble;
+
+ protected String mContentType;
+
+ protected String mBoundary;
+
+ protected String mSubType;
+
+ public MimeMultipart() throws MessagingException {
+ mBoundary = generateBoundary();
+ setSubType("mixed");
+ }
+
+ public MimeMultipart(String contentType) throws MessagingException {
+ this.mContentType = contentType;
+ try {
+ mSubType = MimeUtility.getHeaderParameter(contentType, null).split("/")[1];
+ mBoundary = MimeUtility.getHeaderParameter(contentType, "boundary");
+ if (mBoundary == null) {
+ throw new MessagingException("MultiPart does not contain boundary: " + contentType);
+ }
+ } catch (Exception e) {
+ throw new MessagingException(
+ "Invalid MultiPart Content-Type; must contain subtype and boundary. ("
+ + contentType + ")", e);
+ }
+ }
+
+ public String generateBoundary() {
+ StringBuffer sb = new StringBuffer();
+ sb.append("----");
+ for (int i = 0; i < 30; i++) {
+ sb.append(Integer.toString((int)(Math.random() * 35), 36));
+ }
+ return sb.toString().toUpperCase();
+ }
+
+ public String getPreamble() throws MessagingException {
+ return mPreamble;
+ }
+
+ public void setPreamble(String preamble) throws MessagingException {
+ this.mPreamble = preamble;
+ }
+
+ @Override
+ public String getContentType() throws MessagingException {
+ return mContentType;
+ }
+
+ public void setSubType(String subType) throws MessagingException {
+ this.mSubType = subType;
+ mContentType = String.format("multipart/%s; boundary=\"%s\"", subType, mBoundary);
+ }
+
+ public void writeTo(OutputStream out) throws IOException, MessagingException {
+ BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(out), 1024);
+
+ if (mPreamble != null) {
+ writer.write(mPreamble + "\r\n");
+ }
+
+ for (int i = 0, count = mParts.size(); i < count; i++) {
+ BodyPart bodyPart = mParts.get(i);
+ writer.write("--" + mBoundary + "\r\n");
+ writer.flush();
+ bodyPart.writeTo(out);
+ writer.write("\r\n");
+ }
+
+ writer.write("--" + mBoundary + "--\r\n");
+ writer.flush();
+ }
+
+ public InputStream getInputStream() throws MessagingException {
+ return null;
+ }
+
+ public String getSubTypeForTest() {
+ return mSubType;
+ }
+}
diff --git a/email2/emailcommon/src/com/android/emailcommon/internet/MimeUtility.java b/email2/emailcommon/src/com/android/emailcommon/internet/MimeUtility.java
new file mode 100644
index 0000000..3e1488d
--- /dev/null
+++ b/email2/emailcommon/src/com/android/emailcommon/internet/MimeUtility.java
@@ -0,0 +1,461 @@
+/*
+ * 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.emailcommon.internet;
+
+import com.android.emailcommon.Logging;
+import com.android.emailcommon.mail.Body;
+import com.android.emailcommon.mail.BodyPart;
+import com.android.emailcommon.mail.Message;
+import com.android.emailcommon.mail.MessagingException;
+import com.android.emailcommon.mail.Multipart;
+import com.android.emailcommon.mail.Part;
+
+import org.apache.commons.io.IOUtils;
+import org.apache.james.mime4j.codec.EncoderUtil;
+import org.apache.james.mime4j.decoder.DecoderUtil;
+import org.apache.james.mime4j.decoder.QuotedPrintableInputStream;
+import org.apache.james.mime4j.util.CharsetUtil;
+
+import android.util.Base64;
+import android.util.Base64DataException;
+import android.util.Base64InputStream;
+import android.util.Log;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.util.ArrayList;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+public class MimeUtility {
+
+ public static final String MIME_TYPE_RFC822 = "message/rfc822";
+ private final static Pattern PATTERN_CR_OR_LF = Pattern.compile("\r|\n");
+
+ /**
+ * Replace sequences of CRLF+WSP with WSP. Tries to preserve original string
+ * object whenever possible.
+ */
+ public static String unfold(String s) {
+ if (s == null) {
+ return null;
+ }
+ Matcher patternMatcher = PATTERN_CR_OR_LF.matcher(s);
+ if (patternMatcher.find()) {
+ patternMatcher.reset();
+ s = patternMatcher.replaceAll("");
+ }
+ return s;
+ }
+
+ public static String decode(String s) {
+ if (s == null) {
+ return null;
+ }
+ return DecoderUtil.decodeEncodedWords(s);
+ }
+
+ public static String unfoldAndDecode(String s) {
+ return decode(unfold(s));
+ }
+
+ // TODO implement proper foldAndEncode
+ // NOTE: When this really works, we *must* remove all calls to foldAndEncode2() to prevent
+ // duplication of encoding.
+ public static String foldAndEncode(String s) {
+ return s;
+ }
+
+ /**
+ * INTERIM version of foldAndEncode that will be used only by Subject: headers.
+ * This is safer than implementing foldAndEncode() (see above) and risking unknown damage
+ * to other headers.
+ *
+ * TODO: Copy this code to foldAndEncode(), get rid of this function, confirm all working OK.
+ *
+ * @param s original string to encode and fold
+ * @param usedCharacters number of characters already used up by header name
+
+ * @return the String ready to be transmitted
+ */
+ public static String foldAndEncode2(String s, int usedCharacters) {
+ // james.mime4j.codec.EncoderUtil.java
+ // encode: encodeIfNecessary(text, usage, numUsedInHeaderName)
+ // Usage.TEXT_TOKENlooks like the right thing for subjects
+ // use WORD_ENTITY for address/names
+
+ String encoded = EncoderUtil.encodeIfNecessary(s, EncoderUtil.Usage.TEXT_TOKEN,
+ usedCharacters);
+
+ return fold(encoded, usedCharacters);
+ }
+
+ /**
+ * INTERIM: From newer version of org.apache.james (but we don't want to import
+ * the entire MimeUtil class).
+ *
+ * Splits the specified string into a multiple-line representation with
+ * lines no longer than 76 characters (because the line might contain
+ * encoded words; see <a href='http://www.faqs.org/rfcs/rfc2047.html'>RFC
+ * 2047</a> section 2). If the string contains non-whitespace sequences
+ * longer than 76 characters a line break is inserted at the whitespace
+ * character following the sequence resulting in a line longer than 76
+ * characters.
+ *
+ * @param s
+ * string to split.
+ * @param usedCharacters
+ * number of characters already used up. Usually the number of
+ * characters for header field name plus colon and one space.
+ * @return a multiple-line representation of the given string.
+ */
+ public static String fold(String s, int usedCharacters) {
+ final int maxCharacters = 76;
+
+ final int length = s.length();
+ if (usedCharacters + length <= maxCharacters)
+ return s;
+
+ StringBuilder sb = new StringBuilder();
+
+ int lastLineBreak = -usedCharacters;
+ int wspIdx = indexOfWsp(s, 0);
+ while (true) {
+ if (wspIdx == length) {
+ sb.append(s.substring(Math.max(0, lastLineBreak)));
+ return sb.toString();
+ }
+
+ int nextWspIdx = indexOfWsp(s, wspIdx + 1);
+
+ if (nextWspIdx - lastLineBreak > maxCharacters) {
+ sb.append(s.substring(Math.max(0, lastLineBreak), wspIdx));
+ sb.append("\r\n");
+ lastLineBreak = wspIdx;
+ }
+
+ wspIdx = nextWspIdx;
+ }
+ }
+
+ /**
+ * INTERIM: From newer version of org.apache.james (but we don't want to import
+ * the entire MimeUtil class).
+ *
+ * Search for whitespace.
+ */
+ private static int indexOfWsp(String s, int fromIndex) {
+ final int len = s.length();
+ for (int index = fromIndex; index < len; index++) {
+ char c = s.charAt(index);
+ if (c == ' ' || c == '\t')
+ return index;
+ }
+ return len;
+ }
+
+ /**
+ * Returns the named parameter of a header field. If name is null the first
+ * parameter is returned, or if there are no additional parameters in the
+ * field the entire field is returned. Otherwise the named parameter is
+ * searched for in a case insensitive fashion and returned. If the parameter
+ * cannot be found the method returns null.
+ *
+ * TODO: quite inefficient with the inner trimming & splitting.
+ * TODO: Also has a latent bug: uses "startsWith" to match the name, which can false-positive.
+ * TODO: The doc says that for a null name you get the first param, but you get the header.
+ * Should probably just fix the doc, but if other code assumes that behavior, fix the code.
+ * TODO: Need to decode %-escaped strings, as in: filename="ab%22d".
+ * ('+' -> ' ' conversion too? check RFC)
+ *
+ * @param header
+ * @param name
+ * @return the entire header (if name=null), the found parameter, or null
+ */
+ public static String getHeaderParameter(String header, String name) {
+ if (header == null) {
+ return null;
+ }
+ String[] parts = unfold(header).split(";");
+ if (name == null) {
+ return parts[0].trim();
+ }
+ String lowerCaseName = name.toLowerCase();
+ for (String part : parts) {
+ if (part.trim().toLowerCase().startsWith(lowerCaseName)) {
+ String[] parameterParts = part.split("=", 2);
+ if (parameterParts.length < 2) {
+ return null;
+ }
+ String parameter = parameterParts[1].trim();
+ if (parameter.startsWith("\"") && parameter.endsWith("\"")) {
+ return parameter.substring(1, parameter.length() - 1);
+ } else {
+ return parameter;
+ }
+ }
+ }
+ return null;
+ }
+
+ public static Part findFirstPartByMimeType(Part part, String mimeType)
+ throws MessagingException {
+ if (part.getBody() instanceof Multipart) {
+ Multipart multipart = (Multipart)part.getBody();
+ for (int i = 0, count = multipart.getCount(); i < count; i++) {
+ BodyPart bodyPart = multipart.getBodyPart(i);
+ Part ret = findFirstPartByMimeType(bodyPart, mimeType);
+ if (ret != null) {
+ return ret;
+ }
+ }
+ }
+ else if (part.getMimeType().equalsIgnoreCase(mimeType)) {
+ return part;
+ }
+ return null;
+ }
+
+ public static Part findPartByContentId(Part part, String contentId) throws Exception {
+ if (part.getBody() instanceof Multipart) {
+ Multipart multipart = (Multipart)part.getBody();
+ for (int i = 0, count = multipart.getCount(); i < count; i++) {
+ BodyPart bodyPart = multipart.getBodyPart(i);
+ Part ret = findPartByContentId(bodyPart, contentId);
+ if (ret != null) {
+ return ret;
+ }
+ }
+ }
+ String cid = part.getContentId();
+ if (contentId.equals(cid)) {
+ return part;
+ }
+ return null;
+ }
+
+ /**
+ * Reads the Part's body and returns a String based on any charset conversion that needed
+ * to be done.
+ * @param part The part containing a body
+ * @return a String containing the converted text in the body, or null if there was no text
+ * or an error during conversion.
+ */
+ public static String getTextFromPart(Part part) {
+ try {
+ if (part != null && part.getBody() != null) {
+ InputStream in = part.getBody().getInputStream();
+ String mimeType = part.getMimeType();
+ if (mimeType != null && MimeUtility.mimeTypeMatches(mimeType, "text/*")) {
+ /*
+ * Now we read the part into a buffer for further processing. Because
+ * the stream is now wrapped we'll remove any transfer encoding at this point.
+ */
+ ByteArrayOutputStream out = new ByteArrayOutputStream();
+ IOUtils.copy(in, out);
+ in.close();
+ in = null; // we want all of our memory back, and close might not release
+
+ /*
+ * We've got a text part, so let's see if it needs to be processed further.
+ */
+ String charset = getHeaderParameter(part.getContentType(), "charset");
+ if (charset != null) {
+ /*
+ * See if there is conversion from the MIME charset to the Java one.
+ */
+ charset = CharsetUtil.toJavaCharset(charset);
+ }
+ /*
+ * No encoding, so use us-ascii, which is the standard.
+ */
+ if (charset == null) {
+ charset = "ASCII";
+ }
+ /*
+ * Convert and return as new String
+ */
+ String result = out.toString(charset);
+ out.close();
+ return result;
+ }
+ }
+
+ }
+ catch (OutOfMemoryError oom) {
+ /*
+ * If we are not able to process the body there's nothing we can do about it. Return
+ * null and let the upper layers handle the missing content.
+ */
+ Log.e(Logging.LOG_TAG, "Unable to getTextFromPart " + oom.toString());
+ }
+ catch (Exception e) {
+ /*
+ * If we are not able to process the body there's nothing we can do about it. Return
+ * null and let the upper layers handle the missing content.
+ */
+ Log.e(Logging.LOG_TAG, "Unable to getTextFromPart " + e.toString());
+ }
+ return null;
+ }
+
+ /**
+ * Returns true if the given mimeType matches the matchAgainst specification. The comparison
+ * ignores case and the matchAgainst string may include "*" for a wildcard (e.g. "image/*").
+ *
+ * @param mimeType A MIME type to check.
+ * @param matchAgainst A MIME type to check against. May include wildcards.
+ * @return true if the mimeType matches
+ */
+ public static boolean mimeTypeMatches(String mimeType, String matchAgainst) {
+ Pattern p = Pattern.compile(matchAgainst.replaceAll("\\*", "\\.\\*"),
+ Pattern.CASE_INSENSITIVE);
+ return p.matcher(mimeType).matches();
+ }
+
+ /**
+ * Returns true if the given mimeType matches any of the matchAgainst specifications. The
+ * comparison ignores case and the matchAgainst strings may include "*" for a wildcard
+ * (e.g. "image/*").
+ *
+ * @param mimeType A MIME type to check.
+ * @param matchAgainst An array of MIME types to check against. May include wildcards.
+ * @return true if the mimeType matches any of the matchAgainst strings
+ */
+ public static boolean mimeTypeMatches(String mimeType, String[] matchAgainst) {
+ for (String matchType : matchAgainst) {
+ if (mimeTypeMatches(mimeType, matchType)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Given an input stream and a transfer encoding, return a wrapped input stream for that
+ * encoding (or the original if none is required)
+ * @param in the input stream
+ * @param contentTransferEncoding the content transfer encoding
+ * @return a properly wrapped stream
+ */
+ public static InputStream getInputStreamForContentTransferEncoding(InputStream in,
+ String contentTransferEncoding) {
+ if (contentTransferEncoding != null) {
+ contentTransferEncoding =
+ MimeUtility.getHeaderParameter(contentTransferEncoding, null);
+ if ("quoted-printable".equalsIgnoreCase(contentTransferEncoding)) {
+ in = new QuotedPrintableInputStream(in);
+ }
+ else if ("base64".equalsIgnoreCase(contentTransferEncoding)) {
+ in = new Base64InputStream(in, Base64.DEFAULT);
+ }
+ }
+ return in;
+ }
+
+ /**
+ * Removes any content transfer encoding from the stream and returns a Body.
+ */
+ public static Body decodeBody(InputStream in, String contentTransferEncoding)
+ throws IOException {
+ /*
+ * We'll remove any transfer encoding by wrapping the stream.
+ */
+ in = getInputStreamForContentTransferEncoding(in, contentTransferEncoding);
+ BinaryTempFileBody tempBody = new BinaryTempFileBody();
+ OutputStream out = tempBody.getOutputStream();
+ try {
+ IOUtils.copy(in, out);
+ } catch (Base64DataException bde) {
+ // TODO Need to fix this somehow
+ //String warning = "\n\n" + Email.getMessageDecodeErrorString();
+ //out.write(warning.getBytes());
+ } finally {
+ out.close();
+ }
+ return tempBody;
+ }
+
+ /**
+ * Recursively scan a Part (usually a Message) and sort out which of its children will be
+ * "viewable" and which will be attachments.
+ *
+ * @param part The part to be broken down
+ * @param viewables This arraylist will be populated with all parts that appear to be
+ * the "message" (e.g. text/plain & text/html)
+ * @param attachments This arraylist will be populated with all parts that appear to be
+ * attachments (including inlines)
+ * @throws MessagingException
+ */
+ public static void collectParts(Part part, ArrayList<Part> viewables,
+ ArrayList<Part> attachments) throws MessagingException {
+ String disposition = part.getDisposition();
+ String dispositionType = null;
+ String dispositionFilename = null;
+ if (disposition != null) {
+ dispositionType = MimeUtility.getHeaderParameter(disposition, null);
+ dispositionFilename = MimeUtility.getHeaderParameter(disposition, "filename");
+ }
+ // An attachment filename can be defined in either the Content-Disposition header
+ // or the Content-Type header. Content-Disposition is preferred, so we only try
+ // the Content-Type header as a last resort.
+ if (dispositionFilename == null) {
+ String contentType = part.getContentType();
+ dispositionFilename = MimeUtility.getHeaderParameter(contentType, "name");
+ }
+ boolean attachmentDisposition = "attachment".equalsIgnoreCase(dispositionType);
+ // If a disposition is not specified, default to "inline"
+ boolean inlineDisposition = dispositionType == null
+ || "inline".equalsIgnoreCase(dispositionType);
+
+ // A guess that this part is intended to be an attachment
+ boolean attachment = attachmentDisposition
+ || (dispositionFilename != null && !inlineDisposition);
+
+ // A guess that this part is intended to be an inline.
+ boolean inline = inlineDisposition && (dispositionFilename != null);
+
+ // One or the other
+ boolean attachmentOrInline = attachment || inline;
+
+ if (part.getBody() instanceof Multipart) {
+ // If the part is Multipart but not alternative it's either mixed or
+ // something we don't know about, which means we treat it as mixed
+ // per the spec. We just process its pieces recursively.
+ Multipart mp = (Multipart)part.getBody();
+ for (int i = 0; i < mp.getCount(); i++) {
+ collectParts(mp.getBodyPart(i), viewables, attachments);
+ }
+ } else if (part.getBody() instanceof Message) {
+ // If the part is an embedded message we just continue to process
+ // it, pulling any viewables or attachments into the running list.
+ Message message = (Message)part.getBody();
+ collectParts(message, viewables, attachments);
+ } else if ((!attachmentOrInline) && ("text/html".equalsIgnoreCase(part.getMimeType()))) {
+ // If the part is HTML and we got this far, it's a viewable part of a mixed
+ viewables.add(part);
+ } else if ((!attachmentOrInline) && ("text/plain".equalsIgnoreCase(part.getMimeType()))) {
+ // If the part is text and we got this far, it's a viewable part of a mixed
+ viewables.add(part);
+ } else if (attachmentOrInline) {
+ // Finally, if it's an attachment or an inline we will include it as an attachment.
+ attachments.add(part);
+ }
+ }
+}
diff --git a/email2/emailcommon/src/com/android/emailcommon/internet/Rfc822Output.java b/email2/emailcommon/src/com/android/emailcommon/internet/Rfc822Output.java
new file mode 100644
index 0000000..e03a926
--- /dev/null
+++ b/email2/emailcommon/src/com/android/emailcommon/internet/Rfc822Output.java
@@ -0,0 +1,473 @@
+/*
+ * 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.emailcommon.internet;
+
+import com.android.emailcommon.mail.Address;
+import com.android.emailcommon.mail.MessagingException;
+import com.android.emailcommon.provider.EmailContent.Attachment;
+import com.android.emailcommon.provider.EmailContent.Body;
+import com.android.emailcommon.provider.EmailContent.Message;
+
+import org.apache.commons.io.IOUtils;
+
+import android.content.ContentUris;
+import android.content.Context;
+import android.database.Cursor;
+import android.net.Uri;
+import android.text.Html;
+import android.text.TextUtils;
+import android.util.Base64;
+import android.util.Base64OutputStream;
+
+import java.io.BufferedOutputStream;
+import java.io.ByteArrayInputStream;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.io.OutputStreamWriter;
+import java.io.Writer;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.Locale;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * Utility class to output RFC 822 messages from provider email messages
+ */
+public class Rfc822Output {
+
+ private static final Pattern PATTERN_START_OF_LINE = Pattern.compile("(?m)^");
+ private static final Pattern PATTERN_ENDLINE_CRLF = Pattern.compile("\r\n");
+
+ // In MIME, en_US-like date format should be used. In other words "MMM" should be encoded to
+ // "Jan", not the other localized format like "Ene" (meaning January in locale es).
+ private static final SimpleDateFormat DATE_FORMAT =
+ new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss Z", Locale.US);
+
+ private static final String WHERE_NOT_SMART_FORWARD = "(" + Attachment.FLAGS + "&" +
+ Attachment.FLAG_SMART_FORWARD + ")=0";
+
+ /** A less-than-perfect pattern to pull out <body> content */
+ private static final Pattern BODY_PATTERN = Pattern.compile(
+ "(?:<\\s*body[^>]*>)(.*)(?:<\\s*/\\s*body\\s*>)",
+ Pattern.CASE_INSENSITIVE | Pattern.DOTALL);
+ /** Match group in {@code BODDY_PATTERN} for the body HTML */
+ private static final int BODY_PATTERN_GROUP = 1;
+ /** Pattern to find both dos and unix newlines */
+ private static final Pattern NEWLINE_PATTERN =
+ Pattern.compile("\\r?\\n");
+ /** HTML string to use when replacing text newlines */
+ private static final String NEWLINE_HTML = "<br>";
+ /** Index of the plain text version of the message body */
+ private final static int INDEX_BODY_TEXT = 0;
+ /** Index of the HTML version of the message body */
+ private final static int INDEX_BODY_HTML = 1;
+ /** Single digit [0-9] to ensure uniqueness of the MIME boundary */
+ /*package*/ static byte sBoundaryDigit;
+
+ /**
+ * Returns just the content between the <body></body> tags. This is not perfect and breaks
+ * with malformed HTML or if there happens to be special characters in the attributes of
+ * the <body> tag (e.g. a '>' in a java script block).
+ */
+ /*package*/ static String getHtmlBody(String html) {
+ Matcher match = BODY_PATTERN.matcher(html);
+ if (match.find()) {
+ return match.group(BODY_PATTERN_GROUP); // Found body; return
+ } else {
+ return html; // Body not found; return the full HTML and hope for the best
+ }
+ }
+
+ /**
+ * Returns an HTML encoded message alternate
+ */
+ /*package*/ static String getHtmlAlternate(Body body, boolean useSmartReply) {
+ if (body.mHtmlReply == null) {
+ return null;
+ }
+ StringBuffer altMessage = new StringBuffer();
+ String htmlContent = TextUtils.htmlEncode(body.mTextContent); // Escape HTML reserved chars
+ htmlContent = NEWLINE_PATTERN.matcher(htmlContent).replaceAll(NEWLINE_HTML);
+ altMessage.append(htmlContent);
+ if (body.mIntroText != null) {
+ String htmlIntro = TextUtils.htmlEncode(body.mIntroText);
+ htmlIntro = NEWLINE_PATTERN.matcher(htmlIntro).replaceAll(NEWLINE_HTML);
+ altMessage.append(htmlIntro);
+ }
+ if (!useSmartReply) {
+ String htmlBody = getHtmlBody(body.mHtmlReply);
+ altMessage.append(htmlBody);
+ }
+ return altMessage.toString();
+ }
+
+ /**
+ * Gets both the plain text and HTML versions of the message body.
+ */
+ /*package*/ static String[] buildBodyText(Body body, int flags, boolean useSmartReply) {
+ String[] messageBody = new String[] { null, null };
+ if (body == null) {
+ return messageBody;
+ }
+ String text = body.mTextContent;
+ boolean isReply = (flags & Message.FLAG_TYPE_REPLY) != 0;
+ boolean isForward = (flags & Message.FLAG_TYPE_FORWARD) != 0;
+ // For all forwards/replies, we add the intro text
+ if (isReply || isForward) {
+ String intro = body.mIntroText == null ? "" : body.mIntroText;
+ text += intro;
+ }
+ if (useSmartReply) {
+ // useSmartReply is set to true for use by SmartReply/SmartForward in EAS.
+ // SmartForward doesn't put a break between the original and new text, so we add an LF
+ if (isForward) {
+ text += "\n";
+ }
+ } else {
+ String quotedText = body.mTextReply;
+ // If there is no plain-text body, use de-tagified HTML as the text body
+ if (quotedText == null && body.mHtmlReply != null) {
+ quotedText = Html.fromHtml(body.mHtmlReply).toString();
+ }
+ if (quotedText != null) {
+ // fix CR-LF line endings to LF-only needed by EditText.
+ Matcher matcher = PATTERN_ENDLINE_CRLF.matcher(quotedText);
+ quotedText = matcher.replaceAll("\n");
+ }
+ if (isReply) {
+ if (quotedText != null) {
+ Matcher matcher = PATTERN_START_OF_LINE.matcher(quotedText);
+ text += matcher.replaceAll(">");
+ }
+ } else if (isForward) {
+ if (quotedText != null) {
+ text += quotedText;
+ }
+ }
+ }
+ messageBody[INDEX_BODY_TEXT] = text;
+ // Exchange 2003 doesn't seem to support multipart w/SmartReply and SmartForward, so
+ // we'll skip this. Really, it would only matter if we could compose HTML replies
+ if (!useSmartReply) {
+ messageBody[INDEX_BODY_HTML] = getHtmlAlternate(body, useSmartReply);
+ }
+ return messageBody;
+ }
+
+ /**
+ * Write the entire message to an output stream. This method provides buffering, so it is
+ * not necessary to pass in a buffered output stream here.
+ *
+ * @param context system context for accessing the provider
+ * @param messageId the message to write out
+ * @param out the output stream to write the message to
+ * @param useSmartReply whether or not quoted text is appended to a reply/forward
+ */
+ public static void writeTo(Context context, long messageId, OutputStream out,
+ boolean useSmartReply, boolean sendBcc) throws IOException, MessagingException {
+ Message message = Message.restoreMessageWithId(context, messageId);
+ if (message == null) {
+ // throw something?
+ return;
+ }
+
+ OutputStream stream = new BufferedOutputStream(out, 1024);
+ Writer writer = new OutputStreamWriter(stream);
+
+ // Write the fixed headers. Ordering is arbitrary (the legacy code iterated through a
+ // hashmap here).
+
+ String date = DATE_FORMAT.format(new Date(message.mTimeStamp));
+ writeHeader(writer, "Date", date);
+
+ writeEncodedHeader(writer, "Subject", message.mSubject);
+
+ writeHeader(writer, "Message-ID", message.mMessageId);
+
+ writeAddressHeader(writer, "From", message.mFrom);
+ writeAddressHeader(writer, "To", message.mTo);
+ writeAddressHeader(writer, "Cc", message.mCc);
+ // Address fields. Note that we skip bcc unless the sendBcc argument is true
+ // SMTP should NOT send bcc headers, but EAS must send it!
+ if (sendBcc) {
+ writeAddressHeader(writer, "Bcc", message.mBcc);
+ }
+ writeAddressHeader(writer, "Reply-To", message.mReplyTo);
+ writeHeader(writer, "MIME-Version", "1.0");
+
+ // Analyze message and determine if we have multiparts
+ Body body = Body.restoreBodyWithMessageId(context, message.mId);
+ String[] bodyText = buildBodyText(body, message.mFlags, useSmartReply);
+
+ Uri uri = ContentUris.withAppendedId(Attachment.MESSAGE_ID_URI, messageId);
+ Cursor attachmentsCursor = context.getContentResolver().query(uri,
+ Attachment.CONTENT_PROJECTION, WHERE_NOT_SMART_FORWARD, null, null);
+
+ try {
+ int attachmentCount = attachmentsCursor.getCount();
+ boolean multipart = attachmentCount > 0;
+ String multipartBoundary = null;
+ String multipartType = "mixed";
+
+ // Simplified case for no multipart - just emit text and be done.
+ if (!multipart) {
+ writeTextWithHeaders(writer, stream, bodyText);
+ } else {
+ // continue with multipart headers, then into multipart body
+ multipartBoundary = getNextBoundary();
+
+ // Move to the first attachment; this must succeed because multipart is true
+ attachmentsCursor.moveToFirst();
+ if (attachmentCount == 1) {
+ // If we've got one attachment and it's an ics "attachment", we want to send
+ // this as multipart/alternative instead of multipart/mixed
+ int flags = attachmentsCursor.getInt(Attachment.CONTENT_FLAGS_COLUMN);
+ if ((flags & Attachment.FLAG_ICS_ALTERNATIVE_PART) != 0) {
+ multipartType = "alternative";
+ }
+ }
+
+ writeHeader(writer, "Content-Type",
+ "multipart/" + multipartType + "; boundary=\"" + multipartBoundary + "\"");
+ // Finish headers and prepare for body section(s)
+ writer.write("\r\n");
+
+ // first multipart element is the body
+ if (bodyText[INDEX_BODY_TEXT] != null) {
+ writeBoundary(writer, multipartBoundary, false);
+ writeTextWithHeaders(writer, stream, bodyText);
+ }
+
+ // Write out the attachments until we run out
+ do {
+ writeBoundary(writer, multipartBoundary, false);
+ Attachment attachment =
+ Attachment.getContent(attachmentsCursor, Attachment.class);
+ writeOneAttachment(context, writer, stream, attachment);
+ writer.write("\r\n");
+ } while (attachmentsCursor.moveToNext());
+
+ // end of multipart section
+ writeBoundary(writer, multipartBoundary, true);
+ }
+ } finally {
+ attachmentsCursor.close();
+ }
+
+ writer.flush();
+ out.flush();
+ }
+
+ /**
+ * Write a single attachment and its payload
+ */
+ private static void writeOneAttachment(Context context, Writer writer, OutputStream out,
+ Attachment attachment) throws IOException, MessagingException {
+ writeHeader(writer, "Content-Type",
+ attachment.mMimeType + ";\n name=\"" + attachment.mFileName + "\"");
+ writeHeader(writer, "Content-Transfer-Encoding", "base64");
+ // Most attachments (real files) will send Content-Disposition. The suppression option
+ // is used when sending calendar invites.
+ if ((attachment.mFlags & Attachment.FLAG_ICS_ALTERNATIVE_PART) == 0) {
+ writeHeader(writer, "Content-Disposition",
+ "attachment;"
+ + "\n filename=\"" + attachment.mFileName + "\";"
+ + "\n size=" + Long.toString(attachment.mSize));
+ }
+ if (attachment.mContentId != null) {
+ writeHeader(writer, "Content-ID", attachment.mContentId);
+ }
+ writer.append("\r\n");
+
+ // Set up input stream and write it out via base64
+ InputStream inStream = null;
+ try {
+ // Use content, if provided; otherwise, use the contentUri
+ if (attachment.mContentBytes != null) {
+ inStream = new ByteArrayInputStream(attachment.mContentBytes);
+ } else {
+ // try to open the file
+ Uri fileUri = Uri.parse(attachment.mContentUri);
+ inStream = context.getContentResolver().openInputStream(fileUri);
+ }
+ // switch to output stream for base64 text output
+ writer.flush();
+ Base64OutputStream base64Out = new Base64OutputStream(
+ out, Base64.CRLF | Base64.NO_CLOSE);
+ // copy base64 data and close up
+ IOUtils.copy(inStream, base64Out);
+ base64Out.close();
+
+ // The old Base64OutputStream wrote an extra CRLF after
+ // the output. It's not required by the base-64 spec; not
+ // sure if it's required by RFC 822 or not.
+ out.write('\r');
+ out.write('\n');
+ out.flush();
+ }
+ catch (FileNotFoundException fnfe) {
+ // Ignore this - empty file is OK
+ }
+ catch (IOException ioe) {
+ throw new MessagingException("Invalid attachment.", ioe);
+ }
+ }
+
+ /**
+ * Write a single header with no wrapping or encoding
+ *
+ * @param writer the output writer
+ * @param name the header name
+ * @param value the header value
+ */
+ private static void writeHeader(Writer writer, String name, String value) throws IOException {
+ if (value != null && value.length() > 0) {
+ writer.append(name);
+ writer.append(": ");
+ writer.append(value);
+ writer.append("\r\n");
+ }
+ }
+
+ /**
+ * Write a single header using appropriate folding & encoding
+ *
+ * @param writer the output writer
+ * @param name the header name
+ * @param value the header value
+ */
+ private static void writeEncodedHeader(Writer writer, String name, String value)
+ throws IOException {
+ if (value != null && value.length() > 0) {
+ writer.append(name);
+ writer.append(": ");
+ writer.append(MimeUtility.foldAndEncode2(value, name.length() + 2));
+ writer.append("\r\n");
+ }
+ }
+
+ /**
+ * Unpack, encode, and fold address(es) into a header
+ *
+ * @param writer the output writer
+ * @param name the header name
+ * @param value the header value (a packed list of addresses)
+ */
+ private static void writeAddressHeader(Writer writer, String name, String value)
+ throws IOException {
+ if (value != null && value.length() > 0) {
+ writer.append(name);
+ writer.append(": ");
+ writer.append(MimeUtility.fold(Address.packedToHeader(value), name.length() + 2));
+ writer.append("\r\n");
+ }
+ }
+
+ /**
+ * Write a multipart boundary
+ *
+ * @param writer the output writer
+ * @param boundary the boundary string
+ * @param end false if inner boundary, true if final boundary
+ */
+ private static void writeBoundary(Writer writer, String boundary, boolean end)
+ throws IOException {
+ writer.append("--");
+ writer.append(boundary);
+ if (end) {
+ writer.append("--");
+ }
+ writer.append("\r\n");
+ }
+
+ /**
+ * Write the body text. If only one version of the body is specified (either plain text
+ * or HTML), the text is written directly. Otherwise, the plain text and HTML bodies
+ * are both written with the appropriate headers.
+ *
+ * Note this always uses base64, even when not required. Slightly less efficient for
+ * US-ASCII text, but handles all formats even when non-ascii chars are involved. A small
+ * optimization might be to prescan the string for safety and send raw if possible.
+ *
+ * @param writer the output writer
+ * @param out the output stream inside the writer (used for byte[] access)
+ * @param bodyText Plain text and HTML versions of the original text of the message
+ */
+ private static void writeTextWithHeaders(Writer writer, OutputStream out, String[] bodyText)
+ throws IOException {
+ String text = bodyText[INDEX_BODY_TEXT];
+ String html = bodyText[INDEX_BODY_HTML];
+
+ if (text == null) {
+ writer.write("\r\n"); // a truly empty message
+ } else {
+ String multipartBoundary = null;
+ boolean multipart = html != null;
+
+ // Simplified case for no multipart - just emit text and be done.
+ if (multipart) {
+ // continue with multipart headers, then into multipart body
+ multipartBoundary = getNextBoundary();
+
+ writeHeader(writer, "Content-Type",
+ "multipart/alternative; boundary=\"" + multipartBoundary + "\"");
+ // Finish headers and prepare for body section(s)
+ writer.write("\r\n");
+ writeBoundary(writer, multipartBoundary, false);
+ }
+
+ // first multipart element is the body
+ writeHeader(writer, "Content-Type", "text/plain; charset=utf-8");
+ writeHeader(writer, "Content-Transfer-Encoding", "base64");
+ writer.write("\r\n");
+ byte[] textBytes = text.getBytes("UTF-8");
+ writer.flush();
+ out.write(Base64.encode(textBytes, Base64.CRLF));
+
+ if (multipart) {
+ // next multipart section
+ writeBoundary(writer, multipartBoundary, false);
+
+ writeHeader(writer, "Content-Type", "text/html; charset=utf-8");
+ writeHeader(writer, "Content-Transfer-Encoding", "base64");
+ writer.write("\r\n");
+ byte[] htmlBytes = html.getBytes("UTF-8");
+ writer.flush();
+ out.write(Base64.encode(htmlBytes, Base64.CRLF));
+
+ // end of multipart section
+ writeBoundary(writer, multipartBoundary, true);
+ }
+ }
+ }
+
+ /**
+ * Returns a unique boundary string.
+ */
+ /*package*/ static String getNextBoundary() {
+ StringBuilder boundary = new StringBuilder();
+ boundary.append("--_com.android.email_").append(System.nanoTime());
+ synchronized (Rfc822Output.class) {
+ boundary = boundary.append(sBoundaryDigit);
+ sBoundaryDigit = (byte)((sBoundaryDigit + 1) % 10);
+ }
+ return boundary.toString();
+ }
+}
diff --git a/email2/emailcommon/src/com/android/emailcommon/internet/TextBody.java b/email2/emailcommon/src/com/android/emailcommon/internet/TextBody.java
new file mode 100644
index 0000000..09c265c
--- /dev/null
+++ b/email2/emailcommon/src/com/android/emailcommon/internet/TextBody.java
@@ -0,0 +1,62 @@
+/*
+ * 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.emailcommon.internet;
+
+import com.android.emailcommon.mail.Body;
+import com.android.emailcommon.mail.MessagingException;
+
+import android.util.Base64;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.io.UnsupportedEncodingException;
+
+public class TextBody implements Body {
+ String mBody;
+
+ public TextBody(String body) {
+ this.mBody = body;
+ }
+
+ public void writeTo(OutputStream out) throws IOException, MessagingException {
+ byte[] bytes = mBody.getBytes("UTF-8");
+ out.write(Base64.encode(bytes, Base64.CRLF));
+ }
+
+ /**
+ * Get the text of the body in it's unencoded format.
+ * @return
+ */
+ public String getText() {
+ return mBody;
+ }
+
+ /**
+ * Returns an InputStream that reads this body's text in UTF-8 format.
+ */
+ public InputStream getInputStream() throws MessagingException {
+ try {
+ byte[] b = mBody.getBytes("UTF-8");
+ return new ByteArrayInputStream(b);
+ }
+ catch (UnsupportedEncodingException usee) {
+ return null;
+ }
+ }
+}
diff --git a/email2/emailcommon/src/com/android/emailcommon/mail/Address.java b/email2/emailcommon/src/com/android/emailcommon/mail/Address.java
new file mode 100644
index 0000000..5245c56
--- /dev/null
+++ b/email2/emailcommon/src/com/android/emailcommon/mail/Address.java
@@ -0,0 +1,428 @@
+/*
+ * 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.emailcommon.mail;
+
+import android.text.TextUtils;
+import android.text.util.Rfc822Token;
+import android.text.util.Rfc822Tokenizer;
+
+import com.android.emailcommon.utility.Utility;
+import com.google.common.annotations.VisibleForTesting;
+
+import org.apache.james.mime4j.codec.EncoderUtil;
+import org.apache.james.mime4j.decoder.DecoderUtil;
+
+import java.util.ArrayList;
+import java.util.regex.Pattern;
+
+/**
+ * This class represent email address.
+ *
+ * RFC822 email address may have following format.
+ * "name" <address> (comment)
+ * "name" <address>
+ * name <address>
+ * address
+ * Name and comment part should be MIME/base64 encoded in header if necessary.
+ *
+ */
+public class Address {
+ /**
+ * Address part, in the form local_part@domain_part. No surrounding angle brackets.
+ */
+ private String mAddress;
+
+ /**
+ * Name part. No surrounding double quote, and no MIME/base64 encoding.
+ * This must be null if Address has no name part.
+ */
+ private String mPersonal;
+
+ // Regex that matches address surrounded by '<>' optionally. '^<?([^>]+)>?$'
+ private static final Pattern REMOVE_OPTIONAL_BRACKET = Pattern.compile("^<?([^>]+)>?$");
+ // Regex that matches personal name surrounded by '""' optionally. '^"?([^"]+)"?$'
+ private static final Pattern REMOVE_OPTIONAL_DQUOTE = Pattern.compile("^\"?([^\"]*)\"?$");
+ // Regex that matches escaped character '\\([\\"])'
+ private static final Pattern UNQUOTE = Pattern.compile("\\\\([\\\\\"])");
+
+ private static final Address[] EMPTY_ADDRESS_ARRAY = new Address[0];
+
+ // delimiters are chars that do not appear in an email address, used by pack/unpack
+ private static final char LIST_DELIMITER_EMAIL = '\1';
+ private static final char LIST_DELIMITER_PERSONAL = '\2';
+
+ public Address(String address, String personal) {
+ setAddress(address);
+ setPersonal(personal);
+ }
+
+ public Address(String address) {
+ setAddress(address);
+ }
+
+ public String getAddress() {
+ return mAddress;
+ }
+
+ public void setAddress(String address) {
+ mAddress = REMOVE_OPTIONAL_BRACKET.matcher(address).replaceAll("$1");
+ }
+
+ /**
+ * Get name part as UTF-16 string. No surrounding double quote, and no MIME/base64 encoding.
+ *
+ * @return Name part of email address. Returns null if it is omitted.
+ */
+ public String getPersonal() {
+ return mPersonal;
+ }
+
+ /**
+ * Set name part from UTF-16 string. Optional surrounding double quote will be removed.
+ * It will be also unquoted and MIME/base64 decoded.
+ *
+ * @param personal name part of email address as UTF-16 string. Null is acceptable.
+ */
+ public void setPersonal(String personal) {
+ if (personal != null) {
+ personal = REMOVE_OPTIONAL_DQUOTE.matcher(personal).replaceAll("$1");
+ personal = UNQUOTE.matcher(personal).replaceAll("$1");
+ personal = DecoderUtil.decodeEncodedWords(personal);
+ if (personal.length() == 0) {
+ personal = null;
+ }
+ }
+ mPersonal = personal;
+ }
+
+ /**
+ * This method is used to check that all the addresses that the user
+ * entered in a list (e.g. To:) are valid, so that none is dropped.
+ */
+ public static boolean isAllValid(String addressList) {
+ // This code mimics the parse() method below.
+ // I don't know how to better avoid the code-duplication.
+ if (addressList != null && addressList.length() > 0) {
+ Rfc822Token[] tokens = Rfc822Tokenizer.tokenize(addressList);
+ for (int i = 0, length = tokens.length; i < length; ++i) {
+ Rfc822Token token = tokens[i];
+ String address = token.getAddress();
+ if (!TextUtils.isEmpty(address) && !isValidAddress(address)) {
+ return false;
+ }
+ }
+ }
+ return true;
+ }
+
+ /**
+ * Parse a comma-delimited list of addresses in RFC822 format and return an
+ * array of Address objects.
+ *
+ * @param addressList Address list in comma-delimited string.
+ * @return An array of 0 or more Addresses.
+ */
+ public static Address[] parse(String addressList) {
+ if (addressList == null || addressList.length() == 0) {
+ return EMPTY_ADDRESS_ARRAY;
+ }
+ Rfc822Token[] tokens = Rfc822Tokenizer.tokenize(addressList);
+ ArrayList<Address> addresses = new ArrayList<Address>();
+ for (int i = 0, length = tokens.length; i < length; ++i) {
+ Rfc822Token token = tokens[i];
+ String address = token.getAddress();
+ if (!TextUtils.isEmpty(address)) {
+ if (isValidAddress(address)) {
+ String name = token.getName();
+ if (TextUtils.isEmpty(name)) {
+ name = null;
+ }
+ addresses.add(new Address(address, name));
+ }
+ }
+ }
+ return addresses.toArray(new Address[] {});
+ }
+
+ /**
+ * Checks whether a string email address is valid.
+ * E.g. name@domain.com is valid.
+ */
+ @VisibleForTesting
+ static boolean isValidAddress(String address) {
+ // Note: Some email provider may violate the standard, so here we only check that
+ // address consists of two part that are separated by '@', and domain part contains
+ // at least one '.'.
+ int len = address.length();
+ int firstAt = address.indexOf('@');
+ int lastAt = address.lastIndexOf('@');
+ int firstDot = address.indexOf('.', lastAt + 1);
+ int lastDot = address.lastIndexOf('.');
+ return firstAt > 0 && firstAt == lastAt && lastAt + 1 < firstDot
+ && firstDot <= lastDot && lastDot < len - 1;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (o instanceof Address) {
+ // It seems that the spec says that the "user" part is case-sensitive,
+ // while the domain part in case-insesitive.
+ // So foo@yahoo.com and Foo@yahoo.com are different.
+ // This may seem non-intuitive from the user POV, so we
+ // may re-consider it if it creates UI trouble.
+ // A problem case is "replyAll" sending to both
+ // a@b.c and to A@b.c, which turn out to be the same on the server.
+ // Leave unchanged for now (i.e. case-sensitive).
+ return getAddress().equals(((Address) o).getAddress());
+ }
+ return super.equals(o);
+ }
+
+ public int hashCode() {
+ return getAddress().hashCode();
+ }
+
+ /**
+ * Get human readable address string.
+ * Do not use this for email header.
+ *
+ * @return Human readable address string. Not quoted and not encoded.
+ */
+ @Override
+ public String toString() {
+ if (mPersonal != null && !mPersonal.equals(mAddress)) {
+ if (mPersonal.matches(".*[\\(\\)<>@,;:\\\\\".\\[\\]].*")) {
+ return Utility.quoteString(mPersonal) + " <" + mAddress + ">";
+ } else {
+ return mPersonal + " <" + mAddress + ">";
+ }
+ } else {
+ return mAddress;
+ }
+ }
+
+ /**
+ * Get human readable comma-delimited address string.
+ *
+ * @param addresses Address array
+ * @return Human readable comma-delimited address string.
+ */
+ public static String toString(Address[] addresses) {
+ return toString(addresses, ",");
+ }
+
+ /**
+ * Get human readable address strings joined with the specified separator.
+ *
+ * @param addresses Address array
+ * @param separator Separator
+ * @return Human readable comma-delimited address string.
+ */
+ public static String toString(Address[] addresses, String separator) {
+ if (addresses == null || addresses.length == 0) {
+ return null;
+ }
+ if (addresses.length == 1) {
+ return addresses[0].toString();
+ }
+ StringBuffer sb = new StringBuffer(addresses[0].toString());
+ for (int i = 1; i < addresses.length; i++) {
+ sb.append(separator);
+ // TODO: investigate why this .trim() is needed.
+ sb.append(addresses[i].toString().trim());
+ }
+ return sb.toString();
+ }
+
+ /**
+ * Get RFC822/MIME compatible address string.
+ *
+ * @return RFC822/MIME compatible address string.
+ * It may be surrounded by double quote or quoted and MIME/base64 encoded if necessary.
+ */
+ public String toHeader() {
+ if (mPersonal != null) {
+ return EncoderUtil.encodeAddressDisplayName(mPersonal) + " <" + mAddress + ">";
+ } else {
+ return mAddress;
+ }
+ }
+
+ /**
+ * Get RFC822/MIME compatible comma-delimited address string.
+ *
+ * @param addresses Address array
+ * @return RFC822/MIME compatible comma-delimited address string.
+ * it may be surrounded by double quoted or quoted and MIME/base64 encoded if necessary.
+ */
+ public static String toHeader(Address[] addresses) {
+ if (addresses == null || addresses.length == 0) {
+ return null;
+ }
+ if (addresses.length == 1) {
+ return addresses[0].toHeader();
+ }
+ StringBuffer sb = new StringBuffer(addresses[0].toHeader());
+ for (int i = 1; i < addresses.length; i++) {
+ // We need space character to be able to fold line.
+ sb.append(", ");
+ sb.append(addresses[i].toHeader());
+ }
+ return sb.toString();
+ }
+
+ /**
+ * Get Human friendly address string.
+ *
+ * @return the personal part of this Address, or the address part if the
+ * personal part is not available
+ */
+ public String toFriendly() {
+ if (mPersonal != null && mPersonal.length() > 0) {
+ return mPersonal;
+ } else {
+ return mAddress;
+ }
+ }
+
+ /**
+ * Creates a comma-delimited list of addresses in the "friendly" format (see toFriendly() for
+ * details on the per-address conversion).
+ *
+ * @param addresses Array of Address[] values
+ * @return A comma-delimited string listing all of the addresses supplied. Null if source
+ * was null or empty.
+ */
+ public static String toFriendly(Address[] addresses) {
+ if (addresses == null || addresses.length == 0) {
+ return null;
+ }
+ if (addresses.length == 1) {
+ return addresses[0].toFriendly();
+ }
+ StringBuffer sb = new StringBuffer(addresses[0].toFriendly());
+ for (int i = 1; i < addresses.length; i++) {
+ sb.append(", ");
+ sb.append(addresses[i].toFriendly());
+ }
+ return sb.toString();
+ }
+
+ /**
+ * Returns exactly the same result as Address.toString(Address.unpack(packedList)).
+ */
+ public static String unpackToString(String packedList) {
+ return toString(unpack(packedList));
+ }
+
+ /**
+ * Returns exactly the same result as Address.pack(Address.parse(textList)).
+ */
+ public static String parseAndPack(String textList) {
+ return Address.pack(Address.parse(textList));
+ }
+
+ /**
+ * Returns null if the packedList has 0 addresses, otherwise returns the first address.
+ * The same as Address.unpack(packedList)[0] for non-empty list.
+ * This is an utility method that offers some performance optimization opportunities.
+ */
+ public static Address unpackFirst(String packedList) {
+ Address[] array = unpack(packedList);
+ return array.length > 0 ? array[0] : null;
+ }
+
+ /**
+ * Convert a packed list of addresses to a form suitable for use in an RFC822 header.
+ * This implementation is brute-force, and could be replaced with a more efficient version
+ * if desired.
+ */
+ public static String packedToHeader(String packedList) {
+ return toHeader(unpack(packedList));
+ }
+
+ /**
+ * Unpacks an address list that is either CSV of RFC822 addresses OR (for backward
+ * compatibility) previously packed with pack()
+ * @param addressList string packed with pack() or CSV of RFC822 addresses
+ * @return array of addresses resulting from unpack
+ */
+ public static Address[] unpack(String addressList) {
+ if (addressList == null || addressList.length() == 0) {
+ return EMPTY_ADDRESS_ARRAY;
+ }
+ // IF we're CSV, just parse
+ if ((addressList.indexOf(LIST_DELIMITER_PERSONAL) == -1) &&
+ (addressList.indexOf(LIST_DELIMITER_EMAIL) == -1)) {
+ return Address.parse(addressList);
+ }
+ // Otherwise, do backward-compatibile unpack
+ ArrayList<Address> addresses = new ArrayList<Address>();
+ int length = addressList.length();
+ int pairStartIndex = 0;
+ int pairEndIndex = 0;
+
+ /* addressEndIndex is only re-scanned (indexOf()) when a LIST_DELIMITER_PERSONAL
+ is used, not for every email address; i.e. not for every iteration of the while().
+ This reduces the theoretical complexity from quadratic to linear,
+ and provides some speed-up in practice by removing redundant scans of the string.
+ */
+ int addressEndIndex = addressList.indexOf(LIST_DELIMITER_PERSONAL);
+
+ while (pairStartIndex < length) {
+ pairEndIndex = addressList.indexOf(LIST_DELIMITER_EMAIL, pairStartIndex);
+ if (pairEndIndex == -1) {
+ pairEndIndex = length;
+ }
+ Address address;
+ if (addressEndIndex == -1 || pairEndIndex <= addressEndIndex) {
+ // in this case the DELIMITER_PERSONAL is in a future pair,
+ // so don't use personal, and don't update addressEndIndex
+ address = new Address(addressList.substring(pairStartIndex, pairEndIndex), null);
+ } else {
+ address = new Address(addressList.substring(pairStartIndex, addressEndIndex),
+ addressList.substring(addressEndIndex + 1, pairEndIndex));
+ // only update addressEndIndex when we use the LIST_DELIMITER_PERSONAL
+ addressEndIndex = addressList.indexOf(LIST_DELIMITER_PERSONAL, pairEndIndex + 1);
+ }
+ addresses.add(address);
+ pairStartIndex = pairEndIndex + 1;
+ }
+ return addresses.toArray(EMPTY_ADDRESS_ARRAY);
+ }
+
+ /**
+ * Generate a String containing RFC822 addresses separated by commas
+ * NOTE: We used to "pack" these addresses in an app-specific format, but no longer do so
+ */
+ public static String pack(Address[] addresses) {
+ return Address.toHeader(addresses);
+ }
+
+ /**
+ * Produces the same result as pack(array), but only packs one (this) address.
+ */
+ public String pack() {
+ final String address = getAddress();
+ final String personal = getPersonal();
+ if (personal == null) {
+ return address;
+ } else {
+ return address + LIST_DELIMITER_PERSONAL + personal;
+ }
+ }
+}
diff --git a/email2/emailcommon/src/com/android/emailcommon/mail/AuthenticationFailedException.java b/email2/emailcommon/src/com/android/emailcommon/mail/AuthenticationFailedException.java
new file mode 100644
index 0000000..af8d96c
--- /dev/null
+++ b/email2/emailcommon/src/com/android/emailcommon/mail/AuthenticationFailedException.java
@@ -0,0 +1,34 @@
+/*
+ * 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.emailcommon.mail;
+
+
+public class AuthenticationFailedException extends MessagingException {
+ public static final long serialVersionUID = -1;
+
+ public AuthenticationFailedException(String message) {
+ super(MessagingException.AUTHENTICATION_FAILED, message);
+ }
+
+ public AuthenticationFailedException(int exceptionType, String message) {
+ super(exceptionType, message);
+ }
+
+ public AuthenticationFailedException(String message, Throwable throwable) {
+ super(MessagingException.AUTHENTICATION_FAILED, message, throwable);
+ }
+}
diff --git a/email2/emailcommon/src/com/android/emailcommon/mail/Body.java b/email2/emailcommon/src/com/android/emailcommon/mail/Body.java
new file mode 100644
index 0000000..841ab42
--- /dev/null
+++ b/email2/emailcommon/src/com/android/emailcommon/mail/Body.java
@@ -0,0 +1,26 @@
+/*
+ * 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.emailcommon.mail;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+
+public interface Body {
+ public InputStream getInputStream() throws MessagingException;
+ public void writeTo(OutputStream out) throws IOException, MessagingException;
+}
diff --git a/email2/emailcommon/src/com/android/emailcommon/mail/BodyPart.java b/email2/emailcommon/src/com/android/emailcommon/mail/BodyPart.java
new file mode 100644
index 0000000..f698a13
--- /dev/null
+++ b/email2/emailcommon/src/com/android/emailcommon/mail/BodyPart.java
@@ -0,0 +1,25 @@
+/*
+ * 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.emailcommon.mail;
+
+public abstract class BodyPart implements Part {
+ protected Multipart mParent;
+
+ public Multipart getParent() {
+ return mParent;
+ }
+}
diff --git a/email2/emailcommon/src/com/android/emailcommon/mail/CertificateValidationException.java b/email2/emailcommon/src/com/android/emailcommon/mail/CertificateValidationException.java
new file mode 100644
index 0000000..83c6224
--- /dev/null
+++ b/email2/emailcommon/src/com/android/emailcommon/mail/CertificateValidationException.java
@@ -0,0 +1,30 @@
+/*
+ * 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.emailcommon.mail;
+
+
+public class CertificateValidationException extends MessagingException {
+ public static final long serialVersionUID = -1;
+
+ public CertificateValidationException(String message) {
+ super(MessagingException.CERTIFICATE_VALIDATION_ERROR, message);
+ }
+
+ public CertificateValidationException(String message, Throwable throwable) {
+ super(MessagingException.CERTIFICATE_VALIDATION_ERROR, message, throwable);
+ }
+}
\ No newline at end of file
diff --git a/email2/emailcommon/src/com/android/emailcommon/mail/FetchProfile.java b/email2/emailcommon/src/com/android/emailcommon/mail/FetchProfile.java
new file mode 100644
index 0000000..bfa48d3
--- /dev/null
+++ b/email2/emailcommon/src/com/android/emailcommon/mail/FetchProfile.java
@@ -0,0 +1,85 @@
+/*
+ * 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.emailcommon.mail;
+
+import java.util.ArrayList;
+
+/**
+ * <pre>
+ * A FetchProfile is a list of items that should be downloaded in bulk for a set of messages.
+ * FetchProfile can contain the following objects:
+ * FetchProfile.Item: Described below.
+ * Message: Indicates that the body of the entire message should be fetched.
+ * Synonymous with FetchProfile.Item.BODY.
+ * Part: Indicates that the given Part should be fetched. The provider
+ * is expected have previously created the given BodyPart and stored
+ * any information it needs to download the content.
+ * </pre>
+ */
+public class FetchProfile extends ArrayList<Fetchable> {
+ /**
+ * Default items available for pre-fetching. It should be expected that any
+ * item fetched by using these items could potentially include all of the
+ * previous items.
+ */
+ public enum Item implements Fetchable {
+ /**
+ * Download the flags of the message.
+ */
+ FLAGS,
+
+ /**
+ * Download the envelope of the message. This should include at minimum
+ * the size and the following headers: date, subject, from, content-type, to, cc
+ */
+ ENVELOPE,
+
+ /**
+ * Download the structure of the message. This maps directly to IMAP's BODYSTRUCTURE
+ * and may map to other providers.
+ * The provider should, if possible, fill in a properly formatted MIME structure in
+ * the message without actually downloading any message data. If the provider is not
+ * capable of this operation it should specifically set the body of the message to null
+ * so that upper levels can detect that a full body download is needed.
+ */
+ STRUCTURE,
+
+ /**
+ * A sane portion of the entire message, cut off at a provider determined limit.
+ * This should generaly be around 50kB.
+ */
+ BODY_SANE,
+
+ /**
+ * The entire message.
+ */
+ BODY,
+ }
+
+ /**
+ * @return the first {@link Part} in this collection, or null if it doesn't contain
+ * {@link Part}.
+ */
+ public Part getFirstPart() {
+ for (Fetchable o : this) {
+ if (o instanceof Part) {
+ return (Part) o;
+ }
+ }
+ return null;
+ }
+}
diff --git a/email2/emailcommon/src/com/android/emailcommon/mail/Fetchable.java b/email2/emailcommon/src/com/android/emailcommon/mail/Fetchable.java
new file mode 100644
index 0000000..4314f93
--- /dev/null
+++ b/email2/emailcommon/src/com/android/emailcommon/mail/Fetchable.java
@@ -0,0 +1,24 @@
+/*
+ * 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.emailcommon.mail;
+
+/**
+ * Interface for classes that can be added to {@link FetchProfile}.
+ * i.e. {@link Part} and its subclasses, and {@link FetchProfile.Item}.
+ */
+public interface Fetchable {
+}
diff --git a/email2/emailcommon/src/com/android/emailcommon/mail/Flag.java b/email2/emailcommon/src/com/android/emailcommon/mail/Flag.java
new file mode 100644
index 0000000..bcdcb8b
--- /dev/null
+++ b/email2/emailcommon/src/com/android/emailcommon/mail/Flag.java
@@ -0,0 +1,79 @@
+/*
+ * 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.emailcommon.mail;
+
+/**
+ * Flags that can be applied to Messages.
+ */
+public enum Flag {
+
+ // If adding new flags: ALL FLAGS MUST BE UPPER CASE.
+
+ DELETED,
+ SEEN,
+ ANSWERED,
+ FLAGGED,
+ DRAFT,
+ RECENT,
+
+ /*
+ * The following flags are for internal library use only.
+ * TODO Eventually we should creates a Flags class that extends ArrayList that allows
+ * these flags and Strings to represent user defined flags. At that point the below
+ * flags should become user defined flags.
+ */
+ /**
+ * Delete and remove from the LocalStore immediately.
+ */
+ X_DESTROYED,
+
+ /**
+ * Sending of an unsent message failed. It will be retried. Used to show status.
+ */
+ X_SEND_FAILED,
+
+ /**
+ * Sending of an unsent message is in progress.
+ */
+ X_SEND_IN_PROGRESS,
+
+ /**
+ * Indicates that a message is fully downloaded from the server and can be viewed normally.
+ * This does not include attachments, which are never downloaded fully.
+ */
+ X_DOWNLOADED_FULL,
+
+ /**
+ * Indicates that a message is partially downloaded from the server and can be viewed but
+ * more content is available on the server.
+ * This does not include attachments, which are never downloaded fully.
+ */
+ X_DOWNLOADED_PARTIAL,
+
+ /**
+ * General purpose flag that can be used by any remote store. The flag will be
+ * saved and restored by the LocalStore.
+ */
+ X_STORE_1,
+
+ /**
+ * General purpose flag that can be used by any remote store. The flag will be
+ * saved and restored by the LocalStore.
+ */
+ X_STORE_2,
+
+}
diff --git a/email2/emailcommon/src/com/android/emailcommon/mail/Folder.java b/email2/emailcommon/src/com/android/emailcommon/mail/Folder.java
new file mode 100644
index 0000000..c58988d
--- /dev/null
+++ b/email2/emailcommon/src/com/android/emailcommon/mail/Folder.java
@@ -0,0 +1,209 @@
+/*
+ * 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.emailcommon.mail;
+
+import com.android.emailcommon.service.SearchParams;
+import com.google.common.annotations.VisibleForTesting;
+
+
+public abstract class Folder {
+ public enum OpenMode {
+ READ_WRITE, READ_ONLY,
+ }
+
+ public enum FolderType {
+ HOLDS_FOLDERS, HOLDS_MESSAGES,
+ }
+
+ /**
+ * Identifiers of "special" folders.
+ */
+ public enum FolderRole {
+ INBOX, // NOTE: The folder's name must be INBOX
+ TRASH,
+ SENT,
+ DRAFTS,
+
+ OUTBOX, // Local folders only - not used in remote Stores
+ OTHER, // this folder has no specific role
+ UNKNOWN // the role of this folder is unknown
+ }
+
+ /**
+ * Callback for each message retrieval.
+ *
+ * Not all {@link Folder} implementations may invoke it.
+ */
+ public interface MessageRetrievalListener {
+ public void messageRetrieved(Message message);
+ public void loadAttachmentProgress(int progress);
+ }
+
+ /**
+ * Forces an open of the MailProvider. If the provider is already open this
+ * function returns without doing anything.
+ *
+ * @param mode READ_ONLY or READ_WRITE
+ * @param callbacks Pointer to callbacks class. This may be used by the folder between this
+ * time and when close() is called. This is only used for remote stores - should be null
+ * for LocalStore.LocalFolder.
+ */
+ public abstract void open(OpenMode mode)
+ throws MessagingException;
+
+ /**
+ * Forces a close of the MailProvider. Any further access will attempt to
+ * reopen the MailProvider.
+ *
+ * @param expunge If true all deleted messages will be expunged.
+ */
+ public abstract void close(boolean expunge) throws MessagingException;
+
+ /**
+ * @return True if further commands are not expected to have to open the
+ * connection.
+ */
+ @VisibleForTesting
+ public abstract boolean isOpen();
+
+ /**
+ * Returns the mode the folder was opened with. This may be different than the mode the open
+ * was requested with.
+ */
+ public abstract OpenMode getMode() throws MessagingException;
+
+ /**
+ * Reports if the Store is able to create folders of the given type.
+ * Does not actually attempt to create a folder.
+ * @param type
+ * @return true if can create, false if cannot create
+ */
+ public abstract boolean canCreate(FolderType type);
+
+ /**
+ * Attempt to create the given folder remotely using the given type.
+ * @return true if created, false if cannot create (e.g. server side)
+ */
+ public abstract boolean create(FolderType type) throws MessagingException;
+
+ public abstract boolean exists() throws MessagingException;
+
+ /**
+ * Returns the number of messages in the selected folder.
+ */
+ public abstract int getMessageCount() throws MessagingException;
+
+ public abstract int getUnreadMessageCount() throws MessagingException;
+
+ public abstract Message getMessage(String uid) throws MessagingException;
+
+ /**
+ * Fetches the given list of messages. The specified listener is notified as
+ * each fetch completes. Messages are downloaded as (as) lightweight (as
+ * possible) objects to be filled in with later requests. In most cases this
+ * means that only the UID is downloaded.
+ */
+ public abstract Message[] getMessages(int start, int end, MessageRetrievalListener listener)
+ throws MessagingException;
+
+ public abstract Message[] getMessages(SearchParams params,MessageRetrievalListener listener)
+ throws MessagingException;
+
+ public abstract Message[] getMessages(String[] uids, MessageRetrievalListener listener)
+ throws MessagingException;
+
+ /**
+ * Return a set of messages based on the state of the flags.
+ * Note: Not typically implemented in remote stores, so not abstract.
+ *
+ * @param setFlags The flags that should be set for a message to be selected (can be null)
+ * @param clearFlags The flags that should be clear for a message to be selected (can be null)
+ * @param listener
+ * @return A list of messages matching the desired flag states.
+ * @throws MessagingException
+ */
+ public Message[] getMessages(Flag[] setFlags, Flag[] clearFlags,
+ MessageRetrievalListener listener) throws MessagingException {
+ throw new MessagingException("Not implemented");
+ }
+
+ public abstract void appendMessages(Message[] messages) throws MessagingException;
+
+ /**
+ * Copies the given messages to the destination folder.
+ */
+ public abstract void copyMessages(Message[] msgs, Folder folder,
+ MessageUpdateCallbacks callbacks) throws MessagingException;
+
+ public abstract void setFlags(Message[] messages, Flag[] flags, boolean value)
+ throws MessagingException;
+
+ public abstract Message[] expunge() throws MessagingException;
+
+ public abstract void fetch(Message[] messages, FetchProfile fp,
+ MessageRetrievalListener listener) throws MessagingException;
+
+ public abstract void delete(boolean recurse) throws MessagingException;
+
+ public abstract String getName();
+
+ public abstract Flag[] getPermanentFlags() throws MessagingException;
+
+ /**
+ * This method returns a string identifying the name of a "role" folder
+ * (such as inbox, draft, sent, or trash). Stores that do not implement this
+ * feature can be used - the account UI will provide default strings. To
+ * let the server identify specific folder roles, simply override this method.
+ *
+ * @return The server- or protocol- specific role for this folder. If some roles are known
+ * but this is not one of them, return FolderRole.OTHER. If roles are unsupported here,
+ * return FolderRole.UNKNOWN.
+ */
+ public FolderRole getRole() {
+ return FolderRole.UNKNOWN;
+ }
+
+ /**
+ * Create an empty message of the appropriate type for the Folder.
+ */
+ public abstract Message createMessage(String uid) throws MessagingException;
+
+ /**
+ * Callback interface by which a folder can report UID changes caused by certain operations.
+ */
+ public interface MessageUpdateCallbacks {
+ /**
+ * The operation caused the message's UID to change
+ * @param message The message for which the UID changed
+ * @param newUid The new UID for the message
+ */
+ public void onMessageUidChange(Message message, String newUid) throws MessagingException;
+
+ /**
+ * The operation could not be completed because the message doesn't exist
+ * (for example, it was already deleted from the server side.)
+ * @param message The message that does not exist
+ * @throws MessagingException
+ */
+ public void onMessageNotFound(Message message) throws MessagingException;
+ }
+
+ @Override
+ public String toString() {
+ return getName();
+ }
+}
diff --git a/email2/emailcommon/src/com/android/emailcommon/mail/MeetingInfo.java b/email2/emailcommon/src/com/android/emailcommon/mail/MeetingInfo.java
new file mode 100644
index 0000000..1489a55
--- /dev/null
+++ b/email2/emailcommon/src/com/android/emailcommon/mail/MeetingInfo.java
@@ -0,0 +1,29 @@
+/*
+ * 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.emailcommon.mail;
+
+public class MeetingInfo {
+ // Predefined tags; others can be added
+ public static final String MEETING_DTSTAMP = "DTSTAMP";
+ public static final String MEETING_UID = "UID";
+ public static final String MEETING_ORGANIZER_EMAIL = "ORGMAIL";
+ public static final String MEETING_DTSTART = "DTSTART";
+ public static final String MEETING_DTEND = "DTEND";
+ public static final String MEETING_TITLE = "TITLE";
+ public static final String MEETING_LOCATION = "LOC";
+ public static final String MEETING_RESPONSE_REQUESTED = "RESPONSE";
+}
diff --git a/email2/emailcommon/src/com/android/emailcommon/mail/Message.java b/email2/emailcommon/src/com/android/emailcommon/mail/Message.java
new file mode 100644
index 0000000..09aef87
--- /dev/null
+++ b/email2/emailcommon/src/com/android/emailcommon/mail/Message.java
@@ -0,0 +1,159 @@
+/*
+ * 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.emailcommon.mail;
+
+import java.util.Date;
+import java.util.HashSet;
+
+public abstract class Message implements Part, Body {
+ public static final Message[] EMPTY_ARRAY = new Message[0];
+
+ public enum RecipientType {
+ TO, CC, BCC,
+ }
+
+ protected String mUid;
+
+ private HashSet<Flag> mFlags = null;
+
+ protected Date mInternalDate;
+
+ protected Folder mFolder;
+
+ public String getUid() {
+ return mUid;
+ }
+
+ public void setUid(String uid) {
+ this.mUid = uid;
+ }
+
+ public Folder getFolder() {
+ return mFolder;
+ }
+
+ public abstract String getSubject() throws MessagingException;
+
+ public abstract void setSubject(String subject) throws MessagingException;
+
+ public Date getInternalDate() {
+ return mInternalDate;
+ }
+
+ public void setInternalDate(Date internalDate) {
+ this.mInternalDate = internalDate;
+ }
+
+ public abstract Date getReceivedDate() throws MessagingException;
+
+ public abstract Date getSentDate() throws MessagingException;
+
+ public abstract void setSentDate(Date sentDate) throws MessagingException;
+
+ public abstract Address[] getRecipients(RecipientType type) throws MessagingException;
+
+ public abstract void setRecipients(RecipientType type, Address[] addresses)
+ throws MessagingException;
+
+ public void setRecipient(RecipientType type, Address address) throws MessagingException {
+ setRecipients(type, new Address[] {
+ address
+ });
+ }
+
+ public abstract Address[] getFrom() throws MessagingException;
+
+ public abstract void setFrom(Address from) throws MessagingException;
+
+ public abstract Address[] getReplyTo() throws MessagingException;
+
+ public abstract void setReplyTo(Address[] from) throws MessagingException;
+
+ public abstract Body getBody() throws MessagingException;
+
+ public abstract String getContentType() throws MessagingException;
+
+ public abstract void addHeader(String name, String value) throws MessagingException;
+
+ public abstract void setHeader(String name, String value) throws MessagingException;
+
+ public abstract String[] getHeader(String name) throws MessagingException;
+
+ public abstract void removeHeader(String name) throws MessagingException;
+
+ // Always use these instead of getHeader("Message-ID") or setHeader("Message-ID");
+ public abstract void setMessageId(String messageId) throws MessagingException;
+ public abstract String getMessageId() throws MessagingException;
+
+ public abstract void setBody(Body body) throws MessagingException;
+
+ public boolean isMimeType(String mimeType) throws MessagingException {
+ return getContentType().startsWith(mimeType);
+ }
+
+ private HashSet<Flag> getFlagSet() {
+ if (mFlags == null) {
+ mFlags = new HashSet<Flag>();
+ }
+ return mFlags;
+ }
+
+ /*
+ * TODO Refactor Flags at some point to be able to store user defined flags.
+ */
+ public Flag[] getFlags() {
+ return getFlagSet().toArray(new Flag[] {});
+ }
+
+ /**
+ * Set/clear a flag directly, without involving overrides of {@link #setFlag} in subclasses.
+ * Only used for testing.
+ */
+ public final void setFlagDirectlyForTest(Flag flag, boolean set) throws MessagingException {
+ if (set) {
+ getFlagSet().add(flag);
+ } else {
+ getFlagSet().remove(flag);
+ }
+ }
+
+ public void setFlag(Flag flag, boolean set) throws MessagingException {
+ setFlagDirectlyForTest(flag, set);
+ }
+
+ /**
+ * This method calls setFlag(Flag, boolean)
+ * @param flags
+ * @param set
+ */
+ public void setFlags(Flag[] flags, boolean set) throws MessagingException {
+ for (Flag flag : flags) {
+ setFlag(flag, set);
+ }
+ }
+
+ public boolean isSet(Flag flag) {
+ return getFlagSet().contains(flag);
+ }
+
+ public abstract void saveChanges() throws MessagingException;
+
+ @Override
+ public String toString() {
+ return getClass().getSimpleName() + ':' + mUid;
+ }
+}
diff --git a/email2/emailcommon/src/com/android/emailcommon/mail/MessageDateComparator.java b/email2/emailcommon/src/com/android/emailcommon/mail/MessageDateComparator.java
new file mode 100644
index 0000000..0b1a551
--- /dev/null
+++ b/email2/emailcommon/src/com/android/emailcommon/mail/MessageDateComparator.java
@@ -0,0 +1,34 @@
+/*
+ * 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.emailcommon.mail;
+
+import java.util.Comparator;
+
+public class MessageDateComparator implements Comparator<Message> {
+ public int compare(Message o1, Message o2) {
+ try {
+ if (o1.getSentDate() == null) {
+ return 1;
+ } else if (o2.getSentDate() == null) {
+ return -1;
+ } else
+ return o2.getSentDate().compareTo(o1.getSentDate());
+ } catch (Exception e) {
+ return 0;
+ }
+ }
+}
diff --git a/email2/emailcommon/src/com/android/emailcommon/mail/MessagingException.java b/email2/emailcommon/src/com/android/emailcommon/mail/MessagingException.java
new file mode 100644
index 0000000..4a8ceba
--- /dev/null
+++ b/email2/emailcommon/src/com/android/emailcommon/mail/MessagingException.java
@@ -0,0 +1,136 @@
+/*
+ * 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.emailcommon.mail;
+
+
+/**
+ * This exception is used for most types of failures that occur during server interactions.
+ *
+ * Data passed through this exception should be considered non-localized. Any strings should
+ * either be internal-only (for debugging) or server-generated.
+ *
+ * TO DO: Does it make sense to further collapse AuthenticationFailedException and
+ * CertificateValidationException and any others into this?
+ */
+public class MessagingException extends Exception {
+ public static final long serialVersionUID = -1;
+
+ public static final int NO_ERROR = -1;
+ /** Any exception that does not specify a specific issue */
+ public static final int UNSPECIFIED_EXCEPTION = 0;
+ /** Connection or IO errors */
+ public static final int IOERROR = 1;
+ /** The configuration requested TLS but the server did not support it. */
+ public static final int TLS_REQUIRED = 2;
+ /** Authentication is required but the server did not support it. */
+ public static final int AUTH_REQUIRED = 3;
+ /** General security failures */
+ public static final int GENERAL_SECURITY = 4;
+ /** Authentication failed */
+ public static final int AUTHENTICATION_FAILED = 5;
+ /** Attempt to create duplicate account */
+ public static final int DUPLICATE_ACCOUNT = 6;
+ /** Required security policies reported - advisory only */
+ public static final int SECURITY_POLICIES_REQUIRED = 7;
+ /** Required security policies not supported */
+ public static final int SECURITY_POLICIES_UNSUPPORTED = 8;
+ /** The protocol (or protocol version) isn't supported */
+ public static final int PROTOCOL_VERSION_UNSUPPORTED = 9;
+ /** The server's SSL certificate couldn't be validated */
+ public static final int CERTIFICATE_VALIDATION_ERROR = 10;
+ /** Authentication failed during autodiscover */
+ public static final int AUTODISCOVER_AUTHENTICATION_FAILED = 11;
+ /** Autodiscover completed with a result (non-error) */
+ public static final int AUTODISCOVER_AUTHENTICATION_RESULT = 12;
+ /** Ambiguous failure; server error or bad credentials */
+ public static final int AUTHENTICATION_FAILED_OR_SERVER_ERROR = 13;
+ /** The server refused access */
+ public static final int ACCESS_DENIED = 14;
+ /** The server refused access */
+ public static final int ATTACHMENT_NOT_FOUND = 15;
+ /** A client SSL certificate is required for connections to the server */
+ public static final int CLIENT_CERTIFICATE_REQUIRED = 16;
+ /** The client SSL certificate specified is invalid */
+ public static final int CLIENT_CERTIFICATE_ERROR = 17;
+
+ protected int mExceptionType;
+ // Exception type-specific data
+ protected Object mExceptionData;
+
+ public MessagingException(String message, Throwable throwable) {
+ this(UNSPECIFIED_EXCEPTION, message, throwable);
+ }
+
+ public MessagingException(int exceptionType, String message, Throwable throwable) {
+ super(message, throwable);
+ mExceptionType = exceptionType;
+ mExceptionData = null;
+ }
+
+ /**
+ * Constructs a MessagingException with an exceptionType and a null message.
+ * @param exceptionType The exception type to set for this exception.
+ */
+ public MessagingException(int exceptionType) {
+ this(exceptionType, null, null);
+ }
+
+ /**
+ * Constructs a MessagingException with a message.
+ * @param message the message for this exception
+ */
+ public MessagingException(String message) {
+ this(UNSPECIFIED_EXCEPTION, message, null);
+ }
+
+ /**
+ * Constructs a MessagingException with an exceptionType and a message.
+ * @param exceptionType The exception type to set for this exception.
+ */
+ public MessagingException(int exceptionType, String message) {
+ this(exceptionType, message, null);
+ }
+
+ /**
+ * Constructs a MessagingException with an exceptionType, a message, and data
+ * @param exceptionType The exception type to set for this exception.
+ * @param message the message for the exception (or null)
+ * @param data exception-type specific data for the exception (or null)
+ */
+ public MessagingException(int exceptionType, String message, Object data) {
+ super(message);
+ mExceptionType = exceptionType;
+ mExceptionData = data;
+ }
+
+ /**
+ * Return the exception type. Will be OTHER_EXCEPTION if not explicitly set.
+ *
+ * @return Returns the exception type.
+ */
+ public int getExceptionType() {
+ return mExceptionType;
+ }
+ /**
+ * Return the exception data. Will be null if not explicitly set.
+ *
+ * @return Returns the exception data.
+ */
+ public Object getExceptionData() {
+ return mExceptionData;
+ }
+}
\ No newline at end of file
diff --git a/email2/emailcommon/src/com/android/emailcommon/mail/Multipart.java b/email2/emailcommon/src/com/android/emailcommon/mail/Multipart.java
new file mode 100644
index 0000000..4a1a067
--- /dev/null
+++ b/email2/emailcommon/src/com/android/emailcommon/mail/Multipart.java
@@ -0,0 +1,63 @@
+/*
+ * 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.emailcommon.mail;
+
+import java.util.ArrayList;
+
+public abstract class Multipart implements Body {
+ protected Part mParent;
+
+ protected ArrayList<BodyPart> mParts = new ArrayList<BodyPart>();
+
+ protected String mContentType;
+
+ public void addBodyPart(BodyPart part) throws MessagingException {
+ mParts.add(part);
+ }
+
+ public void addBodyPart(BodyPart part, int index) throws MessagingException {
+ mParts.add(index, part);
+ }
+
+ public BodyPart getBodyPart(int index) throws MessagingException {
+ return mParts.get(index);
+ }
+
+ public String getContentType() throws MessagingException {
+ return mContentType;
+ }
+
+ public int getCount() throws MessagingException {
+ return mParts.size();
+ }
+
+ public boolean removeBodyPart(BodyPart part) throws MessagingException {
+ return mParts.remove(part);
+ }
+
+ public void removeBodyPart(int index) throws MessagingException {
+ mParts.remove(index);
+ }
+
+ public Part getParent() throws MessagingException {
+ return mParent;
+ }
+
+ public void setParent(Part parent) throws MessagingException {
+ this.mParent = parent;
+ }
+}
diff --git a/email2/emailcommon/src/com/android/emailcommon/mail/PackedString.java b/email2/emailcommon/src/com/android/emailcommon/mail/PackedString.java
new file mode 100644
index 0000000..de5fe46
--- /dev/null
+++ b/email2/emailcommon/src/com/android/emailcommon/mail/PackedString.java
@@ -0,0 +1,176 @@
+/*
+ * 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.emailcommon.mail;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * A utility class for creating and modifying Strings that are tagged and packed together.
+ *
+ * Uses non-printable (control chars) for internal delimiters; Intended for regular displayable
+ * strings only, so please use base64 or other encoding if you need to hide any binary data here.
+ *
+ * Binary compatible with Address.pack() format, which should migrate to use this code.
+ */
+public class PackedString {
+
+ /**
+ * Packing format is:
+ * element : [ value ] or [ value TAG-DELIMITER tag ]
+ * packed-string : [ element ] [ ELEMENT-DELIMITER [ element ] ]*
+ */
+ private static final char DELIMITER_ELEMENT = '\1';
+ private static final char DELIMITER_TAG = '\2';
+
+ private String mString;
+ private HashMap<String, String> mExploded;
+ private static final HashMap<String, String> EMPTY_MAP = new HashMap<String, String>();
+
+ /**
+ * Create a packed string using an already-packed string (e.g. from database)
+ * @param string packed string
+ */
+ public PackedString(String string) {
+ mString = string;
+ mExploded = null;
+ }
+
+ /**
+ * Get the value referred to by a given tag. If the tag does not exist, return null.
+ * @param tag identifier of string of interest
+ * @return returns value, or null if no string is found
+ */
+ public String get(String tag) {
+ if (mExploded == null) {
+ mExploded = explode(mString);
+ }
+ return mExploded.get(tag);
+ }
+
+ /**
+ * Return a map of all of the values referred to by a given tag. This is a shallow
+ * copy, don't edit the values.
+ * @return a map of the values in the packed string
+ */
+ public Map<String, String> unpack() {
+ if (mExploded == null) {
+ mExploded = explode(mString);
+ }
+ return new HashMap<String,String>(mExploded);
+ }
+
+ /**
+ * Read out all values into a map.
+ */
+ private static HashMap<String, String> explode(String packed) {
+ if (packed == null || packed.length() == 0) {
+ return EMPTY_MAP;
+ }
+ HashMap<String, String> map = new HashMap<String, String>();
+
+ int length = packed.length();
+ int elementStartIndex = 0;
+ int elementEndIndex = 0;
+ int tagEndIndex = packed.indexOf(DELIMITER_TAG);
+
+ while (elementStartIndex < length) {
+ elementEndIndex = packed.indexOf(DELIMITER_ELEMENT, elementStartIndex);
+ if (elementEndIndex == -1) {
+ elementEndIndex = length;
+ }
+ String tag;
+ String value;
+ if (tagEndIndex == -1 || elementEndIndex <= tagEndIndex) {
+ // in this case the DELIMITER_PERSONAL is in a future pair (or not found)
+ // so synthesize a positional tag for the value, and don't update tagEndIndex
+ value = packed.substring(elementStartIndex, elementEndIndex);
+ tag = Integer.toString(map.size());
+ } else {
+ value = packed.substring(elementStartIndex, tagEndIndex);
+ tag = packed.substring(tagEndIndex + 1, elementEndIndex);
+ // scan forward for next tag, if any
+ tagEndIndex = packed.indexOf(DELIMITER_TAG, elementEndIndex + 1);
+ }
+ map.put(tag, value);
+ elementStartIndex = elementEndIndex + 1;
+ }
+
+ return map;
+ }
+
+ /**
+ * Builder class for creating PackedString values. Can also be used for editing existing
+ * PackedString representations.
+ */
+ static public class Builder {
+ HashMap<String, String> mMap;
+
+ /**
+ * Create a builder that's empty (for filling)
+ */
+ public Builder() {
+ mMap = new HashMap<String, String>();
+ }
+
+ /**
+ * Create a builder using the values of an existing PackedString (for editing).
+ */
+ public Builder(String packed) {
+ mMap = explode(packed);
+ }
+
+ /**
+ * Add a tagged value
+ * @param tag identifier of string of interest
+ * @param value the value to record in this position. null to delete entry.
+ */
+ public void put(String tag, String value) {
+ if (value == null) {
+ mMap.remove(tag);
+ } else {
+ mMap.put(tag, value);
+ }
+ }
+
+ /**
+ * Get the value referred to by a given tag. If the tag does not exist, return null.
+ * @param tag identifier of string of interest
+ * @return returns value, or null if no string is found
+ */
+ public String get(String tag) {
+ return mMap.get(tag);
+ }
+
+ /**
+ * Pack the values and return a single, encoded string
+ */
+ @Override
+ public String toString() {
+ StringBuilder sb = new StringBuilder();
+ for (Map.Entry<String,String> entry : mMap.entrySet()) {
+ if (sb.length() > 0) {
+ sb.append(DELIMITER_ELEMENT);
+ }
+ sb.append(entry.getValue());
+ sb.append(DELIMITER_TAG);
+ sb.append(entry.getKey());
+ }
+ return sb.toString();
+ }
+ }
+}
diff --git a/email2/emailcommon/src/com/android/emailcommon/mail/Part.java b/email2/emailcommon/src/com/android/emailcommon/mail/Part.java
new file mode 100644
index 0000000..eeb233c
--- /dev/null
+++ b/email2/emailcommon/src/com/android/emailcommon/mail/Part.java
@@ -0,0 +1,52 @@
+/*
+ * 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.emailcommon.mail;
+
+import java.io.IOException;
+import java.io.OutputStream;
+
+public interface Part extends Fetchable {
+ public void addHeader(String name, String value) throws MessagingException;
+
+ public void removeHeader(String name) throws MessagingException;
+
+ public void setHeader(String name, String value) throws MessagingException;
+
+ public Body getBody() throws MessagingException;
+
+ public String getContentType() throws MessagingException;
+
+ public String getDisposition() throws MessagingException;
+
+ public String getContentId() throws MessagingException;
+
+ public String[] getHeader(String name) throws MessagingException;
+
+ public void setExtendedHeader(String name, String value) throws MessagingException;
+
+ public String getExtendedHeader(String name) throws MessagingException;
+
+ public int getSize() throws MessagingException;
+
+ public boolean isMimeType(String mimeType) throws MessagingException;
+
+ public String getMimeType() throws MessagingException;
+
+ public void setBody(Body body) throws MessagingException;
+
+ public void writeTo(OutputStream out) throws IOException, MessagingException;
+}
diff --git a/email2/emailcommon/src/com/android/emailcommon/provider/Account.java b/email2/emailcommon/src/com/android/emailcommon/provider/Account.java
new file mode 100755
index 0000000..3914176
--- /dev/null
+++ b/email2/emailcommon/src/com/android/emailcommon/provider/Account.java
@@ -0,0 +1,968 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.emailcommon.provider;
+
+import android.content.ContentProviderOperation;
+import android.content.ContentProviderResult;
+import android.content.ContentResolver;
+import android.content.ContentUris;
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.OperationApplicationException;
+import android.database.Cursor;
+import android.net.ConnectivityManager;
+import android.net.NetworkInfo;
+import android.net.Uri;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.os.RemoteException;
+
+import com.android.emailcommon.provider.EmailContent.AccountColumns;
+import com.android.emailcommon.utility.Utility;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.UUID;
+
+public final class Account extends EmailContent implements AccountColumns, Parcelable {
+ public static final String TABLE_NAME = "Account";
+ @SuppressWarnings("hiding")
+ public static final Uri CONTENT_URI = Uri.parse(EmailContent.CONTENT_URI + "/account");
+ public static final Uri ADD_TO_FIELD_URI =
+ Uri.parse(EmailContent.CONTENT_URI + "/accountIdAddToField");
+ public static final Uri RESET_NEW_MESSAGE_COUNT_URI =
+ Uri.parse(EmailContent.CONTENT_URI + "/resetNewMessageCount");
+ public static final Uri NOTIFIER_URI =
+ Uri.parse(EmailContent.CONTENT_NOTIFIER_URI + "/account");
+ public static final Uri DEFAULT_ACCOUNT_ID_URI =
+ Uri.parse(EmailContent.CONTENT_URI + "/account/default");
+
+ // Define all pseudo account IDs here to avoid conflict with one another.
+ /**
+ * Pseudo account ID to represent a "combined account" that includes messages and mailboxes
+ * from all defined accounts.
+ *
+ * <em>IMPORTANT</em>: This must never be stored to the database.
+ */
+ public static final long ACCOUNT_ID_COMBINED_VIEW = 0x1000000000000000L;
+ /**
+ * Pseudo account ID to represent "no account". This may be used any time the account ID
+ * may not be known or when we want to specifically select "no" account.
+ *
+ * <em>IMPORTANT</em>: This must never be stored to the database.
+ */
+ public static final long NO_ACCOUNT = -1L;
+
+ // Whether or not the user has asked for notifications of new mail in this account
+ public final static int FLAGS_NOTIFY_NEW_MAIL = 1<<0;
+ // Whether or not the user has asked for vibration notifications with all new mail
+ public final static int FLAGS_VIBRATE_ALWAYS = 1<<1;
+ // Bit mask for the account's deletion policy (see DELETE_POLICY_x below)
+ public static final int FLAGS_DELETE_POLICY_MASK = 1<<2 | 1<<3;
+ public static final int FLAGS_DELETE_POLICY_SHIFT = 2;
+ // Whether the account is in the process of being created; any account reconciliation code
+ // MUST ignore accounts with this bit set; in addition, ContentObservers for this data
+ // SHOULD consider the state of this flag during operation
+ public static final int FLAGS_INCOMPLETE = 1<<4;
+ // Security hold is used when the device is not in compliance with security policies
+ // required by the server; in this state, the user MUST be alerted to the need to update
+ // security settings. Sync adapters SHOULD NOT attempt to sync when this flag is set.
+ public static final int FLAGS_SECURITY_HOLD = 1<<5;
+ // Whether or not the user has asked for vibration notifications when the ringer is silent
+ public static final int FLAGS_VIBRATE_WHEN_SILENT = 1<<6;
+ // Whether the account supports "smart forward" (i.e. the server appends the original
+ // message along with any attachments to the outgoing message)
+ public static final int FLAGS_SUPPORTS_SMART_FORWARD = 1<<7;
+ // Whether the account should try to cache attachments in the background
+ public static final int FLAGS_BACKGROUND_ATTACHMENTS = 1<<8;
+ // Available to sync adapter
+ public static final int FLAGS_SYNC_ADAPTER = 1<<9;
+ // Sync disabled is a status commanded by the server; the sync adapter SHOULD NOT try to
+ // sync mailboxes in this account automatically. A manual sync request to sync a mailbox
+ // with sync disabled SHOULD try to sync and report any failure result via the UI.
+ public static final int FLAGS_SYNC_DISABLED = 1<<10;
+ // Whether or not server-side search is supported by this account
+ public static final int FLAGS_SUPPORTS_SEARCH = 1<<11;
+ // Whether or not server-side search supports global search (i.e. all mailboxes); only valid
+ // if FLAGS_SUPPORTS_SEARCH is true
+ public static final int FLAGS_SUPPORTS_GLOBAL_SEARCH = 1<<12;
+
+ // Deletion policy (see FLAGS_DELETE_POLICY_MASK, above)
+ public static final int DELETE_POLICY_NEVER = 0;
+ public static final int DELETE_POLICY_7DAYS = 1<<0; // not supported
+ public static final int DELETE_POLICY_ON_DELETE = 1<<1;
+
+ // Sentinel values for the mSyncInterval field of both Account records
+ public static final int CHECK_INTERVAL_NEVER = -1;
+ public static final int CHECK_INTERVAL_PUSH = -2;
+
+ public String mDisplayName;
+ public String mEmailAddress;
+ public String mSyncKey;
+ public int mSyncLookback;
+ public int mSyncInterval;
+ public long mHostAuthKeyRecv;
+ public long mHostAuthKeySend;
+ public int mFlags;
+ public boolean mIsDefault; // note: callers should use getDefaultAccountId()
+ public String mCompatibilityUuid;
+ public String mSenderName;
+ public String mRingtoneUri;
+ public String mProtocolVersion;
+ public int mNewMessageCount;
+ public String mSecuritySyncKey;
+ public String mSignature;
+ public long mPolicyKey;
+
+ // Convenience for creating/working with an account
+ public transient HostAuth mHostAuthRecv;
+ public transient HostAuth mHostAuthSend;
+ public transient Policy mPolicy;
+ // Might hold the corresponding AccountManager account structure
+ public transient android.accounts.Account mAmAccount;
+
+ public static final int CONTENT_ID_COLUMN = 0;
+ public static final int CONTENT_DISPLAY_NAME_COLUMN = 1;
+ public static final int CONTENT_EMAIL_ADDRESS_COLUMN = 2;
+ public static final int CONTENT_SYNC_KEY_COLUMN = 3;
+ public static final int CONTENT_SYNC_LOOKBACK_COLUMN = 4;
+ public static final int CONTENT_SYNC_INTERVAL_COLUMN = 5;
+ public static final int CONTENT_HOST_AUTH_KEY_RECV_COLUMN = 6;
+ public static final int CONTENT_HOST_AUTH_KEY_SEND_COLUMN = 7;
+ public static final int CONTENT_FLAGS_COLUMN = 8;
+ public static final int CONTENT_IS_DEFAULT_COLUMN = 9;
+ public static final int CONTENT_COMPATIBILITY_UUID_COLUMN = 10;
+ public static final int CONTENT_SENDER_NAME_COLUMN = 11;
+ public static final int CONTENT_RINGTONE_URI_COLUMN = 12;
+ public static final int CONTENT_PROTOCOL_VERSION_COLUMN = 13;
+ public static final int CONTENT_NEW_MESSAGE_COUNT_COLUMN = 14;
+ public static final int CONTENT_SECURITY_SYNC_KEY_COLUMN = 15;
+ public static final int CONTENT_SIGNATURE_COLUMN = 16;
+ public static final int CONTENT_POLICY_KEY = 17;
+
+ public static final String[] CONTENT_PROJECTION = new String[] {
+ RECORD_ID, AccountColumns.DISPLAY_NAME,
+ AccountColumns.EMAIL_ADDRESS, AccountColumns.SYNC_KEY, AccountColumns.SYNC_LOOKBACK,
+ AccountColumns.SYNC_INTERVAL, AccountColumns.HOST_AUTH_KEY_RECV,
+ AccountColumns.HOST_AUTH_KEY_SEND, AccountColumns.FLAGS, AccountColumns.IS_DEFAULT,
+ AccountColumns.COMPATIBILITY_UUID, AccountColumns.SENDER_NAME,
+ AccountColumns.RINGTONE_URI, AccountColumns.PROTOCOL_VERSION,
+ AccountColumns.NEW_MESSAGE_COUNT, AccountColumns.SECURITY_SYNC_KEY,
+ AccountColumns.SIGNATURE, AccountColumns.POLICY_KEY
+ };
+
+ public static final int CONTENT_MAILBOX_TYPE_COLUMN = 1;
+
+ /**
+ * This projection is for listing account id's only
+ */
+ public static final String[] ID_TYPE_PROJECTION = new String[] {
+ RECORD_ID, MailboxColumns.TYPE
+ };
+
+ public static final int ACCOUNT_FLAGS_COLUMN_ID = 0;
+ public static final int ACCOUNT_FLAGS_COLUMN_FLAGS = 1;
+ public static final String[] ACCOUNT_FLAGS_PROJECTION = new String[] {
+ AccountColumns.ID, AccountColumns.FLAGS};
+
+ public static final String MAILBOX_SELECTION =
+ MessageColumns.MAILBOX_KEY + " =?";
+
+ public static final String UNREAD_COUNT_SELECTION =
+ MessageColumns.MAILBOX_KEY + " =? and " + MessageColumns.FLAG_READ + "= 0";
+
+ private static final String UUID_SELECTION = AccountColumns.COMPATIBILITY_UUID + " =?";
+
+ public static final String SECURITY_NONZERO_SELECTION =
+ Account.POLICY_KEY + " IS NOT NULL AND " + Account.POLICY_KEY + "!=0";
+
+ private static final String FIND_INBOX_SELECTION =
+ MailboxColumns.TYPE + " = " + Mailbox.TYPE_INBOX +
+ " AND " + MailboxColumns.ACCOUNT_KEY + " =?";
+
+ /**
+ * This projection is for searching for the default account
+ */
+ private static final String[] DEFAULT_ID_PROJECTION = new String[] {
+ RECORD_ID, IS_DEFAULT
+ };
+
+ /**
+ * no public constructor since this is a utility class
+ */
+ public Account() {
+ mBaseUri = CONTENT_URI;
+
+ // other defaults (policy)
+ mRingtoneUri = "content://settings/system/notification_sound";
+ mSyncInterval = -1;
+ mSyncLookback = -1;
+ mFlags = FLAGS_NOTIFY_NEW_MAIL;
+ mCompatibilityUuid = UUID.randomUUID().toString();
+ }
+
+ public static Account restoreAccountWithId(Context context, long id) {
+ return EmailContent.restoreContentWithId(context, Account.class,
+ Account.CONTENT_URI, Account.CONTENT_PROJECTION, id);
+ }
+
+ /**
+ * Returns {@code true} if the given account ID is a "normal" account. Normal accounts
+ * always have an ID greater than {@code 0} and not equal to any pseudo account IDs
+ * (such as {@link #ACCOUNT_ID_COMBINED_VIEW})
+ */
+ public static boolean isNormalAccount(long accountId) {
+ return (accountId > 0L) && (accountId != ACCOUNT_ID_COMBINED_VIEW);
+ }
+
+ /**
+ * Refresh an account that has already been loaded. This is slightly less expensive
+ * that generating a brand-new account object.
+ */
+ public void refresh(Context context) {
+ Cursor c = context.getContentResolver().query(getUri(), Account.CONTENT_PROJECTION,
+ null, null, null);
+ try {
+ c.moveToFirst();
+ restore(c);
+ } finally {
+ if (c != null) {
+ c.close();
+ }
+ }
+ }
+
+ @Override
+ public void restore(Cursor cursor) {
+ mId = cursor.getLong(CONTENT_ID_COLUMN);
+ mBaseUri = CONTENT_URI;
+ mDisplayName = cursor.getString(CONTENT_DISPLAY_NAME_COLUMN);
+ mEmailAddress = cursor.getString(CONTENT_EMAIL_ADDRESS_COLUMN);
+ mSyncKey = cursor.getString(CONTENT_SYNC_KEY_COLUMN);
+ mSyncLookback = cursor.getInt(CONTENT_SYNC_LOOKBACK_COLUMN);
+ mSyncInterval = cursor.getInt(CONTENT_SYNC_INTERVAL_COLUMN);
+ mHostAuthKeyRecv = cursor.getLong(CONTENT_HOST_AUTH_KEY_RECV_COLUMN);
+ mHostAuthKeySend = cursor.getLong(CONTENT_HOST_AUTH_KEY_SEND_COLUMN);
+ mFlags = cursor.getInt(CONTENT_FLAGS_COLUMN);
+ mIsDefault = cursor.getInt(CONTENT_IS_DEFAULT_COLUMN) == 1;
+ mCompatibilityUuid = cursor.getString(CONTENT_COMPATIBILITY_UUID_COLUMN);
+ mSenderName = cursor.getString(CONTENT_SENDER_NAME_COLUMN);
+ mRingtoneUri = cursor.getString(CONTENT_RINGTONE_URI_COLUMN);
+ mProtocolVersion = cursor.getString(CONTENT_PROTOCOL_VERSION_COLUMN);
+ mNewMessageCount = cursor.getInt(CONTENT_NEW_MESSAGE_COUNT_COLUMN);
+ mSecuritySyncKey = cursor.getString(CONTENT_SECURITY_SYNC_KEY_COLUMN);
+ mSignature = cursor.getString(CONTENT_SIGNATURE_COLUMN);
+ mPolicyKey = cursor.getLong(CONTENT_POLICY_KEY);
+ }
+
+ private long getId(Uri u) {
+ return Long.parseLong(u.getPathSegments().get(1));
+ }
+
+ /**
+ * @return the user-visible name for the account
+ */
+ public String getDisplayName() {
+ return mDisplayName;
+ }
+
+ /**
+ * Set the description. Be sure to call save() to commit to database.
+ * @param description the new description
+ */
+ public void setDisplayName(String description) {
+ mDisplayName = description;
+ }
+
+ /**
+ * @return the email address for this account
+ */
+ public String getEmailAddress() {
+ return mEmailAddress;
+ }
+
+ /**
+ * Set the Email address for this account. Be sure to call save() to commit to database.
+ * @param emailAddress the new email address for this account
+ */
+ public void setEmailAddress(String emailAddress) {
+ mEmailAddress = emailAddress;
+ }
+
+ /**
+ * @return the sender's name for this account
+ */
+ public String getSenderName() {
+ return mSenderName;
+ }
+
+ /**
+ * Set the sender's name. Be sure to call save() to commit to database.
+ * @param name the new sender name
+ */
+ public void setSenderName(String name) {
+ mSenderName = name;
+ }
+
+ public String getSignature() {
+ return mSignature;
+ }
+
+ public void setSignature(String signature) {
+ mSignature = signature;
+ }
+
+ /**
+ * @return the minutes per check (for polling)
+ * TODO define sentinel values for "never", "push", etc. See Account.java
+ */
+ public int getSyncInterval() {
+ return mSyncInterval;
+ }
+
+ /**
+ * Set the minutes per check (for polling). Be sure to call save() to commit to database.
+ * TODO define sentinel values for "never", "push", etc. See Account.java
+ * @param minutes the number of minutes between polling checks
+ */
+ public void setSyncInterval(int minutes) {
+ mSyncInterval = minutes;
+ }
+
+ /**
+ * @return One of the {@code Account.SYNC_WINDOW_*} constants that represents the sync
+ * lookback window.
+ * TODO define sentinel values for "all", "1 month", etc. See Account.java
+ */
+ public int getSyncLookback() {
+ return mSyncLookback;
+ }
+
+ /**
+ * Set the sync lookback window. Be sure to call save() to commit to database.
+ * TODO define sentinel values for "all", "1 month", etc. See Account.java
+ * @param value One of the {@link com.android.emailcommon.service.SyncWindow} constants
+ */
+ public void setSyncLookback(int value) {
+ mSyncLookback = value;
+ }
+
+ /**
+ * @return the flags for this account
+ * @see #FLAGS_NOTIFY_NEW_MAIL
+ * @see #FLAGS_VIBRATE_ALWAYS
+ * @see #FLAGS_VIBRATE_WHEN_SILENT
+ */
+ public int getFlags() {
+ return mFlags;
+ }
+
+ /**
+ * Set the flags for this account
+ * @see #FLAGS_NOTIFY_NEW_MAIL
+ * @see #FLAGS_VIBRATE_ALWAYS
+ * @see #FLAGS_VIBRATE_WHEN_SILENT
+ * @param newFlags the new value for the flags
+ */
+ public void setFlags(int newFlags) {
+ mFlags = newFlags;
+ }
+
+ /**
+ * @return the ringtone Uri for this account
+ */
+ public String getRingtone() {
+ return mRingtoneUri;
+ }
+
+ /**
+ * Set the ringtone Uri for this account
+ * @param newUri the new URI string for the ringtone for this account
+ */
+ public void setRingtone(String newUri) {
+ mRingtoneUri = newUri;
+ }
+
+ /**
+ * Set the "delete policy" as a simple 0,1,2 value set.
+ * @param newPolicy the new delete policy
+ */
+ public void setDeletePolicy(int newPolicy) {
+ mFlags &= ~FLAGS_DELETE_POLICY_MASK;
+ mFlags |= (newPolicy << FLAGS_DELETE_POLICY_SHIFT) & FLAGS_DELETE_POLICY_MASK;
+ }
+
+ /**
+ * Return the "delete policy" as a simple 0,1,2 value set.
+ * @return the current delete policy
+ */
+ public int getDeletePolicy() {
+ return (mFlags & FLAGS_DELETE_POLICY_MASK) >> FLAGS_DELETE_POLICY_SHIFT;
+ }
+
+ /**
+ * Return the Uuid associated with this account. This is primarily for compatibility
+ * with accounts set up by previous versions, because there are externals references
+ * to the Uuid (e.g. desktop shortcuts).
+ */
+ public String getUuid() {
+ return mCompatibilityUuid;
+ }
+
+ public HostAuth getOrCreateHostAuthSend(Context context) {
+ if (mHostAuthSend == null) {
+ if (mHostAuthKeySend != 0) {
+ mHostAuthSend = HostAuth.restoreHostAuthWithId(context, mHostAuthKeySend);
+ } else {
+ mHostAuthSend = new HostAuth();
+ }
+ }
+ return mHostAuthSend;
+ }
+
+ public HostAuth getOrCreateHostAuthRecv(Context context) {
+ if (mHostAuthRecv == null) {
+ if (mHostAuthKeyRecv != 0) {
+ mHostAuthRecv = HostAuth.restoreHostAuthWithId(context, mHostAuthKeyRecv);
+ } else {
+ mHostAuthRecv = new HostAuth();
+ }
+ }
+ return mHostAuthRecv;
+ }
+
+ /**
+ * For compatibility while converting to provider model, generate a "local store URI"
+ *
+ * @return a string in the form of a Uri, as used by the other parts of the email app
+ */
+ public String getLocalStoreUri(Context context) {
+ return "local://localhost/" + context.getDatabasePath(getUuid() + ".db");
+ }
+
+ /**
+ * @return true if the instance is of an EAS account.
+ *
+ * NOTE This method accesses the DB if {@link #mHostAuthRecv} hasn't been restored yet.
+ * Use caution when you use this on the main thread.
+ */
+ public boolean isEasAccount(Context context) {
+ return "eas".equals(getProtocol(context));
+ }
+
+ public boolean supportsMoveMessages(Context context) {
+ String protocol = getProtocol(context);
+ return "eas".equals(protocol) || "imap".equals(protocol);
+ }
+
+ /**
+ * @return true if the account supports "search".
+ */
+ public static boolean supportsServerSearch(Context context, long accountId) {
+ Account account = Account.restoreAccountWithId(context, accountId);
+ if (account == null) return false;
+ return (account.mFlags & Account.FLAGS_SUPPORTS_SEARCH) != 0;
+ }
+
+ /**
+ * Set the account to be the default account. If this is set to "true", when the account
+ * is saved, all other accounts will have the same value set to "false".
+ * @param newDefaultState the new default state - if true, others will be cleared.
+ */
+ public void setDefaultAccount(boolean newDefaultState) {
+ mIsDefault = newDefaultState;
+ }
+
+ /**
+ * @return {@link Uri} to this {@link Account} in the
+ * {@code content://com.android.email.provider/account/UUID} format, which is safe to use
+ * for desktop shortcuts.
+ *
+ * <p>We don't want to store _id in shortcuts, because
+ * {@link com.android.email.provider.AccountBackupRestore} won't preserve it.
+ */
+ public Uri getShortcutSafeUri() {
+ return getShortcutSafeUriFromUuid(mCompatibilityUuid);
+ }
+
+ /**
+ * @return {@link Uri} to an {@link Account} with a {@code uuid}.
+ */
+ public static Uri getShortcutSafeUriFromUuid(String uuid) {
+ return CONTENT_URI.buildUpon().appendEncodedPath(uuid).build();
+ }
+
+ /**
+ * Parse {@link Uri} in the {@code content://com.android.email.provider/account/ID} format
+ * where ID = account id (used on Eclair, Android 2.0-2.1) or UUID, and return _id of
+ * the {@link Account} associated with it.
+ *
+ * @param context context to access DB
+ * @param uri URI of interest
+ * @return _id of the {@link Account} associated with ID, or -1 if none found.
+ */
+ public static long getAccountIdFromShortcutSafeUri(Context context, Uri uri) {
+ // Make sure the URI is in the correct format.
+ if (!"content".equals(uri.getScheme())
+ || !AUTHORITY.equals(uri.getAuthority())) {
+ return -1;
+ }
+
+ final List<String> ps = uri.getPathSegments();
+ if (ps.size() != 2 || !"account".equals(ps.get(0))) {
+ return -1;
+ }
+
+ // Now get the ID part.
+ final String id = ps.get(1);
+
+ // First, see if ID can be parsed as long. (Eclair-style)
+ // (UUIDs have '-' in them, so they are always non-parsable.)
+ try {
+ return Long.parseLong(id);
+ } catch (NumberFormatException ok) {
+ // OK, it's not a long. Continue...
+ }
+
+ // Now id is a UUId.
+ return getAccountIdFromUuid(context, id);
+ }
+
+ /**
+ * @return ID of the account with the given UUID.
+ */
+ public static long getAccountIdFromUuid(Context context, String uuid) {
+ return Utility.getFirstRowLong(context,
+ CONTENT_URI, ID_PROJECTION,
+ UUID_SELECTION, new String[] {uuid}, null, 0, -1L);
+ }
+
+ /**
+ * Return the id of the default account. If one hasn't been explicitly specified, return
+ * the first one in the database (the logic is provided within EmailProvider)
+ * @param context the caller's context
+ * @return the id of the default account, or Account.NO_ACCOUNT if there are no accounts
+ */
+ static public long getDefaultAccountId(Context context) {
+ Cursor c = context.getContentResolver().query(
+ Account.DEFAULT_ACCOUNT_ID_URI, Account.ID_PROJECTION, null, null, null);
+ try {
+ if (c != null && c.moveToFirst()) {
+ return c.getLong(Account.ID_PROJECTION_COLUMN);
+ }
+ } finally {
+ c.close();
+ }
+ return Account.NO_ACCOUNT;
+ }
+
+ /**
+ * Given an account id, return the account's protocol
+ * @param context the caller's context
+ * @param accountId the id of the account to be examined
+ * @return the account's protocol (or null if the Account or HostAuth do not exist)
+ */
+ public static String getProtocol(Context context, long accountId) {
+ Account account = Account.restoreAccountWithId(context, accountId);
+ if (account != null) {
+ return account.getProtocol(context);
+ }
+ return null;
+ }
+
+ /**
+ * Return the account's protocol
+ * @param context the caller's context
+ * @return the account's protocol (or null if the HostAuth doesn't not exist)
+ */
+ public String getProtocol(Context context) {
+ HostAuth hostAuth = HostAuth.restoreHostAuthWithId(context, mHostAuthKeyRecv);
+ if (hostAuth != null) {
+ return hostAuth.mProtocol;
+ }
+ return null;
+ }
+
+ /**
+ * Return the account ID for a message with a given id
+ *
+ * @param context the caller's context
+ * @param messageId the id of the message
+ * @return the account ID, or -1 if the account doesn't exist
+ */
+ public static long getAccountIdForMessageId(Context context, long messageId) {
+ return Message.getKeyColumnLong(context, messageId, MessageColumns.ACCOUNT_KEY);
+ }
+
+ /**
+ * Return the account for a message with a given id
+ * @param context the caller's context
+ * @param messageId the id of the message
+ * @return the account, or null if the account doesn't exist
+ */
+ public static Account getAccountForMessageId(Context context, long messageId) {
+ long accountId = getAccountIdForMessageId(context, messageId);
+ if (accountId != -1) {
+ return Account.restoreAccountWithId(context, accountId);
+ }
+ return null;
+ }
+
+ /**
+ * @return true if an {@code accountId} is assigned to any existing account.
+ */
+ public static boolean isValidId(Context context, long accountId) {
+ return null != Utility.getFirstRowLong(context, CONTENT_URI, ID_PROJECTION,
+ ID_SELECTION, new String[] {Long.toString(accountId)}, null,
+ ID_PROJECTION_COLUMN);
+ }
+
+ /**
+ * Check a single account for security hold status.
+ */
+ public static boolean isSecurityHold(Context context, long accountId) {
+ return (Utility.getFirstRowLong(context,
+ ContentUris.withAppendedId(Account.CONTENT_URI, accountId),
+ ACCOUNT_FLAGS_PROJECTION, null, null, null, ACCOUNT_FLAGS_COLUMN_FLAGS, 0L)
+ & Account.FLAGS_SECURITY_HOLD) != 0;
+ }
+
+ /**
+ * @return id of the "inbox" mailbox, or -1 if not found.
+ */
+ public static long getInboxId(Context context, long accountId) {
+ return Utility.getFirstRowLong(context, Mailbox.CONTENT_URI, ID_PROJECTION,
+ FIND_INBOX_SELECTION, new String[] {Long.toString(accountId)}, null,
+ ID_PROJECTION_COLUMN, -1L);
+ }
+
+ /**
+ * Clear all account hold flags that are set.
+ *
+ * (This will trigger watchers, and in particular will cause EAS to try and resync the
+ * account(s).)
+ */
+ public static void clearSecurityHoldOnAllAccounts(Context context) {
+ ContentResolver resolver = context.getContentResolver();
+ Cursor c = resolver.query(Account.CONTENT_URI, ACCOUNT_FLAGS_PROJECTION,
+ SECURITY_NONZERO_SELECTION, null, null);
+ try {
+ while (c.moveToNext()) {
+ int flags = c.getInt(ACCOUNT_FLAGS_COLUMN_FLAGS);
+
+ if (0 != (flags & FLAGS_SECURITY_HOLD)) {
+ ContentValues cv = new ContentValues();
+ cv.put(AccountColumns.FLAGS, flags & ~FLAGS_SECURITY_HOLD);
+ long accountId = c.getLong(ACCOUNT_FLAGS_COLUMN_ID);
+ Uri uri = ContentUris.withAppendedId(Account.CONTENT_URI, accountId);
+ resolver.update(uri, cv, null, null);
+ }
+ }
+ } finally {
+ c.close();
+ }
+ }
+
+ /**
+ * Given an account id, determine whether the account is currently prohibited from automatic
+ * sync, due to roaming while the account's policy disables this
+ * @param context the caller's context
+ * @param accountId the account id
+ * @return true if the account can't automatically sync due to roaming; false otherwise
+ */
+ public static boolean isAutomaticSyncDisabledByRoaming(Context context, long accountId) {
+ Account account = Account.restoreAccountWithId(context, accountId);
+ // Account being deleted; just return
+ if (account == null) return false;
+ long policyKey = account.mPolicyKey;
+ // If no security policy, we're good
+ if (policyKey <= 0) return false;
+
+ ConnectivityManager cm =
+ (ConnectivityManager)context.getSystemService(Context.CONNECTIVITY_SERVICE);
+ NetworkInfo info = cm.getActiveNetworkInfo();
+ // If we're not on mobile, we're good
+ if (info == null || (info.getType() != ConnectivityManager.TYPE_MOBILE)) return false;
+ // If we're not roaming, we're good
+ if (!info.isRoaming()) return false;
+ Policy policy = Policy.restorePolicyWithId(context, policyKey);
+ // Account being deleted; just return
+ if (policy == null) return false;
+ return policy.mRequireManualSyncWhenRoaming;
+ }
+
+ /**
+ * Override update to enforce a single default account, and do it atomically
+ */
+ @Override
+ public int update(Context context, ContentValues cv) {
+ if (cv.containsKey(AccountColumns.IS_DEFAULT) &&
+ cv.getAsBoolean(AccountColumns.IS_DEFAULT)) {
+ ArrayList<ContentProviderOperation> ops = new ArrayList<ContentProviderOperation>();
+ ContentValues cv1 = new ContentValues();
+ cv1.put(AccountColumns.IS_DEFAULT, false);
+ // Clear the default flag in all accounts
+ ops.add(ContentProviderOperation.newUpdate(CONTENT_URI).withValues(cv1).build());
+ // Update this account
+ ops.add(ContentProviderOperation
+ .newUpdate(ContentUris.withAppendedId(CONTENT_URI, mId))
+ .withValues(cv).build());
+ try {
+ context.getContentResolver().applyBatch(AUTHORITY, ops);
+ return 1;
+ } catch (RemoteException e) {
+ // There is nothing to be done here; fail by returning 0
+ } catch (OperationApplicationException e) {
+ // There is nothing to be done here; fail by returning 0
+ }
+ return 0;
+ }
+ return super.update(context, cv);
+ }
+
+ /*
+ * Override this so that we can store the HostAuth's first and link them to the Account
+ * (non-Javadoc)
+ * @see com.android.email.provider.EmailContent#save(android.content.Context)
+ */
+ @Override
+ public Uri save(Context context) {
+ if (isSaved()) {
+ throw new UnsupportedOperationException();
+ }
+ // This logic is in place so I can (a) short circuit the expensive stuff when
+ // possible, and (b) override (and throw) if anyone tries to call save() or update()
+ // directly for Account, which are unsupported.
+ if (mHostAuthRecv == null && mHostAuthSend == null && mIsDefault == false &&
+ mPolicy != null) {
+ return super.save(context);
+ }
+
+ int index = 0;
+ int recvIndex = -1;
+ int sendIndex = -1;
+
+ // Create operations for saving the send and recv hostAuths
+ // Also, remember which operation in the array they represent
+ ArrayList<ContentProviderOperation> ops = new ArrayList<ContentProviderOperation>();
+ if (mHostAuthRecv != null) {
+ recvIndex = index++;
+ ops.add(ContentProviderOperation.newInsert(mHostAuthRecv.mBaseUri)
+ .withValues(mHostAuthRecv.toContentValues())
+ .build());
+ }
+ if (mHostAuthSend != null) {
+ sendIndex = index++;
+ ops.add(ContentProviderOperation.newInsert(mHostAuthSend.mBaseUri)
+ .withValues(mHostAuthSend.toContentValues())
+ .build());
+ }
+
+ // Create operations for making this the only default account
+ // Note, these are always updates because they change existing accounts
+ if (mIsDefault) {
+ index++;
+ ContentValues cv1 = new ContentValues();
+ cv1.put(AccountColumns.IS_DEFAULT, 0);
+ ops.add(ContentProviderOperation.newUpdate(CONTENT_URI).withValues(cv1).build());
+ }
+
+ // Now do the Account
+ ContentValues cv = null;
+ if (recvIndex >= 0 || sendIndex >= 0) {
+ cv = new ContentValues();
+ if (recvIndex >= 0) {
+ cv.put(Account.HOST_AUTH_KEY_RECV, recvIndex);
+ }
+ if (sendIndex >= 0) {
+ cv.put(Account.HOST_AUTH_KEY_SEND, sendIndex);
+ }
+ }
+
+ ContentProviderOperation.Builder b = ContentProviderOperation.newInsert(mBaseUri);
+ b.withValues(toContentValues());
+ if (cv != null) {
+ b.withValueBackReferences(cv);
+ }
+ ops.add(b.build());
+
+ try {
+ ContentProviderResult[] results =
+ context.getContentResolver().applyBatch(AUTHORITY, ops);
+ // If saving, set the mId's of the various saved objects
+ if (recvIndex >= 0) {
+ long newId = getId(results[recvIndex].uri);
+ mHostAuthKeyRecv = newId;
+ mHostAuthRecv.mId = newId;
+ }
+ if (sendIndex >= 0) {
+ long newId = getId(results[sendIndex].uri);
+ mHostAuthKeySend = newId;
+ mHostAuthSend.mId = newId;
+ }
+ Uri u = results[index].uri;
+ mId = getId(u);
+ return u;
+ } catch (RemoteException e) {
+ // There is nothing to be done here; fail by returning null
+ } catch (OperationApplicationException e) {
+ // There is nothing to be done here; fail by returning null
+ }
+ return null;
+ }
+
+ @Override
+ public ContentValues toContentValues() {
+ ContentValues values = new ContentValues();
+ values.put(AccountColumns.DISPLAY_NAME, mDisplayName);
+ values.put(AccountColumns.EMAIL_ADDRESS, mEmailAddress);
+ values.put(AccountColumns.SYNC_KEY, mSyncKey);
+ values.put(AccountColumns.SYNC_LOOKBACK, mSyncLookback);
+ values.put(AccountColumns.SYNC_INTERVAL, mSyncInterval);
+ values.put(AccountColumns.HOST_AUTH_KEY_RECV, mHostAuthKeyRecv);
+ values.put(AccountColumns.HOST_AUTH_KEY_SEND, mHostAuthKeySend);
+ values.put(AccountColumns.FLAGS, mFlags);
+ values.put(AccountColumns.IS_DEFAULT, mIsDefault);
+ values.put(AccountColumns.COMPATIBILITY_UUID, mCompatibilityUuid);
+ values.put(AccountColumns.SENDER_NAME, mSenderName);
+ values.put(AccountColumns.RINGTONE_URI, mRingtoneUri);
+ values.put(AccountColumns.PROTOCOL_VERSION, mProtocolVersion);
+ values.put(AccountColumns.NEW_MESSAGE_COUNT, mNewMessageCount);
+ values.put(AccountColumns.SECURITY_SYNC_KEY, mSecuritySyncKey);
+ values.put(AccountColumns.SIGNATURE, mSignature);
+ values.put(AccountColumns.POLICY_KEY, mPolicyKey);
+ return values;
+ }
+
+ /**
+ * Supports Parcelable
+ */
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ /**
+ * Supports Parcelable
+ */
+ public static final Parcelable.Creator<Account> CREATOR
+ = new Parcelable.Creator<Account>() {
+ @Override
+ public Account createFromParcel(Parcel in) {
+ return new Account(in);
+ }
+
+ @Override
+ public Account[] newArray(int size) {
+ return new Account[size];
+ }
+ };
+
+ /**
+ * Supports Parcelable
+ */
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ // mBaseUri is not parceled
+ dest.writeLong(mId);
+ dest.writeString(mDisplayName);
+ dest.writeString(mEmailAddress);
+ dest.writeString(mSyncKey);
+ dest.writeInt(mSyncLookback);
+ dest.writeInt(mSyncInterval);
+ dest.writeLong(mHostAuthKeyRecv);
+ dest.writeLong(mHostAuthKeySend);
+ dest.writeInt(mFlags);
+ dest.writeByte(mIsDefault ? (byte)1 : (byte)0);
+ dest.writeString(mCompatibilityUuid);
+ dest.writeString(mSenderName);
+ dest.writeString(mRingtoneUri);
+ dest.writeString(mProtocolVersion);
+ dest.writeInt(mNewMessageCount);
+ dest.writeString(mSecuritySyncKey);
+ dest.writeString(mSignature);
+ dest.writeLong(mPolicyKey);
+
+ if (mHostAuthRecv != null) {
+ dest.writeByte((byte)1);
+ mHostAuthRecv.writeToParcel(dest, flags);
+ } else {
+ dest.writeByte((byte)0);
+ }
+
+ if (mHostAuthSend != null) {
+ dest.writeByte((byte)1);
+ mHostAuthSend.writeToParcel(dest, flags);
+ } else {
+ dest.writeByte((byte)0);
+ }
+ }
+
+ /**
+ * Supports Parcelable
+ */
+ public Account(Parcel in) {
+ mBaseUri = Account.CONTENT_URI;
+ mId = in.readLong();
+ mDisplayName = in.readString();
+ mEmailAddress = in.readString();
+ mSyncKey = in.readString();
+ mSyncLookback = in.readInt();
+ mSyncInterval = in.readInt();
+ mHostAuthKeyRecv = in.readLong();
+ mHostAuthKeySend = in.readLong();
+ mFlags = in.readInt();
+ mIsDefault = in.readByte() == 1;
+ mCompatibilityUuid = in.readString();
+ mSenderName = in.readString();
+ mRingtoneUri = in.readString();
+ mProtocolVersion = in.readString();
+ mNewMessageCount = in.readInt();
+ mSecuritySyncKey = in.readString();
+ mSignature = in.readString();
+ mPolicyKey = in.readLong();
+
+ mHostAuthRecv = null;
+ if (in.readByte() == 1) {
+ mHostAuthRecv = new HostAuth(in);
+ }
+
+ mHostAuthSend = null;
+ if (in.readByte() == 1) {
+ mHostAuthSend = new HostAuth(in);
+ }
+ }
+
+ /**
+ * For debugger support only - DO NOT use for code.
+ */
+ @Override
+ public String toString() {
+ StringBuilder sb = new StringBuilder('[');
+ if (mHostAuthRecv != null && mHostAuthRecv.mProtocol != null) {
+ sb.append(mHostAuthRecv.mProtocol);
+ sb.append(':');
+ }
+ if (mDisplayName != null) sb.append(mDisplayName);
+ sb.append(':');
+ if (mEmailAddress != null) sb.append(mEmailAddress);
+ sb.append(':');
+ if (mSenderName != null) sb.append(mSenderName);
+ sb.append(']');
+ return sb.toString();
+ }
+}
\ No newline at end of file
diff --git a/email2/emailcommon/src/com/android/emailcommon/provider/EmailContent.java b/email2/emailcommon/src/com/android/emailcommon/provider/EmailContent.java
new file mode 100755
index 0000000..eb229be
--- /dev/null
+++ b/email2/emailcommon/src/com/android/emailcommon/provider/EmailContent.java
@@ -0,0 +1,1525 @@
+/*
+ * 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.emailcommon.provider;
+
+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.OperationApplicationException;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.Environment;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.os.RemoteException;
+
+import com.android.emailcommon.utility.TextUtilities;
+import com.android.emailcommon.utility.Utility;
+import com.android.mail.providers.UIProvider;
+import com.google.common.annotations.VisibleForTesting;
+
+import java.io.File;
+import java.util.ArrayList;
+
+
+/**
+ * EmailContent is the superclass of the various classes of content stored by EmailProvider.
+ *
+ * It is intended to include 1) column definitions for use with the Provider, and 2) convenience
+ * methods for saving and retrieving content from the Provider.
+ *
+ * This class will be used by 1) the Email process (which includes the application and
+ * EmaiLProvider) as well as 2) the Exchange process (which runs independently). It will
+ * necessarily be cloned for use in these two cases.
+ *
+ * Conventions used in naming columns:
+ * RECORD_ID is the primary key for all Email records
+ * The SyncColumns interface is used by all classes that are synced to the server directly
+ * (Mailbox and Email)
+ *
+ * <name>_KEY always refers to a foreign key
+ * <name>_ID always refers to a unique identifier (whether on client, server, etc.)
+ *
+ */
+public abstract class EmailContent {
+
+ public static final String AUTHORITY = "com.android.email.provider";
+ // The notifier authority is used to send notifications regarding changes to messages (insert,
+ // delete, or update) and is intended as an optimization for use by clients of message list
+ // cursors (initially, the email AppWidget).
+ public static final String NOTIFIER_AUTHORITY = "com.android.email.notifier";
+
+ public static final Uri CONTENT_URI = Uri.parse("content://" + AUTHORITY);
+ public static final String PARAMETER_LIMIT = "limit";
+
+ public static final Uri CONTENT_NOTIFIER_URI = Uri.parse("content://" + NOTIFIER_AUTHORITY);
+
+ public static final Uri MAILBOX_NOTIFICATION_URI =
+ Uri.parse("content://" + EmailContent.AUTHORITY + "/mailboxNotification");
+ public static final String[] NOTIFICATION_PROJECTION =
+ new String[] {MailboxColumns.ID, MailboxColumns.UNREAD_COUNT, MailboxColumns.MESSAGE_COUNT};
+ public static final int NOTIFICATION_MAILBOX_ID_COLUMN = 0;
+ public static final int NOTIFICATION_MAILBOX_UNREAD_COUNT_COLUMN = 1;
+ public static final int NOTIFICATION_MAILBOX_MESSAGE_COUNT_COLUMN = 2;
+
+ public static final Uri MAILBOX_MOST_RECENT_MESSAGE_URI =
+ Uri.parse("content://" + EmailContent.AUTHORITY + "/mailboxMostRecentMessage");
+
+ public static final String PROVIDER_PERMISSION = "com.android.email.permission.ACCESS_PROVIDER";
+
+ // All classes share this
+ public static final String RECORD_ID = "_id";
+
+ public static final String[] COUNT_COLUMNS = new String[]{"count(*)"};
+
+ /**
+ * This projection can be used with any of the EmailContent classes, when all you need
+ * is a list of id's. Use ID_PROJECTION_COLUMN to access the row data.
+ */
+ public static final String[] ID_PROJECTION = new String[] {
+ RECORD_ID
+ };
+ public static final int ID_PROJECTION_COLUMN = 0;
+
+ public static final String ID_SELECTION = RECORD_ID + " =?";
+
+ public static final String FIELD_COLUMN_NAME = "field";
+ public static final String ADD_COLUMN_NAME = "add";
+ public static final String SET_COLUMN_NAME = "set";
+
+ public static final int SYNC_STATUS_NONE = UIProvider.SyncStatus.NO_SYNC;
+ public static final int SYNC_STATUS_USER = UIProvider.SyncStatus.USER_REFRESH;
+ public static final int SYNC_STATUS_BACKGROUND = UIProvider.SyncStatus.BACKGROUND_SYNC;
+
+ public static final int LAST_SYNC_RESULT_SUCCESS = UIProvider.LastSyncResult.SUCCESS;
+ public static final int LAST_SYNC_RESULT_AUTH_ERROR = UIProvider.LastSyncResult.AUTH_ERROR;
+ public static final int LAST_SYNC_RESULT_SECURITY_ERROR =
+ UIProvider.LastSyncResult.SECURITY_ERROR;
+ public static final int LAST_SYNC_RESULT_CONNECTION_ERROR =
+ UIProvider.LastSyncResult.CONNECTION_ERROR;
+ public static final int LAST_SYNC_RESULT_INTERNAL_ERROR =
+ UIProvider.LastSyncResult.INTERNAL_ERROR;
+
+ // Newly created objects get this id
+ public static final int NOT_SAVED = -1;
+ // The base Uri that this piece of content came from
+ public Uri mBaseUri;
+ // Lazily initialized uri for this Content
+ private Uri mUri = null;
+ // The id of the Content
+ public long mId = NOT_SAVED;
+
+ // Write the Content into a ContentValues container
+ public abstract ContentValues toContentValues();
+ // Read the Content from a ContentCursor
+ public abstract void restore (Cursor cursor);
+
+ // The Uri is lazily initialized
+ public Uri getUri() {
+ if (mUri == null) {
+ mUri = ContentUris.withAppendedId(mBaseUri, mId);
+ }
+ return mUri;
+ }
+
+ public boolean isSaved() {
+ return mId != NOT_SAVED;
+ }
+
+
+ /**
+ * Restore a subclass of EmailContent from the database
+ * @param context the caller's context
+ * @param klass the class to restore
+ * @param contentUri the content uri of the EmailContent subclass
+ * @param contentProjection the content projection for the EmailContent subclass
+ * @param id the unique id of the object
+ * @return the instantiated object
+ */
+ public static <T extends EmailContent> T restoreContentWithId(Context context,
+ Class<T> klass, Uri contentUri, String[] contentProjection, long id) {
+ Uri u = ContentUris.withAppendedId(contentUri, id);
+ Cursor c = context.getContentResolver().query(u, contentProjection, null, null, null);
+ if (c == null) throw new ProviderUnavailableException();
+ try {
+ if (c.moveToFirst()) {
+ return getContent(c, klass);
+ } else {
+ return null;
+ }
+ } finally {
+ c.close();
+ }
+ }
+
+
+ // The Content sub class must have a no-arg constructor
+ static public <T extends EmailContent> T getContent(Cursor cursor, Class<T> klass) {
+ try {
+ T content = klass.newInstance();
+ content.mId = cursor.getLong(0);
+ content.restore(cursor);
+ return content;
+ } catch (IllegalAccessException e) {
+ e.printStackTrace();
+ } catch (InstantiationException e) {
+ e.printStackTrace();
+ }
+ return null;
+ }
+
+ public Uri save(Context context) {
+ if (isSaved()) {
+ throw new UnsupportedOperationException();
+ }
+ Uri res = context.getContentResolver().insert(mBaseUri, toContentValues());
+ mId = Long.parseLong(res.getPathSegments().get(1));
+ return res;
+ }
+
+ public int update(Context context, ContentValues contentValues) {
+ if (!isSaved()) {
+ throw new UnsupportedOperationException();
+ }
+ return context.getContentResolver().update(getUri(), contentValues, null, null);
+ }
+
+ static public int update(Context context, Uri baseUri, long id, ContentValues contentValues) {
+ return context.getContentResolver()
+ .update(ContentUris.withAppendedId(baseUri, id), contentValues, null, null);
+ }
+
+ static public int delete(Context context, Uri baseUri, long id) {
+ return context.getContentResolver()
+ .delete(ContentUris.withAppendedId(baseUri, id), null, null);
+ }
+
+ /**
+ * Generic count method that can be used for any ContentProvider
+ *
+ * @param context the calling Context
+ * @param uri the Uri for the provider query
+ * @param selection as with a query call
+ * @param selectionArgs as with a query call
+ * @return the number of items matching the query (or zero)
+ */
+ static public int count(Context context, Uri uri, String selection, String[] selectionArgs) {
+ return Utility.getFirstRowLong(context,
+ uri, COUNT_COLUMNS, selection, selectionArgs, null, 0, Long.valueOf(0)).intValue();
+ }
+
+ /**
+ * Same as {@link #count(Context, Uri, String, String[])} without selection.
+ */
+ static public int count(Context context, Uri uri) {
+ return count(context, uri, null, null);
+ }
+
+ static public Uri uriWithLimit(Uri uri, int limit) {
+ return uri.buildUpon().appendQueryParameter(EmailContent.PARAMETER_LIMIT,
+ Integer.toString(limit)).build();
+ }
+
+ /**
+ * no public constructor since this is a utility class
+ */
+ protected EmailContent() {
+ }
+
+ public interface SyncColumns {
+ public static final String ID = "_id";
+ // source id (string) : the source's name of this item
+ public static final String SERVER_ID = "syncServerId";
+ // source's timestamp (long) for this item
+ public static final String SERVER_TIMESTAMP = "syncServerTimeStamp";
+ }
+
+ public interface BodyColumns {
+ public static final String ID = "_id";
+ // Foreign key to the message corresponding to this body
+ public static final String MESSAGE_KEY = "messageKey";
+ // The html content itself
+ public static final String HTML_CONTENT = "htmlContent";
+ // The plain text content itself
+ public static final String TEXT_CONTENT = "textContent";
+ // Replied-to or forwarded body (in html form)
+ public static final String HTML_REPLY = "htmlReply";
+ // Replied-to or forwarded body (in text form)
+ public static final String TEXT_REPLY = "textReply";
+ // A reference to a message's unique id used in reply/forward.
+ // Protocol code can be expected to use this column in determining whether a message can be
+ // deleted safely (i.e. isn't referenced by other messages)
+ public static final String SOURCE_MESSAGE_KEY = "sourceMessageKey";
+ // The text to be placed between a reply/forward response and the original message
+ public static final String INTRO_TEXT = "introText";
+ }
+
+ public static final class Body extends EmailContent implements BodyColumns {
+ public static final String TABLE_NAME = "Body";
+
+ @SuppressWarnings("hiding")
+ public static final Uri CONTENT_URI = Uri.parse(EmailContent.CONTENT_URI + "/body");
+
+ public static final int CONTENT_ID_COLUMN = 0;
+ public static final int CONTENT_MESSAGE_KEY_COLUMN = 1;
+ public static final int CONTENT_HTML_CONTENT_COLUMN = 2;
+ public static final int CONTENT_TEXT_CONTENT_COLUMN = 3;
+ public static final int CONTENT_HTML_REPLY_COLUMN = 4;
+ public static final int CONTENT_TEXT_REPLY_COLUMN = 5;
+ public static final int CONTENT_SOURCE_KEY_COLUMN = 6;
+ public static final int CONTENT_INTRO_TEXT_COLUMN = 7;
+ public static final String[] CONTENT_PROJECTION = new String[] {
+ RECORD_ID, BodyColumns.MESSAGE_KEY, BodyColumns.HTML_CONTENT, BodyColumns.TEXT_CONTENT,
+ BodyColumns.HTML_REPLY, BodyColumns.TEXT_REPLY, BodyColumns.SOURCE_MESSAGE_KEY,
+ BodyColumns.INTRO_TEXT
+ };
+
+ public static final String[] COMMON_PROJECTION_TEXT = new String[] {
+ RECORD_ID, BodyColumns.TEXT_CONTENT
+ };
+ public static final String[] COMMON_PROJECTION_HTML = new String[] {
+ RECORD_ID, BodyColumns.HTML_CONTENT
+ };
+ public static final String[] COMMON_PROJECTION_REPLY_TEXT = new String[] {
+ RECORD_ID, BodyColumns.TEXT_REPLY
+ };
+ public static final String[] COMMON_PROJECTION_REPLY_HTML = new String[] {
+ RECORD_ID, BodyColumns.HTML_REPLY
+ };
+ public static final String[] COMMON_PROJECTION_INTRO = new String[] {
+ RECORD_ID, BodyColumns.INTRO_TEXT
+ };
+ public static final String[] COMMON_PROJECTION_SOURCE = new String[] {
+ RECORD_ID, BodyColumns.SOURCE_MESSAGE_KEY
+ };
+ public static final int COMMON_PROJECTION_COLUMN_TEXT = 1;
+
+ private static final String[] PROJECTION_SOURCE_KEY =
+ new String[] { BodyColumns.SOURCE_MESSAGE_KEY };
+
+ public long mMessageKey;
+ public String mHtmlContent;
+ public String mTextContent;
+ public String mHtmlReply;
+ public String mTextReply;
+
+ /**
+ * Points to the ID of the message being replied to or forwarded. Will always be set,
+ * even if {@link #mHtmlReply} and {@link #mTextReply} are null (indicating the user doesn't
+ * want to include quoted text.
+ */
+ public long mSourceKey;
+ public String mIntroText;
+
+ public Body() {
+ mBaseUri = CONTENT_URI;
+ }
+
+ @Override
+ public ContentValues toContentValues() {
+ ContentValues values = new ContentValues();
+
+ // Assign values for each row.
+ values.put(BodyColumns.MESSAGE_KEY, mMessageKey);
+ values.put(BodyColumns.HTML_CONTENT, mHtmlContent);
+ values.put(BodyColumns.TEXT_CONTENT, mTextContent);
+ values.put(BodyColumns.HTML_REPLY, mHtmlReply);
+ values.put(BodyColumns.TEXT_REPLY, mTextReply);
+ values.put(BodyColumns.SOURCE_MESSAGE_KEY, mSourceKey);
+ values.put(BodyColumns.INTRO_TEXT, mIntroText);
+ return values;
+ }
+
+ /**
+ * Given a cursor, restore a Body from it
+ * @param cursor a cursor which must NOT be null
+ * @return the Body as restored from the cursor
+ */
+ private static Body restoreBodyWithCursor(Cursor cursor) {
+ try {
+ if (cursor.moveToFirst()) {
+ return getContent(cursor, Body.class);
+ } else {
+ return null;
+ }
+ } finally {
+ cursor.close();
+ }
+ }
+
+ public static Body restoreBodyWithId(Context context, long id) {
+ Uri u = ContentUris.withAppendedId(Body.CONTENT_URI, id);
+ Cursor c = context.getContentResolver().query(u, Body.CONTENT_PROJECTION,
+ null, null, null);
+ if (c == null) throw new ProviderUnavailableException();
+ return restoreBodyWithCursor(c);
+ }
+
+ public static Body restoreBodyWithMessageId(Context context, long messageId) {
+ Cursor c = context.getContentResolver().query(Body.CONTENT_URI,
+ Body.CONTENT_PROJECTION, Body.MESSAGE_KEY + "=?",
+ new String[] {Long.toString(messageId)}, null);
+ if (c == null) throw new ProviderUnavailableException();
+ return restoreBodyWithCursor(c);
+ }
+
+ /**
+ * Returns the bodyId for the given messageId, or -1 if no body is found.
+ */
+ public static long lookupBodyIdWithMessageId(Context context, long messageId) {
+ return Utility.getFirstRowLong(context, Body.CONTENT_URI,
+ ID_PROJECTION, Body.MESSAGE_KEY + "=?",
+ new String[] {Long.toString(messageId)}, null, ID_PROJECTION_COLUMN,
+ Long.valueOf(-1));
+ }
+
+ /**
+ * Updates the Body for a messageId with the given ContentValues.
+ * If the message has no body, a new body is inserted for the message.
+ * Warning: the argument "values" is modified by this method, setting MESSAGE_KEY.
+ */
+ public static void updateBodyWithMessageId(Context context, long messageId,
+ ContentValues values) {
+ ContentResolver resolver = context.getContentResolver();
+ long bodyId = lookupBodyIdWithMessageId(context, messageId);
+ values.put(BodyColumns.MESSAGE_KEY, messageId);
+ if (bodyId == -1) {
+ resolver.insert(CONTENT_URI, values);
+ } else {
+ final Uri uri = ContentUris.withAppendedId(CONTENT_URI, bodyId);
+ resolver.update(uri, values, null, null);
+ }
+ }
+
+ @VisibleForTesting
+ public static long restoreBodySourceKey(Context context, long messageId) {
+ return Utility.getFirstRowLong(context, Body.CONTENT_URI,
+ Body.PROJECTION_SOURCE_KEY,
+ Body.MESSAGE_KEY + "=?", new String[] {Long.toString(messageId)}, null, 0,
+ Long.valueOf(0));
+ }
+
+ private static String restoreTextWithMessageId(Context context, long messageId,
+ String[] projection) {
+ Cursor c = context.getContentResolver().query(Body.CONTENT_URI, projection,
+ Body.MESSAGE_KEY + "=?", new String[] {Long.toString(messageId)}, null);
+ if (c == null) throw new ProviderUnavailableException();
+ try {
+ if (c.moveToFirst()) {
+ return c.getString(COMMON_PROJECTION_COLUMN_TEXT);
+ } else {
+ return null;
+ }
+ } finally {
+ c.close();
+ }
+ }
+
+ public static String restoreBodyTextWithMessageId(Context context, long messageId) {
+ return restoreTextWithMessageId(context, messageId, Body.COMMON_PROJECTION_TEXT);
+ }
+
+ public static String restoreBodyHtmlWithMessageId(Context context, long messageId) {
+ return restoreTextWithMessageId(context, messageId, Body.COMMON_PROJECTION_HTML);
+ }
+
+ public static String restoreReplyTextWithMessageId(Context context, long messageId) {
+ return restoreTextWithMessageId(context, messageId, Body.COMMON_PROJECTION_REPLY_TEXT);
+ }
+
+ public static String restoreReplyHtmlWithMessageId(Context context, long messageId) {
+ return restoreTextWithMessageId(context, messageId, Body.COMMON_PROJECTION_REPLY_HTML);
+ }
+
+ public static String restoreIntroTextWithMessageId(Context context, long messageId) {
+ return restoreTextWithMessageId(context, messageId, Body.COMMON_PROJECTION_INTRO);
+ }
+
+ @Override
+ public void restore(Cursor cursor) {
+ mBaseUri = EmailContent.Body.CONTENT_URI;
+ mMessageKey = cursor.getLong(CONTENT_MESSAGE_KEY_COLUMN);
+ mHtmlContent = cursor.getString(CONTENT_HTML_CONTENT_COLUMN);
+ mTextContent = cursor.getString(CONTENT_TEXT_CONTENT_COLUMN);
+ mHtmlReply = cursor.getString(CONTENT_HTML_REPLY_COLUMN);
+ mTextReply = cursor.getString(CONTENT_TEXT_REPLY_COLUMN);
+ mSourceKey = cursor.getLong(CONTENT_SOURCE_KEY_COLUMN);
+ mIntroText = cursor.getString(CONTENT_INTRO_TEXT_COLUMN);
+ }
+
+ public boolean update() {
+ // TODO Auto-generated method stub
+ return false;
+ }
+ }
+
+ public interface MessageColumns {
+ public static final String ID = "_id";
+ // Basic columns used in message list presentation
+ // The name as shown to the user in a message list
+ public static final String DISPLAY_NAME = "displayName";
+ // The time (millis) as shown to the user in a message list [INDEX]
+ public static final String TIMESTAMP = "timeStamp";
+ // Message subject
+ public static final String SUBJECT = "subject";
+ // Boolean, unread = 0, read = 1 [INDEX]
+ public static final String FLAG_READ = "flagRead";
+ // Load state, see constants below (unloaded, partial, complete, deleted)
+ public static final String FLAG_LOADED = "flagLoaded";
+ // Boolean, unflagged = 0, flagged (favorite) = 1
+ public static final String FLAG_FAVORITE = "flagFavorite";
+ // Boolean, no attachment = 0, attachment = 1
+ public static final String FLAG_ATTACHMENT = "flagAttachment";
+ // Bit field for flags which we'll not be selecting on
+ public static final String FLAGS = "flags";
+
+ // Sync related identifiers
+ // Any client-required identifier
+ public static final String CLIENT_ID = "clientId";
+ // The message-id in the message's header
+ public static final String MESSAGE_ID = "messageId";
+
+ // References to other Email objects in the database
+ // Foreign key to the Mailbox holding this message [INDEX]
+ public static final String MAILBOX_KEY = "mailboxKey";
+ // Foreign key to the Account holding this message
+ public static final String ACCOUNT_KEY = "accountKey";
+
+ // Address lists, packed with Address.pack()
+ public static final String FROM_LIST = "fromList";
+ public static final String TO_LIST = "toList";
+ public static final String CC_LIST = "ccList";
+ public static final String BCC_LIST = "bccList";
+ public static final String REPLY_TO_LIST = "replyToList";
+ // Meeting invitation related information (for now, start time in ms)
+ public static final String MEETING_INFO = "meetingInfo";
+ // A text "snippet" derived from the body of the message
+ public static final String SNIPPET = "snippet";
+ // A column that can be used by sync adapters to store search-related information about
+ // a retrieved message (the messageKey for search results will be a TYPE_SEARCH mailbox
+ // and the sync adapter might, for example, need more information about the original source
+ // of the message)
+ public static final String PROTOCOL_SEARCH_INFO = "protocolSearchInfo";
+ }
+
+ public static final class Message extends EmailContent implements SyncColumns, MessageColumns {
+ public static final String TABLE_NAME = "Message";
+ public static final String UPDATED_TABLE_NAME = "Message_Updates";
+ public static final String DELETED_TABLE_NAME = "Message_Deletes";
+
+ // To refer to a specific message, use ContentUris.withAppendedId(CONTENT_URI, id)
+ @SuppressWarnings("hiding")
+ public static final Uri CONTENT_URI = Uri.parse(EmailContent.CONTENT_URI + "/message");
+ public static final Uri CONTENT_URI_LIMIT_1 = uriWithLimit(CONTENT_URI, 1);
+ public static final Uri SYNCED_CONTENT_URI =
+ Uri.parse(EmailContent.CONTENT_URI + "/syncedMessage");
+ public static final Uri DELETED_CONTENT_URI =
+ Uri.parse(EmailContent.CONTENT_URI + "/deletedMessage");
+ public static final Uri UPDATED_CONTENT_URI =
+ Uri.parse(EmailContent.CONTENT_URI + "/updatedMessage");
+ public static final Uri NOTIFIER_URI =
+ Uri.parse(EmailContent.CONTENT_NOTIFIER_URI + "/message");
+
+ public static final String KEY_TIMESTAMP_DESC = MessageColumns.TIMESTAMP + " desc";
+
+ public static final int CONTENT_ID_COLUMN = 0;
+ public static final int CONTENT_DISPLAY_NAME_COLUMN = 1;
+ public static final int CONTENT_TIMESTAMP_COLUMN = 2;
+ public static final int CONTENT_SUBJECT_COLUMN = 3;
+ public static final int CONTENT_FLAG_READ_COLUMN = 4;
+ public static final int CONTENT_FLAG_LOADED_COLUMN = 5;
+ public static final int CONTENT_FLAG_FAVORITE_COLUMN = 6;
+ public static final int CONTENT_FLAG_ATTACHMENT_COLUMN = 7;
+ public static final int CONTENT_FLAGS_COLUMN = 8;
+ public static final int CONTENT_SERVER_ID_COLUMN = 9;
+ public static final int CONTENT_CLIENT_ID_COLUMN = 10;
+ public static final int CONTENT_MESSAGE_ID_COLUMN = 11;
+ public static final int CONTENT_MAILBOX_KEY_COLUMN = 12;
+ public static final int CONTENT_ACCOUNT_KEY_COLUMN = 13;
+ public static final int CONTENT_FROM_LIST_COLUMN = 14;
+ public static final int CONTENT_TO_LIST_COLUMN = 15;
+ public static final int CONTENT_CC_LIST_COLUMN = 16;
+ public static final int CONTENT_BCC_LIST_COLUMN = 17;
+ public static final int CONTENT_REPLY_TO_COLUMN = 18;
+ public static final int CONTENT_SERVER_TIMESTAMP_COLUMN = 19;
+ public static final int CONTENT_MEETING_INFO_COLUMN = 20;
+ public static final int CONTENT_SNIPPET_COLUMN = 21;
+ public static final int CONTENT_PROTOCOL_SEARCH_INFO_COLUMN = 22;
+
+ public static final String[] CONTENT_PROJECTION = new String[] {
+ RECORD_ID,
+ MessageColumns.DISPLAY_NAME, MessageColumns.TIMESTAMP,
+ MessageColumns.SUBJECT, MessageColumns.FLAG_READ,
+ MessageColumns.FLAG_LOADED, MessageColumns.FLAG_FAVORITE,
+ MessageColumns.FLAG_ATTACHMENT, MessageColumns.FLAGS,
+ SyncColumns.SERVER_ID, MessageColumns.CLIENT_ID,
+ MessageColumns.MESSAGE_ID, MessageColumns.MAILBOX_KEY,
+ MessageColumns.ACCOUNT_KEY, MessageColumns.FROM_LIST,
+ MessageColumns.TO_LIST, MessageColumns.CC_LIST,
+ MessageColumns.BCC_LIST, MessageColumns.REPLY_TO_LIST,
+ SyncColumns.SERVER_TIMESTAMP, MessageColumns.MEETING_INFO,
+ MessageColumns.SNIPPET, MessageColumns.PROTOCOL_SEARCH_INFO
+ };
+
+ public static final int LIST_ID_COLUMN = 0;
+ public static final int LIST_DISPLAY_NAME_COLUMN = 1;
+ public static final int LIST_TIMESTAMP_COLUMN = 2;
+ public static final int LIST_SUBJECT_COLUMN = 3;
+ public static final int LIST_READ_COLUMN = 4;
+ public static final int LIST_LOADED_COLUMN = 5;
+ public static final int LIST_FAVORITE_COLUMN = 6;
+ public static final int LIST_ATTACHMENT_COLUMN = 7;
+ public static final int LIST_FLAGS_COLUMN = 8;
+ public static final int LIST_MAILBOX_KEY_COLUMN = 9;
+ public static final int LIST_ACCOUNT_KEY_COLUMN = 10;
+ public static final int LIST_SERVER_ID_COLUMN = 11;
+ public static final int LIST_SNIPPET_COLUMN = 12;
+
+ // Public projection for common list columns
+ public static final String[] LIST_PROJECTION = new String[] {
+ RECORD_ID,
+ MessageColumns.DISPLAY_NAME, MessageColumns.TIMESTAMP,
+ MessageColumns.SUBJECT, MessageColumns.FLAG_READ,
+ MessageColumns.FLAG_LOADED, MessageColumns.FLAG_FAVORITE,
+ MessageColumns.FLAG_ATTACHMENT, MessageColumns.FLAGS,
+ MessageColumns.MAILBOX_KEY, MessageColumns.ACCOUNT_KEY,
+ SyncColumns.SERVER_ID, MessageColumns.SNIPPET
+ };
+
+ public static final int ID_COLUMNS_ID_COLUMN = 0;
+ public static final int ID_COLUMNS_SYNC_SERVER_ID = 1;
+ public static final String[] ID_COLUMNS_PROJECTION = new String[] {
+ RECORD_ID, SyncColumns.SERVER_ID
+ };
+
+ public static final int ID_MAILBOX_COLUMN_ID = 0;
+ public static final int ID_MAILBOX_COLUMN_MAILBOX_KEY = 1;
+ public static final String[] ID_MAILBOX_PROJECTION = new String[] {
+ RECORD_ID, MessageColumns.MAILBOX_KEY
+ };
+
+ public static final String[] ID_COLUMN_PROJECTION = new String[] { RECORD_ID };
+
+ private static final String ACCOUNT_KEY_SELECTION =
+ MessageColumns.ACCOUNT_KEY + "=?";
+
+ /**
+ * Selection for messages that are loaded
+ *
+ * POP messages at the initial stage have very little information. (Server UID only)
+ * Use this to make sure they're not visible on any UI.
+ * This means unread counts on the mailbox list can be different from the
+ * number of messages in the message list, but it should be transient...
+ */
+ public static final String FLAG_LOADED_SELECTION =
+ MessageColumns.FLAG_LOADED + " IN ("
+ + Message.FLAG_LOADED_PARTIAL + "," + Message.FLAG_LOADED_COMPLETE
+ + ")";
+
+ public static final String ALL_FAVORITE_SELECTION =
+ MessageColumns.FLAG_FAVORITE + "=1 AND "
+ + MessageColumns.MAILBOX_KEY + " NOT IN ("
+ + "SELECT " + MailboxColumns.ID + " FROM " + Mailbox.TABLE_NAME + ""
+ + " WHERE " + MailboxColumns.TYPE + " = " + Mailbox.TYPE_TRASH
+ + ")"
+ + " AND " + FLAG_LOADED_SELECTION;
+
+ /** Selection to retrieve all messages in "inbox" for any account */
+ public static final String ALL_INBOX_SELECTION =
+ MessageColumns.MAILBOX_KEY + " IN ("
+ + "SELECT " + MailboxColumns.ID + " FROM " + Mailbox.TABLE_NAME
+ + " WHERE " + MailboxColumns.TYPE + " = " + Mailbox.TYPE_INBOX
+ + ")"
+ + " AND " + FLAG_LOADED_SELECTION;
+
+ /** Selection to retrieve all messages in "drafts" for any account */
+ public static final String ALL_DRAFT_SELECTION =
+ MessageColumns.MAILBOX_KEY + " IN ("
+ + "SELECT " + MailboxColumns.ID + " FROM " + Mailbox.TABLE_NAME
+ + " WHERE " + MailboxColumns.TYPE + " = " + Mailbox.TYPE_DRAFTS
+ + ")"
+ + " AND " + FLAG_LOADED_SELECTION;
+
+ /** Selection to retrieve all messages in "outbox" for any account */
+ public static final String ALL_OUTBOX_SELECTION =
+ MessageColumns.MAILBOX_KEY + " IN ("
+ + "SELECT " + MailboxColumns.ID + " FROM " + Mailbox.TABLE_NAME
+ + " WHERE " + MailboxColumns.TYPE + " = " + Mailbox.TYPE_OUTBOX
+ + ")"; // NOTE No flag_loaded test for outboxes.
+
+ /** Selection to retrieve unread messages in "inbox" for any account */
+ public static final String ALL_UNREAD_SELECTION =
+ MessageColumns.FLAG_READ + "=0 AND " + ALL_INBOX_SELECTION;
+
+ /** Selection to retrieve unread messages in "inbox" for one account */
+ public static final String PER_ACCOUNT_UNREAD_SELECTION =
+ ACCOUNT_KEY_SELECTION + " AND " + ALL_UNREAD_SELECTION;
+
+ /** Selection to retrieve all messages in "inbox" for one account */
+ public static final String PER_ACCOUNT_INBOX_SELECTION =
+ ACCOUNT_KEY_SELECTION + " AND " + ALL_INBOX_SELECTION;
+
+ public static final String PER_ACCOUNT_FAVORITE_SELECTION =
+ ACCOUNT_KEY_SELECTION + " AND " + ALL_FAVORITE_SELECTION;
+
+ // _id field is in AbstractContent
+ public String mDisplayName;
+ public long mTimeStamp;
+ public String mSubject;
+ public boolean mFlagRead = false;
+ public int mFlagLoaded = FLAG_LOADED_UNLOADED;
+ public boolean mFlagFavorite = false;
+ public boolean mFlagAttachment = false;
+ public int mFlags = 0;
+
+ public String mServerId;
+ public long mServerTimeStamp;
+ public String mClientId;
+ public String mMessageId;
+
+ public long mMailboxKey;
+ public long mAccountKey;
+
+ public String mFrom;
+ public String mTo;
+ public String mCc;
+ public String mBcc;
+ public String mReplyTo;
+
+ // For now, just the start time of a meeting invite, in ms
+ public String mMeetingInfo;
+
+ public String mSnippet;
+
+ public String mProtocolSearchInfo;
+
+ /**
+ * Base64-encoded representation of the byte array provided by servers for identifying
+ * messages belonging to the same conversation thread. Currently unsupported and not
+ * persisted in the database.
+ */
+ public String mServerConversationId;
+
+ // The following transient members may be used while building and manipulating messages,
+ // but they are NOT persisted directly by EmailProvider. See Body for related fields.
+ transient public String mText;
+ transient public String mHtml;
+ transient public String mTextReply;
+ transient public String mHtmlReply;
+ transient public long mSourceKey;
+ transient public ArrayList<Attachment> mAttachments = null;
+ transient public String mIntroText;
+
+
+ // Values used in mFlagRead
+ public static final int UNREAD = 0;
+ public static final int READ = 1;
+
+ // Values used in mFlagLoaded
+ public static final int FLAG_LOADED_UNLOADED = 0;
+ public static final int FLAG_LOADED_COMPLETE = 1;
+ public static final int FLAG_LOADED_PARTIAL = 2;
+ public static final int FLAG_LOADED_DELETED = 3;
+
+ // Bits used in mFlags
+ // The following three states are mutually exclusive, and indicate whether the message is an
+ // original, a reply, or a forward
+ public static final int FLAG_TYPE_ORIGINAL = 0;
+ public static final int FLAG_TYPE_REPLY = 1<<0;
+ public static final int FLAG_TYPE_FORWARD = 1<<1;
+ public static final int FLAG_TYPE_MASK = FLAG_TYPE_REPLY | FLAG_TYPE_FORWARD;
+ // The following flags indicate messages that are determined to be incoming meeting related
+ // (e.g. invites from others)
+ public static final int FLAG_INCOMING_MEETING_INVITE = 1<<2;
+ public static final int FLAG_INCOMING_MEETING_CANCEL = 1<<3;
+ public static final int FLAG_INCOMING_MEETING_MASK =
+ FLAG_INCOMING_MEETING_INVITE | FLAG_INCOMING_MEETING_CANCEL;
+ // The following flags indicate messages that are outgoing and meeting related
+ // (e.g. invites TO others)
+ public static final int FLAG_OUTGOING_MEETING_INVITE = 1<<4;
+ public static final int FLAG_OUTGOING_MEETING_CANCEL = 1<<5;
+ public static final int FLAG_OUTGOING_MEETING_ACCEPT = 1<<6;
+ public static final int FLAG_OUTGOING_MEETING_DECLINE = 1<<7;
+ public static final int FLAG_OUTGOING_MEETING_TENTATIVE = 1<<8;
+ public static final int FLAG_OUTGOING_MEETING_MASK =
+ FLAG_OUTGOING_MEETING_INVITE | FLAG_OUTGOING_MEETING_CANCEL |
+ FLAG_OUTGOING_MEETING_ACCEPT | FLAG_OUTGOING_MEETING_DECLINE |
+ FLAG_OUTGOING_MEETING_TENTATIVE;
+ public static final int FLAG_OUTGOING_MEETING_REQUEST_MASK =
+ FLAG_OUTGOING_MEETING_INVITE | FLAG_OUTGOING_MEETING_CANCEL;
+ // 8 general purpose flags (bits) that may be used at the discretion of the sync adapter
+ public static final int FLAG_SYNC_ADAPTER_SHIFT = 9;
+ public static final int FLAG_SYNC_ADAPTER_MASK = 255 << FLAG_SYNC_ADAPTER_SHIFT;
+ /** If set, the outgoing message should *not* include the quoted original message. */
+ public static final int FLAG_NOT_INCLUDE_QUOTED_TEXT = 1 << 17;
+ public static final int FLAG_REPLIED_TO = 1 << 18;
+ public static final int FLAG_FORWARDED = 1 << 19;
+
+ /** a pseudo ID for "no message". */
+ public static final long NO_MESSAGE = -1L;
+
+ public Message() {
+ mBaseUri = CONTENT_URI;
+ }
+
+ @Override
+ public ContentValues toContentValues() {
+ ContentValues values = new ContentValues();
+
+ // Assign values for each row.
+ values.put(MessageColumns.DISPLAY_NAME, mDisplayName);
+ values.put(MessageColumns.TIMESTAMP, mTimeStamp);
+ values.put(MessageColumns.SUBJECT, mSubject);
+ values.put(MessageColumns.FLAG_READ, mFlagRead);
+ values.put(MessageColumns.FLAG_LOADED, mFlagLoaded);
+ values.put(MessageColumns.FLAG_FAVORITE, mFlagFavorite);
+ values.put(MessageColumns.FLAG_ATTACHMENT, mFlagAttachment);
+ values.put(MessageColumns.FLAGS, mFlags);
+
+ values.put(SyncColumns.SERVER_ID, mServerId);
+ values.put(SyncColumns.SERVER_TIMESTAMP, mServerTimeStamp);
+ values.put(MessageColumns.CLIENT_ID, mClientId);
+ values.put(MessageColumns.MESSAGE_ID, mMessageId);
+
+ values.put(MessageColumns.MAILBOX_KEY, mMailboxKey);
+ values.put(MessageColumns.ACCOUNT_KEY, mAccountKey);
+
+ values.put(MessageColumns.FROM_LIST, mFrom);
+ values.put(MessageColumns.TO_LIST, mTo);
+ values.put(MessageColumns.CC_LIST, mCc);
+ values.put(MessageColumns.BCC_LIST, mBcc);
+ values.put(MessageColumns.REPLY_TO_LIST, mReplyTo);
+
+ values.put(MessageColumns.MEETING_INFO, mMeetingInfo);
+
+ values.put(MessageColumns.SNIPPET, mSnippet);
+
+ values.put(MessageColumns.PROTOCOL_SEARCH_INFO, mProtocolSearchInfo);
+ return values;
+ }
+
+ public static Message restoreMessageWithId(Context context, long id) {
+ return EmailContent.restoreContentWithId(context, Message.class,
+ Message.CONTENT_URI, Message.CONTENT_PROJECTION, id);
+ }
+
+ @Override
+ public void restore(Cursor cursor) {
+ mBaseUri = CONTENT_URI;
+ mId = cursor.getLong(CONTENT_ID_COLUMN);
+ mDisplayName = cursor.getString(CONTENT_DISPLAY_NAME_COLUMN);
+ mTimeStamp = cursor.getLong(CONTENT_TIMESTAMP_COLUMN);
+ mSubject = cursor.getString(CONTENT_SUBJECT_COLUMN);
+ mFlagRead = cursor.getInt(CONTENT_FLAG_READ_COLUMN) == 1;
+ mFlagLoaded = cursor.getInt(CONTENT_FLAG_LOADED_COLUMN);
+ mFlagFavorite = cursor.getInt(CONTENT_FLAG_FAVORITE_COLUMN) == 1;
+ mFlagAttachment = cursor.getInt(CONTENT_FLAG_ATTACHMENT_COLUMN) == 1;
+ mFlags = cursor.getInt(CONTENT_FLAGS_COLUMN);
+ mServerId = cursor.getString(CONTENT_SERVER_ID_COLUMN);
+ mServerTimeStamp = cursor.getLong(CONTENT_SERVER_TIMESTAMP_COLUMN);
+ mClientId = cursor.getString(CONTENT_CLIENT_ID_COLUMN);
+ mMessageId = cursor.getString(CONTENT_MESSAGE_ID_COLUMN);
+ mMailboxKey = cursor.getLong(CONTENT_MAILBOX_KEY_COLUMN);
+ mAccountKey = cursor.getLong(CONTENT_ACCOUNT_KEY_COLUMN);
+ mFrom = cursor.getString(CONTENT_FROM_LIST_COLUMN);
+ mTo = cursor.getString(CONTENT_TO_LIST_COLUMN);
+ mCc = cursor.getString(CONTENT_CC_LIST_COLUMN);
+ mBcc = cursor.getString(CONTENT_BCC_LIST_COLUMN);
+ mReplyTo = cursor.getString(CONTENT_REPLY_TO_COLUMN);
+ mMeetingInfo = cursor.getString(CONTENT_MEETING_INFO_COLUMN);
+ mSnippet = cursor.getString(CONTENT_SNIPPET_COLUMN);
+ mProtocolSearchInfo = cursor.getString(CONTENT_PROTOCOL_SEARCH_INFO_COLUMN);
+ }
+
+ public boolean update() {
+ // TODO Auto-generated method stub
+ return false;
+ }
+
+ /*
+ * Override this so that we can store the Body first and link it to the Message
+ * Also, attachments when we get there...
+ * (non-Javadoc)
+ * @see com.android.email.provider.EmailContent#save(android.content.Context)
+ */
+ @Override
+ public Uri save(Context context) {
+
+ boolean doSave = !isSaved();
+
+ // This logic is in place so I can (a) short circuit the expensive stuff when
+ // possible, and (b) override (and throw) if anyone tries to call save() or update()
+ // directly for Message, which are unsupported.
+ if (mText == null && mHtml == null && mTextReply == null && mHtmlReply == null &&
+ (mAttachments == null || mAttachments.isEmpty())) {
+ if (doSave) {
+ return super.save(context);
+ } else {
+ // Call update, rather than super.update in case we ever override it
+ if (update(context, toContentValues()) == 1) {
+ return getUri();
+ }
+ return null;
+ }
+ }
+
+ ArrayList<ContentProviderOperation> ops = new ArrayList<ContentProviderOperation>();
+ addSaveOps(ops);
+ try {
+ ContentProviderResult[] results =
+ context.getContentResolver().applyBatch(AUTHORITY, ops);
+ // If saving, set the mId's of the various saved objects
+ if (doSave) {
+ Uri u = results[0].uri;
+ mId = Long.parseLong(u.getPathSegments().get(1));
+ if (mAttachments != null) {
+ int resultOffset = 2;
+ for (Attachment a : mAttachments) {
+ // Save the id of the attachment record
+ u = results[resultOffset++].uri;
+ if (u != null) {
+ a.mId = Long.parseLong(u.getPathSegments().get(1));
+ }
+ a.mMessageKey = mId;
+ }
+ }
+ return u;
+ } else {
+ return null;
+ }
+ } catch (RemoteException e) {
+ // There is nothing to be done here; fail by returning null
+ } catch (OperationApplicationException e) {
+ // There is nothing to be done here; fail by returning null
+ }
+ return null;
+ }
+
+ /**
+ * Save or update a message
+ * @param ops an array of CPOs that we'll add to
+ */
+ public void addSaveOps(ArrayList<ContentProviderOperation> ops) {
+ boolean isNew = !isSaved();
+ ContentProviderOperation.Builder b;
+ // First, save/update the message
+ if (isNew) {
+ b = ContentProviderOperation.newInsert(mBaseUri);
+ } else {
+ b = ContentProviderOperation.newUpdate(mBaseUri)
+ .withSelection(Message.RECORD_ID + "=?", new String[] {Long.toString(mId)});
+ }
+ // Generate the snippet here, before we create the CPO for Message
+ if (mText != null) {
+ mSnippet = TextUtilities.makeSnippetFromPlainText(mText);
+ } else if (mHtml != null) {
+ mSnippet = TextUtilities.makeSnippetFromHtmlText(mHtml);
+ }
+ ops.add(b.withValues(toContentValues()).build());
+
+ // Create and save the body
+ ContentValues cv = new ContentValues();
+ if (mText != null) {
+ cv.put(Body.TEXT_CONTENT, mText);
+ }
+ if (mHtml != null) {
+ cv.put(Body.HTML_CONTENT, mHtml);
+ }
+ if (mTextReply != null) {
+ cv.put(Body.TEXT_REPLY, mTextReply);
+ }
+ if (mHtmlReply != null) {
+ cv.put(Body.HTML_REPLY, mHtmlReply);
+ }
+ if (mSourceKey != 0) {
+ cv.put(Body.SOURCE_MESSAGE_KEY, mSourceKey);
+ }
+ if (mIntroText != null) {
+ cv.put(Body.INTRO_TEXT, mIntroText);
+ }
+ b = ContentProviderOperation.newInsert(Body.CONTENT_URI);
+ // Put our message id in the Body
+ if (!isNew) {
+ cv.put(Body.MESSAGE_KEY, mId);
+ }
+ b.withValues(cv);
+ // We'll need this if we're new
+ int messageBackValue = ops.size() - 1;
+ // If we're new, create a back value entry
+ if (isNew) {
+ ContentValues backValues = new ContentValues();
+ backValues.put(Body.MESSAGE_KEY, messageBackValue);
+ b.withValueBackReferences(backValues);
+ }
+ // And add the Body operation
+ ops.add(b.build());
+
+ // Create the attaachments, if any
+ if (mAttachments != null) {
+ for (Attachment att: mAttachments) {
+ if (!isNew) {
+ att.mMessageKey = mId;
+ }
+ b = ContentProviderOperation.newInsert(Attachment.CONTENT_URI)
+ .withValues(att.toContentValues());
+ if (isNew) {
+ b.withValueBackReference(Attachment.MESSAGE_KEY, messageBackValue);
+ }
+ ops.add(b.build());
+ }
+ }
+ }
+
+ /**
+ * @return number of favorite (starred) messages throughout all accounts.
+ */
+ public static int getFavoriteMessageCount(Context context) {
+ return count(context, Message.CONTENT_URI, ALL_FAVORITE_SELECTION, null);
+ }
+
+ /**
+ * @return number of favorite (starred) messages for an account
+ */
+ public static int getFavoriteMessageCount(Context context, long accountId) {
+ return count(context, Message.CONTENT_URI, PER_ACCOUNT_FAVORITE_SELECTION,
+ new String[]{Long.toString(accountId)});
+ }
+
+ public static long getKeyColumnLong(Context context, long messageId, String column) {
+ String[] columns =
+ Utility.getRowColumns(context, Message.CONTENT_URI, messageId, column);
+ if (columns != null && columns[0] != null) {
+ return Long.parseLong(columns[0]);
+ }
+ return -1;
+ }
+
+ /**
+ * Returns the where clause for a message list selection.
+ *
+ * Accesses the detabase to determine the mailbox type. DO NOT CALL FROM UI THREAD.
+ */
+ public static String buildMessageListSelection(
+ Context context, long accountId, long mailboxId) {
+
+ if (mailboxId == Mailbox.QUERY_ALL_INBOXES) {
+ return Message.ALL_INBOX_SELECTION;
+ }
+ if (mailboxId == Mailbox.QUERY_ALL_DRAFTS) {
+ return Message.ALL_DRAFT_SELECTION;
+ }
+ if (mailboxId == Mailbox.QUERY_ALL_OUTBOX) {
+ return Message.ALL_OUTBOX_SELECTION;
+ }
+ if (mailboxId == Mailbox.QUERY_ALL_UNREAD) {
+ return Message.ALL_UNREAD_SELECTION;
+ }
+ // TODO: we only support per-account starred mailbox right now, but presumably, we
+ // can surface the same thing for unread.
+ if (mailboxId == Mailbox.QUERY_ALL_FAVORITES) {
+ if (accountId == Account.ACCOUNT_ID_COMBINED_VIEW) {
+ return Message.ALL_FAVORITE_SELECTION;
+ }
+
+ final StringBuilder selection = new StringBuilder();
+ selection.append(MessageColumns.ACCOUNT_KEY).append('=').append(accountId)
+ .append(" AND ")
+ .append(Message.ALL_FAVORITE_SELECTION);
+ return selection.toString();
+ }
+
+ // Now it's a regular mailbox.
+ final StringBuilder selection = new StringBuilder();
+
+ selection.append(MessageColumns.MAILBOX_KEY).append('=').append(mailboxId);
+
+ if (Mailbox.getMailboxType(context, mailboxId) != Mailbox.TYPE_OUTBOX) {
+ selection.append(" AND ").append(Message.FLAG_LOADED_SELECTION);
+ }
+ return selection.toString();
+ }
+ }
+
+ public interface AttachmentColumns {
+ public static final String ID = "_id";
+ // The display name of the attachment
+ public static final String FILENAME = "fileName";
+ // The mime type of the attachment
+ public static final String MIME_TYPE = "mimeType";
+ // The size of the attachment in bytes
+ public static final String SIZE = "size";
+ // The (internal) contentId of the attachment (inline attachments will have these)
+ public static final String CONTENT_ID = "contentId";
+ // The location of the loaded attachment (probably a file)
+ @SuppressWarnings("hiding")
+ public static final String CONTENT_URI = "contentUri";
+ // A foreign key into the Message table (the message owning this attachment)
+ public static final String MESSAGE_KEY = "messageKey";
+ // The location of the attachment on the server side
+ // For IMAP, this is a part number (e.g. 2.1); for EAS, it's the internal file name
+ public static final String LOCATION = "location";
+ // The transfer encoding of the attachment
+ public static final String ENCODING = "encoding";
+ // Not currently used
+ public static final String CONTENT = "content";
+ // Flags
+ public static final String FLAGS = "flags";
+ // Content that is actually contained in the Attachment row
+ public static final String CONTENT_BYTES = "content_bytes";
+ // A foreign key into the Account table (for the message owning this attachment)
+ public static final String ACCOUNT_KEY = "accountKey";
+ // The UIProvider state of the attachment
+ public static final String UI_STATE = "uiState";
+ // The UIProvider destination of the attachment
+ public static final String UI_DESTINATION = "uiDestination";
+ // The UIProvider downloaded size of the attachment
+ public static final String UI_DOWNLOADED_SIZE = "uiDownloadedSize";
+ }
+
+ public static final class Attachment extends EmailContent
+ implements AttachmentColumns, Parcelable {
+ public static final String TABLE_NAME = "Attachment";
+ @SuppressWarnings("hiding")
+ public static final Uri CONTENT_URI = Uri.parse(EmailContent.CONTENT_URI + "/attachment");
+ // This must be used with an appended id: ContentUris.withAppendedId(MESSAGE_ID_URI, id)
+ public static final Uri MESSAGE_ID_URI = Uri.parse(
+ EmailContent.CONTENT_URI + "/attachment/message");
+
+ public String mFileName;
+ public String mMimeType;
+ public long mSize;
+ public String mContentId;
+ public String mContentUri;
+ public long mMessageKey;
+ public String mLocation;
+ public String mEncoding;
+ public String mContent; // Not currently used
+ public int mFlags;
+ public byte[] mContentBytes;
+ public long mAccountKey;
+ public int mUiState;
+ public int mUiDestination;
+ public int mUiDownloadedSize;
+
+ public static final int CONTENT_ID_COLUMN = 0;
+ public static final int CONTENT_FILENAME_COLUMN = 1;
+ public static final int CONTENT_MIME_TYPE_COLUMN = 2;
+ public static final int CONTENT_SIZE_COLUMN = 3;
+ public static final int CONTENT_CONTENT_ID_COLUMN = 4;
+ public static final int CONTENT_CONTENT_URI_COLUMN = 5;
+ public static final int CONTENT_MESSAGE_ID_COLUMN = 6;
+ public static final int CONTENT_LOCATION_COLUMN = 7;
+ public static final int CONTENT_ENCODING_COLUMN = 8;
+ public static final int CONTENT_CONTENT_COLUMN = 9; // Not currently used
+ public static final int CONTENT_FLAGS_COLUMN = 10;
+ public static final int CONTENT_CONTENT_BYTES_COLUMN = 11;
+ public static final int CONTENT_ACCOUNT_KEY_COLUMN = 12;
+ public static final int CONTENT_UI_STATE_COLUMN = 13;
+ public static final int CONTENT_UI_DESTINATION_COLUMN = 14;
+ public static final int CONTENT_UI_DOWNLOADED_SIZE_COLUMN = 15;
+ public static final String[] CONTENT_PROJECTION = new String[] {
+ RECORD_ID, AttachmentColumns.FILENAME, AttachmentColumns.MIME_TYPE,
+ AttachmentColumns.SIZE, AttachmentColumns.CONTENT_ID, AttachmentColumns.CONTENT_URI,
+ AttachmentColumns.MESSAGE_KEY, AttachmentColumns.LOCATION, AttachmentColumns.ENCODING,
+ AttachmentColumns.CONTENT, AttachmentColumns.FLAGS, AttachmentColumns.CONTENT_BYTES,
+ AttachmentColumns.ACCOUNT_KEY, AttachmentColumns.UI_STATE,
+ AttachmentColumns.UI_DESTINATION, AttachmentColumns.UI_DOWNLOADED_SIZE
+ };
+
+ // All attachments with an empty URI, regardless of mailbox
+ public static final String PRECACHE_SELECTION =
+ AttachmentColumns.CONTENT_URI + " isnull AND " + Attachment.FLAGS + "=0";
+ // Attachments with an empty URI that are in an inbox
+ public static final String PRECACHE_INBOX_SELECTION =
+ PRECACHE_SELECTION + " AND " + AttachmentColumns.MESSAGE_KEY + " IN ("
+ + "SELECT " + MessageColumns.ID + " FROM " + Message.TABLE_NAME
+ + " WHERE " + Message.ALL_INBOX_SELECTION
+ + ")";
+
+ // Bits used in mFlags
+ // WARNING: AttachmentDownloadService relies on the fact that ALL of the flags below
+ // disqualify attachments for precaching. If you add a flag that does NOT disqualify an
+ // attachment for precaching, you MUST change the PRECACHE_SELECTION definition above
+
+ // Instruct Rfc822Output to 1) not use Content-Disposition and 2) use multipart/alternative
+ // with this attachment. This is only valid if there is one and only one attachment and
+ // that attachment has this flag set
+ public static final int FLAG_ICS_ALTERNATIVE_PART = 1<<0;
+ // Indicate that this attachment has been requested for downloading by the user; this is
+ // the highest priority for attachment downloading
+ public static final int FLAG_DOWNLOAD_USER_REQUEST = 1<<1;
+ // Indicate that this attachment needs to be downloaded as part of an outgoing forwarded
+ // message
+ public static final int FLAG_DOWNLOAD_FORWARD = 1<<2;
+ // Indicates that the attachment download failed in a non-recoverable manner
+ public static final int FLAG_DOWNLOAD_FAILED = 1<<3;
+ // Allow "room" for some additional download-related flags here
+ // Indicates that the attachment will be smart-forwarded
+ public static final int FLAG_SMART_FORWARD = 1<<8;
+ // Indicates that the attachment cannot be forwarded due to a policy restriction
+ public static final int FLAG_POLICY_DISALLOWS_DOWNLOAD = 1<<9;
+ /**
+ * no public constructor since this is a utility class
+ */
+ public Attachment() {
+ mBaseUri = CONTENT_URI;
+ }
+
+ /**
+ * Restore an Attachment from the database, given its unique id
+ * @param context
+ * @param id
+ * @return the instantiated Attachment
+ */
+ public static Attachment restoreAttachmentWithId(Context context, long id) {
+ return EmailContent.restoreContentWithId(context, Attachment.class,
+ Attachment.CONTENT_URI, Attachment.CONTENT_PROJECTION, id);
+ }
+
+ /**
+ * Restore all the Attachments of a message given its messageId
+ */
+ public static Attachment[] restoreAttachmentsWithMessageId(Context context,
+ long messageId) {
+ Uri uri = ContentUris.withAppendedId(MESSAGE_ID_URI, messageId);
+ Cursor c = context.getContentResolver().query(uri, CONTENT_PROJECTION,
+ null, null, null);
+ try {
+ int count = c.getCount();
+ Attachment[] attachments = new Attachment[count];
+ for (int i = 0; i < count; ++i) {
+ c.moveToNext();
+ Attachment attach = new Attachment();
+ attach.restore(c);
+ attachments[i] = attach;
+ }
+ return attachments;
+ } finally {
+ c.close();
+ }
+ }
+
+ /**
+ * Creates a unique file in the external store by appending a hyphen
+ * and a number to the given filename.
+ * @param filename
+ * @return a new File object, or null if one could not be created
+ */
+ public static File createUniqueFile(String filename) {
+ // TODO Handle internal storage, as required
+ if (Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {
+ File directory = Environment.getExternalStorageDirectory();
+ File file = new File(directory, filename);
+ if (!file.exists()) {
+ return file;
+ }
+ // Get the extension of the file, if any.
+ int index = filename.lastIndexOf('.');
+ String name = filename;
+ String extension = "";
+ if (index != -1) {
+ name = filename.substring(0, index);
+ extension = filename.substring(index);
+ }
+ for (int i = 2; i < Integer.MAX_VALUE; i++) {
+ file = new File(directory, name + '-' + i + extension);
+ if (!file.exists()) {
+ return file;
+ }
+ }
+ return null;
+ }
+ return null;
+ }
+
+ @Override
+ public void restore(Cursor cursor) {
+ mBaseUri = CONTENT_URI;
+ mId = cursor.getLong(CONTENT_ID_COLUMN);
+ mFileName= cursor.getString(CONTENT_FILENAME_COLUMN);
+ mMimeType = cursor.getString(CONTENT_MIME_TYPE_COLUMN);
+ mSize = cursor.getLong(CONTENT_SIZE_COLUMN);
+ mContentId = cursor.getString(CONTENT_CONTENT_ID_COLUMN);
+ mContentUri = cursor.getString(CONTENT_CONTENT_URI_COLUMN);
+ mMessageKey = cursor.getLong(CONTENT_MESSAGE_ID_COLUMN);
+ mLocation = cursor.getString(CONTENT_LOCATION_COLUMN);
+ mEncoding = cursor.getString(CONTENT_ENCODING_COLUMN);
+ mContent = cursor.getString(CONTENT_CONTENT_COLUMN);
+ mFlags = cursor.getInt(CONTENT_FLAGS_COLUMN);
+ mContentBytes = cursor.getBlob(CONTENT_CONTENT_BYTES_COLUMN);
+ mAccountKey = cursor.getLong(CONTENT_ACCOUNT_KEY_COLUMN);
+ mUiState = cursor.getInt(CONTENT_UI_STATE_COLUMN);
+ mUiDestination = cursor.getInt(CONTENT_UI_DESTINATION_COLUMN);
+ mUiDownloadedSize = cursor.getInt(CONTENT_UI_DOWNLOADED_SIZE_COLUMN);
+ }
+
+ @Override
+ public ContentValues toContentValues() {
+ ContentValues values = new ContentValues();
+ values.put(AttachmentColumns.FILENAME, mFileName);
+ values.put(AttachmentColumns.MIME_TYPE, mMimeType);
+ values.put(AttachmentColumns.SIZE, mSize);
+ values.put(AttachmentColumns.CONTENT_ID, mContentId);
+ values.put(AttachmentColumns.CONTENT_URI, mContentUri);
+ values.put(AttachmentColumns.MESSAGE_KEY, mMessageKey);
+ values.put(AttachmentColumns.LOCATION, mLocation);
+ values.put(AttachmentColumns.ENCODING, mEncoding);
+ values.put(AttachmentColumns.CONTENT, mContent);
+ values.put(AttachmentColumns.FLAGS, mFlags);
+ values.put(AttachmentColumns.CONTENT_BYTES, mContentBytes);
+ values.put(AttachmentColumns.ACCOUNT_KEY, mAccountKey);
+ values.put(AttachmentColumns.UI_STATE, mUiState);
+ values.put(AttachmentColumns.UI_DESTINATION, mUiDestination);
+ values.put(AttachmentColumns.UI_DOWNLOADED_SIZE, mUiDownloadedSize);
+ return values;
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ // mBaseUri is not parceled
+ dest.writeLong(mId);
+ dest.writeString(mFileName);
+ dest.writeString(mMimeType);
+ dest.writeLong(mSize);
+ dest.writeString(mContentId);
+ dest.writeString(mContentUri);
+ dest.writeLong(mMessageKey);
+ dest.writeString(mLocation);
+ dest.writeString(mEncoding);
+ dest.writeString(mContent);
+ dest.writeInt(mFlags);
+ dest.writeLong(mAccountKey);
+ if (mContentBytes == null) {
+ dest.writeInt(-1);
+ } else {
+ dest.writeInt(mContentBytes.length);
+ dest.writeByteArray(mContentBytes);
+ }
+ dest.writeInt(mUiState);
+ dest.writeInt(mUiDestination);
+ dest.writeInt(mUiDownloadedSize);
+ }
+
+ public Attachment(Parcel in) {
+ mBaseUri = EmailContent.Attachment.CONTENT_URI;
+ mId = in.readLong();
+ mFileName = in.readString();
+ mMimeType = in.readString();
+ mSize = in.readLong();
+ mContentId = in.readString();
+ mContentUri = in.readString();
+ mMessageKey = in.readLong();
+ mLocation = in.readString();
+ mEncoding = in.readString();
+ mContent = in.readString();
+ mFlags = in.readInt();
+ mAccountKey = in.readLong();
+ final int contentBytesLen = in.readInt();
+ if (contentBytesLen == -1) {
+ mContentBytes = null;
+ } else {
+ mContentBytes = new byte[contentBytesLen];
+ in.readByteArray(mContentBytes);
+ }
+ mUiState = in.readInt();
+ mUiDestination = in.readInt();
+ mUiDownloadedSize = in.readInt();
+ }
+
+ public static final Parcelable.Creator<EmailContent.Attachment> CREATOR
+ = new Parcelable.Creator<EmailContent.Attachment>() {
+ @Override
+ public EmailContent.Attachment createFromParcel(Parcel in) {
+ return new EmailContent.Attachment(in);
+ }
+
+ @Override
+ public EmailContent.Attachment[] newArray(int size) {
+ return new EmailContent.Attachment[size];
+ }
+ };
+
+ @Override
+ public String toString() {
+ return "[" + mFileName + ", " + mMimeType + ", " + mSize + ", " + mContentId + ", "
+ + mContentUri + ", " + mMessageKey + ", " + mLocation + ", " + mEncoding + ", "
+ + mFlags + ", " + mContentBytes + ", " + mAccountKey + "," + mUiState + ","
+ + mUiDestination + "," + mUiDownloadedSize + "]";
+ }
+ }
+
+ public interface AccountColumns {
+ public static final String ID = "_id";
+ // The display name of the account (user-settable)
+ public static final String DISPLAY_NAME = "displayName";
+ // The email address corresponding to this account
+ public static final String EMAIL_ADDRESS = "emailAddress";
+ // A server-based sync key on an account-wide basis (EAS needs this)
+ public static final String SYNC_KEY = "syncKey";
+ // The default sync lookback period for this account
+ public static final String SYNC_LOOKBACK = "syncLookback";
+ // The default sync frequency for this account, in minutes
+ public static final String SYNC_INTERVAL = "syncInterval";
+ // A foreign key into the account manager, having host, login, password, port, and ssl flags
+ public static final String HOST_AUTH_KEY_RECV = "hostAuthKeyRecv";
+ // (optional) A foreign key into the account manager, having host, login, password, port,
+ // and ssl flags
+ public static final String HOST_AUTH_KEY_SEND = "hostAuthKeySend";
+ // Flags
+ public static final String FLAGS = "flags";
+ // Default account
+ public static final String IS_DEFAULT = "isDefault";
+ // Old-Style UUID for compatibility with previous versions
+ public static final String COMPATIBILITY_UUID = "compatibilityUuid";
+ // User name (for outgoing messages)
+ public static final String SENDER_NAME = "senderName";
+ // Ringtone
+ public static final String RINGTONE_URI = "ringtoneUri";
+ // Protocol version (arbitrary string, used by EAS currently)
+ public static final String PROTOCOL_VERSION = "protocolVersion";
+ // The number of new messages (reported by the sync/download engines
+ public static final String NEW_MESSAGE_COUNT = "newMessageCount";
+ // Legacy flags defining security (provisioning) requirements of this account; this
+ // information is now found in the Policy table; POLICY_KEY (below) is the foreign key
+ @Deprecated
+ public static final String SECURITY_FLAGS = "securityFlags";
+ // Server-based sync key for the security policies currently enforced
+ public static final String SECURITY_SYNC_KEY = "securitySyncKey";
+ // Signature to use with this account
+ public static final String SIGNATURE = "signature";
+ // A foreign key into the Policy table
+ public static final String POLICY_KEY = "policyKey";
+ }
+
+ public interface QuickResponseColumns {
+ // The QuickResponse text
+ static final String TEXT = "quickResponse";
+ // A foreign key into the Account table owning the QuickResponse
+ static final String ACCOUNT_KEY = "accountKey";
+ }
+
+ public interface MailboxColumns {
+ public static final String ID = "_id";
+ // The display name of this mailbox [INDEX]
+ static final String DISPLAY_NAME = "displayName";
+ // The server's identifier for this mailbox
+ public static final String SERVER_ID = "serverId";
+ // The server's identifier for the parent of this mailbox (null = top-level)
+ public static final String PARENT_SERVER_ID = "parentServerId";
+ // A foreign key for the parent of this mailbox (-1 = top-level, 0=uninitialized)
+ public static final String PARENT_KEY = "parentKey";
+ // A foreign key to the Account that owns this mailbox
+ public static final String ACCOUNT_KEY = "accountKey";
+ // The type (role) of this mailbox
+ public static final String TYPE = "type";
+ // The hierarchy separator character
+ public static final String DELIMITER = "delimiter";
+ // Server-based sync key or validity marker (e.g. "SyncKey" for EAS, "uidvalidity" for IMAP)
+ public static final String SYNC_KEY = "syncKey";
+ // The sync lookback period for this mailbox (or null if using the account default)
+ public static final String SYNC_LOOKBACK = "syncLookback";
+ // The sync frequency for this mailbox (or null if using the account default)
+ public static final String SYNC_INTERVAL = "syncInterval";
+ // The time of last successful sync completion (millis)
+ public static final String SYNC_TIME = "syncTime";
+ // Cached unread count
+ public static final String UNREAD_COUNT = "unreadCount";
+ // Visibility of this folder in a list of folders [INDEX]
+ public static final String FLAG_VISIBLE = "flagVisible";
+ // Other states, as a bit field, e.g. CHILDREN_VISIBLE, HAS_CHILDREN
+ public static final String FLAGS = "flags";
+ // Backward compatible
+ public static final String VISIBLE_LIMIT = "visibleLimit";
+ // Sync status (can be used as desired by sync services)
+ public static final String SYNC_STATUS = "syncStatus";
+ // Number of messages in the mailbox.
+ public static final String MESSAGE_COUNT = "messageCount";
+ // The last time a message in this mailbox has been read (in millis)
+ public static final String LAST_TOUCHED_TIME = "lastTouchedTime";
+ // The UIProvider sync status
+ public static final String UI_SYNC_STATUS = "uiSyncStatus";
+ // The UIProvider last sync result
+ public static final String UI_LAST_SYNC_RESULT = "uiLastSyncResult";
+ // The UIProvider sync status
+ public static final String LAST_NOTIFIED_MESSAGE_KEY = "lastNotifiedMessageKey";
+ // The UIProvider last sync result
+ public static final String LAST_NOTIFIED_MESSAGE_COUNT = "lastNotifiedMessageCount";
+ // The total number of messages in the remote mailbox
+ public static final String TOTAL_COUNT = "totalCount";
+ }
+
+ public interface HostAuthColumns {
+ public static final String ID = "_id";
+ // The protocol (e.g. "imap", "pop3", "eas", "smtp"
+ static final String PROTOCOL = "protocol";
+ // The host address
+ static final String ADDRESS = "address";
+ // The port to use for the connection
+ static final String PORT = "port";
+ // General purpose flags
+ static final String FLAGS = "flags";
+ // The login (user name)
+ static final String LOGIN = "login";
+ // Password
+ static final String PASSWORD = "password";
+ // A domain or path, if required (used in IMAP and EAS)
+ static final String DOMAIN = "domain";
+ // An alias to a local client certificate for SSL
+ static final String CLIENT_CERT_ALIAS = "certAlias";
+ // DEPRECATED - Will not be set or stored
+ static final String ACCOUNT_KEY = "accountKey";
+ }
+
+ public interface PolicyColumns {
+ public static final String ID = "_id";
+ public static final String PASSWORD_MODE = "passwordMode";
+ public static final String PASSWORD_MIN_LENGTH = "passwordMinLength";
+ public static final String PASSWORD_EXPIRATION_DAYS = "passwordExpirationDays";
+ public static final String PASSWORD_HISTORY = "passwordHistory";
+ public static final String PASSWORD_COMPLEX_CHARS = "passwordComplexChars";
+ public static final String PASSWORD_MAX_FAILS = "passwordMaxFails";
+ public static final String MAX_SCREEN_LOCK_TIME = "maxScreenLockTime";
+ public static final String REQUIRE_REMOTE_WIPE = "requireRemoteWipe";
+ public static final String REQUIRE_ENCRYPTION = "requireEncryption";
+ public static final String REQUIRE_ENCRYPTION_EXTERNAL = "requireEncryptionExternal";
+ // ICS additions
+ // Note: the appearance of these columns does not imply that we support these features; only
+ // that we store them in the Policy structure
+ public static final String REQUIRE_MANUAL_SYNC_WHEN_ROAMING = "requireManualSyncRoaming";
+ public static final String DONT_ALLOW_CAMERA = "dontAllowCamera";
+ public static final String DONT_ALLOW_ATTACHMENTS = "dontAllowAttachments";
+ public static final String DONT_ALLOW_HTML = "dontAllowHtml";
+ public static final String MAX_ATTACHMENT_SIZE = "maxAttachmentSize";
+ public static final String MAX_TEXT_TRUNCATION_SIZE = "maxTextTruncationSize";
+ public static final String MAX_HTML_TRUNCATION_SIZE = "maxHTMLTruncationSize";
+ public static final String MAX_EMAIL_LOOKBACK = "maxEmailLookback";
+ public static final String MAX_CALENDAR_LOOKBACK = "maxCalendarLookback";
+ // Indicates that the server allows password recovery, not that we support it
+ public static final String PASSWORD_RECOVERY_ENABLED = "passwordRecoveryEnabled";
+ // Tokenized strings indicating protocol specific policies enforced/unsupported
+ public static final String PROTOCOL_POLICIES_ENFORCED = "protocolPoliciesEnforced";
+ public static final String PROTOCOL_POLICIES_UNSUPPORTED = "protocolPoliciesUnsupported";
+ }
+}
diff --git a/email2/emailcommon/src/com/android/emailcommon/provider/HostAuth.aidl b/email2/emailcommon/src/com/android/emailcommon/provider/HostAuth.aidl
new file mode 100644
index 0000000..88eb88f
--- /dev/null
+++ b/email2/emailcommon/src/com/android/emailcommon/provider/HostAuth.aidl
@@ -0,0 +1,18 @@
+/* Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.emailcommon.provider;
+
+parcelable HostAuth;
\ No newline at end of file
diff --git a/email2/emailcommon/src/com/android/emailcommon/provider/HostAuth.java b/email2/emailcommon/src/com/android/emailcommon/provider/HostAuth.java
new file mode 100644
index 0000000..8729418
--- /dev/null
+++ b/email2/emailcommon/src/com/android/emailcommon/provider/HostAuth.java
@@ -0,0 +1,441 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+
+package com.android.emailcommon.provider;
+
+import android.content.ContentValues;
+import android.content.Context;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.text.TextUtils;
+
+import com.android.emailcommon.provider.EmailContent.HostAuthColumns;
+import com.android.emailcommon.utility.SSLUtils;
+import com.android.emailcommon.utility.Utility;
+
+import java.net.URI;
+import java.net.URISyntaxException;
+
+public final class HostAuth extends EmailContent implements HostAuthColumns, Parcelable {
+ public static final String TABLE_NAME = "HostAuth";
+ @SuppressWarnings("hiding")
+ public static final Uri CONTENT_URI = Uri.parse(EmailContent.CONTENT_URI + "/hostauth");
+ // TODO the three following constants duplicate constants in Store.java; remove those and
+ // just reference these.
+ public static final String SCHEME_IMAP = "imap";
+ public static final String SCHEME_POP3 = "pop3";
+ public static final String SCHEME_EAS = "eas";
+ public static final String SCHEME_SMTP = "smtp";
+ public static final String SCHEME_TRUST_ALL_CERTS = "trustallcerts";
+
+ public static final int PORT_UNKNOWN = -1;
+
+ public static final int FLAG_NONE = 0x00; // No flags
+ public static final int FLAG_SSL = 0x01; // Use SSL
+ public static final int FLAG_TLS = 0x02; // Use TLS
+ public static final int FLAG_AUTHENTICATE = 0x04; // Use name/password for authentication
+ public static final int FLAG_TRUST_ALL = 0x08; // Trust all certificates
+ // Mask of settings directly configurable by the user
+ public static final int USER_CONFIG_MASK = 0x0b;
+
+ public String mProtocol;
+ public String mAddress;
+ public int mPort;
+ public int mFlags;
+ public String mLogin;
+ public String mPassword;
+ public String mDomain;
+ public String mClientCertAlias = null;
+
+ public static final int CONTENT_ID_COLUMN = 0;
+ public static final int CONTENT_PROTOCOL_COLUMN = 1;
+ public static final int CONTENT_ADDRESS_COLUMN = 2;
+ public static final int CONTENT_PORT_COLUMN = 3;
+ public static final int CONTENT_FLAGS_COLUMN = 4;
+ public static final int CONTENT_LOGIN_COLUMN = 5;
+ public static final int CONTENT_PASSWORD_COLUMN = 6;
+ public static final int CONTENT_DOMAIN_COLUMN = 7;
+ public static final int CONTENT_CLIENT_CERT_ALIAS_COLUMN = 8;
+
+ public static final String[] CONTENT_PROJECTION = new String[] {
+ RECORD_ID, HostAuthColumns.PROTOCOL, HostAuthColumns.ADDRESS, HostAuthColumns.PORT,
+ HostAuthColumns.FLAGS, HostAuthColumns.LOGIN,
+ HostAuthColumns.PASSWORD, HostAuthColumns.DOMAIN, HostAuthColumns.CLIENT_CERT_ALIAS
+ };
+
+ /**
+ * no public constructor since this is a utility class
+ */
+ public HostAuth() {
+ mBaseUri = CONTENT_URI;
+
+ // other defaults policy)
+ mPort = PORT_UNKNOWN;
+ }
+
+ /**
+ * Restore a HostAuth from the database, given its unique id
+ * @param context
+ * @param id
+ * @return the instantiated HostAuth
+ */
+ public static HostAuth restoreHostAuthWithId(Context context, long id) {
+ return EmailContent.restoreContentWithId(context, HostAuth.class,
+ HostAuth.CONTENT_URI, HostAuth.CONTENT_PROJECTION, id);
+ }
+
+
+ /**
+ * Returns the scheme for the specified flags.
+ */
+ public static String getSchemeString(String protocol, int flags) {
+ return getSchemeString(protocol, flags, null);
+ }
+
+ /**
+ * Builds a URI scheme name given the parameters for a {@code HostAuth}.
+ * If a {@code clientAlias} is provided, this indicates that a secure connection must be used.
+ */
+ public static String getSchemeString(String protocol, int flags, String clientAlias) {
+ String security = "";
+ switch (flags & USER_CONFIG_MASK) {
+ case FLAG_SSL:
+ security = "+ssl+";
+ break;
+ case FLAG_SSL | FLAG_TRUST_ALL:
+ security = "+ssl+trustallcerts";
+ break;
+ case FLAG_TLS:
+ security = "+tls+";
+ break;
+ case FLAG_TLS | FLAG_TRUST_ALL:
+ security = "+tls+trustallcerts";
+ break;
+ }
+
+ if (!TextUtils.isEmpty(clientAlias)) {
+ if (TextUtils.isEmpty(security)) {
+ throw new IllegalArgumentException(
+ "Can't specify a certificate alias for a non-secure connection");
+ }
+ if (!security.endsWith("+")) {
+ security += "+";
+ }
+ security += SSLUtils.escapeForSchemeName(clientAlias);
+ }
+
+ return protocol + security;
+ }
+
+ /**
+ * Returns the flags for the specified scheme.
+ */
+ public static int getSchemeFlags(String scheme) {
+ String[] schemeParts = scheme.split("\\+");
+ int flags = HostAuth.FLAG_NONE;
+ if (schemeParts.length >= 2) {
+ String part1 = schemeParts[1];
+ if ("ssl".equals(part1)) {
+ flags |= HostAuth.FLAG_SSL;
+ } else if ("tls".equals(part1)) {
+ flags |= HostAuth.FLAG_TLS;
+ }
+ if (schemeParts.length >= 3) {
+ String part2 = schemeParts[2];
+ if (SCHEME_TRUST_ALL_CERTS.equals(part2)) {
+ flags |= HostAuth.FLAG_TRUST_ALL;
+ }
+ }
+ }
+ return flags;
+ }
+
+ @Override
+ public void restore(Cursor cursor) {
+ mBaseUri = CONTENT_URI;
+ mId = cursor.getLong(CONTENT_ID_COLUMN);
+ mProtocol = cursor.getString(CONTENT_PROTOCOL_COLUMN);
+ mAddress = cursor.getString(CONTENT_ADDRESS_COLUMN);
+ mPort = cursor.getInt(CONTENT_PORT_COLUMN);
+ mFlags = cursor.getInt(CONTENT_FLAGS_COLUMN);
+ mLogin = cursor.getString(CONTENT_LOGIN_COLUMN);
+ mPassword = cursor.getString(CONTENT_PASSWORD_COLUMN);
+ mDomain = cursor.getString(CONTENT_DOMAIN_COLUMN);
+ mClientCertAlias = cursor.getString(CONTENT_CLIENT_CERT_ALIAS_COLUMN);
+ }
+
+ @Override
+ public ContentValues toContentValues() {
+ ContentValues values = new ContentValues();
+ values.put(HostAuthColumns.PROTOCOL, mProtocol);
+ values.put(HostAuthColumns.ADDRESS, mAddress);
+ values.put(HostAuthColumns.PORT, mPort);
+ values.put(HostAuthColumns.FLAGS, mFlags);
+ values.put(HostAuthColumns.LOGIN, mLogin);
+ values.put(HostAuthColumns.PASSWORD, mPassword);
+ values.put(HostAuthColumns.DOMAIN, mDomain);
+ values.put(HostAuthColumns.CLIENT_CERT_ALIAS, mClientCertAlias);
+ values.put(HostAuthColumns.ACCOUNT_KEY, 0); // Need something to satisfy the DB
+ return values;
+ }
+
+ /**
+ * Sets the user name and password from URI user info string
+ */
+ public void setLogin(String userInfo) {
+ String userName = null;
+ String userPassword = null;
+ if (!TextUtils.isEmpty(userInfo)) {
+ String[] userInfoParts = userInfo.split(":", 2);
+ userName = userInfoParts[0];
+ if (userInfoParts.length > 1) {
+ userPassword = userInfoParts[1];
+ }
+ }
+ setLogin(userName, userPassword);
+ }
+
+ /**
+ * Sets the user name and password
+ */
+ public void setLogin(String userName, String userPassword) {
+ mLogin = userName;
+ mPassword = userPassword;
+
+ if (mLogin == null) {
+ mFlags &= ~FLAG_AUTHENTICATE;
+ } else {
+ mFlags |= FLAG_AUTHENTICATE;
+ }
+ }
+
+ /**
+ * Returns the login information. [0] is the username and [1] is the password. If
+ * {@link #FLAG_AUTHENTICATE} is not set, {@code null} is returned.
+ */
+ public String[] getLogin() {
+ if ((mFlags & FLAG_AUTHENTICATE) != 0) {
+ String trimUser = (mLogin != null) ? mLogin.trim() : "";
+ String password = (mPassword != null) ? mPassword : "";
+ return new String[] { trimUser, password };
+ }
+ return null;
+ }
+
+ public void setConnection(String protocol, String address, int port, int flags) {
+ setConnection(protocol, address, port, flags, null);
+ }
+
+ /**
+ * Sets the internal connection parameters based on the specified parameter values.
+ * @param protocol the mail protocol to use (e.g. "eas", "imap").
+ * @param address the address of the server
+ * @param port the port for the connection
+ * @param flags flags indicating the security and type of the connection
+ * @param clientCertAlias an optional alias to use if a client user certificate is to be
+ * presented during connection establishment. If this is non-empty, it must be the case
+ * that flags indicates use of a secure connection
+ */
+ public void setConnection(String protocol, String address,
+ int port, int flags, String clientCertAlias) {
+ // Set protocol, security, and additional flags based on uri scheme
+ mProtocol = protocol;
+
+ mFlags &= ~(FLAG_SSL | FLAG_TLS | FLAG_TRUST_ALL);
+ mFlags |= (flags & USER_CONFIG_MASK);
+
+ boolean useSecureConnection = (flags & (FLAG_SSL | FLAG_TLS)) != 0;
+ if (!useSecureConnection && !TextUtils.isEmpty(clientCertAlias)) {
+ throw new IllegalArgumentException("Can't use client alias on non-secure connections");
+ }
+
+ mAddress = address;
+ mPort = port;
+ if (mPort == PORT_UNKNOWN) {
+ boolean useSSL = ((mFlags & FLAG_SSL) != 0);
+ // infer port# from protocol + security
+ // SSL implies a different port - TLS runs in the "regular" port
+ // NOTE: Although the port should be setup in the various setup screens, this
+ // block cannot easily be moved because we get process URIs from other sources
+ // (e.g. for tests, provider templates and account restore) that may or may not
+ // have a port specified.
+ if (SCHEME_POP3.equals(mProtocol)) {
+ mPort = useSSL ? 995 : 110;
+ } else if (SCHEME_IMAP.equals(mProtocol)) {
+ mPort = useSSL ? 993 : 143;
+ } else if (SCHEME_EAS.equals(mProtocol)) {
+ mPort = useSSL ? 443 : 80;
+ } else if (SCHEME_SMTP.equals(mProtocol)) {
+ mPort = useSSL ? 465 : 587;
+ }
+ }
+
+ mClientCertAlias = clientCertAlias;
+ }
+
+ /** Returns {@code true} if this is an EAS connection; otherwise, {@code false}. */
+ public boolean isEasConnection() {
+ return SCHEME_EAS.equals(mProtocol);
+ }
+
+ /** Convenience method to determine if SSL is used. */
+ public boolean shouldUseSsl() {
+ return (mFlags & FLAG_SSL) != 0;
+ }
+
+ /** Convenience method to determine if all server certs should be used. */
+ public boolean shouldTrustAllServerCerts() {
+ return (mFlags & FLAG_TRUST_ALL) != 0;
+ }
+
+ /**
+ * Supports Parcelable
+ */
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ /**
+ * Supports Parcelable
+ */
+ public static final Parcelable.Creator<HostAuth> CREATOR
+ = new Parcelable.Creator<HostAuth>() {
+ @Override
+ public HostAuth createFromParcel(Parcel in) {
+ return new HostAuth(in);
+ }
+
+ @Override
+ public HostAuth[] newArray(int size) {
+ return new HostAuth[size];
+ }
+ };
+
+ /**
+ * Supports Parcelable
+ */
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ // mBaseUri is not parceled
+ dest.writeLong(mId);
+ dest.writeString(mProtocol);
+ dest.writeString(mAddress);
+ dest.writeInt(mPort);
+ dest.writeInt(mFlags);
+ dest.writeString(mLogin);
+ dest.writeString(mPassword);
+ dest.writeString(mDomain);
+ dest.writeString(mClientCertAlias);
+ }
+
+ /**
+ * Supports Parcelable
+ */
+ public HostAuth(Parcel in) {
+ mBaseUri = CONTENT_URI;
+ mId = in.readLong();
+ mProtocol = in.readString();
+ mAddress = in.readString();
+ mPort = in.readInt();
+ mFlags = in.readInt();
+ mLogin = in.readString();
+ mPassword = in.readString();
+ mDomain = in.readString();
+ mClientCertAlias = in.readString();
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (!(o instanceof HostAuth)) {
+ return false;
+ }
+ HostAuth that = (HostAuth)o;
+ return mPort == that.mPort
+ && mFlags == that.mFlags
+ && Utility.areStringsEqual(mProtocol, that.mProtocol)
+ && Utility.areStringsEqual(mAddress, that.mAddress)
+ && Utility.areStringsEqual(mLogin, that.mLogin)
+ && Utility.areStringsEqual(mPassword, that.mPassword)
+ && Utility.areStringsEqual(mDomain, that.mDomain)
+ && Utility.areStringsEqual(mClientCertAlias, that.mClientCertAlias);
+ }
+
+ /**
+ * The flag, password, and client cert alias are the only items likely to change after a
+ * HostAuth is created
+ */
+ @Override
+ public int hashCode() {
+ int hashCode = 29;
+ if (mPassword != null) {
+ hashCode += mPassword.hashCode();
+ }
+ if (mClientCertAlias != null) {
+ hashCode += (mClientCertAlias.hashCode() << 8);
+ }
+ return (hashCode << 8) + mFlags;
+ }
+
+ /**
+ * Legacy URI parser. Used in parsing template from provider.xml
+ * Example string:
+ * "eas+ssl+trustallcerts://user:password@server/domain:123"
+ *
+ * Note that the use of client certificate is specified in the URI, a secure connection type
+ * must be used.
+ */
+ public static void setHostAuthFromString(HostAuth auth, String uriString)
+ throws URISyntaxException {
+ URI uri = new URI(uriString);
+ String path = uri.getPath();
+ String domain = null;
+ if (!TextUtils.isEmpty(path)) {
+ // Strip off the leading slash that begins the path.
+ domain = path.substring(1);
+ }
+ auth.mDomain = domain;
+ auth.setLogin(uri.getUserInfo());
+
+ String scheme = uri.getScheme();
+ auth.setConnection(scheme, uri.getHost(), uri.getPort());
+ }
+
+ /**
+ * Legacy code for setting connection values from a "scheme" (see above)
+ */
+ public void setConnection(String scheme, String host, int port) {
+ String[] schemeParts = scheme.split("\\+");
+ String protocol = schemeParts[0];
+ String clientCertAlias = null;
+ int flags = getSchemeFlags(scheme);
+
+ // Example scheme: "eas+ssl+trustallcerts" or "eas+tls+trustallcerts+client-cert-alias"
+ if (schemeParts.length > 3) {
+ clientCertAlias = schemeParts[3];
+ } else if (schemeParts.length > 2) {
+ if (!SCHEME_TRUST_ALL_CERTS.equals(schemeParts[2])) {
+ mClientCertAlias = schemeParts[2];
+ }
+ }
+
+ setConnection(protocol, host, port, flags, clientCertAlias);
+ }
+
+}
diff --git a/email2/emailcommon/src/com/android/emailcommon/provider/Mailbox.java b/email2/emailcommon/src/com/android/emailcommon/provider/Mailbox.java
new file mode 100644
index 0000000..4e44ad2
--- /dev/null
+++ b/email2/emailcommon/src/com/android/emailcommon/provider/Mailbox.java
@@ -0,0 +1,653 @@
+/*
+ * 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.emailcommon.provider;
+
+import android.content.ContentUris;
+import android.content.ContentValues;
+import android.content.Context;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.util.Log;
+
+import com.android.emailcommon.Logging;
+import com.android.emailcommon.provider.EmailContent.MailboxColumns;
+import com.android.emailcommon.provider.EmailContent.SyncColumns;
+import com.android.emailcommon.utility.Utility;
+
+public class Mailbox extends EmailContent implements SyncColumns, MailboxColumns, Parcelable {
+ public static final String TABLE_NAME = "Mailbox";
+ @SuppressWarnings("hiding")
+ public static final Uri CONTENT_URI = Uri.parse(EmailContent.CONTENT_URI + "/mailbox");
+ public static final Uri ADD_TO_FIELD_URI =
+ Uri.parse(EmailContent.CONTENT_URI + "/mailboxIdAddToField");
+ public static final Uri FROM_ACCOUNT_AND_TYPE_URI =
+ Uri.parse(EmailContent.CONTENT_URI + "/mailboxIdFromAccountAndType");
+
+ public String mDisplayName;
+ public String mServerId;
+ public String mParentServerId;
+ public long mParentKey;
+ public long mAccountKey;
+ public int mType;
+ public int mDelimiter;
+ public String mSyncKey;
+ public int mSyncLookback;
+ public int mSyncInterval;
+ public long mSyncTime;
+ public boolean mFlagVisible = true;
+ public int mFlags;
+ public int mVisibleLimit;
+ public String mSyncStatus;
+ public long mLastTouchedTime;
+ public int mUiSyncStatus;
+ public int mUiLastSyncResult;
+ public long mLastNotifiedMessageKey;
+ public int mLastNotifiedMessageCount;
+ public int mTotalCount;
+
+ public static final int CONTENT_ID_COLUMN = 0;
+ public static final int CONTENT_DISPLAY_NAME_COLUMN = 1;
+ public static final int CONTENT_SERVER_ID_COLUMN = 2;
+ public static final int CONTENT_PARENT_SERVER_ID_COLUMN = 3;
+ public static final int CONTENT_ACCOUNT_KEY_COLUMN = 4;
+ public static final int CONTENT_TYPE_COLUMN = 5;
+ public static final int CONTENT_DELIMITER_COLUMN = 6;
+ public static final int CONTENT_SYNC_KEY_COLUMN = 7;
+ public static final int CONTENT_SYNC_LOOKBACK_COLUMN = 8;
+ public static final int CONTENT_SYNC_INTERVAL_COLUMN = 9;
+ public static final int CONTENT_SYNC_TIME_COLUMN = 10;
+ public static final int CONTENT_FLAG_VISIBLE_COLUMN = 11;
+ public static final int CONTENT_FLAGS_COLUMN = 12;
+ public static final int CONTENT_VISIBLE_LIMIT_COLUMN = 13;
+ public static final int CONTENT_SYNC_STATUS_COLUMN = 14;
+ public static final int CONTENT_PARENT_KEY_COLUMN = 15;
+ public static final int CONTENT_LAST_TOUCHED_TIME_COLUMN = 16;
+ public static final int CONTENT_UI_SYNC_STATUS_COLUMN = 17;
+ public static final int CONTENT_UI_LAST_SYNC_RESULT_COLUMN = 18;
+ public static final int CONTENT_LAST_NOTIFIED_MESSAGE_KEY_COLUMN = 19;
+ public static final int CONTENT_LAST_NOTIFIED_MESSAGE_COUNT_COLUMN = 20;
+ public static final int CONTENT_TOTAL_COUNT_COLUMN = 21;
+
+ /**
+ * <em>NOTE</em>: If fields are added or removed, the method {@link #getHashes()}
+ * MUST be updated.
+ */
+ public static final String[] CONTENT_PROJECTION = new String[] {
+ RECORD_ID, MailboxColumns.DISPLAY_NAME, MailboxColumns.SERVER_ID,
+ MailboxColumns.PARENT_SERVER_ID, MailboxColumns.ACCOUNT_KEY, MailboxColumns.TYPE,
+ MailboxColumns.DELIMITER, MailboxColumns.SYNC_KEY, MailboxColumns.SYNC_LOOKBACK,
+ MailboxColumns.SYNC_INTERVAL, MailboxColumns.SYNC_TIME,
+ MailboxColumns.FLAG_VISIBLE, MailboxColumns.FLAGS, MailboxColumns.VISIBLE_LIMIT,
+ MailboxColumns.SYNC_STATUS, MailboxColumns.PARENT_KEY, MailboxColumns.LAST_TOUCHED_TIME,
+ MailboxColumns.UI_SYNC_STATUS, MailboxColumns.UI_LAST_SYNC_RESULT,
+ MailboxColumns.LAST_NOTIFIED_MESSAGE_KEY, MailboxColumns.LAST_NOTIFIED_MESSAGE_COUNT,
+ MailboxColumns.TOTAL_COUNT
+ };
+
+ private static final String ACCOUNT_AND_MAILBOX_TYPE_SELECTION =
+ MailboxColumns.ACCOUNT_KEY + " =? AND " +
+ MailboxColumns.TYPE + " =?";
+ private static final String MAILBOX_TYPE_SELECTION =
+ MailboxColumns.TYPE + " =?";
+ /** Selection by server pathname for a given account */
+ public static final String PATH_AND_ACCOUNT_SELECTION =
+ MailboxColumns.SERVER_ID + "=? and " + MailboxColumns.ACCOUNT_KEY + "=?";
+
+ private static final String[] MAILBOX_SUM_OF_UNREAD_COUNT_PROJECTION = new String [] {
+ "sum(" + MailboxColumns.UNREAD_COUNT + ")"
+ };
+ private static final int UNREAD_COUNT_COUNT_COLUMN = 0;
+ private static final String[] MAILBOX_SUM_OF_MESSAGE_COUNT_PROJECTION = new String [] {
+ "sum(" + MailboxColumns.MESSAGE_COUNT + ")"
+ };
+ private static final int MESSAGE_COUNT_COUNT_COLUMN = 0;
+
+ private static final String[] MAILBOX_TYPE_PROJECTION = new String [] {
+ MailboxColumns.TYPE
+ };
+ private static final int MAILBOX_TYPE_TYPE_COLUMN = 0;
+
+ private static final String[] MAILBOX_DISPLAY_NAME_PROJECTION = new String [] {
+ MailboxColumns.DISPLAY_NAME
+ };
+ private static final int MAILBOX_DISPLAY_NAME_COLUMN = 0;
+
+ public static final long NO_MAILBOX = -1;
+
+ // Sentinel values for the mSyncInterval field of both Mailbox records
+ public static final int CHECK_INTERVAL_NEVER = -1;
+ public static final int CHECK_INTERVAL_PUSH = -2;
+ // The following two sentinel values are used by EAS
+ // Ping indicates that the EAS mailbox is synced based on a "ping" from the server
+ public static final int CHECK_INTERVAL_PING = -3;
+ // Push-Hold indicates an EAS push or ping Mailbox shouldn't sync just yet
+ public static final int CHECK_INTERVAL_PUSH_HOLD = -4;
+
+ // Sentinel for PARENT_KEY. Use NO_MAILBOX for toplevel mailboxes (i.e. no parents).
+ public static final long PARENT_KEY_UNINITIALIZED = 0L;
+
+ private static final String WHERE_TYPE_AND_ACCOUNT_KEY =
+ MailboxColumns.TYPE + "=? and " + MailboxColumns.ACCOUNT_KEY + "=?";
+
+ public static final Integer[] INVALID_DROP_TARGETS = new Integer[] {Mailbox.TYPE_DRAFTS,
+ Mailbox.TYPE_OUTBOX, Mailbox.TYPE_SENT};
+
+ public static final String USER_VISIBLE_MAILBOX_SELECTION =
+ MailboxColumns.TYPE + "<" + Mailbox.TYPE_NOT_EMAIL +
+ " AND " + MailboxColumns.FLAG_VISIBLE + "=1";
+
+ // Types of mailboxes. The list is ordered to match a typical UI presentation, e.g.
+ // placing the inbox at the top.
+ // Arrays of "special_mailbox_display_names" and "special_mailbox_icons" are depends on
+ // types Id of mailboxes.
+ /** No type specified */
+ public static final int TYPE_NONE = -1;
+ /** The "main" mailbox for the account, almost always referred to as "Inbox" */
+ public static final int TYPE_INBOX = 0;
+ // Types of mailboxes
+ /** Generic mailbox that holds mail */
+ public static final int TYPE_MAIL = 1;
+ /** Parent-only mailbox; does not hold any mail */
+ public static final int TYPE_PARENT = 2;
+ /** Drafts mailbox */
+ public static final int TYPE_DRAFTS = 3;
+ /** Local mailbox associated with the account's outgoing mail */
+ public static final int TYPE_OUTBOX = 4;
+ /** Sent mail; mail that was sent from the account */
+ public static final int TYPE_SENT = 5;
+ /** Deleted mail */
+ public static final int TYPE_TRASH = 6;
+ /** Junk mail */
+ public static final int TYPE_JUNK = 7;
+ /** Search results */
+ public static final int TYPE_SEARCH = 8;
+
+ // Types after this are used for non-mail mailboxes (as in EAS)
+ public static final int TYPE_NOT_EMAIL = 0x40;
+ public static final int TYPE_CALENDAR = 0x41;
+ public static final int TYPE_CONTACTS = 0x42;
+ public static final int TYPE_TASKS = 0x43;
+ public static final int TYPE_EAS_ACCOUNT_MAILBOX = 0x44;
+ public static final int TYPE_UNKNOWN = 0x45;
+
+ public static final int TYPE_NOT_SYNCABLE = 0x100;
+ // A mailbox that holds Messages that are attachments
+ public static final int TYPE_ATTACHMENT = 0x101;
+
+ // Default "touch" time for system mailboxes
+ public static final int DRAFTS_DEFAULT_TOUCH_TIME = 2;
+ public static final int SENT_DEFAULT_TOUCH_TIME = 1;
+
+ // Bit field flags; each is defined below
+ // Warning: Do not read these flags until POP/IMAP/EAS all populate them
+ /** No flags set */
+ public static final int FLAG_NONE = 0;
+ /** Has children in the mailbox hierarchy */
+ public static final int FLAG_HAS_CHILDREN = 1<<0;
+ /** Children are visible in the UI */
+ public static final int FLAG_CHILDREN_VISIBLE = 1<<1;
+ /** cannot receive "pushed" mail */
+ public static final int FLAG_CANT_PUSH = 1<<2;
+ /** can hold emails (i.e. some parent mailboxes cannot themselves contain mail) */
+ public static final int FLAG_HOLDS_MAIL = 1<<3;
+ /** can be used as a target for moving messages within the account */
+ public static final int FLAG_ACCEPTS_MOVED_MAIL = 1<<4;
+ /** can be used as a target for appending messages */
+ public static final int FLAG_ACCEPTS_APPENDED_MAIL = 1<<5;
+
+ // Magic mailbox ID's
+ // NOTE: This is a quick solution for merged mailboxes. I would rather implement this
+ // with a more generic way of packaging and sharing queries between activities
+ public static final long QUERY_ALL_INBOXES = -2;
+ public static final long QUERY_ALL_UNREAD = -3;
+ public static final long QUERY_ALL_FAVORITES = -4;
+ public static final long QUERY_ALL_DRAFTS = -5;
+ public static final long QUERY_ALL_OUTBOX = -6;
+
+ public Mailbox() {
+ mBaseUri = CONTENT_URI;
+ }
+
+ /**
+ * Restore a Mailbox from the database, given its unique id
+ * @param context
+ * @param id
+ * @return the instantiated Mailbox
+ */
+ public static Mailbox restoreMailboxWithId(Context context, long id) {
+ return EmailContent.restoreContentWithId(context, Mailbox.class,
+ Mailbox.CONTENT_URI, Mailbox.CONTENT_PROJECTION, id);
+ }
+
+ /**
+ * Builds a new mailbox with "typical" settings for a system mailbox, such as a local "Drafts"
+ * mailbox. This is useful for protocols like POP3 or IMAP who don't have certain local
+ * system mailboxes synced with the server.
+ * Note: the mailbox is not persisted - clients must call {@link #save} themselves.
+ */
+ public static Mailbox newSystemMailbox(long accountId, int mailboxType, String name) {
+ if (mailboxType == Mailbox.TYPE_MAIL) {
+ throw new IllegalArgumentException("Cannot specify TYPE_MAIL for a system mailbox");
+ }
+ Mailbox box = new Mailbox();
+ box.mAccountKey = accountId;
+ box.mType = mailboxType;
+ box.mSyncInterval = Account.CHECK_INTERVAL_NEVER;
+ box.mFlagVisible = true;
+ box.mServerId = box.mDisplayName = name;
+ box.mParentKey = Mailbox.NO_MAILBOX;
+ box.mFlags = Mailbox.FLAG_HOLDS_MAIL;
+ return box;
+ }
+
+ /**
+ * Returns a Mailbox from the database, given its pathname and account id. All mailbox
+ * paths for a particular account must be unique. Paths are stored in the column
+ * {@link MailboxColumns#SERVER_ID} for want of yet another column in the table.
+ * @param context
+ * @param accountId the ID of the account
+ * @param path the fully qualified, remote pathname
+ */
+ public static Mailbox restoreMailboxForPath(Context context, long accountId, String path) {
+ Cursor c = context.getContentResolver().query(
+ Mailbox.CONTENT_URI,
+ Mailbox.CONTENT_PROJECTION,
+ Mailbox.PATH_AND_ACCOUNT_SELECTION,
+ new String[] { path, Long.toString(accountId) },
+ null);
+ if (c == null) throw new ProviderUnavailableException();
+ try {
+ Mailbox mailbox = null;
+ if (c.moveToFirst()) {
+ mailbox = getContent(c, Mailbox.class);
+ if (c.moveToNext()) {
+ Log.w(Logging.LOG_TAG, "Multiple mailboxes named \"" + path + "\"");
+ }
+ } else {
+ Log.i(Logging.LOG_TAG, "Could not find mailbox at \"" + path + "\"");
+ }
+ return mailbox;
+ } finally {
+ c.close();
+ }
+ }
+
+ /**
+ * Returns a {@link Mailbox} for the given path. If the path is not in the database, a new
+ * mailbox will be created.
+ */
+ public static Mailbox getMailboxForPath(Context context, long accountId, String path) {
+ Mailbox mailbox = restoreMailboxForPath(context, accountId, path);
+ if (mailbox == null) {
+ mailbox = new Mailbox();
+ }
+ return mailbox;
+ }
+
+ @Override
+ public void restore(Cursor cursor) {
+ mBaseUri = CONTENT_URI;
+ mId = cursor.getLong(CONTENT_ID_COLUMN);
+ mDisplayName = cursor.getString(CONTENT_DISPLAY_NAME_COLUMN);
+ mServerId = cursor.getString(CONTENT_SERVER_ID_COLUMN);
+ mParentServerId = cursor.getString(CONTENT_PARENT_SERVER_ID_COLUMN);
+ mParentKey = cursor.getLong(CONTENT_PARENT_KEY_COLUMN);
+ mAccountKey = cursor.getLong(CONTENT_ACCOUNT_KEY_COLUMN);
+ mType = cursor.getInt(CONTENT_TYPE_COLUMN);
+ mDelimiter = cursor.getInt(CONTENT_DELIMITER_COLUMN);
+ mSyncKey = cursor.getString(CONTENT_SYNC_KEY_COLUMN);
+ mSyncLookback = cursor.getInt(CONTENT_SYNC_LOOKBACK_COLUMN);
+ mSyncInterval = cursor.getInt(CONTENT_SYNC_INTERVAL_COLUMN);
+ mSyncTime = cursor.getLong(CONTENT_SYNC_TIME_COLUMN);
+ mFlagVisible = cursor.getInt(CONTENT_FLAG_VISIBLE_COLUMN) == 1;
+ mFlags = cursor.getInt(CONTENT_FLAGS_COLUMN);
+ mVisibleLimit = cursor.getInt(CONTENT_VISIBLE_LIMIT_COLUMN);
+ mSyncStatus = cursor.getString(CONTENT_SYNC_STATUS_COLUMN);
+ mLastTouchedTime = cursor.getLong(CONTENT_LAST_TOUCHED_TIME_COLUMN);
+ mUiSyncStatus = cursor.getInt(CONTENT_UI_SYNC_STATUS_COLUMN);
+ mUiLastSyncResult = cursor.getInt(CONTENT_UI_LAST_SYNC_RESULT_COLUMN);
+ mLastNotifiedMessageKey = cursor.getLong(CONTENT_LAST_NOTIFIED_MESSAGE_KEY_COLUMN);
+ mLastNotifiedMessageCount = cursor.getInt(CONTENT_LAST_NOTIFIED_MESSAGE_COUNT_COLUMN);
+ mTotalCount = cursor.getInt(CONTENT_TOTAL_COUNT_COLUMN);
+ }
+
+ @Override
+ public ContentValues toContentValues() {
+ ContentValues values = new ContentValues();
+ values.put(MailboxColumns.DISPLAY_NAME, mDisplayName);
+ values.put(MailboxColumns.SERVER_ID, mServerId);
+ values.put(MailboxColumns.PARENT_SERVER_ID, mParentServerId);
+ values.put(MailboxColumns.PARENT_KEY, mParentKey);
+ values.put(MailboxColumns.ACCOUNT_KEY, mAccountKey);
+ values.put(MailboxColumns.TYPE, mType);
+ values.put(MailboxColumns.DELIMITER, mDelimiter);
+ values.put(MailboxColumns.SYNC_KEY, mSyncKey);
+ values.put(MailboxColumns.SYNC_LOOKBACK, mSyncLookback);
+ values.put(MailboxColumns.SYNC_INTERVAL, mSyncInterval);
+ values.put(MailboxColumns.SYNC_TIME, mSyncTime);
+ values.put(MailboxColumns.FLAG_VISIBLE, mFlagVisible);
+ values.put(MailboxColumns.FLAGS, mFlags);
+ values.put(MailboxColumns.VISIBLE_LIMIT, mVisibleLimit);
+ values.put(MailboxColumns.SYNC_STATUS, mSyncStatus);
+ values.put(MailboxColumns.LAST_TOUCHED_TIME, mLastTouchedTime);
+ values.put(MailboxColumns.UI_SYNC_STATUS, mUiSyncStatus);
+ values.put(MailboxColumns.UI_LAST_SYNC_RESULT, mUiLastSyncResult);
+ values.put(MailboxColumns.LAST_NOTIFIED_MESSAGE_KEY, mLastNotifiedMessageKey);
+ values.put(MailboxColumns.LAST_NOTIFIED_MESSAGE_COUNT, mLastNotifiedMessageCount);
+ values.put(MailboxColumns.TOTAL_COUNT, mTotalCount);
+ return values;
+ }
+
+ /**
+ * Convenience method to return the id of a given type of Mailbox for a given Account; the
+ * common Mailbox types (Inbox, Outbox, Sent, Drafts, Trash, and Search) are all cached by
+ * EmailProvider; therefore, we warn if the mailbox is not found in the cache
+ *
+ * @param context the caller's context, used to get a ContentResolver
+ * @param accountId the id of the account to be queried
+ * @param type the mailbox type, as defined above
+ * @return the id of the mailbox, or -1 if not found
+ */
+ public static long findMailboxOfType(Context context, long accountId, int type) {
+ // First use special URI
+ Uri uri = FROM_ACCOUNT_AND_TYPE_URI.buildUpon().appendPath(Long.toString(accountId))
+ .appendPath(Integer.toString(type)).build();
+ Cursor c = context.getContentResolver().query(uri, ID_PROJECTION, null, null, null);
+ if (c != null) {
+ try {
+ c.moveToFirst();
+ Long mailboxId = c.getLong(ID_PROJECTION_COLUMN);
+ if (mailboxId != null
+ && mailboxId != 0L
+ && mailboxId != NO_MAILBOX) {
+ return mailboxId;
+ }
+ } finally {
+ c.close();
+ }
+ }
+ // Fallback to querying the database directly.
+ String[] bindArguments = new String[] {Long.toString(type), Long.toString(accountId)};
+ return Utility.getFirstRowLong(context, Mailbox.CONTENT_URI,
+ ID_PROJECTION, WHERE_TYPE_AND_ACCOUNT_KEY, bindArguments, null,
+ ID_PROJECTION_COLUMN, NO_MAILBOX);
+ }
+
+ /**
+ * Convenience method that returns the mailbox found using the method above
+ */
+ public static Mailbox restoreMailboxOfType(Context context, long accountId, int type) {
+ long mailboxId = findMailboxOfType(context, accountId, type);
+ if (mailboxId != Mailbox.NO_MAILBOX) {
+ return Mailbox.restoreMailboxWithId(context, mailboxId);
+ }
+ return null;
+ }
+
+ public static int getUnreadCountByAccountAndMailboxType(Context context, long accountId,
+ int type) {
+ return Utility.getFirstRowInt(context, Mailbox.CONTENT_URI,
+ MAILBOX_SUM_OF_UNREAD_COUNT_PROJECTION,
+ ACCOUNT_AND_MAILBOX_TYPE_SELECTION,
+ new String[] { String.valueOf(accountId), String.valueOf(type) },
+ null, UNREAD_COUNT_COUNT_COLUMN, 0);
+ }
+
+ public static int getUnreadCountByMailboxType(Context context, int type) {
+ return Utility.getFirstRowInt(context, Mailbox.CONTENT_URI,
+ MAILBOX_SUM_OF_UNREAD_COUNT_PROJECTION,
+ MAILBOX_TYPE_SELECTION,
+ new String[] { String.valueOf(type) }, null, UNREAD_COUNT_COUNT_COLUMN, 0);
+ }
+
+ public static int getMessageCountByMailboxType(Context context, int type) {
+ return Utility.getFirstRowInt(context, Mailbox.CONTENT_URI,
+ MAILBOX_SUM_OF_MESSAGE_COUNT_PROJECTION,
+ MAILBOX_TYPE_SELECTION,
+ new String[] { String.valueOf(type) }, null, MESSAGE_COUNT_COUNT_COLUMN, 0);
+ }
+
+ /**
+ * Return the mailbox for a message with a given id
+ * @param context the caller's context
+ * @param messageId the id of the message
+ * @return the mailbox, or null if the mailbox doesn't exist
+ */
+ public static Mailbox getMailboxForMessageId(Context context, long messageId) {
+ long mailboxId = Message.getKeyColumnLong(context, messageId,
+ MessageColumns.MAILBOX_KEY);
+ if (mailboxId != -1) {
+ return Mailbox.restoreMailboxWithId(context, mailboxId);
+ }
+ return null;
+ }
+
+ /**
+ * @return mailbox type, or -1 if mailbox not found.
+ */
+ public static int getMailboxType(Context context, long mailboxId) {
+ Uri url = ContentUris.withAppendedId(Mailbox.CONTENT_URI, mailboxId);
+ return Utility.getFirstRowInt(context, url, MAILBOX_TYPE_PROJECTION,
+ null, null, null, MAILBOX_TYPE_TYPE_COLUMN, -1);
+ }
+
+ /**
+ * @return mailbox display name, or null if mailbox not found.
+ */
+ public static String getDisplayName(Context context, long mailboxId) {
+ Uri url = ContentUris.withAppendedId(Mailbox.CONTENT_URI, mailboxId);
+ return Utility.getFirstRowString(context, url, MAILBOX_DISPLAY_NAME_PROJECTION,
+ null, null, null, MAILBOX_DISPLAY_NAME_COLUMN);
+ }
+
+ /**
+ * @param mailboxId ID of a mailbox. This method accepts magic mailbox IDs, such as
+ * {@link #QUERY_ALL_INBOXES}. (They're all non-refreshable.)
+ * @return true if a mailbox is refreshable.
+ */
+ public static boolean isRefreshable(Context context, long mailboxId) {
+ if (mailboxId < 0) {
+ return false; // magic mailboxes
+ }
+ switch (getMailboxType(context, mailboxId)) {
+ case -1: // not found
+ case TYPE_DRAFTS:
+ case TYPE_OUTBOX:
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * @return whether or not this mailbox supports moving messages out of it
+ */
+ public boolean canHaveMessagesMoved() {
+ switch (mType) {
+ case TYPE_INBOX:
+ case TYPE_MAIL:
+ case TYPE_TRASH:
+ case TYPE_JUNK:
+ return true;
+ }
+ return false; // TYPE_DRAFTS, TYPE_OUTBOX, TYPE_SENT, etc
+ }
+
+ /**
+ * @return whether or not this mailbox retrieves its data from the server (as opposed to just
+ * a local mailbox that is never synced).
+ */
+ public boolean loadsFromServer(String protocol) {
+ if (HostAuth.SCHEME_EAS.equals(protocol)) {
+ return mType != Mailbox.TYPE_DRAFTS
+ && mType != Mailbox.TYPE_OUTBOX
+ && mType != Mailbox.TYPE_SEARCH
+ && mType < Mailbox.TYPE_NOT_SYNCABLE;
+
+ } else if (HostAuth.SCHEME_IMAP.equals(protocol)) {
+ // TODO: actually use a sync flag when creating the mailboxes. Right now we use an
+ // approximation for IMAP.
+ return mType != Mailbox.TYPE_DRAFTS
+ && mType != Mailbox.TYPE_OUTBOX
+ && mType != Mailbox.TYPE_SEARCH;
+
+ } else if (HostAuth.SCHEME_POP3.equals(protocol)) {
+ return TYPE_INBOX == mType;
+ }
+
+ return false;
+ }
+
+ /**
+ * @return true if messages in a mailbox of a type can be replied/forwarded.
+ */
+ public static boolean isMailboxTypeReplyAndForwardable(int type) {
+ return (type != TYPE_TRASH) && (type != TYPE_DRAFTS);
+ }
+
+ /**
+ * Returns a set of hashes that can identify this mailbox. These can be used to
+ * determine if any of the fields have been modified.
+ */
+ public Object[] getHashes() {
+ Object[] hash = new Object[CONTENT_PROJECTION.length];
+
+ hash[CONTENT_ID_COLUMN]
+ = mId;
+ hash[CONTENT_DISPLAY_NAME_COLUMN]
+ = mDisplayName;
+ hash[CONTENT_SERVER_ID_COLUMN]
+ = mServerId;
+ hash[CONTENT_PARENT_SERVER_ID_COLUMN]
+ = mParentServerId;
+ hash[CONTENT_ACCOUNT_KEY_COLUMN]
+ = mAccountKey;
+ hash[CONTENT_TYPE_COLUMN]
+ = mType;
+ hash[CONTENT_DELIMITER_COLUMN]
+ = mDelimiter;
+ hash[CONTENT_SYNC_KEY_COLUMN]
+ = mSyncKey;
+ hash[CONTENT_SYNC_LOOKBACK_COLUMN]
+ = mSyncLookback;
+ hash[CONTENT_SYNC_INTERVAL_COLUMN]
+ = mSyncInterval;
+ hash[CONTENT_SYNC_TIME_COLUMN]
+ = mSyncTime;
+ hash[CONTENT_FLAG_VISIBLE_COLUMN]
+ = mFlagVisible;
+ hash[CONTENT_FLAGS_COLUMN]
+ = mFlags;
+ hash[CONTENT_VISIBLE_LIMIT_COLUMN]
+ = mVisibleLimit;
+ hash[CONTENT_SYNC_STATUS_COLUMN]
+ = mSyncStatus;
+ hash[CONTENT_PARENT_KEY_COLUMN]
+ = mParentKey;
+ hash[CONTENT_LAST_TOUCHED_TIME_COLUMN]
+ = mLastTouchedTime;
+ hash[CONTENT_UI_SYNC_STATUS_COLUMN]
+ = mUiSyncStatus;
+ hash[CONTENT_UI_LAST_SYNC_RESULT_COLUMN]
+ = mUiLastSyncResult;
+ hash[CONTENT_LAST_NOTIFIED_MESSAGE_KEY_COLUMN]
+ = mLastNotifiedMessageKey;
+ hash[CONTENT_LAST_NOTIFIED_MESSAGE_COUNT_COLUMN]
+ = mLastNotifiedMessageCount;
+ hash[CONTENT_TOTAL_COUNT_COLUMN]
+ = mTotalCount;
+ return hash;
+ }
+
+ // Parcelable
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ // Parcelable
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeParcelable(mBaseUri, flags);
+ dest.writeLong(mId);
+ dest.writeString(mDisplayName);
+ dest.writeString(mServerId);
+ dest.writeString(mParentServerId);
+ dest.writeLong(mParentKey);
+ dest.writeLong(mAccountKey);
+ dest.writeInt(mType);
+ dest.writeInt(mDelimiter);
+ dest.writeString(mSyncKey);
+ dest.writeInt(mSyncLookback);
+ dest.writeInt(mSyncInterval);
+ dest.writeLong(mSyncTime);
+ dest.writeInt(mFlagVisible ? 1 : 0);
+ dest.writeInt(mFlags);
+ dest.writeInt(mVisibleLimit);
+ dest.writeString(mSyncStatus);
+ dest.writeLong(mLastTouchedTime);
+ dest.writeInt(mUiSyncStatus);
+ dest.writeInt(mUiLastSyncResult);
+ dest.writeLong(mLastNotifiedMessageKey);
+ dest.writeInt(mLastNotifiedMessageCount);
+ dest.writeInt(mTotalCount);
+ }
+
+ public Mailbox(Parcel in) {
+ mBaseUri = in.readParcelable(null);
+ mId = in.readLong();
+ mDisplayName = in.readString();
+ mServerId = in.readString();
+ mParentServerId = in.readString();
+ mParentKey = in.readLong();
+ mAccountKey = in.readLong();
+ mType = in.readInt();
+ mDelimiter = in.readInt();
+ mSyncKey = in.readString();
+ mSyncLookback = in.readInt();
+ mSyncInterval = in.readInt();
+ mSyncTime = in.readLong();
+ mFlagVisible = in.readInt() == 1;
+ mFlags = in.readInt();
+ mVisibleLimit = in.readInt();
+ mSyncStatus = in.readString();
+ mLastTouchedTime = in.readLong();
+ mUiSyncStatus = in.readInt();
+ mUiLastSyncResult = in.readInt();
+ mLastNotifiedMessageKey = in.readLong();
+ mLastNotifiedMessageCount = in.readInt();
+ mTotalCount = in.readInt();
+ }
+
+ public static final Parcelable.Creator<Mailbox> CREATOR = new Parcelable.Creator<Mailbox>() {
+ @Override
+ public Mailbox createFromParcel(Parcel source) {
+ return new Mailbox(source);
+ }
+
+ @Override
+ public Mailbox[] newArray(int size) {
+ return new Mailbox[size];
+ }
+ };
+
+ public String toString() {
+ return "[Mailbox " + mId + ": " + mDisplayName + "]";
+ }
+}
diff --git a/email2/emailcommon/src/com/android/emailcommon/provider/Policy.aidl b/email2/emailcommon/src/com/android/emailcommon/provider/Policy.aidl
new file mode 100644
index 0000000..02be51b
--- /dev/null
+++ b/email2/emailcommon/src/com/android/emailcommon/provider/Policy.aidl
@@ -0,0 +1,19 @@
+/* Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.emailcommon.provider;
+
+parcelable Policy;
+
diff --git a/email2/emailcommon/src/com/android/emailcommon/provider/Policy.java b/email2/emailcommon/src/com/android/emailcommon/provider/Policy.java
new file mode 100755
index 0000000..d43290f
--- /dev/null
+++ b/email2/emailcommon/src/com/android/emailcommon/provider/Policy.java
@@ -0,0 +1,513 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.emailcommon.provider;
+import android.app.admin.DevicePolicyManager;
+import android.content.ContentResolver;
+import android.content.ContentUris;
+import android.content.ContentValues;
+import android.content.Context;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import com.android.emailcommon.utility.TextUtilities;
+import com.android.emailcommon.utility.Utility;
+
+import java.util.ArrayList;
+
+/**
+ * The Policy class represents a set of security requirements that are associated with an Account.
+ * The requirements may be either device-specific (e.g. password) or application-specific (e.g.
+ * a limit on the sync window for the Account)
+ */
+public final class Policy extends EmailContent implements EmailContent.PolicyColumns, Parcelable {
+ public static final boolean DEBUG_POLICY = false; // DO NOT SUBMIT WITH THIS SET TO TRUE
+ public static final String TAG = "Email/Policy";
+
+ public static final String TABLE_NAME = "Policy";
+ @SuppressWarnings("hiding")
+ public static final Uri CONTENT_URI = Uri.parse(EmailContent.CONTENT_URI + "/policy");
+
+ /* Convert days to mSec (used for password expiration) */
+ private static final long DAYS_TO_MSEC = 24 * 60 * 60 * 1000;
+ /* Small offset (2 minutes) added to policy expiration to make user testing easier. */
+ private static final long EXPIRATION_OFFSET_MSEC = 2 * 60 * 1000;
+
+ public static final int PASSWORD_MODE_NONE = 0;
+ public static final int PASSWORD_MODE_SIMPLE = 1;
+ public static final int PASSWORD_MODE_STRONG = 2;
+
+ public static final char POLICY_STRING_DELIMITER = '\1';
+
+ public int mPasswordMode;
+ public int mPasswordMinLength;
+ public int mPasswordMaxFails;
+ public int mPasswordExpirationDays;
+ public int mPasswordHistory;
+ public int mPasswordComplexChars;
+ public int mMaxScreenLockTime;
+ public boolean mRequireRemoteWipe;
+ public boolean mRequireEncryption;
+ public boolean mRequireEncryptionExternal;
+ public boolean mRequireManualSyncWhenRoaming;
+ public boolean mDontAllowCamera;
+ public boolean mDontAllowAttachments;
+ public boolean mDontAllowHtml;
+ public int mMaxAttachmentSize;
+ public int mMaxTextTruncationSize;
+ public int mMaxHtmlTruncationSize;
+ public int mMaxEmailLookback;
+ public int mMaxCalendarLookback;
+ public boolean mPasswordRecoveryEnabled;
+ public String mProtocolPoliciesEnforced;
+ public String mProtocolPoliciesUnsupported;
+
+ public static final int CONTENT_ID_COLUMN = 0;
+ public static final int CONTENT_PASSWORD_MODE_COLUMN = 1;
+ public static final int CONTENT_PASSWORD_MIN_LENGTH_COLUMN = 2;
+ public static final int CONTENT_PASSWORD_EXPIRATION_DAYS_COLUMN = 3;
+ public static final int CONTENT_PASSWORD_HISTORY_COLUMN = 4;
+ public static final int CONTENT_PASSWORD_COMPLEX_CHARS_COLUMN = 5;
+ public static final int CONTENT_PASSWORD_MAX_FAILS_COLUMN = 6;
+ public static final int CONTENT_MAX_SCREEN_LOCK_TIME_COLUMN = 7;
+ public static final int CONTENT_REQUIRE_REMOTE_WIPE_COLUMN = 8;
+ public static final int CONTENT_REQUIRE_ENCRYPTION_COLUMN = 9;
+ public static final int CONTENT_REQUIRE_ENCRYPTION_EXTERNAL_COLUMN = 10;
+ public static final int CONTENT_REQUIRE_MANUAL_SYNC_WHEN_ROAMING = 11;
+ public static final int CONTENT_DONT_ALLOW_CAMERA_COLUMN = 12;
+ public static final int CONTENT_DONT_ALLOW_ATTACHMENTS_COLUMN = 13;
+ public static final int CONTENT_DONT_ALLOW_HTML_COLUMN = 14;
+ public static final int CONTENT_MAX_ATTACHMENT_SIZE_COLUMN = 15;
+ public static final int CONTENT_MAX_TEXT_TRUNCATION_SIZE_COLUMN = 16;
+ public static final int CONTENT_MAX_HTML_TRUNCATION_SIZE_COLUMN = 17;
+ public static final int CONTENT_MAX_EMAIL_LOOKBACK_COLUMN = 18;
+ public static final int CONTENT_MAX_CALENDAR_LOOKBACK_COLUMN = 19;
+ public static final int CONTENT_PASSWORD_RECOVERY_ENABLED_COLUMN = 20;
+ public static final int CONTENT_PROTOCOL_POLICIES_ENFORCED_COLUMN = 21;
+ public static final int CONTENT_PROTOCOL_POLICIES_UNSUPPORTED_COLUMN = 22;
+
+ public static final String[] CONTENT_PROJECTION = new String[] {RECORD_ID,
+ PolicyColumns.PASSWORD_MODE, PolicyColumns.PASSWORD_MIN_LENGTH,
+ PolicyColumns.PASSWORD_EXPIRATION_DAYS, PolicyColumns.PASSWORD_HISTORY,
+ PolicyColumns.PASSWORD_COMPLEX_CHARS, PolicyColumns.PASSWORD_MAX_FAILS,
+ PolicyColumns.MAX_SCREEN_LOCK_TIME, PolicyColumns.REQUIRE_REMOTE_WIPE,
+ PolicyColumns.REQUIRE_ENCRYPTION, PolicyColumns.REQUIRE_ENCRYPTION_EXTERNAL,
+ PolicyColumns.REQUIRE_MANUAL_SYNC_WHEN_ROAMING, PolicyColumns.DONT_ALLOW_CAMERA,
+ PolicyColumns.DONT_ALLOW_ATTACHMENTS, PolicyColumns.DONT_ALLOW_HTML,
+ PolicyColumns.MAX_ATTACHMENT_SIZE, PolicyColumns.MAX_TEXT_TRUNCATION_SIZE,
+ PolicyColumns.MAX_HTML_TRUNCATION_SIZE, PolicyColumns.MAX_EMAIL_LOOKBACK,
+ PolicyColumns.MAX_CALENDAR_LOOKBACK, PolicyColumns.PASSWORD_RECOVERY_ENABLED,
+ PolicyColumns.PROTOCOL_POLICIES_ENFORCED, PolicyColumns.PROTOCOL_POLICIES_UNSUPPORTED
+ };
+
+ public static final Policy NO_POLICY = new Policy();
+
+ private static final String[] ATTACHMENT_RESET_PROJECTION =
+ new String[] {EmailContent.RECORD_ID, AttachmentColumns.SIZE, AttachmentColumns.FLAGS};
+ private static final int ATTACHMENT_RESET_PROJECTION_ID = 0;
+ private static final int ATTACHMENT_RESET_PROJECTION_SIZE = 1;
+ private static final int ATTACHMENT_RESET_PROJECTION_FLAGS = 2;
+
+ public Policy() {
+ mBaseUri = CONTENT_URI;
+ // By default, the password mode is "none"
+ mPasswordMode = PASSWORD_MODE_NONE;
+ // All server policies require the ability to wipe the device
+ mRequireRemoteWipe = true;
+ }
+
+ public static Policy restorePolicyWithId(Context context, long id) {
+ return EmailContent.restoreContentWithId(context, Policy.class, Policy.CONTENT_URI,
+ Policy.CONTENT_PROJECTION, id);
+ }
+
+ public static long getAccountIdWithPolicyKey(Context context, long id) {
+ return Utility.getFirstRowLong(context, Account.CONTENT_URI, Account.ID_PROJECTION,
+ AccountColumns.POLICY_KEY + "=?", new String[] {Long.toString(id)}, null,
+ Account.ID_PROJECTION_COLUMN, Account.NO_ACCOUNT);
+ }
+
+ public static ArrayList<String> addPolicyStringToList(String policyString,
+ ArrayList<String> policyList) {
+ if (policyString != null) {
+ int start = 0;
+ int len = policyString.length();
+ while(start < len) {
+ int end = policyString.indexOf(POLICY_STRING_DELIMITER, start);
+ if (end > start) {
+ policyList.add(policyString.substring(start, end));
+ start = end + 1;
+ } else {
+ break;
+ }
+ }
+ }
+ return policyList;
+ }
+
+ // We override this method to insure that we never write invalid policy data to the provider
+ @Override
+ public Uri save(Context context) {
+ normalize();
+ return super.save(context);
+ }
+
+ /**
+ * Review all attachment records for this account, and reset the "don't allow download" flag
+ * as required by the account's new security policies
+ * @param context the caller's context
+ * @param account the account whose attachments need to be reviewed
+ * @param policy the new policy for this account
+ */
+ public static void setAttachmentFlagsForNewPolicy(Context context, Account account,
+ Policy policy) {
+ // A nasty bit of work; start with all attachments for a given account
+ ContentResolver resolver = context.getContentResolver();
+ Cursor c = resolver.query(Attachment.CONTENT_URI, ATTACHMENT_RESET_PROJECTION,
+ AttachmentColumns.ACCOUNT_KEY + "=?", new String[] {Long.toString(account.mId)},
+ null);
+ ContentValues cv = new ContentValues();
+ try {
+ // Get maximum allowed size (0 if we don't allow attachments at all)
+ int policyMax = policy.mDontAllowAttachments ? 0 : (policy.mMaxAttachmentSize > 0) ?
+ policy.mMaxAttachmentSize : Integer.MAX_VALUE;
+ while (c.moveToNext()) {
+ int flags = c.getInt(ATTACHMENT_RESET_PROJECTION_FLAGS);
+ int size = c.getInt(ATTACHMENT_RESET_PROJECTION_SIZE);
+ boolean wasRestricted = (flags & Attachment.FLAG_POLICY_DISALLOWS_DOWNLOAD) != 0;
+ boolean isRestricted = size > policyMax;
+ if (isRestricted != wasRestricted) {
+ if (isRestricted) {
+ flags |= Attachment.FLAG_POLICY_DISALLOWS_DOWNLOAD;
+ } else {
+ flags &= ~Attachment.FLAG_POLICY_DISALLOWS_DOWNLOAD;
+ }
+ long id = c.getLong(ATTACHMENT_RESET_PROJECTION_ID);
+ cv.put(AttachmentColumns.FLAGS, flags);
+ resolver.update(ContentUris.withAppendedId(Attachment.CONTENT_URI, id),
+ cv, null, null);
+ }
+ }
+ } finally {
+ c.close();
+ }
+ }
+
+ /**
+ * Normalize the Policy. If the password mode is "none", zero out all password-related fields;
+ * zero out complex characters for simple passwords.
+ */
+ public void normalize() {
+ if (mPasswordMode == PASSWORD_MODE_NONE) {
+ mPasswordMaxFails = 0;
+ mMaxScreenLockTime = 0;
+ mPasswordMinLength = 0;
+ mPasswordComplexChars = 0;
+ mPasswordHistory = 0;
+ mPasswordExpirationDays = 0;
+ } else {
+ if ((mPasswordMode != PASSWORD_MODE_SIMPLE) &&
+ (mPasswordMode != PASSWORD_MODE_STRONG)) {
+ throw new IllegalArgumentException("password mode");
+ }
+ // If we're only requiring a simple password, set complex chars to zero; note
+ // that EAS can erroneously send non-zero values in this case
+ if (mPasswordMode == PASSWORD_MODE_SIMPLE) {
+ mPasswordComplexChars = 0;
+ }
+ }
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ if (!(other instanceof Policy)) return false;
+ Policy otherPolicy = (Policy)other;
+ // Policies here are enforced by the DPM
+ if (mRequireEncryption != otherPolicy.mRequireEncryption) return false;
+ if (mRequireEncryptionExternal != otherPolicy.mRequireEncryptionExternal) return false;
+ if (mRequireRemoteWipe != otherPolicy.mRequireRemoteWipe) return false;
+ if (mMaxScreenLockTime != otherPolicy.mMaxScreenLockTime) return false;
+ if (mPasswordComplexChars != otherPolicy.mPasswordComplexChars) return false;
+ if (mPasswordExpirationDays != otherPolicy.mPasswordExpirationDays) return false;
+ if (mPasswordHistory != otherPolicy.mPasswordHistory) return false;
+ if (mPasswordMaxFails != otherPolicy.mPasswordMaxFails) return false;
+ if (mPasswordMinLength != otherPolicy.mPasswordMinLength) return false;
+ if (mPasswordMode != otherPolicy.mPasswordMode) return false;
+ if (mDontAllowCamera != otherPolicy.mDontAllowCamera) return false;
+
+ // Policies here are enforced by the Exchange sync manager
+ // They should eventually be removed from Policy and replaced with some opaque data
+ if (mRequireManualSyncWhenRoaming != otherPolicy.mRequireManualSyncWhenRoaming) {
+ return false;
+ }
+ if (mDontAllowAttachments != otherPolicy.mDontAllowAttachments) return false;
+ if (mDontAllowHtml != otherPolicy.mDontAllowHtml) return false;
+ if (mMaxAttachmentSize != otherPolicy.mMaxAttachmentSize) return false;
+ if (mMaxTextTruncationSize != otherPolicy.mMaxTextTruncationSize) return false;
+ if (mMaxHtmlTruncationSize != otherPolicy.mMaxHtmlTruncationSize) return false;
+ if (mMaxEmailLookback != otherPolicy.mMaxEmailLookback) return false;
+ if (mMaxCalendarLookback != otherPolicy.mMaxCalendarLookback) return false;
+ if (mPasswordRecoveryEnabled != otherPolicy.mPasswordRecoveryEnabled) return false;
+
+ if (!TextUtilities.stringOrNullEquals(mProtocolPoliciesEnforced,
+ otherPolicy.mProtocolPoliciesEnforced)) {
+ return false;
+ }
+ if (!TextUtilities.stringOrNullEquals(mProtocolPoliciesUnsupported,
+ otherPolicy.mProtocolPoliciesUnsupported)) {
+ return false;
+ }
+ return true;
+ }
+
+ @Override
+ public int hashCode() {
+ int code = mRequireEncryption ? 1 : 0;
+ code += (mRequireEncryptionExternal ? 1 : 0) << 1;
+ code += (mRequireRemoteWipe ? 1 : 0) << 2;
+ code += (mMaxScreenLockTime << 3);
+ code += (mPasswordComplexChars << 6);
+ code += (mPasswordExpirationDays << 12);
+ code += (mPasswordHistory << 15);
+ code += (mPasswordMaxFails << 18);
+ code += (mPasswordMinLength << 22);
+ code += (mPasswordMode << 26);
+ // Don't need to include the other fields
+ return code;
+ }
+
+ @Override
+ public void restore(Cursor cursor) {
+ mBaseUri = CONTENT_URI;
+ mId = cursor.getLong(CONTENT_ID_COLUMN);
+ mPasswordMode = cursor.getInt(CONTENT_PASSWORD_MODE_COLUMN);
+ mPasswordMinLength = cursor.getInt(CONTENT_PASSWORD_MIN_LENGTH_COLUMN);
+ mPasswordMaxFails = cursor.getInt(CONTENT_PASSWORD_MAX_FAILS_COLUMN);
+ mPasswordHistory = cursor.getInt(CONTENT_PASSWORD_HISTORY_COLUMN);
+ mPasswordExpirationDays = cursor.getInt(CONTENT_PASSWORD_EXPIRATION_DAYS_COLUMN);
+ mPasswordComplexChars = cursor.getInt(CONTENT_PASSWORD_COMPLEX_CHARS_COLUMN);
+ mMaxScreenLockTime = cursor.getInt(CONTENT_MAX_SCREEN_LOCK_TIME_COLUMN);
+ mRequireRemoteWipe = cursor.getInt(CONTENT_REQUIRE_REMOTE_WIPE_COLUMN) == 1;
+ mRequireEncryption = cursor.getInt(CONTENT_REQUIRE_ENCRYPTION_COLUMN) == 1;
+ mRequireEncryptionExternal =
+ cursor.getInt(CONTENT_REQUIRE_ENCRYPTION_EXTERNAL_COLUMN) == 1;
+ mRequireManualSyncWhenRoaming =
+ cursor.getInt(CONTENT_REQUIRE_MANUAL_SYNC_WHEN_ROAMING) == 1;
+ mDontAllowCamera = cursor.getInt(CONTENT_DONT_ALLOW_CAMERA_COLUMN) == 1;
+ mDontAllowAttachments = cursor.getInt(CONTENT_DONT_ALLOW_ATTACHMENTS_COLUMN) == 1;
+ mDontAllowHtml = cursor.getInt(CONTENT_DONT_ALLOW_HTML_COLUMN) == 1;
+ mMaxAttachmentSize = cursor.getInt(CONTENT_MAX_ATTACHMENT_SIZE_COLUMN);
+ mMaxTextTruncationSize = cursor.getInt(CONTENT_MAX_TEXT_TRUNCATION_SIZE_COLUMN);
+ mMaxHtmlTruncationSize = cursor.getInt(CONTENT_MAX_HTML_TRUNCATION_SIZE_COLUMN);
+ mMaxEmailLookback = cursor.getInt(CONTENT_MAX_EMAIL_LOOKBACK_COLUMN);
+ mMaxCalendarLookback = cursor.getInt(CONTENT_MAX_CALENDAR_LOOKBACK_COLUMN);
+ mPasswordRecoveryEnabled = cursor.getInt(CONTENT_PASSWORD_RECOVERY_ENABLED_COLUMN) == 1;
+ mProtocolPoliciesEnforced = cursor.getString(CONTENT_PROTOCOL_POLICIES_ENFORCED_COLUMN);
+ mProtocolPoliciesUnsupported =
+ cursor.getString(CONTENT_PROTOCOL_POLICIES_UNSUPPORTED_COLUMN);
+ }
+
+ @Override
+ public ContentValues toContentValues() {
+ ContentValues values = new ContentValues();
+ values.put(PolicyColumns.PASSWORD_MODE, mPasswordMode);
+ values.put(PolicyColumns.PASSWORD_MIN_LENGTH, mPasswordMinLength);
+ values.put(PolicyColumns.PASSWORD_MAX_FAILS, mPasswordMaxFails);
+ values.put(PolicyColumns.PASSWORD_HISTORY, mPasswordHistory);
+ values.put(PolicyColumns.PASSWORD_EXPIRATION_DAYS, mPasswordExpirationDays);
+ values.put(PolicyColumns.PASSWORD_COMPLEX_CHARS, mPasswordComplexChars);
+ values.put(PolicyColumns.MAX_SCREEN_LOCK_TIME, mMaxScreenLockTime);
+ values.put(PolicyColumns.REQUIRE_REMOTE_WIPE, mRequireRemoteWipe);
+ values.put(PolicyColumns.REQUIRE_ENCRYPTION, mRequireEncryption);
+ values.put(PolicyColumns.REQUIRE_ENCRYPTION_EXTERNAL, mRequireEncryptionExternal);
+ values.put(PolicyColumns.REQUIRE_MANUAL_SYNC_WHEN_ROAMING, mRequireManualSyncWhenRoaming);
+ values.put(PolicyColumns.DONT_ALLOW_CAMERA, mDontAllowCamera);
+ values.put(PolicyColumns.DONT_ALLOW_ATTACHMENTS, mDontAllowAttachments);
+ values.put(PolicyColumns.DONT_ALLOW_HTML, mDontAllowHtml);
+ values.put(PolicyColumns.MAX_ATTACHMENT_SIZE, mMaxAttachmentSize);
+ values.put(PolicyColumns.MAX_TEXT_TRUNCATION_SIZE, mMaxTextTruncationSize);
+ values.put(PolicyColumns.MAX_HTML_TRUNCATION_SIZE, mMaxHtmlTruncationSize);
+ values.put(PolicyColumns.MAX_EMAIL_LOOKBACK, mMaxEmailLookback);
+ values.put(PolicyColumns.MAX_CALENDAR_LOOKBACK, mMaxCalendarLookback);
+ values.put(PolicyColumns.PASSWORD_RECOVERY_ENABLED, mPasswordRecoveryEnabled);
+ values.put(PolicyColumns.PROTOCOL_POLICIES_ENFORCED, mProtocolPoliciesEnforced);
+ values.put(PolicyColumns.PROTOCOL_POLICIES_UNSUPPORTED, mProtocolPoliciesUnsupported);
+ return values;
+ }
+
+ /**
+ * Helper to map our internal encoding to DevicePolicyManager password modes.
+ */
+ public int getDPManagerPasswordQuality() {
+ switch (mPasswordMode) {
+ case PASSWORD_MODE_SIMPLE:
+ return DevicePolicyManager.PASSWORD_QUALITY_NUMERIC;
+ case PASSWORD_MODE_STRONG:
+ if (mPasswordComplexChars == 0) {
+ return DevicePolicyManager.PASSWORD_QUALITY_ALPHANUMERIC;
+ } else {
+ return DevicePolicyManager.PASSWORD_QUALITY_COMPLEX;
+ }
+ default:
+ return DevicePolicyManager .PASSWORD_QUALITY_UNSPECIFIED;
+ }
+ }
+
+ /**
+ * Helper to map expiration times to the millisecond values used by DevicePolicyManager.
+ */
+ public long getDPManagerPasswordExpirationTimeout() {
+ long result = mPasswordExpirationDays * DAYS_TO_MSEC;
+ // Add a small offset to the password expiration. This makes it easier to test
+ // by changing (for example) 1 day to 1 day + 5 minutes. If you set an expiration
+ // that is within the warning period, you should get a warning fairly quickly.
+ if (result > 0) {
+ result += EXPIRATION_OFFSET_MSEC;
+ }
+ return result;
+ }
+
+ private void appendPolicy(StringBuilder sb, String code, int value) {
+ sb.append(code);
+ sb.append(":");
+ sb.append(value);
+ sb.append(" ");
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder sb = new StringBuilder("[");
+ if (equals(NO_POLICY)) {
+ sb.append("No policies]");
+ } else {
+ if (mPasswordMode == PASSWORD_MODE_NONE) {
+ sb.append("Pwd none ");
+ } else {
+ appendPolicy(sb, "Pwd strong", mPasswordMode == PASSWORD_MODE_STRONG ? 1 : 0);
+ appendPolicy(sb, "len", mPasswordMinLength);
+ appendPolicy(sb, "cmpx", mPasswordComplexChars);
+ appendPolicy(sb, "expy", mPasswordExpirationDays);
+ appendPolicy(sb, "hist", mPasswordHistory);
+ appendPolicy(sb, "fail", mPasswordMaxFails);
+ appendPolicy(sb, "idle", mMaxScreenLockTime);
+ }
+ if (mRequireEncryption) {
+ sb.append("encrypt ");
+ }
+ if (mRequireEncryptionExternal) {
+ sb.append("encryptsd ");
+ }
+ if (mDontAllowCamera) {
+ sb.append("nocamera ");
+ }
+ if (mDontAllowAttachments) {
+ sb.append("noatts ");
+ }
+ if (mRequireManualSyncWhenRoaming) {
+ sb.append("nopushroam ");
+ }
+ if (mMaxAttachmentSize > 0) {
+ appendPolicy(sb, "attmax", mMaxAttachmentSize);
+ }
+ sb.append("]");
+ }
+ return sb.toString();
+ }
+
+ /**
+ * Supports Parcelable
+ */
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ /**
+ * Supports Parcelable
+ */
+ public static final Parcelable.Creator<Policy> CREATOR = new Parcelable.Creator<Policy>() {
+ public Policy createFromParcel(Parcel in) {
+ return new Policy(in);
+ }
+
+ public Policy[] newArray(int size) {
+ return new Policy[size];
+ }
+ };
+
+ /**
+ * Supports Parcelable
+ */
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ // mBaseUri is not parceled
+ dest.writeLong(mId);
+ dest.writeInt(mPasswordMode);
+ dest.writeInt(mPasswordMinLength);
+ dest.writeInt(mPasswordMaxFails);
+ dest.writeInt(mPasswordHistory);
+ dest.writeInt(mPasswordExpirationDays);
+ dest.writeInt(mPasswordComplexChars);
+ dest.writeInt(mMaxScreenLockTime);
+ dest.writeInt(mRequireRemoteWipe ? 1 : 0);
+ dest.writeInt(mRequireEncryption ? 1 : 0);
+ dest.writeInt(mRequireEncryptionExternal ? 1 : 0);
+ dest.writeInt(mRequireManualSyncWhenRoaming ? 1 : 0);
+ dest.writeInt(mDontAllowCamera ? 1 : 0);
+ dest.writeInt(mDontAllowAttachments ? 1 : 0);
+ dest.writeInt(mDontAllowHtml ? 1 : 0);
+ dest.writeInt(mMaxAttachmentSize);
+ dest.writeInt(mMaxTextTruncationSize);
+ dest.writeInt(mMaxHtmlTruncationSize);
+ dest.writeInt(mMaxEmailLookback);
+ dest.writeInt(mMaxCalendarLookback);
+ dest.writeInt(mPasswordRecoveryEnabled ? 1 : 0);
+ dest.writeString(mProtocolPoliciesEnforced);
+ dest.writeString(mProtocolPoliciesUnsupported);
+ }
+
+ /**
+ * Supports Parcelable
+ */
+ public Policy(Parcel in) {
+ mBaseUri = CONTENT_URI;
+ mId = in.readLong();
+ mPasswordMode = in.readInt();
+ mPasswordMinLength = in.readInt();
+ mPasswordMaxFails = in.readInt();
+ mPasswordHistory = in.readInt();
+ mPasswordExpirationDays = in.readInt();
+ mPasswordComplexChars = in.readInt();
+ mMaxScreenLockTime = in.readInt();
+ mRequireRemoteWipe = in.readInt() == 1;
+ mRequireEncryption = in.readInt() == 1;
+ mRequireEncryptionExternal = in.readInt() == 1;
+ mRequireManualSyncWhenRoaming = in.readInt() == 1;
+ mDontAllowCamera = in.readInt() == 1;
+ mDontAllowAttachments = in.readInt() == 1;
+ mDontAllowHtml = in.readInt() == 1;
+ mMaxAttachmentSize = in.readInt();
+ mMaxTextTruncationSize = in.readInt();
+ mMaxHtmlTruncationSize = in.readInt();
+ mMaxEmailLookback = in.readInt();
+ mMaxCalendarLookback = in.readInt();
+ mPasswordRecoveryEnabled = in.readInt() == 1;
+ mProtocolPoliciesEnforced = in.readString();
+ mProtocolPoliciesUnsupported = in.readString();
+ }
+}
\ No newline at end of file
diff --git a/email2/emailcommon/src/com/android/emailcommon/provider/ProviderUnavailableException.java b/email2/emailcommon/src/com/android/emailcommon/provider/ProviderUnavailableException.java
new file mode 100644
index 0000000..8f33d34
--- /dev/null
+++ b/email2/emailcommon/src/com/android/emailcommon/provider/ProviderUnavailableException.java
@@ -0,0 +1,21 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.emailcommon.provider;
+
+public class ProviderUnavailableException extends RuntimeException {
+ private static final long serialVersionUID = 1L;
+}
\ No newline at end of file
diff --git a/email2/emailcommon/src/com/android/emailcommon/provider/QuickResponse.java b/email2/emailcommon/src/com/android/emailcommon/provider/QuickResponse.java
new file mode 100644
index 0000000..e88e01d
--- /dev/null
+++ b/email2/emailcommon/src/com/android/emailcommon/provider/QuickResponse.java
@@ -0,0 +1,217 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+
+package com.android.emailcommon.provider;
+
+import com.android.emailcommon.provider.EmailContent;
+import com.android.emailcommon.provider.EmailContent.QuickResponseColumns;
+import com.google.common.base.Objects;
+
+import android.content.ContentUris;
+import android.content.ContentValues;
+import android.content.Context;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+/**
+ * A user-modifiable message that may be quickly inserted into the body while user is composing
+ * a message. Tied to a specific account.
+ */
+public final class QuickResponse extends EmailContent
+ implements QuickResponseColumns, Parcelable {
+ public static final String TABLE_NAME = "QuickResponse";
+ @SuppressWarnings("hiding")
+ public static final Uri CONTENT_URI = Uri.parse(EmailContent.CONTENT_URI
+ + "/quickresponse");
+ public static final Uri ACCOUNT_ID_URI = Uri.parse(
+ EmailContent.CONTENT_URI + "/quickresponse/account");
+
+ private String mText;
+ private long mAccountKey;
+
+ private static final int CONTENT_ID_COLUMN = 0;
+ private static final int CONTENT_QUICK_RESPONSE_COLUMN = 1;
+ private static final int CONTENT_ACCOUNT_KEY_COLUMN = 2;
+ public static final String[] CONTENT_PROJECTION = new String[] {
+ RECORD_ID,
+ QuickResponseColumns.TEXT,
+ QuickResponseColumns.ACCOUNT_KEY
+ };
+
+ /**
+ * Creates an empty QuickResponse. Restore should be called after.
+ */
+ private QuickResponse() {
+ // empty
+ }
+
+ /**
+ * Constructor used by CREATOR for parceling.
+ */
+ private QuickResponse(Parcel in) {
+ mBaseUri = CONTENT_URI;
+ mId = in.readLong();
+ mText = in.readString();
+ mAccountKey = in.readLong();
+ }
+
+ /**
+ * Creates QuickResponse associated with a particular account using the given string.
+ */
+ public QuickResponse(long accountKey, String quickResponse) {
+ mBaseUri = CONTENT_URI;
+ mAccountKey = accountKey;
+ mText = quickResponse;
+ }
+
+ /**
+ * @see com.android.emailcommon.provider.EmailContent#restore(android.database.Cursor)
+ */
+ @Override
+ public void restore(Cursor cursor) {
+ mBaseUri = CONTENT_URI;
+ mId = cursor.getLong(CONTENT_ID_COLUMN);
+ mText = cursor.getString(CONTENT_QUICK_RESPONSE_COLUMN);
+ mAccountKey = cursor.getLong(CONTENT_ACCOUNT_KEY_COLUMN);
+ }
+
+ /**
+ * @see com.android.emailcommon.provider.EmailContent#toContentValues()
+ */
+ @Override
+ public ContentValues toContentValues() {
+ ContentValues values = new ContentValues();
+
+ values.put(QuickResponseColumns.TEXT, mText);
+ values.put(QuickResponseColumns.ACCOUNT_KEY, mAccountKey);
+
+ return values;
+ }
+
+ @Override
+ public String toString() {
+ return mText;
+ }
+
+ /**
+ * Given an array of QuickResponses, returns the an array of the String values
+ * corresponding to each QuickResponse.
+ */
+ public static String[] getQuickResponseStrings(QuickResponse[] quickResponses) {
+ int count = quickResponses.length;
+ String[] quickResponseStrings = new String[count];
+ for (int i = 0; i < count; i++) {
+ quickResponseStrings[i] = quickResponses[i].toString();
+ }
+
+ return quickResponseStrings;
+ }
+
+ /**
+ * @param context
+ * @param accountId
+ * @return array of QuickResponses for the account with id accountId
+ */
+ public static QuickResponse[] restoreQuickResponsesWithAccountId(Context context,
+ long accountId) {
+ Uri uri = ContentUris.withAppendedId(ACCOUNT_ID_URI, accountId);
+ Cursor c = context.getContentResolver().query(uri, CONTENT_PROJECTION,
+ null, null, null);
+
+ try {
+ int count = c.getCount();
+ QuickResponse[] quickResponses = new QuickResponse[count];
+ for (int i = 0; i < count; ++i) {
+ c.moveToNext();
+ QuickResponse quickResponse = new QuickResponse();
+ quickResponse.restore(c);
+ quickResponses[i] = quickResponse;
+ }
+ return quickResponses;
+ } finally {
+ c.close();
+ }
+ }
+
+ /**
+ * Returns the base URI for this QuickResponse
+ */
+ public Uri getBaseUri() {
+ return mBaseUri;
+ }
+
+ /**
+ * Returns the unique id for this QuickResponse
+ */
+ public long getId() {
+ return mId;
+ }
+
+ @Override
+ public boolean equals(Object objectThat) {
+ if (this == objectThat) return true;
+ if (!(objectThat instanceof QuickResponse)) return false;
+
+ QuickResponse that = (QuickResponse) objectThat;
+ return
+ mText.equals(that.mText) &&
+ mId == that.mId &&
+ mAccountKey == that.mAccountKey;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hashCode(mId, mText, mAccountKey);
+ }
+
+ /**
+ * Implements Parcelable. Not used.
+ */
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ /**
+ * Implements Parcelable.
+ */
+ public void writeToParcel(Parcel dest, int flags) {
+ // mBaseUri is not parceled
+ dest.writeLong(mId);
+ dest.writeString(mText);
+ dest.writeLong(mAccountKey);
+ }
+
+ /**
+ * Implements Parcelable
+ */
+ public static final Parcelable.Creator<QuickResponse> CREATOR
+ = new Parcelable.Creator<QuickResponse>() {
+ @Override
+ public QuickResponse createFromParcel(Parcel in) {
+ return new QuickResponse(in);
+ }
+
+ @Override
+ public QuickResponse[] newArray(int size) {
+ return new QuickResponse[size];
+ }
+ };
+
+}
\ No newline at end of file
diff --git a/email2/emailcommon/src/com/android/emailcommon/service/AccountServiceProxy.java b/email2/emailcommon/src/com/android/emailcommon/service/AccountServiceProxy.java
new file mode 100644
index 0000000..f4eb930
--- /dev/null
+++ b/email2/emailcommon/src/com/android/emailcommon/service/AccountServiceProxy.java
@@ -0,0 +1,130 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.emailcommon.service;
+
+import android.content.Context;
+import android.content.Intent;
+import android.os.Bundle;
+import android.os.IBinder;
+import android.os.RemoteException;
+
+public class AccountServiceProxy extends ServiceProxy implements IAccountService {
+
+ public static final String ACCOUNT_INTENT = "com.android.email.ACCOUNT_INTENT";
+ public static final int DEFAULT_ACCOUNT_COLOR = 0xFF0000FF;
+
+ private IAccountService mService = null;
+ private Object mReturn;
+
+ public AccountServiceProxy(Context _context) {
+ super(_context, new Intent(ACCOUNT_INTENT));
+ }
+
+ @Override
+ public void onConnected(IBinder binder) {
+ mService = IAccountService.Stub.asInterface(binder);
+ }
+
+ @Override
+ public IBinder asBinder() {
+ return null;
+ }
+
+ @Override
+ public void notifyLoginFailed(final long accountId) {
+ setTask(new ProxyTask() {
+ @Override
+ public void run() throws RemoteException {
+ mService.notifyLoginFailed(accountId);
+ }
+ }, "notifyLoginFailed");
+ }
+
+ @Override
+ public void notifyLoginSucceeded(final long accountId) {
+ setTask(new ProxyTask() {
+ @Override
+ public void run() throws RemoteException {
+ mService.notifyLoginSucceeded(accountId);
+ }
+ }, "notifyLoginSucceeded");
+ }
+
+ // The following call is synchronous, and should not be made from the UI thread
+ @Override
+ public void reconcileAccounts(final String protocol, final String accountManagerType) {
+ setTask(new ProxyTask() {
+ @Override
+ public void run() throws RemoteException {
+ mService.reconcileAccounts(protocol, accountManagerType);
+ }
+ }, "reconcileAccounts");
+ waitForCompletion();
+ }
+
+ // The following call is synchronous, and should not be made from the UI thread
+ @Override
+ public int getAccountColor(final long accountId) {
+ setTask(new ProxyTask() {
+ @Override
+ public void run() throws RemoteException{
+ mReturn = mService.getAccountColor(accountId);
+ }
+ }, "getAccountColor");
+ waitForCompletion();
+ if (mReturn == null) {
+ return DEFAULT_ACCOUNT_COLOR;
+ } else {
+ return (Integer)mReturn;
+ }
+ }
+
+ // The following call is synchronous, and should not be made from the UI thread
+ @Override
+ public Bundle getConfigurationData(final String accountType) {
+ setTask(new ProxyTask() {
+ @Override
+ public void run() throws RemoteException{
+ mReturn = mService.getConfigurationData(accountType);
+ }
+ }, "getConfigurationData");
+ waitForCompletion();
+ if (mReturn == null) {
+ return null;
+ } else {
+ return (Bundle)mReturn;
+ }
+ }
+
+ // The following call is synchronous, and should not be made from the UI thread
+ @Override
+ public String getDeviceId() {
+ setTask(new ProxyTask() {
+ @Override
+ public void run() throws RemoteException{
+ mReturn = mService.getDeviceId();
+ }
+ }, "getDeviceId");
+ waitForCompletion();
+ if (mReturn == null) {
+ return null;
+ } else {
+ return (String)mReturn;
+ }
+ }
+}
+
diff --git a/email2/emailcommon/src/com/android/emailcommon/service/EmailServiceConstants.java b/email2/emailcommon/src/com/android/emailcommon/service/EmailServiceConstants.java
new file mode 100644
index 0000000..8e8d552
--- /dev/null
+++ b/email2/emailcommon/src/com/android/emailcommon/service/EmailServiceConstants.java
@@ -0,0 +1,25 @@
+/*
+ * 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.emailcommon.service;
+
+public class EmailServiceConstants {
+ /** "Not responded yet", used ONLY for UI. */
+ public static final int MEETING_REQUEST_NOT_RESPONDED = 0;
+ public static final int MEETING_REQUEST_ACCEPTED = 1;
+ public static final int MEETING_REQUEST_TENTATIVE = 2;
+ public static final int MEETING_REQUEST_DECLINED = 3;
+}
diff --git a/email2/emailcommon/src/com/android/emailcommon/service/EmailServiceProxy.java b/email2/emailcommon/src/com/android/emailcommon/service/EmailServiceProxy.java
new file mode 100644
index 0000000..b2b50b7
--- /dev/null
+++ b/email2/emailcommon/src/com/android/emailcommon/service/EmailServiceProxy.java
@@ -0,0 +1,468 @@
+/*
+ * 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.emailcommon.service;
+
+import android.content.Context;
+import android.content.Intent;
+import android.os.Bundle;
+import android.os.IBinder;
+import android.os.RemoteException;
+import android.util.Log;
+
+import com.android.emailcommon.Api;
+import com.android.emailcommon.Device;
+import com.android.emailcommon.mail.MessagingException;
+import com.android.emailcommon.provider.HostAuth;
+import com.android.emailcommon.provider.Policy;
+
+import java.io.IOException;
+
+/**
+ * The EmailServiceProxy class provides a simple interface for the UI to call into the various
+ * EmailService classes (e.g. ExchangeService for EAS). It wraps the service connect/disconnect
+ * process so that the caller need not be concerned with it.
+ *
+ * Use the class like this:
+ * new EmailServiceProxy(context, class).loadAttachment(attachmentId, callback)
+ *
+ * Methods without a return value return immediately (i.e. are asynchronous); methods with a
+ * return value wait for a result from the Service (i.e. they should not be called from the UI
+ * thread) with a default timeout of 30 seconds (settable)
+ *
+ * An EmailServiceProxy object cannot be reused (trying to do so generates a RemoteException)
+ */
+
+public class EmailServiceProxy extends ServiceProxy implements IEmailService {
+ private static final String TAG = "EmailServiceProxy";
+
+ // Private intent that will be used to connect to an independent Exchange service
+ public static final String EXCHANGE_INTENT = "com.android.email.EXCHANGE_INTENT";
+ public static final String IMAP_INTENT = "com.android.email.IMAP_INTENT";
+
+ public static final String AUTO_DISCOVER_BUNDLE_ERROR_CODE = "autodiscover_error_code";
+ public static final String AUTO_DISCOVER_BUNDLE_HOST_AUTH = "autodiscover_host_auth";
+
+ public static final String VALIDATE_BUNDLE_RESULT_CODE = "validate_result_code";
+ public static final String VALIDATE_BUNDLE_POLICY_SET = "validate_policy_set";
+ public static final String VALIDATE_BUNDLE_ERROR_MESSAGE = "validate_error_message";
+ public static final String VALIDATE_BUNDLE_UNSUPPORTED_POLICIES =
+ "validate_unsupported_policies";
+
+ private final IEmailServiceCallback mCallback;
+ private Object mReturn = null;
+ private IEmailService mService;
+ private final boolean isRemote;
+
+ // Standard debugging
+ public static final int DEBUG_BIT = 1;
+ // Verbose (parser) logging
+ public static final int DEBUG_VERBOSE_BIT = 2;
+ // File (SD card) logging
+ public static final int DEBUG_FILE_BIT = 4;
+ // Enable strict mode
+ public static final int DEBUG_ENABLE_STRICT_MODE = 8;
+
+ // The first two constructors are used with local services that can be referenced by class
+ public EmailServiceProxy(Context _context, Class<?> _class) {
+ this(_context, _class, null);
+ }
+
+ public EmailServiceProxy(Context _context, Class<?> _class, IEmailServiceCallback _callback) {
+ super(_context, new Intent(_context, _class));
+ mCallback = _callback;
+ isRemote = false;
+ }
+
+ // The following two constructors are used with remote services that must be referenced by
+ // a known action or by a prebuilt intent
+ public EmailServiceProxy(Context _context, Intent _intent, IEmailServiceCallback _callback) {
+ super(_context, _intent);
+ try {
+ Device.getDeviceId(_context);
+ } catch (IOException e) {
+ }
+ mCallback = _callback;
+ isRemote = true;
+ }
+
+ public EmailServiceProxy(Context _context, String _action, IEmailServiceCallback _callback) {
+ super(_context, new Intent(_action));
+ try {
+ Device.getDeviceId(_context);
+ } catch (IOException e) {
+ }
+ mCallback = _callback;
+ isRemote = true;
+ }
+
+ @Override
+ public void onConnected(IBinder binder) {
+ mService = IEmailService.Stub.asInterface(binder);
+ }
+
+ public boolean isRemote() {
+ return isRemote;
+ }
+
+ @Override
+ public int getApiLevel() {
+ return Api.LEVEL;
+ }
+
+ /**
+ * Request an attachment to be loaded; the service MUST give higher priority to
+ * non-background loading. The service MUST use the loadAttachmentStatus callback when
+ * loading has started and stopped and SHOULD send callbacks with progress information if
+ * possible.
+ *
+ * @param attachmentId the id of the attachment record
+ * @param background whether or not this request corresponds to a background action (i.e.
+ * prefetch) vs a foreground action (user request)
+ */
+ @Override
+ public void loadAttachment(final long attachmentId, final boolean background)
+ throws RemoteException {
+ setTask(new ProxyTask() {
+ @Override
+ public void run() throws RemoteException {
+ try {
+ if (mCallback != null) mService.setCallback(mCallback);
+ mService.loadAttachment(attachmentId, background);
+ } catch (RemoteException e) {
+ try {
+ // Try to send a callback (if set)
+ if (mCallback != null) {
+ mCallback.loadAttachmentStatus(-1, attachmentId,
+ EmailServiceStatus.REMOTE_EXCEPTION, 0);
+ }
+ } catch (RemoteException e1) {
+ }
+ }
+ }
+ }, "loadAttachment");
+ }
+
+ /**
+ * Request the sync of a mailbox; the service MUST send the syncMailboxStatus callback
+ * indicating "starting" and "finished" (or error), regardless of whether the mailbox is
+ * actually syncable.
+ *
+ * @param mailboxId the id of the mailbox record
+ * @param userRequest whether or not the user specifically asked for the sync
+ */
+ @Override
+ public void startSync(final long mailboxId, final boolean userRequest) throws RemoteException {
+ setTask(new ProxyTask() {
+ @Override
+ public void run() throws RemoteException {
+ if (mCallback != null) mService.setCallback(mCallback);
+ mService.startSync(mailboxId, userRequest);
+ }
+ }, "startSync");
+ }
+
+ /**
+ * Request the immediate termination of a mailbox sync. Although the service is not required to
+ * acknowledge this request, it MUST send a "finished" (or error) syncMailboxStatus callback if
+ * the sync was started via the startSync service call.
+ *
+ * @param mailboxId the id of the mailbox record
+ * @param userRequest whether or not the user specifically asked for the sync
+ */
+ @Override
+ public void stopSync(final long mailboxId) throws RemoteException {
+ setTask(new ProxyTask() {
+ @Override
+ public void run() throws RemoteException {
+ if (mCallback != null) mService.setCallback(mCallback);
+ mService.stopSync(mailboxId);
+ }
+ }, "stopSync");
+ }
+
+ /**
+ * Validate a user account, given a protocol, host address, port, ssl status, and credentials.
+ * The result of this call is returned in a Bundle which MUST include a result code and MAY
+ * include a PolicySet that is required by the account. A successful validation implies a host
+ * address that serves the specified protocol and credentials sufficient to be authorized
+ * by the server to do so.
+ *
+ * @param hostAuth the hostauth object to validate
+ * @return a Bundle as described above
+ */
+ @Override
+ public Bundle validate(final HostAuth hostAuth) throws RemoteException {
+ setTask(new ProxyTask() {
+ @Override
+ public void run() throws RemoteException{
+ if (mCallback != null) mService.setCallback(mCallback);
+ mReturn = mService.validate(hostAuth);
+ }
+ }, "validate");
+ waitForCompletion();
+ if (mReturn == null) {
+ Bundle bundle = new Bundle();
+ bundle.putInt(VALIDATE_BUNDLE_RESULT_CODE, MessagingException.UNSPECIFIED_EXCEPTION);
+ return bundle;
+ } else {
+ Bundle bundle = (Bundle) mReturn;
+ bundle.setClassLoader(Policy.class.getClassLoader());
+ Log.v(TAG, "validate returns " + bundle.getInt(VALIDATE_BUNDLE_RESULT_CODE));
+ return bundle;
+ }
+ }
+
+ /**
+ * Attempt to determine a user's host address and credentials from an email address and
+ * password. The result is returned in a Bundle which MUST include an error code and MAY (on
+ * success) include a HostAuth record sufficient to enable the service to validate the user's
+ * account.
+ *
+ * @param userName the user's email address
+ * @param password the user's password
+ * @return a Bundle as described above
+ */
+ @Override
+ public Bundle autoDiscover(final String userName, final String password)
+ throws RemoteException {
+ setTask(new ProxyTask() {
+ @Override
+ public void run() throws RemoteException{
+ if (mCallback != null) mService.setCallback(mCallback);
+ mReturn = mService.autoDiscover(userName, password);
+ }
+ }, "autoDiscover");
+ waitForCompletion();
+ if (mReturn == null) {
+ return null;
+ } else {
+ Bundle bundle = (Bundle) mReturn;
+ bundle.setClassLoader(HostAuth.class.getClassLoader());
+ Log.v(TAG, "autoDiscover returns " + bundle.getInt(AUTO_DISCOVER_BUNDLE_ERROR_CODE));
+ return bundle;
+ }
+ }
+
+ /**
+ * Request that the service reload the folder list for the specified account. The service
+ * MUST use the syncMailboxListStatus callback to indicate "starting" and "finished"
+ *
+ * @param accoundId the id of the account whose folder list is to be updated
+ */
+ @Override
+ public void updateFolderList(final long accountId) throws RemoteException {
+ setTask(new ProxyTask() {
+ @Override
+ public void run() throws RemoteException {
+ if (mCallback != null) mService.setCallback(mCallback);
+ mService.updateFolderList(accountId);
+ }
+ }, "updateFolderList");
+ }
+
+ /**
+ * Specify the debug flags selected by the user. The service SHOULD log debug information as
+ * requested.
+ *
+ * @param flags an integer whose bits represent logging flags as defined in DEBUG_* flags above
+ */
+ @Override
+ public void setLogging(final int flags) throws RemoteException {
+ setTask(new ProxyTask() {
+ @Override
+ public void run() throws RemoteException {
+ if (mCallback != null) mService.setCallback(mCallback);
+ mService.setLogging(flags);
+ }
+ }, "setLogging");
+ }
+
+ /**
+ * Set the global callback object to be used by the service; the service MUST always use the
+ * most recently set callback object
+ *
+ * @param cb a callback object through which all service callbacks are executed
+ */
+ @Override
+ public void setCallback(final IEmailServiceCallback cb) throws RemoteException {
+ setTask(new ProxyTask() {
+ @Override
+ public void run() throws RemoteException {
+ mService.setCallback(cb);
+ }
+ }, "setCallback");
+ }
+
+ /**
+ * Alert the sync adapter that the account's host information has (or may have) changed; the
+ * service MUST stop all in-process or pending syncs, clear error states related to the
+ * account and its mailboxes, and restart necessary sync adapters (e.g. pushed mailboxes)
+ *
+ * @param accountId the id of the account whose host information has changed
+ */
+ @Override
+ public void hostChanged(final long accountId) throws RemoteException {
+ setTask(new ProxyTask() {
+ @Override
+ public void run() throws RemoteException {
+ mService.hostChanged(accountId);
+ }
+ }, "hostChanged");
+ }
+
+ /**
+ * Send a meeting response for the specified message
+ *
+ * @param messageId the id of the message containing the meeting request
+ * @param response the response code, as defined in EmailServiceConstants
+ */
+ @Override
+ public void sendMeetingResponse(final long messageId, final int response)
+ throws RemoteException {
+ setTask(new ProxyTask() {
+ @Override
+ public void run() throws RemoteException {
+ if (mCallback != null) mService.setCallback(mCallback);
+ mService.sendMeetingResponse(messageId, response);
+ }
+ }, "sendMeetingResponse");
+ }
+
+ /**
+ * Request the sync adapter to load a complete message
+ *
+ * @param messageId the id of the message to be loaded
+ */
+ @Override
+ public void loadMore(final long messageId) throws RemoteException {
+ setTask(new ProxyTask() {
+ @Override
+ public void run() throws RemoteException {
+ if (mCallback != null) mService.setCallback(mCallback);
+ mService.loadMore(messageId);
+ }
+ }, "startSync");
+ }
+
+ /**
+ * Not yet used
+ *
+ * @param accountId the account in which the folder is to be created
+ * @param name the name of the folder to be created
+ */
+ @Override
+ public boolean createFolder(long accountId, String name) throws RemoteException {
+ return false;
+ }
+
+ /**
+ * Not yet used
+ *
+ * @param accountId the account in which the folder resides
+ * @param name the name of the folder to be deleted
+ */
+ @Override
+ public boolean deleteFolder(long accountId, String name) throws RemoteException {
+ return false;
+ }
+
+ /**
+ * Not yet used
+ *
+ * @param accountId the account in which the folder resides
+ * @param oldName the name of the existing folder
+ * @param newName the new name for the folder
+ */
+ @Override
+ public boolean renameFolder(long accountId, String oldName, String newName)
+ throws RemoteException {
+ return false;
+ }
+
+ /**
+ * Request the service to delete the account's PIM (personal information management) data. This
+ * data includes any data that is 1) associated with the account and 2) created/stored by the
+ * service or its sync adapters and 3) not stored in the EmailProvider database (e.g. contact
+ * and calendar information).
+ *
+ * @param accountId the account whose data is to be deleted
+ */
+ @Override
+ public void deleteAccountPIMData(final long accountId) throws RemoteException {
+ setTask(new ProxyTask() {
+ @Override
+ public void run() throws RemoteException {
+ mService.deleteAccountPIMData(accountId);
+ }
+ }, "deleteAccountPIMData");
+ }
+
+
+ /**
+ * PRELIMINARY
+ * Search for messages given a query string. The string is interpreted as the logical AND of
+ * terms separated by white space. The search is performed on the specified mailbox in the
+ * specified account (including subfolders, as specified by the includeSubfolders parameter).
+ * At most numResults messages matching the query term(s) will be added to the mailbox specified
+ * as destMailboxId. If mailboxId is -1, the entire account will be searched. If firstResult is
+ * specified and non-zero, results will be added starting with the firstResult'th match (i.e.
+ * for the continuation of a previous search)
+ *
+ * @param accountId the id of the account to be searched
+ * @param searchParams the search specification
+ * @param destMailboxId the id of the mailbox into which search results are appended
+ * @return the total number of matches for this search (regardless of how many were requested)
+ */
+ @Override
+ public int searchMessages(final long accountId, final SearchParams searchParams,
+ final long destMailboxId) throws RemoteException {
+ setTask(new ProxyTask() {
+ @Override
+ public void run() throws RemoteException{
+ if (mCallback != null) mService.setCallback(mCallback);
+ mReturn = mService.searchMessages(accountId, searchParams, destMailboxId);
+ }
+ }, "searchMessages");
+ waitForCompletion();
+ if (mReturn == null) {
+ return 0;
+ } else {
+ return (Integer)mReturn;
+ }
+ }
+
+ /**
+ * Request the service to send mail in the specified account's Outbox
+ *
+ * @param accountId the account whose outgoing mail should be sent
+ */
+ @Override
+ public void sendMail(final long accountId) throws RemoteException {
+ setTask(new ProxyTask() {
+ @Override
+ public void run() throws RemoteException{
+ if (mCallback != null) mService.setCallback(mCallback);
+ mService.sendMail(accountId);
+ }
+ }, "sendMail");
+ }
+
+ @Override
+ public IBinder asBinder() {
+ return null;
+ }
+}
diff --git a/email2/emailcommon/src/com/android/emailcommon/service/EmailServiceStatus.java b/email2/emailcommon/src/com/android/emailcommon/service/EmailServiceStatus.java
new file mode 100644
index 0000000..8cd577c
--- /dev/null
+++ b/email2/emailcommon/src/com/android/emailcommon/service/EmailServiceStatus.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright (C) 2008-2009 Marc Blank
+ * Licensed to The Android Open Source Project.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.emailcommon.service;
+
+/**
+ * Definitions of service status codes returned to IEmailServiceCallback's status method
+ */
+public interface EmailServiceStatus {
+ public static final int SUCCESS = 0;
+ public static final int IN_PROGRESS = 1;
+
+ public static final int MESSAGE_NOT_FOUND = 0x10;
+ public static final int ATTACHMENT_NOT_FOUND = 0x11;
+ public static final int FOLDER_NOT_DELETED = 0x12;
+ public static final int FOLDER_NOT_RENAMED = 0x13;
+ public static final int FOLDER_NOT_CREATED = 0x14;
+ public static final int REMOTE_EXCEPTION = 0x15;
+ public static final int LOGIN_FAILED = 0x16;
+ public static final int SECURITY_FAILURE = 0x17;
+ public static final int ACCOUNT_UNINITIALIZED = 0x18;
+ public static final int ACCESS_DENIED = 0x19;
+
+ // Maybe we should automatically retry these?
+ public static final int CONNECTION_ERROR = 0x20;
+
+ // Client certificates used to authenticate cannot be retrieved from the system.
+ public static final int CLIENT_CERTIFICATE_ERROR = 0x21;
+}
diff --git a/email2/emailcommon/src/com/android/emailcommon/service/IAccountService.aidl b/email2/emailcommon/src/com/android/emailcommon/service/IAccountService.aidl
new file mode 100644
index 0000000..a29baf5
--- /dev/null
+++ b/email2/emailcommon/src/com/android/emailcommon/service/IAccountService.aidl
@@ -0,0 +1,32 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.emailcommon.service;
+
+import android.os.Bundle;
+
+interface IAccountService {
+ oneway void notifyLoginFailed(long accountId);
+ oneway void notifyLoginSucceeded(long accountId);
+
+ void reconcileAccounts(String protocol, String accountManagerType);
+
+ int getAccountColor(long accountId);
+
+ Bundle getConfigurationData(String accountType);
+
+ String getDeviceId();
+}
\ No newline at end of file
diff --git a/email2/emailcommon/src/com/android/emailcommon/service/IEmailService.aidl b/email2/emailcommon/src/com/android/emailcommon/service/IEmailService.aidl
new file mode 100644
index 0000000..cd5cd07
--- /dev/null
+++ b/email2/emailcommon/src/com/android/emailcommon/service/IEmailService.aidl
@@ -0,0 +1,62 @@
+/*
+ * Copyright (C) 2008-2010 Marc Blank
+ * Licensed to The Android Open Source Project.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.emailcommon.service;
+
+import com.android.emailcommon.provider.HostAuth;
+import com.android.emailcommon.service.IEmailServiceCallback;
+import com.android.emailcommon.service.SearchParams;
+import android.os.Bundle;
+
+interface IEmailService {
+ Bundle validate(in HostAuth hostauth);
+
+ oneway void startSync(long mailboxId, boolean userRequest);
+ oneway void stopSync(long mailboxId);
+
+ oneway void loadMore(long messageId);
+ oneway void loadAttachment(long attachmentId, boolean background);
+
+ oneway void updateFolderList(long accountId);
+
+ boolean createFolder(long accountId, String name);
+ boolean deleteFolder(long accountId, String name);
+ boolean renameFolder(long accountId, String oldName, String newName);
+
+ // Must not be oneway; unless an exception is thrown, the caller is guaranteed that the callback
+ // has been registered
+ void setCallback(IEmailServiceCallback cb);
+
+ oneway void setLogging(int on);
+
+ oneway void hostChanged(long accountId);
+
+ Bundle autoDiscover(String userName, String password);
+
+ oneway void sendMeetingResponse(long messageId, int response);
+
+ // Must not be oneway; unless an exception is thrown, the caller is guaranteed that the action
+ // has been completed
+ void deleteAccountPIMData(long accountId);
+
+ int getApiLevel();
+
+ // API level 2
+ int searchMessages(long accountId, in SearchParams params, long destMailboxId);
+
+ void sendMail(long accountId);
+}
diff --git a/email2/emailcommon/src/com/android/emailcommon/service/IEmailServiceCallback.aidl b/email2/emailcommon/src/com/android/emailcommon/service/IEmailServiceCallback.aidl
new file mode 100644
index 0000000..c713f52
--- /dev/null
+++ b/email2/emailcommon/src/com/android/emailcommon/service/IEmailServiceCallback.aidl
@@ -0,0 +1,76 @@
+/*
+ * Copyright (C) 2008-2009 Marc Blank
+ * Licensed to The Android Open Source Project.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.emailcommon.service;
+
+oneway interface IEmailServiceCallback {
+ /*
+ * Ordinary results:
+ * statuscode = 1, progress = 0: "starting"
+ * statuscode = 0, progress = n/a: "finished"
+ *
+ * If there is an error, it must be reported as follows:
+ * statuscode = err, progress = n/a: "stopping due to error"
+ *
+ * *Optionally* a callback can also include intermediate values from 1..99 e.g.
+ * statuscode = 1, progress = 0: "starting"
+ * statuscode = 1, progress = 30: "working"
+ * statuscode = 1, progress = 60: "working"
+ * statuscode = 0, progress = n/a: "finished"
+ */
+
+ /**
+ * Callback to indicate that an account is being synced (updating folder list)
+ * accountId = the account being synced
+ * statusCode = 0 for OK, 1 for progress, other codes for error
+ * progress = 0 for "start", 1..100 for optional progress reports
+ */
+ void syncMailboxListStatus(long accountId, int statusCode, int progress);
+
+ /**
+ * Callback to indicate that a mailbox is being synced
+ * mailboxId = the mailbox being synced
+ * statusCode = 0 for OK, 1 for progress, other codes for error
+ * progress = 0 for "start", 1..100 for optional progress reports
+ */
+ void syncMailboxStatus(long mailboxId, int statusCode, int progress);
+
+ /**
+ * Callback to indicate that a particular attachment is being synced
+ * messageId = the message that owns the attachment
+ * attachmentId = the attachment being synced
+ * statusCode = 0 for OK, 1 for progress, other codes for error
+ * progress = 0 for "start", 1..100 for optional progress reports
+ */
+ void loadAttachmentStatus(long messageId, long attachmentId, int statusCode, int progress);
+
+ /**
+ * Callback to indicate that a particular message is being sent
+ * messageId = the message being sent
+ * statusCode = 0 for OK, 1 for progress, other codes for error
+ * progress = 0 for "start", 1..100 for optional progress reports
+ */
+ void sendMessageStatus(long messageId, String subject, int statusCode, int progress);
+
+ /**
+ * Callback to indicate that a particular message is being loaded
+ * messageId = the message being sent
+ * statusCode = 0 for OK, 1 for progress, other codes for error
+ * progress = 0 for "start", 1..100 for optional progress reports
+ */
+ void loadMessageStatus(long messageId, int statusCode, int progress);
+}
diff --git a/email2/emailcommon/src/com/android/emailcommon/service/IPolicyService.aidl b/email2/emailcommon/src/com/android/emailcommon/service/IPolicyService.aidl
new file mode 100755
index 0000000..9d4be36
--- /dev/null
+++ b/email2/emailcommon/src/com/android/emailcommon/service/IPolicyService.aidl
@@ -0,0 +1,25 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.emailcommon.service;
+
+import com.android.emailcommon.provider.Policy;
+
+interface IPolicyService {
+ boolean isActive(in Policy policies);
+ void setAccountHoldFlag(long accountId, boolean newState);
+ void setAccountPolicy(long accountId, in Policy policy, String securityKey);
+ oneway void remoteWipe();
+}
\ No newline at end of file
diff --git a/email2/emailcommon/src/com/android/emailcommon/service/LegacyPolicySet.java b/email2/emailcommon/src/com/android/emailcommon/service/LegacyPolicySet.java
new file mode 100644
index 0000000..e5d4436
--- /dev/null
+++ b/email2/emailcommon/src/com/android/emailcommon/service/LegacyPolicySet.java
@@ -0,0 +1,87 @@
+/* Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.emailcommon.service;
+
+import com.android.emailcommon.provider.Policy;
+
+/**
+ * Legacy class for policy storage as a bit field of flags
+ */
+public class LegacyPolicySet {
+
+ // Security (provisioning) flags
+ // bits 0..4: password length (0=no password required)
+ public static final int PASSWORD_LENGTH_MASK = 31;
+ public static final int PASSWORD_LENGTH_SHIFT = 0;
+ public static final int PASSWORD_LENGTH_MAX = 30;
+ // bits 5..8: password mode
+ public static final int PASSWORD_MODE_SHIFT = 5;
+ public static final int PASSWORD_MODE_MASK = 15 << PASSWORD_MODE_SHIFT;
+ public static final int PASSWORD_MODE_NONE = 0 << PASSWORD_MODE_SHIFT;
+ public static final int PASSWORD_MODE_SIMPLE = 1 << PASSWORD_MODE_SHIFT;
+ public static final int PASSWORD_MODE_STRONG = 2 << PASSWORD_MODE_SHIFT;
+ // bits 9..13: password failures -> wipe device (0=disabled)
+ public static final int PASSWORD_MAX_FAILS_SHIFT = 9;
+ public static final int PASSWORD_MAX_FAILS_MASK = 31 << PASSWORD_MAX_FAILS_SHIFT;
+ public static final int PASSWORD_MAX_FAILS_MAX = 31;
+ // bits 14..24: seconds to screen lock (0=not required)
+ public static final int SCREEN_LOCK_TIME_SHIFT = 14;
+ public static final int SCREEN_LOCK_TIME_MASK = 2047 << SCREEN_LOCK_TIME_SHIFT;
+ public static final int SCREEN_LOCK_TIME_MAX = 2047;
+ // bit 25: remote wipe capability required
+ public static final int REQUIRE_REMOTE_WIPE = 1 << 25;
+ // bit 26..35: password expiration (days; 0=not required)
+ public static final int PASSWORD_EXPIRATION_SHIFT = 26;
+ public static final long PASSWORD_EXPIRATION_MASK = 1023L << PASSWORD_EXPIRATION_SHIFT;
+ public static final int PASSWORD_EXPIRATION_MAX = 1023;
+ // bit 36..43: password history (length; 0=not required)
+ public static final int PASSWORD_HISTORY_SHIFT = 36;
+ public static final long PASSWORD_HISTORY_MASK = 255L << PASSWORD_HISTORY_SHIFT;
+ public static final int PASSWORD_HISTORY_MAX = 255;
+ // bit 44..48: min complex characters (0=not required)
+ public static final int PASSWORD_COMPLEX_CHARS_SHIFT = 44;
+ public static final long PASSWORD_COMPLEX_CHARS_MASK = 31L << PASSWORD_COMPLEX_CHARS_SHIFT;
+ public static final int PASSWORD_COMPLEX_CHARS_MAX = 31;
+ // bit 49: requires device encryption (internal)
+ public static final long REQUIRE_ENCRYPTION = 1L << 49;
+ // bit 50: requires external storage encryption
+ public static final long REQUIRE_ENCRYPTION_EXTERNAL = 1L << 50;
+
+ /**
+ * Convert legacy policy flags to a Policy
+ * @param flags legacy policy flags
+ * @return a Policy representing the legacy policy flag
+ */
+ public static Policy flagsToPolicy(long flags) {
+ Policy policy = new Policy();
+ policy.mPasswordMode = ((int) (flags & PASSWORD_MODE_MASK)) >> PASSWORD_MODE_SHIFT;
+ policy.mPasswordMinLength = (int) ((flags & PASSWORD_LENGTH_MASK) >> PASSWORD_LENGTH_SHIFT);
+ policy.mPasswordMaxFails =
+ (int) ((flags & PASSWORD_MAX_FAILS_MASK) >> PASSWORD_MAX_FAILS_SHIFT);
+ policy.mPasswordComplexChars =
+ (int) ((flags & PASSWORD_COMPLEX_CHARS_MASK) >> PASSWORD_COMPLEX_CHARS_SHIFT);
+ policy.mPasswordHistory = (int) ((flags & PASSWORD_HISTORY_MASK) >> PASSWORD_HISTORY_SHIFT);
+ policy.mPasswordExpirationDays =
+ (int) ((flags & PASSWORD_EXPIRATION_MASK) >> PASSWORD_EXPIRATION_SHIFT);
+ policy.mMaxScreenLockTime =
+ (int) ((flags & SCREEN_LOCK_TIME_MASK) >> SCREEN_LOCK_TIME_SHIFT);
+ policy.mRequireRemoteWipe = 0 != (flags & REQUIRE_REMOTE_WIPE);
+ policy.mRequireEncryption = 0 != (flags & REQUIRE_ENCRYPTION);
+ policy.mRequireEncryptionExternal = 0 != (flags & REQUIRE_ENCRYPTION_EXTERNAL);
+ return policy;
+ }
+}
+
diff --git a/email2/emailcommon/src/com/android/emailcommon/service/PolicyServiceProxy.java b/email2/emailcommon/src/com/android/emailcommon/service/PolicyServiceProxy.java
new file mode 100755
index 0000000..26e820d
--- /dev/null
+++ b/email2/emailcommon/src/com/android/emailcommon/service/PolicyServiceProxy.java
@@ -0,0 +1,133 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.emailcommon.service;
+
+import android.content.Context;
+import android.content.Intent;
+import android.os.IBinder;
+import android.os.RemoteException;
+import android.util.Log;
+
+import com.android.emailcommon.provider.Account;
+import com.android.emailcommon.provider.Policy;
+
+public class PolicyServiceProxy extends ServiceProxy implements IPolicyService {
+ private static final boolean DEBUG_PROXY = false; // DO NOT CHECK THIS IN SET TO TRUE
+ private static final String TAG = "PolicyServiceProxy";
+
+ // The intent used by sync adapter services to connect to the PolicyService
+ public static final String POLICY_INTENT = "com.android.email.POLICY_INTENT";
+
+ private IPolicyService mService = null;
+ private Object mReturn = null;
+
+ public PolicyServiceProxy(Context _context) {
+ super(_context, new Intent(POLICY_INTENT));
+ }
+
+ @Override
+ public void onConnected(IBinder binder) {
+ mService = IPolicyService.Stub.asInterface(binder);
+ }
+
+ public IBinder asBinder() {
+ return null;
+ }
+
+ @Override
+ public boolean isActive(final Policy arg0) throws RemoteException {
+ setTask(new ProxyTask() {
+ public void run() throws RemoteException {
+ mReturn = mService.isActive(arg0);
+ }
+ }, "isActive");
+ waitForCompletion();
+ if (DEBUG_PROXY) {
+ Log.v(TAG, "isActive: " + ((mReturn == null) ? "null" : mReturn));
+ }
+ if (mReturn == null) {
+ throw new ServiceUnavailableException("isActive");
+ } else {
+ return (Boolean)mReturn;
+ }
+ }
+
+ @Override
+ public void setAccountPolicy(final long accountId, final Policy policy,
+ final String securityKey) throws RemoteException {
+ setTask(new ProxyTask() {
+ public void run() throws RemoteException {
+ mService.setAccountPolicy(accountId, policy, securityKey);
+ }
+ }, "setAccountPolicy");
+ waitForCompletion();
+ }
+
+ @Override
+ public void remoteWipe() throws RemoteException {
+ setTask(new ProxyTask() {
+ public void run() throws RemoteException {
+ mService.remoteWipe();
+ }
+ }, "remoteWipe");
+ }
+
+ @Override
+ public void setAccountHoldFlag(final long arg0, final boolean arg1) throws RemoteException {
+ setTask(new ProxyTask() {
+ public void run() throws RemoteException {
+ mService.setAccountHoldFlag(arg0, arg1);
+ }
+ }, "setAccountHoldFlag");
+ }
+
+ // Static methods that encapsulate the proxy calls above
+ public static boolean isActive(Context context, Policy policies) {
+ try {
+ return new PolicyServiceProxy(context).isActive(policies);
+ } catch (RemoteException e) {
+ }
+ return false;
+ }
+
+ public static void setAccountHoldFlag(Context context, Account account, boolean newState) {
+ try {
+ new PolicyServiceProxy(context).setAccountHoldFlag(account.mId, newState);
+ } catch (RemoteException e) {
+ throw new IllegalStateException("PolicyService transaction failed");
+ }
+ }
+
+ public static void remoteWipe(Context context) {
+ try {
+ new PolicyServiceProxy(context).remoteWipe();
+ } catch (RemoteException e) {
+ throw new IllegalStateException("PolicyService transaction failed");
+ }
+ }
+
+ public static void setAccountPolicy(Context context, long accountId, Policy policy,
+ String securityKey) {
+ try {
+ new PolicyServiceProxy(context).setAccountPolicy(accountId, policy, securityKey);
+ return;
+ } catch (RemoteException e) {
+ }
+ throw new IllegalStateException("PolicyService transaction failed");
+ }
+}
+
diff --git a/email2/emailcommon/src/com/android/emailcommon/service/SearchParams.aidl b/email2/emailcommon/src/com/android/emailcommon/service/SearchParams.aidl
new file mode 100644
index 0000000..77dedae
--- /dev/null
+++ b/email2/emailcommon/src/com/android/emailcommon/service/SearchParams.aidl
@@ -0,0 +1,18 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.emailcommon.service;
+
+parcelable SearchParams;
\ No newline at end of file
diff --git a/email2/emailcommon/src/com/android/emailcommon/service/SearchParams.java b/email2/emailcommon/src/com/android/emailcommon/service/SearchParams.java
new file mode 100644
index 0000000..eacf01d
--- /dev/null
+++ b/email2/emailcommon/src/com/android/emailcommon/service/SearchParams.java
@@ -0,0 +1,137 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.emailcommon.service;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import com.android.emailcommon.provider.Mailbox;
+import com.google.common.base.Objects;
+
+public class SearchParams implements Parcelable {
+ public static final long ALL_MAILBOXES = Mailbox.NO_MAILBOX;
+
+ private static final int DEFAULT_LIMIT = 10; // Need input on what this number should be
+ private static final int DEFAULT_OFFSET = 0;
+
+ // The id of the mailbox to be searched; if -1, all mailboxes MUST be searched
+ public final long mMailboxId;
+ // If true, all subfolders of the specified mailbox MUST be searched
+ public boolean mIncludeChildren = true;
+ // The search terms (the search MUST only select messages whose contents include all of the
+ // search terms in the query)
+ public final String mFilter;
+ // The maximum number of results to be created by this search
+ public int mLimit = DEFAULT_LIMIT;
+ // If zero, specifies a "new" search; otherwise, asks for a continuation of the previous
+ // query(ies) starting with the mOffset'th match (0 based)
+ public int mOffset = DEFAULT_OFFSET;
+ // The total number of results for this search
+ public int mTotalCount = 0;
+ // The id of the "search" mailbox being used
+ public long mSearchMailboxId;
+
+ /**
+ * Error codes returned by the searchMessages API
+ */
+ public static class SearchParamsError {
+ public static final int CANT_SEARCH_ALL_MAILBOXES = -1;
+ public static final int CANT_SEARCH_CHILDREN = -2;
+ }
+
+ public SearchParams(long mailboxId, String filter) {
+ mMailboxId = mailboxId;
+ mFilter = filter;
+ }
+
+ public SearchParams(long mailboxId, String filter, long searchMailboxId) {
+ mMailboxId = mailboxId;
+ mFilter = filter;
+ mSearchMailboxId = searchMailboxId;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (o == this) {
+ return true;
+ }
+ if ((o == null) || !(o instanceof SearchParams)) {
+ return false;
+ }
+
+ SearchParams os = (SearchParams) o;
+ return mMailboxId == os.mMailboxId
+ && mIncludeChildren == os.mIncludeChildren
+ && mFilter.equals(os.mFilter)
+ && mLimit == os.mLimit
+ && mOffset == os.mOffset;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hashCode(mMailboxId, mFilter, mOffset);
+ }
+
+ @Override
+ public String toString() {
+ return "[SearchParams " + mMailboxId + ":" + mFilter + " (" + mOffset + ", " + mLimit + "]";
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ /**
+ * Supports Parcelable
+ */
+ public static final Parcelable.Creator<SearchParams> CREATOR
+ = new Parcelable.Creator<SearchParams>() {
+ @Override
+ public SearchParams createFromParcel(Parcel in) {
+ return new SearchParams(in);
+ }
+
+ @Override
+ public SearchParams[] newArray(int size) {
+ return new SearchParams[size];
+ }
+ };
+
+ /**
+ * Supports Parcelable
+ */
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeLong(mMailboxId);
+ dest.writeInt(mIncludeChildren ? 1 : 0);
+ dest.writeString(mFilter);
+ dest.writeInt(mLimit);
+ dest.writeInt(mOffset);
+ }
+
+ /**
+ * Supports Parcelable
+ */
+ public SearchParams(Parcel in) {
+ mMailboxId = in.readLong();
+ mIncludeChildren = in.readInt() == 1;
+ mFilter = in.readString();
+ mLimit = in.readInt();
+ mOffset = in.readInt();
+ }
+}
diff --git a/email2/emailcommon/src/com/android/emailcommon/service/ServiceProxy.java b/email2/emailcommon/src/com/android/emailcommon/service/ServiceProxy.java
new file mode 100644
index 0000000..8e3bcff
--- /dev/null
+++ b/email2/emailcommon/src/com/android/emailcommon/service/ServiceProxy.java
@@ -0,0 +1,204 @@
+/*
+ /*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.emailcommon.service;
+
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.ServiceConnection;
+import android.os.Debug;
+import android.os.IBinder;
+import android.os.RemoteException;
+import android.util.Log;
+
+/**
+ * The EmailServiceProxy class provides a simple interface for the UI to call into the various
+ * EmailService classes (e.g. ExchangeService for EAS). It wraps the service connect/disconnect
+ * process so that the caller need not be concerned with it.
+ *
+ * Use the class like this:
+ * new EmailServiceClass(context, class).loadAttachment(attachmentId, callback)
+ *
+ * Methods without a return value return immediately (i.e. are asynchronous); methods with a
+ * return value wait for a result from the Service (i.e. they should not be called from the UI
+ * thread) with a default timeout of 30 seconds (settable)
+ *
+ * An EmailServiceProxy object cannot be reused (trying to do so generates a RemoteException)
+ */
+
+public abstract class ServiceProxy {
+ private static final boolean DEBUG_PROXY = false; // DO NOT CHECK THIS IN SET TO TRUE
+ private final String mTag;
+
+ private final Context mContext;
+ protected final Intent mIntent;
+ private Runnable mRunnable = new ProxyRunnable();
+ private ProxyTask mTask;
+ private String mName = " unnamed";
+ private final ServiceConnection mConnection = new ProxyConnection();
+ // Service call timeout (in seconds)
+ private int mTimeout = 45;
+ private long mStartTime;
+ private boolean mDead = false;
+
+ public abstract void onConnected(IBinder binder);
+
+ public ServiceProxy(Context _context, Intent _intent) {
+ mContext = _context;
+ mIntent = _intent;
+ mTag = getClass().getSimpleName();
+ if (Debug.isDebuggerConnected()) {
+ mTimeout <<= 2;
+ }
+ }
+
+ private class ProxyConnection implements ServiceConnection {
+ public void onServiceConnected(ComponentName name, IBinder binder) {
+ onConnected(binder);
+ if (DEBUG_PROXY) {
+ Log.v(mTag, "Connected: " + name.getShortClassName());
+ }
+ // Run our task on a new thread
+ new Thread(new Runnable() {
+ public void run() {
+ try {
+ runTask();
+ } finally {
+ endTask();
+ }
+ }}).start();
+ }
+
+ public void onServiceDisconnected(ComponentName name) {
+ if (DEBUG_PROXY) {
+ Log.v(mTag, "Disconnected: " + name.getShortClassName());
+ }
+ }
+ }
+
+ public interface ProxyTask {
+ public void run() throws RemoteException;
+ }
+
+ private class ProxyRunnable implements Runnable {
+ @Override
+ public void run() {
+ try {
+ mTask.run();
+ } catch (RemoteException e) {
+ }
+ }
+ }
+
+ public ServiceProxy setTimeout(int secs) {
+ mTimeout = secs;
+ return this;
+ }
+
+ public int getTimeout() {
+ return mTimeout;
+ }
+
+ public void endTask() {
+ try {
+ mContext.unbindService(mConnection);
+ } catch (IllegalArgumentException e) {
+ // This can happen if the user ended the activity that was using the service
+ // This is harmless, but we've got to catch it
+ }
+
+ mDead = true;
+ synchronized(mConnection) {
+ if (DEBUG_PROXY) {
+ Log.v(mTag, "Task " + mName + " completed; disconnecting");
+ }
+ mConnection.notify();
+ }
+ }
+
+ private void runTask() {
+ Thread thread = new Thread(mRunnable);
+ thread.start();
+ try {
+ thread.join();
+ } catch (InterruptedException e) {
+ }
+ }
+
+ public boolean setTask(ProxyTask task, String name) {
+ mName = name;
+ return setTask(task);
+ }
+
+ public boolean setTask(ProxyTask task) throws IllegalStateException {
+ if (mDead) {
+ throw new IllegalStateException();
+ }
+ mTask = task;
+ mStartTime = System.currentTimeMillis();
+ if (DEBUG_PROXY) {
+ Log.v(mTag, "Bind requested for task " + mName);
+ }
+ return mContext.bindService(mIntent, mConnection, Context.BIND_AUTO_CREATE);
+ }
+
+ public void waitForCompletion() {
+ synchronized (mConnection) {
+ long time = System.currentTimeMillis();
+ try {
+ if (DEBUG_PROXY) {
+ Log.v(mTag, "Waiting for task " + mName + " to complete...");
+ }
+ mConnection.wait(mTimeout * 1000L);
+ } catch (InterruptedException e) {
+ // Can be ignored safely
+ }
+ if (DEBUG_PROXY) {
+ Log.v(mTag, "Wait for " + mName + " finished in " +
+ (System.currentTimeMillis() - time) + "ms");
+ }
+ }
+ }
+
+ public void close() throws RemoteException {
+ if (mDead) {
+ throw new RemoteException();
+ }
+ endTask();
+ }
+
+ /**
+ * Connection test; return indicates whether the remote service can be connected to
+ * @return the result of trying to connect to the remote service
+ */
+ public boolean test() {
+ try {
+ return setTask(new ProxyTask() {
+ public void run() throws RemoteException {
+ if (DEBUG_PROXY) {
+ Log.v(mTag, "Connection test succeeded in " +
+ (System.currentTimeMillis() - mStartTime) + "ms");
+ }
+ }
+ }, "test");
+ } catch (Exception e) {
+ // For any failure, return false.
+ return false;
+ }
+ }
+}
diff --git a/email2/emailcommon/src/com/android/emailcommon/service/ServiceUnavailableException.java b/email2/emailcommon/src/com/android/emailcommon/service/ServiceUnavailableException.java
new file mode 100644
index 0000000..1ba22a7
--- /dev/null
+++ b/email2/emailcommon/src/com/android/emailcommon/service/ServiceUnavailableException.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.emailcommon.service;
+
+/**
+ * An Exception thrown when a service proxy requires a result and there's a remote exception; this
+ * prevents the caller from receiving an invalid result.
+ */
+public class ServiceUnavailableException extends RuntimeException {
+ private static final long serialVersionUID = 1L;
+
+ public ServiceUnavailableException(String string) {
+ super(string);
+ }
+}
\ No newline at end of file
diff --git a/email2/emailcommon/src/com/android/emailcommon/service/SyncWindow.java b/email2/emailcommon/src/com/android/emailcommon/service/SyncWindow.java
new file mode 100644
index 0000000..52839b2
--- /dev/null
+++ b/email2/emailcommon/src/com/android/emailcommon/service/SyncWindow.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.emailcommon.service;
+
+public class SyncWindow {
+ public static final int SYNC_WINDOW_AUTO = -2;
+ public static final int SYNC_WINDOW_USER = -1;
+ public static final int SYNC_WINDOW_UNKNOWN = 0;
+ public static final int SYNC_WINDOW_1_DAY = 1;
+ public static final int SYNC_WINDOW_3_DAYS = 2;
+ public static final int SYNC_WINDOW_1_WEEK = 3;
+ public static final int SYNC_WINDOW_2_WEEKS = 4;
+ public static final int SYNC_WINDOW_1_MONTH = 5;
+ public static final int SYNC_WINDOW_ALL = 6;
+}
diff --git a/email2/emailcommon/src/com/android/emailcommon/utility/AttachmentUtilities.java b/email2/emailcommon/src/com/android/emailcommon/utility/AttachmentUtilities.java
new file mode 100644
index 0000000..e4d119b
--- /dev/null
+++ b/email2/emailcommon/src/com/android/emailcommon/utility/AttachmentUtilities.java
@@ -0,0 +1,352 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.emailcommon.utility;
+
+import com.android.emailcommon.Logging;
+import com.android.emailcommon.provider.EmailContent.Attachment;
+import com.android.emailcommon.provider.EmailContent.Message;
+import com.android.emailcommon.provider.EmailContent.MessageColumns;
+
+import android.content.ContentResolver;
+import android.content.ContentUris;
+import android.content.Context;
+import android.database.Cursor;
+import android.net.Uri;
+import android.text.TextUtils;
+import android.util.Log;
+import android.webkit.MimeTypeMap;
+
+import java.io.File;
+
+public class AttachmentUtilities {
+ public static final String AUTHORITY = "com.android.email.attachmentprovider";
+ public static final Uri CONTENT_URI = Uri.parse( "content://" + AUTHORITY);
+
+ public static final String FORMAT_RAW = "RAW";
+ public static final String FORMAT_THUMBNAIL = "THUMBNAIL";
+
+ public static class Columns {
+ public static final String _ID = "_id";
+ public static final String DATA = "_data";
+ public static final String DISPLAY_NAME = "_display_name";
+ public static final String SIZE = "_size";
+ }
+
+ /**
+ * The MIME type(s) of attachments we're willing to send via attachments.
+ *
+ * Any attachments may be added via Intents with Intent.ACTION_SEND or ACTION_SEND_MULTIPLE.
+ */
+ public static final String[] ACCEPTABLE_ATTACHMENT_SEND_INTENT_TYPES = new String[] {
+ "*/*",
+ };
+ /**
+ * The MIME type(s) of attachments we're willing to send from the internal UI.
+ *
+ * NOTE: At the moment it is not possible to open a chooser with a list of filter types, so
+ * the chooser is only opened with the first item in the list.
+ */
+ public static final String[] ACCEPTABLE_ATTACHMENT_SEND_UI_TYPES = new String[] {
+ "image/*",
+ "video/*",
+ };
+ /**
+ * The MIME type(s) of attachments we're willing to view.
+ */
+ public static final String[] ACCEPTABLE_ATTACHMENT_VIEW_TYPES = new String[] {
+ "*/*",
+ };
+ /**
+ * The MIME type(s) of attachments we're not willing to view.
+ */
+ public static final String[] UNACCEPTABLE_ATTACHMENT_VIEW_TYPES = new String[] {
+ };
+ /**
+ * The MIME type(s) of attachments we're willing to download to SD.
+ */
+ public static final String[] ACCEPTABLE_ATTACHMENT_DOWNLOAD_TYPES = new String[] {
+ "*/*",
+ };
+ /**
+ * The MIME type(s) of attachments we're not willing to download to SD.
+ */
+ public static final String[] UNACCEPTABLE_ATTACHMENT_DOWNLOAD_TYPES = new String[] {
+ };
+ /**
+ * Filename extensions of attachments we're never willing to download (potential malware).
+ * Entries in this list are compared to the end of the lower-cased filename, so they must
+ * be lower case, and should not include a "."
+ */
+ public static final String[] UNACCEPTABLE_ATTACHMENT_EXTENSIONS = new String[] {
+ // File types that contain malware
+ "ade", "adp", "bat", "chm", "cmd", "com", "cpl", "dll", "exe",
+ "hta", "ins", "isp", "jse", "lib", "mde", "msc", "msp",
+ "mst", "pif", "scr", "sct", "shb", "sys", "vb", "vbe",
+ "vbs", "vxd", "wsc", "wsf", "wsh",
+ // File types of common compression/container formats (again, to avoid malware)
+ "zip", "gz", "z", "tar", "tgz", "bz2",
+ };
+ /**
+ * Filename extensions of attachments that can be installed.
+ * Entries in this list are compared to the end of the lower-cased filename, so they must
+ * be lower case, and should not include a "."
+ */
+ public static final String[] INSTALLABLE_ATTACHMENT_EXTENSIONS = new String[] {
+ "apk",
+ };
+ /**
+ * The maximum size of an attachment we're willing to download (either View or Save)
+ * Attachments that are base64 encoded (most) will be about 1.375x their actual size
+ * so we should probably factor that in. A 5MB attachment will generally be around
+ * 6.8MB downloaded but only 5MB saved.
+ */
+ public static final int MAX_ATTACHMENT_DOWNLOAD_SIZE = (5 * 1024 * 1024);
+ /**
+ * The maximum size of an attachment we're willing to upload (measured as stored on disk).
+ * Attachments that are base64 encoded (most) will be about 1.375x their actual size
+ * so we should probably factor that in. A 5MB attachment will generally be around
+ * 6.8MB uploaded.
+ */
+ public static final int MAX_ATTACHMENT_UPLOAD_SIZE = (5 * 1024 * 1024);
+
+ public static Uri getAttachmentUri(long accountId, long id) {
+ return CONTENT_URI.buildUpon()
+ .appendPath(Long.toString(accountId))
+ .appendPath(Long.toString(id))
+ .appendPath(FORMAT_RAW)
+ .build();
+ }
+
+ public static Uri getAttachmentThumbnailUri(long accountId, long id,
+ int width, int height) {
+ return CONTENT_URI.buildUpon()
+ .appendPath(Long.toString(accountId))
+ .appendPath(Long.toString(id))
+ .appendPath(FORMAT_THUMBNAIL)
+ .appendPath(Integer.toString(width))
+ .appendPath(Integer.toString(height))
+ .build();
+ }
+
+ /**
+ * Return the filename for a given attachment. This should be used by any code that is
+ * going to *write* attachments.
+ *
+ * This does not create or write the file, or even the directories. It simply builds
+ * the filename that should be used.
+ */
+ public static File getAttachmentFilename(Context context, long accountId, long attachmentId) {
+ return new File(getAttachmentDirectory(context, accountId), Long.toString(attachmentId));
+ }
+
+ /**
+ * Return the directory for a given attachment. This should be used by any code that is
+ * going to *write* attachments.
+ *
+ * This does not create or write the directory. It simply builds the pathname that should be
+ * used.
+ */
+ public static File getAttachmentDirectory(Context context, long accountId) {
+ return context.getDatabasePath(accountId + ".db_att");
+ }
+
+ /**
+ * Helper to convert unknown or unmapped attachments to something useful based on filename
+ * extensions. The mime type is inferred based upon the table below. It's not perfect, but
+ * it helps.
+ *
+ * <pre>
+ * |---------------------------------------------------------|
+ * | E X T E N S I O N |
+ * |---------------------------------------------------------|
+ * | .eml | known(.png) | unknown(.abc) | none |
+ * | M |-----------------------------------------------------------------------|
+ * | I | none | msg/rfc822 | image/png | app/abc | app/oct-str |
+ * | M |-------------| (always | | | |
+ * | E | app/oct-str | overrides | | | |
+ * | T |-------------| | |-----------------------------|
+ * | Y | text/plain | | | text/plain |
+ * | P |-------------| |-------------------------------------------|
+ * | E | any/type | | any/type |
+ * |---|-----------------------------------------------------------------------|
+ * </pre>
+ *
+ * NOTE: Since mime types on Android are case-*sensitive*, return values are always in
+ * lower case.
+ *
+ * @param fileName The given filename
+ * @param mimeType The given mime type
+ * @return A likely mime type for the attachment
+ */
+ public static String inferMimeType(final String fileName, final String mimeType) {
+ String resultType = null;
+ String fileExtension = getFilenameExtension(fileName);
+ boolean isTextPlain = "text/plain".equalsIgnoreCase(mimeType);
+
+ if ("eml".equals(fileExtension)) {
+ resultType = "message/rfc822";
+ } else {
+ boolean isGenericType =
+ isTextPlain || "application/octet-stream".equalsIgnoreCase(mimeType);
+ // If the given mime type is non-empty and non-generic, return it
+ if (isGenericType || TextUtils.isEmpty(mimeType)) {
+ if (!TextUtils.isEmpty(fileExtension)) {
+ // Otherwise, try to find a mime type based upon the file extension
+ resultType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(fileExtension);
+ if (TextUtils.isEmpty(resultType)) {
+ // Finally, if original mimetype is text/plain, use it; otherwise synthesize
+ resultType = isTextPlain ? mimeType : "application/" + fileExtension;
+ }
+ }
+ } else {
+ resultType = mimeType;
+ }
+ }
+
+ // No good guess could be made; use an appropriate generic type
+ if (TextUtils.isEmpty(resultType)) {
+ resultType = isTextPlain ? "text/plain" : "application/octet-stream";
+ }
+ return resultType.toLowerCase();
+ }
+
+ /**
+ * @return mime-type for a {@link Uri}.
+ * - Use {@link ContentResolver#getType} for a content: URI.
+ * - Use {@link #inferMimeType} for a file: URI.
+ * - Otherwise throw {@link IllegalArgumentException}.
+ */
+ public static String inferMimeTypeForUri(Context context, Uri uri) {
+ final String scheme = uri.getScheme();
+ if ("content".equals(scheme)) {
+ return context.getContentResolver().getType(uri);
+ } else if ("file".equals(scheme)) {
+ return inferMimeType(uri.getLastPathSegment(), "");
+ } else {
+ throw new IllegalArgumentException();
+ }
+ }
+
+ /**
+ * Extract and return filename's extension, converted to lower case, and not including the "."
+ *
+ * @return extension, or null if not found (or null/empty filename)
+ */
+ public static String getFilenameExtension(String fileName) {
+ String extension = null;
+ if (!TextUtils.isEmpty(fileName)) {
+ int lastDot = fileName.lastIndexOf('.');
+ if ((lastDot > 0) && (lastDot < fileName.length() - 1)) {
+ extension = fileName.substring(lastDot + 1).toLowerCase();
+ }
+ }
+ return extension;
+ }
+
+ /**
+ * Resolve attachment id to content URI. Returns the resolved content URI (from the attachment
+ * DB) or, if not found, simply returns the incoming value.
+ *
+ * @param attachmentUri
+ * @return resolved content URI
+ *
+ * TODO: Throws an SQLite exception on a missing DB file (e.g. unknown URI) instead of just
+ * returning the incoming uri, as it should.
+ */
+ public static Uri resolveAttachmentIdToContentUri(ContentResolver resolver, Uri attachmentUri) {
+ Cursor c = resolver.query(attachmentUri,
+ new String[] { Columns.DATA },
+ null, null, null);
+ if (c != null) {
+ try {
+ if (c.moveToFirst()) {
+ final String strUri = c.getString(0);
+ if (strUri != null) {
+ return Uri.parse(strUri);
+ }
+ }
+ } finally {
+ c.close();
+ }
+ }
+ return attachmentUri;
+ }
+
+ /**
+ * In support of deleting a message, find all attachments and delete associated attachment
+ * files.
+ * @param context
+ * @param accountId the account for the message
+ * @param messageId the message
+ */
+ public static void deleteAllAttachmentFiles(Context context, long accountId, long messageId) {
+ Uri uri = ContentUris.withAppendedId(Attachment.MESSAGE_ID_URI, messageId);
+ Cursor c = context.getContentResolver().query(uri, Attachment.ID_PROJECTION,
+ null, null, null);
+ try {
+ while (c.moveToNext()) {
+ long attachmentId = c.getLong(Attachment.ID_PROJECTION_COLUMN);
+ File attachmentFile = getAttachmentFilename(context, accountId, attachmentId);
+ // Note, delete() throws no exceptions for basic FS errors (e.g. file not found)
+ // it just returns false, which we ignore, and proceed to the next file.
+ // This entire loop is best-effort only.
+ attachmentFile.delete();
+ }
+ } finally {
+ c.close();
+ }
+ }
+
+ /**
+ * In support of deleting a mailbox, find all messages and delete their attachments.
+ *
+ * @param context
+ * @param accountId the account for the mailbox
+ * @param mailboxId the mailbox for the messages
+ */
+ public static void deleteAllMailboxAttachmentFiles(Context context, long accountId,
+ long mailboxId) {
+ Cursor c = context.getContentResolver().query(Message.CONTENT_URI,
+ Message.ID_COLUMN_PROJECTION, MessageColumns.MAILBOX_KEY + "=?",
+ new String[] { Long.toString(mailboxId) }, null);
+ try {
+ while (c.moveToNext()) {
+ long messageId = c.getLong(Message.ID_PROJECTION_COLUMN);
+ deleteAllAttachmentFiles(context, accountId, messageId);
+ }
+ } finally {
+ c.close();
+ }
+ }
+
+ /**
+ * In support of deleting or wiping an account, delete all related attachments.
+ *
+ * @param context
+ * @param accountId the account to scrub
+ */
+ public static void deleteAllAccountAttachmentFiles(Context context, long accountId) {
+ File[] files = getAttachmentDirectory(context, accountId).listFiles();
+ if (files == null) return;
+ for (File file : files) {
+ boolean result = file.delete();
+ if (!result) {
+ Log.e(Logging.LOG_TAG, "Failed to delete attachment file " + file.getName());
+ }
+ }
+ }
+}
diff --git a/email2/emailcommon/src/com/android/emailcommon/utility/CertificateRequestor.java b/email2/emailcommon/src/com/android/emailcommon/utility/CertificateRequestor.java
new file mode 100644
index 0000000..b78895b
--- /dev/null
+++ b/email2/emailcommon/src/com/android/emailcommon/utility/CertificateRequestor.java
@@ -0,0 +1,68 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+
+package com.android.emailcommon.utility;
+
+import android.app.Activity;
+import android.content.Intent;
+import android.os.Bundle;
+import android.security.KeyChain;
+import android.security.KeyChainAliasCallback;
+
+/**
+ * A headless Activity which simply calls into the framework {@link KeyChain} service to select
+ * a certificate to use for establishing secure connections in the Email app.
+ */
+public class CertificateRequestor extends Activity implements KeyChainAliasCallback {
+
+ public static final String ACTION_REQUEST_CERT = "com.android.emailcommon.REQUEST_CERT";
+
+ public static final String EXTRA_HOST = "CertificateRequestor.host";
+ public static final String EXTRA_PORT = "CertificateRequestor.port";
+
+ public static final String RESULT_ALIAS = "CertificateRequestor.alias";
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ Intent i = getIntent();
+ String host = i.getStringExtra(EXTRA_HOST);
+ int port = i.getIntExtra(EXTRA_PORT, -1);
+
+ KeyChain.choosePrivateKeyAlias(
+ this, this,
+ null /* keytypes */, null /* issuers */,
+ host, port,
+ null /* alias */);
+ }
+
+ /**
+ * Callback for the certificate request. Does not happen on the UI thread.
+ */
+ @Override
+ public void alias(String alias) {
+ if (alias == null) {
+ setResult(RESULT_CANCELED);
+ } else {
+ Intent data = new Intent();
+ data.putExtra(RESULT_ALIAS, alias);
+ setResult(RESULT_OK, data);
+ }
+ finish();
+ }
+}
diff --git a/email2/emailcommon/src/com/android/emailcommon/utility/ConversionUtilities.java b/email2/emailcommon/src/com/android/emailcommon/utility/ConversionUtilities.java
new file mode 100644
index 0000000..41ba12d
--- /dev/null
+++ b/email2/emailcommon/src/com/android/emailcommon/utility/ConversionUtilities.java
@@ -0,0 +1,138 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.emailcommon.utility;
+
+import com.android.emailcommon.internet.MimeHeader;
+import com.android.emailcommon.internet.MimeUtility;
+import com.android.emailcommon.mail.MessagingException;
+import com.android.emailcommon.mail.Part;
+import com.android.emailcommon.provider.EmailContent;
+
+import android.text.TextUtils;
+
+import java.util.ArrayList;
+
+public class ConversionUtilities {
+ /**
+ * Values for HEADER_ANDROID_BODY_QUOTED_PART to tag body parts
+ */
+ public static final String BODY_QUOTED_PART_REPLY = "quoted-reply";
+ public static final String BODY_QUOTED_PART_FORWARD = "quoted-forward";
+ public static final String BODY_QUOTED_PART_INTRO = "quoted-intro";
+
+ /**
+ * Helper function to append text to a StringBuffer, creating it if necessary.
+ * Optimization: The majority of the time we are *not* appending - we should have a path
+ * that deals with single strings.
+ */
+ private static StringBuffer appendTextPart(StringBuffer sb, String newText) {
+ if (newText == null) {
+ return sb;
+ }
+ else if (sb == null) {
+ sb = new StringBuffer(newText);
+ } else {
+ if (sb.length() > 0) {
+ sb.append('\n');
+ }
+ sb.append(newText);
+ }
+ return sb;
+ }
+
+ /**
+ * Copy body text (plain and/or HTML) from MimeMessage to provider Message
+ */
+ public static boolean updateBodyFields(EmailContent.Body body,
+ EmailContent.Message localMessage, ArrayList<Part> viewables)
+ throws MessagingException {
+
+ body.mMessageKey = localMessage.mId;
+
+ StringBuffer sbHtml = null;
+ StringBuffer sbText = null;
+ StringBuffer sbHtmlReply = null;
+ StringBuffer sbTextReply = null;
+ StringBuffer sbIntroText = null;
+
+ for (Part viewable : viewables) {
+ String text = MimeUtility.getTextFromPart(viewable);
+ String[] replyTags = viewable.getHeader(MimeHeader.HEADER_ANDROID_BODY_QUOTED_PART);
+ String replyTag = null;
+ if (replyTags != null && replyTags.length > 0) {
+ replyTag = replyTags[0];
+ }
+ // Deploy text as marked by the various tags
+ boolean isHtml = "text/html".equalsIgnoreCase(viewable.getMimeType());
+
+ if (replyTag != null) {
+ boolean isQuotedReply = BODY_QUOTED_PART_REPLY.equalsIgnoreCase(replyTag);
+ boolean isQuotedForward = BODY_QUOTED_PART_FORWARD.equalsIgnoreCase(replyTag);
+ boolean isQuotedIntro = BODY_QUOTED_PART_INTRO.equalsIgnoreCase(replyTag);
+
+ if (isQuotedReply || isQuotedForward) {
+ if (isHtml) {
+ sbHtmlReply = appendTextPart(sbHtmlReply, text);
+ } else {
+ sbTextReply = appendTextPart(sbTextReply, text);
+ }
+ // Set message flags as well
+ localMessage.mFlags &= ~EmailContent.Message.FLAG_TYPE_MASK;
+ localMessage.mFlags |= isQuotedReply
+ ? EmailContent.Message.FLAG_TYPE_REPLY
+ : EmailContent.Message.FLAG_TYPE_FORWARD;
+ continue;
+ }
+ if (isQuotedIntro) {
+ sbIntroText = appendTextPart(sbIntroText, text);
+ continue;
+ }
+ }
+
+ // Most of the time, just process regular body parts
+ if (isHtml) {
+ sbHtml = appendTextPart(sbHtml, text);
+ } else {
+ sbText = appendTextPart(sbText, text);
+ }
+ }
+
+ // write the combined data to the body part
+ if (!TextUtils.isEmpty(sbText)) {
+ String text = sbText.toString();
+ body.mTextContent = text;
+ localMessage.mSnippet = TextUtilities.makeSnippetFromPlainText(text);
+ }
+ if (!TextUtils.isEmpty(sbHtml)) {
+ String text = sbHtml.toString();
+ body.mHtmlContent = text;
+ if (localMessage.mSnippet == null) {
+ localMessage.mSnippet = TextUtilities.makeSnippetFromHtmlText(text);
+ }
+ }
+ if (sbHtmlReply != null && sbHtmlReply.length() != 0) {
+ body.mHtmlReply = sbHtmlReply.toString();
+ }
+ if (sbTextReply != null && sbTextReply.length() != 0) {
+ body.mTextReply = sbTextReply.toString();
+ }
+ if (sbIntroText != null && sbIntroText.length() != 0) {
+ body.mIntroText = sbIntroText.toString();
+ }
+ return true;
+ }
+}
diff --git a/email2/emailcommon/src/com/android/emailcommon/utility/DelayedOperations.java b/email2/emailcommon/src/com/android/emailcommon/utility/DelayedOperations.java
new file mode 100644
index 0000000..29324a3
--- /dev/null
+++ b/email2/emailcommon/src/com/android/emailcommon/utility/DelayedOperations.java
@@ -0,0 +1,106 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.emailcommon.utility;
+
+import com.google.common.annotations.VisibleForTesting;
+
+import android.os.Handler;
+
+import java.util.ArrayList;
+import java.util.LinkedList;
+
+/**
+ * Class that helps post {@link Runnable}s to a {@link Handler}, and cancel pending ones
+ * at once.
+ */
+public class DelayedOperations {
+ private final Handler mHandler;
+
+ @VisibleForTesting
+ final LinkedList<QueuedOperation> mPendingOperations = new LinkedList<QueuedOperation>();
+
+ private class QueuedOperation implements Runnable {
+ private final Runnable mActualRannable;
+
+ public QueuedOperation(Runnable actualRannable) {
+ mActualRannable = actualRannable;
+ }
+
+ @Override
+ public void run() {
+ mPendingOperations.remove(this);
+ mActualRannable.run();
+ }
+
+ public void cancel() {
+ mPendingOperations.remove(this);
+ cancelRunnable(this);
+ }
+ }
+
+ public DelayedOperations(Handler handler) {
+ mHandler = handler;
+ }
+
+ /**
+ * Post a {@link Runnable} to the handler. Equivalent to {@link Handler#post(Runnable)}.
+ */
+ public void post(Runnable r) {
+ final QueuedOperation qo = new QueuedOperation(r);
+ mPendingOperations.add(qo);
+ postRunnable(qo);
+ }
+
+ /**
+ * Cancel a runnable that's been posted with {@link #post(Runnable)}.
+ *
+ * Equivalent to {@link Handler#removeCallbacks(Runnable)}.
+ */
+ public void removeCallbacks(Runnable r) {
+ QueuedOperation found = null;
+ for (QueuedOperation qo : mPendingOperations) {
+ if (qo.mActualRannable == r) {
+ found = qo;
+ break;
+ }
+ }
+ if (found != null) {
+ found.cancel();
+ }
+ }
+
+ /**
+ * Cancel all pending {@link Runnable}s.
+ */
+ public void removeCallbacks() {
+ // To avoid ConcurrentModificationException
+ final ArrayList<QueuedOperation> temp = new ArrayList<QueuedOperation>(mPendingOperations);
+ for (QueuedOperation qo : temp) {
+ qo.cancel();
+ }
+ }
+
+ /** Overridden by test, as Handler is not mockable. */
+ void postRunnable(Runnable r) {
+ mHandler.post(r);
+ }
+
+ /** Overridden by test, as Handler is not mockable. */
+ void cancelRunnable(Runnable r) {
+ mHandler.removeCallbacks(r);
+ }
+}
diff --git a/email2/emailcommon/src/com/android/emailcommon/utility/EmailAsyncTask.java b/email2/emailcommon/src/com/android/emailcommon/utility/EmailAsyncTask.java
new file mode 100644
index 0000000..3d2e69c
--- /dev/null
+++ b/email2/emailcommon/src/com/android/emailcommon/utility/EmailAsyncTask.java
@@ -0,0 +1,274 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.emailcommon.utility;
+
+import android.os.AsyncTask;
+
+import java.util.ArrayList;
+import java.util.LinkedList;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.Executor;
+
+/**
+ * {@link AsyncTask} substitution for the email app.
+ *
+ * Modeled after {@link AsyncTask}; the basic usage is the same, with extra features:
+ * - Bulk cancellation of multiple tasks. This is mainly used by UI to cancel pending tasks
+ * in onDestroy() or similar places.
+ * - Instead of {@link AsyncTask#onPostExecute}, it has {@link #onSuccess(Object)}, as the
+ * regular {@link AsyncTask#onPostExecute} is a bit hard to predict when it'll be called and
+ * whel it won't.
+ *
+ * Note this class is missing some of the {@link AsyncTask} features, e.g. it lacks
+ * {@link AsyncTask#onProgressUpdate}. Add these when necessary.
+ */
+public abstract class EmailAsyncTask<Params, Progress, Result> {
+ private static final Executor SERIAL_EXECUTOR = AsyncTask.SERIAL_EXECUTOR;
+ private static final Executor PARALLEL_EXECUTOR = AsyncTask.THREAD_POOL_EXECUTOR;
+
+ /**
+ * Tracks {@link EmailAsyncTask}.
+ *
+ * Call {@link #cancellAllInterrupt()} to cancel all tasks registered.
+ */
+ public static class Tracker {
+ private final LinkedList<EmailAsyncTask<?, ?, ?>> mTasks =
+ new LinkedList<EmailAsyncTask<?, ?, ?>>();
+
+ private void add(EmailAsyncTask<?, ?, ?> task) {
+ synchronized (mTasks) {
+ mTasks.add(task);
+ }
+ }
+
+ private void remove(EmailAsyncTask<?, ?, ?> task) {
+ synchronized (mTasks) {
+ mTasks.remove(task);
+ }
+ }
+
+ /**
+ * Cancel all registered tasks.
+ */
+ public void cancellAllInterrupt() {
+ synchronized (mTasks) {
+ for (EmailAsyncTask<?, ?, ?> task : mTasks) {
+ task.cancel(true);
+ }
+ mTasks.clear();
+ }
+ }
+
+ /**
+ * Cancel all instances of the same class as {@code current} other than
+ * {@code current} itself.
+ */
+ /* package */ void cancelOthers(EmailAsyncTask<?, ?, ?> current) {
+ final Class<?> clazz = current.getClass();
+ synchronized (mTasks) {
+ final ArrayList<EmailAsyncTask<?, ?, ?>> toRemove =
+ new ArrayList<EmailAsyncTask<?, ?, ?>>();
+ for (EmailAsyncTask<?, ?, ?> task : mTasks) {
+ if ((task != current) && task.getClass().equals(clazz)) {
+ task.cancel(true);
+ toRemove.add(task);
+ }
+ }
+ for (EmailAsyncTask<?, ?, ?> task : toRemove) {
+ mTasks.remove(task);
+ }
+ }
+ }
+
+ /* package */ int getTaskCountForTest() {
+ return mTasks.size();
+ }
+
+ /* package */ boolean containsTaskForTest(EmailAsyncTask<?, ?, ?> task) {
+ return mTasks.contains(task);
+ }
+ }
+
+ private final Tracker mTracker;
+
+ private static class InnerTask<Params2, Progress2, Result2>
+ extends AsyncTask<Params2, Progress2, Result2> {
+ private final EmailAsyncTask<Params2, Progress2, Result2> mOwner;
+
+ public InnerTask(EmailAsyncTask<Params2, Progress2, Result2> owner) {
+ mOwner = owner;
+ }
+
+ @Override
+ protected Result2 doInBackground(Params2... params) {
+ return mOwner.doInBackground(params);
+ }
+
+ @Override
+ public void onCancelled(Result2 result) {
+ mOwner.unregisterSelf();
+ mOwner.onCancelled(result);
+ }
+
+ @Override
+ public void onPostExecute(Result2 result) {
+ mOwner.unregisterSelf();
+ if (mOwner.mCancelled) {
+ mOwner.onCancelled(result);
+ } else {
+ mOwner.onSuccess(result);
+ }
+ }
+ }
+
+ private final InnerTask<Params, Progress, Result> mInnerTask;
+ private volatile boolean mCancelled;
+
+ public EmailAsyncTask(Tracker tracker) {
+ mTracker = tracker;
+ if (mTracker != null) {
+ mTracker.add(this);
+ }
+ mInnerTask = new InnerTask<Params, Progress, Result>(this);
+ }
+
+ /* package */ final void unregisterSelf() {
+ if (mTracker != null) {
+ mTracker.remove(this);
+ }
+ }
+
+ /** @see AsyncTask#doInBackground */
+ protected abstract Result doInBackground(Params... params);
+
+
+ /** @see AsyncTask#cancel(boolean) */
+ public final void cancel(boolean mayInterruptIfRunning) {
+ mCancelled = true;
+ mInnerTask.cancel(mayInterruptIfRunning);
+ }
+
+ /** @see AsyncTask#onCancelled */
+ protected void onCancelled(Result result) {
+ }
+
+ /**
+ * Similar to {@link AsyncTask#onPostExecute}, but this will never be executed if
+ * {@link #cancel(boolean)} has been called before its execution, even if
+ * {@link #doInBackground(Object...)} has completed when cancelled.
+ *
+ * @see AsyncTask#onPostExecute
+ */
+ protected void onSuccess(Result result) {
+ }
+
+ /**
+ * execute on {@link #PARALLEL_EXECUTOR}
+ *
+ * @see AsyncTask#execute
+ */
+ public final EmailAsyncTask<Params, Progress, Result> executeParallel(Params... params) {
+ return executeInternal(PARALLEL_EXECUTOR, false, params);
+ }
+
+ /**
+ * execute on {@link #SERIAL_EXECUTOR}
+ *
+ * @see AsyncTask#execute
+ */
+ public final EmailAsyncTask<Params, Progress, Result> executeSerial(Params... params) {
+ return executeInternal(SERIAL_EXECUTOR, false, params);
+ }
+
+ /**
+ * Cancel all previously created instances of the same class tracked by the same
+ * {@link Tracker}, and then {@link #executeParallel}.
+ */
+ public final EmailAsyncTask<Params, Progress, Result> cancelPreviousAndExecuteParallel(
+ Params... params) {
+ return executeInternal(PARALLEL_EXECUTOR, true, params);
+ }
+
+ /**
+ * Cancel all previously created instances of the same class tracked by the same
+ * {@link Tracker}, and then {@link #executeSerial}.
+ */
+ public final EmailAsyncTask<Params, Progress, Result> cancelPreviousAndExecuteSerial(
+ Params... params) {
+ return executeInternal(SERIAL_EXECUTOR, true, params);
+ }
+
+ private final EmailAsyncTask<Params, Progress, Result> executeInternal(Executor executor,
+ boolean cancelPrevious, Params... params) {
+ if (cancelPrevious) {
+ if (mTracker == null) {
+ throw new IllegalStateException();
+ } else {
+ mTracker.cancelOthers(this);
+ }
+ }
+ mInnerTask.executeOnExecutor(executor, params);
+ return this;
+ }
+
+ /**
+ * Runs a {@link Runnable} in a bg thread, using {@link #PARALLEL_EXECUTOR}.
+ */
+ public static EmailAsyncTask<Void, Void, Void> runAsyncParallel(Runnable runnable) {
+ return runAsyncInternal(PARALLEL_EXECUTOR, runnable);
+ }
+
+ /**
+ * Runs a {@link Runnable} in a bg thread, using {@link #SERIAL_EXECUTOR}.
+ */
+ public static EmailAsyncTask<Void, Void, Void> runAsyncSerial(Runnable runnable) {
+ return runAsyncInternal(SERIAL_EXECUTOR, runnable);
+ }
+
+ private static EmailAsyncTask<Void, Void, Void> runAsyncInternal(Executor executor,
+ final Runnable runnable) {
+ EmailAsyncTask<Void, Void, Void> task = new EmailAsyncTask<Void, Void, Void>(null) {
+ @Override
+ protected Void doInBackground(Void... params) {
+ runnable.run();
+ return null;
+ }
+ };
+ return task.executeInternal(executor, false, (Void[]) null);
+ }
+
+ /**
+ * Wait until {@link #doInBackground} finishes and returns the results of the computation.
+ *
+ * @see AsyncTask#get
+ */
+ public final Result get() throws InterruptedException, ExecutionException {
+ return mInnerTask.get();
+ }
+
+ /* package */ final Result callDoInBackgroundForTest(Params... params) {
+ return mInnerTask.doInBackground(params);
+ }
+
+ /* package */ final void callOnCancelledForTest(Result result) {
+ mInnerTask.onCancelled(result);
+ }
+
+ /* package */ final void callOnPostExecuteForTest(Result result) {
+ mInnerTask.onPostExecute(result);
+ }
+}
diff --git a/email2/emailcommon/src/com/android/emailcommon/utility/EmailClientConnectionManager.java b/email2/emailcommon/src/com/android/emailcommon/utility/EmailClientConnectionManager.java
new file mode 100644
index 0000000..36ece42
--- /dev/null
+++ b/email2/emailcommon/src/com/android/emailcommon/utility/EmailClientConnectionManager.java
@@ -0,0 +1,148 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+
+package com.android.emailcommon.utility;
+
+import android.content.Context;
+import android.net.SSLCertificateSocketFactory;
+import android.util.Log;
+
+import com.android.emailcommon.Logging;
+import com.android.emailcommon.utility.SSLUtils.KeyChainKeyManager;
+import com.android.emailcommon.utility.SSLUtils.TrackingKeyManager;
+
+import org.apache.http.conn.scheme.PlainSocketFactory;
+import org.apache.http.conn.scheme.Scheme;
+import org.apache.http.conn.scheme.SchemeRegistry;
+import org.apache.http.impl.conn.tsccm.ThreadSafeClientConnManager;
+import org.apache.http.params.HttpParams;
+
+import java.security.cert.CertificateException;
+
+import javax.net.ssl.KeyManager;
+
+/**
+ * A thread-safe client connection manager that manages the use of client certificates from the
+ * {@link android.security.KeyChain} for SSL connections.
+ */
+public class EmailClientConnectionManager extends ThreadSafeClientConnManager {
+
+ private static final boolean LOG_ENABLED = false;
+
+ /**
+ * A {@link KeyManager} to track client certificate requests from servers.
+ */
+ private final TrackingKeyManager mTrackingKeyManager;
+
+ /**
+ * Not publicly instantiable except via {@link #newInstance(HttpParams)}
+ */
+ private EmailClientConnectionManager(
+ HttpParams params, SchemeRegistry registry, TrackingKeyManager keyManager) {
+ super(params, registry);
+ mTrackingKeyManager = keyManager;
+ }
+
+ public static EmailClientConnectionManager newInstance(HttpParams params) {
+ TrackingKeyManager keyManager = new TrackingKeyManager();
+
+ // Create a registry for our three schemes; http and https will use built-in factories
+ SchemeRegistry registry = new SchemeRegistry();
+ registry.register(new Scheme("http",
+ PlainSocketFactory.getSocketFactory(), 80));
+ registry.register(new Scheme("https",
+ SSLUtils.getHttpSocketFactory(false, keyManager), 443));
+
+ // Register the httpts scheme with our insecure factory
+ registry.register(new Scheme("httpts",
+ SSLUtils.getHttpSocketFactory(true /*insecure*/, keyManager), 443));
+
+ return new EmailClientConnectionManager(params, registry, keyManager);
+ }
+
+ /**
+ * Ensures that a client SSL certificate is known to be used for the specified connection
+ * manager.
+ * A {@link SchemeRegistry} is used to denote which client certificates to use for a given
+ * connection, so clients of this connection manager should use
+ * {@link #makeSchemeForClientCert(String, boolean)}.
+ */
+ public synchronized void registerClientCert(
+ Context context, String clientCertAlias, boolean trustAllServerCerts)
+ throws CertificateException {
+ SchemeRegistry registry = getSchemeRegistry();
+ String schemeName = makeSchemeForClientCert(clientCertAlias, trustAllServerCerts);
+ Scheme existing = registry.get(schemeName);
+ if (existing == null) {
+ if (LOG_ENABLED) {
+ Log.i(Logging.LOG_TAG, "Registering socket factory for certificate alias ["
+ + clientCertAlias + "]");
+ }
+ KeyManager keyManager = KeyChainKeyManager.fromAlias(context, clientCertAlias);
+ SSLCertificateSocketFactory underlying = SSLUtils.getSSLSocketFactory(
+ trustAllServerCerts);
+ underlying.setKeyManagers(new KeyManager[] { keyManager });
+ registry.register(new Scheme(schemeName, new SSLSocketFactory(underlying), 443));
+ }
+ }
+
+ /**
+ * Unregisters a custom connection type that uses a client certificate on the connection
+ * manager.
+ * @see #registerClientCert(Context, String, boolean)
+ */
+ public synchronized void unregisterClientCert(
+ String clientCertAlias, boolean trustAllServerCerts) {
+ SchemeRegistry registry = getSchemeRegistry();
+ String schemeName = makeSchemeForClientCert(clientCertAlias, trustAllServerCerts);
+ Scheme existing = registry.get(schemeName);
+ if (existing != null) {
+ registry.unregister(schemeName);
+ }
+ }
+
+ /**
+ * Builds a custom scheme name to be used in a connection manager according to the connection
+ * parameters.
+ */
+ public static String makeScheme(
+ boolean useSsl, boolean trustAllServerCerts, String clientCertAlias) {
+ if (clientCertAlias != null) {
+ return makeSchemeForClientCert(clientCertAlias, trustAllServerCerts);
+ } else {
+ return useSsl ? (trustAllServerCerts ? "httpts" : "https") : "http";
+ }
+ }
+
+ /**
+ * Builds a unique scheme name for an SSL connection that uses a client user certificate.
+ */
+ private static String makeSchemeForClientCert(
+ String clientCertAlias, boolean trustAllServerCerts) {
+ String safeAlias = SSLUtils.escapeForSchemeName(clientCertAlias);
+ return (trustAllServerCerts ? "httpts" : "https") + "+clientCert+" + safeAlias;
+ }
+
+ /**
+ * @param since A timestamp in millis from epoch from which to check
+ * @return whether or not this connection manager has detected any unsatisfied requests for
+ * a client SSL certificate by any servers
+ */
+ public synchronized boolean hasDetectedUnsatisfiedCertReq(long since) {
+ return mTrackingKeyManager.getLastCertReqTime() >= since;
+ }
+}
diff --git a/email2/emailcommon/src/com/android/emailcommon/utility/IntentUtilities.java b/email2/emailcommon/src/com/android/emailcommon/utility/IntentUtilities.java
new file mode 100644
index 0000000..d38caad
--- /dev/null
+++ b/email2/emailcommon/src/com/android/emailcommon/utility/IntentUtilities.java
@@ -0,0 +1,166 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.emailcommon.utility;
+
+
+import android.app.Activity;
+import android.content.Context;
+import android.content.Intent;
+import android.net.Uri;
+import android.text.TextUtils;
+
+public final class IntentUtilities {
+ // Format for activity URIs: content://ui.email.android.com/...
+ private static final String ACTIVITY_INTENT_SCHEME = "content";
+ private static final String ACTIVITY_INTENT_HOST = "ui.email.android.com";
+
+ private static final String ACCOUNT_ID_PARAM = "ACCOUNT_ID";
+ private static final String MAILBOX_ID_PARAM = "MAILBOX_ID";
+ private static final String MESSAGE_ID_PARAM = "MESSAGE_ID";
+ private static final String ACCOUNT_UUID_PARAM = "ACCOUNT_UUID";
+
+ private IntentUtilities() {
+ }
+
+ /**
+ * @return a URI builder for "content://ui.email.android.com/..."
+ */
+ public static Uri.Builder createActivityIntentUrlBuilder(String path) {
+ final Uri.Builder b = new Uri.Builder();
+ b.scheme(ACTIVITY_INTENT_SCHEME);
+ b.authority(ACTIVITY_INTENT_HOST);
+ b.path(path);
+ return b;
+ }
+
+ /**
+ * Add the account ID parameter.
+ */
+ public static void setAccountId(Uri.Builder b, long accountId) {
+ if (accountId != -1) {
+ b.appendQueryParameter(ACCOUNT_ID_PARAM, Long.toString(accountId));
+ }
+ }
+
+ /**
+ * Add the mailbox ID parameter.
+ */
+ public static void setMailboxId(Uri.Builder b, long mailboxId) {
+ if (mailboxId != -1) {
+ b.appendQueryParameter(MAILBOX_ID_PARAM, Long.toString(mailboxId));
+ }
+ }
+
+ /**
+ * Add the message ID parameter.
+ */
+ public static void setMessageId(Uri.Builder b, long messageId) {
+ if (messageId != -1) {
+ b.appendQueryParameter(MESSAGE_ID_PARAM, Long.toString(messageId));
+ }
+ }
+
+ /**
+ * Add the account UUID parameter.
+ */
+ public static void setAccountUuid(Uri.Builder b, String mUuid) {
+ if (TextUtils.isEmpty(mUuid)) {
+ throw new IllegalArgumentException();
+ }
+ b.appendQueryParameter(ACCOUNT_UUID_PARAM, mUuid);
+ }
+
+ /**
+ * Retrieve the account ID.
+ */
+ public static long getAccountIdFromIntent(Intent intent) {
+ return getLongFromIntent(intent, ACCOUNT_ID_PARAM);
+ }
+
+ /**
+ * Retrieve the mailbox ID.
+ */
+ public static long getMailboxIdFromIntent(Intent intent) {
+ return getLongFromIntent(intent, MAILBOX_ID_PARAM);
+ }
+
+ /**
+ * Retrieve the message ID.
+ */
+ public static long getMessageIdFromIntent(Intent intent) {
+ return getLongFromIntent(intent, MESSAGE_ID_PARAM);
+ }
+
+ /**
+ * Retrieve the account UUID, or null if the UUID param is not found.
+ */
+ public static String getAccountUuidFromIntent(Intent intent) {
+ final Uri uri = intent.getData();
+ if (uri == null) {
+ return null;
+ }
+ String uuid = uri.getQueryParameter(ACCOUNT_UUID_PARAM);
+ return TextUtils.isEmpty(uuid) ? null : uuid;
+ }
+
+ private static long getLongFromIntent(Intent intent, String paramName) {
+ long value = -1;
+ if (intent.getData() != null) {
+ value = getLongParamFromUri(intent.getData(), paramName, -1);
+ }
+ return value;
+ }
+
+ private static long getLongParamFromUri(Uri uri, String paramName, long defaultValue) {
+ final String value = uri.getQueryParameter(paramName);
+ if (!TextUtils.isEmpty(value)) {
+ try {
+ return Long.parseLong(value);
+ } catch (NumberFormatException e) {
+ // return default
+ }
+ }
+ return defaultValue;
+ }
+
+ /**
+ * Create an {@link Intent} to launch an activity as the main entry point. Existing activities
+ * will all be closed.
+ */
+ public static Intent createRestartAppIntent(Context context, Class<? extends Activity> clazz) {
+ Intent i = new Intent(context, clazz);
+ prepareRestartAppIntent(i);
+ return i;
+ }
+
+ /**
+ * Create an {@link Intent} to launch an activity as the main entry point. Existing activities
+ * will all be closed.
+ */
+ public static Intent createRestartAppIntent(Uri data) {
+ Intent i = new Intent(Intent.ACTION_MAIN, data);
+ prepareRestartAppIntent(i);
+ return i;
+ }
+
+ private static void prepareRestartAppIntent(Intent i) {
+ i.setAction(Intent.ACTION_MAIN);
+ i.addCategory(Intent.CATEGORY_LAUNCHER);
+ i.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
+ i.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ }
+}
diff --git a/email2/emailcommon/src/com/android/emailcommon/utility/LoggingInputStream.java b/email2/emailcommon/src/com/android/emailcommon/utility/LoggingInputStream.java
new file mode 100644
index 0000000..212efa1
--- /dev/null
+++ b/email2/emailcommon/src/com/android/emailcommon/utility/LoggingInputStream.java
@@ -0,0 +1,109 @@
+/*
+ * 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.emailcommon.utility;
+
+import com.android.emailcommon.Logging;
+
+import android.util.Log;
+
+import java.io.FilterInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+
+/**
+ * Simple class used for debugging only that affords us a view of the raw IMAP or POP3 stream,
+ * in addition to the tokenized version.
+ *
+ * Use of this class *MUST* be restricted to logging-enabled situations only.
+ */
+public class LoggingInputStream extends FilterInputStream {
+ private StringBuilder mSb;
+ private boolean mDumpEmptyLines;
+ private final String mTag;
+
+ public LoggingInputStream(InputStream in) {
+ this(in, "RAW", false);
+ }
+
+ public LoggingInputStream(InputStream in, String tag, boolean dumpEmptyLines) {
+ super(in);
+ mTag = tag + " ";
+ mDumpEmptyLines = dumpEmptyLines;
+ initBuffer();
+ Log.d(Logging.LOG_TAG, mTag + "dump start");
+ }
+
+ private void initBuffer() {
+ mSb = new StringBuilder(mTag);
+ }
+
+ /**
+ * Collect chars as read, and log them when EOL reached.
+ */
+ @Override
+ public int read() throws IOException {
+ int oneByte = super.read();
+ logRaw(oneByte);
+ return oneByte;
+ }
+
+ /**
+ * Collect chars as read, and log them when EOL reached.
+ */
+ @Override
+ public int read(byte[] b, int offset, int length) throws IOException {
+ int bytesRead = super.read(b, offset, length);
+ int copyBytes = bytesRead;
+ while (copyBytes > 0) {
+ logRaw(b[offset] & 0xFF);
+ copyBytes--;
+ offset++;
+ }
+
+ return bytesRead;
+ }
+
+ /**
+ * Write and clear the buffer
+ */
+ private void logRaw(int oneByte) {
+ if (oneByte == '\r') {
+ // Don't log.
+ } else if (oneByte == '\n') {
+ flushLog();
+ } else if (0x20 <= oneByte && oneByte <= 0x7e) { // Printable ASCII.
+ mSb.append((char)oneByte);
+ } else {
+ // email protocols are supposed to be all 7bits, but there are wrong implementations
+ // that do send 8 bit characters...
+ mSb.append("\\x" + Utility.byteToHex(oneByte));
+ }
+ }
+
+ private void flushLog() {
+ if (mDumpEmptyLines || (mSb.length() > mTag.length())) {
+ Log.d(Logging.LOG_TAG, mSb.toString());
+ initBuffer();
+ }
+ }
+
+ @Override
+ public void close() throws IOException {
+ super.close();
+ flushLog();
+ }
+}
diff --git a/email2/emailcommon/src/com/android/emailcommon/utility/SSLSocketFactory.java b/email2/emailcommon/src/com/android/emailcommon/utility/SSLSocketFactory.java
new file mode 100644
index 0000000..6247455
--- /dev/null
+++ b/email2/emailcommon/src/com/android/emailcommon/utility/SSLSocketFactory.java
@@ -0,0 +1,401 @@
+/*
+ * $HeadURL: http://svn.apache.org/repos/asf/httpcomponents/httpclient/trunk/module-client/src/main/java/org/apache/http/conn/ssl/SSLSocketFactory.java $
+ * $Revision: 659194 $
+ * $Date: 2008-05-22 11:33:47 -0700 (Thu, 22 May 2008) $
+ *
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ * This class was copied from org.apache.http.conn.ssl, because it didn't have a suitable
+ * constructor.
+ */
+
+package com.android.emailcommon.utility;
+
+import org.apache.http.conn.scheme.HostNameResolver;
+import org.apache.http.conn.scheme.LayeredSocketFactory;
+import org.apache.http.conn.ssl.AllowAllHostnameVerifier;
+import org.apache.http.conn.ssl.BrowserCompatHostnameVerifier;
+import org.apache.http.conn.ssl.StrictHostnameVerifier;
+import org.apache.http.conn.ssl.X509HostnameVerifier;
+import org.apache.http.params.HttpConnectionParams;
+import org.apache.http.params.HttpParams;
+
+import javax.net.ssl.HttpsURLConnection;
+import javax.net.ssl.KeyManager;
+import javax.net.ssl.KeyManagerFactory;
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.SSLSocket;
+import javax.net.ssl.TrustManager;
+import javax.net.ssl.TrustManagerFactory;
+import java.io.IOException;
+import java.net.InetAddress;
+import java.net.InetSocketAddress;
+import java.net.Socket;
+import java.net.UnknownHostException;
+import java.security.KeyManagementException;
+import java.security.KeyStore;
+import java.security.KeyStoreException;
+import java.security.NoSuchAlgorithmException;
+import java.security.SecureRandom;
+import java.security.UnrecoverableKeyException;
+
+/**
+ * Layered socket factory for TLS/SSL connections, based on JSSE.
+ *.
+ * <p>
+ * SSLSocketFactory can be used to validate the identity of the HTTPS
+ * server against a list of trusted certificates and to authenticate to
+ * the HTTPS server using a private key.
+ * </p>
+ *
+ * <p>
+ * SSLSocketFactory will enable server authentication when supplied with
+ * a {@link KeyStore truststore} file containg one or several trusted
+ * certificates. The client secure socket will reject the connection during
+ * the SSL session handshake if the target HTTPS server attempts to
+ * authenticate itself with a non-trusted certificate.
+ * </p>
+ *
+ * <p>
+ * Use JDK keytool utility to import a trusted certificate and generate a truststore file:
+ * <pre>
+ * keytool -import -alias "my server cert" -file server.crt -keystore my.truststore
+ * </pre>
+ * </p>
+ *
+ * <p>
+ * SSLSocketFactory will enable client authentication when supplied with
+ * a {@link KeyStore keystore} file containg a private key/public certificate
+ * pair. The client secure socket will use the private key to authenticate
+ * itself to the target HTTPS server during the SSL session handshake if
+ * requested to do so by the server.
+ * The target HTTPS server will in its turn verify the certificate presented
+ * by the client in order to establish client's authenticity
+ * </p>
+ *
+ * <p>
+ * Use the following sequence of actions to generate a keystore file
+ * </p>
+ * <ul>
+ * <li>
+ * <p>
+ * Use JDK keytool utility to generate a new key
+ * <pre>keytool -genkey -v -alias "my client key" -validity 365 -keystore my.keystore</pre>
+ * For simplicity use the same password for the key as that of the keystore
+ * </p>
+ * </li>
+ * <li>
+ * <p>
+ * Issue a certificate signing request (CSR)
+ * <pre>keytool -certreq -alias "my client key" -file mycertreq.csr -keystore my.keystore</pre>
+ * </p>
+ * </li>
+ * <li>
+ * <p>
+ * Send the certificate request to the trusted Certificate Authority for signature.
+ * One may choose to act as her own CA and sign the certificate request using a PKI
+ * tool, such as OpenSSL.
+ * </p>
+ * </li>
+ * <li>
+ * <p>
+ * Import the trusted CA root certificate
+ * <pre>keytool -import -alias "my trusted ca" -file caroot.crt -keystore my.keystore</pre>
+ * </p>
+ * </li>
+ * <li>
+ * <p>
+ * Import the PKCS#7 file containg the complete certificate chain
+ * <pre>keytool -import -alias "my client key" -file mycert.p7 -keystore my.keystore</pre>
+ * </p>
+ * </li>
+ * <li>
+ * <p>
+ * Verify the content the resultant keystore file
+ * <pre>keytool -list -v -keystore my.keystore</pre>
+ * </p>
+ * </li>
+ * </ul>
+ * @author <a href="mailto:oleg at ural.ru">Oleg Kalnichevski</a>
+ * @author Julius Davies
+ */
+
+public class SSLSocketFactory implements LayeredSocketFactory {
+
+ public static final String TLS = "TLS";
+ public static final String SSL = "SSL";
+ public static final String SSLV2 = "SSLv2";
+
+ public static final X509HostnameVerifier ALLOW_ALL_HOSTNAME_VERIFIER
+ = new AllowAllHostnameVerifier();
+
+ public static final X509HostnameVerifier BROWSER_COMPATIBLE_HOSTNAME_VERIFIER
+ = new BrowserCompatHostnameVerifier();
+
+ public static final X509HostnameVerifier STRICT_HOSTNAME_VERIFIER
+ = new StrictHostnameVerifier();
+ /**
+ * The factory using the default JVM settings for secure connections.
+ */
+ private static final SSLSocketFactory DEFAULT_FACTORY = new SSLSocketFactory();
+
+ /**
+ * Gets an singleton instance of the SSLProtocolSocketFactory.
+ * @return a SSLProtocolSocketFactory
+ */
+ public static SSLSocketFactory getSocketFactory() {
+ return DEFAULT_FACTORY;
+ }
+
+ private final SSLContext sslcontext;
+ private final javax.net.ssl.SSLSocketFactory socketfactory;
+ private final HostNameResolver nameResolver;
+ private X509HostnameVerifier hostnameVerifier = BROWSER_COMPATIBLE_HOSTNAME_VERIFIER;
+
+ public SSLSocketFactory(
+ String algorithm,
+ final KeyStore keystore,
+ final String keystorePassword,
+ final KeyStore truststore,
+ final SecureRandom random,
+ final HostNameResolver nameResolver)
+ throws NoSuchAlgorithmException, KeyManagementException, KeyStoreException, UnrecoverableKeyException
+ {
+ super();
+ if (algorithm == null) {
+ algorithm = TLS;
+ }
+ KeyManager[] keymanagers = null;
+ if (keystore != null) {
+ keymanagers = createKeyManagers(keystore, keystorePassword);
+ }
+ TrustManager[] trustmanagers = null;
+ if (truststore != null) {
+ trustmanagers = createTrustManagers(truststore);
+ }
+ sslcontext = SSLContext.getInstance(algorithm);
+ sslcontext.init(keymanagers, trustmanagers, random);
+ socketfactory = sslcontext.getSocketFactory();
+ this.nameResolver = nameResolver;
+ }
+
+ public SSLSocketFactory(
+ final KeyStore keystore,
+ final String keystorePassword,
+ final KeyStore truststore)
+ throws NoSuchAlgorithmException, KeyManagementException, KeyStoreException, UnrecoverableKeyException
+ {
+ this(TLS, keystore, keystorePassword, truststore, null, null);
+ }
+
+ public SSLSocketFactory(final KeyStore keystore, final String keystorePassword)
+ throws NoSuchAlgorithmException, KeyManagementException, KeyStoreException, UnrecoverableKeyException
+ {
+ this(TLS, keystore, keystorePassword, null, null, null);
+ }
+
+ public SSLSocketFactory(final KeyStore truststore)
+ throws NoSuchAlgorithmException, KeyManagementException, KeyStoreException, UnrecoverableKeyException
+ {
+ this(TLS, null, null, truststore, null, null);
+ }
+
+ /**
+ * Constructs an HttpClient SSLSocketFactory backed by the given JSSE
+ * SSLSocketFactory.
+ */
+ public SSLSocketFactory(javax.net.ssl.SSLSocketFactory socketfactory) {
+ super();
+ sslcontext = null;
+ this.socketfactory = socketfactory;
+ nameResolver = null;
+ }
+
+ /**
+ * Creates the default SSL socket factory.
+ * This constructor is used exclusively to instantiate the factory for
+ * {@link #getSocketFactory getSocketFactory}.
+ */
+ private SSLSocketFactory() {
+ super();
+ sslcontext = null;
+ socketfactory = HttpsURLConnection.getDefaultSSLSocketFactory();
+ nameResolver = null;
+ }
+
+ private static KeyManager[] createKeyManagers(final KeyStore keystore, final String password)
+ throws KeyStoreException, NoSuchAlgorithmException, UnrecoverableKeyException {
+ if (keystore == null) {
+ throw new IllegalArgumentException("Keystore may not be null");
+ }
+ KeyManagerFactory kmfactory = KeyManagerFactory.getInstance(
+ KeyManagerFactory.getDefaultAlgorithm());
+ kmfactory.init(keystore, password != null ? password.toCharArray(): null);
+ return kmfactory.getKeyManagers();
+ }
+
+ private static TrustManager[] createTrustManagers(final KeyStore keystore)
+ throws KeyStoreException, NoSuchAlgorithmException {
+ if (keystore == null) {
+ throw new IllegalArgumentException("Keystore may not be null");
+ }
+ TrustManagerFactory tmfactory = TrustManagerFactory.getInstance(
+ TrustManagerFactory.getDefaultAlgorithm());
+ tmfactory.init(keystore);
+ return tmfactory.getTrustManagers();
+ }
+
+
+ // non-javadoc, see interface org.apache.http.conn.SocketFactory
+ public Socket createSocket()
+ throws IOException {
+
+ // the cast makes sure that the factory is working as expected
+ return socketfactory.createSocket();
+ }
+
+
+ // non-javadoc, see interface org.apache.http.conn.SocketFactory
+ public Socket connectSocket(
+ final Socket sock,
+ final String host,
+ final int port,
+ final InetAddress localAddress,
+ int localPort,
+ final HttpParams params
+ ) throws IOException {
+
+ if (host == null) {
+ throw new IllegalArgumentException("Target host may not be null.");
+ }
+ if (params == null) {
+ throw new IllegalArgumentException("Parameters may not be null.");
+ }
+
+ SSLSocket sslsock = (SSLSocket)
+ ((sock != null) ? sock : createSocket());
+
+ if ((localAddress != null) || (localPort > 0)) {
+
+ // we need to bind explicitly
+ if (localPort < 0)
+ localPort = 0; // indicates "any"
+
+ InetSocketAddress isa =
+ new InetSocketAddress(localAddress, localPort);
+ sslsock.bind(isa);
+ }
+
+ int connTimeout = HttpConnectionParams.getConnectionTimeout(params);
+ int soTimeout = HttpConnectionParams.getSoTimeout(params);
+
+ InetSocketAddress remoteAddress;
+ if (nameResolver != null) {
+ remoteAddress = new InetSocketAddress(nameResolver.resolve(host), port);
+ } else {
+ remoteAddress = new InetSocketAddress(host, port);
+ }
+
+ sslsock.connect(remoteAddress, connTimeout);
+
+ sslsock.setSoTimeout(soTimeout);
+ try {
+ hostnameVerifier.verify(host, sslsock);
+ // verifyHostName() didn't blowup - good!
+ } catch (IOException iox) {
+ // close the socket before re-throwing the exception
+ try { sslsock.close(); } catch (Exception x) { /*ignore*/ }
+ throw iox;
+ }
+
+ return sslsock;
+ }
+
+
+ /**
+ * Checks whether a socket connection is secure.
+ * This factory creates TLS/SSL socket connections
+ * which, by default, are considered secure.
+ * <br/>
+ * Derived classes may override this method to perform
+ * runtime checks, for example based on the cypher suite.
+ *
+ * @param sock the connected socket
+ *
+ * @return <code>true</code>
+ *
+ * @throws IllegalArgumentException if the argument is invalid
+ */
+ public boolean isSecure(Socket sock)
+ throws IllegalArgumentException {
+
+ if (sock == null) {
+ throw new IllegalArgumentException("Socket may not be null.");
+ }
+ // This instanceof check is in line with createSocket() above.
+ if (!(sock instanceof SSLSocket)) {
+ throw new IllegalArgumentException
+ ("Socket not created by this factory.");
+ }
+ // This check is performed last since it calls the argument object.
+ if (sock.isClosed()) {
+ throw new IllegalArgumentException("Socket is closed.");
+ }
+
+ return true;
+
+ } // isSecure
+
+
+ // non-javadoc, see interface LayeredSocketFactory
+ public Socket createSocket(
+ final Socket socket,
+ final String host,
+ final int port,
+ final boolean autoClose
+ ) throws IOException, UnknownHostException {
+ SSLSocket sslSocket = (SSLSocket) socketfactory.createSocket(
+ socket,
+ host,
+ port,
+ autoClose
+ );
+ hostnameVerifier.verify(host, sslSocket);
+ // verifyHostName() didn't blowup - good!
+ return sslSocket;
+ }
+
+ public void setHostnameVerifier(X509HostnameVerifier hostnameVerifier) {
+ if ( hostnameVerifier == null ) {
+ throw new IllegalArgumentException("Hostname verifier may not be null");
+ }
+ this.hostnameVerifier = hostnameVerifier;
+ }
+
+ public X509HostnameVerifier getHostnameVerifier() {
+ return hostnameVerifier;
+ }
+
+}
diff --git a/email2/emailcommon/src/com/android/emailcommon/utility/SSLUtils.java b/email2/emailcommon/src/com/android/emailcommon/utility/SSLUtils.java
new file mode 100644
index 0000000..b21c68f
--- /dev/null
+++ b/email2/emailcommon/src/com/android/emailcommon/utility/SSLUtils.java
@@ -0,0 +1,272 @@
+/*
+ * 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.emailcommon.utility;
+
+import android.content.Context;
+import android.net.SSLCertificateSocketFactory;
+import android.security.KeyChain;
+import android.security.KeyChainException;
+import android.util.Log;
+
+import com.google.common.annotations.VisibleForTesting;
+
+import java.net.InetAddress;
+import java.net.Socket;
+import java.security.Principal;
+import java.security.PrivateKey;
+import java.security.cert.CertificateException;
+import java.security.cert.X509Certificate;
+import java.util.Arrays;
+
+import javax.net.ssl.KeyManager;
+import javax.net.ssl.X509ExtendedKeyManager;
+
+public class SSLUtils {
+ private static SSLCertificateSocketFactory sInsecureFactory;
+ private static SSLCertificateSocketFactory sSecureFactory;
+
+ private static final boolean LOG_ENABLED = false;
+ private static final String TAG = "Email.Ssl";
+
+ /**
+ * Returns a {@link javax.net.ssl.SSLSocketFactory}.
+ * Optionally bypass all SSL certificate checks.
+ *
+ * @param insecure if true, bypass all SSL certificate checks
+ */
+ public synchronized static SSLCertificateSocketFactory getSSLSocketFactory(
+ boolean insecure) {
+ if (insecure) {
+ if (sInsecureFactory == null) {
+ sInsecureFactory = (SSLCertificateSocketFactory)
+ SSLCertificateSocketFactory.getInsecure(0, null);
+ }
+ return sInsecureFactory;
+ } else {
+ if (sSecureFactory == null) {
+ sSecureFactory = (SSLCertificateSocketFactory)
+ SSLCertificateSocketFactory.getDefault(0, null);
+ }
+ return sSecureFactory;
+ }
+ }
+
+ /**
+ * Returns a {@link org.apache.http.conn.ssl.SSLSocketFactory SSLSocketFactory} for use with the
+ * Apache HTTP stack.
+ */
+ public static SSLSocketFactory getHttpSocketFactory(boolean insecure, KeyManager keyManager) {
+ SSLCertificateSocketFactory underlying = getSSLSocketFactory(insecure);
+ if (keyManager != null) {
+ underlying.setKeyManagers(new KeyManager[] { keyManager });
+ }
+ SSLSocketFactory wrapped = new SSLSocketFactory(underlying);
+ if (insecure) {
+ wrapped.setHostnameVerifier(SSLSocketFactory.ALLOW_ALL_HOSTNAME_VERIFIER);
+ }
+ return wrapped;
+ }
+
+ /**
+ * Escapes the contents a string to be used as a safe scheme name in the URI according to
+ * http://tools.ietf.org/html/rfc3986#section-3.1
+ *
+ * This does not ensure that the first character is a letter (which is required by the RFC).
+ */
+ @VisibleForTesting
+ public static String escapeForSchemeName(String s) {
+ // According to the RFC, scheme names are case-insensitive.
+ s = s.toLowerCase();
+
+ StringBuilder sb = new StringBuilder();
+ for (int i = 0; i < s.length(); i++) {
+ char c = s.charAt(i);
+ if (Character.isLetter(c) || Character.isDigit(c)
+ || ('-' == c) || ('.' == c)) {
+ // Safe - use as is.
+ sb.append(c);
+ } else if ('+' == c) {
+ // + is used as our escape character, so double it up.
+ sb.append("++");
+ } else {
+ // Unsafe - escape.
+ sb.append('+').append((int) c);
+ }
+ }
+ return sb.toString();
+ }
+
+ private static abstract class StubKeyManager extends X509ExtendedKeyManager {
+ @Override public abstract String chooseClientAlias(
+ String[] keyTypes, Principal[] issuers, Socket socket);
+
+ @Override public abstract X509Certificate[] getCertificateChain(String alias);
+
+ @Override public abstract PrivateKey getPrivateKey(String alias);
+
+
+ // The following methods are unused.
+
+ @Override
+ public final String chooseServerAlias(
+ String keyType, Principal[] issuers, Socket socket) {
+ // not a client SSLSocket callback
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public final String[] getClientAliases(String keyType, Principal[] issuers) {
+ // not a client SSLSocket callback
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public final String[] getServerAliases(String keyType, Principal[] issuers) {
+ // not a client SSLSocket callback
+ throw new UnsupportedOperationException();
+ }
+ }
+
+ /**
+ * A dummy {@link KeyManager} which keeps track of the last time a server has requested
+ * a client certificate.
+ */
+ public static class TrackingKeyManager extends StubKeyManager {
+ private volatile long mLastTimeCertRequested = 0L;
+
+ @Override
+ public String chooseClientAlias(String[] keyTypes, Principal[] issuers, Socket socket) {
+ if (LOG_ENABLED) {
+ InetAddress address = socket.getInetAddress();
+ Log.i(TAG, "TrackingKeyManager: requesting a client cert alias for "
+ + address.getCanonicalHostName());
+ }
+ mLastTimeCertRequested = System.currentTimeMillis();
+ return null;
+ }
+
+ @Override
+ public X509Certificate[] getCertificateChain(String alias) {
+ if (LOG_ENABLED) {
+ Log.i(TAG, "TrackingKeyManager: returning a null cert chain");
+ }
+ return null;
+ }
+
+ @Override
+ public PrivateKey getPrivateKey(String alias) {
+ if (LOG_ENABLED) {
+ Log.i(TAG, "TrackingKeyManager: returning a null private key");
+ }
+ return null;
+ }
+
+ /**
+ * @return the last time that this {@link KeyManager} detected a request by a server
+ * for a client certificate (in millis since epoch).
+ */
+ public long getLastCertReqTime() {
+ return mLastTimeCertRequested;
+ }
+ }
+
+ /**
+ * A {@link KeyManager} that reads uses credentials stored in the system {@link KeyChain}.
+ */
+ public static class KeyChainKeyManager extends StubKeyManager {
+ private final String mClientAlias;
+ private final X509Certificate[] mCertificateChain;
+ private final PrivateKey mPrivateKey;
+
+ /**
+ * Builds an instance of a KeyChainKeyManager using the given certificate alias.
+ * If for any reason retrieval of the credentials from the system {@link KeyChain} fails,
+ * a {@code null} value will be returned.
+ */
+ public static KeyChainKeyManager fromAlias(Context context, String alias)
+ throws CertificateException {
+ X509Certificate[] certificateChain;
+ try {
+ certificateChain = KeyChain.getCertificateChain(context, alias);
+ } catch (KeyChainException e) {
+ logError(alias, "certificate chain", e);
+ throw new CertificateException(e);
+ } catch (InterruptedException e) {
+ logError(alias, "certificate chain", e);
+ throw new CertificateException(e);
+ }
+
+ PrivateKey privateKey;
+ try {
+ privateKey = KeyChain.getPrivateKey(context, alias);
+ } catch (KeyChainException e) {
+ logError(alias, "private key", e);
+ throw new CertificateException(e);
+ } catch (InterruptedException e) {
+ logError(alias, "private key", e);
+ throw new CertificateException(e);
+ }
+
+ if (certificateChain == null || privateKey == null) {
+ throw new CertificateException("Can't access certificate from keystore");
+ }
+
+ return new KeyChainKeyManager(alias, certificateChain, privateKey);
+ }
+
+ private static void logError(String alias, String type, Exception ex) {
+ // Avoid logging PII when explicit logging is not on.
+ if (LOG_ENABLED) {
+ Log.e(TAG, "Unable to retrieve " + type + " for [" + alias + "] due to " + ex);
+ } else {
+ Log.e(TAG, "Unable to retrieve " + type + " due to " + ex);
+ }
+ }
+
+ private KeyChainKeyManager(
+ String clientAlias, X509Certificate[] certificateChain, PrivateKey privateKey) {
+ mClientAlias = clientAlias;
+ mCertificateChain = certificateChain;
+ mPrivateKey = privateKey;
+ }
+
+
+ @Override
+ public String chooseClientAlias(String[] keyTypes, Principal[] issuers, Socket socket) {
+ if (LOG_ENABLED) {
+ Log.i(TAG, "Requesting a client cert alias for " + Arrays.toString(keyTypes));
+ }
+ return mClientAlias;
+ }
+
+ @Override
+ public X509Certificate[] getCertificateChain(String alias) {
+ if (LOG_ENABLED) {
+ Log.i(TAG, "Requesting a client certificate chain for alias [" + alias + "]");
+ }
+ return mCertificateChain;
+ }
+
+ @Override
+ public PrivateKey getPrivateKey(String alias) {
+ if (LOG_ENABLED) {
+ Log.i(TAG, "Requesting a client private key for alias [" + alias + "]");
+ }
+ return mPrivateKey;
+ }
+ }
+}
diff --git a/email2/emailcommon/src/com/android/emailcommon/utility/TextUtilities.java b/email2/emailcommon/src/com/android/emailcommon/utility/TextUtilities.java
new file mode 100755
index 0000000..0aa9190
--- /dev/null
+++ b/email2/emailcommon/src/com/android/emailcommon/utility/TextUtilities.java
@@ -0,0 +1,728 @@
+/*
+ * 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.emailcommon.utility;
+
+import com.google.common.annotations.VisibleForTesting;
+
+import android.graphics.Color;
+import android.text.Spannable;
+import android.text.SpannableString;
+import android.text.SpannableStringBuilder;
+import android.text.TextUtils;
+import android.text.style.BackgroundColorSpan;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.StringTokenizer;
+
+public class TextUtilities {
+ // Highlight color is yellow, as in other apps.
+ // TODO Push for this to be a global (style-related?) constant
+ public static final int HIGHLIGHT_COLOR_INT = Color.YELLOW;
+ // We AND off the "alpha" from the color (i.e. 0xFFFFFF00 -> 0x00FFFF00)
+ /*package*/ static final String HIGHLIGHT_COLOR_STRING =
+ '#' + Integer.toHexString(HIGHLIGHT_COLOR_INT & 0x00FFFFFF);
+
+ // This is how many chars we'll allow in a snippet
+ private static final int MAX_SNIPPET_LENGTH = 200;
+ // For some reason, isWhitespace() returns false with the following...
+ /*package*/ static final char NON_BREAKING_SPACE_CHARACTER = (char)160;
+
+ // Tags whose content must be stripped as well
+ static final String[] STRIP_TAGS =
+ new String[] {"title", "script", "style", "applet", "head"};
+ // The number of characters we peel off for testing against STRIP_TAGS; this should be the
+ // maximum size of the strings in STRIP_TAGS
+ static final int MAX_STRIP_TAG_LENGTH = 6;
+
+ static final Map<String, Character> ESCAPE_STRINGS;
+ static {
+ // HTML character entity references as defined in HTML 4
+ // see http://www.w3.org/TR/REC-html40/sgml/entities.html
+ ESCAPE_STRINGS = new HashMap<String, Character>(252);
+
+ ESCAPE_STRINGS.put(" ", '\u00A0');
+ ESCAPE_STRINGS.put("¡", '\u00A1');
+ ESCAPE_STRINGS.put("¢", '\u00A2');
+ ESCAPE_STRINGS.put("£", '\u00A3');
+ ESCAPE_STRINGS.put("¤", '\u00A4');
+ ESCAPE_STRINGS.put("¥", '\u00A5');
+ ESCAPE_STRINGS.put("¦", '\u00A6');
+ ESCAPE_STRINGS.put("§", '\u00A7');
+ ESCAPE_STRINGS.put("¨", '\u00A8');
+ ESCAPE_STRINGS.put("©", '\u00A9');
+ ESCAPE_STRINGS.put("ª", '\u00AA');
+ ESCAPE_STRINGS.put("«", '\u00AB');
+ ESCAPE_STRINGS.put("¬", '\u00AC');
+ ESCAPE_STRINGS.put("­", '\u00AD');
+ ESCAPE_STRINGS.put("®", '\u00AE');
+ ESCAPE_STRINGS.put("¯", '\u00AF');
+ ESCAPE_STRINGS.put("°", '\u00B0');
+ ESCAPE_STRINGS.put("±", '\u00B1');
+ ESCAPE_STRINGS.put("²", '\u00B2');
+ ESCAPE_STRINGS.put("³", '\u00B3');
+ ESCAPE_STRINGS.put("´", '\u00B4');
+ ESCAPE_STRINGS.put("µ", '\u00B5');
+ ESCAPE_STRINGS.put("¶", '\u00B6');
+ ESCAPE_STRINGS.put("·", '\u00B7');
+ ESCAPE_STRINGS.put("¸", '\u00B8');
+ ESCAPE_STRINGS.put("¹", '\u00B9');
+ ESCAPE_STRINGS.put("º", '\u00BA');
+ ESCAPE_STRINGS.put("»", '\u00BB');
+ ESCAPE_STRINGS.put("¼", '\u00BC');
+ ESCAPE_STRINGS.put("½", '\u00BD');
+ ESCAPE_STRINGS.put("¾", '\u00BE');
+ ESCAPE_STRINGS.put("¿", '\u00BF');
+ ESCAPE_STRINGS.put("À", '\u00C0');
+ ESCAPE_STRINGS.put("Á", '\u00C1');
+ ESCAPE_STRINGS.put("Â", '\u00C2');
+ ESCAPE_STRINGS.put("Ã", '\u00C3');
+ ESCAPE_STRINGS.put("Ä", '\u00C4');
+ ESCAPE_STRINGS.put("Å", '\u00C5');
+ ESCAPE_STRINGS.put("Æ", '\u00C6');
+ ESCAPE_STRINGS.put("Ç", '\u00C7');
+ ESCAPE_STRINGS.put("È", '\u00C8');
+ ESCAPE_STRINGS.put("É", '\u00C9');
+ ESCAPE_STRINGS.put("Ê", '\u00CA');
+ ESCAPE_STRINGS.put("Ë", '\u00CB');
+ ESCAPE_STRINGS.put("Ì", '\u00CC');
+ ESCAPE_STRINGS.put("Í", '\u00CD');
+ ESCAPE_STRINGS.put("Î", '\u00CE');
+ ESCAPE_STRINGS.put("Ï", '\u00CF');
+ ESCAPE_STRINGS.put("Ð", '\u00D0');
+ ESCAPE_STRINGS.put("Ñ", '\u00D1');
+ ESCAPE_STRINGS.put("Ò", '\u00D2');
+ ESCAPE_STRINGS.put("Ó", '\u00D3');
+ ESCAPE_STRINGS.put("Ô", '\u00D4');
+ ESCAPE_STRINGS.put("Õ", '\u00D5');
+ ESCAPE_STRINGS.put("Ö", '\u00D6');
+ ESCAPE_STRINGS.put("×", '\u00D7');
+ ESCAPE_STRINGS.put("Ø", '\u00D8');
+ ESCAPE_STRINGS.put("Ù", '\u00D9');
+ ESCAPE_STRINGS.put("Ú", '\u00DA');
+ ESCAPE_STRINGS.put("Û", '\u00DB');
+ ESCAPE_STRINGS.put("Ü", '\u00DC');
+ ESCAPE_STRINGS.put("Ý", '\u00DD');
+ ESCAPE_STRINGS.put("Þ", '\u00DE');
+ ESCAPE_STRINGS.put("ß", '\u00DF');
+ ESCAPE_STRINGS.put("à", '\u00E0');
+ ESCAPE_STRINGS.put("á", '\u00E1');
+ ESCAPE_STRINGS.put("â", '\u00E2');
+ ESCAPE_STRINGS.put("ã", '\u00E3');
+ ESCAPE_STRINGS.put("ä", '\u00E4');
+ ESCAPE_STRINGS.put("å", '\u00E5');
+ ESCAPE_STRINGS.put("æ", '\u00E6');
+ ESCAPE_STRINGS.put("ç", '\u00E7');
+ ESCAPE_STRINGS.put("è", '\u00E8');
+ ESCAPE_STRINGS.put("é", '\u00E9');
+ ESCAPE_STRINGS.put("ê", '\u00EA');
+ ESCAPE_STRINGS.put("ë", '\u00EB');
+ ESCAPE_STRINGS.put("ì", '\u00EC');
+ ESCAPE_STRINGS.put("í", '\u00ED');
+ ESCAPE_STRINGS.put("î", '\u00EE');
+ ESCAPE_STRINGS.put("ï", '\u00EF');
+ ESCAPE_STRINGS.put("ð", '\u00F0');
+ ESCAPE_STRINGS.put("ñ", '\u00F1');
+ ESCAPE_STRINGS.put("ò", '\u00F2');
+ ESCAPE_STRINGS.put("ó", '\u00F3');
+ ESCAPE_STRINGS.put("ô", '\u00F4');
+ ESCAPE_STRINGS.put("õ", '\u00F5');
+ ESCAPE_STRINGS.put("ö", '\u00F6');
+ ESCAPE_STRINGS.put("÷", '\u00F7');
+ ESCAPE_STRINGS.put("ø", '\u00F8');
+ ESCAPE_STRINGS.put("ù", '\u00F9');
+ ESCAPE_STRINGS.put("ú", '\u00FA');
+ ESCAPE_STRINGS.put("û", '\u00FB');
+ ESCAPE_STRINGS.put("ü", '\u00FC');
+ ESCAPE_STRINGS.put("ý", '\u00FD');
+ ESCAPE_STRINGS.put("þ", '\u00FE');
+ ESCAPE_STRINGS.put("ÿ", '\u00FF');
+ ESCAPE_STRINGS.put("&fnof", '\u0192');
+ ESCAPE_STRINGS.put("&Alpha", '\u0391');
+ ESCAPE_STRINGS.put("&Beta", '\u0392');
+ ESCAPE_STRINGS.put("&Gamma", '\u0393');
+ ESCAPE_STRINGS.put("&Delta", '\u0394');
+ ESCAPE_STRINGS.put("&Epsilon", '\u0395');
+ ESCAPE_STRINGS.put("&Zeta", '\u0396');
+ ESCAPE_STRINGS.put("&Eta", '\u0397');
+ ESCAPE_STRINGS.put("&Theta", '\u0398');
+ ESCAPE_STRINGS.put("&Iota", '\u0399');
+ ESCAPE_STRINGS.put("&Kappa", '\u039A');
+ ESCAPE_STRINGS.put("&Lambda", '\u039B');
+ ESCAPE_STRINGS.put("&Mu", '\u039C');
+ ESCAPE_STRINGS.put("&Nu", '\u039D');
+ ESCAPE_STRINGS.put("&Xi", '\u039E');
+ ESCAPE_STRINGS.put("&Omicron", '\u039F');
+ ESCAPE_STRINGS.put("&Pi", '\u03A0');
+ ESCAPE_STRINGS.put("&Rho", '\u03A1');
+ ESCAPE_STRINGS.put("&Sigma", '\u03A3');
+ ESCAPE_STRINGS.put("&Tau", '\u03A4');
+ ESCAPE_STRINGS.put("&Upsilon", '\u03A5');
+ ESCAPE_STRINGS.put("&Phi", '\u03A6');
+ ESCAPE_STRINGS.put("&Chi", '\u03A7');
+ ESCAPE_STRINGS.put("&Psi", '\u03A8');
+ ESCAPE_STRINGS.put("&Omega", '\u03A9');
+ ESCAPE_STRINGS.put("&alpha", '\u03B1');
+ ESCAPE_STRINGS.put("&beta", '\u03B2');
+ ESCAPE_STRINGS.put("&gamma", '\u03B3');
+ ESCAPE_STRINGS.put("&delta", '\u03B4');
+ ESCAPE_STRINGS.put("&epsilon", '\u03B5');
+ ESCAPE_STRINGS.put("&zeta", '\u03B6');
+ ESCAPE_STRINGS.put("&eta", '\u03B7');
+ ESCAPE_STRINGS.put("&theta", '\u03B8');
+ ESCAPE_STRINGS.put("&iota", '\u03B9');
+ ESCAPE_STRINGS.put("&kappa", '\u03BA');
+ ESCAPE_STRINGS.put("&lambda", '\u03BB');
+ ESCAPE_STRINGS.put("&mu", '\u03BC');
+ ESCAPE_STRINGS.put("&nu", '\u03BD');
+ ESCAPE_STRINGS.put("&xi", '\u03BE');
+ ESCAPE_STRINGS.put("&omicron", '\u03BF');
+ ESCAPE_STRINGS.put("&pi", '\u03C0');
+ ESCAPE_STRINGS.put("&rho", '\u03C1');
+ ESCAPE_STRINGS.put("&sigmaf", '\u03C2');
+ ESCAPE_STRINGS.put("&sigma", '\u03C3');
+ ESCAPE_STRINGS.put("&tau", '\u03C4');
+ ESCAPE_STRINGS.put("&upsilon", '\u03C5');
+ ESCAPE_STRINGS.put("&phi", '\u03C6');
+ ESCAPE_STRINGS.put("&chi", '\u03C7');
+ ESCAPE_STRINGS.put("&psi", '\u03C8');
+ ESCAPE_STRINGS.put("&omega", '\u03C9');
+ ESCAPE_STRINGS.put("&thetasym", '\u03D1');
+ ESCAPE_STRINGS.put("&upsih", '\u03D2');
+ ESCAPE_STRINGS.put("&piv", '\u03D6');
+ ESCAPE_STRINGS.put("&bull", '\u2022');
+ ESCAPE_STRINGS.put("&hellip", '\u2026');
+ ESCAPE_STRINGS.put("&prime", '\u2032');
+ ESCAPE_STRINGS.put("&Prime", '\u2033');
+ ESCAPE_STRINGS.put("&oline", '\u203E');
+ ESCAPE_STRINGS.put("&frasl", '\u2044');
+ ESCAPE_STRINGS.put("&weierp", '\u2118');
+ ESCAPE_STRINGS.put("&image", '\u2111');
+ ESCAPE_STRINGS.put("&real", '\u211C');
+ ESCAPE_STRINGS.put("&trade", '\u2122');
+ ESCAPE_STRINGS.put("&alefsym", '\u2135');
+ ESCAPE_STRINGS.put("&larr", '\u2190');
+ ESCAPE_STRINGS.put("&uarr", '\u2191');
+ ESCAPE_STRINGS.put("&rarr", '\u2192');
+ ESCAPE_STRINGS.put("&darr", '\u2193');
+ ESCAPE_STRINGS.put("&harr", '\u2194');
+ ESCAPE_STRINGS.put("&crarr", '\u21B5');
+ ESCAPE_STRINGS.put("&lArr", '\u21D0');
+ ESCAPE_STRINGS.put("&uArr", '\u21D1');
+ ESCAPE_STRINGS.put("&rArr", '\u21D2');
+ ESCAPE_STRINGS.put("&dArr", '\u21D3');
+ ESCAPE_STRINGS.put("&hArr", '\u21D4');
+ ESCAPE_STRINGS.put("&forall", '\u2200');
+ ESCAPE_STRINGS.put("&part", '\u2202');
+ ESCAPE_STRINGS.put("&exist", '\u2203');
+ ESCAPE_STRINGS.put("&empty", '\u2205');
+ ESCAPE_STRINGS.put("&nabla", '\u2207');
+ ESCAPE_STRINGS.put("&isin", '\u2208');
+ ESCAPE_STRINGS.put("¬in", '\u2209');
+ ESCAPE_STRINGS.put("&ni", '\u220B');
+ ESCAPE_STRINGS.put("&prod", '\u220F');
+ ESCAPE_STRINGS.put("&sum", '\u2211');
+ ESCAPE_STRINGS.put("&minus", '\u2212');
+ ESCAPE_STRINGS.put("&lowast", '\u2217');
+ ESCAPE_STRINGS.put("&radic", '\u221A');
+ ESCAPE_STRINGS.put("&prop", '\u221D');
+ ESCAPE_STRINGS.put("&infin", '\u221E');
+ ESCAPE_STRINGS.put("&ang", '\u2220');
+ ESCAPE_STRINGS.put("&and", '\u2227');
+ ESCAPE_STRINGS.put("&or", '\u2228');
+ ESCAPE_STRINGS.put("&cap", '\u2229');
+ ESCAPE_STRINGS.put("&cup", '\u222A');
+ ESCAPE_STRINGS.put("&int", '\u222B');
+ ESCAPE_STRINGS.put("&there4", '\u2234');
+ ESCAPE_STRINGS.put("&sim", '\u223C');
+ ESCAPE_STRINGS.put("&cong", '\u2245');
+ ESCAPE_STRINGS.put("&asymp", '\u2248');
+ ESCAPE_STRINGS.put("&ne", '\u2260');
+ ESCAPE_STRINGS.put("&equiv", '\u2261');
+ ESCAPE_STRINGS.put("&le", '\u2264');
+ ESCAPE_STRINGS.put("&ge", '\u2265');
+ ESCAPE_STRINGS.put("&sub", '\u2282');
+ ESCAPE_STRINGS.put("&sup", '\u2283');
+ ESCAPE_STRINGS.put("&nsub", '\u2284');
+ ESCAPE_STRINGS.put("&sube", '\u2286');
+ ESCAPE_STRINGS.put("&supe", '\u2287');
+ ESCAPE_STRINGS.put("&oplus", '\u2295');
+ ESCAPE_STRINGS.put("&otimes", '\u2297');
+ ESCAPE_STRINGS.put("&perp", '\u22A5');
+ ESCAPE_STRINGS.put("&sdot", '\u22C5');
+ ESCAPE_STRINGS.put("&lceil", '\u2308');
+ ESCAPE_STRINGS.put("&rceil", '\u2309');
+ ESCAPE_STRINGS.put("&lfloor", '\u230A');
+ ESCAPE_STRINGS.put("&rfloor", '\u230B');
+ ESCAPE_STRINGS.put("&lang", '\u2329');
+ ESCAPE_STRINGS.put("&rang", '\u232A');
+ ESCAPE_STRINGS.put("&loz", '\u25CA');
+ ESCAPE_STRINGS.put("&spades", '\u2660');
+ ESCAPE_STRINGS.put("&clubs", '\u2663');
+ ESCAPE_STRINGS.put("&hearts", '\u2665');
+ ESCAPE_STRINGS.put("&diams", '\u2666');
+ ESCAPE_STRINGS.put(""", '\u0022');
+ ESCAPE_STRINGS.put("&", '\u0026');
+ ESCAPE_STRINGS.put("<", '\u003C');
+ ESCAPE_STRINGS.put(">", '\u003E');
+ ESCAPE_STRINGS.put("&OElig", '\u0152');
+ ESCAPE_STRINGS.put("&oelig", '\u0153');
+ ESCAPE_STRINGS.put("&Scaron", '\u0160');
+ ESCAPE_STRINGS.put("&scaron", '\u0161');
+ ESCAPE_STRINGS.put("&Yuml", '\u0178');
+ ESCAPE_STRINGS.put("&circ", '\u02C6');
+ ESCAPE_STRINGS.put("&tilde", '\u02DC');
+ ESCAPE_STRINGS.put("&ensp", '\u2002');
+ ESCAPE_STRINGS.put("&emsp", '\u2003');
+ ESCAPE_STRINGS.put("&thinsp", '\u2009');
+ ESCAPE_STRINGS.put("&zwnj", '\u200C');
+ ESCAPE_STRINGS.put("&zwj", '\u200D');
+ ESCAPE_STRINGS.put("&lrm", '\u200E');
+ ESCAPE_STRINGS.put("&rlm", '\u200F');
+ ESCAPE_STRINGS.put("&ndash", '\u2013');
+ ESCAPE_STRINGS.put("&mdash", '\u2014');
+ ESCAPE_STRINGS.put("&lsquo", '\u2018');
+ ESCAPE_STRINGS.put("&rsquo", '\u2019');
+ ESCAPE_STRINGS.put("&sbquo", '\u201A');
+ ESCAPE_STRINGS.put("&ldquo", '\u201C');
+ ESCAPE_STRINGS.put("&rdquo", '\u201D');
+ ESCAPE_STRINGS.put("&bdquo", '\u201E');
+ ESCAPE_STRINGS.put("&dagger", '\u2020');
+ ESCAPE_STRINGS.put("&Dagger", '\u2021');
+ ESCAPE_STRINGS.put("&permil", '\u2030');
+ ESCAPE_STRINGS.put("&lsaquo", '\u2039');
+ ESCAPE_STRINGS.put("&rsaquo", '\u203A');
+ ESCAPE_STRINGS.put("&euro", '\u20AC');
+ }
+
+ /**
+ * Code to generate a short 'snippet' from either plain text or html text
+ *
+ * If the sync protocol can get plain text, that's great, but we'll still strip out extraneous
+ * whitespace. If it's HTML, we'll 1) strip out tags, 2) turn entities into the appropriate
+ * characters, and 3) strip out extraneous whitespace, all in one pass
+ *
+ * Why not use an existing class? The best answer is performance; yet another answer is
+ * correctness (e.g. Html.textFromHtml simply doesn't generate well-stripped text). But
+ * performance is key; we frequently sync text that is 10K or (much) longer, yet we really only
+ * care about a small amount of text for the snippet. So it's critically important that we just
+ * stop when we've gotten enough; existing methods that exist will go through the entire
+ * incoming string, at great (and useless, in this case) expense.
+ */
+
+ public static String makeSnippetFromHtmlText(String text) {
+ return makeSnippetFromText(text, true);
+ }
+
+ public static String makeSnippetFromPlainText(String text) {
+ return makeSnippetFromText(text, false);
+ }
+
+ /**
+ * Find the end of this tag; there are two alternatives: <tag .../> or <tag ...> ... </tag>
+ * @param htmlText some HTML text
+ * @param tag the HTML tag
+ * @param startPos the start position in the HTML text where the tag starts
+ * @return the position just before the end of the tag or -1 if not found
+ */
+ /*package*/ static int findTagEnd(String htmlText, String tag, int startPos) {
+ if (tag.endsWith(" ")) {
+ tag = tag.substring(0, tag.length() - 1);
+ }
+ int length = htmlText.length();
+ char prevChar = 0;
+ for (int i = startPos; i < length; i++) {
+ char c = htmlText.charAt(i);
+ if (c == '>') {
+ if (prevChar == '/') {
+ return i - 1;
+ }
+ break;
+ }
+ prevChar = c;
+ }
+ // We didn't find /> at the end of the tag so find </tag>
+ return htmlText.indexOf("/" + tag, startPos);
+ }
+
+ public static String makeSnippetFromText(String text, boolean stripHtml) {
+ // Handle null and empty string
+ if (TextUtils.isEmpty(text)) return "";
+
+ final int length = text.length();
+ // Use char[] instead of StringBuilder purely for performance; fewer method calls, etc.
+ char[] buffer = new char[MAX_SNIPPET_LENGTH];
+ // skipCount is an array of a single int; that int is set inside stripHtmlEntity and is
+ // used to determine how many characters can be "skipped" due to the transformation of the
+ // entity to a single character. When Java allows multiple return values, we can make this
+ // much cleaner :-)
+ int[] skipCount = new int[1];
+ int bufferCount = 0;
+ // Start with space as last character to avoid leading whitespace
+ char last = ' ';
+ // Indicates whether we're in the middle of an HTML tag
+ boolean inTag = false;
+
+ // Walk through the text until we're done with the input OR we've got a large enough snippet
+ for (int i = 0; i < length && bufferCount < MAX_SNIPPET_LENGTH; i++) {
+ char c = text.charAt(i);
+ if (stripHtml && !inTag && (c == '<')) {
+ // Find tags to strip; they will begin with <! or !- or </ or <letter
+ if (i < (length - 1)) {
+ char peek = text.charAt(i + 1);
+ if (peek == '!' || peek == '-' || peek == '/' || Character.isLetter(peek)) {
+ inTag = true;
+ // Strip content of title, script, style and applet tags
+ if (i < (length - (MAX_STRIP_TAG_LENGTH + 2))) {
+ String tag = text.substring(i + 1, i + MAX_STRIP_TAG_LENGTH + 1);
+ String tagLowerCase = tag.toLowerCase();
+ boolean stripContent = false;
+ for (String stripTag: STRIP_TAGS) {
+ if (tagLowerCase.startsWith(stripTag)) {
+ stripContent = true;
+ tag = tag.substring(0, stripTag.length());
+ break;
+ }
+ }
+ if (stripContent) {
+ // Look for the end of this tag
+ int endTagPosition = findTagEnd(text, tag, i);
+ if (endTagPosition < 0) {
+ break;
+ } else {
+ i = endTagPosition;
+ }
+ }
+ }
+ }
+ }
+ } else if (stripHtml && inTag && (c == '>')) {
+ // Terminate stripping here
+ inTag = false;
+ continue;
+ }
+
+ if (inTag) {
+ // We just skip by everything while we're in a tag
+ continue;
+ } else if (stripHtml && (c == '&')) {
+ // Handle a possible HTML entity here
+ // We always get back a character to use; we also get back a "skip count",
+ // indicating how many characters were eaten from the entity
+ c = stripHtmlEntity(text, i, skipCount);
+ i += skipCount[0];
+ }
+
+ if (Character.isWhitespace(c) || (c == NON_BREAKING_SPACE_CHARACTER)) {
+ // The idea is to find the content in the message, not the whitespace, so we'll
+ // turn any combination of contiguous whitespace into a single space
+ if (last == ' ') {
+ continue;
+ } else {
+ // Make every whitespace character a simple space
+ c = ' ';
+ }
+ } else if ((c == '-' || c == '=') && (last == c)) {
+ // Lots of messages (especially digests) have whole lines of --- or ===
+ // We'll get rid of those duplicates here
+ continue;
+ }
+
+ // After all that, maybe we've got a character for our snippet
+ buffer[bufferCount++] = c;
+ last = c;
+ }
+
+ // Lose trailing space and return our snippet
+ if ((bufferCount > 0) && (last == ' ')) {
+ bufferCount--;
+ }
+ return new String(buffer, 0, bufferCount);
+ }
+
+ static /*package*/ char stripHtmlEntity(String text, int pos, int[] skipCount) {
+ int length = text.length();
+ // Ugly, but we store our skip count in this array; we can't use a static here, because
+ // multiple threads might be calling in
+ skipCount[0] = 0;
+ // All entities are <= 8 characters long, so that's how far we'll look for one (+ & and ;)
+ int end = pos + 10;
+ String entity = null;
+ // Isolate the entity
+ for (int i = pos; (i < length) && (i < end); i++) {
+ if (text.charAt(i) == ';') {
+ entity = text.substring(pos, i);
+ break;
+ }
+ }
+ if (entity == null) {
+ // This wasn't really an HTML entity
+ return '&';
+ } else {
+ // Skip count is the length of the entity
+ Character mapping = ESCAPE_STRINGS.get(entity);
+ int entityLength = entity.length();
+ if (mapping != null) {
+ skipCount[0] = entityLength;
+ return mapping;
+ } else if ((entityLength > 2) && (entity.charAt(1) == '#')) {
+ // &#nn; means ascii nn (decimal) and &#xnn means ascii nn (hex)
+ char c = '?';
+ try {
+ int i;
+ if ((entity.charAt(2) == 'x') && (entityLength > 3)) {
+ i = Integer.parseInt(entity.substring(3), 16);
+ } else {
+ i = Integer.parseInt(entity.substring(2));
+ }
+ c = (char)i;
+ } catch (NumberFormatException e) {
+ // We'll just return the ? in this case
+ }
+ skipCount[0] = entityLength;
+ return c;
+ }
+ }
+ // Worst case, we return the original start character, ampersand
+ return '&';
+ }
+
+ /**
+ * Given a string of HTML text and a query containing any number of search terms, returns
+ * an HTML string in which those search terms are highlighted (intended for use in a WebView)
+ *
+ * @param text the HTML text to process
+ * @param query the search terms
+ * @return HTML text with the search terms highlighted
+ */
+ @VisibleForTesting
+ public static String highlightTermsInHtml(String text, String query) {
+ try {
+ return highlightTerms(text, query, true).toString();
+ } catch (IOException e) {
+ // Can't happen, but we must catch this
+ return text;
+ }
+ }
+
+ /**
+ * Given a string of plain text and a query containing any number of search terms, returns
+ * a CharSequence in which those search terms are highlighted (intended for use in a TextView)
+ *
+ * @param text the text to process
+ * @param query the search terms
+ * @return a CharSequence with the search terms highlighted
+ */
+ public static CharSequence highlightTermsInText(String text, String query) {
+ try {
+ return highlightTerms(text, query, false);
+ } catch (IOException e) {
+ // Can't happen, but we must catch this
+ return text;
+ }
+ }
+
+ static class SearchTerm {
+ final String mTerm;
+ final String mTermLowerCase;
+ final int mLength;
+ int mMatchLength = 0;
+ int mMatchStart = -1;
+
+ SearchTerm(String term, boolean html) {
+ mTerm = term;
+ mTermLowerCase = term.toLowerCase();
+ mLength = term.length();
+ }
+ }
+
+ /**
+ * Generate a version of the incoming text in which all search terms in a query are highlighted.
+ * If the input is HTML, we return a StringBuilder with additional markup as required
+ * If the input is text, we return a SpannableStringBuilder with additional spans as required
+ *
+ * @param text the text to be processed
+ * @param query the query, which can contain multiple terms separated by whitespace
+ * @param html whether or not the text to be processed is HTML
+ * @return highlighted text
+ *
+ * @throws IOException as Appendable requires this
+ */
+ public static CharSequence highlightTerms(String text, String query, boolean html)
+ throws IOException {
+ // Handle null and empty string
+ if (TextUtils.isEmpty(text)) return "";
+ final int length = text.length();
+
+ // Break up the query into search terms
+ ArrayList<SearchTerm> terms = new ArrayList<SearchTerm>();
+ if (query != null) {
+ StringTokenizer st = new StringTokenizer(query);
+ while (st.hasMoreTokens()) {
+ terms.add(new SearchTerm(st.nextToken(), html));
+ }
+ }
+
+ // Our appendable depends on whether we're building HTML text (for webview) or spannable
+ // text (for UI)
+ final Appendable sb = html ? new StringBuilder() : new SpannableStringBuilder();
+ // Indicates whether we're in the middle of an HTML tag
+ boolean inTag = false;
+ // The position of the last input character copied to output
+ int lastOut = -1;
+
+ // Walk through the text until we're done with the input
+ // Just copy any HTML tags directly into the output; search for terms in the remaining text
+ for (int i = 0; i < length; i++) {
+ char chr = text.charAt(i);
+ if (html) {
+ if (!inTag && (chr == '<')) {
+ // Find tags; they will begin with <! or !- or </ or <letter
+ if (i < (length - 1)) {
+ char peek = text.charAt(i + 1);
+ if (peek == '!' || peek == '-' || peek == '/' || Character.isLetter(peek)) {
+ inTag = true;
+ // Skip content of title, script, style and applet tags
+ if (i < (length - (MAX_STRIP_TAG_LENGTH + 2))) {
+ String tag = text.substring(i + 1, i + MAX_STRIP_TAG_LENGTH + 1);
+ String tagLowerCase = tag.toLowerCase();
+ boolean stripContent = false;
+ for (String stripTag: STRIP_TAGS) {
+ if (tagLowerCase.startsWith(stripTag)) {
+ stripContent = true;
+ tag = tag.substring(0, stripTag.length());
+ break;
+ }
+ }
+ if (stripContent) {
+ // Look for the end of this tag
+ int endTagPosition = findTagEnd(text, tag, i);
+ if (endTagPosition < 0) {
+ sb.append(text.substring(i));
+ break;
+ } else {
+ sb.append(text.substring(i, endTagPosition - 1));
+ i = endTagPosition - 1;
+ chr = text.charAt(i);
+ }
+ }
+ }
+ }
+ }
+ } else if (inTag && (chr == '>')) {
+ inTag = false;
+ }
+
+ if (inTag) {
+ sb.append(chr);
+ continue;
+ }
+ }
+
+ // After all that, we've got some "body" text
+ char chrLowerCase = Character.toLowerCase(chr);
+ // Whether or not the current character should be appended to the output; we inhibit
+ // this while any search terms match
+ boolean appendNow = true;
+ // Look through search terms for matches
+ for (SearchTerm t: terms) {
+ if (chrLowerCase == t.mTermLowerCase.charAt(t.mMatchLength)) {
+ if (t.mMatchLength++ == 0) {
+ // New match start
+ t.mMatchStart = i;
+ }
+ if (t.mMatchLength == t.mLength) {
+ String matchText = text.substring(t.mMatchStart, t.mMatchStart + t.mLength);
+ // Completed match; add highlight and reset term
+ if (t.mMatchStart <= lastOut) {
+ matchText = text.substring(lastOut + 1, i + 1);
+ }
+ /*else*/
+ if (matchText.length() == 0) {} else
+ if (html) {
+ sb.append("<span style=\"background-color: " + HIGHLIGHT_COLOR_STRING +
+ "\">");
+ sb.append(matchText);
+ sb.append("</span>");
+ } else {
+ SpannableString highlightSpan = new SpannableString(matchText);
+ highlightSpan.setSpan(new BackgroundColorSpan(HIGHLIGHT_COLOR_INT), 0,
+ highlightSpan.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
+ sb.append(highlightSpan);
+ }
+ lastOut = t.mMatchStart + t.mLength - 1;
+ t.mMatchLength = 0;
+ t.mMatchStart = -1;
+ }
+ appendNow = false;
+ } else {
+ if (t.mMatchStart >= 0) {
+ // We're no longer matching; check for other matches in progress
+ int leastOtherStart = -1;
+ for (SearchTerm ot: terms) {
+ // Save away the lowest match start for other search terms
+ if ((ot != t) && (ot.mMatchStart >= 0) && ((leastOtherStart < 0) ||
+ (ot.mMatchStart <= leastOtherStart))) {
+ leastOtherStart = ot.mMatchStart;
+ }
+ }
+ int matchEnd = t.mMatchStart + t.mMatchLength;
+ if (leastOtherStart < 0 || leastOtherStart > matchEnd) {
+ // Append the whole thing
+ if (t.mMatchStart > lastOut) {
+ sb.append(text.substring(t.mMatchStart, matchEnd));
+ lastOut = matchEnd;
+ }
+ } else if (leastOtherStart == t.mMatchStart) {
+ // Ok to append the current char
+ } else if (leastOtherStart < t.mMatchStart) {
+ // We're already covered by another search term, so don't append
+ appendNow = false;
+ } else if (t.mMatchStart > lastOut) {
+ // Append the piece of our term that's not already covered
+ sb.append(text.substring(t.mMatchStart, leastOtherStart));
+ lastOut = leastOtherStart;
+ }
+ }
+ // Reset this term
+ t.mMatchLength = 0;
+ t.mMatchStart = -1;
+ }
+ }
+
+ if (appendNow) {
+ sb.append(chr);
+ lastOut = i;
+ }
+ }
+
+ return (CharSequence)sb;
+ }
+
+ /**
+ * Determine whether two Strings (either of which might be null) are the same; this is true
+ * when both are null or both are Strings that are equal.
+ */
+ public static boolean stringOrNullEquals(String a, String b) {
+ if (a == null && b == null) return true;
+ if (a != null && b != null && a.equals(b)) return true;
+ return false;
+ }
+
+}
diff --git a/email2/emailcommon/src/com/android/emailcommon/utility/Utility.java b/email2/emailcommon/src/com/android/emailcommon/utility/Utility.java
new file mode 100644
index 0000000..b74d4b3
--- /dev/null
+++ b/email2/emailcommon/src/com/android/emailcommon/utility/Utility.java
@@ -0,0 +1,1153 @@
+/*
+ * 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.emailcommon.utility;
+
+import android.app.Activity;
+import android.app.Fragment;
+import android.content.ContentResolver;
+import android.content.ContentUris;
+import android.content.ContentValues;
+import android.content.Context;
+import android.database.Cursor;
+import android.database.CursorWrapper;
+import android.graphics.Typeface;
+import android.net.Uri;
+import android.os.AsyncTask;
+import android.os.Environment;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.StrictMode;
+import android.provider.OpenableColumns;
+import android.text.Spannable;
+import android.text.SpannableString;
+import android.text.SpannableStringBuilder;
+import android.text.TextUtils;
+import android.text.style.StyleSpan;
+import android.util.Base64;
+import android.util.Log;
+import android.widget.ListView;
+import android.widget.TextView;
+import android.widget.Toast;
+
+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.Attachment;
+import com.android.emailcommon.provider.EmailContent.AttachmentColumns;
+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.HostAuth;
+import com.android.emailcommon.provider.Mailbox;
+import com.android.emailcommon.provider.ProviderUnavailableException;
+
+import java.io.ByteArrayInputStream;
+import java.io.File;
+import java.io.FileDescriptor;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.PrintWriter;
+import java.io.StringWriter;
+import java.io.UnsupportedEncodingException;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.nio.ByteBuffer;
+import java.nio.CharBuffer;
+import java.nio.charset.Charset;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.GregorianCalendar;
+import java.util.HashSet;
+import java.util.Set;
+import java.util.TimeZone;
+import java.util.regex.Pattern;
+
+public class Utility {
+ public static final Charset UTF_8 = Charset.forName("UTF-8");
+ public static final Charset ASCII = Charset.forName("US-ASCII");
+
+ public static final String[] EMPTY_STRINGS = new String[0];
+ public static final Long[] EMPTY_LONGS = new Long[0];
+
+ // "GMT" + "+" or "-" + 4 digits
+ private static final Pattern DATE_CLEANUP_PATTERN_WRONG_TIMEZONE =
+ Pattern.compile("GMT([-+]\\d{4})$");
+
+ private static Handler sMainThreadHandler;
+
+ /**
+ * @return a {@link Handler} tied to the main thread.
+ */
+ public static Handler getMainThreadHandler() {
+ if (sMainThreadHandler == null) {
+ // No need to synchronize -- it's okay to create an extra Handler, which will be used
+ // only once and then thrown away.
+ sMainThreadHandler = new Handler(Looper.getMainLooper());
+ }
+ return sMainThreadHandler;
+ }
+
+ public final static String readInputStream(InputStream in, String encoding) throws IOException {
+ InputStreamReader reader = new InputStreamReader(in, encoding);
+ StringBuffer sb = new StringBuffer();
+ int count;
+ char[] buf = new char[512];
+ while ((count = reader.read(buf)) != -1) {
+ sb.append(buf, 0, count);
+ }
+ return sb.toString();
+ }
+
+ public final static boolean arrayContains(Object[] a, Object o) {
+ int index = arrayIndex(a, o);
+ return (index >= 0);
+ }
+
+ public final static int arrayIndex(Object[] a, Object o) {
+ for (int i = 0, count = a.length; i < count; i++) {
+ if (a[i].equals(o)) {
+ return i;
+ }
+ }
+ return -1;
+ }
+
+ /**
+ * Returns a concatenated string containing the output of every Object's
+ * toString() method, each separated by the given separator character.
+ */
+ public static String combine(Object[] parts, char separator) {
+ if (parts == null) {
+ return null;
+ }
+ StringBuffer sb = new StringBuffer();
+ for (int i = 0; i < parts.length; i++) {
+ sb.append(parts[i].toString());
+ if (i < parts.length - 1) {
+ sb.append(separator);
+ }
+ }
+ return sb.toString();
+ }
+ public static String base64Decode(String encoded) {
+ if (encoded == null) {
+ return null;
+ }
+ byte[] decoded = Base64.decode(encoded, Base64.DEFAULT);
+ return new String(decoded);
+ }
+
+ public static String base64Encode(String s) {
+ if (s == null) {
+ return s;
+ }
+ return Base64.encodeToString(s.getBytes(), Base64.NO_WRAP);
+ }
+
+ public static boolean isTextViewNotEmpty(TextView view) {
+ return !TextUtils.isEmpty(view.getText());
+ }
+
+ public static boolean isPortFieldValid(TextView view) {
+ CharSequence chars = view.getText();
+ if (TextUtils.isEmpty(chars)) return false;
+ Integer port;
+ // In theory, we can't get an illegal value here, since the field is monitored for valid
+ // numeric input. But this might be used elsewhere without such a check.
+ try {
+ port = Integer.parseInt(chars.toString());
+ } catch (NumberFormatException e) {
+ return false;
+ }
+ return port > 0 && port < 65536;
+ }
+
+ /**
+ * Validate a hostname name field.
+ *
+ * Because we just use the {@link URI} class for validation, it'll accept some invalid
+ * host names, but it works well enough...
+ */
+ public static boolean isServerNameValid(TextView view) {
+ return isServerNameValid(view.getText().toString());
+ }
+
+ public static boolean isServerNameValid(String serverName) {
+ serverName = serverName.trim();
+ if (TextUtils.isEmpty(serverName)) {
+ return false;
+ }
+ try {
+ URI uri = new URI(
+ "http",
+ null,
+ serverName,
+ -1,
+ null, // path
+ null, // query
+ null);
+ return true;
+ } catch (URISyntaxException e) {
+ return false;
+ }
+ }
+
+ /**
+ * Ensures that the given string starts and ends with the double quote character. The string is
+ * not modified in any way except to add the double quote character to start and end if it's not
+ * already there.
+ *
+ * TODO: Rename this, because "quoteString()" can mean so many different things.
+ *
+ * sample -> "sample"
+ * "sample" -> "sample"
+ * ""sample"" -> "sample"
+ * "sample"" -> "sample"
+ * sa"mp"le -> "sa"mp"le"
+ * "sa"mp"le" -> "sa"mp"le"
+ * (empty string) -> ""
+ * " -> ""
+ */
+ public static String quoteString(String s) {
+ if (s == null) {
+ return null;
+ }
+ if (!s.matches("^\".*\"$")) {
+ return "\"" + s + "\"";
+ }
+ else {
+ return s;
+ }
+ }
+
+ /**
+ * A fast version of URLDecoder.decode() that works only with UTF-8 and does only two
+ * allocations. This version is around 3x as fast as the standard one and I'm using it
+ * hundreds of times in places that slow down the UI, so it helps.
+ */
+ public static String fastUrlDecode(String s) {
+ try {
+ byte[] bytes = s.getBytes("UTF-8");
+ byte ch;
+ int length = 0;
+ for (int i = 0, count = bytes.length; i < count; i++) {
+ ch = bytes[i];
+ if (ch == '%') {
+ int h = (bytes[i + 1] - '0');
+ int l = (bytes[i + 2] - '0');
+ if (h > 9) {
+ h -= 7;
+ }
+ if (l > 9) {
+ l -= 7;
+ }
+ bytes[length] = (byte) ((h << 4) | l);
+ i += 2;
+ }
+ else if (ch == '+') {
+ bytes[length] = ' ';
+ }
+ else {
+ bytes[length] = bytes[i];
+ }
+ length++;
+ }
+ return new String(bytes, 0, length, "UTF-8");
+ }
+ catch (UnsupportedEncodingException uee) {
+ return null;
+ }
+ }
+ private final static String HOSTAUTH_WHERE_CREDENTIALS = HostAuthColumns.ADDRESS + " like ?"
+ + " and " + HostAuthColumns.LOGIN + " like ? ESCAPE '\\'"
+ + " and " + HostAuthColumns.PROTOCOL + " not like \"smtp\"";
+ private final static String ACCOUNT_WHERE_HOSTAUTH = AccountColumns.HOST_AUTH_KEY_RECV + "=?";
+
+ /**
+ * Look for an existing account with the same username & server
+ *
+ * @param context a system context
+ * @param allowAccountId this account Id will not trigger (when editing an existing account)
+ * @param hostName the server's address
+ * @param userLogin the user's login string
+ * @result null = no matching account found. Account = matching account
+ */
+ public static Account findExistingAccount(Context context, long allowAccountId,
+ String hostName, String userLogin) {
+ ContentResolver resolver = context.getContentResolver();
+ String userName = userLogin.replace("_", "\\_");
+ Cursor c = resolver.query(HostAuth.CONTENT_URI, HostAuth.ID_PROJECTION,
+ HOSTAUTH_WHERE_CREDENTIALS, new String[] { hostName, userName }, null);
+ if (c == null) throw new ProviderUnavailableException();
+ try {
+ while (c.moveToNext()) {
+ long hostAuthId = c.getLong(HostAuth.ID_PROJECTION_COLUMN);
+ // Find account with matching hostauthrecv key, and return it
+ Cursor c2 = resolver.query(Account.CONTENT_URI, Account.ID_PROJECTION,
+ ACCOUNT_WHERE_HOSTAUTH, new String[] { Long.toString(hostAuthId) }, null);
+ try {
+ while (c2.moveToNext()) {
+ long accountId = c2.getLong(Account.ID_PROJECTION_COLUMN);
+ if (accountId != allowAccountId) {
+ Account account = Account.restoreAccountWithId(context, accountId);
+ if (account != null) {
+ return account;
+ }
+ }
+ }
+ } finally {
+ c2.close();
+ }
+ }
+ } finally {
+ c.close();
+ }
+
+ return null;
+ }
+
+ /**
+ * Generate a random message-id header for locally-generated messages.
+ */
+ public static String generateMessageId() {
+ StringBuffer sb = new StringBuffer();
+ sb.append("<");
+ for (int i = 0; i < 24; i++) {
+ sb.append(Integer.toString((int)(Math.random() * 35), 36));
+ }
+ sb.append(".");
+ sb.append(Long.toString(System.currentTimeMillis()));
+ sb.append("@email.android.com>");
+ return sb.toString();
+ }
+
+ /**
+ * Generate a time in milliseconds from a date string that represents a date/time in GMT
+ * @param date string in format 20090211T180303Z (rfc2445, iCalendar).
+ * @return the time in milliseconds (since Jan 1, 1970)
+ */
+ public static long parseDateTimeToMillis(String date) {
+ GregorianCalendar cal = parseDateTimeToCalendar(date);
+ return cal.getTimeInMillis();
+ }
+
+ /**
+ * Generate a GregorianCalendar from a date string that represents a date/time in GMT
+ * @param date string in format 20090211T180303Z (rfc2445, iCalendar).
+ * @return the GregorianCalendar
+ */
+ public static GregorianCalendar parseDateTimeToCalendar(String date) {
+ GregorianCalendar cal = new GregorianCalendar(Integer.parseInt(date.substring(0, 4)),
+ Integer.parseInt(date.substring(4, 6)) - 1, Integer.parseInt(date.substring(6, 8)),
+ Integer.parseInt(date.substring(9, 11)), Integer.parseInt(date.substring(11, 13)),
+ Integer.parseInt(date.substring(13, 15)));
+ cal.setTimeZone(TimeZone.getTimeZone("GMT"));
+ return cal;
+ }
+
+ /**
+ * Generate a time in milliseconds from an email date string that represents a date/time in GMT
+ * @param date string in format 2010-02-23T16:00:00.000Z (ISO 8601, rfc3339)
+ * @return the time in milliseconds (since Jan 1, 1970)
+ */
+ public static long parseEmailDateTimeToMillis(String date) {
+ GregorianCalendar cal = new GregorianCalendar(Integer.parseInt(date.substring(0, 4)),
+ Integer.parseInt(date.substring(5, 7)) - 1, Integer.parseInt(date.substring(8, 10)),
+ Integer.parseInt(date.substring(11, 13)), Integer.parseInt(date.substring(14, 16)),
+ Integer.parseInt(date.substring(17, 19)));
+ cal.setTimeZone(TimeZone.getTimeZone("GMT"));
+ return cal.getTimeInMillis();
+ }
+
+ private static byte[] encode(Charset charset, String s) {
+ if (s == null) {
+ return null;
+ }
+ final ByteBuffer buffer = charset.encode(CharBuffer.wrap(s));
+ final byte[] bytes = new byte[buffer.limit()];
+ buffer.get(bytes);
+ return bytes;
+ }
+
+ private static String decode(Charset charset, byte[] b) {
+ if (b == null) {
+ return null;
+ }
+ final CharBuffer cb = charset.decode(ByteBuffer.wrap(b));
+ return new String(cb.array(), 0, cb.length());
+ }
+
+ /** Converts a String to UTF-8 */
+ public static byte[] toUtf8(String s) {
+ return encode(UTF_8, s);
+ }
+
+ /** Builds a String from UTF-8 bytes */
+ public static String fromUtf8(byte[] b) {
+ return decode(UTF_8, b);
+ }
+
+ /** Converts a String to ASCII bytes */
+ public static byte[] toAscii(String s) {
+ return encode(ASCII, s);
+ }
+
+ /** Builds a String from ASCII bytes */
+ public static String fromAscii(byte[] b) {
+ return decode(ASCII, b);
+ }
+
+ /**
+ * @return true if the input is the first (or only) byte in a UTF-8 character
+ */
+ public static boolean isFirstUtf8Byte(byte b) {
+ // If the top 2 bits is '10', it's not a first byte.
+ return (b & 0xc0) != 0x80;
+ }
+
+ public static String byteToHex(int b) {
+ return byteToHex(new StringBuilder(), b).toString();
+ }
+
+ public static StringBuilder byteToHex(StringBuilder sb, int b) {
+ b &= 0xFF;
+ sb.append("0123456789ABCDEF".charAt(b >> 4));
+ sb.append("0123456789ABCDEF".charAt(b & 0xF));
+ return sb;
+ }
+
+ public static String replaceBareLfWithCrlf(String str) {
+ return str.replace("\r", "").replace("\n", "\r\n");
+ }
+
+ /**
+ * Cancel an {@link AsyncTask}. If it's already running, it'll be interrupted.
+ */
+ public static void cancelTaskInterrupt(AsyncTask<?, ?, ?> task) {
+ cancelTask(task, true);
+ }
+
+ /**
+ * Cancel an {@link EmailAsyncTask}. If it's already running, it'll be interrupted.
+ */
+ public static void cancelTaskInterrupt(EmailAsyncTask<?, ?, ?> task) {
+ if (task != null) {
+ task.cancel(true);
+ }
+ }
+
+ /**
+ * Cancel an {@link AsyncTask}.
+ *
+ * @param mayInterruptIfRunning <tt>true</tt> if the thread executing this
+ * task should be interrupted; otherwise, in-progress tasks are allowed
+ * to complete.
+ */
+ public static void cancelTask(AsyncTask<?, ?, ?> task, boolean mayInterruptIfRunning) {
+ if (task != null && task.getStatus() != AsyncTask.Status.FINISHED) {
+ task.cancel(mayInterruptIfRunning);
+ }
+ }
+
+ public static String getSmallHash(final String value) {
+ final MessageDigest sha;
+ try {
+ sha = MessageDigest.getInstance("SHA-1");
+ } catch (NoSuchAlgorithmException impossible) {
+ return null;
+ }
+ sha.update(Utility.toUtf8(value));
+ final int hash = getSmallHashFromSha1(sha.digest());
+ return Integer.toString(hash);
+ }
+
+ /**
+ * @return a non-negative integer generated from 20 byte SHA-1 hash.
+ */
+ /* package for testing */ static int getSmallHashFromSha1(byte[] sha1) {
+ final int offset = sha1[19] & 0xf; // SHA1 is 20 bytes.
+ return ((sha1[offset] & 0x7f) << 24)
+ | ((sha1[offset + 1] & 0xff) << 16)
+ | ((sha1[offset + 2] & 0xff) << 8)
+ | ((sha1[offset + 3] & 0xff));
+ }
+
+ /**
+ * Try to make a date MIME(RFC 2822/5322)-compliant.
+ *
+ * It fixes:
+ * - "Thu, 10 Dec 09 15:08:08 GMT-0700" to "Thu, 10 Dec 09 15:08:08 -0700"
+ * (4 digit zone value can't be preceded by "GMT")
+ * We got a report saying eBay sends a date in this format
+ */
+ public static String cleanUpMimeDate(String date) {
+ if (TextUtils.isEmpty(date)) {
+ return date;
+ }
+ date = DATE_CLEANUP_PATTERN_WRONG_TIMEZONE.matcher(date).replaceFirst("$1");
+ return date;
+ }
+
+ public static ByteArrayInputStream streamFromAsciiString(String ascii) {
+ return new ByteArrayInputStream(toAscii(ascii));
+ }
+
+ /**
+ * A thread safe way to show a Toast. Can be called from any thread.
+ *
+ * @param context context
+ * @param resId Resource ID of the message string.
+ */
+ public static void showToast(Context context, int resId) {
+ showToast(context, context.getResources().getString(resId));
+ }
+
+ /**
+ * A thread safe way to show a Toast. Can be called from any thread.
+ *
+ * @param context context
+ * @param message Message to show.
+ */
+ public static void showToast(final Context context, final String message) {
+ getMainThreadHandler().post(new Runnable() {
+ @Override
+ public void run() {
+ Toast.makeText(context, message, Toast.LENGTH_LONG).show();
+ }
+ });
+ }
+
+ /**
+ * Run {@code r} on a worker thread, returning the AsyncTask
+ * @return the AsyncTask; this is primarily for use by unit tests, which require the
+ * result of the task
+ *
+ * @deprecated use {@link EmailAsyncTask#runAsyncParallel} or
+ * {@link EmailAsyncTask#runAsyncSerial}
+ */
+ @Deprecated
+ public static AsyncTask<Void, Void, Void> runAsync(final Runnable r) {
+ return new AsyncTask<Void, Void, Void>() {
+ @Override protected Void doInBackground(Void... params) {
+ r.run();
+ return null;
+ }
+ }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
+ }
+
+ /**
+ * Interface used in {@link #createUniqueFile} instead of {@link File#createNewFile()} to make
+ * it testable.
+ */
+ /* package */ interface NewFileCreator {
+ public static final NewFileCreator DEFAULT = new NewFileCreator() {
+ @Override public boolean createNewFile(File f) throws IOException {
+ return f.createNewFile();
+ }
+ };
+ public boolean createNewFile(File f) throws IOException ;
+ }
+
+ /**
+ * Creates a new empty file with a unique name in the given directory by appending a hyphen and
+ * a number to the given filename.
+ *
+ * @return a new File object, or null if one could not be created
+ */
+ public static File createUniqueFile(File directory, String filename) throws IOException {
+ return createUniqueFileInternal(NewFileCreator.DEFAULT, directory, filename);
+ }
+
+ /* package */ static File createUniqueFileInternal(NewFileCreator nfc,
+ File directory, String filename) throws IOException {
+ File file = new File(directory, filename);
+ if (nfc.createNewFile(file)) {
+ return file;
+ }
+ // Get the extension of the file, if any.
+ int index = filename.lastIndexOf('.');
+ String format;
+ if (index != -1) {
+ String name = filename.substring(0, index);
+ String extension = filename.substring(index);
+ format = name + "-%d" + extension;
+ } else {
+ format = filename + "-%d";
+ }
+
+ for (int i = 2; i < Integer.MAX_VALUE; i++) {
+ file = new File(directory, String.format(format, i));
+ if (nfc.createNewFile(file)) {
+ return file;
+ }
+ }
+ return null;
+ }
+
+ public interface CursorGetter<T> {
+ T get(Cursor cursor, int column);
+ }
+
+ private static final CursorGetter<Long> LONG_GETTER = new CursorGetter<Long>() {
+ @Override
+ public Long get(Cursor cursor, int column) {
+ return cursor.getLong(column);
+ }
+ };
+
+ private static final CursorGetter<Integer> INT_GETTER = new CursorGetter<Integer>() {
+ @Override
+ public Integer get(Cursor cursor, int column) {
+ return cursor.getInt(column);
+ }
+ };
+
+ private static final CursorGetter<String> STRING_GETTER = new CursorGetter<String>() {
+ @Override
+ public String get(Cursor cursor, int column) {
+ return cursor.getString(column);
+ }
+ };
+
+ private static final CursorGetter<byte[]> BLOB_GETTER = new CursorGetter<byte[]>() {
+ @Override
+ public byte[] get(Cursor cursor, int column) {
+ return cursor.getBlob(column);
+ }
+ };
+
+ /**
+ * @return if {@code original} is to the EmailProvider, add "?limit=1". Otherwise just returns
+ * {@code original}.
+ *
+ * Other providers don't support the limit param. Also, changing URI passed from other apps
+ * can cause permission errors.
+ */
+ /* package */ static Uri buildLimitOneUri(Uri original) {
+ if ("content".equals(original.getScheme()) &&
+ EmailContent.AUTHORITY.equals(original.getAuthority())) {
+ return EmailContent.uriWithLimit(original, 1);
+ }
+ return original;
+ }
+
+ /**
+ * @return a generic in column {@code column} of the first result row, if the query returns at
+ * least 1 row. Otherwise returns {@code defaultValue}.
+ */
+ public static <T extends Object> T getFirstRowColumn(Context context, Uri uri,
+ String[] projection, String selection, String[] selectionArgs, String sortOrder,
+ int column, T defaultValue, CursorGetter<T> getter) {
+ // Use PARAMETER_LIMIT to restrict the query to the single row we need
+ uri = buildLimitOneUri(uri);
+ Cursor c = context.getContentResolver().query(uri, projection, selection, selectionArgs,
+ sortOrder);
+ if (c != null) {
+ try {
+ if (c.moveToFirst()) {
+ return getter.get(c, column);
+ }
+ } finally {
+ c.close();
+ }
+ }
+ return defaultValue;
+ }
+
+ /**
+ * {@link #getFirstRowColumn} for a Long with null as a default value.
+ */
+ public static Long getFirstRowLong(Context context, Uri uri, String[] projection,
+ String selection, String[] selectionArgs, String sortOrder, int column) {
+ return getFirstRowColumn(context, uri, projection, selection, selectionArgs,
+ sortOrder, column, null, LONG_GETTER);
+ }
+
+ /**
+ * {@link #getFirstRowColumn} for a Long with a provided default value.
+ */
+ public static Long getFirstRowLong(Context context, Uri uri, String[] projection,
+ String selection, String[] selectionArgs, String sortOrder, int column,
+ Long defaultValue) {
+ return getFirstRowColumn(context, uri, projection, selection, selectionArgs,
+ sortOrder, column, defaultValue, LONG_GETTER);
+ }
+
+ /**
+ * {@link #getFirstRowColumn} for an Integer with null as a default value.
+ */
+ public static Integer getFirstRowInt(Context context, Uri uri, String[] projection,
+ String selection, String[] selectionArgs, String sortOrder, int column) {
+ return getFirstRowColumn(context, uri, projection, selection, selectionArgs,
+ sortOrder, column, null, INT_GETTER);
+ }
+
+ /**
+ * {@link #getFirstRowColumn} for an Integer with a provided default value.
+ */
+ public static Integer getFirstRowInt(Context context, Uri uri, String[] projection,
+ String selection, String[] selectionArgs, String sortOrder, int column,
+ Integer defaultValue) {
+ return getFirstRowColumn(context, uri, projection, selection, selectionArgs,
+ sortOrder, column, defaultValue, INT_GETTER);
+ }
+
+ /**
+ * {@link #getFirstRowColumn} for a String with null as a default value.
+ */
+ public static String getFirstRowString(Context context, Uri uri, String[] projection,
+ String selection, String[] selectionArgs, String sortOrder, int column) {
+ return getFirstRowString(context, uri, projection, selection, selectionArgs, sortOrder,
+ column, null);
+ }
+
+ /**
+ * {@link #getFirstRowColumn} for a String with a provided default value.
+ */
+ public static String getFirstRowString(Context context, Uri uri, String[] projection,
+ String selection, String[] selectionArgs, String sortOrder, int column,
+ String defaultValue) {
+ return getFirstRowColumn(context, uri, projection, selection, selectionArgs,
+ sortOrder, column, defaultValue, STRING_GETTER);
+ }
+
+ /**
+ * {@link #getFirstRowColumn} for a byte array with a provided default value.
+ */
+ public static byte[] getFirstRowBlob(Context context, Uri uri, String[] projection,
+ String selection, String[] selectionArgs, String sortOrder, int column,
+ byte[] defaultValue) {
+ return getFirstRowColumn(context, uri, projection, selection, selectionArgs, sortOrder,
+ column, defaultValue, BLOB_GETTER);
+ }
+
+ public static boolean attachmentExists(Context context, Attachment attachment) {
+ if (attachment == null) {
+ return false;
+ } else if (attachment.mContentBytes != null) {
+ return true;
+ } else if (TextUtils.isEmpty(attachment.mContentUri)) {
+ return false;
+ }
+ try {
+ Uri fileUri = Uri.parse(attachment.mContentUri);
+ try {
+ InputStream inStream = context.getContentResolver().openInputStream(fileUri);
+ try {
+ inStream.close();
+ } catch (IOException e) {
+ // Nothing to be done if can't close the stream
+ }
+ return true;
+ } catch (FileNotFoundException e) {
+ return false;
+ }
+ } catch (RuntimeException re) {
+ Log.w(Logging.LOG_TAG, "attachmentExists RuntimeException=" + re);
+ return false;
+ }
+ }
+
+ /**
+ * Check whether the message with a given id has unloaded attachments. If the message is
+ * a forwarded message, we look instead at the messages's source for the attachments. If the
+ * message or forward source can't be found, we return false
+ * @param context the caller's context
+ * @param messageId the id of the message
+ * @return whether or not the message has unloaded attachments
+ */
+ public static boolean hasUnloadedAttachments(Context context, long messageId) {
+ Message msg = Message.restoreMessageWithId(context, messageId);
+ if (msg == null) return false;
+ Attachment[] atts = Attachment.restoreAttachmentsWithMessageId(context, messageId);
+ for (Attachment att: atts) {
+ if (!attachmentExists(context, att)) {
+ // If the attachment doesn't exist and isn't marked for download, we're in trouble
+ // since the outbound message will be stuck indefinitely in the Outbox. Instead,
+ // we'll just delete the attachment and continue; this is far better than the
+ // alternative. In theory, this situation shouldn't be possible.
+ if ((att.mFlags & (Attachment.FLAG_DOWNLOAD_FORWARD |
+ Attachment.FLAG_DOWNLOAD_USER_REQUEST)) == 0) {
+ Log.d(Logging.LOG_TAG, "Unloaded attachment isn't marked for download: " +
+ att.mFileName + ", #" + att.mId);
+ Attachment.delete(context, Attachment.CONTENT_URI, att.mId);
+ } else if (att.mContentUri != null) {
+ // In this case, the attachment file is gone from the cache; let's clear the
+ // contentUri; this should be a very unusual case
+ ContentValues cv = new ContentValues();
+ cv.putNull(AttachmentColumns.CONTENT_URI);
+ Attachment.update(context, Attachment.CONTENT_URI, att.mId, cv);
+ }
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Convenience method wrapping calls to retrieve columns from a single row, via EmailProvider.
+ * The arguments are exactly the same as to contentResolver.query(). Results are returned in
+ * an array of Strings corresponding to the columns in the projection. If the cursor has no
+ * rows, null is returned.
+ */
+ public static String[] getRowColumns(Context context, Uri contentUri, String[] projection,
+ String selection, String[] selectionArgs) {
+ String[] values = new String[projection.length];
+ ContentResolver cr = context.getContentResolver();
+ Cursor c = cr.query(contentUri, projection, selection, selectionArgs, null);
+ try {
+ if (c.moveToFirst()) {
+ for (int i = 0; i < projection.length; i++) {
+ values[i] = c.getString(i);
+ }
+ } else {
+ return null;
+ }
+ } finally {
+ c.close();
+ }
+ return values;
+ }
+
+ /**
+ * Convenience method for retrieving columns from a particular row in EmailProvider.
+ * Passed in here are a base uri (e.g. Message.CONTENT_URI), the unique id of a row, and
+ * a projection. This method calls the previous one with the appropriate URI.
+ */
+ public static String[] getRowColumns(Context context, Uri baseUri, long id,
+ String ... projection) {
+ return getRowColumns(context, ContentUris.withAppendedId(baseUri, id), projection, null,
+ null);
+ }
+
+ public static boolean isExternalStorageMounted() {
+ return Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED);
+ }
+
+ /**
+ * Class that supports running any operation for each account.
+ */
+ public abstract static class ForEachAccount extends AsyncTask<Void, Void, Long[]> {
+ private final Context mContext;
+
+ public ForEachAccount(Context context) {
+ mContext = context;
+ }
+
+ @Override
+ protected final Long[] doInBackground(Void... params) {
+ ArrayList<Long> ids = new ArrayList<Long>();
+ Cursor c = mContext.getContentResolver().query(Account.CONTENT_URI,
+ Account.ID_PROJECTION, null, null, null);
+ try {
+ while (c.moveToNext()) {
+ ids.add(c.getLong(Account.ID_PROJECTION_COLUMN));
+ }
+ } finally {
+ c.close();
+ }
+ return ids.toArray(EMPTY_LONGS);
+ }
+
+ @Override
+ protected final void onPostExecute(Long[] ids) {
+ if (ids != null && !isCancelled()) {
+ for (long id : ids) {
+ performAction(id);
+ }
+ }
+ onFinished();
+ }
+
+ /**
+ * This method will be called for each account.
+ */
+ protected abstract void performAction(long accountId);
+
+ /**
+ * Called when the iteration is finished.
+ */
+ protected void onFinished() {
+ }
+ }
+
+ /**
+ * Updates the last seen message key in the mailbox data base for the INBOX of the currently
+ * shown account. If the account is {@link Account#ACCOUNT_ID_COMBINED_VIEW}, the INBOX for
+ * all accounts are updated.
+ * @return an {@link EmailAsyncTask} for test only.
+ */
+ public static EmailAsyncTask<Void, Void, Void> updateLastNotifiedMessageKey(
+ final Context context, final long mailboxId) {
+ return EmailAsyncTask.runAsyncParallel(new Runnable() {
+ private void updateLastSeenMessageKeyForMailbox(long mailboxId) {
+ ContentResolver resolver = context.getContentResolver();
+ if (mailboxId == Mailbox.QUERY_ALL_INBOXES) {
+ Cursor c = resolver.query(
+ Mailbox.CONTENT_URI, EmailContent.ID_PROJECTION, Mailbox.TYPE + "=?",
+ new String[] { Integer.toString(Mailbox.TYPE_INBOX) }, null);
+ if (c == null) throw new ProviderUnavailableException();
+ try {
+ while (c.moveToNext()) {
+ final long id = c.getLong(EmailContent.ID_PROJECTION_COLUMN);
+ updateLastSeenMessageKeyForMailbox(id);
+ }
+ } finally {
+ c.close();
+ }
+ } else if (mailboxId > 0L) {
+ Mailbox mailbox = Mailbox.restoreMailboxWithId(context, mailboxId);
+ // mailbox has been removed
+ if (mailbox == null) {
+ return;
+ }
+ // We use the highest _id for the account the mailbox table as the "last seen
+ // message key". We don't care if the message has been read or not. We only
+ // need a point at which we can compare against in the future. By setting this
+ // value, we are claiming that every message before this has potentially been
+ // seen by the user.
+ long mostRecentMessageId = Utility.getFirstRowLong(context,
+ ContentUris.withAppendedId(
+ EmailContent.MAILBOX_MOST_RECENT_MESSAGE_URI, mailboxId),
+ Message.ID_COLUMN_PROJECTION, null, null, null,
+ Message.ID_MAILBOX_COLUMN_ID, -1L);
+ long lastNotifiedMessageId = mailbox.mLastNotifiedMessageKey;
+ // Only update the db if the value has changed
+ if (mostRecentMessageId != lastNotifiedMessageId) {
+ Log.d(Logging.LOG_TAG, "Most recent = " + mostRecentMessageId +
+ ", last notified: " + lastNotifiedMessageId +
+ "; updating last notified");
+ ContentValues values = mailbox.toContentValues();
+ values.put(MailboxColumns.LAST_NOTIFIED_MESSAGE_KEY, mostRecentMessageId);
+ resolver.update(
+ Mailbox.CONTENT_URI,
+ values,
+ EmailContent.ID_SELECTION,
+ new String[] { Long.toString(mailbox.mId) });
+ } else {
+ Log.d(Logging.LOG_TAG, "Most recent = last notified; no change");
+ }
+ }
+ }
+
+ @Override
+ public void run() {
+ updateLastSeenMessageKeyForMailbox(mailboxId);
+ }
+ });
+ }
+
+ public static long[] toPrimitiveLongArray(Collection<Long> collection) {
+ // Need to do this manually because we're converting to a primitive long array, not
+ // a Long array.
+ final int size = collection.size();
+ final long[] ret = new long[size];
+ // Collection doesn't have get(i). (Iterable doesn't have size())
+ int i = 0;
+ for (Long value : collection) {
+ ret[i++] = value;
+ }
+ return ret;
+ }
+
+ public static Set<Long> toLongSet(long[] array) {
+ // Need to do this manually because we're converting from a primitive long array, not
+ // a Long array.
+ final int size = array.length;
+ HashSet<Long> ret = new HashSet<Long>(size);
+ for (int i = 0; i < size; i++) {
+ ret.add(array[i]);
+ }
+ return ret;
+ }
+
+ /**
+ * Workaround for the {@link ListView#smoothScrollToPosition} randomly scroll the view bug
+ * if it's called right after {@link ListView#setAdapter}.
+ */
+ public static void listViewSmoothScrollToPosition(final Activity activity,
+ final ListView listView, final int position) {
+ // Workarond: delay-call smoothScrollToPosition()
+ new Handler().post(new Runnable() {
+ @Override
+ public void run() {
+ if (activity.isFinishing()) {
+ return; // Activity being destroyed
+ }
+ listView.smoothScrollToPosition(position);
+ }
+ });
+ }
+
+ private static final String[] ATTACHMENT_META_NAME_PROJECTION = {
+ OpenableColumns.DISPLAY_NAME
+ };
+ private static final int ATTACHMENT_META_NAME_COLUMN_DISPLAY_NAME = 0;
+
+ /**
+ * @return Filename of a content of {@code contentUri}. If the provider doesn't provide the
+ * filename, returns the last path segment of the URI.
+ */
+ public static String getContentFileName(Context context, Uri contentUri) {
+ String name = getFirstRowString(context, contentUri, ATTACHMENT_META_NAME_PROJECTION, null,
+ null, null, ATTACHMENT_META_NAME_COLUMN_DISPLAY_NAME);
+ if (name == null) {
+ name = contentUri.getLastPathSegment();
+ }
+ return name;
+ }
+
+ /**
+ * Append a bold span to a {@link SpannableStringBuilder}.
+ */
+ public static SpannableStringBuilder appendBold(SpannableStringBuilder ssb, String text) {
+ if (!TextUtils.isEmpty(text)) {
+ SpannableString ss = new SpannableString(text);
+ ss.setSpan(new StyleSpan(Typeface.BOLD), 0, ss.length(),
+ Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
+ ssb.append(ss);
+ }
+
+ return ssb;
+ }
+
+ /**
+ * Stringify a cursor for logging purpose.
+ */
+ public static String dumpCursor(Cursor c) {
+ StringBuilder sb = new StringBuilder();
+ sb.append("[");
+ while (c != null) {
+ sb.append(c.getClass()); // Class name may not be available if toString() is overridden
+ sb.append("/");
+ sb.append(c.toString());
+ if (c.isClosed()) {
+ sb.append(" (closed)");
+ }
+ if (c instanceof CursorWrapper) {
+ c = ((CursorWrapper) c).getWrappedCursor();
+ sb.append(", ");
+ } else {
+ break;
+ }
+ }
+ sb.append("]");
+ return sb.toString();
+ }
+
+ /**
+ * Cursor wrapper that remembers where it was closed.
+ *
+ * Use {@link #get} to create a wrapped cursor.
+ * USe {@link #getTraceIfAvailable} to get the stack trace.
+ * Use {@link #log} to log if/where it was closed.
+ */
+ public static class CloseTraceCursorWrapper extends CursorWrapper {
+ private static final boolean TRACE_ENABLED = false;
+
+ private Exception mTrace;
+
+ private CloseTraceCursorWrapper(Cursor cursor) {
+ super(cursor);
+ }
+
+ @Override
+ public void close() {
+ mTrace = new Exception("STACK TRACE");
+ super.close();
+ }
+
+ public static Exception getTraceIfAvailable(Cursor c) {
+ if (c instanceof CloseTraceCursorWrapper) {
+ return ((CloseTraceCursorWrapper) c).mTrace;
+ } else {
+ return null;
+ }
+ }
+
+ public static void log(Cursor c) {
+ if (c == null) {
+ return;
+ }
+ if (c.isClosed()) {
+ Log.w(Logging.LOG_TAG, "Cursor was closed here: Cursor=" + c,
+ getTraceIfAvailable(c));
+ } else {
+ Log.w(Logging.LOG_TAG, "Cursor not closed. Cursor=" + c);
+ }
+ }
+
+ public static Cursor get(Cursor original) {
+ return TRACE_ENABLED ? new CloseTraceCursorWrapper(original) : original;
+ }
+
+ /* package */ static CloseTraceCursorWrapper alwaysCreateForTest(Cursor original) {
+ return new CloseTraceCursorWrapper(original);
+ }
+ }
+
+ /**
+ * Test that the given strings are equal in a null-pointer safe fashion.
+ */
+ public static boolean areStringsEqual(String s1, String s2) {
+ return (s1 != null && s1.equals(s2)) || (s1 == null && s2 == null);
+ }
+
+ public static void enableStrictMode(boolean enabled) {
+ StrictMode.setThreadPolicy(enabled
+ ? new StrictMode.ThreadPolicy.Builder().detectAll().build()
+ : StrictMode.ThreadPolicy.LAX);
+ StrictMode.setVmPolicy(enabled
+ ? new StrictMode.VmPolicy.Builder().detectAll().build()
+ : StrictMode.VmPolicy.LAX);
+ }
+
+ public static String dumpFragment(Fragment f) {
+ StringWriter sw = new StringWriter();
+ PrintWriter w = new PrintWriter(sw);
+ f.dump("", new FileDescriptor(), w, new String[0]);
+ return sw.toString();
+ }
+
+ /**
+ * Builds an "in" expression for SQLite.
+ *
+ * e.g. "ID" + 1,2,3 -> "ID in (1,2,3)". If {@code values} is empty or null, it returns an
+ * empty string.
+ */
+ public static String buildInSelection(String columnName, Collection<? extends Number> values) {
+ if ((values == null) || (values.size() == 0)) {
+ return "";
+ }
+ StringBuilder sb = new StringBuilder();
+ sb.append(columnName);
+ sb.append(" in (");
+ String sep = "";
+ for (Number n : values) {
+ sb.append(sep);
+ sb.append(n.toString());
+ sep = ",";
+ }
+ sb.append(')');
+ return sb.toString();
+ }
+}
diff --git a/email2/src/com/android/email/Email.java b/email2/src/com/android/email/Email.java
deleted file mode 100644
index d651649..0000000
--- a/email2/src/com/android/email/Email.java
+++ /dev/null
@@ -1,236 +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.app.Application;
-import android.content.ComponentName;
-import android.content.Context;
-import android.content.Intent;
-import android.content.pm.PackageManager;
-import android.database.Cursor;
-import android.util.Log;
-
-import com.android.email.service.AttachmentDownloadService;
-import com.android.email.service.MailService;
-import com.android.emailcommon.Logging;
-import com.android.emailcommon.TempDirectory;
-import com.android.emailcommon.provider.Account;
-import com.android.emailcommon.service.EmailServiceProxy;
-import com.android.emailcommon.utility.EmailAsyncTask;
-import com.android.emailcommon.utility.Utility;
-
-public class Email extends Application {
- /**
- * If this is enabled there will be additional logging information sent to
- * Log.d, including protocol dumps.
- *
- * This should only be used for logs that are useful for debbuging user problems,
- * not for internal/development logs.
- *
- * This can be enabled by typing "debug" in the AccountFolderList activity.
- * Changing the value to 'true' here will likely have no effect at all!
- *
- * TODO: rename this to sUserDebug, and rename LOGD below to DEBUG.
- */
- public static boolean DEBUG;
-
- // Exchange debugging flags (passed to Exchange, when available, via EmailServiceProxy)
- public static boolean DEBUG_EXCHANGE;
- public static boolean DEBUG_EXCHANGE_VERBOSE;
- public static boolean DEBUG_EXCHANGE_FILE;
-
- /**
- * If true, inhibit hardware graphics acceleration in UI (for a/b testing)
- */
- public static boolean sDebugInhibitGraphicsAcceleration = false;
-
- /**
- * Specifies how many messages will be shown in a folder by default. This number is set
- * on each new folder and can be incremented with "Load more messages..." by the
- * VISIBLE_LIMIT_INCREMENT
- */
- public static final int VISIBLE_LIMIT_DEFAULT = 25;
-
- /**
- * Number of additional messages to load when a user selects "Load more messages..."
- */
- public static final int VISIBLE_LIMIT_INCREMENT = 25;
-
- /**
- * This is used to force stacked UI to return to the "welcome" screen any time we change
- * the accounts list (e.g. deleting accounts in the Account Manager preferences.)
- */
- private static boolean sAccountsChangedNotification = false;
-
- private static String sMessageDecodeErrorString;
-
- private static Thread sUiThread;
-
- /**
- * Asynchronous version of {@link #setServicesEnabledSync(Context)}. Use when calling from
- * UI thread (or lifecycle entry points.)
- *
- * @param context
- */
- public static void setServicesEnabledAsync(final Context context) {
- EmailAsyncTask.runAsyncParallel(new Runnable() {
- @Override
- public void run() {
- setServicesEnabledSync(context);
- }
- });
- }
-
- /**
- * Called throughout the application when the number of accounts has changed. This method
- * enables or disables the Compose activity, the boot receiver and the service based on
- * whether any accounts are configured.
- *
- * Blocking call - do not call from UI/lifecycle threads.
- *
- * @param context
- * @return true if there are any accounts configured.
- */
- public static boolean setServicesEnabledSync(Context context) {
- Cursor c = null;
- try {
- c = context.getContentResolver().query(
- Account.CONTENT_URI,
- Account.ID_PROJECTION,
- null, null, null);
- boolean enable = c.getCount() > 0;
- setServicesEnabled(context, enable);
- return enable;
- } finally {
- if (c != null) {
- c.close();
- }
- }
- }
-
- private static void setServicesEnabled(Context context, boolean enabled) {
- PackageManager pm = context.getPackageManager();
- pm.setComponentEnabledSetting(
- new ComponentName(context, MailService.class),
- enabled ? PackageManager.COMPONENT_ENABLED_STATE_ENABLED :
- PackageManager.COMPONENT_ENABLED_STATE_DISABLED,
- PackageManager.DONT_KILL_APP);
- pm.setComponentEnabledSetting(
- new ComponentName(context, AttachmentDownloadService.class),
- enabled ? PackageManager.COMPONENT_ENABLED_STATE_ENABLED :
- PackageManager.COMPONENT_ENABLED_STATE_DISABLED,
- PackageManager.DONT_KILL_APP);
-
- // Start/stop the various services depending on whether there are any accounts
- startOrStopService(enabled, context, new Intent(context, AttachmentDownloadService.class));
- NotificationController.getInstance(context).watchForMessages(enabled);
- }
-
- /**
- * Starts or stops the service as necessary.
- * @param enabled If {@code true}, the service will be started. Otherwise, it will be stopped.
- * @param context The context to manage the service with.
- * @param intent The intent of the service to be managed.
- */
- private static void startOrStopService(boolean enabled, Context context, Intent intent) {
- if (enabled) {
- context.startService(intent);
- } else {
- context.stopService(intent);
- }
- }
-
- @Override
- public void onCreate() {
- super.onCreate();
- sUiThread = Thread.currentThread();
- Preferences prefs = Preferences.getPreferences(this);
- DEBUG = true; //prefs.getEnableDebugLogging();
- sDebugInhibitGraphicsAcceleration = prefs.getInhibitGraphicsAcceleration();
- enableStrictMode(prefs.getEnableStrictMode());
- TempDirectory.setTempDirectory(this);
-
- // Enable logging in the EAS service, so it starts up as early as possible.
- updateLoggingFlags(this);
-
- // Get a helper string used deep inside message decoders (which don't have context)
- sMessageDecodeErrorString = getString(R.string.message_decode_error);
-
- // Make sure all required services are running when the app is started (can prevent
- // issues after an adb sync/install)
- setServicesEnabledAsync(this);
- }
-
- /**
- * Load enabled debug flags from the preferences and update the EAS debug flag.
- */
- public static void updateLoggingFlags(Context context) {
- Preferences prefs = Preferences.getPreferences(context);
- int debugLogging = prefs.getEnableDebugLogging() ? EmailServiceProxy.DEBUG_BIT : 0;
- int verboseLogging =
- prefs.getEnableExchangeLogging() ? EmailServiceProxy.DEBUG_VERBOSE_BIT : 0;
- int fileLogging =
- prefs.getEnableExchangeFileLogging() ? EmailServiceProxy.DEBUG_FILE_BIT : 0;
- int enableStrictMode =
- prefs.getEnableStrictMode() ? EmailServiceProxy.DEBUG_ENABLE_STRICT_MODE : 0;
- int debugBits = debugLogging | verboseLogging | fileLogging | enableStrictMode;
- //Controller.getInstance(context).serviceLogging(debugBits);
- }
-
- /**
- * Internal, utility method for logging.
- * The calls to log() must be guarded with "if (Email.LOGD)" for performance reasons.
- */
- public static void log(String message) {
- Log.d(Logging.LOG_TAG, message);
- }
-
- /**
- * Called by the accounts reconciler to notify that accounts have changed, or by "Welcome"
- * to clear the flag.
- * @param setFlag true to set the notification flag, false to clear it
- */
- public static synchronized void setNotifyUiAccountsChanged(boolean setFlag) {
- sAccountsChangedNotification = setFlag;
- }
-
- /**
- * Called from activity onResume() functions to check for an accounts-changed condition, at
- * which point they should finish() and jump to the Welcome activity.
- */
- public static synchronized boolean getNotifyUiAccountsChanged() {
- return sAccountsChangedNotification;
- }
-
- public static void warnIfUiThread() {
- if (Thread.currentThread().equals(sUiThread)) {
- Log.w(Logging.LOG_TAG, "Method called on the UI thread", new Exception("STACK TRACE"));
- }
- }
-
- /**
- * Retrieve a simple string that can be used when message decoders encounter bad data.
- * This is provided here because the protocol decoders typically don't have mContext.
- */
- public static String getMessageDecodeErrorString() {
- return sMessageDecodeErrorString != null ? sMessageDecodeErrorString : "";
- }
-
- public static void enableStrictMode(boolean enabled) {
- Utility.enableStrictMode(enabled);
- }
-}
diff --git a/email2/src/com/android/email/EmailConnectivityManager.java b/email2/src/com/android/email/EmailConnectivityManager.java
index c618c38..6746152 100644
--- a/email2/src/com/android/email/EmailConnectivityManager.java
+++ b/email2/src/com/android/email/EmailConnectivityManager.java
@@ -29,6 +29,8 @@
import android.os.PowerManager.WakeLock;
import android.util.Log;
+import com.android.email2.ui.MailActivityEmail;
+
/**
* Encapsulates functionality of ConnectivityManager for use in the Email application. In
* particular, this class provides callbacks for connectivity lost, connectivity restored, and
@@ -179,14 +181,14 @@
if (info != null) {
// We're done if there's an active network
if (waiting) {
- if (Email.DEBUG) {
+ if (MailActivityEmail.DEBUG) {
Log.d(TAG, mName + ": Connectivity wait ended");
}
}
return;
} else {
if (!waiting) {
- if (Email.DEBUG) {
+ if (MailActivityEmail.DEBUG) {
Log.d(TAG, mName + ": Connectivity waiting...");
}
waiting = true;
diff --git a/email2/src/com/android/email/NotificationController.java b/email2/src/com/android/email/NotificationController.java
index 54f03bb..b64f04a 100644
--- a/email2/src/com/android/email/NotificationController.java
+++ b/email2/src/com/android/email/NotificationController.java
@@ -31,6 +31,7 @@
import android.graphics.BitmapFactory;
import android.media.AudioManager;
import android.net.Uri;
+import android.os.Debug;
import android.os.Handler;
import android.os.Looper;
import android.os.Process;
@@ -41,6 +42,7 @@
import com.android.email.activity.setup.AccountSecurity;
import com.android.email.activity.setup.AccountSettings;
import com.android.email.provider.EmailProvider;
+import com.android.email2.ui.MailActivityEmail;
import com.android.emailcommon.Logging;
import com.android.emailcommon.mail.Address;
import com.android.emailcommon.provider.Account;
@@ -230,7 +232,7 @@
* notifications enabled. Otherwise, all observers are unregistered.
*/
public void watchForMessages(final boolean watch) {
- if (Email.DEBUG) {
+ if (MailActivityEmail.DEBUG) {
Log.i(Logging.LOG_TAG, "Notifications being toggled: " + watch);
}
// Don't create the thread if we're only going to stop watching
@@ -259,7 +261,7 @@
registerMessageNotification(Account.ACCOUNT_ID_COMBINED_VIEW);
// If we're already observing account changes, don't do anything else
if (mAccountObserver == null) {
- if (Email.DEBUG) {
+ if (MailActivityEmail.DEBUG) {
Log.i(Logging.LOG_TAG, "Observing account changes for notifications");
}
mAccountObserver = new AccountContentObserver(sNotificationHandler, mContext);
@@ -341,7 +343,7 @@
} else {
ContentObserver obs = mNotificationMap.get(accountId);
if (obs != null) return; // we're already observing; nothing to do
- if (Email.DEBUG) {
+ if (MailActivityEmail.DEBUG) {
Log.i(Logging.LOG_TAG, "Registering for notifications for account " + accountId);
}
ContentObserver observer = new MessageContentObserver(
@@ -364,7 +366,7 @@
private void unregisterMessageNotification(long accountId) {
ContentResolver resolver = mContext.getContentResolver();
if (accountId == Account.ACCOUNT_ID_COMBINED_VIEW) {
- if (Email.DEBUG) {
+ if (MailActivityEmail.DEBUG) {
Log.i(Logging.LOG_TAG, "Unregistering notifications for all accounts");
}
// cancel all existing message observers
@@ -373,7 +375,7 @@
}
mNotificationMap.clear();
} else {
- if (Email.DEBUG) {
+ if (MailActivityEmail.DEBUG) {
Log.i(Logging.LOG_TAG, "Unregistering notifications for account " + accountId);
}
ContentObserver observer = mNotificationMap.remove(accountId);
@@ -407,16 +409,16 @@
public static final String EXTRA_CONVERSATION = "conversationUri";
public static final String EXTRA_FOLDER = "folder";
-// private Intent createViewConversationIntent(Conversation conversation, Folder folder,
-// com.android.mail.providers.Account account) {
-// final Intent intent = new Intent(Intent.ACTION_VIEW);
-// intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
-// intent.setDataAndType(conversation.uri, account.mimeType);
-// intent.putExtra(EXTRA_ACCOUNT, account);
-// intent.putExtra(EXTRA_FOLDER, folder);
-// intent.putExtra(EXTRA_CONVERSATION, conversation);
-// return intent;
-// }
+ private Intent createViewConversationIntent(Conversation conversation, Folder folder,
+ com.android.mail.providers.Account account) {
+ final Intent intent = new Intent(Intent.ACTION_VIEW);
+ intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
+ intent.setDataAndType(conversation.uri, account.mimeType);
+ intent.putExtra(EXTRA_ACCOUNT, account);
+ intent.putExtra(EXTRA_FOLDER, folder);
+ intent.putExtra(EXTRA_CONVERSATION, conversation);
+ return intent;
+ }
private Cursor getUiCursor(Uri uri, String[] projection) {
Cursor c = mContext.getContentResolver().query(uri, projection, null, null, null);
@@ -440,12 +442,12 @@
if (c == null) return null;
Folder folder = new Folder(c);
c.close();
- c = getUiCursor(EmailProvider.uiUri("uimessage", message.mId),
- UIProvider.MESSAGE_PROJECTION);
+ c = getUiCursor(EmailProvider.uiUri("uiconversation", message.mId),
+ UIProvider.CONVERSATION_PROJECTION);
if (c == null) return null;
Conversation conv = new Conversation(c);
c.close();
- return null; //return createViewConversationIntent(conv, folder, acct);
+ return createViewConversationIntent(conv, folder, acct);
}
/**
@@ -498,17 +500,16 @@
// if (unseenMessageCount > 1) {
// intent = createViewConversationIntent(message);
// } else {
-// intent = createViewConversationIntent(message);
+ intent = createViewConversationIntent(message);
// }
-// intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
+ intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
long now = mClock.getTime();
boolean enableAudio = (now - mLastMessageNotifyTime) > MIN_SOUND_INTERVAL_MS;
-// Notification notification = createMailboxNotification(
-// mailbox, title.toString(), title, text,
-// intent, largeIcon, number, enableAudio, false);
+ Notification notification = createMailboxNotification(
+ mailbox, title.toString(), title, text,
+ intent, largeIcon, number, enableAudio, false);
mLastMessageNotifyTime = now;
- return null;
-// return notification;
+ return notification;
}
/**
diff --git a/email2/src/com/android/email/SecurityPolicy.java b/email2/src/com/android/email/SecurityPolicy.java
index c2a2871..9b6cfb8 100644
--- a/email2/src/com/android/email/SecurityPolicy.java
+++ b/email2/src/com/android/email/SecurityPolicy.java
@@ -34,6 +34,7 @@
import com.android.email.provider.EmailProvider;
import com.android.email.service.EmailBroadcastProcessorService;
+import com.android.email2.ui.MailActivityEmail;
import com.android.emailcommon.Logging;
import com.android.emailcommon.provider.Account;
import com.android.emailcommon.provider.EmailContent;
@@ -136,7 +137,7 @@
try {
while (c.moveToNext()) {
policy.restore(c);
- if (Email.DEBUG) {
+ if (MailActivityEmail.DEBUG) {
Log.d(TAG, "Aggregate from: " + policy);
}
aggregate.mPasswordMinLength =
@@ -181,12 +182,12 @@
aggregate.mPasswordExpirationDays = 0;
if (aggregate.mPasswordComplexChars == Integer.MIN_VALUE)
aggregate.mPasswordComplexChars = 0;
- if (Email.DEBUG) {
+ if (MailActivityEmail.DEBUG) {
Log.d(TAG, "Calculated Aggregate: " + aggregate);
}
return aggregate;
}
- if (Email.DEBUG) {
+ if (MailActivityEmail.DEBUG) {
Log.d(TAG, "Calculated Aggregate: no policy");
}
return Policy.NO_POLICY;
@@ -228,7 +229,7 @@
* rollbacks.
*/
public void reducePolicies() {
- if (Email.DEBUG) {
+ if (MailActivityEmail.DEBUG) {
Log.d(TAG, "reducePolicies");
}
policiesUpdated();
@@ -243,7 +244,7 @@
*/
public boolean isActive(Policy policy) {
int reasons = getInactiveReasons(policy);
- if (Email.DEBUG && (reasons != 0)) {
+ if (MailActivityEmail.DEBUG && (reasons != 0)) {
StringBuilder sb = new StringBuilder("isActive for " + policy + ": ");
if (reasons == 0) {
sb.append("true");
@@ -407,12 +408,12 @@
Policy aggregatePolicy = getAggregatePolicy();
// if empty set, detach from policy manager
if (aggregatePolicy == Policy.NO_POLICY) {
- if (Email.DEBUG) {
+ if (MailActivityEmail.DEBUG) {
Log.d(TAG, "setActivePolicies: none, remove admin");
}
dpm.removeActiveAdmin(mAdminName);
} else if (isActiveAdmin()) {
- if (Email.DEBUG) {
+ if (MailActivityEmail.DEBUG) {
Log.d(TAG, "setActivePolicies: " + aggregatePolicy);
}
// set each policy in the policy manager
@@ -488,7 +489,7 @@
if (account.mPolicyKey == 0) return;
Policy policy = Policy.restorePolicyWithId(mContext, account.mPolicyKey);
if (policy == null) return;
- if (Email.DEBUG) {
+ if (MailActivityEmail.DEBUG) {
Log.d(TAG, "policiesRequired for " + account.mDisplayName + ": " + policy);
}
diff --git a/email2/src/com/android/email/activity/ActivityHelper.java b/email2/src/com/android/email/activity/ActivityHelper.java
index 1ada78c..62d21f6 100644
--- a/email2/src/com/android/email/activity/ActivityHelper.java
+++ b/email2/src/com/android/email/activity/ActivityHelper.java
@@ -23,8 +23,8 @@
import android.provider.Browser;
import android.view.WindowManager;
-import com.android.email.Email;
import com.android.email.activity.setup.AccountSecurity;
+import com.android.email2.ui.MailActivityEmail;
import com.android.emailcommon.provider.Account;
/**
@@ -94,7 +94,7 @@
* NOTE: Currently, this only works if HW accel is *not* enabled via the manifest.
*/
public static void debugSetWindowFlags(Activity activity) {
- if (Email.sDebugInhibitGraphicsAcceleration) {
+ if (MailActivityEmail.sDebugInhibitGraphicsAcceleration) {
// Clear the flag in the activity's window
activity.getWindow().setFlags(0,
WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED);
diff --git a/email2/src/com/android/email/activity/setup/AccountSecurity.java b/email2/src/com/android/email/activity/setup/AccountSecurity.java
index ec336d5..5e6dd9f 100644
--- a/email2/src/com/android/email/activity/setup/AccountSecurity.java
+++ b/email2/src/com/android/email/activity/setup/AccountSecurity.java
@@ -29,10 +29,10 @@
import android.os.Bundle;
import android.util.Log;
-import com.android.email.Email;
import com.android.email.R;
import com.android.email.SecurityPolicy;
import com.android.email.activity.ActivityHelper;
+import com.android.email2.ui.MailActivityEmail;
import com.android.emailcommon.provider.Account;
import com.android.emailcommon.provider.HostAuth;
import com.android.emailcommon.utility.Utility;
@@ -178,7 +178,7 @@
// Step 1. Check if we are an active device administrator, and stop here to activate
if (!security.isActiveAdmin()) {
if (mTriedAddAdministrator) {
- if (Email.DEBUG) {
+ if (MailActivityEmail.DEBUG) {
Log.d(TAG, "Not active admin: repost notification");
}
repostNotification(account, security);
@@ -188,13 +188,13 @@
// retrieve name of server for the format string
HostAuth hostAuth = HostAuth.restoreHostAuthWithId(this, account.mHostAuthKeyRecv);
if (hostAuth == null) {
- if (Email.DEBUG) {
+ if (MailActivityEmail.DEBUG) {
Log.d(TAG, "No HostAuth: repost notification");
}
repostNotification(account, security);
finish();
} else {
- if (Email.DEBUG) {
+ if (MailActivityEmail.DEBUG) {
Log.d(TAG, "Not active admin: post initial notification");
}
// try to become active - must happen here in activity, to get result
@@ -213,7 +213,7 @@
// 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 (Email.DEBUG) {
+ if (MailActivityEmail.DEBUG) {
Log.d(TAG, "Security active; clear holds");
}
Account.clearSecurityHoldOnAllAccounts(this);
@@ -232,13 +232,13 @@
// Step 5. If password is needed, try to have the user set it
if ((inactiveReasons & SecurityPolicy.INACTIVE_NEED_PASSWORD) != 0) {
if (mTriedSetPassword) {
- if (Email.DEBUG) {
+ if (MailActivityEmail.DEBUG) {
Log.d(TAG, "Password needed; repost notification");
}
repostNotification(account, security);
finish();
} else {
- if (Email.DEBUG) {
+ if (MailActivityEmail.DEBUG) {
Log.d(TAG, "Password needed; request it via DPM");
}
mTriedSetPassword = true;
@@ -252,13 +252,13 @@
// Step 6. If encryption is needed, try to have the user set it
if ((inactiveReasons & SecurityPolicy.INACTIVE_NEED_ENCRYPTION) != 0) {
if (mTriedSetEncryption) {
- if (Email.DEBUG) {
+ if (MailActivityEmail.DEBUG) {
Log.d(TAG, "Encryption needed; repost notification");
}
repostNotification(account, security);
finish();
} else {
- if (Email.DEBUG) {
+ if (MailActivityEmail.DEBUG) {
Log.d(TAG, "Encryption needed; request it via DPM");
}
mTriedSetEncryption = true;
@@ -270,7 +270,7 @@
}
// Step 7. No problems were found, so clear holds and exit
- if (Email.DEBUG) {
+ if (MailActivityEmail.DEBUG) {
Log.d(TAG, "Policies enforced; clear holds");
}
Account.clearSecurityHoldOnAllAccounts(this);
@@ -324,7 +324,7 @@
b.setMessage(res.getString(R.string.account_security_dialog_content_fmt, accountName));
b.setPositiveButton(R.string.okay_action, this);
b.setNegativeButton(R.string.cancel_action, this);
- if (Email.DEBUG) {
+ if (MailActivityEmail.DEBUG) {
Log.d(TAG, "Posting security needed dialog");
}
return b.create();
@@ -341,13 +341,13 @@
}
switch (which) {
case DialogInterface.BUTTON_POSITIVE:
- if (Email.DEBUG) {
+ if (MailActivityEmail.DEBUG) {
Log.d(TAG, "User accepts; advance to next step");
}
activity.tryAdvanceSecurity(activity.mAccount);
break;
case DialogInterface.BUTTON_NEGATIVE:
- if (Email.DEBUG) {
+ if (MailActivityEmail.DEBUG) {
Log.d(TAG, "User declines; repost notification");
}
activity.repostNotification(
diff --git a/email2/src/com/android/email/activity/setup/AccountSettingsEditQuickResponsesFragment.java b/email2/src/com/android/email/activity/setup/AccountSettingsEditQuickResponsesFragment.java
index 776291e..9e70aeb 100644
--- a/email2/src/com/android/email/activity/setup/AccountSettingsEditQuickResponsesFragment.java
+++ b/email2/src/com/android/email/activity/setup/AccountSettingsEditQuickResponsesFragment.java
@@ -16,9 +16,9 @@
package com.android.email.activity.setup;
-import com.android.email.Email;
import com.android.email.R;
import com.android.email.activity.UiUtilities;
+import com.android.email2.ui.MailActivityEmail;
import com.android.emailcommon.Logging;
import com.android.emailcommon.provider.EmailContent;
import com.android.emailcommon.provider.Account;
@@ -225,7 +225,7 @@
@Override
public void onCreate(Bundle savedInstanceState) {
- if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) {
+ if (Logging.DEBUG_LIFECYCLE && MailActivityEmail.DEBUG) {
Log.d(Logging.LOG_TAG, "AccountSettingsEditQuickResponsesFragment onCreate");
}
super.onCreate(savedInstanceState);
@@ -238,7 +238,7 @@
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
- if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) {
+ if (Logging.DEBUG_LIFECYCLE && MailActivityEmail.DEBUG) {
Log.d(Logging.LOG_TAG, "AccountSettingsEditQuickResponsesFragment onCreateView");
}
int layoutId = R.layout.account_settings_edit_quick_responses_fragment;
diff --git a/email2/src/com/android/email/activity/setup/AccountSettingsFragment.java b/email2/src/com/android/email/activity/setup/AccountSettingsFragment.java
index e955dd4..ccd5f9c 100644
--- a/email2/src/com/android/email/activity/setup/AccountSettingsFragment.java
+++ b/email2/src/com/android/email/activity/setup/AccountSettingsFragment.java
@@ -42,10 +42,10 @@
import android.text.TextUtils;
import android.util.Log;
-import com.android.email.Email;
import com.android.email.R;
import com.android.email.SecurityPolicy;
import com.android.email.mail.Sender;
+import com.android.email2.ui.MailActivityEmail;
import com.android.emailcommon.AccountManagerTypes;
import com.android.emailcommon.CalendarProviderStub;
import com.android.emailcommon.Logging;
@@ -180,7 +180,7 @@
*/
@Override
public void onCreate(Bundle savedInstanceState) {
- if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) {
+ if (Logging.DEBUG_LIFECYCLE && MailActivityEmail.DEBUG) {
Log.d(Logging.LOG_TAG, "AccountSettingsFragment onCreate");
}
super.onCreate(savedInstanceState);
@@ -204,7 +204,7 @@
@Override
public void onActivityCreated(Bundle savedInstanceState) {
- if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) {
+ if (Logging.DEBUG_LIFECYCLE && MailActivityEmail.DEBUG) {
Log.d(Logging.LOG_TAG, "AccountSettingsFragment onActivityCreated");
}
super.onActivityCreated(savedInstanceState);
@@ -215,7 +215,7 @@
*/
@Override
public void onStart() {
- if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) {
+ if (Logging.DEBUG_LIFECYCLE && MailActivityEmail.DEBUG) {
Log.d(Logging.LOG_TAG, "AccountSettingsFragment onStart");
}
super.onStart();
@@ -234,7 +234,7 @@
*/
@Override
public void onResume() {
- if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) {
+ if (Logging.DEBUG_LIFECYCLE && MailActivityEmail.DEBUG) {
Log.d(Logging.LOG_TAG, "AccountSettingsFragment onResume");
}
super.onResume();
@@ -263,7 +263,7 @@
@Override
public void onPause() {
- if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) {
+ if (Logging.DEBUG_LIFECYCLE && MailActivityEmail.DEBUG) {
Log.d(Logging.LOG_TAG, "AccountSettingsFragment onPause");
}
super.onPause();
@@ -277,7 +277,7 @@
*/
@Override
public void onStop() {
- if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) {
+ if (Logging.DEBUG_LIFECYCLE && MailActivityEmail.DEBUG) {
Log.d(Logging.LOG_TAG, "AccountSettingsFragment onStop");
}
super.onStop();
@@ -348,7 +348,7 @@
*/
@Override
public void onDestroy() {
- if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) {
+ if (Logging.DEBUG_LIFECYCLE && MailActivityEmail.DEBUG) {
Log.d(Logging.LOG_TAG, "AccountSettingsFragment onDestroy");
}
super.onDestroy();
@@ -359,7 +359,7 @@
@Override
public void onSaveInstanceState(Bundle outState) {
- if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) {
+ if (Logging.DEBUG_LIFECYCLE && MailActivityEmail.DEBUG) {
Log.d(Logging.LOG_TAG, "AccountSettingsFragment onSaveInstanceState");
}
super.onSaveInstanceState(outState);
@@ -773,7 +773,7 @@
mAccount.update(mContext, cv);
// Run the remaining changes off-thread
- Email.setServicesEnabledAsync(mContext);
+ MailActivityEmail.setServicesEnabledAsync(mContext);
}
/**
diff --git a/email2/src/com/android/email/activity/setup/AccountSetupExchangeFragment.java b/email2/src/com/android/email/activity/setup/AccountSetupExchangeFragment.java
index 1ff5bee..299c1f0 100644
--- a/email2/src/com/android/email/activity/setup/AccountSetupExchangeFragment.java
+++ b/email2/src/com/android/email/activity/setup/AccountSetupExchangeFragment.java
@@ -34,13 +34,13 @@
import android.widget.EditText;
import android.widget.TextView;
-import com.android.email.Email;
import com.android.email.R;
import com.android.email.activity.UiUtilities;
import com.android.email.provider.AccountBackupRestore;
import com.android.email.service.EmailServiceUtils;
import com.android.email.view.CertificateSelector;
import com.android.email.view.CertificateSelector.HostCallback;
+import com.android.email2.ui.MailActivityEmail;
import com.android.emailcommon.Device;
import com.android.emailcommon.Logging;
import com.android.emailcommon.provider.Account;
@@ -81,7 +81,7 @@
*/
@Override
public void onCreate(Bundle savedInstanceState) {
- if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) {
+ if (Logging.DEBUG_LIFECYCLE && MailActivityEmail.DEBUG) {
Log.d(Logging.LOG_TAG, "AccountSetupExchangeFragment onCreate");
}
super.onCreate(savedInstanceState);
@@ -96,7 +96,7 @@
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
- if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) {
+ if (Logging.DEBUG_LIFECYCLE && MailActivityEmail.DEBUG) {
Log.d(Logging.LOG_TAG, "AccountSetupExchangeFragment onCreateView");
}
int layoutId = mSettingsMode
@@ -152,7 +152,7 @@
@Override
public void onActivityCreated(Bundle savedInstanceState) {
- if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) {
+ if (Logging.DEBUG_LIFECYCLE && MailActivityEmail.DEBUG) {
Log.d(Logging.LOG_TAG, "AccountSetupExchangeFragment onActivityCreated");
}
super.onActivityCreated(savedInstanceState);
@@ -164,7 +164,7 @@
*/
@Override
public void onStart() {
- if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) {
+ if (Logging.DEBUG_LIFECYCLE && MailActivityEmail.DEBUG) {
Log.d(Logging.LOG_TAG, "AccountSetupExchangeFragment onStart");
}
super.onStart();
@@ -177,7 +177,7 @@
*/
@Override
public void onResume() {
- if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) {
+ if (Logging.DEBUG_LIFECYCLE && MailActivityEmail.DEBUG) {
Log.d(Logging.LOG_TAG, "AccountSetupExchangeFragment onResume");
}
super.onResume();
@@ -186,7 +186,7 @@
@Override
public void onPause() {
- if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) {
+ if (Logging.DEBUG_LIFECYCLE && MailActivityEmail.DEBUG) {
Log.d(Logging.LOG_TAG, "AccountSetupExchangeFragment onPause");
}
super.onPause();
@@ -197,7 +197,7 @@
*/
@Override
public void onStop() {
- if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) {
+ if (Logging.DEBUG_LIFECYCLE && MailActivityEmail.DEBUG) {
Log.d(Logging.LOG_TAG, "AccountSetupExchangeFragment onStop");
}
super.onStop();
@@ -209,7 +209,7 @@
*/
@Override
public void onDestroy() {
- if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) {
+ if (Logging.DEBUG_LIFECYCLE && MailActivityEmail.DEBUG) {
Log.d(Logging.LOG_TAG, "AccountSetupExchangeFragment onDestroy");
}
super.onDestroy();
@@ -217,7 +217,7 @@
@Override
public void onSaveInstanceState(Bundle outState) {
- if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) {
+ if (Logging.DEBUG_LIFECYCLE && MailActivityEmail.DEBUG) {
Log.d(Logging.LOG_TAG, "AccountSetupExchangeFragment onSaveInstanceState");
}
super.onSaveInstanceState(outState);
diff --git a/email2/src/com/android/email/activity/setup/AccountSetupIncomingFragment.java b/email2/src/com/android/email/activity/setup/AccountSetupIncomingFragment.java
index efd63e3..d55e7df 100644
--- a/email2/src/com/android/email/activity/setup/AccountSetupIncomingFragment.java
+++ b/email2/src/com/android/email/activity/setup/AccountSetupIncomingFragment.java
@@ -34,10 +34,10 @@
import android.widget.Spinner;
import android.widget.TextView;
-import com.android.email.Email;
import com.android.email.R;
import com.android.email.activity.UiUtilities;
import com.android.email.provider.AccountBackupRestore;
+import com.android.email2.ui.MailActivityEmail;
import com.android.emailcommon.Logging;
import com.android.emailcommon.provider.Account;
import com.android.emailcommon.provider.HostAuth;
@@ -85,7 +85,7 @@
*/
@Override
public void onCreate(Bundle savedInstanceState) {
- if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) {
+ if (Logging.DEBUG_LIFECYCLE && MailActivityEmail.DEBUG) {
Log.d(Logging.LOG_TAG, "AccountSetupIncomingFragment onCreate");
}
super.onCreate(savedInstanceState);
@@ -99,7 +99,7 @@
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
- if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) {
+ if (Logging.DEBUG_LIFECYCLE && MailActivityEmail.DEBUG) {
Log.d(Logging.LOG_TAG, "AccountSetupIncomingFragment onCreateView");
}
int layoutId = mSettingsMode
@@ -193,7 +193,7 @@
@Override
public void onActivityCreated(Bundle savedInstanceState) {
- if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) {
+ if (Logging.DEBUG_LIFECYCLE && MailActivityEmail.DEBUG) {
Log.d(Logging.LOG_TAG, "AccountSetupIncomingFragment onActivityCreated");
}
super.onActivityCreated(savedInstanceState);
@@ -204,7 +204,7 @@
*/
@Override
public void onStart() {
- if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) {
+ if (Logging.DEBUG_LIFECYCLE && MailActivityEmail.DEBUG) {
Log.d(Logging.LOG_TAG, "AccountSetupIncomingFragment onStart");
}
super.onStart();
@@ -218,7 +218,7 @@
*/
@Override
public void onResume() {
- if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) {
+ if (Logging.DEBUG_LIFECYCLE && MailActivityEmail.DEBUG) {
Log.d(Logging.LOG_TAG, "AccountSetupIncomingFragment onResume");
}
super.onResume();
@@ -227,7 +227,7 @@
@Override
public void onPause() {
- if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) {
+ if (Logging.DEBUG_LIFECYCLE && MailActivityEmail.DEBUG) {
Log.d(Logging.LOG_TAG, "AccountSetupIncomingFragment onPause");
}
super.onPause();
@@ -238,7 +238,7 @@
*/
@Override
public void onStop() {
- if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) {
+ if (Logging.DEBUG_LIFECYCLE && MailActivityEmail.DEBUG) {
Log.d(Logging.LOG_TAG, "AccountSetupIncomingFragment onStop");
}
super.onStop();
@@ -250,7 +250,7 @@
*/
@Override
public void onDestroy() {
- if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) {
+ if (Logging.DEBUG_LIFECYCLE && MailActivityEmail.DEBUG) {
Log.d(Logging.LOG_TAG, "AccountSetupIncomingFragment onDestroy");
}
super.onDestroy();
@@ -258,7 +258,7 @@
@Override
public void onSaveInstanceState(Bundle outState) {
- if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) {
+ if (Logging.DEBUG_LIFECYCLE && MailActivityEmail.DEBUG) {
Log.d(Logging.LOG_TAG, "AccountSetupIncomingFragment onSaveInstanceState");
}
super.onSaveInstanceState(outState);
diff --git a/email2/src/com/android/email/activity/setup/AccountSetupOptions.java b/email2/src/com/android/email/activity/setup/AccountSetupOptions.java
index 1a246c9..a541751 100644
--- a/email2/src/com/android/email/activity/setup/AccountSetupOptions.java
+++ b/email2/src/com/android/email/activity/setup/AccountSetupOptions.java
@@ -35,12 +35,12 @@
import android.widget.CheckBox;
import android.widget.Spinner;
-import com.android.email.Email;
import com.android.email.R;
import com.android.email.activity.ActivityHelper;
import com.android.email.activity.UiUtilities;
import com.android.email.service.EmailServiceUtils;
import com.android.email.service.MailService;
+import com.android.email2.ui.MailActivityEmail;
import com.android.emailcommon.Logging;
import com.android.emailcommon.provider.Account;
import com.android.emailcommon.provider.HostAuth;
@@ -368,7 +368,7 @@
account.mFlags &= ~Account.FLAGS_SECURITY_HOLD;
AccountSettingsUtils.commitSettings(context, account);
// Start up services based on new account(s)
- Email.setServicesEnabledSync(context);
+ MailActivityEmail.setServicesEnabledSync(context);
EmailServiceUtils.startExchangeService(context);
// Move to final setup screen
AccountSetupNames.actionSetNames(context);
diff --git a/email2/src/com/android/email/activity/setup/AccountSetupOutgoingFragment.java b/email2/src/com/android/email/activity/setup/AccountSetupOutgoingFragment.java
index b903932..59191fb 100644
--- a/email2/src/com/android/email/activity/setup/AccountSetupOutgoingFragment.java
+++ b/email2/src/com/android/email/activity/setup/AccountSetupOutgoingFragment.java
@@ -34,10 +34,10 @@
import android.widget.EditText;
import android.widget.Spinner;
-import com.android.email.Email;
import com.android.email.R;
import com.android.email.activity.UiUtilities;
import com.android.email.provider.AccountBackupRestore;
+import com.android.email2.ui.MailActivityEmail;
import com.android.emailcommon.Logging;
import com.android.emailcommon.provider.Account;
import com.android.emailcommon.provider.HostAuth;
@@ -74,7 +74,7 @@
*/
@Override
public void onCreate(Bundle savedInstanceState) {
- if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) {
+ if (Logging.DEBUG_LIFECYCLE && MailActivityEmail.DEBUG) {
Log.d(Logging.LOG_TAG, "AccountSetupOutgoingFragment onCreate");
}
super.onCreate(savedInstanceState);
@@ -88,7 +88,7 @@
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
- if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) {
+ if (Logging.DEBUG_LIFECYCLE && MailActivityEmail.DEBUG) {
Log.d(Logging.LOG_TAG, "AccountSetupOutgoingFragment onCreateView");
}
int layoutId = mSettingsMode
@@ -160,7 +160,7 @@
@Override
public void onActivityCreated(Bundle savedInstanceState) {
- if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) {
+ if (Logging.DEBUG_LIFECYCLE && MailActivityEmail.DEBUG) {
Log.d(Logging.LOG_TAG, "AccountSetupOutgoingFragment onActivityCreated");
}
super.onActivityCreated(savedInstanceState);
@@ -171,7 +171,7 @@
*/
@Override
public void onStart() {
- if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) {
+ if (Logging.DEBUG_LIFECYCLE && MailActivityEmail.DEBUG) {
Log.d(Logging.LOG_TAG, "AccountSetupOutgoingFragment onStart");
}
super.onStart();
@@ -184,7 +184,7 @@
*/
@Override
public void onResume() {
- if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) {
+ if (Logging.DEBUG_LIFECYCLE && MailActivityEmail.DEBUG) {
Log.d(Logging.LOG_TAG, "AccountSetupOutgoingFragment onResume");
}
super.onResume();
@@ -193,7 +193,7 @@
@Override
public void onPause() {
- if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) {
+ if (Logging.DEBUG_LIFECYCLE && MailActivityEmail.DEBUG) {
Log.d(Logging.LOG_TAG, "AccountSetupOutgoingFragment onPause");
}
super.onPause();
@@ -204,7 +204,7 @@
*/
@Override
public void onStop() {
- if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) {
+ if (Logging.DEBUG_LIFECYCLE && MailActivityEmail.DEBUG) {
Log.d(Logging.LOG_TAG, "AccountSetupOutgoingFragment onStop");
}
super.onStop();
@@ -216,7 +216,7 @@
*/
@Override
public void onDestroy() {
- if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) {
+ if (Logging.DEBUG_LIFECYCLE && MailActivityEmail.DEBUG) {
Log.d(Logging.LOG_TAG, "AccountSetupOutgoingFragment onDestroy");
}
super.onDestroy();
@@ -224,7 +224,7 @@
@Override
public void onSaveInstanceState(Bundle outState) {
- if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) {
+ if (Logging.DEBUG_LIFECYCLE && MailActivityEmail.DEBUG) {
Log.d(Logging.LOG_TAG, "AccountSetupOutgoingFragment onSaveInstanceState");
}
super.onSaveInstanceState(outState);
diff --git a/email2/src/com/android/email/activity/setup/DebugFragment.java b/email2/src/com/android/email/activity/setup/DebugFragment.java
index d2b8409..dab0056 100644
--- a/email2/src/com/android/email/activity/setup/DebugFragment.java
+++ b/email2/src/com/android/email/activity/setup/DebugFragment.java
@@ -16,11 +16,11 @@
package com.android.email.activity.setup;
-import com.android.email.Email;
import com.android.email.Preferences;
import com.android.email.R;
import com.android.email.activity.UiUtilities;
import com.android.email.service.EmailServiceUtils;
+import com.android.email2.ui.MailActivityEmail;
import com.android.emailcommon.Logging;
import android.app.Fragment;
@@ -50,7 +50,7 @@
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
- if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) {
+ if (Logging.DEBUG_LIFECYCLE && MailActivityEmail.DEBUG) {
Log.d(Logging.LOG_TAG, "AccountSetupBasicsFragment onCreateView");
}
View view = inflater.inflate(R.layout.debug, container, false);
@@ -63,7 +63,7 @@
context.getString(R.string.build_number)));
mEnableDebugLoggingView = (CheckBox) UiUtilities.getView(view, R.id.debug_logging);
- mEnableDebugLoggingView.setChecked(Email.DEBUG);
+ mEnableDebugLoggingView.setChecked(MailActivityEmail.DEBUG);
mEnableExchangeLoggingView = (CheckBox) UiUtilities.getView(view, R.id.exchange_logging);
mEnableExchangeFileLoggingView =
@@ -74,8 +74,8 @@
boolean exchangeAvailable = EmailServiceUtils.isExchangeAvailable(context);
if (exchangeAvailable) {
- mEnableExchangeLoggingView.setChecked(Email.DEBUG_EXCHANGE_VERBOSE);
- mEnableExchangeFileLoggingView.setChecked(Email.DEBUG_EXCHANGE_FILE);
+ mEnableExchangeLoggingView.setChecked(MailActivityEmail.DEBUG_EXCHANGE_VERBOSE);
+ mEnableExchangeFileLoggingView.setChecked(MailActivityEmail.DEBUG_EXCHANGE_FILE);
mEnableExchangeLoggingView.setOnCheckedChangeListener(this);
mEnableExchangeFileLoggingView.setOnCheckedChangeListener(this);
} else {
@@ -87,7 +87,7 @@
mInhibitGraphicsAccelerationView = (CheckBox)
UiUtilities.getView(view, R.id.debug_disable_graphics_acceleration);
- mInhibitGraphicsAccelerationView.setChecked(Email.sDebugInhibitGraphicsAcceleration);
+ mInhibitGraphicsAccelerationView.setChecked(MailActivityEmail.sDebugInhibitGraphicsAcceleration);
mInhibitGraphicsAccelerationView.setOnCheckedChangeListener(this);
mEnableStrictModeView = (CheckBox)
@@ -103,28 +103,28 @@
switch (buttonView.getId()) {
case R.id.debug_logging:
mPreferences.setEnableDebugLogging(isChecked);
- Email.DEBUG = isChecked;
- Email.DEBUG_EXCHANGE = isChecked;
+ MailActivityEmail.DEBUG = isChecked;
+ MailActivityEmail.DEBUG_EXCHANGE = isChecked;
break;
case R.id.exchange_logging:
mPreferences.setEnableExchangeLogging(isChecked);
- Email.DEBUG_EXCHANGE_VERBOSE = isChecked;
+ MailActivityEmail.DEBUG_EXCHANGE_VERBOSE = isChecked;
break;
case R.id.exchange_file_logging:
mPreferences.setEnableExchangeFileLogging(isChecked);
- Email.DEBUG_EXCHANGE_FILE = isChecked;
+ MailActivityEmail.DEBUG_EXCHANGE_FILE = isChecked;
break;
case R.id.debug_disable_graphics_acceleration:
- Email.sDebugInhibitGraphicsAcceleration = isChecked;
+ MailActivityEmail.sDebugInhibitGraphicsAcceleration = isChecked;
mPreferences.setInhibitGraphicsAcceleration(isChecked);
break;
case R.id.debug_enable_strict_mode:
mPreferences.setEnableStrictMode(isChecked);
- Email.enableStrictMode(isChecked);
+ MailActivityEmail.enableStrictMode(isChecked);
break;
}
- Email.updateLoggingFlags(getActivity());
+ MailActivityEmail.updateLoggingFlags(getActivity());
}
@Override
diff --git a/email2/src/com/android/email/activity/setup/MailboxSettings.java b/email2/src/com/android/email/activity/setup/MailboxSettings.java
index 99d69fd..a4eb141 100644
--- a/email2/src/com/android/email/activity/setup/MailboxSettings.java
+++ b/email2/src/com/android/email/activity/setup/MailboxSettings.java
@@ -32,9 +32,9 @@
import android.util.Log;
import android.view.MenuItem;
-import com.android.email.Email;
import com.android.email.FolderProperties;
import com.android.email.R;
+import com.android.email2.ui.MailActivityEmail;
import com.android.emailcommon.Logging;
import com.android.emailcommon.provider.Account;
import com.android.emailcommon.provider.Policy;
@@ -292,7 +292,7 @@
return false;
}
mNeedsSave = true;
- if (Email.DEBUG) {
+ if (MailActivityEmail.DEBUG) {
Log.i(Logging.LOG_TAG, "Setting changed");
}
// In order to set the current entry to the summary, we need to udpate the value
@@ -310,7 +310,7 @@
private void updateObjects() {
final int syncInterval = Integer.valueOf(mSyncIntervalPref.getValue());
final int syncLookback = Integer.valueOf(mSyncLookbackPref.getValue());
- if (Email.DEBUG) {
+ if (MailActivityEmail.DEBUG) {
Log.i(Logging.LOG_TAG, "Updating object: " + syncInterval + "," + syncLookback);
}
if (mMailbox.mType == Mailbox.TYPE_INBOX) {
diff --git a/email2/src/com/android/email/mail/Store.java b/email2/src/com/android/email/mail/Store.java
index bdf9039..63e6f5a 100644
--- a/email2/src/com/android/email/mail/Store.java
+++ b/email2/src/com/android/email/mail/Store.java
@@ -20,10 +20,10 @@
import android.os.Bundle;
import android.util.Log;
-import com.android.email.Email;
import com.android.email.mail.store.ExchangeStore;
import com.android.email.mail.store.ImapStore;
import com.android.email.mail.store.Pop3Store;
+import com.android.email2.ui.MailActivityEmail;
import com.android.emailcommon.Logging;
import com.android.emailcommon.mail.Folder;
import com.android.emailcommon.mail.MessagingException;
@@ -204,6 +204,6 @@
//mailbox.mSyncTime;
mailbox.mType = type;
//box.mUnreadCount;
- mailbox.mVisibleLimit = Email.VISIBLE_LIMIT_DEFAULT;
+ mailbox.mVisibleLimit = MailActivityEmail.VISIBLE_LIMIT_DEFAULT;
}
}
diff --git a/email2/src/com/android/email/mail/store/ImapConnection.java b/email2/src/com/android/email/mail/store/ImapConnection.java
index 0fbf603..e83b9fc 100644
--- a/email2/src/com/android/email/mail/store/ImapConnection.java
+++ b/email2/src/com/android/email/mail/store/ImapConnection.java
@@ -19,7 +19,6 @@
import android.text.TextUtils;
import android.util.Log;
-import com.android.email.Email;
import com.android.email.mail.Transport;
import com.android.email.mail.store.ImapStore.ImapException;
import com.android.email.mail.store.imap.ImapConstants;
@@ -29,6 +28,7 @@
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.email2.ui.MailActivityEmail;
import com.android.emailcommon.Logging;
import com.android.emailcommon.mail.AuthenticationFailedException;
import com.android.emailcommon.mail.CertificateValidationException;
@@ -146,7 +146,7 @@
mImapStore.ensurePrefixIsValid();
} catch (SSLException e) {
- if (Email.DEBUG) {
+ if (MailActivityEmail.DEBUG) {
Log.d(Logging.LOG_TAG, e.toString());
}
throw new CertificateValidationException(e.getMessage(), e);
@@ -154,7 +154,7 @@
// 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 (Email.DEBUG) {
+ if (MailActivityEmail.DEBUG) {
Log.d(Logging.LOG_TAG, ioe.toString());
}
throw ioe;
@@ -391,7 +391,7 @@
executeSimpleCommand(mIdPhrase);
} catch (ImapException ie) {
// Log for debugging, but this is not a fatal problem.
- if (Email.DEBUG) {
+ if (MailActivityEmail.DEBUG) {
Log.d(Logging.LOG_TAG, ie.toString());
}
} catch (IOException ioe) {
@@ -416,7 +416,7 @@
responseList = executeSimpleCommand(ImapConstants.NAMESPACE);
} catch (ImapException ie) {
// Log for debugging, but this is not a fatal problem.
- if (Email.DEBUG) {
+ if (MailActivityEmail.DEBUG) {
Log.d(Logging.LOG_TAG, ie.toString());
}
} catch (IOException ioe) {
@@ -447,7 +447,7 @@
// options such as SASL
executeSimpleCommand(mLoginPhrase, true);
} catch (ImapException ie) {
- if (Email.DEBUG) {
+ if (MailActivityEmail.DEBUG) {
Log.d(Logging.LOG_TAG, ie.toString());
}
throw new AuthenticationFailedException(ie.getAlertText(), ie);
@@ -471,7 +471,7 @@
responseList = executeSimpleCommand(ImapConstants.LIST + " \"\" \"\"");
} catch (ImapException ie) {
// Log for debugging, but this is not a fatal problem.
- if (Email.DEBUG) {
+ if (MailActivityEmail.DEBUG) {
Log.d(Logging.LOG_TAG, ie.toString());
}
} catch (IOException ioe) {
@@ -504,7 +504,7 @@
// Per RFC requirement (3501-6.2.1) gather new capabilities
return(queryCapabilities());
} else {
- if (Email.DEBUG) {
+ if (MailActivityEmail.DEBUG) {
Log.d(Logging.LOG_TAG, "TLS not supported but required");
}
throw new MessagingException(MessagingException.TLS_REQUIRED);
diff --git a/email2/src/com/android/email/mail/store/ImapFolder.java b/email2/src/com/android/email/mail/store/ImapFolder.java
index 98d25c9..0609bb8 100644
--- a/email2/src/com/android/email/mail/store/ImapFolder.java
+++ b/email2/src/com/android/email/mail/store/ImapFolder.java
@@ -21,7 +21,6 @@
import android.util.Base64DataException;
import android.util.Log;
-import com.android.email.Email;
import com.android.email.mail.store.ImapStore.ImapException;
import com.android.email.mail.store.ImapStore.ImapMessage;
import com.android.email.mail.store.imap.ImapConstants;
@@ -32,6 +31,7 @@
import com.android.email.mail.store.imap.ImapUtility;
import com.android.email.mail.transport.CountingOutputStream;
import com.android.email.mail.transport.EOLConvertingOutputStream;
+import com.android.email2.ui.MailActivityEmail;
import com.android.emailcommon.Logging;
import com.android.emailcommon.internet.BinaryTempFileBody;
import com.android.emailcommon.internet.MimeBodyPart;
@@ -696,7 +696,7 @@
}
}
} catch (Base64DataException bde) {
- String warning = "\n\n" + Email.getMessageDecodeErrorString();
+ String warning = "\n\n" + MailActivityEmail.getMessageDecodeErrorString();
out.write(warning.getBytes());
} finally {
out.close();
@@ -1110,7 +1110,7 @@
}
private MessagingException ioExceptionHandler(ImapConnection connection, IOException ioe) {
- if (Email.DEBUG) {
+ if (MailActivityEmail.DEBUG) {
Log.d(Logging.LOG_TAG, "IO Exception detected: ", ioe);
}
connection.close();
diff --git a/email2/src/com/android/email/mail/store/Pop3Store.java b/email2/src/com/android/email/mail/store/Pop3Store.java
index 1e01ba8..6eaa6e5 100644
--- a/email2/src/com/android/email/mail/store/Pop3Store.java
+++ b/email2/src/com/android/email/mail/store/Pop3Store.java
@@ -20,11 +20,11 @@
import android.os.Bundle;
import android.util.Log;
-import com.android.email.Email;
import com.android.email.R;
import com.android.email.mail.Store;
import com.android.email.mail.Transport;
import com.android.email.mail.transport.MailTransport;
+import com.android.email2.ui.MailActivityEmail;
import com.android.emailcommon.Logging;
import com.android.emailcommon.internet.MimeMessage;
import com.android.emailcommon.mail.AuthenticationFailedException;
@@ -311,7 +311,7 @@
executeSimpleCommand("STLS");
mTransport.reopenTls();
} else {
- if (Email.DEBUG) {
+ if (MailActivityEmail.DEBUG) {
Log.d(Logging.LOG_TAG, "TLS not supported but required");
}
throw new MessagingException(MessagingException.TLS_REQUIRED);
@@ -322,14 +322,14 @@
executeSensitiveCommand("USER " + mUsername, "USER /redacted/");
executeSensitiveCommand("PASS " + mPassword, "PASS /redacted/");
} catch (MessagingException me) {
- if (Email.DEBUG) {
+ if (MailActivityEmail.DEBUG) {
Log.d(Logging.LOG_TAG, me.toString());
}
throw new AuthenticationFailedException(null, me);
}
} catch (IOException ioe) {
mTransport.close();
- if (Email.DEBUG) {
+ if (MailActivityEmail.DEBUG) {
Log.d(Logging.LOG_TAG, ioe.toString());
}
throw new MessagingException(MessagingException.IOERROR, ioe.toString());
@@ -351,7 +351,7 @@
}
if (statException != null) {
mTransport.close();
- if (Email.DEBUG) {
+ if (MailActivityEmail.DEBUG) {
Log.d(Logging.LOG_TAG, statException.toString());
}
throw new MessagingException("POP3 STAT", statException);
@@ -422,7 +422,7 @@
indexMsgNums(1, mMessageCount);
} catch (IOException ioe) {
mTransport.close();
- if (Email.DEBUG) {
+ if (MailActivityEmail.DEBUG) {
Log.d(Logging.LOG_TAG, "Unable to index during getMessage " + ioe);
}
throw new MessagingException("getMessages", ioe);
@@ -443,7 +443,7 @@
indexMsgNums(start, end);
} catch (IOException ioe) {
mTransport.close();
- if (Email.DEBUG) {
+ if (MailActivityEmail.DEBUG) {
Log.d(Logging.LOG_TAG, ioe.toString());
}
throw new MessagingException("getMessages", ioe);
@@ -687,7 +687,7 @@
}
} catch (IOException ioe) {
mTransport.close();
- if (Email.DEBUG) {
+ if (MailActivityEmail.DEBUG) {
Log.d(Logging.LOG_TAG, ioe.toString());
}
throw new MessagingException("fetch", ioe);
@@ -722,7 +722,7 @@
}
} catch (IOException ioe) {
mTransport.close();
- if (Email.DEBUG) {
+ if (MailActivityEmail.DEBUG) {
Log.d(Logging.LOG_TAG, ioe.toString());
}
throw new MessagingException("Unable to fetch message", ioe);
@@ -829,7 +829,7 @@
if (response != null) {
try {
InputStream in = mTransport.getInputStream();
- if (DEBUG_LOG_RAW_STREAM && Email.DEBUG) {
+ if (DEBUG_LOG_RAW_STREAM && MailActivityEmail.DEBUG) {
in = new LoggingInputStream(in);
}
message.parse(new Pop3ResponseInputStream(in));
@@ -883,7 +883,7 @@
}
catch (IOException ioe) {
mTransport.close();
- if (Email.DEBUG) {
+ if (MailActivityEmail.DEBUG) {
Log.d(Logging.LOG_TAG, ioe.toString());
}
throw new MessagingException("setFlags()", ioe);
diff --git a/email2/src/com/android/email/mail/store/imap/ImapResponseParser.java b/email2/src/com/android/email/mail/store/imap/ImapResponseParser.java
index 6b68c00..39bbe26 100644
--- a/email2/src/com/android/email/mail/store/imap/ImapResponseParser.java
+++ b/email2/src/com/android/email/mail/store/imap/ImapResponseParser.java
@@ -16,10 +16,10 @@
package com.android.email.mail.store.imap;
-import com.android.email.Email;
import com.android.email.FixedLengthInputStream;
import com.android.email.PeekableInputStream;
import com.android.email.mail.transport.DiscourseLogger;
+import com.android.email2.ui.MailActivityEmail;
import com.android.emailcommon.Logging;
import com.android.emailcommon.mail.MessagingException;
import com.android.emailcommon.utility.LoggingInputStream;
@@ -89,7 +89,7 @@
*/
/* package for test */ ImapResponseParser(InputStream in, DiscourseLogger discourseLogger,
int literalKeepInMemoryThreshold) {
- if (DEBUG_LOG_RAW_STREAM && Email.DEBUG) {
+ if (DEBUG_LOG_RAW_STREAM && MailActivityEmail.DEBUG) {
in = new LoggingInputStream(in);
}
mIn = new PeekableInputStream(in);
@@ -99,7 +99,7 @@
private static IOException newEOSException() {
final String message = "End of stream reached";
- if (Email.DEBUG) {
+ if (MailActivityEmail.DEBUG) {
Log.d(Logging.LOG_TAG, message);
}
return new IOException(message);
@@ -161,7 +161,7 @@
ImapResponse response = null;
try {
response = parseResponse();
- if (Email.DEBUG) {
+ if (MailActivityEmail.DEBUG) {
Log.d(Logging.LOG_TAG, "<<< " + response.toString());
}
diff --git a/email2/src/com/android/email/mail/transport/MailTransport.java b/email2/src/com/android/email/mail/transport/MailTransport.java
index 751be50..7f59f46 100644
--- a/email2/src/com/android/email/mail/transport/MailTransport.java
+++ b/email2/src/com/android/email/mail/transport/MailTransport.java
@@ -16,8 +16,8 @@
package com.android.email.mail.transport;
-import com.android.email.Email;
import com.android.email.mail.Transport;
+import com.android.email2.ui.MailActivityEmail;
import com.android.emailcommon.Logging;
import com.android.emailcommon.mail.CertificateValidationException;
import com.android.emailcommon.mail.MessagingException;
@@ -159,7 +159,7 @@
*/
@Override
public void open() throws MessagingException, CertificateValidationException {
- if (Email.DEBUG) {
+ if (MailActivityEmail.DEBUG) {
Log.d(Logging.LOG_TAG, "*** " + mDebugLabel + " open " +
getHost() + ":" + String.valueOf(getPort()));
}
@@ -180,12 +180,12 @@
mOut = new BufferedOutputStream(mSocket.getOutputStream(), 512);
} catch (SSLException e) {
- if (Email.DEBUG) {
+ if (MailActivityEmail.DEBUG) {
Log.d(Logging.LOG_TAG, e.toString());
}
throw new CertificateValidationException(e.getMessage(), e);
} catch (IOException ioe) {
- if (Email.DEBUG) {
+ if (MailActivityEmail.DEBUG) {
Log.d(Logging.LOG_TAG, ioe.toString());
}
throw new MessagingException(MessagingException.IOERROR, ioe.toString());
@@ -210,12 +210,12 @@
mOut = new BufferedOutputStream(mSocket.getOutputStream(), 512);
} catch (SSLException e) {
- if (Email.DEBUG) {
+ if (MailActivityEmail.DEBUG) {
Log.d(Logging.LOG_TAG, e.toString());
}
throw new CertificateValidationException(e.getMessage(), e);
} catch (IOException ioe) {
- if (Email.DEBUG) {
+ if (MailActivityEmail.DEBUG) {
Log.d(Logging.LOG_TAG, ioe.toString());
}
throw new MessagingException(MessagingException.IOERROR, ioe.toString());
@@ -316,7 +316,7 @@
*/
@Override
public void writeLine(String s, String sensitiveReplacement) throws IOException {
- if (Email.DEBUG) {
+ if (MailActivityEmail.DEBUG) {
if (sensitiveReplacement != null && !Logging.DEBUG_SENSITIVE) {
Log.d(Logging.LOG_TAG, ">>> " + sensitiveReplacement);
} else {
@@ -349,11 +349,11 @@
sb.append((char)d);
}
}
- if (d == -1 && Email.DEBUG) {
+ if (d == -1 && MailActivityEmail.DEBUG) {
Log.d(Logging.LOG_TAG, "End of stream reached while trying to read line.");
}
String ret = sb.toString();
- if (Email.DEBUG) {
+ if (MailActivityEmail.DEBUG) {
Log.d(Logging.LOG_TAG, "<<< " + ret);
}
return ret;
diff --git a/email2/src/com/android/email/mail/transport/SmtpSender.java b/email2/src/com/android/email/mail/transport/SmtpSender.java
index a9b13a6..3ceb330 100644
--- a/email2/src/com/android/email/mail/transport/SmtpSender.java
+++ b/email2/src/com/android/email/mail/transport/SmtpSender.java
@@ -20,9 +20,9 @@
import android.util.Base64;
import android.util.Log;
-import com.android.email.Email;
import com.android.email.mail.Sender;
import com.android.email.mail.Transport;
+import com.android.email2.ui.MailActivityEmail;
import com.android.emailcommon.Logging;
import com.android.emailcommon.internet.Rfc822Output;
import com.android.emailcommon.mail.Address;
@@ -145,7 +145,7 @@
*/
result = executeSimpleCommand("EHLO " + localHost);
} else {
- if (Email.DEBUG) {
+ if (MailActivityEmail.DEBUG) {
Log.d(Logging.LOG_TAG, "TLS not supported but required");
}
throw new MessagingException(MessagingException.TLS_REQUIRED);
@@ -167,19 +167,19 @@
saslAuthLogin(mUsername, mPassword);
}
else {
- if (Email.DEBUG) {
+ if (MailActivityEmail.DEBUG) {
Log.d(Logging.LOG_TAG, "No valid authentication mechanism found.");
}
throw new MessagingException(MessagingException.AUTH_REQUIRED);
}
}
} catch (SSLException e) {
- if (Email.DEBUG) {
+ if (MailActivityEmail.DEBUG) {
Log.d(Logging.LOG_TAG, e.toString());
}
throw new CertificateValidationException(e.getMessage(), e);
} catch (IOException ioe) {
- if (Email.DEBUG) {
+ if (MailActivityEmail.DEBUG) {
Log.d(Logging.LOG_TAG, ioe.toString());
}
throw new MessagingException(MessagingException.IOERROR, ioe.toString());
diff --git a/email2/src/com/android/email/provider/ContentCache.java b/email2/src/com/android/email/provider/ContentCache.java
index 041f296..0968fd2 100644
--- a/email2/src/com/android/email/provider/ContentCache.java
+++ b/email2/src/com/android/email/provider/ContentCache.java
@@ -26,7 +26,7 @@
import android.util.Log;
import android.util.LruCache;
-import com.android.email.Email;
+import com.android.email2.ui.MailActivityEmail;
import com.google.common.annotations.VisibleForTesting;
import java.util.ArrayList;
@@ -184,7 +184,7 @@
}
/*package*/ int invalidateTokens(String id) {
- if (Email.DEBUG && DEBUG_TOKENS) {
+ if (MailActivityEmail.DEBUG && DEBUG_TOKENS) {
Log.d(mLogTag, "============ Invalidate tokens for: " + id);
}
ArrayList<CacheToken> removeList = new ArrayList<CacheToken>();
@@ -203,7 +203,7 @@
}
/*package*/ void invalidate() {
- if (Email.DEBUG && DEBUG_TOKENS) {
+ if (MailActivityEmail.DEBUG && DEBUG_TOKENS) {
Log.d(mLogTag, "============ List invalidated");
}
for (CacheToken token: this) {
@@ -214,7 +214,7 @@
/*package*/ boolean remove(CacheToken token) {
boolean result = super.remove(token);
- if (Email.DEBUG && DEBUG_TOKENS) {
+ if (MailActivityEmail.DEBUG && DEBUG_TOKENS) {
if (result) {
Log.d(mLogTag, "============ Removing token for: " + token.mId);
} else {
@@ -227,7 +227,7 @@
public CacheToken add(String id) {
CacheToken token = new CacheToken(id);
super.add(token);
- if (Email.DEBUG && DEBUG_TOKENS) {
+ if (MailActivityEmail.DEBUG && DEBUG_TOKENS) {
Log.d(mLogTag, "============ Taking token for: " + token.mId);
}
return token;
@@ -482,14 +482,14 @@
CacheToken token) {
try {
if (!token.isValid()) {
- if (Email.DEBUG && DEBUG_CACHE) {
+ if (MailActivityEmail.DEBUG && DEBUG_CACHE) {
Log.d(mLogTag, "============ Stale token for " + id);
}
mStats.mStaleCount++;
return c;
}
if (c != null && Arrays.equals(projection, mBaseProjection) && !sLockCache) {
- if (Email.DEBUG && DEBUG_CACHE) {
+ if (MailActivityEmail.DEBUG && DEBUG_CACHE) {
Log.d(mLogTag, "============ Caching cursor for: " + id);
}
// If we've already cached this cursor, invalidate the older one
@@ -513,7 +513,7 @@
* @return a cursor based on cached values, or null if the row is not cached
*/
public synchronized Cursor getCachedCursor(String id, String[] projection) {
- if (Email.DEBUG && DEBUG_STATISTICS) {
+ if (MailActivityEmail.DEBUG && DEBUG_STATISTICS) {
// Every 200 calls to getCursor, report cache statistics
dumpOnCount(200);
}
@@ -594,7 +594,7 @@
mLockMap.add(id);
// Invalidate current tokens
int count = mTokenList.invalidateTokens(id);
- if (Email.DEBUG && DEBUG_TOKENS) {
+ if (MailActivityEmail.DEBUG && DEBUG_TOKENS) {
Log.d(mTokenList.mLogTag, "============ Lock invalidated " + count +
" tokens for: " + id);
}
@@ -631,13 +631,13 @@
private void unlockImpl(String id, ContentValues values, boolean wasLocked) {
Cursor c = get(id);
if (c != null) {
- if (Email.DEBUG && DEBUG_CACHE) {
+ if (MailActivityEmail.DEBUG && DEBUG_CACHE) {
Log.d(mLogTag, "=========== Unlocking cache for: " + id);
}
if (values != null && !sLockCache) {
MatrixCursor cursor = getMatrixCursor(id, mBaseProjection, values);
if (cursor != null) {
- if (Email.DEBUG && DEBUG_CACHE) {
+ if (MailActivityEmail.DEBUG && DEBUG_CACHE) {
Log.d(mLogTag, "=========== Recaching with new values: " + id);
}
cursor.moveToFirst();
diff --git a/email2/src/com/android/email/provider/DBHelper.java b/email2/src/com/android/email/provider/DBHelper.java
index 791adc2..68c4dd9 100644
--- a/email2/src/com/android/email/provider/DBHelper.java
+++ b/email2/src/com/android/email/provider/DBHelper.java
@@ -27,7 +27,7 @@
import android.provider.ContactsContract;
import android.util.Log;
-import com.android.email.Email;
+import com.android.email2.ui.MailActivityEmail;
import com.android.emailcommon.AccountManagerTypes;
import com.android.emailcommon.CalendarProviderStub;
import com.android.emailcommon.mail.Address;
@@ -977,7 +977,7 @@
// If this is a pop3 or imap account, create the account manager account
if (HostAuth.SCHEME_IMAP.equals(protocol) ||
HostAuth.SCHEME_POP3.equals(protocol)) {
- if (Email.DEBUG) {
+ if (MailActivityEmail.DEBUG) {
Log.d(TAG, "Create AccountManager account for " + protocol +
"account: " +
accountCursor.getString(V21_ACCOUNT_EMAIL));
diff --git a/email2/src/com/android/email/provider/EmailProvider.java b/email2/src/com/android/email/provider/EmailProvider.java
index 437475c..bc0e9d9 100644
--- a/email2/src/com/android/email/provider/EmailProvider.java
+++ b/email2/src/com/android/email/provider/EmailProvider.java
@@ -38,13 +38,13 @@
import android.util.Log;
import com.android.common.content.ProjectionMap;
-import com.android.email.Email;
import com.android.email.Preferences;
import com.android.email.R;
import com.android.email.SecurityPolicy;
import com.android.email.provider.ContentCache.CacheToken;
import com.android.email.service.AttachmentDownloadService;
import com.android.email.service.EmailServiceUtils;
+import com.android.email2.ui.MailActivityEmail;
import com.android.emailcommon.Logging;
import com.android.emailcommon.provider.Account;
import com.android.emailcommon.provider.EmailContent;
@@ -216,6 +216,7 @@
private static final int UI_SEARCH = UI_BASE + 16;
private static final int UI_ACCOUNT_DATA = UI_BASE + 17;
private static final int UI_FOLDER_LOAD_MORE = UI_BASE + 18;
+ private static final int UI_CONVERSATION = UI_BASE + 19;
// MUST ALWAYS EQUAL THE LAST OF THE PREVIOUS BASE CONSTANTS
private static final int LAST_EMAIL_PROVIDER_DB_BASE = UI_BASE;
@@ -432,6 +433,7 @@
matcher.addURI(EmailContent.AUTHORITY, "uisearch/#", UI_SEARCH);
matcher.addURI(EmailContent.AUTHORITY, "uiaccountdata/#", UI_ACCOUNT_DATA);
matcher.addURI(EmailContent.AUTHORITY, "uiloadmore/#", UI_FOLDER_LOAD_MORE);
+ matcher.addURI(EmailContent.AUTHORITY, "uiconversation/#", UI_CONVERSATION);
}
/**
@@ -503,7 +505,7 @@
// Restore accounts if the database is corrupted...
restoreIfNeeded(context, mDatabase);
- if (Email.DEBUG) {
+ if (MailActivityEmail.DEBUG) {
Log.d(TAG, "Deleting orphans...");
}
// Check for any orphaned Messages in the updated/deleted tables
@@ -517,11 +519,11 @@
deleteUnlinked(mDatabase, Policy.TABLE_NAME, PolicyColumns.ID, AccountColumns.POLICY_KEY,
Account.TABLE_NAME);
- if (Email.DEBUG) {
+ if (MailActivityEmail.DEBUG) {
Log.d(TAG, "EmailProvider pre-caching...");
}
preCacheData();
- if (Email.DEBUG) {
+ if (MailActivityEmail.DEBUG) {
Log.d(TAG, "EmailProvider ready.");
}
return mDatabase;
@@ -622,7 +624,7 @@
* Restore user Account and HostAuth data from our backup database
*/
public static void restoreIfNeeded(Context context, SQLiteDatabase mainDatabase) {
- if (Email.DEBUG) {
+ if (MailActivityEmail.DEBUG) {
Log.w(TAG, "restoreIfNeeded...");
}
// Check for legacy backup
@@ -642,7 +644,7 @@
null, null, null);
try {
if (c.moveToFirst()) {
- if (Email.DEBUG) {
+ if (MailActivityEmail.DEBUG) {
Log.w(TAG, "restoreIfNeeded: Account exists.");
}
return; // At least one account exists.
@@ -1120,7 +1122,7 @@
public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
String sortOrder) {
long time = 0L;
- if (Email.DEBUG) {
+ if (MailActivityEmail.DEBUG) {
time = System.nanoTime();
}
Cursor c = null;
@@ -1186,6 +1188,7 @@
case UI_SETTINGS:
case UI_ATTACHMENT:
case UI_ATTACHMENTS:
+ case UI_CONVERSATION:
// 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");
@@ -1320,7 +1323,7 @@
e.printStackTrace();
throw e;
} finally {
- if (cache != null && c != null && Email.DEBUG) {
+ if (cache != null && c != null && MailActivityEmail.DEBUG) {
cache.recordQueryTime(c, System.nanoTime() - time);
}
if (c == null) {
@@ -1471,7 +1474,7 @@
* Backup account data, returning the number of accounts backed up
*/
private static int backupAccounts(Context context, SQLiteDatabase mainDatabase) {
- if (Email.DEBUG) {
+ if (MailActivityEmail.DEBUG) {
Log.d(TAG, "backupAccounts...");
}
SQLiteDatabase backupDatabase = getBackupDatabase(context);
@@ -1479,7 +1482,7 @@
int numBackedUp = copyAccountTables(mainDatabase, backupDatabase);
if (numBackedUp < 0) {
Log.e(TAG, "Account backup failed!");
- } else if (Email.DEBUG) {
+ } else if (MailActivityEmail.DEBUG) {
Log.d(TAG, "Backed up " + numBackedUp + " accounts...");
}
return numBackedUp;
@@ -1494,7 +1497,7 @@
* Restore account data, returning the number of accounts restored
*/
private static int restoreAccounts(Context context, SQLiteDatabase mainDatabase) {
- if (Email.DEBUG) {
+ if (MailActivityEmail.DEBUG) {
Log.d(TAG, "restoreAccounts...");
}
SQLiteDatabase backupDatabase = getBackupDatabase(context);
@@ -1504,7 +1507,7 @@
Log.e(TAG, "Recovered " + numRecovered + " accounts!");
} else if (numRecovered < 0) {
Log.e(TAG, "Account recovery failed?");
- } else if (Email.DEBUG) {
+ } else if (MailActivityEmail.DEBUG) {
Log.d(TAG, "No accounts to restore...");
}
return numRecovered;
@@ -2174,6 +2177,19 @@
}
/**
+ * 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 String genQueryConversation(String[] uiProjection) {
+ StringBuilder sb = genSelect(sMessageListMap, uiProjection);
+ // Make constant
+ sb.append(" FROM " + Message.TABLE_NAME + " WHERE " + Message.RECORD_ID + "=?");
+ return sb.toString();
+ }
+
+ /**
* Generate the "top level folder list" SQLite query, given a projection from UnifiedEmail
*
* @param uiProjection as passed from UnifiedEmail
@@ -2431,6 +2447,9 @@
c = db.rawQuery(genQuerySettings(uiProjection, id), new String[] {id});
notifyUri = UIPROVIDER_SETTINGS_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);
@@ -3055,7 +3074,7 @@
// Clean up
AccountBackupRestore.backup(context);
SecurityPolicy.getInstance(context).reducePolicies();
- Email.setServicesEnabledSync(context);
+ MailActivityEmail.setServicesEnabledSync(context);
return 1;
} catch (Exception e) {
Log.w(Logging.LOG_TAG, "Exception while deleting account", e);
diff --git a/email2/src/com/android/email/service/AccountService.java b/email2/src/com/android/email/service/AccountService.java
index 88bd646..a12108a 100644
--- a/email2/src/com/android/email/service/AccountService.java
+++ b/email2/src/com/android/email/service/AccountService.java
@@ -24,11 +24,11 @@
import android.os.Bundle;
import android.os.IBinder;
-import com.android.email.Email;
import com.android.email.NotificationController;
import com.android.email.ResourceHelper;
import com.android.email.VendorPolicyLoader;
import com.android.email.provider.AccountReconciler;
+import com.android.email2.ui.MailActivityEmail;
import com.android.emailcommon.Configuration;
import com.android.emailcommon.Device;
import com.android.emailcommon.provider.Account;
@@ -106,7 +106,7 @@
// Make sure the service is properly running (re: lifecycle)
EmailServiceUtils.startExchangeService(mContext);
// Send current logging flags
- Email.updateLoggingFlags(mContext);
+ MailActivityEmail.updateLoggingFlags(mContext);
}});
return Device.getDeviceId(mContext);
} catch (IOException e) {
diff --git a/email2/src/com/android/email/service/AttachmentDownloadService.java b/email2/src/com/android/email/service/AttachmentDownloadService.java
index 7a72b6d..2a78749 100644
--- a/email2/src/com/android/email/service/AttachmentDownloadService.java
+++ b/email2/src/com/android/email/service/AttachmentDownloadService.java
@@ -33,9 +33,9 @@
import android.util.Log;
import com.android.email.AttachmentInfo;
-import com.android.email.Email;
import com.android.email.EmailConnectivityManager;
import com.android.email.NotificationController;
+import com.android.email2.ui.MailActivityEmail;
import com.android.emailcommon.provider.Account;
import com.android.emailcommon.provider.EmailContent;
import com.android.emailcommon.provider.EmailContent.Attachment;
@@ -257,14 +257,14 @@
DownloadRequest req = findDownloadRequest(att.mId);
long priority = getPriority(att);
if (priority == PRIORITY_NONE) {
- if (Email.DEBUG) {
+ if (MailActivityEmail.DEBUG) {
Log.d(TAG, "== Attachment changed: " + 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
- if (Email.DEBUG) {
+ if (MailActivityEmail.DEBUG) {
Log.d(TAG, "== Attachment " + att.mId + " was in queue, removing");
}
remove(req);
@@ -279,7 +279,7 @@
}
// If the request already existed, we'll update the priority (so that the time is
// up-to-date); otherwise, we create a new request
- if (Email.DEBUG) {
+ if (MailActivityEmail.DEBUG) {
Log.d(TAG, "== Download queued for attachment " + att.mId + ", class " +
req.priority + ", priority time " + req.time);
}
@@ -314,7 +314,7 @@
* the limit on maximum downloads
*/
/*package*/ synchronized void processQueue() {
- if (Email.DEBUG) {
+ if (MailActivityEmail.DEBUG) {
Log.d(TAG, "== Checking attachment queue, " + mDownloadSet.size() + " entries");
}
@@ -325,7 +325,7 @@
DownloadRequest req = iterator.next();
// Enforce per-account limit here
if (downloadsForAccount(req.accountId) >= MAX_SIMULTANEOUS_DOWNLOADS_PER_ACCOUNT) {
- if (Email.DEBUG) {
+ if (MailActivityEmail.DEBUG) {
Log.d(TAG, "== Skip #" + req.attachmentId + "; maxed for acct #" +
req.accountId);
}
@@ -427,7 +427,7 @@
// Check how long it's been since receiving a callback
long timeSinceCallback = now - req.lastCallbackTime;
if (timeSinceCallback > CALLBACK_TIMEOUT) {
- if (Email.DEBUG) {
+ if (MailActivityEmail.DEBUG) {
Log.d(TAG, "== Download of " + req.attachmentId + " timed out");
}
cancelDownload(req);
@@ -458,7 +458,7 @@
if (alreadyInProgress) return false;
try {
- if (Email.DEBUG) {
+ if (MailActivityEmail.DEBUG) {
Log.d(TAG, ">> Starting download for attachment #" + req.attachmentId);
}
startDownload(service, req);
@@ -537,7 +537,7 @@
DownloadRequest req = mDownloadSet.findDownloadRequest(attachmentId);
if (statusCode == EmailServiceStatus.CONNECTION_ERROR) {
// If this needs to be retried, just process the queue again
- if (Email.DEBUG) {
+ if (MailActivityEmail.DEBUG) {
Log.d(TAG, "== The download for attachment #" + attachmentId +
" will be retried");
}
@@ -552,7 +552,7 @@
if (req != null) {
remove(req);
}
- if (Email.DEBUG) {
+ if (MailActivityEmail.DEBUG) {
long secs = 0;
if (req != null) {
secs = (System.currentTimeMillis() - req.time) / 1000;
@@ -588,7 +588,7 @@
// try to send pending mail now (as mediated by MailService)
if ((req != null) &&
!Utility.hasUnloadedAttachments(mContext, attachment.mMessageKey)) {
- if (Email.DEBUG) {
+ if (MailActivityEmail.DEBUG) {
Log.d(TAG, "== Downloads finished for outgoing msg #" + req.messageId);
}
MailService.actionSendPendingMail(mContext, req.accountId);
@@ -657,7 +657,7 @@
// Record status and progress
DownloadRequest req = mDownloadSet.getDownloadInProgress(attachmentId);
if (req != null) {
- if (Email.DEBUG) {
+ if (MailActivityEmail.DEBUG) {
String code;
switch(statusCode) {
case EmailServiceStatus.SUCCESS: code = "Success"; break;
@@ -732,7 +732,7 @@
/*package*/ boolean dequeue(long attachmentId) {
DownloadRequest req = mDownloadSet.findDownloadRequest(attachmentId);
if (req != null) {
- if (Email.DEBUG) {
+ if (MailActivityEmail.DEBUG) {
Log.d(TAG, "Dequeued attachmentId: " + attachmentId);
}
mDownloadSet.remove(req);
@@ -853,7 +853,7 @@
if (accountStorage < perAccountMaxStorage) {
return true;
} else {
- if (Email.DEBUG) {
+ if (MailActivityEmail.DEBUG) {
Log.d(TAG, ">> Prefetch not allowed for account " + account.mId + "; used " +
accountStorage + ", limit " + perAccountMaxStorage);
}
diff --git a/email2/src/com/android/email/service/EmailServiceStub.java b/email2/src/com/android/email/service/EmailServiceStub.java
index ae0f33d..833c4f6 100644
--- a/email2/src/com/android/email/service/EmailServiceStub.java
+++ b/email2/src/com/android/email/service/EmailServiceStub.java
@@ -27,12 +27,12 @@
import android.text.TextUtils;
import android.util.Log;
-import com.android.email.Email;
import com.android.email.LegacyConversions;
import com.android.email.NotificationController;
import com.android.email.mail.Sender;
import com.android.email.mail.Store;
import com.android.email.provider.Utilities;
+import com.android.email2.ui.MailActivityEmail;
import com.android.emailcommon.AccountManagerTypes;
import com.android.emailcommon.Api;
import com.android.emailcommon.Logging;
@@ -486,7 +486,7 @@
messageId = c.getLong(0);
// Don't send messages with unloaded attachments
if (Utility.hasUnloadedAttachments(context, messageId)) {
- if (Email.DEBUG) {
+ if (MailActivityEmail.DEBUG) {
Log.d(Logging.LOG_TAG, "Can't send #" + messageId +
"; unloaded attachments");
}
diff --git a/email2/src/com/android/email/service/ImapService.java b/email2/src/com/android/email/service/ImapService.java
index 6dcfbeb..34a1f2b 100644
--- a/email2/src/com/android/email/service/ImapService.java
+++ b/email2/src/com/android/email/service/ImapService.java
@@ -31,11 +31,11 @@
import android.text.TextUtils;
import android.util.Log;
-import com.android.email.Email;
import com.android.email.LegacyConversions;
import com.android.email.NotificationController;
import com.android.email.mail.Store;
import com.android.email.provider.Utilities;
+import com.android.email2.ui.MailActivityEmail;
import com.android.emailcommon.Logging;
import com.android.emailcommon.TrafficFlags;
import com.android.emailcommon.internet.MimeUtility;
@@ -559,7 +559,7 @@
// 6. Determine the limit # of messages to download
int visibleLimit = mailbox.mVisibleLimit;
if (visibleLimit <= 0) {
- visibleLimit = Email.VISIBLE_LIMIT_DEFAULT;
+ visibleLimit = MailActivityEmail.VISIBLE_LIMIT_DEFAULT;
}
// 7. Create a list of messages to download
@@ -821,7 +821,7 @@
} 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 (Email.DEBUG) {
+ if (MailActivityEmail.DEBUG) {
Log.d(Logging.LOG_TAG, "Unable to process pending delete for id="
+ lastMessageId + ": " + me);
}
@@ -927,7 +927,7 @@
} 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 (Email.DEBUG) {
+ if (MailActivityEmail.DEBUG) {
Log.d(Logging.LOG_TAG, "Unable to process pending upsync for id="
+ lastMessageId + ": " + me);
}
@@ -1016,7 +1016,7 @@
} 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 (Email.DEBUG) {
+ if (MailActivityEmail.DEBUG) {
Log.d(Logging.LOG_TAG, "Unable to process pending update for id="
+ lastMessageId + ": " + me);
}
@@ -1124,7 +1124,7 @@
if (remoteMessage == null) {
return;
}
- if (Email.DEBUG) {
+ if (MailActivityEmail.DEBUG) {
Log.d(Logging.LOG_TAG,
"Update for msg id=" + newMessage.mId
+ " read=" + newMessage.mFlagRead
diff --git a/email2/src/com/android/email/service/Pop3Service.java b/email2/src/com/android/email/service/Pop3Service.java
index d69641a..0d535d8 100644
--- a/email2/src/com/android/email/service/Pop3Service.java
+++ b/email2/src/com/android/email/service/Pop3Service.java
@@ -31,11 +31,11 @@
import android.os.RemoteException;
import android.util.Log;
-import com.android.email.Email;
import com.android.email.LegacyConversions;
import com.android.email.NotificationController;
import com.android.email.mail.Store;
import com.android.email.provider.Utilities;
+import com.android.email2.ui.MailActivityEmail;
import com.android.emailcommon.AccountManagerTypes;
import com.android.emailcommon.Logging;
import com.android.emailcommon.TrafficFlags;
@@ -544,7 +544,7 @@
// 6. Determine the limit # of messages to download
int visibleLimit = mailbox.mVisibleLimit;
if (visibleLimit <= 0) {
- visibleLimit = Email.VISIBLE_LIMIT_DEFAULT;
+ visibleLimit = MailActivityEmail.VISIBLE_LIMIT_DEFAULT;
}
// 7. Create a list of messages to download
diff --git a/email2/src/com/android/email2/ui/MailActivityEmail.java b/email2/src/com/android/email2/ui/MailActivityEmail.java
index 479f944..a7d36e2 100644
--- a/email2/src/com/android/email2/ui/MailActivityEmail.java
+++ b/email2/src/com/android/email2/ui/MailActivityEmail.java
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2012 The Android Open Source Project
+ * 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.
@@ -13,8 +13,236 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
+
package com.android.email2.ui;
-public class MailActivityEmail extends com.android.mail.ui.MailActivity {
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.database.Cursor;
+import android.os.Bundle;
+import android.os.RemoteException;
+import android.util.Log;
-}
\ No newline at end of file
+import com.android.email.NotificationController;
+import com.android.email.Preferences;
+import com.android.email.R;
+import com.android.email.R.string;
+import com.android.email.service.AttachmentDownloadService;
+import com.android.email.service.EmailServiceUtils;
+import com.android.email.service.MailService;
+import com.android.emailcommon.Logging;
+import com.android.emailcommon.TempDirectory;
+import com.android.emailcommon.provider.Account;
+import com.android.emailcommon.service.EmailServiceProxy;
+import com.android.emailcommon.utility.EmailAsyncTask;
+import com.android.emailcommon.utility.Utility;
+
+public class MailActivityEmail extends com.android.mail.ui.MailActivity {
+ /**
+ * If this is enabled there will be additional logging information sent to
+ * Log.d, including protocol dumps.
+ *
+ * This should only be used for logs that are useful for debbuging user problems,
+ * not for internal/development logs.
+ *
+ * This can be enabled by typing "debug" in the AccountFolderList activity.
+ * Changing the value to 'true' here will likely have no effect at all!
+ *
+ * TODO: rename this to sUserDebug, and rename LOGD below to DEBUG.
+ */
+ public static boolean DEBUG;
+
+ // Exchange debugging flags (passed to Exchange, when available, via EmailServiceProxy)
+ public static boolean DEBUG_EXCHANGE;
+ public static boolean DEBUG_EXCHANGE_VERBOSE;
+ public static boolean DEBUG_EXCHANGE_FILE;
+
+ /**
+ * If true, inhibit hardware graphics acceleration in UI (for a/b testing)
+ */
+ public static boolean sDebugInhibitGraphicsAcceleration = false;
+
+ /**
+ * Specifies how many messages will be shown in a folder by default. This number is set
+ * on each new folder and can be incremented with "Load more messages..." by the
+ * VISIBLE_LIMIT_INCREMENT
+ */
+ public static final int VISIBLE_LIMIT_DEFAULT = 25;
+
+ /**
+ * Number of additional messages to load when a user selects "Load more messages..."
+ */
+ public static final int VISIBLE_LIMIT_INCREMENT = 25;
+
+ /**
+ * This is used to force stacked UI to return to the "welcome" screen any time we change
+ * the accounts list (e.g. deleting accounts in the Account Manager preferences.)
+ */
+ private static boolean sAccountsChangedNotification = false;
+
+ private static String sMessageDecodeErrorString;
+
+ private static Thread sUiThread;
+
+ /**
+ * Asynchronous version of {@link #setServicesEnabledSync(Context)}. Use when calling from
+ * UI thread (or lifecycle entry points.)
+ *
+ * @param context
+ */
+ public static void setServicesEnabledAsync(final Context context) {
+ EmailAsyncTask.runAsyncParallel(new Runnable() {
+ @Override
+ public void run() {
+ setServicesEnabledSync(context);
+ }
+ });
+ }
+
+ /**
+ * Called throughout the application when the number of accounts has changed. This method
+ * enables or disables the Compose activity, the boot receiver and the service based on
+ * whether any accounts are configured.
+ *
+ * Blocking call - do not call from UI/lifecycle threads.
+ *
+ * @param context
+ * @return true if there are any accounts configured.
+ */
+ public static boolean setServicesEnabledSync(Context context) {
+ Cursor c = null;
+ try {
+ c = context.getContentResolver().query(
+ Account.CONTENT_URI,
+ Account.ID_PROJECTION,
+ null, null, null);
+ boolean enable = c.getCount() > 0;
+ setServicesEnabled(context, enable);
+ return enable;
+ } finally {
+ if (c != null) {
+ c.close();
+ }
+ }
+ }
+
+ private static void setServicesEnabled(Context context, boolean enabled) {
+ PackageManager pm = context.getPackageManager();
+ pm.setComponentEnabledSetting(
+ new ComponentName(context, MailService.class),
+ enabled ? PackageManager.COMPONENT_ENABLED_STATE_ENABLED :
+ PackageManager.COMPONENT_ENABLED_STATE_DISABLED,
+ PackageManager.DONT_KILL_APP);
+ pm.setComponentEnabledSetting(
+ new ComponentName(context, AttachmentDownloadService.class),
+ enabled ? PackageManager.COMPONENT_ENABLED_STATE_ENABLED :
+ PackageManager.COMPONENT_ENABLED_STATE_DISABLED,
+ PackageManager.DONT_KILL_APP);
+
+ // Start/stop the various services depending on whether there are any accounts
+ startOrStopService(enabled, context, new Intent(context, AttachmentDownloadService.class));
+ NotificationController.getInstance(context).watchForMessages(enabled);
+ }
+
+ /**
+ * Starts or stops the service as necessary.
+ * @param enabled If {@code true}, the service will be started. Otherwise, it will be stopped.
+ * @param context The context to manage the service with.
+ * @param intent The intent of the service to be managed.
+ */
+ private static void startOrStopService(boolean enabled, Context context, Intent intent) {
+ if (enabled) {
+ context.startService(intent);
+ } else {
+ context.stopService(intent);
+ }
+ }
+
+ @Override
+ public void onCreate(Bundle bundle) {
+ super.onCreate(bundle);
+ sUiThread = Thread.currentThread();
+ Preferences prefs = Preferences.getPreferences(this);
+ DEBUG = prefs.getEnableDebugLogging();
+ sDebugInhibitGraphicsAcceleration = prefs.getInhibitGraphicsAcceleration();
+ enableStrictMode(prefs.getEnableStrictMode());
+ TempDirectory.setTempDirectory(this);
+
+ // Enable logging in the EAS service, so it starts up as early as possible.
+ updateLoggingFlags(this);
+
+ // Get a helper string used deep inside message decoders (which don't have context)
+ sMessageDecodeErrorString = getString(R.string.message_decode_error);
+
+ // Make sure all required services are running when the app is started (can prevent
+ // issues after an adb sync/install)
+ setServicesEnabledAsync(this);
+ }
+
+ /**
+ * Load enabled debug flags from the preferences and update the EAS debug flag.
+ */
+ public static void updateLoggingFlags(Context context) {
+ Preferences prefs = Preferences.getPreferences(context);
+ int debugLogging = prefs.getEnableDebugLogging() ? EmailServiceProxy.DEBUG_BIT : 0;
+ int verboseLogging =
+ prefs.getEnableExchangeLogging() ? EmailServiceProxy.DEBUG_VERBOSE_BIT : 0;
+ int fileLogging =
+ prefs.getEnableExchangeFileLogging() ? EmailServiceProxy.DEBUG_FILE_BIT : 0;
+ int enableStrictMode =
+ prefs.getEnableStrictMode() ? EmailServiceProxy.DEBUG_ENABLE_STRICT_MODE : 0;
+ int debugBits = debugLogging | verboseLogging | fileLogging | enableStrictMode;
+ EmailServiceProxy service = EmailServiceUtils.getExchangeService(context, null);
+ if (service != null) {
+ try {
+ service.setLogging(debugBits);
+ } catch (RemoteException e) {
+ }
+ }
+ }
+
+ /**
+ * Internal, utility method for logging.
+ * The calls to log() must be guarded with "if (Email.LOGD)" for performance reasons.
+ */
+ public static void log(String message) {
+ Log.d(Logging.LOG_TAG, message);
+ }
+
+ /**
+ * Called by the accounts reconciler to notify that accounts have changed, or by "Welcome"
+ * to clear the flag.
+ * @param setFlag true to set the notification flag, false to clear it
+ */
+ public static synchronized void setNotifyUiAccountsChanged(boolean setFlag) {
+ sAccountsChangedNotification = setFlag;
+ }
+
+ /**
+ * Called from activity onResume() functions to check for an accounts-changed condition, at
+ * which point they should finish() and jump to the Welcome activity.
+ */
+ public static synchronized boolean getNotifyUiAccountsChanged() {
+ return sAccountsChangedNotification;
+ }
+
+ public static void warnIfUiThread() {
+ if (Thread.currentThread().equals(sUiThread)) {
+ Log.w(Logging.LOG_TAG, "Method called on the UI thread", new Exception("STACK TRACE"));
+ }
+ }
+
+ /**
+ * Retrieve a simple string that can be used when message decoders encounter bad data.
+ * This is provided here because the protocol decoders typically don't have mContext.
+ */
+ public static String getMessageDecodeErrorString() {
+ return sMessageDecodeErrorString != null ? sMessageDecodeErrorString : "";
+ }
+
+ public static void enableStrictMode(boolean enabled) {
+ Utility.enableStrictMode(enabled);
+ }
+}