diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index 00263f209..fa40d48d5 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -1,8 +1,8 @@
@@ -208,6 +208,10 @@
android:launchMode="singleTask"
android:configChanges="locale"
>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/images/show_more_indicator.svg b/images/show_more_indicator.svg
new file mode 100644
index 000000000..55bd6dab0
--- /dev/null
+++ b/images/show_more_indicator.svg
@@ -0,0 +1,20 @@
+
+
+
diff --git a/res/drawable-hdpi/show_less.png b/res/drawable-hdpi/show_less.png
new file mode 100644
index 000000000..c1a758348
Binary files /dev/null and b/res/drawable-hdpi/show_less.png differ
diff --git a/res/drawable-hdpi/show_more.png b/res/drawable-hdpi/show_more.png
new file mode 100644
index 000000000..8fc07e919
Binary files /dev/null and b/res/drawable-hdpi/show_more.png differ
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/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 000000000..bc9e3f948
Binary files /dev/null and b/res/drawable/show_less.png differ
diff --git a/res/drawable/show_more.png b/res/drawable/show_more.png
new file mode 100644
index 000000000..6d0fe5bf1
Binary files /dev/null and b/res/drawable/show_more.png differ
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/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-land/message_view_scrolling_buttons.xml b/res/layout-land/message_view_scrolling_buttons.xml
deleted file mode 100644
index 036eee79e..000000000
--- a/res/layout-land/message_view_scrolling_buttons.xml
+++ /dev/null
@@ -1,75 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
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/message.xml b/res/layout/message.xml
index 8e425eb71..ea40b49cb 100644
--- a/res/layout/message.xml
+++ b/res/layout/message.xml
@@ -1,226 +1,75 @@
+ android:orientation="vertical"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:layout_weight="1">
+
-
-
-
-
-
-
- >
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+ android:id="@+id/message_view_header_container"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content">
+
+
+
-
+
+ android:layout_height="0dip"
+ android:layout_weight="1"
+ android:layout_width="fill_parent"/>
+
+ android:layout_width="fill_parent"/>
+
-
+ android:layout_weight="1">
+
+
+
+
+
+
+
+
+
+
+
+
+
+ android:layout_width="fill_parent"/>
+
diff --git a/res/layout/message_view.xml b/res/layout/message_view.xml
index d653f3dba..7bf2fb51c 100644
--- a/res/layout/message_view.xml
+++ b/res/layout/message_view.xml
@@ -3,27 +3,12 @@
xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="fill_parent"
- android:layout_height="fill_parent"
- >
-
-
-
-
-
-
+ android:layout_height="fill_parent">
+
+
+
+
+
diff --git a/res/layout/message_view_header.xml b/res/layout/message_view_header.xml
new file mode 100644
index 000000000..fe5c486a0
--- /dev/null
+++ b/res/layout/message_view_header.xml
@@ -0,0 +1,289 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/res/layout/message_view_scrolling_buttons.xml b/res/layout/message_view_scrolling_buttons.xml
deleted file mode 100644
index 426ee800c..000000000
--- a/res/layout/message_view_scrolling_buttons.xml
+++ /dev/null
@@ -1,88 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/res/layout/unread_widget_layout.xml b/res/layout/unread_widget_layout.xml
new file mode 100644
index 000000000..4ad2c399e
--- /dev/null
+++ b/res/layout/unread_widget_layout.xml
@@ -0,0 +1,59 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/res/values-ca/strings.xml b/res/values-ca/strings.xml
index df34a514a..0c6c20f79 100644
--- a/res/values-ca/strings.xml
+++ b/res/values-ca/strings.xml
@@ -542,14 +542,8 @@ Benvingut a la configuració del K-9. El K-9 és un client de codi obert per An
Mostra correu no llegitMostra el nombre de missatges no llegits a la barra de notificació.
- Botons de desplaçament
- Mai
- Quan el teclat estigui disponible
- Sempre
-
Habilita botons emplenatMostra els botons d’Arxiu, Mou, i Brossa.
- Navega botons emplenamentSempre mostra imatgesNo
diff --git a/res/values-cs/strings.xml b/res/values-cs/strings.xml
index c2058ab41..411fd90fe 100644
--- a/res/values-cs/strings.xml
+++ b/res/values-cs/strings.xml
@@ -549,14 +549,8 @@ Vítejte v nastavení pošty K-9 Mail. K-9 je open source poštovní klient pro
- Posunovat navigační tlačítka
- Nikdy
- Je-li dostupná klávesnice
- Vždy
-
Povolit přesměrovací tlačítkaZobrazit tlačítka Archív, Přesunout a Nevyžádaná.
- Posunovat přesměrovací tlačítkaVždy zobrazovat obrázkyNe
diff --git a/res/values-da/strings.xml b/res/values-da/strings.xml
index 2848f173e..b02eafb63 100644
--- a/res/values-da/strings.xml
+++ b/res/values-da/strings.xml
@@ -545,14 +545,8 @@ Velkommen til K-9 Mail opsætning. K-9 er en open source mail klient for Androi
Vis antal ulæste mailsVis antallet af ulæste mails i statusbar.
- Scroll navigations knapper
- Aldrig
- Når tastatur er tilgængeligt
- Altid
-
Aktiver Flyt knapperVis Arkiver, Flyt og Spam knapper.
- Scroll Flyt knapperVis altid billederAldrig
diff --git a/res/values-de/strings.xml b/res/values-de/strings.xml
index c502c09b3..e6e26aae5 100644
--- a/res/values-de/strings.xml
+++ b/res/values-de/strings.xml
@@ -285,6 +285,8 @@ Willkommen zum \"K-9 Mail\"-Setup. K-9 ist eine quelloffene E-Mail-Anwendung fü
Anhang konnte nicht auf SD-Karte gespeichert werden.Wählen Sie \"Bilder anzeigen\", um eingebettete Bilder abzurufen.Bilder anzeigen
+ Zeige Anhänge
+ Mehr…Lade Anhang.Es wurde kein Anzeigeprogramm für %s gefunden.
@@ -543,14 +545,8 @@ Willkommen zum \"K-9 Mail\"-Setup. K-9 ist eine quelloffene E-Mail-Anwendung fü
Anzahl ungelesener Nachrichten anzeigenZeigt die Anzahl der ungelesenen Nachrichten in der Statuszeile.
- Scrolle Navigationsleiste
- Leiste bleibt eingeblendet
- Nur wenn Tastatur aktiv
- Immer
-
Spam-LeisteZeige Archivieren-, Verschieben- und Spam-Schaltfläche.
- Scrolle Spam-LeisteBilder automatisch anzeigenNiemals
@@ -564,15 +560,15 @@ Willkommen zum \"K-9 Mail\"-Setup. K-9 ist eine quelloffene E-Mail-Anwendung fü
Antwort unter ZitatDie Antwort auf eine Nachricht unterhalb der Original-Nachricht platzieren.
-
- Unterschrift von zitierter Antwort entfernen
- Beim Antworten von Nachrichten wird die Unterschrift vom zitiertem Textabschnitt entfernt
+
+ Entferne Signatur aus zitierter Antwort
+ Beim Antworten wird die Signatur aus dem Zitat entferntFormatierungEinfacher Text (Bilder und Formatierungen werden entfernt)HTML (Bilder und Formatierungen bleiben erhalten)
- Automatisch (Einfacher Text, es sei denn bei Antwort auf HTML)
-
+ Automatisch (Einfacher Text, es sei denn bei Antwort auf HTML)
+
EmpfangsbestätigungImmer eine Empfangsbestätigung anfordern
@@ -595,11 +591,11 @@ Willkommen zum \"K-9 Mail\"-Setup. K-9 ist eine quelloffene E-Mail-Anwendung fü
Email Adresse des Kontos verwenden um Signaturschlüssel zu schätzen.Automatische verschlüsselung
- Verschlüsselung aktivieren falls für den Empfänger ein öffentlichen Schlüssel abgespeichert ist.
-
+ Verschlüsselung aktivieren falls für den Empfänger ein öffentlichen Schlüssel abgespeichert ist.
+
Häufigkeit der E-Mail-AbfrageHäufigkeit der Abfrage für Nebenordner
-
+
SpeicherFarbe des Kontos
@@ -1022,10 +1018,10 @@ Willkommen zum \"K-9 Mail\"-Setup. K-9 ist eine quelloffene E-Mail-Anwendung fü
Entwurf speichern?Entwurf speichern oder verwerfen?
-
+
Speichern des Entwurfs verweigern.Die Speicherung von als verschlüsselt markierten Entwürfen verweigern.
-
+
Ohne öffentlichen Schlüssel fortfahren?Einer oder mehrere Empfänger besitzen keinen abgespeicherten öffentlichen Schlüssel. Fortfahren?
@@ -1053,7 +1049,7 @@ Willkommen zum \"K-9 Mail\"-Setup. K-9 ist eine quelloffene E-Mail-Anwendung fü
»›Verbindungsfehler.
-
+
Einstellungen Importieren & ExportierenKontoeinstellungen exportierenEinstellungen und Konten exportieren
@@ -1104,5 +1100,5 @@ Willkommen zum \"K-9 Mail\"-Setup. K-9 ist eine quelloffene E-Mail-Anwendung fü
Nach oben verschiebenNach unten verschiebenKonto verschieben...
-
+
diff --git a/res/values-es/strings.xml b/res/values-es/strings.xml
index e5e2cb121..8ff2f55c5 100644
--- a/res/values-es/strings.xml
+++ b/res/values-es/strings.xml
@@ -541,14 +541,8 @@ Bienvenido a la Configuración de K-9. K-9 es un cliente de correo OpenSource pa
Mostrar contador de correos sin leerMostrar el número de mensajes sin leer en la barra de notificaciones.
- Botones de desplazamiento
- Nunca
- Cuando el teclado está disponible
- Siempre
-
Habilitar botones de copiaMostrar botones de archivar, mover y SPAM
- Botones de copiaMostrar imágenesNunca
diff --git a/res/values-fi/strings.xml b/res/values-fi/strings.xml
index 534df7cd7..0ee649056 100644
--- a/res/values-fi/strings.xml
+++ b/res/values-fi/strings.xml
@@ -537,14 +537,8 @@ Tervetuloa K-9 Mail asennukseen. K-9 on avoimen lähdekoodin sähköpostiasiak
Näytä lukemattomien viestien määräNäytä lukemattomien viestien määrä osoiterivillä.
- Selauksen painikkeet
- Ei koskaan
- Kun näppäimistö on käytettävissä
- Aina
-
Ota käyttöön Siirrä-painikkeetNäytä Arkistoi-, Siirrä- ja Roskaposti-painikkeet.
- Vieritä Siirrä painikkeetNäytä kuvat automaattisestiEi koskaan
diff --git a/res/values-fr/strings.xml b/res/values-fr/strings.xml
index 3b3a86fde..e836d5c53 100644
--- a/res/values-fr/strings.xml
+++ b/res/values-fr/strings.xml
@@ -567,14 +567,8 @@ Bienvenue dans la configuration de K-9 Mail. K-9 Mail est un client de messageri
Afficher le nombre de messages non lusAfficher le nombre de messages non lus dans la barre de notification
- Défiler les boutons de navigation
- Jamais
- Lorsque le clavier est disponible
- Toujours
-
Activer les boutons de déplacementAfficher les boutons Archiver, Déplacer ou Spam
- Défiler les boutons de déplacementAfficher automatiquement les imagesJamais
@@ -589,13 +583,13 @@ Bienvenue dans la configuration de K-9 Mail. K-9 Mail est un client de messageri
Réponse après la citationFaire précéder la réponse par le texte d\'origine
- Retirer signature dans réponse citée
+ Retirer signature dans réponse citéeLorsque vous répondez à des messages, la signature du texte cité sera éffacéeFormat du messageHTML (formattage et images conservés)Text brut (formattage et images omis)
- Automatique (Text brut à moins de répondre à un message HTML)
+ Automatique (Text brut à moins de répondre à un message HTML)Accusé de réceptionToujours demander un accusé de réception
@@ -617,9 +611,9 @@ Bienvenue dans la configuration de K-9 Mail. K-9 Mail est un client de messageri
Non disponibleSignature automatiqueUtiliser l\'adresse e-mail du compte pour déduire la clé de signature
- Cryptation automatique
- Cryptation automatique si une clé publique correspond au destinataire.
-
+ Cryptation automatique
+ Cryptation automatique si une clé publique correspond au destinataire.
+
Fréquence de vérification du dossierFréquence de vérification pour 2ème classe
@@ -1045,12 +1039,12 @@ Bienvenue dans la configuration de K-9 Mail. K-9 Mail est un client de messageri
Enregistrer le brouillon\u00A0?Enregistrer ou abandonner ce message\u00A0?
- Refuser d\'enregistrer des brouillons.
- Refuser d\'enregistrer un message marqué comme crypté.
-
- Continuer sans clé publique?
- Un ou plusieurs destinataires n\'ont pas de clé publique enregistrée. Continuer?
-
+ Refuser d\'enregistrer des brouillons.
+ Refuser d\'enregistrer un message marqué comme crypté.
+
+ Continuer sans clé publique?
+ Un ou plusieurs destinataires n\'ont pas de clé publique enregistrée. Continuer?
+
Ce message ne peut être affiché parce que le jeu de caractères «\u00A0%s\u00A0» n\'a pu être trouvé.Sélectionnez le texte à copier
diff --git a/res/values-gl/strings.xml b/res/values-gl/strings.xml
index ff9a06f31..c1336f892 100644
--- a/res/values-gl/strings.xml
+++ b/res/values-gl/strings.xml
@@ -541,14 +541,8 @@ Benvido á Configuración de K-9. K-9 é un cliente de correo OpenSource para An
Amosar número de mensaxes non lidosAmosar número de mensaxen non lidos na barra de notificacións.
- Botóns de desprazamento
- Nunca
- Cando o teclado esté dispoñible
- Sempre
-
Habilitar botóns de copiaAmosar botóns de arquivar, mover e SPAM
- Botóns de copiaAmosar imaxesNunca
diff --git a/res/values-hu/strings.xml b/res/values-hu/strings.xml
index 473aecfe5..f1abb0cfe 100644
--- a/res/values-hu/strings.xml
+++ b/res/values-hu/strings.xml
@@ -154,7 +154,7 @@
%sNéhány üzenetet nem sikerült elküldeniNézze meg a %s mappát a részletekért.
- "A K-9 hibát észlelt üzenetküldés közben. A probléma miatt elképzelhető, hogy a levél nem érkezik meg, de az is lehet hogy igen.
+ "A K-9 hibát észlelt üzenetküldés közben. A probléma miatt elképzelhető, hogy a levél nem érkezik meg, de az is lehet hogy igen.
Az ilyen hibás leveleket csillag jelzi a postázandó mappában. Ha eltávolítja a csillagot a K-9 megpróbálja újból elküldeni a levelet. Hosszan a Postázatlan mappára kattintva válassza az Üzenetek küldését.
@@ -162,26 +162,26 @@ Az ilyen hibás leveleket csillag jelzi a postázandó mappában. Ha eltávolít
K-9 riasztásA szinkronizáció és a levélküldés felfüggesztve, hálozati kapcsolat hiánya miatt.Nincs több üzenet
- "Üdvözöljük a K-9 Mail beállításokban. A K-9 egy nyílt forráskódú levelezőprogram Androidra, a sima mail kliens alapjaira helyezve.
-
-K-9 továbbfejlesztett funkciói:
- * Push mail using IMAP IDLE
- * Jobb teljesítmény
- * Message refiling
- * E-mail aláírások
- * Titkos másolat
- * Mappa-előfizetések
- * Minden mappa szinkronizálása
- * Válasz cím beállítása
- * Gyorsbillentyűk
- * Jobb IMAP támogatás
- * Mellékletek mentése a memóriakártyára
- * Kuka ürítése
- * Üzenetek válogatás
+ "Üdvözöljük a K-9 Mail beállításokban. A K-9 egy nyílt forráskódú levelezőprogram Androidra, a sima mail kliens alapjaira helyezve.
+
+K-9 továbbfejlesztett funkciói:
+ * Push mail using IMAP IDLE
+ * Jobb teljesítmény
+ * Message refiling
+ * E-mail aláírások
+ * Titkos másolat
+ * Mappa-előfizetések
+ * Minden mappa szinkronizálása
+ * Válasz cím beállítása
+ * Gyorsbillentyűk
+ * Jobb IMAP támogatás
+ * Mellékletek mentése a memóriakártyára
+ * Kuka ürítése
+ * Üzenetek válogatás
* ...és még sok más
-Magyarította: Deák Tamás (maya98) és RootRulez
-
+Magyarította: Deák Tamás (maya98) és RootRulez
+
Vegye figyelembe, hogy a K-9 nem támogatja a legtöbb ingyenes hotmail fiókot és még sok más klienst. Történnek furcsaságok, ha Microsoft Exchange-el kommunikál.
Hibajelentéseivel hozzájárul az újabb verziók tökéletesítéséhez, kérdéseit itt teheti fel http://k9mail.googlecode.com/"Verzió: %s
@@ -218,7 +218,7 @@ Vegye figyelembe, hogy a K-9 nem támogatja a legtöbb ingyenes hotmail fiókot
IdézetLegalább egy címzetted adjon meg.E-mail cím nem található.
- Néhány melléklet nem lett letöltve. Levélküldés előtt automatikusan letöltődnek.
+ Néhány melléklet nem lett letöltve. Levélküldés előtt automatikusan letöltődnek.Néhány mellékletet nem lehet továbbítani, mert nem lettek letöltve.Feladó: %s <%s>Címzett:
@@ -278,7 +278,7 @@ Vegye figyelembe, hogy a K-9 nem támogatja a legtöbb ingyenes hotmail fiókot
Törlés (csak üzenetek nézet)LevélszemétÖsszes megjelölése olvasottként
- Küldés
+ KüldésKépernyőzár értesítésekLezárt képernyőnél ne mutassa a levelek tárgyát a állapotsoronCsendes mód
@@ -450,13 +450,8 @@ Levelek letöltése…"Értesítésre kattintva megnyitja az olvasatlan üzeneteketOlvasatlanok kijelzéseOlvasatlan levelek száma az állapotsoron.
- Görgetés iránygombokkal
- Soha
- Ha van billentyűzet
- MindigMűvelet gombok megjelenítéseMutassa a Mozgatás, Archív és Levélszemét gombokat
- Művelet gombok görgetéseKépek megjelenítéseSohaCsak az ismerősökét
@@ -611,7 +606,7 @@ Levelek letöltése…"Aláírás használataAláírásAz aláírás hozzáfűzése minden elküldött levélhez
- "--
+ "--
Ezt a levelet mobiltelefonról küldtem, K-9 Mail-el. Elnézést a levél rövidségéért és az esetleges hibákért."Elsődleges személyazonosságomSzemélyazonosság választása
@@ -654,7 +649,7 @@ Ezt a levelet mobiltelefonról küldtem, K-9 Mail-el. Elnézést a levél rövid
Ha POP3-at vagy IMAP-ot szeretne használni ehhez a szolgáltatóhoz, akkor engedélyeznie kell az IMAP vagy POP3 beállításokat a Hanmail(Daum) oldalán.Ha POP3-at vagy IMAP-ot szeretne használni ehhez a szolgáltatóhoz, akkor engedélyeznie kell az IMAP vagy POP3 beállításokat a Paran oldalán.Ha POP3-at vagy IMAP-ot szeretne használni ehhez a szolgáltatóhoz, akkor engedélyeznie kell az IMAP vagy POP3 beállításokat a Nate oldalán.
- Felismerhetetlen tanúsítvány
+ Felismerhetetlen tanúsítványKulcs elfogadvaKulcs elutasítva"Del (or D) - Delete
@@ -752,7 +747,7 @@ S - Select/deselect"Engedélyezi a kézmozdulakkal való vezérlést.Kompakt elrendezésAdjust layouts to display more on each page
- Hangerő gomb vezérlés
+ Hangerő gomb vezérlésLéptetés az hangerő gombokkalLeveleknélLista nézetek váltása
@@ -766,7 +761,7 @@ S - Select/deselect"Kikcsapcsolva gyorsabb műkodésKülönleges fiókok elrejtéseEgységesen elrejti a fiókok bejövő mappáit
- %s %s
+ %s %s- Csillagos- OlvasatlanMinden levél
@@ -846,11 +841,11 @@ S - Select/deselect"IgenNemCsatolmányok letöltése
- Hibakereső naplózás bekapcsolva
+ Hibakereső naplózás bekapcsolva»›Nem lehet kapcsolódni.
- \"%s\" fiók nem elérhető ellenőríze a tárhelyet
+ \"%s\" fiók nem elérhető ellenőríze a tárhelyetCsatolményok mentése ide:Csatolményok mentéseNincs fájlkezelő. Hova szeretné mentni a csatolmányt?
diff --git a/res/values-it/strings.xml b/res/values-it/strings.xml
index c5d96f662..8d4716fb3 100644
--- a/res/values-it/strings.xml
+++ b/res/values-it/strings.xml
@@ -543,14 +543,8 @@ Benvenuto nella configurazione della posta di K-9. K-9 è un client di posta ope
Vedi numero messaggi non lettiMostra il numero dei messaggi non letti nella barra di notifica.
- Scorri pulsanti navigazione
- Mai
- Quando la tastiera è disponibile
- Sempre
-
Abilita pulsanti archiviazioneMostra i pulsanti Archivia, Sposta e Spam.
- Scorri pulsanti archiviazioneMostra sempre le immaginiNo
diff --git a/res/values-ja/strings.xml b/res/values-ja/strings.xml
index 09390e472..55815636e 100644
--- a/res/values-ja/strings.xml
+++ b/res/values-ja/strings.xml
@@ -71,6 +71,7 @@
メール送信フォルダ一覧フォルダ再読込
+ フォルダを探すすべてのメールを既読にするアカウント追加作成
@@ -544,14 +545,8 @@ K-9 Mail セットアップにようこそ。\nK-9 は標準のAndroidメール
未読件数の表示通知バーに未読メッセージの件数を表示する
- ナビゲーションボタンのスクロール
- しない
- キーボード利用時
- 常に
-
メール整理ボタンを有効にするアーカイブ、移動、迷惑メールボタンを表示
- メール整理ボタンをスクロール画像を自動で表示しない
@@ -822,6 +817,8 @@ K-9 Mail セットアップにようこそ。\nK-9 は標準のAndroidメール
Q - アカウント設定に戻る\u000A
S - アカウント設定編集
+ フォルダ名に含まれる文字
+
フォルダ表示全フォルダ表示1st クラスフォルダ表示
@@ -973,6 +970,9 @@ K-9 Mail セットアップにようこそ。\nK-9 は標準のAndroidメール
日付本文
+ メッセージ作成
+ 入力テキスト
+
極小かなり小やや小
@@ -1014,6 +1014,9 @@ K-9 Mail セットアップにようこそ。\nK-9 は標準のAndroidメール
メッセージの下書き保存メッセージを保存しますか?
+ メッセージ破棄?
+ このメッセージを破棄しますか?
+
下書き保存の拒否暗号化したメッセージは下書き保存できません
@@ -1102,4 +1105,11 @@ K-9 Mail セットアップにようこそ。\nK-9 は標準のAndroidメール
下に移動アカウントを移動しています
+ 未読件数
+ アカウントの未読件数を表示
+
+ ファイルマネージャがありません
+ 設定をインポートするためのアプリケーションがありません。Androidマーケットからファイルマネージャをインストールしてください。
+ マーケット
+ 閉じる
diff --git a/res/values-ko/strings.xml b/res/values-ko/strings.xml
index bbc375844..8ec6c6e1f 100644
--- a/res/values-ko/strings.xml
+++ b/res/values-ko/strings.xml
@@ -542,14 +542,8 @@ K-9 메일 설치를 환영합니다. K-9은 표준 안드로이드 메일 클
읽지 않은 메일 수 세기읽지 않은 메시지의 수를 상태바에 보여줍니다.
- 네비게이션 버튼을 스크롤
- 하지않음
- 키보드를 이용가능할 경우
- 항상
-
재정리(refile) 버튼 활성화보관, 이동, 스팸 버튼을 보이기
- 재정리(refile) 보튼을 스크롤항상 그림 보기아니오
diff --git a/res/values-nl/strings.xml b/res/values-nl/strings.xml
index d1ffcb412..0aa8f88c4 100644
--- a/res/values-nl/strings.xml
+++ b/res/values-nl/strings.xml
@@ -541,14 +541,8 @@ Welkom bij K-9 Mail setup. K-9 is een open source mail cliënt voor Android, ge
Toon aantal ongelezenToon het aantal ongelezen berichten in de \'notification bar\'.
- Scroll navigatie knoppen
- Nooit
- Wanneer toetsenbord beschikbaar is
- Altijd
-
Inschakelen verplaats knoppenLaat de Archief, Verplaats, en Spam knoppen zien.
- Scroll verplaats knoppenLaat afbeeldingen automatisch zienNooit
diff --git a/res/values-pl/strings.xml b/res/values-pl/strings.xml
index 204fd5c57..654a08cc2 100644
--- a/res/values-pl/strings.xml
+++ b/res/values-pl/strings.xml
@@ -553,14 +553,8 @@ Witaj w K-9 Mail, darmowym programie pocztowym dla systemu Android. Najistotniej
Pokaż liczbę nieprzeczytanychPokaż liczbę nieprzeczytanych wiadomości w pasku powiadomień.
- Przyciski nawigacyjne
- Przyciski nie są nigdy przesuwalne
- Przesuwalne, gdy jest klawiatura
- Przyciski są zawsze przesuwalne
-
Użyj przycisków refile Pokaż przyciski Archiwum, Przenieś, Spam.
- Przewijaj przyciski refile Zawsze pokazuj obrazkiNie
diff --git a/res/values-pt-rBR/strings.xml b/res/values-pt-rBR/strings.xml
index 7af2f917a..9983cac18 100644
--- a/res/values-pt-rBR/strings.xml
+++ b/res/values-pt-rBR/strings.xml
@@ -539,14 +539,8 @@ Bem-vindo à configuração do K-9 Mail. K-9 é um cliente de e-mail com código
Mostrar contagem de não lidasMostrar o número de mensagens não lidas na barra de notificação.
- Navegação com botões de scroll
- Nunca
- Quando teclado estiver disponível
- Sempre
-
Habilitar botão de açõesMostrar botões de Arquivar, Mover e Span.
- Scroll para botões de ação Sempre mostrar imagensNão
diff --git a/res/values-ru/strings.xml b/res/values-ru/strings.xml
index d6089d4bf..fb1452369 100644
--- a/res/values-ru/strings.xml
+++ b/res/values-ru/strings.xml
@@ -533,14 +533,8 @@
Показать количество непрочитанныхПоказать количество непрочитанных в строке уведомлений.
- Прокрутка навигационных кнопок
- Никогда
- Когда присутствует клавиатура
- Всегда
-
Разрешить кнопки переноса сообщенийПоказывает кнопки: Архив, Переместить, Спам.
- Спрятать кнопки перемещенияПоказывать изображенияНикогда
diff --git a/res/values-sv/strings.xml b/res/values-sv/strings.xml
index f33a11d6a..e31e80896 100644
--- a/res/values-sv/strings.xml
+++ b/res/values-sv/strings.xml
@@ -543,14 +543,8 @@ Välkommen till installationen av K-9 E-post. K-9 är en e-postklient med öppen
Visa antal olästaVisar antalet olästa brev i notifieringsytan.
- Scrolla navigationsknappar
- Aldrig
- När ett tangentbord är tillgängligt
- Alltid
-
Aktivera flyttknapparVisa knappar för att arkivera, flytta och spam-markera e-post.
- Scrolla flyttknapparVisa bilder automatisktAldrig
diff --git a/res/values-tr/strings.xml b/res/values-tr/strings.xml
index e18585d0c..f041fc740 100644
--- a/res/values-tr/strings.xml
+++ b/res/values-tr/strings.xml
@@ -516,14 +516,8 @@
Okunmamış sayısını gösterBildirim çubuğunda okunmamış mesaj numarasını göster.
- Gezinme tuşlarını kaydırma
- Asla
- Klavye olduğu zaman
- Daima
-
İşaretleme düğmelerini etkinleştirArşiv, Taşıma ve Spam düğmelerini göster.
- İşaretleme düğmelerini kaydırDaima imajları gösterHayır
diff --git a/res/values-zh-rCN/strings.xml b/res/values-zh-rCN/strings.xml
index 96b44f504..0ba2a3617 100644
--- a/res/values-zh-rCN/strings.xml
+++ b/res/values-zh-rCN/strings.xml
@@ -530,14 +530,8 @@
- 滚动导航按钮
- 从不
- 使用键盘时
- 总是
-
启用整理按钮显示归档、移动和标记为垃圾按钮
- 滚动整理按钮显示图片从不
diff --git a/res/values-zh-rTW/strings.xml b/res/values-zh-rTW/strings.xml
index f0ca10ed2..ed4498dd5 100644
--- a/res/values-zh-rTW/strings.xml
+++ b/res/values-zh-rTW/strings.xml
@@ -539,14 +539,8 @@
顯示未讀郵件數在通知欄上顯示未讀郵件數
- 滾動導航按鈕
- 從不
- 使用鍵盤時
- 總是
-
啟用整理按鈕顯示歸檔、移動和標記為垃圾按鈕
- 滾動整理按鈕顯示圖片從不
@@ -799,7 +793,7 @@
要使用此提供者的IMAP或POP3,請先至Hanmail(Daum)郵箱設置頁設定允許使用IMAP或POP3。要使用此提供者的IMAP或POP3,請先至Paran郵箱設置頁設定允許使用IMAP或POP3。要使用此提供者的IMAP或POP3,請先至Nate郵箱設置頁設定允許使用IMAP或POP3。
-
+
無法識別的證書訊息接收密鑰拒絕密鑰
@@ -1060,5 +1054,5 @@
上移下移
-
+
diff --git a/res/values/arrays.xml b/res/values/arrays.xml
index d33328ccb..8608cb367 100644
--- a/res/values/arrays.xml
+++ b/res/values/arrays.xml
@@ -149,30 +149,6 @@
NOT_SECOND_CLASS
-
- @string/account_settings_hide_buttons_never
- @string/account_settings_hide_buttons_keyboard_avail
- @string/account_settings_hide_buttons_always
-
-
-
- NEVER
- KEYBOARD_AVAILABLE
- ALWAYS
-
-
-
- @string/account_settings_hide_buttons_never
- @string/account_settings_hide_buttons_keyboard_avail
- @string/account_settings_hide_buttons_always
-
-
-
- NEVER
- KEYBOARD_AVAILABLE
- ALWAYS
-
-
@string/account_settings_show_pictures_never@string/account_settings_show_pictures_only_from_contacts
diff --git a/res/values/colors.xml b/res/values/colors.xml
index 766227197..17f7ccb30 100644
--- a/res/values/colors.xml
+++ b/res/values/colors.xml
@@ -1,5 +1,6 @@
- #ffffff
+ #ffffff#eeeeee
+ #1a080808
diff --git a/res/values/strings.xml b/res/values/strings.xml
index f5a059261..dbc9a2ec8 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -292,6 +292,9 @@ Welcome to K-9 Mail setup. K-9 is an open source mail client for Android origin
Unable to save attachment to SD card.Select \"Show pictures\" to display embedded pictures.Show pictures
+ Show message
+ Show attachments
+ More…Fetching attachment.Unable to find viewer for %s.
@@ -552,14 +555,8 @@ Welcome to K-9 Mail setup. K-9 is an open source mail client for Android origin
Show unread countShow the number of unread messages in the notification bar.
- Scroll navigation buttons
- Never
- When keyboard is available
- Always
-
Enable refile buttonsShow the Archive, Move, and Spam buttons.
- Scroll refile buttonsAlways show imagesNo
@@ -1036,6 +1033,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.
@@ -1129,4 +1129,12 @@ Welcome to K-9 Mail setup. K-9 is an open source mail client for Android origin
Move downMoving account...
+ 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/res/xml/account_settings_preferences.xml b/res/xml/account_settings_preferences.xml
index a852549f5..9fa5a11a6 100644
--- a/res/xml/account_settings_preferences.xml
+++ b/res/xml/account_settings_preferences.xml
@@ -70,14 +70,6 @@
android:entryValues="@array/account_settings_show_pictures_values"
android:dialogTitle="@string/account_settings_show_pictures_label" />
-
-
-
-
@@ -233,10 +216,10 @@
android:entryValues="@array/account_settings_message_format_values" />
+ android:persistent="false"
+ android:key="message_read_receipt"
+ android:title="@string/account_settings_message_read_receipt_label"
+ android:summary="@string/account_settings_message_read_receipt_summary" />
+
+
+
+
+
+
+
+
diff --git a/res/xml/unread_widget_info.xml b/res/xml/unread_widget_info.xml
new file mode 100644
index 000000000..f2a7b27b0
--- /dev/null
+++ b/res/xml/unread_widget_info.xml
@@ -0,0 +1,8 @@
+
+
+
diff --git a/src/com/fsck/k9/Account.java b/src/com/fsck/k9/Account.java
index 30a4291ad..361c0b2c8 100644
--- a/src/com/fsck/k9/Account.java
+++ b/src/com/fsck/k9/Account.java
@@ -120,8 +120,6 @@ public class Account implements BaseAccount {
private boolean mSaveAllHeaders;
private boolean mPushPollOnConnect;
private boolean mNotifySync;
- private ScrollButtons mScrollMessageViewButtons;
- private ScrollButtons mScrollMessageViewMoveButtons;
private ShowPictures mShowPictures;
private boolean mEnableMoveButtons;
private boolean mIsSignatureBeforeQuotedText;
@@ -182,10 +180,6 @@ public class Account implements BaseAccount {
NONE, ALL, FIRST_CLASS, FIRST_AND_SECOND_CLASS, NOT_SECOND_CLASS
}
- public enum ScrollButtons {
- NEVER, ALWAYS, KEYBOARD_AVAILABLE
- }
-
public enum ShowPictures {
NEVER, ALWAYS, ONLY_FROM_CONTACTS
}
@@ -218,8 +212,6 @@ public class Account implements BaseAccount {
mFolderSyncMode = FolderMode.FIRST_CLASS;
mFolderPushMode = FolderMode.FIRST_CLASS;
mFolderTargetMode = FolderMode.NOT_SECOND_CLASS;
- mScrollMessageViewButtons = ScrollButtons.NEVER;
- mScrollMessageViewMoveButtons = ScrollButtons.NEVER;
mShowPictures = ShowPictures.NEVER;
mEnableMoveButtons = false;
mIsSignatureBeforeQuotedText = false;
@@ -341,20 +333,6 @@ public class Account implements BaseAccount {
(random.nextInt(0x70) * 0xffff) +
0xff000000);
- try {
- mScrollMessageViewButtons = ScrollButtons.valueOf(prefs.getString(mUuid + ".hideButtonsEnum",
- ScrollButtons.NEVER.name()));
- } catch (Exception e) {
- mScrollMessageViewButtons = ScrollButtons.NEVER;
- }
-
- try {
- mScrollMessageViewMoveButtons = ScrollButtons.valueOf(prefs.getString(mUuid + ".hideMoveButtonsEnum",
- ScrollButtons.NEVER.name()));
- } catch (Exception e) {
- mScrollMessageViewMoveButtons = ScrollButtons.NEVER;
- }
-
try {
mShowPictures = ShowPictures.valueOf(prefs.getString(mUuid + ".showPicturesEnum",
ShowPictures.NEVER.name()));
@@ -617,8 +595,6 @@ public class Account implements BaseAccount {
editor.putString(mUuid + ".spamFolderName", mSpamFolderName);
editor.putString(mUuid + ".autoExpandFolderName", mAutoExpandFolderName);
editor.putInt(mUuid + ".accountNumber", mAccountNumber);
- editor.putString(mUuid + ".hideButtonsEnum", mScrollMessageViewButtons.name());
- editor.putString(mUuid + ".hideMoveButtonsEnum", mScrollMessageViewMoveButtons.name());
editor.putString(mUuid + ".showPicturesEnum", mShowPictures.name());
editor.putBoolean(mUuid + ".enableMoveButtons", mEnableMoveButtons);
editor.putString(mUuid + ".folderDisplayMode", mFolderDisplayMode.name());
@@ -1055,22 +1031,6 @@ public class Account implements BaseAccount {
this.mNotifySync = showOngoing;
}
- public synchronized ScrollButtons getScrollMessageViewButtons() {
- return mScrollMessageViewButtons;
- }
-
- public synchronized void setScrollMessageViewButtons(ScrollButtons scrollMessageViewButtons) {
- mScrollMessageViewButtons = scrollMessageViewButtons;
- }
-
- public synchronized ScrollButtons getScrollMessageViewMoveButtons() {
- return mScrollMessageViewMoveButtons;
- }
-
- public synchronized void setScrollMessageViewMoveButtons(ScrollButtons scrollMessageViewButtons) {
- mScrollMessageViewMoveButtons = scrollMessageViewButtons;
- }
-
public synchronized ShowPictures getShowPictures() {
return mShowPictures;
}
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
diff --git a/src/com/fsck/k9/K9.java b/src/com/fsck/k9/K9.java
index 8f30c6260..80866fd3b 100644
--- a/src/com/fsck/k9/K9.java
+++ b/src/com/fsck/k9/K9.java
@@ -34,6 +34,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;
@@ -478,19 +479,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/SearchAccount.java b/src/com/fsck/k9/SearchAccount.java
index b990e8d09..bf2f25570 100644
--- a/src/com/fsck/k9/SearchAccount.java
+++ b/src/com/fsck/k9/SearchAccount.java
@@ -12,6 +12,39 @@ import com.fsck.k9.mail.Flag;
* is defined by {@link com.fsck.k9.activity.SearchModifier}.
*/
public class SearchAccount implements BaseAccount, SearchSpecification, Serializable {
+ /**
+ * Create a {@code SearchAccount} instance for the Unified Inbox.
+ *
+ * @param context
+ * A {@link Context} instance that will be used to get localized strings and will be
+ * passed on to the {@code SearchAccount} instance.
+ *
+ * @return The {@link SearchAccount} instance for the Unified Inbox.
+ */
+ public static SearchAccount createUnifiedInboxAccount(Context context) {
+ SearchAccount unifiedInbox = new SearchAccount(context, true, null, null);
+ unifiedInbox.setDescription(context.getString(R.string.integrated_inbox_title));
+ unifiedInbox.setEmail(context.getString(R.string.integrated_inbox_detail));
+ return unifiedInbox;
+ }
+
+ /**
+ * Create a {@code SearchAccount} instance for the special account "All messages".
+ *
+ * @param context
+ * A {@link Context} instance that will be used to get localized strings and will be
+ * passed on to the {@code SearchAccount} instance.
+ *
+ * @return The {@link SearchAccount} instance for the Unified Inbox.
+ */
+ public static SearchAccount createAllMessagesAccount(Context context) {
+ SearchAccount allMessages = new SearchAccount(context, false, null, null);
+ allMessages.setDescription(context.getString(R.string.search_all_messages_title));
+ allMessages.setEmail(context.getString(R.string.search_all_messages_detail));
+ return allMessages;
+ }
+
+
private static final long serialVersionUID = -4388420303235543976L;
private Flag[] mRequiredFlags = null;
private Flag[] mForbiddenFlags = null;
diff --git a/src/com/fsck/k9/activity/AccountList.java b/src/com/fsck/k9/activity/AccountList.java
new file mode 100644
index 000000000..52c64dbe3
--- /dev/null
+++ b/src/com/fsck/k9/activity/AccountList.java
@@ -0,0 +1,187 @@
+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;
+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;
+
+
+/**
+ * Activity displaying the list of accounts.
+ *
+ *
+ * Classes extending this abstract class have to provide an {@link #onAccountSelected(BaseAccount)}
+ * 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();
+ }
+
+ @Override
+ public void onItemClick(AdapterView> parent, View view, int position, long id) {
+ BaseAccount account = (BaseAccount) parent.getItemAtPosition(position);
+ onAccountSelected(account);
+ }
+
+ /**
+ * Create a new {@link AccountsAdapter} instance and assign it to the {@link ListView}.
+ *
+ * @param realAccounts
+ * An array of accounts to display.
+ */
+ public void populateListView(Account[] realAccounts) {
+ List accounts = new ArrayList();
+
+ if (displaySpecialAccounts() && !K9.isHideSpecialAccounts()) {
+ BaseAccount unifiedInboxAccount = SearchAccount.createUnifiedInboxAccount(this);
+ BaseAccount allMessagesAccount = SearchAccount.createAllMessagesAccount(this);
+
+ accounts.add(unifiedInboxAccount);
+ accounts.add(allMessagesAccount);
+ }
+
+ 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(BaseAccount account);
+
+ class AccountsAdapter extends ArrayAdapter {
+ public AccountsAdapter(List accounts) {
+ super(AccountList.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);
+ 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);
+
+ 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,
+ 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/Accounts.java b/src/com/fsck/k9/activity/Accounts.java
index 35ac53c22..6f9eb3d53 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;
@@ -107,9 +108,21 @@ 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 = "https://market.android.com/search?q=oi+file+manager&c=apps";
+
+ /**
+ * 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;
+ private static final int DIALOG_NO_FILE_MANAGER = 4;
+
private ConcurrentHashMap accountStats = new ConcurrentHashMap();
private ConcurrentHashMap pendingWork = new ConcurrentHashMap();
@@ -326,13 +339,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();
@@ -375,6 +382,14 @@ public class Accounts extends K9ListActivity implements OnItemClickListener, OnC
}
}
+ /**
+ * Creates and initializes the special accounts ('Unified Inbox' and 'All Messages')
+ */
+ private void createSpecialAccounts() {
+ integratedInboxAccount = SearchAccount.createUnifiedInboxAccount(this);
+ unreadAccount = SearchAccount.createAllMessagesAccount(this);
+ }
+
@SuppressWarnings("unchecked")
private void restoreAccountStats(Bundle icicle) {
if (icicle != null) {
@@ -459,9 +474,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 {
@@ -971,6 +990,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);
}
@@ -992,6 +1025,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);
@@ -1244,7 +1280,16 @@ 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 {
+ showDialog(DIALOG_NO_FILE_MANAGER);
+ }
}
@Override
diff --git a/src/com/fsck/k9/activity/FolderList.java b/src/com/fsck/k9/activity/FolderList.java
index 6f8640761..6e272f95c 100644
--- a/src/com/fsck/k9/activity/FolderList.java
+++ b/src/com/fsck/k9/activity/FolderList.java
@@ -733,6 +733,15 @@ public class FolderList extends K9ListActivity {
menu.findItem(R.id.expunge).setVisible(false);
}
+ if (!MessagingController.getInstance(getApplication()).isMoveCapable(mAccount)) {
+ // FIXME: Really we want to do this for all local-only folders
+ if (!mAccount.getInboxFolderName().equals(folder.name)) {
+ menu.findItem(R.id.check_mail).setVisible(false);
+ }
+
+ menu.findItem(R.id.expunge).setVisible(false);
+ }
+
menu.setHeaderTitle(folder.displayName);
}
diff --git a/src/com/fsck/k9/activity/K9Activity.java b/src/com/fsck/k9/activity/K9Activity.java
index 4cbca5b09..e9a68eccf 100644
--- a/src/com/fsck/k9/activity/K9Activity.java
+++ b/src/com/fsck/k9/activity/K9Activity.java
@@ -6,24 +6,21 @@ import java.util.Locale;
import android.app.Activity;
import android.content.Context;
import android.content.res.Configuration;
+import android.content.res.TypedArray;
import android.os.Bundle;
import android.util.Log;
import android.view.GestureDetector;
import android.view.GestureDetector.SimpleOnGestureListener;
import android.view.MotionEvent;
-import android.view.View;
import android.view.animation.AccelerateInterpolator;
import android.view.animation.Animation;
import android.view.animation.TranslateAnimation;
import com.fsck.k9.K9;
-import com.fsck.k9.view.ToggleScrollView;
public class K9Activity extends Activity {
private GestureDetector gestureDetector;
- protected ToggleScrollView mTopView;
-
@Override
public void onCreate(Bundle icicle) {
onCreate(icicle, true);
@@ -144,22 +141,6 @@ public class K9Activity extends Activity {
private static final float SWIPE_MAX_OFF_PATH_DIP = 250f;
private static final float SWIPE_THRESHOLD_VELOCITY_DIP = 325f;
- @Override
- public boolean onDoubleTap(MotionEvent ev) {
- super.onDoubleTap(ev);
- if (mTopView != null) {
- int height = getResources().getDisplayMetrics().heightPixels;
- if (ev.getRawY() < (height / 4)) {
- mTopView.fullScroll(View.FOCUS_UP);
-
- } else if (ev.getRawY() > (height - height / 4)) {
- mTopView.fullScroll(View.FOCUS_DOWN);
-
- }
- }
- return false;
- }
-
@Override
public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
// Do fling-detection if gestures are force-enabled or we have system-wide gestures enabled.
@@ -169,11 +150,11 @@ public class K9Activity extends Activity {
final float mGestureScale = getResources().getDisplayMetrics().density;
final int minVelocity = (int)(SWIPE_THRESHOLD_VELOCITY_DIP * mGestureScale + 0.5f);
final int maxOffPath = (int)(SWIPE_MAX_OFF_PATH_DIP * mGestureScale + 0.5f);
-
+
// Calculate how much was actually swiped.
final float deltaX = e2.getX() - e1.getX();
final float deltaY = e2.getY() - e1.getY();
-
+
// Calculate the minimum distance required for this to be considered a swipe.
final int minDistance = (int)Math.abs(deltaY * 4);
@@ -216,4 +197,14 @@ public class K9Activity extends Activity {
return false;
}
}
+
+ public int getThemeBackgroundColor() {
+ TypedArray array = getTheme().obtainStyledAttributes(new int[] {
+ android.R.attr.colorBackground,
+ });
+ int backgroundColor = array.getColor(0, 0xFF00FF);
+ array.recycle();
+ return backgroundColor;
+ }
+
}
diff --git a/src/com/fsck/k9/activity/LauncherShortcuts.java b/src/com/fsck/k9/activity/LauncherShortcuts.java
index f6e01c1ab..61bde33ca 100644
--- a/src/com/fsck/k9/activity/LauncherShortcuts.java
+++ b/src/com/fsck/k9/activity/LauncherShortcuts.java
@@ -3,53 +3,42 @@ package com.fsck.k9.activity;
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.FontSizes;
-import com.fsck.k9.K9;
-import com.fsck.k9.Preferences;
+import com.fsck.k9.BaseAccount;
import com.fsck.k9.R;
+import com.fsck.k9.SearchSpecification;
-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.launcher_shortcuts);
- ListView listView = getListView();
- listView.setOnItemClickListener(this);
- listView.setItemsCanFocus(false);
-
- refresh();
+ super.onCreate(icicle);
}
- private void refresh() {
- Account[] accounts = Preferences.getPreferences(this).getAccounts();
-
- mAdapter = new AccountsAdapter(accounts);
- getListView().setAdapter(mAdapter);
+ @Override
+ protected boolean displaySpecialAccounts() {
+ return true;
}
- private void setupShortcut(Account account) {
- final Intent shortcutIntent = FolderList.actionHandleAccountIntent(this, account, null, true);
+ @Override
+ protected void onAccountSelected(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);
+ }
+
+ shortcutIntent.addFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT);
Intent intent = new Intent();
intent.putExtra(Intent.EXTRA_SHORTCUT_INTENT, shortcutIntent);
String description = account.getDescription();
@@ -63,66 +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) {
- Account account = (Account) parent.getItemAtPosition(position);
- setupShortcut(account);
- }
-
- class AccountsAdapter extends ArrayAdapter {
- public AccountsAdapter(Account[] accounts) {
- super(LauncherShortcuts.this, 0, accounts);
- }
-
- @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);
- }
-
- 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;
- }
- }
}
diff --git a/src/com/fsck/k9/activity/MessageCompose.java b/src/com/fsck/k9/activity/MessageCompose.java
index 51dedf16c..bd9170841 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;
@@ -1609,7 +1610,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;
@@ -2050,13 +2051,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();
+ }
}
}
@@ -2117,6 +2126,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);
}
@@ -2678,7 +2708,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.
@@ -2987,9 +3017,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/activity/MessageList.java b/src/com/fsck/k9/activity/MessageList.java
index 29c00a1c3..8bcd37a97 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;
@@ -70,6 +72,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;
@@ -616,7 +619,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) {
@@ -632,6 +639,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);
}
@@ -1388,13 +1402,21 @@ public class MessageList
}
private void onToggleRead(MessageInfoHolder holder) {
- mController.setFlag(holder.message.getFolder().getAccount(), holder.message.getFolder().getRemoteName(), new String[] { holder.uid }, Flag.SEEN, !holder.read);
+ LocalMessage message = holder.message;
+ Folder folder = message.getFolder();
+ Account account = folder.getAccount();
+ String folderName = folder.getRemoteName();
+ 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().getRemoteName(), new String[] { holder.uid }, Flag.FLAGGED, !holder.flagged);
+ LocalMessage message = holder.message;
+ Folder folder = message.getFolder();
+ Account account = folder.getAccount();
+ String folderName = folder.getRemoteName();
+ mController.setFlag(account, folderName, new Message[] { message }, Flag.FLAGGED, !holder.flagged);
holder.flagged = !holder.flagged;
mHandler.sortMessages();
}
@@ -1593,6 +1615,19 @@ public class MessageList
if (K9.FOLDER_NONE.equalsIgnoreCase(mAccount.getSpamFolderName())) {
menu.findItem(R.id.batch_spam_op).setVisible(false);
}
+
+ if (!mController.isMoveCapable(mAccount)) {
+ // FIXME: Really we want to do this for all local-only folders
+ if (mCurrentFolder != null &&
+ !mAccount.getInboxFolderName().equals(mCurrentFolder.name)) {
+ menu.findItem(R.id.check_mail).setVisible(false);
+ }
+ menu.findItem(R.id.batch_archive_op).setVisible(false);
+ menu.findItem(R.id.batch_spam_op).setVisible(false);
+ menu.findItem(R.id.batch_move_op).setVisible(false);
+ menu.findItem(R.id.batch_copy_op).setVisible(false);
+ menu.findItem(R.id.expunge).setVisible(false);
+ }
}
boolean newFlagState = computeBatchDirection(true);
diff --git a/src/com/fsck/k9/activity/MessageView.java b/src/com/fsck/k9/activity/MessageView.java
index 67657f8a1..4fcebec4a 100644
--- a/src/com/fsck/k9/activity/MessageView.java
+++ b/src/com/fsck/k9/activity/MessageView.java
@@ -4,11 +4,9 @@ import android.app.Dialog;
import android.app.ProgressDialog;
import android.content.Context;
import android.content.Intent;
-import android.content.res.Configuration;
import android.net.Uri;
import android.os.Bundle;
import android.os.Handler;
-import android.util.Config;
import android.util.Log;
import android.view.*;
import android.view.View.OnClickListener;
@@ -20,9 +18,9 @@ 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;
import com.fsck.k9.view.SingleMessageView;
import com.fsck.k9.view.AttachmentView.AttachmentFileDownloadCallback;
@@ -34,8 +32,6 @@ public class MessageView extends K9Activity implements OnClickListener {
private static final String EXTRA_MESSAGE_REFERENCES = "com.fsck.k9.MessageView_messageReferences";
private static final String EXTRA_NEXT = "com.fsck.k9.MessageView_next";
private static final String EXTRA_MESSAGE_LIST_EXTRAS = "com.fsck.k9.MessageView_messageListExtras";
- private static final String EXTRA_SCROLL_PERCENTAGE = "com.fsck.k9.MessageView_scrollPercentage";
- private static final String SHOW_PICTURES = "showPictures";
private static final String STATE_PGP_DATA = "pgpData";
private static final int ACTIVITY_CHOOSE_FOLDER_MOVE = 1;
private static final int ACTIVITY_CHOOSE_FOLDER_COPY = 2;
@@ -45,7 +41,6 @@ public class MessageView extends K9Activity implements OnClickListener {
private PgpData mPgpData;
-
private View mNext;
private View mPrevious;
private View mDelete;
@@ -106,24 +101,6 @@ public class MessageView extends K9Activity implements OnClickListener {
}
}
-
- @Override
- public boolean dispatchTouchEvent(MotionEvent ev) {
- if (ev.getAction() == MotionEvent.ACTION_UP) {
- // Text selection is finished. Allow scrolling again.
- mTopView.setScrolling(true);
- } else if (K9.zoomControlsEnabled()) {
- // If we have system zoom controls enabled, disable scrolling so the screen isn't wiggling around while
- // trying to zoom.
- if (ev.getAction() == MotionEvent.ACTION_POINTER_2_DOWN) {
- mTopView.setScrolling(false);
- } else if (ev.getAction() == MotionEvent.ACTION_POINTER_2_UP) {
- mTopView.setScrolling(true);
- }
- }
- return super.dispatchTouchEvent(ev);
- }
-
@Override
public boolean dispatchKeyEvent(KeyEvent event) {
boolean ret = false;
@@ -167,15 +144,6 @@ public class MessageView extends K9Activity implements OnClickListener {
}
break;
}
- case KeyEvent.KEYCODE_SHIFT_LEFT:
- case KeyEvent.KEYCODE_SHIFT_RIGHT: {
- /*
- * Selecting text started via shift key. Disable scrolling as
- * this causes problems when selecting text.
- */
- mTopView.setScrolling(false);
- break;
- }
case KeyEvent.KEYCODE_DEL: {
onDelete();
return true;
@@ -326,7 +294,6 @@ public class MessageView extends K9Activity implements OnClickListener {
requestWindowFeature(Window.FEATURE_NO_TITLE);
setContentView(R.layout.message_view);
- mTopView = (ToggleScrollView) findViewById(R.id.top_view);
mMessageView = (SingleMessageView) findViewById(R.id.message_view);
//set a callback for the attachment view. With this callback the attachmentview
@@ -358,10 +325,6 @@ public class MessageView extends K9Activity implements OnClickListener {
mMessageView.initialize(this);
- // Register the ScrollView's listener to handle scrolling to last known location on resume.
- mController.addListener(mTopView.getListener());
- mMessageView.setListeners(mController.getListeners());
-
setTitle("");
final Intent intent = getIntent();
@@ -433,57 +396,29 @@ public class MessageView extends K9Activity implements OnClickListener {
setOnClickListener(R.id.archive);
setOnClickListener(R.id.move);
setOnClickListener(R.id.spam);
- // To show full header
- setOnClickListener(R.id.header_container);
- setOnClickListener(R.id.reply_scrolling);
-// setOnClickListener(R.id.reply_all_scrolling);
- setOnClickListener(R.id.delete_scrolling);
- setOnClickListener(R.id.forward_scrolling);
- setOnClickListener(R.id.next_scrolling);
- setOnClickListener(R.id.previous_scrolling);
- setOnClickListener(R.id.archive_scrolling);
- setOnClickListener(R.id.move_scrolling);
- setOnClickListener(R.id.spam_scrolling);
- setOnClickListener(R.id.show_pictures);
setOnClickListener(R.id.download_remainder);
- // Perhaps the ScrollButtons should be global, instead of account-specific
- Account.ScrollButtons scrollButtons = mAccount.getScrollMessageViewButtons();
- if ((Account.ScrollButtons.ALWAYS == scrollButtons)
- || (Account.ScrollButtons.KEYBOARD_AVAILABLE == scrollButtons &&
- (this.getResources().getConfiguration().hardKeyboardHidden == Configuration.HARDKEYBOARDHIDDEN_NO))) {
- scrollButtons();
- } else { // never or the keyboard is open
- staticButtons();
- }
- Account.ScrollButtons scrollMoveButtons = mAccount.getScrollMessageViewMoveButtons();
- if ((Account.ScrollButtons.ALWAYS == scrollMoveButtons)
- || (Account.ScrollButtons.KEYBOARD_AVAILABLE == scrollMoveButtons &&
- (this.getResources().getConfiguration().hardKeyboardHidden == Configuration.HARDKEYBOARDHIDDEN_NO))) {
- scrollMoveButtons();
- } else {
- staticMoveButtons();
- }
+ mNext = findViewById(R.id.next);
+ mPrevious = findViewById(R.id.previous);
+ mDelete = findViewById(R.id.delete);
+
+ mArchive = findViewById(R.id.archive);
+ mMove = findViewById(R.id.move);
+ mSpam = findViewById(R.id.spam);
+
if (!mAccount.getEnableMoveButtons()) {
View buttons = findViewById(R.id.move_buttons);
- if (buttons != null) {
- buttons.setVisibility(View.GONE);
- }
- buttons = findViewById(R.id.scrolling_move_buttons);
- if (buttons != null) {
- buttons.setVisibility(View.GONE);
- }
+ buttons.setVisibility(View.GONE);
}
}
@Override
protected void onSaveInstanceState(Bundle outState) {
+ super.onSaveInstanceState(outState);
outState.putParcelable(EXTRA_MESSAGE_REFERENCE, mMessageReference);
outState.putParcelableArrayList(EXTRA_MESSAGE_REFERENCES, mMessageReferences);
outState.putSerializable(STATE_PGP_DATA, mPgpData);
- outState.putBoolean(SHOW_PICTURES, mMessageView.showPictures());
- outState.putDouble(EXTRA_SCROLL_PERCENTAGE, mTopView.getScrollPercentage());
}
@Override
@@ -491,8 +426,6 @@ public class MessageView extends K9Activity implements OnClickListener {
super.onRestoreInstanceState(savedInstanceState);
mPgpData = (PgpData) savedInstanceState.getSerializable(STATE_PGP_DATA);
mMessageView.updateCryptoLayout(mAccount.getCryptoProvider(), mPgpData, mMessage);
- mMessageView.setLoadPictures(savedInstanceState.getBoolean(SHOW_PICTURES));
- mTopView.setScrollPercentage(savedInstanceState.getDouble(EXTRA_SCROLL_PERCENTAGE));
}
private void displayMessage(MessageReference ref) {
@@ -500,22 +433,18 @@ public class MessageView extends K9Activity implements OnClickListener {
if (K9.DEBUG)
Log.d(K9.LOG_TAG, "MessageView displaying message " + mMessageReference);
mAccount = Preferences.getPreferences(this).getAccount(mMessageReference.accountUuid);
- clearMessageDisplay();
findSurroundingMessagesUid();
// start with fresh, empty PGP data
mPgpData = new PgpData();
- mTopView.setVisibility(View.VISIBLE);
+
+ // Clear previous message
+ mMessageView.resetView();
+ mMessageView.resetHeaderView();
+
mController.loadMessageForView(mAccount, mMessageReference.folderName, mMessageReference.uid, mListener);
setupDisplayMessageButtons();
}
- private void clearMessageDisplay() {
- mTopView.setVisibility(View.GONE);
- mTopView.scrollTo(0, 0);
- mMessageView.resetView();
-
- }
-
private void setupDisplayMessageButtons() {
mDelete.setEnabled(true);
mNext.setEnabled(mNextMessage != null);
@@ -533,45 +462,6 @@ public class MessageView extends K9Activity implements OnClickListener {
disableMoveButtons();
}
}
- private void staticButtons() {
- View buttons = findViewById(R.id.scrolling_buttons);
- if (buttons != null) {
- buttons.setVisibility(View.GONE);
- }
- mNext = findViewById(R.id.next);
- mPrevious = findViewById(R.id.previous);
- mDelete = findViewById(R.id.delete);
- }
-
- private void scrollButtons() {
- View buttons = findViewById(R.id.bottom_buttons);
- if (buttons != null) {
- buttons.setVisibility(View.GONE);
- }
- mNext = findViewById(R.id.next_scrolling);
- mPrevious = findViewById(R.id.previous_scrolling);
- mDelete = findViewById(R.id.delete_scrolling);
- }
-
- private void staticMoveButtons() {
- View buttons = findViewById(R.id.scrolling_move_buttons);
- if (buttons != null) {
- buttons.setVisibility(View.GONE);
- }
- mArchive = findViewById(R.id.archive);
- mMove = findViewById(R.id.move);
- mSpam = findViewById(R.id.spam);
- }
-
- private void scrollMoveButtons() {
- View buttons = findViewById(R.id.move_buttons);
- if (buttons != null) {
- buttons.setVisibility(View.GONE);
- }
- mArchive = findViewById(R.id.archive_scrolling);
- mMove = findViewById(R.id.move_scrolling);
- mSpam = findViewById(R.id.spam_scrolling);
- }
private void disableButtons() {
mMessageView.setLoadPictures(false);
@@ -612,13 +502,11 @@ public class MessageView extends K9Activity implements OnClickListener {
onAccountUnavailable();
return;
}
- mController.addListener(mTopView.getListener());
StorageManager.getInstance(getApplication()).addListener(mStorageListener);
}
@Override
protected void onPause() {
- mController.removeListener(mTopView.getListener());
StorageManager.getInstance(getApplication()).removeListener(mStorageListener);
super.onPause();
}
@@ -728,14 +616,10 @@ public class MessageView extends K9Activity implements OnClickListener {
private void onFlag() {
if (mMessage != null) {
- mController.setFlag(mAccount,
- mMessage.getFolder().getRemoteName(), new String[] {mMessage.getUid()}, Flag.FLAGGED, !mMessage.isSet(Flag.FLAGGED));
- try {
- mMessage.setFlag(Flag.FLAGGED, !mMessage.isSet(Flag.FLAGGED));
- mMessageView.setHeaders(mMessage, mAccount);
- } catch (MessagingException me) {
- Log.e(K9.LOG_TAG, "Could not set flag on local message", me);
- }
+ boolean newState = !mMessage.isSet(Flag.FLAGGED);
+ mController.setFlag(mAccount, mMessage.getFolder().getRemoteName(),
+ new Message[] { mMessage }, Flag.FLAGGED, newState);
+ mMessageView.setHeaders(mMessage, mAccount);
}
}
@@ -846,7 +730,6 @@ public class MessageView extends K9Activity implements OnClickListener {
protected void onNext() {
// Reset scroll percentage when we change messages
- mTopView.setScrollPercentage(0);
if (mNextMessage == null) {
Toast.makeText(this, getString(R.string.end_of_folder), Toast.LENGTH_SHORT).show();
return;
@@ -854,7 +737,7 @@ public class MessageView extends K9Activity implements OnClickListener {
mLastDirection = NEXT;
disableButtons();
if (K9.showAnimations()) {
- mTopView.startAnimation(outToLeftAnimation());
+ mMessageView.startAnimation(outToLeftAnimation());
}
displayMessage(mNextMessage);
mNext.requestFocus();
@@ -862,7 +745,6 @@ public class MessageView extends K9Activity implements OnClickListener {
protected void onPrevious() {
// Reset scroll percentage when we change messages
- mTopView.setScrollPercentage(0);
if (mPreviousMessage == null) {
Toast.makeText(this, getString(R.string.end_of_folder), Toast.LENGTH_SHORT).show();
return;
@@ -870,7 +752,7 @@ public class MessageView extends K9Activity implements OnClickListener {
mLastDirection = PREVIOUS;
disableButtons();
if (K9.showAnimations()) {
- mTopView.startAnimation(inFromRightAnimation());
+ mMessageView.startAnimation(inFromRightAnimation());
}
displayMessage(mPreviousMessage);
mPrevious.requestFocus();
@@ -878,15 +760,11 @@ 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);
- try {
- mMessage.setFlag(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);
- }
+ mController.setFlag(mAccount, mMessage.getFolder().getName(),
+ new Message[] { mMessage }, Flag.SEEN, false);
+ mMessageView.setHeaders(mMessage, mAccount);
+ String subject = mMessage.getSubject();
+ setTitle(subject);
}
}
@@ -903,46 +781,35 @@ public class MessageView extends K9Activity implements OnClickListener {
public void onClick(View view) {
switch (view.getId()) {
case R.id.reply:
- case R.id.reply_scrolling:
onReply();
break;
case R.id.reply_all:
onReplyAll();
break;
case R.id.delete:
- case R.id.delete_scrolling:
onDelete();
break;
case R.id.forward:
- case R.id.forward_scrolling:
onForward();
break;
case R.id.archive:
- case R.id.archive_scrolling:
onRefile(mAccount.getArchiveFolderName());
break;
case R.id.spam:
- case R.id.spam_scrolling:
onRefile(mAccount.getSpamFolderName());
break;
case R.id.move:
- case R.id.move_scrolling:
onMove();
break;
case R.id.next:
- case R.id.next_scrolling:
onNext();
break;
case R.id.previous:
- case R.id.previous_scrolling:
onPrevious();
break;
case R.id.download:
((AttachmentView)view).saveFile();
break;
- case R.id.show_pictures:
- mMessageView.setLoadPictures(true);
- break;
case R.id.download_remainder:
onDownloadRemainder();
break;
@@ -994,7 +861,6 @@ public class MessageView extends K9Activity implements OnClickListener {
});
break;
case R.id.select_text:
- mTopView.setScrolling(false);
mMessageView.beginSelectingText();
break;
default:
@@ -1083,29 +949,7 @@ public class MessageView extends K9Activity implements OnClickListener {
}
return super.onPrepareOptionsMenu(menu);
}
-
- public void displayMessageBody(final Account account, final String folder, final String uid, final Message message) {
- runOnUiThread(new Runnable() {
- public void run() {
- mTopView.scrollTo(0, 0);
- try {
- if (MessageView.this.mMessage != null
- && MessageView.this.mMessage.isSet(Flag.X_DOWNLOADED_PARTIAL)
- && message.isSet(Flag.X_DOWNLOADED_FULL)) {
- mMessageView.setHeaders(message, account);
- }
- MessageView.this.mMessage = message;
- mMessageView.displayMessageBody(account, folder, uid, message, mPgpData);
- mMessageView.renderAttachments(mMessage, 0, mMessage, mAccount, mController, mListener);
- } catch (MessagingException e) {
- if (Config.LOGV) {
- Log.v(K9.LOG_TAG, "loadMessageForViewBodyAvailable", e);
- }
- }
- }
- });
- }
-
+
class Listener extends MessagingListener {
@Override
public void loadMessageForViewHeadersAvailable(final Account account, String folder, String uid,
@@ -1114,7 +958,6 @@ public class MessageView extends K9Activity implements OnClickListener {
|| !mMessageReference.accountUuid.equals(account.getUuid())) {
return;
}
- MessageView.this.mMessage = message;
/*
* Clone the message object because the original could be modified by
@@ -1148,17 +991,28 @@ public class MessageView extends K9Activity implements OnClickListener {
}
@Override
- public void loadMessageForViewBodyAvailable(Account account, String folder, String uid,
- Message message) {
- if (!mMessageReference.uid.equals(uid) || !mMessageReference.folderName.equals(folder)
- || !mMessageReference.accountUuid.equals(account.getUuid())) {
+ public void loadMessageForViewBodyAvailable(final Account account, String folder,
+ String uid, final Message message) {
+ if (!mMessageReference.uid.equals(uid) ||
+ !mMessageReference.folderName.equals(folder) ||
+ !mMessageReference.accountUuid.equals(account.getUuid())) {
return;
}
- displayMessageBody(account, folder, uid, message);
- }//loadMessageForViewBodyAvailable
-
+ runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ try {
+ mMessage = message;
+ mMessageView.setMessage(account, (LocalMessage) message, mPgpData,
+ mController, mListener);
+ } catch (MessagingException e) {
+ Log.v(K9.LOG_TAG, "loadMessageForViewBodyAvailable", e);
+ }
+ }
+ });
+ }
@Override
public void loadMessageForViewFailed(Account account, String folder, String uid, final Throwable t) {
@@ -1263,8 +1117,14 @@ public class MessageView extends K9Activity implements OnClickListener {
// This REALLY should be in MessageCryptoView
public void onDecryptDone(PgpData pgpData) {
- // TODO: this might not be enough if the orientation was changed while in APG,
- // sometimes shows the original encrypted content
- mMessageView.loadBodyFromText(mAccount.getCryptoProvider(), mPgpData, mMessage, mPgpData.getDecryptedData(), "text/plain");
+ Account account = mAccount;
+ LocalMessage message = (LocalMessage) mMessage;
+ MessagingController controller = mController;
+ Listener listener = mListener;
+ try {
+ mMessageView.setMessage(account, message, pgpData, controller, listener);
+ } catch (MessagingException e) {
+ Log.e(K9.LOG_TAG, "displayMessageBody failed", e);
+ }
}
}
diff --git a/src/com/fsck/k9/activity/UnreadWidgetConfiguration.java b/src/com/fsck/k9/activity/UnreadWidgetConfiguration.java
new file mode 100644
index 000000000..341f895ef
--- /dev/null
+++ b/src/com/fsck/k9/activity/UnreadWidgetConfiguration.java
@@ -0,0 +1,106 @@
+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.BaseAccount;
+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 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);
+
+ // 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/activity/setup/AccountSettings.java b/src/com/fsck/k9/activity/setup/AccountSettings.java
index 1c3f77099..97bb6c9c4 100644
--- a/src/com/fsck/k9/activity/setup/AccountSettings.java
+++ b/src/com/fsck/k9/activity/setup/AccountSettings.java
@@ -18,7 +18,6 @@ import java.util.List;
import com.fsck.k9.Account;
import com.fsck.k9.Account.FolderMode;
import com.fsck.k9.Account.QuoteStyle;
-import com.fsck.k9.Account.ScrollButtons;
import com.fsck.k9.K9;
import com.fsck.k9.NotificationSetting;
import com.fsck.k9.Preferences;
@@ -54,8 +53,6 @@ public class AccountSettings extends K9PreferenceActivity {
private static final String PREFERENCE_FREQUENCY = "account_check_frequency";
private static final String PREFERENCE_DISPLAY_COUNT = "account_display_count";
private static final String PREFERENCE_DEFAULT = "account_default";
- private static final String PREFERENCE_HIDE_BUTTONS = "hide_buttons_enum";
- private static final String PREFERENCE_HIDE_MOVE_BUTTONS = "hide_move_buttons_enum";
private static final String PREFERENCE_SHOW_PICTURES = "show_pictures_enum";
private static final String PREFERENCE_ENABLE_MOVE_BUTTONS = "enable_move_buttons";
private static final String PREFERENCE_NOTIFY = "account_notify";
@@ -100,7 +97,7 @@ public class AccountSettings extends K9PreferenceActivity {
private static final String PREFERENCE_LOCAL_STORAGE_PROVIDER = "local_storage_provider";
-
+ private static final String PREFERENCE_CATEGORY_FOLDERS = "folders";
private static final String PREFERENCE_ARCHIVE_FOLDER = "archive_folder";
private static final String PREFERENCE_DRAFTS_FOLDER = "drafts_folder";
private static final String PREFERENCE_SENT_FOLDER = "sent_folder";
@@ -124,8 +121,6 @@ public class AccountSettings extends K9PreferenceActivity {
private CheckBoxPreference mAccountDefault;
private CheckBoxPreference mAccountNotify;
private CheckBoxPreference mAccountNotifySelf;
- private ListPreference mAccountScrollButtons;
- private ListPreference mAccountScrollMoveButtons;
private ListPreference mAccountShowPictures;
private CheckBoxPreference mAccountEnableMoveButtons;
private CheckBoxPreference mAccountNotifySync;
@@ -428,37 +423,10 @@ public class AccountSettings extends K9PreferenceActivity {
mAccountDefault.setChecked(
mAccount.equals(Preferences.getPreferences(this).getDefaultAccount()));
- mAccountScrollButtons = (ListPreference) findPreference(PREFERENCE_HIDE_BUTTONS);
- mAccountScrollButtons.setValue("" + mAccount.getScrollMessageViewButtons());
- mAccountScrollButtons.setSummary(mAccountScrollButtons.getEntry());
- mAccountScrollButtons.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
- public boolean onPreferenceChange(Preference preference, Object newValue) {
- final String summary = newValue.toString();
- int index = mAccountScrollButtons.findIndexOfValue(summary);
- mAccountScrollButtons.setSummary(mAccountScrollButtons.getEntries()[index]);
- mAccountScrollButtons.setValue(summary);
- return false;
- }
- });
-
mAccountEnableMoveButtons = (CheckBoxPreference) findPreference(PREFERENCE_ENABLE_MOVE_BUTTONS);
mAccountEnableMoveButtons.setEnabled(mIsMoveCapable);
mAccountEnableMoveButtons.setChecked(mAccount.getEnableMoveButtons());
- mAccountScrollMoveButtons = (ListPreference) findPreference(PREFERENCE_HIDE_MOVE_BUTTONS);
- mAccountScrollMoveButtons.setEnabled(mIsMoveCapable);
- mAccountScrollMoveButtons.setValue("" + mAccount.getScrollMessageViewMoveButtons());
- mAccountScrollMoveButtons.setSummary(mAccountScrollMoveButtons.getEntry());
- mAccountScrollMoveButtons.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
- public boolean onPreferenceChange(Preference preference, Object newValue) {
- final String summary = newValue.toString();
- int index = mAccountScrollMoveButtons.findIndexOfValue(summary);
- mAccountScrollMoveButtons.setSummary(mAccountScrollMoveButtons.getEntries()[index]);
- mAccountScrollMoveButtons.setValue(summary);
- return false;
- }
- });
-
mAccountShowPictures = (ListPreference) findPreference(PREFERENCE_SHOW_PICTURES);
mAccountShowPictures.setValue("" + mAccount.getShowPictures());
mAccountShowPictures.setSummary(mAccountShowPictures.getEntry());
@@ -745,11 +713,13 @@ public class AccountSettings extends K9PreferenceActivity {
else
mAccount.setAutoExpandFolderName(reverseTranslateFolder(mAutoExpandFolder.getValue()));
- mAccount.setArchiveFolderName(mArchiveFolder.getValue());
- mAccount.setDraftsFolderName(mDraftsFolder.getValue());
- mAccount.setSentFolderName(mSentFolder.getValue());
- mAccount.setSpamFolderName(mSpamFolder.getValue());
- mAccount.setTrashFolderName(mTrashFolder.getValue());
+ if (mIsMoveCapable) {
+ mAccount.setArchiveFolderName(mArchiveFolder.getValue());
+ mAccount.setDraftsFolderName(mDraftsFolder.getValue());
+ mAccount.setSentFolderName(mSentFolder.getValue());
+ mAccount.setSpamFolderName(mSpamFolder.getValue());
+ mAccount.setTrashFolderName(mTrashFolder.getValue());
+ }
if (mIsPushCapable) {
@@ -760,10 +730,8 @@ public class AccountSettings extends K9PreferenceActivity {
if (!mIsMoveCapable) {
mAccount.setEnableMoveButtons(false);
- mAccount.setScrollMessageViewMoveButtons(ScrollButtons.NEVER);
} else {
mAccount.setEnableMoveButtons(mAccountEnableMoveButtons.isChecked());
- mAccount.setScrollMessageViewMoveButtons(Account.ScrollButtons.valueOf(mAccountScrollMoveButtons.getValue()));
}
boolean needsRefresh = mAccount.setAutomaticCheckIntervalMinutes(Integer.parseInt(mCheckFrequency.getValue()));
@@ -782,24 +750,23 @@ public class AccountSettings extends K9PreferenceActivity {
}
}
- mAccount.setScrollMessageViewButtons(Account.ScrollButtons.valueOf(mAccountScrollButtons.getValue()));
mAccount.setShowPictures(Account.ShowPictures.valueOf(mAccountShowPictures.getValue()));
-
- if (mIsPushCapable) {
- boolean needsPushRestart = mAccount.setFolderPushMode(Account.FolderMode.valueOf(mPushMode.getValue()));
- if (mAccount.getFolderPushMode() != FolderMode.NONE) {
- needsPushRestart |= displayModeChanged;
- needsPushRestart |= mIncomingChanged;
- }
-
- if (needsRefresh && needsPushRestart) {
- MailService.actionReset(this, null);
- } else if (needsRefresh) {
- MailService.actionReschedulePoll(this, null);
- } else if (needsPushRestart) {
- MailService.actionRestartPushers(this, null);
- }
- }
+
+ if (mIsPushCapable) {
+ boolean needsPushRestart = mAccount.setFolderPushMode(Account.FolderMode.valueOf(mPushMode.getValue()));
+ if (mAccount.getFolderPushMode() != FolderMode.NONE) {
+ needsPushRestart |= displayModeChanged;
+ needsPushRestart |= mIncomingChanged;
+ }
+
+ if (needsRefresh && needsPushRestart) {
+ MailService.actionReset(this, null);
+ } else if (needsRefresh) {
+ MailService.actionReschedulePoll(this, null);
+ } else if (needsPushRestart) {
+ MailService.actionRestartPushers(this, null);
+ }
+ }
// TODO: refresh folder list here
mAccount.save(Preferences.getPreferences(this));
}
@@ -946,22 +913,33 @@ public class AccountSettings extends K9PreferenceActivity {
mTrashFolder = (ListPreference)findPreference(PREFERENCE_TRASH_FOLDER);
mTrashFolder.setEnabled(false);
+ if (!mIsMoveCapable) {
+ PreferenceScreen foldersCategory =
+ (PreferenceScreen) findPreference(PREFERENCE_CATEGORY_FOLDERS);
+ foldersCategory.removePreference(mArchiveFolder);
+ foldersCategory.removePreference(mSpamFolder);
+ foldersCategory.removePreference(mDraftsFolder);
+ foldersCategory.removePreference(mSentFolder);
+ foldersCategory.removePreference(mTrashFolder);
+ }
}
@Override
protected void onPostExecute(Void res) {
initListPreference(mAutoExpandFolder, mAccount.getAutoExpandFolderName(), allFolderLabels, allFolderValues);
- initListPreference(mArchiveFolder, mAccount.getArchiveFolderName(), allFolderLabels, allFolderValues);
- initListPreference(mDraftsFolder, mAccount.getDraftsFolderName(), allFolderLabels, allFolderValues);
- initListPreference(mSentFolder, mAccount.getSentFolderName(), allFolderLabels, allFolderValues);
- initListPreference(mSpamFolder, mAccount.getSpamFolderName(), allFolderLabels, allFolderValues);
- initListPreference(mTrashFolder, mAccount.getTrashFolderName(), allFolderLabels, allFolderValues);
mAutoExpandFolder.setEnabled(true);
- mArchiveFolder.setEnabled(true);
- mDraftsFolder.setEnabled(true);
- mSentFolder.setEnabled(true);
- mSpamFolder.setEnabled(true);
- mTrashFolder.setEnabled(true);
+ if (mIsMoveCapable) {
+ initListPreference(mArchiveFolder, mAccount.getArchiveFolderName(), allFolderLabels, allFolderValues);
+ initListPreference(mDraftsFolder, mAccount.getDraftsFolderName(), allFolderLabels, allFolderValues);
+ initListPreference(mSentFolder, mAccount.getSentFolderName(), allFolderLabels, allFolderValues);
+ initListPreference(mSpamFolder, mAccount.getSpamFolderName(), allFolderLabels, allFolderValues);
+ initListPreference(mTrashFolder, mAccount.getTrashFolderName(), allFolderLabels, allFolderValues);
+ mArchiveFolder.setEnabled(true);
+ mSpamFolder.setEnabled(true);
+ mDraftsFolder.setEnabled(true);
+ mSentFolder.setEnabled(true);
+ mTrashFolder.setEnabled(true);
+ }
}
}
}
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();
diff --git a/src/com/fsck/k9/controller/MessagingController.java b/src/com/fsck/k9/controller/MessagingController.java
index e20ef3f59..38cb7e33d 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";
@@ -1845,9 +1846,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.
@@ -2056,6 +2055,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);
@@ -2235,16 +2236,72 @@ 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];
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_NEW;
+
+ 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);
+ }
+ }
+
+ /**
+ * 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.
*
@@ -2256,6 +2313,7 @@ public class MessagingController implements Runnable {
throws MessagingException {
Folder remoteSrcFolder = null;
Folder remoteDestFolder = null;
+ LocalFolder localDestFolder = null;
try {
String srcFolder = command.arguments[0];
if (account.getErrorFolderName().equals(srcFolder)) {
@@ -2263,14 +2321,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 = (LocalFolder) 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));
+ }
}
}
@@ -2291,6 +2377,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 = null;
+
if (!isCopy && destFolder.equals(account.getTrashFolderName())) {
if (K9.DEBUG)
Log.d(K9.LOG_TAG, "processingPendingMoveOrCopy doing special case for deleting message");
@@ -2304,9 +2392,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())) {
@@ -2315,12 +2403,32 @@ 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 != null && !remoteUidMap.isEmpty()) {
+ Set remoteSrcUids = remoteUidMap.keySet();
+ Iterator remoteSrcUidsIterator = remoteSrcUids.iterator();
+
+ while (remoteSrcUidsIterator.hasNext()) {
+ String remoteSrcUid = remoteSrcUidsIterator.next();
+ String localDestUid = localUidMap.get(remoteSrcUid);
+ String newUid = remoteUidMap.get(remoteSrcUid);
+
+ Message localDestMessage = localDestFolder.getMessage(localDestUid);
+ localDestMessage.setUid(newUid);
+ localDestFolder.changeUid((LocalMessage)localDestMessage);
+ for (MessagingListener l : getListeners()) {
+ l.messageUidChanged(account, destFolder, localDestUid, newUid);
+ }
+ }
+ }
} finally {
closeFolder(remoteSrcFolder);
closeFolder(remoteDestFolder);
}
-
-
}
private void queueSetFlag(final Account account, final String folderName, final String newState, final String flag, final String[] uids) {
@@ -2673,65 +2781,117 @@ 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.getRemoteName(), uids, flag, newState);
+ setFlag(account, folder.getRemoteName(), 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 uid
+ * The UID of 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);
+ if (message != null) {
+ 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 {
@@ -2916,9 +3076,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);
}
@@ -3385,6 +3543,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())) {
@@ -3397,12 +3556,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);
@@ -3422,9 +3586,18 @@ 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);
+
+ 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);
+ uidMap = localSrcFolder.moveMessages(messages, localDestFolder);
for (Map.Entry entry : origUidMap.entrySet()) {
String origUid = entry.getKey();
Message message = entry.getValue();
@@ -3433,9 +3606,20 @@ 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));
+ queueMoveOrCopy(account, srcFolder, destFolder, isCopy, origUidMap.keySet().toArray(EMPTY_STRING_ARRAY), uidMap);
}
processPendingCommands(account);
@@ -3465,9 +3649,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);
@@ -3513,6 +3699,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");
@@ -3527,7 +3714,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);
}
}
@@ -3560,7 +3747,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) {
@@ -3622,12 +3809,14 @@ 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.setUnreadMessageCount(0);
+ localFolder.setFlaggedMessageCount(0);
for (MessagingListener l : getListeners()) {
l.emptyTrashCompleted(account);
diff --git a/src/com/fsck/k9/controller/MessagingListener.java b/src/com/fsck/k9/controller/MessagingListener.java
index bdf35fa00..49ddbf4da 100644
--- a/src/com/fsck/k9/controller/MessagingListener.java
+++ b/src/com/fsck/k9/controller/MessagingListener.java
@@ -12,191 +12,154 @@ 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 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 messageDeleted(Account account, String folder, Message message) {}
- public void loadAttachmentFinished(
- Account account,
- Message message,
- Part part,
- Object tag) {
- }
+ public void messageUidChanged(Account account, String folder, String oldUid, String newUid) {}
- public void loadAttachmentFailed(
- Account account,
- Message message,
- Part part,
- Object tag,
- String reason) {
- }
+
+ 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) {}
}
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/Folder.java b/src/com/fsck/k9/mail/Folder.java
index 9d9c3423b..df6a633ec 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/internet/MimeUtility.java b/src/com/fsck/k9/mail/internet/MimeUtility.java
index d8ad792b2..405243d4b 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,867 @@ 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();
+ 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
+ // '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;
+ }
}
+
+ html.append(HtmlConverter.getHtmlFooter());
+
+ 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("
");
+ }
+
+ /**
+ * 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(FILENAME_SUFFIX);
+ text.append(TEXT_DIVIDER.substring(0, TEXT_DIVIDER_LENGTH -
+ FILENAME_PREFIX_LENGTH - filename.length() - FILENAME_SUFFIX_LENGTH));
+ } 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, boolean)} 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, false);
+ }
+ 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();
@@ -1210,6 +2040,17 @@ public class MimeUtility {
return DEFAULT_ATTACHMENT_MIME_TYPE;
}
+ public static String getExtensionByMimeType(String mimeType) {
+ String lowerCaseMimeType = mimeType.toLowerCase(Locale.US);
+ for (String[] contentTypeMapEntry : MIME_TYPE_BY_EXTENSION_MAP) {
+ if (contentTypeMapEntry[1].equals(lowerCaseMimeType)) {
+ return contentTypeMapEntry[0];
+ }
+ }
+
+ return null;
+ }
+
/**
* Convert some wrong MIME types encountered in the wild to canonical MIME types.
*
diff --git a/src/com/fsck/k9/mail/store/EasStore.java b/src/com/fsck/k9/mail/store/EasStore.java
index 18da89b83..9bcff3e14 100644
--- a/src/com/fsck/k9/mail/store/EasStore.java
+++ b/src/com/fsck/k9/mail/store/EasStore.java
@@ -17,6 +17,7 @@ import java.util.Date;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
+import java.util.Map;
import java.util.Map.Entry;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
@@ -1088,13 +1089,15 @@ public class EasStore extends Store {
}
@Override
- public void copyMessages(Message[] messages, Folder folder) throws MessagingException {
+ public Map copyMessages(Message[] messages, Folder folder) throws MessagingException {
moveOrCopyMessages(messages, folder.getRemoteName(), 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.getRemoteName(), true);
+ return null;
}
@Override
@@ -1464,8 +1467,9 @@ public class EasStore extends Store {
}
@Override
- public void appendMessages(Message[] messages) throws MessagingException {
+ public Map appendMessages(Message[] messages) throws MessagingException {
// EASTODO
+ return null;
}
@Override
diff --git a/src/com/fsck/k9/mail/store/ImapStore.java b/src/com/fsck/k9/mail/store/ImapStore.java
index 34bd1af5b..3090e1a42 100644
--- a/src/com/fsck/k9/mail/store/ImapStore.java
+++ b/src/com/fsck/k9/mail/store/ImapStore.java
@@ -61,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;
@@ -89,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;
@@ -1064,53 +1066,127 @@ 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 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;
+ 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++) {
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);
}
- 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);
+ //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);
+ ImapResponse response;
+ do {
+ response = mConnection.readResponse();
+ handleUntaggedResponse(response);
+ } while (response.mTag == null);
+
+ 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 = ImapUtility.getImapSequenceValues(
+ copyList.getString(2));
+ List destUids = ImapUtility.getImapSequenceValues(
+ 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, "Parsing of the sequence set failed.");
+ }
+ }
+ }
+ }
}
+
+ return uidMap;
} catch (IOException ioe) {
throw ioExceptionHandler(mConnection, ioe);
}
}
@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
@@ -1854,20 +1930,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 void appendMessages(Message[] messages) throws MessagingException {
+ public Map appendMessages(Message[] messages) throws MessagingException {
checkOpen();
try {
+ 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();
@@ -1881,16 +1967,54 @@ public class ImapStore extends Store {
}
} while (response.mTag == null);
- String newUid = getUidFromMessageId(message);
- if (K9.DEBUG)
- Log.d(K9.LOG_TAG, "Got UID " + newUid + " for message for " + getLogId());
+ 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 (newUid != null) {
- message.setUid(newUid);
+ if (responseList instanceof ImapList) {
+ ImapList appendList = (ImapList) responseList;
+ if (appendList.size() >= 3 &&
+ appendList.getString(0).equals("APPENDUID")) {
+
+ 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.
+ */
+ String newUid = getUidFromMessageId(message);
+ if (K9.DEBUG) {
+ Log.d(K9.LOG_TAG, "Got UID " + newUid + " for message for " + getLogId());
+ }
+ if (!StringUtils.isNullOrEmpty(newUid)) {
+ uidMap.put(message.getUid(), newUid);
+ message.setUid(newUid);
+ }
}
+
+ /*
+ * 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);
}
@@ -1916,7 +2040,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) {
diff --git a/src/com/fsck/k9/mail/store/LocalStore.java b/src/com/fsck/k9/mail/store/LocalStore.java
index 797aa4403..4cf6accf5 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;
@@ -1720,6 +1721,12 @@ public class LocalStore extends Store implements Serializable {
contentDisposition,
name, // TODO: Should use encoded word defined in RFC 2231.
size));
+ } else {
+ bp.setHeader(MimeHeader.HEADER_CONTENT_TYPE, type);
+ bp.setHeader(MimeHeader.HEADER_CONTENT_DISPOSITION,
+ String.format("%s;\n size=%d",
+ contentDisposition,
+ size));
}
bp.setHeader(MimeHeader.HEADER_CONTENT_ID, contentId);
@@ -1950,21 +1957,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
@@ -1990,7 +1999,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(),
@@ -1998,6 +2010,11 @@ 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);
@@ -2009,6 +2026,7 @@ public class LocalStore extends Store implements Serializable {
return null;
}
});
+ return uidMap;
} catch (WrappedException e) {
throw(MessagingException) e.getCause();
}
@@ -2054,8 +2072,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 {
@@ -2091,10 +2109,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 {
@@ -2107,11 +2127,26 @@ 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();
- if (!copy) {
- message.setUid(uid);
+ /*
+ * 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) {
@@ -2128,45 +2163,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 = HtmlConverter.convertEmoji2Img(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();
@@ -2222,6 +2226,7 @@ public class LocalStore extends Store implements Serializable {
return null;
}
});
+ return uidMap;
} catch (WrappedException e) {
throw(MessagingException) e.getCause();
}
@@ -2244,49 +2249,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 = HtmlConverter.convertEmoji2Img(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 = ?, "
@@ -2420,6 +2393,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
@@ -2647,7 +2632,7 @@ public class LocalStore extends Store implements Serializable {
setVisibleLimit(mAccount.getDisplayCount());
}
- private void resetUnreadAndFlaggedCounts() {
+ public void resetUnreadAndFlaggedCounts() {
try {
int newUnread = 0;
int newFlagged = 0;
@@ -2718,22 +2703,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);
}
@@ -2817,17 +2814,7 @@ 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;
diff --git a/src/com/fsck/k9/mail/store/Pop3Store.java b/src/com/fsck/k9/mail/store/Pop3Store.java
index d1d00dba9..b9e9a3b23 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 String STORE_TYPE = "POP3";
@@ -893,7 +894,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 01aa87b47..ed269cb7e 100644
--- a/src/com/fsck/k9/mail/store/WebDavStore.java
+++ b/src/com/fsck/k9/mail/store/WebDavStore.java
@@ -1333,13 +1333,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.getRemoteName(), 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.getRemoteName(), true);
+ return null;
}
@Override
@@ -1919,8 +1921,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 {
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.
+ *
+ *
+ * @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.
+ *
+ *
+ *
+ * @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;
+ }
+}
diff --git a/src/com/fsck/k9/preferences/AccountSettings.java b/src/com/fsck/k9/preferences/AccountSettings.java
index fd7256268..abdf13c22 100644
--- a/src/com/fsck/k9/preferences/AccountSettings.java
+++ b/src/com/fsck/k9/preferences/AccountSettings.java
@@ -12,7 +12,6 @@ import com.fsck.k9.Account;
import com.fsck.k9.K9;
import com.fsck.k9.R;
import com.fsck.k9.Account.FolderMode;
-import com.fsck.k9.Account.ScrollButtons;
import com.fsck.k9.crypto.Apg;
import com.fsck.k9.mail.store.StorageManager;
import com.fsck.k9.preferences.Settings.*;
@@ -82,12 +81,6 @@ public class AccountSettings {
s.put("goToUnreadMessageSearch", Settings.versions(
new V(1, new BooleanSetting(false))
));
- s.put("hideButtonsEnum", Settings.versions(
- new V(1, new EnumSetting(ScrollButtons.class, ScrollButtons.NEVER))
- ));
- s.put("hideMoveButtonsEnum", Settings.versions(
- new V(1, new EnumSetting(ScrollButtons.class, ScrollButtons.NEVER))
- ));
s.put("idleRefreshMinutes", Settings.versions(
new V(1, new IntegerResourceSetting(24, R.array.idle_refresh_period_values))
));
diff --git a/src/com/fsck/k9/preferences/Settings.java b/src/com/fsck/k9/preferences/Settings.java
index c821c01cb..fd220c8d4 100644
--- a/src/com/fsck/k9/preferences/Settings.java
+++ b/src/com/fsck/k9/preferences/Settings.java
@@ -35,7 +35,7 @@ public class Settings {
*
* @see SettingsExporter
*/
- public static final int VERSION = 5;
+ public static final int VERSION = 6;
public static Map validate(int version, Map> settings,
diff --git a/src/com/fsck/k9/provider/AttachmentProvider.java b/src/com/fsck/k9/provider/AttachmentProvider.java
index 542beab00..ec81e30ba 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,47 @@ 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();
+ }
+ }
+
+ /**
+ * 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() {
/*
@@ -89,21 +140,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,67 +150,24 @@ 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;
+
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";
- }
- File dir = getContext().getCacheDir();
- File file = new File(dir, filename);
+
+ file = getThumbnailFile(getContext(), accountUuid, attachmentId);
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,40 +180,24 @@ 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);
}
- }
- @Override
- public int delete(Uri uri, String arg1, String[] arg2) {
- return 0;
- }
-
- @Override
- public Uri insert(Uri uri, ContentValues values) {
- return null;
+ return ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY);
}
@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);
@@ -232,7 +209,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);
@@ -242,10 +218,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)) {
@@ -265,6 +241,56 @@ 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 {
+ 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) {
if (MimeUtility.mimeTypeMatches(type, "image/*")) {
return createImageThumbnail(data);
diff --git a/src/com/fsck/k9/provider/MessageProvider.java b/src/com/fsck/k9/provider/MessageProvider.java
index 518f976cf..6b42e2b53 100644
--- a/src/com/fsck/k9/provider/MessageProvider.java
+++ b/src/com/fsck/k9/provider/MessageProvider.java
@@ -245,7 +245,7 @@ public class MessageProvider extends ContentProvider {
final BlockingQueue> queue = new SynchronousQueue>();
// new code for integrated inbox, only execute this once as it will be processed afterwards via the listener
- final SearchAccount integratedInboxAccount = new SearchAccount(getContext(), true, null, null);
+ final SearchAccount integratedInboxAccount = SearchAccount.createUnifiedInboxAccount(getContext());
final MessagingController msgController = MessagingController.getInstance(K9.app);
msgController.searchLocalMessages(integratedInboxAccount, null,
diff --git a/src/com/fsck/k9/provider/UnreadWidgetProvider.java b/src/com/fsck/k9/provider/UnreadWidgetProvider.java
new file mode 100644
index 000000000..616707c34
--- /dev/null
+++ b/src/com/fsck/k9/provider/UnreadWidgetProvider.java
@@ -0,0 +1,127 @@
+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 {
+ private static final int MAX_COUNT = 9999;
+
+ /**
+ * 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.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);
+
+ 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);
+ }
+ }
+}
diff --git a/src/com/fsck/k9/view/AttachmentView.java b/src/com/fsck/k9/view/AttachmentView.java
index 9e9a4c441..5eccda9eb 100644
--- a/src/com/fsck/k9/view/AttachmentView.java
+++ b/src/com/fsck/k9/view/AttachmentView.java
@@ -18,6 +18,8 @@ import android.os.Environment;
import android.util.AttributeSet;
import android.util.Log;
import android.view.View;
+import android.view.View.OnClickListener;
+import android.view.View.OnLongClickListener;
import android.widget.Button;
import android.widget.FrameLayout;
import android.widget.ImageView;
@@ -33,12 +35,34 @@ import com.fsck.k9.helper.MediaScannerNotifier;
import com.fsck.k9.helper.SizeFormatter;
import com.fsck.k9.helper.Utility;
import com.fsck.k9.mail.Message;
+import com.fsck.k9.mail.MessagingException;
import com.fsck.k9.mail.Part;
+import com.fsck.k9.mail.internet.MimeHeader;
import com.fsck.k9.mail.internet.MimeUtility;
import com.fsck.k9.mail.store.LocalStore.LocalAttachmentBodyPart;
import com.fsck.k9.provider.AttachmentProvider;
-public class AttachmentView extends FrameLayout {
+public class AttachmentView extends FrameLayout implements OnClickListener, OnLongClickListener {
+ /**
+ * Regular expression that represents characters we won't allow in file names.
+ *
+ *
+ * Allowed are:
+ *
+ *
word characters (letters, digits, and underscores): {@code \w}
+ */
+ private static final String INVALID_CHARACTERS = "[^\\w !#$%&'()\\-@\\^`{}~.,]+";
+
+ /**
+ * Invalid characters in a file name are replaced by this character.
+ */
+ private static final String REPLACEMENT_CHARACTER = "_";
+
private Context mContext;
public Button viewButton;
@@ -81,86 +105,125 @@ public class AttachmentView extends FrameLayout {
*/
public void showFileBrowser(AttachmentView caller);
}
- public boolean populateFromPart(Part inputPart, Message message, Account account, MessagingController controller, MessagingListener listener) {
- try {
- part = (LocalAttachmentBodyPart) inputPart;
- contentType = MimeUtility.unfoldAndDecode(part.getContentType());
- String contentDisposition = MimeUtility.unfoldAndDecode(part.getDisposition());
+ /**
+ * Populates this view with information about the attachment.
+ *
+ *
+ * This method also decides which attachments are displayed when the "show attachments" button
+ * is pressed, and which attachments are only displayed after the "show more attachments"
+ * button was pressed.
+ * Inline attachments with content ID and unnamed attachments fall into the second category.
+ *
+ *
+ * @param inputPart
+ * @param message
+ * @param account
+ * @param controller
+ * @param listener
+ *
+ * @return {@code true} for a regular attachment. {@code false}, otherwise.
+ *
+ * @throws MessagingException
+ * In case of an error
+ */
+ public boolean populateFromPart(Part inputPart, Message message, Account account,
+ MessagingController controller, MessagingListener listener) throws MessagingException {
+ boolean firstClassAttachment = true;
+ part = (LocalAttachmentBodyPart) inputPart;
- name = MimeUtility.getHeaderParameter(contentType, "name");
- if (name == null) {
- name = MimeUtility.getHeaderParameter(contentDisposition, "filename");
- }
- if (name == null) {
- return false;
- }
+ contentType = MimeUtility.unfoldAndDecode(part.getContentType());
+ String contentDisposition = MimeUtility.unfoldAndDecode(part.getDisposition());
- mAccount = account;
- mMessage = message;
- mController = controller;
- mListener = listener;
-
- size = Integer.parseInt(MimeUtility.getHeaderParameter(contentDisposition, "size"));
- contentType = MimeUtility.getMimeTypeForViewing(part.getMimeType(), name);
- TextView attachmentName = (TextView) findViewById(R.id.attachment_name);
- TextView attachmentInfo = (TextView) findViewById(R.id.attachment_info);
- ImageView attachmentIcon = (ImageView) findViewById(R.id.attachment_icon);
- viewButton = (Button) findViewById(R.id.view);
- downloadButton = (Button) findViewById(R.id.download);
- if ((!MimeUtility.mimeTypeMatches(contentType, K9.ACCEPTABLE_ATTACHMENT_VIEW_TYPES))
- || (MimeUtility.mimeTypeMatches(contentType, K9.UNACCEPTABLE_ATTACHMENT_VIEW_TYPES))) {
- viewButton.setVisibility(View.GONE);
- }
- if ((!MimeUtility.mimeTypeMatches(contentType, K9.ACCEPTABLE_ATTACHMENT_DOWNLOAD_TYPES))
- || (MimeUtility.mimeTypeMatches(contentType, K9.UNACCEPTABLE_ATTACHMENT_DOWNLOAD_TYPES))) {
- downloadButton.setVisibility(View.GONE);
- }
- if (size > K9.MAX_ATTACHMENT_DOWNLOAD_SIZE) {
- viewButton.setVisibility(View.GONE);
- downloadButton.setVisibility(View.GONE);
- }
-
- viewButton.setOnClickListener(new OnClickListener() {
- @Override
- public void onClick(View v) {
- onViewButtonClicked();
- return;
- }
- });
-
-
- downloadButton.setOnClickListener(new OnClickListener() {
- @Override
- public void onClick(View v) {
- onSaveButtonClicked();
- return;
- }
- });
- downloadButton.setOnLongClickListener(new OnLongClickListener() {
-
- @Override
- public boolean onLongClick(View v) {
- callback.showFileBrowser(AttachmentView.this);
- return true;
- }
- });
-
- attachmentName.setText(name);
- attachmentInfo.setText(SizeFormatter.formatSize(mContext, size));
- Bitmap previewIcon = getPreviewIcon();
- if (previewIcon != null) {
- attachmentIcon.setImageBitmap(previewIcon);
- } else {
- attachmentIcon.setImageResource(R.drawable.attached_image_placeholder);
- }
+ name = MimeUtility.getHeaderParameter(contentType, "name");
+ if (name == null) {
+ name = MimeUtility.getHeaderParameter(contentDisposition, "filename");
}
- catch (Exception e) {
- Log.e(K9.LOG_TAG, "error ", e);
+ if (name == null) {
+ firstClassAttachment = false;
+ String extension = MimeUtility.getExtensionByMimeType(contentType);
+ name = "noname" + ((extension != null) ? "." + extension : "");
}
- return true;
+ // Inline parts with a content-id are almost certainly components of an HTML message
+ // not attachments. Only show them if the user pressed the button to show more
+ // attachments.
+ if (contentDisposition != null &&
+ MimeUtility.getHeaderParameter(contentDisposition, null).matches("^(?i:inline)")
+ && part.getHeader(MimeHeader.HEADER_CONTENT_ID) != null) {
+ firstClassAttachment = false;
+ }
+
+ mAccount = account;
+ mMessage = message;
+ mController = controller;
+ mListener = listener;
+
+ String sizeParam = MimeUtility.getHeaderParameter(contentDisposition, "size");
+ if (sizeParam != null) {
+ try {
+ size = Integer.parseInt(sizeParam);
+ } catch (NumberFormatException e) { /* ignore */ }
+ }
+
+ contentType = MimeUtility.getMimeTypeForViewing(part.getMimeType(), name);
+ TextView attachmentName = (TextView) findViewById(R.id.attachment_name);
+ TextView attachmentInfo = (TextView) findViewById(R.id.attachment_info);
+ ImageView attachmentIcon = (ImageView) findViewById(R.id.attachment_icon);
+ viewButton = (Button) findViewById(R.id.view);
+ downloadButton = (Button) findViewById(R.id.download);
+ if ((!MimeUtility.mimeTypeMatches(contentType, K9.ACCEPTABLE_ATTACHMENT_VIEW_TYPES))
+ || (MimeUtility.mimeTypeMatches(contentType, K9.UNACCEPTABLE_ATTACHMENT_VIEW_TYPES))) {
+ viewButton.setVisibility(View.GONE);
+ }
+ if ((!MimeUtility.mimeTypeMatches(contentType, K9.ACCEPTABLE_ATTACHMENT_DOWNLOAD_TYPES))
+ || (MimeUtility.mimeTypeMatches(contentType, K9.UNACCEPTABLE_ATTACHMENT_DOWNLOAD_TYPES))) {
+ downloadButton.setVisibility(View.GONE);
+ }
+ if (size > K9.MAX_ATTACHMENT_DOWNLOAD_SIZE) {
+ viewButton.setVisibility(View.GONE);
+ downloadButton.setVisibility(View.GONE);
+ }
+
+ viewButton.setOnClickListener(this);
+ downloadButton.setOnClickListener(this);
+ downloadButton.setOnLongClickListener(this);
+
+ attachmentName.setText(name);
+ attachmentInfo.setText(SizeFormatter.formatSize(mContext, size));
+ Bitmap previewIcon = getPreviewIcon();
+ if (previewIcon != null) {
+ attachmentIcon.setImageBitmap(previewIcon);
+ } else {
+ attachmentIcon.setImageResource(R.drawable.attached_image_placeholder);
+ }
+
+ return firstClassAttachment;
+ }
+
+ @Override
+ public void onClick(View view) {
+ switch (view.getId()) {
+ case R.id.view: {
+ onViewButtonClicked();
+ break;
+ }
+ case R.id.download: {
+ onSaveButtonClicked();
+ break;
+ }
+ }
+ }
+
+ @Override
+ public boolean onLongClick(View view) {
+ if (view.getId() == R.id.download) {
+ callback.showFileBrowser(this);
+ return true;
+ }
+
+ return false;
}
private Bitmap getPreviewIcon() {
@@ -196,7 +259,8 @@ public class AttachmentView extends FrameLayout {
*/
public void writeFile(File directory) {
try {
- File file = Utility.createUniqueFile(directory, name);
+ String filename = sanitizeFilename(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);
@@ -207,9 +271,25 @@ 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();
}
}
+
+ /**
+ * Replace characters we don't allow in file names with a replacement character.
+ *
+ * @param filename
+ * The original file name.
+ *
+ * @return The sanitized file name containing only allowed characters.
+ */
+ private String sanitizeFilename(String filename) {
+ return filename.replaceAll(INVALID_CHARACTERS, REPLACEMENT_CHARACTER);
+ }
+
/**
* saves the file to the defaultpath setting in the config, or if the config
* is not set => to the Environment
diff --git a/src/com/fsck/k9/view/MessageHeader.java b/src/com/fsck/k9/view/MessageHeader.java
index 371bc5905..a3f10afad 100644
--- a/src/com/fsck/k9/view/MessageHeader.java
+++ b/src/com/fsck/k9/view/MessageHeader.java
@@ -2,6 +2,8 @@ package com.fsck.k9.view;
import android.content.Context;
import android.graphics.Typeface;
+import android.os.Parcel;
+import android.os.Parcelable;
import android.text.SpannableString;
import android.text.SpannableStringBuilder;
import android.text.style.StyleSpan;
@@ -10,7 +12,10 @@ import android.util.AttributeSet;
import android.util.TypedValue;
import android.view.Gravity;
import android.view.View;
+import android.view.View.OnClickListener;
import android.widget.CheckBox;
+import android.widget.ImageView;
+import android.widget.ScrollView;
import android.widget.LinearLayout;
import android.widget.TextView;
import android.widget.Toast;
@@ -25,14 +30,14 @@ import com.fsck.k9.mail.Flag;
import com.fsck.k9.mail.Message;
import com.fsck.k9.mail.MessagingException;
import com.fsck.k9.mail.internet.MimeUtility;
-import com.fsck.k9.mail.store.LocalStore;
+
import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;
import java.text.DateFormat;
-public class MessageHeader extends LinearLayout {
+public class MessageHeader extends ScrollView implements OnClickListener {
private Context mContext;
private TextView mFromView;
private TextView mDateView;
@@ -44,17 +49,19 @@ public class MessageHeader extends LinearLayout {
private DateFormat mTimeFormat;
private View mChip;
+ private View mChip2;
private CheckBox mFlagged;
private int defaultSubjectColor;
private LinearLayout mToContainerView;
private LinearLayout mCcContainerView;
private TextView mAdditionalHeadersView;
- private View mAttachmentIcon;
private View mAnsweredIcon;
private Message mMessage;
private Account mAccount;
private FontSizes mFontSizes = K9.getFontSizes();
private Contacts mContacts;
+ private ImageView mShowAdditionalHeadersIcon;
+ private SavedState mSavedState;
/**
* Pair class is only available since API Level 5, so we need
@@ -79,7 +86,6 @@ public class MessageHeader extends LinearLayout {
}
private void initializeLayout() {
- mAttachmentIcon = findViewById(R.id.attachment);
mAnsweredIcon = findViewById(R.id.answered);
mFromView = (TextView) findViewById(R.id.from);
mToView = (TextView) findViewById(R.id.to);
@@ -89,17 +95,20 @@ public class MessageHeader extends LinearLayout {
mSubjectView = (TextView) findViewById(R.id.subject);
mAdditionalHeadersView = (TextView) findViewById(R.id.additional_headers_view);
mChip = findViewById(R.id.chip);
+ mChip2 = findViewById(R.id.chip2);
mDateView = (TextView) findViewById(R.id.date);
mTimeView = (TextView) findViewById(R.id.time);
mFlagged = (CheckBox) findViewById(R.id.flagged);
+ mShowAdditionalHeadersIcon = (ImageView) findViewById(R.id.show_additional_headers_icon);
+
defaultSubjectColor = mSubjectView.getCurrentTextColor();
mSubjectView.setTextSize(TypedValue.COMPLEX_UNIT_SP, mFontSizes.getMessageViewSubject());
mTimeView.setTextSize(TypedValue.COMPLEX_UNIT_SP, mFontSizes.getMessageViewTime());
mDateView.setTextSize(TypedValue.COMPLEX_UNIT_SP, mFontSizes.getMessageViewDate());
mAdditionalHeadersView.setTextSize(TypedValue.COMPLEX_UNIT_SP, mFontSizes.getMessageViewAdditionalHeaders());
- mAdditionalHeadersView.setVisibility(View.GONE);
- mAttachmentIcon.setVisibility(View.GONE);
+ hideAdditionalHeaders();
+
mAnsweredIcon.setVisibility(View.GONE);
mFromView.setTextSize(TypedValue.COMPLEX_UNIT_SP, mFontSizes.getMessageViewSender());
mToView.setTextSize(TypedValue.COMPLEX_UNIT_SP, mFontSizes.getMessageViewTo());
@@ -107,39 +116,45 @@ public class MessageHeader extends LinearLayout {
((TextView) findViewById(R.id.to_label)).setTextSize(TypedValue.COMPLEX_UNIT_SP, mFontSizes.getMessageViewTo());
((TextView) findViewById(R.id.cc_label)).setTextSize(TypedValue.COMPLEX_UNIT_SP, mFontSizes.getMessageViewCC());
- setOnClickListener(new OnClickListener() {
- @Override
- public void onClick(View v) {
- onShowAdditionalHeaders();
- }
- });
+ findViewById(R.id.show_additional_headers_area).setOnClickListener(this);
+ mFromView.setOnClickListener(this);
+ }
- mFromView.setOnClickListener(new OnClickListener() {
- @Override
- public void onClick(View v) {
- if (mMessage != null) {
- try {
- final Address senderEmail = mMessage.getFrom()[0];
- mContacts.createContact(senderEmail);
- } catch (Exception e) {
- Log.e(K9.LOG_TAG, "Couldn't create contact", e);
- }
- }
+ @Override
+ public void onClick(View view) {
+ switch (view.getId()) {
+ case R.id.show_additional_headers_area: {
+ onShowAdditionalHeaders();
+ break;
}
- });
+ case R.id.from: {
+ onAddSenderToContacts();
+ break;
+ }
+ }
+ }
+
+ private void onAddSenderToContacts() {
+ if (mMessage != null) {
+ try {
+ final Address senderEmail = mMessage.getFrom()[0];
+ mContacts.createContact(senderEmail);
+ } catch (Exception e) {
+ Log.e(K9.LOG_TAG, "Couldn't create contact", e);
+ }
+ }
}
public void setOnFlagListener(OnClickListener listener) {
+ if (mFlagged == null)
+ return;
mFlagged.setOnClickListener(listener);
}
public boolean additionalHeadersVisible() {
- if (mAdditionalHeadersView != null && mAdditionalHeadersView.getVisibility() == View.VISIBLE) {
- return true;
- } else {
- return false;
- }
+ return (mAdditionalHeadersView != null &&
+ mAdditionalHeadersView.getVisibility() == View.VISIBLE);
}
/**
@@ -149,7 +164,7 @@ public class MessageHeader extends LinearLayout {
private void hideAdditionalHeaders() {
mAdditionalHeadersView.setVisibility(View.GONE);
mAdditionalHeadersView.setText("");
-
+ mShowAdditionalHeadersIcon.setImageResource(R.drawable.show_more);
}
@@ -168,6 +183,7 @@ public class MessageHeader extends LinearLayout {
// Show the additional headers that we have got.
populateAdditionalHeadersView(additionalHeaders);
mAdditionalHeadersView.setVisibility(View.VISIBLE);
+ mShowAdditionalHeadersIcon.setImageResource(R.drawable.show_less);
}
if (!allHeadersDownloaded) {
/*
@@ -227,14 +243,25 @@ public class MessageHeader extends LinearLayout {
mToView.setText(to);
mCcContainerView.setVisibility((cc != null && cc.length() > 0) ? View.VISIBLE : View.GONE);
mCcView.setText(cc);
- mAttachmentIcon.setVisibility(((LocalStore.LocalMessage) message).hasAttachments() ? View.VISIBLE : View.GONE);
mAnsweredIcon.setVisibility(message.isSet(Flag.ANSWERED) ? View.VISIBLE : View.GONE);
mFlagged.setChecked(message.isSet(Flag.FLAGGED));
- mChip.setBackgroundDrawable(mAccount.generateColorChip().drawable());
- mChip.getBackground().setAlpha(!message.isSet(Flag.SEEN) ? 255 : 127);
+
+ int chipColor = mAccount.getChipColor();
+ int chipColorAlpha = (!message.isSet(Flag.SEEN)) ? 255 : 127;
+ mChip.setBackgroundColor(chipColor);
+ mChip.getBackground().setAlpha(chipColorAlpha);
+ mChip2.setBackgroundColor(chipColor);
+ mChip2.getBackground().setAlpha(chipColorAlpha);
+
setVisibility(View.VISIBLE);
- if (mAdditionalHeadersView.getVisibility() == View.VISIBLE) {
- showAdditionalHeaders();
+
+ if (mSavedState != null) {
+ if (mSavedState.additionalHeadersVisible) {
+ showAdditionalHeaders();
+ }
+ mSavedState = null;
+ } else {
+ hideAdditionalHeaders();
}
}
@@ -294,4 +321,61 @@ public class MessageHeader extends LinearLayout {
mAdditionalHeadersView.setText(sb);
}
+ @Override
+ public Parcelable onSaveInstanceState() {
+ Parcelable superState = super.onSaveInstanceState();
+
+ SavedState savedState = new SavedState(superState);
+
+ savedState.additionalHeadersVisible = additionalHeadersVisible();
+
+ return savedState;
+ }
+
+ @Override
+ public void onRestoreInstanceState(Parcelable state) {
+ if(!(state instanceof SavedState)) {
+ super.onRestoreInstanceState(state);
+ return;
+ }
+
+ SavedState savedState = (SavedState)state;
+ super.onRestoreInstanceState(savedState.getSuperState());
+
+ mSavedState = savedState;
+ }
+
+ static class SavedState extends BaseSavedState {
+ boolean additionalHeadersVisible;
+
+ @SuppressWarnings("hiding")
+ public static final Parcelable.Creator CREATOR =
+ new Parcelable.Creator() {
+ @Override
+ public SavedState createFromParcel(Parcel in) {
+ return new SavedState(in);
+ }
+
+ @Override
+ public SavedState[] newArray(int size) {
+ return new SavedState[size];
+ }
+ };
+
+
+ SavedState(Parcelable superState) {
+ super(superState);
+ }
+
+ private SavedState(Parcel in) {
+ super(in);
+ this.additionalHeadersVisible = (in.readInt() != 0);
+ }
+
+ @Override
+ public void writeToParcel(Parcel out, int flags) {
+ super.writeToParcel(out, flags);
+ out.writeInt((this.additionalHeadersVisible) ? 1 : 0);
+ }
+ }
}
diff --git a/src/com/fsck/k9/view/MessageWebView.java b/src/com/fsck/k9/view/MessageWebView.java
index daf53b993..d992b50be 100644
--- a/src/com/fsck/k9/view/MessageWebView.java
+++ b/src/com/fsck/k9/view/MessageWebView.java
@@ -1,25 +1,20 @@
package com.fsck.k9.view;
import android.content.Context;
-import android.graphics.Picture;
import android.os.Build;
import android.util.AttributeSet;
import android.util.Log;
import android.view.KeyEvent;
+import android.view.View;
import android.webkit.WebSettings;
import android.webkit.WebView;
import android.widget.Toast;
import com.fsck.k9.K9;
import com.fsck.k9.R;
-import com.fsck.k9.controller.MessagingListener;
-
import java.lang.reflect.Method;
-import java.util.Set;
public class MessageWebView extends WebView {
- // Store a reference to the listeners in MessagingController. We can't fetch it directly since
- // we don't know the application name.
- private Set mListeners = null;
+
/**
* We use WebSettings.getBlockNetworkLoads() to prevent the WebView that displays email
@@ -75,7 +70,7 @@ public class MessageWebView extends WebView {
this.setVerticalScrollBarEnabled(true);
this.setVerticalScrollbarOverlay(true);
this.setScrollBarStyle(SCROLLBARS_INSIDE_OVERLAY);
-
+ this.setLongClickable(true);
final WebSettings webSettings = this.getSettings();
@@ -97,22 +92,16 @@ public class MessageWebView extends WebView {
webSettings.setLayoutAlgorithm(WebSettings.LayoutAlgorithm.NARROW_COLUMNS);
}
+ if (Integer.parseInt(Build.VERSION.SDK) >= 9 ) {
+ setOverScrollMode(OVER_SCROLL_NEVER);
+ }
+
+
webSettings.setTextSize(K9.getFontSizes().getMessageViewContent());
// Disable network images by default. This is overridden by preferences.
blockNetworkData(true);
- // Listen for when the screen has finished drawing.
- setPictureListener(new PictureListener() {
- @Override
- public void onNewPicture(WebView webView, Picture picture) {
- if (mListeners != null) {
- for (MessagingListener l : mListeners) {
- l.messageViewFinished();
- }
- }
- }
- });
}
/*
@@ -132,7 +121,15 @@ public class MessageWebView extends WebView {
}
}
- public void setListeners(final Set listeners) {
- this.mListeners = listeners;
+ public void wrapSetTitleBar(final View title) {
+ try {
+ Class> webViewClass = Class.forName("android.webkit.WebView");
+ Method setEmbeddedTitleBar = webViewClass.getMethod("setEmbeddedTitleBar", View.class);
+ setEmbeddedTitleBar.invoke(this, title);
+ }
+
+ catch (Exception e) {
+ Log.v(K9.LOG_TAG, "failed to find the setEmbeddedTitleBar method",e);
+ }
}
}
diff --git a/src/com/fsck/k9/view/SingleMessageView.java b/src/com/fsck/k9/view/SingleMessageView.java
index af3bfd207..b1e0980e9 100644
--- a/src/com/fsck/k9/view/SingleMessageView.java
+++ b/src/com/fsck/k9/view/SingleMessageView.java
@@ -7,16 +7,20 @@ import android.content.Intent;
import android.content.pm.ResolveInfo;
import android.database.Cursor;
import android.net.Uri;
+import android.os.Parcel;
+import android.os.Parcelable;
import android.util.AttributeSet;
import android.util.Log;
import android.view.KeyEvent;
import android.view.LayoutInflater;
import android.view.View;
+import android.view.View.OnClickListener;
import android.widget.Button;
import android.widget.LinearLayout;
import com.fsck.k9.Account;
import com.fsck.k9.K9;
import com.fsck.k9.R;
+import com.fsck.k9.activity.K9Activity;
import com.fsck.k9.controller.MessagingController;
import com.fsck.k9.controller.MessagingListener;
import com.fsck.k9.crypto.CryptoProvider;
@@ -24,49 +28,67 @@ import com.fsck.k9.crypto.PgpData;
import com.fsck.k9.helper.Contacts;
import com.fsck.k9.helper.Utility;
import com.fsck.k9.mail.*;
-import com.fsck.k9.mail.internet.MimeHeader;
-import com.fsck.k9.mail.internet.MimeUtility;
import com.fsck.k9.mail.store.LocalStore;
-
+import com.fsck.k9.mail.store.LocalStore.LocalMessage;
import java.util.List;
-import java.util.Set;
-/**
- */
-public class SingleMessageView extends LinearLayout {
+public class SingleMessageView extends LinearLayout implements OnClickListener {
private boolean mScreenReaderEnabled;
private MessageCryptoView mCryptoView;
private MessageWebView mMessageContentView;
private AccessibleWebView mAccessibleMessageContentView;
private MessageHeader mHeaderContainer;
private LinearLayout mAttachments;
- private View mShowPicturesSection;
+ private Button mShowHiddenAttachments;
+ private LinearLayout mHiddenAttachments;
+ private View mShowPicturesAction;
+ private View mShowMessageAction;
+ private View mShowAttachmentsAction;
private boolean mShowPictures;
+ private boolean mHasAttachments;
private Button mDownloadRemainder;
private LayoutInflater mInflater;
private Contacts mContacts;
private AttachmentView.AttachmentFileDownloadCallback attachmentCallback;
+ private LinearLayout mHeaderPlaceHolder;
+ private LinearLayout mTitleBarHeaderContainer;
+ private View mAttachmentsContainer;
+ private LinearLayout mInsideAttachmentsContainer;
+ private SavedState mSavedState;
public void initialize(Activity activity) {
mMessageContentView = (MessageWebView) findViewById(R.id.message_content);
mAccessibleMessageContentView = (AccessibleWebView) findViewById(R.id.accessible_message_content);
- mAttachments = (LinearLayout) findViewById(R.id.attachments);
+ mMessageContentView.configure();
+
+ mHeaderPlaceHolder = (LinearLayout) findViewById(R.id.message_view_header_container);
+
mHeaderContainer = (MessageHeader) findViewById(R.id.header_container);
+
+ mAttachmentsContainer = findViewById(R.id.attachments_container);
+ mInsideAttachmentsContainer = (LinearLayout) findViewById(R.id.inside_attachments_container);
+ mAttachments = (LinearLayout) findViewById(R.id.attachments);
+ mHiddenAttachments = (LinearLayout) findViewById(R.id.hidden_attachments);
+ mHiddenAttachments.setVisibility(View.GONE);
+ mShowHiddenAttachments = (Button) findViewById(R.id.show_hidden_attachments);
+ mShowHiddenAttachments.setVisibility(View.GONE);
mCryptoView = (MessageCryptoView) findViewById(R.id.layout_decrypt);
mCryptoView.setActivity(activity);
mCryptoView.setupChildViews();
- mShowPicturesSection = findViewById(R.id.show_pictures_section);
+ mShowPicturesAction = findViewById(R.id.show_pictures);
+ mShowMessageAction = findViewById(R.id.show_message);
+
+ mShowAttachmentsAction = findViewById(R.id.show_attachments);
+
mShowPictures = false;
mContacts = Contacts.getInstance(activity);
mInflater = activity.getLayoutInflater();
mDownloadRemainder = (Button) findViewById(R.id.download_remainder);
- mMessageContentView.configure();
-
-
- mAttachments.setVisibility(View.GONE);
+ mDownloadRemainder.setVisibility(View.GONE);
+ mAttachmentsContainer.setVisibility(View.GONE);
if (isScreenReaderActive(activity)) {
mAccessibleMessageContentView.setVisibility(View.VISIBLE);
mMessageContentView.setVisibility(View.GONE);
@@ -75,8 +97,62 @@ public class SingleMessageView extends LinearLayout {
mAccessibleMessageContentView.setVisibility(View.GONE);
mMessageContentView.setVisibility(View.VISIBLE);
mScreenReaderEnabled = false;
+
+ mHeaderPlaceHolder.removeView(mHeaderContainer);
+ // the HTC version of WebView tries to force the background of the
+ // titlebar, which is really unfair.
+ mHeaderContainer.setBackgroundColor(((K9Activity)activity).getThemeBackgroundColor());
+
+ mTitleBarHeaderContainer = new LinearLayout(activity);
+ mTitleBarHeaderContainer.addView(mHeaderContainer);
+ mMessageContentView.wrapSetTitleBar(mTitleBarHeaderContainer);
}
+ mShowHiddenAttachments.setOnClickListener(this);
+ mShowMessageAction.setOnClickListener(this);
+ mShowAttachmentsAction.setOnClickListener(this);
+ mShowPicturesAction.setOnClickListener(this);
+ }
+
+ @Override
+ public void onClick(View view) {
+ switch (view.getId()) {
+ case R.id.show_hidden_attachments: {
+ onShowHiddenAttachments();
+ break;
+ }
+ case R.id.show_message: {
+ onShowMessage();
+ break;
+ }
+ case R.id.show_attachments: {
+ onShowAttachments();
+ break;
+ }
+ case R.id.show_pictures: {
+ setLoadPictures(true);
+ break;
+ }
+ }
+ }
+
+ private void onShowHiddenAttachments() {
+ mShowHiddenAttachments.setVisibility(View.GONE);
+ mHiddenAttachments.setVisibility(View.VISIBLE);
+ }
+
+ public void onShowMessage() {
+ showShowMessageAction(false);
+ showAttachments(false);
+ showShowAttachmentsAction(mHasAttachments);
+ showMessageWebView(true);
+ }
+
+ public void onShowAttachments() {
+ showMessageWebView(false);
+ showShowAttachmentsAction(false);
+ showShowMessageAction(true);
+ showAttachments(true);
}
public SingleMessageView(Context context, AttributeSet attrs) {
@@ -102,15 +178,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();
}
}
}
@@ -135,20 +215,27 @@ public class SingleMessageView extends LinearLayout {
public void setLoadPictures(boolean enable) {
mMessageContentView.blockNetworkData(!enable);
setShowPictures(enable);
- showShowPicturesSection(false);
+ showShowPicturesAction(false);
}
public Button downloadRemainderButton() {
return mDownloadRemainder;
}
- public void showShowPicturesSection(boolean show) {
- mShowPicturesSection.setVisibility(show ? View.VISIBLE : View.GONE);
+ public void showShowPicturesAction(boolean show) {
+ mShowPicturesAction.setVisibility(show ? View.VISIBLE : View.GONE);
+ }
+ public void showShowMessageAction(boolean show) {
+ mShowMessageAction.setVisibility(show ? View.VISIBLE : View.GONE);
+ }
+ public void showShowAttachmentsAction(boolean show) {
+ mShowAttachmentsAction.setVisibility(show ? View.VISIBLE : View.GONE);
}
public void setHeaders(final Message message, Account account) {
try {
mHeaderContainer.populate(message, account);
+ mHeaderContainer.setVisibility(View.VISIBLE);
} catch (Exception me) {
@@ -177,9 +264,9 @@ public class SingleMessageView extends LinearLayout {
return mHeaderContainer.additionalHeadersVisible();
}
- public void displayMessageBody(Account account, String folder, String uid, Message message, PgpData pgpData) throws MessagingException {
- // TODO - really this code path? this is an odd place to put it
- removeAllAttachments();
+ public void setMessage(Account account, LocalMessage message, PgpData pgpData,
+ MessagingController controller, MessagingListener listener) throws MessagingException {
+ resetView();
String type;
String text = pgpData.getDecryptedData();
@@ -187,13 +274,49 @@ public class SingleMessageView extends LinearLayout {
type = "text/plain";
} else {
// getTextForDisplay() always returns HTML-ified content.
- text = ((LocalStore.LocalMessage) message).getTextForDisplay();
+ text = message.getTextForDisplay();
type = "text/html";
}
if (text != null) {
final String emailText = text;
final String contentType = type;
- loadBodyFromText(account.getCryptoProvider(), pgpData, message, emailText, contentType);
+ loadBodyFromText(emailText, contentType);
+ updateCryptoLayout(account.getCryptoProvider(), pgpData, message);
+ } else {
+ loadBodyFromUrl("file:///android_asset/empty.html");
+ }
+
+ mHasAttachments = message.hasAttachments();
+
+ if (mHasAttachments) {
+ renderAttachments(message, 0, message, account, controller, listener);
+ }
+
+ mHiddenAttachments.setVisibility(View.GONE);
+
+ boolean lookForImages = true;
+ if (mSavedState != null) {
+ if (mSavedState.showPictures) {
+ setLoadPictures(true);
+ lookForImages = false;
+ }
+
+ if (mSavedState.attachmentViewVisible) {
+ onShowAttachments();
+ } else {
+ onShowMessage();
+ }
+
+ if (mSavedState.hiddenAttachmentsVisible) {
+ onShowHiddenAttachments();
+ }
+
+ mSavedState = null;
+ } else {
+ onShowMessage();
+ }
+
+ if (text != null && lookForImages) {
// If the message contains external pictures and the "Show pictures"
// button wasn't already pressed, see if the user's preferences has us
// showing them anyway.
@@ -206,11 +329,9 @@ public class SingleMessageView extends LinearLayout {
mContacts.isInContacts(from[0].getAddress()))) {
setLoadPictures(true);
} else {
- showShowPicturesSection(true);
+ showShowPicturesAction(true);
}
}
- } else {
- loadBodyFromUrl("file:///android_asset/empty.html");
}
}
@@ -220,14 +341,13 @@ public class SingleMessageView extends LinearLayout {
}
- public void loadBodyFromText(CryptoProvider cryptoProvider, PgpData pgpData, Message message, String emailText, String contentType) {
+ private void loadBodyFromText(String emailText, String contentType) {
if (mScreenReaderEnabled) {
mAccessibleMessageContentView.loadDataWithBaseURL("http://", emailText, contentType, "utf-8", null);
} else {
mMessageContentView.loadDataWithBaseURL("http://", emailText, contentType, "utf-8", null);
mMessageContentView.scrollTo(0, 0);
}
- updateCryptoLayout(cryptoProvider, pgpData, message);
}
@@ -235,6 +355,23 @@ public class SingleMessageView extends LinearLayout {
mCryptoView.updateLayout(cp, pgpData, message);
}
+ public void showAttachments(boolean show) {
+ mAttachmentsContainer.setVisibility(show ? View.VISIBLE : View.GONE);
+ boolean showHidden = (show && mHiddenAttachments.getVisibility() == View.GONE &&
+ mHiddenAttachments.getChildCount() > 0);
+ mShowHiddenAttachments.setVisibility(showHidden ? View.VISIBLE : View.GONE);
+
+ if (show) {
+ moveHeaderToLayout();
+ } else {
+ moveHeaderToWebViewTitleBar();
+ }
+ }
+
+ public void showMessageWebView(boolean show) {
+ mMessageContentView.setVisibility(show ? View.VISIBLE : View.GONE);
+ }
+
public void setAttachmentsEnabled(boolean enabled) {
for (int i = 0, count = mAttachments.getChildCount(); i < count; i++) {
AttachmentView attachment = (AttachmentView) mAttachments.getChildAt(i);
@@ -243,7 +380,6 @@ public class SingleMessageView extends LinearLayout {
}
}
-
public void removeAllAttachments() {
for (int i = 0, count = mAttachments.getChildCount(); i < count; i++) {
mAttachments.removeView(mAttachments.getChildAt(i));
@@ -259,25 +395,27 @@ public class SingleMessageView extends LinearLayout {
renderAttachments(mp.getBodyPart(i), depth + 1, message, account, controller, listener);
}
} else if (part instanceof LocalStore.LocalAttachmentBodyPart) {
- String contentDisposition = MimeUtility.unfoldAndDecode(part.getDisposition());
- // Inline parts with a content-id are almost certainly components of an HTML message
- // not attachments. Don't show attachment download buttons for them.
- if (contentDisposition != null &&
- MimeUtility.getHeaderParameter(contentDisposition, null).matches("^(?i:inline)")
- && part.getHeader(MimeHeader.HEADER_CONTENT_ID) != null) {
- return;
- }
AttachmentView view = (AttachmentView)mInflater.inflate(R.layout.message_view_attachment, null);
view.setCallback(attachmentCallback);
- if (view.populateFromPart(part, message, account, controller, listener)) {
- addAttachment(view);
+
+ try {
+ if (view.populateFromPart(part, message, account, controller, listener)) {
+ addAttachment(view);
+ } else {
+ addHiddenAttachment(view);
+ }
+ } catch (Exception e) {
+ Log.e(K9.LOG_TAG, "Error adding attachment view", e);
}
}
}
public void addAttachment(View attachmentView) {
mAttachments.addView(attachmentView);
- mAttachments.setVisibility(View.VISIBLE);
+ }
+
+ public void addHiddenAttachment(View attachmentView) {
+ mHiddenAttachments.addView(attachmentView);
}
public void zoom(KeyEvent event) {
@@ -297,11 +435,26 @@ public class SingleMessageView extends LinearLayout {
}
public void resetView() {
+ mDownloadRemainder.setVisibility(View.GONE);
setLoadPictures(false);
- mMessageContentView.scrollTo(0, 0);
- mHeaderContainer.setVisibility(View.GONE);
- mMessageContentView.clearView();
+ showShowAttachmentsAction(false);
+ showShowMessageAction(false);
+ showShowPicturesAction(false);
mAttachments.removeAllViews();
+ mHiddenAttachments.removeAllViews();
+
+ /*
+ * Clear the WebView content
+ *
+ * For some reason WebView.clearView() doesn't clear the contents when the WebView changes
+ * its size because the button to download the complete message was previously shown and
+ * is now hidden.
+ */
+ loadBodyFromText("", "text/plain");
+ }
+
+ public void resetHeaderView() {
+ mHeaderContainer.setVisibility(View.GONE);
}
public AttachmentView.AttachmentFileDownloadCallback getAttachmentCallback() {
@@ -313,20 +466,85 @@ public class SingleMessageView extends LinearLayout {
this.attachmentCallback = attachmentCallback;
}
- /**
- * Save a copy of the {@link com.fsck.k9.controller.MessagingController#getListeners()}. This method will also
- * pass along these listeners to the underlying views.
- * @param listeners Set of listeners.
- */
- public void setListeners(final Set listeners) {
- if (!mScreenReaderEnabled) {
- if (mMessageContentView != null) {
- mMessageContentView.setListeners(listeners);
+ private void moveHeaderToLayout() {
+ if (mTitleBarHeaderContainer != null && mTitleBarHeaderContainer.getChildCount() != 0) {
+ mTitleBarHeaderContainer.removeView(mHeaderContainer);
+ mInsideAttachmentsContainer.addView(mHeaderContainer, 0);
+ }
+ }
+
+ private void moveHeaderToWebViewTitleBar() {
+ if (mTitleBarHeaderContainer != null && mTitleBarHeaderContainer.getChildCount() == 0) {
+ mInsideAttachmentsContainer.removeView(mHeaderContainer);
+ mTitleBarHeaderContainer.addView(mHeaderContainer);
+ }
+ }
+
+ @Override
+ public Parcelable onSaveInstanceState() {
+ Parcelable superState = super.onSaveInstanceState();
+
+ SavedState savedState = new SavedState(superState);
+
+ savedState.attachmentViewVisible = (mAttachmentsContainer != null &&
+ mAttachmentsContainer.getVisibility() == View.VISIBLE);
+ savedState.hiddenAttachmentsVisible = (mHiddenAttachments != null &&
+ mHiddenAttachments.getVisibility() == View.VISIBLE);
+ savedState.showPictures = mShowPictures;
+
+ return savedState;
+ }
+
+ @Override
+ public void onRestoreInstanceState(Parcelable state) {
+ if(!(state instanceof SavedState)) {
+ super.onRestoreInstanceState(state);
+ return;
+ }
+
+ SavedState savedState = (SavedState)state;
+ super.onRestoreInstanceState(savedState.getSuperState());
+
+ mSavedState = savedState;
+ }
+
+ static class SavedState extends BaseSavedState {
+ boolean attachmentViewVisible;
+ boolean hiddenAttachmentsVisible;
+ boolean showPictures;
+
+ @SuppressWarnings("hiding")
+ public static final Parcelable.Creator CREATOR =
+ new Parcelable.Creator() {
+ @Override
+ public SavedState createFromParcel(Parcel in) {
+ return new SavedState(in);
}
- } else {
- if (mAccessibleMessageContentView != null) {
- mAccessibleMessageContentView.setListeners(listeners);
+
+ @Override
+ public SavedState[] newArray(int size) {
+ return new SavedState[size];
}
+ };
+
+
+ SavedState(Parcelable superState) {
+ super(superState);
+ }
+
+ private SavedState(Parcel in) {
+ super(in);
+ this.attachmentViewVisible = (in.readInt() != 0);
+ this.hiddenAttachmentsVisible = (in.readInt() != 0);
+ this.showPictures = (in.readInt() != 0);
+ }
+
+ @Override
+ public void writeToParcel(Parcel out, int flags) {
+ super.writeToParcel(out, flags);
+ out.writeInt((this.attachmentViewVisible) ? 1 : 0);
+ out.writeInt((this.hiddenAttachmentsVisible) ? 1 : 0);
+ out.writeInt((this.showPictures) ? 1 : 0);
}
}
}
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 =
+ "" +
+ "