From 6c84e196aa9ddd37f33c57bcb189f9e3cca590a3 Mon Sep 17 00:00:00 2001 From: Apoorv Khatreja Date: Tue, 21 Jun 2011 02:56:53 +0530 Subject: [PATCH 01/93] Astyle is seriously getting on my nerves. I'm committing this with nothing but astyle changes so that forthcoming commits are clean. --- .../k9/activity/setup/AccountSettings.java | 4 ++-- src/com/fsck/k9/mail/internet/MimeUtility.java | 18 +++++++++--------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/src/com/fsck/k9/activity/setup/AccountSettings.java b/src/com/fsck/k9/activity/setup/AccountSettings.java index 38c9e38e0..1032291f6 100644 --- a/src/com/fsck/k9/activity/setup/AccountSettings.java +++ b/src/com/fsck/k9/activity/setup/AccountSettings.java @@ -884,8 +884,8 @@ public class AccountSettings extends K9PreferenceActivity { } } - allFolderValues = new String[folders.size()+1]; - allFolderLabels = new String[folders.size()+1]; + allFolderValues = new String[folders.size() + 1]; + allFolderLabels = new String[folders.size() + 1]; allFolderValues[0] = K9.FOLDER_NONE; allFolderLabels[0] = K9.FOLDER_NONE; diff --git a/src/com/fsck/k9/mail/internet/MimeUtility.java b/src/com/fsck/k9/mail/internet/MimeUtility.java index 0fd320c0a..54040a364 100644 --- a/src/com/fsck/k9/mail/internet/MimeUtility.java +++ b/src/com/fsck/k9/mail/internet/MimeUtility.java @@ -1076,7 +1076,7 @@ public class MimeUtility { */ if (contentTransferEncoding != null) { contentTransferEncoding = - MimeUtility.getHeaderParameter(contentTransferEncoding, null); + MimeUtility.getHeaderParameter(contentTransferEncoding, null); if ("quoted-printable".equalsIgnoreCase(contentTransferEncoding)) { in = new QuotedPrintableInputStream(in); } else if ("base64".equalsIgnoreCase(contentTransferEncoding)) { @@ -1102,7 +1102,7 @@ public class MimeUtility { * @throws MessagingException */ public static void collectParts(Part part, ArrayList viewables, - ArrayList attachments) throws MessagingException { + ArrayList attachments) throws MessagingException { /* * 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 @@ -1326,10 +1326,10 @@ public class MimeUtility { private static String getJisVariantFromAddress(String address) { if (isInDomain(address, "docomo.ne.jp") || isInDomain(address, "dwmail.jp") || - isInDomain(address, "pdx.ne.jp") || isInDomain(address, "willcom.com")) + isInDomain(address, "pdx.ne.jp") || isInDomain(address, "willcom.com")) return "docomo"; else if (isInDomain(address, "softbank.ne.jp") || isInDomain(address, "vodafone.ne.jp") || - isInDomain(address, "disney.ne.jp") || isInDomain(address, "vertuclub.ne.jp")) + isInDomain(address, "disney.ne.jp") || isInDomain(address, "vertuclub.ne.jp")) return "softbank"; else if (isInDomain(address, "ezweb.ne.jp") || isInDomain(address, "ido.ne.jp")) return "kddi"; @@ -1364,14 +1364,14 @@ public class MimeUtility { // iso-2022-jp variants are supported by no versions as of Dec 2010. if (charset.length() > 19 && charset.startsWith("x-") && - charset.endsWith("-iso-2022-jp-2007") && !Charset.isSupported(charset)) { + charset.endsWith("-iso-2022-jp-2007") && !Charset.isSupported(charset)) { in = new Iso2022JpToShiftJisInputStream(in); charset = "x-" + charset.substring(2, charset.length() - 17) + "-shift_jis-2007"; } // shift_jis variants are supported by Eclair and later. if (charset.length() > 17 && charset.startsWith("x-") && - charset.endsWith("-shift_jis-2007") && !Charset.isSupported(charset)) { + charset.endsWith("-shift_jis-2007") && !Charset.isSupported(charset)) { // If the JIS variant is iPhone, map the Unicode private use area in iPhone to the one in Android after // converting the character set from the standard Shift JIS to Unicode. if (charset.substring(2, charset.length() - 15).equals("iphone")) @@ -1392,7 +1392,7 @@ public class MimeUtility { } if (!supported) { Log.e(K9.LOG_TAG, "I don't know how to deal with the charset " + charset + - ". Falling back to US-ASCII"); + ". Falling back to US-ASCII"); charset = "US-ASCII"; } /* @@ -2393,12 +2393,12 @@ public class MimeUtility { public static void setCharset(String charset, Part part) throws MessagingException { part.setHeader(MimeHeader.HEADER_CONTENT_TYPE, - part.getMimeType() + ";\n charset=" + getExternalCharset(charset)); + part.getMimeType() + ";\n charset=" + getExternalCharset(charset)); } public static String getExternalCharset(String charset) { if (charset.length() > 17 && charset.startsWith("x-") && - charset.endsWith("-shift_jis-2007")) + charset.endsWith("-shift_jis-2007")) return "shift_jis"; return charset; From 970271dbf943e6a71dcb262832c2eaff5880b7cb Mon Sep 17 00:00:00 2001 From: Apoorv Khatreja Date: Tue, 21 Jun 2011 04:34:57 +0530 Subject: [PATCH 02/93] If the response for an APPEND command contains the APPENDUID response code, read the UID of the newly appended message from there. --- src/com/fsck/k9/mail/store/ImapStore.java | 26 +++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/src/com/fsck/k9/mail/store/ImapStore.java b/src/com/fsck/k9/mail/store/ImapStore.java index b639a11f2..f37e44de8 100644 --- a/src/com/fsck/k9/mail/store/ImapStore.java +++ b/src/com/fsck/k9/mail/store/ImapStore.java @@ -52,6 +52,7 @@ import android.content.Context; import android.net.ConnectivityManager; import android.net.NetworkInfo; import android.os.PowerManager; +import android.text.TextUtils; import android.util.Log; import com.beetstra.jutf7.CharsetProvider; @@ -1617,6 +1618,31 @@ public class ImapStore extends Store { while (response.more()); } while (response.mTag == null); + /* + * If the server supports UIDPLUS, then along with the APPEND response it will return an APPENDUID response code. + * e.g. 11 OK [APPENDUID 2 238268] APPEND completed + * + * We can use the UID included in this response to update our records. + * + * Code imported from AOSP Email as of git rev a5c3744a247e432acf9f571a9dfb55321c4baa1a + */ + Object responseList = response.get(1); + + if (responseList instanceof ImapList) { + final ImapList appendList = (ImapList) responseList; + if ((appendList.size() >= 3) && appendList.getString(0).equals("APPENDUID")) { + String serverUid = appendList.getString(2); + if (!TextUtils.isEmpty(serverUid)) { + message.setUid(serverUid); + continue; + } + } + } + + + /* + * This part is executed in case the server does not support UIDPLUS or does not implement the APPENDUID response code. + */ String newUid = getUidFromMessageId(message); if (K9.DEBUG) Log.d(K9.LOG_TAG, "Got UID " + newUid + " for message for " + getLogId()); From 0ba7f206225d4f9c71e83cf94f1bf74ca27aa5aa Mon Sep 17 00:00:00 2001 From: Apoorv Khatreja Date: Fri, 24 Jun 2011 02:54:12 +0530 Subject: [PATCH 03/93] Attempt to implement COPYUID, works for the most part except for updation of the LocalStore with freshly copied messages. --- src/com/fsck/k9/mail/store/ImapStore.java | 130 ++++++++++++++++++++-- 1 file changed, 122 insertions(+), 8 deletions(-) diff --git a/src/com/fsck/k9/mail/store/ImapStore.java b/src/com/fsck/k9/mail/store/ImapStore.java index f37e44de8..8824e0988 100644 --- a/src/com/fsck/k9/mail/store/ImapStore.java +++ b/src/com/fsck/k9/mail/store/ImapStore.java @@ -29,6 +29,7 @@ import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; +import java.util.Comparator; import java.util.Date; import java.util.HashMap; import java.util.HashSet; @@ -39,7 +40,9 @@ import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Set; +import java.util.SortedSet; import java.util.StringTokenizer; +import java.util.TreeSet; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; @@ -813,8 +816,24 @@ public class ImapStore extends Store { ImapFolder iFolder = (ImapFolder)folder; checkOpen(); + + SortedSet messageSet = new TreeSet(new Comparator() { + public int compare(Message m1, Message m2) { + int uid1 = Integer.parseInt(m1.getUid()), uid2 = Integer.parseInt(m2.getUid()); + if (uid1 < uid2) { + return -1; + } else if (uid1 == uid2) { + return 0; + } else { + return 1; + } + } + }); String[] uids = new String[messages.length]; for (int i = 0, count = messages.length; i < count; i++) { + messageSet.add(messages[i]); + + // Not bothering to sort the UIDs in ascending order while sending the command for convenience, and because it does not make a difference. uids[i] = messages[i].getUid(); } try { @@ -829,20 +848,104 @@ public class ImapStore extends Store { iFolder.create(FolderType.HOLDS_MESSAGES); } - if (exists(remoteDestName)) { - executeSimpleCommand(String.format("UID COPY %s %s", - Utility.combine(uids, ','), - remoteDestName)); - } else { - throw new MessagingException("IMAPMessage.copyMessages: remote destination folder " + folder.getName() - + " does not exist and could not be created for " + getLogId() - , true); + mConnection.sendCommand(String.format("UID COPY %s %s", + Utility.combine(uids, ','), + remoteDestName), false); + ImapResponse response; + do { + response = mConnection.readResponse(); + handleUntaggedResponse(response); + if (response.mCommandContinuationRequested) { + EOLConvertingOutputStream eolOut = new EOLConvertingOutputStream(mConnection.mOut); + eolOut.write('\r'); + eolOut.write('\n'); + eolOut.flush(); + } + while (response.more()); + } while (response.mTag == null); + + /* + * If the server supports UIDPLUS, then along with the COPY response it will return an COPYUID response code. + * e.g. 24 OK [COPYUID 38505 304,319:320 3956:3958] Success + * + * COPYUID is followed by UIDVALIDITY, set of UIDs of copied messages from the source folder and set of corresponding UIDs assigned to them in the destination folder. + * + * We can use the new UIDs included in this response to update our records. + */ + + Object responseList = response.get(1); + + if (responseList instanceof ImapList) { + final ImapList copyList = (ImapList) responseList; + if ((copyList.size() >= 4) && copyList.getString(0).equals("COPYUID")) { + List oldUids = parseSequenceSet(copyList.getString(2)); + List newUids = parseSequenceSet(copyList.getString(3)); + if (oldUids.size() == newUids.size()) { + Iterator messageIterator = messageSet.iterator(); + for (int i = 0; i < messages.length && messageIterator.hasNext(); i++) { + Message nextMessage = messageIterator.next(); + if (oldUids.get(i).equals(nextMessage.getUid())) { + /* + * Here, we need to *create* new messages in the localstore, same as the older messages, the only changes are that old UIDs need to be swapped with new UIDs, + * and old folder swapped with new folder. + */ +// nextMessage.setUid(newUids.get(i)); + } + } + } + } } } catch (IOException ioe) { throw ioExceptionHandler(mConnection, ioe); } } + private List parseSequenceSet(String set) { + int index = 0; + List sequenceList = new ArrayList(); + String element = ""; + + while (index < set.length()) { + if (set.charAt(index) == ':') { + String upperBound = ""; + index++; + while (index < set.length()) { + if (!(set.charAt(index) == ',')) { + upperBound += set.charAt(index); + index++; + } else { + break; + } + } + + int lower = Integer.parseInt(element); + int upper = Integer.parseInt(upperBound); + + if (lower < upper) { + for (int i = lower; i <= upper; i++) { + sequenceList.add(String.valueOf(i)); + } + } else { + for (int i = upper; i <= lower; i++) { + sequenceList.add(String.valueOf(i)); + } + } + + element = ""; + } else if (set.charAt(index) == ',') { + sequenceList.add(element); + element = ""; + } else { + element += set.charAt(index); + } + index++; + } + if (!element.equals("")) { + sequenceList.add(element); + } + return sequenceList; + } + @Override public void moveMessages(Message[] messages, Folder folder) throws MessagingException { if (messages.length == 0) @@ -2290,6 +2393,17 @@ public class ImapStore extends Store { return executeSimpleCommand(command, sensitive, null); } +// public void logResponse (ImapList response) { +// for(int i=0;i executeSimpleCommand(String command, boolean sensitive, UntaggedHandler untaggedHandler) throws IOException, ImapException, MessagingException { String commandToLog = command; From bc9b7030d7d250c61b7bf8b624c6fa84c0363dd3 Mon Sep 17 00:00:00 2001 From: Apoorv Khatreja Date: Tue, 28 Jun 2011 16:50:48 +0530 Subject: [PATCH 04/93] COPYUID implementation now in place and working, restructured appendMessages, copyMessages and moveMessages globally to return a Map of srcUids -> destUids rather than returning nothing. This is now used to bring local and remote UIDs upto speed without the need for additional requests. --- .../k9/controller/MessagingController.java | 94 ++++++++++++++++--- src/com/fsck/k9/mail/Folder.java | 11 ++- src/com/fsck/k9/mail/store/ImapStore.java | 83 ++++++++-------- src/com/fsck/k9/mail/store/LocalStore.java | 30 ++++-- src/com/fsck/k9/mail/store/Pop3Store.java | 4 +- src/com/fsck/k9/mail/store/WebDavStore.java | 10 +- 6 files changed, 163 insertions(+), 69 deletions(-) diff --git a/src/com/fsck/k9/controller/MessagingController.java b/src/com/fsck/k9/controller/MessagingController.java index 079b72812..198f4b9a0 100644 --- a/src/com/fsck/k9/controller/MessagingController.java +++ b/src/com/fsck/k9/controller/MessagingController.java @@ -2088,9 +2088,32 @@ public class MessagingController implements Runnable { command.arguments[0] = srcFolder; command.arguments[1] = destFolder; command.arguments[2] = Boolean.toString(isCopy); - System.arraycopy(uids, 0, command.arguments, 3, uids.length); + command.arguments[3] = Boolean.toString(false); + System.arraycopy(uids, 0, command.arguments, 4, uids.length); queuePendingCommand(account, command); } + + private void queueMoveOrCopy(Account account, String srcFolder, String destFolder, boolean isCopy, String uids[], Map uidMap) { + if (uidMap == null || uidMap.isEmpty()) { + queueMoveOrCopy(account, srcFolder, destFolder, isCopy, uids); + } else { + if (account.getErrorFolderName().equals(srcFolder)) { + return; + } + PendingCommand command = new PendingCommand(); + command.command = PENDING_COMMAND_MOVE_OR_COPY_BULK; + + int length = 4 + uidMap.keySet().size() + uidMap.values().size(); + command.arguments = new String[length]; + command.arguments[0] = srcFolder; + command.arguments[1] = destFolder; + command.arguments[2] = Boolean.toString(isCopy); + command.arguments[3] = Boolean.toString(true); + System.arraycopy(uidMap.keySet().toArray(), 0, command.arguments, 4, uidMap.keySet().size()); + System.arraycopy(uidMap.values().toArray(), 0, command.arguments, 4 + uidMap.keySet().size(), uidMap.values().size()); + queuePendingCommand(account, command); + } + } /** * Process a pending trash message command. * @@ -2102,6 +2125,7 @@ public class MessagingController implements Runnable { throws MessagingException { Folder remoteSrcFolder = null; Folder remoteDestFolder = null; + Folder localDestFolder = null; try { String srcFolder = command.arguments[0]; if (account.getErrorFolderName().equals(srcFolder)) { @@ -2109,14 +2133,42 @@ public class MessagingController implements Runnable { } String destFolder = command.arguments[1]; String isCopyS = command.arguments[2]; + String hasNewUidsS = command.arguments[3]; + + boolean hasNewUids = false; + if (hasNewUidsS != null) { + hasNewUids = Boolean.parseBoolean(hasNewUidsS); + } + Store remoteStore = account.getRemoteStore(); remoteSrcFolder = remoteStore.getFolder(srcFolder); + Store localStore = account.getLocalStore(); + localDestFolder = localStore.getFolder(destFolder); List messages = new ArrayList(); - for (int i = 3; i < command.arguments.length; i++) { - String uid = command.arguments[i]; - if (!uid.startsWith(K9.LOCAL_UID_PREFIX)) { - messages.add(remoteSrcFolder.getMessage(uid)); + + /* + * We split up the localUidMap into two parts while sending the command, here we assemble it back. + */ + Map localUidMap = new HashMap(); + if (hasNewUids) { + int offset = (command.arguments.length - 4) / 2; + + for (int i = 4; i < 4 + offset; i++) { + localUidMap.put(command.arguments[i], command.arguments[i + offset]); + + String uid = command.arguments[i]; + if (!uid.startsWith(K9.LOCAL_UID_PREFIX)) { + messages.add(remoteSrcFolder.getMessage(uid)); + } + } + + } else { + for (int i = 4; i < command.arguments.length; i++) { + String uid = command.arguments[i]; + if (!uid.startsWith(K9.LOCAL_UID_PREFIX)) { + messages.add(remoteSrcFolder.getMessage(uid)); + } } } @@ -2137,6 +2189,8 @@ public class MessagingController implements Runnable { Log.d(K9.LOG_TAG, "processingPendingMoveOrCopy: source folder = " + srcFolder + ", " + messages.size() + " messages, destination folder = " + destFolder + ", isCopy = " + isCopy); + Map remoteUidMap = new HashMap(); + if (!isCopy && destFolder.equals(account.getTrashFolderName())) { if (K9.DEBUG) Log.d(K9.LOG_TAG, "processingPendingMoveOrCopy doing special case for deleting message"); @@ -2150,9 +2204,9 @@ public class MessagingController implements Runnable { remoteDestFolder = remoteStore.getFolder(destFolder); if (isCopy) { - remoteSrcFolder.copyMessages(messages.toArray(EMPTY_MESSAGE_ARRAY), remoteDestFolder); + remoteUidMap = remoteSrcFolder.copyMessages(messages.toArray(EMPTY_MESSAGE_ARRAY), remoteDestFolder); } else { - remoteSrcFolder.moveMessages(messages.toArray(EMPTY_MESSAGE_ARRAY), remoteDestFolder); + remoteUidMap = remoteSrcFolder.moveMessages(messages.toArray(EMPTY_MESSAGE_ARRAY), remoteDestFolder); } } if (!isCopy && Account.EXPUNGE_IMMEDIATELY.equals(account.getExpungePolicy())) { @@ -2161,12 +2215,27 @@ public class MessagingController implements Runnable { remoteSrcFolder.expunge(); } + + /* + * This next part is used to bring the local UIDs of the local destination folder + * upto speed with the remote UIDs of remote destionation folder. + */ + if (!localUidMap.isEmpty() && !remoteUidMap.isEmpty()) { + Set remoteSrcUids = remoteUidMap.keySet(); + Iterator remoteSrcUidsIterator = remoteSrcUids.iterator(); + + while (remoteSrcUidsIterator.hasNext()) { + String remoteSrcUid = remoteSrcUidsIterator.next(); + String localDestUid = localUidMap.get(remoteSrcUid); + + Message localDestMessage = localDestFolder.getMessage(localDestUid); + localDestMessage.setUid(remoteUidMap.get(remoteSrcUid)); + } + } } finally { closeFolder(remoteSrcFolder); closeFolder(remoteDestFolder); } - - } private void queueSetFlag(final Account account, final String folderName, final String newState, final String flag, final String[] uids) { @@ -3248,6 +3317,7 @@ public class MessagingController implements Runnable { private void moveOrCopyMessageSynchronous(final Account account, final String srcFolder, final Message[] inMessages, final String destFolder, final boolean isCopy, MessagingListener listener) { try { + Map uidMap = new HashMap(); Store localStore = account.getLocalStore(); Store remoteStore = account.getRemoteStore(); if (!isCopy && (!remoteStore.isMoveCapable() || !localStore.isMoveCapable())) { @@ -3285,9 +3355,9 @@ public class MessagingController implements Runnable { fp.add(FetchProfile.Item.ENVELOPE); fp.add(FetchProfile.Item.BODY); localSrcFolder.fetch(messages, fp, null); - localSrcFolder.copyMessages(messages, localDestFolder); + uidMap = localSrcFolder.copyMessages(messages, localDestFolder); } else { - localSrcFolder.moveMessages(messages, localDestFolder); + uidMap = localSrcFolder.moveMessages(messages, localDestFolder); for (String origUid : origUidMap.keySet()) { for (MessagingListener l : getListeners()) { l.messageUidChanged(account, srcFolder, origUid, origUidMap.get(origUid).getUid()); @@ -3296,7 +3366,7 @@ public class MessagingController implements Runnable { } } - queueMoveOrCopy(account, srcFolder, destFolder, isCopy, origUidMap.keySet().toArray(EMPTY_STRING_ARRAY)); + queueMoveOrCopy(account, srcFolder, destFolder, isCopy, origUidMap.keySet().toArray(EMPTY_STRING_ARRAY), uidMap); } processPendingCommands(account); diff --git a/src/com/fsck/k9/mail/Folder.java b/src/com/fsck/k9/mail/Folder.java index 0f5b96060..e3dafc3e5 100644 --- a/src/com/fsck/k9/mail/Folder.java +++ b/src/com/fsck/k9/mail/Folder.java @@ -1,6 +1,7 @@ package com.fsck.k9.mail; import java.util.Date; +import java.util.Map; import android.util.Log; import com.fsck.k9.Account; @@ -102,11 +103,15 @@ public abstract class Folder { public abstract Message[] getMessages(String[] uids, MessageRetrievalListener listener) throws MessagingException; - public abstract void appendMessages(Message[] messages) throws MessagingException; + public abstract Map appendMessages(Message[] messages) throws MessagingException; - public void copyMessages(Message[] msgs, Folder folder) throws MessagingException {} + public Map copyMessages(Message[] msgs, Folder folder) throws MessagingException { + return null; + } - public void moveMessages(Message[] msgs, Folder folder) throws MessagingException {} + public Map moveMessages(Message[] msgs, Folder folder) throws MessagingException { + return null; + } public void delete(Message[] msgs, String trashFolderName) throws MessagingException { for (Message message : msgs) { diff --git a/src/com/fsck/k9/mail/store/ImapStore.java b/src/com/fsck/k9/mail/store/ImapStore.java index 8824e0988..b4da67248 100644 --- a/src/com/fsck/k9/mail/store/ImapStore.java +++ b/src/com/fsck/k9/mail/store/ImapStore.java @@ -29,7 +29,6 @@ import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; -import java.util.Comparator; import java.util.Date; import java.util.HashMap; import java.util.HashSet; @@ -40,9 +39,7 @@ import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Set; -import java.util.SortedSet; import java.util.StringTokenizer; -import java.util.TreeSet; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; @@ -806,32 +803,19 @@ public class ImapStore extends Store { } @Override - public void copyMessages(Message[] messages, Folder folder) throws MessagingException { + public Map copyMessages(Message[] messages, Folder folder) throws MessagingException { if (!(folder instanceof ImapFolder)) { throw new MessagingException("ImapFolder.copyMessages passed non-ImapFolder"); } if (messages.length == 0) - return; + return null; ImapFolder iFolder = (ImapFolder)folder; checkOpen(); - SortedSet messageSet = new TreeSet(new Comparator() { - public int compare(Message m1, Message m2) { - int uid1 = Integer.parseInt(m1.getUid()), uid2 = Integer.parseInt(m2.getUid()); - if (uid1 < uid2) { - return -1; - } else if (uid1 == uid2) { - return 0; - } else { - return 1; - } - } - }); String[] uids = new String[messages.length]; for (int i = 0, count = messages.length; i < count; i++) { - messageSet.add(messages[i]); // Not bothering to sort the UIDs in ascending order while sending the command for convenience, and because it does not make a difference. uids[i] = messages[i].getUid(); @@ -875,31 +859,36 @@ public class ImapStore extends Store { Object responseList = response.get(1); + Map uidMap = null; + if (responseList instanceof ImapList) { final ImapList copyList = (ImapList) responseList; if ((copyList.size() >= 4) && copyList.getString(0).equals("COPYUID")) { - List oldUids = parseSequenceSet(copyList.getString(2)); - List newUids = parseSequenceSet(copyList.getString(3)); - if (oldUids.size() == newUids.size()) { - Iterator messageIterator = messageSet.iterator(); - for (int i = 0; i < messages.length && messageIterator.hasNext(); i++) { - Message nextMessage = messageIterator.next(); - if (oldUids.get(i).equals(nextMessage.getUid())) { - /* - * Here, we need to *create* new messages in the localstore, same as the older messages, the only changes are that old UIDs need to be swapped with new UIDs, - * and old folder swapped with new folder. - */ -// nextMessage.setUid(newUids.get(i)); - } + List srcUids = parseSequenceSet(copyList.getString(2)); + List destUids = parseSequenceSet(copyList.getString(3)); + if (srcUids.size() == destUids.size()) { + Iterator srcUidsIterator = srcUids.iterator(); + Iterator destUidsIterator = destUids.iterator(); + uidMap = new HashMap(); + while (srcUidsIterator.hasNext() && destUidsIterator.hasNext()) { + uidMap.put(srcUidsIterator.next(), destUidsIterator.next()); } } } } + return uidMap; } catch (IOException ioe) { throw ioExceptionHandler(mConnection, ioe); } } + /** + * Can be used to parse sequence sets or UID sets appearing is responses such as COPYUID. + * e.g. [COPYUID 38505 304,319:320 3956:3958] + * + * @param set + * @return List sequenceSet + */ private List parseSequenceSet(String set) { int index = 0; List sequenceList = new ArrayList(); @@ -947,11 +936,12 @@ public class ImapStore extends Store { } @Override - public void moveMessages(Message[] messages, Folder folder) throws MessagingException { + public Map moveMessages(Message[] messages, Folder folder) throws MessagingException { if (messages.length == 0) - return; - copyMessages(messages, folder); + return null; + Map uidMap = copyMessages(messages, folder); setFlags(messages, new Flag[] { Flag.DELETED }, true); + return uidMap; } @Override @@ -1698,9 +1688,10 @@ public class ImapStore extends Store { * new server UID. */ @Override - public void appendMessages(Message[] messages) throws MessagingException { + public Map appendMessages(Message[] messages) throws MessagingException { checkOpen(); try { + Map uidMap = null; for (Message message : messages) { mConnection.sendCommand( String.format("APPEND %s (%s) {%d}", @@ -1734,9 +1725,18 @@ public class ImapStore extends Store { if (responseList instanceof ImapList) { final ImapList appendList = (ImapList) responseList; if ((appendList.size() >= 3) && appendList.getString(0).equals("APPENDUID")) { - String serverUid = appendList.getString(2); - if (!TextUtils.isEmpty(serverUid)) { - message.setUid(serverUid); + String newUid = appendList.getString(2); + + /* + * We need uidMap to be null initially to maintain consistency with the behavior of other similar methods (copyMessages, moveMessages) which + * return null if new UIDs are not available. Therefore, we initialize uidMap over here. + */ + if (uidMap == null) { + uidMap = new HashMap(); + } + uidMap.put(message.getUid(), newUid); + if (!TextUtils.isEmpty(newUid)) { + message.setUid(newUid); continue; } } @@ -1751,11 +1751,14 @@ public class ImapStore extends Store { Log.d(K9.LOG_TAG, "Got UID " + newUid + " for message for " + getLogId()); if (newUid != null) { + if (uidMap == null) { + uidMap = new HashMap(); + } + uidMap.put(message.getUid(), newUid); message.setUid(newUid); } - - } + return uidMap; } catch (IOException ioe) { throw ioExceptionHandler(mConnection, ioe); } diff --git a/src/com/fsck/k9/mail/store/LocalStore.java b/src/com/fsck/k9/mail/store/LocalStore.java index fba282164..0c3647034 100644 --- a/src/com/fsck/k9/mail/store/LocalStore.java +++ b/src/com/fsck/k9/mail/store/LocalStore.java @@ -1885,21 +1885,23 @@ public class LocalStore extends Store implements Serializable { } @Override - public void copyMessages(Message[] msgs, Folder folder) throws MessagingException { + public Map copyMessages(Message[] msgs, Folder folder) throws MessagingException { if (!(folder instanceof LocalFolder)) { throw new MessagingException("copyMessages called with incorrect Folder"); } - ((LocalFolder) folder).appendMessages(msgs, true); + return ((LocalFolder) folder).appendMessages(msgs, true); } @Override - public void moveMessages(final Message[] msgs, final Folder destFolder) throws MessagingException { + public Map moveMessages(final Message[] msgs, final Folder destFolder) throws MessagingException { if (!(destFolder instanceof LocalFolder)) { throw new MessagingException("moveMessages called with non-LocalFolder"); } final LocalFolder lDestFolder = (LocalFolder)destFolder; + final Map uidMap = new HashMap(); + try { database.execute(false, new DbCallback() { @Override @@ -1936,7 +1938,7 @@ public class LocalStore extends Store implements Serializable { LocalMessage placeHolder = new LocalMessage(oldUID, LocalFolder.this); placeHolder.setFlagInternal(Flag.DELETED, true); placeHolder.setFlagInternal(Flag.SEEN, true); - appendMessages(new Message[] { placeHolder }); + uidMap.putAll(appendMessages(new Message[] { placeHolder })); } } catch (MessagingException e) { throw new WrappedException(e); @@ -1944,6 +1946,7 @@ public class LocalStore extends Store implements Serializable { return null; } }); + return uidMap; } catch (WrappedException e) { throw(MessagingException) e.getCause(); } @@ -1989,8 +1992,8 @@ public class LocalStore extends Store implements Serializable { * message, retrieve the appropriate local message instance first (if it already exists). */ @Override - public void appendMessages(Message[] messages) throws MessagingException { - appendMessages(messages, false); + public Map appendMessages(Message[] messages) throws MessagingException { + return appendMessages(messages, false); } public void destroyMessages(final Message[] messages) throws MessagingException { @@ -2026,10 +2029,12 @@ public class LocalStore extends Store implements Serializable { * message, retrieve the appropriate local message instance first (if it already exists). * @param messages * @param copy + * @return Map uidMap of srcUids -> destUids */ - private void appendMessages(final Message[] messages, final boolean copy) throws MessagingException { + private Map appendMessages(final Message[] messages, final boolean copy) throws MessagingException { open(OpenMode.READ_WRITE); try { + final Map uidMap = new HashMap(); database.execute(true, new DbCallback() { @Override public Void doDbWork(final SQLiteDatabase db) throws WrappedException, UnavailableStorageException { @@ -2040,11 +2045,15 @@ public class LocalStore extends Store implements Serializable { } String uid = message.getUid(); - if (uid == null || copy) { + if (uid == null && !copy) { uid = K9.LOCAL_UID_PREFIX + UUID.randomUUID().toString(); - if (!copy) { - message.setUid(uid); + message.setUid(uid); + } else if (copy) { + String temp = K9.LOCAL_UID_PREFIX + UUID.randomUUID().toString(); + if (uid != null) { + uidMap.put(uid, temp); } + uid = temp; } else { Message oldMessage = getMessage(uid); if (oldMessage != null && !oldMessage.isSet(Flag.SEEN)) { @@ -2150,6 +2159,7 @@ public class LocalStore extends Store implements Serializable { return null; } }); + return uidMap; } catch (WrappedException e) { throw(MessagingException) e.getCause(); } diff --git a/src/com/fsck/k9/mail/store/Pop3Store.java b/src/com/fsck/k9/mail/store/Pop3Store.java index a307c8484..e0c57591e 100644 --- a/src/com/fsck/k9/mail/store/Pop3Store.java +++ b/src/com/fsck/k9/mail/store/Pop3Store.java @@ -24,6 +24,7 @@ import java.util.LinkedList; import java.util.HashMap; import java.util.HashSet; import java.util.List; +import java.util.Map; public class Pop3Store extends Store { public static final int CONNECTION_SECURITY_NONE = 0; @@ -736,7 +737,8 @@ public class Pop3Store extends Store { } @Override - public void appendMessages(Message[] messages) throws MessagingException { + public Map appendMessages(Message[] messages) throws MessagingException { + return null; } @Override diff --git a/src/com/fsck/k9/mail/store/WebDavStore.java b/src/com/fsck/k9/mail/store/WebDavStore.java index 81a498f25..7720b9c18 100644 --- a/src/com/fsck/k9/mail/store/WebDavStore.java +++ b/src/com/fsck/k9/mail/store/WebDavStore.java @@ -51,6 +51,7 @@ import java.util.HashMap; import java.util.LinkedList; import java.util.List; import java.util.Locale; +import java.util.Map; import java.util.Stack; import java.util.zip.GZIPInputStream; @@ -1161,13 +1162,15 @@ public class WebDavStore extends Store { } @Override - public void copyMessages(Message[] messages, Folder folder) throws MessagingException { + public Map copyMessages(Message[] messages, Folder folder) throws MessagingException { moveOrCopyMessages(messages, folder.getName(), false); + return null; } @Override - public void moveMessages(Message[] messages, Folder folder) throws MessagingException { + public Map moveMessages(Message[] messages, Folder folder) throws MessagingException { moveOrCopyMessages(messages, folder.getName(), true); + return null; } @Override @@ -1728,8 +1731,9 @@ public class WebDavStore extends Store { } @Override - public void appendMessages(Message[] messages) throws MessagingException { + public Map appendMessages(Message[] messages) throws MessagingException { appendWebDavMessages(messages); + return null; } public Message[] appendWebDavMessages(Message[] messages) throws MessagingException { From 3321ebdc33f3e51c145d20cc627cafe09021ad81 Mon Sep 17 00:00:00 2001 From: Apoorv Khatreja Date: Sat, 2 Jul 2011 11:59:00 +0530 Subject: [PATCH 05/93] COPYUID changes were being updated only in memory, but were not being written to db. --- src/com/fsck/k9/controller/MessagingController.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/com/fsck/k9/controller/MessagingController.java b/src/com/fsck/k9/controller/MessagingController.java index 198f4b9a0..c971eebce 100644 --- a/src/com/fsck/k9/controller/MessagingController.java +++ b/src/com/fsck/k9/controller/MessagingController.java @@ -2125,7 +2125,7 @@ public class MessagingController implements Runnable { throws MessagingException { Folder remoteSrcFolder = null; Folder remoteDestFolder = null; - Folder localDestFolder = null; + LocalFolder localDestFolder = null; try { String srcFolder = command.arguments[0]; if (account.getErrorFolderName().equals(srcFolder)) { @@ -2144,7 +2144,7 @@ public class MessagingController implements Runnable { remoteSrcFolder = remoteStore.getFolder(srcFolder); Store localStore = account.getLocalStore(); - localDestFolder = localStore.getFolder(destFolder); + localDestFolder = (LocalFolder) localStore.getFolder(destFolder); List messages = new ArrayList(); /* @@ -2230,6 +2230,7 @@ public class MessagingController implements Runnable { Message localDestMessage = localDestFolder.getMessage(localDestUid); localDestMessage.setUid(remoteUidMap.get(remoteSrcUid)); + localDestFolder.changeUid((LocalMessage)localDestMessage); } } } finally { From da9a5e6c17ce89d2fc54cebf35c5397fd609e93d Mon Sep 17 00:00:00 2001 From: Apoorv Khatreja Date: Mon, 25 Jul 2011 06:50:26 +0530 Subject: [PATCH 06/93] Made some cosmetic changes for clarity, added debug messages for erroneous conditions. Fixed potential NPE in ImapFolder.parseSequenceSet(). --- src/com/fsck/k9/mail/store/ImapStore.java | 26 +++++++++++++++++----- src/com/fsck/k9/mail/store/LocalStore.java | 6 ++--- 2 files changed, 23 insertions(+), 9 deletions(-) diff --git a/src/com/fsck/k9/mail/store/ImapStore.java b/src/com/fsck/k9/mail/store/ImapStore.java index b4da67248..4e743e70a 100644 --- a/src/com/fsck/k9/mail/store/ImapStore.java +++ b/src/com/fsck/k9/mail/store/ImapStore.java @@ -866,15 +866,26 @@ public class ImapStore extends Store { if ((copyList.size() >= 4) && copyList.getString(0).equals("COPYUID")) { List srcUids = parseSequenceSet(copyList.getString(2)); List destUids = parseSequenceSet(copyList.getString(3)); - if (srcUids.size() == destUids.size()) { - Iterator srcUidsIterator = srcUids.iterator(); - Iterator destUidsIterator = destUids.iterator(); - uidMap = new HashMap(); - while (srcUidsIterator.hasNext() && destUidsIterator.hasNext()) { - uidMap.put(srcUidsIterator.next(), destUidsIterator.next()); + if (srcUids != null && destUids != null) { + if (srcUids.size() == destUids.size()) { + Iterator srcUidsIterator = srcUids.iterator(); + Iterator destUidsIterator = destUids.iterator(); + uidMap = new HashMap(); + while (srcUidsIterator.hasNext() && destUidsIterator.hasNext()) { + uidMap.put(srcUidsIterator.next(), destUidsIterator.next()); + } + } else { + if(K9.DEBUG) + Log.v(K9.LOG_TAG, "Parse error: size of source UIDs list is not the same as size of destination UIDs list."); } + } else { + if(K9.DEBUG) + Log.v(K9.LOG_TAG, "Parsing of the sequence set failed."); } } + } else { + if(K9.DEBUG) + Log.v(K9.LOG_TAG, "Expected COPYUID response was not found."); } return uidMap; } catch (IOException ioe) { @@ -890,6 +901,9 @@ public class ImapStore extends Store { * @return List sequenceSet */ private List parseSequenceSet(String set) { + if (set == null) { + return null; + } int index = 0; List sequenceList = new ArrayList(); String element = ""; diff --git a/src/com/fsck/k9/mail/store/LocalStore.java b/src/com/fsck/k9/mail/store/LocalStore.java index 0c3647034..7a5a6b437 100644 --- a/src/com/fsck/k9/mail/store/LocalStore.java +++ b/src/com/fsck/k9/mail/store/LocalStore.java @@ -2049,11 +2049,11 @@ public class LocalStore extends Store implements Serializable { uid = K9.LOCAL_UID_PREFIX + UUID.randomUUID().toString(); message.setUid(uid); } else if (copy) { - String temp = K9.LOCAL_UID_PREFIX + UUID.randomUUID().toString(); + String randomLocalUid = K9.LOCAL_UID_PREFIX + UUID.randomUUID().toString(); if (uid != null) { - uidMap.put(uid, temp); + uidMap.put(uid, randomLocalUid); } - uid = temp; + uid = randomLocalUid; } else { Message oldMessage = getMessage(uid); if (oldMessage != null && !oldMessage.isSet(Flag.SEEN)) { From cf390700487daf0f6611415d4531f9b013195b46 Mon Sep 17 00:00:00 2001 From: Apoorv Khatreja Date: Mon, 25 Jul 2011 18:56:21 +0530 Subject: [PATCH 07/93] Whitespaces :/ --- src/com/fsck/k9/mail/store/ImapStore.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/com/fsck/k9/mail/store/ImapStore.java b/src/com/fsck/k9/mail/store/ImapStore.java index 4e743e70a..d9f8b4548 100644 --- a/src/com/fsck/k9/mail/store/ImapStore.java +++ b/src/com/fsck/k9/mail/store/ImapStore.java @@ -875,16 +875,16 @@ public class ImapStore extends Store { uidMap.put(srcUidsIterator.next(), destUidsIterator.next()); } } else { - if(K9.DEBUG) + if(K9.DEBUG) Log.v(K9.LOG_TAG, "Parse error: size of source UIDs list is not the same as size of destination UIDs list."); } } else { - if(K9.DEBUG) + if(K9.DEBUG) Log.v(K9.LOG_TAG, "Parsing of the sequence set failed."); } } } else { - if(K9.DEBUG) + if(K9.DEBUG) Log.v(K9.LOG_TAG, "Expected COPYUID response was not found."); } return uidMap; From 4b0d3ccf21caf250806d5b15565d90b0e242b495 Mon Sep 17 00:00:00 2001 From: Apoorv Khatreja Date: Thu, 17 Nov 2011 02:46:01 +0530 Subject: [PATCH 08/93] Removed an unnecessary portion of code that attempted to handle command continuation requests after a UID COPY command. Also removed some extraneous test code that mysteriously creeped into the repo. --- src/com/fsck/k9/mail/store/ImapStore.java | 17 ----------------- 1 file changed, 17 deletions(-) diff --git a/src/com/fsck/k9/mail/store/ImapStore.java b/src/com/fsck/k9/mail/store/ImapStore.java index d9f8b4548..a0efebeee 100644 --- a/src/com/fsck/k9/mail/store/ImapStore.java +++ b/src/com/fsck/k9/mail/store/ImapStore.java @@ -839,12 +839,6 @@ public class ImapStore extends Store { do { response = mConnection.readResponse(); handleUntaggedResponse(response); - if (response.mCommandContinuationRequested) { - EOLConvertingOutputStream eolOut = new EOLConvertingOutputStream(mConnection.mOut); - eolOut.write('\r'); - eolOut.write('\n'); - eolOut.flush(); - } while (response.more()); } while (response.mTag == null); @@ -2410,17 +2404,6 @@ public class ImapStore extends Store { return executeSimpleCommand(command, sensitive, null); } -// public void logResponse (ImapList response) { -// for(int i=0;i executeSimpleCommand(String command, boolean sensitive, UntaggedHandler untaggedHandler) throws IOException, ImapException, MessagingException { String commandToLog = command; From 99e9bee7248b919c43c12ec49ec6179906afdb18 Mon Sep 17 00:00:00 2001 From: Jesse Vincent Date: Sun, 22 Jan 2012 23:12:16 -0500 Subject: [PATCH 09/93] Bumped manifest to 4.107 --- AndroidManifest.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/AndroidManifest.xml b/AndroidManifest.xml index 00263f209..9b8501028 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -1,8 +1,8 @@ Date: Mon, 23 Jan 2012 19:51:30 -0600 Subject: [PATCH 10/93] added montclair.edu to res/xml/providers.xml based on http://oit.montclair.edu/documentation/msu_apps/email/AndroidPhoneSettingUpYourMSUEmailAccount.pdf --- res/xml/providers.xml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/res/xml/providers.xml b/res/xml/providers.xml index 9a7ecc794..62a7f9c75 100644 --- a/res/xml/providers.xml +++ b/res/xml/providers.xml @@ -170,6 +170,10 @@ + + + + From 7a3cadbf1c838f097332e5d04a05aeaabb6afa28 Mon Sep 17 00:00:00 2001 From: cketti Date: Tue, 24 Jan 2012 14:34:32 +0100 Subject: [PATCH 11/93] Refactored AttachmentProvider.openFile() --- .../fsck/k9/provider/AttachmentProvider.java | 29 +++++++++---------- 1 file changed, 14 insertions(+), 15 deletions(-) diff --git a/src/com/fsck/k9/provider/AttachmentProvider.java b/src/com/fsck/k9/provider/AttachmentProvider.java index 542beab00..1a493a446 100644 --- a/src/com/fsck/k9/provider/AttachmentProvider.java +++ b/src/com/fsck/k9/provider/AttachmentProvider.java @@ -157,24 +157,24 @@ public class AttachmentProvider extends ContentProvider { @Override public ParcelFileDescriptor openFile(Uri uri, String mode) throws FileNotFoundException { + File file; + List segments = uri.getPathSegments(); - String dbName = segments.get(0); // "/sdcard/..." is URL-encoded and makes up only 1 segment - String id = segments.get(1); + String accountUuid = segments.get(0); + String attachmentId = segments.get(1); String format = segments.get(2); + if (FORMAT_THUMBNAIL.equals(format)) { int width = Integer.parseInt(segments.get(3)); int height = Integer.parseInt(segments.get(4)); - String filename = "thmb_" + dbName + "_" + id + ".tmp"; - int index = dbName.lastIndexOf('/'); - if (index >= 0) { - filename = /*dbName.substring(0, index + 1) + */"thmb_" + dbName.substring(index + 1) + "_" + id + ".tmp"; - } + + String filename = "thmb_" + accountUuid + "_" + attachmentId + ".tmp"; File dir = getContext().getCacheDir(); - File file = new File(dir, filename); + file = new File(dir, filename); if (!file.exists()) { - String type = getType(dbName, id, FORMAT_VIEW); + String type = getType(accountUuid, attachmentId, FORMAT_VIEW); try { - FileInputStream in = new FileInputStream(getFile(dbName, id)); + FileInputStream in = new FileInputStream(getFile(accountUuid, attachmentId)); try { Bitmap thumbnail = createThumbnail(type, in); if (thumbnail != null) { @@ -187,18 +187,17 @@ public class AttachmentProvider extends ContentProvider { } } } finally { - try { in.close(); } catch (Throwable ignore) {} + try { in.close(); } catch (Throwable ignore) { /* ignore */ } } } catch (IOException ioe) { return null; } } - return ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY); } else { - return ParcelFileDescriptor.open( - getFile(dbName, id), - ParcelFileDescriptor.MODE_READ_ONLY); + file = getFile(accountUuid, attachmentId); } + + return ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY); } @Override From c6696f632a18a3292f2edc7ee0650154e148fb73 Mon Sep 17 00:00:00 2001 From: cketti Date: Tue, 24 Jan 2012 14:49:08 +0100 Subject: [PATCH 12/93] Code cleanup, fixed some warnings, rearranged some stuff --- .../fsck/k9/provider/AttachmentProvider.java | 168 +++++++++--------- 1 file changed, 88 insertions(+), 80 deletions(-) diff --git a/src/com/fsck/k9/provider/AttachmentProvider.java b/src/com/fsck/k9/provider/AttachmentProvider.java index 1a493a446..4b90305c7 100644 --- a/src/com/fsck/k9/provider/AttachmentProvider.java +++ b/src/com/fsck/k9/provider/AttachmentProvider.java @@ -23,8 +23,12 @@ import java.io.*; import java.util.List; /** - * A simple ContentProvider that allows file access to Email's attachments.
- * Warning! We make heavy assumptions about the Uris used by the {@link LocalStore} for an {@link Account} here. + * A simple ContentProvider that allows file access to attachments. + * + *

+ * Warning! We make heavy assumptions about the Uris used by the {@link LocalStore} for an + * {@link Account} here. + *

*/ public class AttachmentProvider extends ContentProvider { public static final Uri CONTENT_URI = Uri.parse("content://com.fsck.k9.attachmentprovider"); @@ -33,6 +37,11 @@ public class AttachmentProvider extends ContentProvider { private static final String FORMAT_VIEW = "VIEW"; private static final String FORMAT_THUMBNAIL = "THUMBNAIL"; + private static final String[] DEFAULT_PROJECTION = new String[] { + AttachmentProviderColumns._ID, + AttachmentProviderColumns.DATA, + }; + public static class AttachmentProviderColumns { public static final String _ID = "_id"; public static final String DATA = "_data"; @@ -40,6 +49,7 @@ public class AttachmentProvider extends ContentProvider { public static final String SIZE = "_size"; } + public static Uri getAttachmentUri(Account account, long id) { return getAttachmentUri(account.getUuid(), id, true); } @@ -66,6 +76,23 @@ public class AttachmentProvider extends ContentProvider { .build(); } + public static void clear(Context context) { + /* + * We use the cache dir as a temporary directory (since Android doesn't give us one) so + * on startup we'll clean up any .tmp files from the last run. + */ + File[] files = context.getCacheDir().listFiles(); + for (File file : files) { + try { + if (K9.DEBUG) { + Log.d(K9.LOG_TAG, "Deleting file " + file.getCanonicalPath()); + } + } catch (IOException ioe) { /* No need to log failure to log */ } + file.delete(); + } + } + + @Override public boolean onCreate() { /* @@ -89,21 +116,6 @@ public class AttachmentProvider extends ContentProvider { return true; } - public static void clear(Context lContext) { - /* - * We use the cache dir as a temporary directory (since Android doesn't give us one) so - * on startup we'll clean up any .tmp files from the last run. - */ - File[] files = lContext.getCacheDir().listFiles(); - for (File file : files) { - try { - if (K9.DEBUG) - Log.d(K9.LOG_TAG, "Deleting file " + file.getCanonicalPath()); - } catch (IOException ioe) {} // No need to log failure to log - file.delete(); - } - } - @Override public String getType(Uri uri) { List segments = uri.getPathSegments(); @@ -114,47 +126,6 @@ public class AttachmentProvider extends ContentProvider { return getType(dbName, id, format); } - private String getType(String dbName, String id, String format) { - if (FORMAT_THUMBNAIL.equals(format)) { - return "image/png"; - } else { - final Account account = Preferences.getPreferences(getContext()).getAccount(dbName); - - try { - final LocalStore localStore = LocalStore.getLocalInstance(account, K9.app); - - AttachmentInfo attachmentInfo = localStore.getAttachmentInfo(id); - if (FORMAT_VIEW.equals(format)) { - return MimeUtility.getMimeTypeForViewing(attachmentInfo.type, attachmentInfo.name); - } else { - // When accessing the "raw" message we deliver the original MIME type. - return attachmentInfo.type; - } - } catch (MessagingException e) { - Log.e(K9.LOG_TAG, "Unable to retrieve LocalStore for " + account, e); - return null; - } - } - } - - private File getFile(String dbName, String id) - throws FileNotFoundException { - try { - final Account account = Preferences.getPreferences(getContext()).getAccount(dbName); - final File attachmentsDir; - attachmentsDir = StorageManager.getInstance(K9.app).getAttachmentDirectory(dbName, - account.getLocalStorageProviderId()); - final File file = new File(attachmentsDir, id); - if (!file.exists()) { - throw new FileNotFoundException(file.getAbsolutePath()); - } - return file; - } catch (IOException e) { - Log.w(K9.LOG_TAG, null, e); - throw new FileNotFoundException(e.getMessage()); - } - } - @Override public ParcelFileDescriptor openFile(Uri uri, String mode) throws FileNotFoundException { File file; @@ -200,26 +171,11 @@ public class AttachmentProvider extends ContentProvider { return ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY); } - @Override - public int delete(Uri uri, String arg1, String[] arg2) { - return 0; - } - - @Override - public Uri insert(Uri uri, ContentValues values) { - return null; - } - @Override public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) { - if (projection == null) { - projection = - new String[] { - AttachmentProviderColumns._ID, - AttachmentProviderColumns.DATA, - }; - } + + String[] columnNames = (projection == null) ? DEFAULT_PROJECTION : projection; List segments = uri.getPathSegments(); String dbName = segments.get(0); @@ -231,7 +187,6 @@ public class AttachmentProvider extends ContentProvider { dbName = dbName.substring(0, dbName.length() - 3); } - //String format = segments.get(2); final AttachmentInfo attachmentInfo; try { final Account account = Preferences.getPreferences(getContext()).getAccount(dbName); @@ -241,10 +196,10 @@ public class AttachmentProvider extends ContentProvider { return null; } - MatrixCursor ret = new MatrixCursor(projection); - Object[] values = new Object[projection.length]; - for (int i = 0, count = projection.length; i < count; i++) { - String column = projection[i]; + MatrixCursor ret = new MatrixCursor(columnNames); + Object[] values = new Object[columnNames.length]; + for (int i = 0, count = columnNames.length; i < count; i++) { + String column = columnNames[i]; if (AttachmentProviderColumns._ID.equals(column)) { values[i] = id; } else if (AttachmentProviderColumns.DATA.equals(column)) { @@ -264,6 +219,59 @@ public class AttachmentProvider extends ContentProvider { return 0; } + @Override + public int delete(Uri uri, String arg1, String[] arg2) { + return 0; + } + + @Override + public Uri insert(Uri uri, ContentValues values) { + return null; + } + + private String getType(String dbName, String id, String format) { + String type; + if (FORMAT_THUMBNAIL.equals(format)) { + type = "image/png"; + } else { + final Account account = Preferences.getPreferences(getContext()).getAccount(dbName); + + try { + final LocalStore localStore = LocalStore.getLocalInstance(account, K9.app); + + AttachmentInfo attachmentInfo = localStore.getAttachmentInfo(id); + if (FORMAT_VIEW.equals(format)) { + type = MimeUtility.getMimeTypeForViewing(attachmentInfo.type, attachmentInfo.name); + } else { + // When accessing the "raw" message we deliver the original MIME type. + type = attachmentInfo.type; + } + } catch (MessagingException e) { + Log.e(K9.LOG_TAG, "Unable to retrieve LocalStore for " + account, e); + type = null; + } + } + + return type; + } + + private File getFile(String dbName, String id) throws FileNotFoundException { + try { + final Account account = Preferences.getPreferences(getContext()).getAccount(dbName); + final File attachmentsDir; + attachmentsDir = StorageManager.getInstance(K9.app).getAttachmentDirectory(dbName, + account.getLocalStorageProviderId()); + final File file = new File(attachmentsDir, id); + if (!file.exists()) { + throw new FileNotFoundException(file.getAbsolutePath()); + } + return file; + } catch (IOException e) { + Log.w(K9.LOG_TAG, null, e); + throw new FileNotFoundException(e.getMessage()); + } + } + private Bitmap createThumbnail(String type, InputStream data) { if (MimeUtility.mimeTypeMatches(type, "image/*")) { return createImageThumbnail(data); From 4e5d1167138869d42276c7e4114de12c4a31f5ee Mon Sep 17 00:00:00 2001 From: cketti Date: Tue, 24 Jan 2012 15:41:03 +0100 Subject: [PATCH 13/93] Delete attachment metadata and thumbnails when deleting attachments --- src/com/fsck/k9/mail/store/LocalStore.java | 28 +++++++++++++------ .../fsck/k9/provider/AttachmentProvider.java | 28 +++++++++++++++++-- 2 files changed, 45 insertions(+), 11 deletions(-) diff --git a/src/com/fsck/k9/mail/store/LocalStore.java b/src/com/fsck/k9/mail/store/LocalStore.java index abfa3ed1d..38130c509 100644 --- a/src/com/fsck/k9/mail/store/LocalStore.java +++ b/src/com/fsck/k9/mail/store/LocalStore.java @@ -2692,22 +2692,34 @@ public class LocalStore extends Store implements Serializable { public Void doDbWork(final SQLiteDatabase db) throws WrappedException, UnavailableStorageException { Cursor attachmentsCursor = null; try { - attachmentsCursor = db.query("attachments", new String[] - { "id" }, "message_id = ?", new String[] - { Long.toString(messageId) }, null, null, null); + String accountUuid = mAccount.getUuid(); + Context context = mApplication; + + // Get attachment IDs + String[] whereArgs = new String[] { Long.toString(messageId) }; + attachmentsCursor = db.query("attachments", new String[] { "id" }, + "message_id = ?", whereArgs, null, null, null); + final File attachmentDirectory = StorageManager.getInstance(mApplication) - .getAttachmentDirectory(uUid, database.getStorageProviderId()); + .getAttachmentDirectory(uUid, database.getStorageProviderId()); + while (attachmentsCursor.moveToNext()) { - long attachmentId = attachmentsCursor.getLong(0); + String attachmentId = Long.toString(attachmentsCursor.getLong(0)); try { - File file = new File(attachmentDirectory, Long.toString(attachmentId)); + // Delete stored attachment + File file = new File(attachmentDirectory, attachmentId); if (file.exists()) { file.delete(); } - } catch (Exception e) { - } + // Delete thumbnail file + AttachmentProvider.deleteThumbnail(context, accountUuid, + attachmentId); + } catch (Exception e) { /* ignore */ } } + + // Delete attachment metadata from the database + db.delete("attachments", "message_id = ?", whereArgs); } finally { Utility.closeQuietly(attachmentsCursor); } diff --git a/src/com/fsck/k9/provider/AttachmentProvider.java b/src/com/fsck/k9/provider/AttachmentProvider.java index 4b90305c7..dd380e6af 100644 --- a/src/com/fsck/k9/provider/AttachmentProvider.java +++ b/src/com/fsck/k9/provider/AttachmentProvider.java @@ -92,6 +92,30 @@ public class AttachmentProvider extends ContentProvider { } } + /** + * Delete the thumbnail of an attachment. + * + * @param context + * The application context. + * @param accountUuid + * The UUID of the account the attachment belongs to. + * @param attachmentId + * The ID of the attachment the thumbnail was created for. + */ + public static void deleteThumbnail(Context context, String accountUuid, String attachmentId) { + File file = getThumbnailFile(context, accountUuid, attachmentId); + if (file.exists()) { + file.delete(); + } + } + + private static File getThumbnailFile(Context context, String accountUuid, + String attachmentId) { + String filename = "thmb_" + accountUuid + "_" + attachmentId + ".tmp"; + File dir = context.getCacheDir(); + return new File(dir, filename); + } + @Override public boolean onCreate() { @@ -139,9 +163,7 @@ public class AttachmentProvider extends ContentProvider { int width = Integer.parseInt(segments.get(3)); int height = Integer.parseInt(segments.get(4)); - String filename = "thmb_" + accountUuid + "_" + attachmentId + ".tmp"; - File dir = getContext().getCacheDir(); - file = new File(dir, filename); + file = getThumbnailFile(getContext(), accountUuid, attachmentId); if (!file.exists()) { String type = getType(accountUuid, attachmentId, FORMAT_VIEW); try { From aae734c175b630db9654ec221b1210f772a02397 Mon Sep 17 00:00:00 2001 From: cketti Date: Tue, 24 Jan 2012 22:14:21 +0100 Subject: [PATCH 14/93] Don't Log.w() full stacktrace if file wasn't found This happens regularly when AttachmentView tries to get a thumbnail for attachments that haven't been downloaded yet. --- .../fsck/k9/provider/AttachmentProvider.java | 23 ++++++++----------- 1 file changed, 10 insertions(+), 13 deletions(-) diff --git a/src/com/fsck/k9/provider/AttachmentProvider.java b/src/com/fsck/k9/provider/AttachmentProvider.java index dd380e6af..ec81e30ba 100644 --- a/src/com/fsck/k9/provider/AttachmentProvider.java +++ b/src/com/fsck/k9/provider/AttachmentProvider.java @@ -278,20 +278,17 @@ public class AttachmentProvider extends ContentProvider { } private File getFile(String dbName, String id) throws FileNotFoundException { - try { - final Account account = Preferences.getPreferences(getContext()).getAccount(dbName); - final File attachmentsDir; - attachmentsDir = StorageManager.getInstance(K9.app).getAttachmentDirectory(dbName, - account.getLocalStorageProviderId()); - final File file = new File(attachmentsDir, id); - if (!file.exists()) { - throw new FileNotFoundException(file.getAbsolutePath()); - } - return file; - } catch (IOException e) { - Log.w(K9.LOG_TAG, null, e); - throw new FileNotFoundException(e.getMessage()); + Account account = Preferences.getPreferences(getContext()).getAccount(dbName); + + File attachmentsDir = StorageManager.getInstance(K9.app).getAttachmentDirectory(dbName, + account.getLocalStorageProviderId()); + + File file = new File(attachmentsDir, id); + if (!file.exists()) { + throw new FileNotFoundException(file.getAbsolutePath()); } + + return file; } private Bitmap createThumbnail(String type, InputStream data) { From 5a8ddaa039ae325bb80f1cc3542f497bd2e5415d Mon Sep 17 00:00:00 2001 From: cketti Date: Thu, 26 Jan 2012 00:37:25 +0100 Subject: [PATCH 15/93] Make sure draft id points to a valid message before deleting it Avoids IllegalArgumentException in LocalStore.getMessage() --- src/com/fsck/k9/activity/MessageCompose.java | 5 +++-- src/com/fsck/k9/controller/MessagingController.java | 8 +++++--- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/src/com/fsck/k9/activity/MessageCompose.java b/src/com/fsck/k9/activity/MessageCompose.java index 6da95c263..2eac71f19 100644 --- a/src/com/fsck/k9/activity/MessageCompose.java +++ b/src/com/fsck/k9/activity/MessageCompose.java @@ -2988,9 +2988,10 @@ public class MessageCompose extends K9Activity implements OnClickListener, OnFoc } MessagingController.getInstance(getApplication()).sendMessage(mAccount, message, null); - if (mDraftId != INVALID_DRAFT_ID) { - MessagingController.getInstance(getApplication()).deleteDraft(mAccount, mDraftId); + long draftId = mDraftId; + if (draftId != INVALID_DRAFT_ID) { mDraftId = INVALID_DRAFT_ID; + MessagingController.getInstance(getApplication()).deleteDraft(mAccount, draftId); } return null; diff --git a/src/com/fsck/k9/controller/MessagingController.java b/src/com/fsck/k9/controller/MessagingController.java index 2aaa48ae1..b3b6b5462 100644 --- a/src/com/fsck/k9/controller/MessagingController.java +++ b/src/com/fsck/k9/controller/MessagingController.java @@ -3311,9 +3311,11 @@ public class MessagingController implements Runnable { localFolder = localStore.getFolder(account.getDraftsFolderName()); localFolder.open(OpenMode.READ_WRITE); String uid = localFolder.getMessageUidById(id); - Message message = localFolder.getMessage(uid); - if (message != null) { - deleteMessages(new Message[] { message }, null); + if (uid != null) { + Message message = localFolder.getMessage(uid); + if (message != null) { + deleteMessages(new Message[] { message }, null); + } } } catch (MessagingException me) { addErrorMessage(account, null, me); From e8eae3738963cc5f14c8750197fea1423e8818e1 Mon Sep 17 00:00:00 2001 From: cketti Date: Fri, 27 Jan 2012 00:51:18 +0100 Subject: [PATCH 16/93] Reset unread/flagged count on "empty trash" --- src/com/fsck/k9/controller/MessagingController.java | 5 +++-- src/com/fsck/k9/mail/store/LocalStore.java | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/com/fsck/k9/controller/MessagingController.java b/src/com/fsck/k9/controller/MessagingController.java index b3b6b5462..389e6f2a4 100644 --- a/src/com/fsck/k9/controller/MessagingController.java +++ b/src/com/fsck/k9/controller/MessagingController.java @@ -3470,12 +3470,13 @@ public class MessagingController implements Runnable { putBackground("emptyTrash", listener, new Runnable() { @Override public void run() { - Folder localFolder = null; + LocalFolder localFolder = null; try { Store localStore = account.getLocalStore(); - localFolder = localStore.getFolder(account.getTrashFolderName()); + localFolder = (LocalFolder) localStore.getFolder(account.getTrashFolderName()); localFolder.open(OpenMode.READ_WRITE); localFolder.setFlags(new Flag[] { Flag.DELETED }, true); + localFolder.resetUnreadAndFlaggedCounts(); for (MessagingListener l : getListeners()) { l.emptyTrashCompleted(account); diff --git a/src/com/fsck/k9/mail/store/LocalStore.java b/src/com/fsck/k9/mail/store/LocalStore.java index 38130c509..049f4222e 100644 --- a/src/com/fsck/k9/mail/store/LocalStore.java +++ b/src/com/fsck/k9/mail/store/LocalStore.java @@ -2619,7 +2619,7 @@ public class LocalStore extends Store implements Serializable { setVisibleLimit(mAccount.getDisplayCount()); } - private void resetUnreadAndFlaggedCounts() { + public void resetUnreadAndFlaggedCounts() { try { int newUnread = 0; int newFlagged = 0; From 193450cd63086e11b4822b3b3b13d663a47c1bb3 Mon Sep 17 00:00:00 2001 From: wilian-cb Date: Thu, 26 Jan 2012 19:25:46 -0200 Subject: [PATCH 17/93] 3674: Illegal characters used in file names are being removed when saving them on the device. --- src/com/fsck/k9/view/AttachmentView.java | 24 ++++++++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/src/com/fsck/k9/view/AttachmentView.java b/src/com/fsck/k9/view/AttachmentView.java index 9e9a4c441..764663cbe 100644 --- a/src/com/fsck/k9/view/AttachmentView.java +++ b/src/com/fsck/k9/view/AttachmentView.java @@ -52,6 +52,13 @@ public class AttachmentView extends FrameLayout { public String contentType; public long size; public ImageView iconView; + + /** + * Regular expression that represents characters that aren't allowed + * to be used in file names saved using K-9 + */ + private static final String specialCharacters = new String("[^\\d\\s\\w!" + + "#\\$%&'\\(\\)\\-@\\^_`\\{\\}~.,]"); private AttachmentFileDownloadCallback callback; @@ -196,7 +203,8 @@ public class AttachmentView extends FrameLayout { */ public void writeFile(File directory) { try { - File file = Utility.createUniqueFile(directory, name); + String filename = removeSpecialCharacters(name); + File file = Utility.createUniqueFile(directory, filename); Uri uri = AttachmentProvider.getAttachmentUri(mAccount, part.getAttachmentId()); InputStream in = mContext.getContentResolver().openInputStream(uri); OutputStream out = new FileOutputStream(file); @@ -204,12 +212,24 @@ public class AttachmentView extends FrameLayout { out.flush(); out.close(); in.close(); - attachmentSaved(file.toString()); + attachmentSaved(filename.toString()); new MediaScannerNotifier(mContext, file); } catch (IOException ioe) { attachmentNotSaved(); } } + + /** + * Removes characters that aren't allowed to be used in file names saved + * in K-9 application. + * + * @param filename The original file name. + * @return A file name with only legal characters. + */ + private String removeSpecialCharacters(String filename) { + return filename.replaceAll(specialCharacters, ""); + } + /** * saves the file to the defaultpath setting in the config, or if the config * is not set => to the Environment From 9a0f650cd95c93e59f7b21ee38892c26b4d2e9ff Mon Sep 17 00:00:00 2001 From: wilian-cb Date: Thu, 26 Jan 2012 20:53:41 -0200 Subject: [PATCH 18/93] 3674: Displaying the final file name. --- src/com/fsck/k9/view/AttachmentView.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/com/fsck/k9/view/AttachmentView.java b/src/com/fsck/k9/view/AttachmentView.java index 764663cbe..7525456fd 100644 --- a/src/com/fsck/k9/view/AttachmentView.java +++ b/src/com/fsck/k9/view/AttachmentView.java @@ -212,7 +212,7 @@ public class AttachmentView extends FrameLayout { out.flush(); out.close(); in.close(); - attachmentSaved(filename.toString()); + attachmentSaved(file.toString()); new MediaScannerNotifier(mContext, file); } catch (IOException ioe) { attachmentNotSaved(); From 68f5f009f1548efbcb87c4cb837d1201eeaf761c Mon Sep 17 00:00:00 2001 From: cketti Date: Fri, 27 Jan 2012 03:07:44 +0100 Subject: [PATCH 19/93] Whitespace cleanup --- src/com/fsck/k9/view/AttachmentView.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/com/fsck/k9/view/AttachmentView.java b/src/com/fsck/k9/view/AttachmentView.java index 7525456fd..0a851e658 100644 --- a/src/com/fsck/k9/view/AttachmentView.java +++ b/src/com/fsck/k9/view/AttachmentView.java @@ -52,13 +52,13 @@ public class AttachmentView extends FrameLayout { public String contentType; public long size; public ImageView iconView; - + /** * Regular expression that represents characters that aren't allowed * to be used in file names saved using K-9 */ private static final String specialCharacters = new String("[^\\d\\s\\w!" + - "#\\$%&'\\(\\)\\-@\\^_`\\{\\}~.,]"); + "#\\$%&'\\(\\)\\-@\\^_`\\{\\}~.,]"); private AttachmentFileDownloadCallback callback; @@ -222,12 +222,12 @@ public class AttachmentView extends FrameLayout { /** * Removes characters that aren't allowed to be used in file names saved * in K-9 application. - * + * * @param filename The original file name. * @return A file name with only legal characters. */ private String removeSpecialCharacters(String filename) { - return filename.replaceAll(specialCharacters, ""); + return filename.replaceAll(specialCharacters, ""); } /** From 6c23e204baa21d74bec9a6099956061da58116e8 Mon Sep 17 00:00:00 2001 From: cketti Date: Fri, 27 Jan 2012 03:21:20 +0100 Subject: [PATCH 20/93] Replace invalid characters in file name instead of removing them --- src/com/fsck/k9/view/AttachmentView.java | 34 ++++++++++++++---------- 1 file changed, 20 insertions(+), 14 deletions(-) diff --git a/src/com/fsck/k9/view/AttachmentView.java b/src/com/fsck/k9/view/AttachmentView.java index 0a851e658..8621aa7a9 100644 --- a/src/com/fsck/k9/view/AttachmentView.java +++ b/src/com/fsck/k9/view/AttachmentView.java @@ -39,6 +39,18 @@ import com.fsck.k9.mail.store.LocalStore.LocalAttachmentBodyPart; import com.fsck.k9.provider.AttachmentProvider; public class AttachmentView extends FrameLayout { + /** + * Regular expression that represents characters that aren't allowed + * to be used in file names saved using K-9 + */ + private static final String INVALID_CHARACTERS = "[^\\d\\s\\w!" + + "#\\$%&'\\(\\)\\-@\\^_`\\{\\}~.,]+"; + + /** + * Invalid characters in a file name are replaced by this character. + */ + private static final String REPLACEMENT_CHARACTER = "_"; + private Context mContext; public Button viewButton; @@ -53,13 +65,6 @@ public class AttachmentView extends FrameLayout { public long size; public ImageView iconView; - /** - * Regular expression that represents characters that aren't allowed - * to be used in file names saved using K-9 - */ - private static final String specialCharacters = new String("[^\\d\\s\\w!" + - "#\\$%&'\\(\\)\\-@\\^_`\\{\\}~.,]"); - private AttachmentFileDownloadCallback callback; public AttachmentView(Context context, AttributeSet attrs, int defStyle) { @@ -203,7 +208,7 @@ public class AttachmentView extends FrameLayout { */ public void writeFile(File directory) { try { - String filename = removeSpecialCharacters(name); + String filename = sanitizeFilename(name); File file = Utility.createUniqueFile(directory, filename); Uri uri = AttachmentProvider.getAttachmentUri(mAccount, part.getAttachmentId()); InputStream in = mContext.getContentResolver().openInputStream(uri); @@ -220,14 +225,15 @@ public class AttachmentView extends FrameLayout { } /** - * Removes characters that aren't allowed to be used in file names saved - * in K-9 application. + * Replace characters we don't allow in file names with a replacement character. * - * @param filename The original file name. - * @return A file name with only legal characters. + * @param filename + * The original file name. + * + * @return The sanitized file name containing only allowed characters. */ - private String removeSpecialCharacters(String filename) { - return filename.replaceAll(specialCharacters, ""); + private String sanitizeFilename(String filename) { + return filename.replaceAll(INVALID_CHARACTERS, REPLACEMENT_CHARACTER); } /** From 1165787129e27877f4d61099d9e41e490b9886a4 Mon Sep 17 00:00:00 2001 From: cketti Date: Fri, 27 Jan 2012 03:39:08 +0100 Subject: [PATCH 21/93] Changed regular expression for invalid characters in a file name --- src/com/fsck/k9/view/AttachmentView.java | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/src/com/fsck/k9/view/AttachmentView.java b/src/com/fsck/k9/view/AttachmentView.java index 8621aa7a9..bb736f6cc 100644 --- a/src/com/fsck/k9/view/AttachmentView.java +++ b/src/com/fsck/k9/view/AttachmentView.java @@ -40,11 +40,19 @@ import com.fsck.k9.provider.AttachmentProvider; public class AttachmentView extends FrameLayout { /** - * Regular expression that represents characters that aren't allowed - * to be used in file names saved using K-9 + * Regular expression that represents characters we won't allow in file names. + * + *

+ * Allowed are: + *

    + *
  • word characters (letters, digits, and underscores): {@code \w}
  • + *
  • spaces: {@code " "}
  • + *
  • special characters: {@code !}, {@code #}, {@code $}, {@code %}, {@code &}, {@code '}, + * {@code (}, {@code )}, {@code -}, {@code @}, {@code ^}, {@code `}, {, + * }, {@code ~}, {@code .}, {@code ,}
  • + *

*/ - private static final String INVALID_CHARACTERS = "[^\\d\\s\\w!" + - "#\\$%&'\\(\\)\\-@\\^_`\\{\\}~.,]+"; + private static final String INVALID_CHARACTERS = "[^\\w !#$%&'()\\-@\\^`{}~.,]+"; /** * Invalid characters in a file name are replaced by this character. From b1a1de8f7b829e8a07e74c5e79eeb1b29bb19d42 Mon Sep 17 00:00:00 2001 From: cketti Date: Fri, 27 Jan 2012 08:39:10 +0100 Subject: [PATCH 22/93] Restore mark as unread in message view Fixes issue 3958 Fixes issue 3319 --- src/com/fsck/k9/activity/MessageView.java | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/src/com/fsck/k9/activity/MessageView.java b/src/com/fsck/k9/activity/MessageView.java index fb6afcc60..64f4c9208 100644 --- a/src/com/fsck/k9/activity/MessageView.java +++ b/src/com/fsck/k9/activity/MessageView.java @@ -20,6 +20,7 @@ import com.fsck.k9.crypto.PgpData; import com.fsck.k9.helper.FileBrowserHelper; import com.fsck.k9.helper.FileBrowserHelper.FileBrowserFailOverCallback; import com.fsck.k9.mail.*; +import com.fsck.k9.mail.store.LocalStore.LocalMessage; import com.fsck.k9.mail.store.StorageManager; import com.fsck.k9.view.AttachmentView; import com.fsck.k9.view.ToggleScrollView; @@ -726,10 +727,15 @@ public class MessageView extends K9Activity implements OnClickListener { private void onFlag() { if (mMessage != null) { - mController.setFlag(mAccount, - mMessage.getFolder().getName(), new String[] {mMessage.getUid()}, Flag.FLAGGED, !mMessage.isSet(Flag.FLAGGED)); + boolean newState = !mMessage.isSet(Flag.FLAGGED); + mController.setFlag(mAccount, mMessage.getFolder().getName(), + new String[] {mMessage.getUid()}, Flag.FLAGGED, newState); try { - mMessage.setFlag(Flag.FLAGGED, !mMessage.isSet(Flag.FLAGGED)); + // FIXME: This is a hack to change the flagged state of our message object. We + // can't call Message.setFlag() because that would "adjust" the flagged count + // another time (first time by MessagingController.setFlag(...)). + ((LocalMessage)mMessage).setFlagInternal(Flag.FLAGGED, newState); + mMessageView.setHeaders(mMessage, mAccount); } catch (MessagingException me) { Log.e(K9.LOG_TAG, "Could not set flag on local message", me); @@ -876,9 +882,13 @@ public class MessageView extends K9Activity implements OnClickListener { private void onMarkAsUnread() { if (mMessage != null) { -// (Issue 3319) mController.setFlag(mAccount, mMessageReference.folderName, new String[] { mMessage.getUid() }, Flag.SEEN, false); + mController.setFlag(mAccount, mMessage.getFolder().getName(), + new String[] { mMessage.getUid() }, Flag.SEEN, false); try { - mMessage.setFlag(Flag.SEEN, false); + // FIXME: This is a hack to mark our message object as unread. We can't call + // Message.setFlag() because that would "adjust" the unread count twice. + ((LocalMessage)mMessage).setFlagInternal(Flag.SEEN, false); + mMessageView.setHeaders(mMessage, mAccount); String subject = mMessage.getSubject(); setTitle(subject); From 35b5aebdb322fa44e2c8173dbd9a5f5ef3c51c36 Mon Sep 17 00:00:00 2001 From: cketti Date: Fri, 3 Feb 2012 01:22:37 +0100 Subject: [PATCH 23/93] Add debug message when saving an attachment to SD card fails --- src/com/fsck/k9/view/AttachmentView.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/com/fsck/k9/view/AttachmentView.java b/src/com/fsck/k9/view/AttachmentView.java index bb736f6cc..f581c3db2 100644 --- a/src/com/fsck/k9/view/AttachmentView.java +++ b/src/com/fsck/k9/view/AttachmentView.java @@ -228,6 +228,9 @@ public class AttachmentView extends FrameLayout { attachmentSaved(file.toString()); new MediaScannerNotifier(mContext, file); } catch (IOException ioe) { + if (K9.DEBUG) { + Log.e(K9.LOG_TAG, "Error saving attachment", ioe); + } attachmentNotSaved(); } } From 46bf2c5be7e9d3097da736d8448ddd325aae8702 Mon Sep 17 00:00:00 2001 From: Nick Nikolaou Date: Thu, 2 Feb 2012 21:56:56 +0000 Subject: [PATCH 24/93] Add discard confirmation dialog when pressing back --- res/values/strings.xml | 3 ++ src/com/fsck/k9/activity/MessageCompose.java | 42 +++++++++++++++++--- 2 files changed, 39 insertions(+), 6 deletions(-) diff --git a/res/values/strings.xml b/res/values/strings.xml index f2666ff53..9fd5973aa 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -1030,6 +1030,9 @@ Welcome to K-9 Mail setup. K-9 is an open source mail client for Android origin Save draft message? Save or Discard this message? + Discard message? + Are you sure you want to discard this message? + Refuse to save draft message. Refuse to save draft message marked encrypted. diff --git a/src/com/fsck/k9/activity/MessageCompose.java b/src/com/fsck/k9/activity/MessageCompose.java index 2eac71f19..a24423eb3 100644 --- a/src/com/fsck/k9/activity/MessageCompose.java +++ b/src/com/fsck/k9/activity/MessageCompose.java @@ -83,6 +83,7 @@ public class MessageCompose extends K9Activity implements OnClickListener, OnFoc private static final int DIALOG_SAVE_OR_DISCARD_DRAFT_MESSAGE = 1; private static final int DIALOG_REFUSE_TO_SAVE_DRAFT_MARKED_ENCRYPTED = 2; private static final int DIALOG_CONTINUE_WITHOUT_PUBLIC_KEY = 3; + private static final int DIALOG_CONFIRM_DISCARD_ON_BACK = 4; private static final long INVALID_DRAFT_ID = MessagingController.INVALID_MESSAGE_ID; @@ -2051,13 +2052,21 @@ public class MessageCompose extends K9Activity implements OnClickListener, OnFoc @Override public void onBackPressed() { - if (mEncryptCheckbox.isChecked()) { - showDialog(DIALOG_REFUSE_TO_SAVE_DRAFT_MARKED_ENCRYPTED); - } else if (!mDraftNeedsSaving || isDraftsFolderDisabled()) { - Toast.makeText(MessageCompose.this, getString(R.string.message_discarded_toast), Toast.LENGTH_LONG).show(); - super.onBackPressed(); + if (mDraftNeedsSaving) { + if (mEncryptCheckbox.isChecked()) { + showDialog(DIALOG_REFUSE_TO_SAVE_DRAFT_MARKED_ENCRYPTED); + } else if (isDraftsFolderDisabled()) { + showDialog(DIALOG_CONFIRM_DISCARD_ON_BACK); + } else { + showDialog(DIALOG_SAVE_OR_DISCARD_DRAFT_MESSAGE); + } } else { - showDialog(DIALOG_SAVE_OR_DISCARD_DRAFT_MESSAGE); + // Check if editing an existing draft. + if (mDraftId == INVALID_DRAFT_ID) { + onDiscard(); + } else { + super.onBackPressed(); + } } } @@ -2118,6 +2127,27 @@ public class MessageCompose extends K9Activity implements OnClickListener, OnFoc } }) .create(); + case DIALOG_CONFIRM_DISCARD_ON_BACK: + return new AlertDialog.Builder(this) + .setTitle(R.string.confirm_discard_draft_message_title) + .setMessage(R.string.confirm_discard_draft_message) + .setPositiveButton(R.string.cancel_action, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int whichButton) { + dismissDialog(DIALOG_CONFIRM_DISCARD_ON_BACK); + } + }) + .setNegativeButton(R.string.discard_action, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int whichButton) { + dismissDialog(DIALOG_CONFIRM_DISCARD_ON_BACK); + Toast.makeText(MessageCompose.this, + getString(R.string.message_discarded_toast), + Toast.LENGTH_LONG).show(); + onDiscard(); + } + }) + .create(); } return super.onCreateDialog(id); } From 898f65e0819666e17e1b6ebc1447c72888b60689 Mon Sep 17 00:00:00 2001 From: cketti Date: Sat, 4 Feb 2012 14:52:45 +0100 Subject: [PATCH 25/93] Changed the way we set flags to update the original Message object --- src/com/fsck/k9/activity/MessageCompose.java | 2 +- src/com/fsck/k9/activity/MessageList.java | 13 ++- src/com/fsck/k9/activity/MessageView.java | 30 +---- .../k9/controller/MessagingController.java | 109 +++++++++++++----- 4 files changed, 99 insertions(+), 55 deletions(-) diff --git a/src/com/fsck/k9/activity/MessageCompose.java b/src/com/fsck/k9/activity/MessageCompose.java index a24423eb3..8b85cfd95 100644 --- a/src/com/fsck/k9/activity/MessageCompose.java +++ b/src/com/fsck/k9/activity/MessageCompose.java @@ -1611,7 +1611,7 @@ public class MessageCompose extends K9Activity implements OnClickListener, OnFoc final Account account = Preferences.getPreferences(this).getAccount(mMessageReference.accountUuid); final String folderName = mMessageReference.folderName; final String sourceMessageUid = mMessageReference.uid; - MessagingController.getInstance(getApplication()).setFlag(account, folderName, new String[] {sourceMessageUid}, mMessageReference.flag, true); + MessagingController.getInstance(getApplication()).setFlag(account, folderName, sourceMessageUid, mMessageReference.flag, true); } mDraftNeedsSaving = false; diff --git a/src/com/fsck/k9/activity/MessageList.java b/src/com/fsck/k9/activity/MessageList.java index 1d2c8f59e..89d1366ef 100644 --- a/src/com/fsck/k9/activity/MessageList.java +++ b/src/com/fsck/k9/activity/MessageList.java @@ -70,6 +70,7 @@ import com.fsck.k9.mail.Folder; import com.fsck.k9.mail.Message; import com.fsck.k9.mail.store.LocalStore; import com.fsck.k9.mail.store.LocalStore.LocalFolder; +import com.fsck.k9.mail.store.LocalStore.LocalMessage; import com.fsck.k9.mail.store.StorageManager; @@ -1384,13 +1385,21 @@ public class MessageList } private void onToggleRead(MessageInfoHolder holder) { - mController.setFlag(holder.message.getFolder().getAccount(), holder.message.getFolder().getName(), new String[] { holder.uid }, Flag.SEEN, !holder.read); + LocalMessage message = holder.message; + Folder folder = message.getFolder(); + Account account = folder.getAccount(); + String folderName = folder.getName(); + mController.setFlag(account, folderName, new Message[] { message }, Flag.SEEN, !holder.read); holder.read = !holder.read; mHandler.sortMessages(); } private void onToggleFlag(MessageInfoHolder holder) { - mController.setFlag(holder.message.getFolder().getAccount(), holder.message.getFolder().getName(), new String[] { holder.uid }, Flag.FLAGGED, !holder.flagged); + LocalMessage message = holder.message; + Folder folder = message.getFolder(); + Account account = folder.getAccount(); + String folderName = folder.getName(); + mController.setFlag(account, folderName, new Message[] { message }, Flag.FLAGGED, !holder.flagged); holder.flagged = !holder.flagged; mHandler.sortMessages(); } diff --git a/src/com/fsck/k9/activity/MessageView.java b/src/com/fsck/k9/activity/MessageView.java index 64f4c9208..221d195c9 100644 --- a/src/com/fsck/k9/activity/MessageView.java +++ b/src/com/fsck/k9/activity/MessageView.java @@ -20,7 +20,6 @@ import com.fsck.k9.crypto.PgpData; import com.fsck.k9.helper.FileBrowserHelper; import com.fsck.k9.helper.FileBrowserHelper.FileBrowserFailOverCallback; import com.fsck.k9.mail.*; -import com.fsck.k9.mail.store.LocalStore.LocalMessage; import com.fsck.k9.mail.store.StorageManager; import com.fsck.k9.view.AttachmentView; import com.fsck.k9.view.ToggleScrollView; @@ -729,17 +728,8 @@ public class MessageView extends K9Activity implements OnClickListener { if (mMessage != null) { boolean newState = !mMessage.isSet(Flag.FLAGGED); mController.setFlag(mAccount, mMessage.getFolder().getName(), - new String[] {mMessage.getUid()}, Flag.FLAGGED, newState); - try { - // FIXME: This is a hack to change the flagged state of our message object. We - // can't call Message.setFlag() because that would "adjust" the flagged count - // another time (first time by MessagingController.setFlag(...)). - ((LocalMessage)mMessage).setFlagInternal(Flag.FLAGGED, newState); - - mMessageView.setHeaders(mMessage, mAccount); - } catch (MessagingException me) { - Log.e(K9.LOG_TAG, "Could not set flag on local message", me); - } + new Message[] { mMessage }, Flag.FLAGGED, newState); + mMessageView.setHeaders(mMessage, mAccount); } } @@ -883,18 +873,10 @@ public class MessageView extends K9Activity implements OnClickListener { private void onMarkAsUnread() { if (mMessage != null) { mController.setFlag(mAccount, mMessage.getFolder().getName(), - new String[] { mMessage.getUid() }, Flag.SEEN, false); - try { - // FIXME: This is a hack to mark our message object as unread. We can't call - // Message.setFlag() because that would "adjust" the unread count twice. - ((LocalMessage)mMessage).setFlagInternal(Flag.SEEN, false); - - mMessageView.setHeaders(mMessage, mAccount); - String subject = mMessage.getSubject(); - setTitle(subject); - } catch (Exception e) { - Log.e(K9.LOG_TAG, "Unable to unset SEEN flag on message", e); - } + new Message[] { mMessage }, Flag.SEEN, false); + mMessageView.setHeaders(mMessage, mAccount); + String subject = mMessage.getSubject(); + setTitle(subject); } } diff --git a/src/com/fsck/k9/controller/MessagingController.java b/src/com/fsck/k9/controller/MessagingController.java index 389e6f2a4..6a9081c28 100644 --- a/src/com/fsck/k9/controller/MessagingController.java +++ b/src/com/fsck/k9/controller/MessagingController.java @@ -2519,65 +2519,118 @@ public class MessagingController implements Runnable { @Override public void act(final Account account, final Folder folder, final List messages) { - String[] uids = new String[messages.size()]; - for (int i = 0; i < messages.size(); i++) { - uids[i] = messages.get(i).getUid(); - } - setFlag(account, folder.getName(), uids, flag, newState); + setFlag(account, folder.getName(), messages.toArray(EMPTY_MESSAGE_ARRAY), flag, + newState); } }); } - public void setFlag( - final Account account, - final String folderName, - final String[] uids, - final Flag flag, - final boolean newState) { - // TODO: put this into the background, but right now that causes odd behavior - // because the FolderMessageList doesn't have its own cache of the flag states + /** + * Set or remove a flag for a set of messages in a specific folder. + * + *

+ * The {@link Message} objects passed in are updated to reflect the new flag state. + *

+ * + * @param account + * The account the folder containing the messages belongs to. + * @param folderName + * The name of the folder. + * @param messages + * The messages to change the flag for. + * @param flag + * The flag to change. + * @param newState + * {@code true}, if the flag should be set. {@code false} if it should be removed. + */ + public void setFlag(Account account, String folderName, Message[] messages, Flag flag, + boolean newState) { + // TODO: Put this into the background, but right now some callers depend on the message + // objects being modified right after this method returns. Folder localFolder = null; try { Store localStore = account.getLocalStore(); localFolder = localStore.getFolder(folderName); localFolder.open(OpenMode.READ_WRITE); - ArrayList messages = new ArrayList(); - for (String uid : uids) { - // Allows for re-allowing sending of messages that could not be sent - if (flag == Flag.FLAGGED && !newState - && uid != null - && account.getOutboxFolderName().equals(folderName)) { - sendCount.remove(uid); - } - Message msg = localFolder.getMessage(uid); - if (msg != null) { - messages.add(msg); + + // Allows for re-allowing sending of messages that could not be sent + if (flag == Flag.FLAGGED && !newState && + account.getOutboxFolderName().equals(folderName)) { + for (Message message : messages) { + String uid = message.getUid(); + if (uid != null) { + sendCount.remove(uid); + } } } - localFolder.setFlags(messages.toArray(EMPTY_MESSAGE_ARRAY), new Flag[] {flag}, newState); - + // Update the messages in the local store + localFolder.setFlags(messages, new Flag[] {flag}, newState); for (MessagingListener l : getListeners()) { l.folderStatusChanged(account, folderName, localFolder.getUnreadMessageCount()); } + + /* + * Handle the remote side + */ + + // The error folder is always a local folder + // TODO: Skip the remote part for all local-only folders if (account.getErrorFolderName().equals(folderName)) { return; } + String[] uids = new String[messages.length]; + for (int i = 0, end = uids.length; i < end; i++) { + uids[i] = messages[i].getUid(); + } + queueSetFlag(account, folderName, Boolean.toString(newState), flag.toString(), uids); processPendingCommands(account); } catch (MessagingException me) { addErrorMessage(account, null, me); - throw new RuntimeException(me); } finally { closeFolder(localFolder); } - }//setMesssageFlag + } + + /** + * Set or remove a flag for a message referenced by message UID. + * + * @param account + * The account the folder containing the message belongs to. + * @param folderName + * The name of the folder. + * @param message + * The message to change the flag for. + * @param flag + * The flag to change. + * @param newState + * {@code true}, if the flag should be set. {@code false} if it should be removed. + */ + public void setFlag(Account account, String folderName, String uid, Flag flag, + boolean newState) { + Folder localFolder = null; + try { + LocalStore localStore = account.getLocalStore(); + localFolder = localStore.getFolder(folderName); + localFolder.open(OpenMode.READ_WRITE); + + Message message = localFolder.getMessage(uid); + setFlag(account, folderName, new Message[] { message }, flag, newState); + + } catch (MessagingException me) { + addErrorMessage(account, null, me); + throw new RuntimeException(me); + } finally { + closeFolder(localFolder); + } + } public void clearAllPending(final Account account) { try { From 7ef5f9d37e44f6b01228b1e1676dd4accf46f9b5 Mon Sep 17 00:00:00 2001 From: cketti Date: Sat, 4 Feb 2012 18:07:57 +0100 Subject: [PATCH 26/93] Set flagged and unread count to 0 when emptying the trash folder --- src/com/fsck/k9/controller/MessagingController.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/com/fsck/k9/controller/MessagingController.java b/src/com/fsck/k9/controller/MessagingController.java index 6a9081c28..56b853d21 100644 --- a/src/com/fsck/k9/controller/MessagingController.java +++ b/src/com/fsck/k9/controller/MessagingController.java @@ -3529,7 +3529,8 @@ public class MessagingController implements Runnable { localFolder = (LocalFolder) localStore.getFolder(account.getTrashFolderName()); localFolder.open(OpenMode.READ_WRITE); localFolder.setFlags(new Flag[] { Flag.DELETED }, true); - localFolder.resetUnreadAndFlaggedCounts(); + localFolder.setUnreadMessageCount(0); + localFolder.setFlaggedMessageCount(0); for (MessagingListener l : getListeners()) { l.emptyTrashCompleted(account); From 700ba1d781935cbc2add56e1dbae7c57a03b0949 Mon Sep 17 00:00:00 2001 From: Jesse Vincent Date: Mon, 6 Feb 2012 23:11:12 -0500 Subject: [PATCH 27/93] Bumped manifest to 4.108 --- AndroidManifest.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/AndroidManifest.xml b/AndroidManifest.xml index 9b8501028..b619193c3 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -1,8 +1,8 @@ Date: Wed, 1 Feb 2012 00:07:19 -0200 Subject: [PATCH 28/93] Issue 3280: Add launcher shortcuts for special accounts/folders ("Unified Inbox" and "All messages") --- AndroidManifest.xml | 4 ++ .../fsck/k9/activity/LauncherShortcuts.java | 54 ++++++++++++++++--- src/com/fsck/k9/activity/MessageList.java | 15 +++++- 3 files changed, 64 insertions(+), 9 deletions(-) diff --git a/AndroidManifest.xml b/AndroidManifest.xml index b619193c3..d0f09e32e 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -208,6 +208,10 @@ android:launchMode="singleTask" android:configChanges="locale" > + + + + accounts = new ArrayList(); + + if (!K9.isHideSpecialAccounts()) { + BaseAccount integratedInboxAccount = new SearchAccount(this, true, null, null); + integratedInboxAccount.setDescription( + getString(R.string.integrated_inbox_title)); + integratedInboxAccount.setEmail( + getString(R.string.integrated_inbox_detail)); + + BaseAccount unreadAccount = new SearchAccount(this, false, null, null); + unreadAccount.setDescription( + getString(R.string.search_all_messages_title)); + unreadAccount.setEmail( + getString(R.string.search_all_messages_detail)); + + accounts.add(integratedInboxAccount); + accounts.add(unreadAccount); + } + + accounts.addAll(Arrays.asList(Preferences.getPreferences(this).getAccounts())); mAdapter = new AccountsAdapter(accounts); getListView().setAdapter(mAdapter); } - private void setupShortcut(Account account) { - final Intent shortcutIntent = FolderList.actionHandleAccountIntent(this, account, null, true); + private void setupShortcut(BaseAccount account) { + Intent shortcutIntent = null; + + if (account instanceof SearchSpecification) { + shortcutIntent = MessageList.actionHandleAccountIntent( + this, account.getDescription(), (SearchSpecification) account); + } else { + shortcutIntent = FolderList.actionHandleAccountIntent(this, + (Account) account, null, true); + } Intent intent = new Intent(); intent.putExtra(Intent.EXTRA_SHORTCUT_INTENT, shortcutIntent); @@ -65,18 +100,18 @@ public class LauncherShortcuts extends K9ListActivity implements OnItemClickList } public void onItemClick(AdapterView parent, View view, int position, long id) { - Account account = (Account) parent.getItemAtPosition(position); + BaseAccount account = (BaseAccount) parent.getItemAtPosition(position); setupShortcut(account); } - class AccountsAdapter extends ArrayAdapter { - public AccountsAdapter(Account[] accounts) { + class AccountsAdapter extends ArrayAdapter { + public AccountsAdapter(List accounts) { super(LauncherShortcuts.this, 0, accounts); } @Override public View getView(int position, View convertView, ViewGroup parent) { - final Account account = getItem(position); + final BaseAccount account = getItem(position); final View view; if (convertView != null) { @@ -110,7 +145,10 @@ public class LauncherShortcuts extends K9ListActivity implements OnItemClickList holder.description.setText(description); - holder.chip.setBackgroundColor(account.getChipColor()); + if (account instanceof Account) { + holder.chip.setBackgroundColor(((Account) account).getChipColor()); + } + holder.chip.getBackground().setAlpha(255); holder.description.setTextSize(TypedValue.COMPLEX_UNIT_SP, mFontSizes.getAccountName()); diff --git a/src/com/fsck/k9/activity/MessageList.java b/src/com/fsck/k9/activity/MessageList.java index 89d1366ef..867fb53fe 100644 --- a/src/com/fsck/k9/activity/MessageList.java +++ b/src/com/fsck/k9/activity/MessageList.java @@ -52,10 +52,12 @@ import android.widget.Toast; import com.fsck.k9.Account; import com.fsck.k9.AccountStats; +import com.fsck.k9.BaseAccount; import com.fsck.k9.FontSizes; import com.fsck.k9.K9; import com.fsck.k9.Preferences; import com.fsck.k9.R; +import com.fsck.k9.SearchAccount; import com.fsck.k9.SearchSpecification; import com.fsck.k9.activity.setup.AccountSettings; import com.fsck.k9.activity.setup.FolderSettings; @@ -614,7 +616,11 @@ public class MessageList context.startActivity(intent); } - public static void actionHandle(Context context, String title, SearchSpecification searchSpecification) { + /** + * Creates and returns an intent that opens Unified Inbox or All Messages screen. + */ + public static Intent actionHandleAccountIntent(Context context, String title, + SearchSpecification searchSpecification) { Intent intent = new Intent(context, MessageList.class); intent.putExtra(EXTRA_QUERY, searchSpecification.getQuery()); if (searchSpecification.getRequiredFlags() != null) { @@ -630,6 +636,13 @@ public class MessageList intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); intent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP); + + return intent; + } + + public static void actionHandle(Context context, String title, + SearchSpecification searchSpecification) { + Intent intent = actionHandleAccountIntent(context, title, searchSpecification); context.startActivity(intent); } From e766f75da2f7cd9eddaa2eae1e3911a4afa14914 Mon Sep 17 00:00:00 2001 From: wilian-cb Date: Sun, 12 Feb 2012 13:29:16 -0200 Subject: [PATCH 29/93] Creating and initializing special accounts in Accounts activity to avoid NullPointerException when it's resumed. Constant added to represent the number of special accounts. --- src/com/fsck/k9/activity/Accounts.java | 36 +++++++++++++++++++------- 1 file changed, 26 insertions(+), 10 deletions(-) diff --git a/src/com/fsck/k9/activity/Accounts.java b/src/com/fsck/k9/activity/Accounts.java index 70206b9b9..d49ee0be0 100644 --- a/src/com/fsck/k9/activity/Accounts.java +++ b/src/com/fsck/k9/activity/Accounts.java @@ -106,6 +106,11 @@ public class Accounts extends K9ListActivity implements OnItemClickListener, OnC */ private static final Flag[] EMPTY_FLAG_ARRAY = new Flag[0]; + /** + * Number of special accounts ('Unified Inbox' and 'All Messages') + */ + private static final int SPECIAL_ACCOUNTS_COUNT = 2; + private static final int DIALOG_REMOVE_ACCOUNT = 1; private static final int DIALOG_CLEAR_ACCOUNT = 2; private static final int DIALOG_RECREATE_ACCOUNT = 3; @@ -325,13 +330,7 @@ public class Accounts extends K9ListActivity implements OnItemClickListener, OnC super.onCreate(icicle); if (!K9.isHideSpecialAccounts()) { - unreadAccount = new SearchAccount(this, false, null, null); - unreadAccount.setDescription(getString(R.string.search_all_messages_title)); - unreadAccount.setEmail(getString(R.string.search_all_messages_detail)); - - integratedInboxAccount = new SearchAccount(this, true, null, null); - integratedInboxAccount.setDescription(getString(R.string.integrated_inbox_title)); - integratedInboxAccount.setEmail(getString(R.string.integrated_inbox_detail)); + createSpecialAccounts(); } Account[] accounts = Preferences.getPreferences(this).getAccounts(); @@ -374,6 +373,19 @@ public class Accounts extends K9ListActivity implements OnItemClickListener, OnC } } + /** + * Creates and initializes the special accounts ('Integrated Inbox' and 'All Messages') + */ + private void createSpecialAccounts() { + unreadAccount = new SearchAccount(this, false, null, null); + unreadAccount.setDescription(getString(R.string.search_all_messages_title)); + unreadAccount.setEmail(getString(R.string.search_all_messages_detail)); + + integratedInboxAccount = new SearchAccount(this, true, null, null); + integratedInboxAccount.setDescription(getString(R.string.integrated_inbox_title)); + integratedInboxAccount.setEmail(getString(R.string.integrated_inbox_detail)); + } + @SuppressWarnings("unchecked") private void restoreAccountStats(Bundle icicle) { if (icicle != null) { @@ -458,9 +470,13 @@ public class Accounts extends K9ListActivity implements OnItemClickListener, OnC accounts = Preferences.getPreferences(this).getAccounts(); List newAccounts; - if (!K9.isHideSpecialAccounts() - && accounts.length > 0) { - newAccounts = new ArrayList(accounts.length + 2); + if (!K9.isHideSpecialAccounts() && accounts.length > 0) { + if (integratedInboxAccount == null || unreadAccount == null) { + createSpecialAccounts(); + } + + newAccounts = new ArrayList(accounts.length + + SPECIAL_ACCOUNTS_COUNT); newAccounts.add(integratedInboxAccount); newAccounts.add(unreadAccount); } else { From 53604be914486e72cf8740833ccea2273ee1566e Mon Sep 17 00:00:00 2001 From: cketti Date: Mon, 13 Feb 2012 00:56:34 +0100 Subject: [PATCH 30/93] Fixed NullPointerException --- src/com/fsck/k9/controller/MessagingController.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/com/fsck/k9/controller/MessagingController.java b/src/com/fsck/k9/controller/MessagingController.java index 56b853d21..bf766ee4e 100644 --- a/src/com/fsck/k9/controller/MessagingController.java +++ b/src/com/fsck/k9/controller/MessagingController.java @@ -2622,8 +2622,9 @@ public class MessagingController implements Runnable { localFolder.open(OpenMode.READ_WRITE); Message message = localFolder.getMessage(uid); - setFlag(account, folderName, new Message[] { message }, flag, newState); - + if (message != null) { + setFlag(account, folderName, new Message[] { message }, flag, newState); + } } catch (MessagingException me) { addErrorMessage(account, null, me); throw new RuntimeException(me); From 328701e87e935be49a31f96953d672b590db21bf Mon Sep 17 00:00:00 2001 From: cketti Date: Mon, 13 Feb 2012 00:57:06 +0100 Subject: [PATCH 31/93] Fixed JavaDoc --- src/com/fsck/k9/controller/MessagingController.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/com/fsck/k9/controller/MessagingController.java b/src/com/fsck/k9/controller/MessagingController.java index bf766ee4e..f4706581a 100644 --- a/src/com/fsck/k9/controller/MessagingController.java +++ b/src/com/fsck/k9/controller/MessagingController.java @@ -2606,8 +2606,8 @@ public class MessagingController implements Runnable { * The account the folder containing the message belongs to. * @param folderName * The name of the folder. - * @param message - * The message to change the flag for. + * @param uid + * The UID of the message to change the flag for. * @param flag * The flag to change. * @param newState From 29e1a682886d1a86f7cdc37dfb33ce4ef0c9ed42 Mon Sep 17 00:00:00 2001 From: cketti Date: Tue, 7 Feb 2012 01:00:16 +0100 Subject: [PATCH 32/93] Added widget to display the unread count for an account --- AndroidManifest.xml | 20 +++ res/drawable/rounded_corners.xml | 9 + res/drawable/unread_count_background.xml | 9 + res/layout/account_list.xml | 26 +++ res/layout/launcher_shortcuts.xml | 11 -- res/layout/unread_widget_layout.xml | 52 ++++++ res/values/strings.xml | 2 + res/xml/unread_widget_info.xml | 8 + src/com/fsck/k9/activity/AccountList.java | 163 ++++++++++++++++++ .../fsck/k9/activity/LauncherShortcuts.java | 2 +- .../activity/UnreadWidgetConfiguration.java | 93 ++++++++++ src/com/fsck/k9/mail/store/LocalStore.java | 7 + .../k9/provider/UnreadWidgetProvider.java | 123 +++++++++++++ 13 files changed, 513 insertions(+), 12 deletions(-) create mode 100644 res/drawable/rounded_corners.xml create mode 100644 res/drawable/unread_count_background.xml create mode 100644 res/layout/account_list.xml delete mode 100644 res/layout/launcher_shortcuts.xml create mode 100644 res/layout/unread_widget_layout.xml create mode 100644 res/xml/unread_widget_info.xml create mode 100644 src/com/fsck/k9/activity/AccountList.java create mode 100644 src/com/fsck/k9/activity/UnreadWidgetConfiguration.java create mode 100644 src/com/fsck/k9/provider/UnreadWidgetProvider.java diff --git a/AndroidManifest.xml b/AndroidManifest.xml index d0f09e32e..f530b902f 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -391,5 +391,25 @@ otherwise it would make K-9 start at the wrong time android:readPermission="com.fsck.k9.permission.READ_MESSAGES" android:writePermission="com.fsck.k9.permission.DELETE_MESSAGES" /> + + + + + + + + + + + + + + diff --git a/res/drawable/rounded_corners.xml b/res/drawable/rounded_corners.xml new file mode 100644 index 000000000..73f1772ef --- /dev/null +++ b/res/drawable/rounded_corners.xml @@ -0,0 +1,9 @@ + + + + + + + diff --git a/res/drawable/unread_count_background.xml b/res/drawable/unread_count_background.xml new file mode 100644 index 000000000..d8504bd43 --- /dev/null +++ b/res/drawable/unread_count_background.xml @@ -0,0 +1,9 @@ + + + + + + + diff --git a/res/layout/account_list.xml b/res/layout/account_list.xml new file mode 100644 index 000000000..468904618 --- /dev/null +++ b/res/layout/account_list.xml @@ -0,0 +1,26 @@ + + + + + + + + + + + diff --git a/res/layout/launcher_shortcuts.xml b/res/layout/launcher_shortcuts.xml deleted file mode 100644 index f05c883c1..000000000 --- a/res/layout/launcher_shortcuts.xml +++ /dev/null @@ -1,11 +0,0 @@ - - - - diff --git a/res/layout/unread_widget_layout.xml b/res/layout/unread_widget_layout.xml new file mode 100644 index 000000000..2b242b1bd --- /dev/null +++ b/res/layout/unread_widget_layout.xml @@ -0,0 +1,52 @@ + + + + + + + + + + + + + + diff --git a/res/values/strings.xml b/res/values/strings.xml index 9fd5973aa..79af72444 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -1126,4 +1126,6 @@ Welcome to K-9 Mail setup. K-9 is an open source mail client for Android origin Move down Moving account... + Unread count + Show unread count for… diff --git a/res/xml/unread_widget_info.xml b/res/xml/unread_widget_info.xml new file mode 100644 index 000000000..8e060337d --- /dev/null +++ b/res/xml/unread_widget_info.xml @@ -0,0 +1,8 @@ + + + diff --git a/src/com/fsck/k9/activity/AccountList.java b/src/com/fsck/k9/activity/AccountList.java new file mode 100644 index 000000000..3e8ac0109 --- /dev/null +++ b/src/com/fsck/k9/activity/AccountList.java @@ -0,0 +1,163 @@ +package com.fsck.k9.activity; + +import android.os.AsyncTask; +import android.os.Bundle; +import android.util.TypedValue; +import android.view.View; +import android.view.ViewGroup; +import android.widget.AdapterView; +import android.widget.AdapterView.OnItemClickListener; +import android.widget.ArrayAdapter; +import android.widget.ListView; +import android.widget.TextView; + +import com.fsck.k9.Account; +import com.fsck.k9.FontSizes; +import com.fsck.k9.K9; +import com.fsck.k9.Preferences; +import com.fsck.k9.R; + + +/** + * Activity displaying the list of accounts. + * + *

+ * Classes extending this abstract class have to provide an {@link #onAccountSelected(Account)} + * method to perform an action when an account is selected. + *

+ */ +public abstract class AccountList extends K9ListActivity implements OnItemClickListener { + private FontSizes mFontSizes = K9.getFontSizes(); + + @Override + public void onCreate(Bundle icicle) { + super.onCreate(icicle); + + setResult(RESULT_CANCELED); + + setContentView(R.layout.account_list); + + ListView listView = getListView(); + listView.setOnItemClickListener(this); + listView.setItemsCanFocus(false); + } + + /** + * Reload list of accounts when this activity is resumed. + */ + @Override + public void onResume() { + super.onResume(); + new LoadAccounts().execute(); + } + + public void onItemClick(AdapterView parent, View view, int position, long id) { + Account account = (Account) parent.getItemAtPosition(position); + onAccountSelected(account); + } + + /** + * Create a new {@link AccountsAdapter} instance and assign it to the {@link ListView}. + * + * @param accounts + * An array of accounts to display. + */ + public void populateListView(Account[] accounts) { + AccountsAdapter adapter = new AccountsAdapter(accounts); + ListView listView = getListView(); + listView.setAdapter(adapter); + listView.invalidate(); + } + + /** + * This method will be called when an account was selected. + * + * @param account + * The account the user selected. + */ + protected abstract void onAccountSelected(Account account); + + class AccountsAdapter extends ArrayAdapter { + private Account[] mAccounts; + + public AccountsAdapter(Account[] accounts) { + super(AccountList.this, 0, accounts); + mAccounts = accounts; + } + + public Account[] getAccounts() { + return mAccounts; + } + + @Override + public View getView(int position, View convertView, ViewGroup parent) { + final Account account = getItem(position); + + final View view; + if (convertView != null) { + view = convertView; + } else { + view = getLayoutInflater().inflate(R.layout.accounts_item, parent, false); + view.findViewById(R.id.active_icons).setVisibility(View.GONE); + view.findViewById(R.id.folders).setVisibility(View.GONE); + view.getBackground().setAlpha(0); + } + + AccountViewHolder holder = (AccountViewHolder) view.getTag(); + if (holder == null) { + holder = new AccountViewHolder(); + holder.description = (TextView) view.findViewById(R.id.description); + holder.email = (TextView) view.findViewById(R.id.email); + holder.chip = view.findViewById(R.id.chip); + + view.setTag(holder); + } + + String description = account.getDescription(); + if (account.getEmail().equals(description)) { + holder.email.setVisibility(View.GONE); + } else { + holder.email.setVisibility(View.VISIBLE); + holder.email.setText(account.getEmail()); + } + + if (description == null || description.length() == 0) { + description = account.getEmail(); + } + + holder.description.setText(description); + + holder.chip.setBackgroundColor(account.getChipColor()); + holder.chip.getBackground().setAlpha(255); + + holder.description.setTextSize(TypedValue.COMPLEX_UNIT_SP, + mFontSizes.getAccountName()); + holder.email.setTextSize(TypedValue.COMPLEX_UNIT_SP, + mFontSizes.getAccountDescription()); + + return view; + } + + class AccountViewHolder { + public TextView description; + public TextView email; + public View chip; + } + } + + /** + * Load accounts in a background thread + */ + class LoadAccounts extends AsyncTask { + @Override + protected Account[] doInBackground(Void... params) { + Account[] accounts = Preferences.getPreferences(getApplicationContext()).getAccounts(); + return accounts; + } + + @Override + protected void onPostExecute(Account[] accounts) { + populateListView(accounts); + } + } +} diff --git a/src/com/fsck/k9/activity/LauncherShortcuts.java b/src/com/fsck/k9/activity/LauncherShortcuts.java index 580d5e342..aaf2008da 100644 --- a/src/com/fsck/k9/activity/LauncherShortcuts.java +++ b/src/com/fsck/k9/activity/LauncherShortcuts.java @@ -40,7 +40,7 @@ public class LauncherShortcuts extends K9ListActivity implements OnItemClickList return; } - setContentView(R.layout.launcher_shortcuts); + setContentView(R.layout.account_list); ListView listView = getListView(); listView.setOnItemClickListener(this); listView.setItemsCanFocus(false); diff --git a/src/com/fsck/k9/activity/UnreadWidgetConfiguration.java b/src/com/fsck/k9/activity/UnreadWidgetConfiguration.java new file mode 100644 index 000000000..482a51949 --- /dev/null +++ b/src/com/fsck/k9/activity/UnreadWidgetConfiguration.java @@ -0,0 +1,93 @@ +package com.fsck.k9.activity; + +import android.appwidget.AppWidgetManager; +import android.content.Context; +import android.content.Intent; +import android.content.SharedPreferences; +import android.content.SharedPreferences.Editor; +import android.os.Bundle; + +import com.fsck.k9.Account; +import com.fsck.k9.R; +import com.fsck.k9.provider.UnreadWidgetProvider; + + +/** + * Activity to select an account for the unread widget. + */ +public class UnreadWidgetConfiguration extends AccountList { + /** + * Name of the preference file to store the widget configuration. + */ + private static final String PREFS_NAME = "unread_widget_configuration.xml"; + + /** + * Prefix for the preference keys. + */ + private static final String PREF_PREFIX_KEY = "unread_widget."; + + + /** + * The ID of the widget we are configuring. + */ + private int mAppWidgetId = AppWidgetManager.INVALID_APPWIDGET_ID; + + + @Override + public void onCreate(Bundle icicle) { + super.onCreate(icicle); + + // Find the widget ID from the intent. + Intent intent = getIntent(); + Bundle extras = intent.getExtras(); + if (extras != null) { + mAppWidgetId = extras.getInt(AppWidgetManager.EXTRA_APPWIDGET_ID, + AppWidgetManager.INVALID_APPWIDGET_ID); + } + + // If they gave us an intent without the widget ID, just bail. + if (mAppWidgetId == AppWidgetManager.INVALID_APPWIDGET_ID) { + finish(); + return; + } + + setTitle(R.string.unread_widget_select_account); + } + + @Override + protected void onAccountSelected(Account account) { + // Save widget configuration + String accountUuid = account.getUuid(); + saveAccountUuid(this, mAppWidgetId, accountUuid); + + // Update widget + Context context = getApplicationContext(); + AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(context); + UnreadWidgetProvider.updateWidget(context, appWidgetManager, mAppWidgetId, accountUuid); + + // Let the caller know that the configuration was successful + Intent resultValue = new Intent(); + resultValue.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, mAppWidgetId); + setResult(RESULT_OK, resultValue); + finish(); + } + + private static void saveAccountUuid(Context context, int appWidgetId, String accountUuid) { + SharedPreferences.Editor editor = + context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE).edit(); + editor.putString(PREF_PREFIX_KEY + appWidgetId, accountUuid); + editor.commit(); + } + + public static String getAccountUuid(Context context, int appWidgetId) { + SharedPreferences prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE); + String accountUuid = prefs.getString(PREF_PREFIX_KEY + appWidgetId, null); + return accountUuid; + } + + public static void deleteWidgetConfiguration(Context context, int appWidgetId) { + Editor editor = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE).edit(); + editor.remove(PREF_PREFIX_KEY + appWidgetId); + editor.commit(); + } +} diff --git a/src/com/fsck/k9/mail/store/LocalStore.java b/src/com/fsck/k9/mail/store/LocalStore.java index 049f4222e..ad8524090 100644 --- a/src/com/fsck/k9/mail/store/LocalStore.java +++ b/src/com/fsck/k9/mail/store/LocalStore.java @@ -60,6 +60,7 @@ import com.fsck.k9.mail.store.LockableDatabase.DbCallback; import com.fsck.k9.mail.store.LockableDatabase.WrappedException; import com.fsck.k9.mail.store.StorageManager.StorageProvider; import com.fsck.k9.provider.AttachmentProvider; +import com.fsck.k9.provider.UnreadWidgetProvider; /** *
@@ -499,6 +500,9 @@ public class LocalStore extends Store implements Serializable {
             public Void doDbWork(final SQLiteDatabase db) {
                 db.execSQL("DELETE FROM messages WHERE deleted = 0 and uid not like 'Local%'");
                 db.execSQL("update folders set flagged_count = 0, unread_count = 0");
+
+                // FIXME: hack to update unread count widget
+                UnreadWidgetProvider.updateUnreadCount(K9.app);
                 return null;
             }
         });
@@ -1311,6 +1315,9 @@ public class LocalStore extends Store implements Serializable {
         public void setUnreadMessageCount(final int unreadMessageCount) throws MessagingException {
             mUnreadMessageCount = Math.max(0, unreadMessageCount);
             updateFolderColumn("unread_count", mUnreadMessageCount);
+
+            // FIXME: hack to update unread count widget
+            UnreadWidgetProvider.updateUnreadCount(K9.app);
         }
 
         public void setFlaggedMessageCount(final int flaggedMessageCount) throws MessagingException {
diff --git a/src/com/fsck/k9/provider/UnreadWidgetProvider.java b/src/com/fsck/k9/provider/UnreadWidgetProvider.java
new file mode 100644
index 000000000..8913fb4e7
--- /dev/null
+++ b/src/com/fsck/k9/provider/UnreadWidgetProvider.java
@@ -0,0 +1,123 @@
+package com.fsck.k9.provider;
+
+import com.fsck.k9.Account;
+import com.fsck.k9.AccountStats;
+import com.fsck.k9.K9;
+import com.fsck.k9.Preferences;
+import com.fsck.k9.R;
+import com.fsck.k9.activity.UnreadWidgetConfiguration;
+import com.fsck.k9.activity.FolderList;
+import com.fsck.k9.activity.MessageList;
+
+import android.app.PendingIntent;
+import android.appwidget.AppWidgetManager;
+import android.appwidget.AppWidgetProvider;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.util.Log;
+import android.view.View;
+import android.widget.RemoteViews;
+
+public class UnreadWidgetProvider extends AppWidgetProvider {
+
+    /**
+     * Trigger update for all of our unread widgets.
+     *
+     * @param context
+     *         The {@code Context} object to use for the broadcast intent.
+     */
+    public static void updateUnreadCount(Context context) {
+        Context appContext = context.getApplicationContext();
+        AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(appContext);
+
+        ComponentName thisWidget = new ComponentName(appContext, UnreadWidgetProvider.class);
+        int[] widgetIds = appWidgetManager.getAppWidgetIds(thisWidget);
+
+        Intent intent = new Intent(context, UnreadWidgetProvider.class);
+        intent.setAction(AppWidgetManager.ACTION_APPWIDGET_UPDATE);
+        intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, widgetIds);
+
+        context.sendBroadcast(intent);
+    }
+
+    public static void updateWidget(Context context, AppWidgetManager appWidgetManager,
+            int appWidgetId, String accountUuid) {
+
+        RemoteViews remoteViews = new RemoteViews(context.getPackageName(),
+                R.layout.unread_widget_layout);
+
+        int unreadCount = 0;
+        String accountName = context.getString(R.string.app_name);
+        Intent clickIntent = null;
+        try {
+            Account account = Preferences.getPreferences(context).getAccount(accountUuid);
+            if (account != null) {
+                AccountStats stats = new AccountStats();
+                account.getLocalStore().getMessageCounts(stats);
+                unreadCount = stats.unreadMessageCount;
+                accountName = account.getDescription();
+                if (K9.FOLDER_NONE.equals(account.getAutoExpandFolderName())) {
+                    clickIntent = FolderList.actionHandleAccountIntent(context, account, null);
+                } else {
+                    clickIntent = MessageList.actionHandleFolderIntent(context, account,
+                            account.getAutoExpandFolderName());
+                }
+                clickIntent.addFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT);
+            }
+        } catch (Exception e) {
+            if (K9.DEBUG) {
+                Log.e(K9.LOG_TAG, "Error getting widget configuration", e);
+            }
+        }
+
+        if (unreadCount == 0) {
+            // Hide TextView for unread count if there are no unread messages.
+            remoteViews.setViewVisibility(R.id.unread_count, View.GONE);
+        } else {
+            remoteViews.setTextViewText(R.id.unread_count, String.valueOf(unreadCount));
+        }
+
+        remoteViews.setTextViewText(R.id.account_name, accountName);
+
+        if (clickIntent == null) {
+            // If the widget configuration couldn't be loaded we open the configuration
+            // activity when the user clicks the widget.
+            clickIntent = new Intent(context, UnreadWidgetConfiguration.class);
+            clickIntent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId);
+        }
+        clickIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+
+
+        PendingIntent pendingIntent = PendingIntent.getActivity(context, appWidgetId,
+                clickIntent, 0);
+
+        remoteViews.setOnClickPendingIntent(R.id.unread_widget_layout, pendingIntent);
+
+        appWidgetManager.updateAppWidget(appWidgetId, remoteViews);
+
+    }
+
+
+    /**
+     * Called when one or more widgets need to be updated.
+     */
+    @Override
+    public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) {
+        for (int widgetId : appWidgetIds) {
+            String accountUuid = UnreadWidgetConfiguration.getAccountUuid(context, widgetId);
+
+            updateWidget(context, appWidgetManager, widgetId, accountUuid);
+        }
+    }
+
+    /**
+     * Called when a widget instance is deleted.
+     */
+    @Override
+    public void onDeleted(Context context, int[] appWidgetIds) {
+        for (int appWidgetId : appWidgetIds) {
+            UnreadWidgetConfiguration.deleteWidgetConfiguration(context, appWidgetId);
+        }
+    }
+}

From e87f4cd98a064c2f669d0d01c7d065cb5c817142 Mon Sep 17 00:00:00 2001
From: cketti 
Date: Tue, 7 Feb 2012 15:18:21 +0100
Subject: [PATCH 33/93] Code cleanup. No functional changes.

---
 .../fsck/k9/controller/MessagingListener.java | 201 ++++++++----------
 1 file changed, 83 insertions(+), 118 deletions(-)

diff --git a/src/com/fsck/k9/controller/MessagingListener.java b/src/com/fsck/k9/controller/MessagingListener.java
index bdf35fa00..f9ae2c89c 100644
--- a/src/com/fsck/k9/controller/MessagingListener.java
+++ b/src/com/fsck/k9/controller/MessagingListener.java
@@ -12,191 +12,156 @@ import com.fsck.k9.mail.Part;
 import java.util.List;
 
 /**
- * Defines the interface that MessagingController will use to callback to requesters. This class
- * is defined as non-abstract so that someone who wants to receive only a few messages can
- * do so without implementing the entire interface. It is highly recommended that users of
- * this interface use the @Override annotation in their implementations to avoid being caught by
+ * Defines the interface that {@link MessagingController} will use to callback to requesters.
+ *
+ * 

+ * This class is defined as non-abstract so that someone who wants to receive only a few messages + * can do so without implementing the entire interface. It is highly recommended that users of this + * interface use the {@code @Override} annotation in their implementations to avoid being caught by * changes in this class. + *

*/ public class MessagingListener { public void searchStats(AccountStats stats) {} - public void accountStatusChanged(BaseAccount account, AccountStats stats) { - } - public void accountSizeChanged(Account account, long oldSize, long newSize) { - } + public void accountStatusChanged(BaseAccount account, AccountStats stats) {} - public void listFoldersStarted(Account account) { - } + public void accountSizeChanged(Account account, long oldSize, long newSize) {} - public void listFolders(Account account, Folder[] folders) { - } - public void listFoldersFailed(Account account, String message) { - } + public void listFoldersStarted(Account account) {} - public void listFoldersFinished(Account account) { - } + public void listFolders(Account account, Folder[] folders) {} - public void listLocalMessagesStarted(Account account, String folder) { - } + public void listFoldersFinished(Account account) {} - public void listLocalMessages(Account account, String folder, Message[] messages) { - } + public void listFoldersFailed(Account account, String message) {} - public void listLocalMessagesAddMessages(Account account, String folder, List messages) { - } - public void listLocalMessagesUpdateMessage(Account account, String folder, Message message) { - } + public void listLocalMessagesStarted(Account account, String folder) {} - public void listLocalMessagesRemoveMessage(Account account, String folder, Message message) { - } + public void listLocalMessages(Account account, String folder, Message[] messages) {} - public void listLocalMessagesFailed(Account account, String folder, String message) { - } + public void listLocalMessagesAddMessages(Account account, String folder, + List messages) {} - public void listLocalMessagesFinished(Account account, String folder) { - } + public void listLocalMessagesUpdateMessage(Account account, String folder, Message message) {} - public void synchronizeMailboxStarted(Account account, String folder) { - } + public void listLocalMessagesRemoveMessage(Account account, String folder, Message message) {} - public void synchronizeMailboxHeadersStarted(Account account, String folder) { - } + public void listLocalMessagesFinished(Account account, String folder) {} - public void synchronizeMailboxHeadersProgress(Account account, String folder, int completed, int total) { - } + public void listLocalMessagesFailed(Account account, String folder, String message) {} + + + public void synchronizeMailboxStarted(Account account, String folder) {} + + public void synchronizeMailboxHeadersStarted(Account account, String folder) {} + + public void synchronizeMailboxHeadersProgress(Account account, String folder, + int completed, int total) {} public void synchronizeMailboxHeadersFinished(Account account, String folder, - int totalMessagesInMailbox, int numNewMessages) { - } + int totalMessagesInMailbox, int numNewMessages) {} + public void synchronizeMailboxProgress(Account account, String folder, int completed, + int total) {} - public void synchronizeMailboxProgress(Account account, String folder, int completed, int total) - {} + public void synchronizeMailboxNewMessage(Account account, String folder, Message message) {} - public void synchronizeMailboxNewMessage(Account account, String folder, Message message) { - } + public void synchronizeMailboxAddOrUpdateMessage(Account account, String folder, + Message message) {} - public void synchronizeMailboxAddOrUpdateMessage(Account account, String folder, Message message) { - } - - public void synchronizeMailboxRemovedMessage(Account account, String folder, Message message) { - } + public void synchronizeMailboxRemovedMessage(Account account, String folder, + Message message) {} public void synchronizeMailboxFinished(Account account, String folder, - int totalMessagesInMailbox, int numNewMessages) { - } + int totalMessagesInMailbox, int numNewMessages) {} - public void synchronizeMailboxFailed(Account account, String folder, - String message) { - } + public void synchronizeMailboxFailed(Account account, String folder, String message) {} - public void loadMessageForViewStarted(Account account, String folder, String uid) { - } + + public void loadMessageForViewStarted(Account account, String folder, String uid) {} public void loadMessageForViewHeadersAvailable(Account account, String folder, String uid, - Message message) { - } + Message message) {} public void loadMessageForViewBodyAvailable(Account account, String folder, String uid, - Message message) { - } + Message message) {} public void loadMessageForViewFinished(Account account, String folder, String uid, - Message message) { - } + Message message) {} - public void loadMessageForViewFailed(Account account, String folder, String uid, Throwable t) { - } + public void loadMessageForViewFailed(Account account, String folder, String uid, + Throwable t) {} /** * Called when a message for view has been fully displayed on the screen. */ public void messageViewFinished() {} - public void checkMailStarted(Context context, Account account) { - } - public void checkMailFinished(Context context, Account account) { - } + public void checkMailStarted(Context context, Account account) {} - public void checkMailFailed(Context context, Account account, String reason) { - } + public void checkMailFinished(Context context, Account account) {} - public void sendPendingMessagesStarted(Account account) { - } + public void checkMailFailed(Context context, Account account, String reason) {} - public void sendPendingMessagesCompleted(Account account) { - } - public void sendPendingMessagesFailed(Account account) { - } + public void sendPendingMessagesStarted(Account account) {} - public void messageDeleted(Account account, String folder, Message message) { + public void sendPendingMessagesCompleted(Account account) {} - } - public void emptyTrashCompleted(Account account) { - } + public void sendPendingMessagesFailed(Account account) {} - public void folderStatusChanged(Account account, String folderName, int unreadMessageCount) { - } - public void folderStatusChanged(Account account, String folderName) { - } + public void emptyTrashCompleted(Account account) {} - public void systemStatusChanged() { - } - public void messageUidChanged(Account account, String folder, String oldUid, String newUid) { + public void folderStatusChanged(Account account, String folderName, int unreadMessageCount) {} - } + public void folderStatusChanged(Account account, String folderName) {} - public void setPushActive(Account account, String folderName, boolean enabled) { - } + public void systemStatusChanged() {} - public void loadAttachmentStarted( - Account account, - Message message, - Part part, - Object tag, - boolean requiresDownload) { - } - public void loadAttachmentFinished( - Account account, - Message message, - Part part, - Object tag) { - } + public void messageDeleted(Account account, String folder, Message message) {} - public void loadAttachmentFailed( - Account account, - Message message, - Part part, - Object tag, - String reason) { - } + public void messageUidChanged(Account account, String folder, String oldUid, String newUid) {} + + + public void setPushActive(Account account, String folderName, boolean enabled) {} + + + public void loadAttachmentStarted(Account account, Message message, Part part, Object tag, + boolean requiresDownload) {} + + public void loadAttachmentFinished(Account account, Message message, Part part, Object tag) {} + + public void loadAttachmentFailed(Account account, Message message, Part part, Object tag, + String reason) {} + + + + public void pendingCommandStarted(Account account, String commandTitle) {} public void pendingCommandsProcessing(Account account) {} - public void pendingCommandsFinished(Account account) {} - public void pendingCommandStarted(Account account, String commandTitle) - {} - public void pendingCommandCompleted(Account account, String commandTitle) - {} + public void pendingCommandCompleted(Account account, String commandTitle) {} + + public void pendingCommandsFinished(Account account) {} + /** * General notification messages subclasses can override to be notified that the controller * has completed a command. This is useful for turning off progress indicators that may have * been left over from previous commands. - * @param moreCommandsToRun True if the controller will continue on to another command - * immediately. + * + * @param moreCommandsToRun + * {@code true} if the controller will continue on to another command immediately. + * {@code false} otherwise. */ - public void controllerCommandCompleted(boolean moreCommandsToRun) { - - } + public void controllerCommandCompleted(boolean moreCommandsToRun) {} } From d4bc664c4111e102650ea9f0b79a242bc05353f7 Mon Sep 17 00:00:00 2001 From: cketti Date: Tue, 7 Feb 2012 15:26:41 +0100 Subject: [PATCH 34/93] Removed unused method --- src/com/fsck/k9/controller/MessagingListener.java | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/com/fsck/k9/controller/MessagingListener.java b/src/com/fsck/k9/controller/MessagingListener.java index f9ae2c89c..49ddbf4da 100644 --- a/src/com/fsck/k9/controller/MessagingListener.java +++ b/src/com/fsck/k9/controller/MessagingListener.java @@ -121,8 +121,6 @@ public class MessagingListener { public void folderStatusChanged(Account account, String folderName, int unreadMessageCount) {} - public void folderStatusChanged(Account account, String folderName) {} - public void systemStatusChanged() {} From 98461e5a21507c1968f3b5936a15d0337f2785be Mon Sep 17 00:00:00 2001 From: cketti Date: Tue, 7 Feb 2012 16:30:38 +0100 Subject: [PATCH 35/93] Notify listeners if unread count changed due to a copy/move operation --- .../k9/controller/MessagingController.java | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/src/com/fsck/k9/controller/MessagingController.java b/src/com/fsck/k9/controller/MessagingController.java index f4706581a..1d536f91f 100644 --- a/src/com/fsck/k9/controller/MessagingController.java +++ b/src/com/fsck/k9/controller/MessagingController.java @@ -3297,12 +3297,17 @@ public class MessagingController implements Runnable { Folder localSrcFolder = localStore.getFolder(srcFolder); Folder localDestFolder = localStore.getFolder(destFolder); + boolean unreadCountAffected = false; List uids = new LinkedList(); for (Message message : inMessages) { String uid = message.getUid(); if (!uid.startsWith(K9.LOCAL_UID_PREFIX)) { uids.add(uid); } + + if (!unreadCountAffected && !message.isSet(Flag.SEEN)) { + unreadCountAffected = true; + } } Message[] messages = localSrcFolder.getMessages(uids.toArray(EMPTY_STRING_ARRAY), null); @@ -3323,6 +3328,15 @@ public class MessagingController implements Runnable { fp.add(FetchProfile.Item.BODY); localSrcFolder.fetch(messages, fp, null); localSrcFolder.copyMessages(messages, localDestFolder); + + if (unreadCountAffected) { + // If this copy operation changes the unread count in the destination + // folder, notify the listeners. + int unreadMessageCount = localDestFolder.getUnreadMessageCount(); + for (MessagingListener l : getListeners()) { + l.folderStatusChanged(account, destFolder, unreadMessageCount); + } + } } else { localSrcFolder.moveMessages(messages, localDestFolder); for (Map.Entry entry : origUidMap.entrySet()) { @@ -3333,6 +3347,17 @@ public class MessagingController implements Runnable { } unsuppressMessage(account, srcFolder, origUid); } + + if (unreadCountAffected) { + // If this move operation changes the unread count, notify the listeners + // that the unread count changed in both the source and destination folder. + int unreadMessageCountSrc = localSrcFolder.getUnreadMessageCount(); + int unreadMessageCountDest = localDestFolder.getUnreadMessageCount(); + for (MessagingListener l : getListeners()) { + l.folderStatusChanged(account, srcFolder, unreadMessageCountSrc); + l.folderStatusChanged(account, destFolder, unreadMessageCountDest); + } + } } queueMoveOrCopy(account, srcFolder, destFolder, isCopy, origUidMap.keySet().toArray(EMPTY_STRING_ARRAY)); From 7a252bf002560fa9c00f79c962cc77d7021fca86 Mon Sep 17 00:00:00 2001 From: cketti Date: Tue, 7 Feb 2012 16:32:13 +0100 Subject: [PATCH 36/93] Changed method to update the unread widget --- src/com/fsck/k9/K9.java | 20 ++++++++++++++++++++ src/com/fsck/k9/mail/store/LocalStore.java | 7 ------- 2 files changed, 20 insertions(+), 7 deletions(-) diff --git a/src/com/fsck/k9/K9.java b/src/com/fsck/k9/K9.java index d48c79334..1da5b4d81 100644 --- a/src/com/fsck/k9/K9.java +++ b/src/com/fsck/k9/K9.java @@ -31,6 +31,7 @@ import com.fsck.k9.mail.Address; import com.fsck.k9.mail.Message; import com.fsck.k9.mail.MessagingException; import com.fsck.k9.mail.internet.BinaryTempFileBody; +import com.fsck.k9.provider.UnreadWidgetProvider; import com.fsck.k9.service.BootReceiver; import com.fsck.k9.service.MailService; import com.fsck.k9.service.ShutdownReceiver; @@ -504,19 +505,38 @@ public class K9 extends Application { } } + private void updateUnreadWidget() { + try { + UnreadWidgetProvider.updateUnreadCount(K9.this); + } catch (Exception e) { + if (K9.DEBUG) { + Log.e(LOG_TAG, "Error while updating unread widget(s)", e); + } + } + } + @Override public void synchronizeMailboxRemovedMessage(Account account, String folder, Message message) { broadcastIntent(K9.Intents.EmailReceived.ACTION_EMAIL_DELETED, account, folder, message); + updateUnreadWidget(); } @Override public void messageDeleted(Account account, String folder, Message message) { broadcastIntent(K9.Intents.EmailReceived.ACTION_EMAIL_DELETED, account, folder, message); + updateUnreadWidget(); } @Override public void synchronizeMailboxNewMessage(Account account, String folder, Message message) { broadcastIntent(K9.Intents.EmailReceived.ACTION_EMAIL_RECEIVED, account, folder, message); + updateUnreadWidget(); + } + + @Override + public void folderStatusChanged(Account account, String folderName, + int unreadMessageCount) { + updateUnreadWidget(); } @Override diff --git a/src/com/fsck/k9/mail/store/LocalStore.java b/src/com/fsck/k9/mail/store/LocalStore.java index ad8524090..049f4222e 100644 --- a/src/com/fsck/k9/mail/store/LocalStore.java +++ b/src/com/fsck/k9/mail/store/LocalStore.java @@ -60,7 +60,6 @@ import com.fsck.k9.mail.store.LockableDatabase.DbCallback; import com.fsck.k9.mail.store.LockableDatabase.WrappedException; import com.fsck.k9.mail.store.StorageManager.StorageProvider; import com.fsck.k9.provider.AttachmentProvider; -import com.fsck.k9.provider.UnreadWidgetProvider; /** *
@@ -500,9 +499,6 @@ public class LocalStore extends Store implements Serializable {
             public Void doDbWork(final SQLiteDatabase db) {
                 db.execSQL("DELETE FROM messages WHERE deleted = 0 and uid not like 'Local%'");
                 db.execSQL("update folders set flagged_count = 0, unread_count = 0");
-
-                // FIXME: hack to update unread count widget
-                UnreadWidgetProvider.updateUnreadCount(K9.app);
                 return null;
             }
         });
@@ -1315,9 +1311,6 @@ public class LocalStore extends Store implements Serializable {
         public void setUnreadMessageCount(final int unreadMessageCount) throws MessagingException {
             mUnreadMessageCount = Math.max(0, unreadMessageCount);
             updateFolderColumn("unread_count", mUnreadMessageCount);
-
-            // FIXME: hack to update unread count widget
-            UnreadWidgetProvider.updateUnreadCount(K9.app);
         }
 
         public void setFlaggedMessageCount(final int flaggedMessageCount) throws MessagingException {

From f36d2a6b231e013feb77d32d5732c0944785e3e7 Mon Sep 17 00:00:00 2001
From: cketti 
Date: Tue, 7 Feb 2012 19:38:46 +0100
Subject: [PATCH 37/93] Tweak widget layout

---
 res/layout/unread_widget_layout.xml | 18 ++++++++++--------
 res/xml/unread_widget_info.xml      |  4 ++--
 2 files changed, 12 insertions(+), 10 deletions(-)

diff --git a/res/layout/unread_widget_layout.xml b/res/layout/unread_widget_layout.xml
index 2b242b1bd..ce6684193 100644
--- a/res/layout/unread_widget_layout.xml
+++ b/res/layout/unread_widget_layout.xml
@@ -13,13 +13,14 @@
         android:layout_height="wrap_content">
 
         
 
         
+        android:textColor="#ffffff"
+        android:shadowColor="#000000"
+        android:shadowRadius="2.0"/>
 
 
diff --git a/res/xml/unread_widget_info.xml b/res/xml/unread_widget_info.xml
index 8e060337d..f2a7b27b0 100644
--- a/res/xml/unread_widget_info.xml
+++ b/res/xml/unread_widget_info.xml
@@ -1,8 +1,8 @@
 
 
 

From 64f4f7e4a3cd87e431d1beea85a22cb5962a2879 Mon Sep 17 00:00:00 2001
From: cketti 
Date: Tue, 7 Feb 2012 20:19:10 +0100
Subject: [PATCH 38/93] Use the new AccountList activity when creating launcher
 shortcuts

---
 src/com/fsck/k9/activity/AccountList.java     |  61 +++++---
 .../fsck/k9/activity/LauncherShortcuts.java   | 135 ++----------------
 .../activity/UnreadWidgetConfiguration.java   |  15 +-
 3 files changed, 71 insertions(+), 140 deletions(-)

diff --git a/src/com/fsck/k9/activity/AccountList.java b/src/com/fsck/k9/activity/AccountList.java
index 3e8ac0109..a229702ad 100644
--- a/src/com/fsck/k9/activity/AccountList.java
+++ b/src/com/fsck/k9/activity/AccountList.java
@@ -1,5 +1,9 @@
 package com.fsck.k9.activity;
 
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
 import android.os.AsyncTask;
 import android.os.Bundle;
 import android.util.TypedValue;
@@ -12,17 +16,19 @@ import android.widget.ListView;
 import android.widget.TextView;
 
 import com.fsck.k9.Account;
+import com.fsck.k9.BaseAccount;
 import com.fsck.k9.FontSizes;
 import com.fsck.k9.K9;
 import com.fsck.k9.Preferences;
 import com.fsck.k9.R;
+import com.fsck.k9.SearchAccount;
 
 
 /**
  * Activity displaying the list of accounts.
  *
  * 

- * Classes extending this abstract class have to provide an {@link #onAccountSelected(Account)} + * Classes extending this abstract class have to provide an {@link #onAccountSelected(BaseAccount)} * method to perform an action when an account is selected. *

*/ @@ -51,47 +57,64 @@ public abstract class AccountList extends K9ListActivity implements OnItemClickL new LoadAccounts().execute(); } + @Override public void onItemClick(AdapterView parent, View view, int position, long id) { - Account account = (Account) parent.getItemAtPosition(position); + BaseAccount account = (BaseAccount) parent.getItemAtPosition(position); onAccountSelected(account); } /** * Create a new {@link AccountsAdapter} instance and assign it to the {@link ListView}. * - * @param accounts + * @param realAccounts * An array of accounts to display. */ - public void populateListView(Account[] accounts) { + public void populateListView(Account[] realAccounts) { + List accounts = new ArrayList(); + + if (displaySpecialAccounts() && !K9.isHideSpecialAccounts()) { + BaseAccount integratedInboxAccount = new SearchAccount(this, true, null, null); + integratedInboxAccount.setDescription(getString(R.string.integrated_inbox_title)); + integratedInboxAccount.setEmail(getString(R.string.integrated_inbox_detail)); + + BaseAccount unreadAccount = new SearchAccount(this, false, null, null); + unreadAccount.setDescription(getString(R.string.search_all_messages_title)); + unreadAccount.setEmail(getString(R.string.search_all_messages_detail)); + + accounts.add(integratedInboxAccount); + accounts.add(unreadAccount); + } + + accounts.addAll(Arrays.asList(realAccounts)); AccountsAdapter adapter = new AccountsAdapter(accounts); ListView listView = getListView(); listView.setAdapter(adapter); listView.invalidate(); } + /** + * Implementing decide whether or not to display special accounts in the list. + * + * @return {@code true}, if special accounts should be listed. {@code false}, otherwise. + */ + protected abstract boolean displaySpecialAccounts(); + /** * This method will be called when an account was selected. * * @param account * The account the user selected. */ - protected abstract void onAccountSelected(Account account); + protected abstract void onAccountSelected(BaseAccount account); - class AccountsAdapter extends ArrayAdapter { - private Account[] mAccounts; - - public AccountsAdapter(Account[] accounts) { + class AccountsAdapter extends ArrayAdapter { + public AccountsAdapter(List accounts) { super(AccountList.this, 0, accounts); - mAccounts = accounts; - } - - public Account[] getAccounts() { - return mAccounts; } @Override public View getView(int position, View convertView, ViewGroup parent) { - final Account account = getItem(position); + final BaseAccount account = getItem(position); final View view; if (convertView != null) { @@ -127,7 +150,13 @@ public abstract class AccountList extends K9ListActivity implements OnItemClickL holder.description.setText(description); - holder.chip.setBackgroundColor(account.getChipColor()); + if (account instanceof Account) { + Account realAccount = (Account) account; + holder.chip.setBackgroundColor(realAccount.getChipColor()); + } else { + holder.chip.setBackgroundColor(0xff999999); + } + holder.chip.getBackground().setAlpha(255); holder.description.setTextSize(TypedValue.COMPLEX_UNIT_SP, diff --git a/src/com/fsck/k9/activity/LauncherShortcuts.java b/src/com/fsck/k9/activity/LauncherShortcuts.java index aaf2008da..61bde33ca 100644 --- a/src/com/fsck/k9/activity/LauncherShortcuts.java +++ b/src/com/fsck/k9/activity/LauncherShortcuts.java @@ -1,90 +1,44 @@ package com.fsck.k9.activity; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; - import android.content.Intent; import android.os.Bundle; import android.os.Parcelable; -import android.util.TypedValue; -import android.view.View; -import android.view.ViewGroup; -import android.widget.AdapterView; -import android.widget.AdapterView.OnItemClickListener; -import android.widget.ArrayAdapter; -import android.widget.ListView; -import android.widget.TextView; import com.fsck.k9.Account; import com.fsck.k9.BaseAccount; -import com.fsck.k9.FontSizes; -import com.fsck.k9.K9; -import com.fsck.k9.Preferences; import com.fsck.k9.R; -import com.fsck.k9.SearchAccount; import com.fsck.k9.SearchSpecification; -import com.fsck.k9.helper.Utility; - -public class LauncherShortcuts extends K9ListActivity implements OnItemClickListener { - private AccountsAdapter mAdapter; - private FontSizes mFontSizes = K9.getFontSizes(); +public class LauncherShortcuts extends AccountList { @Override public void onCreate(Bundle icicle) { - super.onCreate(icicle); - // finish() immediately if we aren't supposed to be here if (!Intent.ACTION_CREATE_SHORTCUT.equals(getIntent().getAction())) { finish(); return; } - setContentView(R.layout.account_list); - ListView listView = getListView(); - listView.setOnItemClickListener(this); - listView.setItemsCanFocus(false); - - refresh(); + super.onCreate(icicle); } - private void refresh() { - List accounts = new ArrayList(); - - if (!K9.isHideSpecialAccounts()) { - BaseAccount integratedInboxAccount = new SearchAccount(this, true, null, null); - integratedInboxAccount.setDescription( - getString(R.string.integrated_inbox_title)); - integratedInboxAccount.setEmail( - getString(R.string.integrated_inbox_detail)); - - BaseAccount unreadAccount = new SearchAccount(this, false, null, null); - unreadAccount.setDescription( - getString(R.string.search_all_messages_title)); - unreadAccount.setEmail( - getString(R.string.search_all_messages_detail)); - - accounts.add(integratedInboxAccount); - accounts.add(unreadAccount); - } - - accounts.addAll(Arrays.asList(Preferences.getPreferences(this).getAccounts())); - - mAdapter = new AccountsAdapter(accounts); - getListView().setAdapter(mAdapter); + @Override + protected boolean displaySpecialAccounts() { + return true; } - private void setupShortcut(BaseAccount account) { + @Override + protected void onAccountSelected(BaseAccount account) { Intent shortcutIntent = null; if (account instanceof SearchSpecification) { - shortcutIntent = MessageList.actionHandleAccountIntent( - this, account.getDescription(), (SearchSpecification) account); + shortcutIntent = MessageList.actionHandleAccountIntent(this, account.getDescription(), + (SearchSpecification) account); } else { - shortcutIntent = FolderList.actionHandleAccountIntent(this, - (Account) account, null, true); + shortcutIntent = FolderList.actionHandleAccountIntent(this, (Account) account, null, + true); } + shortcutIntent.addFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT); Intent intent = new Intent(); intent.putExtra(Intent.EXTRA_SHORTCUT_INTENT, shortcutIntent); String description = account.getDescription(); @@ -98,69 +52,4 @@ public class LauncherShortcuts extends K9ListActivity implements OnItemClickList setResult(RESULT_OK, intent); finish(); } - - public void onItemClick(AdapterView parent, View view, int position, long id) { - BaseAccount account = (BaseAccount) parent.getItemAtPosition(position); - setupShortcut(account); - } - - class AccountsAdapter extends ArrayAdapter { - public AccountsAdapter(List accounts) { - super(LauncherShortcuts.this, 0, accounts); - } - - @Override - public View getView(int position, View convertView, ViewGroup parent) { - final BaseAccount account = getItem(position); - - final View view; - if (convertView != null) { - view = convertView; - } else { - view = getLayoutInflater().inflate(R.layout.accounts_item, parent, false); - view.findViewById(R.id.active_icons).setVisibility(View.GONE); - } - - AccountViewHolder holder = (AccountViewHolder) view.getTag(); - if (holder == null) { - holder = new AccountViewHolder(); - holder.description = (TextView) view.findViewById(R.id.description); - holder.email = (TextView) view.findViewById(R.id.email); - holder.chip = view.findViewById(R.id.chip); - - view.setTag(holder); - } - - String description = account.getDescription(); - if (account.getEmail().equals(description)) { - holder.email.setVisibility(View.GONE); - } else { - holder.email.setVisibility(View.VISIBLE); - holder.email.setText(account.getEmail()); - } - - if (description == null || description.length() == 0) { - description = account.getEmail(); - } - - holder.description.setText(description); - - if (account instanceof Account) { - holder.chip.setBackgroundColor(((Account) account).getChipColor()); - } - - holder.chip.getBackground().setAlpha(255); - - holder.description.setTextSize(TypedValue.COMPLEX_UNIT_SP, mFontSizes.getAccountName()); - holder.email.setTextSize(TypedValue.COMPLEX_UNIT_SP, mFontSizes.getAccountDescription()); - - return view; - } - - class AccountViewHolder { - public TextView description; - public TextView email; - public View chip; - } - } } diff --git a/src/com/fsck/k9/activity/UnreadWidgetConfiguration.java b/src/com/fsck/k9/activity/UnreadWidgetConfiguration.java index 482a51949..341f895ef 100644 --- a/src/com/fsck/k9/activity/UnreadWidgetConfiguration.java +++ b/src/com/fsck/k9/activity/UnreadWidgetConfiguration.java @@ -8,6 +8,7 @@ import android.content.SharedPreferences.Editor; import android.os.Bundle; import com.fsck.k9.Account; +import com.fsck.k9.BaseAccount; import com.fsck.k9.R; import com.fsck.k9.provider.UnreadWidgetProvider; @@ -55,7 +56,19 @@ public class UnreadWidgetConfiguration extends AccountList { } @Override - protected void onAccountSelected(Account account) { + protected boolean displaySpecialAccounts() { + return false; + } + + @Override + protected void onAccountSelected(BaseAccount baseAccount) { + if (!(baseAccount instanceof Account)) { + finish(); + return; + } + + Account account = (Account) baseAccount; + // Save widget configuration String accountUuid = account.getUuid(); saveAccountUuid(this, mAppWidgetId, accountUuid); From 485a505ca0aad9497b07a017e381a72cb639dbd3 Mon Sep 17 00:00:00 2001 From: cketti Date: Thu, 9 Feb 2012 21:47:47 +0100 Subject: [PATCH 39/93] More tweaks to the unread widget --- res/drawable/unread_widget_background.xml | 15 +++++++++++++++ res/layout/unread_widget_layout.xml | 5 +++++ res/values/strings.xml | 2 +- .../fsck/k9/provider/UnreadWidgetProvider.java | 10 +++++++--- 4 files changed, 28 insertions(+), 4 deletions(-) create mode 100644 res/drawable/unread_widget_background.xml diff --git a/res/drawable/unread_widget_background.xml b/res/drawable/unread_widget_background.xml new file mode 100644 index 000000000..52e295f83 --- /dev/null +++ b/res/drawable/unread_widget_background.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + diff --git a/res/layout/unread_widget_layout.xml b/res/layout/unread_widget_layout.xml index ce6684193..4ad2c399e 100644 --- a/res/layout/unread_widget_layout.xml +++ b/res/layout/unread_widget_layout.xml @@ -6,6 +6,9 @@ android:layout_height="wrap_content" android:padding="2dp" android:orientation="vertical" + android:clickable="true" + android:focusable="true" + android:background="@drawable/unread_widget_background" android:gravity="bottom|center_horizontal"> Move down Moving account... - Unread count + K-9 Unread Show unread count for… diff --git a/src/com/fsck/k9/provider/UnreadWidgetProvider.java b/src/com/fsck/k9/provider/UnreadWidgetProvider.java index 8913fb4e7..616707c34 100644 --- a/src/com/fsck/k9/provider/UnreadWidgetProvider.java +++ b/src/com/fsck/k9/provider/UnreadWidgetProvider.java @@ -20,6 +20,7 @@ import android.view.View; import android.widget.RemoteViews; public class UnreadWidgetProvider extends AppWidgetProvider { + private static final int MAX_COUNT = 9999; /** * Trigger update for all of our unread widgets. @@ -71,11 +72,15 @@ public class UnreadWidgetProvider extends AppWidgetProvider { } } - if (unreadCount == 0) { + if (unreadCount <= 0) { // Hide TextView for unread count if there are no unread messages. remoteViews.setViewVisibility(R.id.unread_count, View.GONE); } else { - remoteViews.setTextViewText(R.id.unread_count, String.valueOf(unreadCount)); + remoteViews.setViewVisibility(R.id.unread_count, View.VISIBLE); + + String displayCount = (unreadCount <= MAX_COUNT) ? + String.valueOf(unreadCount) : String.valueOf(MAX_COUNT) + "+"; + remoteViews.setTextViewText(R.id.unread_count, displayCount); } remoteViews.setTextViewText(R.id.account_name, accountName); @@ -95,7 +100,6 @@ public class UnreadWidgetProvider extends AppWidgetProvider { remoteViews.setOnClickPendingIntent(R.id.unread_widget_layout, pendingIntent); appWidgetManager.updateAppWidget(appWidgetId, remoteViews); - } From cf9631d4810e57e5384d3af5542889ac46d3f815 Mon Sep 17 00:00:00 2001 From: cketti Date: Mon, 13 Feb 2012 23:11:59 +0100 Subject: [PATCH 40/93] Changed the way we decide what message parts to display --- .../k9/controller/MessagingController.java | 8 +- .../fsck/k9/mail/internet/MimeUtility.java | 890 +++++++++++++++++- src/com/fsck/k9/mail/store/LocalStore.java | 98 +- 3 files changed, 884 insertions(+), 112 deletions(-) diff --git a/src/com/fsck/k9/controller/MessagingController.java b/src/com/fsck/k9/controller/MessagingController.java index 1d536f91f..1b2b47a6b 100644 --- a/src/com/fsck/k9/controller/MessagingController.java +++ b/src/com/fsck/k9/controller/MessagingController.java @@ -1691,9 +1691,7 @@ public class MessagingController implements Runnable { * right now, attachments will be left for later. */ - ArrayList viewables = new ArrayList(); - ArrayList attachments = new ArrayList(); - MimeUtility.collectParts(message, viewables, attachments); + Set viewables = MimeUtility.collectTextParts(message); /* * Now download the parts we're interested in storing. @@ -2816,9 +2814,7 @@ public class MessagingController implements Runnable { try { LocalStore localStore = account.getLocalStore(); - ArrayList viewables = new ArrayList(); - ArrayList attachments = new ArrayList(); - MimeUtility.collectParts(message, viewables, attachments); + List attachments = MimeUtility.collectAttachments(message); for (Part attachment : attachments) { attachment.setBody(null); } diff --git a/src/com/fsck/k9/mail/internet/MimeUtility.java b/src/com/fsck/k9/mail/internet/MimeUtility.java index d8ad792b2..7d2755be9 100644 --- a/src/com/fsck/k9/mail/internet/MimeUtility.java +++ b/src/com/fsck/k9/mail/internet/MimeUtility.java @@ -1,9 +1,14 @@ package com.fsck.k9.mail.internet; +import android.content.Context; import android.util.Log; import com.fsck.k9.K9; +import com.fsck.k9.R; +import com.fsck.k9.helper.HtmlConverter; import com.fsck.k9.mail.*; +import com.fsck.k9.mail.Message.RecipientType; + import org.apache.commons.io.IOUtils; import org.apache.james.mime4j.codec.Base64InputStream; import org.apache.james.mime4j.codec.QuotedPrintableInputStream; @@ -12,7 +17,11 @@ import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.util.ArrayList; +import java.util.Date; +import java.util.HashSet; +import java.util.List; import java.util.Locale; +import java.util.Set; import java.util.regex.Pattern; import java.nio.charset.Charset; import java.nio.charset.IllegalCharsetNameException; @@ -23,6 +32,9 @@ public class MimeUtility { public static final String K9_SETTINGS_MIME_TYPE = "application/x-k9settings"; + private static final String TEXT_DIVIDER = + "------------------------------------------------------------------------"; + /* * http://www.w3schools.com/media/media_mimeref.asp * + @@ -1100,49 +1112,863 @@ public class MimeUtility { return tempBody; } + /** - * An unfortunately named method that makes decisions about a Part (usually a Message) - * as to which of it's children will be "viewable" and which will be attachments. - * The method recursively sorts the viewables and attachments into seperate - * lists for further processing. - * @param part - * @param viewables - * @param attachments - * @throws MessagingException + * Empty base class for the class hierarchy used by + * {@link MimeUtility#extractTextAndAttachments(Context, Message)}. + * + * @see Text + * @see Html + * @see MessageHeader + * @see Alternative */ - public static void collectParts(Part part, ArrayList viewables, - ArrayList attachments) throws MessagingException { - /* - * 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 it's pieces recursively. + static abstract class Viewable { /* empty */ } + + /** + * Class representing textual parts of a message that aren't marked as attachments. + * + * @see MimeUtility#isPartTextualBody(Part) + */ + static abstract class Textual extends Viewable { + private Part mPart; + + public Textual(Part part) { + mPart = part; + } + + public Part getPart() { + return mPart; + } + } + + /** + * Class representing a {@code text/plain} part of a message. + */ + static class Text extends Textual { + public Text(Part part) { + super(part); + } + } + + /** + * Class representing a {@code text/html} part of a message. + */ + static class Html extends Textual { + public Html(Part part) { + super(part); + } + } + + /** + * Class representing a {@code message/rfc822} part of a message. + * + *

+ * This is used to extract basic header information when the message contents are displayed + * inline. + *

+ */ + static class MessageHeader extends Viewable { + private Part mContainerPart; + private Message mMessage; + + public MessageHeader(Part containerPart, Message message) { + mContainerPart = containerPart; + mMessage = message; + } + + public Part getContainerPart() { + return mContainerPart; + } + + public Message getMessage() { + return mMessage; + } + } + + /** + * Class representing a {@code multipart/alternative} part of a message. + * + *

+ * Only relevant {@code text/plain} and {@code text/html} children are stored in this container + * class. + *

+ */ + static class Alternative extends Viewable { + private List mText; + private List mHtml; + + public Alternative(List text, List html) { + mText = text; + mHtml = html; + } + + public List getText() { + return mText; + } + + public List getHtml() { + return mHtml; + } + } + + /** + * Store viewable text of a message as plain text and HTML, and the parts considered + * attachments. + * + * @see MimeUtility#extractTextAndAttachments(Context, Message) + */ + public static class ViewableContainer { + /** + * The viewable text of the message in plain text. */ - if (part.getBody() instanceof Multipart) { - Multipart mp = (Multipart)part.getBody(); - for (int i = 0; i < mp.getCount(); i++) { - collectParts(mp.getBodyPart(i), viewables, attachments); + public final String text; + + /** + * The viewable text of the message in HTML. + */ + public final String html; + + /** + * The parts of the message considered attachments (everything not viewable). + */ + public final List attachments; + + ViewableContainer(String text, String html, List attachments) { + this.text = text; + this.html = html; + this.attachments = attachments; + } + } + + /** + * Collect attachment parts of a message. + * + * @param message + * The message to collect the attachment parts from. + * + * @return A list of parts regarded as attachments. + * + * @throws MessagingException + * In case of an error. + */ + public static List collectAttachments(Message message) + throws MessagingException { + try { + List attachments = new ArrayList(); + getViewables(message, attachments); + + return attachments; + } catch (Exception e) { + throw new MessagingException("Couldn't collect attachment parts", e); + } + } + + /** + * Collect the viewable textual parts of a message. + * + * @param message + * The message to extract the viewable parts from. + * + * @return A set of viewable parts of the message. + * + * @throws MessagingException + * In case of an error. + */ + public static Set collectTextParts(Message message) + throws MessagingException { + try { + List attachments = new ArrayList(); + + // Collect all viewable parts + List viewables = getViewables(message, attachments); + + // Extract the Part references + return getParts(viewables); + } catch (Exception e) { + throw new MessagingException("Couldn't extract viewable parts", e); + } + } + + /** + * Extract the viewable textual parts of a message and return the rest as attachments. + * + * @param context + * A {@link Context} instance that will be used to get localized strings. + * @param message + * The message to extract the text and attachments from. + * + * @return A {@link ViewableContainer} instance containing the textual parts of the message as + * plain text and HTML, and a list of message parts considered attachments. + * + * @throws MessagingException + * In case of an error. + */ + public static ViewableContainer extractTextAndAttachments(Context context, Message message) + throws MessagingException { + try { + List attachments = new ArrayList(); + + // Collect all viewable parts + List viewables = getViewables(message, attachments); + + /* + * Convert the tree of viewable parts into text and HTML + */ + + // Used to suppress the divider for the first viewable part + boolean hideDivider = true; + + StringBuilder text = new StringBuilder(); + StringBuilder html = new StringBuilder(); + for (Viewable viewable : viewables) { + if (viewable instanceof Textual) { + // This is either a text/plain or text/html part. Fill the variables 'text' and + // 'html', converting between plain text and HTML as necessary. + text.append(buildText(viewable, !hideDivider)); + html.append(buildHtml(viewable, !hideDivider)); + hideDivider = false; + } else if (viewable instanceof MessageHeader) { + MessageHeader header = (MessageHeader) viewable; + Part containerPart = header.getContainerPart(); + Message innerMessage = header.getMessage(); + + addTextDivider(text, containerPart, !hideDivider); + addMessageHeaderText(context, text, innerMessage); + + addHtmlDivider(html, containerPart, !hideDivider); + addMessageHeaderHtml(context, html, innerMessage); + + hideDivider = true; + } else if (viewable instanceof Alternative) { + // Handle multipart/alternative contents + Alternative alternative = (Alternative) viewable; + + /* + * We made sure at least one of text/plain or text/html is present when + * creating the Alternative object. If one part is not present we convert the + * other one to make sure 'text' and 'html' always contain the same text. + */ + List textAlternative = alternative.getText().isEmpty() ? + alternative.getHtml() : alternative.getText(); + List htmlAlternative = alternative.getHtml().isEmpty() ? + alternative.getText() : alternative.getHtml(); + + // Fill the 'text' variable + boolean divider = !hideDivider; + for (Viewable textViewable : textAlternative) { + text.append(buildText(textViewable, divider)); + divider = true; + } + + // Fill the 'html' variable + divider = !hideDivider; + for (Viewable htmlViewable : htmlAlternative) { + html.append(buildHtml(htmlViewable, divider)); + divider = true; + } + hideDivider = false; + } } + + return new ViewableContainer(text.toString(), html.toString(), attachments); + } catch (Exception e) { + throw new MessagingException("Couldn't extract viewable parts", e); } - /* - * If the part is an embedded message we just continue to process - * it, pulling any viewables or attachments into the running list. - */ - else if (part.getBody() instanceof Message) { - Message message = (Message)part.getBody(); - collectParts(message, viewables, attachments); - } - /* - * If the part is HTML and it got this far it's part of a mixed (et - * al) and should be rendered inline. - */ - else if (isPartTextualBody(part)) { - viewables.add(part); + } + + /** + * Traverse the MIME tree of a message an extract viewable parts. + * + * @param part + * The message part to start from. + * @param attachments + * A list that will receive the parts that are considered attachments. + * + * @return A list of {@link Viewable}s. + * + * @throws MessagingException + * In case of an error. + */ + public static List getViewables(Part part, List attachments) throws MessagingException { + List viewables = new ArrayList(); + + Body body = part.getBody(); + if (body instanceof Multipart) { + Multipart multipart = (Multipart) body; + if (part.getMimeType().equalsIgnoreCase("multipart/alternative")) { + /* + * For multipart/alternative parts we try to find a text/plain and a text/html + * child. Everything else we find is put into 'attachments'. + */ + List text = findTextPart(multipart, true); + + Set knownTextParts = getParts(text); + List html = findHtmlPart(multipart, knownTextParts, attachments, true); + + if (!text.isEmpty() || !html.isEmpty()) { + Alternative alternative = new Alternative(text, html); + viewables.add(alternative); + } + } else { + // For all other multipart parts we recurse to grab all viewable children. + int childCount = multipart.getCount(); + for (int i = 0; i < childCount; i++) { + Part bodyPart = multipart.getBodyPart(i); + viewables.addAll(getViewables(bodyPart, attachments)); + } + } + } else if (body instanceof Message && + !("attachment".equalsIgnoreCase(getContentDisposition(part)))) { + /* + * We only care about message/rfc822 parts whose Content-Disposition header has a value + * other than "attachment". + */ + Message message = (Message) body; + + // We add the Message object so we can extract the filename later. + viewables.add(new MessageHeader(part, message)); + + // Recurse to grab all viewable parts and attachments from that message. + viewables.addAll(getViewables(message, attachments)); + } else if (isPartTextualBody(part)) { + /* + * Save text/plain and text/html + */ + String mimeType = part.getMimeType(); + if (mimeType.equalsIgnoreCase("text/plain")) { + Text text = new Text(part); + viewables.add(text); + } else { + Html html = new Html(part); + viewables.add(html); + } } else { + // Everything else is treated as attachment. attachments.add(part); } + return viewables; } + /** + * Search the children of a {@link Multipart} for {@code text/plain} parts. + * + * @param multipart + * The {@code Multipart} to search through. + * @param directChild + * If {@code true}, this method will return after the first {@code text/plain} was + * found. + * + * @return A list of {@link Text} viewables. + * + * @throws MessagingException + * In case of an error. + */ + private static List findTextPart(Multipart multipart, boolean directChild) + throws MessagingException { + List viewables = new ArrayList(); + + int childCount = multipart.getCount(); + for (int i = 0; i < childCount; i++) { + Part part = multipart.getBodyPart(i); + Body body = part.getBody(); + if (body instanceof Multipart) { + Multipart innerMultipart = (Multipart) body; + + /* + * Recurse to find text parts. Since this is a multipart that is a child of a + * multipart/alternative we don't want to stop after the first text/plain part + * we find. This will allow to get all text parts for constructions like this: + * + * 1. multipart/alternative + * 1.1. multipart/mixed + * 1.1.1. text/plain + * 1.1.2. text/plain + * 1.2. text/html + */ + List textViewables = findTextPart(innerMultipart, false); + + if (!textViewables.isEmpty()) { + viewables.addAll(textViewables); + if (directChild) { + break; + } + } + } else if (isPartTextualBody(part) && part.getMimeType().equalsIgnoreCase("text/plain")) { + Text text = new Text(part); + viewables.add(text); + if (directChild) { + break; + } + } + } + + return viewables; + } + + /** + * Search the children of a {@link Multipart} for {@code text/html} parts. + * + *

+ * Every part that is not a {@code text/html} we want to display, we add to 'attachments'. + *

+ * + * @param multipart + * The {@code Multipart} to search through. + * @param knownTextParts + * A set of {@code text/plain} parts that shouldn't be added to 'attachments'. + * @param attachments + * A list that will receive the parts that are considered attachments. + * @param directChild + * If {@code true}, this method will add all {@code text/html} parts except the first + * found to 'attachments'. + * + * @return A list of {@link Text} viewables. + * + * @throws MessagingException + * In case of an error. + */ + private static List findHtmlPart(Multipart multipart, Set knownTextParts, + List attachments, boolean directChild) throws MessagingException { + List viewables = new ArrayList(); + + boolean partFound = false; + int childCount = multipart.getCount(); + for (int i = 0; i < childCount; i++) { + Part part = multipart.getBodyPart(i); + Body body = part.getBody(); + if (body instanceof Multipart) { + Multipart innerMultipart = (Multipart) body; + + if (directChild && partFound) { + // We already found our text/html part. Now we're only looking for attachments. + findAttachments(innerMultipart, knownTextParts, attachments); + } else { + /* + * Recurse to find HTML parts. Since this is a multipart that is a child of a + * multipart/alternative we don't want to stop after the first text/html part + * we find. This will allow to get all text parts for constructions like this: + * + * 1. multipart/alternative + * 1.1. text/plain + * 1.2. multipart/mixed + * 1.2.1. text/html + * 1.2.2. text/html + * 1.3. image/jpeg + */ + List htmlViewables = findHtmlPart(innerMultipart, knownTextParts, + attachments, false); + + if (!htmlViewables.isEmpty()) { + partFound = true; + viewables.addAll(htmlViewables); + } + } + } else if (!(directChild && partFound) && isPartTextualBody(part) && + part.getMimeType().equalsIgnoreCase("text/html")) { + Html html = new Html(part); + viewables.add(html); + partFound = true; + } else if (!knownTextParts.contains(part)) { + // Only add this part as attachment if it's not a viewable text/plain part found + // earlier. + attachments.add(part); + } + } + + return viewables; + } + + /** + * Build a set of message parts for fast lookups. + * + * @param viewables + * A list of {@link Viewable}s containing references to the message parts to include in + * the set. + * + * @return The set of viewable {@code Part}s. + * + * @see MimeUtility#findHtmlPart(Multipart, Set, List, boolean) + * @see MimeUtility#findAttachments(Multipart, Set, List) + */ + private static Set getParts(List viewables) { + Set parts = new HashSet(); + + for (Viewable viewable : viewables) { + if (viewable instanceof Textual) { + parts.add(((Textual) viewable).getPart()); + } else if (viewable instanceof Alternative) { + Alternative alternative = (Alternative) viewable; + parts.addAll(getParts(alternative.getText())); + parts.addAll(getParts(alternative.getHtml())); + } + } + + return parts; + } + + /** + * Traverse the MIME tree and add everything that's not a known text part to 'attachments'. + * + * @param multipart + * The {@link Multipart} to start from. + * @param knownTextParts + * A set of known text parts we don't want to end up in 'attachments'. + * @param attachments + * A list that will receive the parts that are considered attachments. + */ + private static void findAttachments(Multipart multipart, Set knownTextParts, + List attachments) { + int childCount = multipart.getCount(); + for (int i = 0; i < childCount; i++) { + Part part = multipart.getBodyPart(i); + Body body = part.getBody(); + if (body instanceof Multipart) { + Multipart innerMultipart = (Multipart) body; + findAttachments(innerMultipart, knownTextParts, attachments); + } else if (!knownTextParts.contains(part)) { + attachments.add(part); + } + } + } + + /** + * Extract important header values from a message to display inline (plain text version). + * + * @param context + * A {@link Context} instance that will be used to get localized strings. + * @param text + * The {@link StringBuilder} that will receive the (plain text) output. + * @param message + * The message to extract the header values from. + * + * @throws MessagingException + * In case of an error. + */ + private static void addMessageHeaderText(Context context, StringBuilder text, Message message) + throws MessagingException { + // From: + Address[] from = message.getFrom(); + if (from != null && from.length > 0) { + text.append(context.getString(R.string.message_compose_quote_header_from)); + text.append(' '); + text.append(Address.toString(from)); + text.append("\n"); + } + + // To: + Address[] to = message.getRecipients(RecipientType.TO); + if (to != null && to.length > 0) { + text.append(context.getString(R.string.message_compose_quote_header_to)); + text.append(' '); + text.append(Address.toString(to)); + text.append("\n"); + } + + // Cc: + Address[] cc = message.getRecipients(RecipientType.CC); + if (cc != null && cc.length > 0) { + text.append(context.getString(R.string.message_compose_quote_header_cc)); + text.append(' '); + text.append(Address.toString(cc)); + text.append("\n"); + } + + // Date: + Date date = message.getSentDate(); + if (date != null) { + text.append(context.getString(R.string.message_compose_quote_header_send_date)); + text.append(' '); + text.append(date.toString()); + text.append("\n"); + } + + // Subject: + String subject = message.getSubject(); + text.append(context.getString(R.string.message_compose_quote_header_subject)); + text.append(' '); + if (subject == null) { + text.append(context.getString(R.string.general_no_subject)); + } else { + text.append(subject); + } + text.append("\n\n"); + } + + /** + * Extract important header values from a message to display inline (HTML version). + * + * @param context + * A {@link Context} instance that will be used to get localized strings. + * @param html + * The {@link StringBuilder} that will receive the (HTML) output. + * @param message + * The message to extract the header values from. + * + * @throws MessagingException + * In case of an error. + */ + private static void addMessageHeaderHtml(Context context, StringBuilder html, Message message) + throws MessagingException { + + html.append(""); + + // From: + Address[] from = message.getFrom(); + if (from != null && from.length > 0) { + addTableRow(html, context.getString(R.string.message_compose_quote_header_from), + Address.toString(from)); + } + + // To: + Address[] to = message.getRecipients(RecipientType.TO); + if (to != null && to.length > 0) { + addTableRow(html, context.getString(R.string.message_compose_quote_header_to), + Address.toString(to)); + } + + // Cc: + Address[] cc = message.getRecipients(RecipientType.CC); + if (cc != null && cc.length > 0) { + addTableRow(html, context.getString(R.string.message_compose_quote_header_cc), + Address.toString(cc)); + } + + // Date: + Date date = message.getSentDate(); + if (date != null) { + addTableRow(html, context.getString(R.string.message_compose_quote_header_send_date), + date.toString()); + } + + // Subject: + String subject = message.getSubject(); + addTableRow(html, context.getString(R.string.message_compose_quote_header_subject), + (subject == null) ? context.getString(R.string.general_no_subject) : subject); + + html.append("
"); + } + + /** + * Output an HTML table two column row with some hardcoded style. + * + * @param html + * The {@link StringBuilder} that will receive the output. + * @param header + * The string to be put in the {@code TH} element. + * @param value + * The string to be put in the {@code TD} element. + */ + private static void addTableRow(StringBuilder html, String header, String value) { + html.append(""); + html.append(header); + html.append(""); + html.append(""); + html.append(value); + html.append(""); + } + + /** + * Use the contents of a {@link Viewable} to create the plain text to be displayed. + * + *

+ * This will use {@link HtmlConverter#htmlToText(String)} to convert HTML parts to plain text + * if necessary. + *

+ * + * @param viewable + * The viewable part to build the text from. + * @param prependDivider + * {@code true}, if the text divider should be inserted as first element. + * {@code false}, otherwise. + * + * @return The contents of the supplied viewable instance as plain text. + */ + private static StringBuilder buildText(Viewable viewable, boolean prependDivider) + { + StringBuilder text = new StringBuilder(); + if (viewable instanceof Textual) { + Part part = ((Textual)viewable).getPart(); + addTextDivider(text, part, prependDivider); + + String t = getTextFromPart(part); + if (t == null) { + t = ""; + } else if (viewable instanceof Html) { + t = HtmlConverter.htmlToText(t); + } + text.append(t); + } else if (viewable instanceof Alternative) { + // That's odd - an Alternative as child of an Alternative; go ahead and try to use the + // text/plain child; fall-back to the text/html part. + Alternative alternative = (Alternative) viewable; + + List textAlternative = alternative.getText().isEmpty() ? + alternative.getHtml() : alternative.getText(); + + boolean divider = prependDivider; + for (Viewable textViewable : textAlternative) { + text.append(buildText(textViewable, divider)); + divider = true; + } + } + + return text; + } + + /* + * Some constants that are used by addTextDivider() below. + */ + private static final int TEXT_DIVIDER_LENGTH = TEXT_DIVIDER.length(); + private static final String FILENAME_PREFIX = "----- "; + private static final int FILENAME_PREFIX_LENGTH = FILENAME_PREFIX.length(); + private static final String FILENAME_SUFFIX = " "; + private static final int FILENAME_SUFFIX_LENGTH = FILENAME_SUFFIX.length(); + + /** + * Add a plain text divider between two plain text message parts. + * + * @param text + * The {@link StringBuilder} to append the divider to. + * @param part + * The message part that will follow after the divider. This is used to extract the + * part's name. + * @param prependDivider + * {@code true}, if the divider should be appended. {@code false}, otherwise. + */ + private static void addTextDivider(StringBuilder text, Part part, boolean prependDivider) { + if (prependDivider) { + String filename = getPartName(part); + + text.append("\n\n"); + int len = filename.length(); + if (len > 0) { + if (len > TEXT_DIVIDER_LENGTH - FILENAME_PREFIX_LENGTH - FILENAME_SUFFIX_LENGTH) { + filename = filename.substring(0, TEXT_DIVIDER_LENGTH - FILENAME_PREFIX_LENGTH - + FILENAME_SUFFIX_LENGTH - 3) + "..."; + } + text.append(FILENAME_PREFIX); + text.append(filename); + text.append(TEXT_DIVIDER.substring(0, TEXT_DIVIDER_LENGTH - + FILENAME_PREFIX_LENGTH - filename.length() - FILENAME_SUFFIX_LENGTH)); + text.append(' '); + } else { + text.append(TEXT_DIVIDER); + } + text.append("\n\n"); + } + } + + /** + * Use the contents of a {@link Viewable} to create the HTML to be displayed. + * + *

+ * This will use {@link HtmlConverter#textToHtml(String)} to convert plain text parts to HTML + * if necessary. + *

+ * + * @param viewable + * The viewable part to build the HTML from. + * @param prependDivider + * {@code true}, if the HTML divider should be inserted as first element. + * {@code false}, otherwise. + * + * @return The contents of the supplied viewable instance as HTML. + */ + private static StringBuilder buildHtml(Viewable viewable, boolean prependDivider) + { + StringBuilder html = new StringBuilder(); + if (viewable instanceof Textual) { + Part part = ((Textual)viewable).getPart(); + addHtmlDivider(html, part, prependDivider); + + String t = getTextFromPart(part); + if (t == null) { + t = ""; + } else if (viewable instanceof Text) { + t = HtmlConverter.textToHtml(t); + } + html.append(t); + } else if (viewable instanceof Alternative) { + // That's odd - an Alternative as child of an Alternative; go ahead and try to use the + // text/html child; fall-back to the text/plain part. + Alternative alternative = (Alternative) viewable; + + List htmlAlternative = alternative.getHtml().isEmpty() ? + alternative.getText() : alternative.getHtml(); + + boolean divider = prependDivider; + for (Viewable htmlViewable : htmlAlternative) { + html.append(buildHtml(htmlViewable, divider)); + divider = true; + } + } + + return html; + } + + /** + * Add an HTML divider between two HTML message parts. + * + * @param html + * The {@link StringBuilder} to append the divider to. + * @param part + * The message part that will follow after the divider. This is used to extract the + * part's name. + * @param prependDivider + * {@code true}, if the divider should be appended. {@code false}, otherwise. + */ + private static void addHtmlDivider(StringBuilder html, Part part, boolean prependDivider) { + if (prependDivider) { + String filename = getPartName(part); + + html.append("

"); + html.append(filename); + html.append("

"); + } + } + + /** + * Get the name of the message part. + * + * @param part + * The part to get the name for. + * + * @return The (file)name of the part if available. An empty string, otherwise. + */ + private static String getPartName(Part part) { + try { + String disposition = part.getDisposition(); + if (disposition != null) { + String name = MimeUtility.getHeaderParameter(disposition, "filename"); + return (name == null) ? "" : name; + } + } + catch (MessagingException e) { /* ignore */ } + + return ""; + } + + /** + * Get the value of the {@code Content-Disposition} header. + * + * @param part + * The message part to read the header from. + * + * @return The value of the {@code Content-Disposition} header if available. {@code null}, + * otherwise. + */ + private static String getContentDisposition(Part part) { + try { + String disposition = part.getDisposition(); + if (disposition != null) { + return MimeUtility.getHeaderParameter(disposition, null); + } + } + catch (MessagingException e) { /* ignore */ } + + return null; + } public static Boolean isPartTextualBody(Part part) throws MessagingException { String disposition = part.getDisposition(); diff --git a/src/com/fsck/k9/mail/store/LocalStore.java b/src/com/fsck/k9/mail/store/LocalStore.java index 049f4222e..8f2fbb021 100644 --- a/src/com/fsck/k9/mail/store/LocalStore.java +++ b/src/com/fsck/k9/mail/store/LocalStore.java @@ -55,6 +55,7 @@ import com.fsck.k9.mail.internet.MimeHeader; import com.fsck.k9.mail.internet.MimeMessage; import com.fsck.k9.mail.internet.MimeMultipart; import com.fsck.k9.mail.internet.MimeUtility; +import com.fsck.k9.mail.internet.MimeUtility.ViewableContainer; import com.fsck.k9.mail.internet.TextBody; import com.fsck.k9.mail.store.LockableDatabase.DbCallback; import com.fsck.k9.mail.store.LockableDatabase.WrappedException; @@ -2099,45 +2100,14 @@ public class LocalStore extends Store implements Serializable { deleteAttachments(message.getUid()); } - ArrayList viewables = new ArrayList(); - ArrayList attachments = new ArrayList(); - MimeUtility.collectParts(message, viewables, attachments); + ViewableContainer container = + MimeUtility.extractTextAndAttachments(mApplication, message); - StringBuilder sbHtml = new StringBuilder(); - StringBuilder sbText = new StringBuilder(); - for (Part viewable : viewables) { - try { - String text = MimeUtility.getTextFromPart(viewable); + List attachments = container.attachments; + String text = container.text; + String html = container.html; - /* - * Small hack to make sure the string "null" doesn't end up - * in one of the StringBuilders. - */ - if (text == null) { - text = ""; - } - - /* - * Anything with MIME type text/html will be stored as such. Anything - * else will be stored as text/plain. - */ - if (viewable.getMimeType().equalsIgnoreCase("text/html")) { - sbHtml.append(text); - } else { - sbText.append(text); - } - } catch (Exception e) { - throw new MessagingException("Unable to get text for message part", e); - } - } - - String text = sbText.toString(); - String html = markupContent(text, sbHtml.toString()); String preview = calculateContentPreview(text); - // If we couldn't generate a reasonable preview from the text part, try doing it with the HTML part. - if (preview == null || preview.length() == 0) { - preview = calculateContentPreview(HtmlConverter.htmlToText(html)); - } try { ContentValues cv = new ContentValues(); @@ -2215,49 +2185,17 @@ public class LocalStore extends Store implements Serializable { @Override public Void doDbWork(final SQLiteDatabase db) throws WrappedException, UnavailableStorageException { try { - ArrayList viewables = new ArrayList(); - ArrayList attachments = new ArrayList(); - message.buildMimeRepresentation(); - MimeUtility.collectParts(message, viewables, attachments); + ViewableContainer container = + MimeUtility.extractTextAndAttachments(mApplication, message); - StringBuilder sbHtml = new StringBuilder(); - StringBuilder sbText = new StringBuilder(); - for (int i = 0, count = viewables.size(); i < count; i++) { - Part viewable = viewables.get(i); - try { - String text = MimeUtility.getTextFromPart(viewable); + List attachments = container.attachments; + String text = container.text; + String html = container.html; - /* - * Small hack to make sure the string "null" doesn't end up - * in one of the StringBuilders. - */ - if (text == null) { - text = ""; - } - - /* - * Anything with MIME type text/html will be stored as such. Anything - * else will be stored as text/plain. - */ - if (viewable.getMimeType().equalsIgnoreCase("text/html")) { - sbHtml.append(text); - } else { - sbText.append(text); - } - } catch (Exception e) { - throw new MessagingException("Unable to get text for message part", e); - } - } - - String text = sbText.toString(); - String html = markupContent(text, sbHtml.toString()); String preview = calculateContentPreview(text); - // If we couldn't generate a reasonable preview from the text part, try doing it with the HTML part. - if (preview == null || preview.length() == 0) { - preview = calculateContentPreview(HtmlConverter.htmlToText(html)); - } + try { db.execSQL("UPDATE messages SET " + "uid = ?, subject = ?, sender_list = ?, date = ?, flags = ?, " @@ -2391,6 +2329,18 @@ public class LocalStore extends Store implements Serializable { Body body = attachment.getBody(); if (body instanceof LocalAttachmentBody) { contentUri = ((LocalAttachmentBody) body).getContentUri(); + } else if (body instanceof Message) { + // It's a message, so use Message.writeTo() to output the + // message including all children. + Message message = (Message) body; + tempAttachmentFile = File.createTempFile("att", null, attachmentDirectory); + FileOutputStream out = new FileOutputStream(tempAttachmentFile); + try { + message.writeTo(out); + } finally { + out.close(); + } + size = (int) (tempAttachmentFile.length() & 0x7FFFFFFFL); } else { /* * If the attachment has a body we're expected to save it into the local store From dfd47702bcc634892f4c4f63012010ca0de9568b Mon Sep 17 00:00:00 2001 From: cketti Date: Tue, 14 Feb 2012 00:27:28 +0100 Subject: [PATCH 41/93] Fixed rare NullPointerException --- src/com/fsck/k9/EmailAddressAdapter.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/com/fsck/k9/EmailAddressAdapter.java b/src/com/fsck/k9/EmailAddressAdapter.java index d9bb59940..8cb0240d9 100644 --- a/src/com/fsck/k9/EmailAddressAdapter.java +++ b/src/com/fsck/k9/EmailAddressAdapter.java @@ -48,7 +48,7 @@ public class EmailAddressAdapter extends ResourceCursorAdapter { final String name = mContacts.getName(cursor); final String address = mContacts.getEmail(cursor); - return new Address(address, name).toString(); + return (address == null) ? "" : new Address(address, name).toString(); } @Override From 81b4fef4aac0978459da79d752a18d57ecf3264e Mon Sep 17 00:00:00 2001 From: cketti Date: Tue, 14 Feb 2012 00:49:34 +0100 Subject: [PATCH 42/93] Change protection level for READ_MESSAGES and DELETE_MESSAGES to "dangerous" --- AndroidManifest.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/AndroidManifest.xml b/AndroidManifest.xml index f530b902f..383ec77a3 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -43,13 +43,13 @@ From 4c318d172719905ec4b98d0e56273eecd0d1670b Mon Sep 17 00:00:00 2001 From: wilian-cb Date: Tue, 14 Feb 2012 21:11:15 -0200 Subject: [PATCH 43/93] 3945: Treatment for errors related to not having a file manager application in Android to handle import configurations. --- res/values/strings.xml | 6 +++++ src/com/fsck/k9/activity/Accounts.java | 34 +++++++++++++++++++++++++- 2 files changed, 39 insertions(+), 1 deletion(-) diff --git a/res/values/strings.xml b/res/values/strings.xml index 89e760667..79a9efa0e 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -1128,4 +1128,10 @@ Welcome to K-9 Mail setup. K-9 is an open source mail client for Android origin K-9 Unread Show unread count for… + + Missing File Manager Application + There is no suitable application to handle +the import operation. Please install a file manager application from Android Market + Open Market + Close diff --git a/src/com/fsck/k9/activity/Accounts.java b/src/com/fsck/k9/activity/Accounts.java index d49ee0be0..0aa7693c8 100644 --- a/src/com/fsck/k9/activity/Accounts.java +++ b/src/com/fsck/k9/activity/Accounts.java @@ -24,6 +24,7 @@ import android.content.DialogInterface; import android.content.Intent; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; import android.net.Uri; import android.os.Bundle; import android.os.Handler; @@ -106,6 +107,11 @@ public class Accounts extends K9ListActivity implements OnItemClickListener, OnC */ private static final Flag[] EMPTY_FLAG_ARRAY = new Flag[0]; + /** + * URL used to open Android Market application + */ + private static final String ANDROID_MARKET_URL = "http://market.android.com/"; + /** * Number of special accounts ('Unified Inbox' and 'All Messages') */ @@ -1256,7 +1262,33 @@ public class Accounts extends K9ListActivity implements OnItemClickListener, OnC Intent i = new Intent(Intent.ACTION_GET_CONTENT); i.addCategory(Intent.CATEGORY_OPENABLE); i.setType(MimeUtility.K9_SETTINGS_MIME_TYPE); - startActivityForResult(Intent.createChooser(i, null), ACTIVITY_REQUEST_PICK_SETTINGS_FILE); + + PackageManager packageManager = getPackageManager(); + List infos = packageManager.queryIntentActivities(i, 0); + + if (infos.size() > 0) { + startActivityForResult(Intent.createChooser(i, null), + ACTIVITY_REQUEST_PICK_SETTINGS_FILE); + } else { + DialogInterface.OnClickListener listener = new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int button) { + if (button == DialogInterface.BUTTON_POSITIVE) { + Uri uri = Uri.parse(ANDROID_MARKET_URL); + Intent intent = new Intent(Intent.ACTION_VIEW, uri); + startActivity(intent); + } else if (button == DialogInterface.BUTTON_NEGATIVE) { + dialog.dismiss(); + } + } + }; + + new AlertDialog.Builder(this) + .setTitle(getString(R.string.import_dialog_error_title)) + .setMessage(getString(R.string.import_dialog_error_message)) + .setPositiveButton(getString(R.string.open_market), listener) + .setNegativeButton(getString(R.string.close), listener) + .show(); + } } @Override From 15ffaf301eb626e3b2e69c0edf1fce51998835ec Mon Sep 17 00:00:00 2001 From: ashley willis Date: Tue, 14 Feb 2012 22:48:27 -0600 Subject: [PATCH 44/93] changed ANDROID_MARKET_URL to automatically search for OI File Manager. --- src/com/fsck/k9/activity/Accounts.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/com/fsck/k9/activity/Accounts.java b/src/com/fsck/k9/activity/Accounts.java index 0aa7693c8..9ac2a5926 100644 --- a/src/com/fsck/k9/activity/Accounts.java +++ b/src/com/fsck/k9/activity/Accounts.java @@ -110,7 +110,7 @@ public class Accounts extends K9ListActivity implements OnItemClickListener, OnC /** * URL used to open Android Market application */ - private static final String ANDROID_MARKET_URL = "http://market.android.com/"; + private static final String ANDROID_MARKET_URL = "https://market.android.com/search?q=oi+file+manager&c=apps"; /** * Number of special accounts ('Unified Inbox' and 'All Messages') From ddbd46666a3b79ad6a33fdc20148c7d090d18097 Mon Sep 17 00:00:00 2001 From: Jesse Vincent Date: Wed, 15 Feb 2012 13:46:33 -0500 Subject: [PATCH 45/93] Bumped manifest to 4.109 --- AndroidManifest.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/AndroidManifest.xml b/AndroidManifest.xml index 383ec77a3..cdfe9b30b 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -1,8 +1,8 @@ Date: Thu, 16 Feb 2012 14:43:38 +0100 Subject: [PATCH 46/93] Use the Androiod framework for the file manager error dialog This will automatically recreate the dialog on configuration changes (e.g. orientation change). --- src/com/fsck/k9/activity/Accounts.java | 38 ++++++++++++++------------ 1 file changed, 20 insertions(+), 18 deletions(-) diff --git a/src/com/fsck/k9/activity/Accounts.java b/src/com/fsck/k9/activity/Accounts.java index 9ac2a5926..031785e62 100644 --- a/src/com/fsck/k9/activity/Accounts.java +++ b/src/com/fsck/k9/activity/Accounts.java @@ -120,6 +120,8 @@ public class Accounts extends K9ListActivity implements OnItemClickListener, OnC private static final int DIALOG_REMOVE_ACCOUNT = 1; private static final int DIALOG_CLEAR_ACCOUNT = 2; private static final int DIALOG_RECREATE_ACCOUNT = 3; + private static final int DIALOG_NO_FILE_MANAGER = 4; + private ConcurrentHashMap accountStats = new ConcurrentHashMap(); private ConcurrentHashMap pendingWork = new ConcurrentHashMap(); @@ -986,6 +988,20 @@ public class Accounts extends K9ListActivity implements OnItemClickListener, OnC } } }); + case DIALOG_NO_FILE_MANAGER: + return ConfirmationDialog.create(this, id, + R.string.import_dialog_error_title, + getString(R.string.import_dialog_error_message), + R.string.open_market, + R.string.close, + new Runnable() { + @Override + public void run() { + Uri uri = Uri.parse(ANDROID_MARKET_URL); + Intent intent = new Intent(Intent.ACTION_VIEW, uri); + startActivity(intent); + } + }); } return super.onCreateDialog(id); } @@ -1007,6 +1023,9 @@ public class Accounts extends K9ListActivity implements OnItemClickListener, OnC alert.setMessage(getString(R.string.account_recreate_dlg_instructions_fmt, mSelectedContextAccount.getDescription())); break; + case DIALOG_NO_FILE_MANAGER: + alert.setMessage(getString(R.string.import_dialog_error_message)); + break; } super.onPrepareDialog(id, d); @@ -1270,24 +1289,7 @@ public class Accounts extends K9ListActivity implements OnItemClickListener, OnC startActivityForResult(Intent.createChooser(i, null), ACTIVITY_REQUEST_PICK_SETTINGS_FILE); } else { - DialogInterface.OnClickListener listener = new DialogInterface.OnClickListener() { - public void onClick(DialogInterface dialog, int button) { - if (button == DialogInterface.BUTTON_POSITIVE) { - Uri uri = Uri.parse(ANDROID_MARKET_URL); - Intent intent = new Intent(Intent.ACTION_VIEW, uri); - startActivity(intent); - } else if (button == DialogInterface.BUTTON_NEGATIVE) { - dialog.dismiss(); - } - } - }; - - new AlertDialog.Builder(this) - .setTitle(getString(R.string.import_dialog_error_title)) - .setMessage(getString(R.string.import_dialog_error_message)) - .setPositiveButton(getString(R.string.open_market), listener) - .setNegativeButton(getString(R.string.close), listener) - .show(); + showDialog(DIALOG_NO_FILE_MANAGER); } } From 53ae9d7fe77c5c1ab0c5e5bc1778f8bb061e71cc Mon Sep 17 00:00:00 2001 From: cketti Date: Thu, 16 Feb 2012 14:52:56 +0100 Subject: [PATCH 47/93] Avoid exception when a screen reader is installed but not active. --- src/com/fsck/k9/view/SingleMessageView.java | 22 ++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/src/com/fsck/k9/view/SingleMessageView.java b/src/com/fsck/k9/view/SingleMessageView.java index 48206f8ed..92699ee2c 100644 --- a/src/com/fsck/k9/view/SingleMessageView.java +++ b/src/com/fsck/k9/view/SingleMessageView.java @@ -102,15 +102,19 @@ public class SingleMessageView extends LinearLayout { // content://.providers.StatusProvider cursor = cr.query(Uri.parse("content://" + screenReader.serviceInfo.packageName + ".providers.StatusProvider"), null, null, null, null); - if (cursor != null) { - cursor.moveToFirst(); - // These content providers use a special cursor that only has - // one element, - // an integer that is 1 if the screen reader is running. - status = cursor.getInt(0); - cursor.close(); - if (status == 1) { - return true; + try { + if (cursor != null && cursor.moveToFirst()) { + // These content providers use a special cursor that only has + // one element, + // an integer that is 1 if the screen reader is running. + status = cursor.getInt(0); + if (status == 1) { + return true; + } + } + } finally { + if (cursor != null) { + cursor.close(); } } } From 396005974a269d03f66785f53aa91adbd4c2539d Mon Sep 17 00:00:00 2001 From: cketti Date: Thu, 16 Feb 2012 21:33:53 +0100 Subject: [PATCH 48/93] Cleaned up ImapStore.ImapFolder.appendMessages() --- src/com/fsck/k9/mail/store/ImapStore.java | 85 +++++++++++++---------- 1 file changed, 47 insertions(+), 38 deletions(-) diff --git a/src/com/fsck/k9/mail/store/ImapStore.java b/src/com/fsck/k9/mail/store/ImapStore.java index abd8cd2e2..461339d41 100644 --- a/src/com/fsck/k9/mail/store/ImapStore.java +++ b/src/com/fsck/k9/mail/store/ImapStore.java @@ -54,7 +54,6 @@ import android.content.Context; import android.net.ConnectivityManager; import android.net.NetworkInfo; import android.os.PowerManager; -import android.text.TextUtils; import android.util.Log; import com.beetstra.jutf7.CharsetProvider; @@ -62,6 +61,7 @@ import com.fsck.k9.Account; import com.fsck.k9.K9; import com.fsck.k9.R; import com.fsck.k9.controller.MessageRetrievalListener; +import com.fsck.k9.helper.StringUtils; import com.fsck.k9.helper.Utility; import com.fsck.k9.helper.power.TracingPowerManager; import com.fsck.k9.helper.power.TracingPowerManager.TracingWakeLock; @@ -1099,7 +1099,6 @@ public class ImapStore extends Store { do { response = mConnection.readResponse(); handleUntaggedResponse(response); - while (response.more()); } while (response.mTag == null); /* @@ -1953,21 +1952,30 @@ public class ImapStore extends Store { } /** - * Appends the given messages to the selected folder. This implementation also determines - * the new UID of the given message on the IMAP server and sets the Message's UID to the - * new server UID. + * Appends the given messages to the selected folder. + * + *

+ * This implementation also determines the new UIDs of the given messages on the IMAP + * server and changes the messages' UIDs to the new server UIDs. + *

+ * + * @param messages + * The messages to append to the folder. + * + * @return The mapping of original message UIDs to the new server UIDs. */ @Override public Map appendMessages(Message[] messages) throws MessagingException { checkOpen(); try { - Map uidMap = null; + Map uidMap = new HashMap(); for (Message message : messages) { mConnection.sendCommand( String.format("APPEND %s (%s) {%d}", encodeString(encodeFolderName(getPrefixedName())), combineFlags(message.getFlags()), message.calculateSize()), false); + ImapResponse response; do { response = mConnection.readResponse(); @@ -1981,53 +1989,54 @@ public class ImapStore extends Store { } } while (response.mTag == null); - /* - * If the server supports UIDPLUS, then along with the APPEND response it will return an APPENDUID response code. - * e.g. 11 OK [APPENDUID 2 238268] APPEND completed - * - * We can use the UID included in this response to update our records. - * - * Code imported from AOSP Email as of git rev a5c3744a247e432acf9f571a9dfb55321c4baa1a - */ - Object responseList = response.get(1); + if (response.size() > 1) { + /* + * If the server supports UIDPLUS, then along with the APPEND response it + * will return an APPENDUID response code, e.g. + * + * 11 OK [APPENDUID 2 238268] APPEND completed + * + * We can use the UID included in this response to update our records. + */ + Object responseList = response.get(1); - if (responseList instanceof ImapList) { - final ImapList appendList = (ImapList) responseList; - if ((appendList.size() >= 3) && appendList.getString(0).equals("APPENDUID")) { - String newUid = appendList.getString(2); + if (responseList instanceof ImapList) { + ImapList appendList = (ImapList) responseList; + if (appendList.size() >= 3 && + appendList.getString(0).equals("APPENDUID")) { - /* - * We need uidMap to be null initially to maintain consistency with the behavior of other similar methods (copyMessages, moveMessages) which - * return null if new UIDs are not available. Therefore, we initialize uidMap over here. - */ - if (uidMap == null) { - uidMap = new HashMap(); - } - uidMap.put(message.getUid(), newUid); - if (!TextUtils.isEmpty(newUid)) { - message.setUid(newUid); - continue; + String newUid = appendList.getString(2); + + if (!StringUtils.isNullOrEmpty(newUid)) { + message.setUid(newUid); + uidMap.put(message.getUid(), newUid); + continue; + } } } } - /* - * This part is executed in case the server does not support UIDPLUS or does not implement the APPENDUID response code. + * This part is executed in case the server does not support UIDPLUS or does + * not implement the APPENDUID response code. */ String newUid = getUidFromMessageId(message); - if (K9.DEBUG) + if (K9.DEBUG) { Log.d(K9.LOG_TAG, "Got UID " + newUid + " for message for " + getLogId()); + } - if (newUid != null) { - if (uidMap == null) { - uidMap = new HashMap(); - } + if (!StringUtils.isNullOrEmpty(newUid)) { uidMap.put(message.getUid(), newUid); message.setUid(newUid); } } - return uidMap; + + /* + * We need uidMap to be null if new UIDs are not available to maintain consistency + * with the behavior of other similar methods (copyMessages, moveMessages) which + * return null. + */ + return (uidMap.size() == 0) ? null : uidMap; } catch (IOException ioe) { throw ioExceptionHandler(mConnection, ioe); } From 3e4e6c72fd5180db967ddeaca5febe47954b7121 Mon Sep 17 00:00:00 2001 From: cketti Date: Thu, 16 Feb 2012 22:12:44 +0100 Subject: [PATCH 49/93] Cleaned up ImapStore.ImapFolder.copyMessages() --- src/com/fsck/k9/mail/store/ImapStore.java | 108 ++++++++++++++-------- 1 file changed, 68 insertions(+), 40 deletions(-) diff --git a/src/com/fsck/k9/mail/store/ImapStore.java b/src/com/fsck/k9/mail/store/ImapStore.java index 461339d41..32b07a6bd 100644 --- a/src/com/fsck/k9/mail/store/ImapStore.java +++ b/src/com/fsck/k9/mail/store/ImapStore.java @@ -1062,36 +1062,54 @@ public class ImapStore extends Store { } } + /** + * Copies the given messages to the specified folder. + * + *

+ * Note: + * Only the UIDs of the given {@link Message} instances are used. It is assumed that all + * UIDs represent valid messages in this folder. + *

+ * + * @param messages + * The messages to copy to the specfied folder. + * @param folder + * The name of the target folder. + * + * @return The mapping of original message UIDs to the new server UIDs. + */ @Override - public Map copyMessages(Message[] messages, Folder folder) throws MessagingException { + public Map copyMessages(Message[] messages, Folder folder) + throws MessagingException { if (!(folder instanceof ImapFolder)) { throw new MessagingException("ImapFolder.copyMessages passed non-ImapFolder"); } - if (messages.length == 0) + if (messages.length == 0) { return null; + } ImapFolder iFolder = (ImapFolder)folder; checkOpen(); String[] uids = new String[messages.length]; for (int i = 0, count = messages.length; i < count; i++) { - - // Not bothering to sort the UIDs in ascending order while sending the command for convenience, and because it does not make a difference. uids[i] = messages[i].getUid(); } + try { String remoteDestName = encodeString(encodeFolderName(iFolder.getPrefixedName())); if (!exists(remoteDestName)) { - /* - * If the remote trash folder doesn't exist we try to create it. - */ - if (K9.DEBUG) - Log.i(K9.LOG_TAG, "IMAPMessage.copyMessages: attempting to create remote '" + remoteDestName + "' folder for " + getLogId()); + // If the remote trash folder doesn't exist we try to create it. + if (K9.DEBUG) { + Log.i(K9.LOG_TAG, "Attempting to create remote folder '" + remoteDestName + + "' for " + getLogId()); + } iFolder.create(FolderType.HOLDS_MESSAGES); } + //TODO: Split this into multiple commands if the command exceeds a certain length. mConnection.sendCommand(String.format("UID COPY %s %s", Utility.combine(uids, ','), remoteDestName), false); @@ -1101,45 +1119,55 @@ public class ImapStore extends Store { handleUntaggedResponse(response); } while (response.mTag == null); - /* - * If the server supports UIDPLUS, then along with the COPY response it will return an COPYUID response code. - * e.g. 24 OK [COPYUID 38505 304,319:320 3956:3958] Success - * - * COPYUID is followed by UIDVALIDITY, set of UIDs of copied messages from the source folder and set of corresponding UIDs assigned to them in the destination folder. - * - * We can use the new UIDs included in this response to update our records. - */ - - Object responseList = response.get(1); - Map uidMap = null; + if (response.size() > 1) { + /* + * If the server supports UIDPLUS, then along with the COPY response it will + * return an COPYUID response code, e.g. + * + * 24 OK [COPYUID 38505 304,319:320 3956:3958] Success + * + * COPYUID is followed by UIDVALIDITY, the set of UIDs of copied messages from + * the source folder and the set of corresponding UIDs assigned to them in the + * destination folder. + * + * We can use the new UIDs included in this response to update our records. + */ + Object responseList = response.get(1); - if (responseList instanceof ImapList) { - final ImapList copyList = (ImapList) responseList; - if ((copyList.size() >= 4) && copyList.getString(0).equals("COPYUID")) { - List srcUids = parseSequenceSet(copyList.getString(2)); - List destUids = parseSequenceSet(copyList.getString(3)); - if (srcUids != null && destUids != null) { - if (srcUids.size() == destUids.size()) { - Iterator srcUidsIterator = srcUids.iterator(); - Iterator destUidsIterator = destUids.iterator(); - uidMap = new HashMap(); - while (srcUidsIterator.hasNext() && destUidsIterator.hasNext()) { - uidMap.put(srcUidsIterator.next(), destUidsIterator.next()); + if (responseList instanceof ImapList) { + final ImapList copyList = (ImapList) responseList; + if (copyList.size() >= 4 && copyList.getString(0).equals("COPYUID")) { + List srcUids = parseSequenceSet(copyList.getString(2)); + List destUids = parseSequenceSet(copyList.getString(3)); + + if (srcUids != null && destUids != null) { + if (srcUids.size() == destUids.size()) { + Iterator srcUidsIterator = srcUids.iterator(); + Iterator destUidsIterator = destUids.iterator(); + uidMap = new HashMap(); + while (srcUidsIterator.hasNext() && + destUidsIterator.hasNext()) { + String srcUid = srcUidsIterator.next(); + String destUid = destUidsIterator.next(); + uidMap.put(srcUid, destUid); + } + } else { + if (K9.DEBUG) { + Log.v(K9.LOG_TAG, "Parse error: size of source UIDs " + + "list is not the same as size of destination " + + "UIDs list."); + } } } else { - if(K9.DEBUG) - Log.v(K9.LOG_TAG, "Parse error: size of source UIDs list is not the same as size of destination UIDs list."); + if (K9.DEBUG) { + Log.v(K9.LOG_TAG, "Parsing of the sequence set failed."); + } } - } else { - if(K9.DEBUG) - Log.v(K9.LOG_TAG, "Parsing of the sequence set failed."); } } - } else { - if(K9.DEBUG) - Log.v(K9.LOG_TAG, "Expected COPYUID response was not found."); } + return uidMap; } catch (IOException ioe) { throw ioExceptionHandler(mConnection, ioe); From 89e0ed3c03a6fbab38ffa4c7b45c4fdd4a63de1b Mon Sep 17 00:00:00 2001 From: cketti Date: Thu, 16 Feb 2012 22:34:50 +0100 Subject: [PATCH 50/93] Notify listeners of UID changes after remote copy/move --- src/com/fsck/k9/controller/MessagingController.java | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/com/fsck/k9/controller/MessagingController.java b/src/com/fsck/k9/controller/MessagingController.java index 61ae0f1b4..6673df75f 100644 --- a/src/com/fsck/k9/controller/MessagingController.java +++ b/src/com/fsck/k9/controller/MessagingController.java @@ -2227,10 +2227,14 @@ public class MessagingController implements Runnable { while (remoteSrcUidsIterator.hasNext()) { String remoteSrcUid = remoteSrcUidsIterator.next(); String localDestUid = localUidMap.get(remoteSrcUid); + String newUid = remoteUidMap.get(remoteSrcUid); Message localDestMessage = localDestFolder.getMessage(localDestUid); - localDestMessage.setUid(remoteUidMap.get(remoteSrcUid)); + localDestMessage.setUid(newUid); localDestFolder.changeUid((LocalMessage)localDestMessage); + for (MessagingListener l : getListeners()) { + l.messageUidChanged(account, destFolder, localDestUid, newUid); + } } } } finally { From 4ed77c7a29a508240e926854505953391bb17dfb Mon Sep 17 00:00:00 2001 From: cketti Date: Thu, 16 Feb 2012 22:36:57 +0100 Subject: [PATCH 51/93] Avoid NullPointerException --- src/com/fsck/k9/controller/MessagingController.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/com/fsck/k9/controller/MessagingController.java b/src/com/fsck/k9/controller/MessagingController.java index 6673df75f..06fb59370 100644 --- a/src/com/fsck/k9/controller/MessagingController.java +++ b/src/com/fsck/k9/controller/MessagingController.java @@ -2189,7 +2189,7 @@ public class MessagingController implements Runnable { Log.d(K9.LOG_TAG, "processingPendingMoveOrCopy: source folder = " + srcFolder + ", " + messages.size() + " messages, destination folder = " + destFolder + ", isCopy = " + isCopy); - Map remoteUidMap = new HashMap(); + Map remoteUidMap = null; if (!isCopy && destFolder.equals(account.getTrashFolderName())) { if (K9.DEBUG) @@ -2220,7 +2220,7 @@ public class MessagingController implements Runnable { * This next part is used to bring the local UIDs of the local destination folder * upto speed with the remote UIDs of remote destionation folder. */ - if (!localUidMap.isEmpty() && !remoteUidMap.isEmpty()) { + if (!localUidMap.isEmpty() && remoteUidMap != null && !remoteUidMap.isEmpty()) { Set remoteSrcUids = remoteUidMap.keySet(); Iterator remoteSrcUidsIterator = remoteSrcUids.iterator(); From 023c60513eeb308f280c836b3334c0959d5de4b3 Mon Sep 17 00:00:00 2001 From: cketti Date: Thu, 16 Feb 2012 23:37:44 +0100 Subject: [PATCH 52/93] Fixed UID mapping return value of LocalStore.moveMessages() --- src/com/fsck/k9/mail/store/LocalStore.java | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/com/fsck/k9/mail/store/LocalStore.java b/src/com/fsck/k9/mail/store/LocalStore.java index ca8980ab0..c36350dfa 100644 --- a/src/com/fsck/k9/mail/store/LocalStore.java +++ b/src/com/fsck/k9/mail/store/LocalStore.java @@ -1963,7 +1963,10 @@ public class LocalStore extends Store implements Serializable { Log.d(K9.LOG_TAG, "Updating folder_id to " + lDestFolder.getId() + " for message with UID " + message.getUid() + ", id " + lMessage.getId() + " currently in folder " + getName()); - message.setUid(K9.LOCAL_UID_PREFIX + UUID.randomUUID().toString()); + String newUid = K9.LOCAL_UID_PREFIX + UUID.randomUUID().toString(); + message.setUid(newUid); + + uidMap.put(oldUID, newUid); db.execSQL("UPDATE messages " + "SET folder_id = ?, uid = ? " + "WHERE id = ?", new Object[] { lDestFolder.getId(), @@ -1971,10 +1974,15 @@ public class LocalStore extends Store implements Serializable { lMessage.getId() }); + /* + * Add a placeholder message so we won't download the original + * message again if we synchronize before the remote move is + * complete. + */ LocalMessage placeHolder = new LocalMessage(oldUID, LocalFolder.this); placeHolder.setFlagInternal(Flag.DELETED, true); placeHolder.setFlagInternal(Flag.SEEN, true); - uidMap.putAll(appendMessages(new Message[] { placeHolder })); + appendMessages(new Message[] { placeHolder }); } } catch (MessagingException e) { throw new WrappedException(e); From c3885bc475f95dc655d3adbc706749c3d78d0add Mon Sep 17 00:00:00 2001 From: cketti Date: Thu, 16 Feb 2012 23:52:44 +0100 Subject: [PATCH 53/93] Cleaned up LocalFolder.appendMessages() a bit --- src/com/fsck/k9/mail/store/LocalStore.java | 23 ++++++++++++++++------ 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/src/com/fsck/k9/mail/store/LocalStore.java b/src/com/fsck/k9/mail/store/LocalStore.java index c36350dfa..bf797ebfe 100644 --- a/src/com/fsck/k9/mail/store/LocalStore.java +++ b/src/com/fsck/k9/mail/store/LocalStore.java @@ -2090,16 +2090,27 @@ public class LocalStore extends Store implements Serializable { long oldMessageId = -1; String uid = message.getUid(); - if (uid == null && !copy) { - uid = K9.LOCAL_UID_PREFIX + UUID.randomUUID().toString(); - message.setUid(uid); - } else if (copy) { - String randomLocalUid = K9.LOCAL_UID_PREFIX + UUID.randomUUID().toString(); - if (uid != null) { + if (uid == null || copy) { + /* + * Create a new message in the database + */ + String randomLocalUid = K9.LOCAL_UID_PREFIX + + UUID.randomUUID().toString(); + + if (copy) { + // Save mapping: source UID -> target UID uidMap.put(uid, randomLocalUid); + } else { + // Modify the Message instance to reference the new UID + message.setUid(randomLocalUid); } + + // The message will be saved with the newly generated UID uid = randomLocalUid; } else { + /* + * Replace an existing message in the database + */ LocalMessage oldMessage = (LocalMessage) getMessage(uid); if (oldMessage != null) { From f7299a69d44efbc36d864a0096d1f741d2a797d6 Mon Sep 17 00:00:00 2001 From: cketti Date: Fri, 17 Feb 2012 00:43:35 +0100 Subject: [PATCH 54/93] Keep track of UIDs when moving messages to the trash folder --- src/com/fsck/k9/controller/MessagingController.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/com/fsck/k9/controller/MessagingController.java b/src/com/fsck/k9/controller/MessagingController.java index 06fb59370..cc09d8e31 100644 --- a/src/com/fsck/k9/controller/MessagingController.java +++ b/src/com/fsck/k9/controller/MessagingController.java @@ -3515,6 +3515,7 @@ public class MessagingController implements Runnable { } Store localStore = account.getLocalStore(); localFolder = localStore.getFolder(folder); + Map uidMap = null; if (folder.equals(account.getTrashFolderName()) || K9.FOLDER_NONE.equals(account.getTrashFolderName())) { if (K9.DEBUG) Log.d(K9.LOG_TAG, "Deleting messages in trash folder or trash set to -None-, not copying"); @@ -3529,7 +3530,7 @@ public class MessagingController implements Runnable { if (K9.DEBUG) Log.d(K9.LOG_TAG, "Deleting messages in normal folder, moving"); - localFolder.moveMessages(messages, localTrashFolder); + uidMap = localFolder.moveMessages(messages, localTrashFolder); } } @@ -3562,7 +3563,7 @@ public class MessagingController implements Runnable { if (folder.equals(account.getTrashFolderName())) { queueSetFlag(account, folder, Boolean.toString(true), Flag.DELETED.toString(), uids); } else { - queueMoveOrCopy(account, folder, account.getTrashFolderName(), false, uids); + queueMoveOrCopy(account, folder, account.getTrashFolderName(), false, uids, uidMap); } processPendingCommands(account); } else if (account.getDeletePolicy() == Account.DELETE_POLICY_MARK_AS_READ) { From 4db048343f153b7bb946f9af4e41022b38467041 Mon Sep 17 00:00:00 2001 From: ashley willis Date: Thu, 16 Feb 2012 18:35:25 -0600 Subject: [PATCH 55/93] account setup changes: added gmx.com. set delete policy to DELETE_POLICY_ON_DELETE for IMAP on automatic setup like manual setup does. set default names for Archive and Spam on manual config like automatic setup does. --- res/xml/providers.xml | 4 ++++ .../fsck/k9/activity/setup/AccountSetupBasics.java | 12 ++++++++++++ 2 files changed, 16 insertions(+) diff --git a/res/xml/providers.xml b/res/xml/providers.xml index 62a7f9c75..a00108996 100644 --- a/res/xml/providers.xml +++ b/res/xml/providers.xml @@ -174,6 +174,10 @@ + + + + diff --git a/src/com/fsck/k9/activity/setup/AccountSetupBasics.java b/src/com/fsck/k9/activity/setup/AccountSetupBasics.java index 27a3781dd..dea801ed1 100644 --- a/src/com/fsck/k9/activity/setup/AccountSetupBasics.java +++ b/src/com/fsck/k9/activity/setup/AccountSetupBasics.java @@ -232,6 +232,11 @@ public class AccountSetupBasics extends K9Activity mAccount.setSpamFolderName(getString(R.string.special_mailbox_name_spam)); } mAccount.setSentFolderName(getString(R.string.special_mailbox_name_sent)); + if (incomingUri.toString().startsWith("imap")) { + mAccount.setDeletePolicy(Account.DELETE_POLICY_ON_DELETE); + } else if (incomingUri.toString().startsWith("pop3")) { + mAccount.setDeletePolicy(Account.DELETE_POLICY_NEVER); + } AccountSetupCheckSettings.actionCheckSettings(this, mAccount, true, true); } catch (UnsupportedEncodingException enc) { // This really shouldn't happen since the encoding is hardcoded to UTF-8 @@ -310,6 +315,13 @@ public class AccountSetupBasics extends K9Activity mAccount.setDraftsFolderName(getString(R.string.special_mailbox_name_drafts)); mAccount.setTrashFolderName(getString(R.string.special_mailbox_name_trash)); mAccount.setSentFolderName(getString(R.string.special_mailbox_name_sent)); + mAccount.setArchiveFolderName(getString(R.string.special_mailbox_name_archive)); + // Yahoo! has a special folder for Spam, called "Bulk Mail". + if (domain.endsWith(".yahoo.com")) { + mAccount.setSpamFolderName("Bulk Mail"); + } else { + mAccount.setSpamFolderName(getString(R.string.special_mailbox_name_spam)); + } AccountSetupAccountType.actionSelectAccountType(this, mAccount, mDefaultView.isChecked()); finish(); From c1ed0c78a9c8c08178565ccfa781cb9762884ad0 Mon Sep 17 00:00:00 2001 From: cketti Date: Fri, 17 Feb 2012 01:59:04 +0100 Subject: [PATCH 56/93] Introduced new pending command to retain upgradability --- .../k9/controller/MessagingController.java | 40 ++++++++++++++++++- 1 file changed, 38 insertions(+), 2 deletions(-) diff --git a/src/com/fsck/k9/controller/MessagingController.java b/src/com/fsck/k9/controller/MessagingController.java index cc09d8e31..008b9afe0 100644 --- a/src/com/fsck/k9/controller/MessagingController.java +++ b/src/com/fsck/k9/controller/MessagingController.java @@ -117,6 +117,7 @@ public class MessagingController implements Runnable { private static final String PENDING_COMMAND_MOVE_OR_COPY = "com.fsck.k9.MessagingController.moveOrCopy"; private static final String PENDING_COMMAND_MOVE_OR_COPY_BULK = "com.fsck.k9.MessagingController.moveOrCopyBulk"; + private static final String PENDING_COMMAND_MOVE_OR_COPY_BULK_NEW = "com.fsck.k9.MessagingController.moveOrCopyBulkNew"; private static final String PENDING_COMMAND_EMPTY_TRASH = "com.fsck.k9.MessagingController.emptyTrash"; private static final String PENDING_COMMAND_SET_FLAG_BULK = "com.fsck.k9.MessagingController.setFlagBulk"; private static final String PENDING_COMMAND_SET_FLAG = "com.fsck.k9.MessagingController.setFlag"; @@ -1902,6 +1903,8 @@ public class MessagingController implements Runnable { } else if (PENDING_COMMAND_MARK_ALL_AS_READ.equals(command.command)) { processPendingMarkAllAsRead(command, account); } else if (PENDING_COMMAND_MOVE_OR_COPY_BULK.equals(command.command)) { + processPendingMoveOrCopyOld2(command, account); + } else if (PENDING_COMMAND_MOVE_OR_COPY_BULK_NEW.equals(command.command)) { processPendingMoveOrCopy(command, account); } else if (PENDING_COMMAND_MOVE_OR_COPY.equals(command.command)) { processPendingMoveOrCopyOld(command, account); @@ -2081,7 +2084,7 @@ public class MessagingController implements Runnable { return; } PendingCommand command = new PendingCommand(); - command.command = PENDING_COMMAND_MOVE_OR_COPY_BULK; + command.command = PENDING_COMMAND_MOVE_OR_COPY_BULK_NEW; int length = 3 + uids.length; command.arguments = new String[length]; @@ -2101,7 +2104,7 @@ public class MessagingController implements Runnable { return; } PendingCommand command = new PendingCommand(); - command.command = PENDING_COMMAND_MOVE_OR_COPY_BULK; + command.command = PENDING_COMMAND_MOVE_OR_COPY_BULK_NEW; int length = 4 + uidMap.keySet().size() + uidMap.values().size(); command.arguments = new String[length]; @@ -2114,6 +2117,39 @@ public class MessagingController implements Runnable { queuePendingCommand(account, command); } } + + /** + * Convert pending command to new format and call + * {@link #processPendingMoveOrCopy(PendingCommand, Account)}. + * + *

+ * TODO: This method is obsolete and is only for transition from K-9 4.0 to K-9 4.2 + * Eventually, it should be removed. + *

+ * + * @param command + * Pending move/copy command in old format. + * @param account + * The account the pending command belongs to. + * + * @throws MessagingException + * In case of an error. + */ + private void processPendingMoveOrCopyOld2(PendingCommand command, Account account) + throws MessagingException { + PendingCommand newCommand = new PendingCommand(); + int len = command.arguments.length; + newCommand.command = PENDING_COMMAND_MOVE_OR_COPY_BULK_NEW; + newCommand.arguments = new String[len + 1]; + newCommand.arguments[0] = command.arguments[0]; + newCommand.arguments[1] = command.arguments[1]; + newCommand.arguments[2] = command.arguments[2]; + newCommand.arguments[3] = Boolean.toString(false); + System.arraycopy(command.arguments, 3, newCommand.arguments, 4, len - 3); + + processPendingMoveOrCopy(newCommand, account); + } + /** * Process a pending trash message command. * From deb01bcd16184eeaeac63b9f22c5b91ad6600682 Mon Sep 17 00:00:00 2001 From: cketti Date: Fri, 17 Feb 2012 14:59:50 +0100 Subject: [PATCH 57/93] Imported ImapUtility.java from AOSP Email Modified it to fit our needs. --- .../fsck/k9/mail/store/imap/ImapUtility.java | 113 ++++++++++++++++++ 1 file changed, 113 insertions(+) create mode 100644 src/com/fsck/k9/mail/store/imap/ImapUtility.java diff --git a/src/com/fsck/k9/mail/store/imap/ImapUtility.java b/src/com/fsck/k9/mail/store/imap/ImapUtility.java new file mode 100644 index 000000000..d0ac2b31a --- /dev/null +++ b/src/com/fsck/k9/mail/store/imap/ImapUtility.java @@ -0,0 +1,113 @@ +/* + * Copyright (C) 2012 The K-9 Dog Walkers + * 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.fsck.k9.mail.store.imap; + +import android.util.Log; + +import com.fsck.k9.K9; + +import java.util.ArrayList; +import java.util.List; + +/** + * Utility methods for use with IMAP. + */ +public class ImapUtility { + /** + * Gets all of the values in a sequence set per RFC 3501. + * + *

+ * Any ranges are expanded into a list of individual numbers. + *

+ * + *
+     * sequence-number = nz-number / "*"
+     * sequence-range  = sequence-number ":" sequence-number
+     * sequence-set    = (sequence-number / sequence-range) *("," sequence-set)
+     * 
+ * + * @param set + * The sequence set string as received by the server. + * + * @return The list of IDs as strings in this sequence set. If the set is invalid, an empty + * list is returned. + */ + public static List getImapSequenceValues(String set) { + ArrayList list = new ArrayList(); + if (set != null) { + String[] setItems = set.split(","); + for (String item : setItems) { + if (item.indexOf(':') == -1) { + // simple item + try { + Integer.parseInt(item); // Don't need the value; just ensure it's valid + list.add(item); + } catch (NumberFormatException e) { + Log.d(K9.LOG_TAG, "Invalid UID value", e); + } + } else { + // range + list.addAll(getImapRangeValues(item)); + } + } + } + + return list; + } + + /** + * Expand the given number range into a list of individual numbers. + * + *
+     * sequence-number = nz-number / "*"
+     * sequence-range  = sequence-number ":" sequence-number
+     * sequence-set    = (sequence-number / sequence-range) *("," sequence-set)
+     * 
+ * + * @param range + * The range string as received by the server. + * + * @return The list of IDs as strings in this range. If the range is not valid, an empty list + * is returned. + */ + public static List getImapRangeValues(String range) { + ArrayList list = new ArrayList(); + try { + if (range != null) { + int colonPos = range.indexOf(':'); + if (colonPos > 0) { + int first = Integer.parseInt(range.substring(0, colonPos)); + int second = Integer.parseInt(range.substring(colonPos + 1)); + if (first < second) { + for (int i = first; i <= second; i++) { + list.add(Integer.toString(i)); + } + } else { + for (int i = first; i >= second; i--) { + list.add(Integer.toString(i)); + } + } + } + } + } catch (NumberFormatException e) { + Log.d(K9.LOG_TAG, "Invalid range value", e); + } + + return list; + } +} From 5083b8f1e866a6783f5afe074d13398d493513db Mon Sep 17 00:00:00 2001 From: cketti Date: Fri, 17 Feb 2012 15:01:30 +0100 Subject: [PATCH 58/93] Use newly imported AOSP code instead of ImapStore.parseSequenceSet() --- src/com/fsck/k9/mail/store/ImapStore.java | 63 ++--------------------- 1 file changed, 5 insertions(+), 58 deletions(-) diff --git a/src/com/fsck/k9/mail/store/ImapStore.java b/src/com/fsck/k9/mail/store/ImapStore.java index 32b07a6bd..b0ecd2adc 100644 --- a/src/com/fsck/k9/mail/store/ImapStore.java +++ b/src/com/fsck/k9/mail/store/ImapStore.java @@ -90,6 +90,7 @@ import com.fsck.k9.mail.internet.MimeMultipart; import com.fsck.k9.mail.internet.MimeUtility; import com.fsck.k9.mail.store.ImapResponseParser.ImapList; import com.fsck.k9.mail.store.ImapResponseParser.ImapResponse; +import com.fsck.k9.mail.store.imap.ImapUtility; import com.fsck.k9.mail.transport.imap.ImapSettings; import com.jcraft.jzlib.JZlib; import com.jcraft.jzlib.ZOutputStream; @@ -1138,8 +1139,10 @@ public class ImapStore extends Store { if (responseList instanceof ImapList) { final ImapList copyList = (ImapList) responseList; if (copyList.size() >= 4 && copyList.getString(0).equals("COPYUID")) { - List srcUids = parseSequenceSet(copyList.getString(2)); - List destUids = parseSequenceSet(copyList.getString(3)); + List srcUids = ImapUtility.getImapSequenceValues( + copyList.getString(2)); + List destUids = ImapUtility.getImapSequenceValues( + copyList.getString(3)); if (srcUids != null && destUids != null) { if (srcUids.size() == destUids.size()) { @@ -1174,62 +1177,6 @@ public class ImapStore extends Store { } } - /** - * Can be used to parse sequence sets or UID sets appearing is responses such as COPYUID. - * e.g. [COPYUID 38505 304,319:320 3956:3958] - * - * @param set - * @return List sequenceSet - */ - private List parseSequenceSet(String set) { - if (set == null) { - return null; - } - int index = 0; - List sequenceList = new ArrayList(); - String element = ""; - - while (index < set.length()) { - if (set.charAt(index) == ':') { - String upperBound = ""; - index++; - while (index < set.length()) { - if (!(set.charAt(index) == ',')) { - upperBound += set.charAt(index); - index++; - } else { - break; - } - } - - int lower = Integer.parseInt(element); - int upper = Integer.parseInt(upperBound); - - if (lower < upper) { - for (int i = lower; i <= upper; i++) { - sequenceList.add(String.valueOf(i)); - } - } else { - for (int i = upper; i <= lower; i++) { - sequenceList.add(String.valueOf(i)); - } - } - - element = ""; - } else if (set.charAt(index) == ',') { - sequenceList.add(element); - element = ""; - } else { - element += set.charAt(index); - } - index++; - } - if (!element.equals("")) { - sequenceList.add(element); - } - return sequenceList; - } - @Override public Map moveMessages(Message[] messages, Folder folder) throws MessagingException { if (messages.length == 0) From 33029a1f17149a218d6bb03ce8a4629eb5c4e0b7 Mon Sep 17 00:00:00 2001 From: cketti Date: Fri, 17 Feb 2012 15:34:10 +0100 Subject: [PATCH 59/93] Imported unit tests for ImapUtility from AOSP Email --- .../k9/mail/store/imap/ImapUtilityTest.java | 125 ++++++++++++++++++ 1 file changed, 125 insertions(+) create mode 100644 tests/src/com/fsck/k9/mail/store/imap/ImapUtilityTest.java diff --git a/tests/src/com/fsck/k9/mail/store/imap/ImapUtilityTest.java b/tests/src/com/fsck/k9/mail/store/imap/ImapUtilityTest.java new file mode 100644 index 000000000..496d6922e --- /dev/null +++ b/tests/src/com/fsck/k9/mail/store/imap/ImapUtilityTest.java @@ -0,0 +1,125 @@ +/* + * Copyright (C) 2012 The K-9 Dog Walkers + * 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.fsck.k9.mail.store.imap; + +import java.util.List; +import android.test.MoreAsserts; +import junit.framework.TestCase; + +public class ImapUtilityTest extends TestCase { + /** + * Test getting elements of an IMAP sequence set. + */ + public void testGetImapSequenceValues() { + String[] expected; + List actual; + + // Test valid sets + expected = new String[] {"1"}; + actual = ImapUtility.getImapSequenceValues("1"); + MoreAsserts.assertEquals(expected, actual.toArray()); + + expected = new String[] {"1", "3", "2"}; + actual = ImapUtility.getImapSequenceValues("1,3,2"); + MoreAsserts.assertEquals(expected, actual.toArray()); + + expected = new String[] {"4", "5", "6"}; + actual = ImapUtility.getImapSequenceValues("4:6"); + MoreAsserts.assertEquals(expected, actual.toArray()); + + expected = new String[] {"9", "8", "7"}; + actual = ImapUtility.getImapSequenceValues("9:7"); + MoreAsserts.assertEquals(expected, actual.toArray()); + + expected = new String[] {"1", "2", "3", "4", "9", "8", "7"}; + actual = ImapUtility.getImapSequenceValues("1,2:4,9:7"); + MoreAsserts.assertEquals(expected, actual.toArray()); + + // Test partially invalid sets + expected = new String[] { "1", "5" }; + actual = ImapUtility.getImapSequenceValues("1,x,5"); + MoreAsserts.assertEquals(expected, actual.toArray()); + + expected = new String[] { "1", "2", "3" }; + actual = ImapUtility.getImapSequenceValues("a:d,1:3"); + MoreAsserts.assertEquals(expected, actual.toArray()); + + // Test invalid sets + expected = new String[0]; + actual = ImapUtility.getImapSequenceValues(""); + MoreAsserts.assertEquals(expected, actual.toArray()); + + expected = new String[0]; + actual = ImapUtility.getImapSequenceValues(null); + MoreAsserts.assertEquals(expected, actual.toArray()); + + expected = new String[0]; + actual = ImapUtility.getImapSequenceValues("a"); + MoreAsserts.assertEquals(expected, actual.toArray()); + + expected = new String[0]; + actual = ImapUtility.getImapSequenceValues("1:x"); + MoreAsserts.assertEquals(expected, actual.toArray()); + } + + /** + * Test getting elements of an IMAP range. + */ + public void testGetImapRangeValues() { + String[] expected; + List actual; + + // Test valid ranges + expected = new String[] {"1", "2", "3"}; + actual = ImapUtility.getImapRangeValues("1:3"); + MoreAsserts.assertEquals(expected, actual.toArray()); + + expected = new String[] {"16", "15", "14"}; + actual = ImapUtility.getImapRangeValues("16:14"); + MoreAsserts.assertEquals(expected, actual.toArray()); + + // Test in-valid ranges + expected = new String[0]; + actual = ImapUtility.getImapRangeValues(""); + MoreAsserts.assertEquals(expected, actual.toArray()); + + expected = new String[0]; + actual = ImapUtility.getImapRangeValues(null); + MoreAsserts.assertEquals(expected, actual.toArray()); + + expected = new String[0]; + actual = ImapUtility.getImapRangeValues("a"); + MoreAsserts.assertEquals(expected, actual.toArray()); + + expected = new String[0]; + actual = ImapUtility.getImapRangeValues("6"); + MoreAsserts.assertEquals(expected, actual.toArray()); + + expected = new String[0]; + actual = ImapUtility.getImapRangeValues("1:3,6"); + MoreAsserts.assertEquals(expected, actual.toArray()); + + expected = new String[0]; + actual = ImapUtility.getImapRangeValues("1:x"); + MoreAsserts.assertEquals(expected, actual.toArray()); + + expected = new String[0]; + actual = ImapUtility.getImapRangeValues("1:*"); + MoreAsserts.assertEquals(expected, actual.toArray()); + } +} From 21d0b0275d508442bd4641517c272cbfbeb591a0 Mon Sep 17 00:00:00 2001 From: Koji Arai Date: Sat, 18 Feb 2012 01:29:35 +0900 Subject: [PATCH 60/93] Updated Japanese translation. catch up with 4c318d1. --- res/values-ja/strings.xml | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/res/values-ja/strings.xml b/res/values-ja/strings.xml index 09390e472..ff42ff2e4 100644 --- a/res/values-ja/strings.xml +++ b/res/values-ja/strings.xml @@ -71,6 +71,7 @@ メール送信 フォルダ一覧 フォルダ再読込 + フォルダを探す すべてのメールを既読にする アカウント追加 作成 @@ -822,6 +823,8 @@ K-9 Mail セットアップにようこそ。\nK-9 は標準のAndroidメール Q - アカウント設定に戻る\u000A S - アカウント設定編集 + フォルダ名に含まれる文字 + フォルダ表示 全フォルダ表示 1st クラスフォルダ表示 @@ -973,6 +976,9 @@ K-9 Mail セットアップにようこそ。\nK-9 は標準のAndroidメール 日付 本文 + メッセージ作成 + 入力テキスト + 極小 かなり小 やや小 @@ -1014,6 +1020,9 @@ K-9 Mail セットアップにようこそ。\nK-9 は標準のAndroidメール メッセージの下書き保存 メッセージを保存しますか? + メッセージ破棄? + このメッセージを破棄しますか? + 下書き保存の拒否 暗号化したメッセージは下書き保存できません @@ -1102,4 +1111,11 @@ K-9 Mail セットアップにようこそ。\nK-9 は標準のAndroidメール 下に移動 アカウントを移動しています + 未読件数 + アカウントの未読件数を表示 + + ファイルマネージャがありません + 設定をインポートするためのアプリケーションがありません。Androidマーケットからファイルマネージャをインストールしてください。 + マーケット + 閉じる From 8ce78408c2b66bb09ae41c8a8cfdc220db64ddbc Mon Sep 17 00:00:00 2001 From: cketti Date: Fri, 17 Feb 2012 19:40:58 +0100 Subject: [PATCH 61/93] Fixed HTML generation in MimeUtility.extractTextAndAttachments() --- src/com/fsck/k9/activity/MessageCompose.java | 2 +- src/com/fsck/k9/helper/HtmlConverter.java | 72 +++++++++++++++---- .../fsck/k9/mail/internet/MimeUtility.java | 12 ++-- src/com/fsck/k9/mail/store/LocalStore.java | 13 +--- 4 files changed, 68 insertions(+), 31 deletions(-) diff --git a/src/com/fsck/k9/activity/MessageCompose.java b/src/com/fsck/k9/activity/MessageCompose.java index 8b85cfd95..927078cf2 100644 --- a/src/com/fsck/k9/activity/MessageCompose.java +++ b/src/com/fsck/k9/activity/MessageCompose.java @@ -2709,7 +2709,7 @@ public class MessageCompose extends K9Activity implements OnClickListener, OnFoc if (part != null) { if (K9.DEBUG) Log.d(K9.LOG_TAG, "getBodyTextFromMessage: HTML requested, text found."); - return HtmlConverter.textToHtml(MimeUtility.getTextFromPart(part)); + return HtmlConverter.textToHtml(MimeUtility.getTextFromPart(part), true); } } else if (format == MessageFormat.TEXT) { // Text takes precedence, then html. diff --git a/src/com/fsck/k9/helper/HtmlConverter.java b/src/com/fsck/k9/helper/HtmlConverter.java index 9cf3c38df..e0bf6b720 100644 --- a/src/com/fsck/k9/helper/HtmlConverter.java +++ b/src/com/fsck/k9/helper/HtmlConverter.java @@ -125,19 +125,41 @@ public class HtmlConverter { private static final int MAX_SMART_HTMLIFY_MESSAGE_LENGTH = 1024 * 256 ; + public static final String getHtmlHeader() { + return ""; + } + + public static final String getHtmlFooter() { + return ""; + } + /** - * Naively convert a text string into an HTML document. This method avoids using regular expressions on the entire - * message body to save memory. - * @param text Plain text string. + * Naively convert a text string into an HTML document. + * + *

+ * This method avoids using regular expressions on the entire message body to save memory. + *

+ * + * @param text + * Plain text string. + * @param useHtmlTag + * If {@code true} this method adds headers and footers to create a proper HTML + * document. + * * @return HTML string. */ - private static String simpleTextToHtml(String text) { + private static String simpleTextToHtml(String text, boolean useHtmlTag) { // Encode HTML entities to make sure we don't display something evil. text = TextUtils.htmlEncode(text); StringReader reader = new StringReader(text); StringBuilder buff = new StringBuilder(text.length() + TEXT_TO_HTML_EXTRA_BUFFER_LENGTH); - buff.append(""); + + if (useHtmlTag) { + buff.append(getHtmlHeader()); + } + + buff.append(htmlifyMessageHeader()); int c; try { @@ -159,25 +181,39 @@ public class HtmlConverter { Log.e(K9.LOG_TAG, "Could not read string to convert text to HTML:", e); } - buff.append(""); + buff.append(htmlifyMessageFooter()); + + if (useHtmlTag) { + buff.append(getHtmlFooter()); + } return buff.toString(); } /** - * Convert a text string into an HTML document. Attempts to do smart replacement for large - * documents to prevent OOM errors. This method adds headers and footers to create a proper HTML - * document. To convert to a fragment, use {@link #textToHtmlFragment(String)}. - * @param text Plain text string. + * Convert a text string into an HTML document. + * + *

+ * Attempts to do smart replacement for large documents to prevent OOM errors. This method + * optionally adds headers and footers to create a proper HTML document. To convert to a + * fragment, use {@link #textToHtmlFragment(String)}. + *

+ * + * @param text + * Plain text string. + * @param useHtmlTag + * If {@code true} this method adds headers and footers to create a proper HTML + * document. + * * @return HTML string. */ - public static String textToHtml(String text) { + public static String textToHtml(String text, boolean useHtmlTag) { // Our HTMLification code is somewhat memory intensive // and was causing lots of OOM errors on the market // if the message is big and plain text, just do // a trivial htmlification if (text.length() > MAX_SMART_HTMLIFY_MESSAGE_LENGTH) { - return simpleTextToHtml(text); + return simpleTextToHtml(text, useHtmlTag); } StringReader reader = new StringReader(text); StringBuilder buff = new StringBuilder(text.length() + TEXT_TO_HTML_EXTRA_BUFFER_LENGTH); @@ -221,11 +257,19 @@ public class HtmlConverter { text = text.replaceAll("(?m)(\r\n|\n|\r){4,}", "\n\n"); StringBuffer sb = new StringBuffer(text.length() + TEXT_TO_HTML_EXTRA_BUFFER_LENGTH); - sb.append(""); + + if (useHtmlTag) { + sb.append(getHtmlHeader()); + } + sb.append(htmlifyMessageHeader()); linkifyText(text, sb); sb.append(htmlifyMessageFooter()); - sb.append(""); + + if (useHtmlTag) { + sb.append(getHtmlFooter()); + } + text = sb.toString(); return text; diff --git a/src/com/fsck/k9/mail/internet/MimeUtility.java b/src/com/fsck/k9/mail/internet/MimeUtility.java index 7d2755be9..74e3ce974 100644 --- a/src/com/fsck/k9/mail/internet/MimeUtility.java +++ b/src/com/fsck/k9/mail/internet/MimeUtility.java @@ -1112,7 +1112,7 @@ public class MimeUtility { return tempBody; } - + /** * Empty base class for the class hierarchy used by * {@link MimeUtility#extractTextAndAttachments(Context, Message)}. @@ -1320,6 +1320,8 @@ public class MimeUtility { StringBuilder text = new StringBuilder(); StringBuilder html = new StringBuilder(); + html.append(HtmlConverter.getHtmlHeader()); + for (Viewable viewable : viewables) { if (viewable instanceof Textual) { // This is either a text/plain or text/html part. Fill the variables 'text' and @@ -1370,6 +1372,8 @@ public class MimeUtility { } } + html.append(HtmlConverter.getHtmlFooter()); + return new ViewableContainer(text.toString(), html.toString(), attachments); } catch (Exception e) { throw new MessagingException("Couldn't extract viewable parts", e); @@ -1863,8 +1867,8 @@ public class MimeUtility { * Use the contents of a {@link Viewable} to create the HTML to be displayed. * *

- * This will use {@link HtmlConverter#textToHtml(String)} to convert plain text parts to HTML - * if necessary. + * This will use {@link HtmlConverter#textToHtml(String, boolean)} to convert plain text parts + * to HTML if necessary. *

* * @param viewable @@ -1886,7 +1890,7 @@ public class MimeUtility { if (t == null) { t = ""; } else if (viewable instanceof Text) { - t = HtmlConverter.textToHtml(t); + t = HtmlConverter.textToHtml(t, false); } html.append(t); } else if (viewable instanceof Alternative) { diff --git a/src/com/fsck/k9/mail/store/LocalStore.java b/src/com/fsck/k9/mail/store/LocalStore.java index 8f2fbb021..e59eeeef4 100644 --- a/src/com/fsck/k9/mail/store/LocalStore.java +++ b/src/com/fsck/k9/mail/store/LocalStore.java @@ -2105,7 +2105,7 @@ public class LocalStore extends Store implements Serializable { List attachments = container.attachments; String text = container.text; - String html = container.html; + String html = HtmlConverter.convertEmoji2Img(container.html); String preview = calculateContentPreview(text); @@ -2754,17 +2754,6 @@ public class LocalStore extends Store implements Serializable { } - public String markupContent(String text, String html) { - if (text.length() > 0 && html.length() == 0) { - html = HtmlConverter.textToHtml(text); - } - - html = HtmlConverter.convertEmoji2Img(html); - - return html; - } - - @Override public boolean isInTopGroup() { return mInTopGroup; From b9803ece1942accf30ee7041670de492063f5432 Mon Sep 17 00:00:00 2001 From: cketti Date: Sat, 18 Feb 2012 00:04:09 +0100 Subject: [PATCH 62/93] Fixed divider before text part with filename --- src/com/fsck/k9/mail/internet/MimeUtility.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/com/fsck/k9/mail/internet/MimeUtility.java b/src/com/fsck/k9/mail/internet/MimeUtility.java index 74e3ce974..5fd3c047d 100644 --- a/src/com/fsck/k9/mail/internet/MimeUtility.java +++ b/src/com/fsck/k9/mail/internet/MimeUtility.java @@ -1853,9 +1853,9 @@ public class MimeUtility { } text.append(FILENAME_PREFIX); text.append(filename); + text.append(FILENAME_SUFFIX); text.append(TEXT_DIVIDER.substring(0, TEXT_DIVIDER_LENGTH - FILENAME_PREFIX_LENGTH - filename.length() - FILENAME_SUFFIX_LENGTH)); - text.append(' '); } else { text.append(TEXT_DIVIDER); } From 03d4cee14af8d8520a5fc6a3f124ae6aa85dab8f Mon Sep 17 00:00:00 2001 From: cketti Date: Sat, 18 Feb 2012 00:25:14 +0100 Subject: [PATCH 63/93] Added tests for MimeUtility.extractTextAndAttachments() --- .../fsck/k9/mail/internet/ViewablesTest.java | 188 ++++++++++++++++++ 1 file changed, 188 insertions(+) create mode 100644 tests/src/com/fsck/k9/mail/internet/ViewablesTest.java diff --git a/tests/src/com/fsck/k9/mail/internet/ViewablesTest.java b/tests/src/com/fsck/k9/mail/internet/ViewablesTest.java new file mode 100644 index 000000000..ded79c3d5 --- /dev/null +++ b/tests/src/com/fsck/k9/mail/internet/ViewablesTest.java @@ -0,0 +1,188 @@ +package com.fsck.k9.mail.internet; + +import java.util.Date; +import java.util.Locale; +import java.util.TimeZone; +import android.test.AndroidTestCase; +import com.fsck.k9.mail.Address; +import com.fsck.k9.mail.MessagingException; +import com.fsck.k9.mail.Message.RecipientType; +import com.fsck.k9.mail.internet.MimeUtility.ViewableContainer; + +public class ViewablesTest extends AndroidTestCase { + + public void testSimplePlainTextMessage() throws MessagingException { + String bodyText = "K-9 Mail rocks :>"; + + // Create text/plain body + TextBody body = new TextBody(bodyText); + + // Create message + MimeMessage message = new MimeMessage(); + message.setBody(body); + + // Extract text + ViewableContainer container = MimeUtility.extractTextAndAttachments(getContext(), message); + + String expectedText = bodyText; + String expectedHtml = + "" + + "
" +
+                "K-9 Mail rocks :>" +
+                "
" + + ""; + + assertEquals(expectedText, container.text); + assertEquals(expectedHtml, container.html); + } + + public void testSimpleHtmlMessage() throws MessagingException { + String bodyText = "K-9 Mail rocks :>"; + + // Create text/plain body + TextBody body = new TextBody(bodyText); + + // Create message + MimeMessage message = new MimeMessage(); + message.setHeader("Content-Type", "text/html"); + message.setBody(body); + + // Extract text + ViewableContainer container = MimeUtility.extractTextAndAttachments(getContext(), message); + + String expectedText = "K-9 Mail rocks :>"; + String expectedHtml = + "" + + bodyText + + ""; + + assertEquals(expectedText, container.text); + assertEquals(expectedHtml, container.html); + } + + public void testMultipartPlainTextMessage() throws MessagingException { + String bodyText1 = "text body 1"; + String bodyText2 = "text body 2"; + + // Create text/plain bodies + TextBody body1 = new TextBody(bodyText1); + TextBody body2 = new TextBody(bodyText2); + + // Create multipart/mixed part + MimeMultipart multipart = new MimeMultipart(); + MimeBodyPart bodyPart1 = new MimeBodyPart(body1, "text/plain"); + MimeBodyPart bodyPart2 = new MimeBodyPart(body2, "text/plain"); + multipart.addBodyPart(bodyPart1); + multipart.addBodyPart(bodyPart2); + + // Create message + MimeMessage message = new MimeMessage(); + message.setBody(multipart); + + // Extract text + ViewableContainer container = MimeUtility.extractTextAndAttachments(getContext(), message); + + String expectedText = + bodyText1 + "\n\n" + + "------------------------------------------------------------------------\n\n" + + bodyText2; + String expectedHtml = + "" + + "
" +
+                bodyText1 +
+                "
" + + "

" + + "
" +
+                bodyText2 +
+                "
" + + ""; + + + assertEquals(expectedText, container.text); + assertEquals(expectedHtml, container.html); + } + + public void testTextPlusRfc822Message() throws MessagingException { + Locale.setDefault(Locale.US); + TimeZone.setDefault(TimeZone.getTimeZone("UTC")); + + String bodyText = "Some text here"; + String innerBodyText = "Hey there. I'm inside a message/rfc822 (inline) attachment."; + + // Create text/plain body + TextBody textBody = new TextBody(bodyText); + + // Create inner text/plain body + TextBody innerBody = new TextBody(innerBodyText); + + // Create message/rfc822 body + MimeMessage innerMessage = new MimeMessage(); + innerMessage.addSentDate(new Date(112, 02, 17)); + innerMessage.setRecipients(RecipientType.TO, new Address[] { new Address("to@example.com") }); + innerMessage.setSubject("Subject"); + innerMessage.setFrom(new Address("from@example.com")); + innerMessage.setBody(innerBody); + + // Create multipart/mixed part + MimeMultipart multipart = new MimeMultipart(); + MimeBodyPart bodyPart1 = new MimeBodyPart(textBody, "text/plain"); + MimeBodyPart bodyPart2 = new MimeBodyPart(innerMessage, "message/rfc822"); + bodyPart2.setHeader("Content-Disposition", "inline; filename=\"message.eml\""); + multipart.addBodyPart(bodyPart1); + multipart.addBodyPart(bodyPart2); + + // Create message + MimeMessage message = new MimeMessage(); + message.setBody(multipart); + + // Extract text + ViewableContainer container = MimeUtility.extractTextAndAttachments(getContext(), message); + + String expectedText = + bodyText + + "\n\n" + + "----- message.eml ------------------------------------------------------" + + "\n\n" + + "From: from@example.com" + "\n" + + "To: to@example.com" + "\n" + + "Sent: Sat Mar 17 00:00:00 GMT+00:00 2012" + "\n" + + "Subject: Subject" + "\n" + + "\n" + + innerBodyText; + String expectedHtml = + "" + + "
" +
+                bodyText +
+                "
" + + "

message.eml

" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "
From:from@example.com
To:to@example.com
Sent:Sat Mar 17 00:00:00 GMT+00:00 2012
Subject:Subject
" + + "
" +
+                innerBodyText +
+                "
" + + ""; + + assertEquals(expectedText, container.text); + assertEquals(expectedHtml, container.html); + } +} From 4adfc51339066234af6d14218acffd02b379c0d5 Mon Sep 17 00:00:00 2001 From: cketti Date: Sat, 18 Feb 2012 00:44:24 +0100 Subject: [PATCH 64/93] Use HtmlConverter.convertEmoji2Img() in LocalFolder.updateMessage() --- src/com/fsck/k9/mail/store/LocalStore.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/com/fsck/k9/mail/store/LocalStore.java b/src/com/fsck/k9/mail/store/LocalStore.java index e59eeeef4..fe830ff7b 100644 --- a/src/com/fsck/k9/mail/store/LocalStore.java +++ b/src/com/fsck/k9/mail/store/LocalStore.java @@ -2192,7 +2192,7 @@ public class LocalStore extends Store implements Serializable { List attachments = container.attachments; String text = container.text; - String html = container.html; + String html = HtmlConverter.convertEmoji2Img(container.html); String preview = calculateContentPreview(text); From fbc187a3e2577b48db408296ad2473cf6d2ab1f1 Mon Sep 17 00:00:00 2001 From: cketti Date: Sun, 19 Feb 2012 19:51:06 +0100 Subject: [PATCH 65/93] IMAP: Send the Message-ID as quoted string in getUidFromMessageId() --- src/com/fsck/k9/mail/store/ImapStore.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/com/fsck/k9/mail/store/ImapStore.java b/src/com/fsck/k9/mail/store/ImapStore.java index 17441a600..cc3681706 100644 --- a/src/com/fsck/k9/mail/store/ImapStore.java +++ b/src/com/fsck/k9/mail/store/ImapStore.java @@ -1913,7 +1913,7 @@ public class ImapStore extends Store { List responses = executeSimpleCommand( - String.format("UID SEARCH HEADER MESSAGE-ID %s", messageId)); + String.format("UID SEARCH HEADER MESSAGE-ID %s", encodeString(messageId))); for (ImapResponse response1 : responses) { if (response1.mTag == null && ImapResponseParser.equalsIgnoreCase(response1.get(0), "SEARCH") && response1.size() > 1) { From 309eeb72ac3c4fb2b93766550eed24e5cab39bb8 Mon Sep 17 00:00:00 2001 From: cketti Date: Tue, 21 Feb 2012 04:56:05 +0100 Subject: [PATCH 66/93] Got rid of the ScrollView in MessageView The ScrollView around the WebView caused all sorts of problems. This change removes the ScrollView and uses the undocumented method WebView.setEmbeddedTitleBar() to set the MessageHeader view as "title bar" of the WebView. This allows MessageHeader to scroll away making more room for the WebView. All of the "magic title bar" code was originally implemented by Jesse for Kaiten. Because WebView doesn't support a scrolling footer we can no longer support scrolling buttons or attachments at the end of the message. Now users can switch from message view to attachment view via a button just below the message headers. I also copied some code for which I was too lazy to create a separate commit. It allows to display attachments we didn't use to show by clicking on a "More..." button in the attachment view. Those attachments are mostly images referenced by the HTML part (e.g. background images). Fixes issue 3291 --- res/drawable-hdpi/show_less.png | Bin 0 -> 393 bytes res/drawable-hdpi/show_more.png | Bin 0 -> 389 bytes res/drawable/separator_area_background.xml | 14 + res/drawable/show_less.png | Bin 0 -> 300 bytes res/drawable/show_more.png | Bin 0 -> 299 bytes res/layout/message.xml | 260 ++++--------------- res/layout/message_view.xml | 28 +- res/layout/message_view_header.xml | 269 ++++++++++++++++++++ res/values/colors.xml | 3 +- res/values/strings.xml | 3 + src/com/fsck/k9/activity/K9Activity.java | 35 +-- src/com/fsck/k9/activity/MessageView.java | 101 +++----- src/com/fsck/k9/view/MessageHeader.java | 28 +- src/com/fsck/k9/view/MessageWebView.java | 39 ++- src/com/fsck/k9/view/SingleMessageView.java | 167 ++++++++---- 15 files changed, 555 insertions(+), 392 deletions(-) create mode 100644 res/drawable-hdpi/show_less.png create mode 100644 res/drawable-hdpi/show_more.png create mode 100644 res/drawable/separator_area_background.xml create mode 100644 res/drawable/show_less.png create mode 100644 res/drawable/show_more.png create mode 100644 res/layout/message_view_header.xml diff --git a/res/drawable-hdpi/show_less.png b/res/drawable-hdpi/show_less.png new file mode 100644 index 0000000000000000000000000000000000000000..c1a758348c30827f18d8fb7fe4dc5637e4fec545 GIT binary patch literal 393 zcmeAS@N?(olHy`uVBq!ia0vp^LO{&J!3HGrh2HJ~QY^(zo*^7SP{WbZ0pxQQctjR6 zFff*aFr!O(+B2Y_WQl7;NpOBzNqJ&XDnogBxn5>oc5!lIL8@MUQTpt6Hc~*~Cc%J(Phgs9sgxQC*e7RO%X=@P6cl^K-=deLwZ$qe;;|w*kj~tqp zmUum63}Vk=k?v04Jn^}5{hYM)dgIS?6U)0+t;*Rn<&xaIPhZuP_L)Bwo9&>vRC&)M z9dF?nmm{CJJ^eJ66;=YA$8ov*3Qlm%h($_Ak8K(O)PkINvpLn)>cZpN>S&-fEETpkMJ zyJgt_g=T3$xzdl^*98qX9sQJSpIpE6YJG0wJ3Wtic`bSCelpKmusP3u+ks~gcyqV(DCY@~pynLJ$_ zLo_DNy{zcx>?qRu@crH;Z9uq-+acuYCYDuN_f%9)XqhoMPT0^WrNenr!i9O)szuAV zF0yW5Fi?>=lE7wEIfZF&TH3jt-_P$%TD2^5U7=KR&35Nj0jn9;W+ccty?D#;GO^Ed z?pEhDP8`kk4r~!mBTFBC-N>WZ^PfR7d1ku9Rg<&E-@5m;XvZ9p2<8rWD3rc}>-ExO z0%NoNqL0}CW_axQH=ny=3g&zpHJ-4x%~r5i)0$Lj faQ6J#`!AT8PsYT$eS5eR7-S5du6{1-oD!M<#rc}b literal 0 HcmV?d00001 diff --git a/res/drawable/separator_area_background.xml b/res/drawable/separator_area_background.xml new file mode 100644 index 000000000..01c289eb7 --- /dev/null +++ b/res/drawable/separator_area_background.xml @@ -0,0 +1,14 @@ + + + + + + + + + + diff --git a/res/drawable/show_less.png b/res/drawable/show_less.png new file mode 100644 index 0000000000000000000000000000000000000000..bc9e3f948ca399cf2384cf2edc4589e5e2b792df GIT binary patch literal 300 zcmeAS@N?(olHy`uVBq!ia0vp^JV4CB!3HGHK9Tzfq*#ibJVQ8upoSx*1IXtr@Q5r1 z(tkjh(ZD9L9LSI?ag8Vm&QB{TPb^AhC@(M9%goCzPEIUH)ypqRpZ(583aDtMr;B5V z#MImi3j3ga$GuQU4m;kbZcdd1O|rL_-~ zjPECE{t)`z#Qtuzd*<(?E%$!Q=3V^I`T9)FOflPsJI?U0{r*olsr2iV?5|f+xwil0 qbr;B5V z#MIggx_M540!KgIujX7ejkSc;qGWnNV^wlipiU#pvmQG-|cBo$=>#8_tmAzF_A0 o2J(p&c^U8KC4`^0e_zKiX|vTn4^j0Npqm*yUHx3vIVCg!0RA^{(EtDd literal 0 HcmV?d00001 diff --git a/res/layout/message.xml b/res/layout/message.xml index 8e425eb71..361c90299 100644 --- a/res/layout/message.xml +++ b/res/layout/message.xml @@ -1,226 +1,74 @@ + android:orientation="vertical" + android:layout_width="fill_parent" + android:layout_height="wrap_content" + android:layout_weight="1"> + - - - - - - - > - - - - - - - - - - - - - - - - - - - - - - - - - - - -