am 9c8b4aec: Add a proper radio button asset for use with RadioButton
* commit '9c8b4aec771eed90777823d8644a0093ac7f66b9':
Add a proper radio button asset for use with RadioButton
diff --git a/Android.mk b/Android.mk
index a1c9cb6..1cf0bb6 100644
--- a/Android.mk
+++ b/Android.mk
@@ -39,7 +39,7 @@
LOCAL_SRC_FILES := $(call all-java-files-under, $(src_dirs)) \
$(call all-logtags-files-under, $(src_dirs))
-LOCAL_RESOURCE_DIR := $(addprefix $(LOCAL_PATH)/, $(res_dirs)) $(LOCAL_PATH)/res
+LOCAL_RESOURCE_DIR := $(addprefix $(LOCAL_PATH)/, $(res_dirs))
LOCAL_AAPT_FLAGS := --auto-add-overlay
LOCAL_AAPT_FLAGS += --extra-packages com.android.ex.chips:com.android.ex.photo
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index 24fa2b2..91a8b3c 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -111,6 +111,16 @@
android:label="@string/app_name"
android:theme="@style/PhotoViewTheme" >
</activity>
+ <activity
+ android:name=".browse.EmlViewerActivity"
+ android:label="@string/app_name" >
+ <intent-filter>
+ <action android:name="android.intent.action.VIEW" />
+ <category android:name="android.intent.category.DEFAULT" />
+ <data android:mimeType="message/rfc822" />
+ <data android:mimeType="application/eml" />
+ </intent-filter>
+ </activity>
<provider
android:authorities="com.android.mail.mockprovider"
diff --git a/res/color/folder_item_text_color.xml b/res/color/folder_item_text_color.xml
index ab79a8d..47e27f0 100644
--- a/res/color/folder_item_text_color.xml
+++ b/res/color/folder_item_text_color.xml
@@ -16,7 +16,10 @@
limitations under the License.
-->
-<selector xmlns:android="http://schemas.android.com/apk/res/android">
+<selector xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto">
+ <item app:state_drag_mode="true" android:state_drag_can_accept="false"
+ android:color="@color/folder_disabled_drop_target_text_color" />
<item android:state_activated="true" android:color="@android:color/white" />
<item android:color="@color/dark_gray_text_color" />
</selector>
diff --git a/res/drawable/folder_item.xml b/res/drawable/folder_item.xml
index c28ae55..4fd24d0 100644
--- a/res/drawable/folder_item.xml
+++ b/res/drawable/folder_item.xml
@@ -16,7 +16,13 @@
limitations under the License.
-->
-<selector xmlns:android="http://schemas.android.com/apk/res/android">
+<selector xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto">
+ <item app:state_drag_mode="true"
+ android:state_drag_can_accept="true" android:state_drag_hovered="true"
+ android:drawable="@drawable/list_pressed_holo" />
+ <item app:state_drag_mode="true"
+ android:drawable="@drawable/ic_drawer_divider" />
<item android:state_pressed="true" android:drawable="@drawable/list_pressed_holo" />
<item android:state_activated="true" android:drawable="@color/mail_app_blue" />
<item android:state_focused="true" android:drawable="@drawable/list_focused_holo" />
diff --git a/res/layout/eml_viewer_activity.xml b/res/layout/eml_viewer_activity.xml
new file mode 100644
index 0000000..4999b6f
--- /dev/null
+++ b/res/layout/eml_viewer_activity.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2013 Google Inc.
+ Licensed to The Android Open Source Project.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/eml_root"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:background="@android:color/white" >
+</FrameLayout>
diff --git a/res/layout/folder_item.xml b/res/layout/folder_item.xml
index c36d272..334c312 100644
--- a/res/layout/folder_item.xml
+++ b/res/layout/folder_item.xml
@@ -44,10 +44,12 @@
android:layout_height="wrap_content"
android:layout_centerVertical="true"
android:layout_marginRight="@dimen/folder_list_item_right_margin"
- android:layout_alignParentRight="true">
+ android:layout_alignParentRight="true"
+ android:duplicateParentState="true">
<TextView
android:id="@+id/unread"
+ android:duplicateParentState="true"
style="@style/UnreadCount" />
<TextView
@@ -63,6 +65,7 @@
android:layout_centerVertical="true"
android:layout_marginLeft="@dimen/folder_list_item_left_margin"
android:layout_alignParentLeft="true"
+ android:duplicateParentState="true"
android:visibility="gone" />
<TextView
@@ -75,6 +78,7 @@
android:layout_alignWithParentIfMissing="true"
android:layout_marginLeft="@dimen/folder_list_item_left_margin"
android:layout_marginRight="@dimen/folder_list_item_right_margin"
+ android:duplicateParentState="true"
android:includeFontPadding="false"
android:maxLines="2"
android:ellipsize="end"
diff --git a/res/layout/nested_folder.xml b/res/layout/nested_folder.xml
new file mode 100644
index 0000000..d93765b
--- /dev/null
+++ b/res/layout/nested_folder.xml
@@ -0,0 +1,68 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2013 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<!-- View that displays folders that are contained in a folder. These are shown at the top of
+ the conversation list. Email has them, Gmail doesn't currently. -->
+<com.android.mail.ui.NestedFolderView
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_height="wrap_content"
+ android:layout_width="match_parent"
+ android:minHeight="@dimen/folder_list_item_minimum_height"
+ android:background="@drawable/folder_item" >
+
+ <!--This is a rough layout. We don't have UX specs yet, so all the values are hardcoded.
+ Also, it looks totally ugly. The ugliness is intentional.-->
+ <RelativeLayout
+ android:id="@+id/swipeable_content"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center_vertical"
+ >
+
+ <ImageView
+ android:id="@+id/nested_folder_icon"
+ android:layout_height="wrap_content"
+ android:layout_width="wrap_content"
+ android:layout_marginLeft="16dp"
+ android:layout_marginRight="16dp"
+ android:src="@drawable/ic_menu_folders_holo_light"
+ android:layout_alignParentLeft="true"
+ android:contentDescription="@string/folder_icon_desc"
+ android:layout_centerVertical="true"
+ />
+ <TextView
+ android:layout_width="wrap_content"
+ android:id="@+id/nested_folder_name"
+ android:includeFontPadding="false"
+ android:maxLines="2"
+ android:ellipsize="end"
+ android:textColor="@color/folder_item_text_color"
+ android:textAppearance="?android:attr/textAppearanceMedium"
+ android:layout_centerVertical="true"
+ android:layout_height="match_parent"
+ android:layout_toRightOf="@id/nested_folder_icon" />
+
+ <TextView
+ android:layout_width="wrap_content"
+ android:layout_height="match_parent"
+ android:id="@+id/nested_folder_unread"
+ style="@style/UnreadCount"
+ android:layout_alignBaseline="@id/nested_folder_name"
+ android:layout_alignParentRight="true"
+ android:layout_marginRight="16dp"
+ />
+ </RelativeLayout>
+</com.android.mail.ui.NestedFolderView>
\ No newline at end of file
diff --git a/res/layout/secure_conversation_view.xml b/res/layout/secure_conversation_view.xml
index 26b4920..4ea6c42 100644
--- a/res/layout/secure_conversation_view.xml
+++ b/res/layout/secure_conversation_view.xml
@@ -29,13 +29,17 @@
<include layout="@layout/conversation_view_header"
android:id="@+id/conv_header"
android:layout_width="match_parent"
- android:layout_height="wrap_content" />
+ android:layout_height="wrap_content"
+ android:layout_marginLeft="@dimen/conversation_view_margin_side"
+ android:layout_marginRight="@dimen/conversation_view_margin_side" />
<include layout="@layout/conversation_message_header"
android:id="@+id/message_header"
android:layout_width="match_parent"
android:layout_height="wrap_content"
- android:layout_below="@id/conv_header" />
+ android:layout_below="@id/conv_header"
+ android:layout_marginLeft="@dimen/conversation_view_margin_side"
+ android:layout_marginRight="@dimen/conversation_view_margin_side" />
<!-- base WebView layer -->
<com.android.mail.browse.MessageWebView
android:id="@+id/webview"
@@ -46,6 +50,8 @@
android:id="@+id/message_footer"
android:layout_width="match_parent"
android:layout_height="wrap_content"
+ android:layout_marginLeft="@dimen/conversation_view_margin_side"
+ android:layout_marginRight="@dimen/conversation_view_margin_side"
android:visibility="gone" />
</LinearLayout>
</com.android.mail.browse.MessageScrollView>
diff --git a/res/menu-sw600dp-land/conversation_actions.xml b/res/menu-sw600dp-land/conversation_actions.xml
index f463841..3a74345 100644
--- a/res/menu-sw600dp-land/conversation_actions.xml
+++ b/res/menu-sw600dp-land/conversation_actions.xml
@@ -84,6 +84,11 @@
android:icon="@drawable/ic_menu_folders_holo_light" />
<item
+ android:id="@+id/move_to_inbox"
+ android:showAsAction="never"
+ android:title="@string/menu_move_to_inbox" />
+
+ <item
android:id="@+id/mark_important"
android:showAsAction="never"
android:title="@string/mark_important" />
diff --git a/res/menu/conversation_actions.xml b/res/menu/conversation_actions.xml
index e70d6db..9d63506 100644
--- a/res/menu/conversation_actions.xml
+++ b/res/menu/conversation_actions.xml
@@ -72,6 +72,11 @@
android:showAsAction="never"
android:icon="@drawable/ic_menu_folders_holo_light" />
+ <item
+ android:id="@+id/move_to_inbox"
+ android:showAsAction="never"
+ android:title="@string/menu_move_to_inbox" />
+
<!-- Always available -->
<item
android:id="@+id/mark_important"
diff --git a/res/menu/conversation_list_selection_actions_menu.xml b/res/menu/conversation_list_selection_actions_menu.xml
index f635a4f..b866497 100644
--- a/res/menu/conversation_list_selection_actions_menu.xml
+++ b/res/menu/conversation_list_selection_actions_menu.xml
@@ -78,6 +78,11 @@
android:title="@string/menu_change_folders"
android:icon="@drawable/ic_menu_folders_holo_light" />
+ <item
+ android:id="@+id/move_to_inbox"
+ android:showAsAction="never"
+ android:title="@string/menu_move_to_inbox" />
+
<item android:id="@+id/star"
android:title="@string/add_star"
android:showAsAction="never" />
diff --git a/res/menu/general_prefs_fragment_menu.xml b/res/menu/general_prefs_fragment_menu.xml
new file mode 100644
index 0000000..39b19a5
--- /dev/null
+++ b/res/menu/general_prefs_fragment_menu.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2013 Google Inc.
+ Licensed to The Android Open Source Project.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<menu xmlns:android="http://schemas.android.com/apk/res/android" >
+
+ <item
+ android:id="@+id/clear_picture_approvals_menu_item"
+ android:showAsAction="never"
+ android:title="@string/clear_display_images_whitelist_title"/>
+ <item
+ android:id="@+id/feedback_menu_item"
+ android:icon="@android:drawable/ic_menu_send"
+ android:title="@string/feedback"/>
+</menu>
\ No newline at end of file
diff --git a/res/values-af/strings.xml b/res/values-af/strings.xml
index cc35af9..e114906 100644
--- a/res/values-af/strings.xml
+++ b/res/values-af/strings.xml
@@ -361,6 +361,10 @@
<item msgid="732746454445519134">"Wys ouer gesprek nadat jy uitvee"</item>
<item msgid="2189929276292165301">"Wys gespreklys nadat jy uitvee"</item>
</string-array>
+ <string name="clear_display_images_whitelist_title" msgid="7120575487854245735">"Vee prentgoedkeurings uit"</string>
+ <string name="clear_display_images_whitelist_dialog_title" msgid="3190704164490442683">"Vee prentgoedkeurings uit?"</string>
+ <string name="clear_display_images_whitelist_dialog_message" msgid="1169152185612117654">"Hou op om inlynprente vanaf afsenders wat jy voorheen toegelaat het, te wys."</string>
+ <string name="sender_whitelist_cleared" msgid="917434007919176024">"Prente sal nie outomaties gewys word nie."</string>
<string name="whats_new_dialog_title" msgid="4230806739326698666">"Wat\'s nuut"</string>
<string name="notification_action_reply" msgid="6015299134424685297">"Antwoord"</string>
<string name="notification_action_reply_all" msgid="20020468410400912">"Antwoord almal"</string>
@@ -411,4 +415,5 @@
<string name="drawer_open" msgid="6074646853178471940">"Maak navigasielaai oop"</string>
<string name="drawer_close" msgid="2764774620737876943">"Maak navigasielaai toe"</string>
<string name="conversation_photo_welcome_text" msgid="4274875219447670662">"Raak \'n stuurderbeeld om daardie gesprek te kies."</string>
+ <string name="folder_icon_desc" msgid="1500547397347480618">"Vouer-ikoon"</string>
</resources>
diff --git a/res/values-am/strings.xml b/res/values-am/strings.xml
index aee9994..dd77126 100644
--- a/res/values-am/strings.xml
+++ b/res/values-am/strings.xml
@@ -361,6 +361,10 @@
<item msgid="732746454445519134">"ከሰረዙ በኋላ ይበልጥ የቆዩ ውይይትን ያሳዩ"</item>
<item msgid="2189929276292165301">"ከሰረዙ በኋላ የውይይት ዝርዝርን ያሳዩ"</item>
</string-array>
+ <string name="clear_display_images_whitelist_title" msgid="7120575487854245735">"የስዕል ጽድቆችን አጽዳ"</string>
+ <string name="clear_display_images_whitelist_dialog_title" msgid="3190704164490442683">"የስዕል ጽድቆች ይጽዱ?"</string>
+ <string name="clear_display_images_whitelist_dialog_message" msgid="1169152185612117654">"ከዚህ ቀደም ከፈቀዱላቸው ላኪዎች የሚመጡ የውስጠ-መስመር ምስሎችን ማሳየት ይቁም።"</string>
+ <string name="sender_whitelist_cleared" msgid="917434007919176024">"ስዕሎች በራስ-ሰር አይታዩም።"</string>
<string name="whats_new_dialog_title" msgid="4230806739326698666">"ምን አዲስ ነገር አለ"</string>
<string name="notification_action_reply" msgid="6015299134424685297">"ምላሽ ይስጡ"</string>
<string name="notification_action_reply_all" msgid="20020468410400912">"ለሁሉም ምላሽ ይስጡ"</string>
@@ -411,4 +415,5 @@
<string name="drawer_open" msgid="6074646853178471940">"የአሰሳ መሣቢያውን ክፈት"</string>
<string name="drawer_close" msgid="2764774620737876943">"የአሰሳ መሣቢያውን ዝጋ"</string>
<string name="conversation_photo_welcome_text" msgid="4274875219447670662">"ያንን ውይይት ለመምረጥ የላኪ ምስል ይንኩ።"</string>
+ <string name="folder_icon_desc" msgid="1500547397347480618">"የአቃፊ አዶ"</string>
</resources>
diff --git a/res/values-ar/strings.xml b/res/values-ar/strings.xml
index 628a64b..843d245 100644
--- a/res/values-ar/strings.xml
+++ b/res/values-ar/strings.xml
@@ -361,6 +361,10 @@
<item msgid="732746454445519134">"عرض المحادثة الأقدم بعد الحذف"</item>
<item msgid="2189929276292165301">"عرض قائمة المحادثات بعد الحذف"</item>
</string-array>
+ <string name="clear_display_images_whitelist_title" msgid="7120575487854245735">"محو الموافقات على الصور"</string>
+ <string name="clear_display_images_whitelist_dialog_title" msgid="3190704164490442683">"هل تريد محو الموافقات على الصور؟"</string>
+ <string name="clear_display_images_whitelist_dialog_message" msgid="1169152185612117654">"إيقاف عرض الصور المضمنة من المرسلين الذين سبق أن سمحت لهم."</string>
+ <string name="sender_whitelist_cleared" msgid="917434007919176024">"لن يتم عرض الصور تلقائيًا."</string>
<string name="whats_new_dialog_title" msgid="4230806739326698666">"الميزات الجديدة"</string>
<string name="notification_action_reply" msgid="6015299134424685297">"رد"</string>
<string name="notification_action_reply_all" msgid="20020468410400912">"الرد على الكل"</string>
@@ -411,4 +415,5 @@
<string name="drawer_open" msgid="6074646853178471940">"فتح ساحب التنقل"</string>
<string name="drawer_close" msgid="2764774620737876943">"إغلاق ساحب التنقل"</string>
<string name="conversation_photo_welcome_text" msgid="4274875219447670662">"المس صورة أحد المرسلين لتحديد تلك المحادثة."</string>
+ <string name="folder_icon_desc" msgid="1500547397347480618">"رمز المجلد"</string>
</resources>
diff --git a/res/values-be/strings.xml b/res/values-be/strings.xml
index 05b56cf..c001cfd 100644
--- a/res/values-be/strings.xml
+++ b/res/values-be/strings.xml
@@ -361,6 +361,10 @@
<item msgid="732746454445519134">"Паказаць больш раннюю гутарку пасля выдалення"</item>
<item msgid="2189929276292165301">"Паказаць спіс гутарак пасля выдалення"</item>
</string-array>
+ <string name="clear_display_images_whitelist_title" msgid="7120575487854245735">"Выдаліць дазволы па выявах"</string>
+ <string name="clear_display_images_whitelist_dialog_title" msgid="3190704164490442683">"Выдаліць дазволы па выявах?"</string>
+ <string name="clear_display_images_whitelist_dialog_message" msgid="1169152185612117654">"Спыненне адлюстравання убудаваных выяў ад адпраўнікоў, якое раней было дазволена."</string>
+ <string name="sender_whitelist_cleared" msgid="917434007919176024">"Выявы не будуць адлюстроўвацца аўтаматычна"</string>
<string name="whats_new_dialog_title" msgid="4230806739326698666">"Што новага"</string>
<string name="notification_action_reply" msgid="6015299134424685297">"Адказаць"</string>
<string name="notification_action_reply_all" msgid="20020468410400912">"Адказаць усім"</string>
@@ -412,4 +416,6 @@
<string name="drawer_open" msgid="6074646853178471940">"Адкрыць скрыню навігацыі"</string>
<string name="drawer_close" msgid="2764774620737876943">"Закрыць скрыню навігацыі"</string>
<string name="conversation_photo_welcome_text" msgid="4274875219447670662">"Націсніце на малюнак адпраўніка малюнак, каб выбраць размову."</string>
+ <!-- no translation found for folder_icon_desc (1500547397347480618) -->
+ <skip />
</resources>
diff --git a/res/values-bg/strings.xml b/res/values-bg/strings.xml
index 319924e..2f6ede1 100644
--- a/res/values-bg/strings.xml
+++ b/res/values-bg/strings.xml
@@ -361,6 +361,10 @@
<item msgid="732746454445519134">"Показване на по-старата кореспонденция след изтриване"</item>
<item msgid="2189929276292165301">"Показване на списъка с кореспонденции след изтриване"</item>
</string-array>
+ <string name="clear_display_images_whitelist_title" msgid="7120575487854245735">"Изчистване на одобренията за снимки"</string>
+ <string name="clear_display_images_whitelist_dialog_title" msgid="3190704164490442683">"Да се изчистят ли одобренията за снимки?"</string>
+ <string name="clear_display_images_whitelist_dialog_message" msgid="1169152185612117654">"Спиране на показването на вградени изображения от по-рано разрешени от вас податели."</string>
+ <string name="sender_whitelist_cleared" msgid="917434007919176024">"Снимките няма да бъдат показвани автоматично."</string>
<string name="whats_new_dialog_title" msgid="4230806739326698666">"Новите неща"</string>
<string name="notification_action_reply" msgid="6015299134424685297">"Отговор"</string>
<string name="notification_action_reply_all" msgid="20020468410400912">"Отговор до всички"</string>
@@ -412,4 +416,6 @@
<string name="drawer_open" msgid="6074646853178471940">"Отваряне на слоя за навигация"</string>
<string name="drawer_close" msgid="2764774620737876943">"Затваряне на чекмеджето за навигация"</string>
<string name="conversation_photo_welcome_text" msgid="4274875219447670662">"Докоснете изображението на подателя, за да изберете съответната кореспонденция."</string>
+ <!-- no translation found for folder_icon_desc (1500547397347480618) -->
+ <skip />
</resources>
diff --git a/res/values-ca/strings.xml b/res/values-ca/strings.xml
index 68e7b41..1e16eea 100644
--- a/res/values-ca/strings.xml
+++ b/res/values-ca/strings.xml
@@ -361,6 +361,10 @@
<item msgid="732746454445519134">"Mostra una conversa més antiga després de suprimir"</item>
<item msgid="2189929276292165301">"Mostra la llista de converses després de suprimir"</item>
</string-array>
+ <string name="clear_display_images_whitelist_title" msgid="7120575487854245735">"Esborra les aprovacions d\'imatges"</string>
+ <string name="clear_display_images_whitelist_dialog_title" msgid="3190704164490442683">"Vols esborrar les aprovacions d\'imatges?"</string>
+ <string name="clear_display_images_whitelist_dialog_message" msgid="1169152185612117654">"Deixa de mostrar les imatges en línia de remitents que havies permès anteriorment."</string>
+ <string name="sender_whitelist_cleared" msgid="917434007919176024">"Les imatges no es mostraran de manera automàtica."</string>
<string name="whats_new_dialog_title" msgid="4230806739326698666">"Novetats"</string>
<string name="notification_action_reply" msgid="6015299134424685297">"Respon"</string>
<string name="notification_action_reply_all" msgid="20020468410400912">"Respon a tots"</string>
@@ -411,4 +415,5 @@
<string name="drawer_open" msgid="6074646853178471940">"Obre el tauler de navegació"</string>
<string name="drawer_close" msgid="2764774620737876943">"Tanca el tauler de navegació"</string>
<string name="conversation_photo_welcome_text" msgid="4274875219447670662">"Toca la imatge d\'un remitent per seleccionar-ne la conversa."</string>
+ <string name="folder_icon_desc" msgid="1500547397347480618">"Icona de la carpeta"</string>
</resources>
diff --git a/res/values-cs/strings.xml b/res/values-cs/strings.xml
index 70125fa..3bd0d83 100644
--- a/res/values-cs/strings.xml
+++ b/res/values-cs/strings.xml
@@ -361,6 +361,14 @@
<item msgid="732746454445519134">"Po smazání zobrazit starší konverzaci"</item>
<item msgid="2189929276292165301">"Po smazání zobrazit seznam konverzací"</item>
</string-array>
+ <!-- no translation found for clear_display_images_whitelist_title (7120575487854245735) -->
+ <skip />
+ <!-- no translation found for clear_display_images_whitelist_dialog_title (3190704164490442683) -->
+ <skip />
+ <!-- no translation found for clear_display_images_whitelist_dialog_message (1169152185612117654) -->
+ <skip />
+ <!-- no translation found for sender_whitelist_cleared (917434007919176024) -->
+ <skip />
<string name="whats_new_dialog_title" msgid="4230806739326698666">"Novinky"</string>
<string name="notification_action_reply" msgid="6015299134424685297">"Odpovědět"</string>
<string name="notification_action_reply_all" msgid="20020468410400912">"Odpovědět všem"</string>
@@ -412,4 +420,6 @@
<string name="drawer_open" msgid="6074646853178471940">"Otevřít navigační složku"</string>
<string name="drawer_close" msgid="2764774620737876943">"Zavřít navigační složku"</string>
<string name="conversation_photo_welcome_text" msgid="4274875219447670662">"Klepnutím na obrázek odesílatele vyberete příslušnou konverzaci."</string>
+ <!-- no translation found for folder_icon_desc (1500547397347480618) -->
+ <skip />
</resources>
diff --git a/res/values-da/strings.xml b/res/values-da/strings.xml
index e765222..0a35d06 100644
--- a/res/values-da/strings.xml
+++ b/res/values-da/strings.xml
@@ -361,6 +361,10 @@
<item msgid="732746454445519134">"Vis ældre samtale efter sletning"</item>
<item msgid="2189929276292165301">"Vis samtalelisten efter sletning"</item>
</string-array>
+ <string name="clear_display_images_whitelist_title" msgid="7120575487854245735">"Ryd billedgodkendelserne"</string>
+ <string name="clear_display_images_whitelist_dialog_title" msgid="3190704164490442683">"Vil du rydde billedgodkendelserne?"</string>
+ <string name="clear_display_images_whitelist_dialog_message" msgid="1169152185612117654">"Stop visning af indlejrede billeder i tekst fra afsendere, du tidligere har tilladt."</string>
+ <string name="sender_whitelist_cleared" msgid="917434007919176024">"Billeder vil ikke blive vist automatisk."</string>
<string name="whats_new_dialog_title" msgid="4230806739326698666">"Nyheder"</string>
<string name="notification_action_reply" msgid="6015299134424685297">"Svar"</string>
<string name="notification_action_reply_all" msgid="20020468410400912">"Svar alle"</string>
@@ -412,4 +416,6 @@
<string name="drawer_open" msgid="6074646853178471940">"Åbn navigationsskuffen"</string>
<string name="drawer_close" msgid="2764774620737876943">"Luk navigationsskuffen"</string>
<string name="conversation_photo_welcome_text" msgid="4274875219447670662">"Tryk på et afsenderbillede for at vælge den samtale."</string>
+ <!-- no translation found for folder_icon_desc (1500547397347480618) -->
+ <skip />
</resources>
diff --git a/res/values-de/strings.xml b/res/values-de/strings.xml
index 7556097..70fcbda 100644
--- a/res/values-de/strings.xml
+++ b/res/values-de/strings.xml
@@ -361,6 +361,10 @@
<item msgid="732746454445519134">"Nach dem Löschen ältere Konversation anzeigen"</item>
<item msgid="2189929276292165301">"Nach dem Löschen Konversationsliste anzeigen"</item>
</string-array>
+ <string name="clear_display_images_whitelist_title" msgid="7120575487854245735">"Bildgenehmigungen löschen"</string>
+ <string name="clear_display_images_whitelist_dialog_title" msgid="3190704164490442683">"Bildgenehmigungen löschen?"</string>
+ <string name="clear_display_images_whitelist_dialog_message" msgid="1169152185612117654">"Keine Inline-Bilder mehr von zuvor zugelassenen Absendern anzeigen"</string>
+ <string name="sender_whitelist_cleared" msgid="917434007919176024">"Bilder werden nicht automatisch angezeigt."</string>
<string name="whats_new_dialog_title" msgid="4230806739326698666">"Neue Funktionen"</string>
<string name="notification_action_reply" msgid="6015299134424685297">"Antworten"</string>
<string name="notification_action_reply_all" msgid="20020468410400912">"Allen antworten"</string>
@@ -411,4 +415,5 @@
<string name="drawer_open" msgid="6074646853178471940">"Navigationsleiste öffnen"</string>
<string name="drawer_close" msgid="2764774620737876943">"Navigationsleiste schließen"</string>
<string name="conversation_photo_welcome_text" msgid="4274875219447670662">"Auf Absenderbild tippen, um die entsprechende Konversation auszuwählen"</string>
+ <string name="folder_icon_desc" msgid="1500547397347480618">"Ordnersymbol"</string>
</resources>
diff --git a/res/values-el/strings.xml b/res/values-el/strings.xml
index 73dd1cb..e960002 100644
--- a/res/values-el/strings.xml
+++ b/res/values-el/strings.xml
@@ -361,6 +361,10 @@
<item msgid="732746454445519134">"Εμφάνιση παλαιότερης συνομιλίας μετά τη διαγραφή"</item>
<item msgid="2189929276292165301">"Εμφάνιση λίστας συνομιλιών μετά τη διαγραφή"</item>
</string-array>
+ <string name="clear_display_images_whitelist_title" msgid="7120575487854245735">"Διαγραφή εγκρίσεων εικόνας"</string>
+ <string name="clear_display_images_whitelist_dialog_title" msgid="3190704164490442683">"Να διαγραφούν οι εγκρίσεις εικόνας;"</string>
+ <string name="clear_display_images_whitelist_dialog_message" msgid="1169152185612117654">"Διακόψτε την εμφάνιση ενσωματωμένων εικόνων από αποστολείς που έχετε προηγουμένως επιτρέψει."</string>
+ <string name="sender_whitelist_cleared" msgid="917434007919176024">"Οι εικόνες δεν θα εμφανίζονται αυτόματα."</string>
<string name="whats_new_dialog_title" msgid="4230806739326698666">"Τι νέο υπάρχει"</string>
<string name="notification_action_reply" msgid="6015299134424685297">"Απάντηση"</string>
<string name="notification_action_reply_all" msgid="20020468410400912">"Απάντηση σε όλους"</string>
@@ -411,4 +415,5 @@
<string name="drawer_open" msgid="6074646853178471940">"Άνοιγμα συρταριού πλοήγησης"</string>
<string name="drawer_close" msgid="2764774620737876943">"Κλείσιμο συρταριού πλοήγησης"</string>
<string name="conversation_photo_welcome_text" msgid="4274875219447670662">"Αγγίξτε την εικόνα ενός αποστολέα για να επιλέξετε αυτήν τη συνομιλία."</string>
+ <string name="folder_icon_desc" msgid="1500547397347480618">"Εικονίδιο φακέλου"</string>
</resources>
diff --git a/res/values-en-rGB/strings.xml b/res/values-en-rGB/strings.xml
index 8b24006..910f42f 100644
--- a/res/values-en-rGB/strings.xml
+++ b/res/values-en-rGB/strings.xml
@@ -361,6 +361,10 @@
<item msgid="732746454445519134">"Show older conversation after you delete"</item>
<item msgid="2189929276292165301">"Show conversation list after you delete"</item>
</string-array>
+ <string name="clear_display_images_whitelist_title" msgid="7120575487854245735">"Clear picture approvals"</string>
+ <string name="clear_display_images_whitelist_dialog_title" msgid="3190704164490442683">"Clear picture approvals?"</string>
+ <string name="clear_display_images_whitelist_dialog_message" msgid="1169152185612117654">"Stop displaying inline images from senders that you previously allowed."</string>
+ <string name="sender_whitelist_cleared" msgid="917434007919176024">"Pictures won\'t be shown automatically."</string>
<string name="whats_new_dialog_title" msgid="4230806739326698666">"What\'s new"</string>
<string name="notification_action_reply" msgid="6015299134424685297">"Reply"</string>
<string name="notification_action_reply_all" msgid="20020468410400912">"Reply all"</string>
@@ -411,4 +415,5 @@
<string name="drawer_open" msgid="6074646853178471940">"Open navigation drawer"</string>
<string name="drawer_close" msgid="2764774620737876943">"Close navigation drawer"</string>
<string name="conversation_photo_welcome_text" msgid="4274875219447670662">"Touch a sender image to select that conversation."</string>
+ <string name="folder_icon_desc" msgid="1500547397347480618">"Folder icon"</string>
</resources>
diff --git a/res/values-es-rUS/strings.xml b/res/values-es-rUS/strings.xml
index 1562fac..cab7a88 100644
--- a/res/values-es-rUS/strings.xml
+++ b/res/values-es-rUS/strings.xml
@@ -361,6 +361,14 @@
<item msgid="732746454445519134">"Mostrar la conversación más antigua luego de la eliminación"</item>
<item msgid="2189929276292165301">"Mostrar la lista de conversaciones luego de la eliminación"</item>
</string-array>
+ <!-- no translation found for clear_display_images_whitelist_title (7120575487854245735) -->
+ <skip />
+ <!-- no translation found for clear_display_images_whitelist_dialog_title (3190704164490442683) -->
+ <skip />
+ <!-- no translation found for clear_display_images_whitelist_dialog_message (1169152185612117654) -->
+ <skip />
+ <!-- no translation found for sender_whitelist_cleared (917434007919176024) -->
+ <skip />
<string name="whats_new_dialog_title" msgid="4230806739326698666">"Novedades"</string>
<string name="notification_action_reply" msgid="6015299134424685297">"Responder"</string>
<string name="notification_action_reply_all" msgid="20020468410400912">"Responder a todos"</string>
@@ -412,4 +420,6 @@
<string name="drawer_open" msgid="6074646853178471940">"Abrir panel de navegación"</string>
<string name="drawer_close" msgid="2764774620737876943">"Cerrar panel de navegación"</string>
<string name="conversation_photo_welcome_text" msgid="4274875219447670662">"Toca la imagen de un remitente para seleccionar esa conversación."</string>
+ <!-- no translation found for folder_icon_desc (1500547397347480618) -->
+ <skip />
</resources>
diff --git a/res/values-es/strings.xml b/res/values-es/strings.xml
index 7cb2cfd..1afd9e9 100644
--- a/res/values-es/strings.xml
+++ b/res/values-es/strings.xml
@@ -361,6 +361,10 @@
<item msgid="732746454445519134">"Mostrar conversación más antigua después de eliminar"</item>
<item msgid="2189929276292165301">"Mostrar lista de conversaciones después de eliminar"</item>
</string-array>
+ <string name="clear_display_images_whitelist_title" msgid="7120575487854245735">"Borrar aprobaciones de imágenes"</string>
+ <string name="clear_display_images_whitelist_dialog_title" msgid="3190704164490442683">"¿Borrar aprobaciones de imágenes?"</string>
+ <string name="clear_display_images_whitelist_dialog_message" msgid="1169152185612117654">"Dejar de mostrar imágenes entre líneas de remitentes que se hayan permitido anteriormente."</string>
+ <string name="sender_whitelist_cleared" msgid="917434007919176024">"Las imágenes de los mensajes no se mostrarán automáticamente."</string>
<string name="whats_new_dialog_title" msgid="4230806739326698666">"Novedades"</string>
<string name="notification_action_reply" msgid="6015299134424685297">"Responder"</string>
<string name="notification_action_reply_all" msgid="20020468410400912">"Responder a todos"</string>
@@ -411,4 +415,5 @@
<string name="drawer_open" msgid="6074646853178471940">"Abrir control de navegación"</string>
<string name="drawer_close" msgid="2764774620737876943">"Cerrar control de navegación"</string>
<string name="conversation_photo_welcome_text" msgid="4274875219447670662">"Toca la imagen de un remitente para seleccionar esa conversación."</string>
+ <string name="folder_icon_desc" msgid="1500547397347480618">"Icono de carpeta"</string>
</resources>
diff --git a/res/values-et/strings.xml b/res/values-et/strings.xml
index 2d77c16..ae4ca29 100644
--- a/res/values-et/strings.xml
+++ b/res/values-et/strings.xml
@@ -361,6 +361,10 @@
<item msgid="732746454445519134">"Vanema vestluse näitamine pärast kustutamist"</item>
<item msgid="2189929276292165301">"Vestlusloendi näitamine pärast kustutamist"</item>
</string-array>
+ <string name="clear_display_images_whitelist_title" msgid="7120575487854245735">"Pildilubade kustutamine"</string>
+ <string name="clear_display_images_whitelist_dialog_title" msgid="3190704164490442683">"Kas kustutada pildiload?"</string>
+ <string name="clear_display_images_whitelist_dialog_message" msgid="1169152185612117654">"Varasemalt lubatud saatjate tekstisiseste piltide kuvamise peatamine."</string>
+ <string name="sender_whitelist_cleared" msgid="917434007919176024">"Pilte ei kuvata automaatselt."</string>
<string name="whats_new_dialog_title" msgid="4230806739326698666">"Mis on uut?"</string>
<string name="notification_action_reply" msgid="6015299134424685297">"Vasta"</string>
<string name="notification_action_reply_all" msgid="20020468410400912">"Vasta kõigile"</string>
@@ -411,4 +415,5 @@
<string name="drawer_open" msgid="6074646853178471940">"Ava navigeerimissahtel"</string>
<string name="drawer_close" msgid="2764774620737876943">"Sule navigeerimissahtel"</string>
<string name="conversation_photo_welcome_text" msgid="4274875219447670662">"Vestluse valimiseks puudutage saatja kujutist."</string>
+ <string name="folder_icon_desc" msgid="1500547397347480618">"Kausta ikoon"</string>
</resources>
diff --git a/res/values-fa/strings.xml b/res/values-fa/strings.xml
index 0258337..f2c8fb8 100644
--- a/res/values-fa/strings.xml
+++ b/res/values-fa/strings.xml
@@ -361,6 +361,14 @@
<item msgid="732746454445519134">"بعد از حذف مکالمه قدیمیتر را نشان میدهد"</item>
<item msgid="2189929276292165301">"بعد از حذف فهرست مکالمات را نشان میدهد"</item>
</string-array>
+ <!-- no translation found for clear_display_images_whitelist_title (7120575487854245735) -->
+ <skip />
+ <!-- no translation found for clear_display_images_whitelist_dialog_title (3190704164490442683) -->
+ <skip />
+ <!-- no translation found for clear_display_images_whitelist_dialog_message (1169152185612117654) -->
+ <skip />
+ <!-- no translation found for sender_whitelist_cleared (917434007919176024) -->
+ <skip />
<string name="whats_new_dialog_title" msgid="4230806739326698666">"ویژگی جدید چیست"</string>
<string name="notification_action_reply" msgid="6015299134424685297">"پاسخ"</string>
<string name="notification_action_reply_all" msgid="20020468410400912">"پاسخ به همه"</string>
@@ -411,4 +419,5 @@
<string name="drawer_open" msgid="6074646853178471940">"باز کردن کشوی پیمایش"</string>
<string name="drawer_close" msgid="2764774620737876943">"بستن کشوی پیمایش"</string>
<string name="conversation_photo_welcome_text" msgid="4274875219447670662">"برای انتخاب مکالمه، تصویر فرستنده را لمس کنید."</string>
+ <string name="folder_icon_desc" msgid="1500547397347480618">"نماد پوشه"</string>
</resources>
diff --git a/res/values-fi/strings.xml b/res/values-fi/strings.xml
index 6785c33..72d8977 100644
--- a/res/values-fi/strings.xml
+++ b/res/values-fi/strings.xml
@@ -361,6 +361,14 @@
<item msgid="732746454445519134">"Näytä vanhempi keskustelu poistamisen jälkeen"</item>
<item msgid="2189929276292165301">"Näytä keskusteluluettelo poistamisen jälkeen"</item>
</string-array>
+ <!-- no translation found for clear_display_images_whitelist_title (7120575487854245735) -->
+ <skip />
+ <!-- no translation found for clear_display_images_whitelist_dialog_title (3190704164490442683) -->
+ <skip />
+ <!-- no translation found for clear_display_images_whitelist_dialog_message (1169152185612117654) -->
+ <skip />
+ <!-- no translation found for sender_whitelist_cleared (917434007919176024) -->
+ <skip />
<string name="whats_new_dialog_title" msgid="4230806739326698666">"Uutuudet"</string>
<string name="notification_action_reply" msgid="6015299134424685297">"Vastaa"</string>
<string name="notification_action_reply_all" msgid="20020468410400912">"Vastaa kaikille"</string>
@@ -412,4 +420,6 @@
<string name="drawer_open" msgid="6074646853178471940">"Avaa navigointipalkki"</string>
<string name="drawer_close" msgid="2764774620737876943">"Sulje navigointipalkki"</string>
<string name="conversation_photo_welcome_text" msgid="4274875219447670662">"Valitse keskustelu koskettamalla lähettäjän kuvaa."</string>
+ <!-- no translation found for folder_icon_desc (1500547397347480618) -->
+ <skip />
</resources>
diff --git a/res/values-fr/strings.xml b/res/values-fr/strings.xml
index b0099d1..daa3f5f 100644
--- a/res/values-fr/strings.xml
+++ b/res/values-fr/strings.xml
@@ -361,6 +361,14 @@
<item msgid="732746454445519134">"Afficher les conversations plus anciennes après suppression"</item>
<item msgid="2189929276292165301">"Afficher la liste des conversations après suppression"</item>
</string-array>
+ <!-- no translation found for clear_display_images_whitelist_title (7120575487854245735) -->
+ <skip />
+ <!-- no translation found for clear_display_images_whitelist_dialog_title (3190704164490442683) -->
+ <skip />
+ <!-- no translation found for clear_display_images_whitelist_dialog_message (1169152185612117654) -->
+ <skip />
+ <!-- no translation found for sender_whitelist_cleared (917434007919176024) -->
+ <skip />
<string name="whats_new_dialog_title" msgid="4230806739326698666">"Nouveautés"</string>
<string name="notification_action_reply" msgid="6015299134424685297">"Répondre"</string>
<string name="notification_action_reply_all" msgid="20020468410400912">"Répondre à tous"</string>
@@ -412,4 +420,6 @@
<string name="drawer_open" msgid="6074646853178471940">"Ouvrir le panneau de navigation"</string>
<string name="drawer_close" msgid="2764774620737876943">"Fermer le panneau de navigation"</string>
<string name="conversation_photo_welcome_text" msgid="4274875219447670662">"Appuyez sur l\'image d\'un expéditeur pour sélectionner cette conversation."</string>
+ <!-- no translation found for folder_icon_desc (1500547397347480618) -->
+ <skip />
</resources>
diff --git a/res/values-hi/strings.xml b/res/values-hi/strings.xml
index 240eb18..20b5e21 100644
--- a/res/values-hi/strings.xml
+++ b/res/values-hi/strings.xml
@@ -361,6 +361,10 @@
<item msgid="732746454445519134">"आपके द्वारा हटाए जाने के बाद पुराना वार्तालाप दिखाएं"</item>
<item msgid="2189929276292165301">"आपके द्वारा हटाए जाने के बाद वार्तालाप सूची दिखाएं"</item>
</string-array>
+ <string name="clear_display_images_whitelist_title" msgid="7120575487854245735">"चित्र स्वीकृतियां साफ़ करें"</string>
+ <string name="clear_display_images_whitelist_dialog_title" msgid="3190704164490442683">"चित्र स्वीकृतियां साफ़ करें?"</string>
+ <string name="clear_display_images_whitelist_dialog_message" msgid="1169152185612117654">"आपने जिन प्रेषकों को पहले अनुमति दे रखी है, उनके इनलाइन चित्रों का प्रदर्शन रोकें."</string>
+ <string name="sender_whitelist_cleared" msgid="917434007919176024">"चित्र अपने आप नहीं दिखाए जाएंगे."</string>
<string name="whats_new_dialog_title" msgid="4230806739326698666">"नया क्या है"</string>
<string name="notification_action_reply" msgid="6015299134424685297">"जवाब दें"</string>
<string name="notification_action_reply_all" msgid="20020468410400912">"सभी को जवाब दें"</string>
@@ -411,4 +415,5 @@
<string name="drawer_open" msgid="6074646853178471940">"नेविगेशन ड्रॉवर खोलें"</string>
<string name="drawer_close" msgid="2764774620737876943">"नेविगेशन ड्रॉवर बंद करें"</string>
<string name="conversation_photo_welcome_text" msgid="4274875219447670662">"उस बातचीत को चुनने के लिए प्रेषक के चित्र को स्पर्श करें."</string>
+ <string name="folder_icon_desc" msgid="1500547397347480618">"फ़ोल्डर आइकन"</string>
</resources>
diff --git a/res/values-hr/strings.xml b/res/values-hr/strings.xml
index 45be6b4..0014215 100644
--- a/res/values-hr/strings.xml
+++ b/res/values-hr/strings.xml
@@ -361,6 +361,14 @@
<item msgid="732746454445519134">"Prikaz starijeg razgovora nakon brisanja"</item>
<item msgid="2189929276292165301">"Prikaz popisa razgovora nakon brisanja"</item>
</string-array>
+ <!-- no translation found for clear_display_images_whitelist_title (7120575487854245735) -->
+ <skip />
+ <!-- no translation found for clear_display_images_whitelist_dialog_title (3190704164490442683) -->
+ <skip />
+ <!-- no translation found for clear_display_images_whitelist_dialog_message (1169152185612117654) -->
+ <skip />
+ <!-- no translation found for sender_whitelist_cleared (917434007919176024) -->
+ <skip />
<string name="whats_new_dialog_title" msgid="4230806739326698666">"Što je novo"</string>
<string name="notification_action_reply" msgid="6015299134424685297">"Odgovori"</string>
<string name="notification_action_reply_all" msgid="20020468410400912">"Odgovori svima"</string>
@@ -411,4 +419,5 @@
<string name="drawer_open" msgid="6074646853178471940">"Otvaranje pretinca za navigaciju"</string>
<string name="drawer_close" msgid="2764774620737876943">"Zatvaranje pretinca za navigaciju"</string>
<string name="conversation_photo_welcome_text" msgid="4274875219447670662">"Dodirnite sliku pošiljatelja da biste odabrali taj razgovor."</string>
+ <string name="folder_icon_desc" msgid="1500547397347480618">"Ikona mape"</string>
</resources>
diff --git a/res/values-hu/strings.xml b/res/values-hu/strings.xml
index bc65da1..5e0bcff 100644
--- a/res/values-hu/strings.xml
+++ b/res/values-hu/strings.xml
@@ -361,6 +361,10 @@
<item msgid="732746454445519134">"Törlés után a korábbi beszélgetés megjelenítése"</item>
<item msgid="2189929276292165301">"Törlés után a beszélgetéslista megjelenítése"</item>
</string-array>
+ <string name="clear_display_images_whitelist_title" msgid="7120575487854245735">"Képengedélyek törlése"</string>
+ <string name="clear_display_images_whitelist_dialog_title" msgid="3190704164490442683">"Törli a képengedélyeket?"</string>
+ <string name="clear_display_images_whitelist_dialog_message" msgid="1169152185612117654">"Ne jelenítsen meg beágyazott képeket olyan feladóktól, akiktől ezt korábban engedélyezte."</string>
+ <string name="sender_whitelist_cleared" msgid="917434007919176024">"A képek nem jelennek meg automatikusan."</string>
<string name="whats_new_dialog_title" msgid="4230806739326698666">"Újdonságok"</string>
<string name="notification_action_reply" msgid="6015299134424685297">"Válasz"</string>
<string name="notification_action_reply_all" msgid="20020468410400912">"Válasz mindenkinek"</string>
@@ -411,4 +415,5 @@
<string name="drawer_open" msgid="6074646853178471940">"Navigációs fiók kinyitása"</string>
<string name="drawer_close" msgid="2764774620737876943">"Navigációs fiók bezárása"</string>
<string name="conversation_photo_welcome_text" msgid="4274875219447670662">"A beszélgetés kiválasztásához érintse meg a küldő képét."</string>
+ <string name="folder_icon_desc" msgid="1500547397347480618">"Mappa ikon"</string>
</resources>
diff --git a/res/values-in/strings.xml b/res/values-in/strings.xml
index da5705d..d90df33 100644
--- a/res/values-in/strings.xml
+++ b/res/values-in/strings.xml
@@ -361,6 +361,14 @@
<item msgid="732746454445519134">"Tampilkan percakapan lama setelah menghapus"</item>
<item msgid="2189929276292165301">"Tampilkan daftar percakapan setelah menghapus"</item>
</string-array>
+ <!-- no translation found for clear_display_images_whitelist_title (7120575487854245735) -->
+ <skip />
+ <!-- no translation found for clear_display_images_whitelist_dialog_title (3190704164490442683) -->
+ <skip />
+ <!-- no translation found for clear_display_images_whitelist_dialog_message (1169152185612117654) -->
+ <skip />
+ <!-- no translation found for sender_whitelist_cleared (917434007919176024) -->
+ <skip />
<string name="whats_new_dialog_title" msgid="4230806739326698666">"Yang baru"</string>
<string name="notification_action_reply" msgid="6015299134424685297">"Balas"</string>
<string name="notification_action_reply_all" msgid="20020468410400912">"Balas ke semua"</string>
@@ -411,4 +419,5 @@
<string name="drawer_open" msgid="6074646853178471940">"Buka laci navigasi"</string>
<string name="drawer_close" msgid="2764774620737876943">"Tutup laci navigasi"</string>
<string name="conversation_photo_welcome_text" msgid="4274875219447670662">"Sentuh gambar pengirim untuk memilih percakapan tersebut."</string>
+ <string name="folder_icon_desc" msgid="1500547397347480618">"Ikon folder"</string>
</resources>
diff --git a/res/values-it/strings.xml b/res/values-it/strings.xml
index 3c93c70..41472d8 100644
--- a/res/values-it/strings.xml
+++ b/res/values-it/strings.xml
@@ -361,6 +361,10 @@
<item msgid="732746454445519134">"Mostra conversazione meno recente dopo eliminazione"</item>
<item msgid="2189929276292165301">"Mostra elenco conversazioni dopo eliminazione"</item>
</string-array>
+ <string name="clear_display_images_whitelist_title" msgid="7120575487854245735">"Cancella approvazioni immagini"</string>
+ <string name="clear_display_images_whitelist_dialog_title" msgid="3190704164490442683">"Cancellare le approvazioni per le immagini?"</string>
+ <string name="clear_display_images_whitelist_dialog_message" msgid="1169152185612117654">"Smetti di visualizzare immagini incorporate da mittenti in precedenza consentiti."</string>
+ <string name="sender_whitelist_cleared" msgid="917434007919176024">"Le immagini non saranno mostrate automaticamente."</string>
<string name="whats_new_dialog_title" msgid="4230806739326698666">"Novità"</string>
<string name="notification_action_reply" msgid="6015299134424685297">"Rispondi"</string>
<string name="notification_action_reply_all" msgid="20020468410400912">"Rispondi a tutti"</string>
@@ -411,4 +415,5 @@
<string name="drawer_open" msgid="6074646853178471940">"Apri riquadro di navigazione"</string>
<string name="drawer_close" msgid="2764774620737876943">"Chiudi riquadro di navigazione"</string>
<string name="conversation_photo_welcome_text" msgid="4274875219447670662">"Tocca l\'immagine di un mittente per selezionare tale conversazione."</string>
+ <string name="folder_icon_desc" msgid="1500547397347480618">"Icona cartella"</string>
</resources>
diff --git a/res/values-iw/strings.xml b/res/values-iw/strings.xml
index 5f90ef9..b99362e 100644
--- a/res/values-iw/strings.xml
+++ b/res/values-iw/strings.xml
@@ -361,6 +361,10 @@
<item msgid="732746454445519134">"הצג שיחה ישנה יותר לאחר ביצוע מחיקה"</item>
<item msgid="2189929276292165301">"הצג רשימת שיחות לאחר ביצוע מחיקה"</item>
</string-array>
+ <string name="clear_display_images_whitelist_title" msgid="7120575487854245735">"ניקוי של אישורי תמונות"</string>
+ <string name="clear_display_images_whitelist_dialog_title" msgid="3190704164490442683">"האם לנקות אישורי תמונות?"</string>
+ <string name="clear_display_images_whitelist_dialog_message" msgid="1169152185612117654">"הפסק להציג תמונות מוטבעות משולחים שהתרת זאת עבורם בעבר."</string>
+ <string name="sender_whitelist_cleared" msgid="917434007919176024">"תמונות לא יוצגו באופן אוטומטי."</string>
<string name="whats_new_dialog_title" msgid="4230806739326698666">"מה חדש"</string>
<string name="notification_action_reply" msgid="6015299134424685297">"השב"</string>
<string name="notification_action_reply_all" msgid="20020468410400912">"השב לכולם"</string>
@@ -411,4 +415,5 @@
<string name="drawer_open" msgid="6074646853178471940">"פתח את חלונית הניווט"</string>
<string name="drawer_close" msgid="2764774620737876943">"סגור את חלונית הניווט"</string>
<string name="conversation_photo_welcome_text" msgid="4274875219447670662">"גע בתמונת שולח כדי לבחור בשיחה."</string>
+ <string name="folder_icon_desc" msgid="1500547397347480618">"סמל תיקיה"</string>
</resources>
diff --git a/res/values-ja/strings.xml b/res/values-ja/strings.xml
index 4b76dda..2631e3f 100644
--- a/res/values-ja/strings.xml
+++ b/res/values-ja/strings.xml
@@ -361,6 +361,14 @@
<item msgid="732746454445519134">"削除後に古いスレッドを表示する"</item>
<item msgid="2189929276292165301">"削除後にスレッドリストを表示する"</item>
</string-array>
+ <!-- no translation found for clear_display_images_whitelist_title (7120575487854245735) -->
+ <skip />
+ <!-- no translation found for clear_display_images_whitelist_dialog_title (3190704164490442683) -->
+ <skip />
+ <!-- no translation found for clear_display_images_whitelist_dialog_message (1169152185612117654) -->
+ <skip />
+ <!-- no translation found for sender_whitelist_cleared (917434007919176024) -->
+ <skip />
<string name="whats_new_dialog_title" msgid="4230806739326698666">"新機能"</string>
<string name="notification_action_reply" msgid="6015299134424685297">"返信"</string>
<string name="notification_action_reply_all" msgid="20020468410400912">"全員に返信"</string>
@@ -411,4 +419,5 @@
<string name="drawer_open" msgid="6074646853178471940">"ナビゲーションドロワーを開く"</string>
<string name="drawer_close" msgid="2764774620737876943">"ナビゲーションドロワーを閉じる"</string>
<string name="conversation_photo_welcome_text" msgid="4274875219447670662">"スレッドを選択するには送信者画像をタップしてください。"</string>
+ <string name="folder_icon_desc" msgid="1500547397347480618">"フォルダアイコン"</string>
</resources>
diff --git a/res/values-ko/strings.xml b/res/values-ko/strings.xml
index bba63ad..b29206d 100644
--- a/res/values-ko/strings.xml
+++ b/res/values-ko/strings.xml
@@ -361,6 +361,14 @@
<item msgid="732746454445519134">"삭제 후 이전 대화 표시"</item>
<item msgid="2189929276292165301">"삭제 후 대화 목록 표시"</item>
</string-array>
+ <!-- no translation found for clear_display_images_whitelist_title (7120575487854245735) -->
+ <skip />
+ <!-- no translation found for clear_display_images_whitelist_dialog_title (3190704164490442683) -->
+ <skip />
+ <!-- no translation found for clear_display_images_whitelist_dialog_message (1169152185612117654) -->
+ <skip />
+ <!-- no translation found for sender_whitelist_cleared (917434007919176024) -->
+ <skip />
<string name="whats_new_dialog_title" msgid="4230806739326698666">"새로운 기능"</string>
<string name="notification_action_reply" msgid="6015299134424685297">"답장"</string>
<string name="notification_action_reply_all" msgid="20020468410400912">"전체답장"</string>
@@ -412,4 +420,6 @@
<string name="drawer_open" msgid="6074646853178471940">"내비게이션 폴더 열기"</string>
<string name="drawer_close" msgid="2764774620737876943">"내비게이션 폴더 닫기"</string>
<string name="conversation_photo_welcome_text" msgid="4274875219447670662">"대화를 선택하려면 발신자 이미지를 터치합니다."</string>
+ <!-- no translation found for folder_icon_desc (1500547397347480618) -->
+ <skip />
</resources>
diff --git a/res/values-lt/strings.xml b/res/values-lt/strings.xml
index 2acdff0..8e44b43 100644
--- a/res/values-lt/strings.xml
+++ b/res/values-lt/strings.xml
@@ -361,6 +361,14 @@
<item msgid="732746454445519134">"Ištrynus rodyti senesnį pokalbį"</item>
<item msgid="2189929276292165301">"Ištrynus rodyti pokalbių sąrašą"</item>
</string-array>
+ <!-- no translation found for clear_display_images_whitelist_title (7120575487854245735) -->
+ <skip />
+ <!-- no translation found for clear_display_images_whitelist_dialog_title (3190704164490442683) -->
+ <skip />
+ <!-- no translation found for clear_display_images_whitelist_dialog_message (1169152185612117654) -->
+ <skip />
+ <!-- no translation found for sender_whitelist_cleared (917434007919176024) -->
+ <skip />
<string name="whats_new_dialog_title" msgid="4230806739326698666">"Kas naujo?"</string>
<string name="notification_action_reply" msgid="6015299134424685297">"Atsakyti"</string>
<string name="notification_action_reply_all" msgid="20020468410400912">"Atsakyti visiems"</string>
@@ -411,4 +419,5 @@
<string name="drawer_open" msgid="6074646853178471940">"Atidaryti naršymo juostą"</string>
<string name="drawer_close" msgid="2764774620737876943">"Uždaryti naršymo juostą"</string>
<string name="conversation_photo_welcome_text" msgid="4274875219447670662">"Palieskite siuntėjo vaizdą, kad pasirinktumėte tą pokalbį."</string>
+ <string name="folder_icon_desc" msgid="1500547397347480618">"Aplanko piktograma"</string>
</resources>
diff --git a/res/values-lv/strings.xml b/res/values-lv/strings.xml
index 6828394..cb9adba 100644
--- a/res/values-lv/strings.xml
+++ b/res/values-lv/strings.xml
@@ -361,6 +361,10 @@
<item msgid="732746454445519134">"Rādīt vecāku sarunu pēc dzēšanas"</item>
<item msgid="2189929276292165301">"Rādīt sarunu sarakstu pēc dzēšanas"</item>
</string-array>
+ <string name="clear_display_images_whitelist_title" msgid="7120575487854245735">"Attēlu rādīšanas apstiprinājumu notīrīšana"</string>
+ <string name="clear_display_images_whitelist_dialog_title" msgid="3190704164490442683">"Vai notīrīt attēlu rādīšanas apstiprinājumus?"</string>
+ <string name="clear_display_images_whitelist_dialog_message" msgid="1169152185612117654">"Pārtraukt iekļauto attēlu rādīšanu no sūtītājiem, no kuriem iepriekš tā bija atļauta"</string>
+ <string name="sender_whitelist_cleared" msgid="917434007919176024">"Attēli netiks rādīti automātiski."</string>
<string name="whats_new_dialog_title" msgid="4230806739326698666">"Jaunumi"</string>
<string name="notification_action_reply" msgid="6015299134424685297">"Atbildēt"</string>
<string name="notification_action_reply_all" msgid="20020468410400912">"Atbildēt visiem"</string>
@@ -411,4 +415,5 @@
<string name="drawer_open" msgid="6074646853178471940">"Atvērt navigācijas paneli"</string>
<string name="drawer_close" msgid="2764774620737876943">"Aizvērt navigācijas paneli"</string>
<string name="conversation_photo_welcome_text" msgid="4274875219447670662">"Pieskarieties sūtītāja attēlam, lai atlasītu šo sarunu."</string>
+ <string name="folder_icon_desc" msgid="1500547397347480618">"Mapes ikona"</string>
</resources>
diff --git a/res/values-ms/strings.xml b/res/values-ms/strings.xml
index 5f8537d..6c96686 100644
--- a/res/values-ms/strings.xml
+++ b/res/values-ms/strings.xml
@@ -361,6 +361,14 @@
<item msgid="732746454445519134">"Tunjukkan perbualan yang lebih lama selepas memadamkan"</item>
<item msgid="2189929276292165301">"Tunjukkan senarai perbualan selepas memadamkan"</item>
</string-array>
+ <!-- no translation found for clear_display_images_whitelist_title (7120575487854245735) -->
+ <skip />
+ <!-- no translation found for clear_display_images_whitelist_dialog_title (3190704164490442683) -->
+ <skip />
+ <!-- no translation found for clear_display_images_whitelist_dialog_message (1169152185612117654) -->
+ <skip />
+ <!-- no translation found for sender_whitelist_cleared (917434007919176024) -->
+ <skip />
<string name="whats_new_dialog_title" msgid="4230806739326698666">"Apa yang baharu"</string>
<string name="notification_action_reply" msgid="6015299134424685297">"Balas"</string>
<string name="notification_action_reply_all" msgid="20020468410400912">"Balas kepada semua"</string>
@@ -412,4 +420,6 @@
<string name="drawer_open" msgid="6074646853178471940">"Buka laci navigasi"</string>
<string name="drawer_close" msgid="2764774620737876943">"Tutup laci navigasi"</string>
<string name="conversation_photo_welcome_text" msgid="4274875219447670662">"Sentuh imej penghantar untuk memilih perbualan itu."</string>
+ <!-- no translation found for folder_icon_desc (1500547397347480618) -->
+ <skip />
</resources>
diff --git a/res/values-nb/strings.xml b/res/values-nb/strings.xml
index 2360133..09830f1 100644
--- a/res/values-nb/strings.xml
+++ b/res/values-nb/strings.xml
@@ -361,6 +361,14 @@
<item msgid="732746454445519134">"Vis eldre samtaler etter du har slettet"</item>
<item msgid="2189929276292165301">"Vis samtaleliste etter du har slettet"</item>
</string-array>
+ <!-- no translation found for clear_display_images_whitelist_title (7120575487854245735) -->
+ <skip />
+ <!-- no translation found for clear_display_images_whitelist_dialog_title (3190704164490442683) -->
+ <skip />
+ <!-- no translation found for clear_display_images_whitelist_dialog_message (1169152185612117654) -->
+ <skip />
+ <!-- no translation found for sender_whitelist_cleared (917434007919176024) -->
+ <skip />
<string name="whats_new_dialog_title" msgid="4230806739326698666">"Dette er nytt"</string>
<string name="notification_action_reply" msgid="6015299134424685297">"Svar"</string>
<string name="notification_action_reply_all" msgid="20020468410400912">"Svar alle"</string>
@@ -411,4 +419,5 @@
<string name="drawer_open" msgid="6074646853178471940">"Åpne navigasjonsskuffen"</string>
<string name="drawer_close" msgid="2764774620737876943">"Lukk navigasjonsskuffen"</string>
<string name="conversation_photo_welcome_text" msgid="4274875219447670662">"Trykk på et avsenderbilde for å velge den samtalen."</string>
+ <string name="folder_icon_desc" msgid="1500547397347480618">"Mappeikon"</string>
</resources>
diff --git a/res/values-nl/strings.xml b/res/values-nl/strings.xml
index b57268c..6b4abe4 100644
--- a/res/values-nl/strings.xml
+++ b/res/values-nl/strings.xml
@@ -361,6 +361,10 @@
<item msgid="732746454445519134">"Oudere conversatie weergeven na het verwijderen"</item>
<item msgid="2189929276292165301">"Conversatielijst weergeven na het verwijderen"</item>
</string-array>
+ <string name="clear_display_images_whitelist_title" msgid="7120575487854245735">"Goedkeuringen voor foto\'s wissen"</string>
+ <string name="clear_display_images_whitelist_dialog_title" msgid="3190704164490442683">"Goedkeuringen voor foto\'s wissen?"</string>
+ <string name="clear_display_images_whitelist_dialog_message" msgid="1169152185612117654">"Niet langer inline-afbeeldingen weergeven van afzenders die u eerder heeft toegestaan."</string>
+ <string name="sender_whitelist_cleared" msgid="917434007919176024">"Afbeeldingen worden niet automatisch weergegeven."</string>
<string name="whats_new_dialog_title" msgid="4230806739326698666">"Wat is er nieuw"</string>
<string name="notification_action_reply" msgid="6015299134424685297">"Beantwoorden"</string>
<string name="notification_action_reply_all" msgid="20020468410400912">"Allen beantwoorden"</string>
@@ -411,4 +415,5 @@
<string name="drawer_open" msgid="6074646853178471940">"Navigatielade openen"</string>
<string name="drawer_close" msgid="2764774620737876943">"Navigatielade sluiten"</string>
<string name="conversation_photo_welcome_text" msgid="4274875219447670662">"Raak de afbeelding van een afzender aan om dat gesprek te selecteren."</string>
+ <string name="folder_icon_desc" msgid="1500547397347480618">"Mappictogram"</string>
</resources>
diff --git a/res/values-pl/strings.xml b/res/values-pl/strings.xml
index 0b76f1b..d18d57c 100644
--- a/res/values-pl/strings.xml
+++ b/res/values-pl/strings.xml
@@ -359,6 +359,10 @@
<item msgid="732746454445519134">"Pokazuj starszą konwersację po usunięciu"</item>
<item msgid="2189929276292165301">"Pokazuj listę konwersacji po usunięciu"</item>
</string-array>
+ <string name="clear_display_images_whitelist_title" msgid="7120575487854245735">"Wyłącz pokazywanie obrazów"</string>
+ <string name="clear_display_images_whitelist_dialog_title" msgid="3190704164490442683">"Wyłączyć pokazywanie obrazów?"</string>
+ <string name="clear_display_images_whitelist_dialog_message" msgid="1169152185612117654">"Przestań pokazywać w wiadomościach obrazy od zatwierdzonych wcześniej nadawców."</string>
+ <string name="sender_whitelist_cleared" msgid="917434007919176024">"Obrazy nie będą pokazywane automatycznie."</string>
<string name="whats_new_dialog_title" msgid="4230806739326698666">"Co nowego"</string>
<string name="notification_action_reply" msgid="6015299134424685297">"Odpowiedz"</string>
<string name="notification_action_reply_all" msgid="20020468410400912">"Odpowiedz wszystkim"</string>
@@ -409,4 +413,5 @@
<string name="drawer_open" msgid="6074646853178471940">"Otwórz szufladę nawigacji"</string>
<string name="drawer_close" msgid="2764774620737876943">"Zamknij szufladę nawigacji"</string>
<string name="conversation_photo_welcome_text" msgid="4274875219447670662">"Kliknij zdjęcie nadawcy, by wybrać tę rozmowę."</string>
+ <string name="folder_icon_desc" msgid="1500547397347480618">"Ikona folderu"</string>
</resources>
diff --git a/res/values-pt-rPT/strings.xml b/res/values-pt-rPT/strings.xml
index 481d1bc..145a265 100644
--- a/res/values-pt-rPT/strings.xml
+++ b/res/values-pt-rPT/strings.xml
@@ -361,6 +361,10 @@
<item msgid="732746454445519134">"Mostrar conversa mais antiga após eliminar"</item>
<item msgid="2189929276292165301">"Mostrar lista de conversas após eliminar"</item>
</string-array>
+ <string name="clear_display_images_whitelist_title" msgid="7120575487854245735">"Limpar aprovações de fotografias"</string>
+ <string name="clear_display_images_whitelist_dialog_title" msgid="3190704164490442683">"Limpar aprovações de fotografias?"</string>
+ <string name="clear_display_images_whitelist_dialog_message" msgid="1169152185612117654">"Parar apresentação de imagens inline de remetentes anteriormente permitidos."</string>
+ <string name="sender_whitelist_cleared" msgid="917434007919176024">"As imagens não serão mostradas automaticamente."</string>
<string name="whats_new_dialog_title" msgid="4230806739326698666">"Novidades"</string>
<string name="notification_action_reply" msgid="6015299134424685297">"Responder"</string>
<string name="notification_action_reply_all" msgid="20020468410400912">"Responder a todos"</string>
@@ -412,4 +416,6 @@
<string name="drawer_open" msgid="6074646853178471940">"Abrir separador de navegação"</string>
<string name="drawer_close" msgid="2764774620737876943">"Fechar separador de navegação"</string>
<string name="conversation_photo_welcome_text" msgid="4274875219447670662">"Toque numa imagem de remetente para selecionar essa conversa."</string>
+ <!-- no translation found for folder_icon_desc (1500547397347480618) -->
+ <skip />
</resources>
diff --git a/res/values-pt/strings.xml b/res/values-pt/strings.xml
index 8d8e0fb..2ca5d72 100644
--- a/res/values-pt/strings.xml
+++ b/res/values-pt/strings.xml
@@ -361,6 +361,10 @@
<item msgid="732746454445519134">"Exibir conversa mais antiga depois de excluir"</item>
<item msgid="2189929276292165301">"Exibir a lista de conversas depois de excluir"</item>
</string-array>
+ <string name="clear_display_images_whitelist_title" msgid="7120575487854245735">"Limpar aprovações de fotos"</string>
+ <string name="clear_display_images_whitelist_dialog_title" msgid="3190704164490442683">"Limpar aprovações de fotos?"</string>
+ <string name="clear_display_images_whitelist_dialog_message" msgid="1169152185612117654">"Não mostrar mais imagens in-line de remetentes que você autorizou anteriormente."</string>
+ <string name="sender_whitelist_cleared" msgid="917434007919176024">"As imagens não serão exibidas automaticamente."</string>
<string name="whats_new_dialog_title" msgid="4230806739326698666">"O que há de novo"</string>
<string name="notification_action_reply" msgid="6015299134424685297">"Responder"</string>
<string name="notification_action_reply_all" msgid="20020468410400912">"Responder a todos"</string>
@@ -411,4 +415,5 @@
<string name="drawer_open" msgid="6074646853178471940">"Abrir gaveta de navegação"</string>
<string name="drawer_close" msgid="2764774620737876943">"Fechar gaveta de navegação"</string>
<string name="conversation_photo_welcome_text" msgid="4274875219447670662">"Toque na imagem do remetente para selecionar a conversa."</string>
+ <string name="folder_icon_desc" msgid="1500547397347480618">"Ícone de pasta"</string>
</resources>
diff --git a/res/values-rm/strings.xml b/res/values-rm/strings.xml
index b55956e..4f20042 100644
--- a/res/values-rm/strings.xml
+++ b/res/values-rm/strings.xml
@@ -560,6 +560,14 @@
<!-- no translation found for prefSummaries_autoAdvance:0 (6389534341359835440) -->
<!-- no translation found for prefSummaries_autoAdvance:1 (732746454445519134) -->
<!-- no translation found for prefSummaries_autoAdvance:2 (2189929276292165301) -->
+ <!-- no translation found for clear_display_images_whitelist_title (7120575487854245735) -->
+ <skip />
+ <!-- no translation found for clear_display_images_whitelist_dialog_title (3190704164490442683) -->
+ <skip />
+ <!-- no translation found for clear_display_images_whitelist_dialog_message (1169152185612117654) -->
+ <skip />
+ <!-- no translation found for sender_whitelist_cleared (917434007919176024) -->
+ <skip />
<!-- no translation found for whats_new_dialog_title (4230806739326698666) -->
<skip />
<!-- no translation found for notification_action_reply (6015299134424685297) -->
@@ -636,4 +644,6 @@
<skip />
<!-- no translation found for conversation_photo_welcome_text (4274875219447670662) -->
<skip />
+ <!-- no translation found for folder_icon_desc (1500547397347480618) -->
+ <skip />
</resources>
diff --git a/res/values-ro/strings.xml b/res/values-ro/strings.xml
index 0f3b114..08bf770 100644
--- a/res/values-ro/strings.xml
+++ b/res/values-ro/strings.xml
@@ -361,6 +361,14 @@
<item msgid="732746454445519134">"Afişaţi conversaţia mai veche după ce ştergeţi"</item>
<item msgid="2189929276292165301">"Afişaţi lista de conversaţii după ce ştergeţi"</item>
</string-array>
+ <!-- no translation found for clear_display_images_whitelist_title (7120575487854245735) -->
+ <skip />
+ <!-- no translation found for clear_display_images_whitelist_dialog_title (3190704164490442683) -->
+ <skip />
+ <!-- no translation found for clear_display_images_whitelist_dialog_message (1169152185612117654) -->
+ <skip />
+ <!-- no translation found for sender_whitelist_cleared (917434007919176024) -->
+ <skip />
<string name="whats_new_dialog_title" msgid="4230806739326698666">"Ce este nou"</string>
<string name="notification_action_reply" msgid="6015299134424685297">"Răspundeți"</string>
<string name="notification_action_reply_all" msgid="20020468410400912">"Răspundeți tuturor"</string>
@@ -412,4 +420,6 @@
<string name="drawer_open" msgid="6074646853178471940">"Deschideți panoul de navigare"</string>
<string name="drawer_close" msgid="2764774620737876943">"Închideți panoul de navigare"</string>
<string name="conversation_photo_welcome_text" msgid="4274875219447670662">"Atingeți imaginea unui expeditor pentru a selecta respectiva conversație."</string>
+ <!-- no translation found for folder_icon_desc (1500547397347480618) -->
+ <skip />
</resources>
diff --git a/res/values-ru/strings.xml b/res/values-ru/strings.xml
index bdf935b..5a7c073 100644
--- a/res/values-ru/strings.xml
+++ b/res/values-ru/strings.xml
@@ -361,6 +361,14 @@
<item msgid="732746454445519134">"Показывать более раннюю цепочку после удаления текущей"</item>
<item msgid="2189929276292165301">"Показывать список цепочек после удаления текущей цепочки"</item>
</string-array>
+ <!-- no translation found for clear_display_images_whitelist_title (7120575487854245735) -->
+ <skip />
+ <!-- no translation found for clear_display_images_whitelist_dialog_title (3190704164490442683) -->
+ <skip />
+ <!-- no translation found for clear_display_images_whitelist_dialog_message (1169152185612117654) -->
+ <skip />
+ <!-- no translation found for sender_whitelist_cleared (917434007919176024) -->
+ <skip />
<string name="whats_new_dialog_title" msgid="4230806739326698666">"Что нового"</string>
<string name="notification_action_reply" msgid="6015299134424685297">"Ответить"</string>
<string name="notification_action_reply_all" msgid="20020468410400912">"Ответить всем"</string>
@@ -412,4 +420,6 @@
<string name="drawer_open" msgid="6074646853178471940">"Открыть панель навигации"</string>
<string name="drawer_close" msgid="2764774620737876943">"Закрыть панель навигации"</string>
<string name="conversation_photo_welcome_text" msgid="4274875219447670662">"Чтобы открыть цепочку писем, нажмите на фото отправителя."</string>
+ <!-- no translation found for folder_icon_desc (1500547397347480618) -->
+ <skip />
</resources>
diff --git a/res/values-sk/strings.xml b/res/values-sk/strings.xml
index 9e4999d..adc0d17 100644
--- a/res/values-sk/strings.xml
+++ b/res/values-sk/strings.xml
@@ -361,6 +361,14 @@
<item msgid="732746454445519134">"Po odstránení zobraziť staršiu konverzáciu"</item>
<item msgid="2189929276292165301">"Po odstránení zobraziť zoznam konverzácií"</item>
</string-array>
+ <!-- no translation found for clear_display_images_whitelist_title (7120575487854245735) -->
+ <skip />
+ <!-- no translation found for clear_display_images_whitelist_dialog_title (3190704164490442683) -->
+ <skip />
+ <!-- no translation found for clear_display_images_whitelist_dialog_message (1169152185612117654) -->
+ <skip />
+ <!-- no translation found for sender_whitelist_cleared (917434007919176024) -->
+ <skip />
<string name="whats_new_dialog_title" msgid="4230806739326698666">"Novinky"</string>
<string name="notification_action_reply" msgid="6015299134424685297">"Odpovedať"</string>
<string name="notification_action_reply_all" msgid="20020468410400912">"Odpovedať všetkým"</string>
@@ -411,4 +419,5 @@
<string name="drawer_open" msgid="6074646853178471940">"Otvoriť priečinok s navigáciou"</string>
<string name="drawer_close" msgid="2764774620737876943">"Zavrieť priečinok s navigáciou"</string>
<string name="conversation_photo_welcome_text" msgid="4274875219447670662">"Dotknutím sa obrázka odosielateľa vyberiete danú konverzáciu."</string>
+ <string name="folder_icon_desc" msgid="1500547397347480618">"Ikona priečinka"</string>
</resources>
diff --git a/res/values-sl/strings.xml b/res/values-sl/strings.xml
index 721d21b..17e1d57 100644
--- a/res/values-sl/strings.xml
+++ b/res/values-sl/strings.xml
@@ -361,6 +361,10 @@
<item msgid="732746454445519134">"Po izbrisu pokaži starejši pogovor"</item>
<item msgid="2189929276292165301">"Po izbrisu pokaži seznam pogovorov"</item>
</string-array>
+ <string name="clear_display_images_whitelist_title" msgid="7120575487854245735">"Izbriši odobritve slik"</string>
+ <string name="clear_display_images_whitelist_dialog_title" msgid="3190704164490442683">"Želite izbrisati odobritve slik?"</string>
+ <string name="clear_display_images_whitelist_dialog_message" msgid="1169152185612117654">"Nehaj prikazovati slike v sporočilih pošiljateljev, za katere ste to odobrili."</string>
+ <string name="sender_whitelist_cleared" msgid="917434007919176024">"Slike ne bodo samodejno prikazane."</string>
<string name="whats_new_dialog_title" msgid="4230806739326698666">"Kaj je novega?"</string>
<string name="notification_action_reply" msgid="6015299134424685297">"Odgovori"</string>
<string name="notification_action_reply_all" msgid="20020468410400912">"Odgovori vsem"</string>
@@ -411,4 +415,5 @@
<string name="drawer_open" msgid="6074646853178471940">"Odpri predal za navigacijo"</string>
<string name="drawer_close" msgid="2764774620737876943">"Zapri predal za navigacijo"</string>
<string name="conversation_photo_welcome_text" msgid="4274875219447670662">"Če želite izbrati ta pogovor, se dotaknite slike pošiljatelja."</string>
+ <string name="folder_icon_desc" msgid="1500547397347480618">"Ikona mape"</string>
</resources>
diff --git a/res/values-sr/strings.xml b/res/values-sr/strings.xml
index 879d19c..399965e 100644
--- a/res/values-sr/strings.xml
+++ b/res/values-sr/strings.xml
@@ -361,6 +361,14 @@
<item msgid="732746454445519134">"Приказ старије преписке након брисања"</item>
<item msgid="2189929276292165301">"Приказ листе преписки након брисања"</item>
</string-array>
+ <!-- no translation found for clear_display_images_whitelist_title (7120575487854245735) -->
+ <skip />
+ <!-- no translation found for clear_display_images_whitelist_dialog_title (3190704164490442683) -->
+ <skip />
+ <!-- no translation found for clear_display_images_whitelist_dialog_message (1169152185612117654) -->
+ <skip />
+ <!-- no translation found for sender_whitelist_cleared (917434007919176024) -->
+ <skip />
<string name="whats_new_dialog_title" msgid="4230806739326698666">"Шта је ново"</string>
<string name="notification_action_reply" msgid="6015299134424685297">"Одговори"</string>
<string name="notification_action_reply_all" msgid="20020468410400912">"Одговори свима"</string>
@@ -411,4 +419,5 @@
<string name="drawer_open" msgid="6074646853178471940">"Отвори фиоку за навигацију"</string>
<string name="drawer_close" msgid="2764774620737876943">"Затвори фиоку за навигацију"</string>
<string name="conversation_photo_welcome_text" msgid="4274875219447670662">"Додирните слику пошиљаоца да бисте изабрали ту преписку."</string>
+ <string name="folder_icon_desc" msgid="1500547397347480618">"Икона директоријума"</string>
</resources>
diff --git a/res/values-sv/strings.xml b/res/values-sv/strings.xml
index 55cc331..8f27d48 100644
--- a/res/values-sv/strings.xml
+++ b/res/values-sv/strings.xml
@@ -361,6 +361,14 @@
<item msgid="732746454445519134">"Visa en äldre konversation efter borttagning"</item>
<item msgid="2189929276292165301">"Visa konversationslistan efter borttagning"</item>
</string-array>
+ <!-- no translation found for clear_display_images_whitelist_title (7120575487854245735) -->
+ <skip />
+ <!-- no translation found for clear_display_images_whitelist_dialog_title (3190704164490442683) -->
+ <skip />
+ <!-- no translation found for clear_display_images_whitelist_dialog_message (1169152185612117654) -->
+ <skip />
+ <!-- no translation found for sender_whitelist_cleared (917434007919176024) -->
+ <skip />
<string name="whats_new_dialog_title" msgid="4230806739326698666">"Nyheter"</string>
<string name="notification_action_reply" msgid="6015299134424685297">"Svara"</string>
<string name="notification_action_reply_all" msgid="20020468410400912">"Svara alla"</string>
@@ -412,4 +420,6 @@
<string name="drawer_open" msgid="6074646853178471940">"Öppna navigeringsmenyn"</string>
<string name="drawer_close" msgid="2764774620737876943">"Stäng navigeringsmenyn"</string>
<string name="conversation_photo_welcome_text" msgid="4274875219447670662">"Tryck på avsändarens bild om du vill välja den konversationen."</string>
+ <!-- no translation found for folder_icon_desc (1500547397347480618) -->
+ <skip />
</resources>
diff --git a/res/values-sw/strings.xml b/res/values-sw/strings.xml
index 44fcc7a..2ab0659 100644
--- a/res/values-sw/strings.xml
+++ b/res/values-sw/strings.xml
@@ -361,6 +361,10 @@
<item msgid="732746454445519134">"Onyesha mazungumzo ya zamani zaidi baada ya kufuta"</item>
<item msgid="2189929276292165301">"Onyesha orodha ya mazungumzo baada ya kufuta"</item>
</string-array>
+ <string name="clear_display_images_whitelist_title" msgid="7120575487854245735">"Futa uidhinishaji wa picha"</string>
+ <string name="clear_display_images_whitelist_dialog_title" msgid="3190704164490442683">"Futa uidhinishaji wa picha?"</string>
+ <string name="clear_display_images_whitelist_dialog_message" msgid="1169152185612117654">"Acha kuonyesha picha za mistari ya ndani kutoka kwa watumaji uliowaruhusu hapo awali."</string>
+ <string name="sender_whitelist_cleared" msgid="917434007919176024">"Picha hazitaonyeshwa kiotomatiki."</string>
<string name="whats_new_dialog_title" msgid="4230806739326698666">"Nini kipya"</string>
<string name="notification_action_reply" msgid="6015299134424685297">"Jibu"</string>
<string name="notification_action_reply_all" msgid="20020468410400912">"Jibu wote"</string>
@@ -412,4 +416,6 @@
<string name="drawer_open" msgid="6074646853178471940">"Fungua menyu"</string>
<string name="drawer_close" msgid="2764774620737876943">"Funga menyu"</string>
<string name="conversation_photo_welcome_text" msgid="4274875219447670662">"Gusa picha ya mtumaji ili uchague mazungumzo hayo."</string>
+ <!-- no translation found for folder_icon_desc (1500547397347480618) -->
+ <skip />
</resources>
diff --git a/res/values-th/strings.xml b/res/values-th/strings.xml
index 77bd6aa..d73fb4e 100644
--- a/res/values-th/strings.xml
+++ b/res/values-th/strings.xml
@@ -361,6 +361,14 @@
<item msgid="732746454445519134">"แสดงการสนทนาที่เก่ากว่าหลังจากลบ"</item>
<item msgid="2189929276292165301">"แสดงรายการสนทนาหลังจากลบ"</item>
</string-array>
+ <!-- no translation found for clear_display_images_whitelist_title (7120575487854245735) -->
+ <skip />
+ <!-- no translation found for clear_display_images_whitelist_dialog_title (3190704164490442683) -->
+ <skip />
+ <!-- no translation found for clear_display_images_whitelist_dialog_message (1169152185612117654) -->
+ <skip />
+ <!-- no translation found for sender_whitelist_cleared (917434007919176024) -->
+ <skip />
<string name="whats_new_dialog_title" msgid="4230806739326698666">"มีอะไรใหม่"</string>
<string name="notification_action_reply" msgid="6015299134424685297">"ตอบ"</string>
<string name="notification_action_reply_all" msgid="20020468410400912">"ตอบทั้งหมด"</string>
@@ -411,4 +419,5 @@
<string name="drawer_open" msgid="6074646853178471940">"เปิดช่องการนำทาง"</string>
<string name="drawer_close" msgid="2764774620737876943">"ปิดช่องการนำทาง"</string>
<string name="conversation_photo_welcome_text" msgid="4274875219447670662">"แตะภาพผู้ส่งเพื่อเลือกการสนทนานั้น"</string>
+ <string name="folder_icon_desc" msgid="1500547397347480618">"ไอคอนโฟลเดอร์"</string>
</resources>
diff --git a/res/values-tl/strings.xml b/res/values-tl/strings.xml
index d4c10ae..9196102 100644
--- a/res/values-tl/strings.xml
+++ b/res/values-tl/strings.xml
@@ -361,6 +361,14 @@
<item msgid="732746454445519134">"Magpakita ng mas lumang pag-uusap pagkatapos mong magtanggal"</item>
<item msgid="2189929276292165301">"Ipakita ang listahan ng pag-uusap pagkatapos mong magtanggal"</item>
</string-array>
+ <!-- no translation found for clear_display_images_whitelist_title (7120575487854245735) -->
+ <skip />
+ <!-- no translation found for clear_display_images_whitelist_dialog_title (3190704164490442683) -->
+ <skip />
+ <!-- no translation found for clear_display_images_whitelist_dialog_message (1169152185612117654) -->
+ <skip />
+ <!-- no translation found for sender_whitelist_cleared (917434007919176024) -->
+ <skip />
<string name="whats_new_dialog_title" msgid="4230806739326698666">"Ano\'ng bago"</string>
<string name="notification_action_reply" msgid="6015299134424685297">"Tumugon"</string>
<string name="notification_action_reply_all" msgid="20020468410400912">"Tumugon sa lahat"</string>
@@ -411,4 +419,5 @@
<string name="drawer_open" msgid="6074646853178471940">"Buksan ang drawer ng nabigasyon"</string>
<string name="drawer_close" msgid="2764774620737876943">"Isara ang drawer ng nabigasyon"</string>
<string name="conversation_photo_welcome_text" msgid="4274875219447670662">"Pumidot ng larawan ng nagpadala upang piliin ang pag-uusap na iyon."</string>
+ <string name="folder_icon_desc" msgid="1500547397347480618">"Icon ng folder"</string>
</resources>
diff --git a/res/values-tr/strings.xml b/res/values-tr/strings.xml
index 0efdae5..17b48ca 100644
--- a/res/values-tr/strings.xml
+++ b/res/values-tr/strings.xml
@@ -361,6 +361,14 @@
<item msgid="732746454445519134">"Sildikten sonra daha eski ileti dizisini göster"</item>
<item msgid="2189929276292165301">"Sildikten sonra ileti dizisi listesini göster"</item>
</string-array>
+ <!-- no translation found for clear_display_images_whitelist_title (7120575487854245735) -->
+ <skip />
+ <!-- no translation found for clear_display_images_whitelist_dialog_title (3190704164490442683) -->
+ <skip />
+ <!-- no translation found for clear_display_images_whitelist_dialog_message (1169152185612117654) -->
+ <skip />
+ <!-- no translation found for sender_whitelist_cleared (917434007919176024) -->
+ <skip />
<string name="whats_new_dialog_title" msgid="4230806739326698666">"Yenilikler"</string>
<string name="notification_action_reply" msgid="6015299134424685297">"Yanıtla"</string>
<string name="notification_action_reply_all" msgid="20020468410400912">"Tümünü yanıtla"</string>
@@ -412,4 +420,6 @@
<string name="drawer_open" msgid="6074646853178471940">"Gezinme menüsünü aç"</string>
<string name="drawer_close" msgid="2764774620737876943">"Gezinme menüsünü kapat"</string>
<string name="conversation_photo_welcome_text" msgid="4274875219447670662">"İlgili ileti dizisini seçmek için gönderenin resmine dokunun."</string>
+ <!-- no translation found for folder_icon_desc (1500547397347480618) -->
+ <skip />
</resources>
diff --git a/res/values-uk/strings.xml b/res/values-uk/strings.xml
index 238066a..7d017fe 100644
--- a/res/values-uk/strings.xml
+++ b/res/values-uk/strings.xml
@@ -361,6 +361,10 @@
<item msgid="732746454445519134">"Показувати старішу бесіду після видалення"</item>
<item msgid="2189929276292165301">"Показувати список бесід після видалення"</item>
</string-array>
+ <string name="clear_display_images_whitelist_title" msgid="7120575487854245735">"Очистити схвалення зображень"</string>
+ <string name="clear_display_images_whitelist_dialog_title" msgid="3190704164490442683">"Очистити схвалення зображень?"</string>
+ <string name="clear_display_images_whitelist_dialog_message" msgid="1169152185612117654">"Не показувати раніше дозволені вставлені зображення від відправників."</string>
+ <string name="sender_whitelist_cleared" msgid="917434007919176024">"Зображення не показуватимуться автоматично."</string>
<string name="whats_new_dialog_title" msgid="4230806739326698666">"Новинки"</string>
<string name="notification_action_reply" msgid="6015299134424685297">"Відповісти"</string>
<string name="notification_action_reply_all" msgid="20020468410400912">"Відповісти всім"</string>
@@ -411,4 +415,5 @@
<string name="drawer_open" msgid="6074646853178471940">"Висунути навігаційну панель"</string>
<string name="drawer_close" msgid="2764774620737876943">"Сховати навігаційну панель"</string>
<string name="conversation_photo_welcome_text" msgid="4274875219447670662">"Торкніться зображення відправника, щоб вибрати бесіду."</string>
+ <string name="folder_icon_desc" msgid="1500547397347480618">"Значок папки"</string>
</resources>
diff --git a/res/values-vi/strings.xml b/res/values-vi/strings.xml
index 62cc02e..8088d66 100644
--- a/res/values-vi/strings.xml
+++ b/res/values-vi/strings.xml
@@ -361,6 +361,14 @@
<item msgid="732746454445519134">"Hiển thị cuộc hội thoại cũ hơn sau khi bạn xóa"</item>
<item msgid="2189929276292165301">"Hiển thị danh sách cuộc hội thoại sau khi bạn xóa"</item>
</string-array>
+ <!-- no translation found for clear_display_images_whitelist_title (7120575487854245735) -->
+ <skip />
+ <!-- no translation found for clear_display_images_whitelist_dialog_title (3190704164490442683) -->
+ <skip />
+ <!-- no translation found for clear_display_images_whitelist_dialog_message (1169152185612117654) -->
+ <skip />
+ <!-- no translation found for sender_whitelist_cleared (917434007919176024) -->
+ <skip />
<string name="whats_new_dialog_title" msgid="4230806739326698666">"Tính năng mới"</string>
<string name="notification_action_reply" msgid="6015299134424685297">"Trả lời"</string>
<string name="notification_action_reply_all" msgid="20020468410400912">"Trả lời tất cả"</string>
@@ -411,4 +419,5 @@
<string name="drawer_open" msgid="6074646853178471940">"Mở ngăn kéo điều hướng"</string>
<string name="drawer_close" msgid="2764774620737876943">"Đóng ngăn kéo điều hướng"</string>
<string name="conversation_photo_welcome_text" msgid="4274875219447670662">"Chạm vào hình ảnh của người gửi để chọn cuộc hội thoại đó."</string>
+ <string name="folder_icon_desc" msgid="1500547397347480618">"Biểu tượng thư mục"</string>
</resources>
diff --git a/res/values-zh-rCN/strings.xml b/res/values-zh-rCN/strings.xml
index 1ffa890..5900800 100644
--- a/res/values-zh-rCN/strings.xml
+++ b/res/values-zh-rCN/strings.xml
@@ -361,6 +361,14 @@
<item msgid="732746454445519134">"在您执行删除操作后显示较旧的前一个会话"</item>
<item msgid="2189929276292165301">"在您执行删除操作后显示会话列表"</item>
</string-array>
+ <!-- no translation found for clear_display_images_whitelist_title (7120575487854245735) -->
+ <skip />
+ <!-- no translation found for clear_display_images_whitelist_dialog_title (3190704164490442683) -->
+ <skip />
+ <!-- no translation found for clear_display_images_whitelist_dialog_message (1169152185612117654) -->
+ <skip />
+ <!-- no translation found for sender_whitelist_cleared (917434007919176024) -->
+ <skip />
<string name="whats_new_dialog_title" msgid="4230806739326698666">"新功能"</string>
<string name="notification_action_reply" msgid="6015299134424685297">"回复"</string>
<string name="notification_action_reply_all" msgid="20020468410400912">"全部回复"</string>
@@ -412,4 +420,6 @@
<string name="drawer_open" msgid="6074646853178471940">"打开抽屉式导航栏"</string>
<string name="drawer_close" msgid="2764774620737876943">"关闭抽屉式导航栏"</string>
<string name="conversation_photo_welcome_text" msgid="4274875219447670662">"触摸发件人头像即可选择该会话。"</string>
+ <!-- no translation found for folder_icon_desc (1500547397347480618) -->
+ <skip />
</resources>
diff --git a/res/values-zh-rTW/strings.xml b/res/values-zh-rTW/strings.xml
index 828e041..2197cd2 100644
--- a/res/values-zh-rTW/strings.xml
+++ b/res/values-zh-rTW/strings.xml
@@ -361,6 +361,14 @@
<item msgid="732746454445519134">"在刪除動作完成後顯示較舊的會話群組"</item>
<item msgid="2189929276292165301">"在刪除動作完成後顯示會話群組清單"</item>
</string-array>
+ <!-- no translation found for clear_display_images_whitelist_title (7120575487854245735) -->
+ <skip />
+ <!-- no translation found for clear_display_images_whitelist_dialog_title (3190704164490442683) -->
+ <skip />
+ <!-- no translation found for clear_display_images_whitelist_dialog_message (1169152185612117654) -->
+ <skip />
+ <!-- no translation found for sender_whitelist_cleared (917434007919176024) -->
+ <skip />
<string name="whats_new_dialog_title" msgid="4230806739326698666">"新功能"</string>
<string name="notification_action_reply" msgid="6015299134424685297">"回覆"</string>
<string name="notification_action_reply_all" msgid="20020468410400912">"回覆所有人"</string>
@@ -411,4 +419,5 @@
<string name="drawer_open" msgid="6074646853178471940">"開啟導覽匣"</string>
<string name="drawer_close" msgid="2764774620737876943">"關閉導覽匣"</string>
<string name="conversation_photo_welcome_text" msgid="4274875219447670662">"輕觸寄件者圖片即可選取該會話群組。"</string>
+ <string name="folder_icon_desc" msgid="1500547397347480618">"資料夾圖示"</string>
</resources>
diff --git a/res/values-zu/strings.xml b/res/values-zu/strings.xml
index 3ad8e38..39a9f13 100644
--- a/res/values-zu/strings.xml
+++ b/res/values-zu/strings.xml
@@ -361,6 +361,10 @@
<item msgid="732746454445519134">"Bonisa ingxoxo endala ngemuva kokususa"</item>
<item msgid="2189929276292165301">"Bonisa uhlu lwengxoxo ngemuva kokususa"</item>
</string-array>
+ <string name="clear_display_images_whitelist_title" msgid="7120575487854245735">"Sula ukuvunywa kwesithombe"</string>
+ <string name="clear_display_images_whitelist_dialog_title" msgid="3190704164490442683">"Sula ukuvunywa kwesithombe?"</string>
+ <string name="clear_display_images_whitelist_dialog_message" msgid="1169152185612117654">"Misa ukubonisa izithombe ezisemugqeni ezisuka kubathumeli obavumele ngaphambilini."</string>
+ <string name="sender_whitelist_cleared" msgid="917434007919176024">"Izithombe azizukukhonjiswa ngokuzenzakalela."</string>
<string name="whats_new_dialog_title" msgid="4230806739326698666">"Yini entsha"</string>
<string name="notification_action_reply" msgid="6015299134424685297">"Phendula"</string>
<string name="notification_action_reply_all" msgid="20020468410400912">"Phendula konke"</string>
@@ -412,4 +416,6 @@
<string name="drawer_open" msgid="6074646853178471940">"Vula ukuzulazula kwekhabethe"</string>
<string name="drawer_close" msgid="2764774620737876943">"Vala ukuzulazula kwekhabethe"</string>
<string name="conversation_photo_welcome_text" msgid="4274875219447670662">"Thinta isithombe somthumeli ukuze ukhethe ingxoxo."</string>
+ <!-- no translation found for folder_icon_desc (1500547397347480618) -->
+ <skip />
</resources>
diff --git a/res/values/attrs.xml b/res/values/attrs.xml
index 70fc10a..0a2d767 100644
--- a/res/values/attrs.xml
+++ b/res/values/attrs.xml
@@ -27,4 +27,7 @@
<add-resource name="RecipientComposeFieldLayout" type="style" />
<add-resource name="ComposeBodyStyle" type="style" />
<add-resource name="ComposeSubjectStyle" type="style" />
+ <declare-styleable name="FolderItemViewDrawableState">
+ <attr name="state_drag_mode" format="boolean" />
+ </declare-styleable>
</resources>
diff --git a/res/values/strings.xml b/res/values/strings.xml
index 4c6ed01..cb17a11 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -151,6 +151,8 @@
<string name="menu_change_folders">Change folders</string>
<!-- Menu item: moves to folders for selected conversation(s). [CHAR LIMIT = 30] -->
<string name="menu_move_to">Move to</string>
+ <!-- Menu item: moves current or selected conversation(s) to Inbox. [CHAR LIMIT = 30] -->
+ <string name="menu_move_to_inbox">Move to Inbox</string>
<!-- Menu item: manages the folders for this account. [CHAR LIMIT = 30] -->
<string name="menu_manage_folders">Folder settings</string>
<!-- Menu item: report an email was not readable or poorly rendered -->
@@ -795,6 +797,20 @@
<item>list</item>
</string-array>
+ <!-- Settings screen, title of "Restore default for "Show pictures"" [CHAR LIMIT=1000]-->
+ <string name="clear_display_images_whitelist_title">Clear picture approvals</string>
+
+ <!-- Settings screen, title of dialog shown to confirm action when user taps
+ "Clear picture approvals" in preferences [CHAR LIMIT=200]-->
+ <string name="clear_display_images_whitelist_dialog_title">Clear picture approvals?</string>
+ <!-- Settings screen, message of dialog shown to confirm action when tapping
+ "Clear picture approvals" [CHAR LIMIT=1000]-->
+ <string name="clear_display_images_whitelist_dialog_message">Stop displaying inline images from senders you previously allowed.</string>
+
+ <!-- Message shown in toast when the user taps "Restore default for "Show pictures"" in Gmail general preferences. [CHAR LIMIT=50] -->
+ <string name="sender_whitelist_cleared">Pictures won\'t be shown automatically.</string>
+
+
<!-- Dialog title for the What's New dialog. [CHAR LIMIT=50] -->
<string name="whats_new_dialog_title">What\'s new</string>
@@ -911,4 +927,6 @@
<string name="drawer_close">Close navigation drawer</string>
<string name="conversation_photo_welcome_text">Touch a sender image to select that conversation.</string>
+ <!-- Content description for the folder icon for nested folders. -->
+ <string name="folder_icon_desc">Folder icon</string>
</resources>
diff --git a/src/com/android/emailcommon/TempDirectory.java b/src/com/android/emailcommon/TempDirectory.java
new file mode 100644
index 0000000..252488c
--- /dev/null
+++ b/src/com/android/emailcommon/TempDirectory.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.emailcommon;
+
+import android.content.Context;
+
+import java.io.File;
+
+/**
+ * TempDirectory caches the directory used for caching file. It is set up during application
+ * initialization.
+ */
+public class TempDirectory {
+ private static File sTempDirectory = null;
+
+ public static void setTempDirectory(Context context) {
+ sTempDirectory = context.getCacheDir();
+ }
+
+ public static File getTempDirectory() {
+ if (sTempDirectory == null) {
+ throw new RuntimeException(
+ "TempDirectory not set. " +
+ "If in a unit test, call Email.setTempDirectory(context) in setUp().");
+ }
+ return sTempDirectory;
+ }
+}
diff --git a/src/com/android/emailcommon/internet/BinaryTempFileBody.java b/src/com/android/emailcommon/internet/BinaryTempFileBody.java
new file mode 100644
index 0000000..f0821ed
--- /dev/null
+++ b/src/com/android/emailcommon/internet/BinaryTempFileBody.java
@@ -0,0 +1,89 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.emailcommon.internet;
+
+import com.android.emailcommon.TempDirectory;
+import com.android.emailcommon.mail.Body;
+import com.android.emailcommon.mail.MessagingException;
+
+import org.apache.commons.io.IOUtils;
+
+import android.util.Base64;
+import android.util.Base64OutputStream;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.FilterInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+
+/**
+ * A Body that is backed by a temp file. The Body exposes a getOutputStream method that allows
+ * the user to write to the temp file. After the write the body is available via getInputStream
+ * and writeTo one time. After writeTo is called, or the InputStream returned from
+ * getInputStream is closed the file is deleted and the Body should be considered disposed of.
+ */
+public class BinaryTempFileBody implements Body {
+ private File mFile;
+
+ /**
+ * An alternate way to put data into a BinaryTempFileBody is to simply supply an already-
+ * created file. Note that this file will be deleted after it is read.
+ * @param filePath The file containing the data to be stored on disk temporarily
+ */
+ public void setFile(String filePath) {
+ mFile = new File(filePath);
+ }
+
+ public OutputStream getOutputStream() throws IOException {
+ mFile = File.createTempFile("body", null, TempDirectory.getTempDirectory());
+ mFile.deleteOnExit();
+ return new FileOutputStream(mFile);
+ }
+
+ public InputStream getInputStream() throws MessagingException {
+ try {
+ return new BinaryTempFileBodyInputStream(new FileInputStream(mFile));
+ }
+ catch (IOException ioe) {
+ throw new MessagingException("Unable to open body", ioe);
+ }
+ }
+
+ public void writeTo(OutputStream out) throws IOException, MessagingException {
+ InputStream in = getInputStream();
+ Base64OutputStream base64Out = new Base64OutputStream(
+ out, Base64.CRLF | Base64.NO_CLOSE);
+ IOUtils.copy(in, base64Out);
+ base64Out.close();
+ mFile.delete();
+ }
+
+ class BinaryTempFileBodyInputStream extends FilterInputStream {
+ public BinaryTempFileBodyInputStream(InputStream in) {
+ super(in);
+ }
+
+ @Override
+ public void close() throws IOException {
+ super.close();
+ mFile.delete();
+ }
+ }
+}
diff --git a/src/com/android/emailcommon/internet/MimeBodyPart.java b/src/com/android/emailcommon/internet/MimeBodyPart.java
new file mode 100644
index 0000000..01efd55
--- /dev/null
+++ b/src/com/android/emailcommon/internet/MimeBodyPart.java
@@ -0,0 +1,193 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.emailcommon.internet;
+
+import com.android.emailcommon.mail.Body;
+import com.android.emailcommon.mail.BodyPart;
+import com.android.emailcommon.mail.MessagingException;
+
+import java.io.BufferedWriter;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.OutputStreamWriter;
+import java.util.regex.Pattern;
+
+/**
+ * TODO this is a close approximation of Message, need to update along with
+ * Message.
+ */
+public class MimeBodyPart extends BodyPart {
+ protected MimeHeader mHeader = new MimeHeader();
+ protected MimeHeader mExtendedHeader;
+ protected Body mBody;
+ protected int mSize;
+
+ // regex that matches content id surrounded by "<>" optionally.
+ private static final Pattern REMOVE_OPTIONAL_BRACKETS = Pattern.compile("^<?([^>]+)>?$");
+ // regex that matches end of line.
+ private static final Pattern END_OF_LINE = Pattern.compile("\r?\n");
+
+ public MimeBodyPart() throws MessagingException {
+ this(null);
+ }
+
+ public MimeBodyPart(Body body) throws MessagingException {
+ this(body, null);
+ }
+
+ public MimeBodyPart(Body body, String mimeType) throws MessagingException {
+ if (mimeType != null) {
+ setHeader(MimeHeader.HEADER_CONTENT_TYPE, mimeType);
+ }
+ setBody(body);
+ }
+
+ protected String getFirstHeader(String name) throws MessagingException {
+ return mHeader.getFirstHeader(name);
+ }
+
+ public void addHeader(String name, String value) throws MessagingException {
+ mHeader.addHeader(name, value);
+ }
+
+ public void setHeader(String name, String value) throws MessagingException {
+ mHeader.setHeader(name, value);
+ }
+
+ public String[] getHeader(String name) throws MessagingException {
+ return mHeader.getHeader(name);
+ }
+
+ public void removeHeader(String name) throws MessagingException {
+ mHeader.removeHeader(name);
+ }
+
+ public Body getBody() throws MessagingException {
+ return mBody;
+ }
+
+ public void setBody(Body body) throws MessagingException {
+ this.mBody = body;
+ if (body instanceof com.android.emailcommon.mail.Multipart) {
+ com.android.emailcommon.mail.Multipart multipart =
+ ((com.android.emailcommon.mail.Multipart)body);
+ multipart.setParent(this);
+ setHeader(MimeHeader.HEADER_CONTENT_TYPE, multipart.getContentType());
+ }
+ else if (body instanceof TextBody) {
+ String contentType = String.format("%s;\n charset=utf-8", getMimeType());
+ String name = MimeUtility.getHeaderParameter(getContentType(), "name");
+ if (name != null) {
+ contentType += String.format(";\n name=\"%s\"", name);
+ }
+ setHeader(MimeHeader.HEADER_CONTENT_TYPE, contentType);
+ setHeader(MimeHeader.HEADER_CONTENT_TRANSFER_ENCODING, "base64");
+ }
+ }
+
+ public String getContentType() throws MessagingException {
+ String contentType = getFirstHeader(MimeHeader.HEADER_CONTENT_TYPE);
+ if (contentType == null) {
+ return "text/plain";
+ } else {
+ return contentType;
+ }
+ }
+
+ public String getDisposition() throws MessagingException {
+ String contentDisposition = getFirstHeader(MimeHeader.HEADER_CONTENT_DISPOSITION);
+ if (contentDisposition == null) {
+ return null;
+ } else {
+ return contentDisposition;
+ }
+ }
+
+ public String getContentId() throws MessagingException {
+ String contentId = getFirstHeader(MimeHeader.HEADER_CONTENT_ID);
+ if (contentId == null) {
+ return null;
+ } else {
+ // remove optionally surrounding brackets.
+ return REMOVE_OPTIONAL_BRACKETS.matcher(contentId).replaceAll("$1");
+ }
+ }
+
+ public String getMimeType() throws MessagingException {
+ return MimeUtility.getHeaderParameter(getContentType(), null);
+ }
+
+ public boolean isMimeType(String mimeType) throws MessagingException {
+ return getMimeType().equals(mimeType);
+ }
+
+ public void setSize(int size) {
+ this.mSize = size;
+ }
+
+ public int getSize() throws MessagingException {
+ return mSize;
+ }
+
+ /**
+ * Set extended header
+ *
+ * @param name Extended header name
+ * @param value header value - flattened by removing CR-NL if any
+ * remove header if value is null
+ * @throws MessagingException
+ */
+ public void setExtendedHeader(String name, String value) throws MessagingException {
+ if (value == null) {
+ if (mExtendedHeader != null) {
+ mExtendedHeader.removeHeader(name);
+ }
+ return;
+ }
+ if (mExtendedHeader == null) {
+ mExtendedHeader = new MimeHeader();
+ }
+ mExtendedHeader.setHeader(name, END_OF_LINE.matcher(value).replaceAll(""));
+ }
+
+ /**
+ * Get extended header
+ *
+ * @param name Extended header name
+ * @return header value - null if header does not exist
+ * @throws MessagingException
+ */
+ public String getExtendedHeader(String name) throws MessagingException {
+ if (mExtendedHeader == null) {
+ return null;
+ }
+ return mExtendedHeader.getFirstHeader(name);
+ }
+
+ /**
+ * Write the MimeMessage out in MIME format.
+ */
+ public void writeTo(OutputStream out) throws IOException, MessagingException {
+ BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(out), 1024);
+ mHeader.writeTo(out);
+ writer.write("\r\n");
+ writer.flush();
+ if (mBody != null) {
+ mBody.writeTo(out);
+ }
+ }
+}
diff --git a/src/com/android/emailcommon/internet/MimeHeader.java b/src/com/android/emailcommon/internet/MimeHeader.java
new file mode 100644
index 0000000..e9b0212
--- /dev/null
+++ b/src/com/android/emailcommon/internet/MimeHeader.java
@@ -0,0 +1,166 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.emailcommon.internet;
+
+import com.android.emailcommon.mail.MessagingException;
+
+import java.io.BufferedWriter;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.OutputStreamWriter;
+import java.util.ArrayList;
+
+public class MimeHeader {
+ /**
+ * Application specific header that contains Store specific information about an attachment.
+ * In IMAP this contains the IMAP BODYSTRUCTURE part id so that the ImapStore can later
+ * retrieve the attachment at will from the server.
+ * The info is recorded from this header on LocalStore.appendMessages and is put back
+ * into the MIME data by LocalStore.fetch.
+ */
+ public static final String HEADER_ANDROID_ATTACHMENT_STORE_DATA = "X-Android-Attachment-StoreData";
+ /**
+ * Application specific header that is used to tag body parts for quoted/forwarded messages.
+ */
+ public static final String HEADER_ANDROID_BODY_QUOTED_PART = "X-Android-Body-Quoted-Part";
+
+ public static final String HEADER_CONTENT_TYPE = "Content-Type";
+ public static final String HEADER_CONTENT_TRANSFER_ENCODING = "Content-Transfer-Encoding";
+ public static final String HEADER_CONTENT_DISPOSITION = "Content-Disposition";
+ public static final String HEADER_CONTENT_ID = "Content-ID";
+
+ /**
+ * Fields that should be omitted when writing the header using writeTo()
+ */
+ private static final String[] WRITE_OMIT_FIELDS = {
+// HEADER_ANDROID_ATTACHMENT_DOWNLOADED,
+// HEADER_ANDROID_ATTACHMENT_ID,
+ HEADER_ANDROID_ATTACHMENT_STORE_DATA
+ };
+
+ protected final ArrayList<Field> mFields = new ArrayList<Field>();
+
+ public void clear() {
+ mFields.clear();
+ }
+
+ public String getFirstHeader(String name) throws MessagingException {
+ String[] header = getHeader(name);
+ if (header == null) {
+ return null;
+ }
+ return header[0];
+ }
+
+ public void addHeader(String name, String value) throws MessagingException {
+ mFields.add(new Field(name, value));
+ }
+
+ public void setHeader(String name, String value) throws MessagingException {
+ if (name == null || value == null) {
+ return;
+ }
+ removeHeader(name);
+ addHeader(name, value);
+ }
+
+ public String[] getHeader(String name) throws MessagingException {
+ ArrayList<String> values = new ArrayList<String>();
+ for (Field field : mFields) {
+ if (field.name.equalsIgnoreCase(name)) {
+ values.add(field.value);
+ }
+ }
+ if (values.size() == 0) {
+ return null;
+ }
+ return values.toArray(new String[] {});
+ }
+
+ public void removeHeader(String name) throws MessagingException {
+ ArrayList<Field> removeFields = new ArrayList<Field>();
+ for (Field field : mFields) {
+ if (field.name.equalsIgnoreCase(name)) {
+ removeFields.add(field);
+ }
+ }
+ mFields.removeAll(removeFields);
+ }
+
+ /**
+ * Write header into String
+ *
+ * @return CR-NL separated header string except the headers in writeOmitFields
+ * null if header is empty
+ */
+ public String writeToString() {
+ if (mFields.size() == 0) {
+ return null;
+ }
+ StringBuilder builder = new StringBuilder();
+ for (Field field : mFields) {
+ if (!arrayContains(WRITE_OMIT_FIELDS, field.name)) {
+ builder.append(field.name + ": " + field.value + "\r\n");
+ }
+ }
+ return builder.toString();
+ }
+
+ public void writeTo(OutputStream out) throws IOException, MessagingException {
+ BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(out), 1024);
+ for (Field field : mFields) {
+ if (!arrayContains(WRITE_OMIT_FIELDS, field.name)) {
+ writer.write(field.name + ": " + field.value + "\r\n");
+ }
+ }
+ writer.flush();
+ }
+
+ private static class Field {
+ final String name;
+ final String value;
+
+ public Field(String name, String value) {
+ this.name = name;
+ this.value = value;
+ }
+
+ @Override
+ public String toString() {
+ return name + "=" + value;
+ }
+ }
+
+ @Override
+ public String toString() {
+ return (mFields == null) ? null : mFields.toString();
+ }
+
+ public final static boolean arrayContains(Object[] a, Object o) {
+ int index = arrayIndex(a, o);
+ return (index >= 0);
+ }
+
+ public final static int arrayIndex(Object[] a, Object o) {
+ for (int i = 0, count = a.length; i < count; i++) {
+ if (a[i].equals(o)) {
+ return i;
+ }
+ }
+ return -1;
+ }
+}
diff --git a/src/com/android/emailcommon/internet/MimeMessage.java b/src/com/android/emailcommon/internet/MimeMessage.java
new file mode 100644
index 0000000..b3ee70e
--- /dev/null
+++ b/src/com/android/emailcommon/internet/MimeMessage.java
@@ -0,0 +1,644 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.emailcommon.internet;
+
+import com.android.emailcommon.mail.Address;
+import com.android.emailcommon.mail.Body;
+import com.android.emailcommon.mail.BodyPart;
+import com.android.emailcommon.mail.Message;
+import com.android.emailcommon.mail.MessagingException;
+import com.android.emailcommon.mail.Multipart;
+import com.android.emailcommon.mail.Part;
+
+import org.apache.james.mime4j.BodyDescriptor;
+import org.apache.james.mime4j.ContentHandler;
+import org.apache.james.mime4j.EOLConvertingInputStream;
+import org.apache.james.mime4j.MimeStreamParser;
+import org.apache.james.mime4j.field.DateTimeField;
+import org.apache.james.mime4j.field.Field;
+
+import android.text.TextUtils;
+
+import java.io.BufferedWriter;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.io.OutputStreamWriter;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.Locale;
+import java.util.Stack;
+import java.util.regex.Pattern;
+
+/**
+ * An implementation of Message that stores all of its metadata in RFC 822 and
+ * RFC 2045 style headers.
+ *
+ * NOTE: Automatic generation of a local message-id is becoming unwieldy and should be removed.
+ * It would be better to simply do it explicitly on local creation of new outgoing messages.
+ */
+public class MimeMessage extends Message {
+ private MimeHeader mHeader;
+ private MimeHeader mExtendedHeader;
+
+ // NOTE: The fields here are transcribed out of headers, and values stored here will supercede
+ // the values found in the headers. Use caution to prevent any out-of-phase errors. In
+ // particular, any adds/changes/deletes here must be echoed by changes in the parse() function.
+ private Address[] mFrom;
+ private Address[] mTo;
+ private Address[] mCc;
+ private Address[] mBcc;
+ private Address[] mReplyTo;
+ private Date mSentDate;
+ private Body mBody;
+ protected int mSize;
+ private boolean mInhibitLocalMessageId = false;
+ private boolean mComplete = true;
+
+ // Shared random source for generating local message-id values
+ private static final java.util.Random sRandom = new java.util.Random();
+
+ // In MIME, en_US-like date format should be used. In other words "MMM" should be encoded to
+ // "Jan", not the other localized format like "Ene" (meaning January in locale es).
+ // This conversion is used when generating outgoing MIME messages. Incoming MIME date
+ // headers are parsed by org.apache.james.mime4j.field.DateTimeField which does not have any
+ // localization code.
+ private static final SimpleDateFormat DATE_FORMAT =
+ new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss Z", Locale.US);
+
+ // regex that matches content id surrounded by "<>" optionally.
+ private static final Pattern REMOVE_OPTIONAL_BRACKETS = Pattern.compile("^<?([^>]+)>?$");
+ // regex that matches end of line.
+ private static final Pattern END_OF_LINE = Pattern.compile("\r?\n");
+
+ public MimeMessage() {
+ mHeader = null;
+ }
+
+ /**
+ * Generate a local message id. This is only used when none has been assigned, and is
+ * installed lazily. Any remote (typically server-assigned) message id takes precedence.
+ * @return a long, locally-generated message-ID value
+ */
+ private String generateMessageId() {
+ StringBuffer sb = new StringBuffer();
+ sb.append("<");
+ for (int i = 0; i < 24; i++) {
+ // We'll use a 5-bit range (0..31)
+ int value = sRandom.nextInt() & 31;
+ char c = "0123456789abcdefghijklmnopqrstuv".charAt(value);
+ sb.append(c);
+ }
+ sb.append(".");
+ sb.append(Long.toString(System.currentTimeMillis()));
+ sb.append("@email.android.com>");
+ return sb.toString();
+ }
+
+ /**
+ * Parse the given InputStream using Apache Mime4J to build a MimeMessage.
+ *
+ * @param in
+ * @throws IOException
+ * @throws MessagingException
+ */
+ public MimeMessage(InputStream in) throws IOException, MessagingException {
+ parse(in);
+ }
+
+ private MimeStreamParser init() {
+ // Before parsing the input stream, clear all local fields that may be superceded by
+ // the new incoming message.
+ getMimeHeaders().clear();
+ mInhibitLocalMessageId = true;
+ mFrom = null;
+ mTo = null;
+ mCc = null;
+ mBcc = null;
+ mReplyTo = null;
+ mSentDate = null;
+ mBody = null;
+
+ MimeStreamParser parser = new MimeStreamParser();
+ parser.setContentHandler(new MimeMessageBuilder());
+ return parser;
+ }
+
+ protected void parse(InputStream in) throws IOException, MessagingException {
+ MimeStreamParser parser = init();
+ parser.parse(new EOLConvertingInputStream(in));
+ mComplete = !parser.getPrematureEof();
+ }
+
+ public void parse(InputStream in, EOLConvertingInputStream.Callback callback)
+ throws IOException, MessagingException {
+ MimeStreamParser parser = init();
+ parser.parse(new EOLConvertingInputStream(in, getSize(), callback));
+ mComplete = !parser.getPrematureEof();
+ }
+
+ /**
+ * Return the internal mHeader value, with very lazy initialization.
+ * The goal is to save memory by not creating the headers until needed.
+ */
+ private MimeHeader getMimeHeaders() {
+ if (mHeader == null) {
+ mHeader = new MimeHeader();
+ }
+ return mHeader;
+ }
+
+ @Override
+ public Date getReceivedDate() throws MessagingException {
+ return null;
+ }
+
+ @Override
+ public Date getSentDate() throws MessagingException {
+ if (mSentDate == null) {
+ try {
+ DateTimeField field = (DateTimeField)Field.parse("Date: "
+ + MimeUtility.unfoldAndDecode(getFirstHeader("Date")));
+ mSentDate = field.getDate();
+ } catch (Exception e) {
+
+ }
+ }
+ return mSentDate;
+ }
+
+ @Override
+ public void setSentDate(Date sentDate) throws MessagingException {
+ setHeader("Date", DATE_FORMAT.format(sentDate));
+ this.mSentDate = sentDate;
+ }
+
+ @Override
+ public String getContentType() throws MessagingException {
+ String contentType = getFirstHeader(MimeHeader.HEADER_CONTENT_TYPE);
+ if (contentType == null) {
+ return "text/plain";
+ } else {
+ return contentType;
+ }
+ }
+
+ public String getDisposition() throws MessagingException {
+ String contentDisposition = getFirstHeader(MimeHeader.HEADER_CONTENT_DISPOSITION);
+ if (contentDisposition == null) {
+ return null;
+ } else {
+ return contentDisposition;
+ }
+ }
+
+ public String getContentId() throws MessagingException {
+ String contentId = getFirstHeader(MimeHeader.HEADER_CONTENT_ID);
+ if (contentId == null) {
+ return null;
+ } else {
+ // remove optionally surrounding brackets.
+ return REMOVE_OPTIONAL_BRACKETS.matcher(contentId).replaceAll("$1");
+ }
+ }
+
+ public boolean isComplete() {
+ return mComplete;
+ }
+
+ public String getMimeType() throws MessagingException {
+ return MimeUtility.getHeaderParameter(getContentType(), null);
+ }
+
+ public int getSize() throws MessagingException {
+ return mSize;
+ }
+
+ /**
+ * Returns a list of the given recipient type from this message. If no addresses are
+ * found the method returns an empty array.
+ */
+ @Override
+ public Address[] getRecipients(RecipientType type) throws MessagingException {
+ if (type == RecipientType.TO) {
+ if (mTo == null) {
+ mTo = Address.parse(MimeUtility.unfold(getFirstHeader("To")));
+ }
+ return mTo;
+ } else if (type == RecipientType.CC) {
+ if (mCc == null) {
+ mCc = Address.parse(MimeUtility.unfold(getFirstHeader("CC")));
+ }
+ return mCc;
+ } else if (type == RecipientType.BCC) {
+ if (mBcc == null) {
+ mBcc = Address.parse(MimeUtility.unfold(getFirstHeader("BCC")));
+ }
+ return mBcc;
+ } else {
+ throw new MessagingException("Unrecognized recipient type.");
+ }
+ }
+
+ @Override
+ public void setRecipients(RecipientType type, Address[] addresses) throws MessagingException {
+ final int TO_LENGTH = 4; // "To: "
+ final int CC_LENGTH = 4; // "Cc: "
+ final int BCC_LENGTH = 5; // "Bcc: "
+ if (type == RecipientType.TO) {
+ if (addresses == null || addresses.length == 0) {
+ removeHeader("To");
+ this.mTo = null;
+ } else {
+ setHeader("To", MimeUtility.fold(Address.toHeader(addresses), TO_LENGTH));
+ this.mTo = addresses;
+ }
+ } else if (type == RecipientType.CC) {
+ if (addresses == null || addresses.length == 0) {
+ removeHeader("CC");
+ this.mCc = null;
+ } else {
+ setHeader("CC", MimeUtility.fold(Address.toHeader(addresses), CC_LENGTH));
+ this.mCc = addresses;
+ }
+ } else if (type == RecipientType.BCC) {
+ if (addresses == null || addresses.length == 0) {
+ removeHeader("BCC");
+ this.mBcc = null;
+ } else {
+ setHeader("BCC", MimeUtility.fold(Address.toHeader(addresses), BCC_LENGTH));
+ this.mBcc = addresses;
+ }
+ } else {
+ throw new MessagingException("Unrecognized recipient type.");
+ }
+ }
+
+ /**
+ * Returns the unfolded, decoded value of the Subject header.
+ */
+ @Override
+ public String getSubject() throws MessagingException {
+ return MimeUtility.unfoldAndDecode(getFirstHeader("Subject"));
+ }
+
+ @Override
+ public void setSubject(String subject) throws MessagingException {
+ final int HEADER_NAME_LENGTH = 9; // "Subject: "
+ setHeader("Subject", MimeUtility.foldAndEncode2(subject, HEADER_NAME_LENGTH));
+ }
+
+ @Override
+ public Address[] getFrom() throws MessagingException {
+ if (mFrom == null) {
+ String list = MimeUtility.unfold(getFirstHeader("From"));
+ if (list == null || list.length() == 0) {
+ list = MimeUtility.unfold(getFirstHeader("Sender"));
+ }
+ mFrom = Address.parse(list);
+ }
+ return mFrom;
+ }
+
+ @Override
+ public void setFrom(Address from) throws MessagingException {
+ final int FROM_LENGTH = 6; // "From: "
+ if (from != null) {
+ setHeader("From", MimeUtility.fold(from.toHeader(), FROM_LENGTH));
+ this.mFrom = new Address[] {
+ from
+ };
+ } else {
+ this.mFrom = null;
+ }
+ }
+
+ @Override
+ public Address[] getReplyTo() throws MessagingException {
+ if (mReplyTo == null) {
+ mReplyTo = Address.parse(MimeUtility.unfold(getFirstHeader("Reply-to")));
+ }
+ return mReplyTo;
+ }
+
+ @Override
+ public void setReplyTo(Address[] replyTo) throws MessagingException {
+ final int REPLY_TO_LENGTH = 10; // "Reply-to: "
+ if (replyTo == null || replyTo.length == 0) {
+ removeHeader("Reply-to");
+ mReplyTo = null;
+ } else {
+ setHeader("Reply-to", MimeUtility.fold(Address.toHeader(replyTo), REPLY_TO_LENGTH));
+ mReplyTo = replyTo;
+ }
+ }
+
+ /**
+ * Set the mime "Message-ID" header
+ * @param messageId the new Message-ID value
+ * @throws MessagingException
+ */
+ @Override
+ public void setMessageId(String messageId) throws MessagingException {
+ setHeader("Message-ID", messageId);
+ }
+
+ /**
+ * Get the mime "Message-ID" header. This value will be preloaded with a locally-generated
+ * random ID, if the value has not previously been set. Local generation can be inhibited/
+ * overridden by explicitly clearing the headers, removing the message-id header, etc.
+ * @return the Message-ID header string, or null if explicitly has been set to null
+ */
+ @Override
+ public String getMessageId() throws MessagingException {
+ String messageId = getFirstHeader("Message-ID");
+ if (messageId == null && !mInhibitLocalMessageId) {
+ messageId = generateMessageId();
+ setMessageId(messageId);
+ }
+ return messageId;
+ }
+
+ @Override
+ public void saveChanges() throws MessagingException {
+ throw new MessagingException("saveChanges not yet implemented");
+ }
+
+ @Override
+ public Body getBody() throws MessagingException {
+ return mBody;
+ }
+
+ @Override
+ public void setBody(Body body) throws MessagingException {
+ this.mBody = body;
+ if (body instanceof Multipart) {
+ Multipart multipart = ((Multipart)body);
+ multipart.setParent(this);
+ setHeader(MimeHeader.HEADER_CONTENT_TYPE, multipart.getContentType());
+ setHeader("MIME-Version", "1.0");
+ }
+ else if (body instanceof TextBody) {
+ setHeader(MimeHeader.HEADER_CONTENT_TYPE, String.format("%s;\n charset=utf-8",
+ getMimeType()));
+ setHeader(MimeHeader.HEADER_CONTENT_TRANSFER_ENCODING, "base64");
+ }
+ }
+
+ protected String getFirstHeader(String name) throws MessagingException {
+ return getMimeHeaders().getFirstHeader(name);
+ }
+
+ @Override
+ public void addHeader(String name, String value) throws MessagingException {
+ getMimeHeaders().addHeader(name, value);
+ }
+
+ @Override
+ public void setHeader(String name, String value) throws MessagingException {
+ getMimeHeaders().setHeader(name, value);
+ }
+
+ @Override
+ public String[] getHeader(String name) throws MessagingException {
+ return getMimeHeaders().getHeader(name);
+ }
+
+ @Override
+ public void removeHeader(String name) throws MessagingException {
+ getMimeHeaders().removeHeader(name);
+ if ("Message-ID".equalsIgnoreCase(name)) {
+ mInhibitLocalMessageId = true;
+ }
+ }
+
+ /**
+ * Set extended header
+ *
+ * @param name Extended header name
+ * @param value header value - flattened by removing CR-NL if any
+ * remove header if value is null
+ * @throws MessagingException
+ */
+ public void setExtendedHeader(String name, String value) throws MessagingException {
+ if (value == null) {
+ if (mExtendedHeader != null) {
+ mExtendedHeader.removeHeader(name);
+ }
+ return;
+ }
+ if (mExtendedHeader == null) {
+ mExtendedHeader = new MimeHeader();
+ }
+ mExtendedHeader.setHeader(name, END_OF_LINE.matcher(value).replaceAll(""));
+ }
+
+ /**
+ * Get extended header
+ *
+ * @param name Extended header name
+ * @return header value - null if header does not exist
+ * @throws MessagingException
+ */
+ public String getExtendedHeader(String name) throws MessagingException {
+ if (mExtendedHeader == null) {
+ return null;
+ }
+ return mExtendedHeader.getFirstHeader(name);
+ }
+
+ /**
+ * Set entire extended headers from String
+ *
+ * @param headers Extended header and its value - "CR-NL-separated pairs
+ * if null or empty, remove entire extended headers
+ * @throws MessagingException
+ */
+ public void setExtendedHeaders(String headers) throws MessagingException {
+ if (TextUtils.isEmpty(headers)) {
+ mExtendedHeader = null;
+ } else {
+ mExtendedHeader = new MimeHeader();
+ for (String header : END_OF_LINE.split(headers)) {
+ String[] tokens = header.split(":", 2);
+ if (tokens.length != 2) {
+ throw new MessagingException("Illegal extended headers: " + headers);
+ }
+ mExtendedHeader.setHeader(tokens[0].trim(), tokens[1].trim());
+ }
+ }
+ }
+
+ /**
+ * Get entire extended headers as String
+ *
+ * @return "CR-NL-separated extended headers - null if extended header does not exist
+ */
+ public String getExtendedHeaders() {
+ if (mExtendedHeader != null) {
+ return mExtendedHeader.writeToString();
+ }
+ return null;
+ }
+
+ /**
+ * Write message header and body to output stream
+ *
+ * @param out Output steam to write message header and body.
+ */
+ public void writeTo(OutputStream out) throws IOException, MessagingException {
+ BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(out), 1024);
+ // Force creation of local message-id
+ getMessageId();
+ getMimeHeaders().writeTo(out);
+ // mExtendedHeader will not be write out to external output stream,
+ // because it is intended to internal use.
+ writer.write("\r\n");
+ writer.flush();
+ if (mBody != null) {
+ mBody.writeTo(out);
+ }
+ }
+
+ public InputStream getInputStream() throws MessagingException {
+ return null;
+ }
+
+ class MimeMessageBuilder implements ContentHandler {
+ private Stack<Object> stack = new Stack<Object>();
+
+ public MimeMessageBuilder() {
+ }
+
+ private void expect(Class c) {
+ if (!c.isInstance(stack.peek())) {
+ throw new IllegalStateException("Internal stack error: " + "Expected '"
+ + c.getName() + "' found '" + stack.peek().getClass().getName() + "'");
+ }
+ }
+
+ public void startMessage() {
+ if (stack.isEmpty()) {
+ stack.push(MimeMessage.this);
+ } else {
+ expect(Part.class);
+ try {
+ MimeMessage m = new MimeMessage();
+ ((Part)stack.peek()).setBody(m);
+ stack.push(m);
+ } catch (MessagingException me) {
+ throw new Error(me);
+ }
+ }
+ }
+
+ public void endMessage() {
+ expect(MimeMessage.class);
+ stack.pop();
+ }
+
+ public void startHeader() {
+ expect(Part.class);
+ }
+
+ public void field(String fieldData) {
+ expect(Part.class);
+ try {
+ String[] tokens = fieldData.split(":", 2);
+ ((Part)stack.peek()).addHeader(tokens[0], tokens[1].trim());
+ } catch (MessagingException me) {
+ throw new Error(me);
+ }
+ }
+
+ public void endHeader() {
+ expect(Part.class);
+ }
+
+ public void startMultipart(BodyDescriptor bd) {
+ expect(Part.class);
+
+ Part e = (Part)stack.peek();
+ try {
+ MimeMultipart multiPart = new MimeMultipart(e.getContentType());
+ e.setBody(multiPart);
+ stack.push(multiPart);
+ } catch (MessagingException me) {
+ throw new Error(me);
+ }
+ }
+
+ public void body(BodyDescriptor bd, InputStream in) throws IOException {
+ expect(Part.class);
+ Body body = MimeUtility.decodeBody(in, bd.getTransferEncoding());
+ try {
+ ((Part)stack.peek()).setBody(body);
+ } catch (MessagingException me) {
+ throw new Error(me);
+ }
+ }
+
+ public void endMultipart() {
+ stack.pop();
+ }
+
+ public void startBodyPart() {
+ expect(MimeMultipart.class);
+
+ try {
+ MimeBodyPart bodyPart = new MimeBodyPart();
+ ((MimeMultipart)stack.peek()).addBodyPart(bodyPart);
+ stack.push(bodyPart);
+ } catch (MessagingException me) {
+ throw new Error(me);
+ }
+ }
+
+ public void endBodyPart() {
+ expect(BodyPart.class);
+ stack.pop();
+ }
+
+ public void epilogue(InputStream is) throws IOException {
+ expect(MimeMultipart.class);
+ StringBuffer sb = new StringBuffer();
+ int b;
+ while ((b = is.read()) != -1) {
+ sb.append((char)b);
+ }
+ // ((Multipart) stack.peek()).setEpilogue(sb.toString());
+ }
+
+ public void preamble(InputStream is) throws IOException {
+ expect(MimeMultipart.class);
+ StringBuffer sb = new StringBuffer();
+ int b;
+ while ((b = is.read()) != -1) {
+ sb.append((char)b);
+ }
+ try {
+ ((MimeMultipart)stack.peek()).setPreamble(sb.toString());
+ } catch (MessagingException me) {
+ throw new Error(me);
+ }
+ }
+
+ public void raw(InputStream is) throws IOException {
+ throw new UnsupportedOperationException("Not supported");
+ }
+ }
+}
diff --git a/src/com/android/emailcommon/internet/MimeMultipart.java b/src/com/android/emailcommon/internet/MimeMultipart.java
new file mode 100644
index 0000000..e6977ee
--- /dev/null
+++ b/src/com/android/emailcommon/internet/MimeMultipart.java
@@ -0,0 +1,111 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.emailcommon.internet;
+
+import com.android.emailcommon.mail.BodyPart;
+import com.android.emailcommon.mail.MessagingException;
+import com.android.emailcommon.mail.Multipart;
+
+import java.io.BufferedWriter;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.io.OutputStreamWriter;
+
+public class MimeMultipart extends Multipart {
+ protected String mPreamble;
+
+ protected String mContentType;
+
+ protected String mBoundary;
+
+ protected String mSubType;
+
+ public MimeMultipart() throws MessagingException {
+ mBoundary = generateBoundary();
+ setSubType("mixed");
+ }
+
+ public MimeMultipart(String contentType) throws MessagingException {
+ this.mContentType = contentType;
+ try {
+ mSubType = MimeUtility.getHeaderParameter(contentType, null).split("/")[1];
+ mBoundary = MimeUtility.getHeaderParameter(contentType, "boundary");
+ if (mBoundary == null) {
+ throw new MessagingException("MultiPart does not contain boundary: " + contentType);
+ }
+ } catch (Exception e) {
+ throw new MessagingException(
+ "Invalid MultiPart Content-Type; must contain subtype and boundary. ("
+ + contentType + ")", e);
+ }
+ }
+
+ public String generateBoundary() {
+ StringBuffer sb = new StringBuffer();
+ sb.append("----");
+ for (int i = 0; i < 30; i++) {
+ sb.append(Integer.toString((int)(Math.random() * 35), 36));
+ }
+ return sb.toString().toUpperCase();
+ }
+
+ public String getPreamble() throws MessagingException {
+ return mPreamble;
+ }
+
+ public void setPreamble(String preamble) throws MessagingException {
+ this.mPreamble = preamble;
+ }
+
+ @Override
+ public String getContentType() throws MessagingException {
+ return mContentType;
+ }
+
+ public void setSubType(String subType) throws MessagingException {
+ this.mSubType = subType;
+ mContentType = String.format("multipart/%s; boundary=\"%s\"", subType, mBoundary);
+ }
+
+ public void writeTo(OutputStream out) throws IOException, MessagingException {
+ BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(out), 1024);
+
+ if (mPreamble != null) {
+ writer.write(mPreamble + "\r\n");
+ }
+
+ for (int i = 0, count = mParts.size(); i < count; i++) {
+ BodyPart bodyPart = mParts.get(i);
+ writer.write("--" + mBoundary + "\r\n");
+ writer.flush();
+ bodyPart.writeTo(out);
+ writer.write("\r\n");
+ }
+
+ writer.write("--" + mBoundary + "--\r\n");
+ writer.flush();
+ }
+
+ public InputStream getInputStream() throws MessagingException {
+ return null;
+ }
+
+ public String getSubTypeForTest() {
+ return mSubType;
+ }
+}
diff --git a/src/com/android/emailcommon/internet/MimeUtility.java b/src/com/android/emailcommon/internet/MimeUtility.java
new file mode 100644
index 0000000..a4cada9
--- /dev/null
+++ b/src/com/android/emailcommon/internet/MimeUtility.java
@@ -0,0 +1,453 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.emailcommon.internet;
+
+import android.text.TextUtils;
+import android.util.Base64;
+import android.util.Base64DataException;
+import android.util.Base64InputStream;
+import android.util.Log;
+
+import com.android.emailcommon.mail.Body;
+import com.android.emailcommon.mail.BodyPart;
+import com.android.emailcommon.mail.Message;
+import com.android.emailcommon.mail.MessagingException;
+import com.android.emailcommon.mail.Multipart;
+import com.android.emailcommon.mail.Part;
+
+import org.apache.commons.io.IOUtils;
+import org.apache.james.mime4j.codec.EncoderUtil;
+import org.apache.james.mime4j.decoder.DecoderUtil;
+import org.apache.james.mime4j.decoder.QuotedPrintableInputStream;
+import org.apache.james.mime4j.util.CharsetUtil;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.util.ArrayList;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+public class MimeUtility {
+ private static final String LOG_TAG = "Email";
+
+ public static final String MIME_TYPE_RFC822 = "message/rfc822";
+ private final static Pattern PATTERN_CR_OR_LF = Pattern.compile("\r|\n");
+
+ /**
+ * Replace sequences of CRLF+WSP with WSP. Tries to preserve original string
+ * object whenever possible.
+ */
+ public static String unfold(String s) {
+ if (s == null) {
+ return null;
+ }
+ Matcher patternMatcher = PATTERN_CR_OR_LF.matcher(s);
+ if (patternMatcher.find()) {
+ patternMatcher.reset();
+ s = patternMatcher.replaceAll("");
+ }
+ return s;
+ }
+
+ public static String decode(String s) {
+ if (s == null) {
+ return null;
+ }
+ return DecoderUtil.decodeEncodedWords(s);
+ }
+
+ public static String unfoldAndDecode(String s) {
+ return decode(unfold(s));
+ }
+
+ // TODO implement proper foldAndEncode
+ // NOTE: When this really works, we *must* remove all calls to foldAndEncode2() to prevent
+ // duplication of encoding.
+ public static String foldAndEncode(String s) {
+ return s;
+ }
+
+ /**
+ * INTERIM version of foldAndEncode that will be used only by Subject: headers.
+ * This is safer than implementing foldAndEncode() (see above) and risking unknown damage
+ * to other headers.
+ *
+ * TODO: Copy this code to foldAndEncode(), get rid of this function, confirm all working OK.
+ *
+ * @param s original string to encode and fold
+ * @param usedCharacters number of characters already used up by header name
+
+ * @return the String ready to be transmitted
+ */
+ public static String foldAndEncode2(String s, int usedCharacters) {
+ // james.mime4j.codec.EncoderUtil.java
+ // encode: encodeIfNecessary(text, usage, numUsedInHeaderName)
+ // Usage.TEXT_TOKENlooks like the right thing for subjects
+ // use WORD_ENTITY for address/names
+
+ String encoded = EncoderUtil.encodeIfNecessary(s, EncoderUtil.Usage.TEXT_TOKEN,
+ usedCharacters);
+
+ return fold(encoded, usedCharacters);
+ }
+
+ /**
+ * INTERIM: From newer version of org.apache.james (but we don't want to import
+ * the entire MimeUtil class).
+ *
+ * Splits the specified string into a multiple-line representation with
+ * lines no longer than 76 characters (because the line might contain
+ * encoded words; see <a href='http://www.faqs.org/rfcs/rfc2047.html'>RFC
+ * 2047</a> section 2). If the string contains non-whitespace sequences
+ * longer than 76 characters a line break is inserted at the whitespace
+ * character following the sequence resulting in a line longer than 76
+ * characters.
+ *
+ * @param s
+ * string to split.
+ * @param usedCharacters
+ * number of characters already used up. Usually the number of
+ * characters for header field name plus colon and one space.
+ * @return a multiple-line representation of the given string.
+ */
+ public static String fold(String s, int usedCharacters) {
+ final int maxCharacters = 76;
+
+ final int length = s.length();
+ if (usedCharacters + length <= maxCharacters)
+ return s;
+
+ StringBuilder sb = new StringBuilder();
+
+ int lastLineBreak = -usedCharacters;
+ int wspIdx = indexOfWsp(s, 0);
+ while (true) {
+ if (wspIdx == length) {
+ sb.append(s.substring(Math.max(0, lastLineBreak)));
+ return sb.toString();
+ }
+
+ int nextWspIdx = indexOfWsp(s, wspIdx + 1);
+
+ if (nextWspIdx - lastLineBreak > maxCharacters) {
+ sb.append(s.substring(Math.max(0, lastLineBreak), wspIdx));
+ sb.append("\r\n");
+ lastLineBreak = wspIdx;
+ }
+
+ wspIdx = nextWspIdx;
+ }
+ }
+
+ /**
+ * INTERIM: From newer version of org.apache.james (but we don't want to import
+ * the entire MimeUtil class).
+ *
+ * Search for whitespace.
+ */
+ private static int indexOfWsp(String s, int fromIndex) {
+ final int len = s.length();
+ for (int index = fromIndex; index < len; index++) {
+ char c = s.charAt(index);
+ if (c == ' ' || c == '\t')
+ return index;
+ }
+ return len;
+ }
+
+ /**
+ * Returns the named parameter of a header field. If name is null the first
+ * parameter is returned, or if there are no additional parameters in the
+ * field the entire field is returned. Otherwise the named parameter is
+ * searched for in a case insensitive fashion and returned. If the parameter
+ * cannot be found the method returns null.
+ *
+ * TODO: quite inefficient with the inner trimming & splitting.
+ * TODO: Also has a latent bug: uses "startsWith" to match the name, which can false-positive.
+ * TODO: The doc says that for a null name you get the first param, but you get the header.
+ * Should probably just fix the doc, but if other code assumes that behavior, fix the code.
+ * TODO: Need to decode %-escaped strings, as in: filename="ab%22d".
+ * ('+' -> ' ' conversion too? check RFC)
+ *
+ * @param header
+ * @param name
+ * @return the entire header (if name=null), the found parameter, or null
+ */
+ public static String getHeaderParameter(String header, String name) {
+ if (header == null) {
+ return null;
+ }
+ String[] parts = unfold(header).split(";");
+ if (name == null) {
+ return parts[0].trim();
+ }
+ String lowerCaseName = name.toLowerCase();
+ for (String part : parts) {
+ if (part.trim().toLowerCase().startsWith(lowerCaseName)) {
+ String[] parameterParts = part.split("=", 2);
+ if (parameterParts.length < 2) {
+ return null;
+ }
+ String parameter = parameterParts[1].trim();
+ if (parameter.startsWith("\"") && parameter.endsWith("\"")) {
+ return parameter.substring(1, parameter.length() - 1);
+ } else {
+ return parameter;
+ }
+ }
+ }
+ return null;
+ }
+
+ public static Part findFirstPartByMimeType(Part part, String mimeType)
+ throws MessagingException {
+ if (part.getBody() instanceof Multipart) {
+ Multipart multipart = (Multipart)part.getBody();
+ for (int i = 0, count = multipart.getCount(); i < count; i++) {
+ BodyPart bodyPart = multipart.getBodyPart(i);
+ Part ret = findFirstPartByMimeType(bodyPart, mimeType);
+ if (ret != null) {
+ return ret;
+ }
+ }
+ }
+ else if (part.getMimeType().equalsIgnoreCase(mimeType)) {
+ return part;
+ }
+ return null;
+ }
+
+ public static Part findPartByContentId(Part part, String contentId) throws Exception {
+ if (part.getBody() instanceof Multipart) {
+ Multipart multipart = (Multipart)part.getBody();
+ for (int i = 0, count = multipart.getCount(); i < count; i++) {
+ BodyPart bodyPart = multipart.getBodyPart(i);
+ Part ret = findPartByContentId(bodyPart, contentId);
+ if (ret != null) {
+ return ret;
+ }
+ }
+ }
+ String cid = part.getContentId();
+ if (contentId.equals(cid)) {
+ return part;
+ }
+ return null;
+ }
+
+ /**
+ * Reads the Part's body and returns a String based on any charset conversion that needed
+ * to be done.
+ * @param part The part containing a body
+ * @return a String containing the converted text in the body, or null if there was no text
+ * or an error during conversion.
+ */
+ public static String getTextFromPart(Part part) {
+ try {
+ if (part != null && part.getBody() != null) {
+ InputStream in = part.getBody().getInputStream();
+ String mimeType = part.getMimeType();
+ if (mimeType != null && MimeUtility.mimeTypeMatches(mimeType, "text/*")) {
+ /*
+ * Now we read the part into a buffer for further processing. Because
+ * the stream is now wrapped we'll remove any transfer encoding at this point.
+ */
+ ByteArrayOutputStream out = new ByteArrayOutputStream();
+ IOUtils.copy(in, out);
+ in.close();
+ in = null; // we want all of our memory back, and close might not release
+
+ /*
+ * We've got a text part, so let's see if it needs to be processed further.
+ */
+ String charset = getHeaderParameter(part.getContentType(), "charset");
+ if (charset != null) {
+ /*
+ * See if there is conversion from the MIME charset to the Java one.
+ */
+ charset = CharsetUtil.toJavaCharset(charset);
+ }
+ /*
+ * No encoding, so use us-ascii, which is the standard.
+ */
+ if (charset == null) {
+ charset = "ASCII";
+ }
+ /*
+ * Convert and return as new String
+ */
+ String result = out.toString(charset);
+ out.close();
+ return result;
+ }
+ }
+
+ }
+ catch (OutOfMemoryError oom) {
+ /*
+ * If we are not able to process the body there's nothing we can do about it. Return
+ * null and let the upper layers handle the missing content.
+ */
+ Log.e(LOG_TAG, "Unable to getTextFromPart " + oom.toString());
+ }
+ catch (Exception e) {
+ /*
+ * If we are not able to process the body there's nothing we can do about it. Return
+ * null and let the upper layers handle the missing content.
+ */
+ Log.e(LOG_TAG, "Unable to getTextFromPart " + e.toString());
+ }
+ return null;
+ }
+
+ /**
+ * Returns true if the given mimeType matches the matchAgainst specification. The comparison
+ * ignores case and the matchAgainst string may include "*" for a wildcard (e.g. "image/*").
+ *
+ * @param mimeType A MIME type to check.
+ * @param matchAgainst A MIME type to check against. May include wildcards.
+ * @return true if the mimeType matches
+ */
+ public static boolean mimeTypeMatches(String mimeType, String matchAgainst) {
+ Pattern p = Pattern.compile(matchAgainst.replaceAll("\\*", "\\.\\*"),
+ Pattern.CASE_INSENSITIVE);
+ return p.matcher(mimeType).matches();
+ }
+
+ /**
+ * Returns true if the given mimeType matches any of the matchAgainst specifications. The
+ * comparison ignores case and the matchAgainst strings may include "*" for a wildcard
+ * (e.g. "image/*").
+ *
+ * @param mimeType A MIME type to check.
+ * @param matchAgainst An array of MIME types to check against. May include wildcards.
+ * @return true if the mimeType matches any of the matchAgainst strings
+ */
+ public static boolean mimeTypeMatches(String mimeType, String[] matchAgainst) {
+ for (String matchType : matchAgainst) {
+ if (mimeTypeMatches(mimeType, matchType)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Given an input stream and a transfer encoding, return a wrapped input stream for that
+ * encoding (or the original if none is required)
+ * @param in the input stream
+ * @param contentTransferEncoding the content transfer encoding
+ * @return a properly wrapped stream
+ */
+ public static InputStream getInputStreamForContentTransferEncoding(InputStream in,
+ String contentTransferEncoding) {
+ if (contentTransferEncoding != null) {
+ contentTransferEncoding =
+ MimeUtility.getHeaderParameter(contentTransferEncoding, null);
+ if ("quoted-printable".equalsIgnoreCase(contentTransferEncoding)) {
+ in = new QuotedPrintableInputStream(in);
+ }
+ else if ("base64".equalsIgnoreCase(contentTransferEncoding)) {
+ in = new Base64InputStream(in, Base64.DEFAULT);
+ }
+ }
+ return in;
+ }
+
+ /**
+ * Removes any content transfer encoding from the stream and returns a Body.
+ */
+ public static Body decodeBody(InputStream in, String contentTransferEncoding)
+ throws IOException {
+ /*
+ * We'll remove any transfer encoding by wrapping the stream.
+ */
+ in = getInputStreamForContentTransferEncoding(in, contentTransferEncoding);
+ BinaryTempFileBody tempBody = new BinaryTempFileBody();
+ OutputStream out = tempBody.getOutputStream();
+ try {
+ IOUtils.copy(in, out);
+ } catch (Base64DataException bde) {
+ // TODO Need to fix this somehow
+ //String warning = "\n\n" + Email.getMessageDecodeErrorString();
+ //out.write(warning.getBytes());
+ } finally {
+ out.close();
+ }
+ return tempBody;
+ }
+
+ /**
+ * Recursively scan a Part (usually a Message) and sort out which of its children will be
+ * "viewable" and which will be attachments.
+ *
+ * @param part The part to be broken down
+ * @param viewables This arraylist will be populated with all parts that appear to be
+ * the "message" (e.g. text/plain & text/html)
+ * @param attachments This arraylist will be populated with all parts that appear to be
+ * attachments (including inlines)
+ * @throws MessagingException
+ */
+ public static void collectParts(Part part, ArrayList<Part> viewables,
+ ArrayList<Part> attachments) throws MessagingException {
+ String disposition = part.getDisposition();
+ String dispositionType = MimeUtility.getHeaderParameter(disposition, null);
+ // If a disposition is not specified, default to "inline"
+ boolean inline =
+ TextUtils.isEmpty(dispositionType) || "inline".equalsIgnoreCase(dispositionType);
+ // The lower-case mime type
+ String mimeType = part.getMimeType().toLowerCase();
+
+ if (part.getBody() instanceof Multipart) {
+ // If the part is Multipart but not alternative it's either mixed or
+ // something we don't know about, which means we treat it as mixed
+ // per the spec. We just process its pieces recursively.
+ MimeMultipart mp = (MimeMultipart)part.getBody();
+ boolean foundHtml = false;
+ if (mp.getSubTypeForTest().equals("alternative")) {
+ for (int i = 0; i < mp.getCount(); i++) {
+ if (mp.getBodyPart(i).isMimeType("text/html")) {
+ foundHtml = true;
+ break;
+ }
+ }
+ }
+ for (int i = 0; i < mp.getCount(); i++) {
+ // See if we have text and html
+ BodyPart bp = mp.getBodyPart(i);
+ // If there's html, don't bother loading text
+ if (foundHtml && bp.isMimeType("text/plain")) {
+ continue;
+ }
+ collectParts(bp, viewables, attachments);
+ }
+ } else if (part.getBody() instanceof Message) {
+ // If the part is an embedded message we just continue to process
+ // it, pulling any viewables or attachments into the running list.
+ Message message = (Message)part.getBody();
+ collectParts(message, viewables, attachments);
+ } else if (inline && (mimeType.startsWith("text") || (mimeType.startsWith("image")))) {
+ // We'll treat text and images as viewables
+ viewables.add(part);
+ } else {
+ // Everything else is an attachment.
+ attachments.add(part);
+ }
+ }
+}
diff --git a/src/com/android/emailcommon/internet/TextBody.java b/src/com/android/emailcommon/internet/TextBody.java
new file mode 100644
index 0000000..09c265c
--- /dev/null
+++ b/src/com/android/emailcommon/internet/TextBody.java
@@ -0,0 +1,62 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.emailcommon.internet;
+
+import com.android.emailcommon.mail.Body;
+import com.android.emailcommon.mail.MessagingException;
+
+import android.util.Base64;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.io.UnsupportedEncodingException;
+
+public class TextBody implements Body {
+ String mBody;
+
+ public TextBody(String body) {
+ this.mBody = body;
+ }
+
+ public void writeTo(OutputStream out) throws IOException, MessagingException {
+ byte[] bytes = mBody.getBytes("UTF-8");
+ out.write(Base64.encode(bytes, Base64.CRLF));
+ }
+
+ /**
+ * Get the text of the body in it's unencoded format.
+ * @return
+ */
+ public String getText() {
+ return mBody;
+ }
+
+ /**
+ * Returns an InputStream that reads this body's text in UTF-8 format.
+ */
+ public InputStream getInputStream() throws MessagingException {
+ try {
+ byte[] b = mBody.getBytes("UTF-8");
+ return new ByteArrayInputStream(b);
+ }
+ catch (UnsupportedEncodingException usee) {
+ return null;
+ }
+ }
+}
diff --git a/src/com/android/emailcommon/mail/Address.java b/src/com/android/emailcommon/mail/Address.java
new file mode 100644
index 0000000..d87e8c2
--- /dev/null
+++ b/src/com/android/emailcommon/mail/Address.java
@@ -0,0 +1,455 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.emailcommon.mail;
+
+import android.text.TextUtils;
+import android.text.util.Rfc822Token;
+import android.text.util.Rfc822Tokenizer;
+
+import com.google.common.annotations.VisibleForTesting;
+
+import org.apache.james.mime4j.codec.EncoderUtil;
+import org.apache.james.mime4j.decoder.DecoderUtil;
+
+import java.util.ArrayList;
+import java.util.regex.Pattern;
+
+/**
+ * This class represent email address.
+ *
+ * RFC822 email address may have following format.
+ * "name" <address> (comment)
+ * "name" <address>
+ * name <address>
+ * address
+ * Name and comment part should be MIME/base64 encoded in header if necessary.
+ *
+ */
+public class Address {
+ /**
+ * Address part, in the form local_part@domain_part. No surrounding angle brackets.
+ */
+ private String mAddress;
+
+ /**
+ * Name part. No surrounding double quote, and no MIME/base64 encoding.
+ * This must be null if Address has no name part.
+ */
+ private String mPersonal;
+
+ // Regex that matches address surrounded by '<>' optionally. '^<?([^>]+)>?$'
+ private static final Pattern REMOVE_OPTIONAL_BRACKET = Pattern.compile("^<?([^>]+)>?$");
+ // Regex that matches personal name surrounded by '""' optionally. '^"?([^"]+)"?$'
+ private static final Pattern REMOVE_OPTIONAL_DQUOTE = Pattern.compile("^\"?([^\"]*)\"?$");
+ // Regex that matches escaped character '\\([\\"])'
+ private static final Pattern UNQUOTE = Pattern.compile("\\\\([\\\\\"])");
+
+ private static final Address[] EMPTY_ADDRESS_ARRAY = new Address[0];
+
+ // delimiters are chars that do not appear in an email address, used by pack/unpack
+ private static final char LIST_DELIMITER_EMAIL = '\1';
+ private static final char LIST_DELIMITER_PERSONAL = '\2';
+
+ public Address(String address, String personal) {
+ setAddress(address);
+ setPersonal(personal);
+ }
+
+ public Address(String address) {
+ setAddress(address);
+ }
+
+ public String getAddress() {
+ return mAddress;
+ }
+
+ public void setAddress(String address) {
+ mAddress = REMOVE_OPTIONAL_BRACKET.matcher(address).replaceAll("$1");
+ }
+
+ /**
+ * Get name part as UTF-16 string. No surrounding double quote, and no MIME/base64 encoding.
+ *
+ * @return Name part of email address. Returns null if it is omitted.
+ */
+ public String getPersonal() {
+ return mPersonal;
+ }
+
+ /**
+ * Set name part from UTF-16 string. Optional surrounding double quote will be removed.
+ * It will be also unquoted and MIME/base64 decoded.
+ *
+ * @param personal name part of email address as UTF-16 string. Null is acceptable.
+ */
+ public void setPersonal(String personal) {
+ if (personal != null) {
+ personal = REMOVE_OPTIONAL_DQUOTE.matcher(personal).replaceAll("$1");
+ personal = UNQUOTE.matcher(personal).replaceAll("$1");
+ personal = DecoderUtil.decodeEncodedWords(personal);
+ if (personal.length() == 0) {
+ personal = null;
+ }
+ }
+ mPersonal = personal;
+ }
+
+ /**
+ * This method is used to check that all the addresses that the user
+ * entered in a list (e.g. To:) are valid, so that none is dropped.
+ */
+ public static boolean isAllValid(String addressList) {
+ // This code mimics the parse() method below.
+ // I don't know how to better avoid the code-duplication.
+ if (addressList != null && addressList.length() > 0) {
+ Rfc822Token[] tokens = Rfc822Tokenizer.tokenize(addressList);
+ for (int i = 0, length = tokens.length; i < length; ++i) {
+ Rfc822Token token = tokens[i];
+ String address = token.getAddress();
+ if (!TextUtils.isEmpty(address) && !isValidAddress(address)) {
+ return false;
+ }
+ }
+ }
+ return true;
+ }
+
+ /**
+ * Parse a comma-delimited list of addresses in RFC822 format and return an
+ * array of Address objects.
+ *
+ * @param addressList Address list in comma-delimited string.
+ * @return An array of 0 or more Addresses.
+ */
+ public static Address[] parse(String addressList) {
+ if (addressList == null || addressList.length() == 0) {
+ return EMPTY_ADDRESS_ARRAY;
+ }
+ Rfc822Token[] tokens = Rfc822Tokenizer.tokenize(addressList);
+ ArrayList<Address> addresses = new ArrayList<Address>();
+ for (int i = 0, length = tokens.length; i < length; ++i) {
+ Rfc822Token token = tokens[i];
+ String address = token.getAddress();
+ if (!TextUtils.isEmpty(address)) {
+ if (isValidAddress(address)) {
+ String name = token.getName();
+ if (TextUtils.isEmpty(name)) {
+ name = null;
+ }
+ addresses.add(new Address(address, name));
+ }
+ }
+ }
+ return addresses.toArray(new Address[] {});
+ }
+
+ /**
+ * Checks whether a string email address is valid.
+ * E.g. name@domain.com is valid.
+ */
+ @VisibleForTesting
+ static boolean isValidAddress(String address) {
+ // Note: Some email provider may violate the standard, so here we only check that
+ // address consists of two part that are separated by '@', and domain part contains
+ // at least one '.'.
+ int len = address.length();
+ int firstAt = address.indexOf('@');
+ int lastAt = address.lastIndexOf('@');
+ int firstDot = address.indexOf('.', lastAt + 1);
+ int lastDot = address.lastIndexOf('.');
+ return firstAt > 0 && firstAt == lastAt && lastAt + 1 < firstDot
+ && firstDot <= lastDot && lastDot < len - 1;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (o instanceof Address) {
+ // It seems that the spec says that the "user" part is case-sensitive,
+ // while the domain part in case-insesitive.
+ // So foo@yahoo.com and Foo@yahoo.com are different.
+ // This may seem non-intuitive from the user POV, so we
+ // may re-consider it if it creates UI trouble.
+ // A problem case is "replyAll" sending to both
+ // a@b.c and to A@b.c, which turn out to be the same on the server.
+ // Leave unchanged for now (i.e. case-sensitive).
+ return getAddress().equals(((Address) o).getAddress());
+ }
+ return super.equals(o);
+ }
+
+ public int hashCode() {
+ return getAddress().hashCode();
+ }
+
+ /**
+ * Get human readable address string.
+ * Do not use this for email header.
+ *
+ * @return Human readable address string. Not quoted and not encoded.
+ */
+ @Override
+ public String toString() {
+ if (mPersonal != null && !mPersonal.equals(mAddress)) {
+ if (mPersonal.matches(".*[\\(\\)<>@,;:\\\\\".\\[\\]].*")) {
+ return quoteString(mPersonal) + " <" + mAddress + ">";
+ } else {
+ return mPersonal + " <" + mAddress + ">";
+ }
+ } else {
+ return mAddress;
+ }
+ }
+
+ /**
+ * Ensures that the given string starts and ends with the double quote character. The string is
+ * not modified in any way except to add the double quote character to start and end if it's not
+ * already there.
+ *
+ * TODO: Rename this, because "quoteString()" can mean so many different things.
+ *
+ * sample -> "sample"
+ * "sample" -> "sample"
+ * ""sample"" -> "sample"
+ * "sample"" -> "sample"
+ * sa"mp"le -> "sa"mp"le"
+ * "sa"mp"le" -> "sa"mp"le"
+ * (empty string) -> ""
+ * " -> ""
+ */
+ public static String quoteString(String s) {
+ if (s == null) {
+ return null;
+ }
+ if (!s.matches("^\".*\"$")) {
+ return "\"" + s + "\"";
+ }
+ else {
+ return s;
+ }
+ }
+
+ /**
+ * Get human readable comma-delimited address string.
+ *
+ * @param addresses Address array
+ * @return Human readable comma-delimited address string.
+ */
+ public static String toString(Address[] addresses) {
+ return toString(addresses, ",");
+ }
+
+ /**
+ * Get human readable address strings joined with the specified separator.
+ *
+ * @param addresses Address array
+ * @param separator Separator
+ * @return Human readable comma-delimited address string.
+ */
+ public static String toString(Address[] addresses, String separator) {
+ if (addresses == null || addresses.length == 0) {
+ return null;
+ }
+ if (addresses.length == 1) {
+ return addresses[0].toString();
+ }
+ StringBuffer sb = new StringBuffer(addresses[0].toString());
+ for (int i = 1; i < addresses.length; i++) {
+ sb.append(separator);
+ // TODO: investigate why this .trim() is needed.
+ sb.append(addresses[i].toString().trim());
+ }
+ return sb.toString();
+ }
+
+ /**
+ * Get RFC822/MIME compatible address string.
+ *
+ * @return RFC822/MIME compatible address string.
+ * It may be surrounded by double quote or quoted and MIME/base64 encoded if necessary.
+ */
+ public String toHeader() {
+ if (mPersonal != null) {
+ return EncoderUtil.encodeAddressDisplayName(mPersonal) + " <" + mAddress + ">";
+ } else {
+ return mAddress;
+ }
+ }
+
+ /**
+ * Get RFC822/MIME compatible comma-delimited address string.
+ *
+ * @param addresses Address array
+ * @return RFC822/MIME compatible comma-delimited address string.
+ * it may be surrounded by double quoted or quoted and MIME/base64 encoded if necessary.
+ */
+ public static String toHeader(Address[] addresses) {
+ if (addresses == null || addresses.length == 0) {
+ return null;
+ }
+ if (addresses.length == 1) {
+ return addresses[0].toHeader();
+ }
+ StringBuffer sb = new StringBuffer(addresses[0].toHeader());
+ for (int i = 1; i < addresses.length; i++) {
+ // We need space character to be able to fold line.
+ sb.append(", ");
+ sb.append(addresses[i].toHeader());
+ }
+ return sb.toString();
+ }
+
+ /**
+ * Get Human friendly address string.
+ *
+ * @return the personal part of this Address, or the address part if the
+ * personal part is not available
+ */
+ public String toFriendly() {
+ if (mPersonal != null && mPersonal.length() > 0) {
+ return mPersonal;
+ } else {
+ return mAddress;
+ }
+ }
+
+ /**
+ * Creates a comma-delimited list of addresses in the "friendly" format (see toFriendly() for
+ * details on the per-address conversion).
+ *
+ * @param addresses Array of Address[] values
+ * @return A comma-delimited string listing all of the addresses supplied. Null if source
+ * was null or empty.
+ */
+ public static String toFriendly(Address[] addresses) {
+ if (addresses == null || addresses.length == 0) {
+ return null;
+ }
+ if (addresses.length == 1) {
+ return addresses[0].toFriendly();
+ }
+ StringBuffer sb = new StringBuffer(addresses[0].toFriendly());
+ for (int i = 1; i < addresses.length; i++) {
+ sb.append(", ");
+ sb.append(addresses[i].toFriendly());
+ }
+ return sb.toString();
+ }
+
+ /**
+ * Returns exactly the same result as Address.toString(Address.unpack(packedList)).
+ */
+ public static String unpackToString(String packedList) {
+ return toString(unpack(packedList));
+ }
+
+ /**
+ * Returns exactly the same result as Address.pack(Address.parse(textList)).
+ */
+ public static String parseAndPack(String textList) {
+ return Address.pack(Address.parse(textList));
+ }
+
+ /**
+ * Returns null if the packedList has 0 addresses, otherwise returns the first address.
+ * The same as Address.unpack(packedList)[0] for non-empty list.
+ * This is an utility method that offers some performance optimization opportunities.
+ */
+ public static Address unpackFirst(String packedList) {
+ Address[] array = unpack(packedList);
+ return array.length > 0 ? array[0] : null;
+ }
+
+ /**
+ * Convert a packed list of addresses to a form suitable for use in an RFC822 header.
+ * This implementation is brute-force, and could be replaced with a more efficient version
+ * if desired.
+ */
+ public static String packedToHeader(String packedList) {
+ return toHeader(unpack(packedList));
+ }
+
+ /**
+ * Unpacks an address list that is either CSV of RFC822 addresses OR (for backward
+ * compatibility) previously packed with pack()
+ * @param addressList string packed with pack() or CSV of RFC822 addresses
+ * @return array of addresses resulting from unpack
+ */
+ public static Address[] unpack(String addressList) {
+ if (addressList == null || addressList.length() == 0) {
+ return EMPTY_ADDRESS_ARRAY;
+ }
+ // IF we're CSV, just parse
+ if ((addressList.indexOf(LIST_DELIMITER_PERSONAL) == -1) &&
+ (addressList.indexOf(LIST_DELIMITER_EMAIL) == -1)) {
+ return Address.parse(addressList);
+ }
+ // Otherwise, do backward-compatibile unpack
+ ArrayList<Address> addresses = new ArrayList<Address>();
+ int length = addressList.length();
+ int pairStartIndex = 0;
+ int pairEndIndex = 0;
+
+ /* addressEndIndex is only re-scanned (indexOf()) when a LIST_DELIMITER_PERSONAL
+ is used, not for every email address; i.e. not for every iteration of the while().
+ This reduces the theoretical complexity from quadratic to linear,
+ and provides some speed-up in practice by removing redundant scans of the string.
+ */
+ int addressEndIndex = addressList.indexOf(LIST_DELIMITER_PERSONAL);
+
+ while (pairStartIndex < length) {
+ pairEndIndex = addressList.indexOf(LIST_DELIMITER_EMAIL, pairStartIndex);
+ if (pairEndIndex == -1) {
+ pairEndIndex = length;
+ }
+ Address address;
+ if (addressEndIndex == -1 || pairEndIndex <= addressEndIndex) {
+ // in this case the DELIMITER_PERSONAL is in a future pair,
+ // so don't use personal, and don't update addressEndIndex
+ address = new Address(addressList.substring(pairStartIndex, pairEndIndex), null);
+ } else {
+ address = new Address(addressList.substring(pairStartIndex, addressEndIndex),
+ addressList.substring(addressEndIndex + 1, pairEndIndex));
+ // only update addressEndIndex when we use the LIST_DELIMITER_PERSONAL
+ addressEndIndex = addressList.indexOf(LIST_DELIMITER_PERSONAL, pairEndIndex + 1);
+ }
+ addresses.add(address);
+ pairStartIndex = pairEndIndex + 1;
+ }
+ return addresses.toArray(EMPTY_ADDRESS_ARRAY);
+ }
+
+ /**
+ * Generate a String containing RFC822 addresses separated by commas
+ * NOTE: We used to "pack" these addresses in an app-specific format, but no longer do so
+ */
+ public static String pack(Address[] addresses) {
+ return Address.toHeader(addresses);
+ }
+
+ /**
+ * Produces the same result as pack(array), but only packs one (this) address.
+ */
+ public String pack() {
+ final String address = getAddress();
+ final String personal = getPersonal();
+ if (personal == null) {
+ return address;
+ } else {
+ return address + LIST_DELIMITER_PERSONAL + personal;
+ }
+ }
+}
diff --git a/src/com/android/emailcommon/mail/AuthenticationFailedException.java b/src/com/android/emailcommon/mail/AuthenticationFailedException.java
new file mode 100644
index 0000000..af8d96c
--- /dev/null
+++ b/src/com/android/emailcommon/mail/AuthenticationFailedException.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.emailcommon.mail;
+
+
+public class AuthenticationFailedException extends MessagingException {
+ public static final long serialVersionUID = -1;
+
+ public AuthenticationFailedException(String message) {
+ super(MessagingException.AUTHENTICATION_FAILED, message);
+ }
+
+ public AuthenticationFailedException(int exceptionType, String message) {
+ super(exceptionType, message);
+ }
+
+ public AuthenticationFailedException(String message, Throwable throwable) {
+ super(MessagingException.AUTHENTICATION_FAILED, message, throwable);
+ }
+}
diff --git a/src/com/android/emailcommon/mail/Body.java b/src/com/android/emailcommon/mail/Body.java
new file mode 100644
index 0000000..841ab42
--- /dev/null
+++ b/src/com/android/emailcommon/mail/Body.java
@@ -0,0 +1,26 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.emailcommon.mail;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+
+public interface Body {
+ public InputStream getInputStream() throws MessagingException;
+ public void writeTo(OutputStream out) throws IOException, MessagingException;
+}
diff --git a/unified_src/com/android/mail/utils/LogTag.java b/src/com/android/emailcommon/mail/BodyPart.java
similarity index 61%
copy from unified_src/com/android/mail/utils/LogTag.java
copy to src/com/android/emailcommon/mail/BodyPart.java
index 01e2cf8..f698a13 100644
--- a/unified_src/com/android/mail/utils/LogTag.java
+++ b/src/com/android/emailcommon/mail/BodyPart.java
@@ -1,28 +1,25 @@
-/**
- * Copyright (c) 2012, Google Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.mail.utils;
-
-public class LogTag {
- private static String LOG_TAG = "UnifiedEmail";
-
- /**
- * Get the log tag to apply to logging.
- */
- public static String getLogTag() {
- return LOG_TAG;
- }
-}
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.emailcommon.mail;
+
+public abstract class BodyPart implements Part {
+ protected Multipart mParent;
+
+ public Multipart getParent() {
+ return mParent;
+ }
+}
diff --git a/src/com/android/emailcommon/mail/CertificateValidationException.java b/src/com/android/emailcommon/mail/CertificateValidationException.java
new file mode 100644
index 0000000..83c6224
--- /dev/null
+++ b/src/com/android/emailcommon/mail/CertificateValidationException.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.emailcommon.mail;
+
+
+public class CertificateValidationException extends MessagingException {
+ public static final long serialVersionUID = -1;
+
+ public CertificateValidationException(String message) {
+ super(MessagingException.CERTIFICATE_VALIDATION_ERROR, message);
+ }
+
+ public CertificateValidationException(String message, Throwable throwable) {
+ super(MessagingException.CERTIFICATE_VALIDATION_ERROR, message, throwable);
+ }
+}
\ No newline at end of file
diff --git a/src/com/android/emailcommon/mail/FetchProfile.java b/src/com/android/emailcommon/mail/FetchProfile.java
new file mode 100644
index 0000000..bfa48d3
--- /dev/null
+++ b/src/com/android/emailcommon/mail/FetchProfile.java
@@ -0,0 +1,85 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.emailcommon.mail;
+
+import java.util.ArrayList;
+
+/**
+ * <pre>
+ * A FetchProfile is a list of items that should be downloaded in bulk for a set of messages.
+ * FetchProfile can contain the following objects:
+ * FetchProfile.Item: Described below.
+ * Message: Indicates that the body of the entire message should be fetched.
+ * Synonymous with FetchProfile.Item.BODY.
+ * Part: Indicates that the given Part should be fetched. The provider
+ * is expected have previously created the given BodyPart and stored
+ * any information it needs to download the content.
+ * </pre>
+ */
+public class FetchProfile extends ArrayList<Fetchable> {
+ /**
+ * Default items available for pre-fetching. It should be expected that any
+ * item fetched by using these items could potentially include all of the
+ * previous items.
+ */
+ public enum Item implements Fetchable {
+ /**
+ * Download the flags of the message.
+ */
+ FLAGS,
+
+ /**
+ * Download the envelope of the message. This should include at minimum
+ * the size and the following headers: date, subject, from, content-type, to, cc
+ */
+ ENVELOPE,
+
+ /**
+ * Download the structure of the message. This maps directly to IMAP's BODYSTRUCTURE
+ * and may map to other providers.
+ * The provider should, if possible, fill in a properly formatted MIME structure in
+ * the message without actually downloading any message data. If the provider is not
+ * capable of this operation it should specifically set the body of the message to null
+ * so that upper levels can detect that a full body download is needed.
+ */
+ STRUCTURE,
+
+ /**
+ * A sane portion of the entire message, cut off at a provider determined limit.
+ * This should generaly be around 50kB.
+ */
+ BODY_SANE,
+
+ /**
+ * The entire message.
+ */
+ BODY,
+ }
+
+ /**
+ * @return the first {@link Part} in this collection, or null if it doesn't contain
+ * {@link Part}.
+ */
+ public Part getFirstPart() {
+ for (Fetchable o : this) {
+ if (o instanceof Part) {
+ return (Part) o;
+ }
+ }
+ return null;
+ }
+}
diff --git a/unified_src/com/android/mail/utils/LogTag.java b/src/com/android/emailcommon/mail/Fetchable.java
similarity index 61%
copy from unified_src/com/android/mail/utils/LogTag.java
copy to src/com/android/emailcommon/mail/Fetchable.java
index 01e2cf8..4314f93 100644
--- a/unified_src/com/android/mail/utils/LogTag.java
+++ b/src/com/android/emailcommon/mail/Fetchable.java
@@ -1,11 +1,11 @@
-/**
- * Copyright (c) 2012, Google Inc.
+/*
+ * Copyright (C) 2010 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
- * http://www.apache.org/licenses/LICENSE-2.0
+ * 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,
@@ -14,15 +14,11 @@
* limitations under the License.
*/
-package com.android.mail.utils;
+package com.android.emailcommon.mail;
-public class LogTag {
- private static String LOG_TAG = "UnifiedEmail";
-
- /**
- * Get the log tag to apply to logging.
- */
- public static String getLogTag() {
- return LOG_TAG;
- }
+/**
+ * Interface for classes that can be added to {@link FetchProfile}.
+ * i.e. {@link Part} and its subclasses, and {@link FetchProfile.Item}.
+ */
+public interface Fetchable {
}
diff --git a/src/com/android/emailcommon/mail/Flag.java b/src/com/android/emailcommon/mail/Flag.java
new file mode 100644
index 0000000..bcdcb8b
--- /dev/null
+++ b/src/com/android/emailcommon/mail/Flag.java
@@ -0,0 +1,79 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.emailcommon.mail;
+
+/**
+ * Flags that can be applied to Messages.
+ */
+public enum Flag {
+
+ // If adding new flags: ALL FLAGS MUST BE UPPER CASE.
+
+ DELETED,
+ SEEN,
+ ANSWERED,
+ FLAGGED,
+ DRAFT,
+ RECENT,
+
+ /*
+ * The following flags are for internal library use only.
+ * TODO Eventually we should creates a Flags class that extends ArrayList that allows
+ * these flags and Strings to represent user defined flags. At that point the below
+ * flags should become user defined flags.
+ */
+ /**
+ * Delete and remove from the LocalStore immediately.
+ */
+ X_DESTROYED,
+
+ /**
+ * Sending of an unsent message failed. It will be retried. Used to show status.
+ */
+ X_SEND_FAILED,
+
+ /**
+ * Sending of an unsent message is in progress.
+ */
+ X_SEND_IN_PROGRESS,
+
+ /**
+ * Indicates that a message is fully downloaded from the server and can be viewed normally.
+ * This does not include attachments, which are never downloaded fully.
+ */
+ X_DOWNLOADED_FULL,
+
+ /**
+ * Indicates that a message is partially downloaded from the server and can be viewed but
+ * more content is available on the server.
+ * This does not include attachments, which are never downloaded fully.
+ */
+ X_DOWNLOADED_PARTIAL,
+
+ /**
+ * General purpose flag that can be used by any remote store. The flag will be
+ * saved and restored by the LocalStore.
+ */
+ X_STORE_1,
+
+ /**
+ * General purpose flag that can be used by any remote store. The flag will be
+ * saved and restored by the LocalStore.
+ */
+ X_STORE_2,
+
+}
diff --git a/src/com/android/emailcommon/mail/Folder.java b/src/com/android/emailcommon/mail/Folder.java
new file mode 100644
index 0000000..c58988d
--- /dev/null
+++ b/src/com/android/emailcommon/mail/Folder.java
@@ -0,0 +1,209 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.emailcommon.mail;
+
+import com.android.emailcommon.service.SearchParams;
+import com.google.common.annotations.VisibleForTesting;
+
+
+public abstract class Folder {
+ public enum OpenMode {
+ READ_WRITE, READ_ONLY,
+ }
+
+ public enum FolderType {
+ HOLDS_FOLDERS, HOLDS_MESSAGES,
+ }
+
+ /**
+ * Identifiers of "special" folders.
+ */
+ public enum FolderRole {
+ INBOX, // NOTE: The folder's name must be INBOX
+ TRASH,
+ SENT,
+ DRAFTS,
+
+ OUTBOX, // Local folders only - not used in remote Stores
+ OTHER, // this folder has no specific role
+ UNKNOWN // the role of this folder is unknown
+ }
+
+ /**
+ * Callback for each message retrieval.
+ *
+ * Not all {@link Folder} implementations may invoke it.
+ */
+ public interface MessageRetrievalListener {
+ public void messageRetrieved(Message message);
+ public void loadAttachmentProgress(int progress);
+ }
+
+ /**
+ * Forces an open of the MailProvider. If the provider is already open this
+ * function returns without doing anything.
+ *
+ * @param mode READ_ONLY or READ_WRITE
+ * @param callbacks Pointer to callbacks class. This may be used by the folder between this
+ * time and when close() is called. This is only used for remote stores - should be null
+ * for LocalStore.LocalFolder.
+ */
+ public abstract void open(OpenMode mode)
+ throws MessagingException;
+
+ /**
+ * Forces a close of the MailProvider. Any further access will attempt to
+ * reopen the MailProvider.
+ *
+ * @param expunge If true all deleted messages will be expunged.
+ */
+ public abstract void close(boolean expunge) throws MessagingException;
+
+ /**
+ * @return True if further commands are not expected to have to open the
+ * connection.
+ */
+ @VisibleForTesting
+ public abstract boolean isOpen();
+
+ /**
+ * Returns the mode the folder was opened with. This may be different than the mode the open
+ * was requested with.
+ */
+ public abstract OpenMode getMode() throws MessagingException;
+
+ /**
+ * Reports if the Store is able to create folders of the given type.
+ * Does not actually attempt to create a folder.
+ * @param type
+ * @return true if can create, false if cannot create
+ */
+ public abstract boolean canCreate(FolderType type);
+
+ /**
+ * Attempt to create the given folder remotely using the given type.
+ * @return true if created, false if cannot create (e.g. server side)
+ */
+ public abstract boolean create(FolderType type) throws MessagingException;
+
+ public abstract boolean exists() throws MessagingException;
+
+ /**
+ * Returns the number of messages in the selected folder.
+ */
+ public abstract int getMessageCount() throws MessagingException;
+
+ public abstract int getUnreadMessageCount() throws MessagingException;
+
+ public abstract Message getMessage(String uid) throws MessagingException;
+
+ /**
+ * Fetches the given list of messages. The specified listener is notified as
+ * each fetch completes. Messages are downloaded as (as) lightweight (as
+ * possible) objects to be filled in with later requests. In most cases this
+ * means that only the UID is downloaded.
+ */
+ public abstract Message[] getMessages(int start, int end, MessageRetrievalListener listener)
+ throws MessagingException;
+
+ public abstract Message[] getMessages(SearchParams params,MessageRetrievalListener listener)
+ throws MessagingException;
+
+ public abstract Message[] getMessages(String[] uids, MessageRetrievalListener listener)
+ throws MessagingException;
+
+ /**
+ * Return a set of messages based on the state of the flags.
+ * Note: Not typically implemented in remote stores, so not abstract.
+ *
+ * @param setFlags The flags that should be set for a message to be selected (can be null)
+ * @param clearFlags The flags that should be clear for a message to be selected (can be null)
+ * @param listener
+ * @return A list of messages matching the desired flag states.
+ * @throws MessagingException
+ */
+ public Message[] getMessages(Flag[] setFlags, Flag[] clearFlags,
+ MessageRetrievalListener listener) throws MessagingException {
+ throw new MessagingException("Not implemented");
+ }
+
+ public abstract void appendMessages(Message[] messages) throws MessagingException;
+
+ /**
+ * Copies the given messages to the destination folder.
+ */
+ public abstract void copyMessages(Message[] msgs, Folder folder,
+ MessageUpdateCallbacks callbacks) throws MessagingException;
+
+ public abstract void setFlags(Message[] messages, Flag[] flags, boolean value)
+ throws MessagingException;
+
+ public abstract Message[] expunge() throws MessagingException;
+
+ public abstract void fetch(Message[] messages, FetchProfile fp,
+ MessageRetrievalListener listener) throws MessagingException;
+
+ public abstract void delete(boolean recurse) throws MessagingException;
+
+ public abstract String getName();
+
+ public abstract Flag[] getPermanentFlags() throws MessagingException;
+
+ /**
+ * This method returns a string identifying the name of a "role" folder
+ * (such as inbox, draft, sent, or trash). Stores that do not implement this
+ * feature can be used - the account UI will provide default strings. To
+ * let the server identify specific folder roles, simply override this method.
+ *
+ * @return The server- or protocol- specific role for this folder. If some roles are known
+ * but this is not one of them, return FolderRole.OTHER. If roles are unsupported here,
+ * return FolderRole.UNKNOWN.
+ */
+ public FolderRole getRole() {
+ return FolderRole.UNKNOWN;
+ }
+
+ /**
+ * Create an empty message of the appropriate type for the Folder.
+ */
+ public abstract Message createMessage(String uid) throws MessagingException;
+
+ /**
+ * Callback interface by which a folder can report UID changes caused by certain operations.
+ */
+ public interface MessageUpdateCallbacks {
+ /**
+ * The operation caused the message's UID to change
+ * @param message The message for which the UID changed
+ * @param newUid The new UID for the message
+ */
+ public void onMessageUidChange(Message message, String newUid) throws MessagingException;
+
+ /**
+ * The operation could not be completed because the message doesn't exist
+ * (for example, it was already deleted from the server side.)
+ * @param message The message that does not exist
+ * @throws MessagingException
+ */
+ public void onMessageNotFound(Message message) throws MessagingException;
+ }
+
+ @Override
+ public String toString() {
+ return getName();
+ }
+}
diff --git a/src/com/android/emailcommon/mail/MeetingInfo.java b/src/com/android/emailcommon/mail/MeetingInfo.java
new file mode 100644
index 0000000..87637f7
--- /dev/null
+++ b/src/com/android/emailcommon/mail/MeetingInfo.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.emailcommon.mail;
+
+public class MeetingInfo {
+ // Predefined tags; others can be added
+ public static final String MEETING_DTSTAMP = "DTSTAMP";
+ public static final String MEETING_UID = "UID";
+ public static final String MEETING_ORGANIZER_EMAIL = "ORGMAIL";
+ public static final String MEETING_DTSTART = "DTSTART";
+ public static final String MEETING_DTEND = "DTEND";
+ public static final String MEETING_TITLE = "TITLE";
+ public static final String MEETING_LOCATION = "LOC";
+ public static final String MEETING_RESPONSE_REQUESTED = "RESPONSE";
+ public static final String MEETING_ALL_DAY = "ALLDAY";
+}
diff --git a/src/com/android/emailcommon/mail/Message.java b/src/com/android/emailcommon/mail/Message.java
new file mode 100644
index 0000000..09aef87
--- /dev/null
+++ b/src/com/android/emailcommon/mail/Message.java
@@ -0,0 +1,159 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.emailcommon.mail;
+
+import java.util.Date;
+import java.util.HashSet;
+
+public abstract class Message implements Part, Body {
+ public static final Message[] EMPTY_ARRAY = new Message[0];
+
+ public enum RecipientType {
+ TO, CC, BCC,
+ }
+
+ protected String mUid;
+
+ private HashSet<Flag> mFlags = null;
+
+ protected Date mInternalDate;
+
+ protected Folder mFolder;
+
+ public String getUid() {
+ return mUid;
+ }
+
+ public void setUid(String uid) {
+ this.mUid = uid;
+ }
+
+ public Folder getFolder() {
+ return mFolder;
+ }
+
+ public abstract String getSubject() throws MessagingException;
+
+ public abstract void setSubject(String subject) throws MessagingException;
+
+ public Date getInternalDate() {
+ return mInternalDate;
+ }
+
+ public void setInternalDate(Date internalDate) {
+ this.mInternalDate = internalDate;
+ }
+
+ public abstract Date getReceivedDate() throws MessagingException;
+
+ public abstract Date getSentDate() throws MessagingException;
+
+ public abstract void setSentDate(Date sentDate) throws MessagingException;
+
+ public abstract Address[] getRecipients(RecipientType type) throws MessagingException;
+
+ public abstract void setRecipients(RecipientType type, Address[] addresses)
+ throws MessagingException;
+
+ public void setRecipient(RecipientType type, Address address) throws MessagingException {
+ setRecipients(type, new Address[] {
+ address
+ });
+ }
+
+ public abstract Address[] getFrom() throws MessagingException;
+
+ public abstract void setFrom(Address from) throws MessagingException;
+
+ public abstract Address[] getReplyTo() throws MessagingException;
+
+ public abstract void setReplyTo(Address[] from) throws MessagingException;
+
+ public abstract Body getBody() throws MessagingException;
+
+ public abstract String getContentType() throws MessagingException;
+
+ public abstract void addHeader(String name, String value) throws MessagingException;
+
+ public abstract void setHeader(String name, String value) throws MessagingException;
+
+ public abstract String[] getHeader(String name) throws MessagingException;
+
+ public abstract void removeHeader(String name) throws MessagingException;
+
+ // Always use these instead of getHeader("Message-ID") or setHeader("Message-ID");
+ public abstract void setMessageId(String messageId) throws MessagingException;
+ public abstract String getMessageId() throws MessagingException;
+
+ public abstract void setBody(Body body) throws MessagingException;
+
+ public boolean isMimeType(String mimeType) throws MessagingException {
+ return getContentType().startsWith(mimeType);
+ }
+
+ private HashSet<Flag> getFlagSet() {
+ if (mFlags == null) {
+ mFlags = new HashSet<Flag>();
+ }
+ return mFlags;
+ }
+
+ /*
+ * TODO Refactor Flags at some point to be able to store user defined flags.
+ */
+ public Flag[] getFlags() {
+ return getFlagSet().toArray(new Flag[] {});
+ }
+
+ /**
+ * Set/clear a flag directly, without involving overrides of {@link #setFlag} in subclasses.
+ * Only used for testing.
+ */
+ public final void setFlagDirectlyForTest(Flag flag, boolean set) throws MessagingException {
+ if (set) {
+ getFlagSet().add(flag);
+ } else {
+ getFlagSet().remove(flag);
+ }
+ }
+
+ public void setFlag(Flag flag, boolean set) throws MessagingException {
+ setFlagDirectlyForTest(flag, set);
+ }
+
+ /**
+ * This method calls setFlag(Flag, boolean)
+ * @param flags
+ * @param set
+ */
+ public void setFlags(Flag[] flags, boolean set) throws MessagingException {
+ for (Flag flag : flags) {
+ setFlag(flag, set);
+ }
+ }
+
+ public boolean isSet(Flag flag) {
+ return getFlagSet().contains(flag);
+ }
+
+ public abstract void saveChanges() throws MessagingException;
+
+ @Override
+ public String toString() {
+ return getClass().getSimpleName() + ':' + mUid;
+ }
+}
diff --git a/src/com/android/emailcommon/mail/MessageDateComparator.java b/src/com/android/emailcommon/mail/MessageDateComparator.java
new file mode 100644
index 0000000..0b1a551
--- /dev/null
+++ b/src/com/android/emailcommon/mail/MessageDateComparator.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.emailcommon.mail;
+
+import java.util.Comparator;
+
+public class MessageDateComparator implements Comparator<Message> {
+ public int compare(Message o1, Message o2) {
+ try {
+ if (o1.getSentDate() == null) {
+ return 1;
+ } else if (o2.getSentDate() == null) {
+ return -1;
+ } else
+ return o2.getSentDate().compareTo(o1.getSentDate());
+ } catch (Exception e) {
+ return 0;
+ }
+ }
+}
diff --git a/src/com/android/emailcommon/mail/MessagingException.java b/src/com/android/emailcommon/mail/MessagingException.java
new file mode 100644
index 0000000..4a8ceba
--- /dev/null
+++ b/src/com/android/emailcommon/mail/MessagingException.java
@@ -0,0 +1,136 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.emailcommon.mail;
+
+
+/**
+ * This exception is used for most types of failures that occur during server interactions.
+ *
+ * Data passed through this exception should be considered non-localized. Any strings should
+ * either be internal-only (for debugging) or server-generated.
+ *
+ * TO DO: Does it make sense to further collapse AuthenticationFailedException and
+ * CertificateValidationException and any others into this?
+ */
+public class MessagingException extends Exception {
+ public static final long serialVersionUID = -1;
+
+ public static final int NO_ERROR = -1;
+ /** Any exception that does not specify a specific issue */
+ public static final int UNSPECIFIED_EXCEPTION = 0;
+ /** Connection or IO errors */
+ public static final int IOERROR = 1;
+ /** The configuration requested TLS but the server did not support it. */
+ public static final int TLS_REQUIRED = 2;
+ /** Authentication is required but the server did not support it. */
+ public static final int AUTH_REQUIRED = 3;
+ /** General security failures */
+ public static final int GENERAL_SECURITY = 4;
+ /** Authentication failed */
+ public static final int AUTHENTICATION_FAILED = 5;
+ /** Attempt to create duplicate account */
+ public static final int DUPLICATE_ACCOUNT = 6;
+ /** Required security policies reported - advisory only */
+ public static final int SECURITY_POLICIES_REQUIRED = 7;
+ /** Required security policies not supported */
+ public static final int SECURITY_POLICIES_UNSUPPORTED = 8;
+ /** The protocol (or protocol version) isn't supported */
+ public static final int PROTOCOL_VERSION_UNSUPPORTED = 9;
+ /** The server's SSL certificate couldn't be validated */
+ public static final int CERTIFICATE_VALIDATION_ERROR = 10;
+ /** Authentication failed during autodiscover */
+ public static final int AUTODISCOVER_AUTHENTICATION_FAILED = 11;
+ /** Autodiscover completed with a result (non-error) */
+ public static final int AUTODISCOVER_AUTHENTICATION_RESULT = 12;
+ /** Ambiguous failure; server error or bad credentials */
+ public static final int AUTHENTICATION_FAILED_OR_SERVER_ERROR = 13;
+ /** The server refused access */
+ public static final int ACCESS_DENIED = 14;
+ /** The server refused access */
+ public static final int ATTACHMENT_NOT_FOUND = 15;
+ /** A client SSL certificate is required for connections to the server */
+ public static final int CLIENT_CERTIFICATE_REQUIRED = 16;
+ /** The client SSL certificate specified is invalid */
+ public static final int CLIENT_CERTIFICATE_ERROR = 17;
+
+ protected int mExceptionType;
+ // Exception type-specific data
+ protected Object mExceptionData;
+
+ public MessagingException(String message, Throwable throwable) {
+ this(UNSPECIFIED_EXCEPTION, message, throwable);
+ }
+
+ public MessagingException(int exceptionType, String message, Throwable throwable) {
+ super(message, throwable);
+ mExceptionType = exceptionType;
+ mExceptionData = null;
+ }
+
+ /**
+ * Constructs a MessagingException with an exceptionType and a null message.
+ * @param exceptionType The exception type to set for this exception.
+ */
+ public MessagingException(int exceptionType) {
+ this(exceptionType, null, null);
+ }
+
+ /**
+ * Constructs a MessagingException with a message.
+ * @param message the message for this exception
+ */
+ public MessagingException(String message) {
+ this(UNSPECIFIED_EXCEPTION, message, null);
+ }
+
+ /**
+ * Constructs a MessagingException with an exceptionType and a message.
+ * @param exceptionType The exception type to set for this exception.
+ */
+ public MessagingException(int exceptionType, String message) {
+ this(exceptionType, message, null);
+ }
+
+ /**
+ * Constructs a MessagingException with an exceptionType, a message, and data
+ * @param exceptionType The exception type to set for this exception.
+ * @param message the message for the exception (or null)
+ * @param data exception-type specific data for the exception (or null)
+ */
+ public MessagingException(int exceptionType, String message, Object data) {
+ super(message);
+ mExceptionType = exceptionType;
+ mExceptionData = data;
+ }
+
+ /**
+ * Return the exception type. Will be OTHER_EXCEPTION if not explicitly set.
+ *
+ * @return Returns the exception type.
+ */
+ public int getExceptionType() {
+ return mExceptionType;
+ }
+ /**
+ * Return the exception data. Will be null if not explicitly set.
+ *
+ * @return Returns the exception data.
+ */
+ public Object getExceptionData() {
+ return mExceptionData;
+ }
+}
\ No newline at end of file
diff --git a/src/com/android/emailcommon/mail/Multipart.java b/src/com/android/emailcommon/mail/Multipart.java
new file mode 100644
index 0000000..4a1a067
--- /dev/null
+++ b/src/com/android/emailcommon/mail/Multipart.java
@@ -0,0 +1,63 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.emailcommon.mail;
+
+import java.util.ArrayList;
+
+public abstract class Multipart implements Body {
+ protected Part mParent;
+
+ protected ArrayList<BodyPart> mParts = new ArrayList<BodyPart>();
+
+ protected String mContentType;
+
+ public void addBodyPart(BodyPart part) throws MessagingException {
+ mParts.add(part);
+ }
+
+ public void addBodyPart(BodyPart part, int index) throws MessagingException {
+ mParts.add(index, part);
+ }
+
+ public BodyPart getBodyPart(int index) throws MessagingException {
+ return mParts.get(index);
+ }
+
+ public String getContentType() throws MessagingException {
+ return mContentType;
+ }
+
+ public int getCount() throws MessagingException {
+ return mParts.size();
+ }
+
+ public boolean removeBodyPart(BodyPart part) throws MessagingException {
+ return mParts.remove(part);
+ }
+
+ public void removeBodyPart(int index) throws MessagingException {
+ mParts.remove(index);
+ }
+
+ public Part getParent() throws MessagingException {
+ return mParent;
+ }
+
+ public void setParent(Part parent) throws MessagingException {
+ this.mParent = parent;
+ }
+}
diff --git a/src/com/android/emailcommon/mail/PackedString.java b/src/com/android/emailcommon/mail/PackedString.java
new file mode 100644
index 0000000..de5fe46
--- /dev/null
+++ b/src/com/android/emailcommon/mail/PackedString.java
@@ -0,0 +1,176 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.emailcommon.mail;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * A utility class for creating and modifying Strings that are tagged and packed together.
+ *
+ * Uses non-printable (control chars) for internal delimiters; Intended for regular displayable
+ * strings only, so please use base64 or other encoding if you need to hide any binary data here.
+ *
+ * Binary compatible with Address.pack() format, which should migrate to use this code.
+ */
+public class PackedString {
+
+ /**
+ * Packing format is:
+ * element : [ value ] or [ value TAG-DELIMITER tag ]
+ * packed-string : [ element ] [ ELEMENT-DELIMITER [ element ] ]*
+ */
+ private static final char DELIMITER_ELEMENT = '\1';
+ private static final char DELIMITER_TAG = '\2';
+
+ private String mString;
+ private HashMap<String, String> mExploded;
+ private static final HashMap<String, String> EMPTY_MAP = new HashMap<String, String>();
+
+ /**
+ * Create a packed string using an already-packed string (e.g. from database)
+ * @param string packed string
+ */
+ public PackedString(String string) {
+ mString = string;
+ mExploded = null;
+ }
+
+ /**
+ * Get the value referred to by a given tag. If the tag does not exist, return null.
+ * @param tag identifier of string of interest
+ * @return returns value, or null if no string is found
+ */
+ public String get(String tag) {
+ if (mExploded == null) {
+ mExploded = explode(mString);
+ }
+ return mExploded.get(tag);
+ }
+
+ /**
+ * Return a map of all of the values referred to by a given tag. This is a shallow
+ * copy, don't edit the values.
+ * @return a map of the values in the packed string
+ */
+ public Map<String, String> unpack() {
+ if (mExploded == null) {
+ mExploded = explode(mString);
+ }
+ return new HashMap<String,String>(mExploded);
+ }
+
+ /**
+ * Read out all values into a map.
+ */
+ private static HashMap<String, String> explode(String packed) {
+ if (packed == null || packed.length() == 0) {
+ return EMPTY_MAP;
+ }
+ HashMap<String, String> map = new HashMap<String, String>();
+
+ int length = packed.length();
+ int elementStartIndex = 0;
+ int elementEndIndex = 0;
+ int tagEndIndex = packed.indexOf(DELIMITER_TAG);
+
+ while (elementStartIndex < length) {
+ elementEndIndex = packed.indexOf(DELIMITER_ELEMENT, elementStartIndex);
+ if (elementEndIndex == -1) {
+ elementEndIndex = length;
+ }
+ String tag;
+ String value;
+ if (tagEndIndex == -1 || elementEndIndex <= tagEndIndex) {
+ // in this case the DELIMITER_PERSONAL is in a future pair (or not found)
+ // so synthesize a positional tag for the value, and don't update tagEndIndex
+ value = packed.substring(elementStartIndex, elementEndIndex);
+ tag = Integer.toString(map.size());
+ } else {
+ value = packed.substring(elementStartIndex, tagEndIndex);
+ tag = packed.substring(tagEndIndex + 1, elementEndIndex);
+ // scan forward for next tag, if any
+ tagEndIndex = packed.indexOf(DELIMITER_TAG, elementEndIndex + 1);
+ }
+ map.put(tag, value);
+ elementStartIndex = elementEndIndex + 1;
+ }
+
+ return map;
+ }
+
+ /**
+ * Builder class for creating PackedString values. Can also be used for editing existing
+ * PackedString representations.
+ */
+ static public class Builder {
+ HashMap<String, String> mMap;
+
+ /**
+ * Create a builder that's empty (for filling)
+ */
+ public Builder() {
+ mMap = new HashMap<String, String>();
+ }
+
+ /**
+ * Create a builder using the values of an existing PackedString (for editing).
+ */
+ public Builder(String packed) {
+ mMap = explode(packed);
+ }
+
+ /**
+ * Add a tagged value
+ * @param tag identifier of string of interest
+ * @param value the value to record in this position. null to delete entry.
+ */
+ public void put(String tag, String value) {
+ if (value == null) {
+ mMap.remove(tag);
+ } else {
+ mMap.put(tag, value);
+ }
+ }
+
+ /**
+ * Get the value referred to by a given tag. If the tag does not exist, return null.
+ * @param tag identifier of string of interest
+ * @return returns value, or null if no string is found
+ */
+ public String get(String tag) {
+ return mMap.get(tag);
+ }
+
+ /**
+ * Pack the values and return a single, encoded string
+ */
+ @Override
+ public String toString() {
+ StringBuilder sb = new StringBuilder();
+ for (Map.Entry<String,String> entry : mMap.entrySet()) {
+ if (sb.length() > 0) {
+ sb.append(DELIMITER_ELEMENT);
+ }
+ sb.append(entry.getValue());
+ sb.append(DELIMITER_TAG);
+ sb.append(entry.getKey());
+ }
+ return sb.toString();
+ }
+ }
+}
diff --git a/src/com/android/emailcommon/mail/Part.java b/src/com/android/emailcommon/mail/Part.java
new file mode 100644
index 0000000..eeb233c
--- /dev/null
+++ b/src/com/android/emailcommon/mail/Part.java
@@ -0,0 +1,52 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.emailcommon.mail;
+
+import java.io.IOException;
+import java.io.OutputStream;
+
+public interface Part extends Fetchable {
+ public void addHeader(String name, String value) throws MessagingException;
+
+ public void removeHeader(String name) throws MessagingException;
+
+ public void setHeader(String name, String value) throws MessagingException;
+
+ public Body getBody() throws MessagingException;
+
+ public String getContentType() throws MessagingException;
+
+ public String getDisposition() throws MessagingException;
+
+ public String getContentId() throws MessagingException;
+
+ public String[] getHeader(String name) throws MessagingException;
+
+ public void setExtendedHeader(String name, String value) throws MessagingException;
+
+ public String getExtendedHeader(String name) throws MessagingException;
+
+ public int getSize() throws MessagingException;
+
+ public boolean isMimeType(String mimeType) throws MessagingException;
+
+ public String getMimeType() throws MessagingException;
+
+ public void setBody(Body body) throws MessagingException;
+
+ public void writeTo(OutputStream out) throws IOException, MessagingException;
+}
diff --git a/src/com/android/emailcommon/service/SearchParams.java b/src/com/android/emailcommon/service/SearchParams.java
new file mode 100644
index 0000000..3b9d6c9
--- /dev/null
+++ b/src/com/android/emailcommon/service/SearchParams.java
@@ -0,0 +1,135 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.emailcommon.service;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import com.google.common.base.Objects;
+
+public class SearchParams implements Parcelable {
+
+ private static final int DEFAULT_LIMIT = 10; // Need input on what this number should be
+ private static final int DEFAULT_OFFSET = 0;
+
+ // The id of the mailbox to be searched; if -1, all mailboxes MUST be searched
+ public final long mMailboxId;
+ // If true, all subfolders of the specified mailbox MUST be searched
+ public boolean mIncludeChildren = true;
+ // The search terms (the search MUST only select messages whose contents include all of the
+ // search terms in the query)
+ public final String mFilter;
+ // The maximum number of results to be created by this search
+ public int mLimit = DEFAULT_LIMIT;
+ // If zero, specifies a "new" search; otherwise, asks for a continuation of the previous
+ // query(ies) starting with the mOffset'th match (0 based)
+ public int mOffset = DEFAULT_OFFSET;
+ // The total number of results for this search
+ public int mTotalCount = 0;
+ // The id of the "search" mailbox being used
+ public long mSearchMailboxId;
+
+ /**
+ * Error codes returned by the searchMessages API
+ */
+ public static class SearchParamsError {
+ public static final int CANT_SEARCH_ALL_MAILBOXES = -1;
+ public static final int CANT_SEARCH_CHILDREN = -2;
+ }
+
+ public SearchParams(long mailboxId, String filter) {
+ mMailboxId = mailboxId;
+ mFilter = filter;
+ }
+
+ public SearchParams(long mailboxId, String filter, long searchMailboxId) {
+ mMailboxId = mailboxId;
+ mFilter = filter;
+ mSearchMailboxId = searchMailboxId;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (o == this) {
+ return true;
+ }
+ if ((o == null) || !(o instanceof SearchParams)) {
+ return false;
+ }
+
+ SearchParams os = (SearchParams) o;
+ return mMailboxId == os.mMailboxId
+ && mIncludeChildren == os.mIncludeChildren
+ && mFilter.equals(os.mFilter)
+ && mLimit == os.mLimit
+ && mOffset == os.mOffset;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hashCode(mMailboxId, mFilter, mOffset);
+ }
+
+ @Override
+ public String toString() {
+ return "[SearchParams " + mMailboxId + ":" + mFilter + " (" + mOffset + ", " + mLimit + "]";
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ /**
+ * Supports Parcelable
+ */
+ public static final Parcelable.Creator<SearchParams> CREATOR
+ = new Parcelable.Creator<SearchParams>() {
+ @Override
+ public SearchParams createFromParcel(Parcel in) {
+ return new SearchParams(in);
+ }
+
+ @Override
+ public SearchParams[] newArray(int size) {
+ return new SearchParams[size];
+ }
+ };
+
+ /**
+ * Supports Parcelable
+ */
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeLong(mMailboxId);
+ dest.writeInt(mIncludeChildren ? 1 : 0);
+ dest.writeString(mFilter);
+ dest.writeInt(mLimit);
+ dest.writeInt(mOffset);
+ }
+
+ /**
+ * Supports Parcelable
+ */
+ public SearchParams(Parcel in) {
+ mMailboxId = in.readLong();
+ mIncludeChildren = in.readInt() == 1;
+ mFilter = in.readString();
+ mLimit = in.readInt();
+ mOffset = in.readInt();
+ }
+}
diff --git a/src/com/android/emailcommon/utility/ConversionUtilities.java b/src/com/android/emailcommon/utility/ConversionUtilities.java
new file mode 100644
index 0000000..0dbb520
--- /dev/null
+++ b/src/com/android/emailcommon/utility/ConversionUtilities.java
@@ -0,0 +1,144 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.emailcommon.utility;
+
+import com.android.emailcommon.internet.MimeHeader;
+import com.android.emailcommon.internet.MimeUtility;
+import com.android.emailcommon.mail.MessagingException;
+import com.android.emailcommon.mail.Part;
+
+import android.text.TextUtils;
+
+import java.util.ArrayList;
+
+public class ConversionUtilities {
+ /**
+ * Values for HEADER_ANDROID_BODY_QUOTED_PART to tag body parts
+ */
+ public static final String BODY_QUOTED_PART_REPLY = "quoted-reply";
+ public static final String BODY_QUOTED_PART_FORWARD = "quoted-forward";
+ public static final String BODY_QUOTED_PART_INTRO = "quoted-intro";
+
+ /**
+ * Helper function to append text to a StringBuffer, creating it if necessary.
+ * Optimization: The majority of the time we are *not* appending - we should have a path
+ * that deals with single strings.
+ */
+ private static StringBuffer appendTextPart(StringBuffer sb, String newText) {
+ if (newText == null) {
+ return sb;
+ }
+ else if (sb == null) {
+ sb = new StringBuffer(newText);
+ } else {
+ if (sb.length() > 0) {
+ sb.append('\n');
+ }
+ sb.append(newText);
+ }
+ return sb;
+ }
+
+ /**
+ * Plain-Old-Data class to return parsed body data from
+ * {@link ConversionUtilities#parseBodyFields}
+ */
+ public static class BodyFieldData {
+ public String textContent;
+ public String htmlContent;
+ public String textReply;
+ public String htmlReply;
+ public String introText;
+ public String snippet;
+ public boolean isQuotedReply;
+ public boolean isQuotedForward;
+ }
+
+ /**
+ * Parse body text (plain and/or HTML) from MimeMessage to {@link BodyFieldData}.
+ */
+ public static BodyFieldData parseBodyFields(ArrayList<Part> viewables)
+ throws MessagingException {
+ final BodyFieldData data = new BodyFieldData();
+ StringBuffer sbHtml = null;
+ StringBuffer sbText = null;
+ StringBuffer sbHtmlReply = null;
+ StringBuffer sbTextReply = null;
+ StringBuffer sbIntroText = null;
+
+ for (Part viewable : viewables) {
+ String text = MimeUtility.getTextFromPart(viewable);
+ String[] replyTags = viewable.getHeader(MimeHeader.HEADER_ANDROID_BODY_QUOTED_PART);
+ String replyTag = null;
+ if (replyTags != null && replyTags.length > 0) {
+ replyTag = replyTags[0];
+ }
+ // Deploy text as marked by the various tags
+ boolean isHtml = "text/html".equalsIgnoreCase(viewable.getMimeType());
+
+ if (replyTag != null) {
+ data.isQuotedReply = BODY_QUOTED_PART_REPLY.equalsIgnoreCase(replyTag);
+ data.isQuotedForward = BODY_QUOTED_PART_FORWARD.equalsIgnoreCase(replyTag);
+ boolean isQuotedIntro = BODY_QUOTED_PART_INTRO.equalsIgnoreCase(replyTag);
+
+ if (data.isQuotedReply || data.isQuotedForward) {
+ if (isHtml) {
+ sbHtmlReply = appendTextPart(sbHtmlReply, text);
+ } else {
+ sbTextReply = appendTextPart(sbTextReply, text);
+ }
+ continue;
+ }
+ if (isQuotedIntro) {
+ sbIntroText = appendTextPart(sbIntroText, text);
+ continue;
+ }
+ }
+
+ // Most of the time, just process regular body parts
+ if (isHtml) {
+ sbHtml = appendTextPart(sbHtml, text);
+ } else {
+ sbText = appendTextPart(sbText, text);
+ }
+ }
+
+ // write the combined data to the body part
+ if (!TextUtils.isEmpty(sbText)) {
+ String text = sbText.toString();
+ data.textContent = text;
+ data.snippet = TextUtilities.makeSnippetFromPlainText(text);
+ }
+ if (!TextUtils.isEmpty(sbHtml)) {
+ String text = sbHtml.toString();
+ data.htmlContent = text;
+ if (data.snippet == null) {
+ data.snippet = TextUtilities.makeSnippetFromHtmlText(text);
+ }
+ }
+ if (sbHtmlReply != null && sbHtmlReply.length() != 0) {
+ data.htmlReply = sbHtmlReply.toString();
+ }
+ if (sbTextReply != null && sbTextReply.length() != 0) {
+ data.textReply = sbTextReply.toString();
+ }
+ if (sbIntroText != null && sbIntroText.length() != 0) {
+ data.introText = sbIntroText.toString();
+ }
+ return data;
+ }
+}
diff --git a/src/com/android/emailcommon/utility/TextUtilities.java b/src/com/android/emailcommon/utility/TextUtilities.java
new file mode 100755
index 0000000..0aa9190
--- /dev/null
+++ b/src/com/android/emailcommon/utility/TextUtilities.java
@@ -0,0 +1,728 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.emailcommon.utility;
+
+import com.google.common.annotations.VisibleForTesting;
+
+import android.graphics.Color;
+import android.text.Spannable;
+import android.text.SpannableString;
+import android.text.SpannableStringBuilder;
+import android.text.TextUtils;
+import android.text.style.BackgroundColorSpan;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.StringTokenizer;
+
+public class TextUtilities {
+ // Highlight color is yellow, as in other apps.
+ // TODO Push for this to be a global (style-related?) constant
+ public static final int HIGHLIGHT_COLOR_INT = Color.YELLOW;
+ // We AND off the "alpha" from the color (i.e. 0xFFFFFF00 -> 0x00FFFF00)
+ /*package*/ static final String HIGHLIGHT_COLOR_STRING =
+ '#' + Integer.toHexString(HIGHLIGHT_COLOR_INT & 0x00FFFFFF);
+
+ // This is how many chars we'll allow in a snippet
+ private static final int MAX_SNIPPET_LENGTH = 200;
+ // For some reason, isWhitespace() returns false with the following...
+ /*package*/ static final char NON_BREAKING_SPACE_CHARACTER = (char)160;
+
+ // Tags whose content must be stripped as well
+ static final String[] STRIP_TAGS =
+ new String[] {"title", "script", "style", "applet", "head"};
+ // The number of characters we peel off for testing against STRIP_TAGS; this should be the
+ // maximum size of the strings in STRIP_TAGS
+ static final int MAX_STRIP_TAG_LENGTH = 6;
+
+ static final Map<String, Character> ESCAPE_STRINGS;
+ static {
+ // HTML character entity references as defined in HTML 4
+ // see http://www.w3.org/TR/REC-html40/sgml/entities.html
+ ESCAPE_STRINGS = new HashMap<String, Character>(252);
+
+ ESCAPE_STRINGS.put(" ", '\u00A0');
+ ESCAPE_STRINGS.put("¡", '\u00A1');
+ ESCAPE_STRINGS.put("¢", '\u00A2');
+ ESCAPE_STRINGS.put("£", '\u00A3');
+ ESCAPE_STRINGS.put("¤", '\u00A4');
+ ESCAPE_STRINGS.put("¥", '\u00A5');
+ ESCAPE_STRINGS.put("¦", '\u00A6');
+ ESCAPE_STRINGS.put("§", '\u00A7');
+ ESCAPE_STRINGS.put("¨", '\u00A8');
+ ESCAPE_STRINGS.put("©", '\u00A9');
+ ESCAPE_STRINGS.put("ª", '\u00AA');
+ ESCAPE_STRINGS.put("«", '\u00AB');
+ ESCAPE_STRINGS.put("¬", '\u00AC');
+ ESCAPE_STRINGS.put("­", '\u00AD');
+ ESCAPE_STRINGS.put("®", '\u00AE');
+ ESCAPE_STRINGS.put("¯", '\u00AF');
+ ESCAPE_STRINGS.put("°", '\u00B0');
+ ESCAPE_STRINGS.put("±", '\u00B1');
+ ESCAPE_STRINGS.put("²", '\u00B2');
+ ESCAPE_STRINGS.put("³", '\u00B3');
+ ESCAPE_STRINGS.put("´", '\u00B4');
+ ESCAPE_STRINGS.put("µ", '\u00B5');
+ ESCAPE_STRINGS.put("¶", '\u00B6');
+ ESCAPE_STRINGS.put("·", '\u00B7');
+ ESCAPE_STRINGS.put("¸", '\u00B8');
+ ESCAPE_STRINGS.put("¹", '\u00B9');
+ ESCAPE_STRINGS.put("º", '\u00BA');
+ ESCAPE_STRINGS.put("»", '\u00BB');
+ ESCAPE_STRINGS.put("¼", '\u00BC');
+ ESCAPE_STRINGS.put("½", '\u00BD');
+ ESCAPE_STRINGS.put("¾", '\u00BE');
+ ESCAPE_STRINGS.put("¿", '\u00BF');
+ ESCAPE_STRINGS.put("À", '\u00C0');
+ ESCAPE_STRINGS.put("Á", '\u00C1');
+ ESCAPE_STRINGS.put("Â", '\u00C2');
+ ESCAPE_STRINGS.put("Ã", '\u00C3');
+ ESCAPE_STRINGS.put("Ä", '\u00C4');
+ ESCAPE_STRINGS.put("Å", '\u00C5');
+ ESCAPE_STRINGS.put("Æ", '\u00C6');
+ ESCAPE_STRINGS.put("Ç", '\u00C7');
+ ESCAPE_STRINGS.put("È", '\u00C8');
+ ESCAPE_STRINGS.put("É", '\u00C9');
+ ESCAPE_STRINGS.put("Ê", '\u00CA');
+ ESCAPE_STRINGS.put("Ë", '\u00CB');
+ ESCAPE_STRINGS.put("Ì", '\u00CC');
+ ESCAPE_STRINGS.put("Í", '\u00CD');
+ ESCAPE_STRINGS.put("Î", '\u00CE');
+ ESCAPE_STRINGS.put("Ï", '\u00CF');
+ ESCAPE_STRINGS.put("Ð", '\u00D0');
+ ESCAPE_STRINGS.put("Ñ", '\u00D1');
+ ESCAPE_STRINGS.put("Ò", '\u00D2');
+ ESCAPE_STRINGS.put("Ó", '\u00D3');
+ ESCAPE_STRINGS.put("Ô", '\u00D4');
+ ESCAPE_STRINGS.put("Õ", '\u00D5');
+ ESCAPE_STRINGS.put("Ö", '\u00D6');
+ ESCAPE_STRINGS.put("×", '\u00D7');
+ ESCAPE_STRINGS.put("Ø", '\u00D8');
+ ESCAPE_STRINGS.put("Ù", '\u00D9');
+ ESCAPE_STRINGS.put("Ú", '\u00DA');
+ ESCAPE_STRINGS.put("Û", '\u00DB');
+ ESCAPE_STRINGS.put("Ü", '\u00DC');
+ ESCAPE_STRINGS.put("Ý", '\u00DD');
+ ESCAPE_STRINGS.put("Þ", '\u00DE');
+ ESCAPE_STRINGS.put("ß", '\u00DF');
+ ESCAPE_STRINGS.put("à", '\u00E0');
+ ESCAPE_STRINGS.put("á", '\u00E1');
+ ESCAPE_STRINGS.put("â", '\u00E2');
+ ESCAPE_STRINGS.put("ã", '\u00E3');
+ ESCAPE_STRINGS.put("ä", '\u00E4');
+ ESCAPE_STRINGS.put("å", '\u00E5');
+ ESCAPE_STRINGS.put("æ", '\u00E6');
+ ESCAPE_STRINGS.put("ç", '\u00E7');
+ ESCAPE_STRINGS.put("è", '\u00E8');
+ ESCAPE_STRINGS.put("é", '\u00E9');
+ ESCAPE_STRINGS.put("ê", '\u00EA');
+ ESCAPE_STRINGS.put("ë", '\u00EB');
+ ESCAPE_STRINGS.put("ì", '\u00EC');
+ ESCAPE_STRINGS.put("í", '\u00ED');
+ ESCAPE_STRINGS.put("î", '\u00EE');
+ ESCAPE_STRINGS.put("ï", '\u00EF');
+ ESCAPE_STRINGS.put("ð", '\u00F0');
+ ESCAPE_STRINGS.put("ñ", '\u00F1');
+ ESCAPE_STRINGS.put("ò", '\u00F2');
+ ESCAPE_STRINGS.put("ó", '\u00F3');
+ ESCAPE_STRINGS.put("ô", '\u00F4');
+ ESCAPE_STRINGS.put("õ", '\u00F5');
+ ESCAPE_STRINGS.put("ö", '\u00F6');
+ ESCAPE_STRINGS.put("÷", '\u00F7');
+ ESCAPE_STRINGS.put("ø", '\u00F8');
+ ESCAPE_STRINGS.put("ù", '\u00F9');
+ ESCAPE_STRINGS.put("ú", '\u00FA');
+ ESCAPE_STRINGS.put("û", '\u00FB');
+ ESCAPE_STRINGS.put("ü", '\u00FC');
+ ESCAPE_STRINGS.put("ý", '\u00FD');
+ ESCAPE_STRINGS.put("þ", '\u00FE');
+ ESCAPE_STRINGS.put("ÿ", '\u00FF');
+ ESCAPE_STRINGS.put("&fnof", '\u0192');
+ ESCAPE_STRINGS.put("&Alpha", '\u0391');
+ ESCAPE_STRINGS.put("&Beta", '\u0392');
+ ESCAPE_STRINGS.put("&Gamma", '\u0393');
+ ESCAPE_STRINGS.put("&Delta", '\u0394');
+ ESCAPE_STRINGS.put("&Epsilon", '\u0395');
+ ESCAPE_STRINGS.put("&Zeta", '\u0396');
+ ESCAPE_STRINGS.put("&Eta", '\u0397');
+ ESCAPE_STRINGS.put("&Theta", '\u0398');
+ ESCAPE_STRINGS.put("&Iota", '\u0399');
+ ESCAPE_STRINGS.put("&Kappa", '\u039A');
+ ESCAPE_STRINGS.put("&Lambda", '\u039B');
+ ESCAPE_STRINGS.put("&Mu", '\u039C');
+ ESCAPE_STRINGS.put("&Nu", '\u039D');
+ ESCAPE_STRINGS.put("&Xi", '\u039E');
+ ESCAPE_STRINGS.put("&Omicron", '\u039F');
+ ESCAPE_STRINGS.put("&Pi", '\u03A0');
+ ESCAPE_STRINGS.put("&Rho", '\u03A1');
+ ESCAPE_STRINGS.put("&Sigma", '\u03A3');
+ ESCAPE_STRINGS.put("&Tau", '\u03A4');
+ ESCAPE_STRINGS.put("&Upsilon", '\u03A5');
+ ESCAPE_STRINGS.put("&Phi", '\u03A6');
+ ESCAPE_STRINGS.put("&Chi", '\u03A7');
+ ESCAPE_STRINGS.put("&Psi", '\u03A8');
+ ESCAPE_STRINGS.put("&Omega", '\u03A9');
+ ESCAPE_STRINGS.put("&alpha", '\u03B1');
+ ESCAPE_STRINGS.put("&beta", '\u03B2');
+ ESCAPE_STRINGS.put("&gamma", '\u03B3');
+ ESCAPE_STRINGS.put("&delta", '\u03B4');
+ ESCAPE_STRINGS.put("&epsilon", '\u03B5');
+ ESCAPE_STRINGS.put("&zeta", '\u03B6');
+ ESCAPE_STRINGS.put("&eta", '\u03B7');
+ ESCAPE_STRINGS.put("&theta", '\u03B8');
+ ESCAPE_STRINGS.put("&iota", '\u03B9');
+ ESCAPE_STRINGS.put("&kappa", '\u03BA');
+ ESCAPE_STRINGS.put("&lambda", '\u03BB');
+ ESCAPE_STRINGS.put("&mu", '\u03BC');
+ ESCAPE_STRINGS.put("&nu", '\u03BD');
+ ESCAPE_STRINGS.put("&xi", '\u03BE');
+ ESCAPE_STRINGS.put("&omicron", '\u03BF');
+ ESCAPE_STRINGS.put("&pi", '\u03C0');
+ ESCAPE_STRINGS.put("&rho", '\u03C1');
+ ESCAPE_STRINGS.put("&sigmaf", '\u03C2');
+ ESCAPE_STRINGS.put("&sigma", '\u03C3');
+ ESCAPE_STRINGS.put("&tau", '\u03C4');
+ ESCAPE_STRINGS.put("&upsilon", '\u03C5');
+ ESCAPE_STRINGS.put("&phi", '\u03C6');
+ ESCAPE_STRINGS.put("&chi", '\u03C7');
+ ESCAPE_STRINGS.put("&psi", '\u03C8');
+ ESCAPE_STRINGS.put("&omega", '\u03C9');
+ ESCAPE_STRINGS.put("&thetasym", '\u03D1');
+ ESCAPE_STRINGS.put("&upsih", '\u03D2');
+ ESCAPE_STRINGS.put("&piv", '\u03D6');
+ ESCAPE_STRINGS.put("&bull", '\u2022');
+ ESCAPE_STRINGS.put("&hellip", '\u2026');
+ ESCAPE_STRINGS.put("&prime", '\u2032');
+ ESCAPE_STRINGS.put("&Prime", '\u2033');
+ ESCAPE_STRINGS.put("&oline", '\u203E');
+ ESCAPE_STRINGS.put("&frasl", '\u2044');
+ ESCAPE_STRINGS.put("&weierp", '\u2118');
+ ESCAPE_STRINGS.put("&image", '\u2111');
+ ESCAPE_STRINGS.put("&real", '\u211C');
+ ESCAPE_STRINGS.put("&trade", '\u2122');
+ ESCAPE_STRINGS.put("&alefsym", '\u2135');
+ ESCAPE_STRINGS.put("&larr", '\u2190');
+ ESCAPE_STRINGS.put("&uarr", '\u2191');
+ ESCAPE_STRINGS.put("&rarr", '\u2192');
+ ESCAPE_STRINGS.put("&darr", '\u2193');
+ ESCAPE_STRINGS.put("&harr", '\u2194');
+ ESCAPE_STRINGS.put("&crarr", '\u21B5');
+ ESCAPE_STRINGS.put("&lArr", '\u21D0');
+ ESCAPE_STRINGS.put("&uArr", '\u21D1');
+ ESCAPE_STRINGS.put("&rArr", '\u21D2');
+ ESCAPE_STRINGS.put("&dArr", '\u21D3');
+ ESCAPE_STRINGS.put("&hArr", '\u21D4');
+ ESCAPE_STRINGS.put("&forall", '\u2200');
+ ESCAPE_STRINGS.put("&part", '\u2202');
+ ESCAPE_STRINGS.put("&exist", '\u2203');
+ ESCAPE_STRINGS.put("&empty", '\u2205');
+ ESCAPE_STRINGS.put("&nabla", '\u2207');
+ ESCAPE_STRINGS.put("&isin", '\u2208');
+ ESCAPE_STRINGS.put("¬in", '\u2209');
+ ESCAPE_STRINGS.put("&ni", '\u220B');
+ ESCAPE_STRINGS.put("&prod", '\u220F');
+ ESCAPE_STRINGS.put("&sum", '\u2211');
+ ESCAPE_STRINGS.put("&minus", '\u2212');
+ ESCAPE_STRINGS.put("&lowast", '\u2217');
+ ESCAPE_STRINGS.put("&radic", '\u221A');
+ ESCAPE_STRINGS.put("&prop", '\u221D');
+ ESCAPE_STRINGS.put("&infin", '\u221E');
+ ESCAPE_STRINGS.put("&ang", '\u2220');
+ ESCAPE_STRINGS.put("&and", '\u2227');
+ ESCAPE_STRINGS.put("&or", '\u2228');
+ ESCAPE_STRINGS.put("&cap", '\u2229');
+ ESCAPE_STRINGS.put("&cup", '\u222A');
+ ESCAPE_STRINGS.put("&int", '\u222B');
+ ESCAPE_STRINGS.put("&there4", '\u2234');
+ ESCAPE_STRINGS.put("&sim", '\u223C');
+ ESCAPE_STRINGS.put("&cong", '\u2245');
+ ESCAPE_STRINGS.put("&asymp", '\u2248');
+ ESCAPE_STRINGS.put("&ne", '\u2260');
+ ESCAPE_STRINGS.put("&equiv", '\u2261');
+ ESCAPE_STRINGS.put("&le", '\u2264');
+ ESCAPE_STRINGS.put("&ge", '\u2265');
+ ESCAPE_STRINGS.put("&sub", '\u2282');
+ ESCAPE_STRINGS.put("&sup", '\u2283');
+ ESCAPE_STRINGS.put("&nsub", '\u2284');
+ ESCAPE_STRINGS.put("&sube", '\u2286');
+ ESCAPE_STRINGS.put("&supe", '\u2287');
+ ESCAPE_STRINGS.put("&oplus", '\u2295');
+ ESCAPE_STRINGS.put("&otimes", '\u2297');
+ ESCAPE_STRINGS.put("&perp", '\u22A5');
+ ESCAPE_STRINGS.put("&sdot", '\u22C5');
+ ESCAPE_STRINGS.put("&lceil", '\u2308');
+ ESCAPE_STRINGS.put("&rceil", '\u2309');
+ ESCAPE_STRINGS.put("&lfloor", '\u230A');
+ ESCAPE_STRINGS.put("&rfloor", '\u230B');
+ ESCAPE_STRINGS.put("&lang", '\u2329');
+ ESCAPE_STRINGS.put("&rang", '\u232A');
+ ESCAPE_STRINGS.put("&loz", '\u25CA');
+ ESCAPE_STRINGS.put("&spades", '\u2660');
+ ESCAPE_STRINGS.put("&clubs", '\u2663');
+ ESCAPE_STRINGS.put("&hearts", '\u2665');
+ ESCAPE_STRINGS.put("&diams", '\u2666');
+ ESCAPE_STRINGS.put(""", '\u0022');
+ ESCAPE_STRINGS.put("&", '\u0026');
+ ESCAPE_STRINGS.put("<", '\u003C');
+ ESCAPE_STRINGS.put(">", '\u003E');
+ ESCAPE_STRINGS.put("&OElig", '\u0152');
+ ESCAPE_STRINGS.put("&oelig", '\u0153');
+ ESCAPE_STRINGS.put("&Scaron", '\u0160');
+ ESCAPE_STRINGS.put("&scaron", '\u0161');
+ ESCAPE_STRINGS.put("&Yuml", '\u0178');
+ ESCAPE_STRINGS.put("&circ", '\u02C6');
+ ESCAPE_STRINGS.put("&tilde", '\u02DC');
+ ESCAPE_STRINGS.put("&ensp", '\u2002');
+ ESCAPE_STRINGS.put("&emsp", '\u2003');
+ ESCAPE_STRINGS.put("&thinsp", '\u2009');
+ ESCAPE_STRINGS.put("&zwnj", '\u200C');
+ ESCAPE_STRINGS.put("&zwj", '\u200D');
+ ESCAPE_STRINGS.put("&lrm", '\u200E');
+ ESCAPE_STRINGS.put("&rlm", '\u200F');
+ ESCAPE_STRINGS.put("&ndash", '\u2013');
+ ESCAPE_STRINGS.put("&mdash", '\u2014');
+ ESCAPE_STRINGS.put("&lsquo", '\u2018');
+ ESCAPE_STRINGS.put("&rsquo", '\u2019');
+ ESCAPE_STRINGS.put("&sbquo", '\u201A');
+ ESCAPE_STRINGS.put("&ldquo", '\u201C');
+ ESCAPE_STRINGS.put("&rdquo", '\u201D');
+ ESCAPE_STRINGS.put("&bdquo", '\u201E');
+ ESCAPE_STRINGS.put("&dagger", '\u2020');
+ ESCAPE_STRINGS.put("&Dagger", '\u2021');
+ ESCAPE_STRINGS.put("&permil", '\u2030');
+ ESCAPE_STRINGS.put("&lsaquo", '\u2039');
+ ESCAPE_STRINGS.put("&rsaquo", '\u203A');
+ ESCAPE_STRINGS.put("&euro", '\u20AC');
+ }
+
+ /**
+ * Code to generate a short 'snippet' from either plain text or html text
+ *
+ * If the sync protocol can get plain text, that's great, but we'll still strip out extraneous
+ * whitespace. If it's HTML, we'll 1) strip out tags, 2) turn entities into the appropriate
+ * characters, and 3) strip out extraneous whitespace, all in one pass
+ *
+ * Why not use an existing class? The best answer is performance; yet another answer is
+ * correctness (e.g. Html.textFromHtml simply doesn't generate well-stripped text). But
+ * performance is key; we frequently sync text that is 10K or (much) longer, yet we really only
+ * care about a small amount of text for the snippet. So it's critically important that we just
+ * stop when we've gotten enough; existing methods that exist will go through the entire
+ * incoming string, at great (and useless, in this case) expense.
+ */
+
+ public static String makeSnippetFromHtmlText(String text) {
+ return makeSnippetFromText(text, true);
+ }
+
+ public static String makeSnippetFromPlainText(String text) {
+ return makeSnippetFromText(text, false);
+ }
+
+ /**
+ * Find the end of this tag; there are two alternatives: <tag .../> or <tag ...> ... </tag>
+ * @param htmlText some HTML text
+ * @param tag the HTML tag
+ * @param startPos the start position in the HTML text where the tag starts
+ * @return the position just before the end of the tag or -1 if not found
+ */
+ /*package*/ static int findTagEnd(String htmlText, String tag, int startPos) {
+ if (tag.endsWith(" ")) {
+ tag = tag.substring(0, tag.length() - 1);
+ }
+ int length = htmlText.length();
+ char prevChar = 0;
+ for (int i = startPos; i < length; i++) {
+ char c = htmlText.charAt(i);
+ if (c == '>') {
+ if (prevChar == '/') {
+ return i - 1;
+ }
+ break;
+ }
+ prevChar = c;
+ }
+ // We didn't find /> at the end of the tag so find </tag>
+ return htmlText.indexOf("/" + tag, startPos);
+ }
+
+ public static String makeSnippetFromText(String text, boolean stripHtml) {
+ // Handle null and empty string
+ if (TextUtils.isEmpty(text)) return "";
+
+ final int length = text.length();
+ // Use char[] instead of StringBuilder purely for performance; fewer method calls, etc.
+ char[] buffer = new char[MAX_SNIPPET_LENGTH];
+ // skipCount is an array of a single int; that int is set inside stripHtmlEntity and is
+ // used to determine how many characters can be "skipped" due to the transformation of the
+ // entity to a single character. When Java allows multiple return values, we can make this
+ // much cleaner :-)
+ int[] skipCount = new int[1];
+ int bufferCount = 0;
+ // Start with space as last character to avoid leading whitespace
+ char last = ' ';
+ // Indicates whether we're in the middle of an HTML tag
+ boolean inTag = false;
+
+ // Walk through the text until we're done with the input OR we've got a large enough snippet
+ for (int i = 0; i < length && bufferCount < MAX_SNIPPET_LENGTH; i++) {
+ char c = text.charAt(i);
+ if (stripHtml && !inTag && (c == '<')) {
+ // Find tags to strip; they will begin with <! or !- or </ or <letter
+ if (i < (length - 1)) {
+ char peek = text.charAt(i + 1);
+ if (peek == '!' || peek == '-' || peek == '/' || Character.isLetter(peek)) {
+ inTag = true;
+ // Strip content of title, script, style and applet tags
+ if (i < (length - (MAX_STRIP_TAG_LENGTH + 2))) {
+ String tag = text.substring(i + 1, i + MAX_STRIP_TAG_LENGTH + 1);
+ String tagLowerCase = tag.toLowerCase();
+ boolean stripContent = false;
+ for (String stripTag: STRIP_TAGS) {
+ if (tagLowerCase.startsWith(stripTag)) {
+ stripContent = true;
+ tag = tag.substring(0, stripTag.length());
+ break;
+ }
+ }
+ if (stripContent) {
+ // Look for the end of this tag
+ int endTagPosition = findTagEnd(text, tag, i);
+ if (endTagPosition < 0) {
+ break;
+ } else {
+ i = endTagPosition;
+ }
+ }
+ }
+ }
+ }
+ } else if (stripHtml && inTag && (c == '>')) {
+ // Terminate stripping here
+ inTag = false;
+ continue;
+ }
+
+ if (inTag) {
+ // We just skip by everything while we're in a tag
+ continue;
+ } else if (stripHtml && (c == '&')) {
+ // Handle a possible HTML entity here
+ // We always get back a character to use; we also get back a "skip count",
+ // indicating how many characters were eaten from the entity
+ c = stripHtmlEntity(text, i, skipCount);
+ i += skipCount[0];
+ }
+
+ if (Character.isWhitespace(c) || (c == NON_BREAKING_SPACE_CHARACTER)) {
+ // The idea is to find the content in the message, not the whitespace, so we'll
+ // turn any combination of contiguous whitespace into a single space
+ if (last == ' ') {
+ continue;
+ } else {
+ // Make every whitespace character a simple space
+ c = ' ';
+ }
+ } else if ((c == '-' || c == '=') && (last == c)) {
+ // Lots of messages (especially digests) have whole lines of --- or ===
+ // We'll get rid of those duplicates here
+ continue;
+ }
+
+ // After all that, maybe we've got a character for our snippet
+ buffer[bufferCount++] = c;
+ last = c;
+ }
+
+ // Lose trailing space and return our snippet
+ if ((bufferCount > 0) && (last == ' ')) {
+ bufferCount--;
+ }
+ return new String(buffer, 0, bufferCount);
+ }
+
+ static /*package*/ char stripHtmlEntity(String text, int pos, int[] skipCount) {
+ int length = text.length();
+ // Ugly, but we store our skip count in this array; we can't use a static here, because
+ // multiple threads might be calling in
+ skipCount[0] = 0;
+ // All entities are <= 8 characters long, so that's how far we'll look for one (+ & and ;)
+ int end = pos + 10;
+ String entity = null;
+ // Isolate the entity
+ for (int i = pos; (i < length) && (i < end); i++) {
+ if (text.charAt(i) == ';') {
+ entity = text.substring(pos, i);
+ break;
+ }
+ }
+ if (entity == null) {
+ // This wasn't really an HTML entity
+ return '&';
+ } else {
+ // Skip count is the length of the entity
+ Character mapping = ESCAPE_STRINGS.get(entity);
+ int entityLength = entity.length();
+ if (mapping != null) {
+ skipCount[0] = entityLength;
+ return mapping;
+ } else if ((entityLength > 2) && (entity.charAt(1) == '#')) {
+ // &#nn; means ascii nn (decimal) and &#xnn means ascii nn (hex)
+ char c = '?';
+ try {
+ int i;
+ if ((entity.charAt(2) == 'x') && (entityLength > 3)) {
+ i = Integer.parseInt(entity.substring(3), 16);
+ } else {
+ i = Integer.parseInt(entity.substring(2));
+ }
+ c = (char)i;
+ } catch (NumberFormatException e) {
+ // We'll just return the ? in this case
+ }
+ skipCount[0] = entityLength;
+ return c;
+ }
+ }
+ // Worst case, we return the original start character, ampersand
+ return '&';
+ }
+
+ /**
+ * Given a string of HTML text and a query containing any number of search terms, returns
+ * an HTML string in which those search terms are highlighted (intended for use in a WebView)
+ *
+ * @param text the HTML text to process
+ * @param query the search terms
+ * @return HTML text with the search terms highlighted
+ */
+ @VisibleForTesting
+ public static String highlightTermsInHtml(String text, String query) {
+ try {
+ return highlightTerms(text, query, true).toString();
+ } catch (IOException e) {
+ // Can't happen, but we must catch this
+ return text;
+ }
+ }
+
+ /**
+ * Given a string of plain text and a query containing any number of search terms, returns
+ * a CharSequence in which those search terms are highlighted (intended for use in a TextView)
+ *
+ * @param text the text to process
+ * @param query the search terms
+ * @return a CharSequence with the search terms highlighted
+ */
+ public static CharSequence highlightTermsInText(String text, String query) {
+ try {
+ return highlightTerms(text, query, false);
+ } catch (IOException e) {
+ // Can't happen, but we must catch this
+ return text;
+ }
+ }
+
+ static class SearchTerm {
+ final String mTerm;
+ final String mTermLowerCase;
+ final int mLength;
+ int mMatchLength = 0;
+ int mMatchStart = -1;
+
+ SearchTerm(String term, boolean html) {
+ mTerm = term;
+ mTermLowerCase = term.toLowerCase();
+ mLength = term.length();
+ }
+ }
+
+ /**
+ * Generate a version of the incoming text in which all search terms in a query are highlighted.
+ * If the input is HTML, we return a StringBuilder with additional markup as required
+ * If the input is text, we return a SpannableStringBuilder with additional spans as required
+ *
+ * @param text the text to be processed
+ * @param query the query, which can contain multiple terms separated by whitespace
+ * @param html whether or not the text to be processed is HTML
+ * @return highlighted text
+ *
+ * @throws IOException as Appendable requires this
+ */
+ public static CharSequence highlightTerms(String text, String query, boolean html)
+ throws IOException {
+ // Handle null and empty string
+ if (TextUtils.isEmpty(text)) return "";
+ final int length = text.length();
+
+ // Break up the query into search terms
+ ArrayList<SearchTerm> terms = new ArrayList<SearchTerm>();
+ if (query != null) {
+ StringTokenizer st = new StringTokenizer(query);
+ while (st.hasMoreTokens()) {
+ terms.add(new SearchTerm(st.nextToken(), html));
+ }
+ }
+
+ // Our appendable depends on whether we're building HTML text (for webview) or spannable
+ // text (for UI)
+ final Appendable sb = html ? new StringBuilder() : new SpannableStringBuilder();
+ // Indicates whether we're in the middle of an HTML tag
+ boolean inTag = false;
+ // The position of the last input character copied to output
+ int lastOut = -1;
+
+ // Walk through the text until we're done with the input
+ // Just copy any HTML tags directly into the output; search for terms in the remaining text
+ for (int i = 0; i < length; i++) {
+ char chr = text.charAt(i);
+ if (html) {
+ if (!inTag && (chr == '<')) {
+ // Find tags; they will begin with <! or !- or </ or <letter
+ if (i < (length - 1)) {
+ char peek = text.charAt(i + 1);
+ if (peek == '!' || peek == '-' || peek == '/' || Character.isLetter(peek)) {
+ inTag = true;
+ // Skip content of title, script, style and applet tags
+ if (i < (length - (MAX_STRIP_TAG_LENGTH + 2))) {
+ String tag = text.substring(i + 1, i + MAX_STRIP_TAG_LENGTH + 1);
+ String tagLowerCase = tag.toLowerCase();
+ boolean stripContent = false;
+ for (String stripTag: STRIP_TAGS) {
+ if (tagLowerCase.startsWith(stripTag)) {
+ stripContent = true;
+ tag = tag.substring(0, stripTag.length());
+ break;
+ }
+ }
+ if (stripContent) {
+ // Look for the end of this tag
+ int endTagPosition = findTagEnd(text, tag, i);
+ if (endTagPosition < 0) {
+ sb.append(text.substring(i));
+ break;
+ } else {
+ sb.append(text.substring(i, endTagPosition - 1));
+ i = endTagPosition - 1;
+ chr = text.charAt(i);
+ }
+ }
+ }
+ }
+ }
+ } else if (inTag && (chr == '>')) {
+ inTag = false;
+ }
+
+ if (inTag) {
+ sb.append(chr);
+ continue;
+ }
+ }
+
+ // After all that, we've got some "body" text
+ char chrLowerCase = Character.toLowerCase(chr);
+ // Whether or not the current character should be appended to the output; we inhibit
+ // this while any search terms match
+ boolean appendNow = true;
+ // Look through search terms for matches
+ for (SearchTerm t: terms) {
+ if (chrLowerCase == t.mTermLowerCase.charAt(t.mMatchLength)) {
+ if (t.mMatchLength++ == 0) {
+ // New match start
+ t.mMatchStart = i;
+ }
+ if (t.mMatchLength == t.mLength) {
+ String matchText = text.substring(t.mMatchStart, t.mMatchStart + t.mLength);
+ // Completed match; add highlight and reset term
+ if (t.mMatchStart <= lastOut) {
+ matchText = text.substring(lastOut + 1, i + 1);
+ }
+ /*else*/
+ if (matchText.length() == 0) {} else
+ if (html) {
+ sb.append("<span style=\"background-color: " + HIGHLIGHT_COLOR_STRING +
+ "\">");
+ sb.append(matchText);
+ sb.append("</span>");
+ } else {
+ SpannableString highlightSpan = new SpannableString(matchText);
+ highlightSpan.setSpan(new BackgroundColorSpan(HIGHLIGHT_COLOR_INT), 0,
+ highlightSpan.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
+ sb.append(highlightSpan);
+ }
+ lastOut = t.mMatchStart + t.mLength - 1;
+ t.mMatchLength = 0;
+ t.mMatchStart = -1;
+ }
+ appendNow = false;
+ } else {
+ if (t.mMatchStart >= 0) {
+ // We're no longer matching; check for other matches in progress
+ int leastOtherStart = -1;
+ for (SearchTerm ot: terms) {
+ // Save away the lowest match start for other search terms
+ if ((ot != t) && (ot.mMatchStart >= 0) && ((leastOtherStart < 0) ||
+ (ot.mMatchStart <= leastOtherStart))) {
+ leastOtherStart = ot.mMatchStart;
+ }
+ }
+ int matchEnd = t.mMatchStart + t.mMatchLength;
+ if (leastOtherStart < 0 || leastOtherStart > matchEnd) {
+ // Append the whole thing
+ if (t.mMatchStart > lastOut) {
+ sb.append(text.substring(t.mMatchStart, matchEnd));
+ lastOut = matchEnd;
+ }
+ } else if (leastOtherStart == t.mMatchStart) {
+ // Ok to append the current char
+ } else if (leastOtherStart < t.mMatchStart) {
+ // We're already covered by another search term, so don't append
+ appendNow = false;
+ } else if (t.mMatchStart > lastOut) {
+ // Append the piece of our term that's not already covered
+ sb.append(text.substring(t.mMatchStart, leastOtherStart));
+ lastOut = leastOtherStart;
+ }
+ }
+ // Reset this term
+ t.mMatchLength = 0;
+ t.mMatchStart = -1;
+ }
+ }
+
+ if (appendNow) {
+ sb.append(chr);
+ lastOut = i;
+ }
+ }
+
+ return (CharSequence)sb;
+ }
+
+ /**
+ * Determine whether two Strings (either of which might be null) are the same; this is true
+ * when both are null or both are Strings that are equal.
+ */
+ public static boolean stringOrNullEquals(String a, String b) {
+ if (a == null && b == null) return true;
+ if (a != null && b != null && a.equals(b)) return true;
+ return false;
+ }
+
+}
diff --git a/src/com/android/mail/MailIntentService.java b/src/com/android/mail/MailIntentService.java
index 065e5fd..2fa0717 100644
--- a/src/com/android/mail/MailIntentService.java
+++ b/src/com/android/mail/MailIntentService.java
@@ -66,7 +66,7 @@
final Account account = intent.getParcelableExtra(Utils.EXTRA_ACCOUNT);
final Folder folder = intent.getParcelableExtra(Utils.EXTRA_FOLDER);
- NotificationUtils.clearFolderNotification(this, account, folder);
+ NotificationUtils.clearFolderNotification(this, account, folder, true /* markSeen */);
} else if (ACTION_RESEND_NOTIFICATIONS.equals(action)) {
final Uri accountUri = intent.getParcelableExtra(Utils.EXTRA_ACCOUNT_URI);
final Uri folderUri = intent.getParcelableExtra(Utils.EXTRA_FOLDER_URI);
diff --git a/src/com/android/mail/MailLogService.java b/src/com/android/mail/MailLogService.java
index 4fd63bb..12b4a22 100644
--- a/src/com/android/mail/MailLogService.java
+++ b/src/com/android/mail/MailLogService.java
@@ -23,7 +23,6 @@
import android.app.Service;
import android.content.Intent;
import android.os.IBinder;
-import android.util.Log;
import android.util.Pair;
import java.io.FileDescriptor;
@@ -143,7 +142,7 @@
* @return true if this service is functioning at the current log level. False otherwise.
*/
public static boolean isLoggingLevelHighEnough() {
- return LogUtils.isLoggable(LOG_TAG, Log.DEBUG);
+ return LogUtils.isLoggable(LOG_TAG, LogUtils.DEBUG);
}
/**
diff --git a/src/com/android/mail/adapter/DrawerItem.java b/src/com/android/mail/adapter/DrawerItem.java
index aa5b08b..06a9365 100644
--- a/src/com/android/mail/adapter/DrawerItem.java
+++ b/src/com/android/mail/adapter/DrawerItem.java
@@ -31,7 +31,19 @@
import android.view.ViewGroup;
import android.widget.TextView;
-/** An account, a system folder, a recent folder, or a header (a resource string) */
+/**
+ * An element that is shown in the {@link com.android.mail.ui.FolderListFragment}. This class is
+ * only used for elements that are shown in the {@link com.android.mail.ui.DrawerFragment}.
+ * This class is an enumeration of a few element types: Account, a folder, a recent folder,
+ * or a header (a resource string). A {@link DrawerItem} can only be one type and can never
+ * switch types. Items are created using methods like
+ * {@link DrawerItem#ofAccount(com.android.mail.ui.ControllableActivity,
+ com.android.mail.providers.Account, int, boolean)},
+ * {@link DrawerItem#ofWaitView(com.android.mail.ui.ControllableActivity)}, etc.
+ *
+ * Once created, the item can create a view using {@link #getView(int, android.view.View,
+ android.view.ViewGroup)}.
+ */
public class DrawerItem {
private static final String LOG_TAG = LogTag.getLogTag();
// TODO(viki): Remove this: http://b/8478715
@@ -205,7 +217,7 @@
* @param activity the underlying activity
* @return a drawer item with an indeterminate progress indicator.
*/
- public static DrawerItem forWaitView(ControllableActivity activity) {
+ public static DrawerItem ofWaitView(ControllableActivity activity) {
return new DrawerItem(
VIEW_WAITING_FOR_SYNC, activity, null, INERT_HEADER, null, -1, false, -1);
}
@@ -214,20 +226,24 @@
return "[DrawerItem VIEW_WAITING_FOR_SYNC ]";
}
+ /**
+ * Returns a view for the given item. The method signature is identical to that required by a
+ * {@link android.widget.ListAdapter#getView(int, android.view.View, android.view.ViewGroup)}.
+ */
public View getView(int position, View convertView, ViewGroup parent) {
final View result;
switch (mType) {
case VIEW_FOLDER:
- result = getFolderView(position, convertView, parent);
+ result = getFolderView(convertView, parent);
break;
case VIEW_HEADER:
- result = getHeaderView(position, convertView, parent);
+ result = getHeaderView(convertView, parent);
break;
case VIEW_ACCOUNT:
- result = getAccountView(position, convertView, parent);
+ result = getAccountView(convertView, parent);
break;
case VIEW_WAITING_FOR_SYNC:
- result = getEmptyView(position, convertView, parent);
+ result = getEmptyView(convertView, parent);
break;
default:
LogUtils.wtf(LOG_TAG, "DrawerItem.getView(%d) for an invalid type!", mType);
@@ -246,8 +262,9 @@
}
/**
- * Returns whether this view is enabled or not.
- * @return
+ * Returns whether this view is enabled or not. An enabled view is one that accepts user taps
+ * and acts upon them.
+ * @return true if this view is enabled, false otherwise.
*/
public boolean isItemEnabled() {
return mIsEnabled;
@@ -277,9 +294,16 @@
/**
* Returns whether this view is highlighted or not.
*
- * @param currentFolder
- * @param currentType
- * @return
+ * @param currentFolder The current folder, according to the
+ * {@link com.android.mail.ui.FolderListFragment}
+ * @param currentType The type of the current folder. We want to only highlight a folder once.
+ * A folder might be in two places at once: in "All Folders", and in
+ * "Recent Folder". Valid types of selected folders are :
+ * {@link DrawerItem#FOLDER_INBOX}, {@link DrawerItem#FOLDER_RECENT} or
+ * {@link DrawerItem#FOLDER_OTHER}, or {@link DrawerItem#UNSET}.
+
+ * @return true if this DrawerItem results in a view that is highlighted (this DrawerItem is
+ * the current folder.
*/
public boolean isHighlighted(Folder currentFolder, int currentType){
switch (mType) {
@@ -306,12 +330,12 @@
/**
* Return a view for an account object.
- * @param position a zero indexed position in to the list.
+ *
* @param convertView a view, possibly null, to be recycled.
* @param parent the parent viewgroup to attach to.
* @return a view to display at this position.
*/
- private View getAccountView(int position, View convertView, ViewGroup parent) {
+ private View getAccountView(View convertView, ViewGroup parent) {
final AccountItemView accountItemView;
if (convertView != null) {
accountItemView = (AccountItemView) convertView;
@@ -326,12 +350,13 @@
}
/**
- * Returns a text divider between sections.
+ * Returns a text divider between divisions.
+ *
* @param convertView a previous view, perhaps null
* @param parent the parent of this view
* @return a text header at the given position.
*/
- private View getHeaderView(int position, View convertView, ViewGroup parent) {
+ private View getHeaderView(View convertView, ViewGroup parent) {
final TextView headerView;
if (convertView != null) {
headerView = (TextView) convertView;
@@ -346,12 +371,12 @@
/**
* Return a folder: either a parent folder or a normal (child or flat)
* folder.
- * @param position a zero indexed position into the top level list.
+ *
* @param convertView a view, possibly null, to be recycled.
* @param parent the parent hosting this view.
* @return a view showing a folder at the given position.
*/
- private View getFolderView(int position, View convertView, ViewGroup parent) {
+ private View getFolderView(View convertView, ViewGroup parent) {
final FolderItemView folderItemView;
if (convertView != null) {
folderItemView = (FolderItemView) convertView;
@@ -367,12 +392,12 @@
/**
* Return a view for the 'Waiting for sync' item with the indeterminate progress indicator.
- * @param position a zero indexed position into the top level list.
+ *
* @param convertView a view, possibly null, to be recycled.
* @param parent the parent hosting this view.
* @return a view for "Waiting for sync..." at given position.
*/
- private View getEmptyView(int position, View convertView, ViewGroup parent) {
+ private View getEmptyView(View convertView, ViewGroup parent) {
final ViewGroup emptyView;
if (convertView != null) {
emptyView = (ViewGroup) convertView;
diff --git a/src/com/android/mail/browse/ConversationCursor.java b/src/com/android/mail/browse/ConversationCursor.java
index b71efa5..a5fda5f 100644
--- a/src/com/android/mail/browse/ConversationCursor.java
+++ b/src/com/android/mail/browse/ConversationCursor.java
@@ -37,7 +37,6 @@
import android.os.SystemClock;
import android.support.v4.util.SparseArrayCompat;
import android.text.TextUtils;
-import android.util.Log;
import com.android.mail.content.ThreadSafeCursorWrapper;
import com.android.mail.providers.Conversation;
@@ -103,7 +102,7 @@
*/
private static final int URI_COLUMN_INDEX = UIProvider.CONVERSATION_URI_COLUMN;
- private static final boolean DEBUG_DUPLICATE_KEYS = false;
+ private static final boolean DEBUG_DUPLICATE_KEYS = true;
/** The resolver for the cursor instantiator's context */
private final ContentResolver mResolver;
@@ -280,19 +279,24 @@
@Override
public Void doInBackground(Void... param) {
- for (int i = mStartPos; i < getCount(); i++) {
- if (isCancelled()) {
- break;
- }
+ try {
+ Utils.traceBeginSection("backgroundCaching");
+ for (int i = mStartPos; i < getCount(); i++) {
+ if (isCancelled()) {
+ break;
+ }
- final UnderlyingRowData rowData = mRowCache.get(i);
- if (rowData.conversation == null) {
- // We are running in a background thread. Set the position to the row
- // we are interested in.
- if (moveToPosition(i)) {
- cacheConversation(new Conversation(UnderlyingCursorWrapper.this));
+ final UnderlyingRowData rowData = mRowCache.get(i);
+ if (rowData.conversation == null) {
+ // We are running in a background thread. Set the position to the row
+ // we are interested in.
+ if (moveToPosition(i)) {
+ cacheConversation(new Conversation(UnderlyingCursorWrapper.this));
+ }
}
}
+ } finally {
+ Utils.traceEndSection();
}
return null;
}
@@ -343,6 +347,7 @@
final UnderlyingRowData[] cache;
final int count;
int numCached = 0;
+ Utils.traceBeginSection("blockingCaching");
if (result != null && super.moveToFirst()) {
count = super.getCount();
cache = new UnderlyingRowData[count];
@@ -421,6 +426,8 @@
" %sms n=%s cached=%s CONV_PRECACHING=%s",
(end-start), count, numCached, ENABLE_CONVERSATION_PRECACHING);
+ Utils.traceEndSection();
+
// If we haven't cached all of the conversations, start a task to do that
if (ENABLE_CONVERSATION_PRECACHING && numCached < count) {
mCacheLoaderTask = new CacheLoaderTask(numCached);
@@ -561,7 +568,7 @@
final Cursor result = mResolver.query(uri, qProjection, null, null, null);
if (result == null) {
- Log.w(LOG_TAG, "doQuery returning null cursor, uri: " + uri);
+ LogUtils.w(LOG_TAG, "doQuery returning null cursor, uri: " + uri);
} else if (DEBUG) {
time = System.currentTimeMillis() - time;
LogUtils.i(LOG_TAG, "ConversationCursor query: %s, %dms, %d results",
diff --git a/src/com/android/mail/browse/ConversationItemView.java b/src/com/android/mail/browse/ConversationItemView.java
index a13fdad..346b645 100644
--- a/src/com/android/mail/browse/ConversationItemView.java
+++ b/src/com/android/mail/browse/ConversationItemView.java
@@ -1402,10 +1402,10 @@
@Override
public boolean performClick() {
- boolean handled = super.performClick();
- SwipeableListView list = getListView();
+ final boolean handled = super.performClick();
+ final SwipeableListView list = getListView();
if (list != null && list.getAdapter() != null) {
- int pos = list.findConversation(this, mHeader.conversation);
+ final int pos = list.findConversation(this, mHeader.conversation);
list.performItemClick(this, pos, mHeader.conversation.id);
}
return handled;
diff --git a/src/com/android/mail/browse/ConversationMessage.java b/src/com/android/mail/browse/ConversationMessage.java
new file mode 100644
index 0000000..e66494f
--- /dev/null
+++ b/src/com/android/mail/browse/ConversationMessage.java
@@ -0,0 +1,113 @@
+/*
+ * Copyright (C) 2013 Google Inc.
+ * Licensed to The Android Open Source Project.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.mail.browse;
+
+import android.database.Cursor;
+import android.net.Uri;
+
+import com.android.emailcommon.internet.MimeMessage;
+import com.android.emailcommon.mail.MessagingException;
+import com.android.mail.browse.MessageCursor.ConversationController;
+import com.android.mail.content.CursorCreator;
+import com.android.mail.providers.Attachment;
+import com.android.mail.providers.Conversation;
+import com.android.mail.providers.Message;
+import com.android.mail.ui.ConversationUpdater;
+import com.google.common.base.Objects;
+
+/**
+ * A message created as part of a conversation view. Sometimes, like during star/unstar, it's
+ * handy to have the owning {@link com.android.mail.providers.Conversation} for context.
+ *
+ * <p>This class must remain separate from the {@link MessageCursor} from whence it came,
+ * because cursors can be closed by their Loaders at any time. The
+ * {@link ConversationController} intermediate is used to obtain the currently opened cursor.
+ *
+ * <p>(N.B. This is a {@link android.os.Parcelable}, so try not to add non-transient fields here.
+ * Parcelable state belongs either in {@link com.android.mail.providers.Message} or
+ * {@link com.android.mail.ui.ConversationViewState.MessageViewState}. The
+ * assumption is that this class never needs the state of its extra context saved.)
+ */
+public final class ConversationMessage extends Message {
+
+ private transient ConversationController mController;
+
+ private ConversationMessage(Cursor cursor) {
+ super(cursor);
+ }
+
+ public ConversationMessage(MimeMessage mimeMessage) throws MessagingException {
+ super(mimeMessage);
+ }
+
+ public void setController(ConversationController controller) {
+ mController = controller;
+ }
+
+ public Conversation getConversation() {
+ return mController != null ? mController.getConversation() : null;
+ }
+
+ /**
+ * Returns a hash code based on this message's identity, contents and current state.
+ * This is a separate method from hashCode() to allow for an instance of this class to be
+ * a functional key in a hash-based data structure.
+ *
+ */
+ public int getStateHashCode() {
+ return Objects.hashCode(uri, read, starred, getAttachmentsStateHashCode());
+ }
+
+ private int getAttachmentsStateHashCode() {
+ int hash = 0;
+ for (Attachment a : getAttachments()) {
+ final Uri uri = a.getIdentifierUri();
+ hash += (uri != null ? uri.hashCode() : 0);
+ }
+ return hash;
+ }
+
+ public boolean isConversationStarred() {
+ final MessageCursor c = mController.getMessageCursor();
+ return c != null && c.isConversationStarred();
+ }
+
+ public void star(boolean newStarred) {
+ final ConversationUpdater listController = mController.getListController();
+ if (listController != null) {
+ listController.starMessage(this, newStarred);
+ }
+ }
+
+ /**
+ * Public object that knows how to construct Messages given Cursors.
+ */
+ public static final CursorCreator<ConversationMessage> FACTORY =
+ new CursorCreator<ConversationMessage>() {
+ @Override
+ public ConversationMessage createFromCursor(Cursor c) {
+ return new ConversationMessage(c);
+ }
+
+ @Override
+ public String toString() {
+ return "ConversationMessage CursorCreator";
+ }
+ };
+
+}
diff --git a/src/com/android/mail/browse/ConversationOverlayItem.java b/src/com/android/mail/browse/ConversationOverlayItem.java
index ee43363..cae878f 100644
--- a/src/com/android/mail/browse/ConversationOverlayItem.java
+++ b/src/com/android/mail/browse/ConversationOverlayItem.java
@@ -25,7 +25,6 @@
import android.widget.Adapter;
import android.widget.CursorAdapter;
-import com.android.mail.browse.MessageCursor.ConversationMessage;
import com.android.mail.ui.ConversationViewFragment;
import com.android.mail.utils.LogUtils;
diff --git a/src/com/android/mail/browse/ConversationViewAdapter.java b/src/com/android/mail/browse/ConversationViewAdapter.java
index 8f84c0c..89ccb09 100644
--- a/src/com/android/mail/browse/ConversationViewAdapter.java
+++ b/src/com/android/mail/browse/ConversationViewAdapter.java
@@ -30,7 +30,6 @@
import com.android.mail.FormattedDateBuilder;
import com.android.mail.R;
import com.android.mail.browse.ConversationViewHeader.ConversationViewHeaderCallbacks;
-import com.android.mail.browse.MessageCursor.ConversationMessage;
import com.android.mail.browse.MessageHeaderView.MessageHeaderViewCallbacks;
import com.android.mail.browse.SuperCollapsedBlock.OnClickListener;
import com.android.mail.providers.Address;
@@ -120,7 +119,10 @@
}
- public class MessageHeaderItem extends ConversationOverlayItem {
+ public static class MessageHeaderItem extends ConversationOverlayItem {
+
+ private final ConversationViewAdapter mAdapter;
+
private ConversationMessage mMessage;
// view state variables
@@ -133,7 +135,9 @@
public CharSequence timestampLong;
public CharSequence recipientSummaryText;
- MessageHeaderItem(ConversationMessage message, boolean expanded, boolean showImages) {
+ MessageHeaderItem(ConversationViewAdapter adapter, ConversationMessage message,
+ boolean expanded, boolean showImages) {
+ mAdapter = adapter;
mMessage = message;
mExpanded = expanded;
mShowImages = showImages;
@@ -154,10 +158,11 @@
public View createView(Context context, LayoutInflater inflater, ViewGroup parent) {
final MessageHeaderView v = (MessageHeaderView) inflater.inflate(
R.layout.conversation_message_header, parent, false);
- v.initialize(mDateBuilder, mAccountController, mAddressCache);
- v.setCallbacks(mMessageCallbacks);
- v.setContactInfoSource(mContactInfoSource);
- v.setVeiledMatcher(mMatcher);
+ v.initialize(mAdapter.mDateBuilder, mAdapter.mAccountController,
+ mAdapter.mAddressCache);
+ v.setCallbacks(mAdapter.mMessageCallbacks);
+ v.setContactInfoSource(mAdapter.mContactInfoSource);
+ v.setVeiledMatcher(mAdapter.mMatcher);
return v;
}
@@ -403,16 +408,16 @@
}
public int addMessageHeader(ConversationMessage msg, boolean expanded, boolean showImages) {
- return addItem(new MessageHeaderItem(msg, expanded, showImages));
+ return addItem(new MessageHeaderItem(this, msg, expanded, showImages));
}
public int addMessageFooter(MessageHeaderItem headerItem) {
return addItem(new MessageFooterItem(headerItem));
}
- public MessageHeaderItem newMessageHeaderItem(ConversationMessage message, boolean expanded,
- boolean showImages) {
- return new MessageHeaderItem(message, expanded, showImages);
+ public static MessageHeaderItem newMessageHeaderItem(ConversationViewAdapter adapter,
+ ConversationMessage message, boolean expanded, boolean showImages) {
+ return new MessageHeaderItem(adapter, message, expanded, showImages);
}
public MessageFooterItem newMessageFooterItem(MessageHeaderItem headerItem) {
diff --git a/src/com/android/mail/browse/EmlMessageLoader.java b/src/com/android/mail/browse/EmlMessageLoader.java
new file mode 100644
index 0000000..09e5b6a
--- /dev/null
+++ b/src/com/android/mail/browse/EmlMessageLoader.java
@@ -0,0 +1,180 @@
+/*
+ * Copyright (C) 2013 Google Inc.
+ * Licensed to The Android Open Source Project.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *and
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.mail.browse;
+
+import android.content.AsyncTaskLoader;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.net.Uri;
+
+import com.android.emailcommon.TempDirectory;
+import com.android.emailcommon.internet.MimeMessage;
+import com.android.emailcommon.mail.MessagingException;
+import com.android.mail.utils.LogTag;
+import com.android.mail.utils.LogUtils;
+
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStream;
+
+/**
+ * Loader that builds a ConversationMessage from an EML file Uri.
+ */
+public class EmlMessageLoader extends AsyncTaskLoader<ConversationMessage> {
+ private static final String LOG_TAG = LogTag.getLogTag();
+
+ private Uri mEmlFileUri;
+ private ConversationMessage mMessage;
+
+ public EmlMessageLoader(Context context, Uri emlFileUri) {
+ super(context);
+ mEmlFileUri = emlFileUri;
+ }
+
+ @Override
+ public ConversationMessage loadInBackground() {
+ final Context context = getContext();
+ TempDirectory.setTempDirectory(context);
+ final ContentResolver resolver = context.getContentResolver();
+ final InputStream stream;
+ try {
+ stream = resolver.openInputStream(mEmlFileUri);
+ } catch (FileNotFoundException e) {
+ LogUtils.e(LOG_TAG, e, "Could not find eml file at uri: %s", mEmlFileUri);
+ return null;
+ }
+
+ final MimeMessage mimeMessage;
+ final ConversationMessage convMessage;
+ try {
+ mimeMessage = new MimeMessage(stream);
+ convMessage = new ConversationMessage(mimeMessage);
+ } catch (IOException e) {
+ LogUtils.e(LOG_TAG, e, "Could not read eml file");
+ return null;
+ } catch (MessagingException e) {
+ LogUtils.e(LOG_TAG, e, "Error in parsing eml file");
+ return null;
+ } finally {
+ try {
+ stream.close();
+ } catch (IOException e) {
+ return null;
+ }
+ }
+
+ return convMessage;
+ }
+
+ /**
+ * Called when there is new data to deliver to the client. The
+ * super class will take care of delivering it; the implementation
+ * here just adds a little more logic.
+ */
+ @Override
+ public void deliverResult(ConversationMessage result) {
+ if (isReset()) {
+ // An async query came in while the loader is stopped. We
+ // don't need the result.
+ if (result != null) {
+ onReleaseResources(result);
+ }
+ }
+ ConversationMessage oldMessage = mMessage;
+ mMessage = result;
+
+ if (isStarted()) {
+ // If the Loader is currently started, we can immediately
+ // deliver its results.
+ super.deliverResult(result);
+ }
+
+ // At this point we can release the resources associated with
+ // 'oldMessage' if needed; now that the new result is delivered we
+ // know that it is no longer in use.
+ if (oldMessage != null && oldMessage != mMessage) {
+ onReleaseResources(oldMessage);
+ }
+ }
+
+ /**
+ * Handles a request to start the Loader.
+ */
+ @Override
+ protected void onStartLoading() {
+ if (mMessage != null) {
+ // If we currently have a result available, deliver it immediately.
+ deliverResult(mMessage);
+ }
+
+ if (takeContentChanged() || mMessage == null) {
+ // If the data has changed since the last time it was loaded
+ // or is not currently available, start a load.
+ forceLoad();
+ }
+ }
+
+ /**
+ * Handles a request to stop the Loader.
+ */
+ @Override protected void onStopLoading() {
+ // Attempt to cancel the current load task if possible.
+ cancelLoad();
+ }
+
+ /**
+ * Handles a request to cancel a load.
+ */
+ @Override
+ public void onCanceled(ConversationMessage result) {
+ super.onCanceled(result);
+
+ // At this point we can release the resources associated with
+ // the message, if needed.
+ if (result != null) {
+ onReleaseResources(result);
+ }
+ }
+
+ /**
+ * Handles a request to completely reset the Loader.
+ */
+ @Override
+ protected void onReset() {
+ super.onReset();
+
+ // Ensure the loader is stopped
+ onStopLoading();
+
+ // At this point we can release the resources associated with
+ // the message, if needed.
+ if (mMessage != null) {
+ onReleaseResources(mMessage);
+ mMessage = null;
+ }
+ }
+
+
+ /**
+ * Helper function to take care of releasing resources associated
+ * with an actively loaded data set.
+ */
+ protected void onReleaseResources(ConversationMessage message) {
+ // DO NOTHING
+ }
+}
diff --git a/src/com/android/mail/browse/EmlMessageViewFragment.java b/src/com/android/mail/browse/EmlMessageViewFragment.java
new file mode 100644
index 0000000..13a3e54
--- /dev/null
+++ b/src/com/android/mail/browse/EmlMessageViewFragment.java
@@ -0,0 +1,234 @@
+/*
+ * Copyright (C) 2013 Google Inc.
+ * Licensed to The Android Open Source Project.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.mail.browse;
+
+import android.app.Fragment;
+import android.app.LoaderManager;
+import android.content.Loader;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.Handler;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.webkit.WebView;
+
+import com.android.mail.providers.Account;
+import com.android.mail.providers.Address;
+import com.android.mail.ui.AbstractConversationWebViewClient;
+import com.android.mail.ui.ContactLoaderCallbacks;
+import com.android.mail.ui.SecureConversationViewController;
+import com.android.mail.ui.SecureConversationViewControllerCallbacks;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Sets;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Fragment that is used to view EML files. It is mostly stubs
+ * that calls {@link SecureConversationViewController} to do most
+ * of the rendering work.
+ */
+public class EmlMessageViewFragment extends Fragment
+ implements SecureConversationViewControllerCallbacks,
+ LoaderManager.LoaderCallbacks<ConversationMessage> {
+ private static final String ARG_EML_FILE_URI = "eml_file_uri";
+ private static final String BASE_URI = "x-thread://message/rfc822/";
+ private static final int MESSAGE_LOADER = 0;
+ private static final int CONTACT_LOADER = 1;
+
+ private final Handler mHandler = new Handler();
+
+ private EmlWebViewClient mWebViewClient;
+ private SecureConversationViewController mViewController;
+ private ContactLoaderCallbacks mContactLoaderCallbacks;
+
+ private Uri mEmlFileUri;
+
+ /**
+ * Cache of email address strings to parsed Address objects.
+ * <p>
+ * Remember to synchronize on the map when reading or writing to this cache, because some
+ * instances use it off the UI thread (e.g. from WebView).
+ */
+ protected final Map<String, Address> mAddressCache = Collections.synchronizedMap(
+ new HashMap<String, Address>());
+
+ private class EmlWebViewClient extends AbstractConversationWebViewClient {
+ public EmlWebViewClient(Account account) {
+ super(account);
+ }
+
+ @Override
+ public void onPageFinished(WebView view, String url) {
+ mViewController.dismissLoadingStatus();
+
+ final Set<String> emailAddresses = Sets.newHashSet();
+ final List<Address> cacheCopy;
+ synchronized (mAddressCache) {
+ cacheCopy = ImmutableList.copyOf(mAddressCache.values());
+ }
+ for (Address addr : cacheCopy) {
+ emailAddresses.add(addr.getAddress());
+ }
+ final ContactLoaderCallbacks callbacks = getContactInfoSource();
+ callbacks.setSenders(emailAddresses);
+ getLoaderManager().restartLoader(CONTACT_LOADER, Bundle.EMPTY, callbacks);
+ }
+ };
+
+ /**
+ * Creates a new instance of {@link EmlMessageViewFragment},
+ * initialized to display an eml file from the specified {@link Uri}.
+ */
+ public static EmlMessageViewFragment newInstance(Uri emlFileUri) {
+ EmlMessageViewFragment f = new EmlMessageViewFragment();
+ Bundle args = new Bundle();
+ args.putParcelable(ARG_EML_FILE_URI, emlFileUri);
+ f.setArguments(args);
+ return f;
+ }
+
+ /**
+ * Constructor needs to be public to handle orientation changes and activity
+ * lifecycle events.
+ */
+ public EmlMessageViewFragment() {
+ super();
+ }
+
+ @Override
+ public void onCreate(Bundle savedState) {
+ super.onCreate(savedState);
+
+ Bundle args = getArguments();
+ mEmlFileUri = args.getParcelable(ARG_EML_FILE_URI);
+
+ mWebViewClient = new EmlWebViewClient(null);
+ mViewController = new SecureConversationViewController(this);
+ }
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container,
+ Bundle savedInstanceState) {
+ return mViewController.onCreateView(inflater, container, savedInstanceState);
+ }
+
+ @Override
+ public void onActivityCreated(Bundle savedInstanceState) {
+ super.onActivityCreated(savedInstanceState);
+ mWebViewClient.setActivity(getActivity());
+ mViewController.onActivityCreated(savedInstanceState);
+ }
+
+ // Start SecureConversationViewControllerCallbacks
+
+ @Override
+ public Handler getHandler() {
+ return mHandler;
+ }
+
+ @Override
+ public AbstractConversationWebViewClient getWebViewClient() {
+ return mWebViewClient;
+ }
+
+ @Override
+ public Fragment getFragment() {
+ return this;
+ }
+
+ @Override
+ public void setupConversationHeaderView(ConversationViewHeader headerView) {
+ // DO NOTHING
+ }
+
+ @Override
+ public boolean isViewVisibleToUser() {
+ return true;
+ }
+
+ @Override
+ public ContactLoaderCallbacks getContactInfoSource() {
+ if (mContactLoaderCallbacks == null) {
+ mContactLoaderCallbacks = new ContactLoaderCallbacks(getActivity());
+ }
+ return mContactLoaderCallbacks;
+ }
+
+ @Override
+ public ConversationAccountController getConversationAccountController() {
+ return null;
+ }
+
+ @Override
+ public Map<String, Address> getAddressCache() {
+ return mAddressCache;
+ }
+
+ @Override
+ public void setupMessageHeaderVeiledMatcher(MessageHeaderView messageHeaderView) {
+ // DO NOTHING
+ }
+
+ @Override
+ public void startMessageLoader() {
+ getLoaderManager().initLoader(MESSAGE_LOADER, null, this);
+ }
+
+ @Override
+ public String getBaseUri() {
+ return BASE_URI;
+ }
+
+ @Override
+ public boolean isViewOnlyMode() {
+ return true;
+ }
+
+ // End SecureConversationViewControllerCallbacks
+
+ // Start LoaderCallbacks
+
+ @Override
+ public Loader<ConversationMessage> onCreateLoader(int id, Bundle args) {
+ switch (id) {
+ case MESSAGE_LOADER:
+ return new EmlMessageLoader(getActivity(), mEmlFileUri);
+ default:
+ return null;
+ }
+ }
+
+ @Override
+ public void onLoadFinished(Loader<ConversationMessage> loader, ConversationMessage data) {
+ mViewController.setSubject(data.subject);
+ mViewController.renderMessage(data);
+ }
+
+ @Override
+ public void onLoaderReset(Loader<ConversationMessage> loader) {
+ // Do nothing
+ }
+
+ // End LoaderCallbacks
+}
diff --git a/src/com/android/mail/browse/EmlViewerActivity.java b/src/com/android/mail/browse/EmlViewerActivity.java
new file mode 100644
index 0000000..2524b0f
--- /dev/null
+++ b/src/com/android/mail/browse/EmlViewerActivity.java
@@ -0,0 +1,79 @@
+/*
+ * Copyright (C) 2013 Google Inc.
+ * Licensed to The Android Open Source Project.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.mail.browse;
+
+import android.app.ActionBar;
+import android.app.Activity;
+import android.app.FragmentManager;
+import android.app.FragmentTransaction;
+import android.content.Intent;
+import android.os.Bundle;
+import android.view.MenuItem;
+
+import com.android.mail.R;
+import com.android.mail.utils.LogTag;
+import com.android.mail.utils.LogUtils;
+import com.android.mail.utils.MimeType;
+
+public class EmlViewerActivity extends Activity {
+ private static final String LOG_TAG = LogTag.getLogTag();
+
+ private static final String FRAGMENT_TAG = "eml_message_fragment";
+
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.eml_viewer_activity);
+
+ final ActionBar actionBar = getActionBar();
+ actionBar.setDisplayHomeAsUpEnabled(true);
+
+ final Intent intent = getIntent();
+ final String action = intent.getAction();
+ final String type = intent.getType();
+
+ if (Intent.ACTION_VIEW.equals(action) &&
+ MimeType.isEmlMimeType(type)) {
+ final FragmentManager manager = getFragmentManager();
+
+ if (manager.findFragmentByTag(FRAGMENT_TAG) == null) {
+ final FragmentTransaction transaction = manager.beginTransaction();
+ transaction.add(R.id.eml_root,
+ EmlMessageViewFragment.newInstance(intent.getData()), FRAGMENT_TAG);
+ transaction.commit();
+ }
+ } else {
+ LogUtils.wtf(LOG_TAG,
+ "Entered EmlViewerActivity with wrong intent action or type: %s, %s",
+ action, type);
+ finish(); // we should not be here. bail out. bail out.
+ }
+ }
+
+
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ switch (item.getItemId()) {
+ case android.R.id.home:
+ finish();
+ return true;
+ }
+
+ return super.onOptionsItemSelected(item);
+ }
+}
diff --git a/src/com/android/mail/browse/MessageAttachmentBar.java b/src/com/android/mail/browse/MessageAttachmentBar.java
index 3c6f4b4..e61fdcf 100644
--- a/src/com/android/mail/browse/MessageAttachmentBar.java
+++ b/src/com/android/mail/browse/MessageAttachmentBar.java
@@ -283,8 +283,17 @@
Intent intent = new Intent(Intent.ACTION_VIEW);
intent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION
| Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET);
+
+ final String contentType = mAttachment.getContentType();
Utils.setIntentDataAndTypeAndNormalize(
- intent, mAttachment.contentUri, mAttachment.getContentType());
+ intent, mAttachment.contentUri, contentType);
+
+ // For EML files, we want to open our dedicated
+ // viewer rather than let any activity open it.
+ if (MimeType.isEmlMimeType(contentType)) {
+ intent.setClass(getContext(), EmlViewerActivity.class);
+ }
+
try {
getContext().startActivity(intent);
} catch (ActivityNotFoundException e) {
diff --git a/src/com/android/mail/browse/MessageCursor.java b/src/com/android/mail/browse/MessageCursor.java
index 0858448..5a4146a 100644
--- a/src/com/android/mail/browse/MessageCursor.java
+++ b/src/com/android/mail/browse/MessageCursor.java
@@ -20,19 +20,15 @@
import android.database.Cursor;
import android.net.Uri;
import android.os.Bundle;
-import android.os.Parcelable;
-import com.android.mail.content.CursorCreator;
import com.android.mail.content.ObjectCursor;
import com.android.mail.providers.Account;
import com.android.mail.providers.Attachment;
import com.android.mail.providers.Conversation;
-import com.android.mail.providers.Message;
import com.android.mail.providers.UIProvider.CursorExtraKeys;
import com.android.mail.providers.UIProvider.CursorStatus;
import com.android.mail.ui.ConversationUpdater;
-import com.google.common.base.Objects;
import com.google.common.collect.Lists;
import java.util.List;
@@ -41,7 +37,7 @@
* MessageCursor contains the messages within a conversation; the public methods within should
* only be called by the UI thread, as cursor position isn't guaranteed to be maintained
*/
-public class MessageCursor extends ObjectCursor<MessageCursor.ConversationMessage> {
+public class MessageCursor extends ObjectCursor<ConversationMessage> {
/**
* The current controller that this cursor can use to reference the owning {@link Conversation},
* and a current {@link ConversationUpdater}. Since this cursor will survive a rotation, but
@@ -59,83 +55,6 @@
Account getAccount();
}
- /**
- * A message created as part of a conversation view. Sometimes, like during star/unstar, it's
- * handy to have the owning {@link Conversation} for context.
- *
- * <p>This class must remain separate from the {@link MessageCursor} from whence it came,
- * because cursors can be closed by their Loaders at any time. The
- * {@link ConversationController} intermediate is used to obtain the currently opened cursor.
- *
- * <p>(N.B. This is a {@link Parcelable}, so try not to add non-transient fields here.
- * Parcelable state belongs either in {@link Message} or {@link MessageViewState}. The
- * assumption is that this class never needs the state of its extra context saved.)
- */
- public static final class ConversationMessage extends Message {
-
- private transient ConversationController mController;
-
- private ConversationMessage(Cursor cursor) {
- super(cursor);
- }
-
- public void setController(ConversationController controller) {
- mController = controller;
- }
-
- public Conversation getConversation() {
- return mController.getConversation();
- }
-
- /**
- * Returns a hash code based on this message's identity, contents and current state.
- * This is a separate method from hashCode() to allow for an instance of this class to be
- * a functional key in a hash-based data structure.
- *
- */
- public int getStateHashCode() {
- return Objects.hashCode(uri, read, starred, getAttachmentsStateHashCode());
- }
-
- private int getAttachmentsStateHashCode() {
- int hash = 0;
- for (Attachment a : getAttachments()) {
- final Uri uri = a.getIdentifierUri();
- hash += (uri != null ? uri.hashCode() : 0);
- }
- return hash;
- }
-
- public boolean isConversationStarred() {
- final MessageCursor c = mController.getMessageCursor();
- return c != null && c.isConversationStarred();
- }
-
- public void star(boolean newStarred) {
- final ConversationUpdater listController = mController.getListController();
- if (listController != null) {
- listController.starMessage(this, newStarred);
- }
- }
-
- /**
- * Public object that knows how to construct Messages given Cursors.
- */
- public static final CursorCreator<ConversationMessage> FACTORY =
- new CursorCreator<ConversationMessage>() {
- @Override
- public ConversationMessage createFromCursor(Cursor c) {
- return new ConversationMessage(c);
- }
-
- @Override
- public String toString() {
- return "ConversationMessage CursorCreator";
- }
- };
-
- }
-
public MessageCursor(Cursor inner) {
super(inner, ConversationMessage.FACTORY);
}
diff --git a/src/com/android/mail/browse/MessageHeaderView.java b/src/com/android/mail/browse/MessageHeaderView.java
index d0a23e2..5036818 100644
--- a/src/com/android/mail/browse/MessageHeaderView.java
+++ b/src/com/android/mail/browse/MessageHeaderView.java
@@ -50,12 +50,12 @@
import com.android.mail.FormattedDateBuilder;
import com.android.mail.R;
import com.android.mail.browse.ConversationViewAdapter.MessageHeaderItem;
-import com.android.mail.browse.MessageCursor.ConversationMessage;
import com.android.mail.compose.ComposeActivity;
import com.android.mail.perf.Timer;
import com.android.mail.preferences.MailPrefs;
import com.android.mail.providers.Account;
import com.android.mail.providers.Address;
+import com.android.mail.providers.Conversation;
import com.android.mail.providers.Folder;
import com.android.mail.providers.Message;
import com.android.mail.providers.UIProvider;
@@ -218,6 +218,8 @@
private VeiledAddressMatcher mVeiledMatcher;
+ private boolean mIsViewOnlyMode = false;
+
public interface MessageHeaderViewCallbacks {
void setMessageSpacerHeight(MessageHeaderItem item, int newSpacerHeight);
@@ -231,6 +233,7 @@
void showExternalResources(String senderRawAddress);
boolean supportsMessageTransforms();
+
String getMessageTransforms(Message msg);
}
@@ -332,7 +335,7 @@
mLeftSpacer.setVisibility(View.VISIBLE);
mRightSpacer.setVisibility(View.VISIBLE);
} else {
- setBackgroundColor(android.R.color.white);
+ setBackgroundColor(getResources().getColor(android.R.color.white));
// scrolling layer does have padding so we don't need spacers
mLeftSpacer.setVisibility(View.GONE);
mRightSpacer.setVisibility(View.GONE);
@@ -369,7 +372,7 @@
}
private Account getAccount() {
- return mAccountController.getAccount();
+ return mAccountController != null ? mAccountController.getAccount() : null;
}
public void bind(MessageHeaderItem headerItem, boolean measureOnly) {
@@ -438,10 +441,14 @@
mStarView.setContentDescription(getResources().getString(
mStarView.isSelected() ? R.string.remove_star : R.string.add_star));
mStarShown = true;
- for (Folder folder : mMessage.getConversation().getRawFolders()) {
- if (folder.isTrash()) {
- mStarShown = false;
- break;
+
+ final Conversation conversation = mMessage.getConversation();
+ if (conversation != null) {
+ for (Folder folder : conversation.getRawFolders()) {
+ if (folder.isTrash()) {
+ mStarShown = false;
+ break;
+ }
}
}
@@ -601,7 +608,18 @@
private void updateChildVisibility() {
// Too bad this can't be done with an XML state list...
- if (isExpanded()) {
+ if (mIsViewOnlyMode) {
+ setMessageDetailsVisibility(VISIBLE);
+
+
+ setChildVisibility(GONE, mReplyButton, mReplyAllButton, mForwardButton,
+ mOverflowButton, mDraftIcon, mEditDraftButton, mStarView,
+ mAttachmentIcon, mUpperDateView);
+ setChildVisibility(VISIBLE, mPhotoView, mPhotoSpacerView,
+ mSenderEmailView);
+
+ setChildMarginRight(mTitleContainerView, 0);
+ } else if (isExpanded()) {
int normalVis, draftVis;
setMessageDetailsVisibility((mIsSnappy) ? GONE : VISIBLE);
@@ -670,8 +688,9 @@
return;
}
- final boolean defaultReplyAll = getAccount().settings.replyBehavior
- == UIProvider.DefaultReplyBehavior.REPLY_ALL;
+ final Account account = getAccount();
+ final boolean defaultReplyAll = (account != null) ? account.settings.replyBehavior
+ == UIProvider.DefaultReplyBehavior.REPLY_ALL : false;
setChildVisibility(defaultReplyAll ? GONE : VISIBLE, mReplyButton);
setChildVisibility(defaultReplyAll ? VISIBLE : GONE, mReplyAllButton);
}
@@ -703,7 +722,8 @@
final String address = email.getAddress();
// Check if the address here is a veiled address. If it is, we need to display an
// alternate layout
- final boolean isVeiledAddress = mVeiledMatcher.isVeiledAddress(address);
+ final boolean isVeiledAddress = mVeiledMatcher != null &&
+ mVeiledMatcher.isVeiledAddress(address);
final String addressShown;
if (isVeiledAddress) {
// Add the warning at the end of the name, and remove the address. The alternate
@@ -798,7 +818,7 @@
final Address email = getAddress(mAddressCache, rawAddrs[i]);
final String emailAddress = email.getAddress();
final String name;
- if (mMatcher.isVeiledAddress(emailAddress)) {
+ if (mMatcher != null && mMatcher.isVeiledAddress(emailAddress)) {
if (TextUtils.isEmpty(email.getName())) {
// Let's write something more readable.
name = mContext.getString(VeiledAddressMatcher.VEILED_SUMMARY_UNKNOWN);
@@ -984,6 +1004,16 @@
return handled;
}
+ /**
+ * Set to true if the user should not be able to perfrom message actions
+ * on the message such as reply/reply all/forward/star/etc.
+ *
+ * Default is false.
+ */
+ public void setViewOnlyMode(boolean isViewOnlyMode) {
+ mIsViewOnlyMode = isViewOnlyMode;
+ }
+
public void setExpandable(boolean expandable) {
mExpandable = expandable;
}
@@ -1195,7 +1225,11 @@
if (mMessageHeaderItem != null) {
mMessageHeaderItem.setShowImages(true);
}
- showImagePromptAlways(false);
+ if (mIsViewOnlyMode) {
+ hideShowImagePrompt();
+ } else {
+ showImagePromptAlways(false);
+ }
break;
case SHOW_IMAGE_PROMPT_ALWAYS:
mMessage.markAlwaysShowImages(getQueryHandler(), 0 /* token */, null /* cookie */);
@@ -1234,8 +1268,10 @@
}
if (!mCollapsedDetailsValid) {
if (mMessageHeaderItem.recipientSummaryText == null) {
+ final Account account = getAccount();
+ final String name = (account != null) ? account.name : "";
mMessageHeaderItem.recipientSummaryText = getRecipientSummaryText(getContext(),
- getAccount().name, mMyName, mTo, mCc, mBcc, mAddressCache, mVeiledMatcher);
+ name, mMyName, mTo, mCc, mBcc, mAddressCache, mVeiledMatcher);
}
((TextView) findViewById(R.id.recipients_summary))
.setText(mMessageHeaderItem.recipientSummaryText);
diff --git a/src/com/android/mail/browse/SelectedConversationsActionMenu.java b/src/com/android/mail/browse/SelectedConversationsActionMenu.java
index de3c968..604be37 100644
--- a/src/com/android/mail/browse/SelectedConversationsActionMenu.java
+++ b/src/com/android/mail/browse/SelectedConversationsActionMenu.java
@@ -19,6 +19,7 @@
import android.content.Context;
import android.net.Uri;
+import android.os.AsyncTask;
import android.view.ActionMode;
import android.view.Menu;
import android.view.MenuInflater;
@@ -43,14 +44,17 @@
import com.android.mail.ui.ConversationSetObserver;
import com.android.mail.ui.ConversationUpdater;
import com.android.mail.ui.DestructiveAction;
+import com.android.mail.ui.FolderOperation;
import com.android.mail.ui.FolderSelectionDialog;
import com.android.mail.ui.MailActionBarView;
import com.android.mail.utils.LogTag;
import com.android.mail.utils.LogUtils;
import com.android.mail.utils.Utils;
import com.google.common.annotations.VisibleForTesting;
+import com.google.common.collect.Lists;
import java.util.Collection;
+import java.util.List;
/**
* A component that displays a custom view for an {@code ActionBar}'s {@code
@@ -198,6 +202,25 @@
}
}
break;
+ case R.id.move_to_inbox:
+ new AsyncTask<Void, Void, Folder>() {
+ @Override
+ protected Folder doInBackground(final Void... params) {
+ // Get the "move to" inbox
+ return Utils.getFolder(mContext, mAccount.settings.moveToInbox,
+ true /* allowHidden */);
+ }
+
+ @Override
+ protected void onPostExecute(final Folder moveToInbox) {
+ final List<FolderOperation> ops = Lists.newArrayListWithCapacity(1);
+ // Add inbox
+ ops.add(new FolderOperation(moveToInbox, true));
+ mUpdater.assignFolder(ops, mSelectionSet.values(), true,
+ true /* showUndo */, false /* isMoveTo */);
+ }
+ }.execute((Void[]) null);
+ break;
case R.id.mark_important:
markConversationsImportant(true);
break;
@@ -382,13 +405,18 @@
// archive icon if the setting for that is true.
final MenuItem removeFolder = menu.findItem(R.id.remove_folder);
final MenuItem moveTo = menu.findItem(R.id.move_to);
+ final MenuItem moveToInbox = menu.findItem(R.id.move_to_inbox);
final boolean showRemoveFolder = mFolder != null && mFolder.isType(FolderType.DEFAULT)
&& mFolder.supportsCapability(FolderCapabilities.CAN_ACCEPT_MOVED_MESSAGES)
&& !mFolder.isProviderFolder();
final boolean showMoveTo = mFolder != null
&& mFolder.supportsCapability(FolderCapabilities.ALLOWS_REMOVE_CONVERSATION);
+ final boolean showMoveToInbox = mFolder != null
+ && mFolder.supportsCapability(FolderCapabilities.ALLOWS_MOVE_TO_INBOX);
removeFolder.setVisible(showRemoveFolder);
moveTo.setVisible(showMoveTo);
+ moveToInbox.setVisible(showMoveToInbox);
+
if (mFolder != null && showRemoveFolder) {
removeFolder.setTitle(mActivity.getActivityContext().getString(R.string.remove_folder,
mFolder.name));
diff --git a/src/com/android/mail/compose/ComposeActivity.java b/src/com/android/mail/compose/ComposeActivity.java
index 914c065..98fad35 100644
--- a/src/com/android/mail/compose/ComposeActivity.java
+++ b/src/com/android/mail/compose/ComposeActivity.java
@@ -211,6 +211,8 @@
private static final String MIME_TYPE_PHOTO = "image/*";
private static final String MIME_TYPE_VIDEO = "video/*";
+ private static final String KEY_INNER_SAVED_STATE = "compose_state";
+
/**
* A single thread for running tasks in the background.
*/
@@ -264,7 +266,7 @@
private RecipientTextWatcher mBccListener;
private Uri mRefMessageUri;
private boolean mShowQuotedText = false;
- private Bundle mSavedInstanceState;
+ private Bundle mInnerSavedState;
// Array of the outstanding send or save tasks. Access is synchronized
@@ -370,12 +372,13 @@
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.compose);
- mSavedInstanceState = savedInstanceState;
+ mInnerSavedState = (savedInstanceState != null) ?
+ savedInstanceState.getBundle(KEY_INNER_SAVED_STATE) : null;
checkValidAccounts();
}
private void finishCreate() {
- Bundle savedInstanceState = mSavedInstanceState;
+ final Bundle savedState = mInnerSavedState;
findViews();
Intent intent = getIntent();
Message message;
@@ -384,13 +387,13 @@
int action;
// Check for any of the possibly supplied accounts.;
Account account = null;
- if (hadSavedInstanceStateMessage(savedInstanceState)) {
- action = savedInstanceState.getInt(EXTRA_ACTION, COMPOSE);
- account = savedInstanceState.getParcelable(Utils.EXTRA_ACCOUNT);
- message = (Message) savedInstanceState.getParcelable(EXTRA_MESSAGE);
+ if (hadSavedInstanceStateMessage(savedState)) {
+ action = savedState.getInt(EXTRA_ACTION, COMPOSE);
+ account = savedState.getParcelable(Utils.EXTRA_ACCOUNT);
+ message = (Message) savedState.getParcelable(EXTRA_MESSAGE);
- previews = savedInstanceState.getParcelableArrayList(EXTRA_ATTACHMENT_PREVIEWS);
- mRefMessage = (Message) savedInstanceState.getParcelable(EXTRA_IN_REFERENCE_TO_MESSAGE);
+ previews = savedState.getParcelableArrayList(EXTRA_ATTACHMENT_PREVIEWS);
+ mRefMessage = (Message) savedState.getParcelable(EXTRA_IN_REFERENCE_TO_MESSAGE);
} else {
account = obtainAccount(intent);
action = intent.getIntExtra(EXTRA_ACTION, COMPOSE);
@@ -439,7 +442,7 @@
} else if (message != null && action != EDIT_DRAFT) {
initFromDraftMessage(message);
initQuotedTextFromRefMessage(mRefMessage, action);
- showCcBcc(savedInstanceState);
+ showCcBcc(savedState);
mShowQuotedText = message.appendRefMessageContent;
} else if (action == EDIT_DRAFT) {
initFromDraftMessage(message);
@@ -484,7 +487,7 @@
}
mComposeMode = action;
- finishSetup(action, intent, savedInstanceState);
+ finishSetup(action, intent, savedState);
}
private void checkValidAccounts() {
@@ -593,8 +596,8 @@
updateHideOrShowCcBcc();
updateHideOrShowQuotedText(mShowQuotedText);
- mRespondedInline = mSavedInstanceState != null ?
- mSavedInstanceState.getBoolean(EXTRA_RESPONDED_INLINE) : false;
+ mRespondedInline = mInnerSavedState != null ?
+ mInnerSavedState.getBoolean(EXTRA_RESPONDED_INLINE) : false;
if (mRespondedInline) {
mQuotedTextView.setVisibility(View.GONE);
}
@@ -686,12 +689,6 @@
}
@Override
- protected void onStop() {
- super.onStop();
- mSavedInstanceState = null;
- }
-
- @Override
protected final void onActivityResult(int request, int result, Intent data) {
if (request == RESULT_PICK_ATTACHMENT && result == RESULT_OK) {
addAttachmentAndUpdateView(data);
@@ -716,10 +713,10 @@
clearChangeListeners();
}
super.onRestoreInstanceState(savedInstanceState);
- if (savedInstanceState != null) {
- if (savedInstanceState.containsKey(EXTRA_FOCUS_SELECTION_START)) {
- int selectionStart = savedInstanceState.getInt(EXTRA_FOCUS_SELECTION_START);
- int selectionEnd = savedInstanceState.getInt(EXTRA_FOCUS_SELECTION_END);
+ if (mInnerSavedState != null) {
+ if (mInnerSavedState.containsKey(EXTRA_FOCUS_SELECTION_START)) {
+ int selectionStart = mInnerSavedState.getInt(EXTRA_FOCUS_SELECTION_START);
+ int selectionEnd = mInnerSavedState.getInt(EXTRA_FOCUS_SELECTION_END);
// There should be a focus and it should be an EditText since we
// only save these extras if these conditions are true.
EditText focusEditText = (EditText) getCurrentFocus();
@@ -737,6 +734,12 @@
@Override
public final void onSaveInstanceState(Bundle state) {
super.onSaveInstanceState(state);
+ final Bundle inner = new Bundle();
+ saveState(inner);
+ state.putBundle(KEY_INNER_SAVED_STATE, inner);
+ }
+
+ private void saveState(Bundle state) {
// We have no accounts so there is nothing to compose, and therefore, nothing to save.
if (mAccounts == null || mAccounts.length == 0) {
return;
@@ -1818,8 +1821,8 @@
*/
mSave = menu.findItem(R.id.save);
String action = getIntent() != null ? getIntent().getAction() : null;
- enableSave(mSavedInstanceState != null ?
- mSavedInstanceState.getBoolean(EXTRA_SAVE_ENABLED)
+ enableSave(mInnerSavedState != null ?
+ mInnerSavedState.getBoolean(EXTRA_SAVE_ENABLED)
: (Intent.ACTION_SEND.equals(action)
|| Intent.ACTION_SEND_MULTIPLE.equals(action)
|| Intent.ACTION_SENDTO.equals(action)
@@ -3175,7 +3178,7 @@
if (data != null && data.moveToFirst()) {
mRefMessage = new Message(data);
}
- finishSetup(mComposeMode, getIntent(), mSavedInstanceState);
+ finishSetup(mComposeMode, getIntent(), mInnerSavedState);
break;
case LOADER_ACCOUNT_CURSOR:
if (data != null && data.moveToFirst()) {
diff --git a/src/com/android/mail/preferences/AccountPreferences.java b/src/com/android/mail/preferences/AccountPreferences.java
index 4471942..0f205df 100644
--- a/src/com/android/mail/preferences/AccountPreferences.java
+++ b/src/com/android/mail/preferences/AccountPreferences.java
@@ -19,8 +19,6 @@
import android.content.Context;
-import com.android.mail.MailIntentService;
-
/**
* Preferences relevant to one specific account.
*/
@@ -91,6 +89,6 @@
public void setNotificationsEnabled(final boolean enabled) {
getEditor().putBoolean(PreferenceKeys.NOTIFICATIONS_ENABLED, enabled).apply();
- MailIntentService.broadcastBackupDataChanged(getContext());
+ notifyBackupPreferenceChanged();
}
}
diff --git a/src/com/android/mail/preferences/FolderPreferences.java b/src/com/android/mail/preferences/FolderPreferences.java
index e058613..33023e2 100644
--- a/src/com/android/mail/preferences/FolderPreferences.java
+++ b/src/com/android/mail/preferences/FolderPreferences.java
@@ -25,7 +25,6 @@
import android.net.Uri;
import android.provider.Settings;
-import com.android.mail.MailIntentService;
import com.android.mail.providers.Account;
import com.android.mail.providers.Folder;
import com.android.mail.providers.UIProvider.AccountCapabilities;
@@ -215,7 +214,7 @@
public void setNotificationsEnabled(final boolean enabled) {
getEditor().putBoolean(PreferenceKeys.NOTIFICATIONS_ENABLED, enabled).apply();
- MailIntentService.broadcastBackupDataChanged(getContext());
+ notifyBackupPreferenceChanged();
}
public String getNotificationRingtoneUri() {
@@ -225,7 +224,7 @@
public void setNotificationRingtoneUri(final String uri) {
getEditor().putString(PreferenceKeys.NOTIFICATION_RINGTONE, uri).apply();
- MailIntentService.broadcastBackupDataChanged(getContext());
+ notifyBackupPreferenceChanged();
}
public boolean isNotificationVibrateEnabled() {
@@ -234,7 +233,7 @@
public void setNotificationVibrateEnabled(final boolean enabled) {
getEditor().putBoolean(PreferenceKeys.NOTIFICATION_VIBRATE, enabled).apply();
- MailIntentService.broadcastBackupDataChanged(getContext());
+ notifyBackupPreferenceChanged();
}
public boolean isEveryMessageNotificationEnabled() {
@@ -244,7 +243,7 @@
public void setEveryMessageNotificationEnabled(final boolean enabled) {
getEditor().putBoolean(PreferenceKeys.NOTIFICATION_NOTIFY_EVERY_MESSAGE, enabled).apply();
- MailIntentService.broadcastBackupDataChanged(getContext());
+ notifyBackupPreferenceChanged();
}
public Set<String> getNotificationActions(final Account account) {
diff --git a/src/com/android/mail/preferences/MailPrefs.java b/src/com/android/mail/preferences/MailPrefs.java
index d8ec3cf..ea588bc 100644
--- a/src/com/android/mail/preferences/MailPrefs.java
+++ b/src/com/android/mail/preferences/MailPrefs.java
@@ -20,13 +20,15 @@
import android.content.Context;
import android.content.SharedPreferences;
-import com.android.mail.MailIntentService;
import com.android.mail.providers.Account;
import com.android.mail.providers.UIProvider;
import com.android.mail.widget.BaseWidgetProvider;
import com.google.common.collect.ImmutableSet;
+import java.util.Collections;
+import java.util.List;
import java.util.Set;
+import java.util.regex.Pattern;
/**
* A high-level API to store and retrieve unified mail preferences.
@@ -35,7 +37,7 @@
*/
public final class MailPrefs extends VersionedPrefs {
- public static final boolean SHOW_EXPERIMENTAL_PREFS = false;
+ public static final boolean SHOW_EXPERIMENTAL_PREFS = true;
private static final String PREFS_NAME = "UnifiedEmail";
@@ -71,12 +73,17 @@
private static final String
CONVERSATION_PHOTO_TEASER_SHOWN = "conversation-photo-teaser-shown-two";
+ public static final String DISPLAY_IMAGES = "display_images";
+ public static final String DISPLAY_IMAGES_PATTERNS = "display_sender_images_patterns_set";
+
public static final ImmutableSet<String> BACKUP_KEYS =
new ImmutableSet.Builder<String>()
.add(DEFAULT_REPLY_ALL)
.add(CONVERSATION_LIST_SWIPE)
.add(REMOVAL_ACTION)
.add(REMOVAL_ACTION_DIALOG_SHOWN)
+ .add(DISPLAY_IMAGES)
+ .add(DISPLAY_IMAGES_PATTERNS)
.build();
}
@@ -164,7 +171,7 @@
public void setDefaultReplyAll(final boolean replyAll) {
getEditor().putBoolean(PreferenceKeys.DEFAULT_REPLY_ALL, replyAll).apply();
- MailIntentService.broadcastBackupDataChanged(getContext());
+ notifyBackupPreferenceChanged();
}
/**
@@ -185,7 +192,7 @@
*/
public void setRemovalAction(final String removalAction) {
getEditor().putString(PreferenceKeys.REMOVAL_ACTION, removalAction).apply();
- MailIntentService.broadcastBackupDataChanged(getContext());
+ notifyBackupPreferenceChanged();
}
/**
@@ -198,7 +205,7 @@
public void setConversationListSwipeEnabled(final boolean enabled) {
getEditor().putBoolean(PreferenceKeys.CONVERSATION_LIST_SWIPE, enabled).apply();
- MailIntentService.broadcastBackupDataChanged(getContext());
+ notifyBackupPreferenceChanged();
}
/**
@@ -267,6 +274,93 @@
public void setRemovalActionDialogShown() {
getEditor().putBoolean(PreferenceKeys.REMOVAL_ACTION_DIALOG_SHOWN, true).apply();
- MailIntentService.broadcastBackupDataChanged(getContext());
+ notifyBackupPreferenceChanged();
+ }
+
+ void setSenderWhitelist(Set<String> addresses) {
+ getEditor().putStringSet(PreferenceKeys.DISPLAY_IMAGES, addresses).apply();
+ notifyBackupPreferenceChanged();
+ }
+ void setSenderWhitelistPatterns(Set<String> patterns) {
+ getEditor().putStringSet(PreferenceKeys.DISPLAY_IMAGES_PATTERNS, patterns).apply();
+ notifyBackupPreferenceChanged();
+ }
+
+ /**
+ * Returns whether or not an email address is in the whitelist of senders to show images for.
+ * This method reads the entire whitelist, so if you have multiple emails to check, you should
+ * probably call getSenderWhitelist() and check membership yourself.
+ *
+ * @param sender raw email address ("foo@bar.com")
+ * @return whether we should show pictures for this sender
+ */
+ public boolean getDisplayImagesFromSender(String sender) {
+ boolean displayImages = getSenderWhitelist().contains(sender);
+ if (!displayImages) {
+ final SharedPreferences sharedPreferences = getSharedPreferences();
+ // Check the saved email address patterns to determine if this pattern matches
+ final Set<String> defaultPatternSet = Collections.emptySet();
+ final Set<String> currentPatterns = sharedPreferences.getStringSet(
+ PreferenceKeys.DISPLAY_IMAGES_PATTERNS, defaultPatternSet);
+ for (String pattern : currentPatterns) {
+ displayImages = Pattern.compile(pattern).matcher(sender).matches();
+ if (displayImages) {
+ break;
+ }
+ }
+ }
+
+ return displayImages;
+ }
+
+
+ public void setDisplayImagesFromSender(String sender, List<Pattern> allowedPatterns) {
+ if (allowedPatterns != null) {
+ // Look at the list of patterns where we want to allow a particular class of
+ // email address
+ for (Pattern pattern : allowedPatterns) {
+ if (pattern.matcher(sender).matches()) {
+ // The specified email address matches one of the social network patterns.
+ // Save the pattern itself
+ final Set<String> currentPatterns = getSenderWhitelistPatterns();
+ final String patternRegex = pattern.pattern();
+ if (!currentPatterns.contains(patternRegex)) {
+ currentPatterns.add(patternRegex);
+ setSenderWhitelistPatterns(currentPatterns);
+ }
+ return;
+ }
+ }
+ }
+ final Set<String> whitelist = getSenderWhitelist();
+ if (!whitelist.contains(sender)) {
+ // Storing a JSONObject is slightly more nice in that maps are guaranteed to not have
+ // duplicate entries, but using a Set as intermediate representation guarantees this
+ // for us anyway. Also, using maps to represent sets forces you to pick values for
+ // them, and that's weird.
+ whitelist.add(sender);
+ setSenderWhitelist(whitelist);
+ }
+ }
+
+ private Set<String> getSenderWhitelist() {
+ final SharedPreferences sharedPreferences = getSharedPreferences();
+ final Set<String> defaultAddressSet = Collections.emptySet();
+ return sharedPreferences.getStringSet(PreferenceKeys.DISPLAY_IMAGES, defaultAddressSet);
+ }
+
+
+ private Set<String> getSenderWhitelistPatterns() {
+ final SharedPreferences sharedPreferences = getSharedPreferences();
+ final Set<String> defaultPatternSet = Collections.emptySet();
+ return sharedPreferences.getStringSet(PreferenceKeys.DISPLAY_IMAGES_PATTERNS,
+ defaultPatternSet);
+ }
+
+ public void clearSenderWhiteList() {
+ final SharedPreferences.Editor editor = getEditor();
+ editor.putStringSet(PreferenceKeys.DISPLAY_IMAGES, Collections.EMPTY_SET);
+ editor.putStringSet(PreferenceKeys.DISPLAY_IMAGES_PATTERNS, Collections.EMPTY_SET);
+ editor.apply();
}
}
diff --git a/src/com/android/mail/preferences/VersionedPrefs.java b/src/com/android/mail/preferences/VersionedPrefs.java
index b5dd9c9..bc50d98 100644
--- a/src/com/android/mail/preferences/VersionedPrefs.java
+++ b/src/com/android/mail/preferences/VersionedPrefs.java
@@ -20,10 +20,12 @@
import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.Lists;
+import android.app.backup.BackupManager;
import android.content.Context;
import android.content.SharedPreferences;
import android.content.SharedPreferences.Editor;
+import com.android.mail.MailIntentService;
import com.android.mail.utils.LogTag;
import com.android.mail.utils.LogUtils;
@@ -49,7 +51,7 @@
* The current version number for {@link SharedPreferences}. This is a constant for all
* applications based on UnifiedEmail.
*/
- protected static final int CURRENT_VERSION_NUMBER = 1;
+ protected static final int CURRENT_VERSION_NUMBER = 2;
protected static final String LOG_TAG = LogTag.getLogTag();
@@ -250,4 +252,11 @@
return false;
}
+
+ /**
+ * Notifies {@link BackupManager} that we have new data to back up.
+ */
+ protected void notifyBackupPreferenceChanged() {
+ MailIntentService.broadcastBackupDataChanged(getContext());
+ }
}
diff --git a/src/com/android/mail/providers/Account.java b/src/com/android/mail/providers/Account.java
index 391ffe0..256b30a 100644
--- a/src/com/android/mail/providers/Account.java
+++ b/src/com/android/mail/providers/Account.java
@@ -756,6 +756,7 @@
settings.conversationViewMode);
map.put(AccountColumns.SettingsColumns.VEILED_ADDRESS_PATTERN,
settings.veiledAddressPattern);
+ map.put(AccountColumns.SettingsColumns.MOVE_TO_INBOX, settings.moveToInbox);
return map;
}
diff --git a/src/com/android/mail/providers/Conversation.java b/src/com/android/mail/providers/Conversation.java
index d873ec6..6e9546b 100644
--- a/src/com/android/mail/providers/Conversation.java
+++ b/src/com/android/mail/providers/Conversation.java
@@ -172,8 +172,6 @@
private transient boolean viewed;
- private ArrayList<Folder> cachedDisplayableFolders;
-
private static String sSubjectAndSnippet;
// Constituents of convFlags below
@@ -518,33 +516,9 @@
}
public void setRawFolders(FolderList folders) {
- clearCachedFolders();
rawFolders = folders;
}
- private void clearCachedFolders() {
- cachedDisplayableFolders = null;
- }
-
- public ArrayList<Folder> getRawFoldersForDisplay(final Uri ignoreFolderUri,
- final int ignoreFolderType) {
- if (cachedDisplayableFolders == null) {
- cachedDisplayableFolders = new ArrayList<Folder>();
- for (Folder folder : rawFolders.folders) {
- // skip the ignoreFolder
- if (ignoreFolderUri != null && ignoreFolderUri.equals(folder.uri)) {
- continue;
- }
- // Skip the ignoreFolderType
- if (ignoreFolderType >= 0 && folder.isType(ignoreFolderType)) {
- continue;
- }
- cachedDisplayableFolders.add(folder);
- }
- }
- return cachedDisplayableFolders;
- }
-
@Override
public boolean equals(Object o) {
if (o instanceof Conversation) {
diff --git a/src/com/android/mail/providers/Message.java b/src/com/android/mail/providers/Message.java
index ff007eb..cc9ea3f 100644
--- a/src/com/android/mail/providers/Message.java
+++ b/src/com/android/mail/providers/Message.java
@@ -29,10 +29,16 @@
import android.text.util.Rfc822Token;
import android.text.util.Rfc822Tokenizer;
+import com.android.emailcommon.internet.MimeMessage;
+import com.android.emailcommon.internet.MimeUtility;
+import com.android.emailcommon.mail.MessagingException;
+import com.android.emailcommon.mail.Part;
+import com.android.emailcommon.utility.ConversionUtilities;
import com.android.mail.providers.UIProvider.MessageColumns;
import com.android.mail.utils.Utils;
import com.google.common.base.Objects;
+import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.regex.Pattern;
@@ -354,6 +360,44 @@
}
}
+ public Message(MimeMessage mimeMessage) throws MessagingException {
+ // Set message header values.
+ setFrom(com.android.emailcommon.mail.Address.pack(mimeMessage.getFrom()));
+ setTo(com.android.emailcommon.mail.Address.pack(mimeMessage.getRecipients(
+ com.android.emailcommon.mail.Message.RecipientType.TO)));
+ setCc(com.android.emailcommon.mail.Address.pack(mimeMessage.getRecipients(
+ com.android.emailcommon.mail.Message.RecipientType.CC)));
+ setBcc(com.android.emailcommon.mail.Address.pack(mimeMessage.getRecipients(
+ com.android.emailcommon.mail.Message.RecipientType.BCC)));
+ setReplyTo(com.android.emailcommon.mail.Address.pack(mimeMessage.getReplyTo()));
+ subject = mimeMessage.getSubject();
+ dateReceivedMs = mimeMessage.getSentDate().getTime();
+
+ // for now, always set defaults
+ alwaysShowImages = false;
+ viaDomain = null;
+ draftType = UIProvider.DraftType.NOT_A_DRAFT;
+ isSending = false;
+ starred = false;
+ spamWarningString = null;
+ messageFlags = 0;
+ hasAttachments = false;
+
+ // body values (snippet/bodyText/bodyHtml)
+ // Now process body parts & attachments
+ ArrayList<Part> viewables = new ArrayList<Part>();
+ ArrayList<Part> attachments = new ArrayList<Part>();
+ MimeUtility.collectParts(mimeMessage, viewables, attachments);
+
+ ConversionUtilities.BodyFieldData data =
+ ConversionUtilities.parseBodyFields(viewables);
+
+ snippet = data.snippet;
+ bodyText = data.textContent;
+ bodyHtml = data.htmlContent;
+ // TODO - attachments?
+ }
+
public boolean isFlaggedReplied() {
return (messageFlags & UIProvider.MessageFlags.REPLIED) ==
UIProvider.MessageFlags.REPLIED;
diff --git a/src/com/android/mail/providers/Settings.java b/src/com/android/mail/providers/Settings.java
index 9a8fddd..d07a60e 100644
--- a/src/com/android/mail/providers/Settings.java
+++ b/src/com/android/mail/providers/Settings.java
@@ -89,6 +89,12 @@
public final Uri setupIntentUri;
public final String veiledAddressPattern;
+ /**
+ * The {@link Uri} to use when moving a conversation to the inbox. May
+ * differ from {@link #defaultInbox}.
+ */
+ public final Uri moveToInbox;
+
/** Cached value of hashCode */
private int mHashCode;
@@ -114,6 +120,7 @@
setupIntentUri = Uri.EMPTY;
conversationViewMode = UIProvider.ConversationViewMode.UNDEFINED;
veiledAddressPattern = null;
+ moveToInbox = Uri.EMPTY;
}
public Settings(Parcel inParcel) {
@@ -135,6 +142,7 @@
setupIntentUri = Utils.getValidUri(inParcel.readString());
conversationViewMode = inParcel.readInt();
veiledAddressPattern = inParcel.readString();
+ moveToInbox = Utils.getValidUri(inParcel.readString());
}
public Settings(Cursor cursor) {
@@ -164,6 +172,8 @@
cursor.getInt(cursor.getColumnIndex(SettingsColumns.CONVERSATION_VIEW_MODE));
veiledAddressPattern =
cursor.getString(cursor.getColumnIndex(SettingsColumns.VEILED_ADDRESS_PATTERN));
+ moveToInbox = Utils.getValidUri(
+ cursor.getString(cursor.getColumnIndex(SettingsColumns.MOVE_TO_INBOX)));
}
private Settings(JSONObject json) {
@@ -190,6 +200,7 @@
conversationViewMode = json.optInt(SettingsColumns.CONVERSATION_VIEW_MODE,
UIProvider.ConversationViewMode.UNDEFINED);
veiledAddressPattern = json.optString(SettingsColumns.VEILED_ADDRESS_PATTERN, null);
+ moveToInbox = Utils.getValidUri(json.optString(SettingsColumns.MOVE_TO_INBOX));
}
/**
@@ -232,6 +243,8 @@
json.put(SettingsColumns.SETUP_INTENT_URI, setupIntentUri);
json.put(SettingsColumns.CONVERSATION_VIEW_MODE, conversationViewMode);
json.put(SettingsColumns.VEILED_ADDRESS_PATTERN, veiledAddressPattern);
+ json.put(SettingsColumns.MOVE_TO_INBOX,
+ getNonNull(moveToInbox, sDefault.moveToInbox));
} catch (JSONException e) {
LogUtils.wtf(LOG_TAG, e, "Could not serialize settings");
}
@@ -298,6 +311,7 @@
dest.writeString(((Uri) getNonNull(setupIntentUri, sDefault.setupIntentUri)).toString());
dest.writeInt(conversationViewMode);
dest.writeString(veiledAddressPattern);
+ dest.writeString(((Uri) getNonNull(moveToInbox, sDefault.moveToInbox)).toString());
}
/**
@@ -403,7 +417,8 @@
&& priorityArrowsEnabled == that.priorityArrowsEnabled
&& setupIntentUri == that.setupIntentUri
&& conversationViewMode == that.conversationViewMode
- && TextUtils.equals(veiledAddressPattern, that.veiledAddressPattern));
+ && TextUtils.equals(veiledAddressPattern, that.veiledAddressPattern))
+ && Objects.equal(moveToInbox, that.moveToInbox);
}
@Override
@@ -423,6 +438,6 @@
snapHeaders, replyBehavior, convListIcon, confirmDelete, confirmArchive,
confirmSend, defaultInbox, forceReplyFromDefault, maxAttachmentSize, swipe,
priorityArrowsEnabled, setupIntentUri, conversationViewMode,
- veiledAddressPattern);
+ veiledAddressPattern, moveToInbox);
}
}
diff --git a/src/com/android/mail/providers/UIProvider.java b/src/com/android/mail/providers/UIProvider.java
index a41f8a0..7fd379d 100644
--- a/src/com/android/mail/providers/UIProvider.java
+++ b/src/com/android/mail/providers/UIProvider.java
@@ -170,6 +170,7 @@
.put(AccountColumns.SettingsColumns.VEILED_ADDRESS_PATTERN, String.class)
.put(AccountColumns.UPDATE_SETTINGS_URI, String.class)
.put(AccountColumns.ENABLE_MESSAGE_TRANSFORMS, Integer.class)
+ .put(AccountColumns.SettingsColumns.MOVE_TO_INBOX, String.class)
.build();
public static final Map<String, Class<?>> ACCOUNTS_COLUMNS =
@@ -557,6 +558,11 @@
* constants from {@link ConversationViewMode}
*/
public static final String CONVERSATION_VIEW_MODE = "conversation_view_mode";
+ /**
+ * String containing the URI for the inbox conversations should be moved to for this
+ * account.
+ */
+ public static final String MOVE_TO_INBOX = "move_to_inbox";
}
}
@@ -577,11 +583,6 @@
*/
public static final String QUERY = "query";
- /**
- * If specified, the query results will be limited to this folder.
- */
- public static final String FOLDER = "folder";
-
private SearchQueryParameters() {}
}
@@ -681,9 +682,9 @@
public static final int STARRED = 1 << 7;
/** Any other system label that we do not have a specific name for. */
public static final int OTHER_PROVIDER_FOLDER = 1 << 8;
- /** All mail folder **/
+ /** All mail folder */
public static final int ALL_MAIL = 1 << 9;
- /** Gmail's inbox sections **/
+ /** Gmail's inbox sections */
public static final int INBOX_SECTION = 1 << 10;
}
@@ -758,6 +759,12 @@
* {@link com.android.mail.ui.MultiFoldersSelectionDialog}).
*/
public static final int MULTI_MOVE = 0x8000;
+
+ /**
+ * This flag indicates that a conversation may be moved from this folder into the account's
+ * inbox.
+ */
+ public static final int ALLOWS_MOVE_TO_INBOX = 0x10000;
}
public static final class FolderColumns {
@@ -901,6 +908,13 @@
ConversationColumns.REMOTE
};
+ /**
+ * This integer corresponds to the number of rows of queries that specify the
+ * {@link UIProvider#CONVERSATION_PROJECTION} projection will fit in a single
+ * {@link android.database.CursorWindow}
+ */
+ public static final int CONVERSATION_PROJECTION_QUERY_CURSOR_WINDOW_LIMT = 2000;
+
// These column indexes only work when the caller uses the
// default CONVERSATION_PROJECTION defined above.
public static final int CONVERSATION_ID_COLUMN = 0;
@@ -1943,6 +1957,11 @@
*/
public static final String FORCE_UI_NOTIFICATIONS_QUERY_PARAMETER = "forceUiNotifications";
+ /**
+ * Parameter used to allow returning hidden folders.
+ */
+ public static final String ALLOW_HIDDEN_FOLDERS_QUERY_PARAM = "allowHiddenFolders";
+
public static final String AUTO_ADVANCE_MODE_OLDER = "older";
public static final String AUTO_ADVANCE_MODE_NEWER = "newer";
public static final String AUTO_ADVANCE_MODE_LIST = "list";
diff --git a/src/com/android/mail/ui/AbstractActivityController.java b/src/com/android/mail/ui/AbstractActivityController.java
index 7bc6797..d91a450 100644
--- a/src/com/android/mail/ui/AbstractActivityController.java
+++ b/src/com/android/mail/ui/AbstractActivityController.java
@@ -66,8 +66,8 @@
import com.android.mail.browse.ConversationCursor;
import com.android.mail.browse.ConversationCursor.ConversationOperation;
import com.android.mail.browse.ConversationItemViewModel;
+import com.android.mail.browse.ConversationMessage;
import com.android.mail.browse.ConversationPagerController;
-import com.android.mail.browse.MessageCursor.ConversationMessage;
import com.android.mail.browse.SelectedConversationsActionMenu;
import com.android.mail.browse.SyncErrorDialogFragment;
import com.android.mail.compose.ComposeActivity;
@@ -268,8 +268,9 @@
protected ActionableToastBar mToastBar;
protected ConversationPagerController mPagerController;
- // this is split out from the general loader dispatcher because its loader doesn't return a
+ // This is split out from the general loader dispatcher because its loader doesn't return a
// basic Cursor
+ /** Handles loader callbacks to create a convesation cursor. */
private final ConversationListLoaderCallbacks mListCursorCallbacks =
new ConversationListLoaderCallbacks();
@@ -286,17 +287,101 @@
private final VeiledAddressMatcher mVeiledMatcher;
protected static final String LOG_TAG = LogTag.getLogTag();
- /** Constants used to differentiate between the types of loaders. */
+
+ // Loader constants: Accounts
+ /**
+ * The list of accounts. This loader is started early in the application life-cycle since
+ * the list of accounts is central to all other data the application needs: unread counts for
+ * folders, critical UI settings like show/hide checkboxes, ...
+ * The loader is started when the application is created: both in
+ * {@link #onCreate(Bundle)} and in {@link #onActivityResult(int, int, Intent)}. It is never
+ * destroyed since the cursor is needed through the life of the application. When the list of
+ * accounts changes, we notify {@link #mAllAccountObservers}.
+ */
private static final int LOADER_ACCOUNT_CURSOR = 0;
- private static final int LOADER_FOLDER_CURSOR = 2;
- private static final int LOADER_RECENT_FOLDERS = 3;
- private static final int LOADER_CONVERSATION_LIST = 4;
- private static final int LOADER_ACCOUNT_INBOX = 5;
- private static final int LOADER_SEARCH = 6;
+
+ /**
+ * The current account. This loader is started when we have an account. The mail application
+ * <b>needs</b> a valid account to function. As soon as we set {@link #mAccount},
+ * we start a loader to observe for changes on the current account.
+ * The loader is always restarted when an account is set in {@link #setAccount(Account)}.
+ * When the current account object changes, we notify {@link #mAccountObservers}.
+ * A possible performance improvement would be to listen purely on
+ * {@link #LOADER_ACCOUNT_CURSOR}. The current account is guaranteed to be in the list,
+ * and would avoid two updates when a single setting on the current account changes.
+ */
private static final int LOADER_ACCOUNT_UPDATE_CURSOR = 7;
- /** Loader for showing the initial folder/conversation at app start. */
+
+ // Loader constants: Folders
+ /** The current folder. This loader watches for updates to the current folder in a manner
+ * analogous to the {@link #LOADER_ACCOUNT_UPDATE_CURSOR}. Updates to the current folder
+ * might be due to server-side changes (unread count), or local changes (sync window or sync
+ * status change).
+ * The change of current folder calls {@link #updateFolder(Folder)}.
+ * This is responsible for restarting a loader using the URI of the provided folder. When the
+ * loader returns, the current folder is updated and consumers, if any, are notified.
+ * When the current folder changes, we notify {@link #mFolderObservable}
+ */
+ private static final int LOADER_FOLDER_CURSOR = 2;
+ /**
+ * The list of recent folders. Recent folders are shown in the DrawerFragment. The recent
+ * folders are tied to the current account being viewed. When the account is changed,
+ * we restart this loader to retrieve the recent accounts. Recents are pre-populated for
+ * phones historically, when they were displayed in the spinner. On the tablet,
+ * they showed in the {@link FolderListFragment} and were not-populated. The code to
+ * pre-populate the recents is somewhat convoluted: when the loader returns a short list of
+ * recent folders, it issues an update on the Recent Folder URI. The underlying provider then
+ * does the appropriate thing to populate recent folders, and notify of a change on the cursor.
+ * Recent folders are needed for the life of the current account.
+ * When the recent folders change, we notify {@link #mRecentFolderObservers}.
+ */
+ private static final int LOADER_RECENT_FOLDERS = 3;
+ /**
+ * The primary inbox for the current account. The mechanism to load the default inbox for the
+ * current account is (sadly) different from loading other folders. The method
+ * {@link #loadAccountInbox()} is called, and it restarts this loader. When the loader returns
+ * a valid cursor, we create a folder, call {@link #onFolderChanged{Folder)} eventually
+ * calling {@link #updateFolder(Folder)} which starts a loader {@link #LOADER_FOLDER_CURSOR}
+ * over the current folder.
+ * When we have a valid cursor, we destroy this loader, This convoluted flow is historical.
+ */
+ private static final int LOADER_ACCOUNT_INBOX = 5;
+ /**
+ * The fake folder of search results for a term. When we search for a term,
+ * a new activity is created with {@link Intent#ACTION_SEARCH}. For this new activity,
+ * we start a loader which returns conversations that match the user-provided query.
+ * We destroy the loader when we obtain a valid cursor since subsequent searches will create
+ * a new activity.
+ */
+ private static final int LOADER_SEARCH = 6;
+ /**
+ * The initial folder at app start. When the application is launched from an intent that
+ * specifies the initial folder (notifications/widgets/shortcuts),
+ * then we extract the folder URI from the intent, but we cannot trust the folder object. Since
+ * shortcuts and widgets persist past application update, they might have incorrect
+ * information encoded in them. So, to obtain a {@link Folder} object from a {@link Uri},
+ * we need to start another loader. Upon obtaining a valid cursor, the loader is destroyed.
+ * An additional complication arises if we have to view a specific conversation within this
+ * folder. This is the case when launching the app from a single conversation notification
+ * or tapping on a specific conversation in the widget. In these cases, the conversation is
+ * saved in {@link #mConversationToShow} and is retrieved when the loader returns.
+ */
public static final int LOADER_FIRST_FOLDER = 8;
+ // Loader constants: Conversations
+ /** The conversation cursor over the current conversation list. This loader provides
+ * a cursor over conversation entries from a folder to display a conversation
+ * list.
+ * This loader is started when the user switches folders (in {@link #updateFolder(Folder)},
+ * or when the controller is told that a folder/account change is imminent
+ * (in {@link #preloadConvList(Account, Folder)}. The loader is maintained for the life of
+ * the current folder. When the user switches folders, the old loader is destroyed and a new
+ * one is created.
+ *
+ * When the conversation list changes, we notify {@link #mConversationListObservable}.
+ */
+ private static final int LOADER_CONVERSATION_LIST = 4;
+
/**
* Guaranteed to be the last loader ID used by the activity. Loaders are owned by Activity or
* fragments, and within an activity, loader IDs need to be unique. A hack to ensure that the
@@ -315,7 +400,9 @@
*/
public static final int LAST_FRAGMENT_LOADER_ID = 1000;
+ /** Code returned after an account has been added. */
private static final int ADD_ACCOUNT_REQUEST_CODE = 1;
+ /** Code returned when the user has to enter the new password on an existing account. */
private static final int REAUTHENTICATE_REQUEST_CODE = 2;
/** The pending destructive action to be carried out before swapping the conversation cursor.*/
@@ -553,12 +640,11 @@
LogUtils.d(LOG_TAG, "AAC.switchToDefaultAccount(%s)", account);
final boolean firstLoad = mAccount == null;
final boolean switchToDefaultInbox = !firstLoad && account.uri.equals(mAccount.uri);
- // if the active account has been clicked in the drawer, go to default inbox
+ // If the active account has been clicked in the drawer, go to default inbox
if (switchToDefaultInbox) {
loadAccountInbox();
return;
}
-
changeAccount(account);
}
@@ -694,8 +780,13 @@
}
/**
- * Load the conversation list early for the given folder.
- * @param nextFolder
+ * Load the conversation list early for the given folder. This happens when some UI element
+ * (usually the drawer) instructs the controller that an account change or folder change is
+ * imminent. While the UI element is animating, the controller can preload the conversation
+ * list for the default inbox of the account provided here or to the folder provided here.
+ *
+ * @param nextAccount The account which the app will switch to shortly, possibly null.
+ * @param nextFolder The folder which the app will switch to shortly, possibly null.
*/
protected void preloadConvList(Account nextAccount, Folder nextFolder) {
// Fire off the conversation list loader for this account already with a fake
@@ -717,6 +808,11 @@
lm.initLoader(LOADER_CONVERSATION_LIST, args, mListCursorCallbacks);
}
+ /**
+ * Initiates the async request to create a fake search folder, which returns conversations that
+ * match the query term provided by the user. Returns immediately.
+ * @param intent Intent that the app was started with. This intent contains the search query.
+ */
private void fetchSearchFolder(Intent intent) {
final Bundle args = new Bundle();
args.putString(ConversationListContext.EXTRA_SEARCH_QUERY, intent
@@ -905,6 +1001,17 @@
mFolderListFolder = folder;
}
+ /**
+ * The mail activity calls other activities for two specific reasons:
+ * <ul>
+ * <li>To add an account. And receives the result {@link #ADD_ACCOUNT_REQUEST_CODE}</li>
+ * <li>To update the password on a current account. The result {@link
+ * #REAUTHENTICATE_REQUEST_CODE} is received.</li>
+ * </ul>
+ * @param requestCode
+ * @param resultCode
+ * @param data
+ */
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
switch (requestCode) {
@@ -1017,6 +1124,44 @@
mHandler.post(mLogServiceChecker);
}
+ /**
+ * The application can be started from the following entry points:
+ * <ul>
+ * <li>Launcher: you tap on the Gmail icon in the launcher. This is what most users think of
+ * as “Starting the app”.</li>
+ * <li>Shortcut: Users can make a shortcut to take them directly to a label.</li>
+ * <li>Widget: Shows the contents of a synced label, and allows:
+ * <ul>
+ * <li>Viewing the list (tapping on the title)</li>
+ * <li>Composing a new message (tapping on the new message icon in the title. This
+ * launches the {@link ComposeActivity}.
+ * </li>
+ * <li>Viewing a single message (tapping on a list element)</li>
+ * </ul>
+ *
+ * </li>
+ * <li>Tapping on a notification:
+ * <ul>
+ * <li>Shows message list if more than one message</li>
+ * <li>Shows the conversation if the notification is for a single message</li>
+ * </ul>
+ * </li>
+ * <li>...and most importantly, the activity life cycle can tear down the application and
+ * restart it:
+ * <ul>
+ * <li>Rotate the application: it is destroyed and recreated.</li>
+ * <li>Navigate away, and return from recent applications.</li>
+ * </ul>
+ * </li>
+ * <li>Add a new account: fires off an intent to add an account,
+ * and returns in {@link #onActivityResult(int, int, android.content.Intent)} .</li>
+ * <li>Re-authenticate your account: again returns in onActivityResult().</li>
+ * <li>Composing can happen from many entry points: third party applications fire off an
+ * intent to compose email, and launch directly into the {@link ComposeActivity}
+ * .</li>
+ * </ul>
+ * {@inheritDoc}
+ */
@Override
public boolean onCreate(Bundle savedState) {
initializeActionBar();
@@ -1079,7 +1224,7 @@
// Sync the toggle state after onRestoreInstanceState has occurred.
mDrawerToggle.syncState();
- mHideMenuItems = isDrawerEnabled() ? mDrawerContainer.isDrawerOpen(mDrawerPullout) : false;
+ mHideMenuItems = isDrawerEnabled() && mDrawerContainer.isDrawerOpen(mDrawerPullout);
}
@Override
@@ -1249,6 +1394,25 @@
dialog.show();
}
break;
+ case R.id.move_to_inbox:
+ new AsyncTask<Void, Void, Folder>() {
+ @Override
+ protected Folder doInBackground(final Void... params) {
+ // Get the "move to" inbox
+ return Utils.getFolder(mContext, mAccount.settings.moveToInbox,
+ true /* allowHidden */);
+ }
+
+ @Override
+ protected void onPostExecute(final Folder moveToInbox) {
+ final List<FolderOperation> ops = Lists.newArrayListWithCapacity(1);
+ // Add inbox
+ ops.add(new FolderOperation(moveToInbox, true));
+ assignFolder(ops, Conversation.listOf(mCurrentConversation), true,
+ true /* showUndo */, false /* isMoveTo */);
+ }
+ }.execute((Void[]) null);
+ break;
case R.id.empty_trash:
showEmptyDialog();
break;
@@ -2061,6 +2225,10 @@
* Handle an intent to open the app. This method is called only when there is no saved state,
* so we need to set state that wasn't set before. It is correct to change the viewmode here
* since it has not been previously set.
+ *
+ * This method is called for a subset of the reasons mentioned in
+ * {@link #onCreate(android.os.Bundle)}. Notably, this is called when launching the app from
+ * notifications, widgets, and shortcuts.
* @param intent intent passed to the activity.
*/
private void handleIntent(Intent intent) {
@@ -3098,6 +3266,9 @@
return in != null && in.isVisible() && mActivity.hasWindowFocus();
}
+ /**
+ * This class handles callbacks that create a {@link ConversationCursor}.
+ */
private class ConversationListLoaderCallbacks implements
LoaderManager.LoaderCallbacks<ConversationCursor> {
@@ -3355,6 +3526,7 @@
}
switch (loader.getId()) {
case LOADER_ACCOUNT_CURSOR:
+ // We have received an update on the list of accounts.
if (data == null) {
// Nothing useful to do if we have no valid data.
break;
@@ -3389,13 +3561,10 @@
break;
case LOADER_ACCOUNT_UPDATE_CURSOR:
// We have received an update for current account.
-
- // Make sure that this is an update for the current account
if (data != null && data.moveToFirst()) {
final Account updatedAccount = data.getModel();
-
+ // Make sure that this is an update for the current account
if (updatedAccount.uri.equals(mAccount.uri)) {
- // Keep a reference to the previous settings object
final Settings previousSettings = mAccount.settings;
// Update the controller's reference to the current account
@@ -3422,6 +3591,7 @@
@Override
public void onLoaderReset(Loader<ObjectCursor<Account>> loader) {
+ // Do nothing. In onLoadFinished() we copy the relevant data from the cursor.
}
}
diff --git a/src/com/android/mail/ui/AbstractConversationViewFragment.java b/src/com/android/mail/ui/AbstractConversationViewFragment.java
index 5a6399c..29cedef 100644
--- a/src/com/android/mail/ui/AbstractConversationViewFragment.java
+++ b/src/com/android/mail/ui/AbstractConversationViewFragment.java
@@ -17,46 +17,25 @@
package com.android.mail.ui;
-import android.animation.Animator;
-import android.animation.AnimatorInflater;
-import android.animation.AnimatorListenerAdapter;
import android.app.Activity;
import android.app.Fragment;
import android.app.LoaderManager;
-import android.content.ActivityNotFoundException;
import android.content.Context;
-import android.content.Intent;
import android.content.Loader;
-import android.content.pm.ActivityInfo;
-import android.content.pm.PackageManager;
-import android.content.pm.ResolveInfo;
-import android.content.res.Resources;
import android.database.Cursor;
-import android.database.DataSetObservable;
-import android.database.DataSetObserver;
import android.net.Uri;
import android.os.Bundle;
import android.os.Handler;
-import android.provider.Browser;
-import android.util.Log;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
-import android.view.View;
-import android.webkit.WebView;
-import android.webkit.WebViewClient;
-import com.android.mail.ContactInfo;
-import com.android.mail.ContactInfoSource;
-import com.android.mail.FormattedDateBuilder;
import com.android.mail.R;
-import com.android.mail.SenderInfoLoader;
import com.android.mail.browse.ConversationAccountController;
+import com.android.mail.browse.ConversationMessage;
import com.android.mail.browse.ConversationViewHeader.ConversationViewHeaderCallbacks;
import com.android.mail.browse.MessageCursor;
import com.android.mail.browse.MessageCursor.ConversationController;
-import com.android.mail.browse.MessageCursor.ConversationMessage;
-import com.android.mail.browse.MessageHeaderView.MessageHeaderViewCallbacks;
import com.android.mail.content.ObjectCursor;
import com.android.mail.content.ObjectCursorLoader;
import com.android.mail.providers.Account;
@@ -70,17 +49,15 @@
import com.android.mail.utils.LogTag;
import com.android.mail.utils.LogUtils;
import com.android.mail.utils.Utils;
-import com.google.common.collect.ImmutableMap;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
-import java.util.List;
import java.util.Map;
-import java.util.Set;
+
public abstract class AbstractConversationViewFragment extends Fragment implements
- ConversationController, ConversationAccountController, MessageHeaderViewCallbacks,
+ ConversationController, ConversationAccountController,
ConversationViewHeaderCallbacks {
private static final String ARG_ACCOUNT = "account";
@@ -89,17 +66,20 @@
private static final String LOG_TAG = LogTag.getLogTag();
protected static final int MESSAGE_LOADER = 0;
protected static final int CONTACT_LOADER = 1;
- private static int sMinDelay = -1;
- private static int sMinShowTime = -1;
protected ControllableActivity mActivity;
private final MessageLoaderCallbacks mMessageLoaderCallbacks = new MessageLoaderCallbacks();
- protected FormattedDateBuilder mDateBuilder;
- private final ContactLoaderCallbacks mContactLoaderCallbacks = new ContactLoaderCallbacks();
+ private ContactLoaderCallbacks mContactLoaderCallbacks;
private MenuItem mChangeFoldersMenuItem;
protected Conversation mConversation;
protected Folder mFolder;
protected String mBaseUri;
protected Account mAccount;
+
+ /**
+ * Must be instantiated in a derived class's onCreate.
+ */
+ protected AbstractConversationWebViewClient mWebViewClient;
+
/**
* Cache of email address strings to parsed Address objects.
* <p>
@@ -115,8 +95,7 @@
* this flag is saved and restored.
*/
private boolean mUserVisible;
- private View mProgressView;
- private View mBackgroundView;
+
private final Handler mHandler = new Handler();
/** True if we want to avoid marking the conversation as viewed and read. */
private boolean mSuppressMarkingViewed;
@@ -126,26 +105,17 @@
*/
protected ConversationViewState mViewState;
- private long mLoadingShownTime = -1;
-
private boolean mIsDetached;
private boolean mHasConversationBeenTransformed;
private boolean mHasConversationTransformBeenReverted;
- private final Runnable mDelayedShow = new FragmentRunnable("mDelayedShow") {
- @Override
- public void go() {
- mLoadingShownTime = System.currentTimeMillis();
- mProgressView.setVisibility(View.VISIBLE);
- }
- };
-
private final AccountObserver mAccountObserver = new AccountObserver() {
@Override
public void onChanged(Account newAccount) {
final Account oldAccount = mAccount;
mAccount = newAccount;
+ mWebViewClient.setAccount(mAccount);
onAccountChanged(newAccount, oldAccount);
}
};
@@ -255,81 +225,6 @@
return "(" + s + " conv=" + mConversation + ")";
}
- protected abstract WebView getWebView();
-
- public void instantiateProgressIndicators(View rootView) {
- mBackgroundView = rootView.findViewById(R.id.background_view);
- mProgressView = rootView.findViewById(R.id.loading_progress);
- }
-
- protected void dismissLoadingStatus() {
- dismissLoadingStatus(null);
- }
-
- /**
- * Begin the fade-out animation to hide the Progress overlay, either immediately or after some
- * timeout (to ensure that the progress minimum time elapses).
- *
- * @param doAfter an optional Runnable action to execute after the animation completes
- */
- protected void dismissLoadingStatus(final Runnable doAfter) {
- if (mLoadingShownTime == -1) {
- // The runnable hasn't run yet, so just remove it.
- mHandler.removeCallbacks(mDelayedShow);
- dismiss(doAfter);
- return;
- }
- final long diff = Math.abs(System.currentTimeMillis() - mLoadingShownTime);
- if (diff > sMinShowTime) {
- dismiss(doAfter);
- } else {
- mHandler.postDelayed(new FragmentRunnable("dismissLoadingStatus") {
- @Override
- public void go() {
- dismiss(doAfter);
- }
- }, Math.abs(sMinShowTime - diff));
- }
- }
-
- private void dismiss(final Runnable doAfter) {
- // Reset loading shown time.
- mLoadingShownTime = -1;
- mProgressView.setVisibility(View.GONE);
- if (mBackgroundView.getVisibility() == View.VISIBLE) {
- animateDismiss(doAfter);
- } else {
- if (doAfter != null) {
- doAfter.run();
- }
- }
- }
-
- private void animateDismiss(final Runnable doAfter) {
- // the animation can only work (and is only worth doing) if this fragment is added
- // reasons it may not be added: fragment is being destroyed, or in the process of being
- // restored
- if (!isAdded()) {
- mBackgroundView.setVisibility(View.GONE);
- return;
- }
-
- Utils.enableHardwareLayer(mBackgroundView);
- final Animator animator = AnimatorInflater.loadAnimator(getContext(), R.anim.fade_out);
- animator.setTarget(mBackgroundView);
- animator.addListener(new AnimatorListenerAdapter() {
- @Override
- public void onAnimationEnd(Animator animation) {
- mBackgroundView.setVisibility(View.GONE);
- mBackgroundView.setLayerType(View.LAYER_TYPE_NONE, null);
- if (doAfter != null) {
- doAfter.run();
- }
- }
- });
- animator.start();
- }
-
@Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
@@ -344,8 +239,9 @@
}
mActivity = (ControllableActivity) activity;
mContext = activity.getApplicationContext();
- mDateBuilder = new FormattedDateBuilder((Context) mActivity);
+ mWebViewClient.setActivity(activity);
mAccount = mAccountObserver.initialize(mActivity.getAccountController());
+ mWebViewClient.setAccount(mAccount);
}
@Override
@@ -354,23 +250,6 @@
return activity != null ? activity.getConversationUpdater() : null;
}
-
- protected void showLoadingStatus() {
- if (!mUserVisible) {
- return;
- }
- if (sMinDelay == -1) {
- Resources res = getContext().getResources();
- sMinDelay = res.getInteger(R.integer.conversationview_show_loading_delay);
- sMinShowTime = res.getInteger(R.integer.conversationview_min_show_loading);
- }
- // If the loading view isn't already showing, show it and remove any
- // pending calls to show the loading screen.
- mBackgroundView.setVisibility(View.VISIBLE);
- mHandler.removeCallbacks(mDelayedShow);
- mHandler.postDelayed(mDelayedShow, sMinDelay);
- }
-
public Context getContext() {
return mContext;
}
@@ -394,6 +273,9 @@
}
public ContactLoaderCallbacks getContactInfoSource() {
+ if (mContactLoaderCallbacks == null) {
+ mContactLoaderCallbacks = new ContactLoaderCallbacks(mActivity.getActivityContext());
+ }
return mContactLoaderCallbacks;
}
@@ -421,7 +303,7 @@
LogUtils.e(LOG_TAG,
"ACVF ignoring onOptionsItemSelected b/c userVisibleHint is false. f=%s", this);
if (LogUtils.isLoggable(LOG_TAG, LogUtils.DEBUG)) {
- Log.e(LOG_TAG, Utils.dumpFragment(this)); // the dump has '%' chars in it...
+ LogUtils.e(LOG_TAG, Utils.dumpFragment(this)); // the dump has '%' chars in it...
}
return false;
}
@@ -447,6 +329,8 @@
mHasConversationBeenTransformed && !mHasConversationTransformBeenReverted);
}
+ abstract boolean supportsMessageTransforms();
+
// BEGIN conversation header callbacks
@Override
public void onFoldersClicked() {
@@ -591,7 +475,7 @@
}
private void popOut() {
- mHandler.post(new FragmentRunnable("popOut") {
+ mHandler.post(new FragmentRunnable("popOut", this) {
@Override
public void go() {
if (mActivity != null) {
@@ -687,154 +571,8 @@
}
}
- /**
- * Inner class to to asynchronously load contact data for all senders in the conversation,
- * and notify observers when the data is ready.
- *
- */
- protected class ContactLoaderCallbacks implements ContactInfoSource,
- LoaderManager.LoaderCallbacks<ImmutableMap<String, ContactInfo>> {
-
- private Set<String> mSenders;
- private ImmutableMap<String, ContactInfo> mContactInfoMap;
- private DataSetObservable mObservable = new DataSetObservable();
-
- public void setSenders(Set<String> emailAddresses) {
- mSenders = emailAddresses;
- }
-
- @Override
- public Loader<ImmutableMap<String, ContactInfo>> onCreateLoader(int id, Bundle args) {
- return new SenderInfoLoader(mActivity.getActivityContext(), mSenders);
- }
-
- @Override
- public void onLoadFinished(Loader<ImmutableMap<String, ContactInfo>> loader,
- ImmutableMap<String, ContactInfo> data) {
- mContactInfoMap = data;
- mObservable.notifyChanged();
- }
-
- @Override
- public void onLoaderReset(Loader<ImmutableMap<String, ContactInfo>> loader) {
- }
-
- @Override
- public ContactInfo getContactInfo(String email) {
- if (mContactInfoMap == null) {
- return null;
- }
- return mContactInfoMap.get(email);
- }
-
- @Override
- public void registerObserver(DataSetObserver observer) {
- mObservable.registerObserver(observer);
- }
-
- @Override
- public void unregisterObserver(DataSetObserver observer) {
- mObservable.unregisterObserver(observer);
- }
- }
-
- protected class AbstractConversationWebViewClient extends WebViewClient {
- @Override
- public boolean shouldOverrideUrlLoading(WebView view, String url) {
- final Activity activity = getActivity();
- if (activity == null) {
- return false;
- }
-
- boolean result = false;
- final Intent intent;
- Uri uri = Uri.parse(url);
- if (!Utils.isEmpty(mAccount.viewIntentProxyUri)) {
- intent = generateProxyIntent(uri);
- } else {
- intent = new Intent(Intent.ACTION_VIEW, uri);
- intent.putExtra(Browser.EXTRA_APPLICATION_ID, activity.getPackageName());
- }
-
- try {
- intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET);
- activity.startActivity(intent);
- result = true;
- } catch (ActivityNotFoundException ex) {
- // If no application can handle the URL, assume that the
- // caller can handle it.
- }
-
- return result;
- }
-
- private Intent generateProxyIntent(Uri uri) {
- final Intent intent = new Intent(Intent.ACTION_VIEW, mAccount.viewIntentProxyUri);
- intent.putExtra(UIProvider.ViewProxyExtras.EXTRA_ORIGINAL_URI, uri);
- intent.putExtra(UIProvider.ViewProxyExtras.EXTRA_ACCOUNT, mAccount);
-
- final Context context = getContext();
- PackageManager manager = null;
- // We need to catch the exception to make CanvasConversationHeaderView
- // test pass. Bug: http://b/issue?id=3470653.
- try {
- manager = context.getPackageManager();
- } catch (UnsupportedOperationException e) {
- LogUtils.e(LOG_TAG, e, "Error getting package manager");
- }
-
- if (manager != null) {
- // Try and resolve the intent, to find an activity from this package
- final List<ResolveInfo> resolvedActivities = manager.queryIntentActivities(
- intent, PackageManager.MATCH_DEFAULT_ONLY);
-
- final String packageName = context.getPackageName();
-
- // Now try and find one that came from this package, if one is not found, the UI
- // provider must have specified an intent that is to be handled by a different apk.
- // In that case, the class name will not be set on the intent, so the default
- // intent resolution will be used.
- for (ResolveInfo resolveInfo: resolvedActivities) {
- final ActivityInfo activityInfo = resolveInfo.activityInfo;
- if (packageName.equals(activityInfo.packageName)) {
- intent.setClassName(activityInfo.packageName, activityInfo.name);
- break;
- }
- }
- }
-
- return intent;
- }
- }
-
public abstract void onConversationUpdated(Conversation conversation);
- /**
- * Small Runnable-like wrapper that first checks that the Fragment is in a good state before
- * doing any work. Ideal for use with a {@link Handler}.
- */
- protected abstract class FragmentRunnable implements Runnable {
-
- private final String mOpName;
-
- public FragmentRunnable(String opName) {
- mOpName = opName;
- }
-
- public abstract void go();
-
- @Override
- public void run() {
- if (!isAdded()) {
- LogUtils.i(LOG_TAG, "Unable to run op='%s' b/c fragment is not attached: %s",
- mOpName, AbstractConversationViewFragment.this);
- return;
- }
- go();
- }
-
- }
-
public void onDetachedModeEntered() {
// If we have no messages, then we have nothing to display, so leave this view.
// Otherwise, just set the detached flag.
@@ -854,7 +592,7 @@
*/
public void onConversationTransformed() {
mHasConversationBeenTransformed = true;
- mHandler.post(new FragmentRunnable("invalidateOptionsMenu") {
+ mHandler.post(new FragmentRunnable("invalidateOptionsMenu", this) {
@Override
public void go() {
mActivity.invalidateOptionsMenu();
diff --git a/src/com/android/mail/ui/AbstractConversationWebViewClient.java b/src/com/android/mail/ui/AbstractConversationWebViewClient.java
new file mode 100644
index 0000000..0684b0c
--- /dev/null
+++ b/src/com/android/mail/ui/AbstractConversationWebViewClient.java
@@ -0,0 +1,126 @@
+/*
+ * Copyright (C) 2013 Google Inc.
+ * Licensed to The Android Open Source Project.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.mail.ui;
+
+import android.app.Activity;
+import android.content.ActivityNotFoundException;
+import android.content.Intent;
+import android.content.pm.ActivityInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.net.Uri;
+import android.provider.Browser;
+import android.webkit.WebView;
+import android.webkit.WebViewClient;
+
+import com.android.mail.providers.Account;
+import com.android.mail.providers.UIProvider;
+import com.android.mail.utils.LogTag;
+import com.android.mail.utils.LogUtils;
+import com.android.mail.utils.Utils;
+
+import java.util.List;
+
+/**
+ * Base implementation of a web view client for the conversation views.
+ * Handles proxying the view intent so that additional information can
+ * be sent with the intent when links are clicked.
+ */
+public class AbstractConversationWebViewClient extends WebViewClient {
+ private static final String LOG_TAG = LogTag.getLogTag();
+
+ private Account mAccount;
+ private Activity mActivity;
+
+ public AbstractConversationWebViewClient(Account account) {
+ mAccount = account;
+ }
+
+ public void setAccount(Account account) {
+ mAccount = account;
+ }
+
+ public void setActivity(Activity activity) {
+ mActivity = activity;
+ }
+
+ @Override
+ public boolean shouldOverrideUrlLoading(WebView view, String url) {
+ if (mActivity == null) {
+ return false;
+ }
+
+ boolean result = false;
+ final Intent intent;
+ Uri uri = Uri.parse(url);
+ if (mAccount != null && !Utils.isEmpty(mAccount.viewIntentProxyUri)) {
+ intent = generateProxyIntent(uri);
+ } else {
+ intent = new Intent(Intent.ACTION_VIEW, uri);
+ intent.putExtra(Browser.EXTRA_APPLICATION_ID, mActivity.getPackageName());
+ }
+
+ try {
+ intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET);
+ mActivity.startActivity(intent);
+ result = true;
+ } catch (ActivityNotFoundException ex) {
+ // If no application can handle the URL, assume that the
+ // caller can handle it.
+ }
+
+ return result;
+ }
+
+ private Intent generateProxyIntent(Uri uri) {
+ final Intent intent = new Intent(Intent.ACTION_VIEW, mAccount.viewIntentProxyUri);
+ intent.putExtra(UIProvider.ViewProxyExtras.EXTRA_ORIGINAL_URI, uri);
+ intent.putExtra(UIProvider.ViewProxyExtras.EXTRA_ACCOUNT, mAccount);
+
+ PackageManager manager = null;
+ // We need to catch the exception to make CanvasConversationHeaderView
+ // test pass. Bug: http://b/issue?id=3470653.
+ try {
+ manager = mActivity.getPackageManager();
+ } catch (UnsupportedOperationException e) {
+ LogUtils.e(LOG_TAG, e, "Error getting package manager");
+ }
+
+ if (manager != null) {
+ // Try and resolve the intent, to find an activity from this package
+ final List<ResolveInfo> resolvedActivities = manager.queryIntentActivities(
+ intent, PackageManager.MATCH_DEFAULT_ONLY);
+
+ final String packageName = mActivity.getPackageName();
+
+ // Now try and find one that came from this package, if one is not found, the UI
+ // provider must have specified an intent that is to be handled by a different apk.
+ // In that case, the class name will not be set on the intent, so the default
+ // intent resolution will be used.
+ for (ResolveInfo resolveInfo: resolvedActivities) {
+ final ActivityInfo activityInfo = resolveInfo.activityInfo;
+ if (packageName.equals(activityInfo.packageName)) {
+ intent.setClassName(activityInfo.packageName, activityInfo.name);
+ break;
+ }
+ }
+ }
+
+ return intent;
+ }
+}
diff --git a/src/com/android/mail/ui/AbstractMailActivity.java b/src/com/android/mail/ui/AbstractMailActivity.java
index 33a7da2..43f3b23 100644
--- a/src/com/android/mail/ui/AbstractMailActivity.java
+++ b/src/com/android/mail/ui/AbstractMailActivity.java
@@ -39,7 +39,7 @@
private final UiHandler mUiHandler = new UiHandler();
- private static final boolean STRICT_MODE = false;
+ private static final boolean STRICT_MODE = true;
@Override
protected void onCreate(Bundle savedInstanceState) {
diff --git a/src/com/android/mail/ui/AccountController.java b/src/com/android/mail/ui/AccountController.java
index f2b36f9..3d8ab2f 100644
--- a/src/com/android/mail/ui/AccountController.java
+++ b/src/com/android/mail/ui/AccountController.java
@@ -19,6 +19,7 @@
import android.database.DataSetObservable;
import android.database.DataSetObserver;
+import android.widget.ListView;
import com.android.mail.providers.Account;
import com.android.mail.providers.AccountObserver;
@@ -108,4 +109,10 @@
* @return <code>true</code> if the drawer pull action is enabled, <code>false</code> otherwise
*/
boolean isDrawerPullEnabled();
+
+ /**
+ * @return the choice mode to use in the {@link ListView} in the default folder list (subclasses
+ * of {@link FolderListFragment} may override this
+ */
+ int getFolderListViewChoiceMode();
}
diff --git a/src/com/android/mail/ui/ActivityController.java b/src/com/android/mail/ui/ActivityController.java
index ed30f29..993d5d4 100644
--- a/src/com/android/mail/ui/ActivityController.java
+++ b/src/com/android/mail/ui/ActivityController.java
@@ -38,11 +38,28 @@
* An Activity controller knows how to combine views and listeners into a functioning activity.
* ActivityControllers are delegates that implement methods by calling underlying views to modify,
* or respond to user action.
+ *
+ * There are two ways of adding methods to this interface:
+ * <ul>
+ * <li>When the methods pertain to a single logical grouping: consider adding a new
+ * interface and putting all the methods in that interface. As an example,
+ * look at {@link AccountController}. The controller implements this,
+ * and returns itself in
+ * {@link com.android.mail.ui.ControllableActivity#getAccountController()}. This allows
+ * for account-specific methods to be added without creating new methods in this interface
+ * .</li>
+ * <li>Methods that relate to an activity can be added directly. As an example,
+ * look at {@link #onActivityResult(int, int, android.content.Intent)} which is identical to
+ * its declaration in {@link android.app.Activity}.</li>
+ * <li>Everything else. As an example, look at {@link #isDrawerEnabled()}. Try to avoid
+ * this path because an implementation has to provided in many classes:
+ * {@link MailActivity}, {@link FolderSelectionActivity}, and the controllers.</li>
+ * </ul>
*/
public interface ActivityController extends LayoutListener,
ModeChangeListener, ConversationListCallbacks,
- FolderChangeListener, ConversationSetObserver, ConversationListener,
- FolderListFragment.FolderListSelectionListener, HelpCallback, UndoListener,
+ FolderChangeListener, ConversationSetObserver, ConversationListener, FolderSelector,
+ HelpCallback, UndoListener,
ConversationUpdater, ErrorListener, FolderController, AccountController,
ConversationPositionTracker.Callbacks, ConversationListFooterView.FooterViewClickListener,
RecentFolderController, UpOrBackController {
diff --git a/src/com/android/mail/ui/AnimatedAdapter.java b/src/com/android/mail/ui/AnimatedAdapter.java
index 6c6fb14..ce7dced 100644
--- a/src/com/android/mail/ui/AnimatedAdapter.java
+++ b/src/com/android/mail/ui/AnimatedAdapter.java
@@ -23,6 +23,7 @@
import android.animation.AnimatorSet;
import android.animation.ObjectAnimator;
import android.content.Context;
+import android.content.res.Resources;
import android.database.Cursor;
import android.os.Bundle;
import android.os.Handler;
@@ -37,6 +38,7 @@
import com.android.mail.browse.ConversationItemView;
import com.android.mail.browse.ConversationItemViewCoordinates;
import com.android.mail.browse.SwipeableConversationItemView;
+import com.android.mail.content.ObjectCursor;
import com.android.mail.providers.Account;
import com.android.mail.providers.AccountObserver;
import com.android.mail.providers.Conversation;
@@ -46,6 +48,7 @@
import com.android.mail.ui.SwipeableListView.ListItemsRemovedListener;
import com.android.mail.utils.LogTag;
import com.android.mail.utils.LogUtils;
+import com.google.common.collect.ImmutableList;
import com.google.common.collect.Maps;
import java.util.ArrayList;
@@ -134,10 +137,11 @@
/**
* The next action to perform. Do not read or write this. All accesses should
- * be in {@link #performAndSetNextAction(DestructiveAction)} which commits the
- * previous action, if any.
+ * be in {@link #performAndSetNextAction(SwipeableListView.ListItemsRemovedListener)} which
+ * commits the previous action, if any.
*/
private ListItemsRemovedListener mPendingDestruction;
+
/**
* A destructive action that refreshes the list and performs no other action.
*/
@@ -169,33 +173,38 @@
}
};
- private final List<ConversationSpecialItemView> mSpecialViews;
- private final SparseArray<ConversationSpecialItemView> mSpecialViewPositions;
+ /**
+ * A list of all views that are not conversations. These include temporary views from
+ * {@link #mFleetingViews} and child folders from {@link #mFolderViews}.
+ */
+ private final SparseArray<ConversationSpecialItemView> mSpecialViews;
private final SparseArray<ConversationItemViewCoordinates> mCoordinatesCache =
new SparseArray<ConversationItemViewCoordinates>();
- private final void setAccount(Account newAccount) {
+ /**
+ * Temporary views insert at specific positions relative to conversations. These can be
+ * related to showing new features (on-boarding) or showing information about new mailboxes
+ * that have been added by the system.
+ */
+ private final List<ConversationSpecialItemView> mFleetingViews;
+
+ /** List of all child folders for this folder. */
+ private List<NestedFolderView> mFolderViews;
+
+ private void setAccount(Account newAccount) {
mAccount = newAccount;
mPriorityMarkersEnabled = mAccount.settings.priorityArrowsEnabled;
mSwipeEnabled = mAccount.supportsCapability(UIProvider.AccountCapabilities.UNDO);
}
- /**
- * Used only for debugging.
- */
private static final String LOG_TAG = LogTag.getLogTag();
private static final int INCREASE_WAIT_COUNT = 2;
public AnimatedAdapter(Context context, ConversationCursor cursor,
ConversationSelectionSet batch, ControllableActivity activity,
- SwipeableListView listView) {
- this(context, cursor, batch, activity, listView, null);
- }
-
- public AnimatedAdapter(Context context, ConversationCursor cursor,
- ConversationSelectionSet batch, ControllableActivity activity,
- SwipeableListView listView, final List<ConversationSpecialItemView> specialViews) {
+ SwipeableListView listView, final List<ConversationSpecialItemView> specialViews,
+ final ObjectCursor<Folder> childFolders) {
super(context, -1, cursor, UIProvider.CONVERSATION_PROJECTION, null, 0);
mContext = context;
mBatchConversations = batch;
@@ -203,27 +212,61 @@
mActivity = activity;
mShowFooter = false;
mListView = listView;
+ mFolderViews = getNestedFolders(childFolders);
+
mHandler = new Handler();
if (sDismissAllShortDelay == -1) {
- sDismissAllShortDelay =
- context.getResources()
- .getInteger(R.integer.dismiss_all_leavebehinds_short_delay);
- sDismissAllLongDelay =
- context.getResources()
- .getInteger(R.integer.dismiss_all_leavebehinds_long_delay);
+ final Resources r = context.getResources();
+ sDismissAllShortDelay = r.getInteger(R.integer.dismiss_all_leavebehinds_short_delay);
+ sDismissAllLongDelay = r.getInteger(R.integer.dismiss_all_leavebehinds_long_delay);
}
- mSpecialViews =
- specialViews == null ? new ArrayList<ConversationSpecialItemView>(0)
- : new ArrayList<ConversationSpecialItemView>(specialViews);
- mSpecialViewPositions = new SparseArray<ConversationSpecialItemView>(mSpecialViews.size());
+ if (specialViews != null) {
+ mFleetingViews = new ArrayList<ConversationSpecialItemView>(specialViews);
+ } else {
+ mFleetingViews = new ArrayList<ConversationSpecialItemView>(0);
+ }
+ /** Total number of special views */
+ final int size = mFleetingViews.size() + mFolderViews.size();
+ mSpecialViews = new SparseArray<ConversationSpecialItemView>(size);
- for (final ConversationSpecialItemView view : mSpecialViews) {
+ // Only set the adapter in teaser views. Folder views don't care about the adapter.
+ for (final ConversationSpecialItemView view : mFleetingViews) {
view.setAdapter(this);
}
-
updateSpecialViews();
}
+ /**
+ * Returns a list containing views for all the nested folders.
+ * @param cursor cursor containing the folders nested within the current folder
+ * @return a list, possibly empty of the views representing the folders.
+ */
+ private List<NestedFolderView> getNestedFolders (final ObjectCursor<Folder> cursor) {
+ if (cursor == null || !cursor.moveToFirst()) {
+ // The cursor has nothing valid. Return an empty list.
+ return ImmutableList.of();
+ }
+
+ final LayoutInflater inflater = LayoutInflater.from(mContext);
+ final List<NestedFolderView> folders = new ArrayList<NestedFolderView>(cursor.getCount());
+ do {
+ final NestedFolderView view =
+ (NestedFolderView) inflater.inflate(R.layout.nested_folder, null);
+ view.setFolder(cursor.getModel());
+ folders.add(view);
+ } while (cursor.moveToNext());
+ return folders;
+ }
+
+ /**
+ * Updates the list of folders for the current list with the cursor provided here.
+ * @param childFolders A cursor containing child folders for the current folder.
+ */
+ public void updateNestedFolders (ObjectCursor<Folder> childFolders) {
+ mFolderViews = getNestedFolders(childFolders);
+ notifyDataSetChanged();
+ }
+
public void cancelDismissCounter() {
cancelLeaveBehindFadeInAnimation();
mHandler.removeCallbacks(mCountDown);
@@ -245,8 +288,8 @@
@Override
public int getCount() {
- // mSpecialViewPositions only contains the views that are currently being displayed
- final int specialViewCount = mSpecialViewPositions.size();
+ // mSpecialViews only contains the views that are currently being displayed
+ final int specialViewCount = mSpecialViews.size();
final int count = super.getCount() + specialViewCount;
return mShowFooter ? count + 1 : count;
@@ -339,7 +382,7 @@
// types. In a future release, use position/id map to try to make
// this cleaner / faster to determine if the view is animating.
return TYPE_VIEW_DONT_RECYCLE;
- } else if (mSpecialViewPositions.get(position) != null) {
+ } else if (mSpecialViews.get(position) != null) {
// Don't recycle the special views
return TYPE_VIEW_DONT_RECYCLE;
}
@@ -412,12 +455,12 @@
}
// Check if this is a special view
- final View specialView = (View) mSpecialViewPositions.get(position);
+ final View specialView = (View) mSpecialViews.get(position);
if (specialView != null) {
return specialView;
}
- ConversationCursor cursor = (ConversationCursor) getItem(position);
+ final ConversationCursor cursor = (ConversationCursor) getItem(position);
final Conversation conv = cursor.getConversation();
// Notify the provider of this change in the position of Conversation cursor
@@ -616,7 +659,7 @@
@Override
public long getItemId(int position) {
if (mShowFooter && position == getCount() - 1
- || mSpecialViewPositions.get(position) != null) {
+ || mSpecialViews.get(position) != null) {
return -1;
}
final int cursorPos = position - getPositionOffset(position);
@@ -668,9 +711,7 @@
@Override
public View newView(Context context, Cursor cursor, ViewGroup parent) {
- SwipeableConversationItemView view = new SwipeableConversationItemView(context,
- mAccount.name);
- return view;
+ return new SwipeableConversationItemView(context, mAccount.name);
}
@Override
@@ -700,8 +741,8 @@
public Object getItem(int position) {
if (mShowFooter && position == getCount() - 1) {
return mFooter;
- } else if (mSpecialViewPositions.get(position) != null) {
- return mSpecialViewPositions.get(position);
+ } else if (mSpecialViews.get(position) != null) {
+ return mSpecialViews.get(position);
}
return super.getItem(position - getPositionOffset(position));
}
@@ -739,7 +780,7 @@
* @param next The next action that is to be performed, possibly null (if no next action is
* needed).
*/
- private final void performAndSetNextAction(ListItemsRemovedListener next) {
+ private void performAndSetNextAction(ListItemsRemovedListener next) {
if (mPendingDestruction != null) {
mPendingDestruction.onListItemsRemoved();
}
@@ -763,28 +804,21 @@
@Override
public boolean areAllItemsEnabled() {
- // The animating positions are not enabled.
+ // The animating items and some special views are not enabled.
return false;
}
@Override
public boolean isEnabled(final int position) {
- if (mSpecialViewPositions.get(position) != null) {
- // This is a special view
- return false;
+ final ConversationSpecialItemView view = mSpecialViews.get(position);
+ if (view != null) {
+ final boolean enabled = view.acceptsUserTaps();
+ LogUtils.d(LOG_TAG, "AA.isEnabled(%d) = %b", position, enabled);
+ return enabled;
}
-
return !isPositionDeleting(position) && !isPositionUndoing(position);
}
- public void showFooter() {
- setFooterVisibility(true);
- }
-
- public void hideFooter() {
- setFooterVisibility(false);
- }
-
public void setFooterVisibility(boolean show) {
if (mShowFooter != show) {
mShowFooter = show;
@@ -836,8 +870,8 @@
public void onRestoreInstanceState(Bundle outState) {
if (outState.containsKey(LAST_DELETING_ITEMS)) {
final long[] lastDeleting = outState.getLongArray(LAST_DELETING_ITEMS);
- for (int i = 0; i < lastDeleting.length; i++) {
- mLastDeletingItems.add(lastDeleting[i]);
+ for (final long aLastDeleting : lastDeleting) {
+ mLastDeletingItems.add(aLastDeleting);
}
}
if (outState.containsKey(LEAVE_BEHIND_ITEM_DATA)) {
@@ -910,24 +944,40 @@
}
}
+ /**
+ * Updates special (non-conversation view) when either {@link #mFolderViews} or
+ * {@link #mFleetingViews} changed
+ */
private void updateSpecialViews() {
- mSpecialViewPositions.clear();
+ // We recreate all the special views using mFolderViews and mFleetingViews (in that order).
+ mSpecialViews.clear();
- for (int i = 0; i < mSpecialViews.size(); i++) {
- final ConversationSpecialItemView specialView = mSpecialViews.get(i);
+ int folderCount = 0;
+ // Nested folders are added initially. They don't specify positions: we put them at the
+ // very top.
+ for (final NestedFolderView view : mFolderViews) {
+ mSpecialViews.put(folderCount, view);
+ folderCount++;
+ }
+
+ // Fleeting (temporary) views go after this. They specify a position,which is 0-indexed and
+ // has to be adjusted for the number of folders above it.
+ for (final ConversationSpecialItemView specialView : mFleetingViews) {
specialView.onUpdate(mAccount.name, mFolder, getConversationCursor());
if (specialView.getShouldDisplayInList()) {
- int position = specialView.getPosition();
+ // If the special view asks for position 0, it wants to be at the top. However,
+ // if there are already 3 folders above it, the real position it needs is 0+3 (4th
+ // from top, since everything is 0-indexed).
+ int position = (specialView.getPosition() + folderCount);
// insert the special view into the position, but if there is
// already an item occupying that position, move that item back
// one position, and repeat
ConversationSpecialItemView insert = specialView;
while (insert != null) {
- final ConversationSpecialItemView kickedOut = mSpecialViewPositions.get(
- position);
- mSpecialViewPositions.put(position, insert);
+ final ConversationSpecialItemView kickedOut = mSpecialViews.get(position);
+ mSpecialViews.put(position, insert);
insert = kickedOut;
position++;
}
@@ -968,9 +1018,8 @@
public int getPositionOffset(final int position) {
int offset = 0;
- for (int i = 0; i < mSpecialViewPositions.size(); i++) {
- final int key = mSpecialViewPositions.keyAt(i);
- final ConversationSpecialItemView specialView = mSpecialViewPositions.get(key);
+ for (int i = 0, size = mSpecialViews.size(); i < size; i++) {
+ final int key = mSpecialViews.keyAt(i);
if (key <= position) {
offset++;
}
@@ -980,20 +1029,21 @@
}
public void cleanup() {
- for (final ConversationSpecialItemView view : mSpecialViews) {
+ // Only clean up teaser views. Folder views don't care about clean up.
+ for (final ConversationSpecialItemView view : mFleetingViews) {
view.cleanup();
}
}
public void onConversationSelected() {
- for (int i = 0; i < mSpecialViews.size(); i++) {
- final ConversationSpecialItemView specialView = mSpecialViews.get(i);
+ // Only notify teaser views. Folder views don't care about selected conversations.
+ for (final ConversationSpecialItemView specialView : mFleetingViews) {
specialView.onConversationSelected();
}
}
public void onCabModeEntered() {
- for (final ConversationSpecialItemView specialView : mSpecialViews) {
+ for (final ConversationSpecialItemView specialView : mFleetingViews) {
specialView.onCabModeEntered();
}
}
diff --git a/src/com/android/mail/ui/ContactLoaderCallbacks.java b/src/com/android/mail/ui/ContactLoaderCallbacks.java
new file mode 100644
index 0000000..cd09398
--- /dev/null
+++ b/src/com/android/mail/ui/ContactLoaderCallbacks.java
@@ -0,0 +1,88 @@
+/*
+ * Copyright (C) 2013 Google Inc.
+ * Licensed to The Android Open Source Project.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.mail.ui;
+
+import android.app.LoaderManager;
+import android.content.Context;
+import android.content.Loader;
+import android.database.DataSetObservable;
+import android.database.DataSetObserver;
+import android.os.Bundle;
+
+import com.android.mail.ContactInfo;
+import com.android.mail.ContactInfoSource;
+import com.android.mail.SenderInfoLoader;
+import com.google.common.collect.ImmutableMap;
+
+import java.util.Set;
+
+/**
+ * Asynchronously loads contact data for all senders in the conversation,
+ * and notifies observers when the data is ready.
+ */
+public class ContactLoaderCallbacks implements ContactInfoSource,
+ LoaderManager.LoaderCallbacks<ImmutableMap<String, ContactInfo>> {
+
+ private Set<String> mSenders;
+ private ImmutableMap<String, ContactInfo> mContactInfoMap;
+ private DataSetObservable mObservable = new DataSetObservable();
+
+ private Context mContext;
+
+ public ContactLoaderCallbacks(Context context) {
+ mContext = context;
+ }
+
+ public void setSenders(Set<String> emailAddresses) {
+ mSenders = emailAddresses;
+ }
+
+ @Override
+ public Loader<ImmutableMap<String, ContactInfo>> onCreateLoader(int id, Bundle args) {
+ return new SenderInfoLoader(mContext, mSenders);
+ }
+
+ @Override
+ public void onLoadFinished(Loader<ImmutableMap<String, ContactInfo>> loader,
+ ImmutableMap<String, ContactInfo> data) {
+ mContactInfoMap = data;
+ mObservable.notifyChanged();
+ }
+
+ @Override
+ public void onLoaderReset(Loader<ImmutableMap<String, ContactInfo>> loader) {
+ }
+
+ @Override
+ public ContactInfo getContactInfo(String email) {
+ if (mContactInfoMap == null) {
+ return null;
+ }
+ return mContactInfoMap.get(email);
+ }
+
+ @Override
+ public void registerObserver(DataSetObserver observer) {
+ mObservable.registerObserver(observer);
+ }
+
+ @Override
+ public void unregisterObserver(DataSetObserver observer) {
+ mObservable.unregisterObserver(observer);
+ }
+}
diff --git a/src/com/android/mail/ui/ControllableActivity.java b/src/com/android/mail/ui/ControllableActivity.java
index fae8665..9bd563b 100644
--- a/src/com/android/mail/ui/ControllableActivity.java
+++ b/src/com/android/mail/ui/ControllableActivity.java
@@ -72,7 +72,7 @@
* fragment so that activity controllers can track the last folder list
* pushed for hierarchical folders.
*/
- FolderListFragment.FolderListSelectionListener getFolderListSelectionListener();
+ FolderSelector getFolderSelector();
/**
* Get the folder currently being accessed by the activity.
diff --git a/src/com/android/mail/ui/ConversationCursorLoader.java b/src/com/android/mail/ui/ConversationCursorLoader.java
index 530d991..8d9df0b 100644
--- a/src/com/android/mail/ui/ConversationCursorLoader.java
+++ b/src/com/android/mail/ui/ConversationCursorLoader.java
@@ -20,11 +20,11 @@
import android.app.Activity;
import android.content.AsyncTaskLoader;
import android.net.Uri;
-import android.util.Log;
import com.android.mail.browse.ConversationCursor;
import com.android.mail.providers.Account;
import com.android.mail.providers.UIProvider.AccountCapabilities;
+import com.android.mail.utils.LogUtils;
import java.util.ArrayList;
@@ -58,16 +58,16 @@
private static void dumpLoaders() {
if (DEBUG) {
- Log.d(TAG, "Loaders: ");
+ LogUtils.d(TAG, "Loaders: ");
for (ConversationCursorLoader loader: sLoaders) {
- Log.d(TAG, " >> " + loader.mName + " (" + loader.mUri + ")");
+ LogUtils.d(TAG, " >> " + loader.mName + " (" + loader.mUri + ")");
}
}
}
private void addLoader() {
if (DEBUG) {
- Log.d(TAG, "Add loader: " + mUri);
+ LogUtils.d(TAG, "Add loader: " + mUri);
sLoaders.add(this);
if (sLoaders.size() > 1) {
dumpLoaders();
@@ -90,7 +90,7 @@
mConversationCursor.disable();
mClosed = true;
if (DEBUG) {
- Log.d(TAG, "Reset loader/disable cursor: " + mName);
+ LogUtils.d(TAG, "Reset loader/disable cursor: " + mName);
sLoaders.remove(this);
if (!sLoaders.isEmpty()) {
dumpLoaders();
@@ -98,7 +98,7 @@
}
} else {
if (DEBUG) {
- Log.d(TAG, "Reset loader/retain cursor: " + mName);
+ LogUtils.d(TAG, "Reset loader/retain cursor: " + mName);
mRetained = true;
}
}
@@ -120,12 +120,12 @@
mConversationCursor.load();
addLoader();
if (DEBUG) {
- Log.d(TAG, "Restarting reset loader: " + mName);
+ LogUtils.d(TAG, "Restarting reset loader: " + mName);
}
} else if (mRetained) {
mRetained = false;
if (DEBUG) {
- Log.d(TAG, "Resuming retained loader: " + mName);
+ LogUtils.d(TAG, "Resuming retained loader: " + mName);
}
}
forceLoad();
diff --git a/src/com/android/mail/ui/ConversationListFragment.java b/src/com/android/mail/ui/ConversationListFragment.java
index 16fc684..b11cadd 100644
--- a/src/com/android/mail/ui/ConversationListFragment.java
+++ b/src/com/android/mail/ui/ConversationListFragment.java
@@ -21,15 +21,15 @@
import android.app.Activity;
import android.app.ListFragment;
+import android.app.LoaderManager;
import android.content.Context;
+import android.content.Loader;
import android.content.res.Resources;
import android.database.DataSetObserver;
-import android.graphics.PixelFormat;
-import android.graphics.Rect;
+import android.net.Uri;
import android.os.Bundle;
import android.os.Handler;
import android.text.format.DateUtils;
-import android.util.TypedValue;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
@@ -46,6 +46,8 @@
import com.android.mail.browse.ConversationItemViewModel;
import com.android.mail.browse.ConversationListFooterView;
import com.android.mail.browse.ToggleableItem;
+import com.android.mail.content.ObjectCursor;
+import com.android.mail.content.ObjectCursorLoader;
import com.android.mail.providers.Account;
import com.android.mail.providers.AccountObserver;
import com.android.mail.providers.Conversation;
@@ -147,6 +149,52 @@
private int mConversationCursorHash;
/**
+ * If the current list is for a folder with children, this set of loader callbacks will
+ * create a loader for all the child folders, and will return an {@link ObjectCursor} over the
+ * list.
+ */
+ private final class ChildFolderLoads
+ implements LoaderManager.LoaderCallbacks<ObjectCursor<Folder>> {
+ /** Load all child folders for the current folder. */
+ private static final int LOADER_CHIDREN = 0;
+ public static final String CHILD_URI = "arg-child-uri";
+ private final String[] projection = UIProvider.FOLDERS_PROJECTION;
+
+ @Override
+ public Loader<ObjectCursor<Folder>> onCreateLoader(int id, Bundle args) {
+ if (id != LOADER_CHIDREN) {
+ throw new IllegalStateException("ChildFolderLoads loading ID=" + id);
+ }
+ final Uri childUri = Uri.parse(args.getString(CHILD_URI));
+ return new ObjectCursorLoader<Folder>(
+ getActivity(), childUri, projection, Folder.FACTORY);
+ }
+
+ @Override
+ public void onLoadFinished(Loader<ObjectCursor<Folder>> loader, ObjectCursor<Folder> data) {
+ if (data != null && data.getCount() >= 0 && mListAdapter != null) {
+ mListAdapter.updateNestedFolders(data);
+ }
+ }
+
+ @Override
+ public void onLoaderReset(Loader<ObjectCursor<Folder>> loader) {
+ // Do nothing.
+ }
+ }
+
+ /** Callbacks to handle creating a loader and receiving child folders from it. */
+ private final ChildFolderLoads mChildCallback = new ChildFolderLoads();
+
+ /**
+ * Include all the folders at the cursor provided here in the conversation list.
+ * @param cursor The cursor containing child folders for the current folder.
+ */
+ private void showChildFolders(ObjectCursor<Folder> cursor) {
+
+ }
+
+ /**
* Constructor needs to be public to handle orientation changes and activity
* lifecycle events.
*/
@@ -265,26 +313,35 @@
mFooterView.setClickListener(mActivity);
mConversationListView.setActivity(mActivity);
final ConversationCursor conversationCursor = getConversationListCursor();
+ final LoaderManager manager = getLoaderManager();
+
+ // If this a parent folder, load all the child folders.
+ if (mViewContext.folder.hasChildren) {
+ final Uri childUri = mViewContext.folder.childFoldersListUri;
+ final Bundle args = new Bundle();
+ args.putString(ChildFolderLoads.CHILD_URI, childUri.toString());
+ manager.initLoader(ChildFolderLoads.LOADER_CHIDREN, args, mChildCallback);
+ }
final ConversationListHelper helper = mActivity.getConversationListHelper();
final List<ConversationSpecialItemView> specialItemViews = helper != null ?
ImmutableList.copyOf(helper.makeConversationListSpecialViews(
- getActivity(), mAccount, mActivity.getFolderListSelectionListener()))
+ activity, mAccount, mActivity.getFolderSelector()))
: null;
if (specialItemViews != null) {
// Attach to the LoaderManager
for (final ConversationSpecialItemView view : specialItemViews) {
- view.bindLoaderManager(getLoaderManager());
+ view.bindLoaderManager(manager);
}
}
mListAdapter = new AnimatedAdapter(mActivity.getApplicationContext(), conversationCursor,
- mActivity.getSelectedSet(), mActivity, mListView, specialItemViews);
+ mActivity.getSelectedSet(), mActivity, mListView, specialItemViews, null);
mListAdapter.addFooter(mFooterView);
mListView.setAdapter(mListAdapter);
mSelectedSet = mActivity.getSelectedSet();
mListView.setSelectionSet(mSelectedSet);
- mListAdapter.hideFooter();
+ mListAdapter.setFooterVisibility(false);
mFolderObserver = new FolderObserver(){
@Override
public void onChanged(Folder newFolder) {
@@ -521,19 +578,22 @@
*/
@Override
public void onListItemClick(ListView l, View view, int position, long id) {
- // Ignore anything that is not a conversation item. Could be a footer.
- // If we are using a keyboard, the highlighted item is the parent;
- // otherwise, this is a direct call from the ConverationItemView
- if (!(view instanceof ToggleableItem)) {
- return;
- }
- boolean showSenderImage = (mAccount.settings.convListIcon ==
- ConversationListIcon.SENDER_IMAGE);
- if (!showSenderImage && !mSelectedSet.isEmpty()) {
- ToggleableItem v = (ToggleableItem) view;
- v.toggleSelectedState();
+ if (view instanceof NestedFolderView) {
+ final FolderSelector selector = mActivity.getFolderSelector();
+ selector.onFolderSelected(((NestedFolderView) view).getFolder());
+ } else if (view instanceof ToggleableItem) {
+ final boolean showSenderImage =
+ (mAccount.settings.convListIcon == ConversationListIcon.SENDER_IMAGE);
+ if (!showSenderImage && !mSelectedSet.isEmpty()) {
+ ((ToggleableItem) view).toggleSelectedState();
+ } else {
+ viewConversation(position);
+ }
} else {
- viewConversation(position);
+ // Ignore anything that is not a conversation item. Could be a footer.
+ // If we are using a keyboard, the highlighted item is the parent;
+ // otherwise, this is a direct call from the ConverationItemView
+ return;
}
// When a new list item is clicked, commit any existing leave behind
// items. Wait until we have opened the desired conversation to cause
diff --git a/src/com/android/mail/ui/ConversationListHelper.java b/src/com/android/mail/ui/ConversationListHelper.java
index 05c27d0..f7a6862 100644
--- a/src/com/android/mail/ui/ConversationListHelper.java
+++ b/src/com/android/mail/ui/ConversationListHelper.java
@@ -21,7 +21,6 @@
import android.content.Context;
import com.android.mail.providers.Account;
-import com.android.mail.ui.FolderListFragment.FolderListSelectionListener;
import java.util.ArrayList;
@@ -30,7 +29,7 @@
* Creates a list of newly created special views.
*/
public ArrayList<ConversationSpecialItemView> makeConversationListSpecialViews(Context context,
- Account account, FolderListSelectionListener listener) {
+ Account account, FolderSelector listener) {
// TODO: Move conversation photo teaser view here once
// getConversationListIcon() is moved out of Persistence
return Lists.newArrayList();
diff --git a/src/com/android/mail/ui/ConversationPhotoTeaserView.java b/src/com/android/mail/ui/ConversationPhotoTeaserView.java
index 88f148e..74e7667 100644
--- a/src/com/android/mail/ui/ConversationPhotoTeaserView.java
+++ b/src/com/android/mail/ui/ConversationPhotoTeaserView.java
@@ -141,6 +141,12 @@
@Override
+ public boolean acceptsUserTaps() {
+ // No, we don't allow user taps.
+ return false;
+ }
+
+ @Override
public void dismiss() {
setDismissed();
startDestroyAnimation();
diff --git a/src/com/android/mail/ui/ConversationSpecialItemView.java b/src/com/android/mail/ui/ConversationSpecialItemView.java
index 8a25ea2..26aaa44 100644
--- a/src/com/android/mail/ui/ConversationSpecialItemView.java
+++ b/src/com/android/mail/ui/ConversationSpecialItemView.java
@@ -33,8 +33,17 @@
*/
void onUpdate(String account, Folder folder, ConversationCursor cursor);
+ /**
+ * Returns whether this view is to be displayed in the list or not. A view can be added freely
+ * and it might decide to disable itself by returning false here.
+ * @return true if this view should be displayed, false otherwise.
+ */
boolean getShouldDisplayInList();
+ /**
+ * Returns the position (0 indexed) where this element expects to be inserted.
+ * @return
+ */
int getPosition();
void setAdapter(AnimatedAdapter adapter);
@@ -55,4 +64,7 @@
* Called whenever Cab Mode has been entered via long press or selecting a sender image.
*/
void onCabModeEntered();
+
+ /** Returns whether this special view is enabled (= accepts user taps). */
+ boolean acceptsUserTaps();
}
diff --git a/src/com/android/mail/ui/ConversationUpdater.java b/src/com/android/mail/ui/ConversationUpdater.java
index a63ffa8..c3b71af 100644
--- a/src/com/android/mail/ui/ConversationUpdater.java
+++ b/src/com/android/mail/ui/ConversationUpdater.java
@@ -23,7 +23,7 @@
import com.android.mail.browse.ConfirmDialogFragment;
import com.android.mail.browse.ConversationCursor;
-import com.android.mail.browse.MessageCursor.ConversationMessage;
+import com.android.mail.browse.ConversationMessage;
import com.android.mail.providers.Conversation;
import com.android.mail.providers.ConversationInfo;
import com.android.mail.providers.Folder;
diff --git a/src/com/android/mail/ui/ConversationViewFragment.java b/src/com/android/mail/ui/ConversationViewFragment.java
index c119861..6de375e 100644
--- a/src/com/android/mail/ui/ConversationViewFragment.java
+++ b/src/com/android/mail/ui/ConversationViewFragment.java
@@ -17,7 +17,6 @@
package com.android.mail.ui;
-
import android.content.ContentResolver;
import android.content.Context;
import android.content.Loader;
@@ -42,13 +41,13 @@
import android.webkit.WebChromeClient;
import android.webkit.WebSettings;
import android.webkit.WebView;
-import android.webkit.WebViewClient;
import android.widget.TextView;
import com.android.mail.FormattedDateBuilder;
import com.android.mail.R;
import com.android.mail.browse.ConversationContainer;
import com.android.mail.browse.ConversationContainer.OverlayPosition;
+import com.android.mail.browse.ConversationMessage;
import com.android.mail.browse.ConversationOverlayItem;
import com.android.mail.browse.ConversationViewAdapter;
import com.android.mail.browse.ConversationViewAdapter.MessageFooterItem;
@@ -58,7 +57,6 @@
import com.android.mail.browse.ConversationWebView;
import com.android.mail.browse.MailWebView.ContentSizeChangeListener;
import com.android.mail.browse.MessageCursor;
-import com.android.mail.browse.MessageCursor.ConversationMessage;
import com.android.mail.browse.MessageHeaderView;
import com.android.mail.browse.ScrollIndicatorsView;
import com.android.mail.browse.SuperCollapsedBlock;
@@ -71,6 +69,7 @@
import com.android.mail.providers.Message;
import com.android.mail.providers.UIProvider;
import com.android.mail.ui.ConversationViewState.ExpansionState;
+import com.android.mail.utils.ConversationViewUtils;
import com.android.mail.utils.LogTag;
import com.android.mail.utils.LogUtils;
import com.android.mail.utils.Utils;
@@ -84,13 +83,12 @@
import java.util.Map;
import java.util.Set;
-
/**
* The conversation view UI component.
*/
public final class ConversationViewFragment extends AbstractConversationViewFragment implements
- SuperCollapsedBlock.OnClickListener,
- OnLayoutChangeListener {
+ SuperCollapsedBlock.OnClickListener, OnLayoutChangeListener,
+ MessageHeaderView.MessageHeaderViewCallbacks {
private static final String LOG_TAG = LogTag.getLogTag();
public static final String LAYOUT_TAG = "ConvLayout";
@@ -127,14 +125,14 @@
private ScrollIndicatorsView mScrollIndicators;
+ private ConversationViewProgressController mProgressController;
+
private View mNewMessageBar;
private HtmlConversationTemplates mTemplates;
private final MailJsBridge mJsBridge = new MailJsBridge();
- private final WebViewClient mWebViewClient = new ConversationWebViewClient();
-
private ConversationViewAdapter mAdapter;
private boolean mViewsCreated;
@@ -179,7 +177,8 @@
private final DataSetObserver mLoadedObserver = new DataSetObserver() {
@Override
public void onChanged() {
- getHandler().post(new FragmentRunnable("delayedConversationLoad") {
+ getHandler().post(new FragmentRunnable("delayedConversationLoad",
+ ConversationViewFragment.this) {
@Override
public void go() {
LogUtils.d(LOG_TAG, "CVF load observer fired, this=%s",
@@ -190,7 +189,7 @@
}
};
- private final Runnable mOnProgressDismiss = new FragmentRunnable("onProgressDismiss") {
+ private final Runnable mOnProgressDismiss = new FragmentRunnable("onProgressDismiss", this) {
@Override
public void go() {
LogUtils.d(LOG_TAG, "onProgressDismiss go() - isUserVisible() = %b", isUserVisible());
@@ -295,7 +294,7 @@
// the initial primary item
// Then CPC immediately sets the primary item to #10, which tears down #0/#1 and sets up
// #9/#10/#11.
- getHandler().post(new FragmentRunnable("showConversation") {
+ getHandler().post(new FragmentRunnable("showConversation", this) {
@Override
public void go() {
showConversation();
@@ -321,6 +320,8 @@
public void onCreate(Bundle savedState) {
super.onCreate(savedState);
+ mWebViewClient = new ConversationWebViewClient(mAccount);
+
if (savedState != null) {
mWebViewYPercent = savedState.getFloat(BUNDLE_KEY_WEBVIEW_Y_PERCENT);
}
@@ -343,7 +344,8 @@
}
});
- instantiateProgressIndicators(rootView);
+ mProgressController = new ConversationViewProgressController(this, getHandler());
+ mProgressController.instantiateProgressIndicators(rootView);
mWebView = (ConversationWebView) mConversationContainer.findViewById(R.id.webview);
@@ -380,18 +382,7 @@
settings.setJavaScriptEnabled(true);
- final float fontScale = getResources().getConfiguration().fontScale;
- final int desiredFontSizePx = getResources()
- .getInteger(R.integer.conversation_desired_font_size_px);
- final int unstyledFontSizePx = getResources()
- .getInteger(R.integer.conversation_unstyled_font_size_px);
-
- int textZoom = settings.getTextZoom();
- // apply a correction to the default body text style to get regular text to the size we want
- textZoom = textZoom * desiredFontSizePx / unstyledFontSizePx;
- // then apply any system font scaling
- textZoom = (int) (textZoom * fontScale);
- settings.setTextZoom(textZoom);
+ ConversationViewUtils.setTextZoom(getResources(), settings);
mViewsCreated = true;
mWebViewLoadedData = false;
@@ -409,11 +400,6 @@
}
@Override
- protected WebView getWebView() {
- return mWebView;
- }
-
- @Override
public void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
@@ -476,7 +462,7 @@
userVisible);
if (!userVisible) {
- dismissLoadingStatus();
+ mProgressController.dismissLoadingStatus();
} else if (mViewsCreated) {
if (getMessageCursor() != null) {
LogUtils.d(LOG_TAG, "Fragment is now user-visible, onConversationSeen: %s", this);
@@ -543,12 +529,12 @@
// TODO(mindyp): don't show loading status for a previously rendered
// conversation. Ielieve this is better done by making sure don't show loading status
// until XX ms have passed without loading completed.
- showLoadingStatus();
+ mProgressController.showLoadingStatus(isUserVisible());
}
private void revealConversation() {
timerMark("revealing conversation");
- dismissLoadingStatus(mOnProgressDismiss);
+ mProgressController.dismissLoadingStatus(mOnProgressDismiss);
}
private boolean isLoadWaiting() {
@@ -677,6 +663,10 @@
}
prevCollapsedMsg = msg;
prevSafeForImages = safeForImages;
+
+ // This line puts the from address in the address cache so that
+ // we get the sender image for it if it's in a super-collapsed block.
+ getAddress(msg.getFrom());
continue;
}
@@ -745,8 +735,8 @@
for (int i = blockToReplace.getStart(), end = blockToReplace.getEnd(); i <= end; i++) {
cursor.moveToPosition(i);
final ConversationMessage msg = cursor.getMessage();
- final MessageHeaderItem header = mAdapter.newMessageHeaderItem(msg,
- false /* expanded */, mViewState.getShouldShowImages(msg));
+ final MessageHeaderItem header = ConversationViewAdapter.newMessageHeaderItem(
+ mAdapter, msg, false /* expanded */, mViewState.getShouldShowImages(msg));
final MessageFooterItem footer = mAdapter.newMessageFooterItem(header);
final int headerPx = measureOverlayHeight(header);
@@ -986,6 +976,10 @@
}
private class ConversationWebViewClient extends AbstractConversationWebViewClient {
+ public ConversationWebViewClient(Account account) {
+ super(account);
+ }
+
@Override
public void onPageFinished(WebView view, String url) {
// Ignore unsafe calls made after a fragment is detached from an activity.
@@ -1015,8 +1009,8 @@
for (Address addr : cacheCopy) {
emailAddresses.add(addr.getAddress());
}
- ContactLoaderCallbacks callbacks = getContactInfoSource();
- getContactInfoSource().setSenders(emailAddresses);
+ final ContactLoaderCallbacks callbacks = getContactInfoSource();
+ callbacks.setSenders(emailAddresses);
getLoaderManager().restartLoader(CONTACT_LOADER, Bundle.EMPTY, callbacks);
}
@@ -1037,7 +1031,8 @@
@JavascriptInterface
public void onWebContentGeometryChange(final String[] overlayTopStrs,
final String[] overlayBottomStrs) {
- getHandler().post(new FragmentRunnable("onWebContentGeometryChange") {
+ getHandler().post(new FragmentRunnable("onWebContentGeometryChange",
+ ConversationViewFragment.this) {
@Override
public void go() {
@@ -1134,7 +1129,8 @@
@SuppressWarnings("unused")
@JavascriptInterface
public void onContentReady() {
- getHandler().post(new FragmentRunnable("onContentReady") {
+ getHandler().post(new FragmentRunnable("onContentReady",
+ ConversationViewFragment.this) {
@Override
public void go() {
try {
diff --git a/src/com/android/mail/ui/ConversationViewProgressController.java b/src/com/android/mail/ui/ConversationViewProgressController.java
new file mode 100644
index 0000000..9bce381
--- /dev/null
+++ b/src/com/android/mail/ui/ConversationViewProgressController.java
@@ -0,0 +1,153 @@
+/*
+ * Copyright (C) 2013 Google Inc.
+ * Licensed to The Android Open Source Project.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.mail.ui;
+
+import android.animation.Animator;
+import android.animation.AnimatorInflater;
+import android.animation.AnimatorListenerAdapter;
+import android.app.Fragment;
+import android.content.res.Resources;
+import android.os.Handler;
+import android.view.View;
+
+import com.android.mail.R;
+import com.android.mail.utils.Utils;
+
+/**
+ * <p>Controller to handle showing and hiding progress in the conversation views.</p>
+ * <br>
+ * <b>NOTE:</b> This class makes several assumptions about the view hierarchy
+ * of the conversation view. Use with care.
+ */
+public class ConversationViewProgressController {
+ private static int sMinDelay = -1;
+ private static int sMinShowTime = -1;
+
+ private final Handler mHandler;
+
+ private final Fragment mFragment;
+
+ private long mLoadingShownTime = -1;
+ private View mProgressView;
+ private View mBackgroundView;
+
+ private final Runnable mDelayedShow;
+
+ public ConversationViewProgressController(Fragment fragment, Handler handler) {
+ mFragment = fragment;
+ mHandler = handler;
+
+ mDelayedShow = new FragmentRunnable("mDelayedShow", mFragment) {
+ @Override
+ public void go() {
+ mLoadingShownTime = System.currentTimeMillis();
+ mProgressView.setVisibility(View.VISIBLE);
+ }
+ };
+ }
+
+ public void instantiateProgressIndicators(View rootView) {
+ mBackgroundView = rootView.findViewById(R.id.background_view);
+ mProgressView = rootView.findViewById(R.id.loading_progress);
+ }
+
+ public void showLoadingStatus(boolean isFragmentUserVisible) {
+ if (!isFragmentUserVisible) {
+ return;
+ }
+ if (sMinDelay == -1) {
+ Resources res = mFragment.getResources();
+ sMinDelay = res.getInteger(R.integer.conversationview_show_loading_delay);
+ sMinShowTime = res.getInteger(R.integer.conversationview_min_show_loading);
+ }
+ // If the loading view isn't already showing, show it and remove any
+ // pending calls to show the loading screen.
+ mBackgroundView.setVisibility(View.VISIBLE);
+ mHandler.removeCallbacks(mDelayedShow);
+ mHandler.postDelayed(mDelayedShow, sMinDelay);
+ }
+
+ protected void dismissLoadingStatus() {
+ dismissLoadingStatus(null);
+ }
+
+ /**
+ * Begin the fade-out animation to hide the Progress overlay, either immediately or after some
+ * timeout (to ensure that the progress minimum time elapses).
+ *
+ * @param doAfter an optional Runnable action to execute after the animation completes
+ */
+ protected void dismissLoadingStatus(final Runnable doAfter) {
+ if (mLoadingShownTime == -1) {
+ // The runnable hasn't run yet, so just remove it.
+ mHandler.removeCallbacks(mDelayedShow);
+ dismiss(doAfter);
+ return;
+ }
+ final long diff = Math.abs(System.currentTimeMillis() - mLoadingShownTime);
+ if (diff > sMinShowTime) {
+ dismiss(doAfter);
+ } else {
+ mHandler.postDelayed(new FragmentRunnable("dismissLoadingStatus", mFragment) {
+ @Override
+ public void go() {
+ dismiss(doAfter);
+ }
+ }, Math.abs(sMinShowTime - diff));
+ }
+ }
+
+ private void dismiss(final Runnable doAfter) {
+ // Reset loading shown time.
+ mLoadingShownTime = -1;
+ mProgressView.setVisibility(View.GONE);
+ if (mBackgroundView.getVisibility() == View.VISIBLE) {
+ animateDismiss(doAfter);
+ } else {
+ if (doAfter != null) {
+ doAfter.run();
+ }
+ }
+ }
+
+ private void animateDismiss(final Runnable doAfter) {
+ // the animation can only work (and is only worth doing) if this fragment is added
+ // reasons it may not be added: fragment is being destroyed, or in the process of being
+ // restored
+ if (!mFragment.isAdded()) {
+ mBackgroundView.setVisibility(View.GONE);
+ return;
+ }
+
+ Utils.enableHardwareLayer(mBackgroundView);
+ final Animator animator = AnimatorInflater.loadAnimator(
+ mFragment.getActivity().getApplicationContext(), R.anim.fade_out);
+ animator.setTarget(mBackgroundView);
+ animator.addListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ mBackgroundView.setVisibility(View.GONE);
+ mBackgroundView.setLayerType(View.LAYER_TYPE_NONE, null);
+ if (doAfter != null) {
+ doAfter.run();
+ }
+ }
+ });
+ animator.start();
+ }
+}
diff --git a/src/com/android/mail/ui/DrawerFragment.java b/src/com/android/mail/ui/DrawerFragment.java
index 85436e4..d2c3227 100644
--- a/src/com/android/mail/ui/DrawerFragment.java
+++ b/src/com/android/mail/ui/DrawerFragment.java
@@ -17,14 +17,40 @@
package com.android.mail.ui;
+import android.widget.ListView;
+
/**
* A drawer that is shown in one pane mode, as a pull-out from the left. All the
* implementation is inherited from the FolderListFragment.
+ *
+ * The drawer shows a list of accounts, the recent folders, and a list of top-level folders for
+ * the given account. This fragment is created using no arguments, it gets all its state from the
+ * controller in {@link #onActivityCreated(android.os.Bundle)}. In particular, it gets the current
+ * account, the list of accounts, and the current folder from the {@link ControllableActivity}.
+ *
+ * Once it has this information, the drawer sets itself up to observe for changes and allows the
+ * user to change folders and accounts.
+ *
+ * The drawer is always instantiated through XML resources: in one_pane_activity.xml and in
+ * two_pane_activity.xml
*/
public class DrawerFragment extends FolderListFragment {
+ /**
+ * The only way a drawer is constructed is through XML layouts, and so it needs no constructor
+ * like {@link FolderListFragment#ofTopLevelTree(android.net.Uri, java.util.ArrayList}
+ */
public DrawerFragment() {
super();
- // Drawer is always sectioned.
- mIsSectioned = true;
+ // Drawer is always divided: it shows groups for inboxes, recent folders and all other
+ // folders.
+ mIsDivided = true;
+ // The drawer also switches accounts, so don't hide accounts.
+ mHideAccounts = false;
+ }
+
+ @Override
+ protected int getListViewChoiceMode() {
+ // Always let one item be selected
+ return ListView.CHOICE_MODE_SINGLE;
}
}
diff --git a/src/com/android/mail/ui/FolderDisplayer.java b/src/com/android/mail/ui/FolderDisplayer.java
index 4c4a68c..2e12074 100644
--- a/src/com/android/mail/ui/FolderDisplayer.java
+++ b/src/com/android/mail/ui/FolderDisplayer.java
@@ -32,7 +32,7 @@
/**
* Used to generate folder display information given a raw folders string.
- * (The raw folders string can be obtained from {@link Conversation#rawFolders}.)
+ * (The raw folders string can be obtained from {@link Conversation#getRawFolders()}.)
*
*/
public class FolderDisplayer {
@@ -51,16 +51,26 @@
}
/**
- * Configure the FolderDisplayer object by parsing the rawFolders string.
+ * Configure the FolderDisplayer object by filtering and copying from the list of raw folders.
*
- * @param foldersString string containing serialized folders to display.
+ * @param conv {@link Conversation} containing the folders to display.
* @param ignoreFolderUri (optional) folder to omit from the displayed set
* @param ignoreFolderType -1, or the {@link FolderType} to omit from the displayed set
*/
public void loadConversationFolders(Conversation conv, final Uri ignoreFolderUri,
final int ignoreFolderType) {
mFoldersSortedSet.clear();
- mFoldersSortedSet.addAll(conv.getRawFoldersForDisplay(ignoreFolderUri, ignoreFolderType));
+ for (Folder folder : conv.getRawFolders()) {
+ // Skip the ignoreFolderType
+ if (ignoreFolderType >= 0 && folder.isType(ignoreFolderType)) {
+ continue;
+ }
+ // skip the ignoreFolder
+ if (ignoreFolderUri != null && ignoreFolderUri.equals(folder.uri)) {
+ continue;
+ }
+ mFoldersSortedSet.add(folder);
+ }
}
/**
diff --git a/src/com/android/mail/ui/FolderItemView.java b/src/com/android/mail/ui/FolderItemView.java
index 329f59f..ef0cfee 100644
--- a/src/com/android/mail/ui/FolderItemView.java
+++ b/src/com/android/mail/ui/FolderItemView.java
@@ -15,47 +15,29 @@
*/
package com.android.mail.ui;
-import com.android.mail.R;
-
+import android.content.Context;
+import android.graphics.Color;
+import android.util.AttributeSet;
+import android.view.DragEvent;
+import android.view.View;
import android.widget.ImageView;
+import android.widget.RelativeLayout;
import android.widget.TextView;
+import com.android.mail.R;
import com.android.mail.providers.Account;
import com.android.mail.providers.Folder;
import com.android.mail.utils.LogTag;
import com.android.mail.utils.LogUtils;
import com.android.mail.utils.Utils;
-import android.content.Context;
-import android.content.res.ColorStateList;
-import android.content.res.Resources;
-import android.graphics.Bitmap;
-import android.graphics.BitmapFactory;
-import android.graphics.Color;
-import android.graphics.drawable.Drawable;
-import android.util.AttributeSet;
-import android.view.DragEvent;
-import android.view.View;
-import android.widget.RelativeLayout;
-
/**
* The view for each folder in the folder list.
*/
public class FolderItemView extends RelativeLayout {
private final String LOG_TAG = LogTag.getLogTag();
- // Static colors
- private static int NON_DROPPABLE_TARGET_TEXT_COLOR;
- // Static bitmap
- private static Bitmap SHORTCUT_ICON;
-
- // These are fine to be static, as these Drawables only have one state
- private static Drawable DROPPABLE_HOVER_BACKGROUND;
- private static Drawable DRAG_STEADY_STATE_BACKGROUND;
-
- private Drawable mBackground;
- private ColorStateList mInitialFolderTextColor;
- private ColorStateList mInitialUnreadCountTextColor;
+ private static final int[] STATE_DRAG_MODE = {R.attr.state_drag_mode};
private Folder mFolder;
private TextView mFolderTextView;
@@ -64,6 +46,8 @@
private DropHandler mDropHandler;
private ImageView mFolderParentIcon;
+ private boolean mIsDragMode;
+
/**
* A delegate for a handler to handle a drop of an item.
*/
@@ -91,28 +75,17 @@
public FolderItemView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
+
+ mIsDragMode = false;
}
@Override
protected void onFinishInflate() {
super.onFinishInflate();
- if (SHORTCUT_ICON == null) {
- final Resources res = getResources();
- SHORTCUT_ICON = BitmapFactory.decodeResource(
- res, R.mipmap.ic_launcher_shortcut_folder);
- DROPPABLE_HOVER_BACKGROUND =
- res.getDrawable(R.drawable.folder_drag_target);
- DRAG_STEADY_STATE_BACKGROUND =
- res.getDrawable(R.drawable.folder_no_hover);
- NON_DROPPABLE_TARGET_TEXT_COLOR =
- res.getColor(R.color.folder_disabled_drop_target_text_color);
- }
+
mFolderTextView = (TextView)findViewById(R.id.name);
mUnreadCountTextView = (TextView)findViewById(R.id.unread);
mUnseenCountTextView = (TextView)findViewById(R.id.unseen);
- mBackground = getBackground();
- mInitialFolderTextColor = mFolderTextView.getTextColors();
- mInitialUnreadCountTextColor = mUnreadCountTextView.getTextColors();
mFolderParentIcon = (ImageView) findViewById(R.id.folder_parent_icon);
}
@@ -221,43 +194,20 @@
public boolean onDragEvent(DragEvent event) {
switch (event.getAction()) {
case DragEvent.ACTION_DRAG_STARTED:
- // If this folder is not a drop target, dim the text.
- if (!isDroppableTarget(event)) {
- // Make sure we update this at the time we drop on the target.
- mInitialFolderTextColor = mFolderTextView.getTextColors();
- mInitialUnreadCountTextColor = mUnreadCountTextView.getTextColors();
- mFolderTextView.setTextColor(NON_DROPPABLE_TARGET_TEXT_COLOR);
- mUnreadCountTextView.setTextColor(NON_DROPPABLE_TARGET_TEXT_COLOR);
- }
- // Set the background to a steady state background.
- setBackgroundDrawable(DRAG_STEADY_STATE_BACKGROUND);
- return true;
-
+ // Set drag mode state to true now that we have entered drag mode.
+ // This change updates the states of icons and text colors.
+ // Additional drawable states are updated by the framework
+ // based on the DragEvent.
+ setDragMode(true);
case DragEvent.ACTION_DRAG_ENTERED:
- // Change background color to indicate this folder is the drop target.
- if (isDroppableTarget(event)) {
- setBackgroundDrawable(DROPPABLE_HOVER_BACKGROUND);
- return true;
- }
- break;
-
case DragEvent.ACTION_DRAG_EXITED:
- // If this is a droppable target, make sure that it is set back to steady state,
- // when the drag leaves the view.
- if (isDroppableTarget(event)) {
- setBackgroundDrawable(DRAG_STEADY_STATE_BACKGROUND);
- return true;
- }
- break;
-
+ // All of these states return based on isDroppableTarget's return value.
+ // If modifying, watch the switch's drop-through effects.
+ return isDroppableTarget(event);
case DragEvent.ACTION_DRAG_ENDED:
- // Reset the text of the non draggable views back to the color it had been..
- if (!isDroppableTarget(event)) {
- mFolderTextView.setTextColor(mInitialFolderTextColor);
- mUnreadCountTextView.setTextColor(mInitialUnreadCountTextColor);
- }
- // Restore the background of the view.
- setBackgroundDrawable(mBackground);
+ // Set drag mode to false since we're leaving drag mode.
+ // Updates all the states of icons and text colors back to non-drag values.
+ setDragMode(false);
return true;
case DragEvent.ACTION_DRAG_LOCATION:
@@ -273,4 +223,18 @@
}
return false;
}
+
+ @Override
+ protected int[] onCreateDrawableState(int extraSpace) {
+ final int[] drawableState = super.onCreateDrawableState(extraSpace + 1);
+ if (mIsDragMode) {
+ mergeDrawableStates(drawableState, STATE_DRAG_MODE);
+ }
+ return drawableState;
+ }
+
+ private void setDragMode(boolean isDragMode) {
+ mIsDragMode = isDragMode;
+ refreshDrawableState();
+ }
}
diff --git a/src/com/android/mail/ui/FolderListFragment.java b/src/com/android/mail/ui/FolderListFragment.java
index 6e10fa3..dc4c5ec 100644
--- a/src/com/android/mail/ui/FolderListFragment.java
+++ b/src/com/android/mail/ui/FolderListFragment.java
@@ -21,7 +21,6 @@
import android.app.ListFragment;
import android.app.LoaderManager;
import android.content.Loader;
-import android.database.Cursor;
import android.net.Uri;
import android.os.Bundle;
import android.view.LayoutInflater;
@@ -55,7 +54,33 @@
import java.util.List;
/**
- * The folder list UI component.
+ * This fragment shows the list of folders and the list of accounts. Prior to June 2013,
+ * the mail application had a spinner in the top action bar. Now, the list of accounts is displayed
+ * in a drawer along with the list of folders.
+ *
+ * This class has the following use-cases:
+ * <ul>
+ * <li>
+ * Show a list of accounts and a divided list of folders. In this case, the list shows
+ * Accounts, Inboxes, Recent Folders, All folders.
+ * Tapping on Accounts takes the user to the default Inbox for that account. Tapping on
+ * folders switches folders.
+ * This is created through XML resources as a {@link DrawerFragment}. Since it is created
+ * through resources, it receives all arguments through callbacks.
+ * </li>
+ * <li>
+ * Show a list of folders for a specific level. At the top-level, this shows Inbox, Sent,
+ * Drafts, Starred, and any user-created folders. For providers that allow nested folders,
+ * this will only show the folders at the top-level.
+ * <br /> Tapping on a parent folder creates a new fragment with the child folders at
+ * that level.
+ * </li>
+ * <li>
+ * Shows a list of folders that can be turned into widgets/shortcuts. This is used by the
+ * {@link FolderSelectionActivity} to allow the user to create a shortcut or widget for
+ * any folder for a given account.
+ * </li>
+ * </ul>
*/
public class FolderListFragment extends ListFragment implements
LoaderManager.LoaderCallbacks<ObjectCursor<Folder>> {
@@ -66,16 +91,22 @@
private ListView mListView;
/** URI that points to the list of folders for the current account. */
private Uri mFolderListUri;
- /** True if you want a sectioned FolderList, false otherwise. */
- protected boolean mIsSectioned;
+ /**
+ * True if you want a divided FolderList. A divided folder list shows the following groups:
+ * Inboxes, Recent Folders, All folders.
+ *
+ * An undivided FolderList shows all folders without any divisions and without recent folders.
+ * This is true only for the drawer: for all others it is false.
+ */
+ protected boolean mIsDivided = false;
/** True if the folder list belongs to a folder selection activity (one account only) */
- private boolean mHideAccounts;
+ protected boolean mHideAccounts = true;
/** An {@link ArrayList} of {@link FolderType}s to exclude from displaying. */
private ArrayList<Integer> mExcludedFolderTypes;
/** Object that changes folders on our behalf. */
- private FolderListSelectionListener mFolderChanger;
+ private FolderSelector mFolderChanger;
/** Object that changes accounts on our behalf */
- private AccountController mAccountChanger;
+ private AccountController mAccountController;
/** The currently selected folder (the folder being viewed). This is never null. */
private Uri mSelectedFolderUri = Uri.EMPTY;
@@ -87,26 +118,15 @@
/** Parent of the current folder, or null if the current folder is not a child. */
private Folder mParentFolder;
- private static final int FOLDER_LOADER_ID = 0;
+ private static final int FOLDER_LIST_LOADER_ID = 0;
+ /** Loader id for the full list of folders in the account */
+ private static final int FULL_FOLDER_LIST_LOADER_ID = 1;
/** Key to store {@link #mParentFolder}. */
private static final String ARG_PARENT_FOLDER = "arg-parent-folder";
- /** Key to store {@link #mIsSectioned} */
- private static final String ARG_IS_SECTIONED = "arg-is-sectioned";
/** Key to store {@link #mFolderListUri}. */
private static final String ARG_FOLDER_LIST_URI = "arg-folder-list-uri";
/** Key to store {@link #mExcludedFolderTypes} */
private static final String ARG_EXCLUDED_FOLDER_TYPES = "arg-excluded-folder-types";
- /** Key to store {@link #mType} */
- private static final String ARG_TYPE = "arg-flf-type";
- /** Key to store {@link #mHideAccounts} */
- private static final String ARG_HIDE_ACCOUNTS = "arg-hide-accounts";
-
- /** Either {@link #TYPE_DRAWER} for drawers or {@link #TYPE_TREE} for hierarchy trees */
- private int mType;
- /** This fragment is a drawer */
- private static final int TYPE_DRAWER = 0;
- /** This fragment is a folder tree */
- private static final int TYPE_TREE = 1;
private static final String BUNDLE_LIST_STATE = "flf-list-state";
private static final String BUNDLE_SELECTED_FOLDER = "flf-selected-folder";
@@ -156,75 +176,52 @@
}
/**
- * Creates a new instance of {@link FolderListFragment}. Gets the current account and current
- * folder through observers.
- */
- public static FolderListFragment ofDrawer() {
- final FolderListFragment fragment = new FolderListFragment();
- // The drawer is always sectioned
- final boolean isSectioned = true;
- fragment.setArguments(getBundleFromArgs(TYPE_DRAWER, null, null, isSectioned, null, false));
- return fragment;
- }
-
- /**
* Creates a new instance of {@link FolderListFragment}, initialized
* to display the folder and its immediate children.
* @param folder parent folder whose children are shown
- * @param hideAccounts True if accounts should be hidden, false otherwise
+ *
*/
- public static FolderListFragment ofTree(Folder folder, final boolean hideAccounts) {
+ public static FolderListFragment ofTree(Folder folder) {
final FolderListFragment fragment = new FolderListFragment();
- // Trees are never sectioned.
- final boolean isSectioned = false;
- fragment.setArguments(getBundleFromArgs(TYPE_TREE, folder, folder.childFoldersListUri,
- isSectioned, null, hideAccounts));
+ fragment.setArguments(getBundleFromArgs(folder, folder.childFoldersListUri, null));
return fragment;
}
/**
* Creates a new instance of {@link FolderListFragment}, initialized
- * to display the folder and its immediate children.
+ * to display the top level: where we have no parent folder, but we have a list of folders
+ * from the account.
* @param folderListUri the URI which contains all the list of folders
* @param excludedFolderTypes A list of {@link FolderType}s to exclude from displaying
- * @param hideAccounts True if accounts should be hidden, false otherwise
*/
public static FolderListFragment ofTopLevelTree(Uri folderListUri,
- final ArrayList<Integer> excludedFolderTypes, final boolean hideAccounts) {
+ final ArrayList<Integer> excludedFolderTypes) {
final FolderListFragment fragment = new FolderListFragment();
- // Trees are never sectioned.
- final boolean isSectioned = false;
- fragment.setArguments(getBundleFromArgs(TYPE_TREE, null, folderListUri,
- isSectioned, excludedFolderTypes, hideAccounts));
+ fragment.setArguments(getBundleFromArgs(null, folderListUri, excludedFolderTypes));
return fragment;
}
/**
* Construct a bundle that represents the state of this fragment.
- * @param type the type of FLF: {@link #TYPE_DRAWER} or {@link #TYPE_TREE}
+ *
* @param parentFolder non-null for trees, the parent of this list
- * @param isSectioned true if this drawer is sectioned, false otherwise
* @param folderListUri the URI which contains all the list of folders
* @param excludedFolderTypes if non-null, this indicates folders to exclude in lists.
- * @return Bundle containing parentFolder, sectioned list boolean and
+ * @return Bundle containing parentFolder, divided list boolean and
* excluded folder types
*/
- private static Bundle getBundleFromArgs(int type, Folder parentFolder, Uri folderListUri,
- boolean isSectioned, final ArrayList<Integer> excludedFolderTypes,
- final boolean hideAccounts) {
+ private static Bundle getBundleFromArgs(Folder parentFolder, Uri folderListUri,
+ final ArrayList<Integer> excludedFolderTypes) {
final Bundle args = new Bundle();
- args.putInt(ARG_TYPE, type);
if (parentFolder != null) {
args.putParcelable(ARG_PARENT_FOLDER, parentFolder);
}
if (folderListUri != null) {
args.putString(ARG_FOLDER_LIST_URI, folderListUri.toString());
}
- args.putBoolean(ARG_IS_SECTIONED, isSectioned);
if (excludedFolderTypes != null) {
args.putIntegerArrayList(ARG_EXCLUDED_FOLDER_TYPES, excludedFolderTypes);
}
- args.putBoolean(ARG_HIDE_ACCOUNTS, hideAccounts);
return args;
}
@@ -237,10 +234,10 @@
// activity is creating ConversationListFragments. This activity must be of type
// ControllableActivity.
final Activity activity = getActivity();
- Folder currentFolder = null;
if (! (activity instanceof ControllableActivity)){
LogUtils.wtf(LOG_TAG, "FolderListFragment expects only a ControllableActivity to" +
"create it. Cannot proceed.");
+ return;
}
mActivity = (ControllableActivity) activity;
final FolderController controller = mActivity.getFolderController();
@@ -251,10 +248,13 @@
setSelectedFolder(newFolder);
}
};
+ final Folder currentFolder;
if (controller != null) {
// Only register for selected folder updates if we have a controller.
currentFolder = mFolderObserver.initialize(controller);
mCurrentFolderForUnreadCheck = currentFolder;
+ } else {
+ currentFolder = null;
}
// Initialize adapter for folder/heirarchical list. Note this relies on
@@ -264,7 +264,7 @@
mCursorAdapter = new HierarchicalFolderListAdapter(null, mParentFolder);
selectedFolder = mActivity.getHierarchyFolder();
} else {
- mCursorAdapter = new FolderListAdapter(mIsSectioned);
+ mCursorAdapter = new FolderListAdapter(mIsDivided);
selectedFolder = currentFolder;
}
// Is the selected folder fresher than the one we have restored from a bundle?
@@ -280,7 +280,7 @@
setSelectedAccount(newAccount);
}
};
- mFolderChanger = mActivity.getFolderListSelectionListener();
+ mFolderChanger = mActivity.getFolderSelector();
if (accountController != null) {
// Current account and its observer.
setSelectedAccount(mAccountObserver.initialize(accountController));
@@ -292,7 +292,7 @@
}
};
mAllAccountsObserver.initialize(accountController);
- mAccountChanger = accountController;
+ mAccountController = accountController;
// Observer for when the drawer is closed
mDrawerObserver = new DrawerClosedObserver() {
@@ -301,12 +301,11 @@
// First, check if there's a folder to change to
if (mNextFolder != null) {
mFolderChanger.onFolderSelected(mNextFolder);
- // Wait for an update to the current folder. When we get the next folder,
- // then we null it out.
+ mNextFolder = null;
}
// Next, check if there's an account to change to
if (mNextAccount != null) {
- mAccountChanger.switchToDefaultInboxOrChangeAccount(mNextAccount);
+ mAccountController.switchToDefaultInboxOrChangeAccount(mNextAccount);
mNextAccount = null;
}
}
@@ -319,40 +318,34 @@
return;
}
+ mListView.setChoiceMode(getListViewChoiceMode());
+
setListAdapter(mCursorAdapter);
}
/**
* Set the instance variables from the arguments provided here.
- * @param args
+ * @param args bundle of arguments with keys named ARG_*
*/
private void setInstanceFromBundle(Bundle args) {
if (args == null) {
return;
}
- mParentFolder = (Folder) args.getParcelable(ARG_PARENT_FOLDER);
+ mParentFolder = args.getParcelable(ARG_PARENT_FOLDER);
final String folderUri = args.getString(ARG_FOLDER_LIST_URI);
- if (folderUri == null) {
- mFolderListUri = Uri.EMPTY;
- } else {
+ if (folderUri != null) {
mFolderListUri = Uri.parse(folderUri);
}
- mIsSectioned = args.getBoolean(ARG_IS_SECTIONED);
mExcludedFolderTypes = args.getIntegerArrayList(ARG_EXCLUDED_FOLDER_TYPES);
- mType = args.getInt(ARG_TYPE);
- mHideAccounts = args.getBoolean(ARG_HIDE_ACCOUNTS, false);
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedState) {
- final Bundle args = getArguments();
- if (args != null) {
- mHideAccounts = args.getBoolean(ARG_HIDE_ACCOUNTS, false);
- }
+ setInstanceFromBundle(getArguments());
+
final View rootView = inflater.inflate(R.layout.folder_list, null);
mListView = (ListView) rootView.findViewById(android.R.id.list);
- mListView.setChoiceMode(ListView.CHOICE_MODE_SINGLE);
mListView.setEmptyView(null);
mListView.setDivider(null);
if (savedState != null && savedState.containsKey(BUNDLE_LIST_STATE)) {
@@ -438,7 +431,7 @@
// Switching accounts takes you to the default inbox for that account.
mSelectedFolderType = DrawerItem.FOLDER_INBOX;
mNextAccount = account;
- mAccountChanger.closeDrawer(true, mNextAccount, getDefaultInbox(mNextAccount));
+ mAccountController.closeDrawer(true, mNextAccount, getDefaultInbox(mNextAccount));
}
/**
@@ -465,7 +458,7 @@
mListView.setItemChecked(defaultInboxPosition, true);
}
// ... and close the drawer (no new target folders/accounts)
- mAccountChanger.closeDrawer(false, mNextAccount,
+ mAccountController.closeDrawer(false, mNextAccount,
getDefaultInbox(mNextAccount));
} else {
changeAccount(account);
@@ -484,8 +477,6 @@
}
} else if (item instanceof Folder) {
folder = (Folder) item;
- } else if (item instanceof ObjectCursor){
- folder = ((ObjectCursor<Folder>) item).getModel();
} else {
// Don't know how we got here.
LogUtils.wtf(LOG_TAG, "viewFolderOrChangeAccount(): invalid item");
@@ -502,10 +493,10 @@
// Go to the conversation list for this folder.
if (!folder.uri.equals(mSelectedFolderUri)) {
mNextFolder = folder;
- mAccountChanger.closeDrawer(true, nextAccount, folder);
+ mAccountController.closeDrawer(true, nextAccount, folder);
} else {
// Clicked on same folder, just close drawer
- mAccountChanger.closeDrawer(false, nextAccount, folder);
+ mAccountController.closeDrawer(false, nextAccount, folder);
}
}
}
@@ -514,17 +505,16 @@
public Loader<ObjectCursor<Folder>> onCreateLoader(int id, Bundle args) {
mListView.setEmptyView(null);
final Uri folderListUri;
- if (mType == TYPE_TREE) {
- // Folder trees, they specify a URI at construction time.
- folderListUri = mFolderListUri;
- } else if (mType == TYPE_DRAWER) {
- // Drawers should have a valid account
- if (mCurrentAccount != null) {
- folderListUri = mCurrentAccount.folderListUri;
+ if (id == FOLDER_LIST_LOADER_ID) {
+ if (mFolderListUri != null) {
+ // Folder trees, they specify a URI at construction time.
+ folderListUri = mFolderListUri;
} else {
- LogUtils.wtf(LOG_TAG, "FLF.onCreateLoader() for Drawer with null account");
- return null;
+ // Drawers get the folder list from the current account.
+ folderListUri = mCurrentAccount.folderListUri;
}
+ } else if (id == FULL_FOLDER_LIST_LOADER_ID) {
+ folderListUri = mCurrentAccount.fullFolderListUri;
} else {
LogUtils.wtf(LOG_TAG, "FLF.onCreateLoader() with weird type");
return null;
@@ -536,14 +526,22 @@
@Override
public void onLoadFinished(Loader<ObjectCursor<Folder>> loader, ObjectCursor<Folder> data) {
if (mCursorAdapter != null) {
- mCursorAdapter.setCursor(data);
+ if (loader.getId() == FOLDER_LIST_LOADER_ID) {
+ mCursorAdapter.setCursor(data);
+ } else if (loader.getId() == FULL_FOLDER_LIST_LOADER_ID) {
+ mCursorAdapter.setFullFolderListCursor(data);
+ }
}
}
@Override
public void onLoaderReset(Loader<ObjectCursor<Folder>> loader) {
if (mCursorAdapter != null) {
- mCursorAdapter.setCursor(null);
+ if (loader.getId() == FOLDER_LIST_LOADER_ID) {
+ mCursorAdapter.setCursor(null);
+ } else if (loader.getId() == FULL_FOLDER_LIST_LOADER_ID) {
+ mCursorAdapter.setFullFolderListCursor(null);
+ }
}
}
@@ -565,14 +563,14 @@
private interface FolderListFragmentCursorAdapter extends ListAdapter {
/** Update the folder list cursor with the cursor given here. */
void setCursor(ObjectCursor<Folder> cursor);
+ /** Update the full folder list cursor with the cursor given here. */
+ void setFullFolderListCursor(ObjectCursor<Folder> cursor);
/**
* Given an item, find the type of the item, which should only be {@link
* DrawerItem#VIEW_FOLDER} or {@link DrawerItem#VIEW_ACCOUNT}
* @return item the type of the item.
*/
int getItemType(DrawerItem item);
- /** Get the folder associated with this item. **/
- Folder getFullFolder(DrawerItem item);
/** Notify that the all accounts changed. */
void notifyAllAccountsChanged();
/** Remove all observers and destroy the object. */
@@ -591,7 +589,7 @@
private final RecentFolderObserver mRecentFolderObserver = new RecentFolderObserver() {
@Override
public void onChanged() {
- if (!isCursorInvalid(mCursor)) {
+ if (!isCursorInvalid()) {
recalculateList();
}
}
@@ -600,12 +598,15 @@
private static final int NO_HEADER_RESOURCE = -1;
/** Cache of most recently used folders */
private final RecentFolderList mRecentFolders;
- /** True if the list is sectioned, false otherwise */
- private final boolean mIsSectioned;
+ /** True if the list is divided, false otherwise. See the comment on
+ * {@link FolderListFragment#mIsDivided} for more information */
+ private final boolean mIsDivided;
/** All the items */
private List<DrawerItem> mItemList = new ArrayList<DrawerItem>();
/** Cursor into the folder list. This might be null. */
private ObjectCursor<Folder> mCursor = null;
+ /** Cursor into the full folder list. This might be null. */
+ private ObjectCursor<Folder> mFullFolderListCursor = null;
/** Watcher for tracking and receiving unread counts for mail */
private FolderWatcher mFolderWatcher = null;
private boolean mRegistered = false;
@@ -613,13 +614,14 @@
/**
* Creates a {@link FolderListAdapter}.This is a list of all the accounts and folders.
*
- * @param isSectioned true if folder list is flat, false if sectioned by label group
+ * @param isDivided true if folder list is flat, false if divided by label group. See
+ * the comments on {@link #mIsDivided} for more information
*/
- public FolderListAdapter(boolean isSectioned) {
+ public FolderListAdapter(boolean isDivided) {
super();
- mIsSectioned = isSectioned;
+ mIsDivided = isDivided;
final RecentFolderController controller = mActivity.getRecentFolderController();
- if (controller != null && mIsSectioned) {
+ if (controller != null && mIsDivided) {
mRecentFolders = mRecentFolderObserver.initialize(controller);
} else {
mRecentFolders = null;
@@ -630,9 +632,9 @@
@Override
public void notifyAllAccountsChanged() {
- if (!mRegistered && mAccountChanger != null) {
+ if (!mRegistered && mAccountController != null) {
// TODO(viki): Round-about way of setting the watcher. http://b/8750610
- mAccountChanger.setFolderWatcher(mFolderWatcher);
+ mAccountController.setFolderWatcher(mFolderWatcher);
mRegistered = true;
}
mFolderWatcher.updateAccountList(getAllAccounts());
@@ -680,12 +682,7 @@
@Override
public boolean isEnabled(int position) {
final DrawerItem drawerItem = ((DrawerItem) getItem(position));
- if (drawerItem == null) {
- // If there is no item, return false as there's nothing there to be enabled
- return false;
- } else {
- return drawerItem.isItemEnabled();
- }
+ return drawerItem != null && drawerItem.isItemEnabled();
}
private Uri getCurrentAccountUri() {
@@ -764,14 +761,14 @@
// If we are waiting for folder initialization, we don't have any kinds of folders,
// just the "Waiting for initialization" item. Note, this should only be done
// when we're waiting for account initialization or initial sync.
- if (isCursorInvalid(mCursor)) {
+ if (isCursorInvalid()) {
if(!mCurrentAccount.isAccountReady()) {
- itemList.add(DrawerItem.forWaitView(mActivity));
+ itemList.add(DrawerItem.ofWaitView(mActivity));
}
return;
}
- if (!mIsSectioned) {
+ if (!mIsDivided) {
// Adapter for a flat list. Everything is a FOLDER_OTHER, and there are no headers.
do {
final Folder f = mCursor.getModel();
@@ -784,10 +781,9 @@
return;
}
- // Otherwise, this is an adapter for a sectioned list.
+ // Otherwise, this is an adapter for a divided list.
final List<DrawerItem> allFoldersList = new ArrayList<DrawerItem>();
final List<DrawerItem> inboxFolders = new ArrayList<DrawerItem>();
- boolean currentFolderFound = false;
do {
final Folder f = mCursor.getModel();
if (!isFolderTypeExcluded(f)) {
@@ -798,42 +794,60 @@
allFoldersList.add(DrawerItem.ofFolder(
mActivity, f, DrawerItem.FOLDER_OTHER, mCursor.getPosition()));
}
- if (f.equals(mCurrentFolderForUnreadCheck)) {
- currentFolderFound = true;
- }
}
} while (mCursor.moveToNext());
- if (!currentFolderFound && mCurrentFolderForUnreadCheck != null
- && mCurrentAccount != null && mAccountChanger != null
- && mAccountChanger.isDrawerPullEnabled()) {
- LogUtils.d(LOG_TAG, "Current folder (%1$s) has disappeared for %2$s",
- mCurrentFolderForUnreadCheck.name, mCurrentAccount.name);
- changeAccount(mCurrentAccount);
+ // If we have the full folder list, verify that the current folder exists
+ boolean currentFolderFound = false;
+ if (mFullFolderListCursor != null) {
+ final String folderName = mCurrentFolderForUnreadCheck == null
+ ? "null" : mCurrentFolderForUnreadCheck.name;
+ LogUtils.d(LOG_TAG, "Checking if full folder list contains %s", folderName);
+
+ if (mFullFolderListCursor.moveToFirst()) {
+ LogUtils.d(LOG_TAG, "Cursor for %s seems reasonably valid", folderName);
+ do {
+ final Folder f = mFullFolderListCursor.getModel();
+ if (!isFolderTypeExcluded(f)) {
+ if (f.equals(mCurrentFolderForUnreadCheck)) {
+ LogUtils.d(LOG_TAG, "Found %s !", folderName);
+ currentFolderFound = true;
+ }
+ }
+ } while (mFullFolderListCursor.moveToNext());
+ }
+
+ if (!currentFolderFound && mCurrentFolderForUnreadCheck != null
+ && mCurrentAccount != null && mAccountController != null
+ && mAccountController.isDrawerPullEnabled()) {
+ LogUtils.d(LOG_TAG, "Current folder (%1$s) has disappeared for %2$s",
+ mCurrentFolderForUnreadCheck.name, mCurrentAccount.name);
+ changeAccount(mCurrentAccount);
+ }
}
- // Add all inboxes (sectioned included) before recents.
- addFolderSection(itemList, inboxFolders, R.string.inbox_folders_heading);
+ // Add all inboxes (sectioned Inboxes included) before recent folders.
+ addFolderDivision(itemList, inboxFolders, R.string.inbox_folders_heading);
- // Add most recently folders (in alphabetical order) next.
+ // Add recent folders next.
addRecentsToList(itemList);
- // Add the remaining provider folders followed by all labels.
- addFolderSection(itemList, allFoldersList, R.string.all_folders_heading);
+ // Add the remaining folders.
+ addFolderDivision(itemList, allFoldersList, R.string.all_folders_heading);
}
/**
- * Given a list of folders as {@link DrawerItem}s, add them to the item
- * list as needed. Passing in a non-0 integer for the resource will
- * enable a header
+ * Given a list of folders as {@link DrawerItem}s, add them as a group.
+ * Passing in a non-0 integer for the resource will enable a header.
*
* @param destination List of drawer items to populate
* @param source List of drawer items representing folders to add to the drawer
* @param headerStringResource
* {@link FolderListAdapter#NO_HEADER_RESOURCE} if no header
- * is required, or res-id otherwise
+ * is required, or res-id otherwise. The integer is interpreted as the string
+ * for the header's title.
*/
- private void addFolderSection(List<DrawerItem> destination, List<DrawerItem> source,
+ private void addFolderDivision(List<DrawerItem> destination, List<DrawerItem> source,
int headerStringResource) {
if (source.size() > 0) {
if(headerStringResource != NO_HEADER_RESOURCE) {
@@ -875,10 +889,9 @@
/**
* Check if the cursor provided is valid.
- * @param mCursor
* @return True if cursor is invalid, false otherwise
*/
- private boolean isCursorInvalid(Cursor mCursor) {
+ private boolean isCursorInvalid() {
return mCursor == null || mCursor.isClosed()|| mCursor.getCount() <= 0
|| !mCursor.moveToFirst();
}
@@ -890,6 +903,12 @@
}
@Override
+ public void setFullFolderListCursor(final ObjectCursor<Folder> cursor) {
+ mFullFolderListCursor = cursor;
+ recalculateList();
+ }
+
+ @Override
public Object getItem(int position) {
// Is there an attempt made to access outside of the drawer item list?
if (position >= mItemList.size()) {
@@ -921,33 +940,16 @@
public int getItemType(DrawerItem item) {
return item.mType;
}
-
- // TODO(viki): This is strange. We have the full folder and yet we create on from scratch.
- @Override
- public Folder getFullFolder(DrawerItem folderItem) {
- if (folderItem.mFolderType == DrawerItem.FOLDER_RECENT) {
- return folderItem.mFolder;
- } else {
- final int pos = folderItem.mPosition;
- if (pos > -1 && mCursor != null && !mCursor.isClosed()
- && mCursor.moveToPosition(folderItem.mPosition)) {
- return mCursor.getModel();
- } else {
- return null;
- }
- }
- }
}
private class HierarchicalFolderListAdapter extends ArrayAdapter<Folder>
- implements FolderListFragmentCursorAdapter{
+ implements FolderListFragmentCursorAdapter {
private static final int PARENT = 0;
private static final int CHILD = 1;
private final Uri mParentUri;
private final Folder mParent;
private final FolderItemView.DropHandler mDropHandler;
- private ObjectCursor<Folder> mCursor;
public HierarchicalFolderListAdapter(ObjectCursor<Folder> c, Folder parentFolder) {
super(mActivity.getActivityContext(), R.layout.folder_item);
@@ -999,7 +1001,6 @@
@Override
public void setCursor(ObjectCursor<Folder> cursor) {
- mCursor = cursor;
clear();
if (mParent != null) {
add(mParent);
@@ -1015,6 +1016,11 @@
}
@Override
+ public void setFullFolderListCursor(final ObjectCursor<Folder> cursor) {
+ // Not necessary in HierarchicalFolderListAdapter
+ }
+
+ @Override
public void destroy() {
// Do nothing.
}
@@ -1031,20 +1037,6 @@
}
@Override
- public Folder getFullFolder(DrawerItem folderItem) {
- final int pos = folderItem.mPosition;
- if (mCursor == null || mCursor.isClosed()) {
- return null;
- }
- if (pos > -1 && mCursor != null && !mCursor.isClosed()
- && mCursor.moveToPosition(folderItem.mPosition)) {
- return mCursor.getModel();
- } else {
- return null;
- }
- }
-
- @Override
public void notifyAllAccountsChanged() {
// Do nothing. We don't care about changes to all accounts.
}
@@ -1056,7 +1048,7 @@
/**
* Sets the currently selected folder safely.
- * @param folder
+ * @param folder the folder to change to. It is an error to pass null here.
*/
private void setSelectedFolder(Folder folder) {
if (folder == null) {
@@ -1065,12 +1057,6 @@
LogUtils.e(LOG_TAG, "FolderListFragment.setSelectedFolder(null) called!");
return;
}
- // Is this the folder we changed to previously? If not, ignore the update
- if (mNextFolder != null && !folder.uri.equals(mNextFolder.uri)) {
- // Update to a folder that we don't care about. Ignore
- return;
- }
- mNextFolder = null;
final boolean viewChanged =
!FolderItemView.areSameViews(folder, mCurrentFolderForUnreadCheck);
@@ -1110,8 +1096,10 @@
// comment on {@link AbstractActivityController#restartOptionalLoader} to see why we
// don't just do restartLoader.
final LoaderManager manager = getLoaderManager();
- manager.destroyLoader(FOLDER_LOADER_ID);
- manager.restartLoader(FOLDER_LOADER_ID, Bundle.EMPTY, this);
+ manager.destroyLoader(FOLDER_LIST_LOADER_ID);
+ manager.restartLoader(FOLDER_LIST_LOADER_ID, Bundle.EMPTY, this);
+ manager.destroyLoader(FULL_FOLDER_LIST_LOADER_ID);
+ manager.restartLoader(FULL_FOLDER_LIST_LOADER_ID, Bundle.EMPTY, this);
// An updated cursor causes the entire list to refresh. No need to refresh the list.
// But we do need to blank out the current folder, since the account might not be
// synced.
@@ -1122,14 +1110,11 @@
// non-null account -> null account transition.
LogUtils.e(LOG_TAG, "FLF.setSelectedAccount(null) called! Destroying existing loader.");
final LoaderManager manager = getLoaderManager();
- manager.destroyLoader(FOLDER_LOADER_ID);
+ manager.destroyLoader(FOLDER_LIST_LOADER_ID);
+ manager.destroyLoader(FULL_FOLDER_LIST_LOADER_ID);
}
}
- public interface FolderListSelectionListener {
- public void onFolderSelected(Folder folder);
- }
-
/**
* Get whether the FolderListFragment is currently showing the hierarchy
* under a single parent.
@@ -1154,4 +1139,11 @@
return false;
}
+
+ /**
+ * @return the choice mode to use for the {@link ListView}
+ */
+ protected int getListViewChoiceMode() {
+ return mAccountController.getFolderListViewChoiceMode();
+ }
}
diff --git a/src/com/android/mail/ui/FolderSelectionActivity.java b/src/com/android/mail/ui/FolderSelectionActivity.java
index be2608e..f490bc7 100644
--- a/src/com/android/mail/ui/FolderSelectionActivity.java
+++ b/src/com/android/mail/ui/FolderSelectionActivity.java
@@ -27,17 +27,16 @@
import android.database.DataSetObservable;
import android.database.DataSetObserver;
import android.os.Bundle;
-import android.util.Log;
import android.view.DragEvent;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.Button;
+import android.widget.ListView;
import com.android.mail.R;
import com.android.mail.providers.Account;
import com.android.mail.providers.Folder;
import com.android.mail.providers.FolderWatcher;
-import com.android.mail.ui.FolderListFragment.FolderListSelectionListener;
import com.android.mail.ui.ViewMode.ModeChangeListener;
import com.android.mail.utils.LogTag;
import com.android.mail.utils.LogUtils;
@@ -53,7 +52,7 @@
*/
public class FolderSelectionActivity extends Activity implements OnClickListener,
DialogInterface.OnClickListener, FolderChangeListener, ControllableActivity,
- FolderListSelectionListener {
+ FolderSelector {
public static final String EXTRA_ACCOUNT_SHORTCUT = "account-shortcut";
private static final String LOG_TAG = LogTag.getLogTag();
@@ -110,13 +109,14 @@
@Override
public void changeAccount(Account account) {
// Never gets called, so do nothing here.
- Log.wtf(LOG_TAG, "FolderSelectionActivity.changeAccount() called when NOT expected.");
+ LogUtils.wtf(LOG_TAG,
+ "FolderSelectionActivity.changeAccount() called when NOT expected.");
}
@Override
public void switchToDefaultInboxOrChangeAccount(Account account) {
// Never gets called, so do nothing here.
- Log.wtf(LOG_TAG,"FolderSelectionActivity.switchToDefaultInboxOrChangeAccount() " +
+ LogUtils.wtf(LOG_TAG,"FolderSelectionActivity.switchToDefaultInboxOrChangeAccount() " +
"called when NOT expected.");
}
@@ -149,6 +149,11 @@
// Unsupported
return false;
}
+
+ @Override
+ public int getFolderListViewChoiceMode() {
+ return ListView.CHOICE_MODE_NONE;
+ }
};
@Override
@@ -191,7 +196,7 @@
}
firstButton.setOnClickListener(this);
createFolderListFragment(FolderListFragment.ofTopLevelTree(mAccount.folderListUri,
- getExcludedFolderTypes(), true));
+ getExcludedFolderTypes()));
}
/**
@@ -351,14 +356,14 @@
// Replace this fragment with a new FolderListFragment
// showing this folder's children if we are not already looking
// at the child view for this folder.
- createFolderListFragment(FolderListFragment.ofTree(folder, true));
+ createFolderListFragment(FolderListFragment.ofTree(folder));
return;
}
onFolderChanged(folder);
}
@Override
- public FolderListSelectionListener getFolderListSelectionListener() {
+ public FolderSelector getFolderSelector() {
return this;
}
diff --git a/src/com/android/mail/ui/FolderSelector.java b/src/com/android/mail/ui/FolderSelector.java
new file mode 100644
index 0000000..6523a34
--- /dev/null
+++ b/src/com/android/mail/ui/FolderSelector.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+
+package com.android.mail.ui;
+
+import com.android.mail.providers.Folder;
+
+/**
+ * Interface that permits elements to implement selecting a folder.
+ * The single method {@link #onFolderSelected(com.android.mail.providers.Folder)} defines what
+ * happens when a folder is selected.
+ */
+public interface FolderSelector {
+ /**
+ * Selects the folder provided as an argument here. This corresponds to the user
+ * selecting a folder in the UI element, either for creating a widget/shortcut (as in the
+ * case of {@link FolderSelectionActivity} or for viewing the contents of
+ * the folder (as in the case of {@link AbstractActivityController}.
+ * @param folder
+ */
+ public void onFolderSelected(Folder folder);
+}
diff --git a/src/com/android/mail/ui/FragmentRunnable.java b/src/com/android/mail/ui/FragmentRunnable.java
new file mode 100644
index 0000000..c422838
--- /dev/null
+++ b/src/com/android/mail/ui/FragmentRunnable.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright (C) 2013 Google Inc.
+ * Licensed to The Android Open Source Project.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.mail.ui;
+
+import android.app.Fragment;
+
+import com.android.mail.utils.LogTag;
+import com.android.mail.utils.LogUtils;
+
+/**
+ * Small Runnable-like wrapper that first checks that the Fragment is in a good state before
+ * doing any work. Ideal for use with a {@link android.os.Handler}.
+ */
+public abstract class FragmentRunnable implements Runnable {
+ private static final String LOG_TAG = LogTag.getLogTag();
+
+ private final String mOpName;
+ private final Fragment mFragment;
+
+ public FragmentRunnable(String opName, Fragment fragment) {
+ mOpName = opName;
+ mFragment = fragment;
+ }
+
+ public abstract void go();
+
+ @Override
+ public void run() {
+ if (!mFragment.isAdded()) {
+ LogUtils.i(LOG_TAG, "Unable to run op='%s' b/c fragment is not attached: %s",
+ mOpName, mFragment);
+ return;
+ }
+ go();
+ }
+}
\ No newline at end of file
diff --git a/src/com/android/mail/ui/MailActionBarView.java b/src/com/android/mail/ui/MailActionBarView.java
index d7bf719..c967530 100644
--- a/src/com/android/mail/ui/MailActionBarView.java
+++ b/src/com/android/mail/ui/MailActionBarView.java
@@ -894,6 +894,9 @@
&& !mFolder.isProviderFolder());
Utils.setMenuItemVisibility(menu, R.id.move_to, mFolder != null
&& mFolder.supportsCapability(FolderCapabilities.ALLOWS_REMOVE_CONVERSATION));
+ Utils.setMenuItemVisibility(menu, R.id.move_to_inbox, mFolder != null
+ && mFolder.supportsCapability(FolderCapabilities.ALLOWS_MOVE_TO_INBOX));
+
final MenuItem removeFolder = menu.findItem(R.id.remove_folder);
if (mFolder != null && removeFolder != null) {
removeFolder.setTitle(mActivity.getApplicationContext().getString(
diff --git a/src/com/android/mail/ui/MailActivity.java b/src/com/android/mail/ui/MailActivity.java
index c02f8a2..2cf2256 100644
--- a/src/com/android/mail/ui/MailActivity.java
+++ b/src/com/android/mail/ui/MailActivity.java
@@ -36,7 +36,6 @@
import com.android.mail.compose.ComposeActivity;
import com.android.mail.providers.Folder;
-import com.android.mail.ui.FolderListFragment.FolderListSelectionListener;
import com.android.mail.ui.ViewMode.ModeChangeListener;
import com.android.mail.utils.StorageLowState;
import com.android.mail.utils.Utils;
@@ -50,8 +49,6 @@
* conversation list or a conversation view).
*/
public class MailActivity extends AbstractMailActivity implements ControllableActivity {
- // TODO(viki) This class lacks: Sync Window Upgrade dialog
-
/**
* The activity controller to which we delegate most Activity lifecycle events.
*/
@@ -310,7 +307,7 @@
}
@Override
- public FolderListSelectionListener getFolderListSelectionListener() {
+ public FolderSelector getFolderSelector() {
return mController;
}
diff --git a/src/com/android/mail/ui/NestedFolderView.java b/src/com/android/mail/ui/NestedFolderView.java
new file mode 100644
index 0000000..578a3ba
--- /dev/null
+++ b/src/com/android/mail/ui/NestedFolderView.java
@@ -0,0 +1,162 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+
+package com.android.mail.ui;
+
+import android.app.LoaderManager;
+import android.content.Context;
+import android.util.AttributeSet;
+import android.view.View;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+
+import com.android.mail.R;
+import com.android.mail.browse.ConversationCursor;
+import com.android.mail.providers.Folder;
+import com.android.mail.utils.LogTag;
+import com.android.mail.utils.LogUtils;
+
+/**
+ * For folders that might contain other folders, we show the nested folders within this view.
+ * Tapping on this opens the folder.
+ */
+public class NestedFolderView extends LinearLayout implements ConversationSpecialItemView,
+ SwipeableItemView {
+ protected static final String LOG_TAG = LogTag.getLogTag();
+ /**
+ * The actual view that is displayed and is perhaps swiped away. We don't allow swiping,
+ * but this is required by the {@link SwipeableItemView} interface.
+ */
+ private View mSwipeableContent;
+ /** The folder this view represents */
+ private Folder mFolder;
+
+ public NestedFolderView(Context context) {
+ super(context);
+ }
+
+ public NestedFolderView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ public NestedFolderView(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+ }
+
+ @Override
+ protected void onFinishInflate() {
+ super.onFinishInflate();
+ mSwipeableContent = findViewById(R.id.swipeable_content);
+ }
+
+ @Override
+ public void onUpdate(String s, Folder folder, ConversationCursor conversationCursor) {
+ // Do nothing. We don't care about the change to the conversation cursor here.
+ // Nested folders only care if they were removed from the parent folder,
+ // so supposing we should check for that here.
+ }
+
+ /**
+ * Sets the folder associated with this view. This method is meant to be called infrequently,
+ * since we assume that a new view will be created when the unread count changes.
+ * @param folder the folder that this view represents.
+ */
+ public void setFolder(Folder folder) {
+ mFolder = folder;
+ // Since we assume that setFolder will be called infrequently (once currently),
+ // we don't bother saving the textviews for folder name and folder unread count. If we
+ // find that setFolder gets called repeatedly, it might be prudent to remember the
+ // references to these textviews, making setFolder slightly faster.
+ TextView t = (TextView) findViewById(R.id.nested_folder_name);
+ t.setText(folder.name);
+ t = (TextView) findViewById(R.id.nested_folder_unread);
+ t.setText("" + folder.unreadCount);
+ }
+
+ /**
+ * Returns the folder associated with this view
+ * @return a folder that this view represents.
+ */
+ public Folder getFolder() {
+ return mFolder;
+ }
+
+ @Override
+ public boolean getShouldDisplayInList() {
+ // Nested folders once created are always displayed in the list.
+ return true;
+ }
+
+ @Override
+ public int getPosition() {
+ // We only have one element, and that's always at the top for now.
+ return 0;
+ }
+
+ @Override
+ public void setAdapter(AnimatedAdapter animatedAdapter) {
+ // Do nothing, since the adapter creates these views.
+ }
+
+ @Override
+ public void bindLoaderManager(LoaderManager loaderManager) {
+ // Do nothing. We don't need the loader manager.
+ }
+
+ @Override
+ public void cleanup() {
+ // Do nothing.
+ }
+
+ @Override
+ public void onConversationSelected() {
+ // Do nothing. We don't care if conversations are selected.
+ }
+
+ @Override
+ public void onCabModeEntered() {
+ // Do nothing. We don't care if cab mode was entered.
+ }
+
+ @Override
+ public boolean acceptsUserTaps() {
+ return true;
+ }
+
+ @Override
+ public SwipeableView getSwipeableView() {
+ return SwipeableView.from(mSwipeableContent);
+ }
+
+ @Override
+ public boolean canChildBeDismissed() {
+ // The folders can never be dismissed, return false.
+ return false;
+ }
+
+ @Override
+ public void dismiss() {
+ /** How did this happen? We returned false in {@link #canChildBeDismissed()} so this
+ * method should never be called. */
+ LogUtils.wtf(LOG_TAG, "NestedFolderView.dismiss() called. Not expected.");
+ }
+
+ @Override
+ public float getMinAllowScrollDistance() {
+ return -1;
+ }
+}
diff --git a/src/com/android/mail/ui/OnePaneController.java b/src/com/android/mail/ui/OnePaneController.java
index 527513a..1ab18f3 100644
--- a/src/com/android/mail/ui/OnePaneController.java
+++ b/src/com/android/mail/ui/OnePaneController.java
@@ -24,6 +24,7 @@
import android.net.Uri;
import android.os.Bundle;
import android.support.v4.widget.DrawerLayout;
+import android.widget.ListView;
import com.android.mail.ConversationListContext;
import com.android.mail.R;
@@ -200,7 +201,8 @@
final int transition = mConversationListNeverShown
? FragmentTransaction.TRANSIT_FRAGMENT_FADE
: FragmentTransaction.TRANSIT_FRAGMENT_OPEN;
- Fragment conversationListFragment = ConversationListFragment.newInstance(listContext);
+ final Fragment conversationListFragment =
+ ConversationListFragment.newInstance(listContext);
if (!inInbox(mAccount, listContext)) {
// Maintain fragment transaction history so we can get back to the
@@ -374,6 +376,12 @@
mActivity.finish();
} else if (mViewMode.isListMode() && !inInbox(mAccount, mConvListContext)) {
if (mLastFolderListTransactionId != INVALID_ID) {
+ // Set the hierarchy folder to what it will be once we go up
+ final Folder hierarchyFolder = getHierarchyFolder();
+ if (hierarchyFolder != null && hierarchyFolder.parent != null) {
+ setHierarchyFolder(hierarchyFolder.parent);
+ }
+
// If the user got here by navigating via the folder list, back
// should bring them back to the folder list.
mViewMode.enterFolderListMode();
@@ -391,22 +399,11 @@
}
private void goUpFolderHierarchy(Folder current) {
- Folder top = current.parent;
+ final Folder top = current.parent;
if (top != null) {
// FIXME: This is silly. we worked so hard to add folder fragments to the back stack.
// it should either just pop back, or should not use the back stack at all.
-
- setHierarchyFolder(top);
- // Replace this fragment with a new FolderListFragment
- // showing this folder's children if we are not already
- // looking at the child view for this folder.
- mLastFolderListTransactionId = replaceFragmentWithBack(
- FolderListFragment.ofTree(top, false),
- FragmentTransaction.TRANSIT_FRAGMENT_OPEN, TAG_FOLDER_LIST, R.id.content_pane);
- } else {
- // Otherwise, clear the selected folder and go back to whatever the
- // last folder list displayed was.
- // TODO(viki): Load folder list for parent folder.
+ onFolderSelected(top);
}
}
@@ -424,18 +421,8 @@
@Override
public void onFolderSelected(Folder folder) {
- if (folder.hasChildren && !folder.equals(getHierarchyFolder())) {
- mViewMode.enterFolderListMode();
- setHierarchyFolder(folder);
- // Replace this fragment with a new FolderListFragment
- // showing this folder's children if we are not already
- // looking at the child view for this folder.
- mLastFolderListTransactionId = replaceFragmentWithBack(
- FolderListFragment.ofTree(folder, false),
- FragmentTransaction.TRANSIT_FRAGMENT_OPEN, TAG_FOLDER_LIST, R.id.content_pane);
- } else {
- super.onFolderSelected(folder);
- }
+ setHierarchyFolder(folder);
+ super.onFolderSelected(folder);
}
private static boolean isTransactionIdValid(int id) {
@@ -511,10 +498,6 @@
onConversationListVisibilityChanged(true);
}
- private void safelyPopBackStack(boolean inLoaderCallbacks) {
- safelyPopBackStack(-1, inLoaderCallbacks);
- }
-
/**
* Pop to a specified point in the fragment back stack without causing IllegalStateExceptions
* from committing a fragment transaction "at the wrong time".
@@ -652,4 +635,10 @@
// The drawer is enabled for one pane mode
return true;
}
+
+ @Override
+ public int getFolderListViewChoiceMode() {
+ // By default, we do not want to allow any item to be selected in the folder list
+ return ListView.CHOICE_MODE_NONE;
+ }
}
diff --git a/src/com/android/mail/ui/SecureConversationViewController.java b/src/com/android/mail/ui/SecureConversationViewController.java
new file mode 100644
index 0000000..a3e6ec8
--- /dev/null
+++ b/src/com/android/mail/ui/SecureConversationViewController.java
@@ -0,0 +1,210 @@
+/*
+ * Copyright (C) 2013 Google Inc.
+ * Licensed to The Android Open Source Project.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.mail.ui;
+
+import android.app.Fragment;
+import android.content.res.Resources;
+import android.os.Bundle;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.webkit.WebSettings;
+
+import com.android.mail.FormattedDateBuilder;
+import com.android.mail.R;
+import com.android.mail.browse.ConversationMessage;
+import com.android.mail.browse.ConversationViewAdapter;
+import com.android.mail.browse.ConversationViewAdapter.MessageHeaderItem;
+import com.android.mail.browse.ConversationViewHeader;
+import com.android.mail.browse.MessageFooterView;
+import com.android.mail.browse.MessageHeaderView;
+import com.android.mail.browse.MessageScrollView;
+import com.android.mail.browse.MessageWebView;
+import com.android.mail.providers.Message;
+import com.android.mail.utils.ConversationViewUtils;
+
+/**
+ * Controller to do most of the heavy lifting for {@link SecureConversationViewFragment}
+ * and {@link com.android.mail.browse.EmlMessageViewFragment}. Currently that work is
+ * pretty much the rendering logic.
+ */
+public class SecureConversationViewController implements
+ MessageHeaderView.MessageHeaderViewCallbacks {
+ private static final String BEGIN_HTML =
+ "<body style=\"margin: 0 %spx;\"><div style=\"margin: 16px 0; font-size: 80%%\">";
+ private static final String END_HTML = "</div></body>";
+
+ private final SecureConversationViewControllerCallbacks mCallbacks;
+
+ private MessageWebView mWebView;
+ private ConversationViewHeader mConversationHeaderView;
+ private MessageHeaderView mMessageHeaderView;
+ private MessageFooterView mMessageFooterView;
+ private ConversationMessage mMessage;
+ private MessageScrollView mScrollView;
+
+ private ConversationViewProgressController mProgressController;
+ private FormattedDateBuilder mDateBuilder;
+
+ private int mSideMarginInWebPx;
+
+ public SecureConversationViewController(SecureConversationViewControllerCallbacks callbacks) {
+ mCallbacks = callbacks;
+ }
+
+ public View onCreateView(LayoutInflater inflater, ViewGroup container,
+ Bundle savedInstanceState) {
+ View rootView = inflater.inflate(R.layout.secure_conversation_view, container, false);
+ mScrollView = (MessageScrollView) rootView.findViewById(R.id.scroll_view);
+ mConversationHeaderView = (ConversationViewHeader) rootView.findViewById(R.id.conv_header);
+ mMessageHeaderView = (MessageHeaderView) rootView.findViewById(R.id.message_header);
+ mMessageFooterView = (MessageFooterView) rootView.findViewById(R.id.message_footer);
+
+ mProgressController = new ConversationViewProgressController(
+ mCallbacks.getFragment(), mCallbacks.getHandler());
+ mProgressController.instantiateProgressIndicators(rootView);
+ mWebView = (MessageWebView) rootView.findViewById(R.id.webview);
+ mWebView.setWebViewClient(mCallbacks.getWebViewClient());
+ mWebView.setFocusable(false);
+ final WebSettings settings = mWebView.getSettings();
+
+ settings.setJavaScriptEnabled(false);
+ settings.setLayoutAlgorithm(WebSettings.LayoutAlgorithm.NORMAL);
+
+ ConversationViewUtils.setTextZoom(mCallbacks.getFragment().getResources(), settings);
+
+ settings.setSupportZoom(true);
+ settings.setBuiltInZoomControls(true);
+ settings.setDisplayZoomControls(false);
+
+ mScrollView.setInnerScrollableView(mWebView);
+
+ return rootView;
+ }
+
+ public void onActivityCreated(Bundle savedInstanceState) {
+ mCallbacks.setupConversationHeaderView(mConversationHeaderView);
+
+ final Fragment fragment = mCallbacks.getFragment();
+
+ mDateBuilder = new FormattedDateBuilder(fragment.getActivity());
+ mMessageHeaderView.initialize(mDateBuilder,
+ mCallbacks.getConversationAccountController(), mCallbacks.getAddressCache());
+ mMessageHeaderView.setExpandMode(MessageHeaderView.POPUP_MODE);
+ mMessageHeaderView.setContactInfoSource(mCallbacks.getContactInfoSource());
+ mMessageHeaderView.setCallbacks(this);
+ mMessageHeaderView.setExpandable(false);
+ mMessageHeaderView.setViewOnlyMode(mCallbacks.isViewOnlyMode());
+
+ mCallbacks.setupMessageHeaderVeiledMatcher(mMessageHeaderView);
+
+ mMessageFooterView.initialize(fragment.getLoaderManager(), fragment.getFragmentManager());
+
+ mCallbacks.startMessageLoader();
+
+ mProgressController.showLoadingStatus(mCallbacks.isViewVisibleToUser());
+
+ final Resources r = mCallbacks.getFragment().getResources();
+ mSideMarginInWebPx = (int) ((r.getDimensionPixelOffset(
+ R.dimen.conversation_view_margin_side) + r.getDimensionPixelOffset(
+ R.dimen.conversation_message_content_margin_side)) / r.getDisplayMetrics().density);
+ }
+
+ /**
+ * Populate the adapter with overlay views (message headers, super-collapsed
+ * blocks, a conversation header), and return an HTML document with spacer
+ * divs inserted for all overlays.
+ */
+ public void renderMessage(ConversationMessage message) {
+ mMessage = message;
+
+ mWebView.getSettings().setBlockNetworkImage(!mMessage.alwaysShowImages);
+
+ // Add formatting to message body
+ // At this point, only adds margins.
+ StringBuilder dataBuilder = new StringBuilder(
+ String.format(BEGIN_HTML, mSideMarginInWebPx));
+ dataBuilder.append(mMessage.getBodyAsHtml());
+ dataBuilder.append(END_HTML);
+
+ mWebView.loadDataWithBaseURL(mCallbacks.getBaseUri(), dataBuilder.toString(),
+ "text/html", "utf-8", null);
+ final MessageHeaderItem item = ConversationViewAdapter.newMessageHeaderItem(
+ null, mMessage, true, mMessage.alwaysShowImages);
+ mMessageHeaderView.bind(item, false);
+ if (mMessage.hasAttachments) {
+ mMessageFooterView.setVisibility(View.VISIBLE);
+ mMessageFooterView.bind(item, false);
+ }
+ }
+
+ public ConversationMessage getMessage() {
+ return mMessage;
+ }
+
+ public ConversationViewHeader getConversationHeaderView() {
+ return mConversationHeaderView;
+ }
+
+ public void dismissLoadingStatus() {
+ mProgressController.dismissLoadingStatus();
+ }
+
+ public void setSubject(String subject) {
+ mConversationHeaderView.setSubject(subject);
+ }
+
+ // Start MessageHeaderViewCallbacks implementations
+
+ @Override
+ public void setMessageSpacerHeight(MessageHeaderItem item, int newSpacerHeight) {
+ // Do nothing.
+ }
+
+ @Override
+ public void setMessageExpanded(MessageHeaderItem item, int newSpacerHeight) {
+ // Do nothing.
+ }
+
+ @Override
+ public void setMessageDetailsExpanded(MessageHeaderItem i, boolean expanded, int heightBefore) {
+ // Do nothing.
+ }
+
+ @Override
+ public void showExternalResources(final Message msg) {
+ mWebView.getSettings().setBlockNetworkImage(false);
+ }
+
+ @Override
+ public void showExternalResources(final String rawSenderAddress) {
+ mWebView.getSettings().setBlockNetworkImage(false);
+ }
+
+ @Override
+ public boolean supportsMessageTransforms() {
+ return false;
+ }
+
+ @Override
+ public String getMessageTransforms(final Message msg) {
+ return null;
+ }
+
+ // End MessageHeaderViewCallbacks implementations
+}
diff --git a/src/com/android/mail/ui/SecureConversationViewControllerCallbacks.java b/src/com/android/mail/ui/SecureConversationViewControllerCallbacks.java
new file mode 100644
index 0000000..06857ed
--- /dev/null
+++ b/src/com/android/mail/ui/SecureConversationViewControllerCallbacks.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright (C) 2013 Google Inc.
+ * Licensed to The Android Open Source Project.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.mail.ui;
+
+import android.app.Fragment;
+import android.os.Handler;
+
+import com.android.mail.ContactInfoSource;
+import com.android.mail.browse.ConversationAccountController;
+import com.android.mail.browse.ConversationViewHeader;
+import com.android.mail.browse.MessageHeaderView;
+import com.android.mail.providers.Address;
+
+import java.util.Map;
+
+/**
+ * Callbacks for fragments that use the {@link SecureConversationViewController}.
+ */
+public interface SecureConversationViewControllerCallbacks {
+ public Handler getHandler();
+ public AbstractConversationWebViewClient getWebViewClient();
+ public Fragment getFragment();
+ public void setupConversationHeaderView(ConversationViewHeader headerView);
+ public boolean isViewVisibleToUser();
+ public ContactInfoSource getContactInfoSource();
+ public ConversationAccountController getConversationAccountController();
+ public Map<String, Address> getAddressCache();
+ public void setupMessageHeaderVeiledMatcher(MessageHeaderView messageHeaderView);
+ public void startMessageLoader();
+ public String getBaseUri();
+ public boolean isViewOnlyMode();
+}
diff --git a/src/com/android/mail/ui/SecureConversationViewFragment.java b/src/com/android/mail/ui/SecureConversationViewFragment.java
index 3a74d35..c3efd59 100644
--- a/src/com/android/mail/ui/SecureConversationViewFragment.java
+++ b/src/com/android/mail/ui/SecureConversationViewFragment.java
@@ -17,56 +17,64 @@
package com.android.mail.ui;
+import android.app.Fragment;
import android.content.Loader;
-import android.database.Cursor;
import android.net.Uri;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
-import android.webkit.WebSettings;
-import android.webkit.WebSettings.LayoutAlgorithm;
import android.webkit.WebView;
-import android.webkit.WebViewClient;
-import com.android.mail.R;
-import com.android.mail.browse.ConversationViewAdapter;
-import com.android.mail.browse.ConversationViewAdapter.MessageHeaderItem;
+import com.android.mail.browse.ConversationAccountController;
+import com.android.mail.browse.ConversationMessage;
import com.android.mail.browse.ConversationViewHeader;
import com.android.mail.browse.MessageCursor;
-import com.android.mail.browse.MessageCursor.ConversationMessage;
-import com.android.mail.browse.MessageFooterView;
import com.android.mail.browse.MessageHeaderView;
-import com.android.mail.browse.MessageHeaderView.MessageHeaderViewCallbacks;
-import com.android.mail.browse.MessageScrollView;
-import com.android.mail.browse.MessageWebView;
import com.android.mail.content.ObjectCursor;
import com.android.mail.providers.Account;
+import com.android.mail.providers.Address;
import com.android.mail.providers.Conversation;
-import com.android.mail.providers.Message;
import com.android.mail.utils.LogTag;
import com.android.mail.utils.LogUtils;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Sets;
import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
-public class SecureConversationViewFragment extends AbstractConversationViewFragment implements
- MessageHeaderViewCallbacks {
+public class SecureConversationViewFragment extends AbstractConversationViewFragment
+ implements SecureConversationViewControllerCallbacks {
private static final String LOG_TAG = LogTag.getLogTag();
- private MessageWebView mWebView;
- private ConversationViewHeader mConversationHeaderView;
- private MessageHeaderView mMessageHeaderView;
- private MessageFooterView mMessageFooterView;
- private ConversationMessage mMessage;
- private MessageScrollView mScrollView;
- private final WebViewClient mWebViewClient = new AbstractConversationWebViewClient() {
+ private SecureConversationViewController mViewController;
+
+ private class SecureConversationWebViewClient extends AbstractConversationWebViewClient {
+ public SecureConversationWebViewClient(Account account) {
+ super(account);
+ }
+
@Override
public void onPageFinished(WebView view, String url) {
if (isUserVisible()) {
onConversationSeen();
}
- dismissLoadingStatus();
+ mViewController.dismissLoadingStatus();
+
+ final Set<String> emailAddresses = Sets.newHashSet();
+ final List<Address> cacheCopy;
+ synchronized (mAddressCache) {
+ cacheCopy = ImmutableList.copyOf(mAddressCache.values());
+ }
+ for (Address addr : cacheCopy) {
+ emailAddresses.add(addr.getAddress());
+ }
+ final ContactLoaderCallbacks callbacks = getContactInfoSource();
+ callbacks.setSenders(emailAddresses);
+ getLoaderManager().restartLoader(CONTACT_LOADER, Bundle.EMPTY, callbacks);
}
};
@@ -93,67 +101,96 @@
}
@Override
- public void onActivityCreated(Bundle savedInstanceState) {
- super.onActivityCreated(savedInstanceState);
- mConversationHeaderView.setCallbacks(this, this);
- mConversationHeaderView.setFoldersVisible(false);
- mConversationHeaderView.setSubject(mConversation.subject);
- mMessageHeaderView.initialize(mDateBuilder, this, mAddressCache);
- mMessageHeaderView.setExpandMode(MessageHeaderView.POPUP_MODE);
- mMessageHeaderView.setContactInfoSource(getContactInfoSource());
- mMessageHeaderView.setCallbacks(this);
- mMessageHeaderView.setExpandable(false);
- mMessageHeaderView.setVeiledMatcher(
- ((ControllableActivity) getActivity()).getAccountController()
- .getVeiledAddressMatcher());
- mMessageFooterView.initialize(getLoaderManager(), getFragmentManager());
- getLoaderManager().initLoader(MESSAGE_LOADER, null, getMessageLoaderCallbacks());
- showLoadingStatus();
+ public void onCreate(Bundle savedState) {
+ super.onCreate(savedState);
+
+ mWebViewClient = new SecureConversationWebViewClient(mAccount);
+ mViewController = new SecureConversationViewController(this);
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
- View rootView = inflater.inflate(R.layout.secure_conversation_view, container, false);
- mScrollView = (MessageScrollView) rootView.findViewById(R.id.scroll_view);
- mConversationHeaderView = (ConversationViewHeader) rootView.findViewById(R.id.conv_header);
- mMessageHeaderView = (MessageHeaderView) rootView.findViewById(R.id.message_header);
- mMessageFooterView = (MessageFooterView) rootView.findViewById(R.id.message_footer);
- instantiateProgressIndicators(rootView);
- mWebView = (MessageWebView) rootView.findViewById(R.id.webview);
- mWebView.setWebViewClient(mWebViewClient);
- mWebView.setFocusable(false);
- final WebSettings settings = mWebView.getSettings();
-
- settings.setJavaScriptEnabled(false);
- settings.setLayoutAlgorithm(LayoutAlgorithm.NORMAL);
-
- settings.setSupportZoom(true);
- settings.setBuiltInZoomControls(true);
- settings.setDisplayZoomControls(false);
-
- mScrollView.setInnerScrollableView(mWebView);
-
- return rootView;
+ return mViewController.onCreateView(inflater, container, savedInstanceState);
}
@Override
- protected WebView getWebView() {
- return mWebView;
+ public void onActivityCreated(Bundle savedInstanceState) {
+ super.onActivityCreated(savedInstanceState);
+ mViewController.onActivityCreated(savedInstanceState);
}
+ // Start implementations of SecureConversationViewControllerCallbacks
+
+ @Override
+ public Fragment getFragment() {
+ return this;
+ }
+
+ @Override
+ public AbstractConversationWebViewClient getWebViewClient() {
+ return mWebViewClient;
+ }
+
+ @Override
+ public void setupConversationHeaderView(ConversationViewHeader headerView) {
+ headerView.setCallbacks(this, this);
+ headerView.setFolders(mConversation);
+ headerView.setSubject(mConversation.subject);
+ }
+
+ @Override
+ public boolean isViewVisibleToUser() {
+ return isUserVisible();
+ }
+
+ @Override
+ public ConversationAccountController getConversationAccountController() {
+ return this;
+ }
+
+ @Override
+ public Map<String, Address> getAddressCache() {
+ return mAddressCache;
+ }
+
+ @Override
+ public void setupMessageHeaderVeiledMatcher(MessageHeaderView messageHeaderView) {
+ messageHeaderView.setVeiledMatcher(
+ ((ControllableActivity) getActivity()).getAccountController()
+ .getVeiledAddressMatcher());
+ }
+
+ @Override
+ public void startMessageLoader() {
+ getLoaderManager().initLoader(MESSAGE_LOADER, null, getMessageLoaderCallbacks());
+ }
+
+ @Override
+ public String getBaseUri() {
+ return mBaseUri;
+ }
+
+ @Override
+ public boolean isViewOnlyMode() {
+ return false;
+ }
+
+ // End implementations of SecureConversationViewControllerCallbacks
+
@Override
protected void markUnread() {
super.markUnread();
// Ignore unsafe calls made after a fragment is detached from an activity
final ControllableActivity activity = (ControllableActivity) getActivity();
- if (activity == null || mConversation == null || mMessage == null) {
+ final ConversationMessage message = mViewController.getMessage();
+ if (activity == null || mConversation == null || message == null) {
LogUtils.w(LOG_TAG, "ignoring markUnread for conv=%s",
mConversation != null ? mConversation.id : 0);
return;
}
final HashSet<Uri> uris = new HashSet<Uri>();
- uris.add(mMessage.uri);
+ uris.add(message.uri);
activity.getConversationUpdater().markConversationMessagesUnread(mConversation, uris,
mViewState.getConversationInfo());
}
@@ -179,41 +216,6 @@
}
@Override
- public void setMessageSpacerHeight(MessageHeaderItem item, int newSpacerHeight) {
- // Do nothing.
- }
-
- @Override
- public void setMessageExpanded(MessageHeaderItem item, int newSpacerHeight) {
- // Do nothing.
- }
-
- @Override
- public void setMessageDetailsExpanded(MessageHeaderItem i, boolean expanded, int heightbefore) {
- // Do nothing.
- }
-
- @Override
- public void showExternalResources(final Message msg) {
- mWebView.getSettings().setBlockNetworkImage(false);
- }
-
- @Override
- public void showExternalResources(final String rawSenderAddress) {
- mWebView.getSettings().setBlockNetworkImage(false);
- }
-
- @Override
- public boolean supportsMessageTransforms() {
- return false;
- }
-
- @Override
- public String getMessageTransforms(final Message msg) {
- return null;
- }
-
- @Override
protected void onMessageCursorLoadFinished(Loader<ObjectCursor<ConversationMessage>> loader,
MessageCursor newCursor, MessageCursor oldCursor) {
// ignore cursors that are still loading results
@@ -225,41 +227,25 @@
// Activity is finishing, just bail.
return;
}
- renderMessageBodies(newCursor);
- }
-
- /**
- * Populate the adapter with overlay views (message headers, super-collapsed
- * blocks, a conversation header), and return an HTML document with spacer
- * divs inserted for all overlays.
- */
- private void renderMessageBodies(MessageCursor messageCursor) {
- if (!messageCursor.moveToFirst()) {
+ if (!newCursor.moveToFirst()) {
LogUtils.e(LOG_TAG, "unable to open message cursor");
return;
}
- final ConversationMessage m = messageCursor.getMessage();
- mMessage = messageCursor.getMessage();
- mWebView.getSettings().setBlockNetworkImage(!mMessage.alwaysShowImages);
- mWebView.loadDataWithBaseURL(mBaseUri, m.getBodyAsHtml(), "text/html", "utf-8", null);
- final ConversationViewAdapter adapter = new ConversationViewAdapter(mActivity, null, null,
- null, null, null, null, null, null);
- final MessageHeaderItem item = adapter.newMessageHeaderItem(mMessage, true,
- mMessage.alwaysShowImages);
- mMessageHeaderView.bind(item, false);
- if (mMessage.hasAttachments) {
- mMessageFooterView.setVisibility(View.VISIBLE);
- mMessageFooterView.bind(item, false);
- }
+
+ mViewController.renderMessage(newCursor.getMessage());
}
@Override
public void onConversationUpdated(Conversation conv) {
- final ConversationViewHeader headerView = mConversationHeaderView;
+ final ConversationViewHeader headerView = mViewController.getConversationHeaderView();
if (headerView != null) {
headerView.onConversationUpdated(conv);
headerView.setSubject(conv.subject);
}
}
+ // Need this stub here
+ public boolean supportsMessageTransforms() {
+ return false;
+ }
}
diff --git a/src/com/android/mail/ui/SwipeHelper.java b/src/com/android/mail/ui/SwipeHelper.java
index 4effb17..7b8b1da 100644
--- a/src/com/android/mail/ui/SwipeHelper.java
+++ b/src/com/android/mail/ui/SwipeHelper.java
@@ -26,7 +26,6 @@
import android.content.Context;
import android.content.res.Resources;
import android.graphics.RectF;
-import android.util.Log;
import android.view.MotionEvent;
import android.view.VelocityTracker;
import android.view.View;
@@ -34,6 +33,7 @@
import com.android.mail.R;
import com.android.mail.browse.ConversationItemView;
+import com.android.mail.utils.LogUtils;
import com.android.mail.utils.Utils;
import java.util.ArrayList;
@@ -59,10 +59,7 @@
private static int MAX_ESCAPE_ANIMATION_DURATION;
private static int MAX_DISMISS_VELOCITY;
private static int SNAP_ANIM_LEN;
- private static int DISMISS_ANIMATION_DURATION;
private static float MIN_SWIPE;
- private static float MIN_VERT;
- private static float MIN_LOCK;
public static float ALPHA_FADE_START = 0f; // fraction of thumbnail width
// where fade starts
@@ -70,7 +67,6 @@
static final float ALPHA_FADE_END = 0.7f; // fraction of thumbnail width
// beyond which alpha->0
private static final float FACTOR = 1.2f;
- private float mMinAlpha = 0.5f;
/* Dead region where swipe cannot be initiated. */
private final static int DEAD_REGION_FOR_SWIPE = 56;
@@ -104,10 +100,7 @@
MAX_ESCAPE_ANIMATION_DURATION = res.getInteger(R.integer.max_escape_animation_duration);
MAX_DISMISS_VELOCITY = res.getInteger(R.integer.max_dismiss_velocity);
SNAP_ANIM_LEN = res.getInteger(R.integer.snap_animation_duration);
- DISMISS_ANIMATION_DURATION = res.getInteger(R.integer.dismiss_animation_duration);
MIN_SWIPE = res.getDimension(R.dimen.min_swipe);
- MIN_VERT = res.getDimension(R.dimen.min_vert);
- MIN_LOCK = res.getDimension(R.dimen.min_lock);
}
}
@@ -155,10 +148,6 @@
v.getMeasuredHeight();
}
- public void setMinAlpha(float minAlpha) {
- mMinAlpha = minAlpha;
- }
-
private float getAlphaForOffset(View view) {
float viewSize = getSize(view);
final float fadeSize = ALPHA_FADE_END * viewSize;
@@ -169,7 +158,8 @@
} else if (pos < viewSize * (1.0f - ALPHA_FADE_START)) {
result = 1.0f + (viewSize * ALPHA_FADE_START + pos) / fadeSize;
}
- return Math.max(mMinAlpha, result);
+ float minAlpha = 0.5f;
+ return Math.max(minAlpha, result);
}
private float getTextAlphaForOffset(View view) {
@@ -197,7 +187,7 @@
public static void invalidateGlobalRegion(View view, RectF childBounds) {
//childBounds.offset(view.getTranslationX(), view.getTranslationY());
if (DEBUG_INVALIDATE)
- Log.v(TAG, "-------------");
+ LogUtils.v(TAG, "-------------");
while (view.getParent() != null && view.getParent() instanceof View) {
view = (View) view.getParent();
view.getMatrix().mapRect(childBounds);
@@ -206,7 +196,7 @@
(int) Math.ceil(childBounds.right),
(int) Math.ceil(childBounds.bottom));
if (DEBUG_INVALIDATE) {
- Log.v(TAG, "INVALIDATE(" + (int) Math.floor(childBounds.left)
+ LogUtils.v(TAG, "INVALIDATE(" + (int) Math.floor(childBounds.left)
+ "," + (int) Math.floor(childBounds.top)
+ "," + (int) Math.ceil(childBounds.right)
+ "," + (int) Math.ceil(childBounds.bottom));
@@ -221,6 +211,12 @@
mLastY = ev.getY();
mDragging = false;
View view = mCallback.getChildAtPosition(ev);
+ if (view instanceof NestedFolderView) {
+ // We don't want to allow nested folders to swipe at all. This would give the
+ // false hope that they might be deleted by swiping away. Instead, treat them
+ // like a plain list view element that doesn't allow any swipe gesture.
+ return false;
+ }
if (view instanceof SwipeableItemView) {
mCurrView = (SwipeableItemView) view;
}
@@ -305,46 +301,6 @@
anim.start();
}
- private void dismissChildren(final Collection<ConversationItemView> views, float velocity,
- AnimatorListenerAdapter listener) {
- final View animView = mCurrView.getSwipeableView().getView();
- final boolean canAnimViewBeDismissed = mCallback.canChildBeDismissed(mCurrView);
- float newPos = determinePos(animView, velocity);
- int duration = DISMISS_ANIMATION_DURATION;
- ArrayList<Animator> animations = new ArrayList<Animator>();
- ObjectAnimator anim;
- for (final ConversationItemView view : views) {
- Utils.enableHardwareLayer(view);
- anim = createDismissAnimation(view, newPos, duration);
- anim.addUpdateListener(new AnimatorUpdateListener() {
- @Override
- public void onAnimationUpdate(ValueAnimator animation) {
- if (FADE_OUT_DURING_SWIPE && canAnimViewBeDismissed) {
- view.setAlpha(getAlphaForOffset(view));
- }
- invalidateGlobalRegion(view);
- }
- });
- anim.addListener(new AnimatorListenerAdapter() {
- @Override
- public void onAnimationEnd(Animator animation) {
- view.setLayerType(View.LAYER_TYPE_NONE, null);
- }
- });
- animations.add(anim);
- }
- AnimatorSet transitionSet = new AnimatorSet();
- transitionSet.playTogether(animations);
- transitionSet.addListener(listener);
- transitionSet.start();
- }
-
- public void dismissChildren(ConversationItemView first,
- final Collection<ConversationItemView> views, AnimatorListenerAdapter listener) {
- mCurrView = first;
- dismissChildren(views, 0f, listener);
- }
-
private static int determineDuration(View animView, float newPos, float velocity) {
int duration = MAX_ESCAPE_ANIMATION_DURATION;
if (velocity != 0) {
@@ -423,11 +379,6 @@
// If the user has gone vertical and not gone horizontalish AT
// LEAST minBeforeLock, switch to scroll. Otherwise, cancel
// the swipe.
- if (!mDragging && deltaY > MIN_VERT && (Math.abs(deltaX)) < MIN_LOCK
- && deltaY > (FACTOR * Math.abs(deltaX))) {
- mCallback.onScroll();
- return false;
- }
float minDistance = MIN_SWIPE;
if (Math.abs(deltaX) < minDistance) {
// Don't start the drag until at least X distance has
@@ -482,7 +433,7 @@
&& (velocity > 0) == (mCurrAnimView.getTranslationX() > 0)
&& translation > 0.05 * currAnimViewSize;
if (LOG_SWIPE_DISMISS_VELOCITY) {
- Log.v(TAG, "Swipe/Dismiss: " + velocity + "/" + escapeVelocity + "/"
+ LogUtils.v(TAG, "Swipe/Dismiss: " + velocity + "/" + escapeVelocity + "/"
+ perpendicularVelocity + ", x: " + translation + "/"
+ currAnimViewSize);
}
diff --git a/src/com/android/mail/ui/SwipeableItemView.java b/src/com/android/mail/ui/SwipeableItemView.java
index 9bf7f34..3a4a13f 100644
--- a/src/com/android/mail/ui/SwipeableItemView.java
+++ b/src/com/android/mail/ui/SwipeableItemView.java
@@ -29,6 +29,11 @@
public void dismiss();
+ /**
+ * Returns the minimum allowed displacement in the Y axis that is considered a scroll. After
+ * this displacement, all future events are considered scroll events rather than swipes.
+ * @return
+ */
public float getMinAllowScrollDistance();
public static class SwipeableView {
diff --git a/src/com/android/mail/ui/SwipeableListView.java b/src/com/android/mail/ui/SwipeableListView.java
index 517cc15..987e52a 100644
--- a/src/com/android/mail/ui/SwipeableListView.java
+++ b/src/com/android/mail/ui/SwipeableListView.java
@@ -160,7 +160,7 @@
public View getChildAtPosition(MotionEvent ev) {
// find the view under the pointer, accounting for GONE views
final int count = getChildCount();
- int touchY = (int) ev.getY();
+ final int touchY = (int) ev.getY();
int childIdx = 0;
View slidingChild;
for (; childIdx < count; childIdx++) {
@@ -331,15 +331,15 @@
@Override
public boolean performItemClick(View view, int pos, long id) {
- int previousPosition = getCheckedItemPosition();
- boolean selectionSetEmpty = mConvSelectionSet.isEmpty();
+ final int previousPosition = getCheckedItemPosition();
+ final boolean selectionSetEmpty = mConvSelectionSet.isEmpty();
// Superclass method modifies the selection set
- boolean handled = super.performItemClick(view, pos, id);
+ final boolean handled = super.performItemClick(view, pos, id);
// If we are in CAB mode with no checkboxes then a click shouldn't
// activate the new item, it should only add it to the selection set
- boolean showSenderImage = mAccount != null
+ final boolean showSenderImage = mAccount != null
&& (mAccount.settings.convListIcon == ConversationListIcon.SENDER_IMAGE);
if (!showSenderImage && !selectionSetEmpty && previousPosition != -1) {
setItemChecked(previousPosition, true);
diff --git a/src/com/android/mail/ui/TwoPaneController.java b/src/com/android/mail/ui/TwoPaneController.java
index e02a5ba..ede87fd 100644
--- a/src/com/android/mail/ui/TwoPaneController.java
+++ b/src/com/android/mail/ui/TwoPaneController.java
@@ -24,6 +24,7 @@
import android.support.v4.widget.DrawerLayout;
import android.view.Gravity;
import android.widget.FrameLayout;
+import android.widget.ListView;
import com.android.mail.ConversationListContext;
import com.android.mail.R;
@@ -69,7 +70,8 @@
FragmentTransaction fragmentTransaction = mActivity.getFragmentManager().beginTransaction();
// Use cross fading animation.
fragmentTransaction.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE);
- Fragment conversationListFragment = ConversationListFragment.newInstance(mConvListContext);
+ final Fragment conversationListFragment =
+ ConversationListFragment.newInstance(mConvListContext);
fragmentTransaction.replace(R.id.conversation_list_pane, conversationListFragment,
TAG_CONVERSATION_LIST);
fragmentTransaction.commitAllowingStateLoss();
@@ -90,30 +92,6 @@
}
}
- /**
- * Create a {@link FolderListFragment} for trees with the specified parent
- * @param parent the parent folder whose children need to be displayed in this list
- */
- private void createFolderTree(Folder parent) {
- setHierarchyFolder(parent);
- createFolderListFragment(FolderListFragment.ofTree(parent, false));
- }
-
- private void createFolderListFragment(Fragment folderList) {
- // Create a sectioned FolderListFragment.
- FragmentTransaction fragmentTransaction = mActivity.getFragmentManager().beginTransaction();
- if (Utils.useFolderListFragmentTransition(mActivity.getActivityContext())) {
- fragmentTransaction.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_OPEN);
- }
- fragmentTransaction.replace(R.id.content_pane, folderList, TAG_FOLDER_LIST);
- fragmentTransaction.commitAllowingStateLoss();
- // We only set the action bar if the viewmode has been set previously. Otherwise, we leave
- // the action bar in the state it is currently in.
- if (mViewMode.getMode() != ViewMode.UNKNOWN) {
- resetActionBarIcon();
- }
- }
-
@Override
protected boolean isConversationListVisible() {
return !mLayout.isConversationListCollapsed();
@@ -132,8 +110,9 @@
mDrawerPullout = mDrawerContainer.findViewById(R.id.content_pane);
mLayout = (TwoPaneLayout) mActivity.findViewById(R.id.two_pane_activity);
if (mLayout == null) {
- // We need the layout for everything. Crash early if it is null.
+ // We need the layout for everything. Crash/Return early if it is null.
LogUtils.wtf(LOG_TAG, "mLayout is null!");
+ return false;
}
mLayout.setController(this, Intent.ACTION_SEARCH.equals(mActivity.getIntent().getAction()));
mLayout.setDrawerLayout(mDrawerContainer);
@@ -166,26 +145,18 @@
mViewMode.enterConversationListMode();
}
- if (folder.hasChildren && !folder.equals(getHierarchyFolder())) {
- // Replace this fragment with a new FolderListFragment
- // showing this folder's children if we are not already looking
- // at the child view for this folder.
- createFolderTree(folder);
+ if (folder.hasChildren) {
// Show the up affordance when digging into child folders.
mActionBarView.setBackButton();
- } else {
- setHierarchyFolder(folder);
}
+ setHierarchyFolder(folder);
super.onFolderSelected(folder);
}
private void goUpFolderHierarchy(Folder current) {
- Folder parent = current.parent;
- if (parent.parent != null) {
- createFolderTree(parent.parent);
- // Show the up affordance when digging into child folders.
- mActionBarView.setBackButton();
- } else {
+ // If the current folder is a child, up should show the parent folder.
+ final Folder parent = current.parent;
+ if (parent != null) {
onFolderSelected(parent);
}
}
@@ -428,7 +399,6 @@
// Show inbox; we are at the top of the hierarchy we were
// showing, and it doesn't have a parent, so we must want to
// the basic account folder list.
- createFolderListFragment(FolderListFragment.ofDrawer());
loadAccountInbox();
}
// Otherwise, if we are in the conversation list but not in the default
@@ -573,4 +543,10 @@
public boolean isDrawerEnabled() {
return mLayout.isDrawerEnabled();
}
+
+ @Override
+ public int getFolderListViewChoiceMode() {
+ // By default, we want to allow one item to be selected in the folder list
+ return ListView.CHOICE_MODE_SINGLE;
+ }
}
diff --git a/src/com/android/mail/ui/settings/ClearPictureApprovalsDialogFragment.java b/src/com/android/mail/ui/settings/ClearPictureApprovalsDialogFragment.java
new file mode 100644
index 0000000..f04eb9f
--- /dev/null
+++ b/src/com/android/mail/ui/settings/ClearPictureApprovalsDialogFragment.java
@@ -0,0 +1,64 @@
+/*
+ * Copyright (C) 2013 Google Inc.
+ * Licensed to The Android Open Source Project.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.mail.ui.settings;
+
+import android.app.AlertDialog;
+import android.app.Dialog;
+import android.app.DialogFragment;
+import android.content.DialogInterface;
+import android.content.DialogInterface.OnClickListener;
+import android.os.Bundle;
+import android.widget.Toast;
+
+import com.android.mail.R;
+import com.android.mail.preferences.MailPrefs;
+
+public class ClearPictureApprovalsDialogFragment extends DialogFragment implements OnClickListener {
+
+ public static final String FRAGMENT_TAG = "ClearPictureApprovalsDialogFragment";
+
+ /**
+ * Creates a new instance of {@link ClearPictureApprovalsDialogFragment}.
+ * @return The newly created {@link ClearPictureApprovalsDialogFragment}.
+ */
+ public static ClearPictureApprovalsDialogFragment newInstance() {
+ final ClearPictureApprovalsDialogFragment fragment =
+ new ClearPictureApprovalsDialogFragment();
+ return fragment;
+ }
+
+ @Override
+ public Dialog onCreateDialog(final Bundle savedInstanceState) {
+ return new AlertDialog.Builder(getActivity())
+ .setTitle(R.string.clear_display_images_whitelist_dialog_title)
+ .setMessage(R.string.clear_display_images_whitelist_dialog_message)
+ .setIconAttribute(android.R.attr.alertDialogIcon)
+ .setPositiveButton(R.string.clear, this)
+ .setNegativeButton(R.string.cancel, this)
+ .create();
+ }
+
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ if (which == DialogInterface.BUTTON_POSITIVE) {
+ final MailPrefs mailPrefs = MailPrefs.get(getActivity());
+ mailPrefs.clearSenderWhiteList();
+ Toast.makeText(getActivity(), R.string.sender_whitelist_cleared, Toast.LENGTH_SHORT)
+ .show();
+ }
+ }
+}
diff --git a/src/com/android/mail/utils/ConversationViewUtils.java b/src/com/android/mail/utils/ConversationViewUtils.java
new file mode 100644
index 0000000..482b746
--- /dev/null
+++ b/src/com/android/mail/utils/ConversationViewUtils.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 2013 Google Inc.
+ * Licensed to The Android Open Source Project.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.mail.utils;
+
+import android.content.res.Resources;
+import android.webkit.WebSettings;
+
+import com.android.mail.R;
+
+public class ConversationViewUtils {
+ public static void setTextZoom(Resources resources, WebSettings settings) {
+ final float fontScale = resources.getConfiguration().fontScale;
+ final int desiredFontSizePx = resources.getInteger(
+ R.integer.conversation_desired_font_size_px);
+ final int unstyledFontSizePx = resources.getInteger(
+ R.integer.conversation_unstyled_font_size_px);
+
+ int textZoom = settings.getTextZoom();
+ // apply a correction to the default body text style to get regular text to the size we want
+ textZoom = textZoom * desiredFontSizePx / unstyledFontSizePx;
+ // then apply any system font scaling
+ textZoom = (int) (textZoom * fontScale);
+ settings.setTextZoom(textZoom);
+ }
+}
diff --git a/src/com/android/mail/utils/FragmentStatePagerAdapter2.java b/src/com/android/mail/utils/FragmentStatePagerAdapter2.java
index 7d0914a..a8cba20 100644
--- a/src/com/android/mail/utils/FragmentStatePagerAdapter2.java
+++ b/src/com/android/mail/utils/FragmentStatePagerAdapter2.java
@@ -25,7 +25,6 @@
import android.support.v13.app.FragmentStatePagerAdapter;
import android.support.v4.util.SparseArrayCompat;
import android.support.v4.view.PagerAdapter;
-import android.util.Log;
import android.view.View;
import android.view.ViewGroup;
@@ -90,7 +89,7 @@
}
Fragment fragment = getItem(position);
- if (DEBUG) Log.v(TAG, "Adding item #" + position + ": f=" + fragment);
+ if (DEBUG) LogUtils.v(TAG, "Adding item #" + position + ": f=" + fragment);
if (mEnableSavedStates && mSavedState.size() > position) {
Fragment.SavedState fss = mSavedState.get(position);
if (fss != null) {
@@ -113,7 +112,7 @@
if (mCurTransaction == null) {
mCurTransaction = mFragmentManager.beginTransaction();
}
- if (DEBUG) Log.v(TAG, "Removing item #" + position + ": f=" + object
+ if (DEBUG) LogUtils.v(TAG, "Removing item #" + position + ": f=" + object
+ " v=" + ((Fragment)object).getView());
if (mEnableSavedStates) {
while (mSavedState.size() <= position) {
@@ -199,7 +198,7 @@
setItemVisible(f, false);
mFragments.put(index, f);
} else {
- Log.w(TAG, "Bad fragment at key " + key);
+ LogUtils.w(TAG, "Bad fragment at key " + key);
}
}
}
diff --git a/unified_src/com/android/mail/utils/LogTag.java b/src/com/android/mail/utils/LogTag.java
similarity index 68%
rename from unified_src/com/android/mail/utils/LogTag.java
rename to src/com/android/mail/utils/LogTag.java
index 01e2cf8..76a598b 100644
--- a/unified_src/com/android/mail/utils/LogTag.java
+++ b/src/com/android/mail/utils/LogTag.java
@@ -17,12 +17,20 @@
package com.android.mail.utils;
public class LogTag {
- private static String LOG_TAG = "UnifiedEmail";
+ private static String sLogTag = "UnifiedEmail";
/**
* Get the log tag to apply to logging.
*/
public static String getLogTag() {
- return LOG_TAG;
+ return sLogTag;
+ }
+
+ /**
+ * Sets the app-wide log tag to be used in most log messages, and for enabling logging
+ * verbosity. This should be called at most once, during app start-up.
+ */
+ public static void setLogTag(final String logTag) {
+ sLogTag = logTag;
}
}
diff --git a/src/com/android/mail/utils/LogUtils.java b/src/com/android/mail/utils/LogUtils.java
index f4fb230..229f4ee 100644
--- a/src/com/android/mail/utils/LogUtils.java
+++ b/src/com/android/mail/utils/LogUtils.java
@@ -25,7 +25,7 @@
public class LogUtils {
- public static final String TAG = "UnifiedEmail";
+ public static final String TAG = LogTag.getLogTag();
// "GMT" + "+" or "-" + 4 digits
private static final Pattern DATE_CLEANUP_PATTERN_WRONG_TIMEZONE =
@@ -61,7 +61,7 @@
* production releases. This should be set to DEBUG for production releases, and VERBOSE for
* internal builds.
*/
- private static final int MAX_ENABLED_LOG_LEVEL = DEBUG;
+ private static final int MAX_ENABLED_LOG_LEVEL = VERBOSE;
private static Boolean sDebugLoggingEnabledForTests = null;
@@ -69,7 +69,7 @@
* Enable debug logging for unit tests.
*/
@VisibleForTesting
- static void setDebugLoggingEnabledForTests(boolean enabled) {
+ public static void setDebugLoggingEnabledForTests(boolean enabled) {
setDebugLoggingEnabledForTestsInternal(enabled);
}
@@ -95,7 +95,15 @@
if (sDebugLoggingEnabledForTests != null) {
return sDebugLoggingEnabledForTests.booleanValue();
}
- return Log.isLoggable(tag, Log.DEBUG);
+ return Log.isLoggable(tag, Log.DEBUG) || Log.isLoggable(TAG, Log.DEBUG);
+ }
+
+ /**
+ * Returns a String for the specified content provider uri. This will do
+ * sanitation of the uri to remove PII if debug logging is not enabled.
+ */
+ public static String contentUriToString(final Uri uri) {
+ return contentUriToString(TAG, uri);
}
/**
@@ -128,39 +136,6 @@
}
}
- /* TODO: what is the correct behavior for base case and the Gmail case? Seems like this
- * belongs in override code in UnifiedGmail.
- *Converts the specified set of labels to a string, and removes any PII as necessary
- * public static String labelSetToString(Set<String> labelSet) {
- if (isDebugLoggingEnabled() || labelSet == null) {
- return labelSet != null ? labelSet.toString() : "";
- } else {
- final StringBuilder builder = new StringBuilder("[");
- int i = 0;
- for(String label : labelSet) {
- if (i > 0) {
- builder.append(", ");
- }
- builder.append(sanitizeLabelName(label));
- i++;
- }
- builder.append(']');
- return builder.toString();
- }
- }
-
- private static String sanitizeLabelName(String canonicalName) {
- if (TextUtils.isEmpty(canonicalName)) {
- return "";
- }
-
- if (Gmail.isSystemLabel(canonicalName)) {
- return canonicalName;
- }
-
- return USER_LABEL_PREFIX + String.valueOf(canonicalName.hashCode());
- }*/
-
/**
* Checks to see whether or not a log for the specified tag is loggable at the specified level.
*/
@@ -168,7 +143,7 @@
if (MAX_ENABLED_LOG_LEVEL > level) {
return false;
}
- return Log.isLoggable(tag, level);
+ return Log.isLoggable(tag, level) || Log.isLoggable(TAG, level);
}
/**
diff --git a/src/com/android/mail/utils/MimeType.java b/src/com/android/mail/utils/MimeType.java
index 8f0ca96..ab2c6e4 100644
--- a/src/com/android/mail/utils/MimeType.java
+++ b/src/com/android/mail/utils/MimeType.java
@@ -15,8 +15,6 @@
*/
package com.android.mail.utils;
-import com.android.mail.utils.LogTag;
-
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
@@ -43,20 +41,14 @@
static final String GENERIC_MIMETYPE = "application/octet-stream";
@VisibleForTesting
- static final String EML_ATTACHMENT_CONTENT_TYPE = "application/eml";
+ private static final Set<String> EML_ATTACHMENT_CONTENT_TYPES = ImmutableSet.of(
+ "message/rfc822", "application/eml");
+ public static final String EML_ATTACHMENT_CONTENT_TYPE = "message/rfc822";
private static final String NULL_ATTACHMENT_CONTENT_TYPE = "null";
private static final Set<String> UNACCEPTABLE_ATTACHMENT_TYPES = ImmutableSet.of(
"application/zip", "application/x-gzip", "application/x-bzip2",
"application/x-compress", "application/x-compressed", "application/x-tar");
- private static Set<String> sGviewSupportedTypes = ImmutableSet.of(
- "application/pdf",
- "application/vnd.ms-powerpoint",
- "image/tiff",
- "application/msword",
- "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
- "application/vnd.openxmlformats-officedocument.presentationml.presentation");
-
/**
* Returns whether or not an attachment of the specified type is installable (e.g. an apk).
*/
@@ -120,16 +112,6 @@
return UNACCEPTABLE_ATTACHMENT_TYPES.contains(contentType);
}
- /* TODO: what do we want to do about GSF keys for the unified app?
- public static boolean isPreviewable(Context context, String contentType) {
- final String supportedTypes = Gservices.getString(
- context.getContentResolver(), GservicesKeys.GMAIL_GVIEW_SUPPORTED_TYPES);
- if (supportedTypes != null) {
- sGviewSupportedTypes = ImmutableSet.of(TextUtils.split(supportedTypes, ","));
- }
- return sGviewSupportedTypes.contains(contentType);
- }*/
-
/**
* Extract and return filename's extension, converted to lower case, and not including the "."
*
@@ -151,7 +133,7 @@
* Returns the mime type of the attachment based on its name and
* original mime type. This is an workaround for bugs where Gmail
* server doesn't set content-type for certain types correctly.
- * 1) EML files -> "application/eml".
+ * 1) EML files -> "message/rfc822".
* @param name name of the attachment.
* @param mimeType original mime type of the attachment.
* @return the inferred mime type of the attachment.
@@ -174,7 +156,7 @@
if (!TextUtils.isEmpty(type)) {
return type;
} if (extension.equals("eml")) {
- // Extension is ".eml", return mime type "application/eml"
+ // Extension is ".eml", return mime type "message/rfc822"
return EML_ATTACHMENT_CONTENT_TYPE;
} else {
// Extension is not ".eml", just return original mime type.
@@ -182,4 +164,14 @@
}
}
}
+
+ /**
+ * Checks the supplied mime type to determine if it is a valid eml file.
+ * Valid mime types are "message/rfc822" and "application/eml".
+ * @param mimeType the mime type to check
+ * @return {@code true} if the mime type is one of the valid mime types.
+ */
+ public static boolean isEmlMimeType(String mimeType) {
+ return EML_ATTACHMENT_CONTENT_TYPES.contains(mimeType);
+ }
}
diff --git a/src/com/android/mail/utils/NotificationUtils.java b/src/com/android/mail/utils/NotificationUtils.java
index de4d887..1e2ffaf 100644
--- a/src/com/android/mail/utils/NotificationUtils.java
+++ b/src/com/android/mail/utils/NotificationUtils.java
@@ -32,13 +32,9 @@
import android.provider.ContactsContract.CommonDataKinds.Email;
import android.provider.ContactsContract.Contacts.Photo;
import android.support.v4.app.NotificationCompat;
-import android.text.Html;
-import android.text.Spannable;
import android.text.SpannableString;
import android.text.SpannableStringBuilder;
-import android.text.Spanned;
import android.text.TextUtils;
-import android.text.TextUtils.SimpleStringSplitter;
import android.text.style.CharacterStyle;
import android.text.style.TextAppearanceSpan;
import android.util.Pair;
@@ -53,6 +49,7 @@
import com.android.mail.preferences.FolderPreferences;
import com.android.mail.preferences.MailPrefs;
import com.android.mail.providers.Account;
+import com.android.mail.providers.Address;
import com.android.mail.providers.Conversation;
import com.android.mail.providers.Folder;
import com.android.mail.providers.Message;
@@ -63,17 +60,15 @@
import com.google.android.common.html.parser.HtmlDocument;
import com.google.android.common.html.parser.HtmlTree;
import com.google.common.base.Objects;
+import com.google.common.collect.ImmutableList;
import com.google.common.collect.Lists;
-import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import java.io.ByteArrayInputStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
-import java.util.Deque;
import java.util.List;
-import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
@@ -88,11 +83,6 @@
private static TextAppearanceSpan sNotificationUnreadStyleSpan;
private static CharacterStyle sNotificationReadStyleSpan;
- private static final Map<Integer, Integer> sPriorityToLength = Maps.newHashMap();
- private static final SimpleStringSplitter SENDER_LIST_SPLITTER =
- new SimpleStringSplitter(Utils.SENDER_LIST_SEPARATOR);
- private static String[] sSenderFragments = new String[8];
-
/** A factory that produces a plain text converter that removes elided text. */
private static final HtmlTree.PlainTextConverterFactory MESSAGE_CONVERTER_FACTORY =
new HtmlTree.PlainTextConverterFactory() {
@@ -565,14 +555,15 @@
if (unreadCount > 0) {
// How can I order this properly?
if (cursor.moveToNext()) {
- Intent notificationIntent = createViewConversationIntent(context, account,
- folder, null);
+ final Intent notificationIntent;
- // Launch directly to the conversation, if the
- // number of unseen conversations == 1
+ // Launch directly to the conversation, if there is only 1 unseen conversation
if (unseenCount == 1) {
notificationIntent = createViewConversationIntent(context, account, folder,
cursor);
+ } else {
+ notificationIntent = createViewConversationIntent(context, account, folder,
+ null);
}
if (notificationIntent == null) {
@@ -1205,47 +1196,10 @@
}
/**
- * Adds a fragment with given style to a string builder.
- *
- * @param builder the current string builder
- * @param fragment the fragment to be added
- * @param style the style of the fragment
- * @param withSpaces whether to add the whole fragment or to divide it into
- * smaller ones
+ * Clears the notifications for the specified account/folder.
*/
- private static void addStyledFragment(SpannableStringBuilder builder, String fragment,
- CharacterStyle style, boolean withSpaces) {
- if (withSpaces) {
- int pos = builder.length();
- builder.append(fragment);
- builder.setSpan(CharacterStyle.wrap(style), pos, builder.length(),
- Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
- } else {
- int start = 0;
- while (true) {
- int pos = fragment.substring(start).indexOf(' ');
- if (pos == -1) {
- addStyledFragment(builder, fragment.substring(start), style, true);
- break;
- } else {
- pos += start;
- if (start < pos) {
- addStyledFragment(builder, fragment.substring(start, pos), style, true);
- builder.append(' ');
- }
- start = pos + 1;
- if (start >= fragment.length()) {
- break;
- }
- }
- }
- }
- }
-
- /**
- * Clears the notifications for the specified account/folder/conversation.
- */
- public static void clearFolderNotification(Context context, Account account, Folder folder) {
+ public static void clearFolderNotification(Context context, Account account, Folder folder,
+ final boolean markSeen) {
LogUtils.v(LOG_TAG, "NotificationUtils: Clearing all notifications for %s/%s", account.name,
folder.name);
final NotificationMap notificationMap = getNotificationMap(context);
@@ -1253,7 +1207,43 @@
notificationMap.remove(key);
notificationMap.saveNotificationMap(context);
- markSeen(context, folder);
+ final NotificationManager notificationManager =
+ (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
+ notificationManager.cancel(getNotificationId(account.name, folder));
+
+ if (markSeen) {
+ markSeen(context, folder);
+ }
+ }
+
+ /**
+ * Clears all notifications for the specified account.
+ */
+ public static void clearAccountNotifications(final Context context, final String account) {
+ LogUtils.v(LOG_TAG, "NotificationUtils: Clearing all notifications for %s", account);
+ final NotificationMap notificationMap = getNotificationMap(context);
+
+ // Find all NotificationKeys for this account
+ final ImmutableList.Builder<NotificationKey> keyBuilder = ImmutableList.builder();
+
+ for (final NotificationKey key : notificationMap.keySet()) {
+ if (account.equals(key.account.name)) {
+ keyBuilder.add(key);
+ }
+ }
+
+ final List<NotificationKey> notificationKeys = keyBuilder.build();
+
+ final NotificationManager notificationManager =
+ (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
+
+ for (final NotificationKey notificationKey : notificationKeys) {
+ final Folder folder = notificationKey.folder;
+ notificationManager.cancel(getNotificationId(account, folder));
+ notificationMap.remove(notificationKey);
+ }
+
+ notificationMap.saveNotificationMap(context);
}
private static ArrayList<Long> findContacts(Context context, Collection<String> addresses) {
@@ -1371,10 +1361,14 @@
final EmailAddress address = EmailAddress.getEmailAddress(sender);
String displayableSender = address.getName();
- // If that fails, default to the sender address.
- if (TextUtils.isEmpty(displayableSender)) {
- displayableSender = address.getAddress();
+
+ if (!TextUtils.isEmpty(displayableSender)) {
+ return Address.decodeAddressName(displayableSender);
}
+
+ // If that fails, default to the sender address.
+ displayableSender = address.getAddress();
+
// If we were unable to tokenize a name or address,
// just use whatever was in the sender.
if (TextUtils.isEmpty(displayableSender)) {
@@ -1450,14 +1444,7 @@
private static final HtmlDocument.Node ELIDED_TEXT_REPLACEMENT_NODE =
HtmlDocument.createSelfTerminatingTag(HTML4.BR_ELEMENT, null, null, null);
- private static final String STYLE_ELEMENT_ATTRIBUTE_CLASS_VALUE = "style";
-
private int mEndNodeElidedTextBlock = -1;
- /**
- * A stack of the end tag numbers for <style /> tags. We don't want to
- * include anything between these.
- */
- private Deque<Integer> mStyleNodeEnds = Lists.newLinkedList();
@Override
public void addNode(HtmlDocument.Node n, int nodeNum, int endNum) {
@@ -1487,8 +1474,6 @@
break;
}
}
- } else if (STYLE_ELEMENT_ATTRIBUTE_CLASS_VALUE.equals(htmlElement.getName())) {
- mStyleNodeEnds.push(endNum);
}
if (foundElidedTextTag) {
@@ -1496,13 +1481,7 @@
}
}
- if (!mStyleNodeEnds.isEmpty() && mStyleNodeEnds.peek() == nodeNum) {
- mStyleNodeEnds.pop();
- }
-
- if (mStyleNodeEnds.isEmpty()) {
- super.addNode(n, nodeNum, endNum);
- }
+ super.addNode(n, nodeNum, endNum);
}
}
diff --git a/src/com/android/mail/utils/Utils.java b/src/com/android/mail/utils/Utils.java
index 38e6c1d..e5b9e10 100644
--- a/src/com/android/mail/utils/Utils.java
+++ b/src/com/android/mail/utils/Utils.java
@@ -899,8 +899,14 @@
* Show the feedback screen for the supplied account.
*/
public static void sendFeedback(FeedbackEnabledActivity activity, Account account,
+ boolean reportingProblem) {
+ if (activity != null && account != null) {
+ sendFeedback(activity, account.sendFeedbackIntentUri, reportingProblem);
+ }
+ }
+ public static void sendFeedback(FeedbackEnabledActivity activity, Uri feedbackIntentUri,
boolean reportingProblem) {
- if (activity != null && account != null && !isEmpty(account.sendFeedbackIntentUri)) {
+ if (activity != null && !isEmpty(feedbackIntentUri)) {
final Bundle optionalExtras = new Bundle(2);
optionalExtras.putBoolean(
UIProvider.SendFeedbackExtras.EXTRA_REPORTING_PROBLEM, reportingProblem);
@@ -909,10 +915,11 @@
optionalExtras.putParcelable(
UIProvider.SendFeedbackExtras.EXTRA_SCREEN_SHOT, screenBitmap);
}
- openUrl(activity.getActivityContext(), account.sendFeedbackIntentUri, optionalExtras);
+ openUrl(activity.getActivityContext(), feedbackIntentUri, optionalExtras);
}
}
+
public static Bitmap getReducedSizeBitmap(FeedbackEnabledActivity activity) {
final Window activityWindow = activity.getWindow();
final View currentView = activityWindow != null ? activityWindow.getDecorView() : null;
@@ -1280,4 +1287,64 @@
return uri.buildUpon().appendQueryParameter(APP_VERSION_QUERY_PARAMETER,
Integer.toString(appVersion)).build();
}
+
+ /**
+ * Gets the specified {@link Folder} object.
+ *
+ * @param folderUri The {@link Uri} for the folder
+ * @param allowHidden <code>true</code> to allow a hidden folder to be returned,
+ * <code>false</code> to return <code>null</code> instead
+ * @return the specified {@link Folder} object, or <code>null</code>
+ */
+ public static Folder getFolder(final Context context, final Uri folderUri,
+ final boolean allowHidden) {
+ final Uri uri = folderUri
+ .buildUpon()
+ .appendQueryParameter(UIProvider.ALLOW_HIDDEN_FOLDERS_QUERY_PARAM,
+ Boolean.toString(allowHidden))
+ .build();
+
+ final Cursor cursor = context.getContentResolver().query(uri,
+ UIProvider.FOLDERS_PROJECTION, null, null, null);
+
+ if (cursor == null) {
+ return null;
+ }
+
+ try {
+ if (cursor.moveToFirst()) {
+ return new Folder(cursor);
+ } else {
+ return null;
+ }
+ } finally {
+ cursor.close();
+ }
+ }
+
+ /**
+ * Begins systrace tracing for a given tag. No-op on unsupported platform versions.
+ *
+ * @param tag systrace tag to use
+ *
+ * @see android.os.Trace#beginSection(String)
+ */
+ public static void traceBeginSection(String tag) {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) {
+ android.os.Trace.beginSection(tag);
+ }
+ }
+
+ /**
+ * Ends systrace tracing for the most recently begun section. No-op on unsupported platform
+ * versions.
+ *
+ * @see android.os.Trace#endSection()
+ */
+ public static void traceEndSection() {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) {
+ android.os.Trace.endSection();
+ }
+ }
+
}
diff --git a/src/com/google/android/common/html/parser/HtmlTree.java b/src/com/google/android/common/html/parser/HtmlTree.java
index 24ce526..35615f0 100644
--- a/src/com/google/android/common/html/parser/HtmlTree.java
+++ b/src/com/google/android/common/html/parser/HtmlTree.java
@@ -18,6 +18,7 @@
import com.google.android.common.base.CharMatcher;
import com.google.android.common.base.Preconditions;
import com.google.android.common.base.X;
+import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.ImmutableSet;
import java.util.ArrayList;
@@ -46,6 +47,8 @@
* @author jlim@google.com (Jing Yee Lim)
*/
public class HtmlTree {
+ // http://www.w3.org/TR/html4/struct/text.html#h-9.1
+ private static final CharMatcher HTML_WHITESPACE = CharMatcher.anyOf(" \t\f\u200b\r\n");
/**
* An interface that allows clients to provide their own implementation
@@ -88,6 +91,7 @@
/** A factory that produces converters of the default implementation. */
private static final PlainTextConverterFactory DEFAULT_CONVERTER_FACTORY =
new PlainTextConverterFactory() {
+ @Override
public PlainTextConverter createInstance() {
return new DefaultPlainTextConverter();
}
@@ -150,6 +154,35 @@
}
/**
+ * Returns number of matching open tag node, or {@code endTagNodeNum} itself
+ * if it does not point to a closing tag.
+ */
+ public int findOpenTag(int endTagNodeNum) {
+ X.assertTrue(endTagNodeNum >= 0 && endTagNodeNum < nodes.size());
+ return begins.get(endTagNodeNum);
+ }
+
+ /**
+ * Returns number of matching closing tag node, or {@code openTagNodeNum} itself
+ * if it does not point to an open tag or points to an open tag with no closing one.
+ */
+ public int findEndTag(int openTagNodeNum) {
+ X.assertTrue(openTagNodeNum >= 0 && openTagNodeNum < nodes.size());
+ return ends.get(openTagNodeNum);
+ }
+
+ /**
+ * Returns number of matching open/closing tag node, or {@code tagNodeNum} itself
+ * if it does not point to an open/closing tag (e.g text node or comment).
+ */
+ public int findPairedTag(int tagNodeNum) {
+ X.assertTrue(tagNodeNum >= 0 && tagNodeNum < nodes.size());
+ int openNodeNum = begins.get(tagNodeNum);
+ int endNodeNum = ends.get(tagNodeNum);
+ return tagNodeNum == openNodeNum ? endNodeNum : openNodeNum;
+ }
+
+ /**
* Gets the entire html.
*/
public String getHtml() {
@@ -238,13 +271,13 @@
if (node instanceof HtmlDocument.Tag) {
if (HTML4.TEXTAREA_ELEMENT.equals(
- ((HtmlDocument.Tag)node).getElement())) {
+ ((HtmlDocument.Tag) node).getElement())) {
stack++;
}
}
if (node instanceof HtmlDocument.EndTag) {
if (HTML4.TEXTAREA_ELEMENT.equals(
- ((HtmlDocument.EndTag)node).getElement())) {
+ ((HtmlDocument.EndTag) node).getElement())) {
if (stack == 0) {
balanced = false;
} else {
@@ -435,7 +468,7 @@
if (ch == '\n') {
return true;
}
- if (i < textPos && !Character.isWhitespace(ch)) {
+ if (i < textPos && !HTML_WHITESPACE.matches(ch)) {
return false;
}
}
@@ -556,6 +589,7 @@
* Encapsulates the logic for outputting plain text with respect to text
* segments, white space separators, line breaks, and quote marks.
*/
+ @VisibleForTesting
static final class PlainTextPrinter {
/**
* Separators are whitespace inserted between segments of text. The
@@ -813,7 +847,9 @@
private final PlainTextPrinter printer = new PlainTextPrinter();
private int preDepth = 0;
+ private int styleDepth = 0;
+ @Override
public void addNode(HtmlDocument.Node n, int nodeNum, int endNum) {
if (n instanceof HtmlDocument.Text) { // A string node
@@ -823,6 +859,8 @@
if (preDepth > 0) {
printer.appendPreText(str);
+ } else if (styleDepth > 0) {
+ // Append nothing
} else {
printer.appendNormalText(str);
}
@@ -855,6 +893,8 @@
} else if (HTML4.PRE_ELEMENT.equals(element)) {
preDepth++;
+ } else if (HTML4.STYLE_ELEMENT.equals(element)) {
+ styleDepth++;
}
} else if (n instanceof HtmlDocument.EndTag) {
@@ -876,14 +916,18 @@
} else if (HTML4.PRE_ELEMENT.equals(element)) {
preDepth--;
+ } else if (HTML4.STYLE_ELEMENT.equals(element)) {
+ styleDepth--;
}
}
}
+ @Override
public final int getPlainTextLength() {
return printer.getTextLength();
}
+ @Override
public final String getPlainText() {
return printer.getText();
}
@@ -909,6 +953,7 @@
}
/**
+ * Adds a html start tag, there must followed later by a call to addEndTag()
* to add the matching end tag
*/
void addStartTag(HtmlDocument.Tag t) {
@@ -930,7 +975,6 @@
ends.set(parent, nodenum);
}
- //is this the right pop?
parent = stack.pop();
}
@@ -951,7 +995,6 @@
/** Adds a node */
private void addNode(HtmlDocument.Node n, int begin, int end) {
-
nodes.add(n);
begins.add(begin);
ends.add(end);
diff --git a/tests/src/com/android/mail/utils/MimeTypeTest.java b/tests/src/com/android/mail/utils/MimeTypeTest.java
new file mode 100644
index 0000000..9a8cd32
--- /dev/null
+++ b/tests/src/com/android/mail/utils/MimeTypeTest.java
@@ -0,0 +1,37 @@
+// Copyright 2011 Google Inc. All Rights Reserved.
+
+package com.android.mail.utils;
+
+import android.test.AndroidTestCase;
+import android.test.suitebuilder.annotation.SmallTest;
+
+@SmallTest
+public class MimeTypeTest extends AndroidTestCase {
+
+ private static final String TEST_MIME_TYPE = "test/mimetype";
+ public void testInferMimeType() {
+ // eml file
+ assertEquals(MimeType.EML_ATTACHMENT_CONTENT_TYPE,
+ MimeType.inferMimeType("filename.eml", MimeType.GENERIC_MIMETYPE));
+
+ // mpeg4 video files
+ assertEquals("video/mp4", MimeType.inferMimeType("video.mp4", MimeType.GENERIC_MIMETYPE));
+
+ // file with no extension, should return the mimetype that was specified
+ assertEquals(TEST_MIME_TYPE, MimeType.inferMimeType("filename", TEST_MIME_TYPE));
+
+ // file with extension, and empty mimetype, where an mimetype can be derived
+ // from the extension.
+ assertEquals("video/mp4", MimeType.inferMimeType("video.mp4", ""));
+
+ // file with extension, and empty mimetype, where an mimetype can not be derived
+ // from the extension.
+ assertEquals(MimeType.GENERIC_MIMETYPE, MimeType.inferMimeType("video.foo", ""));
+
+ // rtf files, with a generic mimetype
+ assertEquals("text/rtf", MimeType.inferMimeType("filename.rtf", MimeType.GENERIC_MIMETYPE));
+
+ // rtf files, with a specified mimetype
+ assertEquals("application/rtf", MimeType.inferMimeType("filename.rtf", "application/rtf"));
+ }
+}