am 661e4783: (-s ours) DO NOT MERGE Prevent crash with bad Intent
* commit '661e4783a7c57f42849489ed54cf5495ada26314':
DO NOT MERGE Prevent crash with bad Intent
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index 8ddad7e..24fa2b2 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -143,12 +143,6 @@
<provider android:name="com.android.mail.providers.SuggestionsProvider"
android:authorities="com.android.mail.suggestionsprovider" />
- <receiver android:name=".providers.protos.boot.AccountReceiver">
- <intent-filter>
- <action android:name="com.android.mail.providers.protos.boot.intent.ACTION_PROVIDER_CREATED" />
- </intent-filter>
- </receiver>
-
<service android:name=".compose.EmptyService"/>
<!-- Widget -->
@@ -165,6 +159,7 @@
<service android:name=".widget.WidgetService"
android:permission="android.permission.BIND_REMOTEVIEWS"
android:exported="false" />
+ <service android:name=".MailLogService"/>
</application>
diff --git a/assets/script.js b/assets/script.js
index 215349d..51a9d69 100644
--- a/assets/script.js
+++ b/assets/script.js
@@ -24,6 +24,15 @@
var gScaleInfo;
/**
+ * Only revert transforms that do an imperfect job of shrinking content if they fail
+ * to shrink by this much. Expressed as a ratio of:
+ * (original width difference : width difference after transforms);
+ */
+TRANSFORM_MINIMUM_EFFECTIVE_RATIO = 0.7;
+
+var gTransformText = {};
+
+/**
* Returns the page offset of an element.
*
* @param {Element} element The element to return the page offset for.
@@ -136,10 +145,6 @@
var contentValues;
var isEmpty;
- if (!NORMALIZE_MESSAGE_WIDTHS) {
- return;
- }
-
expandedBodyDivs = document.querySelectorAll(".expanded > .mail-message-content");
isEmpty = isConversationEmpty(expandedBodyDivs);
@@ -169,10 +174,6 @@
var documentWidth;
var newZoom, oldZoom;
- if (!NORMALIZE_MESSAGE_WIDTHS) {
- return;
- }
-
documentWidth = document.body.offsetWidth;
for (i = 0; i < elements.length; i++) {
@@ -183,10 +184,235 @@
el.style.zoom = 1;
}
newZoom = documentWidth / el.scrollWidth;
- el.style.zoom = newZoom;
+ transformContent(el, documentWidth, el.scrollWidth);
+ newZoom = documentWidth / el.scrollWidth;
+ if (NORMALIZE_MESSAGE_WIDTHS) {
+ el.style.zoom = newZoom;
+ }
}
}
+function transformContent(el, docWidth, elWidth) {
+ var nodes;
+ var i, len;
+ var index;
+ var newWidth = elWidth;
+ var wStr;
+ var touched;
+ // the format of entries in this array is:
+ // entry := [ undoFunction, undoFunctionThis, undoFunctionParamArray ]
+ var actionLog = [];
+ var node;
+ var done = false;
+ var msgId;
+ var transformText;
+ var existingText;
+ var textElement;
+ var start;
+ if (elWidth <= docWidth) {
+ return;
+ }
+
+ start = Date.now();
+
+ if (el.parentElement.classList.contains("mail-message")) {
+ msgId = el.parentElement.id;
+ transformText = "[origW=" + elWidth + "/" + docWidth;
+ }
+
+ // Try munging all divs or textareas with inline styles where the width
+ // is wider than docWidth, and change it to be a max-width.
+ touched = false;
+ nodes = ENABLE_MUNGE_TABLES ? el.querySelectorAll("div[style], textarea[style]") : [];
+ touched = transformBlockElements(nodes, docWidth, actionLog);
+ if (touched) {
+ newWidth = el.scrollWidth;
+ console.log("ran div-width munger on el=" + el + " oldW=" + elWidth + " newW=" + newWidth
+ + " docW=" + docWidth);
+ if (msgId) {
+ transformText += " DIV:newW=" + newWidth;
+ }
+ if (newWidth <= docWidth) {
+ done = true;
+ }
+ }
+
+ if (!done) {
+ // OK, that wasn't enough. Find images with widths and override their widths.
+ nodes = ENABLE_MUNGE_IMAGES ? el.querySelectorAll("img") : [];
+ touched = transformImages(nodes, docWidth, actionLog);
+ if (touched) {
+ newWidth = el.scrollWidth;
+ console.log("ran img munger on el=" + el + " oldW=" + elWidth + " newW=" + newWidth
+ + " docW=" + docWidth);
+ if (msgId) {
+ transformText += " IMG:newW=" + newWidth;
+ }
+ if (newWidth <= docWidth) {
+ done = true;
+ }
+ }
+ }
+
+ if (!done) {
+ // OK, that wasn't enough. Find tables with widths and override their widths.
+ nodes = ENABLE_MUNGE_TABLES ? el.querySelectorAll("table") : [];
+ touched = addClassToElements(nodes, shouldMungeTable, "munged",
+ actionLog);
+ if (touched) {
+ newWidth = el.scrollWidth;
+ console.log("ran table munger on el=" + el + " oldW=" + elWidth + " newW=" + newWidth
+ + " docW=" + docWidth);
+ if (msgId) {
+ transformText += " TABLE:newW=" + newWidth;
+ }
+ if (newWidth <= docWidth) {
+ done = true;
+ }
+ }
+ }
+
+ if (!done) {
+ // OK, that wasn't enough. Try munging all <td> to override any width and nowrap set.
+ nodes = ENABLE_MUNGE_TABLES ? el.querySelectorAll("td") : [];
+ touched = addClassToElements(nodes, null /* mungeAll */, "munged",
+ actionLog);
+ if (touched) {
+ newWidth = el.scrollWidth;
+ console.log("ran td munger on el=" + el + " oldW=" + elWidth + " newW=" + newWidth
+ + " docW=" + docWidth);
+ if (msgId) {
+ transformText += " TD:newW=" + newWidth;
+ }
+ if (newWidth <= docWidth) {
+ done = true;
+ }
+ }
+ }
+
+ // If the transformations shrank the width significantly enough, leave them in place.
+ // We figure that in those cases, the benefits outweight the risk of rendering artifacts.
+ if (!done && (elWidth - newWidth) / (elWidth - docWidth) >
+ TRANSFORM_MINIMUM_EFFECTIVE_RATIO) {
+ console.log("transform(s) deemed effective enough");
+ done = true;
+ }
+
+ if (done) {
+ if (msgId) {
+ transformText += "]";
+ existingText = gTransformText[msgId];
+ if (!existingText) {
+ transformText = "Message transforms: " + transformText;
+ } else {
+ transformText = existingText + " " + transformText;
+ }
+ gTransformText[msgId] = transformText;
+ window.mail.onMessageTransform(msgId, transformText);
+ textElement = el.firstChild;
+ if (!textElement.classList || !textElement.classList.contains("transform-text")) {
+ textElement = document.createElement("div");
+ textElement.classList.add("transform-text");
+ textElement.style.fontSize = "10px";
+ textElement.style.color = "#ccc";
+ el.insertBefore(textElement, el.firstChild);
+ }
+ textElement.innerHTML = transformText + "<br>";
+ }
+ console.log("munger(s) succeeded, elapsed time=" + (Date.now() - start));
+ return;
+ }
+
+ // reverse all changes if the width is STILL not narrow enough
+ // (except the width->maxWidth change, which is not particularly destructive)
+ for (i = 0, len = actionLog.length; i < len; i++) {
+ actionLog[i][0].apply(actionLog[i][1], actionLog[i][2]);
+ }
+ if (actionLog.length > 0) {
+ console.log("all mungers failed, changes reversed. elapsed time=" + (Date.now() - start));
+ }
+}
+
+function addClassToElements(nodes, conditionFn, classToAdd, actionLog) {
+ var i, len;
+ var node;
+ var added = false;
+ for (i = 0, len = nodes.length; i < len; i++) {
+ node = nodes[i];
+ if (!conditionFn || conditionFn(node)) {
+ if (node.classList.contains(classToAdd)) {
+ continue;
+ }
+ node.classList.add(classToAdd);
+ added = true;
+ actionLog.push([node.classList.remove, node.classList, [classToAdd]]);
+ }
+ }
+ return added;
+}
+
+function transformBlockElements(nodes, docWidth, actionLog) {
+ var i, len;
+ var node;
+ var wStr;
+ var index;
+ var touched = false;
+
+ for (i = 0, len = nodes.length; i < len; i++) {
+ node = nodes[i];
+ wStr = node.style.width || node.style.minWidth;
+ index = wStr ? wStr.indexOf("px") : -1;
+ if (index >= 0 && wStr.slice(0, index) > docWidth) {
+ saveStyleProperty(node, "width", actionLog);
+ saveStyleProperty(node, "minWidth", actionLog);
+ saveStyleProperty(node, "maxWidth", actionLog);
+ node.style.width = "100%";
+ node.style.minWidth = "";
+ node.style.maxWidth = wStr;
+ touched = true;
+ }
+ }
+ return touched;
+}
+
+function transformImages(nodes, docWidth, actionLog) {
+ var i, len;
+ var node;
+ var w, h;
+ var touched = false;
+
+ for (i = 0, len = nodes.length; i < len; i++) {
+ node = nodes[i];
+ w = node.offsetWidth;
+ h = node.offsetHeight;
+ // shrink w/h proportionally if the img is wider than available width
+ if (w > docWidth) {
+ saveStyleProperty(node, "maxWidth", actionLog);
+ saveStyleProperty(node, "width", actionLog);
+ saveStyleProperty(node, "height", actionLog);
+ node.style.maxWidth = docWidth + "px";
+ node.style.width = "100%";
+ node.style.height = "auto";
+ touched = true;
+ }
+ }
+ return touched;
+}
+
+function saveStyleProperty(node, property, actionLog) {
+ var savedName = "data-" + property;
+ node.setAttribute(savedName, node.style[property]);
+ actionLog.push([undoSetProperty, node, [property, savedName]]);
+}
+
+function undoSetProperty(property, savedProperty) {
+ this.style[property] = savedProperty ? this.getAttribute(savedProperty) : "";
+}
+
+function shouldMungeTable(table) {
+ return table.hasAttribute("width") || table.style.width;
+}
+
function hideAllUnsafeImages() {
hideUnsafeImages(document.getElementsByClassName("mail-message-content"));
}
diff --git a/res/drawable-hdpi/ic_menu_search_holo_light.png b/res/drawable-hdpi/ic_menu_search_holo_light.png
index dae6979..1cb61fa 100644
--- a/res/drawable-hdpi/ic_menu_search_holo_light.png
+++ b/res/drawable-hdpi/ic_menu_search_holo_light.png
Binary files differ
diff --git a/res/drawable-hdpi/stat_notify_email.png b/res/drawable-hdpi/stat_notify_email.png
index 276d429..d40e2fe 100644
--- a/res/drawable-hdpi/stat_notify_email.png
+++ b/res/drawable-hdpi/stat_notify_email.png
Binary files differ
diff --git a/res/drawable-mdpi/ic_menu_search_holo_light.png b/res/drawable-mdpi/ic_menu_search_holo_light.png
index 2769fd2..2369d03 100644
--- a/res/drawable-mdpi/ic_menu_search_holo_light.png
+++ b/res/drawable-mdpi/ic_menu_search_holo_light.png
Binary files differ
diff --git a/res/drawable-mdpi/stat_notify_email.png b/res/drawable-mdpi/stat_notify_email.png
deleted file mode 100644
index f808685..0000000
--- a/res/drawable-mdpi/stat_notify_email.png
+++ /dev/null
Binary files differ
diff --git a/res/drawable-sw600dp-hdpi/stat_notify_email.png b/res/drawable-sw600dp-hdpi/stat_notify_email.png
deleted file mode 100644
index b40df8e..0000000
--- a/res/drawable-sw600dp-hdpi/stat_notify_email.png
+++ /dev/null
Binary files differ
diff --git a/res/drawable-sw600dp-mdpi/stat_notify_email.png b/res/drawable-sw600dp-mdpi/stat_notify_email.png
deleted file mode 100644
index 1c0ecfa..0000000
--- a/res/drawable-sw600dp-mdpi/stat_notify_email.png
+++ /dev/null
Binary files differ
diff --git a/res/drawable-sw600dp-xhdpi/stat_notify_email.png b/res/drawable-sw600dp-xhdpi/stat_notify_email.png
deleted file mode 100644
index 09fa242..0000000
--- a/res/drawable-sw600dp-xhdpi/stat_notify_email.png
+++ /dev/null
Binary files differ
diff --git a/res/drawable-xhdpi/ic_menu_search_holo_light.png b/res/drawable-xhdpi/ic_menu_search_holo_light.png
index d285976..578cb24 100644
--- a/res/drawable-xhdpi/ic_menu_search_holo_light.png
+++ b/res/drawable-xhdpi/ic_menu_search_holo_light.png
Binary files differ
diff --git a/res/drawable-xhdpi/stat_notify_email.png b/res/drawable-xhdpi/stat_notify_email.png
index 8b6d94a..0317760 100644
--- a/res/drawable-xhdpi/stat_notify_email.png
+++ b/res/drawable-xhdpi/stat_notify_email.png
Binary files differ
diff --git a/res/layout-sw600dp/account_switch_spinner_item.xml b/res/layout-sw600dp/account_switch_spinner_item.xml
deleted file mode 100644
index 6247095..0000000
--- a/res/layout-sw600dp/account_switch_spinner_item.xml
+++ /dev/null
@@ -1,59 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
- Copyright (C) 2011 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.
--->
-
-<!-- View shown in the navigation spinner in the actionbar. -->
-<RelativeLayout
- xmlns:android="http://schemas.android.com/apk/res/android"
- android:layout_height="match_parent"
- android:paddingLeft="0dip"
- android:layout_marginLeft="0dip"
- android:layout_width="match_parent">
-
- <LinearLayout
- android:layout_height="match_parent"
- android:id="@+id/account_spinner_container"
- android:orientation="vertical"
- android:gravity="center_vertical"
- android:background="@drawable/spinner_ab_holo_light"
- android:duplicateParentState="false"
- android:layout_alignParentLeft="true"
- style="@style/AccountSpinnerStyle">
- <TextView
- android:id="@+id/account_first"
- style="@style/AccountSpinnerAnchorTextPrimary"
- android:singleLine="true"
- android:ellipsize="end"
- android:includeFontPadding="false"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content" />
- <TextView
- android:id="@+id/account_second"
- style="@style/AccountSpinnerAnchorTextSecondary"
- android:singleLine="true"
- android:ellipsize="end"
- android:includeFontPadding="false"
- android:layout_marginRight="4dp"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content" />
- </LinearLayout>
-
- <TextView
- android:id="@+id/account_unread"
- android:layout_toRightOf="@id/account_spinner_container"
- style="@style/unreadCountActionBarTablet"/>
-</RelativeLayout>
diff --git a/res/layout/account_switch_spinner_item.xml b/res/layout/account_switch_spinner_item.xml
deleted file mode 100644
index 0ab8b62..0000000
--- a/res/layout/account_switch_spinner_item.xml
+++ /dev/null
@@ -1,79 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
- Copyright (C) 2011 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.
--->
-
-<!-- View shown in the navigation spinner in the actionbar. -->
-<RelativeLayout
- xmlns:android="http://schemas.android.com/apk/res/android"
- android:layout_height="match_parent"
- android:paddingLeft="0dip"
- android:layout_marginLeft="0dip"
- android:layout_width="match_parent">
-
- <!-- Place the unread count first, taking all the space it needs
- to fit its content -->
- <TextView
- android:id="@+id/account_unread"
- android:layout_alignParentRight="true"
- android:layout_width="wrap_content"
- android:layout_marginLeft="4dp"
- style="@style/UnreadCountActionBar"/>
-
- <!-- Container to soak up space and ensure that the caret attaches
- to a short label name. This container should be anonymous because nothing
- should reference it.
- Phone only: On tablets, the width of the spinner is a constant for each
- orientation. -->
- <LinearLayout
- android:layout_height="match_parent"
- android:orientation="horizontal"
- android:layout_width="fill_parent"
- android:gravity="center_vertical"
- android:clickable="false"
- android:focusable="false"
- android:layout_toLeftOf="@id/account_unread">
-
- <!-- In the container, the label name takes up as much space
- as it needs to show its content: this is to ensure that the
- dropdown caret is flush with the label name. -->
- <LinearLayout
- android:layout_height="match_parent"
- android:id="@+id/account_spinner_container"
- android:orientation="vertical"
- android:gravity="center_vertical"
- android:background="@drawable/spinner_ab_holo_light"
- style="@style/AccountSpinnerStyle">
- <TextView
- android:id="@+id/account_first"
- style="@style/AccountSpinnerAnchorTextPrimary"
- android:singleLine="true"
- android:ellipsize="end"
- android:includeFontPadding="false"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content" />
- <TextView
- android:id="@+id/account_second"
- style="@style/AccountSpinnerAnchorTextSecondary"
- android:singleLine="true"
- android:ellipsize="end"
- android:includeFontPadding="false"
- android:layout_marginRight="4dp"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content" />
- </LinearLayout>
- </LinearLayout>
-</RelativeLayout>
diff --git a/res/layout/actionbar_view.xml b/res/layout/actionbar_view.xml
index bcad65b..7d4b792 100644
--- a/res/layout/actionbar_view.xml
+++ b/res/layout/actionbar_view.xml
@@ -21,18 +21,10 @@
label, and subject).
-->
<com.android.mail.ui.MailActionBarView xmlns:android="http://schemas.android.com/apk/res/android"
- android:layout_width="match_parent"
+ android:layout_width="wrap_content"
android:layout_height="match_parent"
android:orientation="horizontal" >
-
- <com.android.mail.ui.MailSpinner
- android:id="@+id/account_spinner"
- android:layout_height="match_parent"
- style="@style/AccountSwitchSpinnerItem"
- android:focusableInTouchMode="false"
- android:focusable="false"
- android:clickable="true"/>
-
+ <!-- Only used for displaying a subject view -->
<include layout="@layout/actionbar_subject" />
</com.android.mail.ui.MailActionBarView>
diff --git a/res/layout/child_folder_item.xml b/res/layout/child_folder_item.xml
index 70793e4..b8dc6c6 100644
--- a/res/layout/child_folder_item.xml
+++ b/res/layout/child_folder_item.xml
@@ -34,7 +34,7 @@
android:src="@drawable/folder_parent_icon" />
<ImageView
- android:id="@+id/folder_box"
+ android:id="@+id/color_block"
style="@style/FolderItemIcon"
android:layout_alignParentLeft="true"
android:layout_alignParentTop="true" />
@@ -48,19 +48,37 @@
android:layout_toLeftOf="@id/folder_parent_icon"
android:textColor="@color/folder_name_color_primary_invertible" />
+ <TextView
+ android:id="@+id/unseen"
+ style="@style/UnseenCount"
+ android:layout_marginRight="@dimen/folder_list_item_right_margin"
+ android:layout_alignWithParentIfMissing="true"
+ android:layout_alignParentRight="true"
+ android:layout_toLeftOf="@id/folder_parent_icon" />
+
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_centerVertical="true"
android:layout_toLeftOf="@id/unread"
+ android:layout_alignWithParentIfMissing="true"
android:layout_marginLeft="@dimen/folder_list_item_left_margin"
android:layout_marginTop="@dimen/folder_swatch_height"
android:layout_marginBottom="@dimen/folder_swatch_height">
+ <ImageView
+ android:id="@+id/folder_icon"
+ android:layout_width="20dp"
+ android:layout_height="20dp"
+ android:layout_centerVertical="true"
+ android:layout_marginRight="10dp"
+ android:visibility="gone" />
+
<TextView
android:id="@+id/name"
android:layout_height="wrap_content"
android:layout_width="match_parent"
+ android:layout_toRightOf="@id/folder_icon"
android:maxLines="2"
android:ellipsize="end"
android:textColor="@color/folder_name_color_primary_invertible"
@@ -71,6 +89,7 @@
android:layout_height="wrap_content"
android:layout_width="match_parent"
android:layout_below="@id/name"
+ android:layout_toRightOf="@id/folder_icon"
android:textColor="@color/folder_name_color_primary_invertible"
android:textAppearance="?android:attr/textAppearanceSmall"
android:visibility="gone" />
diff --git a/res/layout/folder_expand_item.xml b/res/layout/folder_expand_item.xml
new file mode 100644
index 0000000..5041582
--- /dev/null
+++ b/res/layout/folder_expand_item.xml
@@ -0,0 +1,55 @@
+<?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.
+-->
+
+<!-- This is a button for expanding/collapsing emails in FLF -->
+<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/folder_expand_item"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:gravity="center_vertical">
+
+ <View
+ android:layout_width="match_parent"
+ android:layout_height="@dimen/message_details_header_bottom_border_height"
+ android:visibility="visible"
+ android:background="@color/conv_subject_border" />
+
+ <TextView
+ android:id="@+id/folder_expand_text"
+ android:layout_width="match_parent"
+ android:layout_height="48dip"
+ android:layout_marginLeft="@dimen/folder_list_item_left_margin"
+ android:gravity="center_vertical"
+ android:ellipsize="end"
+ android:singleLine="true"
+ android:textAllCaps="true"
+ android:textColor="@color/folder_list_heading_text_color"/>
+
+ <View
+ android:layout_width="match_parent"
+ android:layout_height="@dimen/message_details_header_bottom_border_height"
+ android:visibility="visible"
+ android:background="@color/conv_subject_border" />
+
+ <ImageView
+ android:layout_gravity="top|right"
+ android:layout_marginTop="14dip"
+ android:layout_marginRight="14dip"
+ android:id="@+id/details_expander"
+ style="@style/MessageHeaderExpanderMinimizedStyle" />
+</FrameLayout>
diff --git a/res/layout/folder_item.xml b/res/layout/folder_item.xml
index 2426a77..c68e442 100644
--- a/res/layout/folder_item.xml
+++ b/res/layout/folder_item.xml
@@ -47,6 +47,14 @@
android:layout_toLeftOf="@id/folder_parent_icon"
android:textColor="@color/folder_name_color_primary_invertible" />
+ <TextView
+ android:id="@+id/unseen"
+ style="@style/UnseenCount"
+ android:layout_marginRight="@dimen/folder_list_item_right_margin"
+ android:layout_alignWithParentIfMissing="true"
+ android:layout_alignParentRight="true"
+ android:layout_toLeftOf="@id/folder_parent_icon" />
+
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
@@ -57,10 +65,19 @@
android:layout_marginTop="@dimen/folder_swatch_height"
android:layout_marginBottom="@dimen/folder_swatch_height">
+ <ImageView
+ android:id="@+id/folder_icon"
+ android:layout_width="20dp"
+ android:layout_height="20dp"
+ android:layout_centerVertical="true"
+ android:layout_marginRight="10dp"
+ android:visibility="gone" />
+
<TextView
android:id="@+id/name"
android:layout_height="wrap_content"
android:layout_width="match_parent"
+ android:layout_toRightOf="@id/folder_icon"
android:maxLines="2"
android:ellipsize="end"
android:textColor="@color/folder_name_color_primary_invertible"
@@ -71,6 +88,7 @@
android:layout_height="wrap_content"
android:layout_width="match_parent"
android:layout_below="@id/name"
+ android:layout_toRightOf="@id/folder_icon"
android:textColor="@color/folder_name_color_primary_invertible"
android:textAppearance="?android:attr/textAppearanceSmall"
android:visibility="gone" />
diff --git a/res/layout/one_pane_activity.xml b/res/layout/one_pane_activity.xml
index 07974b7..eb5b3c6 100644
--- a/res/layout/one_pane_activity.xml
+++ b/res/layout/one_pane_activity.xml
@@ -15,20 +15,34 @@
limitations under the License.
-->
-<com.android.mail.ui.OnePaneRoot xmlns:android="http://schemas.android.com/apk/res/android"
- android:id="@+id/one_pane_root"
+<android.support.v4.widget.DrawerLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/drawer_container"
android:layout_width="match_parent"
- android:layout_height="match_parent" >
+ android:layout_height="match_parent">
+
+ <com.android.mail.ui.OnePaneRoot
+ android:id="@+id/one_pane_root"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent" >
+
+ <FrameLayout
+ android:id="@+id/content_pane"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent" />
+
+ <include layout="@layout/conversation_pager" />
+
+ <com.android.mail.ui.ActionableToastBar
+ android:id="@+id/toast_bar"
+ style="@style/ToastBarStyle" />
+
+ </com.android.mail.ui.OnePaneRoot>
<FrameLayout
- android:id="@+id/content_pane"
- android:layout_width="match_parent"
- android:layout_height="match_parent" />
+ android:id="@+id/drawer_pullout"
+ android:layout_width="300dp"
+ android:layout_height="match_parent"
+ android:layout_gravity="start"
+ style="@style/FolderListStyle"/>
- <include layout="@layout/conversation_pager" />
-
- <com.android.mail.ui.ActionableToastBar
- android:id="@+id/toast_bar"
- style="@style/ToastBarStyle" />
-
-</com.android.mail.ui.OnePaneRoot>
\ No newline at end of file
+</android.support.v4.widget.DrawerLayout>
diff --git a/res/layout/search_actionbar_view.xml b/res/layout/search_actionbar_view.xml
index 93d8de8..613d8c4 100644
--- a/res/layout/search_actionbar_view.xml
+++ b/res/layout/search_actionbar_view.xml
@@ -26,15 +26,6 @@
android:layout_height="match_parent"
android:orientation="horizontal" >
- <com.android.mail.ui.MailSpinner
- android:id="@+id/account_spinner"
- android:layout_height="match_parent"
- android:layout_width="wrap_content"
- style="@style/PlainSpinnerDropdown"
- android:focusableInTouchMode="false"
- android:focusable="false"
- android:clickable="true"/>
-
<include layout="@layout/actionbar_subject" />
</com.android.mail.ui.SearchMailActionBarView>
\ No newline at end of file
diff --git a/res/layout/secure_conversation_view.xml b/res/layout/secure_conversation_view.xml
index 56679f2..da1dae3 100644
--- a/res/layout/secure_conversation_view.xml
+++ b/res/layout/secure_conversation_view.xml
@@ -26,21 +26,16 @@
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
- <RelativeLayout android:id="@+id/header"
+ <include layout="@layout/conversation_view_header"
+ android:id="@+id/conv_header"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content" />
+
+ <include layout="@layout/conversation_message_header"
+ android:id="@+id/message_header"
android:layout_width="match_parent"
android:layout_height="wrap_content"
- android:background="@android:color/white">
- <include layout="@layout/conversation_view_header"
- android:id="@+id/conv_header"
- android:layout_width="match_parent"
- android:layout_height="wrap_content" />
-
- <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" />
- </RelativeLayout>
+ android:layout_below="@id/conv_header" />
<!-- base WebView layer -->
<WebView
android:id="@+id/webview"
@@ -54,12 +49,6 @@
android:visibility="gone" />
</LinearLayout>
</ScrollView>
- <FrameLayout
- android:id="@+id/conversation_topmost_overlay"
- android:layout_width="match_parent"
- android:layout_height="match_parent">
- <!-- TODO: scroll indicators go HERE on top of all other layers -->
- </FrameLayout>
<include layout="@layout/conversation_load_spinner"/>
diff --git a/res/menu-sw600dp-port/conversation_actions.xml b/res/menu-sw600dp-port/conversation_actions.xml
index 0762dae..170551a 100644
--- a/res/menu-sw600dp-port/conversation_actions.xml
+++ b/res/menu-sw600dp-port/conversation_actions.xml
@@ -62,6 +62,10 @@
android:icon="@drawable/ic_menu_mark_unread_holo_light" />
<item
+ android:id="@+id/move_to"
+ android:title="@string/menu_move_to" />
+
+ <item
android:id="@+id/mark_important"
android:showAsAction="never"
android:title="@string/mark_important" />
diff --git a/res/menu-sw600dp-port/conversation_list_search_results_actions.xml b/res/menu-sw600dp-port/conversation_list_search_results_actions.xml
index 357ba1b..ba722e6 100644
--- a/res/menu-sw600dp-port/conversation_list_search_results_actions.xml
+++ b/res/menu-sw600dp-port/conversation_list_search_results_actions.xml
@@ -35,11 +35,6 @@
android:icon="@drawable/ic_menu_refresh_holo_light"
android:alphabeticShortcut="@string/trigger_refresh_char" />
- <!-- Available for Folders with SUPPORTS_SETTINGS capability -->
- <item android:id="@+id/folder_options"
- android:title="@string/menu_folder_options"
- android:showAsAction="never" />
-
<item android:id="@+id/settings"
android:title="@string/menu_settings"
android:showAsAction="never" />
diff --git a/res/menu-sw600dp-port/conversation_search_results_actions.xml b/res/menu-sw600dp-port/conversation_search_results_actions.xml
index ca13500..e228059 100644
--- a/res/menu-sw600dp-port/conversation_search_results_actions.xml
+++ b/res/menu-sw600dp-port/conversation_search_results_actions.xml
@@ -51,11 +51,6 @@
android:title="@string/report_spam"
android:showAsAction="never"/>
- <!-- Available for Folders with SUPPORTS_SETTINGS capability -->
- <item android:id="@+id/folder_options"
- android:title="@string/menu_folder_options"
- android:showAsAction="never" />
-
<item android:id="@+id/settings"
android:title="@string/menu_settings"
android:showAsAction="never" />
@@ -72,4 +67,4 @@
android:icon="@android:drawable/ic_menu_help"
android:title="@string/help_and_info" />
-</menu>
\ No newline at end of file
+</menu>
diff --git a/res/menu-sw600dp/conversation_actions.xml b/res/menu-sw600dp/conversation_actions.xml
index 48359cc..fce09e0 100644
--- a/res/menu-sw600dp/conversation_actions.xml
+++ b/res/menu-sw600dp/conversation_actions.xml
@@ -75,6 +75,10 @@
android:icon="@drawable/ic_menu_mark_unread_holo_light" />
<item
+ android:id="@+id/move_to"
+ android:title="@string/menu_move_to" />
+
+ <item
android:id="@+id/mark_important"
android:showAsAction="never"
android:title="@string/mark_important" />
diff --git a/res/menu-sw600dp/conversation_list_selection_actions_menu.xml b/res/menu-sw600dp/conversation_list_selection_actions_menu.xml
index 64b1b8c..2563f54 100644
--- a/res/menu-sw600dp/conversation_list_selection_actions_menu.xml
+++ b/res/menu-sw600dp/conversation_list_selection_actions_menu.xml
@@ -80,6 +80,11 @@
android:icon="@drawable/ic_menu_star_off_holo_light" />
<item
+ android:id="@+id/move_to"
+ android:title="@string/menu_move_to"
+ android:showAsAction="never" />
+
+ <item
android:id="@+id/mark_important"
android:title="@string/mark_important"
android:showAsAction="never"
diff --git a/res/menu/conversation_actions.xml b/res/menu/conversation_actions.xml
index 7a24d8d..91dda4e 100644
--- a/res/menu/conversation_actions.xml
+++ b/res/menu/conversation_actions.xml
@@ -64,6 +64,11 @@
<!-- Always available -->
<item
+ android:id="@+id/move_to"
+ android:title="@string/menu_move_to" />
+
+ <!-- Always available -->
+ <item
android:id="@+id/mark_important"
android:title="@string/mark_important"
android:icon="@drawable/ic_email_caret_double" />
diff --git a/res/menu/conversation_list_menu.xml b/res/menu/conversation_list_menu.xml
index 589097a..76ae716 100644
--- a/res/menu/conversation_list_menu.xml
+++ b/res/menu/conversation_list_menu.xml
@@ -34,7 +34,7 @@
<!-- Always available -->
<item android:id="@+id/show_all_folders"
android:title="@string/show_all_folders"
- android:showAsAction="ifRoom"
+ android:showAsAction="never"
android:icon="@drawable/ic_menu_folders_holo_light" />
<!-- Always available -->
diff --git a/res/menu/conversation_list_selection_actions_menu.xml b/res/menu/conversation_list_selection_actions_menu.xml
index 2b1c149..c08cf92 100644
--- a/res/menu/conversation_list_selection_actions_menu.xml
+++ b/res/menu/conversation_list_selection_actions_menu.xml
@@ -80,6 +80,11 @@
android:icon="@drawable/ic_menu_star_off_holo_light" />
<item
+ android:id="@+id/move_to"
+ android:title="@string/menu_move_to"
+ android:showAsAction="never" />
+
+ <item
android:id="@+id/mark_important"
android:title="@string/mark_important"
android:showAsAction="never"
diff --git a/res/menu/email_copy_context_menu.xml b/res/menu/email_copy_context_menu.xml
new file mode 100644
index 0000000..d40be51
--- /dev/null
+++ b/res/menu/email_copy_context_menu.xml
@@ -0,0 +1,25 @@
+<?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/mail_context_menu_id"
+ android:title="@string/contextmenu_send_mail"/>
+ <item android:id="@+id/copy_mail_context_menu_id"
+ android:title="@string/contextmenu_copy"/>
+</menu>
+
diff --git a/res/menu/message_header_overflow_menu.xml b/res/menu/message_header_overflow_menu.xml
index e3e9f0b..035eca2 100644
--- a/res/menu/message_header_overflow_menu.xml
+++ b/res/menu/message_header_overflow_menu.xml
@@ -26,4 +26,10 @@
<item android:id="@+id/forward"
android:icon="@drawable/ic_forward_holo_dark"
android:title="@string/forward" />
+ <item android:id="@+id/report_rendering_problem"
+ android:icon="@drawable/ic_forward_holo_dark"
+ android:title="@string/report_rendering_problem" />
+ <item android:id="@+id/report_rendering_improvement"
+ android:icon="@drawable/ic_forward_holo_dark"
+ android:title="@string/report_rendering_improvement" />
</menu>
diff --git a/res/raw/template_conversation_lower.html b/res/raw/template_conversation_lower.html
index 6cc1934..b128077 100644
--- a/res/raw/template_conversation_lower.html
+++ b/res/raw/template_conversation_lower.html
@@ -9,6 +9,8 @@
var WIDE_VIEWPORT_WIDTH = %s;
var ENABLE_CONTENT_READY = %s;
var NORMALIZE_MESSAGE_WIDTHS = %s;
+ var ENABLE_MUNGE_TABLES = %s;
+ var ENABLE_MUNGE_IMAGES = %s;
</script>
<script type="text/javascript" src="file:///android_asset/script.js"></script>
</html>
diff --git a/res/raw/template_conversation_upper.html b/res/raw/template_conversation_upper.html
index df844f8..6222762 100644
--- a/res/raw/template_conversation_upper.html
+++ b/res/raw/template_conversation_upper.html
@@ -21,6 +21,19 @@
body {
font-size: 80%%;
}
+ blockquote {
+ margin-left: 0.8ex !important;
+ margin-right: 0 !important;
+ border-left:1px #ccc solid !important;
+ padding-left: 1ex !important;
+ }
+ table.munged {
+ width: auto !important;
+ }
+ td.munged {
+ width: auto !important;
+ white-space: normal !important;
+ }
.initial-load {
/* 0x0 and 1x1 may be short-circuited by WebView */
width: 2px;
diff --git a/res/values-af/strings.xml b/res/values-af/strings.xml
index 44d74bb..e2e55c0 100644
--- a/res/values-af/strings.xml
+++ b/res/values-af/strings.xml
@@ -74,8 +74,13 @@
<string name="forward" msgid="6822914459902983767">"Stuur aan"</string>
<string name="menu_compose" msgid="6274193058224230645">"Skryf"</string>
<string name="menu_change_folders" msgid="1542713666608888717">"Verander vouers"</string>
+ <string name="menu_move_to" msgid="9138296669516358542">"Skuif na"</string>
<string name="menu_manage_folders" msgid="6755623004628177492">"Vouerinstellings"</string>
<string name="folder_list_title" msgid="4276644062440415214">"Vouers"</string>
+ <!-- no translation found for folder_list_more (537172187223133825) -->
+ <skip />
+ <!-- no translation found for folder_list_show_all_accounts (8054807182336991835) -->
+ <skip />
<string name="manage_folders_subtitle" msgid="7702199674083260433">"Sinkroniseer en stel in kennis"</string>
<string name="menu_folder_options" msgid="8897520487430647932">"Vouerinstellings"</string>
<string name="menu_account_settings" msgid="8230989362863431918">"Rekeninginstellings"</string>
@@ -220,6 +225,8 @@
<item quantity="one" msgid="4930161390461457462">"Het vouer verander."</item>
<item quantity="other" msgid="8918589141287976985">"Het vouers verander."</item>
</plurals>
+ <!-- no translation found for conversation_folder_moved (297469098857964678) -->
+ <skip />
<string name="search_results_header" msgid="4669917471897026269">"Resultate"</string>
<string name="search_unsupported" msgid="4654227193354052607">"Search word nie ondersteun op hierdie rekening nie."</string>
<string name="searchMode" msgid="3329807422114758583">"Soekmodus"</string>
@@ -367,4 +374,9 @@
<string name="veiled_alternate_text" msgid="7370933826442034211"></string>
<string name="veiled_alternate_text_unknown_person" msgid="5132515097905273819"></string>
<string name="veiled_summary_unknown_person" msgid="4030928895738205054"></string>
+ <string name="label_notification_ticker" msgid="1684732605316462621">"<xliff:g id="LABEL">%s</xliff:g>: <xliff:g id="NOTIFICATION">%s</xliff:g>"</string>
+ <string name="new_messages" msgid="4419173946074516420">"<xliff:g id="COUNT">%1$d</xliff:g> nuwe boodskappe"</string>
+ <string name="single_new_message_notification_title" msgid="4138237430881084155">"<xliff:g id="SENDER">%1$s</xliff:g>: <xliff:g id="SUBJECT">%2$s</xliff:g>"</string>
+ <string name="silent_ringtone" msgid="5856834572357761687">"Stil"</string>
+ <string name="inbox_unseen_banner" msgid="4561582938706814621">"<xliff:g id="NUMBER">%d</xliff:g> NUUT"</string>
</resources>
diff --git a/res/values-am/strings.xml b/res/values-am/strings.xml
index 6fc5026..5b832d4 100644
--- a/res/values-am/strings.xml
+++ b/res/values-am/strings.xml
@@ -74,8 +74,13 @@
<string name="forward" msgid="6822914459902983767">"አስተላልፍ"</string>
<string name="menu_compose" msgid="6274193058224230645">"አዲስ ጻፍ"</string>
<string name="menu_change_folders" msgid="1542713666608888717">"አቃፊዎችን ቀይር"</string>
+ <string name="menu_move_to" msgid="9138296669516358542">"አንቀሳቅስ ወደ"</string>
<string name="menu_manage_folders" msgid="6755623004628177492">"የአቃፊ ቅንብሮች"</string>
<string name="folder_list_title" msgid="4276644062440415214">"አቃፊዎች"</string>
+ <!-- no translation found for folder_list_more (537172187223133825) -->
+ <skip />
+ <!-- no translation found for folder_list_show_all_accounts (8054807182336991835) -->
+ <skip />
<string name="manage_folders_subtitle" msgid="7702199674083260433">"አመሳስልና አሳውቅ"</string>
<string name="menu_folder_options" msgid="8897520487430647932">"የአቃፊ ቅንብሮች"</string>
<string name="menu_account_settings" msgid="8230989362863431918">"የመለያ ቅንብሮች"</string>
@@ -220,6 +225,8 @@
<item quantity="one" msgid="4930161390461457462">"የተቀየረ አቃፊ። የተለወጠ አቃፊ።"</item>
<item quantity="other" msgid="8918589141287976985">"የተቀየሩ አቃፊዎች።"</item>
</plurals>
+ <!-- no translation found for conversation_folder_moved (297469098857964678) -->
+ <skip />
<string name="search_results_header" msgid="4669917471897026269">"ውጤቶች"</string>
<string name="search_unsupported" msgid="4654227193354052607">"ፍለጋ በዚህ መለያ አይደገፍም።"</string>
<string name="searchMode" msgid="3329807422114758583">"የፍለጋ ሁናቴ"</string>
@@ -367,4 +374,9 @@
<string name="veiled_alternate_text" msgid="7370933826442034211"></string>
<string name="veiled_alternate_text_unknown_person" msgid="5132515097905273819"></string>
<string name="veiled_summary_unknown_person" msgid="4030928895738205054"></string>
+ <string name="label_notification_ticker" msgid="1684732605316462621">"<xliff:g id="LABEL">%s</xliff:g>: <xliff:g id="NOTIFICATION">%s</xliff:g>"</string>
+ <string name="new_messages" msgid="4419173946074516420">"<xliff:g id="COUNT">%1$d</xliff:g> አዲስ መልዕክቶች"</string>
+ <string name="single_new_message_notification_title" msgid="4138237430881084155">"<xliff:g id="SENDER">%1$s</xliff:g>: <xliff:g id="SUBJECT">%2$s</xliff:g>"</string>
+ <string name="silent_ringtone" msgid="5856834572357761687">"ፀጥታ"</string>
+ <string name="inbox_unseen_banner" msgid="4561582938706814621">"<xliff:g id="NUMBER">%d</xliff:g> አዲስ"</string>
</resources>
diff --git a/res/values-ar/strings.xml b/res/values-ar/strings.xml
index da73a7a..cd7f37c 100644
--- a/res/values-ar/strings.xml
+++ b/res/values-ar/strings.xml
@@ -74,8 +74,13 @@
<string name="forward" msgid="6822914459902983767">"إعادة توجيه"</string>
<string name="menu_compose" msgid="6274193058224230645">"إنشاء"</string>
<string name="menu_change_folders" msgid="1542713666608888717">"تغيير المجلدات"</string>
+ <string name="menu_move_to" msgid="9138296669516358542">"نقل إلى"</string>
<string name="menu_manage_folders" msgid="6755623004628177492">"إعدادات المجلدات"</string>
<string name="folder_list_title" msgid="4276644062440415214">"المجلدات"</string>
+ <!-- no translation found for folder_list_more (537172187223133825) -->
+ <skip />
+ <!-- no translation found for folder_list_show_all_accounts (8054807182336991835) -->
+ <skip />
<string name="manage_folders_subtitle" msgid="7702199674083260433">"المزامنة والإشعارات"</string>
<string name="menu_folder_options" msgid="8897520487430647932">"إعدادات المجلد"</string>
<string name="menu_account_settings" msgid="8230989362863431918">"إعدادات الحساب"</string>
@@ -220,6 +225,8 @@
<item quantity="one" msgid="4930161390461457462">"تم تغيير المجلد."</item>
<item quantity="other" msgid="8918589141287976985">"تم تغيير المجلدات."</item>
</plurals>
+ <!-- no translation found for conversation_folder_moved (297469098857964678) -->
+ <skip />
<string name="search_results_header" msgid="4669917471897026269">"النتائج"</string>
<string name="search_unsupported" msgid="4654227193354052607">"لا يمكن استخدام البحث على هذا الحساب."</string>
<string name="searchMode" msgid="3329807422114758583">"وضع البحث"</string>
@@ -367,4 +374,9 @@
<string name="veiled_alternate_text" msgid="7370933826442034211"></string>
<string name="veiled_alternate_text_unknown_person" msgid="5132515097905273819"></string>
<string name="veiled_summary_unknown_person" msgid="4030928895738205054"></string>
+ <string name="label_notification_ticker" msgid="1684732605316462621">"<xliff:g id="LABEL">%s</xliff:g>: <xliff:g id="NOTIFICATION">%s</xliff:g>"</string>
+ <string name="new_messages" msgid="4419173946074516420">"<xliff:g id="COUNT">%1$d</xliff:g> من الرسائل الجديدة"</string>
+ <string name="single_new_message_notification_title" msgid="4138237430881084155">"<xliff:g id="SENDER">%1$s</xliff:g>: <xliff:g id="SUBJECT">%2$s</xliff:g>"</string>
+ <string name="silent_ringtone" msgid="5856834572357761687">"صامت"</string>
+ <string name="inbox_unseen_banner" msgid="4561582938706814621">"<xliff:g id="NUMBER">%d</xliff:g> جديدة"</string>
</resources>
diff --git a/res/values-be/strings.xml b/res/values-be/strings.xml
index 79128ca..d119d6c 100644
--- a/res/values-be/strings.xml
+++ b/res/values-be/strings.xml
@@ -74,8 +74,13 @@
<string name="forward" msgid="6822914459902983767">"Пераслаць"</string>
<string name="menu_compose" msgid="6274193058224230645">"Напісаць"</string>
<string name="menu_change_folders" msgid="1542713666608888717">"Змена тэчкі"</string>
+ <string name="menu_move_to" msgid="9138296669516358542">"Перанесці ў"</string>
<string name="menu_manage_folders" msgid="6755623004628177492">"Налады тэчкi"</string>
<string name="folder_list_title" msgid="4276644062440415214">"Тэчкі"</string>
+ <!-- no translation found for folder_list_more (537172187223133825) -->
+ <skip />
+ <!-- no translation found for folder_list_show_all_accounts (8054807182336991835) -->
+ <skip />
<string name="manage_folders_subtitle" msgid="7702199674083260433">"Сінхранізацыя і апавяшчэнне"</string>
<string name="menu_folder_options" msgid="8897520487430647932">"Налады тэчак"</string>
<string name="menu_account_settings" msgid="8230989362863431918">"Налады ўліковага запісу"</string>
@@ -220,6 +225,8 @@
<item quantity="one" msgid="4930161390461457462">"Змененая тэчка."</item>
<item quantity="other" msgid="8918589141287976985">"Змененыя тэчкі."</item>
</plurals>
+ <!-- no translation found for conversation_folder_moved (297469098857964678) -->
+ <skip />
<string name="search_results_header" msgid="4669917471897026269">"Вынікі"</string>
<string name="search_unsupported" msgid="4654227193354052607">"У гэтым уліковым запісе пошук не падтрымліваецца."</string>
<string name="searchMode" msgid="3329807422114758583">"Рэжым пошуку"</string>
@@ -367,4 +374,9 @@
<string name="veiled_alternate_text" msgid="7370933826442034211"></string>
<string name="veiled_alternate_text_unknown_person" msgid="5132515097905273819"></string>
<string name="veiled_summary_unknown_person" msgid="4030928895738205054"></string>
+ <string name="label_notification_ticker" msgid="1684732605316462621">"<xliff:g id="LABEL">%s</xliff:g>: <xliff:g id="NOTIFICATION">%s</xliff:g>"</string>
+ <string name="new_messages" msgid="4419173946074516420">"Новых паведамленняў: <xliff:g id="COUNT">%1$d</xliff:g>"</string>
+ <string name="single_new_message_notification_title" msgid="4138237430881084155">"<xliff:g id="SENDER">%1$s</xliff:g>: <xliff:g id="SUBJECT">%2$s</xliff:g>"</string>
+ <string name="silent_ringtone" msgid="5856834572357761687">"Ціхі рэжым"</string>
+ <string name="inbox_unseen_banner" msgid="4561582938706814621">"НОВЫХ: <xliff:g id="NUMBER">%d</xliff:g>"</string>
</resources>
diff --git a/res/values-bg/strings.xml b/res/values-bg/strings.xml
index 4f88f4f..f35f63d 100644
--- a/res/values-bg/strings.xml
+++ b/res/values-bg/strings.xml
@@ -74,8 +74,13 @@
<string name="forward" msgid="6822914459902983767">"Препращане"</string>
<string name="menu_compose" msgid="6274193058224230645">"Ново съобщение"</string>
<string name="menu_change_folders" msgid="1542713666608888717">"Промяна на папките"</string>
+ <string name="menu_move_to" msgid="9138296669516358542">"Преместване във:"</string>
<string name="menu_manage_folders" msgid="6755623004628177492">"Настройки за папките"</string>
<string name="folder_list_title" msgid="4276644062440415214">"Папки"</string>
+ <!-- no translation found for folder_list_more (537172187223133825) -->
+ <skip />
+ <!-- no translation found for folder_list_show_all_accounts (8054807182336991835) -->
+ <skip />
<string name="manage_folders_subtitle" msgid="7702199674083260433">"Синхронизиране и известяване"</string>
<string name="menu_folder_options" msgid="8897520487430647932">"Настройки за папката"</string>
<string name="menu_account_settings" msgid="8230989362863431918">"Настройки на профила"</string>
@@ -220,6 +225,8 @@
<item quantity="one" msgid="4930161390461457462">"Променихте папката."</item>
<item quantity="other" msgid="8918589141287976985">"Променихте папките."</item>
</plurals>
+ <!-- no translation found for conversation_folder_moved (297469098857964678) -->
+ <skip />
<string name="search_results_header" msgid="4669917471897026269">"Резултати"</string>
<string name="search_unsupported" msgid="4654227193354052607">"Търсенето не се поддържа за този профил."</string>
<string name="searchMode" msgid="3329807422114758583">"Режим на търсене"</string>
@@ -367,4 +374,9 @@
<string name="veiled_alternate_text" msgid="7370933826442034211"></string>
<string name="veiled_alternate_text_unknown_person" msgid="5132515097905273819"></string>
<string name="veiled_summary_unknown_person" msgid="4030928895738205054"></string>
+ <string name="label_notification_ticker" msgid="1684732605316462621">"<xliff:g id="LABEL">%s</xliff:g>: <xliff:g id="NOTIFICATION">%s</xliff:g>"</string>
+ <string name="new_messages" msgid="4419173946074516420">"<xliff:g id="COUNT">%1$d</xliff:g> нови съобщения"</string>
+ <string name="single_new_message_notification_title" msgid="4138237430881084155">"<xliff:g id="SENDER">%1$s</xliff:g>: <xliff:g id="SUBJECT">%2$s</xliff:g>"</string>
+ <string name="silent_ringtone" msgid="5856834572357761687">"Тих режим"</string>
+ <string name="inbox_unseen_banner" msgid="4561582938706814621">"<xliff:g id="NUMBER">%d</xliff:g> НОВИ"</string>
</resources>
diff --git a/res/values-ca/strings.xml b/res/values-ca/strings.xml
index c6c6211..8b30f07 100644
--- a/res/values-ca/strings.xml
+++ b/res/values-ca/strings.xml
@@ -74,8 +74,13 @@
<string name="forward" msgid="6822914459902983767">"Reenvia"</string>
<string name="menu_compose" msgid="6274193058224230645">"Redacta"</string>
<string name="menu_change_folders" msgid="1542713666608888717">"Canvia les carpetes"</string>
+ <string name="menu_move_to" msgid="9138296669516358542">"Mou a"</string>
<string name="menu_manage_folders" msgid="6755623004628177492">"Configuració de la carpeta"</string>
<string name="folder_list_title" msgid="4276644062440415214">"Carpetes"</string>
+ <!-- no translation found for folder_list_more (537172187223133825) -->
+ <skip />
+ <!-- no translation found for folder_list_show_all_accounts (8054807182336991835) -->
+ <skip />
<string name="manage_folders_subtitle" msgid="7702199674083260433">"Sincronitza i notifica"</string>
<string name="menu_folder_options" msgid="8897520487430647932">"Configuració de la carpeta"</string>
<string name="menu_account_settings" msgid="8230989362863431918">"Configuració del compte"</string>
@@ -220,6 +225,8 @@
<item quantity="one" msgid="4930161390461457462">"S\'ha canviat la carpeta."</item>
<item quantity="other" msgid="8918589141287976985">"S\'han canviat les carpetes."</item>
</plurals>
+ <!-- no translation found for conversation_folder_moved (297469098857964678) -->
+ <skip />
<string name="search_results_header" msgid="4669917471897026269">"Resultats"</string>
<string name="search_unsupported" msgid="4654227193354052607">"La cerca no és compatible en aquest compte."</string>
<string name="searchMode" msgid="3329807422114758583">"Mode de cerca"</string>
@@ -367,4 +374,9 @@
<string name="veiled_alternate_text" msgid="7370933826442034211"></string>
<string name="veiled_alternate_text_unknown_person" msgid="5132515097905273819"></string>
<string name="veiled_summary_unknown_person" msgid="4030928895738205054"></string>
+ <string name="label_notification_ticker" msgid="1684732605316462621">"<xliff:g id="LABEL">%s</xliff:g>: <xliff:g id="NOTIFICATION">%s</xliff:g>"</string>
+ <string name="new_messages" msgid="4419173946074516420">"<xliff:g id="COUNT">%1$d</xliff:g> missatges nous"</string>
+ <string name="single_new_message_notification_title" msgid="4138237430881084155">"<xliff:g id="SENDER">%1$s</xliff:g>: <xliff:g id="SUBJECT">%2$s</xliff:g>"</string>
+ <string name="silent_ringtone" msgid="5856834572357761687">"Silenci"</string>
+ <string name="inbox_unseen_banner" msgid="4561582938706814621">"NOUS: <xliff:g id="NUMBER">%d</xliff:g>"</string>
</resources>
diff --git a/res/values-cs/strings.xml b/res/values-cs/strings.xml
index 592bb38..487aaad 100644
--- a/res/values-cs/strings.xml
+++ b/res/values-cs/strings.xml
@@ -74,8 +74,13 @@
<string name="forward" msgid="6822914459902983767">"Přeposlat"</string>
<string name="menu_compose" msgid="6274193058224230645">"Nová zpráva"</string>
<string name="menu_change_folders" msgid="1542713666608888717">"Změnit složky"</string>
+ <string name="menu_move_to" msgid="9138296669516358542">"Přesunout do"</string>
<string name="menu_manage_folders" msgid="6755623004628177492">"Nastavení složek"</string>
<string name="folder_list_title" msgid="4276644062440415214">"Složky"</string>
+ <!-- no translation found for folder_list_more (537172187223133825) -->
+ <skip />
+ <!-- no translation found for folder_list_show_all_accounts (8054807182336991835) -->
+ <skip />
<string name="manage_folders_subtitle" msgid="7702199674083260433">"Synchronizace a upozornění"</string>
<string name="menu_folder_options" msgid="8897520487430647932">"Nastavení složek"</string>
<string name="menu_account_settings" msgid="8230989362863431918">"Nastavení účtu"</string>
@@ -220,6 +225,8 @@
<item quantity="one" msgid="4930161390461457462">"Složka změněna."</item>
<item quantity="other" msgid="8918589141287976985">"Složky změněny."</item>
</plurals>
+ <!-- no translation found for conversation_folder_moved (297469098857964678) -->
+ <skip />
<string name="search_results_header" msgid="4669917471897026269">"Výsledky"</string>
<string name="search_unsupported" msgid="4654227193354052607">"Vyhledávání není v tomto účtu podporováno."</string>
<string name="searchMode" msgid="3329807422114758583">"Režim vyhledávání"</string>
@@ -367,4 +374,9 @@
<string name="veiled_alternate_text" msgid="7370933826442034211"></string>
<string name="veiled_alternate_text_unknown_person" msgid="5132515097905273819"></string>
<string name="veiled_summary_unknown_person" msgid="4030928895738205054"></string>
+ <string name="label_notification_ticker" msgid="1684732605316462621">"<xliff:g id="LABEL">%s</xliff:g>: <xliff:g id="NOTIFICATION">%s</xliff:g>"</string>
+ <string name="new_messages" msgid="4419173946074516420">"Počet nových zpráv: <xliff:g id="COUNT">%1$d</xliff:g>"</string>
+ <string name="single_new_message_notification_title" msgid="4138237430881084155">"<xliff:g id="SENDER">%1$s</xliff:g>: <xliff:g id="SUBJECT">%2$s</xliff:g>"</string>
+ <string name="silent_ringtone" msgid="5856834572357761687">"Tichý režim"</string>
+ <string name="inbox_unseen_banner" msgid="4561582938706814621">"NOVÉ: <xliff:g id="NUMBER">%d</xliff:g>"</string>
</resources>
diff --git a/res/values-da/strings.xml b/res/values-da/strings.xml
index 7966bdd..ae7c73a 100644
--- a/res/values-da/strings.xml
+++ b/res/values-da/strings.xml
@@ -74,8 +74,13 @@
<string name="forward" msgid="6822914459902983767">"Videresend"</string>
<string name="menu_compose" msgid="6274193058224230645">"Skriv"</string>
<string name="menu_change_folders" msgid="1542713666608888717">"Skift mapper"</string>
+ <string name="menu_move_to" msgid="9138296669516358542">"Flyt til"</string>
<string name="menu_manage_folders" msgid="6755623004628177492">"Indstillinger for mapper"</string>
<string name="folder_list_title" msgid="4276644062440415214">"Mapper"</string>
+ <!-- no translation found for folder_list_more (537172187223133825) -->
+ <skip />
+ <!-- no translation found for folder_list_show_all_accounts (8054807182336991835) -->
+ <skip />
<string name="manage_folders_subtitle" msgid="7702199674083260433">"Synkroniser og underret"</string>
<string name="menu_folder_options" msgid="8897520487430647932">"Indstillinger for mapper"</string>
<string name="menu_account_settings" msgid="8230989362863431918">"Kontoindstillinger"</string>
@@ -220,6 +225,8 @@
<item quantity="one" msgid="4930161390461457462">"Ændret mappe."</item>
<item quantity="other" msgid="8918589141287976985">"Ændrede mapper."</item>
</plurals>
+ <!-- no translation found for conversation_folder_moved (297469098857964678) -->
+ <skip />
<string name="search_results_header" msgid="4669917471897026269">"Resultater"</string>
<string name="search_unsupported" msgid="4654227193354052607">"Søgningen understøttes ikke på denne konto."</string>
<string name="searchMode" msgid="3329807422114758583">"Søgetilstand"</string>
@@ -367,4 +374,9 @@
<string name="veiled_alternate_text" msgid="7370933826442034211"></string>
<string name="veiled_alternate_text_unknown_person" msgid="5132515097905273819"></string>
<string name="veiled_summary_unknown_person" msgid="4030928895738205054"></string>
+ <string name="label_notification_ticker" msgid="1684732605316462621">"<xliff:g id="LABEL">%s</xliff:g>: <xliff:g id="NOTIFICATION">%s</xliff:g>"</string>
+ <string name="new_messages" msgid="4419173946074516420">"<xliff:g id="COUNT">%1$d</xliff:g> nye beskeder"</string>
+ <string name="single_new_message_notification_title" msgid="4138237430881084155">"<xliff:g id="SENDER">%1$s</xliff:g>: <xliff:g id="SUBJECT">%2$s</xliff:g>"</string>
+ <string name="silent_ringtone" msgid="5856834572357761687">"Lydløs"</string>
+ <string name="inbox_unseen_banner" msgid="4561582938706814621">"<xliff:g id="NUMBER">%d</xliff:g> NYE"</string>
</resources>
diff --git a/res/values-de/strings.xml b/res/values-de/strings.xml
index f3d15b1..2ca30c3 100644
--- a/res/values-de/strings.xml
+++ b/res/values-de/strings.xml
@@ -74,8 +74,13 @@
<string name="forward" msgid="6822914459902983767">"Weiterleiten"</string>
<string name="menu_compose" msgid="6274193058224230645">"Schreiben"</string>
<string name="menu_change_folders" msgid="1542713666608888717">"Ordner ändern"</string>
+ <string name="menu_move_to" msgid="9138296669516358542">"Verschieben nach"</string>
<string name="menu_manage_folders" msgid="6755623004628177492">"Ordnereinstellungen"</string>
<string name="folder_list_title" msgid="4276644062440415214">"Ordner"</string>
+ <!-- no translation found for folder_list_more (537172187223133825) -->
+ <skip />
+ <!-- no translation found for folder_list_show_all_accounts (8054807182336991835) -->
+ <skip />
<string name="manage_folders_subtitle" msgid="7702199674083260433">"Synchron. & benachrichtigen"</string>
<string name="menu_folder_options" msgid="8897520487430647932">"Ordnereinstellungen"</string>
<string name="menu_account_settings" msgid="8230989362863431918">"Kontoeinstellungen"</string>
@@ -220,6 +225,8 @@
<item quantity="one" msgid="4930161390461457462">"Ordner geändert"</item>
<item quantity="other" msgid="8918589141287976985">"Ordner geändert"</item>
</plurals>
+ <!-- no translation found for conversation_folder_moved (297469098857964678) -->
+ <skip />
<string name="search_results_header" msgid="4669917471897026269">"Ergebnisse"</string>
<string name="search_unsupported" msgid="4654227193354052607">"Die Suche wird für dieses Konto nicht unterstützt."</string>
<string name="searchMode" msgid="3329807422114758583">"Suchmodus"</string>
@@ -367,4 +374,9 @@
<string name="veiled_alternate_text" msgid="7370933826442034211"></string>
<string name="veiled_alternate_text_unknown_person" msgid="5132515097905273819"></string>
<string name="veiled_summary_unknown_person" msgid="4030928895738205054"></string>
+ <string name="label_notification_ticker" msgid="1684732605316462621">"<xliff:g id="LABEL">%s</xliff:g>: <xliff:g id="NOTIFICATION">%s</xliff:g>"</string>
+ <string name="new_messages" msgid="4419173946074516420">"<xliff:g id="COUNT">%1$d</xliff:g> neue Nachrichten"</string>
+ <string name="single_new_message_notification_title" msgid="4138237430881084155">"<xliff:g id="SENDER">%1$s</xliff:g>: <xliff:g id="SUBJECT">%2$s</xliff:g>"</string>
+ <string name="silent_ringtone" msgid="5856834572357761687">"Lautlos"</string>
+ <string name="inbox_unseen_banner" msgid="4561582938706814621">"<xliff:g id="NUMBER">%d</xliff:g> neu"</string>
</resources>
diff --git a/res/values-el/strings.xml b/res/values-el/strings.xml
index d0a6118..59f1748 100644
--- a/res/values-el/strings.xml
+++ b/res/values-el/strings.xml
@@ -74,8 +74,13 @@
<string name="forward" msgid="6822914459902983767">"Προώθηση"</string>
<string name="menu_compose" msgid="6274193058224230645">"Σύνταξη"</string>
<string name="menu_change_folders" msgid="1542713666608888717">"Αλλαγή φακέλων"</string>
+ <string name="menu_move_to" msgid="9138296669516358542">"Μετακίνηση σε"</string>
<string name="menu_manage_folders" msgid="6755623004628177492">"Ρυθμίσεις φακέλων"</string>
<string name="folder_list_title" msgid="4276644062440415214">"Φάκελοι"</string>
+ <!-- no translation found for folder_list_more (537172187223133825) -->
+ <skip />
+ <!-- no translation found for folder_list_show_all_accounts (8054807182336991835) -->
+ <skip />
<string name="manage_folders_subtitle" msgid="7702199674083260433">"Συγχρονισμός και ειδοποίηση"</string>
<string name="menu_folder_options" msgid="8897520487430647932">"Ρυθμίσεις φακέλου..."</string>
<string name="menu_account_settings" msgid="8230989362863431918">"Ρυθμίσεις Λογαριασμού"</string>
@@ -220,6 +225,8 @@
<item quantity="one" msgid="4930161390461457462">"Αλλαγή φακέλου."</item>
<item quantity="other" msgid="8918589141287976985">"Αλλαγή φακέλων."</item>
</plurals>
+ <!-- no translation found for conversation_folder_moved (297469098857964678) -->
+ <skip />
<string name="search_results_header" msgid="4669917471897026269">"Αποτελέσματα"</string>
<string name="search_unsupported" msgid="4654227193354052607">"Η αναζήτηση δεν υποστηρίζεται σε αυτόν τον λογαριασμό."</string>
<string name="searchMode" msgid="3329807422114758583">"Λειτουργία αναζήτησης"</string>
@@ -367,4 +374,9 @@
<string name="veiled_alternate_text" msgid="7370933826442034211"></string>
<string name="veiled_alternate_text_unknown_person" msgid="5132515097905273819"></string>
<string name="veiled_summary_unknown_person" msgid="4030928895738205054"></string>
+ <string name="label_notification_ticker" msgid="1684732605316462621">"<xliff:g id="LABEL">%s</xliff:g>: <xliff:g id="NOTIFICATION">%s</xliff:g>"</string>
+ <string name="new_messages" msgid="4419173946074516420">"<xliff:g id="COUNT">%1$d</xliff:g> νέα μηνύματα"</string>
+ <string name="single_new_message_notification_title" msgid="4138237430881084155">"<xliff:g id="SENDER">%1$s</xliff:g>: <xliff:g id="SUBJECT">%2$s</xliff:g>"</string>
+ <string name="silent_ringtone" msgid="5856834572357761687">"Σίγαση"</string>
+ <string name="inbox_unseen_banner" msgid="4561582938706814621">"<xliff:g id="NUMBER">%d</xliff:g> ΝΕΑ"</string>
</resources>
diff --git a/res/values-en-rGB/strings.xml b/res/values-en-rGB/strings.xml
index 261c001..b3e54f6 100644
--- a/res/values-en-rGB/strings.xml
+++ b/res/values-en-rGB/strings.xml
@@ -74,8 +74,13 @@
<string name="forward" msgid="6822914459902983767">"Forward"</string>
<string name="menu_compose" msgid="6274193058224230645">"Compose"</string>
<string name="menu_change_folders" msgid="1542713666608888717">"Change folders"</string>
+ <string name="menu_move_to" msgid="9138296669516358542">"Move to"</string>
<string name="menu_manage_folders" msgid="6755623004628177492">"Folder settings"</string>
<string name="folder_list_title" msgid="4276644062440415214">"Folders"</string>
+ <!-- no translation found for folder_list_more (537172187223133825) -->
+ <skip />
+ <!-- no translation found for folder_list_show_all_accounts (8054807182336991835) -->
+ <skip />
<string name="manage_folders_subtitle" msgid="7702199674083260433">"Sync & notify"</string>
<string name="menu_folder_options" msgid="8897520487430647932">"Folder settings"</string>
<string name="menu_account_settings" msgid="8230989362863431918">"Account settings"</string>
@@ -220,6 +225,8 @@
<item quantity="one" msgid="4930161390461457462">"Changed folder."</item>
<item quantity="other" msgid="8918589141287976985">"Changed folders."</item>
</plurals>
+ <!-- no translation found for conversation_folder_moved (297469098857964678) -->
+ <skip />
<string name="search_results_header" msgid="4669917471897026269">"Results"</string>
<string name="search_unsupported" msgid="4654227193354052607">"Search is not supported on this account."</string>
<string name="searchMode" msgid="3329807422114758583">"Search Mode"</string>
@@ -367,4 +374,9 @@
<string name="veiled_alternate_text" msgid="7370933826442034211"></string>
<string name="veiled_alternate_text_unknown_person" msgid="5132515097905273819"></string>
<string name="veiled_summary_unknown_person" msgid="4030928895738205054"></string>
+ <string name="label_notification_ticker" msgid="1684732605316462621">"<xliff:g id="LABEL">%s</xliff:g>: <xliff:g id="NOTIFICATION">%s</xliff:g>"</string>
+ <string name="new_messages" msgid="4419173946074516420">"<xliff:g id="COUNT">%1$d</xliff:g> new messages"</string>
+ <string name="single_new_message_notification_title" msgid="4138237430881084155">"<xliff:g id="SENDER">%1$s</xliff:g>: <xliff:g id="SUBJECT">%2$s</xliff:g>"</string>
+ <string name="silent_ringtone" msgid="5856834572357761687">"Silent"</string>
+ <string name="inbox_unseen_banner" msgid="4561582938706814621">"<xliff:g id="NUMBER">%d</xliff:g> NEW"</string>
</resources>
diff --git a/res/values-es-rUS/strings.xml b/res/values-es-rUS/strings.xml
index 11ffc7d..3a845c4 100644
--- a/res/values-es-rUS/strings.xml
+++ b/res/values-es-rUS/strings.xml
@@ -74,8 +74,13 @@
<string name="forward" msgid="6822914459902983767">"Reenviar"</string>
<string name="menu_compose" msgid="6274193058224230645">"Redactar"</string>
<string name="menu_change_folders" msgid="1542713666608888717">"Cambiar carpetas"</string>
+ <string name="menu_move_to" msgid="9138296669516358542">"Mover a"</string>
<string name="menu_manage_folders" msgid="6755623004628177492">"Configuración de carpetas"</string>
<string name="folder_list_title" msgid="4276644062440415214">"Carpetas"</string>
+ <!-- no translation found for folder_list_more (537172187223133825) -->
+ <skip />
+ <!-- no translation found for folder_list_show_all_accounts (8054807182336991835) -->
+ <skip />
<string name="manage_folders_subtitle" msgid="7702199674083260433">"Sincronizar y notificar"</string>
<string name="menu_folder_options" msgid="8897520487430647932">"Configuración de la carpeta"</string>
<string name="menu_account_settings" msgid="8230989362863431918">"Configuración de la cuenta"</string>
@@ -220,6 +225,8 @@
<item quantity="one" msgid="4930161390461457462">"Carpeta modificada"</item>
<item quantity="other" msgid="8918589141287976985">"Carpetas modificadas"</item>
</plurals>
+ <!-- no translation found for conversation_folder_moved (297469098857964678) -->
+ <skip />
<string name="search_results_header" msgid="4669917471897026269">"Resultados"</string>
<string name="search_unsupported" msgid="4654227193354052607">"Esta cuenta no admite la función de búsqueda."</string>
<string name="searchMode" msgid="3329807422114758583">"Buscar modo"</string>
@@ -367,4 +374,9 @@
<string name="veiled_alternate_text" msgid="7370933826442034211"></string>
<string name="veiled_alternate_text_unknown_person" msgid="5132515097905273819"></string>
<string name="veiled_summary_unknown_person" msgid="4030928895738205054"></string>
+ <string name="label_notification_ticker" msgid="1684732605316462621">"<xliff:g id="LABEL">%s</xliff:g>: <xliff:g id="NOTIFICATION">%s</xliff:g>"</string>
+ <string name="new_messages" msgid="4419173946074516420">"<xliff:g id="COUNT">%1$d</xliff:g> mensajes nuevos"</string>
+ <string name="single_new_message_notification_title" msgid="4138237430881084155">"<xliff:g id="SENDER">%1$s</xliff:g>: <xliff:g id="SUBJECT">%2$s</xliff:g>"</string>
+ <string name="silent_ringtone" msgid="5856834572357761687">"Silencio"</string>
+ <string name="inbox_unseen_banner" msgid="4561582938706814621">"<xliff:g id="NUMBER">%d</xliff:g> NUEVOS"</string>
</resources>
diff --git a/res/values-es/strings.xml b/res/values-es/strings.xml
index 05d640e..f6f692c 100644
--- a/res/values-es/strings.xml
+++ b/res/values-es/strings.xml
@@ -74,8 +74,13 @@
<string name="forward" msgid="6822914459902983767">"Reenviar"</string>
<string name="menu_compose" msgid="6274193058224230645">"Redactar"</string>
<string name="menu_change_folders" msgid="1542713666608888717">"Cambiar carpetas"</string>
+ <string name="menu_move_to" msgid="9138296669516358542">"Mover a"</string>
<string name="menu_manage_folders" msgid="6755623004628177492">"Ajustes de carpeta"</string>
<string name="folder_list_title" msgid="4276644062440415214">"Carpetas"</string>
+ <!-- no translation found for folder_list_more (537172187223133825) -->
+ <skip />
+ <!-- no translation found for folder_list_show_all_accounts (8054807182336991835) -->
+ <skip />
<string name="manage_folders_subtitle" msgid="7702199674083260433">"Sincronizar y notificar"</string>
<string name="menu_folder_options" msgid="8897520487430647932">"Ajustes de carpeta"</string>
<string name="menu_account_settings" msgid="8230989362863431918">"Ajustes de cuenta"</string>
@@ -220,6 +225,8 @@
<item quantity="one" msgid="4930161390461457462">"Carpeta cambiada"</item>
<item quantity="other" msgid="8918589141287976985">"Carpetas cambiadas"</item>
</plurals>
+ <!-- no translation found for conversation_folder_moved (297469098857964678) -->
+ <skip />
<string name="search_results_header" msgid="4669917471897026269">"Resultados"</string>
<string name="search_unsupported" msgid="4654227193354052607">"Esta cuenta no admite la función de búsqueda."</string>
<string name="searchMode" msgid="3329807422114758583">"Modo de búsqueda"</string>
@@ -367,4 +374,9 @@
<string name="veiled_alternate_text" msgid="7370933826442034211"></string>
<string name="veiled_alternate_text_unknown_person" msgid="5132515097905273819"></string>
<string name="veiled_summary_unknown_person" msgid="4030928895738205054"></string>
+ <string name="label_notification_ticker" msgid="1684732605316462621">"<xliff:g id="LABEL">%s</xliff:g>: <xliff:g id="NOTIFICATION">%s</xliff:g>"</string>
+ <string name="new_messages" msgid="4419173946074516420">"<xliff:g id="COUNT">%1$d</xliff:g> mensajes nuevos"</string>
+ <string name="single_new_message_notification_title" msgid="4138237430881084155">"<xliff:g id="SENDER">%1$s</xliff:g>: <xliff:g id="SUBJECT">%2$s</xliff:g>"</string>
+ <string name="silent_ringtone" msgid="5856834572357761687">"Silencio"</string>
+ <string name="inbox_unseen_banner" msgid="4561582938706814621">"<xliff:g id="NUMBER">%d</xliff:g> NUEVOS"</string>
</resources>
diff --git a/res/values-et/strings.xml b/res/values-et/strings.xml
index 21c6445..857e31f 100644
--- a/res/values-et/strings.xml
+++ b/res/values-et/strings.xml
@@ -74,8 +74,13 @@
<string name="forward" msgid="6822914459902983767">"Edasta"</string>
<string name="menu_compose" msgid="6274193058224230645">"Koosta"</string>
<string name="menu_change_folders" msgid="1542713666608888717">"Muuda kaustu"</string>
+ <string name="menu_move_to" msgid="9138296669516358542">"Teisalda asukohta"</string>
<string name="menu_manage_folders" msgid="6755623004628177492">"Kausta seaded"</string>
<string name="folder_list_title" msgid="4276644062440415214">"Kaustad"</string>
+ <!-- no translation found for folder_list_more (537172187223133825) -->
+ <skip />
+ <!-- no translation found for folder_list_show_all_accounts (8054807182336991835) -->
+ <skip />
<string name="manage_folders_subtitle" msgid="7702199674083260433">"Sünkroonimine ja teavitamine"</string>
<string name="menu_folder_options" msgid="8897520487430647932">"Kausta seaded"</string>
<string name="menu_account_settings" msgid="8230989362863431918">"Konto seaded"</string>
@@ -220,6 +225,8 @@
<item quantity="one" msgid="4930161390461457462">"Muudetud kaust."</item>
<item quantity="other" msgid="8918589141287976985">"Muudetud kaustad."</item>
</plurals>
+ <!-- no translation found for conversation_folder_moved (297469098857964678) -->
+ <skip />
<string name="search_results_header" msgid="4669917471897026269">"Tulemused"</string>
<string name="search_unsupported" msgid="4654227193354052607">"Sellel kontol ei toetata otsingut."</string>
<string name="searchMode" msgid="3329807422114758583">"Otsimisrežiim"</string>
@@ -367,4 +374,9 @@
<string name="veiled_alternate_text" msgid="7370933826442034211"></string>
<string name="veiled_alternate_text_unknown_person" msgid="5132515097905273819"></string>
<string name="veiled_summary_unknown_person" msgid="4030928895738205054"></string>
+ <string name="label_notification_ticker" msgid="1684732605316462621">"<xliff:g id="LABEL">%s</xliff:g>: <xliff:g id="NOTIFICATION">%s</xliff:g>"</string>
+ <string name="new_messages" msgid="4419173946074516420">"<xliff:g id="COUNT">%1$d</xliff:g> uut sõnumit"</string>
+ <string name="single_new_message_notification_title" msgid="4138237430881084155">"<xliff:g id="SENDER">%1$s</xliff:g>: <xliff:g id="SUBJECT">%2$s</xliff:g>"</string>
+ <string name="silent_ringtone" msgid="5856834572357761687">"Hääletu"</string>
+ <string name="inbox_unseen_banner" msgid="4561582938706814621">"UUSI: <xliff:g id="NUMBER">%d</xliff:g>"</string>
</resources>
diff --git a/res/values-fa/strings.xml b/res/values-fa/strings.xml
index 98aea75..444a3c4 100644
--- a/res/values-fa/strings.xml
+++ b/res/values-fa/strings.xml
@@ -74,8 +74,13 @@
<string name="forward" msgid="6822914459902983767">"باز ارسال"</string>
<string name="menu_compose" msgid="6274193058224230645">"نوشتن نامه"</string>
<string name="menu_change_folders" msgid="1542713666608888717">"تغییر پوشهها"</string>
+ <string name="menu_move_to" msgid="9138296669516358542">"انتقال به"</string>
<string name="menu_manage_folders" msgid="6755623004628177492">"تنظیمات پوشه"</string>
<string name="folder_list_title" msgid="4276644062440415214">"پوشهها"</string>
+ <!-- no translation found for folder_list_more (537172187223133825) -->
+ <skip />
+ <!-- no translation found for folder_list_show_all_accounts (8054807182336991835) -->
+ <skip />
<string name="manage_folders_subtitle" msgid="7702199674083260433">"همگامسازی و اعلان"</string>
<string name="menu_folder_options" msgid="8897520487430647932">"تنظیمات پوشه"</string>
<string name="menu_account_settings" msgid="8230989362863431918">"تنظیمات حساب"</string>
@@ -220,6 +225,8 @@
<item quantity="one" msgid="4930161390461457462">"پوشه تغییر کرد."</item>
<item quantity="other" msgid="8918589141287976985">"پوشهها تغییر کردند."</item>
</plurals>
+ <!-- no translation found for conversation_folder_moved (297469098857964678) -->
+ <skip />
<string name="search_results_header" msgid="4669917471897026269">"نتایج"</string>
<string name="search_unsupported" msgid="4654227193354052607">"جستجو در این حساب پشتیبانی نمیشود."</string>
<string name="searchMode" msgid="3329807422114758583">"حالت جستجو"</string>
@@ -367,4 +374,9 @@
<string name="veiled_alternate_text" msgid="7370933826442034211"></string>
<string name="veiled_alternate_text_unknown_person" msgid="5132515097905273819"></string>
<string name="veiled_summary_unknown_person" msgid="4030928895738205054"></string>
+ <string name="label_notification_ticker" msgid="1684732605316462621">"<xliff:g id="LABEL">%s</xliff:g>: <xliff:g id="NOTIFICATION">%s</xliff:g>"</string>
+ <string name="new_messages" msgid="4419173946074516420">"<xliff:g id="COUNT">%1$d</xliff:g> پیام جدید"</string>
+ <string name="single_new_message_notification_title" msgid="4138237430881084155">"<xliff:g id="SENDER">%1$s</xliff:g>: <xliff:g id="SUBJECT">%2$s</xliff:g>"</string>
+ <string name="silent_ringtone" msgid="5856834572357761687">"بیصدا"</string>
+ <string name="inbox_unseen_banner" msgid="4561582938706814621">"<xliff:g id="NUMBER">%d</xliff:g> پیام جدید"</string>
</resources>
diff --git a/res/values-fi/strings.xml b/res/values-fi/strings.xml
index f956ba2..994a196 100644
--- a/res/values-fi/strings.xml
+++ b/res/values-fi/strings.xml
@@ -74,8 +74,13 @@
<string name="forward" msgid="6822914459902983767">"Lähetä edelleen"</string>
<string name="menu_compose" msgid="6274193058224230645">"Viestin kirjoitus"</string>
<string name="menu_change_folders" msgid="1542713666608888717">"Vaihda kansiota"</string>
+ <string name="menu_move_to" msgid="9138296669516358542">"Siirrä kansioon"</string>
<string name="menu_manage_folders" msgid="6755623004628177492">"Kansion asetukset"</string>
<string name="folder_list_title" msgid="4276644062440415214">"Kansiot"</string>
+ <!-- no translation found for folder_list_more (537172187223133825) -->
+ <skip />
+ <!-- no translation found for folder_list_show_all_accounts (8054807182336991835) -->
+ <skip />
<string name="manage_folders_subtitle" msgid="7702199674083260433">"Synkronoi ja ilmoita"</string>
<string name="menu_folder_options" msgid="8897520487430647932">"Kansion asetukset..."</string>
<string name="menu_account_settings" msgid="8230989362863431918">"Tilin asetukset"</string>
@@ -220,6 +225,8 @@
<item quantity="one" msgid="4930161390461457462">"Kansiota muutettu."</item>
<item quantity="other" msgid="8918589141287976985">"Kansioita muutettu."</item>
</plurals>
+ <!-- no translation found for conversation_folder_moved (297469098857964678) -->
+ <skip />
<string name="search_results_header" msgid="4669917471897026269">"Tulokset"</string>
<string name="search_unsupported" msgid="4654227193354052607">"Hakua ei voi käyttää tällä tilillä."</string>
<string name="searchMode" msgid="3329807422114758583">"Hakutapa"</string>
@@ -367,4 +374,9 @@
<string name="veiled_alternate_text" msgid="7370933826442034211"></string>
<string name="veiled_alternate_text_unknown_person" msgid="5132515097905273819"></string>
<string name="veiled_summary_unknown_person" msgid="4030928895738205054"></string>
+ <string name="label_notification_ticker" msgid="1684732605316462621">"<xliff:g id="LABEL">%s</xliff:g>: <xliff:g id="NOTIFICATION">%s</xliff:g>"</string>
+ <string name="new_messages" msgid="4419173946074516420">"<xliff:g id="COUNT">%1$d</xliff:g> uutta viestiä"</string>
+ <string name="single_new_message_notification_title" msgid="4138237430881084155">"<xliff:g id="SENDER">%1$s</xliff:g>: <xliff:g id="SUBJECT">%2$s</xliff:g>"</string>
+ <string name="silent_ringtone" msgid="5856834572357761687">"Äänetön"</string>
+ <string name="inbox_unseen_banner" msgid="4561582938706814621">"<xliff:g id="NUMBER">%d</xliff:g> UUTTA"</string>
</resources>
diff --git a/res/values-fr/strings.xml b/res/values-fr/strings.xml
index f2a2836..f1a7e0f 100644
--- a/res/values-fr/strings.xml
+++ b/res/values-fr/strings.xml
@@ -74,8 +74,13 @@
<string name="forward" msgid="6822914459902983767">"Transférer"</string>
<string name="menu_compose" msgid="6274193058224230645">"Nouveau message"</string>
<string name="menu_change_folders" msgid="1542713666608888717">"Modifier les dossiers"</string>
+ <string name="menu_move_to" msgid="9138296669516358542">"Déplacer vers"</string>
<string name="menu_manage_folders" msgid="6755623004628177492">"Paramètres des dossiers"</string>
<string name="folder_list_title" msgid="4276644062440415214">"Dossiers"</string>
+ <!-- no translation found for folder_list_more (537172187223133825) -->
+ <skip />
+ <!-- no translation found for folder_list_show_all_accounts (8054807182336991835) -->
+ <skip />
<string name="manage_folders_subtitle" msgid="7702199674083260433">"Synchroniser et signaler"</string>
<string name="menu_folder_options" msgid="8897520487430647932">"Paramètres du dossier"</string>
<string name="menu_account_settings" msgid="8230989362863431918">"Paramètres de compte"</string>
@@ -220,6 +225,8 @@
<item quantity="one" msgid="4930161390461457462">"Dossier modifié."</item>
<item quantity="other" msgid="8918589141287976985">"Dossiers modifiés."</item>
</plurals>
+ <!-- no translation found for conversation_folder_moved (297469098857964678) -->
+ <skip />
<string name="search_results_header" msgid="4669917471897026269">"Résultats"</string>
<string name="search_unsupported" msgid="4654227193354052607">"La fonctionnalité de recherche n\'est pas compatible avec ce compte."</string>
<string name="searchMode" msgid="3329807422114758583">"Mode Recherche"</string>
@@ -367,4 +374,9 @@
<string name="veiled_alternate_text" msgid="7370933826442034211"></string>
<string name="veiled_alternate_text_unknown_person" msgid="5132515097905273819"></string>
<string name="veiled_summary_unknown_person" msgid="4030928895738205054"></string>
+ <string name="label_notification_ticker" msgid="1684732605316462621">"<xliff:g id="LABEL">%s</xliff:g> : <xliff:g id="NOTIFICATION">%s</xliff:g>"</string>
+ <string name="new_messages" msgid="4419173946074516420">"<xliff:g id="COUNT">%1$d</xliff:g> nouveaux messages"</string>
+ <string name="single_new_message_notification_title" msgid="4138237430881084155">"<xliff:g id="SENDER">%1$s</xliff:g> : <xliff:g id="SUBJECT">%2$s</xliff:g>"</string>
+ <string name="silent_ringtone" msgid="5856834572357761687">"Mode silencieux"</string>
+ <string name="inbox_unseen_banner" msgid="4561582938706814621">"<xliff:g id="NUMBER">%d</xliff:g> NOUVEAUX"</string>
</resources>
diff --git a/res/values-hi/strings.xml b/res/values-hi/strings.xml
index d0fda64..b1f8f3d 100644
--- a/res/values-hi/strings.xml
+++ b/res/values-hi/strings.xml
@@ -74,8 +74,13 @@
<string name="forward" msgid="6822914459902983767">"अग्रेषित करें"</string>
<string name="menu_compose" msgid="6274193058224230645">"लिखें"</string>
<string name="menu_change_folders" msgid="1542713666608888717">"फ़ोल्डर बदलें"</string>
+ <string name="menu_move_to" msgid="9138296669516358542">"इसमें ले जाएं"</string>
<string name="menu_manage_folders" msgid="6755623004628177492">"फ़ोल्डर सेटिंग"</string>
<string name="folder_list_title" msgid="4276644062440415214">"फ़ोल्डर"</string>
+ <!-- no translation found for folder_list_more (537172187223133825) -->
+ <skip />
+ <!-- no translation found for folder_list_show_all_accounts (8054807182336991835) -->
+ <skip />
<string name="manage_folders_subtitle" msgid="7702199674083260433">"समन्वयित करें और सूचित करें"</string>
<string name="menu_folder_options" msgid="8897520487430647932">"फ़ोल्डर सेटिंग"</string>
<string name="menu_account_settings" msgid="8230989362863431918">"खाता सेटिंग"</string>
@@ -220,6 +225,8 @@
<item quantity="one" msgid="4930161390461457462">"फ़ोल्डर बदला गया."</item>
<item quantity="other" msgid="8918589141287976985">"फ़ोल्डर बदले गए."</item>
</plurals>
+ <!-- no translation found for conversation_folder_moved (297469098857964678) -->
+ <skip />
<string name="search_results_header" msgid="4669917471897026269">"परिणाम"</string>
<string name="search_unsupported" msgid="4654227193354052607">"इस खाते पर खोज समर्थित नहीं है."</string>
<string name="searchMode" msgid="3329807422114758583">"खोज मोड"</string>
@@ -367,4 +374,9 @@
<string name="veiled_alternate_text" msgid="7370933826442034211"></string>
<string name="veiled_alternate_text_unknown_person" msgid="5132515097905273819"></string>
<string name="veiled_summary_unknown_person" msgid="4030928895738205054"></string>
+ <string name="label_notification_ticker" msgid="1684732605316462621">"<xliff:g id="LABEL">%s</xliff:g>: <xliff:g id="NOTIFICATION">%s</xliff:g>"</string>
+ <string name="new_messages" msgid="4419173946074516420">"<xliff:g id="COUNT">%1$d</xliff:g> नए संदेश"</string>
+ <string name="single_new_message_notification_title" msgid="4138237430881084155">"<xliff:g id="SENDER">%1$s</xliff:g>: <xliff:g id="SUBJECT">%2$s</xliff:g>"</string>
+ <string name="silent_ringtone" msgid="5856834572357761687">"मौन"</string>
+ <string name="inbox_unseen_banner" msgid="4561582938706814621">"<xliff:g id="NUMBER">%d</xliff:g> नए"</string>
</resources>
diff --git a/res/values-hr/strings.xml b/res/values-hr/strings.xml
index d02a35f..ea7f309 100644
--- a/res/values-hr/strings.xml
+++ b/res/values-hr/strings.xml
@@ -74,8 +74,13 @@
<string name="forward" msgid="6822914459902983767">"Dalje"</string>
<string name="menu_compose" msgid="6274193058224230645">"Nova poruka"</string>
<string name="menu_change_folders" msgid="1542713666608888717">"Promjena mapa"</string>
+ <string name="menu_move_to" msgid="9138296669516358542">"Premjesti u/na"</string>
<string name="menu_manage_folders" msgid="6755623004628177492">"Postavke mapa"</string>
<string name="folder_list_title" msgid="4276644062440415214">"Mape"</string>
+ <!-- no translation found for folder_list_more (537172187223133825) -->
+ <skip />
+ <!-- no translation found for folder_list_show_all_accounts (8054807182336991835) -->
+ <skip />
<string name="manage_folders_subtitle" msgid="7702199674083260433">"Sinkroniziraj i obavijesti"</string>
<string name="menu_folder_options" msgid="8897520487430647932">"Postavke mape"</string>
<string name="menu_account_settings" msgid="8230989362863431918">"Postavke računa"</string>
@@ -220,6 +225,8 @@
<item quantity="one" msgid="4930161390461457462">"Promijenjena mapa."</item>
<item quantity="other" msgid="8918589141287976985">"Promijenjene mape."</item>
</plurals>
+ <!-- no translation found for conversation_folder_moved (297469098857964678) -->
+ <skip />
<string name="search_results_header" msgid="4669917471897026269">"Rezultati"</string>
<string name="search_unsupported" msgid="4654227193354052607">"Pretraživanje nije podržano na ovom računu."</string>
<string name="searchMode" msgid="3329807422114758583">"Način pretraživanja"</string>
@@ -367,4 +374,9 @@
<string name="veiled_alternate_text" msgid="7370933826442034211"></string>
<string name="veiled_alternate_text_unknown_person" msgid="5132515097905273819"></string>
<string name="veiled_summary_unknown_person" msgid="4030928895738205054"></string>
+ <string name="label_notification_ticker" msgid="1684732605316462621">"<xliff:g id="LABEL">%s</xliff:g>: <xliff:g id="NOTIFICATION">%s</xliff:g>"</string>
+ <string name="new_messages" msgid="4419173946074516420">"Novih poruka: <xliff:g id="COUNT">%1$d</xliff:g>"</string>
+ <string name="single_new_message_notification_title" msgid="4138237430881084155">"<xliff:g id="SENDER">%1$s</xliff:g>: <xliff:g id="SUBJECT">%2$s</xliff:g>"</string>
+ <string name="silent_ringtone" msgid="5856834572357761687">"Bešumno"</string>
+ <string name="inbox_unseen_banner" msgid="4561582938706814621">"NOVE PORUKE (<xliff:g id="NUMBER">%d</xliff:g>)"</string>
</resources>
diff --git a/res/values-hu/strings.xml b/res/values-hu/strings.xml
index b6f519c..e1f12a9 100644
--- a/res/values-hu/strings.xml
+++ b/res/values-hu/strings.xml
@@ -74,8 +74,13 @@
<string name="forward" msgid="6822914459902983767">"Továbbítás"</string>
<string name="menu_compose" msgid="6274193058224230645">"Levélírás"</string>
<string name="menu_change_folders" msgid="1542713666608888717">"Mappaváltás"</string>
+ <string name="menu_move_to" msgid="9138296669516358542">"Áthelyezés ide:"</string>
<string name="menu_manage_folders" msgid="6755623004628177492">"Mappabeállítások"</string>
<string name="folder_list_title" msgid="4276644062440415214">"Mappák"</string>
+ <!-- no translation found for folder_list_more (537172187223133825) -->
+ <skip />
+ <!-- no translation found for folder_list_show_all_accounts (8054807182336991835) -->
+ <skip />
<string name="manage_folders_subtitle" msgid="7702199674083260433">"Szinkronizálás és értesítés"</string>
<string name="menu_folder_options" msgid="8897520487430647932">"Mappabeállítások"</string>
<string name="menu_account_settings" msgid="8230989362863431918">"Fiókbeállítások"</string>
@@ -220,6 +225,8 @@
<item quantity="one" msgid="4930161390461457462">"A mappa módosult."</item>
<item quantity="other" msgid="8918589141287976985">"A mappák módosultak."</item>
</plurals>
+ <!-- no translation found for conversation_folder_moved (297469098857964678) -->
+ <skip />
<string name="search_results_header" msgid="4669917471897026269">"Találatok"</string>
<string name="search_unsupported" msgid="4654227193354052607">"A keresés nem támogatott ebben a fiókban."</string>
<string name="searchMode" msgid="3329807422114758583">"Keresési mód"</string>
@@ -367,4 +374,9 @@
<string name="veiled_alternate_text" msgid="7370933826442034211"></string>
<string name="veiled_alternate_text_unknown_person" msgid="5132515097905273819"></string>
<string name="veiled_summary_unknown_person" msgid="4030928895738205054"></string>
+ <string name="label_notification_ticker" msgid="1684732605316462621">"<xliff:g id="LABEL">%s</xliff:g>: <xliff:g id="NOTIFICATION">%s</xliff:g>"</string>
+ <string name="new_messages" msgid="4419173946074516420">"<xliff:g id="COUNT">%1$d</xliff:g> új üzenet"</string>
+ <string name="single_new_message_notification_title" msgid="4138237430881084155">"<xliff:g id="SENDER">%1$s</xliff:g>: <xliff:g id="SUBJECT">%2$s</xliff:g>"</string>
+ <string name="silent_ringtone" msgid="5856834572357761687">"Néma"</string>
+ <string name="inbox_unseen_banner" msgid="4561582938706814621">"<xliff:g id="NUMBER">%d</xliff:g> ÚJ"</string>
</resources>
diff --git a/res/values-in/strings.xml b/res/values-in/strings.xml
index dd5ba36..50d0352 100644
--- a/res/values-in/strings.xml
+++ b/res/values-in/strings.xml
@@ -74,8 +74,13 @@
<string name="forward" msgid="6822914459902983767">"Teruskan"</string>
<string name="menu_compose" msgid="6274193058224230645">"Tulis"</string>
<string name="menu_change_folders" msgid="1542713666608888717">"Ganti folder"</string>
+ <string name="menu_move_to" msgid="9138296669516358542">"Pindahkan ke"</string>
<string name="menu_manage_folders" msgid="6755623004628177492">"Setelan folder"</string>
<string name="folder_list_title" msgid="4276644062440415214">"Folder"</string>
+ <!-- no translation found for folder_list_more (537172187223133825) -->
+ <skip />
+ <!-- no translation found for folder_list_show_all_accounts (8054807182336991835) -->
+ <skip />
<string name="manage_folders_subtitle" msgid="7702199674083260433">"Sinkronkan & beri tahu"</string>
<string name="menu_folder_options" msgid="8897520487430647932">"Setelan folder"</string>
<string name="menu_account_settings" msgid="8230989362863431918">"Pengaturan akun"</string>
@@ -220,6 +225,8 @@
<item quantity="one" msgid="4930161390461457462">"Folder diubah."</item>
<item quantity="other" msgid="8918589141287976985">"Folder diubah."</item>
</plurals>
+ <!-- no translation found for conversation_folder_moved (297469098857964678) -->
+ <skip />
<string name="search_results_header" msgid="4669917471897026269">"Hasil"</string>
<string name="search_unsupported" msgid="4654227193354052607">"Penelusuran tidak didukung pada akun ini."</string>
<string name="searchMode" msgid="3329807422114758583">"Mode Penelusuran"</string>
@@ -367,4 +374,9 @@
<string name="veiled_alternate_text" msgid="7370933826442034211"></string>
<string name="veiled_alternate_text_unknown_person" msgid="5132515097905273819"></string>
<string name="veiled_summary_unknown_person" msgid="4030928895738205054"></string>
+ <string name="label_notification_ticker" msgid="1684732605316462621">"<xliff:g id="LABEL">%s</xliff:g>: <xliff:g id="NOTIFICATION">%s</xliff:g>"</string>
+ <string name="new_messages" msgid="4419173946074516420">"<xliff:g id="COUNT">%1$d</xliff:g> pesan baru"</string>
+ <string name="single_new_message_notification_title" msgid="4138237430881084155">"<xliff:g id="SENDER">%1$s</xliff:g>: <xliff:g id="SUBJECT">%2$s</xliff:g>"</string>
+ <string name="silent_ringtone" msgid="5856834572357761687">"Tidak Berbunyi"</string>
+ <string name="inbox_unseen_banner" msgid="4561582938706814621">"<xliff:g id="NUMBER">%d</xliff:g> BARU"</string>
</resources>
diff --git a/res/values-it/strings.xml b/res/values-it/strings.xml
index 8122943..2e17b51 100644
--- a/res/values-it/strings.xml
+++ b/res/values-it/strings.xml
@@ -74,8 +74,13 @@
<string name="forward" msgid="6822914459902983767">"Inoltra"</string>
<string name="menu_compose" msgid="6274193058224230645">"Scrivi"</string>
<string name="menu_change_folders" msgid="1542713666608888717">"Cambia cartelle"</string>
+ <string name="menu_move_to" msgid="9138296669516358542">"Sposta in"</string>
<string name="menu_manage_folders" msgid="6755623004628177492">"Impostazioni cartella"</string>
<string name="folder_list_title" msgid="4276644062440415214">"Cartelle"</string>
+ <!-- no translation found for folder_list_more (537172187223133825) -->
+ <skip />
+ <!-- no translation found for folder_list_show_all_accounts (8054807182336991835) -->
+ <skip />
<string name="manage_folders_subtitle" msgid="7702199674083260433">"Sincronizza e notifica"</string>
<string name="menu_folder_options" msgid="8897520487430647932">"Impostazioni cartella"</string>
<string name="menu_account_settings" msgid="8230989362863431918">"Impostazioni account"</string>
@@ -220,6 +225,8 @@
<item quantity="one" msgid="4930161390461457462">"Cartella cambiata."</item>
<item quantity="other" msgid="8918589141287976985">"Cartelle cambiate."</item>
</plurals>
+ <!-- no translation found for conversation_folder_moved (297469098857964678) -->
+ <skip />
<string name="search_results_header" msgid="4669917471897026269">"Risultati"</string>
<string name="search_unsupported" msgid="4654227193354052607">"La ricerca non è supportata per l\'account in uso."</string>
<string name="searchMode" msgid="3329807422114758583">"Modalità di ricerca"</string>
@@ -367,4 +374,9 @@
<string name="veiled_alternate_text" msgid="7370933826442034211"></string>
<string name="veiled_alternate_text_unknown_person" msgid="5132515097905273819"></string>
<string name="veiled_summary_unknown_person" msgid="4030928895738205054"></string>
+ <string name="label_notification_ticker" msgid="1684732605316462621">"<xliff:g id="LABEL">%s</xliff:g>: <xliff:g id="NOTIFICATION">%s</xliff:g>"</string>
+ <string name="new_messages" msgid="4419173946074516420">"<xliff:g id="COUNT">%1$d</xliff:g> nuovi messaggi"</string>
+ <string name="single_new_message_notification_title" msgid="4138237430881084155">"<xliff:g id="SENDER">%1$s</xliff:g>: <xliff:g id="SUBJECT">%2$s</xliff:g>"</string>
+ <string name="silent_ringtone" msgid="5856834572357761687">"Silenzioso"</string>
+ <string name="inbox_unseen_banner" msgid="4561582938706814621">"<xliff:g id="NUMBER">%d</xliff:g> NUOVI"</string>
</resources>
diff --git a/res/values-iw/strings.xml b/res/values-iw/strings.xml
index 7ff0529..dd9dd84 100644
--- a/res/values-iw/strings.xml
+++ b/res/values-iw/strings.xml
@@ -74,8 +74,13 @@
<string name="forward" msgid="6822914459902983767">"העבר"</string>
<string name="menu_compose" msgid="6274193058224230645">"כתוב"</string>
<string name="menu_change_folders" msgid="1542713666608888717">"שנה תיקיות"</string>
+ <string name="menu_move_to" msgid="9138296669516358542">"העבר אל"</string>
<string name="menu_manage_folders" msgid="6755623004628177492">"הגדרות תיקיה"</string>
<string name="folder_list_title" msgid="4276644062440415214">"תיקיות"</string>
+ <!-- no translation found for folder_list_more (537172187223133825) -->
+ <skip />
+ <!-- no translation found for folder_list_show_all_accounts (8054807182336991835) -->
+ <skip />
<string name="manage_folders_subtitle" msgid="7702199674083260433">"סנכרן ושלח התראה"</string>
<string name="menu_folder_options" msgid="8897520487430647932">"הגדרות תיקיה"</string>
<string name="menu_account_settings" msgid="8230989362863431918">"הגדרות חשבון"</string>
@@ -220,6 +225,8 @@
<item quantity="one" msgid="4930161390461457462">"התיקיה שונתה."</item>
<item quantity="other" msgid="8918589141287976985">"התיקיות שונו."</item>
</plurals>
+ <!-- no translation found for conversation_folder_moved (297469098857964678) -->
+ <skip />
<string name="search_results_header" msgid="4669917471897026269">"תוצאות"</string>
<string name="search_unsupported" msgid="4654227193354052607">"חיפוש אינו נתמך בחשבון זה."</string>
<string name="searchMode" msgid="3329807422114758583">"מצב חיפוש"</string>
@@ -367,4 +374,9 @@
<string name="veiled_alternate_text" msgid="7370933826442034211"></string>
<string name="veiled_alternate_text_unknown_person" msgid="5132515097905273819"></string>
<string name="veiled_summary_unknown_person" msgid="4030928895738205054"></string>
+ <string name="label_notification_ticker" msgid="1684732605316462621">"<xliff:g id="LABEL">%s</xliff:g>: <xliff:g id="NOTIFICATION">%s</xliff:g>"</string>
+ <string name="new_messages" msgid="4419173946074516420">"<xliff:g id="COUNT">%1$d</xliff:g> הודעות חדשות"</string>
+ <string name="single_new_message_notification_title" msgid="4138237430881084155">"<xliff:g id="SENDER">%1$s</xliff:g>: <xliff:g id="SUBJECT">%2$s</xliff:g>"</string>
+ <string name="silent_ringtone" msgid="5856834572357761687">"שקט"</string>
+ <string name="inbox_unseen_banner" msgid="4561582938706814621">"<xliff:g id="NUMBER">%d</xliff:g> חדשות"</string>
</resources>
diff --git a/res/values-ja/strings.xml b/res/values-ja/strings.xml
index bd4a14b..6bb7191 100644
--- a/res/values-ja/strings.xml
+++ b/res/values-ja/strings.xml
@@ -74,8 +74,13 @@
<string name="forward" msgid="6822914459902983767">"転送"</string>
<string name="menu_compose" msgid="6274193058224230645">"作成"</string>
<string name="menu_change_folders" msgid="1542713666608888717">"フォルダを変更"</string>
+ <string name="menu_move_to" msgid="9138296669516358542">"移動"</string>
<string name="menu_manage_folders" msgid="6755623004628177492">"フォルダの設定"</string>
<string name="folder_list_title" msgid="4276644062440415214">"フォルダ"</string>
+ <!-- no translation found for folder_list_more (537172187223133825) -->
+ <skip />
+ <!-- no translation found for folder_list_show_all_accounts (8054807182336991835) -->
+ <skip />
<string name="manage_folders_subtitle" msgid="7702199674083260433">"同期と通知"</string>
<string name="menu_folder_options" msgid="8897520487430647932">"フォルダの設定"</string>
<string name="menu_account_settings" msgid="8230989362863431918">"アカウント設定"</string>
@@ -220,6 +225,8 @@
<item quantity="one" msgid="4930161390461457462">"フォルダを変更しました。"</item>
<item quantity="other" msgid="8918589141287976985">"フォルダを変更しました。"</item>
</plurals>
+ <!-- no translation found for conversation_folder_moved (297469098857964678) -->
+ <skip />
<string name="search_results_header" msgid="4669917471897026269">"検索結果"</string>
<string name="search_unsupported" msgid="4654227193354052607">"このアカウントでは検索をご利用いただけません。"</string>
<string name="searchMode" msgid="3329807422114758583">"検索モード"</string>
@@ -367,4 +374,9 @@
<string name="veiled_alternate_text" msgid="7370933826442034211"></string>
<string name="veiled_alternate_text_unknown_person" msgid="5132515097905273819"></string>
<string name="veiled_summary_unknown_person" msgid="4030928895738205054"></string>
+ <string name="label_notification_ticker" msgid="1684732605316462621">"<xliff:g id="LABEL">%s</xliff:g>: <xliff:g id="NOTIFICATION">%s</xliff:g>"</string>
+ <string name="new_messages" msgid="4419173946074516420">"<xliff:g id="COUNT">%1$d</xliff:g>件の新着メッセージ"</string>
+ <string name="single_new_message_notification_title" msgid="4138237430881084155">"<xliff:g id="SENDER">%1$s</xliff:g>: <xliff:g id="SUBJECT">%2$s</xliff:g>"</string>
+ <string name="silent_ringtone" msgid="5856834572357761687">"マナーモード"</string>
+ <string name="inbox_unseen_banner" msgid="4561582938706814621">"<xliff:g id="NUMBER">%d</xliff:g>件の新着"</string>
</resources>
diff --git a/res/values-ko/strings.xml b/res/values-ko/strings.xml
index cbb9812..842dbb1 100644
--- a/res/values-ko/strings.xml
+++ b/res/values-ko/strings.xml
@@ -74,8 +74,13 @@
<string name="forward" msgid="6822914459902983767">"전달"</string>
<string name="menu_compose" msgid="6274193058224230645">"편지쓰기"</string>
<string name="menu_change_folders" msgid="1542713666608888717">"폴더 변경"</string>
+ <string name="menu_move_to" msgid="9138296669516358542">"이동"</string>
<string name="menu_manage_folders" msgid="6755623004628177492">"폴더 설정"</string>
<string name="folder_list_title" msgid="4276644062440415214">"폴더"</string>
+ <!-- no translation found for folder_list_more (537172187223133825) -->
+ <skip />
+ <!-- no translation found for folder_list_show_all_accounts (8054807182336991835) -->
+ <skip />
<string name="manage_folders_subtitle" msgid="7702199674083260433">"동기화 및 알림"</string>
<string name="menu_folder_options" msgid="8897520487430647932">"폴더 설정"</string>
<string name="menu_account_settings" msgid="8230989362863431918">"계정 설정"</string>
@@ -220,6 +225,8 @@
<item quantity="one" msgid="4930161390461457462">"폴더가 변경되었습니다."</item>
<item quantity="other" msgid="8918589141287976985">"폴더가 변경되었습니다."</item>
</plurals>
+ <!-- no translation found for conversation_folder_moved (297469098857964678) -->
+ <skip />
<string name="search_results_header" msgid="4669917471897026269">"검색결과"</string>
<string name="search_unsupported" msgid="4654227193354052607">"이 계정에서는 검색이 지원되지 않습니다."</string>
<string name="searchMode" msgid="3329807422114758583">"검색 모드"</string>
@@ -367,4 +374,9 @@
<string name="veiled_alternate_text" msgid="7370933826442034211"></string>
<string name="veiled_alternate_text_unknown_person" msgid="5132515097905273819"></string>
<string name="veiled_summary_unknown_person" msgid="4030928895738205054"></string>
+ <string name="label_notification_ticker" msgid="1684732605316462621">"<xliff:g id="LABEL">%s</xliff:g>: <xliff:g id="NOTIFICATION">%s</xliff:g>"</string>
+ <string name="new_messages" msgid="4419173946074516420">"새 메시지 <xliff:g id="COUNT">%1$d</xliff:g>개"</string>
+ <string name="single_new_message_notification_title" msgid="4138237430881084155">"<xliff:g id="SENDER">%1$s</xliff:g>: <xliff:g id="SUBJECT">%2$s</xliff:g>"</string>
+ <string name="silent_ringtone" msgid="5856834572357761687">"무음"</string>
+ <string name="inbox_unseen_banner" msgid="4561582938706814621">"<xliff:g id="NUMBER">%d</xliff:g>개의 새 이메일"</string>
</resources>
diff --git a/res/values-lt/strings.xml b/res/values-lt/strings.xml
index 5240e9a..23944b8 100644
--- a/res/values-lt/strings.xml
+++ b/res/values-lt/strings.xml
@@ -74,8 +74,13 @@
<string name="forward" msgid="6822914459902983767">"Persiųsti"</string>
<string name="menu_compose" msgid="6274193058224230645">"Sukurti"</string>
<string name="menu_change_folders" msgid="1542713666608888717">"Keisti aplankus"</string>
+ <string name="menu_move_to" msgid="9138296669516358542">"Perkelti į"</string>
<string name="menu_manage_folders" msgid="6755623004628177492">"Aplankų nustatymai"</string>
<string name="folder_list_title" msgid="4276644062440415214">"Aplankai"</string>
+ <!-- no translation found for folder_list_more (537172187223133825) -->
+ <skip />
+ <!-- no translation found for folder_list_show_all_accounts (8054807182336991835) -->
+ <skip />
<string name="manage_folders_subtitle" msgid="7702199674083260433">"Sinchronizuoti ir pranešti"</string>
<string name="menu_folder_options" msgid="8897520487430647932">"Aplankų nustatymai"</string>
<string name="menu_account_settings" msgid="8230989362863431918">"Paskyros nustatymai"</string>
@@ -220,6 +225,8 @@
<item quantity="one" msgid="4930161390461457462">"Aplankas pakeistas."</item>
<item quantity="other" msgid="8918589141287976985">"Aplankai pakeisti."</item>
</plurals>
+ <!-- no translation found for conversation_folder_moved (297469098857964678) -->
+ <skip />
<string name="search_results_header" msgid="4669917471897026269">"Rezultatai"</string>
<string name="search_unsupported" msgid="4654227193354052607">"Šioje paskyroje paieška nepalaikoma."</string>
<string name="searchMode" msgid="3329807422114758583">"Paieškos režimas"</string>
@@ -367,4 +374,9 @@
<string name="veiled_alternate_text" msgid="7370933826442034211"></string>
<string name="veiled_alternate_text_unknown_person" msgid="5132515097905273819"></string>
<string name="veiled_summary_unknown_person" msgid="4030928895738205054"></string>
+ <string name="label_notification_ticker" msgid="1684732605316462621">"<xliff:g id="LABEL">%s</xliff:g>: <xliff:g id="NOTIFICATION">%s</xliff:g>"</string>
+ <string name="new_messages" msgid="4419173946074516420">"Naujų pranešimų: <xliff:g id="COUNT">%1$d</xliff:g>"</string>
+ <string name="single_new_message_notification_title" msgid="4138237430881084155">"<xliff:g id="SENDER">%1$s</xliff:g>: <xliff:g id="SUBJECT">%2$s</xliff:g>"</string>
+ <string name="silent_ringtone" msgid="5856834572357761687">"Tylus"</string>
+ <string name="inbox_unseen_banner" msgid="4561582938706814621">"NAUJA: <xliff:g id="NUMBER">%d</xliff:g>"</string>
</resources>
diff --git a/res/values-lv/strings.xml b/res/values-lv/strings.xml
index 343c348..bef8502 100644
--- a/res/values-lv/strings.xml
+++ b/res/values-lv/strings.xml
@@ -74,8 +74,13 @@
<string name="forward" msgid="6822914459902983767">"Pārsūtīt"</string>
<string name="menu_compose" msgid="6274193058224230645">"Rakstīt"</string>
<string name="menu_change_folders" msgid="1542713666608888717">"Mainīt mapes"</string>
+ <string name="menu_move_to" msgid="9138296669516358542">"Pārvietot uz:"</string>
<string name="menu_manage_folders" msgid="6755623004628177492">"Mapju iestatījumi"</string>
<string name="folder_list_title" msgid="4276644062440415214">"Mapes"</string>
+ <!-- no translation found for folder_list_more (537172187223133825) -->
+ <skip />
+ <!-- no translation found for folder_list_show_all_accounts (8054807182336991835) -->
+ <skip />
<string name="manage_folders_subtitle" msgid="7702199674083260433">"Sinhronizācija un paziņojumi"</string>
<string name="menu_folder_options" msgid="8897520487430647932">"Mapes iestatījumi"</string>
<string name="menu_account_settings" msgid="8230989362863431918">"Konta iestatījumi"</string>
@@ -220,6 +225,8 @@
<item quantity="one" msgid="4930161390461457462">"Mape ir mainīta."</item>
<item quantity="other" msgid="8918589141287976985">"Mapes ir mainītas."</item>
</plurals>
+ <!-- no translation found for conversation_folder_moved (297469098857964678) -->
+ <skip />
<string name="search_results_header" msgid="4669917471897026269">"Rezultāti"</string>
<string name="search_unsupported" msgid="4654227193354052607">"Šajā kontā netiek atbalstīta meklēšana."</string>
<string name="searchMode" msgid="3329807422114758583">"Meklēšanas režīms"</string>
@@ -367,4 +374,9 @@
<string name="veiled_alternate_text" msgid="7370933826442034211"></string>
<string name="veiled_alternate_text_unknown_person" msgid="5132515097905273819"></string>
<string name="veiled_summary_unknown_person" msgid="4030928895738205054"></string>
+ <string name="label_notification_ticker" msgid="1684732605316462621">"<xliff:g id="LABEL">%s</xliff:g>: <xliff:g id="NOTIFICATION">%s</xliff:g>"</string>
+ <string name="new_messages" msgid="4419173946074516420">"<xliff:g id="COUNT">%1$d</xliff:g> jauni ziņojumi"</string>
+ <string name="single_new_message_notification_title" msgid="4138237430881084155">"<xliff:g id="SENDER">%1$s</xliff:g>: <xliff:g id="SUBJECT">%2$s</xliff:g>"</string>
+ <string name="silent_ringtone" msgid="5856834572357761687">"Klusums"</string>
+ <string name="inbox_unseen_banner" msgid="4561582938706814621">"JAUNI (<xliff:g id="NUMBER">%d</xliff:g>)"</string>
</resources>
diff --git a/res/values-ms/strings.xml b/res/values-ms/strings.xml
index 9f0664e..0c364af 100644
--- a/res/values-ms/strings.xml
+++ b/res/values-ms/strings.xml
@@ -74,8 +74,13 @@
<string name="forward" msgid="6822914459902983767">"Kirim semula"</string>
<string name="menu_compose" msgid="6274193058224230645">"Karang"</string>
<string name="menu_change_folders" msgid="1542713666608888717">"Tukar folder"</string>
+ <string name="menu_move_to" msgid="9138296669516358542">"Alih ke"</string>
<string name="menu_manage_folders" msgid="6755623004628177492">"Tetapan folder"</string>
<string name="folder_list_title" msgid="4276644062440415214">"Folder"</string>
+ <!-- no translation found for folder_list_more (537172187223133825) -->
+ <skip />
+ <!-- no translation found for folder_list_show_all_accounts (8054807182336991835) -->
+ <skip />
<string name="manage_folders_subtitle" msgid="7702199674083260433">"Segerakkan & beritahu"</string>
<string name="menu_folder_options" msgid="8897520487430647932">"Tetapan folder"</string>
<string name="menu_account_settings" msgid="8230989362863431918">"Tetapan akaun"</string>
@@ -220,6 +225,8 @@
<item quantity="one" msgid="4930161390461457462">"Folder yang diubah."</item>
<item quantity="other" msgid="8918589141287976985">"Folder yang diubah."</item>
</plurals>
+ <!-- no translation found for conversation_folder_moved (297469098857964678) -->
+ <skip />
<string name="search_results_header" msgid="4669917471897026269">"Hasil"</string>
<string name="search_unsupported" msgid="4654227193354052607">"Carian tidak disokong pada akaun ini."</string>
<string name="searchMode" msgid="3329807422114758583">"Cari dalam Mod"</string>
@@ -367,4 +374,9 @@
<string name="veiled_alternate_text" msgid="7370933826442034211"></string>
<string name="veiled_alternate_text_unknown_person" msgid="5132515097905273819"></string>
<string name="veiled_summary_unknown_person" msgid="4030928895738205054"></string>
+ <string name="label_notification_ticker" msgid="1684732605316462621">"<xliff:g id="LABEL">%s</xliff:g>: <xliff:g id="NOTIFICATION">%s</xliff:g>"</string>
+ <string name="new_messages" msgid="4419173946074516420">"<xliff:g id="COUNT">%1$d</xliff:g> mesej baharu"</string>
+ <string name="single_new_message_notification_title" msgid="4138237430881084155">"<xliff:g id="SENDER">%1$s</xliff:g>: <xliff:g id="SUBJECT">%2$s</xliff:g>"</string>
+ <string name="silent_ringtone" msgid="5856834572357761687">"Senyap"</string>
+ <string name="inbox_unseen_banner" msgid="4561582938706814621">"<xliff:g id="NUMBER">%d</xliff:g> BAHARU"</string>
</resources>
diff --git a/res/values-nb/strings.xml b/res/values-nb/strings.xml
index 01af04e..656ebd4 100644
--- a/res/values-nb/strings.xml
+++ b/res/values-nb/strings.xml
@@ -74,8 +74,13 @@
<string name="forward" msgid="6822914459902983767">"Videresend"</string>
<string name="menu_compose" msgid="6274193058224230645">"Skriv ny"</string>
<string name="menu_change_folders" msgid="1542713666608888717">"Endre mapper"</string>
+ <string name="menu_move_to" msgid="9138296669516358542">"Flytt til"</string>
<string name="menu_manage_folders" msgid="6755623004628177492">"Mappeinnstillinger"</string>
<string name="folder_list_title" msgid="4276644062440415214">"Mapper"</string>
+ <!-- no translation found for folder_list_more (537172187223133825) -->
+ <skip />
+ <!-- no translation found for folder_list_show_all_accounts (8054807182336991835) -->
+ <skip />
<string name="manage_folders_subtitle" msgid="7702199674083260433">"Synkronisering og varsler"</string>
<string name="menu_folder_options" msgid="8897520487430647932">"Mappeinnstillinger"</string>
<string name="menu_account_settings" msgid="8230989362863431918">"Kontoinnstillinger"</string>
@@ -220,6 +225,8 @@
<item quantity="one" msgid="4930161390461457462">"Endret mappe."</item>
<item quantity="other" msgid="8918589141287976985">"Endret mapper."</item>
</plurals>
+ <!-- no translation found for conversation_folder_moved (297469098857964678) -->
+ <skip />
<string name="search_results_header" msgid="4669917471897026269">"Resultater"</string>
<string name="search_unsupported" msgid="4654227193354052607">"Søk støttes ikke på denne kontoen."</string>
<string name="searchMode" msgid="3329807422114758583">"Søkemodus"</string>
@@ -367,4 +374,9 @@
<string name="veiled_alternate_text" msgid="7370933826442034211"></string>
<string name="veiled_alternate_text_unknown_person" msgid="5132515097905273819"></string>
<string name="veiled_summary_unknown_person" msgid="4030928895738205054"></string>
+ <string name="label_notification_ticker" msgid="1684732605316462621">"<xliff:g id="LABEL">%s</xliff:g>: <xliff:g id="NOTIFICATION">%s</xliff:g>"</string>
+ <string name="new_messages" msgid="4419173946074516420">"<xliff:g id="COUNT">%1$d</xliff:g> nye e-poster"</string>
+ <string name="single_new_message_notification_title" msgid="4138237430881084155">"<xliff:g id="SENDER">%1$s</xliff:g>: <xliff:g id="SUBJECT">%2$s</xliff:g>"</string>
+ <string name="silent_ringtone" msgid="5856834572357761687">"Stille"</string>
+ <string name="inbox_unseen_banner" msgid="4561582938706814621">"<xliff:g id="NUMBER">%d</xliff:g> NYE"</string>
</resources>
diff --git a/res/values-nl/strings.xml b/res/values-nl/strings.xml
index 0c64057..cc91bda 100644
--- a/res/values-nl/strings.xml
+++ b/res/values-nl/strings.xml
@@ -74,8 +74,13 @@
<string name="forward" msgid="6822914459902983767">"Doorsturen"</string>
<string name="menu_compose" msgid="6274193058224230645">"Opstellen"</string>
<string name="menu_change_folders" msgid="1542713666608888717">"Mappen wijzigen"</string>
+ <string name="menu_move_to" msgid="9138296669516358542">"Verplaatsen naar"</string>
<string name="menu_manage_folders" msgid="6755623004628177492">"Mapinstellingen"</string>
<string name="folder_list_title" msgid="4276644062440415214">"Mappen"</string>
+ <!-- no translation found for folder_list_more (537172187223133825) -->
+ <skip />
+ <!-- no translation found for folder_list_show_all_accounts (8054807182336991835) -->
+ <skip />
<string name="manage_folders_subtitle" msgid="7702199674083260433">"Synchroniseren en melden"</string>
<string name="menu_folder_options" msgid="8897520487430647932">"Mapinstellingen"</string>
<string name="menu_account_settings" msgid="8230989362863431918">"Accountinstellingen"</string>
@@ -220,6 +225,8 @@
<item quantity="one" msgid="4930161390461457462">"Map gewijzigd."</item>
<item quantity="other" msgid="8918589141287976985">"Mappen gewijzigd."</item>
</plurals>
+ <!-- no translation found for conversation_folder_moved (297469098857964678) -->
+ <skip />
<string name="search_results_header" msgid="4669917471897026269">"Resultaten"</string>
<string name="search_unsupported" msgid="4654227193354052607">"Zoeken wordt niet ondersteund in dit account."</string>
<string name="searchMode" msgid="3329807422114758583">"Zoekmodus"</string>
@@ -276,7 +283,7 @@
<string-array name="sync_status">
<item msgid="2446076619901049026">"Geslaagd"</item>
<item msgid="7109065688039971961">"Geen verbinding."</item>
- <item msgid="8437496123716232060">"Kan niet aanmelden."</item>
+ <item msgid="8437496123716232060">"Kan niet inloggen."</item>
<item msgid="1651266301325684887">"Beveiligingsfout."</item>
<item msgid="1461520171154288533">"Kan niet synchroniseren."</item>
<item msgid="4779810016424303449">"Interne fout"</item>
@@ -322,7 +329,7 @@
<item msgid="6593672292311851204">"Veelgebruikt"</item>
<item msgid="3584541772344786752">"Alle mappen"</item>
</string-array>
- <string name="signin" msgid="8958889809095796177">"Aanmelden"</string>
+ <string name="signin" msgid="8958889809095796177">"Inloggen"</string>
<string name="info" msgid="6009817562073541204">"Info"</string>
<string name="report" msgid="5417082746232614958">"Rapporteren"</string>
<string name="sync_error" msgid="7368819509040597851">"Kan niet synchroniseren."</string>
@@ -367,4 +374,9 @@
<string name="veiled_alternate_text" msgid="7370933826442034211"></string>
<string name="veiled_alternate_text_unknown_person" msgid="5132515097905273819"></string>
<string name="veiled_summary_unknown_person" msgid="4030928895738205054"></string>
+ <string name="label_notification_ticker" msgid="1684732605316462621">"<xliff:g id="LABEL">%s</xliff:g>: <xliff:g id="NOTIFICATION">%s</xliff:g>"</string>
+ <string name="new_messages" msgid="4419173946074516420">"<xliff:g id="COUNT">%1$d</xliff:g> nieuwe berichten"</string>
+ <string name="single_new_message_notification_title" msgid="4138237430881084155">"<xliff:g id="SENDER">%1$s</xliff:g>: <xliff:g id="SUBJECT">%2$s</xliff:g>"</string>
+ <string name="silent_ringtone" msgid="5856834572357761687">"Stil"</string>
+ <string name="inbox_unseen_banner" msgid="4561582938706814621">"<xliff:g id="NUMBER">%d</xliff:g> NIEUW"</string>
</resources>
diff --git a/res/values-pl/strings.xml b/res/values-pl/strings.xml
index eaa4452..45efbaf 100644
--- a/res/values-pl/strings.xml
+++ b/res/values-pl/strings.xml
@@ -74,8 +74,13 @@
<string name="forward" msgid="6822914459902983767">"Przekaż dalej"</string>
<string name="menu_compose" msgid="6274193058224230645">"Utwórz"</string>
<string name="menu_change_folders" msgid="1542713666608888717">"Zmień foldery"</string>
+ <string name="menu_move_to" msgid="9138296669516358542">"Przenieś do"</string>
<string name="menu_manage_folders" msgid="6755623004628177492">"Ustawienia folderów"</string>
<string name="folder_list_title" msgid="4276644062440415214">"Foldery"</string>
+ <!-- no translation found for folder_list_more (537172187223133825) -->
+ <skip />
+ <!-- no translation found for folder_list_show_all_accounts (8054807182336991835) -->
+ <skip />
<string name="manage_folders_subtitle" msgid="7702199674083260433">"Synchronizuj i powiadamiaj"</string>
<string name="menu_folder_options" msgid="8897520487430647932">"Ustawienia folderu"</string>
<string name="menu_account_settings" msgid="8230989362863431918">"Ustawienia konta"</string>
@@ -218,6 +223,8 @@
<item quantity="one" msgid="4930161390461457462">"Zmieniono folder."</item>
<item quantity="other" msgid="8918589141287976985">"Zmieniono foldery."</item>
</plurals>
+ <!-- no translation found for conversation_folder_moved (297469098857964678) -->
+ <skip />
<string name="search_results_header" msgid="4669917471897026269">"Wyniki"</string>
<string name="search_unsupported" msgid="4654227193354052607">"Wyszukiwanie nie jest obsługiwane na tym koncie."</string>
<string name="searchMode" msgid="3329807422114758583">"Tryb wyszukiwania"</string>
@@ -365,4 +372,9 @@
<string name="veiled_alternate_text" msgid="7370933826442034211"></string>
<string name="veiled_alternate_text_unknown_person" msgid="5132515097905273819"></string>
<string name="veiled_summary_unknown_person" msgid="4030928895738205054"></string>
+ <string name="label_notification_ticker" msgid="1684732605316462621">"<xliff:g id="LABEL">%s</xliff:g>: <xliff:g id="NOTIFICATION">%s</xliff:g>"</string>
+ <string name="new_messages" msgid="4419173946074516420">"Nowe wiadomości: <xliff:g id="COUNT">%1$d</xliff:g>"</string>
+ <string name="single_new_message_notification_title" msgid="4138237430881084155">"<xliff:g id="SENDER">%1$s</xliff:g>: <xliff:g id="SUBJECT">%2$s</xliff:g>"</string>
+ <string name="silent_ringtone" msgid="5856834572357761687">"Cichy"</string>
+ <string name="inbox_unseen_banner" msgid="4561582938706814621">"NOWE: <xliff:g id="NUMBER">%d</xliff:g>"</string>
</resources>
diff --git a/res/values-pt-rPT/strings.xml b/res/values-pt-rPT/strings.xml
index 3fdb5f4..b33baa4 100644
--- a/res/values-pt-rPT/strings.xml
+++ b/res/values-pt-rPT/strings.xml
@@ -74,8 +74,13 @@
<string name="forward" msgid="6822914459902983767">"Encaminhar"</string>
<string name="menu_compose" msgid="6274193058224230645">"Compor"</string>
<string name="menu_change_folders" msgid="1542713666608888717">"Alterar pastas"</string>
+ <string name="menu_move_to" msgid="9138296669516358542">"Mover para"</string>
<string name="menu_manage_folders" msgid="6755623004628177492">"Definições da pasta"</string>
<string name="folder_list_title" msgid="4276644062440415214">"Pastas"</string>
+ <!-- no translation found for folder_list_more (537172187223133825) -->
+ <skip />
+ <!-- no translation found for folder_list_show_all_accounts (8054807182336991835) -->
+ <skip />
<string name="manage_folders_subtitle" msgid="7702199674083260433">"Sincronizar e notificar"</string>
<string name="menu_folder_options" msgid="8897520487430647932">"Definições da pasta"</string>
<string name="menu_account_settings" msgid="8230989362863431918">"Definições da conta"</string>
@@ -220,6 +225,8 @@
<item quantity="one" msgid="4930161390461457462">"Pasta alterada."</item>
<item quantity="other" msgid="8918589141287976985">"Pastas alteradas."</item>
</plurals>
+ <!-- no translation found for conversation_folder_moved (297469098857964678) -->
+ <skip />
<string name="search_results_header" msgid="4669917471897026269">"Resultados"</string>
<string name="search_unsupported" msgid="4654227193354052607">"A pesquisa não é suportada nesta conta."</string>
<string name="searchMode" msgid="3329807422114758583">"Modo de Pesquisa"</string>
@@ -367,4 +374,9 @@
<string name="veiled_alternate_text" msgid="7370933826442034211"></string>
<string name="veiled_alternate_text_unknown_person" msgid="5132515097905273819"></string>
<string name="veiled_summary_unknown_person" msgid="4030928895738205054"></string>
+ <string name="label_notification_ticker" msgid="1684732605316462621">"<xliff:g id="LABEL">%s</xliff:g>: <xliff:g id="NOTIFICATION">%s</xliff:g>"</string>
+ <string name="new_messages" msgid="4419173946074516420">"<xliff:g id="COUNT">%1$d</xliff:g> mensagens novas"</string>
+ <string name="single_new_message_notification_title" msgid="4138237430881084155">"<xliff:g id="SENDER">%1$s</xliff:g>: <xliff:g id="SUBJECT">%2$s</xliff:g>"</string>
+ <string name="silent_ringtone" msgid="5856834572357761687">"Silencioso"</string>
+ <string name="inbox_unseen_banner" msgid="4561582938706814621">"<xliff:g id="NUMBER">%d</xliff:g> NOVAS"</string>
</resources>
diff --git a/res/values-pt/strings.xml b/res/values-pt/strings.xml
index a3a4c4f..ee01f8d 100644
--- a/res/values-pt/strings.xml
+++ b/res/values-pt/strings.xml
@@ -74,8 +74,13 @@
<string name="forward" msgid="6822914459902983767">"Encaminhar"</string>
<string name="menu_compose" msgid="6274193058224230645">"Escrever"</string>
<string name="menu_change_folders" msgid="1542713666608888717">"Alterar pastas"</string>
+ <string name="menu_move_to" msgid="9138296669516358542">"Mover para"</string>
<string name="menu_manage_folders" msgid="6755623004628177492">"Configurações de pastas"</string>
<string name="folder_list_title" msgid="4276644062440415214">"Pastas"</string>
+ <!-- no translation found for folder_list_more (537172187223133825) -->
+ <skip />
+ <!-- no translation found for folder_list_show_all_accounts (8054807182336991835) -->
+ <skip />
<string name="manage_folders_subtitle" msgid="7702199674083260433">"Sincronizar e notificar"</string>
<string name="menu_folder_options" msgid="8897520487430647932">"Configurações da pasta"</string>
<string name="menu_account_settings" msgid="8230989362863431918">"Configurações da conta"</string>
@@ -220,6 +225,8 @@
<item quantity="one" msgid="4930161390461457462">"Pasta alterada."</item>
<item quantity="other" msgid="8918589141287976985">"Pastas alteradas."</item>
</plurals>
+ <!-- no translation found for conversation_folder_moved (297469098857964678) -->
+ <skip />
<string name="search_results_header" msgid="4669917471897026269">"Resultados"</string>
<string name="search_unsupported" msgid="4654227193354052607">"Não há suporte para pesquisa nesta conta."</string>
<string name="searchMode" msgid="3329807422114758583">"Modo de pesquisa"</string>
@@ -367,4 +374,9 @@
<string name="veiled_alternate_text" msgid="7370933826442034211"></string>
<string name="veiled_alternate_text_unknown_person" msgid="5132515097905273819"></string>
<string name="veiled_summary_unknown_person" msgid="4030928895738205054"></string>
+ <string name="label_notification_ticker" msgid="1684732605316462621">"<xliff:g id="LABEL">%s</xliff:g>: <xliff:g id="NOTIFICATION">%s</xliff:g>"</string>
+ <string name="new_messages" msgid="4419173946074516420">"<xliff:g id="COUNT">%1$d</xliff:g> novas mensagens"</string>
+ <string name="single_new_message_notification_title" msgid="4138237430881084155">"<xliff:g id="SENDER">%1$s</xliff:g>: <xliff:g id="SUBJECT">%2$s</xliff:g>"</string>
+ <string name="silent_ringtone" msgid="5856834572357761687">"Silencioso"</string>
+ <string name="inbox_unseen_banner" msgid="4561582938706814621">"<xliff:g id="NUMBER">%d</xliff:g> NOVAS"</string>
</resources>
diff --git a/res/values-rm/strings.xml b/res/values-rm/strings.xml
index 981ccbc..a47b6ce 100644
--- a/res/values-rm/strings.xml
+++ b/res/values-rm/strings.xml
@@ -123,10 +123,16 @@
<skip />
<!-- no translation found for menu_change_folders (1542713666608888717) -->
<skip />
+ <!-- no translation found for menu_move_to (9138296669516358542) -->
+ <skip />
<!-- no translation found for menu_manage_folders (6755623004628177492) -->
<skip />
<!-- no translation found for folder_list_title (4276644062440415214) -->
<skip />
+ <!-- no translation found for folder_list_more (537172187223133825) -->
+ <skip />
+ <!-- no translation found for folder_list_show_all_accounts (8054807182336991835) -->
+ <skip />
<!-- no translation found for manage_folders_subtitle (7702199674083260433) -->
<skip />
<!-- no translation found for menu_folder_options (8897520487430647932) -->
@@ -319,6 +325,8 @@
<skip />
<!-- no translation found for conversation_folder_changed:one (4930161390461457462) -->
<!-- no translation found for conversation_folder_changed:other (8918589141287976985) -->
+ <!-- no translation found for conversation_folder_moved (297469098857964678) -->
+ <skip />
<!-- no translation found for search_results_header (4669917471897026269) -->
<skip />
<!-- no translation found for search_unsupported (4654227193354052607) -->
@@ -570,4 +578,14 @@
<string name="veiled_alternate_text" msgid="7370933826442034211"></string>
<string name="veiled_alternate_text_unknown_person" msgid="5132515097905273819"></string>
<string name="veiled_summary_unknown_person" msgid="4030928895738205054"></string>
+ <!-- no translation found for label_notification_ticker (1684732605316462621) -->
+ <skip />
+ <!-- no translation found for new_messages (4419173946074516420) -->
+ <skip />
+ <!-- no translation found for single_new_message_notification_title (4138237430881084155) -->
+ <skip />
+ <!-- no translation found for silent_ringtone (5856834572357761687) -->
+ <skip />
+ <!-- no translation found for inbox_unseen_banner (4561582938706814621) -->
+ <skip />
</resources>
diff --git a/res/values-ro/strings.xml b/res/values-ro/strings.xml
index c609858..f537450 100644
--- a/res/values-ro/strings.xml
+++ b/res/values-ro/strings.xml
@@ -63,8 +63,8 @@
<string name="report_spam" msgid="6467567747975393907">"Raportaţi ca spam"</string>
<string name="mark_not_spam" msgid="694891665407228160">"Nu este spam"</string>
<string name="report_phishing" msgid="5714205737453138338">"Raportaţi phishing"</string>
- <string name="delete" msgid="844871204175957681">"Ştergeţi"</string>
- <string name="discard_drafts" msgid="6862272443470085375">"Ştergeţi mesajele nefinalizate"</string>
+ <string name="delete" msgid="844871204175957681">"Ștergeţi"</string>
+ <string name="discard_drafts" msgid="6862272443470085375">"Ștergeţi mesajele nefinalizate"</string>
<string name="next" msgid="4674401197968248302">"Mai vechi"</string>
<string name="previous" msgid="309943944831349924">"Mai noi"</string>
<string name="refresh" msgid="490989798005710951">"Actualizaţi"</string>
@@ -74,8 +74,13 @@
<string name="forward" msgid="6822914459902983767">"Redirecţionaţi"</string>
<string name="menu_compose" msgid="6274193058224230645">"Scrieţi"</string>
<string name="menu_change_folders" msgid="1542713666608888717">"Modificaţi dosarele"</string>
+ <string name="menu_move_to" msgid="9138296669516358542">"Mutați în"</string>
<string name="menu_manage_folders" msgid="6755623004628177492">"Setări pentru dosare"</string>
<string name="folder_list_title" msgid="4276644062440415214">"Dosare"</string>
+ <!-- no translation found for folder_list_more (537172187223133825) -->
+ <skip />
+ <!-- no translation found for folder_list_show_all_accounts (8054807182336991835) -->
+ <skip />
<string name="manage_folders_subtitle" msgid="7702199674083260433">"Sincronizare şi notificare"</string>
<string name="menu_folder_options" msgid="8897520487430647932">"Setări pentru dosar"</string>
<string name="menu_account_settings" msgid="8230989362863431918">"Setările contului"</string>
@@ -166,16 +171,16 @@
<string name="me" msgid="6480762904022198669">"eu"</string>
<string name="show_all_folders" msgid="3281420732307737553">"Afişaţi toate dosarele"</string>
<plurals name="confirm_delete_conversation">
- <item quantity="one" msgid="3731948757247905508">"Ştergeţi această conversaţie?"</item>
- <item quantity="other" msgid="930334208937121234">"Ştergeţi aceste <xliff:g id="COUNT">%1$d</xliff:g> (de) conversaţii?"</item>
+ <item quantity="one" msgid="3731948757247905508">"Ștergeţi această conversaţie?"</item>
+ <item quantity="other" msgid="930334208937121234">"Ștergeţi aceste <xliff:g id="COUNT">%1$d</xliff:g> (de) conversaţii?"</item>
</plurals>
<plurals name="confirm_archive_conversation">
<item quantity="one" msgid="2990537295519552069">"Arhivaţi această conversaţie?"</item>
<item quantity="other" msgid="4713469868399246772">"Arhivaţi aceste <xliff:g id="COUNT">%1$d</xliff:g> (de) conversaţii?"</item>
</plurals>
<plurals name="confirm_discard_drafts_conversation">
- <item quantity="one" msgid="5974090449454432874">"Ştergeţi mesajele nefinalizate din conversaţie?"</item>
- <item quantity="other" msgid="4173815457177336569">"Ştergeţi mesajele nefinalizate din <xliff:g id="COUNT">%1$d</xliff:g> conversaţii?"</item>
+ <item quantity="one" msgid="5974090449454432874">"Ștergeţi mesajele nefinalizate din conversaţie?"</item>
+ <item quantity="other" msgid="4173815457177336569">"Ștergeţi mesajele nefinalizate din <xliff:g id="COUNT">%1$d</xliff:g> conversaţii?"</item>
</plurals>
<string name="confirm_discard_text" msgid="1149834186404614612">"Renunţaţi la acest mesaj?"</string>
<string name="loading_conversations" msgid="2649440958602369555">"Se încarcă…"</string>
@@ -213,13 +218,15 @@
<item quantity="one" msgid="4398693029405479323">"<b><xliff:g id="COUNT">%1$d</xliff:g></b> a fost ştearsă."</item>
<item quantity="other" msgid="8630099095360065837">"<b><xliff:g id="COUNT">%1$d</xliff:g></b> au fost şterse."</item>
</plurals>
- <string name="deleted" msgid="2757349161107268029">"Ştearsă"</string>
+ <string name="deleted" msgid="2757349161107268029">"Ștearsă"</string>
<string name="archived" msgid="7533995360704366325">"Arhivată"</string>
<string name="folder_removed" msgid="1047474677580149436">"Eliminat din <xliff:g id="FOLDERNAME">%1$s</xliff:g>"</string>
<plurals name="conversation_folder_changed">
<item quantity="one" msgid="4930161390461457462">"Dosar modificat."</item>
<item quantity="other" msgid="8918589141287976985">"Dosare modificate."</item>
</plurals>
+ <!-- no translation found for conversation_folder_moved (297469098857964678) -->
+ <skip />
<string name="search_results_header" msgid="4669917471897026269">"Rezultate"</string>
<string name="search_unsupported" msgid="4654227193354052607">"Căutarea nu este acceptată pentru acest cont."</string>
<string name="searchMode" msgid="3329807422114758583">"Modul Căutare"</string>
@@ -367,4 +374,9 @@
<string name="veiled_alternate_text" msgid="7370933826442034211"></string>
<string name="veiled_alternate_text_unknown_person" msgid="5132515097905273819"></string>
<string name="veiled_summary_unknown_person" msgid="4030928895738205054"></string>
+ <string name="label_notification_ticker" msgid="1684732605316462621">"<xliff:g id="LABEL">%s</xliff:g>: <xliff:g id="NOTIFICATION">%s</xliff:g>"</string>
+ <string name="new_messages" msgid="4419173946074516420">"<xliff:g id="COUNT">%1$d</xliff:g> (de) mesaje noi"</string>
+ <string name="single_new_message_notification_title" msgid="4138237430881084155">"<xliff:g id="SENDER">%1$s</xliff:g>: <xliff:g id="SUBJECT">%2$s</xliff:g>"</string>
+ <string name="silent_ringtone" msgid="5856834572357761687">"Silențios"</string>
+ <string name="inbox_unseen_banner" msgid="4561582938706814621">"<xliff:g id="NUMBER">%d</xliff:g> NOI"</string>
</resources>
diff --git a/res/values-ru/strings.xml b/res/values-ru/strings.xml
index 0f6ce66..47726e7 100644
--- a/res/values-ru/strings.xml
+++ b/res/values-ru/strings.xml
@@ -74,8 +74,13 @@
<string name="forward" msgid="6822914459902983767">"Переслать"</string>
<string name="menu_compose" msgid="6274193058224230645">"Написать"</string>
<string name="menu_change_folders" msgid="1542713666608888717">"Изменить папки"</string>
+ <string name="menu_move_to" msgid="9138296669516358542">"Переместить в"</string>
<string name="menu_manage_folders" msgid="6755623004628177492">"Настройки папок"</string>
<string name="folder_list_title" msgid="4276644062440415214">"Папки"</string>
+ <!-- no translation found for folder_list_more (537172187223133825) -->
+ <skip />
+ <!-- no translation found for folder_list_show_all_accounts (8054807182336991835) -->
+ <skip />
<string name="manage_folders_subtitle" msgid="7702199674083260433">"Синхронизация и уведомления"</string>
<string name="menu_folder_options" msgid="8897520487430647932">"Настройки папки"</string>
<string name="menu_account_settings" msgid="8230989362863431918">"Настройки аккаунта"</string>
@@ -220,6 +225,8 @@
<item quantity="one" msgid="4930161390461457462">"Папка изменена."</item>
<item quantity="other" msgid="8918589141287976985">"Папки изменены."</item>
</plurals>
+ <!-- no translation found for conversation_folder_moved (297469098857964678) -->
+ <skip />
<string name="search_results_header" msgid="4669917471897026269">"Результаты"</string>
<string name="search_unsupported" msgid="4654227193354052607">"В этом аккаунте не поддерживается поиск."</string>
<string name="searchMode" msgid="3329807422114758583">"Режим поиска"</string>
@@ -367,4 +374,9 @@
<string name="veiled_alternate_text" msgid="7370933826442034211"></string>
<string name="veiled_alternate_text_unknown_person" msgid="5132515097905273819"></string>
<string name="veiled_summary_unknown_person" msgid="4030928895738205054"></string>
+ <string name="label_notification_ticker" msgid="1684732605316462621">"<xliff:g id="LABEL">%s</xliff:g>: <xliff:g id="NOTIFICATION">%s</xliff:g>"</string>
+ <string name="new_messages" msgid="4419173946074516420">"Новых сообщений: <xliff:g id="COUNT">%1$d</xliff:g>"</string>
+ <string name="single_new_message_notification_title" msgid="4138237430881084155">"<xliff:g id="SENDER">%1$s</xliff:g>: <xliff:g id="SUBJECT">%2$s</xliff:g>"</string>
+ <string name="silent_ringtone" msgid="5856834572357761687">"Без звука"</string>
+ <string name="inbox_unseen_banner" msgid="4561582938706814621">"НОВЫХ: <xliff:g id="NUMBER">%d</xliff:g>"</string>
</resources>
diff --git a/res/values-sk/strings.xml b/res/values-sk/strings.xml
index b798a24..534430b 100644
--- a/res/values-sk/strings.xml
+++ b/res/values-sk/strings.xml
@@ -74,8 +74,13 @@
<string name="forward" msgid="6822914459902983767">"Poslať ďalej"</string>
<string name="menu_compose" msgid="6274193058224230645">"Napísať správu"</string>
<string name="menu_change_folders" msgid="1542713666608888717">"Zmeniť priečinky"</string>
+ <string name="menu_move_to" msgid="9138296669516358542">"Presunúť do"</string>
<string name="menu_manage_folders" msgid="6755623004628177492">"Nastavenia priečinka"</string>
<string name="folder_list_title" msgid="4276644062440415214">"Priečinky"</string>
+ <!-- no translation found for folder_list_more (537172187223133825) -->
+ <skip />
+ <!-- no translation found for folder_list_show_all_accounts (8054807182336991835) -->
+ <skip />
<string name="manage_folders_subtitle" msgid="7702199674083260433">"Synchronizovať a upozorniť"</string>
<string name="menu_folder_options" msgid="8897520487430647932">"Nastavenia priečinka"</string>
<string name="menu_account_settings" msgid="8230989362863431918">"Nastavenia účtu"</string>
@@ -220,6 +225,8 @@
<item quantity="one" msgid="4930161390461457462">"Zmenený priečinok."</item>
<item quantity="other" msgid="8918589141287976985">"Zmenené priečinky."</item>
</plurals>
+ <!-- no translation found for conversation_folder_moved (297469098857964678) -->
+ <skip />
<string name="search_results_header" msgid="4669917471897026269">"Výsledky"</string>
<string name="search_unsupported" msgid="4654227193354052607">"Hľadanie nie je v tomto účte podporované."</string>
<string name="searchMode" msgid="3329807422114758583">"Režim vyhľadávania"</string>
@@ -367,4 +374,9 @@
<string name="veiled_alternate_text" msgid="7370933826442034211"></string>
<string name="veiled_alternate_text_unknown_person" msgid="5132515097905273819"></string>
<string name="veiled_summary_unknown_person" msgid="4030928895738205054"></string>
+ <string name="label_notification_ticker" msgid="1684732605316462621">"<xliff:g id="LABEL">%s</xliff:g>: <xliff:g id="NOTIFICATION">%s</xliff:g>"</string>
+ <string name="new_messages" msgid="4419173946074516420">"Počet nových správ: <xliff:g id="COUNT">%1$d</xliff:g>"</string>
+ <string name="single_new_message_notification_title" msgid="4138237430881084155">"<xliff:g id="SENDER">%1$s</xliff:g>: <xliff:g id="SUBJECT">%2$s</xliff:g>"</string>
+ <string name="silent_ringtone" msgid="5856834572357761687">"Tichý režim"</string>
+ <string name="inbox_unseen_banner" msgid="4561582938706814621">"NOVÉ: <xliff:g id="NUMBER">%d</xliff:g>"</string>
</resources>
diff --git a/res/values-sl/strings.xml b/res/values-sl/strings.xml
index 0f940dd..d406dc9 100644
--- a/res/values-sl/strings.xml
+++ b/res/values-sl/strings.xml
@@ -74,8 +74,13 @@
<string name="forward" msgid="6822914459902983767">"Posreduj"</string>
<string name="menu_compose" msgid="6274193058224230645">"Novo"</string>
<string name="menu_change_folders" msgid="1542713666608888717">"Spremenite mape"</string>
+ <string name="menu_move_to" msgid="9138296669516358542">"Premakni v"</string>
<string name="menu_manage_folders" msgid="6755623004628177492">"Nastavitve mape"</string>
<string name="folder_list_title" msgid="4276644062440415214">"Mape"</string>
+ <!-- no translation found for folder_list_more (537172187223133825) -->
+ <skip />
+ <!-- no translation found for folder_list_show_all_accounts (8054807182336991835) -->
+ <skip />
<string name="manage_folders_subtitle" msgid="7702199674083260433">"Sinhronizacija in obveščanje"</string>
<string name="menu_folder_options" msgid="8897520487430647932">"Nastavitve mape"</string>
<string name="menu_account_settings" msgid="8230989362863431918">"Nastavitve računa"</string>
@@ -220,6 +225,8 @@
<item quantity="one" msgid="4930161390461457462">"Spremenjena mapa."</item>
<item quantity="other" msgid="8918589141287976985">"Spremenjene mape."</item>
</plurals>
+ <!-- no translation found for conversation_folder_moved (297469098857964678) -->
+ <skip />
<string name="search_results_header" msgid="4669917471897026269">"Rezultati"</string>
<string name="search_unsupported" msgid="4654227193354052607">"Iskanje ni podprto za ta račun."</string>
<string name="searchMode" msgid="3329807422114758583">"Način iskanja"</string>
@@ -367,4 +374,9 @@
<string name="veiled_alternate_text" msgid="7370933826442034211"></string>
<string name="veiled_alternate_text_unknown_person" msgid="5132515097905273819"></string>
<string name="veiled_summary_unknown_person" msgid="4030928895738205054"></string>
+ <string name="label_notification_ticker" msgid="1684732605316462621">"<xliff:g id="LABEL">%s</xliff:g>: <xliff:g id="NOTIFICATION">%s</xliff:g>"</string>
+ <string name="new_messages" msgid="4419173946074516420">"Št. novih sporočil: <xliff:g id="COUNT">%1$d</xliff:g>"</string>
+ <string name="single_new_message_notification_title" msgid="4138237430881084155">"<xliff:g id="SENDER">%1$s</xliff:g>: <xliff:g id="SUBJECT">%2$s</xliff:g>"</string>
+ <string name="silent_ringtone" msgid="5856834572357761687">"Tiho"</string>
+ <string name="inbox_unseen_banner" msgid="4561582938706814621">"ŠT. NOVIH: <xliff:g id="NUMBER">%d</xliff:g>"</string>
</resources>
diff --git a/res/values-sr/strings.xml b/res/values-sr/strings.xml
index 5ec567f..77bb3f2 100644
--- a/res/values-sr/strings.xml
+++ b/res/values-sr/strings.xml
@@ -74,8 +74,13 @@
<string name="forward" msgid="6822914459902983767">"Проследи"</string>
<string name="menu_compose" msgid="6274193058224230645">"Нова порука"</string>
<string name="menu_change_folders" msgid="1542713666608888717">"Промени директоријуме"</string>
+ <string name="menu_move_to" msgid="9138296669516358542">"Премести у"</string>
<string name="menu_manage_folders" msgid="6755623004628177492">"Подешавања директоријума"</string>
<string name="folder_list_title" msgid="4276644062440415214">"Директоријуми"</string>
+ <!-- no translation found for folder_list_more (537172187223133825) -->
+ <skip />
+ <!-- no translation found for folder_list_show_all_accounts (8054807182336991835) -->
+ <skip />
<string name="manage_folders_subtitle" msgid="7702199674083260433">"Синхронизовање и обавештавање"</string>
<string name="menu_folder_options" msgid="8897520487430647932">"Подешавања директоријума"</string>
<string name="menu_account_settings" msgid="8230989362863431918">"Подешавања налога"</string>
@@ -220,6 +225,8 @@
<item quantity="one" msgid="4930161390461457462">"Директоријум је промењен."</item>
<item quantity="other" msgid="8918589141287976985">"Директоријуми су промењени."</item>
</plurals>
+ <!-- no translation found for conversation_folder_moved (297469098857964678) -->
+ <skip />
<string name="search_results_header" msgid="4669917471897026269">"Резултати"</string>
<string name="search_unsupported" msgid="4654227193354052607">"Претрага није подржана на овом налогу."</string>
<string name="searchMode" msgid="3329807422114758583">"Режим претраге"</string>
@@ -367,4 +374,9 @@
<string name="veiled_alternate_text" msgid="7370933826442034211"></string>
<string name="veiled_alternate_text_unknown_person" msgid="5132515097905273819"></string>
<string name="veiled_summary_unknown_person" msgid="4030928895738205054"></string>
+ <string name="label_notification_ticker" msgid="1684732605316462621">"<xliff:g id="LABEL">%s</xliff:g>: <xliff:g id="NOTIFICATION">%s</xliff:g>"</string>
+ <string name="new_messages" msgid="4419173946074516420">"Нових порука: <xliff:g id="COUNT">%1$d</xliff:g>"</string>
+ <string name="single_new_message_notification_title" msgid="4138237430881084155">"<xliff:g id="SENDER">%1$s</xliff:g>: <xliff:g id="SUBJECT">%2$s</xliff:g>"</string>
+ <string name="silent_ringtone" msgid="5856834572357761687">"Нечујно"</string>
+ <string name="inbox_unseen_banner" msgid="4561582938706814621">"НОВИХ: <xliff:g id="NUMBER">%d</xliff:g>"</string>
</resources>
diff --git a/res/values-sv/strings.xml b/res/values-sv/strings.xml
index 1687f8b..021a3e8 100644
--- a/res/values-sv/strings.xml
+++ b/res/values-sv/strings.xml
@@ -74,8 +74,13 @@
<string name="forward" msgid="6822914459902983767">"Vidarebefordra"</string>
<string name="menu_compose" msgid="6274193058224230645">"Skriv"</string>
<string name="menu_change_folders" msgid="1542713666608888717">"Byt mapp"</string>
+ <string name="menu_move_to" msgid="9138296669516358542">"Flytta till"</string>
<string name="menu_manage_folders" msgid="6755623004628177492">"Mappinställningar"</string>
<string name="folder_list_title" msgid="4276644062440415214">"Mappar"</string>
+ <!-- no translation found for folder_list_more (537172187223133825) -->
+ <skip />
+ <!-- no translation found for folder_list_show_all_accounts (8054807182336991835) -->
+ <skip />
<string name="manage_folders_subtitle" msgid="7702199674083260433">"Synka och meddela"</string>
<string name="menu_folder_options" msgid="8897520487430647932">"Mappinställningar"</string>
<string name="menu_account_settings" msgid="8230989362863431918">"Kontoinställningar"</string>
@@ -220,6 +225,8 @@
<item quantity="one" msgid="4930161390461457462">"Mappen har ändrats."</item>
<item quantity="other" msgid="8918589141287976985">"Mapparna har ändrats."</item>
</plurals>
+ <!-- no translation found for conversation_folder_moved (297469098857964678) -->
+ <skip />
<string name="search_results_header" msgid="4669917471897026269">"Resultat"</string>
<string name="search_unsupported" msgid="4654227193354052607">"Det går inte att söka i det här kontot."</string>
<string name="searchMode" msgid="3329807422114758583">"Sökläge"</string>
@@ -367,4 +374,9 @@
<string name="veiled_alternate_text" msgid="7370933826442034211"></string>
<string name="veiled_alternate_text_unknown_person" msgid="5132515097905273819"></string>
<string name="veiled_summary_unknown_person" msgid="4030928895738205054"></string>
+ <string name="label_notification_ticker" msgid="1684732605316462621">"<xliff:g id="LABEL">%s</xliff:g>: <xliff:g id="NOTIFICATION">%s</xliff:g>"</string>
+ <string name="new_messages" msgid="4419173946074516420">"<xliff:g id="COUNT">%1$d</xliff:g> nya meddelanden"</string>
+ <string name="single_new_message_notification_title" msgid="4138237430881084155">"<xliff:g id="SENDER">%1$s</xliff:g>: <xliff:g id="SUBJECT">%2$s</xliff:g>"</string>
+ <string name="silent_ringtone" msgid="5856834572357761687">"Tyst"</string>
+ <string name="inbox_unseen_banner" msgid="4561582938706814621">"<xliff:g id="NUMBER">%d</xliff:g> NYA"</string>
</resources>
diff --git a/res/values-sw/strings.xml b/res/values-sw/strings.xml
index 7c852c0..1ddf455 100644
--- a/res/values-sw/strings.xml
+++ b/res/values-sw/strings.xml
@@ -74,8 +74,13 @@
<string name="forward" msgid="6822914459902983767">"Sambaza"</string>
<string name="menu_compose" msgid="6274193058224230645">"Tunga"</string>
<string name="menu_change_folders" msgid="1542713666608888717">"Badilisha folda"</string>
+ <string name="menu_move_to" msgid="9138296669516358542">"Hamisha hadi"</string>
<string name="menu_manage_folders" msgid="6755623004628177492">"Mipangilio ya folda"</string>
<string name="folder_list_title" msgid="4276644062440415214">"Folda"</string>
+ <!-- no translation found for folder_list_more (537172187223133825) -->
+ <skip />
+ <!-- no translation found for folder_list_show_all_accounts (8054807182336991835) -->
+ <skip />
<string name="manage_folders_subtitle" msgid="7702199674083260433">"Sawazisha na uarifu"</string>
<string name="menu_folder_options" msgid="8897520487430647932">"Mipangilio ya folda"</string>
<string name="menu_account_settings" msgid="8230989362863431918">"Mipangilio ya akaunti"</string>
@@ -220,6 +225,8 @@
<item quantity="one" msgid="4930161390461457462">"Folda iliyobadilishwa."</item>
<item quantity="other" msgid="8918589141287976985">"Folda zilizobadilishwa."</item>
</plurals>
+ <!-- no translation found for conversation_folder_moved (297469098857964678) -->
+ <skip />
<string name="search_results_header" msgid="4669917471897026269">"Matokeo"</string>
<string name="search_unsupported" msgid="4654227193354052607">"Utafutaji hauauniwi kwenye akaunti hii."</string>
<string name="searchMode" msgid="3329807422114758583">"Hali ya Utafutaji"</string>
@@ -367,4 +374,9 @@
<string name="veiled_alternate_text" msgid="7370933826442034211"></string>
<string name="veiled_alternate_text_unknown_person" msgid="5132515097905273819"></string>
<string name="veiled_summary_unknown_person" msgid="4030928895738205054"></string>
+ <string name="label_notification_ticker" msgid="1684732605316462621">"<xliff:g id="LABEL">%s</xliff:g>: <xliff:g id="NOTIFICATION">%s</xliff:g>"</string>
+ <string name="new_messages" msgid="4419173946074516420">"Ujumbe <xliff:g id="COUNT">%1$d</xliff:g> mpya"</string>
+ <string name="single_new_message_notification_title" msgid="4138237430881084155">"<xliff:g id="SENDER">%1$s</xliff:g>: <xliff:g id="SUBJECT">%2$s</xliff:g>"</string>
+ <string name="silent_ringtone" msgid="5856834572357761687">"Kimya"</string>
+ <string name="inbox_unseen_banner" msgid="4561582938706814621">"<xliff:g id="NUMBER">%d</xliff:g> MPYA"</string>
</resources>
diff --git a/res/values-th/strings.xml b/res/values-th/strings.xml
index 8d7aab0..a638516 100644
--- a/res/values-th/strings.xml
+++ b/res/values-th/strings.xml
@@ -74,8 +74,13 @@
<string name="forward" msgid="6822914459902983767">"ส่งต่อ"</string>
<string name="menu_compose" msgid="6274193058224230645">"เขียน"</string>
<string name="menu_change_folders" msgid="1542713666608888717">"เปลี่ยนโฟลเดอร์"</string>
+ <string name="menu_move_to" msgid="9138296669516358542">"ย้ายไปที่"</string>
<string name="menu_manage_folders" msgid="6755623004628177492">"การตั้งค่าโฟลเดอร์"</string>
<string name="folder_list_title" msgid="4276644062440415214">"โฟลเดอร์"</string>
+ <!-- no translation found for folder_list_more (537172187223133825) -->
+ <skip />
+ <!-- no translation found for folder_list_show_all_accounts (8054807182336991835) -->
+ <skip />
<string name="manage_folders_subtitle" msgid="7702199674083260433">"ซิงค์และแจ้งเตือน"</string>
<string name="menu_folder_options" msgid="8897520487430647932">"การตั้งค่าโฟลเดอร์"</string>
<string name="menu_account_settings" msgid="8230989362863431918">"การตั้งค่าบัญชี"</string>
@@ -220,6 +225,8 @@
<item quantity="one" msgid="4930161390461457462">"เปลี่ยนโฟลเดอร์แล้ว"</item>
<item quantity="other" msgid="8918589141287976985">"เปลี่ยนโฟลเดอร์แล้ว"</item>
</plurals>
+ <!-- no translation found for conversation_folder_moved (297469098857964678) -->
+ <skip />
<string name="search_results_header" msgid="4669917471897026269">"ผลการค้นหา"</string>
<string name="search_unsupported" msgid="4654227193354052607">"บัญชีนี้ไม่สนับสนุนการค้นหา"</string>
<string name="searchMode" msgid="3329807422114758583">"โหมดการค้นหา"</string>
@@ -367,4 +374,9 @@
<string name="veiled_alternate_text" msgid="7370933826442034211"></string>
<string name="veiled_alternate_text_unknown_person" msgid="5132515097905273819"></string>
<string name="veiled_summary_unknown_person" msgid="4030928895738205054"></string>
+ <string name="label_notification_ticker" msgid="1684732605316462621">"<xliff:g id="LABEL">%s</xliff:g>: <xliff:g id="NOTIFICATION">%s</xliff:g>"</string>
+ <string name="new_messages" msgid="4419173946074516420">"<xliff:g id="COUNT">%1$d</xliff:g> ข้อความใหม่"</string>
+ <string name="single_new_message_notification_title" msgid="4138237430881084155">"<xliff:g id="SENDER">%1$s</xliff:g>: <xliff:g id="SUBJECT">%2$s</xliff:g>"</string>
+ <string name="silent_ringtone" msgid="5856834572357761687">"เงียบ"</string>
+ <string name="inbox_unseen_banner" msgid="4561582938706814621">"<xliff:g id="NUMBER">%d</xliff:g> ข้อความใหม่"</string>
</resources>
diff --git a/res/values-tl/strings.xml b/res/values-tl/strings.xml
index f2ae5ec..5c00f6f 100644
--- a/res/values-tl/strings.xml
+++ b/res/values-tl/strings.xml
@@ -74,8 +74,13 @@
<string name="forward" msgid="6822914459902983767">"Ipasa"</string>
<string name="menu_compose" msgid="6274193058224230645">"Bumuo"</string>
<string name="menu_change_folders" msgid="1542713666608888717">"Baguhin ang mga folder"</string>
+ <string name="menu_move_to" msgid="9138296669516358542">"Ilipat sa"</string>
<string name="menu_manage_folders" msgid="6755623004628177492">"Mga setting ng folder"</string>
<string name="folder_list_title" msgid="4276644062440415214">"Mga Folder"</string>
+ <!-- no translation found for folder_list_more (537172187223133825) -->
+ <skip />
+ <!-- no translation found for folder_list_show_all_accounts (8054807182336991835) -->
+ <skip />
<string name="manage_folders_subtitle" msgid="7702199674083260433">"I-sync at i-notify"</string>
<string name="menu_folder_options" msgid="8897520487430647932">"Mga setting ng folder"</string>
<string name="menu_account_settings" msgid="8230989362863431918">"Mga setting ng account"</string>
@@ -220,6 +225,8 @@
<item quantity="one" msgid="4930161390461457462">"Binagong folder."</item>
<item quantity="other" msgid="8918589141287976985">"Mga binagong folder."</item>
</plurals>
+ <!-- no translation found for conversation_folder_moved (297469098857964678) -->
+ <skip />
<string name="search_results_header" msgid="4669917471897026269">"Mga resulta"</string>
<string name="search_unsupported" msgid="4654227193354052607">"Hindi sinusuportahan ang paghahanap sa account na ito."</string>
<string name="searchMode" msgid="3329807422114758583">"Mode ng Paghahanap"</string>
@@ -367,4 +374,9 @@
<string name="veiled_alternate_text" msgid="7370933826442034211"></string>
<string name="veiled_alternate_text_unknown_person" msgid="5132515097905273819"></string>
<string name="veiled_summary_unknown_person" msgid="4030928895738205054"></string>
+ <string name="label_notification_ticker" msgid="1684732605316462621">"<xliff:g id="LABEL">%s</xliff:g>: <xliff:g id="NOTIFICATION">%s</xliff:g>"</string>
+ <string name="new_messages" msgid="4419173946074516420">"<xliff:g id="COUNT">%1$d</xliff:g> (na) bagong mensahe"</string>
+ <string name="single_new_message_notification_title" msgid="4138237430881084155">"<xliff:g id="SENDER">%1$s</xliff:g>: <xliff:g id="SUBJECT">%2$s</xliff:g>"</string>
+ <string name="silent_ringtone" msgid="5856834572357761687">"Naka-silent"</string>
+ <string name="inbox_unseen_banner" msgid="4561582938706814621">"<xliff:g id="NUMBER">%d</xliff:g> (na) BAGO"</string>
</resources>
diff --git a/res/values-tr/strings.xml b/res/values-tr/strings.xml
index b8a6c79..27233f2 100644
--- a/res/values-tr/strings.xml
+++ b/res/values-tr/strings.xml
@@ -74,8 +74,13 @@
<string name="forward" msgid="6822914459902983767">"Yönlendir"</string>
<string name="menu_compose" msgid="6274193058224230645">"Oluştur"</string>
<string name="menu_change_folders" msgid="1542713666608888717">"Klasörleri değiştir"</string>
+ <string name="menu_move_to" msgid="9138296669516358542">"Taşı"</string>
<string name="menu_manage_folders" msgid="6755623004628177492">"Klasör ayarları"</string>
<string name="folder_list_title" msgid="4276644062440415214">"Klasörler"</string>
+ <!-- no translation found for folder_list_more (537172187223133825) -->
+ <skip />
+ <!-- no translation found for folder_list_show_all_accounts (8054807182336991835) -->
+ <skip />
<string name="manage_folders_subtitle" msgid="7702199674083260433">"Senkronize et ve bildir"</string>
<string name="menu_folder_options" msgid="8897520487430647932">"Klasör ayarları"</string>
<string name="menu_account_settings" msgid="8230989362863431918">"Hesap ayarları"</string>
@@ -220,6 +225,8 @@
<item quantity="one" msgid="4930161390461457462">"Klasör değişti."</item>
<item quantity="other" msgid="8918589141287976985">"Klasörler değişti."</item>
</plurals>
+ <!-- no translation found for conversation_folder_moved (297469098857964678) -->
+ <skip />
<string name="search_results_header" msgid="4669917471897026269">"Sonuçlar"</string>
<string name="search_unsupported" msgid="4654227193354052607">"Bu hesapta arama desteklenmiyor"</string>
<string name="searchMode" msgid="3329807422114758583">"Arama Modu"</string>
@@ -367,4 +374,9 @@
<string name="veiled_alternate_text" msgid="7370933826442034211"></string>
<string name="veiled_alternate_text_unknown_person" msgid="5132515097905273819"></string>
<string name="veiled_summary_unknown_person" msgid="4030928895738205054"></string>
+ <string name="label_notification_ticker" msgid="1684732605316462621">"<xliff:g id="LABEL">%s</xliff:g>: <xliff:g id="NOTIFICATION">%s</xliff:g>"</string>
+ <string name="new_messages" msgid="4419173946074516420">"<xliff:g id="COUNT">%1$d</xliff:g> yeni ileti"</string>
+ <string name="single_new_message_notification_title" msgid="4138237430881084155">"<xliff:g id="SENDER">%1$s</xliff:g>: <xliff:g id="SUBJECT">%2$s</xliff:g>"</string>
+ <string name="silent_ringtone" msgid="5856834572357761687">"Sessiz"</string>
+ <string name="inbox_unseen_banner" msgid="4561582938706814621">"<xliff:g id="NUMBER">%d</xliff:g> YENİ"</string>
</resources>
diff --git a/res/values-uk/strings.xml b/res/values-uk/strings.xml
index 67cc4e5..7986e2e 100644
--- a/res/values-uk/strings.xml
+++ b/res/values-uk/strings.xml
@@ -74,8 +74,13 @@
<string name="forward" msgid="6822914459902983767">"Переслати"</string>
<string name="menu_compose" msgid="6274193058224230645">"Написати"</string>
<string name="menu_change_folders" msgid="1542713666608888717">"Змінити папки"</string>
+ <string name="menu_move_to" msgid="9138296669516358542">"Перемістити в"</string>
<string name="menu_manage_folders" msgid="6755623004628177492">"Налаштування папки"</string>
<string name="folder_list_title" msgid="4276644062440415214">"Папки"</string>
+ <!-- no translation found for folder_list_more (537172187223133825) -->
+ <skip />
+ <!-- no translation found for folder_list_show_all_accounts (8054807182336991835) -->
+ <skip />
<string name="manage_folders_subtitle" msgid="7702199674083260433">"Синхронізація та сповіщення"</string>
<string name="menu_folder_options" msgid="8897520487430647932">"Налаштування папки"</string>
<string name="menu_account_settings" msgid="8230989362863431918">"Налаштування облікового запису"</string>
@@ -220,6 +225,8 @@
<item quantity="one" msgid="4930161390461457462">"Змінено папку."</item>
<item quantity="other" msgid="8918589141287976985">"Змінено папки."</item>
</plurals>
+ <!-- no translation found for conversation_folder_moved (297469098857964678) -->
+ <skip />
<string name="search_results_header" msgid="4669917471897026269">"Результати"</string>
<string name="search_unsupported" msgid="4654227193354052607">"Функція пошуку в цьому обліковому записі не підтримується."</string>
<string name="searchMode" msgid="3329807422114758583">"Режим пошуку"</string>
@@ -367,4 +374,9 @@
<string name="veiled_alternate_text" msgid="7370933826442034211"></string>
<string name="veiled_alternate_text_unknown_person" msgid="5132515097905273819"></string>
<string name="veiled_summary_unknown_person" msgid="4030928895738205054"></string>
+ <string name="label_notification_ticker" msgid="1684732605316462621">"<xliff:g id="LABEL">%s</xliff:g>: <xliff:g id="NOTIFICATION">%s</xliff:g>"</string>
+ <string name="new_messages" msgid="4419173946074516420">"Нових повідомлень: <xliff:g id="COUNT">%1$d</xliff:g>"</string>
+ <string name="single_new_message_notification_title" msgid="4138237430881084155">"<xliff:g id="SENDER">%1$s</xliff:g>: <xliff:g id="SUBJECT">%2$s</xliff:g>"</string>
+ <string name="silent_ringtone" msgid="5856834572357761687">"Без звуку"</string>
+ <string name="inbox_unseen_banner" msgid="4561582938706814621">"НОВИХ: <xliff:g id="NUMBER">%d</xliff:g>"</string>
</resources>
diff --git a/res/values-vi/strings.xml b/res/values-vi/strings.xml
index f8a95e4..6aa6ac7 100644
--- a/res/values-vi/strings.xml
+++ b/res/values-vi/strings.xml
@@ -74,8 +74,13 @@
<string name="forward" msgid="6822914459902983767">"Chuyển tiếp"</string>
<string name="menu_compose" msgid="6274193058224230645">"Soạn thư"</string>
<string name="menu_change_folders" msgid="1542713666608888717">"Thay đổi thư mục"</string>
+ <string name="menu_move_to" msgid="9138296669516358542">"Di chuyển tới"</string>
<string name="menu_manage_folders" msgid="6755623004628177492">"Cài đặt thư mục"</string>
<string name="folder_list_title" msgid="4276644062440415214">"Thư mục"</string>
+ <!-- no translation found for folder_list_more (537172187223133825) -->
+ <skip />
+ <!-- no translation found for folder_list_show_all_accounts (8054807182336991835) -->
+ <skip />
<string name="manage_folders_subtitle" msgid="7702199674083260433">"Đồng bộ hóa và thông báo"</string>
<string name="menu_folder_options" msgid="8897520487430647932">"Cài đặt thư mục"</string>
<string name="menu_account_settings" msgid="8230989362863431918">"Cài đặt tài khoản"</string>
@@ -220,6 +225,8 @@
<item quantity="one" msgid="4930161390461457462">"Đã thay đổi thư mục."</item>
<item quantity="other" msgid="8918589141287976985">"Đã thay đổi thư mục."</item>
</plurals>
+ <!-- no translation found for conversation_folder_moved (297469098857964678) -->
+ <skip />
<string name="search_results_header" msgid="4669917471897026269">"Kết quả"</string>
<string name="search_unsupported" msgid="4654227193354052607">"Không hỗ trợ tính năng tìm kiếm trên tài khoản này."</string>
<string name="searchMode" msgid="3329807422114758583">"Chế độ tìm kiếm"</string>
@@ -367,4 +374,9 @@
<string name="veiled_alternate_text" msgid="7370933826442034211"></string>
<string name="veiled_alternate_text_unknown_person" msgid="5132515097905273819"></string>
<string name="veiled_summary_unknown_person" msgid="4030928895738205054"></string>
+ <string name="label_notification_ticker" msgid="1684732605316462621">"<xliff:g id="LABEL">%s</xliff:g>: <xliff:g id="NOTIFICATION">%s</xliff:g>"</string>
+ <string name="new_messages" msgid="4419173946074516420">"<xliff:g id="COUNT">%1$d</xliff:g> tin nhắn mới"</string>
+ <string name="single_new_message_notification_title" msgid="4138237430881084155">"<xliff:g id="SENDER">%1$s</xliff:g>: <xliff:g id="SUBJECT">%2$s</xliff:g>"</string>
+ <string name="silent_ringtone" msgid="5856834572357761687">"Im lặng"</string>
+ <string name="inbox_unseen_banner" msgid="4561582938706814621">"<xliff:g id="NUMBER">%d</xliff:g> MỚI"</string>
</resources>
diff --git a/res/values-zh-rCN/strings.xml b/res/values-zh-rCN/strings.xml
index 9711d1e..a3ffadb 100644
--- a/res/values-zh-rCN/strings.xml
+++ b/res/values-zh-rCN/strings.xml
@@ -74,8 +74,13 @@
<string name="forward" msgid="6822914459902983767">"转发"</string>
<string name="menu_compose" msgid="6274193058224230645">"写邮件"</string>
<string name="menu_change_folders" msgid="1542713666608888717">"更改文件夹"</string>
+ <string name="menu_move_to" msgid="9138296669516358542">"移至"</string>
<string name="menu_manage_folders" msgid="6755623004628177492">"文件夹设置"</string>
<string name="folder_list_title" msgid="4276644062440415214">"文件夹"</string>
+ <!-- no translation found for folder_list_more (537172187223133825) -->
+ <skip />
+ <!-- no translation found for folder_list_show_all_accounts (8054807182336991835) -->
+ <skip />
<string name="manage_folders_subtitle" msgid="7702199674083260433">"同步与通知"</string>
<string name="menu_folder_options" msgid="8897520487430647932">"文件夹设置"</string>
<string name="menu_account_settings" msgid="8230989362863431918">"帐户设置"</string>
@@ -220,6 +225,8 @@
<item quantity="one" msgid="4930161390461457462">"已更改文件夹。"</item>
<item quantity="other" msgid="8918589141287976985">"已更改文件夹。"</item>
</plurals>
+ <!-- no translation found for conversation_folder_moved (297469098857964678) -->
+ <skip />
<string name="search_results_header" msgid="4669917471897026269">"结果"</string>
<string name="search_unsupported" msgid="4654227193354052607">"此帐户不支持搜索。"</string>
<string name="searchMode" msgid="3329807422114758583">"搜索模式"</string>
@@ -367,4 +374,9 @@
<string name="veiled_alternate_text" msgid="7370933826442034211"></string>
<string name="veiled_alternate_text_unknown_person" msgid="5132515097905273819"></string>
<string name="veiled_summary_unknown_person" msgid="4030928895738205054"></string>
+ <string name="label_notification_ticker" msgid="1684732605316462621">"<xliff:g id="LABEL">%s</xliff:g>:<xliff:g id="NOTIFICATION">%s</xliff:g>"</string>
+ <string name="new_messages" msgid="4419173946074516420">"<xliff:g id="COUNT">%1$d</xliff:g>封新邮件"</string>
+ <string name="single_new_message_notification_title" msgid="4138237430881084155">"<xliff:g id="SENDER">%1$s</xliff:g>:<xliff:g id="SUBJECT">%2$s</xliff:g>"</string>
+ <string name="silent_ringtone" msgid="5856834572357761687">"静音"</string>
+ <string name="inbox_unseen_banner" msgid="4561582938706814621">"<xliff:g id="NUMBER">%d</xliff:g>封新邮件"</string>
</resources>
diff --git a/res/values-zh-rTW/strings.xml b/res/values-zh-rTW/strings.xml
index 765be73..ee35ca4 100644
--- a/res/values-zh-rTW/strings.xml
+++ b/res/values-zh-rTW/strings.xml
@@ -74,8 +74,13 @@
<string name="forward" msgid="6822914459902983767">"轉寄"</string>
<string name="menu_compose" msgid="6274193058224230645">"撰寫"</string>
<string name="menu_change_folders" msgid="1542713666608888717">"變更資料夾"</string>
+ <string name="menu_move_to" msgid="9138296669516358542">"移至"</string>
<string name="menu_manage_folders" msgid="6755623004628177492">"資料夾設定"</string>
<string name="folder_list_title" msgid="4276644062440415214">"資料夾"</string>
+ <!-- no translation found for folder_list_more (537172187223133825) -->
+ <skip />
+ <!-- no translation found for folder_list_show_all_accounts (8054807182336991835) -->
+ <skip />
<string name="manage_folders_subtitle" msgid="7702199674083260433">"同步處理並通知"</string>
<string name="menu_folder_options" msgid="8897520487430647932">"資料夾設定"</string>
<string name="menu_account_settings" msgid="8230989362863431918">"帳戶設定"</string>
@@ -220,6 +225,8 @@
<item quantity="one" msgid="4930161390461457462">"已變更資料夾。"</item>
<item quantity="other" msgid="8918589141287976985">"已變更資料夾。"</item>
</plurals>
+ <!-- no translation found for conversation_folder_moved (297469098857964678) -->
+ <skip />
<string name="search_results_header" msgid="4669917471897026269">"搜尋結果"</string>
<string name="search_unsupported" msgid="4654227193354052607">"這個帳戶不支援搜尋功能。"</string>
<string name="searchMode" msgid="3329807422114758583">"搜尋模式"</string>
@@ -367,4 +374,9 @@
<string name="veiled_alternate_text" msgid="7370933826442034211"></string>
<string name="veiled_alternate_text_unknown_person" msgid="5132515097905273819"></string>
<string name="veiled_summary_unknown_person" msgid="4030928895738205054"></string>
+ <string name="label_notification_ticker" msgid="1684732605316462621">"<xliff:g id="LABEL">%s</xliff:g>:<xliff:g id="NOTIFICATION">%s</xliff:g>"</string>
+ <string name="new_messages" msgid="4419173946074516420">"<xliff:g id="COUNT">%1$d</xliff:g> 封新郵件"</string>
+ <string name="single_new_message_notification_title" msgid="4138237430881084155">"<xliff:g id="SENDER">%1$s</xliff:g>:<xliff:g id="SUBJECT">%2$s</xliff:g>"</string>
+ <string name="silent_ringtone" msgid="5856834572357761687">"靜音"</string>
+ <string name="inbox_unseen_banner" msgid="4561582938706814621">"<xliff:g id="NUMBER">%d</xliff:g> 封新郵件"</string>
</resources>
diff --git a/res/values-zu/strings.xml b/res/values-zu/strings.xml
index 044a4eb..ed52d66 100644
--- a/res/values-zu/strings.xml
+++ b/res/values-zu/strings.xml
@@ -74,8 +74,13 @@
<string name="forward" msgid="6822914459902983767">"Dlulisela"</string>
<string name="menu_compose" msgid="6274193058224230645">"Bhala"</string>
<string name="menu_change_folders" msgid="1542713666608888717">"Shintsha amafolda"</string>
+ <string name="menu_move_to" msgid="9138296669516358542">"Hambisa ku-"</string>
<string name="menu_manage_folders" msgid="6755623004628177492">"Izilungiselelo zefolda"</string>
<string name="folder_list_title" msgid="4276644062440415214">"Amafolda"</string>
+ <!-- no translation found for folder_list_more (537172187223133825) -->
+ <skip />
+ <!-- no translation found for folder_list_show_all_accounts (8054807182336991835) -->
+ <skip />
<string name="manage_folders_subtitle" msgid="7702199674083260433">"Ukuvumelanisa & ukuqaphelisa"</string>
<string name="menu_folder_options" msgid="8897520487430647932">"Izilungiselelo zefolda"</string>
<string name="menu_account_settings" msgid="8230989362863431918">"Izilungiselelo ze-akhawunti"</string>
@@ -220,6 +225,8 @@
<item quantity="one" msgid="4930161390461457462">"Ifolda eshintshiwe."</item>
<item quantity="other" msgid="8918589141287976985">"Amafolda ashintshiwe."</item>
</plurals>
+ <!-- no translation found for conversation_folder_moved (297469098857964678) -->
+ <skip />
<string name="search_results_header" msgid="4669917471897026269">"Imiphumela"</string>
<string name="search_unsupported" msgid="4654227193354052607">"Usesho alusekelwe kule akhawunti."</string>
<string name="searchMode" msgid="3329807422114758583">"Imodi yokusesha"</string>
@@ -367,4 +374,9 @@
<string name="veiled_alternate_text" msgid="7370933826442034211"></string>
<string name="veiled_alternate_text_unknown_person" msgid="5132515097905273819"></string>
<string name="veiled_summary_unknown_person" msgid="4030928895738205054"></string>
+ <string name="label_notification_ticker" msgid="1684732605316462621">"<xliff:g id="LABEL">%s</xliff:g><xliff:g id="NOTIFICATION">%s</xliff:g>"</string>
+ <string name="new_messages" msgid="4419173946074516420">"<xliff:g id="COUNT">%1$d</xliff:g> imilayezo emisha"</string>
+ <string name="single_new_message_notification_title" msgid="4138237430881084155">"<xliff:g id="SENDER">%1$s</xliff:g>: <xliff:g id="SUBJECT">%2$s</xliff:g>"</string>
+ <string name="silent_ringtone" msgid="5856834572357761687">"Thulile"</string>
+ <string name="inbox_unseen_banner" msgid="4561582938706814621">"<xliff:g id="NUMBER">%d</xliff:g> OKUSHA"</string>
</resources>
diff --git a/res/values/accountprovider.xml b/res/values/accountprovider.xml
new file mode 100644
index 0000000..742a7e5
--- /dev/null
+++ b/res/values/accountprovider.xml
@@ -0,0 +1,25 @@
+<?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.
+-->
+<resources>
+ <!-- List of content provider uris for -->
+ <string-array name="account_providers" translatable="false">
+ <!-- mock account list -->
+ <item>content://com.android.mail.mockprovider/accounts</item>
+ </string-array>
+
+</resources>
\ No newline at end of file
diff --git a/res/values/constants.xml b/res/values/constants.xml
index 5cd820f..84bfa4f 100644
--- a/res/values/constants.xml
+++ b/res/values/constants.xml
@@ -114,4 +114,5 @@
<!-- Whether to show the priority indicator inline with the senders in conversation list view -->
<bool name="inline_personal_level">true</bool>
+ <integer name="swipe_senders_length">25</integer>
</resources>
diff --git a/res/values/ids.xml b/res/values/ids.xml
index 84042ce..0039aab 100644
--- a/res/values/ids.xml
+++ b/res/values/ids.xml
@@ -21,4 +21,5 @@
<item type="id" name="reply_state" />
<item type="id" name="manage_folders_item"/>
<item type="id" name="contact_image" />
+ <item type="id" name="move_folder" />
</resources>
diff --git a/res/values/strings.xml b/res/values/strings.xml
index c939677..dee3df6 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -67,7 +67,7 @@
<item>Forward</item>
</string-array>
<!-- Formatting string for the subject when it contains a reply or forward identifier. Do not translate.-->
- <string name="formatted_subject"><xliff:g id="prefix">%1$s</xliff:g> <xliff:g id="subject">%2$s</xliff:g></string>
+ <string name="formatted_subject" translatable="false"><xliff:g id="prefix">%1$s</xliff:g> <xliff:g id="subject">%2$s</xliff:g></string>
<!-- Compose screen, prefixed to the subject of a message when replying to it (if not already present). Do not translate. -->
<string name="reply_subject_label" translatable="false">Re:</string>
<!-- Compose screen, Prefix to forwarded message subject. Do not translate. -->
@@ -149,10 +149,28 @@
<string name="menu_compose">Compose</string>
<!-- Menu item: change the folders for this conversation. -->
<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: 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 -->
+ <string name="report_rendering_problem" translatable="false">Looks bad</string>
+ <!-- Menu item: report an email's readability has improved -->
+ <string name="report_rendering_improvement" translatable="false">Looks good</string>
+ <!-- Temporary text used for reporting rendering issues Googlers see in testing -->
+ <string name="report_rendering_problem_desc" translatable="false">
+ This message looks bad.
+ </string>
+ <!-- Temporary text used for reporting rendering improvements Googlers see in testing -->
+ <string name="report_rendering_improvement_desc" translatable="false">
+ This message looks good.
+ </string>
<!-- Title for the Folder list screen. [CHAR LIMIT = 30] -->
<string name="folder_list_title">Folders</string>
+ <!-- Folder list item: show more folders. [CHAR LIMIT = 30] -->
+ <string name="folder_list_more">More</string>
+ <!-- Folder list item: show all accounts. [CHAR LIMIT = 30] -->
+ <string name="folder_list_show_all_accounts">More accounts</string>
<!-- action bar sub title for manage folder mode. [CHAR LIMIT = 30] -->
<string name="manage_folders_subtitle">Sync & notify</string>
<!-- Menu item: options for this folder. When source text cannot be translated within the char limit, please translate the shorter "Folder options" instead. [CHAR LIMIT = 30] -->
@@ -438,6 +456,9 @@
<item quantity="other">Changed folders.</item>
</plurals>
+ <!-- Displayed after moving a conversation to a different folder. [CHAR LIMIT=100] -->
+ <string name="conversation_folder_moved">Moved to <xliff:g id="folderName">%1$s</xliff:g></string>
+
<!-- Search Results: Text for header that is shown above search results [CHAR LIMIT=30] -->
<string name="search_results_header">Results</string>
<!-- Toast shown when the user taps the search hard key when viewing an account that does not support search [CHAR LIMIT=100] -->
@@ -787,11 +808,26 @@
<string name="notification_action_preference_summary_not_set">Not set</string>
<!-- Regex that specifies veiled addresses. These are all empty because this is disabled currently. -->
- <string name="veiled_address"></string>
+ <string name="veiled_address"/>
<!-- String to be shown instead of a veiled addresses. [CHAR LIMIT=50] -->
- <string name="veiled_alternate_text"></string>
+ <string name="veiled_alternate_text"/>
<!-- String to be shown instead of a veiled addresses. [CHAR LIMIT=50] -->
- <string name="veiled_alternate_text_unknown_person"></string>
+ <string name="veiled_alternate_text_unknown_person"/>
<!-- Summary string to be shown instead of a veiled recipient. [CHAR LIMIT=50] -->
- <string name="veiled_summary_unknown_person"></string>
+ <string name="veiled_summary_unknown_person"/>
+
+ <!-- Notification ticker text for per-label notification [CHAR LIMIT=30]-->
+ <string name="label_notification_ticker">"<xliff:g id="label">%s</xliff:g>: <xliff:g id="notification">%s</xliff:g>"</string>
+
+ <!-- Notification message to the user upon new messages for a conversation. [CHAR LIMIT=120] -->
+ <string name="new_messages"><xliff:g id="count">%1$d</xliff:g> new messages</string>
+
+ <!-- Format string used when displaying the title of a notification that was triggered by a single new conversation. [CHAR LIMIT=120] -->
+ <string name="single_new_message_notification_title"><xliff:g id="sender">%1$s</xliff:g>: <xliff:g id="subject">%2$s</xliff:g></string>
+
+ <!-- Settings screen, what to display for Ringtone when the user chooses "silent" [CHAR LIMIT=100]-->
+ <string name="silent_ringtone">Silent</string>
+
+ <!-- Label list screen, banner for number of unseen messages [CHAR LIMIT=15] -->
+ <string name="inbox_unseen_banner"><xliff:g id="number" example="7">%d</xliff:g> NEW</string>
</resources>
diff --git a/res/values/styles.xml b/res/values/styles.xml
index ab7e47a..054d3be 100644
--- a/res/values/styles.xml
+++ b/res/values/styles.xml
@@ -179,6 +179,23 @@
<item name="android:gravity">center_vertical|right</item>
</style>
+ <style name="UnseenCount">
+ <item name="android:layout_width">wrap_content</item>
+ <item name="android:layout_height">wrap_content</item>
+ <item name="android:layout_gravity">center_vertical</item>
+ <item name="android:layout_centerVertical">true</item>
+ <item name="android:layout_marginLeft">8dip</item>
+ <item name="android:singleLine">true</item>
+ <item name="android:includeFontPadding">false</item>
+ <item name="android:textSize">13sp</item>
+ <item name="android:textColor">@android:color/white</item>
+ <item name="android:textStyle">bold</item>
+ <item name="android:paddingLeft">8dp</item>
+ <item name="android:paddingRight">10dp</item>
+ <item name="android:paddingTop">4dp</item>
+ <item name="android:paddingBottom">4dp</item>
+ </style>
+
<!-- No change in the default case. -->
<style name="AccountSpinnerAnchorTextPrimary" parent="@android:style/TextAppearance.Holo.Widget.ActionBar.Title">
</style>
diff --git a/src/com/android/mail/EmailAddress.java b/src/com/android/mail/EmailAddress.java
new file mode 100644
index 0000000..1218da9
--- /dev/null
+++ b/src/com/android/mail/EmailAddress.java
@@ -0,0 +1,87 @@
+/*
+ * 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;
+
+import android.text.Html;
+import android.text.util.Rfc822Token;
+import android.text.util.Rfc822Tokenizer;
+
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * Class representing a single email address. Thread-safe, immutable value class suitable for
+ * caching.
+ * TODO(pwestbro): move to provider
+ */
+public class EmailAddress {
+
+ private final String mName;
+
+ private final String mAddress;
+
+ private static final Matcher sEmailMatcher =
+ Pattern.compile("\\\"?([^\"<]*?)\\\"?\\s*<(.*)>").matcher("");
+
+ private EmailAddress(String name, String address) {
+ mName = name;
+ mAddress = address;
+ }
+
+ public String getName() {
+ return mName;
+ }
+
+ /**
+ * @return either the parsed out e-mail address, or the full raw address if it is not in
+ * an expected format. This is not guaranteed to be HTML safe.
+ */
+ public String getAddress() {
+ return mAddress;
+ }
+
+ // TODO (pwestbro): move to provider
+ public static synchronized EmailAddress getEmailAddress(String rawAddress) {
+ String name, address;
+ Matcher m = sEmailMatcher.reset(rawAddress);
+ if (m.matches()) {
+ name = m.group(1);
+ address = m.group(2);
+ if (name == null) {
+ name = "";
+ } else {
+ name = Html.fromHtml(name.trim()).toString();
+ }
+ if (address == null) {
+ address = "";
+ } else {
+ address = Html.fromHtml(address).toString();
+ }
+ } else {
+ // Try and tokenize the string
+ final Rfc822Token[] tokens = Rfc822Tokenizer.tokenize(rawAddress);
+ if (tokens.length > 0) {
+ final String tokenizedName = tokens[0].getName();
+ name = tokenizedName != null ? Html.fromHtml(tokenizedName.trim()).toString() : "";
+ address = Html.fromHtml(tokens[0].getAddress()).toString();
+ } else {
+ name = "";
+ address = rawAddress == null ? "" : Html.fromHtml(rawAddress).toString();
+ }
+ }
+ return new EmailAddress(name, address);
+ }
+}
\ No newline at end of file
diff --git a/src/com/android/mail/MailIntentService.java b/src/com/android/mail/MailIntentService.java
index 08b9175..bd74c32 100644
--- a/src/com/android/mail/MailIntentService.java
+++ b/src/com/android/mail/MailIntentService.java
@@ -18,20 +18,32 @@
import com.android.mail.utils.StorageLowState;
import android.app.IntentService;
+import android.content.Context;
import android.content.Intent;
+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.NotificationUtils;
+
/**
* A service to handle various intents asynchronously.
*/
public class MailIntentService extends IntentService {
+ private static final String LOG_TAG = LogTag.getLogTag();
+
public static final String ACTION_RESEND_NOTIFICATIONS =
"com.android.mail.action.RESEND_NOTIFICATIONS";
public static final String ACTION_CLEAR_NEW_MAIL_NOTIFICATIONS =
"com.android.mail.action.CLEAR_NEW_MAIL_NOTIFICATIONS";
public static final String ACTION_MARK_SEEN = "com.android.mail.action.MARK_SEEN";
+ public static final String ACTION_BACKUP_DATA_CHANGED =
+ "com.android.mail.action.BACKUP_DATA_CHANGED";
public static final String ACCOUNT_EXTRA = "account";
public static final String FOLDER_EXTRA = "folder";
+ public static final String CONVERSATION_EXTRA = "conversation";
public MailIntentService() {
super("MailIntentService");
@@ -43,13 +55,42 @@
@Override
protected void onHandleIntent(final Intent intent) {
- // The storage_low state is recorded centrally even though no handler might be present to
- // change application state based on state changes.
+ // UnifiedEmail does not handle all Intents
+
+ LogUtils.v(LOG_TAG, "Handling intent %s", intent);
+
final String action = intent.getAction();
- if (Intent.ACTION_DEVICE_STORAGE_LOW.equals(action)) {
+
+ if (Intent.ACTION_LOCALE_CHANGED.equals(action)) {
+ handleLocaleChanged();
+ } else if (ACTION_CLEAR_NEW_MAIL_NOTIFICATIONS.equals(action)) {
+ final Account account = intent.getParcelableExtra(ACCOUNT_EXTRA);
+ final Folder folder = intent.getParcelableExtra(FOLDER_EXTRA);
+
+ NotificationUtils.clearFolderNotification(this, account, folder);
+ } else if (ACTION_RESEND_NOTIFICATIONS.equals(action)) {
+ NotificationUtils.resendNotifications(this, false);
+ } else if (ACTION_MARK_SEEN.equals(action)) {
+ final Folder folder = intent.getParcelableExtra(FOLDER_EXTRA);
+
+ NotificationUtils.markSeen(this, folder);
+ } else if (Intent.ACTION_DEVICE_STORAGE_LOW.equals(action)) {
+ // The storage_low state is recorded centrally even though
+ // no handler might be present to change application state
+ // based on state changes.
StorageLowState.setIsStorageLow(true);
} else if (Intent.ACTION_DEVICE_STORAGE_OK.equals(action)) {
StorageLowState.setIsStorageLow(false);
}
}
+
+ public static void broadcastBackupDataChanged(final Context context) {
+ final Intent intent = new Intent(ACTION_BACKUP_DATA_CHANGED);
+ context.startService(intent);
+ }
+
+ private void handleLocaleChanged() {
+ // Cancel all notifications. The correct ones will be recreated when the app starts back up
+ NotificationUtils.cancelAndResendNotifications(this);
+ }
}
diff --git a/src/com/android/mail/MailLogService.java b/src/com/android/mail/MailLogService.java
new file mode 100644
index 0000000..bd0cfbf
--- /dev/null
+++ b/src/com/android/mail/MailLogService.java
@@ -0,0 +1,183 @@
+/*******************************************************************************
+ * 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;
+
+import com.android.mail.utils.LogTag;
+import com.android.mail.utils.LogUtils;
+
+import android.app.Service;
+import android.content.Intent;
+import android.os.IBinder;
+import android.util.Log;
+import android.util.Pair;
+
+import java.io.FileDescriptor;
+import java.io.PrintWriter;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.LinkedList;
+import java.util.Map;
+import java.util.Queue;
+
+/**
+ * A write-only device for sensitive logs. Turned on only during debugging.
+ *
+ * Dump valuable system state by sending a local broadcast to the associated activity.
+ * Broadcast receivers are responsible for dumping state as they see fit.
+ * This service is only started when the log level is high, so there is no risk of user
+ * data being logged by mistake.
+ *
+ * To add logging to this service, call {@link #log(String, String, Object...)} with a tag name,
+ * which is a class name, like "AbstractActivityController", which is a unique ID. Then, add to the
+ * resulting buffer any information of interest at logging time. This is kept in a ring buffer,
+ * which is overwritten with new information.
+ */
+public class MailLogService extends Service {
+ /**
+ * This is the top level flag that enables this service.
+ * STOPSHIP: Turn to false before a release.
+ */
+ public static boolean DEBUG_ENABLED = true;
+
+ /** The tag which needs to be turned to DEBUG to get logging going. */
+ protected static final String LOG_TAG = LogTag.getLogTag();
+
+ /**
+ * A circular buffer of {@value #SIZE} lines. To insert into this buffer,
+ * call the {@link #put(String)} method. To retrieve the most recent logs,
+ * call the {@link #toString()} method.
+ */
+ private static class CircularBuffer {
+ // We accept fifty lines of input.
+ public static final int SIZE = 50;
+ /** The actual list of strings to be printed. */
+ final Queue<Pair<Long, String>> mList = new LinkedList<Pair<Long, String>>();
+ /** The current size of the buffer */
+ int mCurrentSize = 0;
+
+ /** Create an empty log buffer. */
+ private CircularBuffer() {
+ // Do nothing
+ }
+
+ /** Get the current timestamp */
+ private static String dateToString(long timestamp) {
+ final Date d = new Date(timestamp);
+ return String.format("%d-%d %d:%d:%d: ", d.getDay(), d.getMonth(), d.getHours(),
+ d.getMinutes(), d.getSeconds());
+ }
+
+ /**
+ * Insert a log message into the buffer. This might evict the oldest message if the log
+ * is at capacity.
+ * @param message a log message for this buffer.
+ */
+ private synchronized void put(String message) {
+ if (mCurrentSize == SIZE) {
+ // At capacity, we'll remove the head, and add to the tail. Size is unchanged.
+ mList.remove();
+ } else {
+ // Less than capacity. Adding a new element at the end.
+ mCurrentSize++;
+ }
+ // Add the current timestamp along with the message.
+ mList.add(new Pair<Long, String>(System.currentTimeMillis(), message));
+ }
+
+ @Override
+ public String toString() {
+ final StringBuilder builder = new StringBuilder();
+ for (final Pair<Long, String> s : mList) {
+ // Print the timestamp as an actual date, and then the message.
+ builder.append(dateToString(s.first));
+ builder.append(s.second);
+ // Put a newline at the end of each log line.
+ builder.append("\n");
+ }
+ return builder.toString();
+ }
+ }
+
+ /** Header printed at the start of the dump. */
+ private static final String HEADER = "**** MailLogService ***\n";
+ /** Map of current tag -> log. */
+ private static final Map<String, CircularBuffer> sLogs = new HashMap<String, CircularBuffer>();
+
+ public IBinder onBind(Intent intent) {
+ return null;
+ }
+
+ /**
+ * Return the circular buffer associated with this tag, or create a new buffer if none is
+ * currently associated.
+ * @param tag a string to identify a unique tag.
+ * @return a circular buffer associated with a string tag.
+ */
+ private static CircularBuffer getOrCreate(String tag) {
+ if (sLogs.containsKey(tag)) {
+ return sLogs.get(tag);
+ }
+ // Create a new CircularBuffer with this tag
+ final CircularBuffer buffer = new CircularBuffer();
+ sLogs.put(tag, buffer);
+ return buffer;
+ }
+
+ /**
+ * Return true if the logging level is high enough for this service to function.
+ * @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);
+ }
+
+ /**
+ * Add to the log for the tag given.
+ * @param tag a unique tag to add the message to
+ * @param format a string format for the message
+ * @param args optional list of arguments for the format.
+ */
+ public static void log(String tag, String format, Object... args) {
+ if (!DEBUG_ENABLED || !isLoggingLevelHighEnough()) {
+ return;
+ }
+ // The message we are printing.
+ final String logMessage = String.format(format, args);
+ // Find the circular buffer to go with this tag, or create a new one.
+ getOrCreate(tag).put(logMessage);
+ }
+
+ @Override
+ protected void dump(FileDescriptor fd, PrintWriter writer, String[] args) {
+ if (!DEBUG_ENABLED) {
+ return;
+ }
+ writer.print(HEADER);
+ // Go through all the tags, and write them all out sequentially.
+ for (final String tag : sLogs.keySet()) {
+ // Write out a sub-header: Logging for tag "MyModuleName"
+ writer.append("Logging for tag: \"");
+ writer.append(tag);
+ writer.append("\"\n");
+
+ writer.append(sLogs.get(tag).toString());
+ }
+ // Go through all the buffers.
+ super.dump(fd, writer,args);
+ }
+}
diff --git a/src/com/android/mail/adapter/DrawerItem.java b/src/com/android/mail/adapter/DrawerItem.java
new file mode 100644
index 0000000..84b6540
--- /dev/null
+++ b/src/com/android/mail/adapter/DrawerItem.java
@@ -0,0 +1,318 @@
+/*
+ * 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.adapter;
+
+import com.android.mail.R;
+import com.android.mail.providers.Account;
+import com.android.mail.providers.Folder;
+import com.android.mail.ui.ControllableActivity;
+import com.android.mail.ui.FolderItemView;
+import com.android.mail.utils.LogTag;
+import com.android.mail.utils.LogUtils;
+
+import android.net.Uri;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+/** An account, a system folder, a recent folder, or a header (a resource string) */
+public class DrawerItem {
+
+ private static final String LOG_TAG = LogTag.getLogTag();
+ public int mPosition;
+ public final Folder mFolder;
+ public final Account mAccount;
+ public final int mResource;
+ /** True if expand item view for expanding accounts. False otherwise */
+ public final boolean mIsExpandForAccount;
+ /** Either {@link #VIEW_ACCOUNT}, {@link #VIEW_FOLDER} or {@link #VIEW_HEADER} */
+ public final int mType;
+ /** A normal folder, also a child, if a parent is specified. */
+ public static final int VIEW_FOLDER = 0;
+ /** A text-label which serves as a header in sectioned lists. */
+ public static final int VIEW_HEADER = 1;
+ /** An account object, which allows switching accounts rather than folders. */
+ public static final int VIEW_ACCOUNT = 2;
+ /** An expandable object for expanding/collapsing more of the list */
+ public static final int VIEW_MORE = 3;
+ /** TODO: On adding another type, be sure to change getViewTypes() */
+
+ /** The parent activity */
+ private final ControllableActivity mActivity;
+ private final LayoutInflater mInflater;
+
+ /**
+ * Either {@link #FOLDER_SYSTEM}, {@link #FOLDER_RECENT} or {@link #FOLDER_USER} when
+ * {@link #mType} is {@link #VIEW_FOLDER}, or an {@link #ACCOUNT} in the case of
+ * accounts, {@link #EXPAND} for expand blocks, and {@link #INERT_HEADER} otherwise.
+ */
+ public final int mFolderType;
+ /** An unclickable text-header visually separating the different types. */
+ public static final int INERT_HEADER = 0;
+ /** A system-defined folder: Inbox/Drafts, ...*/
+ public static final int FOLDER_SYSTEM = 1;
+ /** A folder from whom a conversation was recently viewed */
+ public static final int FOLDER_RECENT = 2;
+ /** A user created folder */
+ public static final int FOLDER_USER = 3;
+ /** An entry for the accounts the user has on the device. */
+ public static final int ACCOUNT = 4;
+ /** A clickable block to expand list as requested */
+ public static final int EXPAND = 5;
+
+ /** True if this view is enabled, false otherwise. */
+ private boolean isEnabled = false;
+
+ /**
+ * Create a folder item with the given type.
+ * @param folder a folder that this item represents
+ * @param folderType one of {@link #FOLDER_SYSTEM}, {@link #FOLDER_RECENT} or
+ * {@link #FOLDER_USER}
+ */
+ public DrawerItem(ControllableActivity activity, Folder folder, int folderType,
+ int cursorPosition) {
+ mActivity = activity;
+ mInflater = LayoutInflater.from(mActivity.getActivityContext());
+ mFolder = folder;
+ mAccount = null;
+ mResource = -1;
+ mType = VIEW_FOLDER;
+ mFolderType = folderType;
+ mPosition = cursorPosition;
+ mIsExpandForAccount = false;
+ }
+
+ /**
+ * Creates an item from an account.
+ * @param account an account that this item represents.
+ */
+ public DrawerItem(ControllableActivity activity, Account account, int count) {
+ mActivity = activity;
+ mInflater = LayoutInflater.from(mActivity.getActivityContext());
+ mFolder = null;
+ mType = VIEW_ACCOUNT;
+ mResource = count;
+ mFolderType = ACCOUNT;
+ mAccount = account;
+ mIsExpandForAccount = false;
+ }
+
+ /**
+ * Create a header item with a string resource.
+ * @param resource the string resource: R.string.all_folders_heading
+ */
+ public DrawerItem(ControllableActivity activity, int resource) {
+ mActivity = activity;
+ mInflater = LayoutInflater.from(mActivity.getActivityContext());
+ mFolder = null;
+ mResource = resource;
+ mType = VIEW_HEADER;
+ mFolderType = INERT_HEADER;
+ mAccount = null;
+ mIsExpandForAccount = false;
+ }
+
+ /**
+ * Creates an item for expanding or contracting for emails/items
+ * @param resource the string resource: R.string.folder_list_*
+ * @param isExpand true if "more" and false if "less"
+ */
+ public DrawerItem(ControllableActivity activity, int resource, boolean isExpandForAccount) {
+ mActivity = activity;
+ mInflater = LayoutInflater.from(mActivity.getActivityContext());
+ mFolder = null;
+ mType = VIEW_MORE;
+ mResource = resource;
+ mFolderType = EXPAND;
+ mAccount = null;
+ mIsExpandForAccount = isExpandForAccount;
+ }
+
+ public View getView(int position, View convertView, ViewGroup parent) {
+ final View result;
+ switch (mType) {
+ case VIEW_FOLDER:
+ result = getFolderView(position, convertView, parent);
+ break;
+ case VIEW_HEADER:
+ result = getHeaderView(position, convertView, parent);
+ break;
+ case VIEW_ACCOUNT:
+ result = getAccountView(position, convertView, parent);
+ break;
+ case VIEW_MORE:
+ result = getExpandView(position, convertView, parent);
+ break;
+ default:
+ LogUtils.wtf(LOG_TAG, "DrawerItem.getView(%d) for an invalid type!", mType);
+ result = null;
+ }
+ return result;
+ }
+
+ /**
+ * Book-keeping for how many different view types there are. Be sure to
+ * increment this appropriately once adding more types as drawer items
+ * @return number of different types of view items
+ */
+ public static int getViewTypes() {
+ return VIEW_MORE + 1;
+ }
+
+ /**
+ * Returns whether this view is enabled or not.
+ * @return
+ */
+ public boolean isItemEnabled(Uri currentAccountUri) {
+ switch (mType) {
+ case VIEW_HEADER :
+ // Headers are never enabled.
+ return false;
+ case VIEW_FOLDER :
+ // Folders are always enabled.
+ return true;
+ case VIEW_ACCOUNT:
+ // Accounts are only enabled if they are not the current account.
+ return !currentAccountUri.equals(mAccount.uri);
+ case VIEW_MORE:
+ // 'Expand/Collapse' items are always enabled.
+ return true;
+ default:
+ LogUtils.wtf(LOG_TAG, "DrawerItem.isItemEnabled() for invalid type %d", mType);
+ return false;
+ }
+ }
+
+ /**
+ * Returns whether this view is highlighted or not.
+ *
+ * @param currentFolder
+ * @param currentType
+ * @return
+ */
+ public boolean isHighlighted(Folder currentFolder, int currentType){
+ switch (mType) {
+ case VIEW_HEADER :
+ // Headers are never highlighted
+ return false;
+ case VIEW_FOLDER :
+ return (mFolderType == currentType) && mFolder.uri.equals(currentFolder.uri);
+ case VIEW_ACCOUNT:
+ // Accounts are never highlighted
+ return false;
+ case VIEW_MORE:
+ // Expand/Collapse items are never highlighted
+ return false;
+ default:
+ LogUtils.wtf(LOG_TAG, "DrawerItem.isHighlighted() for invalid type %d", mType);
+ return false;
+ }
+ }
+
+ /**
+ * 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) {
+ // Shoe-horn an account object into a Folder DrawerItem for now.
+ // TODO(viki): Stop this ugly shoe-horning and use a real layout.
+ final FolderItemView folderItemView;
+ if (convertView != null) {
+ folderItemView = (FolderItemView) convertView;
+ } else {
+ folderItemView =
+ (FolderItemView) mInflater.inflate(R.layout.folder_item, null, false);
+ }
+ // Temporary. Ideally we want a totally different item.
+ folderItemView.bind(mAccount, mActivity, mResource);
+ View v = folderItemView.findViewById(R.id.color_block);
+ v.setBackgroundColor(mAccount.color);
+ v = folderItemView.findViewById(R.id.folder_icon);
+ v.setVisibility(View.GONE);
+ return folderItemView;
+ }
+
+ /**
+ * Returns a text divider between sections.
+ * @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) {
+ final TextView headerView;
+ if (convertView != null) {
+ headerView = (TextView) convertView;
+ } else {
+ headerView = (TextView) mInflater.inflate(
+ R.layout.folder_list_header, parent, false);
+ }
+ headerView.setText(mResource);
+ return headerView;
+ }
+
+ /**
+ * 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) {
+ final FolderItemView folderItemView;
+ if (convertView != null) {
+ folderItemView = (FolderItemView) convertView;
+ } else {
+ folderItemView =
+ (FolderItemView) mInflater.inflate(R.layout.folder_item, null, false);
+ }
+ folderItemView.bind(mFolder, mActivity);
+ Folder.setFolderBlockColor(mFolder, folderItemView.findViewById(R.id.color_block));
+ Folder.setIcon(mFolder, (ImageView) folderItemView.findViewById(R.id.folder_icon));
+ return folderItemView;
+ }
+
+ /**
+ * Return a view for the 'Expand/Collapse' item.
+ * @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 an item for folder/account expansion at given position.
+ */
+ private View getExpandView(int position, View convertView, ViewGroup parent) {
+ final ViewGroup headerView;
+ if (convertView != null) {
+ headerView = (ViewGroup) convertView;
+ } else {
+ headerView = (ViewGroup) mInflater.inflate(
+ R.layout.folder_expand_item, parent, false);
+ }
+ TextView direction =
+ (TextView)headerView.findViewById(R.id.folder_expand_text);
+ if(direction != null) {
+ direction.setText(mResource);
+ }
+ return headerView;
+ }
+}
+
diff --git a/src/com/android/mail/browse/ConversationCursor.java b/src/com/android/mail/browse/ConversationCursor.java
index beab823..b3c9f79 100644
--- a/src/com/android/mail/browse/ConversationCursor.java
+++ b/src/com/android/mail/browse/ConversationCursor.java
@@ -35,6 +35,7 @@
import android.os.Handler;
import android.os.Looper;
import android.os.RemoteException;
+import android.os.SystemClock;
import android.support.v4.util.SparseArrayCompat;
import android.text.TextUtils;
import android.util.Log;
@@ -54,12 +55,14 @@
import com.android.mail.utils.Utils;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Lists;
import com.google.common.collect.Sets;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
+import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
@@ -71,7 +74,10 @@
* caching for quick UI response. This is effectively a singleton class, as the cache is
* implemented as a static HashMap.
*/
-public final class ConversationCursor implements Cursor {
+public final class ConversationCursor implements Cursor, ConversationCursorMarkSeenListener {
+
+ private static final boolean ENABLE_CONVERSATION_PRECACHING = true;
+
private static final String LOG_TAG = LogTag.getLogTag();
/** Turn to true for debugging. */
private static final boolean DEBUG = false;
@@ -133,6 +139,8 @@
private final String mName;
/** Column names for this cursor */
private String[] mColumnNames;
+ // Column names as above, as a Set for quick membership checking
+ private Set<String> mColumnNameSet;
/** An observer on the underlying cursor (so we can detect changes from outside the UI) */
private final CursorObserver mCursorObserver;
/** Whether our observer is currently registered with the underlying cursor */
@@ -162,6 +170,11 @@
close();
}
mColumnNames = cursor.getColumnNames();
+ ImmutableSet.Builder<String> builder = ImmutableSet.builder();
+ for (String name : mColumnNames) {
+ builder.add(name);
+ }
+ mColumnNameSet = builder.build();
mRefreshRequired = false;
mRefreshReady = false;
mRefreshTask = null;
@@ -244,42 +257,87 @@
return mUnderlyingCursor != null ? mUnderlyingCursor.conversationIds() : null;
}
+ private static class UnderlyingRowData {
+ public final String wrappedUri;
+ public final String innerUri;
+ public final Conversation conversation;
+
+ public UnderlyingRowData(String wrappedUri, String innerUri, Conversation conversation) {
+ this.wrappedUri = wrappedUri;
+ this.innerUri = innerUri;
+ this.conversation = conversation;
+ }
+ }
+
/**
* Simple wrapper for a cursor that provides methods for quickly determining
* the existence of a row.
*/
- private class UnderlyingCursorWrapper extends CursorWrapper {
+ private static class UnderlyingCursorWrapper extends CursorWrapper {
// Ideally these two objects could be combined into a Map from
// conversationId -> position, but the cached values uses the conversation
// uri as a key.
private final Map<String, Integer> mConversationUriPositionMap;
private final Map<Long, Integer> mConversationIdPositionMap;
+ private final List<UnderlyingRowData> mRowCache;
public UnderlyingCursorWrapper(Cursor result) {
super(result);
+ long start = SystemClock.uptimeMillis();
final ImmutableMap.Builder<String, Integer> conversationUriPositionMapBuilder =
new ImmutableMap.Builder<String, Integer>();
final ImmutableMap.Builder<Long, Integer> conversationIdPositionMapBuilder =
new ImmutableMap.Builder<Long, Integer>();
+ final UnderlyingRowData[] cache;
+ final int count;
if (result != null && result.moveToFirst()) {
+ count = result.getCount();
+ cache = new UnderlyingRowData[count];
// We don't want iterating over this cursor to trigger a network
// request
final boolean networkWasEnabled =
Utils.disableConversationCursorNetworkAccess(result);
+ int i = 0;
do {
- final int position = result.getPosition();
- conversationUriPositionMapBuilder.put(
- result.getString(URI_COLUMN_INDEX), position);
- conversationIdPositionMapBuilder.put(
- result.getLong(UIProvider.CONVERSATION_ID_COLUMN), position);
- } while (result.moveToNext());
+ final Conversation c;
+ final String innerUriString;
+ final String wrappedUriString;
+ final long convId;
+
+ if (ENABLE_CONVERSATION_PRECACHING) {
+ c = new Conversation(this);
+ innerUriString = c.uri.toString();
+ wrappedUriString = uriToCachingUriString(c.uri);
+ convId = c.id;
+ } else {
+ c = null;
+ innerUriString = result.getString(URI_COLUMN_INDEX);
+ wrappedUriString = uriToCachingUriString(Uri.parse(innerUriString));
+ convId = result.getLong(UIProvider.CONVERSATION_ID_COLUMN);
+ }
+ conversationUriPositionMapBuilder.put(innerUriString, i);
+ conversationIdPositionMapBuilder.put(convId, i);
+ cache[i] = new UnderlyingRowData(
+ wrappedUriString,
+ innerUriString,
+ c);
+
+ } while (result.moveToPosition(++i));
if (networkWasEnabled) {
Utils.enableConversationCursorNetworkAccess(result);
}
+ } else {
+ count = 0;
+ cache = new UnderlyingRowData[0];
}
mConversationUriPositionMap = conversationUriPositionMapBuilder.build();
mConversationIdPositionMap = conversationIdPositionMapBuilder.build();
+ mRowCache = Collections.unmodifiableList(Arrays.asList(cache));
+ long end = SystemClock.uptimeMillis();
+ LogUtils.i(LOG_TAG, "*** ConversationCursor pre-loading took" +
+ " %sms n=%s CONV_PRECACHING=%s",
+ (end-start), count, ENABLE_CONVERSATION_PRECACHING);
}
public boolean contains(String uri) {
@@ -299,6 +357,18 @@
final Integer position = mConversationUriPositionMap.get(conversationUri);
return position != null ? position.intValue() : -1;
}
+
+ public String getWrappedUri() {
+ return mRowCache.get(getPosition()).wrappedUri;
+ }
+
+ public String getInnerUri() {
+ return mRowCache.get(getPosition()).innerUri;
+ }
+
+ public Conversation getConversation() {
+ return mRowCache.get(getPosition()).conversation;
+ }
}
/**
@@ -629,22 +699,7 @@
return;
}
}
- // ContentValues has no generic "put", so we must test. For now, the only classes
- // of values implemented are Boolean/Integer/String/Blob, though others are trivially
- // added
- if (value instanceof Boolean) {
- map.put(columnName, ((Boolean) value).booleanValue() ? 1 : 0);
- } else if (value instanceof Integer) {
- map.put(columnName, (Integer) value);
- } else if (value instanceof String) {
- map.put(columnName, (String) value);
- } else if (value instanceof byte[]) {
- map.put(columnName, (byte[])value);
- } else {
- final String cname = value.getClass().getName();
- throw new IllegalArgumentException("Value class not compatible with cache: "
- + cname);
- }
+ putInValues(map, columnName, value);
map.put(UPDATE_TIME_COLUMN, System.currentTimeMillis());
if (DEBUG && (columnName != DELETED_COLUMN)) {
LogUtils.i(LOG_TAG, "Caching value for %s: %s", uriString, columnName);
@@ -658,7 +713,7 @@
* @return the cached value for this column, or null if there is none
*/
private Object getCachedValue(int columnIndex) {
- String uri = mUnderlyingCursor.getString(URI_COLUMN_INDEX);
+ final String uri = mUnderlyingCursor.getInnerUri();
return getCachedValue(uri, columnIndex);
}
@@ -1021,8 +1076,7 @@
// If we're asking for the Uri for the conversation list, we return a forwarding URI
// so that we can intercept update/delete and handle it ourselves
if (columnIndex == URI_COLUMN_INDEX) {
- Uri uri = Uri.parse(mUnderlyingCursor.getString(columnIndex));
- return uriToCachingUriString(uri);
+ return mUnderlyingCursor.getWrappedUri();
}
Object obj = getCachedValue(columnIndex);
if (obj != null) return (String)obj;
@@ -1036,6 +1090,56 @@
return mUnderlyingCursor.getBlob(columnIndex);
}
+ public Conversation getConversation() {
+ Conversation c = mUnderlyingCursor.getConversation();
+
+ if (c == null) {
+ // not pre-cached. fall back to just-in-time construction.
+ c = new Conversation(this);
+ } else {
+ // apply any cached values
+ // but skip over any cached values that aren't part of the cursor projection
+ final ContentValues values = mCacheMap.get(mUnderlyingCursor.getInnerUri());
+ if (values != null) {
+ final ContentValues queryableValues = new ContentValues();
+ for (String key : values.keySet()) {
+ if (!mColumnNameSet.contains(key)) {
+ continue;
+ }
+ putInValues(queryableValues, key, values.get(key));
+ }
+ if (queryableValues.size() > 0) {
+ // copy-on-write to help ensure the underlying cached Conversation is immutable
+ // of course, any callers this method should also try not to modify them
+ // overmuch...
+ c = new Conversation(c);
+ c.applyCachedValues(queryableValues);
+ }
+ }
+ }
+
+ return c;
+ }
+
+ private static void putInValues(ContentValues dest, String key, Object value) {
+ // ContentValues has no generic "put", so we must test. For now, the only classes
+ // of values implemented are Boolean/Integer/String/Blob, though others are trivially
+ // added
+ if (value instanceof Boolean) {
+ dest.put(key, ((Boolean) value).booleanValue() ? 1 : 0);
+ } else if (value instanceof Integer) {
+ dest.put(key, (Integer) value);
+ } else if (value instanceof String) {
+ dest.put(key, (String) value);
+ } else if (value instanceof byte[]) {
+ dest.put(key, (byte[])value);
+ } else {
+ final String cname = value.getClass().getName();
+ throw new IllegalArgumentException("Value class not compatible with cache: "
+ + cname);
+ }
+ }
+
/**
* Observer of changes to underlying data
*/
@@ -1997,4 +2101,12 @@
});
}
}
+
+ /**
+ * Marks all contents of this cursor as seen. This may have no effect with certain providers.
+ */
+ @Override
+ public void markContentsSeen() {
+ ConversationCursorMarkSeenListener.MarkSeenHelper.markContentsSeen(mUnderlyingCursor);
+ }
}
diff --git a/src/com/android/mail/browse/ConversationCursorMarkSeenListener.java b/src/com/android/mail/browse/ConversationCursorMarkSeenListener.java
new file mode 100644
index 0000000..0956859
--- /dev/null
+++ b/src/com/android/mail/browse/ConversationCursorMarkSeenListener.java
@@ -0,0 +1,46 @@
+/*******************************************************************************
+ * 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.database.CursorWrapper;
+
+public interface ConversationCursorMarkSeenListener {
+ /**
+ * Marks all contents of this cursor as seen.
+ */
+ void markContentsSeen();
+
+ public class MarkSeenHelper {
+ /**
+ * Invokes {@link ConversationCursorMarkSeenListener#markContentsSeen(Cursor)} on the
+ * specified {@link Cursor}, recursively calls {@link #markContentsSeen(Cursor)} on a
+ * wrapped cursor, or returns.
+ */
+ public static void markContentsSeen(final Cursor cursor) {
+ if (cursor == null) {
+ return;
+ }
+
+ if (cursor instanceof ConversationCursorMarkSeenListener) {
+ ((ConversationCursorMarkSeenListener) cursor).markContentsSeen();
+ } else if (cursor instanceof CursorWrapper) {
+ markContentsSeen(((CursorWrapper) cursor).getWrappedCursor());
+ }
+ }
+ }
+}
diff --git a/src/com/android/mail/browse/ConversationItemView.java b/src/com/android/mail/browse/ConversationItemView.java
index b3abaf2..5165610 100644
--- a/src/com/android/mail/browse/ConversationItemView.java
+++ b/src/com/android/mail/browse/ConversationItemView.java
@@ -37,6 +37,7 @@
import android.graphics.Shader;
import android.graphics.Typeface;
import android.graphics.drawable.Drawable;
+import android.net.Uri;
import android.text.Layout.Alignment;
import android.text.Spannable;
import android.text.SpannableString;
@@ -71,7 +72,6 @@
import com.android.mail.ui.AnimatedAdapter;
import com.android.mail.ui.ControllableActivity;
import com.android.mail.ui.ConversationSelectionSet;
-
import com.android.mail.ui.DividedImageCanvas;
import com.android.mail.ui.DividedImageCanvas.InvalidateCallback;
import com.android.mail.ui.EllipsizedMultilineTextView;
@@ -213,8 +213,8 @@
}
@Override
- public void loadConversationFolders(Conversation conv, Folder ignoreFolder) {
- super.loadConversationFolders(conv, ignoreFolder);
+ public void loadConversationFolders(Conversation conv, final Uri ignoreFolderUri) {
+ super.loadConversationFolders(conv, ignoreFolderUri);
mFoldersCount = mFoldersSortedSet.size();
mHasMoreFolders = mFoldersCount > MAX_DISPLAYED_FOLDERS_COUNT;
@@ -569,7 +569,8 @@
} else {
mHeader.folderDisplayer.reset();
}
- mHeader.folderDisplayer.loadConversationFolders(mHeader.conversation, mDisplayedFolder);
+ mHeader.folderDisplayer.loadConversationFolders(mHeader.conversation,
+ mDisplayedFolder.uri);
}
if (mSelectedConversationSet != null) {
@@ -587,7 +588,7 @@
if (mHeader.conversation.conversationInfo != null) {
Context context = getContext();
mHeader.messageInfoString = SendersView
- .createMessageInfo(context, mHeader.conversation);
+ .createMessageInfo(context, mHeader.conversation, true);
int maxChars = ConversationItemViewCoordinates.getSendersLength(context,
ConversationItemViewCoordinates.getMode(context, mActivity.getViewMode()),
mHeader.conversation.hasAttachments);
@@ -596,11 +597,12 @@
mHeader.styledSenders = new ArrayList<SpannableString>();
SendersView.format(context, mHeader.conversation.conversationInfo,
mHeader.messageInfoString.toString(), maxChars, mHeader.styledSenders,
- mHeader.displayableSenderNames, mHeader.displayableSenderEmails, mAccount);
+ mHeader.displayableSenderNames, mHeader.displayableSenderEmails, mAccount,
+ true);
// If we have displayable sendres, load their thumbnails
loadSenderImages();
} else {
- SendersView.formatSenders(mHeader, getContext());
+ SendersView.formatSenders(mHeader, getContext(), true);
}
mHeader.dateText = DateUtils.getRelativeTimeSpanString(mContext,
@@ -1217,6 +1219,7 @@
* Toggle the check mark on this view and update the conversation or begin
* drag, if drag is enabled.
*/
+ @Override
public void toggleCheckMarkOrBeginDrag() {
ViewMode mode = mActivity.getViewMode();
if (!mTabletDevice || !mode.isListMode()) {
@@ -1264,9 +1267,11 @@
postInvalidate(mCoordinates.starX, mCoordinates.starY, mCoordinates.starX
+ starBitmap.getWidth(),
mCoordinates.starY + starBitmap.getHeight());
- ConversationCursor cursor = (ConversationCursor)mAdapter.getCursor();
- cursor.updateBoolean(mContext, mHeader.conversation, ConversationColumns.STARRED,
- mHeader.conversation.starred);
+ ConversationCursor cursor = (ConversationCursor) mAdapter.getCursor();
+ if (cursor != null) {
+ cursor.updateBoolean(mContext, mHeader.conversation, ConversationColumns.STARRED,
+ mHeader.conversation.starred);
+ }
}
private boolean isTouchInCheckmark(float x, float y) {
diff --git a/src/com/android/mail/browse/ConversationListFooterView.java b/src/com/android/mail/browse/ConversationListFooterView.java
index 0d72a90..f6eb098 100644
--- a/src/com/android/mail/browse/ConversationListFooterView.java
+++ b/src/com/android/mail/browse/ConversationListFooterView.java
@@ -111,6 +111,7 @@
mErrorStatus = extras.containsKey(UIProvider.CursorExtraKeys.EXTRA_ERROR) ?
extras.getInt(UIProvider.CursorExtraKeys.EXTRA_ERROR)
: UIProvider.LastSyncResult.SUCCESS;
+ final int totalCount = extras.getInt(UIProvider.CursorExtraKeys.EXTRA_TOTAL_COUNT);
if (UIProvider.CursorStatus.isWaitingForResults(cursorStatus)) {
mLoading.setVisibility(View.VISIBLE);
mNetworkError.setVisibility(View.GONE);
@@ -151,7 +152,7 @@
}
mErrorActionButton.setText(actionTextResourceId);
- } else if (mLoadMoreUri != null) {
+ } else if (mLoadMoreUri != null && cursor.getCount() < totalCount) {
mLoading.setVisibility(View.GONE);
mNetworkError.setVisibility(View.GONE);
mLoadMore.setVisibility(View.VISIBLE);
diff --git a/src/com/android/mail/browse/ConversationPagerAdapter.java b/src/com/android/mail/browse/ConversationPagerAdapter.java
index 6ba7886..239d6dc 100644
--- a/src/com/android/mail/browse/ConversationPagerAdapter.java
+++ b/src/com/android/mail/browse/ConversationPagerAdapter.java
@@ -31,6 +31,7 @@
import com.android.mail.providers.Account;
import com.android.mail.providers.Conversation;
import com.android.mail.providers.Folder;
+import com.android.mail.providers.FolderObserver;
import com.android.mail.providers.UIProvider;
import com.android.mail.ui.AbstractConversationViewFragment;
import com.android.mail.ui.ActivityController;
@@ -44,7 +45,12 @@
implements ViewPager.OnPageChangeListener {
private final DataSetObserver mListObserver = new ListObserver();
- private final DataSetObserver mFolderObserver = new FolderObserver();
+ private final FolderObserver mFolderObserver = new FolderObserver() {
+ @Override
+ public void onChanged(Folder newFolder) {
+ notifyDataSetChanged();
+ }
+ };
private ActivityController mController;
private final Bundle mCommonFragmentArgs;
private final Conversation mInitialConversation;
@@ -208,6 +214,13 @@
@Override
public int getCount() {
if (mStopListeningMode) {
+ if (LogUtils.isLoggable(LOG_TAG, LogUtils.DEBUG)) {
+ final Cursor cursor = getCursor();
+ LogUtils.d(LOG_TAG,
+ "IN CPA.getCount stopListeningMode, returning lastKnownCount=%d."
+ + " cursor=%s real count=%s", mLastKnownCount, cursor,
+ (cursor != null) ? cursor.getCount() : "N/A");
+ }
return mLastKnownCount;
}
@@ -420,7 +433,7 @@
mController = controller;
if (mController != null && !mStopListeningMode) {
mController.registerConversationListObserver(mListObserver);
- mController.registerFolderObserver(mFolderObserver);
+ mFolderObserver.initialize(mController);
notifyDataSetChanged();
} else {
@@ -441,13 +454,14 @@
// disable the observer, but save off the current count, in case the Pager asks for it
// from now until imminent destruction
- mStopListeningMode = true;
if (mController != null) {
mController.unregisterConversationListObserver(mListObserver);
- mController.unregisterFolderObserver(mFolderObserver);
+ mFolderObserver.unregisterAndDestroy();
}
mLastKnownCount = getCount();
+ mStopListeningMode = true;
+ LogUtils.d(LOG_TAG, "CPA.stopListening, recording lastKnownCount=%d", mLastKnownCount);
}
@Override
@@ -476,14 +490,6 @@
// no-op
}
- // update the pager title strip as the Folder's conversation count changes
- private class FolderObserver extends DataSetObserver {
- @Override
- public void onChanged() {
- notifyDataSetChanged();
- }
- }
-
// update the pager dataset as the Controller's cursor changes
private class ListObserver extends DataSetObserver {
@Override
diff --git a/src/com/android/mail/browse/ConversationPagerController.java b/src/com/android/mail/browse/ConversationPagerController.java
index 6c2e4ec..21cf6b9 100644
--- a/src/com/android/mail/browse/ConversationPagerController.java
+++ b/src/com/android/mail/browse/ConversationPagerController.java
@@ -37,6 +37,7 @@
import com.android.mail.ui.SubjectDisplayChanger;
import com.android.mail.utils.LogTag;
import com.android.mail.utils.LogUtils;
+import com.android.mail.utils.Utils;
/**
* A simple controller for a {@link ViewPager} of conversations.
@@ -128,6 +129,7 @@
mPagerAdapter.setPager(mPager);
LogUtils.d(LOG_TAG, "IN CPC.show, adapter=%s", mPagerAdapter);
+ Utils.sConvLoadTimer.mark("pager init");
LogUtils.d(LOG_TAG, "init pager adapter, count=%d initialConv=%s", mPagerAdapter.getCount(),
initialConversation);
mPager.setAdapter(mPagerAdapter);
@@ -140,6 +142,7 @@
mPager.setCurrentItem(initialPos);
}
}
+ Utils.sConvLoadTimer.mark("pager setAdapter");
mShown = true;
}
diff --git a/src/com/android/mail/browse/EmailCopyContextMenu.java b/src/com/android/mail/browse/EmailCopyContextMenu.java
new file mode 100644
index 0000000..5441c9f
--- /dev/null
+++ b/src/com/android/mail/browse/EmailCopyContextMenu.java
@@ -0,0 +1,118 @@
+/*
+ * 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.content.ClipData;
+import android.content.ClipboardManager;
+import android.content.Context;
+import android.content.Intent;
+import android.net.Uri;
+import android.text.TextUtils;
+import android.view.ContextMenu;
+import android.view.MenuInflater;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.ContextMenu.ContextMenuInfo;
+import android.view.View.OnCreateContextMenuListener;
+import android.webkit.WebView;
+import android.widget.TextView;
+
+import com.android.mail.R;
+
+/**
+ * <p>
+ * Handles display and behavior for long clicking on expanded messages' headers.
+ * Requires a context to use for inflation and clipboard copying.
+ * </p>
+ * <br>
+ * Dependencies:
+ * <ul>
+ * <li>res/menu/email_copy_context_menu.xml</li>
+ * </ul>
+ */
+public class EmailCopyContextMenu implements OnCreateContextMenuListener{
+
+ // IDs for displaying in the menu
+ private static final int SEND_EMAIL_ITEM = R.id.mail_context_menu_id;
+ private static final int COPY_CONTACT_ITEM = R.id.copy_mail_context_menu_id;
+
+ // Reference to context for layout inflation & copying the email
+ private final Context mContext;
+ private CharSequence mAddress;
+
+ public EmailCopyContextMenu(Context context) {
+ mContext = context;
+ }
+
+ /**
+ * Copy class for handling click event on the "Copy" button and storing
+ * copied text.
+ */
+ private class Copy implements MenuItem.OnMenuItemClickListener {
+ private final CharSequence mText;
+
+ public Copy(CharSequence text) {
+ mText = text;
+ }
+
+ @Override
+ public boolean onMenuItemClick(MenuItem item) {
+ copy(mText);
+ //Handled & consumed event
+ return true;
+ }
+ }
+
+ /**
+ * Copy the input text sequence to the system clipboard.
+ * @param text CharSequence to be copied.
+ */
+ private void copy(CharSequence text) {
+ ClipboardManager clipboard =
+ (ClipboardManager) mContext.getSystemService(Context.CLIPBOARD_SERVICE);
+ clipboard.setPrimaryClip(ClipData.newPlainText(null, text));
+ }
+
+ public void setAddress(CharSequence address) {
+ this.mAddress = address;
+ }
+
+ /**
+ * Creates context menu via MenuInflater and populates with items defined
+ * in res/menu/email_copy_context_menu.xml
+ */
+ @Override
+ public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo info) {
+ if(!TextUtils.isEmpty(mAddress)) {
+ MenuInflater inflater = new MenuInflater(mContext);
+ inflater.inflate(getMenuResourceId(), menu);
+
+ // Create menu and bind listener/intent
+ menu.setHeaderTitle(mAddress);
+ menu.findItem(SEND_EMAIL_ITEM).setIntent(new Intent(Intent.ACTION_VIEW,
+ Uri.parse(WebView.SCHEME_MAILTO + mAddress)));
+ menu.findItem(COPY_CONTACT_ITEM).setOnMenuItemClickListener(
+ new Copy(mAddress));
+ }
+ }
+
+ // Location of Context Menu layout
+ private int getMenuResourceId() {
+ return R.menu.email_copy_context_menu;
+ }
+}
diff --git a/src/com/android/mail/browse/MessageCursor.java b/src/com/android/mail/browse/MessageCursor.java
index 7a78f33..6c6d4ec 100644
--- a/src/com/android/mail/browse/MessageCursor.java
+++ b/src/com/android/mail/browse/MessageCursor.java
@@ -216,8 +216,13 @@
return mStatus;
}
+ /**
+ * Returns true if the cursor is fully loaded. Returns false if the cursor is expected to get
+ * new messages.
+ * @return
+ */
public boolean isLoaded() {
- return getStatus() >= CursorStatus.LOADED || getCount() > 0; // FIXME: remove count hack
+ return !CursorStatus.isWaitingForResults(getStatus());
}
public String getDebugDump() {
diff --git a/src/com/android/mail/browse/MessageHeaderView.java b/src/com/android/mail/browse/MessageHeaderView.java
index b75a9c2..8bd6981 100644
--- a/src/com/android/mail/browse/MessageHeaderView.java
+++ b/src/com/android/mail/browse/MessageHeaderView.java
@@ -16,13 +16,12 @@
package com.android.mail.browse;
-import android.app.Dialog;
import android.app.AlertDialog;
+import android.app.Dialog;
import android.content.AsyncQueryHandler;
import android.content.ClipData;
import android.content.ClipboardManager;
import android.content.Context;
-import android.content.DialogInterface;
import android.content.res.Resources;
import android.database.DataSetObserver;
import android.graphics.Typeface;
@@ -33,12 +32,11 @@
import android.text.style.StyleSpan;
import android.util.AttributeSet;
import android.view.LayoutInflater;
+import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.view.View.OnClickListener;
-import android.view.View.OnLongClickListener;
import android.view.ViewGroup;
-import android.widget.Button;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.PopupMenu;
@@ -55,6 +53,7 @@
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.Folder;
@@ -64,7 +63,6 @@
import com.android.mail.utils.LogUtils;
import com.android.mail.utils.Utils;
import com.android.mail.utils.VeiledAddressMatcher;
-
import com.google.common.annotations.VisibleForTesting;
import java.io.IOException;
@@ -72,7 +70,7 @@
import java.util.Map;
public class MessageHeaderView extends LinearLayout implements OnClickListener,
- OnLongClickListener, OnMenuItemClickListener, ConversationContainer.DetachListener {
+ OnMenuItemClickListener, ConversationContainer.DetachListener {
/**
* Cap very long recipient lists during summary construction for efficiency.
@@ -99,6 +97,10 @@
public static final int POPUP_MODE = 1;
+ // This is a debug only feature
+ public static final boolean ENABLE_REPORT_RENDERING_PROBLEM =
+ MailPrefs.SHOW_EXPERIMENTAL_PREFS;
+
private MessageHeaderViewCallbacks mCallbacks;
private ViewGroup mUpperHeaderView;
@@ -125,6 +127,7 @@
private View mAttachmentIcon;
private View mLeftSpacer;
private View mRightSpacer;
+ private final EmailCopyContextMenu mEmailCopyMenu;
// temporary fields to reference raw data between initial render and details
// expansion
@@ -226,6 +229,9 @@
void showExternalResources(Message msg);
void showExternalResources(String senderRawAddress);
+
+ boolean supportsMessageTransforms();
+ String getMessageTransforms(Message msg);
}
public MessageHeaderView(Context context) {
@@ -239,6 +245,7 @@
public MessageHeaderView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
+ mEmailCopyMenu = new EmailCopyContextMenu(getContext());
mInflater = LayoutInflater.from(context);
mMyName = context.getString(R.string.me);
}
@@ -282,6 +289,8 @@
registerMessageClickTargets(R.id.reply, R.id.reply_all, R.id.forward, R.id.star,
R.id.edit_draft, R.id.overflow, R.id.upper_header);
+
+ mUpperHeaderView.setOnCreateContextMenuListener(mEmailCopyMenu);
}
private void registerMessageClickTargets(int... ids) {
@@ -289,7 +298,6 @@
View v = findViewById(id);
if (v != null) {
v.setOnClickListener(this);
- v.setOnLongClickListener(this);
}
}
}
@@ -458,6 +466,7 @@
mSenderNameView.setText(getHeaderTitle());
mSenderEmailView.setText(getHeaderSubtitle());
+ setAddressOnContextMenu();
if (mUpperDateView != null) {
mUpperDateView.setText(mTimestampShort);
@@ -477,6 +486,14 @@
t.pause(HEADER_RENDER_TAG);
}
+ /**
+ * Update context menu's address field for when the user long presses
+ * on the message header and attempts to copy/send email.
+ */
+ private void setAddressOnContextMenu() {
+ mEmailCopyMenu.setAddress(mSender.getAddress());
+ }
+
public boolean isBoundTo(ConversationOverlayItem item) {
return item == mMessageHeaderItem;
}
@@ -888,11 +905,6 @@
onClick(v, v.getId());
}
- @Override
- public boolean onLongClick(View v) {
- return onLongClick(v.getId());
- }
-
/**
* Handles clicks on either views or menu items. View parameter can be null
* for menu item clicks.
@@ -923,6 +935,16 @@
case R.id.forward:
ComposeActivity.forward(getContext(), getAccount(), mMessage);
break;
+ case R.id.report_rendering_problem:
+ String text = getContext().getString(R.string.report_rendering_problem_desc);
+ ComposeActivity.reportRenderingFeedback(getContext(), getAccount(), mMessage,
+ text + "\n\n" + mCallbacks.getMessageTransforms(mMessage));
+ break;
+ case R.id.report_rendering_improvement:
+ text = getContext().getString(R.string.report_rendering_improvement_desc);
+ ComposeActivity.reportRenderingFeedback(getContext(), getAccount(), mMessage,
+ text + "\n\n" + mCallbacks.getMessageTransforms(mMessage));
+ break;
case R.id.star: {
final boolean newValue = !v.isSelected();
v.setSelected(newValue);
@@ -941,8 +963,14 @@
}
final boolean defaultReplyAll = getAccount().settings.replyBehavior
== UIProvider.DefaultReplyBehavior.REPLY_ALL;
- mPopup.getMenu().findItem(R.id.reply).setVisible(defaultReplyAll);
- mPopup.getMenu().findItem(R.id.reply_all).setVisible(!defaultReplyAll);
+ final Menu m = mPopup.getMenu();
+ m.findItem(R.id.reply).setVisible(defaultReplyAll);
+ m.findItem(R.id.reply_all).setVisible(!defaultReplyAll);
+
+ final boolean reportRendering = ENABLE_REPORT_RENDERING_PROBLEM
+ && mCallbacks.supportsMessageTransforms();
+ m.findItem(R.id.report_rendering_improvement).setVisible(reportRendering);
+ m.findItem(R.id.report_rendering_problem).setVisible(reportRendering);
mPopup.show();
break;
@@ -965,30 +993,6 @@
return handled;
}
- /**
- * Handles long click on message upper header to show dialog for copying
- * the email address. Does not consume the click, otherwise.
- */
- private boolean onLongClick(int id) {
- if (id == R.id.upper_header) {
- if(isExpanded()) {
- mCopyAddress = mSender.getAddress();
- mEmailCopyPopup = new Dialog(getContext());
- mEmailCopyPopup.setTitle(mCopyAddress);
- mEmailCopyPopup.setContentView(R.layout.copy_chip_dialog_layout);
- mEmailCopyPopup.setCancelable(true);
- mEmailCopyPopup.setCanceledOnTouchOutside(true);
- Button button =
- (Button)mEmailCopyPopup.findViewById(android.R.id.button1);
- button.setOnClickListener(this);
- button.setText(R.string.copy_email);
- mEmailCopyPopup.show();
- return true;
- }
- }
- return false;
- }
-
public void setExpandable(boolean expandable) {
mExpandable = expandable;
}
@@ -1056,6 +1060,7 @@
hideSpamWarning();
hideShowImagePrompt();
hideInvite();
+ mUpperHeaderView.setOnCreateContextMenuListener(null);
} else {
setMessageDetailsExpanded(mMessageHeaderItem.detailsExpanded);
if (mMessage.spamWarningString == null) {
@@ -1077,6 +1082,7 @@
} else {
hideInvite();
}
+ mUpperHeaderView.setOnCreateContextMenuListener(mEmailCopyMenu);
}
if (mBottomBorderView != null) {
mBottomBorderView.setVisibility(vis);
diff --git a/src/com/android/mail/browse/SelectedConversationsActionMenu.java b/src/com/android/mail/browse/SelectedConversationsActionMenu.java
index 5fd1414..43efc86 100644
--- a/src/com/android/mail/browse/SelectedConversationsActionMenu.java
+++ b/src/com/android/mail/browse/SelectedConversationsActionMenu.java
@@ -163,6 +163,8 @@
starConversations(false);
}
break;
+ case R.id.move_to:
+ /* fall through */
case R.id.change_folder:
boolean cantMove = false;
Account acct = mAccount;
@@ -187,7 +189,8 @@
}
if (!cantMove) {
final FolderSelectionDialog dialog = FolderSelectionDialog.getInstance(
- mContext, acct, mUpdater, mSelectionSet.values(), true, mFolder);
+ mContext, acct, mUpdater, mSelectionSet.values(), true, mFolder,
+ item.getItemId() == R.id.move_to);
if (dialog != null) {
dialog.show();
}
@@ -369,10 +372,14 @@
// 3) If we show neither archive or remove folder, then show a disabled
// 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 boolean showRemoveFolder = mFolder != null && mFolder.type == FolderType.DEFAULT
&& mFolder.supportsCapability(FolderCapabilities.CAN_ACCEPT_MOVED_MESSAGES)
&& !mFolder.isProviderFolder();
+ final boolean showMoveTo = mFolder != null
+ && mFolder.supportsCapability(FolderCapabilities.ALLOWS_REMOVE_CONVERSATION);
removeFolder.setVisible(showRemoveFolder);
+ moveTo.setVisible(showMoveTo);
if (mFolder != null && showRemoveFolder) {
removeFolder.setTitle(mActivity.getActivityContext().getString(R.string.remove_folder,
mFolder.name));
diff --git a/src/com/android/mail/browse/SendersView.java b/src/com/android/mail/browse/SendersView.java
index b6870a9..4f43326 100644
--- a/src/com/android/mail/browse/SendersView.java
+++ b/src/com/android/mail/browse/SendersView.java
@@ -95,13 +95,14 @@
return isUnread ? Typeface.DEFAULT_BOLD : Typeface.DEFAULT;
}
- private static void getSenderResources(Context context) {
- if (sConfigurationChangedReceiver == null) {
+ private static synchronized void getSenderResources(
+ Context context, final boolean resourceCachingRequired) {
+ if (sConfigurationChangedReceiver == null && resourceCachingRequired) {
sConfigurationChangedReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
sDraftSingularString = null;
- getSenderResources(context);
+ getSenderResources(context, true);
}
};
context.registerReceiver(sConfigurationChangedReceiver, new IntentFilter(
@@ -127,87 +128,108 @@
}
}
- public static SpannableStringBuilder createMessageInfo(Context context, Conversation conv) {
- ConversationInfo conversationInfo = conv.conversationInfo;
- int sendingStatus = conv.sendingState;
+ public static SpannableStringBuilder createMessageInfo(Context context, Conversation conv,
+ final boolean resourceCachingRequired) {
SpannableStringBuilder messageInfo = new SpannableStringBuilder();
- boolean hasSenders = false;
- // This covers the case where the sender is "me" and this is a draft
- // message, which means this will only run once most of the time.
- for (MessageInfo m : conversationInfo.messageInfos) {
- if (!TextUtils.isEmpty(m.sender)) {
- hasSenders = true;
- break;
+
+ try {
+ ConversationInfo conversationInfo = conv.conversationInfo;
+ int sendingStatus = conv.sendingState;
+ boolean hasSenders = false;
+ // This covers the case where the sender is "me" and this is a draft
+ // message, which means this will only run once most of the time.
+ for (MessageInfo m : conversationInfo.messageInfos) {
+ if (!TextUtils.isEmpty(m.sender)) {
+ hasSenders = true;
+ break;
+ }
+ }
+ getSenderResources(context, resourceCachingRequired);
+ if (conversationInfo != null) {
+ int count = conversationInfo.messageCount;
+ int draftCount = conversationInfo.draftCount;
+ boolean showSending = sendingStatus == UIProvider.ConversationSendingState.SENDING;
+ if (count > 1) {
+ messageInfo.append(count + "");
+ }
+ messageInfo.setSpan(CharacterStyle.wrap(
+ conv.read ? sMessageInfoReadStyleSpan : sMessageInfoUnreadStyleSpan),
+ 0, messageInfo.length(), 0);
+ if (draftCount > 0) {
+ // If we are showing a message count or any draft text and there
+ // is at least 1 sender, prepend the sending state text with a
+ // comma.
+ if (hasSenders || count > 1) {
+ messageInfo.append(sSendersSplitToken);
+ }
+ SpannableStringBuilder draftString = new SpannableStringBuilder();
+ if (draftCount == 1) {
+ draftString.append(sDraftSingularString);
+ } else {
+ draftString.append(sDraftPluralString
+ + String.format(sDraftCountFormatString, draftCount));
+ }
+ draftString.setSpan(CharacterStyle.wrap(sDraftsStyleSpan), 0,
+ draftString.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
+ messageInfo.append(draftString);
+ }
+ if (showSending) {
+ // If we are showing a message count or any draft text, prepend
+ // the sending state text with a comma.
+ if (count > 1 || draftCount > 0) {
+ messageInfo.append(sSendersSplitToken);
+ }
+ SpannableStringBuilder sending = new SpannableStringBuilder();
+ sending.append(sSendingString);
+ sending.setSpan(sSendingStyleSpan, 0, sending.length(), 0);
+ messageInfo.append(sending);
+ }
+ // Prepend a space if we are showing other message info text.
+ if (count > 1 || (draftCount > 0 && hasSenders) || showSending) {
+ messageInfo = new SpannableStringBuilder(sMessageCountSpacerString)
+ .append(messageInfo);
+ }
+ }
+ } finally {
+ if (!resourceCachingRequired) {
+ clearResourceCache();
}
}
- getSenderResources(context);
- if (conversationInfo != null) {
- int count = conversationInfo.messageCount;
- int draftCount = conversationInfo.draftCount;
- boolean showSending = sendingStatus == UIProvider.ConversationSendingState.SENDING;
- if (count > 1) {
- messageInfo.append(count + "");
- }
- messageInfo.setSpan(CharacterStyle.wrap(
- conv.read ? sMessageInfoReadStyleSpan : sMessageInfoUnreadStyleSpan),
- 0, messageInfo.length(), 0);
- if (draftCount > 0) {
- // If we are showing a message count or any draft text and there
- // is at least 1 sender, prepend the sending state text with a
- // comma.
- if (hasSenders || count > 1) {
- messageInfo.append(sSendersSplitToken);
- }
- SpannableStringBuilder draftString = new SpannableStringBuilder();
- if (draftCount == 1) {
- draftString.append(sDraftSingularString);
- } else {
- draftString.append(sDraftPluralString
- + String.format(sDraftCountFormatString, draftCount));
- }
- draftString.setSpan(CharacterStyle.wrap(sDraftsStyleSpan), 0, draftString.length(),
- Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
- messageInfo.append(draftString);
- }
- if (showSending) {
- // If we are showing a message count or any draft text, prepend
- // the sending state text with a comma.
- if (count > 1 || draftCount > 0) {
- messageInfo.append(sSendersSplitToken);
- }
- SpannableStringBuilder sending = new SpannableStringBuilder();
- sending.append(sSendingString);
- sending.setSpan(sSendingStyleSpan, 0, sending.length(), 0);
- messageInfo.append(sending);
- }
- // Prepend a space if we are showing other message info text.
- if (count > 1 || (draftCount > 0 && hasSenders) || showSending) {
- messageInfo = new SpannableStringBuilder(sMessageCountSpacerString)
- .append(messageInfo);
- }
- }
+
return messageInfo;
}
public static void format(Context context, ConversationInfo conversationInfo,
String messageInfo, int maxChars, ArrayList<SpannableString> styledSenders,
ArrayList<String> displayableSenderNames, ArrayList<String> displayableSenderEmails,
- String account) {
- getSenderResources(context);
- format(context, conversationInfo, messageInfo, maxChars, styledSenders,
- displayableSenderNames, displayableSenderEmails, account,
- sUnreadStyleSpan, sReadStyleSpan);
+ String account, final boolean resourceCachingRequired) {
+ try {
+ getSenderResources(context, resourceCachingRequired);
+ format(context, conversationInfo, messageInfo, maxChars, styledSenders,
+ displayableSenderNames, displayableSenderEmails, account,
+ sUnreadStyleSpan, sReadStyleSpan, resourceCachingRequired);
+ } finally {
+ if (!resourceCachingRequired) {
+ clearResourceCache();
+ }
+ }
}
public static void format(Context context, ConversationInfo conversationInfo,
String messageInfo, int maxChars, ArrayList<SpannableString> styledSenders,
ArrayList<String> displayableSenderNames, ArrayList<String> displayableSenderEmails,
String account, final TextAppearanceSpan notificationUnreadStyleSpan,
- final CharacterStyle notificationReadStyleSpan) {
- getSenderResources(context);
- handlePriority(context, maxChars, messageInfo, conversationInfo, styledSenders,
- displayableSenderNames, displayableSenderEmails, account,
- notificationUnreadStyleSpan, notificationReadStyleSpan);
+ final CharacterStyle notificationReadStyleSpan, final boolean resourceCachingRequired) {
+ try {
+ getSenderResources(context, resourceCachingRequired);
+ handlePriority(context, maxChars, messageInfo, conversationInfo, styledSenders,
+ displayableSenderNames, displayableSenderEmails, account,
+ notificationUnreadStyleSpan, notificationReadStyleSpan);
+ } finally {
+ if (!resourceCachingRequired) {
+ clearResourceCache();
+ }
+ }
}
public static void handlePriority(Context context, int maxChars, String messageInfoString,
@@ -363,25 +385,32 @@
}
private static void formatDefault(ConversationItemViewModel header, String sendersString,
- Context context, final CharacterStyle readStyleSpan) {
- getSenderResources(context);
- // Clear any existing sender fragments; we must re-make all of them.
- header.senderFragments.clear();
- String[] senders = TextUtils.split(sendersString, Address.ADDRESS_DELIMETER);
- String[] namesOnly = new String[senders.length];
- Rfc822Token[] senderTokens;
- String display;
- for (int i = 0; i < senders.length; i++) {
- senderTokens = Rfc822Tokenizer.tokenize(senders[i]);
- if (senderTokens != null && senderTokens.length > 0) {
- display = senderTokens[0].getName();
- if (TextUtils.isEmpty(display)) {
- display = senderTokens[0].getAddress();
+ Context context, final CharacterStyle readStyleSpan,
+ final boolean resourceCachingRequired) {
+ try {
+ getSenderResources(context, resourceCachingRequired);
+ // Clear any existing sender fragments; we must re-make all of them.
+ header.senderFragments.clear();
+ String[] senders = TextUtils.split(sendersString, Address.ADDRESS_DELIMETER);
+ String[] namesOnly = new String[senders.length];
+ Rfc822Token[] senderTokens;
+ String display;
+ for (int i = 0; i < senders.length; i++) {
+ senderTokens = Rfc822Tokenizer.tokenize(senders[i]);
+ if (senderTokens != null && senderTokens.length > 0) {
+ display = senderTokens[0].getName();
+ if (TextUtils.isEmpty(display)) {
+ display = senderTokens[0].getAddress();
+ }
+ namesOnly[i] = display;
}
- namesOnly[i] = display;
+ }
+ generateSenderFragments(header, namesOnly, readStyleSpan);
+ } finally {
+ if (!resourceCachingRequired) {
+ clearResourceCache();
}
}
- generateSenderFragments(header, namesOnly, readStyleSpan);
}
private static void generateSenderFragments(ConversationItemViewModel header, String[] names,
@@ -391,13 +420,31 @@
true);
}
- public static void formatSenders(ConversationItemViewModel header, Context context) {
- getSenderResources(context);
- formatSenders(header, context, sReadStyleSpan);
+ public static void formatSenders(ConversationItemViewModel header, Context context,
+ final boolean resourceCachingRequired) {
+ try {
+ getSenderResources(context, resourceCachingRequired);
+ formatSenders(header, context, sReadStyleSpan, resourceCachingRequired);
+ } finally {
+ if (!resourceCachingRequired) {
+ clearResourceCache();
+ }
+ }
}
public static void formatSenders(ConversationItemViewModel header, Context context,
- final CharacterStyle readStyleSpan) {
- formatDefault(header, header.conversation.senders, context, readStyleSpan);
+ final CharacterStyle readStyleSpan, final boolean resourceCachingRequired) {
+ try {
+ formatDefault(header, header.conversation.senders, context, readStyleSpan,
+ resourceCachingRequired);
+ } finally {
+ if (!resourceCachingRequired) {
+ clearResourceCache();
+ }
+ }
+ }
+
+ private static void clearResourceCache() {
+ sDraftSingularString = null;
}
}
diff --git a/src/com/android/mail/compose/ComposeActivity.java b/src/com/android/mail/compose/ComposeActivity.java
index acab1cc..77c29d7 100644
--- a/src/com/android/mail/compose/ComposeActivity.java
+++ b/src/com/android/mail/compose/ComposeActivity.java
@@ -38,8 +38,8 @@
import android.os.Bundle;
import android.os.Handler;
import android.os.HandlerThread;
-import android.os.Parcelable;
import android.os.ParcelFileDescriptor;
+import android.os.Parcelable;
import android.provider.BaseColumns;
import android.text.Editable;
import android.text.Html;
@@ -70,6 +70,7 @@
import com.android.ex.chips.RecipientEditTextView;
import com.android.mail.MailIntentService;
import com.android.mail.R;
+import com.android.mail.browse.MessageHeaderView;
import com.android.mail.compose.AttachmentsView.AttachmentAddedOrDeletedListener;
import com.android.mail.compose.AttachmentsView.AttachmentFailureException;
import com.android.mail.compose.FromAddressSpinner.OnAccountChangedListener;
@@ -86,10 +87,10 @@
import com.android.mail.providers.UIProvider;
import com.android.mail.providers.UIProvider.AccountCapabilities;
import com.android.mail.providers.UIProvider.DraftType;
+import com.android.mail.ui.AttachmentTile.AttachmentPreview;
import com.android.mail.ui.FeedbackEnabledActivity;
import com.android.mail.ui.MailActivity;
import com.android.mail.ui.WaitFragment;
-import com.android.mail.ui.AttachmentTile.AttachmentPreview;
import com.android.mail.utils.AccountUtils;
import com.android.mail.utils.AttachmentUtils;
import com.android.mail.utils.ContentProviderTask;
@@ -116,8 +117,9 @@
public class ComposeActivity extends Activity implements OnClickListener, OnNavigationListener,
RespondInlineListener, DialogInterface.OnClickListener, TextWatcher,
- AttachmentAddedOrDeletedListener, OnAccountChangedListener, LoaderManager.LoaderCallbacks<Cursor>,
- TextView.OnEditorActionListener, FeedbackEnabledActivity {
+ AttachmentAddedOrDeletedListener, OnAccountChangedListener,
+ LoaderManager.LoaderCallbacks<Cursor>, TextView.OnEditorActionListener,
+ FeedbackEnabledActivity {
// Identifiers for which type of composition this is
protected static final int COMPOSE = -1;
protected static final int REPLY = 0;
@@ -205,6 +207,7 @@
private static final String EXTRA_MESSAGE = "extraMessage";
private static final int REFERENCE_MESSAGE_LOADER = 0;
private static final int LOADER_ACCOUNT_CURSOR = 1;
+ private static final int INIT_DRAFT_USING_REFERENCE_MESSAGE = 2;
private static final String EXTRA_SELECTED_ACCOUNT = "selectedAccount";
private static final String TAG_WAIT = "wait-fragment";
private static final String MIME_TYPE_PHOTO = "image/*";
@@ -262,6 +265,7 @@
private RecipientTextWatcher mCcListener;
private RecipientTextWatcher mBccListener;
private Uri mRefMessageUri;
+ private boolean mShowQuotedText = false;
private Bundle mSavedInstanceState;
@@ -280,14 +284,14 @@
* Can be called from a non-UI thread.
*/
public static void editDraft(Context launcher, Account account, Message message) {
- launch(launcher, account, message, EDIT_DRAFT);
+ launch(launcher, account, message, EDIT_DRAFT, null, null);
}
/**
* Can be called from a non-UI thread.
*/
public static void compose(Context launcher, Account account) {
- launch(launcher, account, null, COMPOSE);
+ launch(launcher, account, null, COMPOSE, null, null);
}
/**
@@ -322,24 +326,30 @@
* Can be called from a non-UI thread.
*/
public static void reply(Context launcher, Account account, Message message) {
- launch(launcher, account, message, REPLY);
+ launch(launcher, account, message, REPLY, null, null);
}
/**
* Can be called from a non-UI thread.
*/
public static void replyAll(Context launcher, Account account, Message message) {
- launch(launcher, account, message, REPLY_ALL);
+ launch(launcher, account, message, REPLY_ALL, null, null);
}
/**
* Can be called from a non-UI thread.
*/
public static void forward(Context launcher, Account account, Message message) {
- launch(launcher, account, message, FORWARD);
+ launch(launcher, account, message, FORWARD, null, null);
}
- private static void launch(Context launcher, Account account, Message message, int action) {
+ public static void reportRenderingFeedback(Context launcher, Account account, Message message,
+ String body) {
+ launch(launcher, account, message, FORWARD, "android-gmail-readability@google.com", body);
+ }
+
+ private static void launch(Context launcher, Account account, Message message, int action,
+ String toAddress, String body) {
Intent intent = new Intent(launcher, ComposeActivity.class);
intent.putExtra(EXTRA_FROM_EMAIL_TASK, true);
intent.putExtra(EXTRA_ACTION, action);
@@ -349,6 +359,12 @@
} else {
intent.putExtra(EXTRA_IN_REFERENCE_TO_MESSAGE, message);
}
+ if (toAddress != null) {
+ intent.putExtra(EXTRA_TO, toAddress);
+ }
+ if (body != null) {
+ intent.putExtra(EXTRA_BODY, body);
+ }
launcher.startActivity(intent);
}
@@ -366,7 +382,7 @@
Intent intent = getIntent();
Message message;
ArrayList<AttachmentPreview> previews;
- boolean showQuotedText = false;
+ mShowQuotedText = false;
int action;
// Check for any of the possibly supplied accounts.;
Account account = null;
@@ -399,7 +415,8 @@
if (notificationFolder != null) {
final Intent clearNotifIntent =
new Intent(MailIntentService.ACTION_CLEAR_NEW_MAIL_NOTIFICATIONS);
- clearNotifIntent.putExtra(MailIntentService.ACCOUNT_EXTRA, account.name);
+ clearNotifIntent.setPackage(getPackageName());
+ clearNotifIntent.putExtra(MailIntentService.ACCOUNT_EXTRA, account);
clearNotifIntent.putExtra(MailIntentService.FOLDER_EXTRA, notificationFolder);
startService(clearNotifIntent);
@@ -417,14 +434,15 @@
}
if (mRefMessageUri != null) {
- // We have a referenced message that we must look up.
- getLoaderManager().initLoader(REFERENCE_MESSAGE_LOADER, null, this);
+ mShowQuotedText = true;
+ mComposeMode = action;
+ getLoaderManager().initLoader(INIT_DRAFT_USING_REFERENCE_MESSAGE, null, this);
return;
} else if (message != null && action != EDIT_DRAFT) {
initFromDraftMessage(message);
initQuotedTextFromRefMessage(mRefMessage, action);
showCcBcc(savedInstanceState);
- showQuotedText = message.appendRefMessageContent;
+ mShowQuotedText = message.appendRefMessageContent;
} else if (action == EDIT_DRAFT) {
initFromDraftMessage(message);
boolean showBcc = !TextUtils.isEmpty(message.getBcc());
@@ -446,17 +464,29 @@
action = COMPOSE;
break;
}
- initQuotedTextFromRefMessage(mRefMessage, action);
- showQuotedText = message.appendRefMessageContent;
+ LogUtils.d(LOG_TAG, "Previous draft had action type: %d", action);
+
+ mShowQuotedText = message.appendRefMessageContent;
+ if (message.refMessageUri != null) {
+ // If we're editing an existing draft that was in reference to an existing message,
+ // still need to load that original message since we might need to refer to the
+ // original sender and recipients if user switches "reply <-> reply-all".
+ mRefMessageUri = message.refMessageUri;
+ mComposeMode = action;
+ getLoaderManager().initLoader(REFERENCE_MESSAGE_LOADER, null, this);
+ return;
+ }
} else if ((action == REPLY || action == REPLY_ALL || action == FORWARD)) {
if (mRefMessage != null) {
initFromRefMessage(action);
- showQuotedText = true;
+ mShowQuotedText = true;
}
} else {
initFromExtras(intent);
}
- finishSetup(action, intent, savedInstanceState, showQuotedText);
+
+ mComposeMode = action;
+ finishSetup(action, intent, savedInstanceState);
}
private void checkValidAccounts() {
@@ -540,8 +570,7 @@
return account;
}
- private void finishSetup(int action, Intent intent, Bundle savedInstanceState,
- boolean showQuotedText) {
+ private void finishSetup(int action, Intent intent, Bundle savedInstanceState) {
setFocus(action);
if (action == COMPOSE) {
mQuotedTextView.setVisibility(View.GONE);
@@ -552,7 +581,7 @@
if (!hadSavedInstanceStateMessage(savedInstanceState)) {
initAttachmentsFromIntent(intent);
}
- initActionBar(action);
+ initActionBar();
initFromSpinner(savedInstanceState != null ? savedInstanceState : intent.getExtras(),
action);
@@ -564,7 +593,7 @@
initChangeListeners();
updateHideOrShowCcBcc();
- updateHideOrShowQuotedText(showQuotedText);
+ updateHideOrShowQuotedText(mShowQuotedText);
mRespondedInline = mSavedInstanceState != null ?
mSavedInstanceState.getBoolean(EXTRA_RESPONDED_INLINE) : false;
@@ -663,7 +692,7 @@
addAttachmentAndUpdateView(data);
mAddingAttachment = false;
} else if (request == RESULT_CREATE_ACCOUNT) {
- // We were waiting for the user to create an account
+ // We were waiting for the user to create an account
if (result != RESULT_OK) {
finish();
} else {
@@ -783,14 +812,12 @@
message.bodyHtml = fullBody.toString();
message.bodyText = mBodyView.getText().toString();
message.embedsExternalResources = false;
- message.refMessageId = mRefMessage != null ? mRefMessage.uri.toString() : null;
+ message.refMessageUri = mRefMessage != null ? mRefMessage.uri : null;
message.appendRefMessageContent = mQuotedTextView.getQuotedTextIfIncluded() != null;
ArrayList<Attachment> attachments = mAttachmentsView.getAttachments();
message.hasAttachments = attachments != null && attachments.size() > 0;
message.attachmentListUri = null;
message.messageFlags = 0;
- message.saveUri = null;
- message.sendUri = null;
message.alwaysShowImages = false;
message.attachmentsJson = Attachment.toJSONArray(attachments);
CharSequence quotedText = mQuotedTextView.getQuotedText();
@@ -1055,13 +1082,13 @@
mAttachmentsView.setAttachmentChangesListener(this);
}
- private void initActionBar(int action) {
- mComposeMode = action;
+ private void initActionBar() {
+ LogUtils.d(LOG_TAG, "initializing action bar in ComposeActivity");
ActionBar actionBar = getActionBar();
if (actionBar == null) {
return;
}
- if (action == ComposeActivity.COMPOSE) {
+ if (mComposeMode == ComposeActivity.COMPOSE) {
actionBar.setNavigationMode(ActionBar.NAVIGATION_MODE_STANDARD);
actionBar.setTitle(R.string.compose);
} else {
@@ -1071,7 +1098,7 @@
}
actionBar.setNavigationMode(ActionBar.NAVIGATION_MODE_LIST);
actionBar.setListNavigationCallbacks(mComposeModeAdapter, this);
- switch (action) {
+ switch (mComposeMode) {
case ComposeActivity.REPLY:
actionBar.setSelectedNavigationItem(0);
break;
@@ -1090,6 +1117,23 @@
private void initFromRefMessage(int action) {
setFieldsFromRefMessage(action);
+
+ // Check if To: address and email body needs to be prefilled based on extras.
+ // This is used for reporting rendering feedback.
+ if (MessageHeaderView.ENABLE_REPORT_RENDERING_PROBLEM) {
+ Intent intent = getIntent();
+ if (intent.getExtras() != null) {
+ String toAddresses = intent.getStringExtra(EXTRA_TO);
+ if (toAddresses != null) {
+ addToAddresses(Arrays.asList(TextUtils.split(toAddresses, ",")));
+ }
+ String body = intent.getStringExtra(EXTRA_BODY);
+ if (body != null) {
+ setBody(body, false /* withSignature */);
+ }
+ }
+ }
+
if (mRefMessage != null) {
// CC field only gets populated when doing REPLY_ALL.
// BCC never gets auto-populated, unless the user is editing
@@ -1959,29 +2003,16 @@
if (updateExistingMessage) {
sendOrSaveMessage.mValues.put(BaseColumns._ID, messageIdToSave);
- final Bundle result = callAccountSendSaveMethod(resolver,
+ callAccountSendSaveMethod(resolver,
selectedAccount.account, accountMethod, sendOrSaveMessage);
- if (result == null) {
- // TODO(pwestbro): Once Email supports the call api, remove this block
- // If null was returned, then the provider didn't handle the call method
- final Uri updateUri = Uri.parse(sendOrSaveMessage.mSave ?
- message.saveUri : message.sendUri);
- resolver.update(updateUri, sendOrSaveMessage.mValues, null, null);
- }
} else {
- final Uri messageUri;
+ Uri messageUri = null;
final Bundle result = callAccountSendSaveMethod(resolver,
selectedAccount.account, accountMethod, sendOrSaveMessage);
if (result != null) {
// If a non-null value was returned, then the provider handled the call
// method
messageUri = result.getParcelable(UIProvider.MessageColumns.URI);
- } else {
- // TODO(pwestbro): Once Email supports the call api, remove this block
- messageUri = resolver.insert(
- sendOrSaveMessage.mSave ? selectedAccount.account.saveDraftUri
- : selectedAccount.account.sendMessageUri,
- sendOrSaveMessage.mValues);
}
if (sendOrSaveMessage.mSave && messageUri != null) {
final Cursor messageCursor = resolver.query(messageUri,
@@ -3092,6 +3123,9 @@
@Override
public Loader<Cursor> onCreateLoader(int id, Bundle args) {
switch (id) {
+ case INIT_DRAFT_USING_REFERENCE_MESSAGE:
+ return new CursorLoader(this, mRefMessageUri, UIProvider.MESSAGE_PROJECTION, null,
+ null, null);
case REFERENCE_MESSAGE_LOADER:
return new CursorLoader(this, mRefMessageUri, UIProvider.MESSAGE_PROJECTION, null,
null, null);
@@ -3106,14 +3140,13 @@
public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
int id = loader.getId();
switch (id) {
- case REFERENCE_MESSAGE_LOADER:
+ case INIT_DRAFT_USING_REFERENCE_MESSAGE:
if (data != null && data.moveToFirst()) {
mRefMessage = new Message(data);
Intent intent = getIntent();
- int action = intent.getIntExtra(EXTRA_ACTION, COMPOSE);
- initFromRefMessage(action);
- finishSetup(action, intent, null, true);
- if (action != FORWARD) {
+ initFromRefMessage(mComposeMode);
+ finishSetup(mComposeMode, intent, null);
+ if (mComposeMode != FORWARD) {
String to = intent.getStringExtra(EXTRA_TO);
if (!TextUtils.isEmpty(to)) {
mRefMessage.setTo(null);
@@ -3127,6 +3160,13 @@
finish();
}
break;
+ case REFERENCE_MESSAGE_LOADER:
+ // Only populate mRefMessage and leave other fields untouched.
+ if (data != null && data.moveToFirst()) {
+ mRefMessage = new Message(data);
+ }
+ finishSetup(mComposeMode, getIntent(), mSavedInstanceState);
+ break;
case LOADER_ACCOUNT_CURSOR:
if (data != null && data.moveToFirst()) {
// there are accounts now!
diff --git a/src/com/android/mail/content/CursorCreator.java b/src/com/android/mail/content/CursorCreator.java
new file mode 100644
index 0000000..f4d1abb
--- /dev/null
+++ b/src/com/android/mail/content/CursorCreator.java
@@ -0,0 +1,34 @@
+/*
+ * 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.content;
+
+import android.database.Cursor;
+
+/**
+ * An object that knows how to create its implementing class using a single row of a cursor alone.
+ * @param <T>
+ */
+public interface CursorCreator<T> {
+
+ /**
+ * Creates an object using the current row of the cursor given here. The implementation should
+ * not advance/rewind the cursor, and is only allowed to read the existing row.
+ * @param c
+ * @return a real object of the implementing class.
+ */
+ T createFromCursor(Cursor c);
+}
diff --git a/src/com/android/mail/content/ObjectCursor.java b/src/com/android/mail/content/ObjectCursor.java
new file mode 100644
index 0000000..63c3ea4
--- /dev/null
+++ b/src/com/android/mail/content/ObjectCursor.java
@@ -0,0 +1,94 @@
+/*
+ * 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.content;
+
+import android.database.Cursor;
+import android.database.CursorWrapper;
+import android.util.SparseArray;
+
+/**
+ * A cursor-backed type that can return an object for each row of the cursor. This class is most
+ * useful when:
+ * 1. The cursor is returned in conjuction with an AsyncTaskLoader and created off the UI thread.
+ * 2. A single row in the cursor specifies everything for an object.
+ */
+public class ObjectCursor <T> extends CursorWrapper {
+ /** The cache for objects in the underlying cursor. */
+ private final SparseArray<T> mCache;
+ /** An object that knows how to construct {@link T} objects using cursors. */
+ private final CursorCreator<T> mFactory;
+
+ /**
+ * Creates a new object cursor.
+ * @param cursor the underlying cursor this wraps.
+ */
+ public ObjectCursor(Cursor cursor, CursorCreator<T> factory) {
+ super(cursor);
+ if (cursor != null) {
+ mCache = new SparseArray<T>(cursor.getCount());
+ } else {
+ mCache = null;
+ }
+ mFactory = factory;
+ }
+
+ /**
+ * Create a concrete object at the current cursor position. There is no guarantee on object
+ * creation: an object might have been previously created, or the cache might be populated
+ * by calling {@link #fillCache()}. In both these cases, the previously created object is
+ * returned.
+ * @return a model
+ */
+ public final T getModel() {
+ final Cursor c = getWrappedCursor();
+ if (c == null ) {
+ return null;
+ }
+ final int currentPosition = c.getPosition();
+ // The cache contains this object, return it.
+ final T prev = mCache.get(currentPosition);
+ if (prev != null) {
+ return prev;
+ }
+ // Get the object at the current position and add it to the cache.
+ final T model = mFactory.createFromCursor(c);
+ mCache.put(currentPosition, model);
+ return model;
+ }
+
+ /**
+ * Reads the entire cursor to populate the objects in the cache. Subsequent calls to {@link
+ * #getModel()} will return the cached objects as far as the underlying cursor does not change.
+ */
+ final void fillCache() {
+ final Cursor c = getWrappedCursor();
+ if (c == null || !c.moveToFirst()) {
+ return;
+ }
+ do {
+ // As a side effect of getModel, the model is cached away.
+ getModel();
+ } while (c.moveToNext());
+ }
+
+ @Override
+ public void close() {
+ super.close();
+ mCache.clear();
+ }
+
+}
diff --git a/src/com/android/mail/content/ObjectCursorLoader.java b/src/com/android/mail/content/ObjectCursorLoader.java
new file mode 100644
index 0000000..a9a930f
--- /dev/null
+++ b/src/com/android/mail/content/ObjectCursorLoader.java
@@ -0,0 +1,170 @@
+/*
+ * 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.content;
+
+import com.android.mail.utils.LogTag;
+
+import android.content.AsyncTaskLoader;
+import android.content.Context;
+import android.database.Cursor;
+import android.net.Uri;
+
+import java.io.FileDescriptor;
+import java.io.PrintWriter;
+import java.util.Arrays;
+
+/**
+ * A copy of the framework's {@link android.content.CursorLoader} class. Copied because
+ * CursorLoader is not parameterized, and we want to parameterize over the underlying cursor type.
+ * @param <T>
+ */
+public class ObjectCursorLoader<T> extends AsyncTaskLoader<ObjectCursor<T>> {
+ final ForceLoadContentObserver mObserver;
+ protected static final String LOG_TAG = LogTag.getLogTag();
+
+ final Uri mUri;
+ final String[] mProjection;
+ // Copied over from CursorLoader, but none of our uses specify this. So these are hardcoded to
+ // null right here.
+ final String mSelection = null;
+ final String[] mSelectionArgs = null;
+ final String mSortOrder = null;
+
+ /** The underlying cursor that contains the data. */
+ ObjectCursor<T> mCursor;
+
+ /** The factory that knows how to create T objects from cursors: one object per row. */
+ private final CursorCreator<T> mFactory;
+
+ public ObjectCursorLoader(Context context, Uri uri, String[] projection,
+ CursorCreator<T> factory) {
+ super(context);
+
+ /*
+ * If these are null, it's going to crash anyway in loadInBackground(), but this stack trace
+ * is much more useful.
+ */
+ if (uri == null) {
+ throw new NullPointerException("The uri cannot be null");
+ }
+ if (factory == null) {
+ throw new NullPointerException("The factory cannot be null");
+ }
+
+ mObserver = new ForceLoadContentObserver();
+ mUri = uri;
+ mProjection = projection;
+ mFactory = factory;
+ }
+
+ /* Runs on a worker thread */
+ @Override
+ public ObjectCursor<T> loadInBackground() {
+ final Cursor inner = getContext().getContentResolver().query(mUri, mProjection,
+ mSelection, mSelectionArgs, mSortOrder);
+ if (inner != null) {
+ // Ensure the cursor window is filled
+ inner.getCount();
+ inner.registerContentObserver(mObserver);
+ }
+ // Modifications to the ObjectCursor, create an Object Cursor and fill the cache.
+ final ObjectCursor<T> cursor = new ObjectCursor<T>(inner, mFactory);
+ cursor.fillCache();
+ return cursor;
+ }
+
+ /* Runs on the UI thread */
+ @Override
+ public void deliverResult(ObjectCursor<T> cursor) {
+ if (isReset()) {
+ // An async query came in while the loader is stopped
+ if (cursor != null) {
+ cursor.close();
+ }
+ return;
+ }
+ final Cursor oldCursor = mCursor;
+ mCursor = cursor;
+
+ if (isStarted()) {
+ super.deliverResult(cursor);
+ }
+
+ if (oldCursor != null && oldCursor != cursor && !oldCursor.isClosed()) {
+ oldCursor.close();
+ }
+ }
+
+ /**
+ * Starts an asynchronous load of the contacts list data. When the result is ready the callbacks
+ * will be called on the UI thread. If a previous load has been completed and is still valid
+ * the result may be passed to the callbacks immediately.
+ *
+ * Must be called from the UI thread
+ */
+ @Override
+ protected void onStartLoading() {
+ if (mCursor != null) {
+ deliverResult(mCursor);
+ }
+ if (takeContentChanged() || mCursor == null) {
+ forceLoad();
+ }
+ }
+
+ /**
+ * Must be called from the UI thread
+ */
+ @Override
+ protected void onStopLoading() {
+ // Attempt to cancel the current load task if possible.
+ cancelLoad();
+ }
+
+ @Override
+ public void onCanceled(ObjectCursor<T> cursor) {
+ if (cursor != null && !cursor.isClosed()) {
+ cursor.close();
+ }
+ }
+
+ @Override
+ protected void onReset() {
+ super.onReset();
+
+ // Ensure the loader is stopped
+ onStopLoading();
+
+ if (mCursor != null && !mCursor.isClosed()) {
+ mCursor.close();
+ }
+ mCursor = null;
+ }
+
+ @Override
+ public void dump(String prefix, FileDescriptor fd, PrintWriter writer, String[] args) {
+ super.dump(prefix, fd, writer, args);
+ writer.print(prefix); writer.print("mUri="); writer.println(mUri);
+ writer.print(prefix); writer.print("mProjection=");
+ writer.println(Arrays.toString(mProjection));
+ writer.print(prefix); writer.print("mSelection="); writer.println(mSelection);
+ writer.print(prefix); writer.print("mSelectionArgs=");
+ writer.println(Arrays.toString(mSelectionArgs));
+ writer.print(prefix); writer.print("mSortOrder="); writer.println(mSortOrder);
+ writer.print(prefix); writer.print("mCursor="); writer.println(mCursor);
+ }
+}
diff --git a/src/com/android/mail/perf/SimpleTimer.java b/src/com/android/mail/perf/SimpleTimer.java
index a59b17d..8b45b0a 100644
--- a/src/com/android/mail/perf/SimpleTimer.java
+++ b/src/com/android/mail/perf/SimpleTimer.java
@@ -1,25 +1,39 @@
-// Copyright 2011 Google Inc. All Rights Reserved.
-
+/*
+ * 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.perf;
import android.os.SystemClock;
+import android.text.TextUtils;
-import com.android.mail.utils.LogTag;
import com.android.mail.utils.LogUtils;
-import com.android.mail.utils.Utils;
/**
- * A simple perf timer class that supports lap-time-style measurements. Once a timer is started,
- * any number of laps can be marked, but they are all relative to the original start time.
- *
+ * A simple perf timer class that supports lap-time-style measurements. Once a
+ * timer is started, any number of laps can be marked, but they are all relative
+ * to the original start time.
*/
public class SimpleTimer {
- private static final boolean ENABLE_SIMPLE_TIMER = true;
- private static final String LOG_TAG = LogTag.getLogTag();
+ private static final String DEFAULT_LOG_TAG = "SimpleTimer";
+
+ private static final boolean ENABLE_SIMPLE_TIMER = false;
private final boolean mEnabled;
private long mStartTime;
+ private long mLastMarkTime;
private String mSessionName;
public SimpleTimer() {
@@ -30,34 +44,32 @@
mEnabled = enabled;
}
- public boolean isEnabled() {
- return ENABLE_SIMPLE_TIMER && LogUtils.isLoggable(LOG_TAG, LogUtils.DEBUG)
+ public final boolean isEnabled() {
+ return ENABLE_SIMPLE_TIMER && LogUtils.isLoggable(getTag(), LogUtils.DEBUG)
&& mEnabled;
}
- public void start() {
- start(null);
+ public SimpleTimer withSessionName(String sessionName) {
+ mSessionName = sessionName;
+ return this;
}
- public void start(String sessionName) {
- mStartTime = SystemClock.uptimeMillis();
- mSessionName = sessionName;
+ public void start() {
+ mStartTime = mLastMarkTime = SystemClock.uptimeMillis();
+ LogUtils.d(getTag(), "timer START");
}
public void mark(String msg) {
if (isEnabled()) {
- StringBuilder sb = new StringBuilder();
- if (mSessionName != null) {
- sb.append("(");
- sb.append(mSessionName);
- sb.append(") ");
- }
- sb.append(msg);
- sb.append(": ");
- sb.append(SystemClock.uptimeMillis() - mStartTime);
- sb.append("ms elapsed");
- LogUtils.d(LOG_TAG, sb.toString());
+ long now = SystemClock.uptimeMillis();
+ LogUtils.d(getTag(), "[%s] %sms elapsed (%sms since last mark)", msg, now - mStartTime,
+ now - mLastMarkTime);
+ mLastMarkTime = now;
}
}
+ private String getTag() {
+ return TextUtils.isEmpty(mSessionName) ? DEFAULT_LOG_TAG : mSessionName;
+ }
+
}
diff --git a/src/com/android/mail/perf/Timer.java b/src/com/android/mail/perf/Timer.java
index 57e5a27..668fdfd 100644
--- a/src/com/android/mail/perf/Timer.java
+++ b/src/com/android/mail/perf/Timer.java
@@ -1,5 +1,18 @@
-// Copyright 2010 Google Inc. All Rights Reserved.
-
+/*
+ * 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.perf;
import com.android.mail.utils.LogTag;
diff --git a/src/com/android/mail/photo/MailPhotoViewActivity.java b/src/com/android/mail/photo/MailPhotoViewActivity.java
index 787f660..f3c0ede 100644
--- a/src/com/android/mail/photo/MailPhotoViewActivity.java
+++ b/src/com/android/mail/photo/MailPhotoViewActivity.java
@@ -233,12 +233,11 @@
@Override
public void onClick(View view) {
downloadAttachment();
+ emptyText.setVisibility(View.GONE);
+ retryButton.setVisibility(View.GONE);
}
});
progressBar.setVisibility(View.GONE);
- } else {
- emptyText.setVisibility(View.GONE);
- retryButton.setVisibility(View.GONE);
}
}
diff --git a/src/com/android/mail/preferences/AccountPreferences.java b/src/com/android/mail/preferences/AccountPreferences.java
new file mode 100644
index 0000000..4471942
--- /dev/null
+++ b/src/com/android/mail/preferences/AccountPreferences.java
@@ -0,0 +1,96 @@
+/*
+ * 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.preferences;
+
+import com.google.common.collect.ImmutableSet;
+
+import android.content.Context;
+
+import com.android.mail.MailIntentService;
+
+/**
+ * Preferences relevant to one specific account.
+ */
+public class AccountPreferences extends VersionedPrefs {
+
+ private static final String PREFS_NAME_PREFIX = "Account";
+
+ public static final class PreferenceKeys {
+ /**
+ * A temporary preference that can be set during account setup, if we do not know what the
+ * default inbox is yet. This value should be moved into the appropriate
+ * {@link FolderPreferences} once we have the inbox, and removed from here.
+ */
+ private static final String DEFAULT_INBOX_NOTIFICATIONS_ENABLED =
+ "inbox-notifications-enabled";
+
+ /** Boolean value indicating whether notifications are enabled */
+ public static final String NOTIFICATIONS_ENABLED = "notifications-enabled";
+
+ public static final ImmutableSet<String> BACKUP_KEYS =
+ new ImmutableSet.Builder<String>().add(NOTIFICATIONS_ENABLED).build();
+ }
+
+ /**
+ * @param account The account name. This must never change for the account.
+ */
+ public AccountPreferences(final Context context, final String account) {
+ super(context, buildSharedPrefsName(account));
+ }
+
+ private static String buildSharedPrefsName(final String account) {
+ return PREFS_NAME_PREFIX + '-' + account;
+ }
+
+ @Override
+ protected void performUpgrade(final int oldVersion, final int newVersion) {
+ if (oldVersion > newVersion) {
+ throw new IllegalStateException(
+ "You appear to have downgraded your app. Please clear app data.");
+ }
+ }
+
+ @Override
+ protected boolean canBackup(final String key) {
+ return PreferenceKeys.BACKUP_KEYS.contains(key);
+ }
+
+ public boolean isDefaultInboxNotificationsEnabledSet() {
+ return getSharedPreferences().contains(PreferenceKeys.DEFAULT_INBOX_NOTIFICATIONS_ENABLED);
+ }
+
+ public boolean getDefaultInboxNotificationsEnabled() {
+ return getSharedPreferences()
+ .getBoolean(PreferenceKeys.DEFAULT_INBOX_NOTIFICATIONS_ENABLED, true);
+ }
+
+ public void setDefaultInboxNotificationsEnabled(final boolean enabled) {
+ getEditor().putBoolean(PreferenceKeys.DEFAULT_INBOX_NOTIFICATIONS_ENABLED, enabled).apply();
+ }
+
+ public void clearDefaultInboxNotificationsEnabled() {
+ getEditor().remove(PreferenceKeys.DEFAULT_INBOX_NOTIFICATIONS_ENABLED).apply();
+ }
+
+ public boolean areNotificationsEnabled() {
+ return getSharedPreferences().getBoolean(PreferenceKeys.NOTIFICATIONS_ENABLED, true);
+ }
+
+ public void setNotificationsEnabled(final boolean enabled) {
+ getEditor().putBoolean(PreferenceKeys.NOTIFICATIONS_ENABLED, enabled).apply();
+ MailIntentService.broadcastBackupDataChanged(getContext());
+ }
+}
diff --git a/src/com/android/mail/preferences/BackupSharedPreference.java b/src/com/android/mail/preferences/BackupSharedPreference.java
new file mode 100644
index 0000000..37a27f8
--- /dev/null
+++ b/src/com/android/mail/preferences/BackupSharedPreference.java
@@ -0,0 +1,31 @@
+/*
+ * 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.preferences;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+
+/**
+ * Wraps around various classes used in Gmail's backup/restore mechanism.
+ */
+public interface BackupSharedPreference {
+ String getKey();
+
+ Object getValue();
+
+ JSONObject toJson() throws JSONException;
+}
diff --git a/src/com/android/mail/preferences/BasePreferenceMigrator.java b/src/com/android/mail/preferences/BasePreferenceMigrator.java
new file mode 100644
index 0000000..0e0b766
--- /dev/null
+++ b/src/com/android/mail/preferences/BasePreferenceMigrator.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright (C) 2012 Google Inc.
+ * Licensed to The Android Open Source Project.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.mail.preferences;
+
+import android.content.Context;
+
+import java.util.concurrent.atomic.AtomicBoolean;
+
+/**
+ * Interface to allow migrating preferences from other projects into the UnifiedEmail code, so apps
+ * can slowly move their preferences into the shared code.
+ */
+public abstract class BasePreferenceMigrator {
+ /** If <code>true</code>, we have not attempted a migration since the app started. */
+ private static final AtomicBoolean sMigrationNecessary = new AtomicBoolean(true);
+
+ public final void performMigration(
+ final Context context, final int oldVersion, final int newVersion) {
+ // Ensure we only run this once
+ if (sMigrationNecessary.getAndSet(false)) {
+ migrate(context, oldVersion, newVersion);
+ }
+ }
+
+ /**
+ * Migrates preferences to UnifiedEmail.
+ *
+ * @param oldVersion The previous version of UnifiedEmail's preferences
+ * @param newVersion The new version of UnifiedEmail's preferences
+ */
+ protected abstract void migrate(final Context context, int oldVersion, int newVersion);
+}
diff --git a/src/com/android/mail/preferences/FolderPreferences.java b/src/com/android/mail/preferences/FolderPreferences.java
new file mode 100644
index 0000000..c62f3c6
--- /dev/null
+++ b/src/com/android/mail/preferences/FolderPreferences.java
@@ -0,0 +1,287 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.mail.preferences;
+
+import com.google.android.common.base.Strings;
+import com.google.common.collect.ImmutableSet;
+
+import android.content.ContentUris;
+import android.content.Context;
+import android.database.Cursor;
+import android.media.RingtoneManager;
+import android.net.Uri;
+import android.provider.Settings;
+
+import com.android.mail.MailIntentService;
+import com.android.mail.providers.Folder;
+import com.android.mail.providers.UIProvider.FolderCapabilities;
+import com.android.mail.utils.NotificationActionUtils.NotificationActionType;
+
+import java.util.LinkedHashSet;
+import java.util.Set;
+
+/**
+ * Preferences relevant to one specific folder. In Email, this would only be used for an account's
+ * inbox. In Gmail, this is used for every account/label pair.
+ */
+public class FolderPreferences extends VersionedPrefs {
+
+ private static final String PREFS_NAME_PREFIX = "Folder";
+
+ public static final class PreferenceKeys {
+ /** Boolean value indicating whether notifications are enabled */
+ public static final String NOTIFICATIONS_ENABLED = "notifications-enabled";
+ /** String value of the notification ringtone URI */
+ public static final String NOTIFICATION_RINGTONE = "notification-ringtone";
+ /** Boolean value indicating whether we should explicitly vibrate */
+ public static final String NOTIFICATION_VIBRATE = "notification-vibrate";
+ /**
+ * Boolean value indicating whether we notify for every message (<code>true</code>), or just
+ * once for the folder (<code>false</code>)
+ */
+ public static final String NOTIFICATION_NOTIFY_EVERY_MESSAGE =
+ "notification-notify-every-message";
+ /** String set of the notification actions (from {@link NotificationActionType} */
+ public static final String NOTIFICATION_ACTIONS = "notification-actions";
+
+ public static final ImmutableSet<String> BACKUP_KEYS =
+ new ImmutableSet.Builder<String>()
+ .add(NOTIFICATIONS_ENABLED)
+ .add(NOTIFICATION_RINGTONE)
+ .add(NOTIFICATION_VIBRATE)
+ .add(NOTIFICATION_NOTIFY_EVERY_MESSAGE)
+ .build();
+ }
+
+ private final Folder mFolder;
+ /** An id that is constant across app installations. */
+ private final String mPersistentId;
+ private final boolean mUseInboxDefaultNotificationSettings;
+
+ /**
+ * @param account The account name. This must never change for the account.
+ * @param folder The folder
+ */
+ public FolderPreferences(final Context context, final String account, final Folder folder,
+ final boolean useInboxDefaultNotificationSettings) {
+ this(context, account, folder, folder.persistentId, useInboxDefaultNotificationSettings);
+ }
+
+ /**
+ * A constructor that can be used when no {@link Folder} object is available (like during a
+ * restore). While this will probably function as expected at other times,
+ * {@link #FolderPreferences(Context, String, Folder, boolean)} should be used if at all
+ * possible.
+ *
+ * @param account The account name. This must never change for the account.
+ * @param persistentId An identifier for the folder that does not change across app
+ * installations.
+ */
+ public FolderPreferences(final Context context, final String account, final String persistentId,
+ final boolean useInboxDefaultNotificationSettings) {
+ this(context, account, null, persistentId, useInboxDefaultNotificationSettings);
+ }
+
+ private FolderPreferences(final Context context, final String account, final Folder folder,
+ final String persistentId, final boolean useInboxDefaultNotificationSettings) {
+ super(context, buildSharedPrefsName(account, persistentId));
+ mFolder = folder;
+ mPersistentId = persistentId;
+ mUseInboxDefaultNotificationSettings = useInboxDefaultNotificationSettings;
+ }
+
+ private static String buildSharedPrefsName(final String account, final String persistentId) {
+ return PREFS_NAME_PREFIX + '-' + account + '-' + persistentId;
+ }
+
+ @Override
+ protected void performUpgrade(final int oldVersion, final int newVersion) {
+ if (oldVersion > newVersion) {
+ throw new IllegalStateException(
+ "You appear to have downgraded your app. Please clear app data.");
+ }
+ }
+
+ public String getPersistentId() {
+ return mPersistentId;
+ }
+
+ @Override
+ protected boolean canBackup(final String key) {
+ if (mPersistentId == null) {
+ return false;
+ }
+
+ return PreferenceKeys.BACKUP_KEYS.contains(key);
+ }
+
+ @Override
+ protected Object getBackupValue(final String key, final Object value) {
+ if (PreferenceKeys.NOTIFICATION_RINGTONE.equals(key)) {
+ return getRingtoneTitle((String) value);
+ }
+
+ return super.getBackupValue(key, value);
+ }
+
+ @Override
+ protected Object getRestoreValue(final String key, final Object value) {
+ if (PreferenceKeys.NOTIFICATION_RINGTONE.equals(key)) {
+ return getRingtoneUri((String) value);
+ }
+
+ return super.getBackupValue(key, value);
+ }
+
+ private String getRingtoneTitle(final String ringtoneUriString) {
+ if (ringtoneUriString.length() == 0) {
+ return ringtoneUriString;
+ }
+ final Uri uri = Uri.parse(ringtoneUriString);
+ if (RingtoneManager.isDefault(uri)) {
+ return ringtoneUriString;
+ }
+ final RingtoneManager ringtoneManager = new RingtoneManager(getContext());
+ ringtoneManager.setType(RingtoneManager.TYPE_NOTIFICATION);
+ final Cursor cursor = ringtoneManager.getCursor();
+ try {
+ while (cursor.moveToNext()) {
+ final Uri cursorUri = ContentUris.withAppendedId(
+ Uri.parse(cursor.getString(RingtoneManager.URI_COLUMN_INDEX)),
+ cursor.getLong(RingtoneManager.ID_COLUMN_INDEX));
+ if (cursorUri.toString().equals(ringtoneUriString)) {
+ final String title = cursor.getString(RingtoneManager.TITLE_COLUMN_INDEX);
+ if (!Strings.isNullOrEmpty(title)) {
+ return title;
+ }
+ }
+ }
+ } finally {
+ cursor.close();
+ }
+ return null;
+ }
+
+ private String getRingtoneUri(final String name) {
+ if (name.length() == 0 || RingtoneManager.isDefault(Uri.parse(name))) {
+ return name;
+ }
+
+ final RingtoneManager ringtoneManager = new RingtoneManager(getContext());
+ ringtoneManager.setType(RingtoneManager.TYPE_NOTIFICATION);
+ final Cursor cursor = ringtoneManager.getCursor();
+ try {
+ while (cursor.moveToNext()) {
+ String title = cursor.getString(RingtoneManager.TITLE_COLUMN_INDEX);
+ if (name.equals(title)) {
+ Uri uri = ContentUris.withAppendedId(
+ Uri.parse(cursor.getString(RingtoneManager.URI_COLUMN_INDEX)),
+ cursor.getLong(RingtoneManager.ID_COLUMN_INDEX));
+ return uri.toString();
+ }
+ }
+ } finally {
+ cursor.close();
+ }
+ return null;
+ }
+
+ /**
+ * If <code>true</code>, we use inbox-defaults for notification settings. If <code>false</code>,
+ * we use standard defaults.
+ */
+ private boolean getUseInboxDefaultNotificationSettings() {
+ return mUseInboxDefaultNotificationSettings;
+ }
+
+ public boolean isNotificationsEnabledSet() {
+ return getSharedPreferences().contains(PreferenceKeys.NOTIFICATIONS_ENABLED);
+ }
+
+ public boolean areNotificationsEnabled() {
+ return getSharedPreferences().getBoolean(
+ PreferenceKeys.NOTIFICATIONS_ENABLED, getUseInboxDefaultNotificationSettings());
+ }
+
+ public void setNotificationsEnabled(final boolean enabled) {
+ getEditor().putBoolean(PreferenceKeys.NOTIFICATIONS_ENABLED, enabled).apply();
+ MailIntentService.broadcastBackupDataChanged(getContext());
+ }
+
+ public String getNotificationRingtoneUri() {
+ return getSharedPreferences().getString(PreferenceKeys.NOTIFICATION_RINGTONE,
+ Settings.System.DEFAULT_NOTIFICATION_URI.toString());
+ }
+
+ public void setNotificationRingtoneUri(final String uri) {
+ getEditor().putString(PreferenceKeys.NOTIFICATION_RINGTONE, uri).apply();
+ MailIntentService.broadcastBackupDataChanged(getContext());
+ }
+
+ public boolean isNotificationVibrateEnabled() {
+ return getSharedPreferences().getBoolean(PreferenceKeys.NOTIFICATION_VIBRATE, false);
+ }
+
+ public void setNotificationVibrateEnabled(final boolean enabled) {
+ getEditor().putBoolean(PreferenceKeys.NOTIFICATION_VIBRATE, enabled).apply();
+ MailIntentService.broadcastBackupDataChanged(getContext());
+ }
+
+ public boolean isEveryMessageNotificationEnabled() {
+ return getSharedPreferences()
+ .getBoolean(PreferenceKeys.NOTIFICATION_NOTIFY_EVERY_MESSAGE, false);
+ }
+
+ public void setEveryMessageNotificationEnabled(final boolean enabled) {
+ getEditor().putBoolean(PreferenceKeys.NOTIFICATION_NOTIFY_EVERY_MESSAGE, enabled).apply();
+ MailIntentService.broadcastBackupDataChanged(getContext());
+ }
+
+ private Set<String> getDefaultNotificationActions(final Context context) {
+ final boolean supportsArchive = mFolder.supportsCapability(FolderCapabilities.ARCHIVE);
+ final boolean supportsRemoveLabel =
+ mFolder.supportsCapability(FolderCapabilities.ALLOWS_REMOVE_CONVERSATION);
+ // Use the swipe setting, since it is essentially a way to allow the user to specify
+ // whether they prefer archive or delete, without adding another setting
+ final boolean preferDelete =
+ MailPrefs.ConversationListSwipeActions.DELETE.equals(MailPrefs.get(context)
+ .getConversationListSwipeAction(true /* supportsArchive */));
+ final NotificationActionType destructiveActionType =
+ (supportsArchive || supportsRemoveLabel) && !preferDelete ?
+ NotificationActionType.ARCHIVE_REMOVE_LABEL : NotificationActionType.DELETE;
+ final String destructiveAction = destructiveActionType.getPersistedValue();
+
+ final String replyAction =
+ MailPrefs.get(context).getDefaultReplyAll() ? NotificationActionType.REPLY_ALL
+ .getPersistedValue()
+ : NotificationActionType.REPLY.getPersistedValue();
+
+ final Set<String> notificationActions = new LinkedHashSet<String>(2);
+ notificationActions.add(destructiveAction);
+ notificationActions.add(replyAction);
+
+ return notificationActions;
+ }
+
+ /**
+ * Gets the notification settings configured for this account and label, or the default if none
+ * have been set.
+ */
+ public Set<String> getNotificationActions() {
+ return getSharedPreferences().getStringSet(
+ PreferenceKeys.NOTIFICATION_ACTIONS, getDefaultNotificationActions(getContext()));
+ }
+}
diff --git a/src/com/android/mail/preferences/MailPrefs.java b/src/com/android/mail/preferences/MailPrefs.java
index 694654d..5a0b0d7 100644
--- a/src/com/android/mail/preferences/MailPrefs.java
+++ b/src/com/android/mail/preferences/MailPrefs.java
@@ -17,37 +17,70 @@
package com.android.mail.preferences;
-import android.content.Context;
-import android.content.SharedPreferences;
-import android.content.SharedPreferences.Editor;
+import com.google.common.collect.ImmutableSet;
+import android.content.Context;
+
+import com.android.mail.MailIntentService;
import com.android.mail.providers.Account;
-import com.android.mail.providers.Folder;
+import com.android.mail.providers.UIProvider;
+import com.android.mail.widget.BaseWidgetProvider;
+
+import java.util.Set;
/**
* A high-level API to store and retrieve unified mail preferences.
* <p>
* This will serve as an eventual replacement for Gmail's Persistence class.
*/
-public final class MailPrefs {
+public final class MailPrefs extends VersionedPrefs {
- public static final boolean SHOW_EXPERIMENTAL_PREFS = false;
-
- // TODO: support account-specific prefs. probably just use a different prefs name instead of
- // prepending every key.
+ public static final boolean SHOW_EXPERIMENTAL_PREFS = true;
private static final String PREFS_NAME = "UnifiedEmail";
private static MailPrefs sInstance;
- private final SharedPreferences mPrefs;
- private static final String WIDGET_ACCOUNT_PREFIX = "widget-account-";
- private static final String ACCOUNT_FOLDER_PREFERENCE_SEPARATOR = " ";
+ public static final class PreferenceKeys {
+ private static final String MIGRATED_VERSION = "migrated-version";
- private static final String ENABLE_FTS = "enable-fts";
- private static final String ENABLE_CHIP_DRAG_AND_DROP = "enable-chip-drag-and-drop";
- public static final String ENABLE_CONVLIST_PHOTOS = "enable-convlist-photos";
- public static final String ENABLE_WHOOSH_ZOOM = "enable-whoosh-zoom";
+ public static final String WIDGET_ACCOUNT_PREFIX = "widget-account-";
+
+ /** Hidden preference to indicate what version a "What's New" dialog was last shown for. */
+ public static final String WHATS_NEW_LAST_SHOWN_VERSION = "whats-new-last-shown-version";
+
+ /**
+ * A boolean that, if <code>true</code>, means we should default all replies to "reply all"
+ */
+ public static final String DEFAULT_REPLY_ALL = "default-reply-all";
+
+ public static final String CONVERSATION_LIST_SWIPE_ACTION =
+ "conversation-list-swipe-action";
+
+ /** Hidden preference used to cache the active notification set */
+ private static final String CACHED_ACTIVE_NOTIFICATION_SET =
+ "cache-active-notification-set";
+
+ public static final ImmutableSet<String> BACKUP_KEYS =
+ new ImmutableSet.Builder<String>()
+ .add(DEFAULT_REPLY_ALL)
+ .add(CONVERSATION_LIST_SWIPE_ACTION)
+ .build();
+
+ private static final String ENABLE_CHIP_DRAG_AND_DROP = "enable-chip-drag-and-drop";
+ public static final String ENABLE_CONVLIST_PHOTOS = "enable-convlist-photos";
+ public static final String ENABLE_WHOOSH_ZOOM = "enable-whoosh-zoom";
+ public static final String ENABLE_MUNGE_TABLES = "enable-munge-tables";
+ public static final String ENABLE_MUNGE_IMAGES = "enable-munge-images";
+ public static final String ENABLE_SECTIONED_INBOX_EXPERIMENT = "enable-sectioned-inbox";
+
+ }
+
+ public static final class ConversationListSwipeActions {
+ public static final String ARCHIVE = "archive";
+ public static final String DELETE = "delete";
+ public static final String DISABLED = "disabled";
+ }
public static MailPrefs get(Context c) {
if (sInstance == null) {
@@ -57,70 +90,173 @@
}
private MailPrefs(Context c) {
- mPrefs = c.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE);
+ super(c, PREFS_NAME);
}
- public String getSharedPreferencesName() {
- return PREFS_NAME;
+ @Override
+ protected void performUpgrade(final int oldVersion, final int newVersion) {
+ if (oldVersion > newVersion) {
+ throw new IllegalStateException(
+ "You appear to have downgraded your app. Please clear app data.");
+ } else if (oldVersion == newVersion) {
+ return;
+ }
}
- /**
- * Set the value of a shared preference of type boolean.
- */
- public void setSharedBooleanPreference(String pref, boolean value) {
- mPrefs.edit().putBoolean(pref, value).apply();
+ @Override
+ protected boolean canBackup(final String key) {
+ return PreferenceKeys.BACKUP_KEYS.contains(key);
+ }
+
+ @Override
+ protected boolean hasMigrationCompleted() {
+ return getSharedPreferences().getInt(PreferenceKeys.MIGRATED_VERSION, 0)
+ >= CURRENT_VERSION_NUMBER;
+ }
+
+ @Override
+ protected void setMigrationComplete() {
+ getEditor().putInt(PreferenceKeys.MIGRATED_VERSION, CURRENT_VERSION_NUMBER).apply();
}
public boolean isWidgetConfigured(int appWidgetId) {
- return mPrefs.contains(WIDGET_ACCOUNT_PREFIX + appWidgetId);
+ return getSharedPreferences().contains(PreferenceKeys.WIDGET_ACCOUNT_PREFIX + appWidgetId);
}
- public void configureWidget(int appWidgetId, Account account, Folder folder) {
- mPrefs.edit()
- .putString(WIDGET_ACCOUNT_PREFIX + appWidgetId,
- createWidgetPreferenceValue(account, folder))
- .apply();
+ public void configureWidget(int appWidgetId, Account account, final String folderUri) {
+ getEditor().putString(PreferenceKeys.WIDGET_ACCOUNT_PREFIX + appWidgetId,
+ createWidgetPreferenceValue(account, folderUri)).apply();
}
public String getWidgetConfiguration(int appWidgetId) {
- return mPrefs.getString(WIDGET_ACCOUNT_PREFIX + appWidgetId, null);
+ return getSharedPreferences().getString(PreferenceKeys.WIDGET_ACCOUNT_PREFIX + appWidgetId,
+ null);
}
- public boolean fullTextSearchEnabled() {
- // If experimental preferences are not enabled, return true.
- return !SHOW_EXPERIMENTAL_PREFS || mPrefs.getBoolean(ENABLE_FTS, true);
- }
-
+ @SuppressWarnings("unused")
public boolean chipDragAndDropEnabled() {
// If experimental preferences are not enabled, return false.
- return SHOW_EXPERIMENTAL_PREFS && mPrefs.getBoolean(ENABLE_CHIP_DRAG_AND_DROP, false);
+ return SHOW_EXPERIMENTAL_PREFS && getSharedPreferences().getBoolean(
+ PreferenceKeys.ENABLE_CHIP_DRAG_AND_DROP, false);
}
/**
* Get whether to show the experimental inline contact photos in the
* conversation list.
*/
+ @SuppressWarnings("unused")
public boolean areConvListPhotosEnabled() {
// If experimental preferences are not enabled, return false.
- return SHOW_EXPERIMENTAL_PREFS && mPrefs.getBoolean(ENABLE_CONVLIST_PHOTOS, false);
+ return SHOW_EXPERIMENTAL_PREFS && getSharedPreferences().getBoolean(
+ PreferenceKeys.ENABLE_CONVLIST_PHOTOS, false);
}
+ public void setConvListPhotosEnabled(final boolean enabled) {
+ getEditor().putBoolean(PreferenceKeys.ENABLE_CONVLIST_PHOTOS, enabled).apply();
+ }
+
+ @SuppressWarnings("unused")
public boolean isWhooshZoomEnabled() {
// If experimental preferences are not enabled, return false.
- return SHOW_EXPERIMENTAL_PREFS && mPrefs.getBoolean(ENABLE_WHOOSH_ZOOM, false);
+ return SHOW_EXPERIMENTAL_PREFS && getSharedPreferences().getBoolean(
+ PreferenceKeys.ENABLE_WHOOSH_ZOOM, false);
}
- private static String createWidgetPreferenceValue(Account account, Folder folder) {
- return account.uri.toString() +
- ACCOUNT_FOLDER_PREFERENCE_SEPARATOR + folder.uri.toString();
+ @SuppressWarnings("unused")
+ public boolean shouldMungeTables() {
+ // If experimental preferences are not enabled, return false.
+ return SHOW_EXPERIMENTAL_PREFS && getSharedPreferences().getBoolean(
+ PreferenceKeys.ENABLE_MUNGE_TABLES, true);
+ }
+
+ @SuppressWarnings("unused")
+ public boolean shouldMungeImages() {
+ // If experimental preferences are not enabled, return false.
+ return SHOW_EXPERIMENTAL_PREFS && getSharedPreferences().getBoolean(
+ PreferenceKeys.ENABLE_MUNGE_IMAGES, true);
+ }
+
+ private static String createWidgetPreferenceValue(Account account, String folderUri) {
+ return account.uri.toString() + BaseWidgetProvider.ACCOUNT_FOLDER_PREFERENCE_SEPARATOR
+ + folderUri;
}
public void clearWidgets(int[] appWidgetIds) {
- final Editor e = mPrefs.edit();
for (int id : appWidgetIds) {
- e.remove(WIDGET_ACCOUNT_PREFIX + id);
+ getEditor().remove(PreferenceKeys.WIDGET_ACCOUNT_PREFIX + id);
}
- e.apply();
+ getEditor().apply();
+ }
+
+ /** If <code>true</code>, we should default all replies to "reply all" rather than "reply" */
+ public boolean getDefaultReplyAll() {
+ return getSharedPreferences().getBoolean(PreferenceKeys.DEFAULT_REPLY_ALL, false);
+ }
+
+ public void setDefaultReplyAll(final boolean replyAll) {
+ getEditor().putBoolean(PreferenceKeys.DEFAULT_REPLY_ALL, replyAll).apply();
+ MailIntentService.broadcastBackupDataChanged(getContext());
+ }
+
+ /**
+ * Gets the action to take (one of the values from {@link ConversationListSwipeActions}) when an
+ * item in the conversation list is swiped.
+ *
+ * @param allowArchive <code>true</code> if Archive is an acceptable action (this will affect
+ * the default return value)
+ */
+ public String getConversationListSwipeAction(final boolean allowArchive) {
+ return getSharedPreferences().getString(
+ PreferenceKeys.CONVERSATION_LIST_SWIPE_ACTION,
+ allowArchive ? ConversationListSwipeActions.ARCHIVE
+ : ConversationListSwipeActions.DELETE);
+ }
+
+ /**
+ * Gets the action to take (one of the values from {@link UIProvider.Swipe}) when an item in the
+ * conversation list is swiped.
+ *
+ * @param allowArchive <code>true</code> if Archive is an acceptable action (this will affect
+ * the default return value)
+ */
+ public int getConversationListSwipeActionInteger(final boolean allowArchive) {
+ final String swipeAction = getConversationListSwipeAction(allowArchive);
+ if (ConversationListSwipeActions.ARCHIVE.equals(swipeAction)) {
+ return UIProvider.Swipe.ARCHIVE;
+ } else if (ConversationListSwipeActions.DELETE.equals(swipeAction)) {
+ return UIProvider.Swipe.DELETE;
+ } else if (ConversationListSwipeActions.DISABLED.equals(swipeAction)) {
+ return UIProvider.Swipe.DISABLED;
+ } else {
+ return UIProvider.Swipe.DEFAULT;
+ }
+ }
+
+ public void setConversationListSwipeAction(final String swipeAction) {
+ getEditor().putString(PreferenceKeys.CONVERSATION_LIST_SWIPE_ACTION, swipeAction).apply();
+ MailIntentService.broadcastBackupDataChanged(getContext());
+ }
+
+ /**
+ * Returns the previously cached notification set
+ */
+ public Set<String> getActiveNotificationSet() {
+ return getSharedPreferences()
+ .getStringSet(PreferenceKeys.CACHED_ACTIVE_NOTIFICATION_SET, null);
+ }
+
+ /**
+ * Caches the current notification set.
+ */
+ public void cacheActiveNotificationSet(final Set<String> notificationSet) {
+ getEditor().putStringSet(PreferenceKeys.CACHED_ACTIVE_NOTIFICATION_SET, notificationSet)
+ .apply();
+ }
+
+ public boolean isSectionedInboxExperimentEnabled() {
+ // If experimental preferences are not enabled, return false.
+ return SHOW_EXPERIMENTAL_PREFS && getSharedPreferences().getBoolean(
+ PreferenceKeys.ENABLE_SECTIONED_INBOX_EXPERIMENT, false);
}
}
diff --git a/src/com/android/mail/preferences/SimpleBackupSharedPreference.java b/src/com/android/mail/preferences/SimpleBackupSharedPreference.java
new file mode 100644
index 0000000..58ea1c7
--- /dev/null
+++ b/src/com/android/mail/preferences/SimpleBackupSharedPreference.java
@@ -0,0 +1,90 @@
+/*
+ * 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.preferences;
+
+import com.google.common.collect.Sets;
+
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.util.Set;
+
+/**
+* A POJO for shared preferences to be used for backing up and restoring.
+*/
+public class SimpleBackupSharedPreference implements BackupSharedPreference {
+ private String mKey;
+ private Object mValue;
+
+ private static final String KEY = "key";
+ private static final String VALUE = "value";
+
+ public SimpleBackupSharedPreference(final String key, final Object value) {
+ mKey = key;
+ mValue = value;
+ }
+
+ @Override
+ public String getKey() {
+ return mKey;
+ }
+
+ @Override
+ public Object getValue() {
+ return mValue;
+ }
+
+ public void setValue(Object value) {
+ mValue = value;
+ }
+
+ @Override
+ public JSONObject toJson() throws JSONException {
+ final JSONObject json = new JSONObject();
+ json.put(KEY, mKey);
+ if (mValue instanceof Set) {
+ final Set<?> set = (Set<?>) mValue;
+ final JSONArray array = new JSONArray();
+ for (final Object o : set) {
+ array.put(o);
+ }
+ json.put(VALUE, array);
+ } else {
+ json.put(VALUE, mValue);
+ }
+ return json;
+ }
+
+ public static BackupSharedPreference fromJson(final JSONObject json) throws JSONException {
+ Object value = json.get(VALUE);
+ if (value instanceof JSONArray) {
+ final Set<Object> set = Sets.newHashSet();
+ final JSONArray array = (JSONArray) value;
+ for (int i = 0, len = array.length(); i < len; i++) {
+ set.add(array.get(i));
+ }
+ value = set;
+ }
+ return new SimpleBackupSharedPreference(json.getString(KEY), value);
+ }
+
+ @Override
+ public String toString() {
+ return "BackupSharedPreference{" + "mKey='" + mKey + '\'' + ", mValue=" + mValue + '}';
+ }
+}
diff --git a/src/com/android/mail/preferences/VersionedPrefs.java b/src/com/android/mail/preferences/VersionedPrefs.java
new file mode 100644
index 0000000..2a20f91
--- /dev/null
+++ b/src/com/android/mail/preferences/VersionedPrefs.java
@@ -0,0 +1,243 @@
+/*
+ * Copyright (C) 2012 Google Inc.
+ * Licensed to The Android Open Source Project.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.mail.preferences;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.collect.Lists;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.content.SharedPreferences.Editor;
+
+import com.android.mail.utils.LogTag;
+import com.android.mail.utils.LogUtils;
+
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * A high-level API to store and retrieve preferences, that can be versioned in a similar manner as
+ * SQLite databases. You must not use the preference key
+ * {@value VersionedPrefs#PREFS_VERSION_NUMBER}
+ */
+public abstract class VersionedPrefs {
+ private final Context mContext;
+ private final String mSharedPreferencesName;
+ private final SharedPreferences mSharedPreferences;
+ private final Editor mEditor;
+
+ /** The key for the version number of the {@link SharedPreferences} file. */
+ private static final String PREFS_VERSION_NUMBER = "prefs-version-number";
+
+ /**
+ * 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 String LOG_TAG = LogTag.getLogTag();
+
+ /**
+ * @param sharedPrefsName The name of the {@link SharedPreferences} file to use
+ */
+ protected VersionedPrefs(final Context context, final String sharedPrefsName) {
+ mContext = context.getApplicationContext();
+ mSharedPreferencesName = sharedPrefsName;
+ mSharedPreferences = context.getSharedPreferences(sharedPrefsName, Context.MODE_PRIVATE);
+ mEditor = mSharedPreferences.edit();
+
+ final int oldVersion = getCurrentVersion();
+
+ performUpgrade(oldVersion, CURRENT_VERSION_NUMBER);
+ setCurrentVersion(CURRENT_VERSION_NUMBER);
+
+ if (!hasMigrationCompleted()) {
+ new PreferenceMigrator().performMigration(context, oldVersion, CURRENT_VERSION_NUMBER);
+
+ setMigrationComplete();
+ }
+ }
+
+ protected Context getContext() {
+ return mContext;
+ }
+
+ public String getSharedPreferencesName() {
+ return mSharedPreferencesName;
+ }
+
+ protected SharedPreferences getSharedPreferences() {
+ return mSharedPreferences;
+ }
+
+ protected Editor getEditor() {
+ return mEditor;
+ }
+
+ /**
+ * Returns the current version of the {@link SharedPreferences} file.
+ */
+ private int getCurrentVersion() {
+ return mSharedPreferences.getInt(PREFS_VERSION_NUMBER, 0);
+ }
+
+ private void setCurrentVersion(final int versionNumber) {
+ getEditor().putInt(PREFS_VERSION_NUMBER, versionNumber);
+
+ /*
+ * If the only preference we have is the version number, we do not want to commit it.
+ * Instead, we will wait for some other preference to be written. This prevents us from
+ * creating a file with only the version number.
+ */
+ if (shouldBackUp()) {
+ getEditor().apply();
+ }
+ }
+
+ protected boolean hasMigrationCompleted() {
+ return MailPrefs.get(mContext).hasMigrationCompleted();
+ }
+
+ protected void setMigrationComplete() {
+ MailPrefs.get(mContext).setMigrationComplete();
+ }
+
+ /**
+ * Upgrades the {@link SharedPreferences} file.
+ *
+ * @param oldVersion The current version
+ * @param newVersion The new version
+ */
+ protected abstract void performUpgrade(int oldVersion, int newVersion);
+
+ @VisibleForTesting
+ public void clearAllPreferences() {
+ getEditor().clear().commit();
+ }
+
+ protected abstract boolean canBackup(String key);
+
+ /**
+ * Gets the value to backup for a given key-value pair. By default, returns the passed in value.
+ *
+ * @param key The key to backup
+ * @param value The locally stored value for the given key
+ * @return The value to backup
+ */
+ protected Object getBackupValue(final String key, final Object value) {
+ return value;
+ }
+
+ /**
+ * Gets the value to restore for a given key-value pair. By default, returns the passed in
+ * value.
+ *
+ * @param key The key to restore
+ * @param value The backed up value for the given key
+ * @return The value to restore
+ */
+ protected Object getRestoreValue(final String key, final Object value) {
+ return value;
+ }
+
+ /**
+ * Return a list of shared preferences that should be backed up.
+ */
+ public List<BackupSharedPreference> getBackupPreferences() {
+ final List<BackupSharedPreference> backupPreferences = Lists.newArrayList();
+ final SharedPreferences sharedPreferences = getSharedPreferences();
+ final Map<String, ?> preferences = sharedPreferences.getAll();
+
+ for (final Map.Entry<String, ?> entry : preferences.entrySet()) {
+ final String key = entry.getKey();
+
+ if (!canBackup(key)) {
+ continue;
+ }
+
+ final Object value = entry.getValue();
+ final Object backupValue = getBackupValue(key, value);
+
+ if (backupValue != null) {
+ backupPreferences.add(new SimpleBackupSharedPreference(key, backupValue));
+ }
+ }
+
+ return backupPreferences;
+ }
+
+ /**
+ * Restores preferences from a backup.
+ */
+ public void restorePreferences(final List<BackupSharedPreference> preferences) {
+ for (final BackupSharedPreference preference : preferences) {
+ final String key = preference.getKey();
+ final Object value = preference.getValue();
+
+ if (!canBackup(key) || value == null) {
+ continue;
+ }
+
+ final Object restoreValue = getRestoreValue(key, value);
+
+ if (restoreValue instanceof Boolean) {
+ getEditor().putBoolean(key, (Boolean) restoreValue);
+ LogUtils.v(LOG_TAG, "MailPrefs Restore: %s", preference);
+ } else if (restoreValue instanceof Float) {
+ getEditor().putFloat(key, (Float) restoreValue);
+ LogUtils.v(LOG_TAG, "MailPrefs Restore: %s", preference);
+ } else if (restoreValue instanceof Integer) {
+ getEditor().putInt(key, (Integer) restoreValue);
+ LogUtils.v(LOG_TAG, "MailPrefs Restore: %s", preference);
+ } else if (restoreValue instanceof Long) {
+ getEditor().putLong(key, (Long) restoreValue);
+ LogUtils.v(LOG_TAG, "MailPrefs Restore: %s", preference);
+ } else if (restoreValue instanceof String) {
+ getEditor().putString(key, (String) restoreValue);
+ LogUtils.v(LOG_TAG, "MailPrefs Restore: %s", preference);
+ } else if (restoreValue instanceof Set) {
+ getEditor().putStringSet(key, (Set<String>) restoreValue);
+ } else {
+ LogUtils.e(LOG_TAG, "Unknown MailPrefs preference data type: %s", value.getClass());
+ }
+ }
+
+ getEditor().apply();
+ }
+
+ /**
+ * <p>
+ * Checks if any of the preferences eligible for backup have been modified from their default
+ * values, and therefore should be backed up.
+ * </p>
+ *
+ * @return <code>true</code> if anything has been modified, <code>false</code> otherwise
+ */
+ public boolean shouldBackUp() {
+ final Map<String, ?> allPrefs = getSharedPreferences().getAll();
+
+ for (final String key : allPrefs.keySet()) {
+ if (canBackup(key)) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+}
diff --git a/src/com/android/mail/providers/Account.java b/src/com/android/mail/providers/Account.java
index 47403fb..5b8522f 100644
--- a/src/com/android/mail/providers/Account.java
+++ b/src/com/android/mail/providers/Account.java
@@ -18,12 +18,16 @@
import android.content.ContentValues;
import android.database.Cursor;
+import android.database.MatrixCursor;
import android.net.Uri;
import android.os.Parcel;
import android.os.Parcelable;
import android.text.TextUtils;
+import com.android.mail.content.CursorCreator;
+import com.android.mail.content.ObjectCursor;
import com.android.mail.providers.UIProvider.AccountCapabilities;
+import com.android.mail.providers.UIProvider.AccountColumns;
import com.android.mail.providers.UIProvider.SyncStatus;
import com.android.mail.utils.LogTag;
import com.android.mail.utils.LogUtils;
@@ -35,7 +39,9 @@
import org.json.JSONException;
import org.json.JSONObject;
+import java.util.HashMap;
import java.util.List;
+import java.util.Map;
public class Account extends android.accounts.Account implements Parcelable {
private static final String SETTINGS_KEY = "settings";
@@ -77,23 +83,6 @@
public String accountFromAddresses;
/**
- * The content provider uri that can be used to save (insert) new draft
- * messages for this account. NOTE: This might be better to be an update
- * operation on the messageUri.
- */
- @Deprecated
- public final Uri saveDraftUri;
-
- /**
- * The content provider uri that can be used to send a message for this
- * account.
- * NOTE: This might be better to be an update operation on the
- * messageUri.
- */
- @Deprecated
- public final Uri sendMessageUri;
-
- /**
* The content provider uri that can be used to expunge message from this
* account. NOTE: This might be better to be an update operation on the
* messageUri.
@@ -205,8 +194,6 @@
json.put(UIProvider.AccountColumns.FULL_FOLDER_LIST_URI, fullFolderListUri);
json.put(UIProvider.AccountColumns.SEARCH_URI, searchUri);
json.put(UIProvider.AccountColumns.ACCOUNT_FROM_ADDRESSES, accountFromAddresses);
- json.put(UIProvider.AccountColumns.SAVE_DRAFT_URI, saveDraftUri);
- json.put(UIProvider.AccountColumns.SEND_MAIL_URI, sendMessageUri);
json.put(UIProvider.AccountColumns.EXPUNGE_MESSAGE_URI, expungeMessageUri);
json.put(UIProvider.AccountColumns.UNDO_URI, undoUri);
json.put(UIProvider.AccountColumns.SETTINGS_INTENT_URI, settingsIntentUri);
@@ -287,8 +274,6 @@
searchUri = Utils.getValidUri(json.optString(UIProvider.AccountColumns.SEARCH_URI));
accountFromAddresses = json.optString(UIProvider.AccountColumns.ACCOUNT_FROM_ADDRESSES,
"");
- saveDraftUri = Utils.getValidUri(json.optString(UIProvider.AccountColumns.SAVE_DRAFT_URI));
- sendMessageUri = Utils.getValidUri(json.optString(UIProvider.AccountColumns.SEND_MAIL_URI));
expungeMessageUri = Utils.getValidUri(json
.optString(UIProvider.AccountColumns.EXPUNGE_MESSAGE_URI));
undoUri = Utils.getValidUri(json.optString(UIProvider.AccountColumns.UNDO_URI));
@@ -336,8 +321,6 @@
fullFolderListUri = in.readParcelable(null);
searchUri = in.readParcelable(null);
accountFromAddresses = in.readString();
- saveDraftUri = in.readParcelable(null);
- sendMessageUri = in.readParcelable(null);
expungeMessageUri = in.readParcelable(null);
undoUri = in.readParcelable(null);
settingsIntentUri = in.readParcelable(null);
@@ -365,47 +348,56 @@
}
public Account(Cursor cursor) {
- super(cursor.getString(UIProvider.ACCOUNT_NAME_COLUMN), "unknown");
- accountFromAddresses = cursor.getString(UIProvider.ACCOUNT_FROM_ADDRESSES_COLUMN);
- capabilities = cursor.getInt(UIProvider.ACCOUNT_CAPABILITIES_COLUMN);
- providerVersion = cursor.getInt(UIProvider.ACCOUNT_PROVIDER_VERISON_COLUMN);
- uri = Uri.parse(cursor.getString(UIProvider.ACCOUNT_URI_COLUMN));
- folderListUri = Uri.parse(cursor.getString(UIProvider.ACCOUNT_FOLDER_LIST_URI_COLUMN));
- fullFolderListUri = Utils.getValidUri(cursor
- .getString(UIProvider.ACCOUNT_FULL_FOLDER_LIST_URI_COLUMN));
- searchUri = Utils.getValidUri(cursor.getString(UIProvider.ACCOUNT_SEARCH_URI_COLUMN));
- saveDraftUri = Utils
- .getValidUri(cursor.getString(UIProvider.ACCOUNT_SAVE_DRAFT_URI_COLUMN));
- sendMessageUri = Utils.getValidUri(cursor
- .getString(UIProvider.ACCOUNT_SEND_MESSAGE_URI_COLUMN));
- expungeMessageUri = Utils.getValidUri(cursor
- .getString(UIProvider.ACCOUNT_EXPUNGE_MESSAGE_URI_COLUMN));
- undoUri = Utils.getValidUri(cursor.getString(UIProvider.ACCOUNT_UNDO_URI_COLUMN));
- settingsIntentUri = Utils.getValidUri(cursor
- .getString(UIProvider.ACCOUNT_SETTINGS_INTENT_URI_COLUMN));
- helpIntentUri = Utils.getValidUri(cursor
- .getString(UIProvider.ACCOUNT_HELP_INTENT_URI_COLUMN));
- sendFeedbackIntentUri = Utils.getValidUri(cursor
- .getString(UIProvider.ACCOUNT_SEND_FEEDBACK_INTENT_URI_COLUMN));
- reauthenticationIntentUri = Utils.getValidUri(
- cursor.getString(UIProvider.ACCOUNT_REAUTHENTICATION_INTENT_URI_COLUMN));
- syncStatus = cursor.getInt(UIProvider.ACCOUNT_SYNC_STATUS_COLUMN);
- composeIntentUri = Utils.getValidUri(cursor
- .getString(UIProvider.ACCOUNT_COMPOSE_INTENT_URI_COLUMN));
- mimeType = cursor.getString(UIProvider.ACCOUNT_MIME_TYPE_COLUMN);
- recentFolderListUri = Utils.getValidUri(cursor
- .getString(UIProvider.ACCOUNT_RECENT_FOLDER_LIST_URI_COLUMN));
- color = cursor.getInt(UIProvider.ACCOUNT_COLOR_COLUMN);
- defaultRecentFolderListUri = Utils.getValidUri(cursor
- .getString(UIProvider.ACCOUNT_DEFAULT_RECENT_FOLDER_LIST_URI_COLUMN));
- manualSyncUri = Utils.getValidUri(cursor
- .getString(UIProvider.ACCOUNT_MANUAL_SYNC_URI_COLUMN));
- viewIntentProxyUri = Utils.getValidUri(cursor
- .getString(UIProvider.ACCOUNT_VIEW_INTENT_PROXY_URI_COLUMN));
- accoutCookieQueryUri = Utils.getValidUri(cursor
- .getString(UIProvider.ACCOUNT_COOKIE_QUERY_URI_COLUMN));
- updateSettingsUri = Utils.getValidUri(cursor
- .getString(UIProvider.ACCOUNT_UPDATE_SETTINGS_URI_COLUMN));
+ super(cursor.getString(cursor.getColumnIndex(UIProvider.AccountColumns.NAME)), "unknown");
+ accountFromAddresses = cursor.getString(
+ cursor.getColumnIndex(UIProvider.AccountColumns.ACCOUNT_FROM_ADDRESSES));
+
+ final int capabilitiesColumnIndex =
+ cursor.getColumnIndex(UIProvider.AccountColumns.CAPABILITIES);
+ if (capabilitiesColumnIndex != -1) {
+ capabilities = cursor.getInt(capabilitiesColumnIndex);
+ } else {
+ capabilities = 0;
+ }
+
+ providerVersion =
+ cursor.getInt(cursor.getColumnIndex(UIProvider.AccountColumns.PROVIDER_VERSION));
+ uri = Uri.parse(cursor.getString(cursor.getColumnIndex(UIProvider.AccountColumns.URI)));
+ folderListUri = Uri.parse(
+ cursor.getString(cursor.getColumnIndex(UIProvider.AccountColumns.FOLDER_LIST_URI)));
+ fullFolderListUri = Utils.getValidUri(cursor.getString(
+ cursor.getColumnIndex(UIProvider.AccountColumns.FULL_FOLDER_LIST_URI)));
+ searchUri = Utils.getValidUri(
+ cursor.getString(cursor.getColumnIndex(UIProvider.AccountColumns.SEARCH_URI)));
+ expungeMessageUri = Utils.getValidUri(cursor.getString(
+ cursor.getColumnIndex(UIProvider.AccountColumns.EXPUNGE_MESSAGE_URI)));
+ undoUri = Utils.getValidUri(
+ cursor.getString(cursor.getColumnIndex(UIProvider.AccountColumns.UNDO_URI)));
+ settingsIntentUri = Utils.getValidUri(cursor.getString(
+ cursor.getColumnIndex(UIProvider.AccountColumns.SETTINGS_INTENT_URI)));
+ helpIntentUri = Utils.getValidUri(
+ cursor.getString(cursor.getColumnIndex(UIProvider.AccountColumns.HELP_INTENT_URI)));
+ sendFeedbackIntentUri = Utils.getValidUri(cursor.getString(
+ cursor.getColumnIndex(UIProvider.AccountColumns.SEND_FEEDBACK_INTENT_URI)));
+ reauthenticationIntentUri = Utils.getValidUri(cursor.getString(
+ cursor.getColumnIndex(UIProvider.AccountColumns.REAUTHENTICATION_INTENT_URI)));
+ syncStatus = cursor.getInt(cursor.getColumnIndex(UIProvider.AccountColumns.SYNC_STATUS));
+ composeIntentUri = Utils.getValidUri(
+ cursor.getString(cursor.getColumnIndex(UIProvider.AccountColumns.COMPOSE_URI)));
+ mimeType = cursor.getString(cursor.getColumnIndex(UIProvider.AccountColumns.MIME_TYPE));
+ recentFolderListUri = Utils.getValidUri(cursor.getString(
+ cursor.getColumnIndex(UIProvider.AccountColumns.RECENT_FOLDER_LIST_URI)));
+ color = cursor.getInt(cursor.getColumnIndex(UIProvider.AccountColumns.COLOR));
+ defaultRecentFolderListUri = Utils.getValidUri(cursor.getString(
+ cursor.getColumnIndex(UIProvider.AccountColumns.DEFAULT_RECENT_FOLDER_LIST_URI)));
+ manualSyncUri = Utils.getValidUri(
+ cursor.getString(cursor.getColumnIndex(UIProvider.AccountColumns.MANUAL_SYNC_URI)));
+ viewIntentProxyUri = Utils.getValidUri(cursor.getString(
+ cursor.getColumnIndex(UIProvider.AccountColumns.VIEW_INTENT_PROXY_URI)));
+ accoutCookieQueryUri = Utils.getValidUri(cursor.getString(
+ cursor.getColumnIndex(UIProvider.AccountColumns.ACCOUNT_COOKIE_QUERY_URI)));
+ updateSettingsUri = Utils.getValidUri(cursor.getString(
+ cursor.getColumnIndex(UIProvider.AccountColumns.UPDATE_SETTINGS_URI)));
settings = new Settings(cursor);
}
@@ -415,7 +407,7 @@
* @param cursor cursor pointing to the list of accounts
* @return the array of all accounts stored at this cursor.
*/
- public static Account[] getAllAccounts(Cursor cursor) {
+ public static Account[] getAllAccounts(ObjectCursor<Account> cursor) {
final int initialLength = cursor.getCount();
if (initialLength <= 0 || !cursor.moveToFirst()) {
// Return zero length account array rather than null
@@ -425,7 +417,7 @@
final Account[] allAccounts = new Account[initialLength];
int i = 0;
do {
- allAccounts[i++] = new Account(cursor);
+ allAccounts[i++] = cursor.getModel();
} while (cursor.moveToNext());
// Ensure that the length of the array is accurate
assert (i == initialLength);
@@ -463,8 +455,6 @@
dest.writeParcelable(fullFolderListUri, 0);
dest.writeParcelable(searchUri, 0);
dest.writeString(accountFromAddresses);
- dest.writeParcelable(saveDraftUri, 0);
- dest.writeParcelable(sendMessageUri, 0);
dest.writeParcelable(expungeMessageUri, 0);
dest.writeParcelable(undoUri, 0);
dest.writeParcelable(settingsIntentUri, 0);
@@ -508,9 +498,6 @@
sb.append(",searchUri=");
sb.append(searchUri);
sb.append(",saveDraftUri=");
- sb.append(saveDraftUri);
- sb.append(",sendMessageUri=");
- sb.append(sendMessageUri);
sb.append(",expungeMessageUri=");
sb.append(expungeMessageUri);
sb.append(",undoUri=");
@@ -559,8 +546,6 @@
Objects.equal(fullFolderListUri, other.fullFolderListUri) &&
Objects.equal(searchUri, other.searchUri) &&
Objects.equal(accountFromAddresses, other.accountFromAddresses) &&
- Objects.equal(saveDraftUri, other.saveDraftUri) &&
- Objects.equal(sendMessageUri, other.sendMessageUri) &&
Objects.equal(expungeMessageUri, other.expungeMessageUri) &&
Objects.equal(undoUri, other.undoUri) &&
Objects.equal(settingsIntentUri, other.settingsIntentUri) &&
@@ -601,12 +586,11 @@
public int hashCode() {
return super.hashCode()
^ Objects.hashCode(name, type, capabilities, providerVersion, uri, folderListUri,
- fullFolderListUri, searchUri, accountFromAddresses, saveDraftUri,
- sendMessageUri, expungeMessageUri, undoUri, settingsIntentUri,
- helpIntentUri, sendFeedbackIntentUri, reauthenticationIntentUri, syncStatus,
- composeIntentUri, mimeType, recentFolderListUri, color,
- defaultRecentFolderListUri, viewIntentProxyUri, accoutCookieQueryUri,
- updateSettingsUri);
+ fullFolderListUri, searchUri, accountFromAddresses, expungeMessageUri,
+ undoUri, settingsIntentUri, helpIntentUri, sendFeedbackIntentUri,
+ reauthenticationIntentUri, syncStatus, composeIntentUri, mimeType,
+ recentFolderListUri, color, defaultRecentFolderListUri, viewIntentProxyUri,
+ accoutCookieQueryUri, updateSettingsUri);
}
/**
@@ -699,4 +683,83 @@
}
return -1;
}
+
+ /**
+ * Creates a {@link Map} where the column name is the key and the value is the value, which can
+ * be used for populating a {@link MatrixCursor}.
+ */
+ public Map<String, Object> getMatrixCursorValueMap() {
+ // ImmutableMap.Builder does not allow null values
+ final Map<String, Object> map = new HashMap<String, Object>();
+
+ map.put(UIProvider.AccountColumns._ID, 0);
+ map.put(UIProvider.AccountColumns.NAME, name);
+ map.put(UIProvider.AccountColumns.TYPE, type);
+ map.put(UIProvider.AccountColumns.PROVIDER_VERSION, providerVersion);
+ map.put(UIProvider.AccountColumns.URI, uri);
+ map.put(UIProvider.AccountColumns.CAPABILITIES, capabilities);
+ map.put(UIProvider.AccountColumns.FOLDER_LIST_URI, folderListUri);
+ map.put(UIProvider.AccountColumns.FULL_FOLDER_LIST_URI, fullFolderListUri);
+ map.put(UIProvider.AccountColumns.SEARCH_URI, searchUri);
+ map.put(UIProvider.AccountColumns.ACCOUNT_FROM_ADDRESSES, accountFromAddresses);
+ map.put(UIProvider.AccountColumns.EXPUNGE_MESSAGE_URI, expungeMessageUri);
+ map.put(UIProvider.AccountColumns.UNDO_URI, undoUri);
+ map.put(UIProvider.AccountColumns.SETTINGS_INTENT_URI, settingsIntentUri);
+ map.put(UIProvider.AccountColumns.HELP_INTENT_URI, helpIntentUri);
+ map.put(UIProvider.AccountColumns.SEND_FEEDBACK_INTENT_URI, sendFeedbackIntentUri);
+ map.put(
+ UIProvider.AccountColumns.REAUTHENTICATION_INTENT_URI, reauthenticationIntentUri);
+ map.put(UIProvider.AccountColumns.SYNC_STATUS, syncStatus);
+ map.put(UIProvider.AccountColumns.COMPOSE_URI, composeIntentUri);
+ map.put(UIProvider.AccountColumns.MIME_TYPE, mimeType);
+ map.put(UIProvider.AccountColumns.RECENT_FOLDER_LIST_URI, recentFolderListUri);
+ map.put(UIProvider.AccountColumns.DEFAULT_RECENT_FOLDER_LIST_URI,
+ defaultRecentFolderListUri);
+ map.put(UIProvider.AccountColumns.MANUAL_SYNC_URI, manualSyncUri);
+ map.put(UIProvider.AccountColumns.VIEW_INTENT_PROXY_URI, viewIntentProxyUri);
+ map.put(UIProvider.AccountColumns.ACCOUNT_COOKIE_QUERY_URI, accoutCookieQueryUri);
+ map.put(UIProvider.AccountColumns.COLOR, color);
+ map.put(UIProvider.AccountColumns.UPDATE_SETTINGS_URI, updateSettingsUri);
+ map.put(AccountColumns.SettingsColumns.SIGNATURE, settings.signature);
+ map.put(AccountColumns.SettingsColumns.AUTO_ADVANCE, settings.getAutoAdvanceSetting());
+ map.put(AccountColumns.SettingsColumns.MESSAGE_TEXT_SIZE, settings.messageTextSize);
+ map.put(AccountColumns.SettingsColumns.SNAP_HEADERS, settings.snapHeaders);
+ map.put(AccountColumns.SettingsColumns.REPLY_BEHAVIOR, settings.replyBehavior);
+ map.put(
+ AccountColumns.SettingsColumns.HIDE_CHECKBOXES, settings.hideCheckboxes ? 1 : 0);
+ map.put(AccountColumns.SettingsColumns.CONFIRM_DELETE, settings.confirmDelete ? 1 : 0);
+ map.put(
+ AccountColumns.SettingsColumns.CONFIRM_ARCHIVE, settings.confirmArchive ? 1 : 0);
+ map.put(AccountColumns.SettingsColumns.CONFIRM_SEND, settings.confirmSend ? 1 : 0);
+ map.put(AccountColumns.SettingsColumns.DEFAULT_INBOX, settings.defaultInbox);
+ map.put(AccountColumns.SettingsColumns.DEFAULT_INBOX_NAME, settings.defaultInboxName);
+ map.put(AccountColumns.SettingsColumns.FORCE_REPLY_FROM_DEFAULT,
+ settings.forceReplyFromDefault ? 1 : 0);
+ map.put(AccountColumns.SettingsColumns.MAX_ATTACHMENT_SIZE, settings.maxAttachmentSize);
+ map.put(AccountColumns.SettingsColumns.SWIPE, settings.swipe);
+ map.put(AccountColumns.SettingsColumns.PRIORITY_ARROWS_ENABLED,
+ settings.priorityArrowsEnabled ? 1 : 0);
+ map.put(AccountColumns.SettingsColumns.SETUP_INTENT_URI, settings.setupIntentUri);
+ map.put(AccountColumns.SettingsColumns.CONVERSATION_VIEW_MODE,
+ settings.conversationViewMode);
+ map.put(AccountColumns.SettingsColumns.VEILED_ADDRESS_PATTERN,
+ settings.veiledAddressPattern);
+
+ return map;
+ }
+
+ /**
+ * Public object that knows how to construct Accounts given Cursors.
+ */
+ public final static CursorCreator<Account> FACTORY = new CursorCreator<Account>() {
+ @Override
+ public Account createFromCursor(Cursor c) {
+ return new Account(c);
+ }
+
+ @Override
+ public String toString() {
+ return "Account CursorCreator";
+ }
+ };
}
diff --git a/src/com/android/mail/providers/AllAccountObserver.java b/src/com/android/mail/providers/AllAccountObserver.java
new file mode 100644
index 0000000..50ce259
--- /dev/null
+++ b/src/com/android/mail/providers/AllAccountObserver.java
@@ -0,0 +1,99 @@
+/*
+ * 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.providers;
+
+import com.android.mail.ui.AccountController;
+import com.android.mail.utils.LogTag;
+import com.android.mail.utils.LogUtils;
+
+import android.database.DataSetObserver;
+
+/**
+ * A simple extension of {@link android.database.DataSetObserver} to provide all Accounts in
+ * {@link #onChanged(Account[])} when the list of Accounts changes. Initializing the object
+ * registers with the observer with the {@link com.android.mail.ui.AccountController} provided.
+ * The object will then begin to receive {@link #onChanged(Account[])} till {@link
+ * #unregisterAndDestroy()} is called. <p> To implement an {@link com.android.mail.providers
+ * .AllAccountObserver}, you need to implement the {@link #onChanged(Account[])} method.
+ */
+public abstract class AllAccountObserver extends DataSetObserver {
+ /**
+ * The AccountController that the observer is registered with.
+ */
+ private AccountController mController;
+
+ private static final String LOG_TAG = LogTag.getLogTag();
+
+ /**
+ * The no-argument constructor leaves the object unusable till
+ * {@link #initialize(com.android.mail.ui.AccountController)} is called.
+ */
+ public AllAccountObserver() {
+ }
+
+ /**
+ * Initializes an {@link com.android.mail.providers.AllAccountObserver} object that receives
+ * a call to {@link #onChanged(Account[])} when the controller changes the list of accounts.
+ *
+ * @param controller
+ */
+ public Account[] initialize(AccountController controller) {
+ if (controller == null) {
+ LogUtils.wtf(LOG_TAG, "AllAccountObserver initialized with null controller!");
+ }
+ mController = controller;
+ mController.registerAllAccountObserver(this);
+ return mController.getAllAccounts();
+ }
+
+ @Override
+ public final void onChanged() {
+ if (mController == null) {
+ return;
+ }
+ onChanged(mController.getAllAccounts());
+ }
+
+ /**
+ * Callback invoked when the list of Accounts changes.
+ * The updated list is passed as the argument.
+ * @param allAccounts the array of all accounts in the current application.
+ */
+ public abstract void onChanged(Account[] allAccounts);
+
+ /**
+ * Return the array of existing accounts.
+ * @return the array of existing accounts.
+ */
+ public final Account[] getAllAccounts() {
+ if (mController == null) {
+ return null;
+ }
+ return mController.getAllAccounts();
+ }
+
+ /**
+ * Unregisters for list of Account changes and makes the object unusable.
+ */
+ public void unregisterAndDestroy() {
+ if (mController == null) {
+ return;
+ }
+ mController.unregisterAllAccountObserver(this);
+ }
+}
diff --git a/src/com/android/mail/providers/Attachment.java b/src/com/android/mail/providers/Attachment.java
index 8a2e81d..0542998 100644
--- a/src/com/android/mail/providers/Attachment.java
+++ b/src/com/android/mail/providers/Attachment.java
@@ -43,6 +43,11 @@
public class Attachment implements Parcelable {
public static final String LOG_TAG = LogTag.getLogTag();
+ /**
+ * Workaround for b/8070022 so that appending a null partId to the end of a
+ * uri wouldn't remove the trailing backslash
+ */
+ public static final String EMPTY_PART_ID = "empty";
/**
* Part id of the attachment.
@@ -201,16 +206,16 @@
public JSONObject toJSON() throws JSONException {
final JSONObject result = new JSONObject();
- result.putOpt(AttachmentColumns.NAME, name);
- result.putOpt(AttachmentColumns.SIZE, size);
- result.putOpt(AttachmentColumns.URI, stringify(uri));
- result.putOpt(AttachmentColumns.CONTENT_TYPE, contentType);
- result.putOpt(AttachmentColumns.STATE, state);
- result.putOpt(AttachmentColumns.DESTINATION, destination);
- result.putOpt(AttachmentColumns.DOWNLOADED_SIZE, downloadedSize);
- result.putOpt(AttachmentColumns.CONTENT_URI, stringify(contentUri));
- result.putOpt(AttachmentColumns.THUMBNAIL_URI, stringify(thumbnailUri));
- result.putOpt(AttachmentColumns.PREVIEW_INTENT_URI, stringify(previewIntentUri));
+ result.put(AttachmentColumns.NAME, name);
+ result.put(AttachmentColumns.SIZE, size);
+ result.put(AttachmentColumns.URI, stringify(uri));
+ result.put(AttachmentColumns.CONTENT_TYPE, contentType);
+ result.put(AttachmentColumns.STATE, state);
+ result.put(AttachmentColumns.DESTINATION, destination);
+ result.put(AttachmentColumns.DOWNLOADED_SIZE, downloadedSize);
+ result.put(AttachmentColumns.CONTENT_URI, stringify(contentUri));
+ result.put(AttachmentColumns.THUMBNAIL_URI, stringify(thumbnailUri));
+ result.put(AttachmentColumns.PREVIEW_INTENT_URI, stringify(previewIntentUri));
result.put(AttachmentColumns.PROVIDER_DATA, providerData);
return result;
@@ -222,7 +227,12 @@
final JSONObject jsonObject = toJSON();
// Add some additional fields that are helpful when debugging issues
jsonObject.put("partId", partId);
- return jsonObject.toString();
+ try {
+ // pretty print the provider data
+ jsonObject.put(AttachmentColumns.PROVIDER_DATA, new JSONObject(providerData));
+ } catch (JSONException e) {
+ }
+ return jsonObject.toString(4);
} catch (JSONException e) {
LogUtils.e(LOG_TAG, e, "JSONException in toString");
return super.toString();
diff --git a/src/com/android/mail/providers/Conversation.java b/src/com/android/mail/providers/Conversation.java
index 4646085..f978d00 100644
--- a/src/com/android/mail/providers/Conversation.java
+++ b/src/com/android/mail/providers/Conversation.java
@@ -16,6 +16,7 @@
package com.android.mail.providers;
+import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.net.Uri;
@@ -97,6 +98,10 @@
*/
public boolean read;
/**
+ * @see UIProvider.ConversationColumns#SEEN
+ */
+ public boolean seen;
+ /**
* @see UIProvider.ConversationColumns#STARRED
*/
public boolean starred;
@@ -190,6 +195,7 @@
dest.writeInt(sendingState);
dest.writeInt(priority);
dest.writeInt(read ? 1 : 0);
+ dest.writeInt(seen ? 1 : 0);
dest.writeInt(starred ? 1 : 0);
dest.writeParcelable(rawFolders, 0);
dest.writeInt(convFlags);
@@ -218,6 +224,7 @@
sendingState = in.readInt();
priority = in.readInt();
read = (in.readInt() != 0);
+ seen = (in.readInt() != 0);
starred = (in.readInt() != 0);
rawFolders = in.readParcelable(loader);
convFlags = in.readInt();
@@ -290,6 +297,7 @@
sendingState = cursor.getInt(UIProvider.CONVERSATION_SENDING_STATE_COLUMN);
priority = cursor.getInt(UIProvider.CONVERSATION_PRIORITY_COLUMN);
read = cursor.getInt(UIProvider.CONVERSATION_READ_COLUMN) != 0;
+ seen = cursor.getInt(UIProvider.CONVERSATION_SEEN_COLUMN) != 0;
starred = cursor.getInt(UIProvider.CONVERSATION_STARRED_COLUMN) != 0;
rawFolders = FolderList.fromBlob(
cursor.getBlob(UIProvider.CONVERSATION_RAW_FOLDERS_COLUMN));
@@ -319,15 +327,52 @@
}
}
+ public Conversation(Conversation other) {
+ if (other == null) {
+ return;
+ }
+
+ id = other.id;
+ uri = other.uri;
+ dateMs = other.dateMs;
+ subject = other.subject;
+ hasAttachments = other.hasAttachments;
+ messageListUri = other.messageListUri;
+ sendingState = other.sendingState;
+ priority = other.priority;
+ read = other.read;
+ seen = other.seen;
+ starred = other.starred;
+ rawFolders = other.rawFolders; // FolderList is immutable, shallow copy is OK
+ convFlags = other.convFlags;
+ personalLevel = other.personalLevel;
+ spam = other.spam;
+ phishing = other.phishing;
+ muted = other.muted;
+ color = other.color;
+ accountUri = other.accountUri;
+ position = other.position;
+ localDeleteOnUpdate = other.localDeleteOnUpdate;
+ // although ConversationInfo is mutable (see ConversationInfo.markRead), applyCachedValues
+ // will overwrite this if cached changes exist anyway, so a shallow copy is OK
+ conversationInfo = other.conversationInfo;
+ conversationBaseUri = other.conversationBaseUri;
+ snippet = other.snippet;
+ senders = other.senders;
+ numMessages = other.numMessages;
+ numDrafts = other.numDrafts;
+ isRemote = other.isRemote;
+ }
+
public Conversation() {
}
public static Conversation create(long id, Uri uri, String subject, long dateMs,
String snippet, boolean hasAttachment, Uri messageListUri, String senders,
int numMessages, int numDrafts, int sendingState, int priority, boolean read,
- boolean starred, FolderList rawFolders, int convFlags, int personalLevel, boolean spam,
- boolean phishing, boolean muted, Uri accountUri, ConversationInfo conversationInfo,
- Uri conversationBase, boolean isRemote) {
+ boolean seen, boolean starred, FolderList rawFolders, int convFlags, int personalLevel,
+ boolean spam, boolean phishing, boolean muted, Uri accountUri,
+ ConversationInfo conversationInfo, Uri conversationBase, boolean isRemote) {
final Conversation conversation = new Conversation();
@@ -344,6 +389,7 @@
conversation.sendingState = sendingState;
conversation.priority = priority;
conversation.read = read;
+ conversation.seen = seen;
conversation.starred = starred;
conversation.rawFolders = rawFolders;
conversation.convFlags = convFlags;
@@ -360,6 +406,40 @@
}
/**
+ * Apply any column values from the given {@link ContentValues} (where column names are the
+ * keys) to this conversation.
+ *
+ */
+ public void applyCachedValues(ContentValues values) {
+ if (values == null) {
+ return;
+ }
+ for (String key : values.keySet()) {
+ final Object val = values.get(key);
+ LogUtils.i(LOG_TAG, "Conversation: applying cached value to col=%s val=%s", key,
+ val);
+ if (ConversationColumns.READ.equals(key)) {
+ read = (Integer) val != 0;
+ } else if (ConversationColumns.CONVERSATION_INFO.equals(key)) {
+ conversationInfo = ConversationInfo.fromBlob((byte[]) val);
+ } else if (ConversationColumns.FLAGS.equals(key)) {
+ convFlags = (Integer) val;
+ } else if (ConversationColumns.STARRED.equals(key)) {
+ starred = (Integer) val != 0;
+ } else if (ConversationColumns.SEEN.equals(key)) {
+ seen = (Integer) val != 0;
+ } else if (ConversationColumns.RAW_FOLDERS.equals(key)) {
+ rawFolders = FolderList.fromBlob((byte[]) val);
+ } else if (ConversationColumns.VIEWED.equals(key)) {
+ // ignore. this is not read from the cursor, either.
+ } else {
+ LogUtils.e(LOG_TAG, new UnsupportedOperationException(),
+ "unsupported cached conv value in col=%s", key);
+ }
+ }
+ }
+
+ /**
* Get the <strong>immutable</strong> list of {@link Folder}s for this conversation. To modify
* this list, make a new {@link FolderList} and use {@link #setRawFolders(FolderList)}.
*
@@ -378,12 +458,12 @@
cachedDisplayableFolders = null;
}
- public ArrayList<Folder> getRawFoldersForDisplay(Folder ignoreFolder) {
+ public ArrayList<Folder> getRawFoldersForDisplay(final Uri ignoreFolderUri) {
if (cachedDisplayableFolders == null) {
cachedDisplayableFolders = new ArrayList<Folder>();
for (Folder folder : rawFolders.folders) {
// skip the ignoreFolder
- if (ignoreFolder != null && ignoreFolder.equals(folder)) {
+ if (ignoreFolderUri != null && ignoreFolderUri.equals(folder.uri)) {
continue;
}
cachedDisplayableFolders.add(folder);
@@ -480,7 +560,7 @@
}
}
- private String getSendersDelimeter(Context context) {
+ private static String getSendersDelimeter(Context context) {
if (sSendersDelimeter == null) {
sSendersDelimeter = context.getResources().getString(R.string.senders_split_token);
}
@@ -550,8 +630,14 @@
if (sSubjectAndSnippet == null) {
sSubjectAndSnippet = context.getString(R.string.subject_and_snippet);
}
- return (!TextUtils.isEmpty(snippet)) ?
- String.format(sSubjectAndSnippet, filteredSubject, snippet)
- : filteredSubject;
+ if (TextUtils.isEmpty(filteredSubject) && TextUtils.isEmpty(snippet)) {
+ return "";
+ } else if (TextUtils.isEmpty(filteredSubject)) {
+ return snippet;
+ } else if (TextUtils.isEmpty(snippet)) {
+ return filteredSubject;
+ }
+
+ return String.format(sSubjectAndSnippet, filteredSubject, snippet);
}
}
diff --git a/src/com/android/mail/providers/Folder.java b/src/com/android/mail/providers/Folder.java
index 57bb5bd..928d6bb 100644
--- a/src/com/android/mail/providers/Folder.java
+++ b/src/com/android/mail/providers/Folder.java
@@ -18,7 +18,6 @@
package com.android.mail.providers;
import android.content.Context;
-import android.content.CursorLoader;
import android.database.Cursor;
import android.graphics.drawable.PaintDrawable;
import android.net.Uri;
@@ -29,6 +28,9 @@
import android.view.View;
import android.widget.ImageView;
+import com.android.mail.content.CursorCreator;
+import com.android.mail.content.ObjectCursorLoader;
+import com.android.mail.providers.UIProvider.FolderType;
import com.android.mail.utils.LogTag;
import com.android.mail.utils.LogUtils;
import com.android.mail.utils.Utils;
@@ -39,7 +41,6 @@
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
-import java.util.regex.Pattern;
/**
* A folder is a collection of conversations, and perhaps other folders.
@@ -64,6 +65,11 @@
public int id;
/**
+ * Persistent (across installations) id of this folder.
+ */
+ public String persistentId;
+
+ /**
* The content provider URI that returns this folder for this account.
*/
public Uri uri;
@@ -101,6 +107,11 @@
public Uri childFoldersListUri;
/**
+ * The number of messages that are unseen in this folder.
+ */
+ public int unseenCount;
+
+ /**
* The number of messages that are unread in this folder.
*/
public int unreadCount;
@@ -136,8 +147,12 @@
/**
* Icon for this folder; 0 implies no icon.
*/
- // FIXME: resource IDs are ints, not longs.
- public long iconResId;
+ public int iconResId;
+
+ /**
+ * Notification icon for this folder; 0 implies no icon.
+ */
+ public int notificationIconResId;
public String bgColor;
public String fgColor;
@@ -160,21 +175,23 @@
*/
public Folder parent;
+ /**
+ * The time at which the last message was received.
+ */
+ public long lastMessageTimestamp;
+
/** An immutable, empty conversation list */
public static final Collection<Folder> EMPTY = Collections.emptyList();
- @Deprecated
- public static final String SPLITTER = "^*^";
- @Deprecated
- private static final Pattern SPLITTER_REGEX = Pattern.compile("\\^\\*\\^");
-
// TODO: we desperately need a Builder here
- public Folder(int id, Uri uri, String name, int capabilities, boolean hasChildren,
- int syncWindow, Uri conversationListUri, Uri childFoldersListUri, int unreadCount,
- int totalCount, Uri refreshUri, int syncStatus, int lastSyncResult, int type,
- long iconResId, String bgColor, String fgColor, Uri loadMoreUri,
- String hierarchicalDesc, Folder parent) {
+ public Folder(int id, String persistentId, Uri uri, String name, int capabilities,
+ boolean hasChildren, int syncWindow, Uri conversationListUri, Uri childFoldersListUri,
+ int unseenCount, int unreadCount, int totalCount, Uri refreshUri, int syncStatus,
+ int lastSyncResult, int type, int iconResId, int notificationIconResId, String bgColor,
+ String fgColor, Uri loadMoreUri, String hierarchicalDesc, Folder parent,
+ final long lastMessageTimestamp) {
this.id = id;
+ this.persistentId = persistentId;
this.uri = uri;
this.name = name;
this.capabilities = capabilities;
@@ -182,6 +199,7 @@
this.syncWindow = syncWindow;
this.conversationListUri = conversationListUri;
this.childFoldersListUri = childFoldersListUri;
+ this.unseenCount = unseenCount;
this.unreadCount = unreadCount;
this.totalCount = totalCount;
this.refreshUri = refreshUri;
@@ -189,39 +207,18 @@
this.lastSyncResult = lastSyncResult;
this.type = type;
this.iconResId = iconResId;
+ this.notificationIconResId = notificationIconResId;
this.bgColor = bgColor;
this.fgColor = fgColor;
this.loadMoreUri = loadMoreUri;
this.hierarchicalDesc = hierarchicalDesc;
this.parent = parent;
+ this.lastMessageTimestamp = lastMessageTimestamp;
}
- public Folder(Parcel in, ClassLoader loader) {
- id = in.readInt();
- uri = in.readParcelable(loader);
- name = in.readString();
- capabilities = in.readInt();
- // 1 for true, 0 for false.
- hasChildren = in.readInt() == 1;
- syncWindow = in.readInt();
- conversationListUri = in.readParcelable(loader);
- childFoldersListUri = in.readParcelable(loader);
- unreadCount = in.readInt();
- totalCount = in.readInt();
- refreshUri = in.readParcelable(loader);
- syncStatus = in.readInt();
- lastSyncResult = in.readInt();
- type = in.readInt();
- iconResId = in.readLong();
- bgColor = in.readString();
- fgColor = in.readString();
- loadMoreUri = in.readParcelable(loader);
- hierarchicalDesc = in.readString();
- parent = in.readParcelable(loader);
- }
-
public Folder(Cursor cursor) {
id = cursor.getInt(UIProvider.FOLDER_ID_COLUMN);
+ persistentId = cursor.getString(UIProvider.FOLDER_PERSISTENT_ID_COLUMN);
uri = Uri.parse(cursor.getString(UIProvider.FOLDER_URI_COLUMN));
name = cursor.getString(UIProvider.FOLDER_NAME_COLUMN);
capabilities = cursor.getInt(UIProvider.FOLDER_CAPABILITIES_COLUMN);
@@ -233,6 +230,7 @@
String childList = cursor.getString(UIProvider.FOLDER_CHILD_FOLDERS_LIST_COLUMN);
childFoldersListUri = (hasChildren && !TextUtils.isEmpty(childList)) ? Uri.parse(childList)
: null;
+ unseenCount = cursor.getInt(UIProvider.FOLDER_UNSEEN_COUNT_COLUMN);
unreadCount = cursor.getInt(UIProvider.FOLDER_UNREAD_COUNT_COLUMN);
totalCount = cursor.getInt(UIProvider.FOLDER_TOTAL_COUNT_COLUMN);
String refresh = cursor.getString(UIProvider.FOLDER_REFRESH_URI_COLUMN);
@@ -240,18 +238,64 @@
syncStatus = cursor.getInt(UIProvider.FOLDER_SYNC_STATUS_COLUMN);
lastSyncResult = cursor.getInt(UIProvider.FOLDER_LAST_SYNC_RESULT_COLUMN);
type = cursor.getInt(UIProvider.FOLDER_TYPE_COLUMN);
- iconResId = cursor.getLong(UIProvider.FOLDER_ICON_RES_ID_COLUMN);
+ iconResId = cursor.getInt(UIProvider.FOLDER_ICON_RES_ID_COLUMN);
+ notificationIconResId = cursor.getInt(UIProvider.FOLDER_NOTIFICATION_ICON_RES_ID_COLUMN);
bgColor = cursor.getString(UIProvider.FOLDER_BG_COLOR_COLUMN);
fgColor = cursor.getString(UIProvider.FOLDER_FG_COLOR_COLUMN);
String loadMore = cursor.getString(UIProvider.FOLDER_LOAD_MORE_URI_COLUMN);
loadMoreUri = !TextUtils.isEmpty(loadMore) ? Uri.parse(loadMore) : null;
hierarchicalDesc = cursor.getString(UIProvider.FOLDER_HIERARCHICAL_DESC_COLUMN);
parent = null;
+ lastMessageTimestamp = cursor.getLong(UIProvider.FOLDER_LAST_MESSAGE_TIMESTAMP_COLUMN);
}
+ /**
+ * Public object that knows how to construct Folders given Cursors.
+ */
+ public static final CursorCreator<Folder> FACTORY = new CursorCreator<Folder>() {
+ @Override
+ public Folder createFromCursor(Cursor c) {
+ return new Folder(c);
+ }
+
+ @Override
+ public String toString() {
+ return "Folder CursorCreator";
+ }
+ };
+
+ public Folder(Parcel in, ClassLoader loader) {
+ id = in.readInt();
+ persistentId = in.readString();
+ uri = in.readParcelable(loader);
+ name = in.readString();
+ capabilities = in.readInt();
+ // 1 for true, 0 for false.
+ hasChildren = in.readInt() == 1;
+ syncWindow = in.readInt();
+ conversationListUri = in.readParcelable(loader);
+ childFoldersListUri = in.readParcelable(loader);
+ unseenCount = in.readInt();
+ unreadCount = in.readInt();
+ totalCount = in.readInt();
+ refreshUri = in.readParcelable(loader);
+ syncStatus = in.readInt();
+ lastSyncResult = in.readInt();
+ type = in.readInt();
+ iconResId = in.readInt();
+ notificationIconResId = in.readInt();
+ bgColor = in.readString();
+ fgColor = in.readString();
+ loadMoreUri = in.readParcelable(loader);
+ hierarchicalDesc = in.readString();
+ parent = in.readParcelable(loader);
+ lastMessageTimestamp = in.readLong();
+ }
+
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeInt(id);
+ dest.writeString(persistentId);
dest.writeParcelable(uri, 0);
dest.writeString(name);
dest.writeInt(capabilities);
@@ -260,31 +304,35 @@
dest.writeInt(syncWindow);
dest.writeParcelable(conversationListUri, 0);
dest.writeParcelable(childFoldersListUri, 0);
+ dest.writeInt(unseenCount);
dest.writeInt(unreadCount);
dest.writeInt(totalCount);
dest.writeParcelable(refreshUri, 0);
dest.writeInt(syncStatus);
dest.writeInt(lastSyncResult);
dest.writeInt(type);
- dest.writeLong(iconResId);
+ dest.writeInt(iconResId);
+ dest.writeInt(notificationIconResId);
dest.writeString(bgColor);
dest.writeString(fgColor);
dest.writeParcelable(loadMoreUri, 0);
dest.writeString(hierarchicalDesc);
dest.writeParcelable(parent, 0);
+ dest.writeLong(lastMessageTimestamp);
}
/**
* Construct a folder that queries for search results. Do not call on the UI
* thread.
*/
- public static CursorLoader forSearchResults(Account account, String query, Context context) {
+ public static ObjectCursorLoader<Folder> forSearchResults(Account account, String query,
+ Context context) {
if (account.searchUri != null) {
- Builder searchBuilder = account.searchUri.buildUpon();
+ final Builder searchBuilder = account.searchUri.buildUpon();
searchBuilder.appendQueryParameter(UIProvider.SearchQueryParameters.QUERY, query);
- Uri searchUri = searchBuilder.build();
- return new CursorLoader(context, searchUri, UIProvider.FOLDERS_PROJECTION, null, null,
- null);
+ final Uri searchUri = searchBuilder.build();
+ return new ObjectCursorLoader<Folder>(context, searchUri, UIProvider.FOLDERS_PROJECTION,
+ FACTORY);
}
return null;
}
@@ -297,13 +345,6 @@
return folders;
}
- private static Uri getValidUri(String uri) {
- if (TextUtils.isEmpty(uri)) {
- return null;
- }
- return Uri.parse(uri);
- }
-
/**
* Constructor that leaves everything uninitialized.
*/
@@ -321,7 +362,6 @@
return new Folder();
}
- @SuppressWarnings("hiding")
public static final ClassLoaderCreator<Folder> CREATOR = new ClassLoaderCreator<Folder>() {
@Override
public Folder createFromParcel(Parcel source) {
@@ -395,7 +435,8 @@
if (colorBlock == null) {
return;
}
- boolean showBg = !TextUtils.isEmpty(folder.bgColor);
+ boolean showBg =
+ !TextUtils.isEmpty(folder.bgColor) && folder.type != FolderType.INBOX_SECTION;
final int backgroundColor = showBg ? Integer.parseInt(folder.bgColor) : 0;
if (backgroundColor == Utils.getDefaultFolderBackgroundColor(colorBlock.getContext())) {
showBg = false;
@@ -415,12 +456,12 @@
if (iconView == null) {
return;
}
- final long icon = folder.iconResId;
+ final int icon = folder.iconResId;
if (icon > 0) {
- iconView.setImageResource((int)icon);
+ iconView.setImageResource(icon);
iconView.setVisibility(View.VISIBLE);
} else {
- iconView.setVisibility(View.INVISIBLE);
+ iconView.setVisibility(View.GONE);
}
}
@@ -439,115 +480,6 @@
return TextUtils.isEmpty(fgColor) ? defaultColor : Integer.parseInt(fgColor);
}
- @Deprecated
- public static Folder fromString(String inString) {
- if (TextUtils.isEmpty(inString)) {
- return null;
- }
- final Folder f = new Folder();
- int indexOf = inString.indexOf(SPLITTER);
- int id = -1;
- if (indexOf != -1) {
- id = Integer.valueOf(inString.substring(0, indexOf));
- } else {
- // If no separator was found, we can't parse this folder and the
- // TextUtils.split call would also fail. Return null.
- LogUtils.w(LOG_TAG, "Problem parsing folderId: original string: %s", inString);
- return null;
- }
- final String[] split = TextUtils.split(inString, SPLITTER_REGEX);
- if (split.length < 20) {
- return null;
- }
- f.id = id;
- f.uri = Folder.getValidUri(split[1]);
- f.name = split[2];
- f.hasChildren = Integer.parseInt(split[3]) != 0;
- f.capabilities = Integer.parseInt(split[4]);
- f.syncWindow = Integer.parseInt(split[5]);
- f.conversationListUri = getValidUri(split[6]);
- f.childFoldersListUri = getValidUri(split[7]);
- f.unreadCount = Integer.parseInt(split[8]);
- f.totalCount = Integer.parseInt(split[9]);
- f.refreshUri = getValidUri(split[10]);
- f.syncStatus = Integer.parseInt(split[11]);
- f.lastSyncResult = Integer.parseInt(split[12]);
- f.type = Integer.parseInt(split[13]);
- f.iconResId = Integer.parseInt(split[14]);
- f.bgColor = split[15];
- f.fgColor = split[16];
- f.loadMoreUri = getValidUri(split[17]);
- f.hierarchicalDesc = split[18];
- f.parent = Folder.fromString(split[19]);
- return f;
- }
-
- /**
- * Create a string representation of a folder.
- */
- @Deprecated
- public static String createAsString(int id, Uri uri, String name, boolean hasChildren,
- int capabilities, int syncWindow, Uri convListUri, Uri childFoldersListUri,
- int unreadCount, int totalCount, Uri refreshUri, int syncStatus, int lastSyncResult,
- int type, long iconResId, String bgColor, String fgColor, Uri loadMore,
- String hierarchicalDesc, Folder parent) {
- StringBuilder builder = new StringBuilder();
- builder.append(id);
- builder.append(SPLITTER);
- builder.append(uri != null ? uri : "");
- builder.append(SPLITTER);
- builder.append(name != null ? name : "");
- builder.append(SPLITTER);
- builder.append(hasChildren ? 1 : 0);
- builder.append(SPLITTER);
- builder.append(capabilities);
- builder.append(SPLITTER);
- builder.append(syncWindow);
- builder.append(SPLITTER);
- builder.append(convListUri != null ? convListUri : "");
- builder.append(SPLITTER);
- builder.append(childFoldersListUri != null ? childFoldersListUri : "");
- builder.append(SPLITTER);
- builder.append(unreadCount);
- builder.append(SPLITTER);
- builder.append(totalCount);
- builder.append(SPLITTER);
- builder.append(refreshUri != null ? refreshUri : "");
- builder.append(SPLITTER);
- builder.append(syncStatus);
- builder.append(SPLITTER);
- builder.append(lastSyncResult);
- builder.append(SPLITTER);
- builder.append(type);
- builder.append(SPLITTER);
- builder.append(iconResId);
- builder.append(SPLITTER);
- builder.append(bgColor != null ? bgColor : "");
- builder.append(SPLITTER);
- builder.append(fgColor != null ? fgColor : "");
- builder.append(SPLITTER);
- builder.append(loadMore != null ? loadMore : "");
- builder.append(SPLITTER);
- builder.append(hierarchicalDesc != null ? hierarchicalDesc : "");
- builder.append(SPLITTER);
- if (parent != null) {
- builder.append(Folder.toString(parent));
- } else {
- builder.append("");
- }
- return builder.toString();
- }
-
- @Deprecated
- public static String toString(Folder folder) {
- return createAsString(folder.id, folder.uri, folder.name, folder.hasChildren,
- folder.capabilities, folder.syncWindow, folder.conversationListUri,
- folder.childFoldersListUri, folder.unreadCount, folder.totalCount,
- folder.refreshUri, folder.syncStatus, folder.lastSyncResult, folder.type,
- folder.iconResId, folder.bgColor, folder.fgColor, folder.loadMoreUri,
- folder.hierarchicalDesc, folder.parent);
- }
-
/**
* Returns a comma separated list of folder URIs for all the folders in the collection.
* @param folders
@@ -692,6 +624,7 @@
f.id = cursor.getInt(UIProvider.FOLDER_ID_COLUMN);
f.uri = Utils.getValidUri(cursor.getString(UIProvider.FOLDER_URI_COLUMN));
f.totalCount = cursor.getInt(UIProvider.FOLDER_TOTAL_COUNT_COLUMN);
+ f.unseenCount = cursor.getInt(UIProvider.FOLDER_UNSEEN_COUNT_COLUMN);
f.unreadCount = cursor.getInt(UIProvider.FOLDER_UNREAD_COUNT_COLUMN);
f.conversationListUri = Utils.getValidUri(cursor
.getString(UIProvider.FOLDER_CONVERSATION_LIST_URI_COLUMN));
@@ -699,6 +632,9 @@
f.capabilities = cursor.getInt(UIProvider.FOLDER_CAPABILITIES_COLUMN);
f.bgColor = cursor.getString(UIProvider.FOLDER_BG_COLOR_COLUMN);
f.name = cursor.getString(UIProvider.FOLDER_NAME_COLUMN);
+ f.iconResId = cursor.getInt(UIProvider.FOLDER_ICON_RES_ID_COLUMN);
+ f.notificationIconResId = cursor.getInt(UIProvider.FOLDER_NOTIFICATION_ICON_RES_ID_COLUMN);
+ f.lastMessageTimestamp = cursor.getLong(UIProvider.FOLDER_LAST_MESSAGE_TIMESTAMP_COLUMN);
return f;
}
}
diff --git a/src/com/android/mail/providers/FolderObserver.java b/src/com/android/mail/providers/FolderObserver.java
new file mode 100644
index 0000000..03c1ac8
--- /dev/null
+++ b/src/com/android/mail/providers/FolderObserver.java
@@ -0,0 +1,102 @@
+/*
+ * 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.providers;
+
+import com.android.mail.ui.FolderController;
+import com.android.mail.utils.LogTag;
+import com.android.mail.utils.LogUtils;
+
+import android.database.DataSetObserver;
+
+/**
+ * A simple extension of {@link android.database.DataSetObserver} to provide the updated Folder in
+ * {@link #onChanged(Folder)} when the Folder changes. Initializing the object registers with
+ * the observer with the {@link com.android.mail.ui.FolderController} provided. The object will then begin to
+ * receive {@link #onChanged(Folder)} till {@link #unregisterAndDestroy()} is called.
+ * <p>
+ * To implement an {@link FolderObserver}, you need to implement the {@link #onChanged(Folder)}
+ * method.
+ */
+public abstract class FolderObserver extends DataSetObserver {
+ /**
+ * The FolderController that the observer is registered with.
+ */
+ private FolderController mController;
+
+ private static final String LOG_TAG = LogTag.getLogTag();
+
+ /**
+ * The no-argument constructor leaves the object unusable till
+ * {@link #initialize(FolderController)} is called.
+ */
+ public FolderObserver () {
+ }
+
+ /**
+ * Initializes an {@link FolderObserver} object that receives a call to
+ * {@link #onChanged(Folder)} when the controller changes the Folder.
+ *
+ * @param controller
+ */
+ public Folder initialize(FolderController controller) {
+ if (controller == null) {
+ LogUtils.wtf(LOG_TAG, "FolderObserver initialized with null controller!");
+ }
+ mController = controller;
+ mController.registerFolderObserver(this);
+ return mController.getFolder();
+ }
+
+ @Override
+ public final void onChanged() {
+ if (mController == null) {
+ return;
+ }
+ onChanged(mController.getFolder());
+ }
+
+ /**
+ * Callback invoked when the Folder object is changed. Since {@link Folder} objects are
+ * immutable, updates can be received on changes to individual settings (sync on/off)
+ * in addition to changes of Folders: alice@example.com -> bob@example.com.
+ * The updated Folder is passed as the argument.
+ * @param newFolder
+ */
+ public abstract void onChanged(Folder newFolder);
+
+ /**
+ * Return the current folder.
+ * @return
+ */
+ public final Folder getFolder() {
+ if (mController == null) {
+ return null;
+ }
+ return mController.getFolder();
+ }
+
+ /**
+ * Unregisters for Folder changes and makes the object unusable.
+ */
+ public void unregisterAndDestroy() {
+ if (mController == null) {
+ return;
+ }
+ mController.unregisterFolderObserver(this);
+ }
+}
diff --git a/src/com/android/mail/providers/MailAppProvider.java b/src/com/android/mail/providers/MailAppProvider.java
index 587170d..23c8965 100644
--- a/src/com/android/mail/providers/MailAppProvider.java
+++ b/src/com/android/mail/providers/MailAppProvider.java
@@ -27,18 +27,18 @@
import android.content.Loader;
import android.content.Loader.OnLoadCompleteListener;
import android.content.SharedPreferences;
+import android.content.res.Resources;
import android.database.Cursor;
import android.database.MatrixCursor;
import android.net.Uri;
import android.os.Bundle;
+import com.android.mail.R;
import com.android.mail.providers.UIProvider.AccountCursorExtraKeys;
-import com.android.mail.providers.protos.boot.AccountReceiver;
import com.android.mail.utils.LogTag;
import com.android.mail.utils.LogUtils;
import com.android.mail.utils.MatrixCursorWithExtra;
import com.google.common.collect.ImmutableList;
-import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
@@ -86,7 +86,6 @@
private ContentResolver mResolver;
private static String sAuthority;
private static MailAppProvider sInstance;
- private final static Set<Uri> PENDING_ACCOUNT_URIS = Sets.newHashSet();
private volatile boolean mAccountsFullyLoaded = false;
@@ -141,24 +140,19 @@
@Override
public boolean onCreate() {
sAuthority = getAuthority();
+ sInstance = this;
mResolver = getContext().getContentResolver();
- final Intent intent = new Intent(AccountReceiver.ACTION_PROVIDER_CREATED);
- getContext().sendBroadcast(intent);
-
// Load the previously saved account list
loadCachedAccountList();
- synchronized (PENDING_ACCOUNT_URIS) {
- sInstance = this;
+ final Resources res = getContext().getResources();
+ // Load the uris for the account list
+ final String[] accountQueryUris = res.getStringArray(R.array.account_providers);
- // Handle the case where addAccountsForUriAsync was called before
- // this Provider instance was created
- final Set<Uri> urisToQery = ImmutableSet.copyOf(PENDING_ACCOUNT_URIS);
- PENDING_ACCOUNT_URIS.clear();
- for (Uri accountQueryUri : urisToQery) {
- addAccountsForUriAsync(accountQueryUri);
- }
+ for (String accountQueryUri : accountQueryUris) {
+ final Uri uri = Uri.parse(accountQueryUri);
+ addAccountsForUriAsync(uri);
}
return true;
@@ -166,9 +160,7 @@
@Override
public void shutdown() {
- synchronized (PENDING_ACCOUNT_URIS) {
- sInstance = null;
- }
+ sInstance = null;
for (CursorLoader loader : mCursorLoaderMap.values()) {
loader.stopLoading();
@@ -199,149 +191,13 @@
for (AccountCacheEntry accountEntry : accountList) {
final Account account = accountEntry.mAccount;
final MatrixCursor.RowBuilder builder = cursor.newRow();
+ final Map<String, Object> accountValues = account.getMatrixCursorValueMap();
for (final String columnName : resultProjection) {
- final int column = UIProvider.getAccountColumn(columnName);
- switch (column) {
- case UIProvider.ACCOUNT_ID_COLUMN:
- builder.add(Integer.valueOf(0));
- break;
- case UIProvider.ACCOUNT_NAME_COLUMN:
- builder.add(account.name);
- break;
- case UIProvider.ACCOUNT_PROVIDER_VERISON_COLUMN:
- // TODO fix this
- builder.add(Integer.valueOf(account.providerVersion));
- break;
- case UIProvider.ACCOUNT_URI_COLUMN:
- builder.add(account.uri);
- break;
- case UIProvider.ACCOUNT_CAPABILITIES_COLUMN:
- builder.add(Integer.valueOf(account.capabilities));
- break;
- case UIProvider.ACCOUNT_FOLDER_LIST_URI_COLUMN:
- builder.add(account.folderListUri);
- break;
- case UIProvider.ACCOUNT_FULL_FOLDER_LIST_URI_COLUMN:
- builder.add(account.fullFolderListUri);
- break;
- case UIProvider.ACCOUNT_SEARCH_URI_COLUMN:
- builder.add(account.searchUri);
- break;
- case UIProvider.ACCOUNT_FROM_ADDRESSES_COLUMN:
- builder.add(account.accountFromAddresses);
- break;
- case UIProvider.ACCOUNT_SAVE_DRAFT_URI_COLUMN:
- builder.add(account.saveDraftUri);
- break;
- case UIProvider.ACCOUNT_SEND_MESSAGE_URI_COLUMN:
- builder.add(account.sendMessageUri);
- break;
- case UIProvider.ACCOUNT_EXPUNGE_MESSAGE_URI_COLUMN:
- builder.add(account.expungeMessageUri);
- break;
- case UIProvider.ACCOUNT_UNDO_URI_COLUMN:
- builder.add(account.undoUri);
- break;
- case UIProvider.ACCOUNT_SETTINGS_INTENT_URI_COLUMN:
- builder.add(account.settingsIntentUri);
- break;
- case UIProvider.ACCOUNT_HELP_INTENT_URI_COLUMN:
- builder.add(account.helpIntentUri);
- break;
- case UIProvider.ACCOUNT_SEND_FEEDBACK_INTENT_URI_COLUMN:
- builder.add(account.sendFeedbackIntentUri);
- break;
- case UIProvider.ACCOUNT_REAUTHENTICATION_INTENT_URI_COLUMN:
- builder.add(account.reauthenticationIntentUri);
- break;
- case UIProvider.ACCOUNT_SYNC_STATUS_COLUMN:
- builder.add(Integer.valueOf(account.syncStatus));
- break;
- case UIProvider.ACCOUNT_COMPOSE_INTENT_URI_COLUMN:
- builder.add(account.composeIntentUri);
- break;
- case UIProvider.ACCOUNT_MIME_TYPE_COLUMN:
- builder.add(account.mimeType);
- break;
- case UIProvider.ACCOUNT_RECENT_FOLDER_LIST_URI_COLUMN:
- builder.add(account.recentFolderListUri);
- break;
- case UIProvider.ACCOUNT_DEFAULT_RECENT_FOLDER_LIST_URI_COLUMN:
- builder.add(account.defaultRecentFolderListUri);
- break;
- case UIProvider.ACCOUNT_MANUAL_SYNC_URI_COLUMN:
- builder.add(account.manualSyncUri);
- break;
- case UIProvider.ACCOUNT_VIEW_INTENT_PROXY_URI_COLUMN:
- builder.add(account.viewIntentProxyUri);
- break;
- case UIProvider.ACCOUNT_COOKIE_QUERY_URI_COLUMN:
- builder.add(account.accoutCookieQueryUri);
- break;
- case UIProvider.ACCOUNT_COLOR_COLUMN:
- builder.add(account.color);
- break;
-
- case UIProvider.ACCOUNT_SETTINGS_SIGNATURE_COLUMN:
- builder.add(account.settings.signature);
- break;
- case UIProvider.ACCOUNT_SETTINGS_AUTO_ADVANCE_COLUMN:
- builder.add(Integer.valueOf(account.settings.getAutoAdvanceSetting()));
- break;
- case UIProvider.ACCOUNT_SETTINGS_MESSAGE_TEXT_SIZE_COLUMN:
- builder.add(Integer.valueOf(account.settings.messageTextSize));
- break;
- case UIProvider.ACCOUNT_SETTINGS_REPLY_BEHAVIOR_COLUMN:
- builder.add(Integer.valueOf(account.settings.replyBehavior));
- break;
- case UIProvider.ACCOUNT_SETTINGS_HIDE_CHECKBOXES_COLUMN:
- builder.add(Integer.valueOf(account.settings.hideCheckboxes ? 1 : 0));
- break;
- case UIProvider.ACCOUNT_SETTINGS_CONFIRM_DELETE_COLUMN:
- builder.add(Integer.valueOf(account.settings.confirmDelete ? 1 : 0));
- break;
- case UIProvider.ACCOUNT_SETTINGS_CONFIRM_ARCHIVE_COLUMN:
- builder.add(Integer.valueOf(account.settings.confirmArchive ? 1 : 0));
- break;
- case UIProvider.ACCOUNT_SETTINGS_CONFIRM_SEND_COLUMN:
- builder.add(Integer.valueOf(account.settings.confirmSend ? 1 : 0));
- break;
- case UIProvider.ACCOUNT_SETTINGS_DEFAULT_INBOX_COLUMN:
- builder.add(account.settings.defaultInbox);
- break;
- case UIProvider.ACCOUNT_SETTINGS_DEFAULT_INBOX_NAME_COLUMN:
- builder.add(account.settings.defaultInboxName);
- break;
- case UIProvider.ACCOUNT_SETTINGS_SNAP_HEADERS_COLUMN:
- builder.add(Integer.valueOf(account.settings.snapHeaders));
- break;
- case UIProvider.ACCOUNT_SETTINGS_FORCE_REPLY_FROM_DEFAULT_COLUMN:
- builder.add(Integer.valueOf(account.settings.forceReplyFromDefault ? 1 : 0));
- break;
- case UIProvider.ACCOUNT_SETTINGS_MAX_ATTACHMENT_SIZE_COLUMN:
- builder.add(account.settings.maxAttachmentSize);
- break;
- case UIProvider.ACCOUNT_SETTINGS_SWIPE_COLUMN:
- builder.add(account.settings.swipe);
- break;
- case UIProvider.ACCOUNT_SETTINGS_PRIORITY_ARROWS_ENABLED_COLUMN:
- builder.add(Integer.valueOf(account.settings.priorityArrowsEnabled ? 1 : 0));
- break;
- case UIProvider.ACCOUNT_SETTINGS_SETUP_INTENT_URI:
- builder.add(account.settings.setupIntentUri);
- break;
- case UIProvider.ACCOUNT_SETTINGS_CONVERSATION_MODE_COLUMN:
- builder.add(account.settings.conversationViewMode);
- break;
- case UIProvider.ACCOUNT_SETTINGS_VEILED_ADDRESS_PATTERN_COLUMN:
- builder.add(account.settings.veiledAddressPattern);
- break;
- case UIProvider.ACCOUNT_UPDATE_SETTINGS_URI_COLUMN:
- builder.add(account.updateSettingsUri);
- break;
- default:
- throw new IllegalStateException("Column not found: " + columnName);
+ if (accountValues.containsKey(columnName)) {
+ builder.add(accountValues.get(columnName));
+ } else {
+ throw new IllegalStateException("Unexpected column: " + columnName);
}
}
}
@@ -379,15 +235,8 @@
* @param resolver
* @param accountsQueryUri
*/
- public static void addAccountsForUriAsync(Uri accountsQueryUri) {
- synchronized (PENDING_ACCOUNT_URIS) {
- final MailAppProvider instance = getInstance();
- if (instance != null) {
- instance.startAccountsLoader(accountsQueryUri);
- } else {
- PENDING_ACCOUNT_URIS.add(accountsQueryUri);
- }
- }
+ private void addAccountsForUriAsync(Uri accountsQueryUri) {
+ startAccountsLoader(accountsQueryUri);
}
/**
diff --git a/src/com/android/mail/providers/Message.java b/src/com/android/mail/providers/Message.java
index 37e60f2..ff007eb 100644
--- a/src/com/android/mail/providers/Message.java
+++ b/src/com/android/mail/providers/Message.java
@@ -110,7 +110,7 @@
/**
* @see UIProvider.MessageColumns#REF_MESSAGE_ID
*/
- public String refMessageId;
+ public Uri refMessageUri;
/**
* @see UIProvider.MessageColumns#DRAFT_TYPE
*/
@@ -132,16 +132,6 @@
*/
public long messageFlags;
/**
- * @see UIProvider.MessageColumns#SAVE_MESSAGE_URI
- */
- @Deprecated
- public String saveUri;
- /**
- * @see UIProvider.MessageColumns#SEND_MESSAGE_URI
- */
- @Deprecated
- public String sendUri;
- /**
* @see UIProvider.MessageColumns#ALWAYS_SHOW_IMAGES
*/
public boolean alwaysShowImages;
@@ -150,6 +140,10 @@
*/
public boolean read;
/**
+ * @see UIProvider.MessageColumns#SEEN
+ */
+ public boolean seen;
+ /**
* @see UIProvider.MessageColumns#STARRED
*/
public boolean starred;
@@ -233,14 +227,12 @@
dest.writeString(bodyHtml);
dest.writeString(bodyText);
dest.writeInt(embedsExternalResources ? 1 : 0);
- dest.writeString(refMessageId);
+ dest.writeParcelable(refMessageUri, 0);
dest.writeInt(draftType);
dest.writeInt(appendRefMessageContent ? 1 : 0);
dest.writeInt(hasAttachments ? 1 : 0);
dest.writeParcelable(attachmentListUri, 0);
dest.writeLong(messageFlags);
- dest.writeString(saveUri);
- dest.writeString(sendUri);
dest.writeInt(alwaysShowImages ? 1 : 0);
dest.writeInt(quotedTextOffset);
dest.writeString(attachmentsJson);
@@ -269,14 +261,12 @@
bodyHtml = in.readString();
bodyText = in.readString();
embedsExternalResources = in.readInt() != 0;
- refMessageId = in.readString();
+ refMessageUri = in.readParcelable(null);
draftType = in.readInt();
appendRefMessageContent = in.readInt() != 0;
hasAttachments = in.readInt() != 0;
attachmentListUri = in.readParcelable(null);
messageFlags = in.readLong();
- saveUri = in.readString();
- sendUri = in.readString();
alwaysShowImages = in.readInt() != 0;
quotedTextOffset = in.readInt();
attachmentsJson = in.readString();
@@ -332,7 +322,10 @@
bodyText = cursor.getString(UIProvider.MESSAGE_BODY_TEXT_COLUMN);
embedsExternalResources = cursor
.getInt(UIProvider.MESSAGE_EMBEDS_EXTERNAL_RESOURCES_COLUMN) != 0;
- refMessageId = cursor.getString(UIProvider.MESSAGE_REF_MESSAGE_ID_COLUMN);
+ final String refMessageUriStr =
+ cursor.getString(UIProvider.MESSAGE_REF_MESSAGE_URI_COLUMN);
+ refMessageUri = !TextUtils.isEmpty(refMessageUriStr) ?
+ Uri.parse(refMessageUriStr) : null;
draftType = cursor.getInt(UIProvider.MESSAGE_DRAFT_TYPE_COLUMN);
appendRefMessageContent = cursor
.getInt(UIProvider.MESSAGE_APPEND_REF_MESSAGE_CONTENT_COLUMN) != 0;
@@ -342,12 +335,9 @@
attachmentListUri = hasAttachments && !TextUtils.isEmpty(attachmentsUri) ? Uri
.parse(attachmentsUri) : null;
messageFlags = cursor.getLong(UIProvider.MESSAGE_FLAGS_COLUMN);
- saveUri = cursor
- .getString(UIProvider.MESSAGE_SAVE_URI_COLUMN);
- sendUri = cursor
- .getString(UIProvider.MESSAGE_SEND_URI_COLUMN);
alwaysShowImages = cursor.getInt(UIProvider.MESSAGE_ALWAYS_SHOW_IMAGES_COLUMN) != 0;
read = cursor.getInt(UIProvider.MESSAGE_READ_COLUMN) != 0;
+ seen = cursor.getInt(UIProvider.MESSAGE_SEEN_COLUMN) != 0;
starred = cursor.getInt(UIProvider.MESSAGE_STARRED_COLUMN) != 0;
quotedTextOffset = cursor.getInt(UIProvider.QUOTED_TEXT_OFFSET_COLUMN);
attachmentsJson = cursor.getString(UIProvider.MESSAGE_ATTACHMENTS_COLUMN);
diff --git a/src/com/android/mail/providers/MessageModification.java b/src/com/android/mail/providers/MessageModification.java
index 9c0f7a8..c94de02 100644
--- a/src/com/android/mail/providers/MessageModification.java
+++ b/src/com/android/mail/providers/MessageModification.java
@@ -156,8 +156,6 @@
}
public static void putAttachments(ContentValues values, List<Attachment> attachments) {
- values.put(
- MessageColumns.JOINED_ATTACHMENT_INFOS, Attachment.toJSONArray(attachments));
values.put(MessageColumns.ATTACHMENTS, Attachment.toJSONArray(attachments));
}
}
diff --git a/src/com/android/mail/providers/Settings.java b/src/com/android/mail/providers/Settings.java
index b77a19d..6cb7c3a 100644
--- a/src/com/android/mail/providers/Settings.java
+++ b/src/com/android/mail/providers/Settings.java
@@ -138,29 +138,42 @@
}
public Settings(Cursor cursor) {
- signature = cursor.getString(UIProvider.ACCOUNT_SETTINGS_SIGNATURE_COLUMN);
- mAutoAdvance = cursor.getInt(UIProvider.ACCOUNT_SETTINGS_AUTO_ADVANCE_COLUMN);
- messageTextSize = cursor.getInt(UIProvider.ACCOUNT_SETTINGS_MESSAGE_TEXT_SIZE_COLUMN);
- snapHeaders = cursor.getInt(UIProvider.ACCOUNT_SETTINGS_SNAP_HEADERS_COLUMN);
- replyBehavior = cursor.getInt(UIProvider.ACCOUNT_SETTINGS_REPLY_BEHAVIOR_COLUMN);
- hideCheckboxes = cursor.getInt(UIProvider.ACCOUNT_SETTINGS_HIDE_CHECKBOXES_COLUMN) != 0;
- confirmDelete = cursor.getInt(UIProvider.ACCOUNT_SETTINGS_CONFIRM_DELETE_COLUMN) != 0;
- confirmArchive = cursor.getInt(UIProvider.ACCOUNT_SETTINGS_CONFIRM_ARCHIVE_COLUMN) != 0;
- confirmSend = cursor.getInt(UIProvider.ACCOUNT_SETTINGS_CONFIRM_SEND_COLUMN) != 0;
- defaultInbox = Utils.getValidUri(
- cursor.getString(UIProvider.ACCOUNT_SETTINGS_DEFAULT_INBOX_COLUMN));
- defaultInboxName = cursor.getString(UIProvider.ACCOUNT_SETTINGS_DEFAULT_INBOX_NAME_COLUMN);
- forceReplyFromDefault = cursor.getInt(
- UIProvider.ACCOUNT_SETTINGS_FORCE_REPLY_FROM_DEFAULT_COLUMN) != 0;
- maxAttachmentSize = cursor.getInt(UIProvider.ACCOUNT_SETTINGS_MAX_ATTACHMENT_SIZE_COLUMN);
- swipe = cursor.getInt(UIProvider.ACCOUNT_SETTINGS_SWIPE_COLUMN);
- priorityArrowsEnabled =
- cursor.getInt(UIProvider.ACCOUNT_SETTINGS_PRIORITY_ARROWS_ENABLED_COLUMN) != 0;
- setupIntentUri = Utils.getValidUri(
- cursor.getString(UIProvider.ACCOUNT_SETTINGS_SETUP_INTENT_URI));
- conversationViewMode = cursor.getInt(UIProvider.ACCOUNT_SETTINGS_CONVERSATION_MODE_COLUMN);
- veiledAddressPattern =
- cursor.getString(UIProvider.ACCOUNT_SETTINGS_VEILED_ADDRESS_PATTERN_COLUMN);
+ signature = cursor.getString(
+ cursor.getColumnIndex(UIProvider.AccountColumns.SettingsColumns.SIGNATURE));
+ mAutoAdvance = cursor.getInt(
+ cursor.getColumnIndex(UIProvider.AccountColumns.SettingsColumns.AUTO_ADVANCE));
+ messageTextSize = cursor.getInt(
+ cursor.getColumnIndex(UIProvider.AccountColumns.SettingsColumns.MESSAGE_TEXT_SIZE));
+ snapHeaders = cursor.getInt(
+ cursor.getColumnIndex(UIProvider.AccountColumns.SettingsColumns.SNAP_HEADERS));
+ replyBehavior = cursor.getInt(
+ cursor.getColumnIndex(UIProvider.AccountColumns.SettingsColumns.REPLY_BEHAVIOR));
+ hideCheckboxes = cursor.getInt(cursor.getColumnIndex(
+ UIProvider.AccountColumns.SettingsColumns.HIDE_CHECKBOXES)) != 0;
+ confirmDelete = cursor.getInt(cursor.getColumnIndex(
+ UIProvider.AccountColumns.SettingsColumns.CONFIRM_DELETE)) != 0;
+ confirmArchive = cursor.getInt(cursor.getColumnIndex(
+ UIProvider.AccountColumns.SettingsColumns.CONFIRM_ARCHIVE)) != 0;
+ confirmSend = cursor.getInt(
+ cursor.getColumnIndex(UIProvider.AccountColumns.SettingsColumns.CONFIRM_SEND)) != 0;
+ defaultInbox = Utils.getValidUri(cursor.getString(
+ cursor.getColumnIndex(UIProvider.AccountColumns.SettingsColumns.DEFAULT_INBOX)));
+ defaultInboxName = cursor.getString(cursor.getColumnIndex(
+ UIProvider.AccountColumns.SettingsColumns.DEFAULT_INBOX_NAME));
+ forceReplyFromDefault = cursor.getInt(cursor.getColumnIndex(
+ UIProvider.AccountColumns.SettingsColumns.FORCE_REPLY_FROM_DEFAULT)) != 0;
+ maxAttachmentSize = cursor.getInt(cursor.getColumnIndex(
+ UIProvider.AccountColumns.SettingsColumns.MAX_ATTACHMENT_SIZE));
+ swipe = cursor.getInt(
+ cursor.getColumnIndex(UIProvider.AccountColumns.SettingsColumns.SWIPE));
+ priorityArrowsEnabled = cursor.getInt(cursor.getColumnIndex(
+ UIProvider.AccountColumns.SettingsColumns.PRIORITY_ARROWS_ENABLED)) != 0;
+ setupIntentUri = Utils.getValidUri(cursor.getString(
+ cursor.getColumnIndex(UIProvider.AccountColumns.SettingsColumns.SETUP_INTENT_URI)));
+ conversationViewMode = cursor.getInt(cursor.getColumnIndex(
+ UIProvider.AccountColumns.SettingsColumns.CONVERSATION_VIEW_MODE));
+ veiledAddressPattern = cursor.getString(cursor.getColumnIndex(
+ UIProvider.AccountColumns.SettingsColumns.VEILED_ADDRESS_PATTERN));
}
private Settings(JSONObject json) {
@@ -347,6 +360,19 @@
}
/**
+ * @return true if {@link UIProvider.ConversationViewMode.OVERVIEW} mode is set. In the event
+ * that the setting is not yet set, fall back to
+ * {@link UIProvider.ConversationViewMode.DEFAULT}.
+ */
+ public boolean isOverviewMode() {
+ final boolean isDefined = (conversationViewMode
+ != UIProvider.ConversationViewMode.UNDEFINED);
+ final int val = (conversationViewMode != UIProvider.ConversationViewMode.UNDEFINED) ?
+ conversationViewMode : UIProvider.ConversationViewMode.DEFAULT;
+ return (val == UIProvider.ConversationViewMode.OVERVIEW);
+ }
+
+ /**
* Return the swipe setting for the settings provided. It is safe to pass this method
* a null object. It always returns a valid {@link Swipe} setting.
* @return the auto advance setting, a constant from {@link Swipe}
diff --git a/src/com/android/mail/providers/UIProvider.java b/src/com/android/mail/providers/UIProvider.java
index c60de8b..86ab8aa 100644
--- a/src/com/android/mail/providers/UIProvider.java
+++ b/src/com/android/mail/providers/UIProvider.java
@@ -116,21 +116,19 @@
* required to respect this query parameter
*/
public static final String LIST_PARAMS_QUERY_PARAMETER = "listParams";
+ public static final String LABEL_QUERY_PARAMETER = "label";
+ public static final String SEEN_QUERY_PARAMETER = "seen";
- public static final Map<String, Class<?>> ACCOUNTS_COLUMNS =
+ public static final Map<String, Class<?>> ACCOUNTS_COLUMNS_NO_CAPABILITIES =
new ImmutableMap.Builder<String, Class<?>>()
- // order matters! (ImmutableMap.Builder preserves insertion order)
- .put(BaseColumns._ID, Integer.class)
+ .put(AccountColumns._ID, Integer.class)
.put(AccountColumns.NAME, String.class)
.put(AccountColumns.PROVIDER_VERSION, Integer.class)
.put(AccountColumns.URI, String.class)
- .put(AccountColumns.CAPABILITIES, Integer.class)
.put(AccountColumns.FOLDER_LIST_URI, String.class)
.put(AccountColumns.FULL_FOLDER_LIST_URI, String.class)
.put(AccountColumns.SEARCH_URI, String.class)
.put(AccountColumns.ACCOUNT_FROM_ADDRESSES, String.class)
- .put(AccountColumns.SAVE_DRAFT_URI, String.class)
- .put(AccountColumns.SEND_MAIL_URI, String.class)
.put(AccountColumns.EXPUNGE_MESSAGE_URI, String.class)
.put(AccountColumns.UNDO_URI, String.class)
.put(AccountColumns.SETTINGS_INTENT_URI, String.class)
@@ -167,57 +165,19 @@
.put(AccountColumns.UPDATE_SETTINGS_URI, String.class)
.build();
- // pull out the (ordered!) keyset from above to form the projection
- public static final String[] ACCOUNTS_PROJECTION = ACCOUNTS_COLUMNS.keySet()
- .toArray(new String[ACCOUNTS_COLUMNS.size()]);
+ public static final Map<String, Class<?>> ACCOUNTS_COLUMNS =
+ new ImmutableMap.Builder<String, Class<?>>()
+ .putAll(ACCOUNTS_COLUMNS_NO_CAPABILITIES)
+ .put(AccountColumns.CAPABILITIES, Integer.class)
+ .build();
- public static final int ACCOUNT_ID_COLUMN = 0;
- public static final int ACCOUNT_NAME_COLUMN = 1;
- public static final int ACCOUNT_PROVIDER_VERISON_COLUMN = 2;
- public static final int ACCOUNT_URI_COLUMN = 3;
- public static final int ACCOUNT_CAPABILITIES_COLUMN = 4;
- public static final int ACCOUNT_FOLDER_LIST_URI_COLUMN = 5;
- public static final int ACCOUNT_FULL_FOLDER_LIST_URI_COLUMN = 6;
- public static final int ACCOUNT_SEARCH_URI_COLUMN = 7;
- public static final int ACCOUNT_FROM_ADDRESSES_COLUMN = 8;
- public static final int ACCOUNT_SAVE_DRAFT_URI_COLUMN = 9;
- public static final int ACCOUNT_SEND_MESSAGE_URI_COLUMN = 10;
- public static final int ACCOUNT_EXPUNGE_MESSAGE_URI_COLUMN = 11;
- public static final int ACCOUNT_UNDO_URI_COLUMN = 12;
- public static final int ACCOUNT_SETTINGS_INTENT_URI_COLUMN = 13;
- public static final int ACCOUNT_SYNC_STATUS_COLUMN = 14;
- public static final int ACCOUNT_HELP_INTENT_URI_COLUMN = 15;
- public static final int ACCOUNT_SEND_FEEDBACK_INTENT_URI_COLUMN = 16;
- public static final int ACCOUNT_REAUTHENTICATION_INTENT_URI_COLUMN = 17;
- public static final int ACCOUNT_COMPOSE_INTENT_URI_COLUMN = 18;
- public static final int ACCOUNT_MIME_TYPE_COLUMN = 19;
- public static final int ACCOUNT_RECENT_FOLDER_LIST_URI_COLUMN = 20;
- public static final int ACCOUNT_COLOR_COLUMN = 21;
- public static final int ACCOUNT_DEFAULT_RECENT_FOLDER_LIST_URI_COLUMN = 22;
- public static final int ACCOUNT_MANUAL_SYNC_URI_COLUMN = 23;
- public static final int ACCOUNT_VIEW_INTENT_PROXY_URI_COLUMN = 24;
- public static final int ACCOUNT_COOKIE_QUERY_URI_COLUMN = 25;
+ // pull out the keyset from above to form the projection
+ public static final String[] ACCOUNTS_PROJECTION =
+ ACCOUNTS_COLUMNS.keySet().toArray(new String[ACCOUNTS_COLUMNS.size()]);
- public static final int ACCOUNT_SETTINGS_SIGNATURE_COLUMN = 26;
- public static final int ACCOUNT_SETTINGS_AUTO_ADVANCE_COLUMN = 27;
- public static final int ACCOUNT_SETTINGS_MESSAGE_TEXT_SIZE_COLUMN = 28;
- public static final int ACCOUNT_SETTINGS_SNAP_HEADERS_COLUMN = 29;
- public static final int ACCOUNT_SETTINGS_REPLY_BEHAVIOR_COLUMN = 30;
- public static final int ACCOUNT_SETTINGS_HIDE_CHECKBOXES_COLUMN = 31;
- public static final int ACCOUNT_SETTINGS_CONFIRM_DELETE_COLUMN = 32;
- public static final int ACCOUNT_SETTINGS_CONFIRM_ARCHIVE_COLUMN = 33;
- public static final int ACCOUNT_SETTINGS_CONFIRM_SEND_COLUMN = 34;
- public static final int ACCOUNT_SETTINGS_DEFAULT_INBOX_COLUMN = 35;
- public static final int ACCOUNT_SETTINGS_DEFAULT_INBOX_NAME_COLUMN = 36;
- public static final int ACCOUNT_SETTINGS_FORCE_REPLY_FROM_DEFAULT_COLUMN = 37;
- public static final int ACCOUNT_SETTINGS_MAX_ATTACHMENT_SIZE_COLUMN = 38;
- public static final int ACCOUNT_SETTINGS_SWIPE_COLUMN = 39;
- public static final int ACCOUNT_SETTINGS_PRIORITY_ARROWS_ENABLED_COLUMN = 40;
- public static final int ACCOUNT_SETTINGS_SETUP_INTENT_URI = 41;
- public static final int ACCOUNT_SETTINGS_CONVERSATION_MODE_COLUMN = 42;
- public static final int ACCOUNT_SETTINGS_VEILED_ADDRESS_PATTERN_COLUMN = 43;
-
- public static final int ACCOUNT_UPDATE_SETTINGS_URI_COLUMN = 44;
+ public static final
+ String[] ACCOUNTS_PROJECTION_NO_CAPABILITIES = ACCOUNTS_COLUMNS_NO_CAPABILITIES.keySet()
+ .toArray(new String[ACCOUNTS_COLUMNS_NO_CAPABILITIES.size()]);
public static final class AccountCapabilities {
/**
@@ -323,7 +283,7 @@
public static final int DISCARD_CONVERSATION_DRAFTS = 0x100000;
}
- public static final class AccountColumns {
+ public static final class AccountColumns implements BaseColumns {
/**
* This string column contains the human visible name for the account.
*/
@@ -381,22 +341,6 @@
public static final String ACCOUNT_FROM_ADDRESSES = "accountFromAddresses";
/**
- * This string column contains the content provider uri that can be used to save (insert)
- * new draft messages for this account. NOTE: This might be better to
- * be an update operation on the messageUri.
- */
- @Deprecated
- public static final String SAVE_DRAFT_URI = "saveDraftUri";
-
- /**
- * This string column contains the content provider uri that can be used to send
- * a message for this account.
- * NOTE: This might be better to be an update operation on the messageUri.
- */
- @Deprecated
- public static final String SEND_MAIL_URI = "sendMailUri";
-
- /**
* This string column contains the content provider uri that can be used
* to expunge a message from this account. NOTE: This might be better to
* be an update operation on the messageUri.
@@ -596,91 +540,6 @@
}
}
- /**
- * Map to go from account column name to account column. Can only be used through
- * {@link #getAccountColumn(String)}.
- */
- private static final ImmutableMap<String, Integer> ACCOUNT_TO_ID_MAP =
- new ImmutableMap.Builder<String, Integer>()
- .put(BaseColumns._ID, ACCOUNT_ID_COLUMN)
- .put(AccountColumns.NAME, ACCOUNT_NAME_COLUMN)
- .put(AccountColumns.TYPE, -1)
- .put(AccountColumns.PROVIDER_VERSION, ACCOUNT_PROVIDER_VERISON_COLUMN)
- .put(AccountColumns.URI, ACCOUNT_URI_COLUMN)
- .put(AccountColumns.CAPABILITIES, ACCOUNT_CAPABILITIES_COLUMN)
- .put(AccountColumns.FOLDER_LIST_URI, ACCOUNT_FOLDER_LIST_URI_COLUMN)
- .put(AccountColumns.FULL_FOLDER_LIST_URI, ACCOUNT_FULL_FOLDER_LIST_URI_COLUMN)
- .put(AccountColumns.SEARCH_URI, ACCOUNT_SEARCH_URI_COLUMN)
- .put(AccountColumns.ACCOUNT_FROM_ADDRESSES, ACCOUNT_FROM_ADDRESSES_COLUMN)
- .put(AccountColumns.SAVE_DRAFT_URI, ACCOUNT_SAVE_DRAFT_URI_COLUMN)
- .put(AccountColumns.SEND_MAIL_URI, ACCOUNT_SEND_MESSAGE_URI_COLUMN)
- .put(AccountColumns.EXPUNGE_MESSAGE_URI, ACCOUNT_EXPUNGE_MESSAGE_URI_COLUMN)
- .put(AccountColumns.UNDO_URI, ACCOUNT_UNDO_URI_COLUMN)
- .put(AccountColumns.SETTINGS_INTENT_URI, ACCOUNT_SETTINGS_INTENT_URI_COLUMN)
- .put(AccountColumns.HELP_INTENT_URI, ACCOUNT_HELP_INTENT_URI_COLUMN)
- .put(AccountColumns.SEND_FEEDBACK_INTENT_URI, ACCOUNT_SEND_FEEDBACK_INTENT_URI_COLUMN)
- .put(AccountColumns.REAUTHENTICATION_INTENT_URI,
- ACCOUNT_REAUTHENTICATION_INTENT_URI_COLUMN)
- .put(AccountColumns.SYNC_STATUS, ACCOUNT_SYNC_STATUS_COLUMN)
- .put(AccountColumns.COMPOSE_URI, ACCOUNT_COMPOSE_INTENT_URI_COLUMN)
- .put(AccountColumns.MIME_TYPE, ACCOUNT_MIME_TYPE_COLUMN)
- .put(AccountColumns.RECENT_FOLDER_LIST_URI, ACCOUNT_RECENT_FOLDER_LIST_URI_COLUMN)
- .put(AccountColumns.DEFAULT_RECENT_FOLDER_LIST_URI,
- ACCOUNT_DEFAULT_RECENT_FOLDER_LIST_URI_COLUMN)
- .put(AccountColumns.COLOR, ACCOUNT_COLOR_COLUMN)
- .put(AccountColumns.MANUAL_SYNC_URI, ACCOUNT_MANUAL_SYNC_URI_COLUMN)
- .put(AccountColumns.VIEW_INTENT_PROXY_URI, ACCOUNT_VIEW_INTENT_PROXY_URI_COLUMN)
- .put(AccountColumns.ACCOUNT_COOKIE_QUERY_URI, ACCOUNT_COOKIE_QUERY_URI_COLUMN)
- .put(AccountColumns.SettingsColumns.SIGNATURE, ACCOUNT_SETTINGS_SIGNATURE_COLUMN)
- .put(AccountColumns.SettingsColumns.AUTO_ADVANCE, ACCOUNT_SETTINGS_AUTO_ADVANCE_COLUMN)
- .put(AccountColumns.SettingsColumns.MESSAGE_TEXT_SIZE,
- ACCOUNT_SETTINGS_MESSAGE_TEXT_SIZE_COLUMN)
- .put(AccountColumns.SettingsColumns.SNAP_HEADERS,ACCOUNT_SETTINGS_SNAP_HEADERS_COLUMN)
- .put(AccountColumns.SettingsColumns.REPLY_BEHAVIOR,
- ACCOUNT_SETTINGS_REPLY_BEHAVIOR_COLUMN)
- .put(AccountColumns.SettingsColumns.HIDE_CHECKBOXES,
- ACCOUNT_SETTINGS_HIDE_CHECKBOXES_COLUMN)
- .put(AccountColumns.SettingsColumns.CONFIRM_DELETE,
- ACCOUNT_SETTINGS_CONFIRM_DELETE_COLUMN)
- .put(AccountColumns.SettingsColumns.CONFIRM_ARCHIVE,
- ACCOUNT_SETTINGS_CONFIRM_ARCHIVE_COLUMN)
- .put(AccountColumns.SettingsColumns.CONFIRM_SEND,
- ACCOUNT_SETTINGS_CONFIRM_SEND_COLUMN)
- .put(AccountColumns.SettingsColumns.DEFAULT_INBOX,
- ACCOUNT_SETTINGS_DEFAULT_INBOX_COLUMN)
- .put(AccountColumns.SettingsColumns.DEFAULT_INBOX_NAME,
- ACCOUNT_SETTINGS_DEFAULT_INBOX_NAME_COLUMN)
- .put(AccountColumns.SettingsColumns.FORCE_REPLY_FROM_DEFAULT,
- ACCOUNT_SETTINGS_FORCE_REPLY_FROM_DEFAULT_COLUMN)
- .put(AccountColumns.SettingsColumns.MAX_ATTACHMENT_SIZE,
- ACCOUNT_SETTINGS_MAX_ATTACHMENT_SIZE_COLUMN)
- .put(AccountColumns.SettingsColumns.SWIPE, ACCOUNT_SETTINGS_SWIPE_COLUMN)
- .put(AccountColumns.SettingsColumns.PRIORITY_ARROWS_ENABLED,
- ACCOUNT_SETTINGS_PRIORITY_ARROWS_ENABLED_COLUMN)
- .put(AccountColumns.SettingsColumns.SETUP_INTENT_URI,
- ACCOUNT_SETTINGS_SETUP_INTENT_URI)
- .put(AccountColumns.SettingsColumns.CONVERSATION_VIEW_MODE,
- ACCOUNT_SETTINGS_CONVERSATION_MODE_COLUMN)
- .put(AccountColumns.SettingsColumns.VEILED_ADDRESS_PATTERN,
- ACCOUNT_SETTINGS_VEILED_ADDRESS_PATTERN_COLUMN)
- .put(AccountColumns.UPDATE_SETTINGS_URI, ACCOUNT_UPDATE_SETTINGS_URI_COLUMN)
- .build();
-
- /**
- * Returns the column number for a given column name. The column numbers are guaranteed to be
- * unique for distinct column names. Column names are values from {@link AccountColumns} while
- * columns are integers.
- * @param columnName
- * @return
- */
- public static final int getAccountColumn(String columnName) {
- final Integer id = ACCOUNT_TO_ID_MAP.get(columnName);
- if (id == null) {
- return -1;
- }
- return id.intValue();
- }
-
public static final String[] ACCOUNT_COOKIE_PROJECTION = {
AccountCookieColumns.COOKIE
};
@@ -735,6 +594,7 @@
public static final String[] FOLDERS_PROJECTION = {
BaseColumns._ID,
+ FolderColumns.PERSISTENT_ID,
FolderColumns.URI,
FolderColumns.NAME,
FolderColumns.HAS_CHILDREN,
@@ -742,6 +602,7 @@
FolderColumns.SYNC_WINDOW,
FolderColumns.CONVERSATION_LIST_URI,
FolderColumns.CHILD_FOLDERS_LIST_URI,
+ FolderColumns.UNSEEN_COUNT,
FolderColumns.UNREAD_COUNT,
FolderColumns.TOTAL_COUNT,
FolderColumns.REFRESH_URI,
@@ -749,31 +610,37 @@
FolderColumns.LAST_SYNC_RESULT,
FolderColumns.TYPE,
FolderColumns.ICON_RES_ID,
+ FolderColumns.NOTIFICATION_ICON_RES_ID,
FolderColumns.BG_COLOR,
FolderColumns.FG_COLOR,
FolderColumns.LOAD_MORE_URI,
- FolderColumns.HIERARCHICAL_DESC
+ FolderColumns.HIERARCHICAL_DESC,
+ FolderColumns.LAST_MESSAGE_TIMESTAMP
};
public static final int FOLDER_ID_COLUMN = 0;
- public static final int FOLDER_URI_COLUMN = 1;
- public static final int FOLDER_NAME_COLUMN = 2;
- public static final int FOLDER_HAS_CHILDREN_COLUMN = 3;
- public static final int FOLDER_CAPABILITIES_COLUMN = 4;
- public static final int FOLDER_SYNC_WINDOW_COLUMN = 5;
- public static final int FOLDER_CONVERSATION_LIST_URI_COLUMN = 6;
- public static final int FOLDER_CHILD_FOLDERS_LIST_COLUMN = 7;
- public static final int FOLDER_UNREAD_COUNT_COLUMN = 8;
- public static final int FOLDER_TOTAL_COUNT_COLUMN = 9;
- public static final int FOLDER_REFRESH_URI_COLUMN = 10;
- public static final int FOLDER_SYNC_STATUS_COLUMN = 11;
- public static final int FOLDER_LAST_SYNC_RESULT_COLUMN = 12;
- public static final int FOLDER_TYPE_COLUMN = 13;
- public static final int FOLDER_ICON_RES_ID_COLUMN = 14;
- public static final int FOLDER_BG_COLOR_COLUMN = 15;
- public static final int FOLDER_FG_COLOR_COLUMN = 16;
- public static final int FOLDER_LOAD_MORE_URI_COLUMN = 17;
- public static final int FOLDER_HIERARCHICAL_DESC_COLUMN = 18;
+ public static final int FOLDER_PERSISTENT_ID_COLUMN = 1;
+ public static final int FOLDER_URI_COLUMN = 2;
+ public static final int FOLDER_NAME_COLUMN = 3;
+ public static final int FOLDER_HAS_CHILDREN_COLUMN = 4;
+ public static final int FOLDER_CAPABILITIES_COLUMN = 5;
+ public static final int FOLDER_SYNC_WINDOW_COLUMN = 6;
+ public static final int FOLDER_CONVERSATION_LIST_URI_COLUMN = 7;
+ public static final int FOLDER_CHILD_FOLDERS_LIST_COLUMN = 8;
+ public static final int FOLDER_UNSEEN_COUNT_COLUMN = 9;
+ public static final int FOLDER_UNREAD_COUNT_COLUMN = 10;
+ public static final int FOLDER_TOTAL_COUNT_COLUMN = 11;
+ public static final int FOLDER_REFRESH_URI_COLUMN = 12;
+ public static final int FOLDER_SYNC_STATUS_COLUMN = 13;
+ public static final int FOLDER_LAST_SYNC_RESULT_COLUMN = 14;
+ public static final int FOLDER_TYPE_COLUMN = 15;
+ public static final int FOLDER_ICON_RES_ID_COLUMN = 16;
+ public static final int FOLDER_NOTIFICATION_ICON_RES_ID_COLUMN = 17;
+ public static final int FOLDER_BG_COLOR_COLUMN = 18;
+ public static final int FOLDER_FG_COLOR_COLUMN = 19;
+ public static final int FOLDER_LOAD_MORE_URI_COLUMN = 20;
+ public static final int FOLDER_HIERARCHICAL_DESC_COLUMN = 21;
+ public static final int FOLDER_LAST_MESSAGE_TIMESTAMP_COLUMN = 22;
public static final class FolderType {
/** A user defined label. */
@@ -796,6 +663,8 @@
public static final int OTHER_PROVIDER_FOLDER = 8;
/** All mail folder **/
public static final int ALL_MAIL = 9;
+ /** Gmail's inbox sections **/
+ public static final int INBOX_SECTION = 10;
}
public static final class FolderCapabilities {
@@ -856,10 +725,21 @@
* the report phishing functionality.
*/
public static final int REPORT_PHISHING = 0x2000;
+
+ /**
+ * The flag indicates that the user has the ability to move conversations
+ * from this folder.
+ */
+ public static final int ALLOWS_REMOVE_CONVERSATION = 0x4000;
}
public static final class FolderColumns {
/**
+ * This string column contains an id for the folder that is constant across devices, or
+ * null if there is no constant id.
+ */
+ public static final String PERSISTENT_ID = "persistentId";
+ /**
* This string column contains the uri of the folder.
*/
public static final String URI = "folderUri";
@@ -891,7 +771,13 @@
* list of child folders of this folder.
*/
public static final String CHILD_FOLDERS_LIST_URI = "childFoldersListUri";
-
+ /**
+ * This int column contains the current unseen count for the folder, if known.
+ */
+ public static final String UNSEEN_COUNT = "unseenCount";
+ /**
+ * This int column contains the current unread count for the folder.
+ */
public static final String UNREAD_COUNT = "unreadCount";
public static final String TOTAL_COUNT = "totalCount";
@@ -911,10 +797,15 @@
*/
public static final String LAST_SYNC_RESULT = "lastSyncResult";
/**
- * This long column contains the icon res id for this folder, or 0 if there is none.
+ * This int column contains the icon res id for this folder, or 0 if there is none.
*/
public static final String ICON_RES_ID = "iconResId";
/**
+ * This int column contains the notification icon res id for this folder, or 0 if there is
+ * none.
+ */
+ public static final String NOTIFICATION_ICON_RES_ID = "notificationIconResId";
+ /**
* This int column contains the type of the folder. Zero is default.
*/
public static final String TYPE = "type";
@@ -939,6 +830,11 @@
*/
public static final String HIERARCHICAL_DESC = "hierarchicalDesc";
+ /**
+ * The timestamp of the last message received in this folder.
+ */
+ public static final String LAST_MESSAGE_TIMESTAMP = "lastMessageTimestamp";
+
public FolderColumns() {}
}
@@ -963,6 +859,7 @@
ConversationColumns.SENDING_STATE,
ConversationColumns.PRIORITY,
ConversationColumns.READ,
+ ConversationColumns.SEEN,
ConversationColumns.STARRED,
ConversationColumns.RAW_FOLDERS,
ConversationColumns.FLAGS,
@@ -992,18 +889,19 @@
public static final int CONVERSATION_SENDING_STATE_COLUMN = 10;
public static final int CONVERSATION_PRIORITY_COLUMN = 11;
public static final int CONVERSATION_READ_COLUMN = 12;
- public static final int CONVERSATION_STARRED_COLUMN = 13;
- public static final int CONVERSATION_RAW_FOLDERS_COLUMN = 14;
- public static final int CONVERSATION_FLAGS_COLUMN = 15;
- public static final int CONVERSATION_PERSONAL_LEVEL_COLUMN = 16;
- public static final int CONVERSATION_IS_SPAM_COLUMN = 17;
- public static final int CONVERSATION_IS_PHISHING_COLUMN = 18;
- public static final int CONVERSATION_MUTED_COLUMN = 19;
- public static final int CONVERSATION_COLOR_COLUMN = 20;
- public static final int CONVERSATION_ACCOUNT_URI_COLUMN = 21;
- public static final int CONVERSATION_SENDER_INFO_COLUMN = 22;
- public static final int CONVERSATION_BASE_URI_COLUMN = 23;
- public static final int CONVERSATION_REMOTE_COLUMN = 24;
+ public static final int CONVERSATION_SEEN_COLUMN = 13;
+ public static final int CONVERSATION_STARRED_COLUMN = 14;
+ public static final int CONVERSATION_RAW_FOLDERS_COLUMN = 15;
+ public static final int CONVERSATION_FLAGS_COLUMN = 16;
+ public static final int CONVERSATION_PERSONAL_LEVEL_COLUMN = 17;
+ public static final int CONVERSATION_IS_SPAM_COLUMN = 18;
+ public static final int CONVERSATION_IS_PHISHING_COLUMN = 19;
+ public static final int CONVERSATION_MUTED_COLUMN = 20;
+ public static final int CONVERSATION_COLOR_COLUMN = 21;
+ public static final int CONVERSATION_ACCOUNT_URI_COLUMN = 22;
+ public static final int CONVERSATION_SENDER_INFO_COLUMN = 23;
+ public static final int CONVERSATION_BASE_URI_COLUMN = 24;
+ public static final int CONVERSATION_REMOTE_COLUMN = 25;
public static final class ConversationSendingState {
public static final int OTHER = 0;
@@ -1109,6 +1007,12 @@
* This int column indicates whether the conversation has been read
*/
public static String READ = "read";
+
+ /**
+ * This int column indicates whether the conversation has been seen
+ */
+ public static String SEEN = "seen";
+
/**
* This int column indicates whether the conversation has been starred
*/
@@ -1399,11 +1303,9 @@
MessageColumns.HAS_ATTACHMENTS,
MessageColumns.ATTACHMENT_LIST_URI,
MessageColumns.MESSAGE_FLAGS,
- MessageColumns.JOINED_ATTACHMENT_INFOS,
- MessageColumns.SAVE_MESSAGE_URI,
- MessageColumns.SEND_MESSAGE_URI,
MessageColumns.ALWAYS_SHOW_IMAGES,
MessageColumns.READ,
+ MessageColumns.SEEN,
MessageColumns.STARRED,
MessageColumns.QUOTE_START_POS,
MessageColumns.ATTACHMENTS,
@@ -1440,28 +1342,26 @@
public static final int MESSAGE_BODY_HTML_COLUMN = 12;
public static final int MESSAGE_BODY_TEXT_COLUMN = 13;
public static final int MESSAGE_EMBEDS_EXTERNAL_RESOURCES_COLUMN = 14;
- public static final int MESSAGE_REF_MESSAGE_ID_COLUMN = 15;
+ public static final int MESSAGE_REF_MESSAGE_URI_COLUMN = 15;
public static final int MESSAGE_DRAFT_TYPE_COLUMN = 16;
public static final int MESSAGE_APPEND_REF_MESSAGE_CONTENT_COLUMN = 17;
public static final int MESSAGE_HAS_ATTACHMENTS_COLUMN = 18;
public static final int MESSAGE_ATTACHMENT_LIST_URI_COLUMN = 19;
public static final int MESSAGE_FLAGS_COLUMN = 20;
- public static final int MESSAGE_JOINED_ATTACHMENT_INFOS_COLUMN = 21;
- public static final int MESSAGE_SAVE_URI_COLUMN = 22;
- public static final int MESSAGE_SEND_URI_COLUMN = 23;
- public static final int MESSAGE_ALWAYS_SHOW_IMAGES_COLUMN = 24;
- public static final int MESSAGE_READ_COLUMN = 25;
- public static final int MESSAGE_STARRED_COLUMN = 26;
- public static final int QUOTED_TEXT_OFFSET_COLUMN = 27;
- public static final int MESSAGE_ATTACHMENTS_COLUMN = 28;
- public static final int MESSAGE_CUSTOM_FROM_ADDRESS_COLUMN = 29;
- public static final int MESSAGE_ACCOUNT_URI_COLUMN = 30;
- public static final int MESSAGE_EVENT_INTENT_COLUMN = 31;
- public static final int MESSAGE_SPAM_WARNING_STRING_ID_COLUMN = 32;
- public static final int MESSAGE_SPAM_WARNING_LEVEL_COLUMN = 33;
- public static final int MESSAGE_SPAM_WARNING_LINK_TYPE_COLUMN = 34;
- public static final int MESSAGE_VIA_DOMAIN_COLUMN = 35;
- public static final int MESSAGE_IS_SENDING_COLUMN = 36;
+ public static final int MESSAGE_ALWAYS_SHOW_IMAGES_COLUMN = 21;
+ public static final int MESSAGE_READ_COLUMN = 22;
+ public static final int MESSAGE_SEEN_COLUMN = 23;
+ public static final int MESSAGE_STARRED_COLUMN = 24;
+ public static final int QUOTED_TEXT_OFFSET_COLUMN = 25;
+ public static final int MESSAGE_ATTACHMENTS_COLUMN = 26;
+ public static final int MESSAGE_CUSTOM_FROM_ADDRESS_COLUMN = 27;
+ public static final int MESSAGE_ACCOUNT_URI_COLUMN = 28;
+ public static final int MESSAGE_EVENT_INTENT_COLUMN = 29;
+ public static final int MESSAGE_SPAM_WARNING_STRING_ID_COLUMN = 30;
+ public static final int MESSAGE_SPAM_WARNING_LEVEL_COLUMN = 31;
+ public static final int MESSAGE_SPAM_WARNING_LINK_TYPE_COLUMN = 32;
+ public static final int MESSAGE_VIA_DOMAIN_COLUMN = 33;
+ public static final int MESSAGE_IS_SENDING_COLUMN = 34;
public static final class CursorStatus {
// The cursor is actively loading more data
@@ -1495,6 +1395,11 @@
*/
public static final String EXTRA_ERROR = "cursor_error";
+
+ /**
+ * This integer column contains the total message count for this folder.
+ */
+ public static final String EXTRA_TOTAL_COUNT = "cursor_total_count";
}
public static final class AccountCursorExtraKeys {
@@ -1598,27 +1503,6 @@
*/
public static final String MESSAGE_FLAGS = "messageFlags";
/**
- * This string column contains a specially formatted string representing all
- * attachments that we added to a message that is being sent or saved.
- *
- * TODO: remove this and use {@link #ATTACHMENTS} instead
- */
- @Deprecated
- public static final String JOINED_ATTACHMENT_INFOS = "joinedAttachmentInfos";
- /**
- * This string column contains the content provider URI for saving this
- * message.
- */
- @Deprecated
- public static final String SAVE_MESSAGE_URI = "saveMessageUri";
- /**
- * This string column contains content provider URI for sending this
- * message.
- */
- @Deprecated
- public static final String SEND_MESSAGE_URI = "sendMessageUri";
-
- /**
* This integer column represents whether the user has specified that images should always
* be shown. The value of "1" indicates that the user has specified that images should be
* shown, while the value of "0" indicates that the user should be prompted before loading
@@ -1632,6 +1516,11 @@
public static String READ = "read";
/**
+ * This boolean column indicates whether the message has been seen
+ */
+ public static String SEEN = "seen";
+
+ /**
* This boolean column indicates whether the message has been starred
*/
public static String STARRED = "starred";
@@ -1929,12 +1818,15 @@
*/
public static final int BEST = 1;
+ private static final String SIMPLE_STRING = "SIMPLE";
+ private static final String BEST_STRING = "BEST";
+
public static int parseRendition(String rendition) {
- return rendition.equalsIgnoreCase("BEST") ? BEST : SIMPLE;
+ return TextUtils.equals(rendition, SIMPLE_STRING) ? SIMPLE : BEST;
}
public static String toString(int rendition) {
- return rendition == BEST ? "BEST" : "SIMPLE";
+ return rendition == BEST ? BEST_STRING : SIMPLE_STRING;
}
}
@@ -2042,6 +1934,7 @@
* require panning
*/
public static final int READING = 1;
+ public static final int DEFAULT = OVERVIEW;
}
public static final class SnapHeaderValue {
diff --git a/src/com/android/mail/providers/protos/mock/MockUiProvider.java b/src/com/android/mail/providers/protos/mock/MockUiProvider.java
index 97bad3c..1055b42 100644
--- a/src/com/android/mail/providers/protos/mock/MockUiProvider.java
+++ b/src/com/android/mail/providers/protos/mock/MockUiProvider.java
@@ -28,6 +28,7 @@
import com.android.mail.providers.Account;
import com.android.mail.providers.ConversationInfo;
import com.android.mail.providers.Folder;
+import com.android.mail.providers.FolderList;
import com.android.mail.providers.MailAppProvider;
import com.android.mail.providers.MessageInfo;
import com.android.mail.providers.ReplyFromAccount;
@@ -212,37 +213,34 @@
conversationMap.put(ConversationColumns.NUM_DRAFTS, 1);
conversationMap.put(ConversationColumns.SENDING_STATE, 1);
conversationMap.put(ConversationColumns.READ, 0);
+ conversationMap.put(ConversationColumns.SEEN, 0);
conversationMap.put(ConversationColumns.STARRED, 0);
conversationMap.put(ConversationColumns.CONVERSATION_INFO,
generateConversationInfo(messageCount, draftCount));
- Folder[] folders = new Folder[3];
- StringBuilder foldersString = new StringBuilder();
- for (int i = 0; i < folders.length; i++) {
- folders[i] = Folder.newUnsafeInstance();
- folders[i].name = "folder" + i;
+ final List<Folder> folders = new ArrayList<Folder>(3);
+ for (int i = 0; i < 3; i++) {
+ final Folder folder = Folder.newUnsafeInstance();
+ folder.name = "folder" + i;
switch (i) {
case 0:
- folders[i].bgColor = "#fff000";
+ folder.bgColor = "#fff000";
break;
case 1:
- folders[i].bgColor = "#0000FF";
+ folder.bgColor = "#0000FF";
break;
case 2:
- folders[i].bgColor = "#FFFF00";
+ folder.bgColor = "#FFFF00";
break;
default:
- folders[i].bgColor = null;
+ folder.bgColor = null;
break;
}
+ folders.add(folder);
+
}
- for (int i = 0; i < folders.length; i++) {
- foldersString.append(Folder.toString(folders[i]));
- if (i < folders.length - 1) {
- foldersString.append(",");
- }
- }
- conversationMap.put(ConversationColumns.RAW_FOLDERS, foldersString.toString());
+ final FolderList folderList = FolderList.copyOf(folders);
+ conversationMap.put(ConversationColumns.RAW_FOLDERS, folderList);
return conversationMap;
}
@@ -356,8 +354,6 @@
accountMap.put(AccountColumns.ACCOUNT_FROM_ADDRESSES, replyFroms.toString());
accountMap.put(AccountColumns.FOLDER_LIST_URI, Uri.parse(accountUri + "/folders"));
accountMap.put(AccountColumns.SEARCH_URI, Uri.parse(accountUri + "/search"));
- accountMap.put(AccountColumns.SAVE_DRAFT_URI, Uri.parse(accountUri + "/saveDraft"));
- accountMap.put(AccountColumns.SEND_MAIL_URI, Uri.parse(accountUri + "/sendMail"));
accountMap.put(AccountColumns.EXPUNGE_MESSAGE_URI,
Uri.parse(accountUri + "/expungeMessage"));
accountMap.put(AccountColumns.UNDO_URI, Uri.parse(accountUri + "/undo"));
@@ -391,6 +387,7 @@
@Override
public boolean onCreate() {
+ MockUiProvider.initializeMockProvider();
return true;
}
@@ -467,8 +464,6 @@
dest.writeParcelable((Uri) accountInfo.get(AccountColumns.FULL_FOLDER_LIST_URI), 0);
dest.writeParcelable((Uri) accountInfo.get(AccountColumns.SEARCH_URI), 0);
dest.writeString((String) accountInfo.get(AccountColumns.ACCOUNT_FROM_ADDRESSES));
- dest.writeParcelable((Uri) accountInfo.get(AccountColumns.SAVE_DRAFT_URI), 0);
- dest.writeParcelable((Uri) accountInfo.get(AccountColumns.SEND_MAIL_URI), 0);
dest.writeParcelable((Uri) accountInfo.get(AccountColumns.EXPUNGE_MESSAGE_URI), 0);
dest.writeParcelable((Uri) accountInfo.get(AccountColumns.UNDO_URI), 0);
dest.writeParcelable((Uri) accountInfo.get(AccountColumns.SETTINGS_INTENT_URI), 0);
diff --git a/src/com/android/mail/ui/AbstractActivityController.java b/src/com/android/mail/ui/AbstractActivityController.java
index f035089..0f07ef0 100644
--- a/src/com/android/mail/ui/AbstractActivityController.java
+++ b/src/com/android/mail/ui/AbstractActivityController.java
@@ -31,22 +31,18 @@
import android.content.ContentResolver;
import android.content.ContentValues;
import android.content.Context;
-import android.content.CursorLoader;
import android.content.DialogInterface;
import android.content.DialogInterface.OnClickListener;
import android.content.Intent;
import android.content.Loader;
import android.content.res.Resources;
-import android.database.Cursor;
import android.database.DataSetObservable;
import android.database.DataSetObserver;
import android.net.Uri;
import android.os.AsyncTask;
-import android.os.BadParcelableException;
import android.os.Bundle;
import android.os.Handler;
import android.provider.SearchRecentSuggestions;
-import android.text.TextUtils;
import android.view.DragEvent;
import android.view.KeyEvent;
import android.view.LayoutInflater;
@@ -58,6 +54,7 @@
import android.widget.Toast;
import com.android.mail.ConversationListContext;
+import com.android.mail.MailLogService;
import com.android.mail.R;
import com.android.mail.browse.ConfirmDialogFragment;
import com.android.mail.browse.ConversationCursor;
@@ -68,6 +65,9 @@
import com.android.mail.browse.SelectedConversationsActionMenu;
import com.android.mail.browse.SyncErrorDialogFragment;
import com.android.mail.compose.ComposeActivity;
+import com.android.mail.content.CursorCreator;
+import com.android.mail.content.ObjectCursor;
+import com.android.mail.content.ObjectCursorLoader;
import com.android.mail.providers.Account;
import com.android.mail.providers.Conversation;
import com.android.mail.providers.ConversationInfo;
@@ -89,6 +89,7 @@
import com.android.mail.utils.LogTag;
import com.android.mail.utils.LogUtils;
import com.android.mail.utils.NotificationActionUtils;
+import com.android.mail.utils.Observable;
import com.android.mail.utils.Utils;
import com.android.mail.utils.VeiledAddressMatcher;
@@ -173,6 +174,7 @@
/** A {@link android.content.BroadcastReceiver} that suppresses new e-mail notifications. */
private SuppressNotificationReceiver mNewEmailReceiver = null;
+ /** Handler for all our local runnables. */
protected Handler mHandler = new Handler();
/**
@@ -202,22 +204,12 @@
private final Set<Uri> mCurrentAccountUris = Sets.newHashSet();
protected ConversationCursor mConversationListCursor;
- private final DataSetObservable mConversationListObservable = new DataSetObservable() {
- @Override
- public void registerObserver(DataSetObserver observer) {
- final int count = mObservers.size();
- super.registerObserver(observer);
- LogUtils.d(LOG_TAG, "IN AAC.register(List)Observer: %s before=%d after=%d", observer,
- count, mObservers.size());
- }
- @Override
- public void unregisterObserver(DataSetObserver observer) {
- final int count = mObservers.size();
- super.unregisterObserver(observer);
- LogUtils.d(LOG_TAG, "IN AAC.unregister(List)Observer: %s before=%d after=%d", observer,
- count, mObservers.size());
- }
- };
+ private final DataSetObservable mConversationListObservable = new Observable("List");
+
+ /** Runnable that checks the logging level to enable/disable the logging service. */
+ private Runnable mLogServiceChecker = null;
+ /** List of all accounts currently known to the controller. */
+ private Account[] mAllAccounts;
/**
* Interface for actions that are deferred until after a load completes. This is for handling
@@ -235,40 +227,13 @@
private RefreshTimerTask mConversationListRefreshTask;
/** Listeners that are interested in changes to the current account. */
- private final DataSetObservable mAccountObservers = new DataSetObservable() {
- @Override
- public void registerObserver(DataSetObserver observer) {
- final int count = mObservers.size();
- super.registerObserver(observer);
- LogUtils.d(LOG_TAG, "IN AAC.register(Account)Observer: %s before=%d after=%d",
- observer, count, mObservers.size());
- }
- @Override
- public void unregisterObserver(DataSetObserver observer) {
- final int count = mObservers.size();
- super.unregisterObserver(observer);
- LogUtils.d(LOG_TAG, "IN AAC.unregister(Account)Observer: %s before=%d after=%d",
- observer, count, mObservers.size());
- }
- };
-
+ private final DataSetObservable mAccountObservers = new Observable("Account");
/** Listeners that are interested in changes to the recent folders. */
- private final DataSetObservable mRecentFolderObservers = new DataSetObservable() {
- @Override
- public void registerObserver(DataSetObserver observer) {
- final int count = mObservers.size();
- super.registerObserver(observer);
- LogUtils.d(LOG_TAG, "IN AAC.register(RecentFolder)Observer: %s before=%d after=%d",
- observer, count, mObservers.size());
- }
- @Override
- public void unregisterObserver(DataSetObserver observer) {
- final int count = mObservers.size();
- super.unregisterObserver(observer);
- LogUtils.d(LOG_TAG, "IN AAC.unregister(RecentFolder)Observer: %s before=%d after=%d",
- observer, count, mObservers.size());
- }
- };
+ private final DataSetObservable mRecentFolderObservers = new Observable("RecentFolder");
+ /** Listeners that are interested in changes to the list of all accounts. */
+ private final DataSetObservable mAllAccountObservers = new Observable("AllAccounts");
+ /** Listeners that are interested in changes to the current folder. */
+ private final DataSetObservable mFolderObservable = new Observable("CurrentFolder");
/**
* Selected conversations, if any.
@@ -292,7 +257,10 @@
private final ConversationListLoaderCallbacks mListCursorCallbacks =
new ConversationListLoaderCallbacks();
- private final DataSetObservable mFolderObservable = new DataSetObservable();
+ /** Object that listens to all LoaderCallbacks that result in {@link Folder} creation. */
+ private final FolderLoads mFolderCallbacks = new FolderLoads();
+ /** Object that listens to all LoaderCallbacks that result in {@link Account} creation. */
+ private final AccountLoads mAccountCallbacks = new AccountLoads();
/**
* Matched addresses that must be shielded from users because they are temporary. Even though
@@ -310,6 +278,9 @@
private static final int LOADER_ACCOUNT_INBOX = 5;
private static final int LOADER_SEARCH = 6;
private static final int LOADER_ACCOUNT_UPDATE_CURSOR = 7;
+ /** Loader for showing the initial folder/conversation at app start. */
+ public static final int LOADER_FIRST_FOLDER = 8;
+
/**
* 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
@@ -320,6 +291,13 @@
* perhaps.
*/
public static final int LAST_LOADER_ID = 100;
+ /**
+ * Guaranteed to be the last loader ID used by the Fragment. Loaders are owned by Activity or
+ * fragments, and within an activity, loader IDs need to be unique. Currently,
+ * {@link SectionedInboxTeaserView} is the only class that uses the
+ * {@link ConversationListFragment}'s LoaderManager.
+ */
+ public static final int LAST_FRAGMENT_LOADER_ID = 1000;
private static final int ADD_ACCOUNT_REQUEST_CODE = 1;
private static final int REAUTHENTICATE_REQUEST_CODE = 2;
@@ -327,10 +305,6 @@
/** The pending destructive action to be carried out before swapping the conversation cursor.*/
private DestructiveAction mPendingDestruction;
protected AsyncRefreshTask mFolderSyncTask;
- // Task for setting any share intents for the account to enabled.
- // This gets cancelled if the user kills the app before it finishes, and
- // will just run the next time the user opens the app.
- private AsyncTask<String, Void, Void> mEnableShareIntents;
private Folder mFolderListFolder;
private boolean mIsDragHappening;
private int mShowUndoBarDelay;
@@ -352,6 +326,9 @@
*/
private boolean mDialogFromSelectedSet;
+ /** Which conversation to show, if started from widget/notification. */
+ private Conversation mConversationToShow = null;
+
private final Deque<UpOrBackHandler> mUpOrBackHandlers = Lists.newLinkedList();
public static final String SYNC_ERROR_DIALOG_FRAGMENT_TAG = "SyncErrorDialogFragment";
@@ -416,14 +393,11 @@
/**
* Check if the fragment is attached to an activity and has a root view.
- * @param in
+ * @param in fragment to be checked
* @return true if the fragment is valid, false otherwise
*/
- private static final boolean isValidFragment(Fragment in) {
- if (in == null || in.getActivity() == null || in.getView() == null) {
- return false;
- }
- return true;
+ private static boolean isValidFragment(Fragment in) {
+ return !(in == null || in.getActivity() == null || in.getView() == null);
}
/**
@@ -480,7 +454,7 @@
&& Intent.ACTION_SEARCH.equals(mActivity.getIntent().getAction());
mActionBarView = (MailActionBarView) inflater.inflate(
isSearch ? R.layout.search_actionbar_view : R.layout.actionbar_view, null);
- mActionBarView.initialize(mActivity, this, mViewMode, actionBar, mRecentFolderList);
+ mActionBarView.initialize(mActivity, this, actionBar);
}
/**
@@ -491,11 +465,10 @@
if (actionBar != null && mActionBarView != null) {
actionBar.setCustomView(mActionBarView, new ActionBar.LayoutParams(
LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT));
- // Show a custom view and home icon, but remove the title
+ // Show a custom view and home icon, keep the title and subttitle
final int mask = ActionBar.DISPLAY_SHOW_CUSTOM | ActionBar.DISPLAY_SHOW_TITLE
| ActionBar.DISPLAY_SHOW_HOME;
- final int enabled = ActionBar.DISPLAY_SHOW_CUSTOM | ActionBar.DISPLAY_SHOW_HOME;
- actionBar.setDisplayOptions(enabled, mask);
+ actionBar.setDisplayOptions(mask, mask);
mActionBarView.attach();
}
mViewMode.addListener(mActionBarView);
@@ -537,7 +510,7 @@
}
@Override
- public void onAccountChanged(Account account) {
+ public void changeAccount(Account account) {
// Is the account or account settings different from the existing account?
final boolean firstLoad = mAccount == null;
final boolean accountChanged = firstLoad || !account.uri.equals(mAccount.uri);
@@ -547,7 +520,7 @@
}
// We also don't want to do anything if the new account is null
if (account == null) {
- LogUtils.e(LOG_TAG, "AAC.onAccountChanged(null) called.");
+ LogUtils.e(LOG_TAG, "AAC.changeAccount(null) called.");
return;
}
final String accountName = account.name;
@@ -596,6 +569,21 @@
}
@Override
+ public void registerAllAccountObserver(DataSetObserver observer) {
+ mAllAccountObservers.registerObserver(observer);
+ }
+
+ @Override
+ public void unregisterAllAccountObserver(DataSetObserver observer) {
+ mAllAccountObservers.unregisterObserver(observer);
+ }
+
+ @Override
+ public Account[] getAllAccounts() {
+ return mAllAccounts;
+ }
+
+ @Override
public Account getAccount() {
return mAccount;
}
@@ -604,7 +592,7 @@
final Bundle args = new Bundle();
args.putString(ConversationListContext.EXTRA_SEARCH_QUERY, intent
.getStringExtra(ConversationListContext.EXTRA_SEARCH_QUERY));
- mActivity.getLoaderManager().restartLoader(LOADER_SEARCH, args, this);
+ mActivity.getLoaderManager().restartLoader(LOADER_SEARCH, args, mFolderCallbacks);
}
@Override
@@ -615,7 +603,8 @@
/**
* Sets the folder state without changing view mode and without creating a list fragment, if
* possible.
- * @param folder
+ * @param folder the folder whose list of conversations are to be shown
+ * @param query the query string for a list of conversations matching a search
*/
private void setListContext(Folder folder, String query) {
updateFolder(folder);
@@ -632,7 +621,7 @@
* @param folder the folder to change to
* @param query if non-null, this represents the search string that the folder represents.
*/
- private final void changeFolder(Folder folder, String query) {
+ private void changeFolder(Folder folder, String query) {
if (!Objects.equal(mFolder, folder)) {
commitDestructiveActions(false);
}
@@ -686,16 +675,16 @@
// field in the account.
@Override
public void loadAccountInbox() {
- restartOptionalLoader(LOADER_ACCOUNT_INBOX);
+ restartOptionalLoader(LOADER_ACCOUNT_INBOX, mFolderCallbacks, Bundle.EMPTY);
}
/**
* Marks the {@link #mFolderChanged} value if the newFolder is different from the existing
* {@link #mFolder}. This should be called immediately <b>before</b> assigning newFolder to
* mFolder.
- * @param newFolder
+ * @param newFolder the new folder we are switching to.
*/
- private final void setHasFolderChanged(final Folder newFolder) {
+ private void setHasFolderChanged(final Folder newFolder) {
// We should never try to assign a null folder. But in the rare event that we do, we should
// only set the bit when we have a valid folder, and null is not valid.
if (newFolder == null) {
@@ -744,21 +733,21 @@
// previous loader's instance and data upon configuration change (e.g. rotation).
// If there was not already an instance of the loader, init it.
if (lm.getLoader(LOADER_FOLDER_CURSOR) == null) {
- lm.initLoader(LOADER_FOLDER_CURSOR, null, this);
+ lm.initLoader(LOADER_FOLDER_CURSOR, Bundle.EMPTY, mFolderCallbacks);
} else {
- lm.restartLoader(LOADER_FOLDER_CURSOR, null, this);
+ lm.restartLoader(LOADER_FOLDER_CURSOR, Bundle.EMPTY, mFolderCallbacks);
}
// In this case, we are starting from no folder, which would occur
// the first time the app was launched or on orientation changes.
// We want to attach to an existing loader, if available.
if (wasNull || lm.getLoader(LOADER_CONVERSATION_LIST) == null) {
- lm.initLoader(LOADER_CONVERSATION_LIST, null, mListCursorCallbacks);
+ lm.initLoader(LOADER_CONVERSATION_LIST, Bundle.EMPTY, mListCursorCallbacks);
} else {
// However, if there was an existing folder AND we have changed
// folders, we want to restart the loader to get the information
// for the newly selected folder
lm.destroyLoader(LOADER_CONVERSATION_LIST);
- lm.initLoader(LOADER_CONVERSATION_LIST, null, mListCursorCallbacks);
+ lm.initLoader(LOADER_CONVERSATION_LIST, Bundle.EMPTY, mListCursorCallbacks);
}
}
@@ -784,8 +773,8 @@
// We were waiting for the user to create an account
if (resultCode == Activity.RESULT_OK) {
// restart the loader to get the updated list of accounts
- mActivity.getLoaderManager().initLoader(
- LOADER_ACCOUNT_CURSOR, null, this);
+ mActivity.getLoaderManager().initLoader(LOADER_ACCOUNT_CURSOR, Bundle.EMPTY,
+ mAccountCallbacks);
} else {
// The user failed to create an account, just exit the app
mActivity.finish();
@@ -805,7 +794,7 @@
/**
* Inform the conversation cursor that there has been a visibility change.
- * @param visible
+ * @param visible true if the conversation list is visible, false otherwise.
*/
protected synchronized void informCursorVisiblity(boolean visible) {
if (mConversationListCursor != null) {
@@ -829,9 +818,57 @@
public void onConversationVisibilityChanged(boolean visible) {
}
+ /**
+ * Initialize development time logging. This can potentially log a lot of PII, and we don't want
+ * to turn it on for shipped versions.
+ */
+ private void initializeDevLoggingService() {
+ if (!MailLogService.DEBUG_ENABLED) {
+ return;
+ }
+ // Check every 5 minutes.
+ final int WAIT_TIME = 5 * 60 * 1000;
+ // Start a runnable that periodically checks the log level and starts/stops the service.
+ mLogServiceChecker = new Runnable() {
+ /** True if currently logging. */
+ private boolean mCurrentlyLogging = false;
+
+ /**
+ * If the logging level has been changed since the previous run, start or stop the
+ * service.
+ */
+ private void startOrStopService() {
+ // If the log level is already high, start the service.
+ final Intent i = new Intent(mContext, MailLogService.class);
+ final boolean loggingEnabled = MailLogService.isLoggingLevelHighEnough();
+ if (mCurrentlyLogging == loggingEnabled) {
+ // No change since previous run, just return;
+ return;
+ }
+ if (loggingEnabled) {
+ LogUtils.e(LOG_TAG, "Starting MailLogService");
+ mContext.startService(i);
+ } else {
+ LogUtils.e(LOG_TAG, "Stopping MailLogService");
+ mContext.stopService(i);
+ }
+ mCurrentlyLogging = loggingEnabled;
+ }
+
+ @Override
+ public void run() {
+ startOrStopService();
+ mHandler.postDelayed(this, WAIT_TIME);
+ }
+ };
+ // Start the runnable right away.
+ mHandler.post(mLogServiceChecker);
+ }
+
@Override
public boolean onCreate(Bundle savedState) {
initializeActionBar();
+ initializeDevLoggingService();
// Allow shortcut keys to function for the ActionBar and menus.
mActivity.setDefaultKeyMode(Activity.DEFAULT_KEYS_SHORTCUT);
mResolver = mActivity.getContentResolver();
@@ -871,7 +908,8 @@
handleIntent(intent);
}
// Create the accounts loader; this loads the account switch spinner.
- mActivity.getLoaderManager().initLoader(LOADER_ACCOUNT_CURSOR, null, this);
+ mActivity.getLoaderManager().initLoader(LOADER_ACCOUNT_CURSOR, Bundle.EMPTY,
+ mAccountCallbacks);
return true;
}
@@ -884,7 +922,7 @@
@Override
public void onRestart() {
- DialogFragment fragment = (DialogFragment)
+ final DialogFragment fragment = (DialogFragment)
mFragmentManager.findFragmentByTag(SYNC_ERROR_DIALOG_FRAGMENT_TAG);
if (fragment != null) {
fragment.dismiss();
@@ -987,7 +1025,7 @@
ComposeActivity.compose(mActivity.getActivityContext(), mAccount);
break;
case R.id.show_all_folders:
- showFolderList();
+ toggleFolderListState();
break;
case R.id.refresh:
requestFolderRefresh();
@@ -1007,10 +1045,13 @@
case R.id.manage_folders_item:
Utils.showManageFolder(mActivity.getActivityContext(), mAccount);
break;
+ case R.id.move_to:
+ /* fall through */
case R.id.change_folder:
final FolderSelectionDialog dialog = FolderSelectionDialog.getInstance(
mActivity.getActivityContext(), mAccount, this,
- Conversation.listOf(mCurrentConversation), false, mFolder);
+ Conversation.listOf(mCurrentConversation), false, mFolder,
+ id == R.id.move_to);
if (dialog != null) {
dialog.show();
}
@@ -1022,6 +1063,14 @@
return handled;
}
+ /**
+ * Stand-in method overriden in OnePaneController for toggling the state
+ * of the drawer.
+ */
+ protected void toggleFolderListState() {
+ //Implemented in OnePaneController.java
+ }
+
@Override
public final boolean onUpPressed() {
for (UpOrBackHandler h : mUpOrBackHandlers) {
@@ -1212,6 +1261,11 @@
final ContentValues value = new ContentValues();
value.put(ConversationColumns.READ, read);
+ // We never want to mark unseen here, but we do want to mark it seen
+ if (read || markViewed) {
+ value.put(ConversationColumns.SEEN, Boolean.TRUE);
+ }
+
// The mark read/unread/viewed operations do not show an undo bar
value.put(ConversationOperations.Parameters.SUPPRESS_UNDO, true);
if (markViewed) {
@@ -1438,11 +1492,9 @@
/**
* Requests that the action be performed and the UI state is updated to reflect the new change.
- * @param target
- * @param action
+ * @param action the action to be performed, specified as a menu id: R.id.archive, ...
*/
- private void requestUpdate(final Collection<Conversation> target,
- final DestructiveAction action) {
+ private void requestUpdate(final DestructiveAction action) {
action.performAction();
refreshConversationList();
}
@@ -1509,8 +1561,7 @@
outState.putParcelable(SAVED_DETACHED_CONV_URI, mDetachedConvUri);
}
mSafeToModifyFragments = false;
- outState.putString(SAVED_HIERARCHICAL_FOLDER,
- (mFolderListFolder != null) ? Folder.toString(mFolderListFolder) : null);
+ outState.putParcelable(SAVED_HIERARCHICAL_FOLDER, mFolderListFolder);
}
/**
@@ -1533,10 +1584,6 @@
@Override
public void onStop() {
- if (mEnableShareIntents != null) {
- mEnableShareIntents.cancel(true);
- }
-
NotificationActionUtils.unregisterUndoNotificationObserver(mUndoNotificationObserver);
}
@@ -1551,6 +1598,8 @@
mActionBarView.onDestroy();
mRecentFolderList.destroy();
mDestroyed = true;
+ mHandler.removeCallbacks(mLogServiceChecker);
+ mLogServiceChecker = null;
}
/**
@@ -1606,7 +1655,7 @@
/**
* Set the account, and carry out all the account-related changes that rely on this.
- * @param account
+ * @param account new account to set to.
*/
private void setAccount(Account account) {
if (account == null) {
@@ -1618,10 +1667,10 @@
mAccount = account;
// Only change AAC state here. Do *not* modify any other object's state. The object
// should listen on account changes.
- restartOptionalLoader(LOADER_RECENT_FOLDERS);
+ restartOptionalLoader(LOADER_RECENT_FOLDERS, mFolderCallbacks, Bundle.EMPTY);
mActivity.invalidateOptionsMenu();
disableNotificationsOnAccountChange(mAccount);
- restartOptionalLoader(LOADER_ACCOUNT_UPDATE_CURSOR);
+ restartOptionalLoader(LOADER_ACCOUNT_UPDATE_CURSOR, mAccountCallbacks, Bundle.EMPTY);
// The Mail instance can be null during test runs.
final MailAppProvider instance = MailAppProvider.getInstance();
if (instance != null) {
@@ -1640,7 +1689,7 @@
* method from the parent class, since it performs important UI
* initialization.
*
- * @param savedState
+ * @param savedState previous state
*/
@Override
public void onRestoreInstanceState(Bundle savedState) {
@@ -1666,10 +1715,7 @@
}
}
}
- final String folderString = savedState.getString(SAVED_HIERARCHICAL_FOLDER, null);
- if (!TextUtils.isEmpty(folderString)) {
- mFolderListFolder = Folder.fromString(folderString);
- }
+ mFolderListFolder = savedState.getParcelable(SAVED_HIERARCHICAL_FOLDER);
final ConversationListFragment convListFragment = getConversationListFragment();
if (convListFragment != null) {
convListFragment.getAnimatedAdapter().onRestoreInstanceState(savedState);
@@ -1694,10 +1740,9 @@
* 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.
- * @param intent
+ * @param intent intent passed to the activity.
*/
private void handleIntent(Intent intent) {
- boolean handled = false;
if (Intent.ACTION_VIEW.equals(intent.getAction())) {
if (intent.hasExtra(Utils.EXTRA_ACCOUNT)) {
setAccount(Account.newinstance(intent.getStringExtra(Utils.EXTRA_ACCOUNT)));
@@ -1705,48 +1750,21 @@
if (mAccount == null) {
return;
}
- boolean isConversationMode;
- try {
- isConversationMode = intent.hasExtra(Utils.EXTRA_CONVERSATION);
- } catch (BadParcelableException e) {
- // If a previous version of Gmail had a PendingIntent for a conversation when the
- // app upgrades, the system may not actually update the PendingIntent to the new
- // format. If we see one of these old intents, just treat it as a conversation list
- // intent.
- LogUtils.e(LOG_TAG, e, "Error parsing conversation");
- isConversationMode = false;
- }
+ final boolean isConversationMode = intent.hasExtra(Utils.EXTRA_CONVERSATION);
if (isConversationMode && mViewMode.getMode() == ViewMode.UNKNOWN) {
mViewMode.enterConversationMode();
} else {
mViewMode.enterConversationListMode();
}
- final Folder folder = intent.hasExtra(Utils.EXTRA_FOLDER) ?
- Folder.fromString(intent.getStringExtra(Utils.EXTRA_FOLDER)) : null;
- if (folder != null) {
- onFolderChanged(folder);
- handled = true;
- }
-
- if (isConversationMode) {
- // Open the conversation.
- LogUtils.d(LOG_TAG, "SHOW THE CONVERSATION at %s",
- intent.getParcelableExtra(Utils.EXTRA_CONVERSATION));
- final Conversation conversation =
- intent.getParcelableExtra(Utils.EXTRA_CONVERSATION);
- if (conversation != null && conversation.position < 0) {
- // Set the position to 0 on this conversation, as we don't know where it is
- // in the list
- conversation.position = 0;
- }
- showConversation(conversation);
- handled = true;
- }
-
- if (!handled) {
- // We have an account, but nothing else: load the default inbox.
- loadAccountInbox();
- }
+ // Put the folder and conversation, and ask the loader to create this folder.
+ final Bundle args = new Bundle();
+ final Uri folderUri = intent.hasExtra(Utils.EXTRA_FOLDER_URI)
+ ? (Uri) intent.getParcelableExtra(Utils.EXTRA_FOLDER_URI)
+ : mAccount.settings.defaultInbox;
+ args.putParcelable(Utils.EXTRA_FOLDER_URI, folderUri);
+ args.putParcelable(Utils.EXTRA_CONVERSATION,
+ intent.getParcelableExtra(Utils.EXTRA_CONVERSATION));
+ restartOptionalLoader(LOADER_FIRST_FOLDER, mFolderCallbacks, args);
} else if (Intent.ACTION_SEARCH.equals(intent.getAction())) {
if (intent.hasExtra(Utils.EXTRA_ACCOUNT)) {
mHaveSearchResults = false;
@@ -1769,7 +1787,7 @@
}
}
if (mAccount != null) {
- restartOptionalLoader(LOADER_ACCOUNT_UPDATE_CURSOR);
+ restartOptionalLoader(LOADER_ACCOUNT_UPDATE_CURSOR, mAccountCallbacks, Bundle.EMPTY);
}
}
@@ -1785,7 +1803,7 @@
* triggering {@link ConversationSetObserver} callbacks as our selection set changes.
*
*/
- private final void restoreSelectedConversations(Bundle savedState) {
+ private void restoreSelectedConversations(Bundle savedState) {
if (savedState == null) {
mSelectedSet.clear();
return;
@@ -1805,7 +1823,7 @@
return mActionBarView;
}
- private final void showConversation(Conversation conversation) {
+ private void showConversation(Conversation conversation) {
showConversation(conversation, false /* inLoaderCallbacks */);
}
@@ -1813,11 +1831,17 @@
* Show the conversation provided in the arguments. It is safe to pass a null conversation
* object, which is a signal to back out of conversation view mode.
* Child classes must call super.showConversation() <b>before</b> their own implementations.
- * @param conversation
+ * @param conversation the conversation to be shown, or null if we want to back out to list
+ * mode.
* @param inLoaderCallbacks true if the method is called as a result of
- * {@link #onLoadFinished(Loader, Cursor)}
+ * onLoadFinished(Loader, Cursor) on any callback.
*/
protected void showConversation(Conversation conversation, boolean inLoaderCallbacks) {
+ if (conversation != null) {
+ Utils.sConvLoadTimer.start();
+ }
+
+ MailLogService.log("AbstractActivityController", "showConversation(%s)", conversation);
// Set the current conversation just in case it wasn't already set.
setCurrentConversation(conversation);
// Add the folder that we were viewing to the recent folders list.
@@ -1858,7 +1882,7 @@
* Use the instance variable and the wait fragment's tag to get the wait fragment. This is
* far superior to using the value of mWaitFragment, which might be invalid or might refer
* to a fragment after it has been destroyed.
- * @return
+ * @return a wait fragment that is already attached to the activity, if one exists
*/
protected final WaitFragment getWaitFragment() {
final FragmentManager manager = mActivity.getFragmentManager();
@@ -1909,7 +1933,8 @@
* Set the current conversation. This is the conversation on which all actions are performed.
* Do not modify mCurrentConversation except through this method, which makes it easy to
* perform common actions associated with changing the current conversation.
- * @param conversation
+ * @param conversation new conversation to view. Passing null indicates that we are backing
+ * out to conversation list mode.
*/
@Override
public void setCurrentConversation(Conversation conversation) {
@@ -1933,54 +1958,6 @@
}
/**
- * {@inheritDoc}
- */
- @Override
- public Loader<Cursor> onCreateLoader(int id, Bundle args) {
- switch (id) {
- case LOADER_ACCOUNT_CURSOR:
- return new CursorLoader(mContext, MailAppProvider.getAccountsUri(),
- UIProvider.ACCOUNTS_PROJECTION, null, null, null);
- case LOADER_FOLDER_CURSOR:
- final CursorLoader loader = new CursorLoader(mContext, mFolder.uri,
- UIProvider.FOLDERS_PROJECTION, null, null, null);
- loader.setUpdateThrottle(mFolderItemUpdateDelayMs);
- return loader;
- case LOADER_RECENT_FOLDERS:
- if (mAccount != null && mAccount.recentFolderListUri != null) {
- return new CursorLoader(mContext, mAccount.recentFolderListUri,
- UIProvider.FOLDERS_PROJECTION, null, null, null);
- }
- break;
- case LOADER_ACCOUNT_INBOX:
- final Uri defaultInbox = Settings.getDefaultInboxUri(mAccount.settings);
- final Uri inboxUri = defaultInbox.equals(Uri.EMPTY) ?
- mAccount.folderListUri : defaultInbox;
- LogUtils.d(LOG_TAG, "Loading the default inbox: %s", inboxUri);
- if (inboxUri != null) {
- return new CursorLoader(mContext, inboxUri, UIProvider.FOLDERS_PROJECTION, null,
- null, null);
- }
- break;
- case LOADER_SEARCH:
- return Folder.forSearchResults(mAccount,
- args.getString(ConversationListContext.EXTRA_SEARCH_QUERY),
- mActivity.getActivityContext());
- case LOADER_ACCOUNT_UPDATE_CURSOR:
- return new CursorLoader(mContext, mAccount.uri, UIProvider.ACCOUNTS_PROJECTION,
- null, null, null);
- default:
- LogUtils.wtf(LOG_TAG, "Loader returned unexpected id: %d", id);
- }
- return null;
- }
-
- @Override
- public void onLoaderReset(Loader<Cursor> loader) {
-
- }
-
- /**
* {@link LoaderManager} currently has a bug in
* {@link LoaderManager#restartLoader(int, Bundle, android.app.LoaderManager.LoaderCallbacks)}
* where, if a previous onCreateLoader returned a null loader, this method will NPE. Work around
@@ -1991,11 +1968,14 @@
* give the controller a chance to invalidate UI corresponding the prior loader result.
*
* @param id loader ID to safely restart
+ * @param handler the LoaderCallback which will handle this loader ID.
+ * @param args arguments, if any, to be passed to the loader. Use {@link Bundle#EMPTY} if no
+ * arguments need to be specified.
*/
- private void restartOptionalLoader(int id) {
+ private void restartOptionalLoader(int id, LoaderManager.LoaderCallbacks handler, Bundle args) {
final LoaderManager lm = mActivity.getLoaderManager();
lm.destroyLoader(id);
- lm.restartLoader(id, Bundle.EMPTY, this);
+ lm.restartLoader(id, args, handler);
}
@Override
@@ -2049,10 +2029,10 @@
/**
* Returns true if the number of accounts is different, or if the current account has been
* removed from the device
- * @param accountCursor
- * @return
+ * @param accountCursor the cursor which points to all the accounts.
+ * @return true if the number of accounts is changed or current account missing from the list.
*/
- private boolean accountsUpdated(Cursor accountCursor) {
+ private boolean accountsUpdated(ObjectCursor<Account> accountCursor) {
// Check to see if the current account hasn't been set, or the account cursor is empty
if (mAccount == null || !accountCursor.moveToFirst()) {
return true;
@@ -2068,8 +2048,8 @@
// the cursor.
boolean foundCurrentAccount = false;
do {
- final Uri accountUri =
- Uri.parse(accountCursor.getString(UIProvider.ACCOUNT_URI_COLUMN));
+ final Uri accountUri = Uri.parse(accountCursor.getString(
+ accountCursor.getColumnIndex(UIProvider.AccountColumns.URI)));
if (!foundCurrentAccount && mAccount.uri.equals(accountUri)) {
foundCurrentAccount = true;
}
@@ -2090,7 +2070,7 @@
* @param accounts cursor into the AccountCache
* @return true if the update was successful, false otherwise
*/
- private boolean updateAccounts(Cursor accounts) {
+ private boolean updateAccounts(ObjectCursor<Account> accounts) {
if (accounts == null || !accounts.moveToFirst()) {
return false;
}
@@ -2143,11 +2123,13 @@
}
}
if (accountChanged) {
- onAccountChanged(newAccount);
+ changeAccount(newAccount);
}
+
// Whether we have updated the current account or not, we need to update the list of
// accounts in the ActionBar.
- mActionBarView.setAccounts(allAccounts);
+ mAllAccounts = allAccounts;
+ mAllAccountObservers.notifyChanged();
return (allAccounts.length > 0);
}
@@ -2171,148 +2153,6 @@
}
/**
- * {@inheritDoc}
- */
- @Override
- public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
- // We want to reinitialize only if we haven't ever been initialized, or
- // if the current account has vanished.
- if (data == null) {
- LogUtils.e(LOG_TAG, "Received null cursor from loader id: %d", loader.getId());
- }
- switch (loader.getId()) {
- case LOADER_ACCOUNT_CURSOR:
- if (data == null) {
- // Nothing useful to do if we have no valid data.
- break;
- }
- if (data.getCount() == 0) {
- // If an empty cursor is returned, the MailAppProvider is indicating that
- // no accounts have been specified. We want to navigate to the "add account"
- // activity that will handle the intent returned by the MailAppProvider
-
- // If the MailAppProvider believes that all accounts have been loaded, and the
- // account list is still empty, we want to prompt the user to add an account
- final Bundle extras = data.getExtras();
- final boolean accountsLoaded =
- extras.getInt(AccountCursorExtraKeys.ACCOUNTS_LOADED) != 0;
-
- if (accountsLoaded) {
- final Intent noAccountIntent = MailAppProvider.getNoAccountIntent(mContext);
- if (noAccountIntent != null) {
- mActivity.startActivityForResult(noAccountIntent,
- ADD_ACCOUNT_REQUEST_CODE);
- }
- }
- } else {
- final boolean accountListUpdated = accountsUpdated(data);
- if (!isLoaderInitialized || accountListUpdated) {
- isLoaderInitialized = updateAccounts(data);
- }
- }
- break;
- case LOADER_ACCOUNT_UPDATE_CURSOR:
- // We have gotten an update for current account.
-
- // Make sure that this is an update for the current account
- if (data != null && data.moveToFirst()) {
- final Account updatedAccount = new Account(data);
-
- 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
- mAccount = updatedAccount;
- LogUtils.d(LOG_TAG, "AbstractActivityController.onLoadFinished(): "
- + "mAccount = %s", mAccount.uri);
-
- // Only notify about a settings change if something differs
- if (!Objects.equal(mAccount.settings, previousSettings)) {
- mAccountObservers.notifyChanged();
- }
- perhapsEnterWaitMode();
- } else {
- LogUtils.e(LOG_TAG, "Got update for account: %s with current account: %s",
- updatedAccount.uri, mAccount.uri);
- // We need to restart the loader, so the correct account information will
- // be returned
- restartOptionalLoader(LOADER_ACCOUNT_UPDATE_CURSOR);
- }
- }
- break;
- case LOADER_FOLDER_CURSOR:
- // Check status of the cursor.
- if (data != null && data.moveToFirst()) {
- final Folder folder = new Folder(data);
- LogUtils.d(LOG_TAG, "FOLDER STATUS = %d", folder.syncStatus);
- setHasFolderChanged(folder);
- mFolder = folder;
- mFolderObservable.notifyChanged();
- } else {
- LogUtils.d(LOG_TAG, "Unable to get the folder %s",
- mFolder != null ? mAccount.name : "");
- }
- break;
- case LOADER_RECENT_FOLDERS:
- // Few recent folders and we are running on a phone? Populate the default recents.
- // The number of default recent folders is at least 2: every provider has at
- // least two folders, and the recent folder count never decreases. Having a single
- // recent folder is an erroneous case, and we can gracefully recover by populating
- // default recents. The default recents will not stomp on the existing value: it
- // will be shown in addition to the default folders: the max number of recent
- // folders is more than 1+num(defaultRecents).
- if (data != null && data.getCount() <= 1 && !mIsTablet) {
- final class PopulateDefault extends AsyncTask<Uri, Void, Void> {
- @Override
- protected Void doInBackground(Uri... uri) {
- // Asking for an update on the URI and ignore the result.
- final ContentResolver resolver = mContext.getContentResolver();
- resolver.update(uri[0], null, null, null);
- return null;
- }
- }
- final Uri uri = mAccount.defaultRecentFolderListUri;
- LogUtils.v(LOG_TAG, "Default recents at %s", uri);
- new PopulateDefault().execute(uri);
- break;
- }
- LogUtils.v(LOG_TAG, "Reading recent folders from the cursor.");
- loadRecentFolders(data);
- break;
- case LOADER_ACCOUNT_INBOX:
- if (data != null && !data.isClosed() && data.moveToFirst()) {
- Folder inbox = new Folder(data);
- onFolderChanged(inbox);
- // Just want to get the inbox, don't care about updates to it
- // as this will be tracked by the folder change listener.
- mActivity.getLoaderManager().destroyLoader(LOADER_ACCOUNT_INBOX);
- } else {
- LogUtils.d(LOG_TAG, "Unable to get the account inbox for account %s",
- mAccount != null ? mAccount.name : "");
- }
- break;
- case LOADER_SEARCH:
- if (data != null && data.getCount() > 0) {
- data.moveToFirst();
- final Folder search = new Folder(data);
- updateFolder(search);
- mConvListContext = ConversationListContext.forSearchQuery(mAccount, mFolder,
- mActivity.getIntent()
- .getStringExtra(UIProvider.SearchQueryParameters.QUERY));
- showConversationList(mConvListContext);
- mActivity.invalidateOptionsMenu();
- mHaveSearchResults = search.totalCount > 0;
- mActivity.getLoaderManager().destroyLoader(LOADER_SEARCH);
- } else {
- LogUtils.e(LOG_TAG, "Null or empty cursor returned by LOADER_SEARCH loader");
- }
- break;
- }
- }
-
-
- /**
* Destructive actions on Conversations. This class should only be created by controllers, and
* clients should only require {@link DestructiveAction}s, not specific implementations of the.
* Only the controllers should know what kind of destructive actions are being created.
@@ -2331,9 +2171,9 @@
private final boolean mIsSelectedSet;
/**
- * Create a listener object. action is one of four constants: R.id.y_button (archive),
+ * Create a listener object.
+ * @param action action is one of four constants: R.id.y_button (archive),
* R.id.delete , R.id.mute, and R.id.report_spam.
- * @param action
* @param target Conversation that we want to apply the action to.
* @param isBatch whether the conversations are in the currently selected batch set.
*/
@@ -2437,7 +2277,7 @@
@Override
public void run() {
onUndoAvailable(new ToastBarOperation(mTarget.size(), mAction,
- ToastBarOperation.UNDO, mIsSelectedSet));
+ ToastBarOperation.UNDO, mIsSelectedSet, mFolder));
}
}, mShowUndoBarDelay);
}
@@ -2464,7 +2304,8 @@
// conversations to.
@Override
public final void assignFolder(Collection<FolderOperation> folderOps,
- Collection<Conversation> target, boolean batch, boolean showUndo) {
+ Collection<Conversation> target, boolean batch, boolean showUndo,
+ final boolean isMoveTo) {
// Actions are destructive only when the current folder can be assigned
// to (which is the same as being able to un-assign a conversation from the folder) and
// when the list of folders contains the current folder.
@@ -2481,13 +2322,41 @@
// Update the UI elements depending no their visibility and availability
// TODO(viki): Consolidate this into a single method requestDelete.
if (isDestructive) {
+ /*
+ * If this is a MOVE operation, we want the action folder to be the destination folder.
+ * Otherwise, we want it to be the current folder.
+ *
+ * A set of folder operations is a move if there are exactly two operations: an add and
+ * a remove.
+ */
+ final Folder actionFolder;
+ if (folderOps.size() != 2) {
+ actionFolder = mFolder;
+ } else {
+ Folder addedFolder = null;
+ boolean hasRemove = false;
+ for (final FolderOperation folderOperation : folderOps) {
+ if (folderOperation.mAdd) {
+ addedFolder = folderOperation.mFolder;
+ } else {
+ hasRemove = true;
+ }
+ }
+
+ if (hasRemove && addedFolder != null) {
+ actionFolder = addedFolder;
+ } else {
+ actionFolder = mFolder;
+ }
+ }
+
folderChange = getDeferredFolderChange(target, folderOps, isDestructive,
- batch, showUndo);
+ batch, showUndo, isMoveTo, actionFolder);
delete(0, target, folderChange);
} else {
folderChange = getFolderChange(target, folderOps, isDestructive,
- batch, showUndo);
- requestUpdate(target, folderChange);
+ batch, showUndo, false /* isMoveTo */, mFolder);
+ requestUpdate(folderChange);
}
}
@@ -2572,7 +2441,7 @@
/**
* If the Conversation List Fragment is visible, updates the fragment.
*/
- private final void updateConversationListFragment() {
+ private void updateConversationListFragment() {
final ConversationListFragment convList = getConversationListFragment();
if (convList != null) {
refreshConversationList();
@@ -2615,15 +2484,6 @@
}
}
- private void loadRecentFolders(Cursor data) {
- mRecentFolderList.loadFromUiProvider(data);
- if (isAnimating()) {
- mRecentsDataUpdated = true;
- } else {
- mRecentFolderObservers.notifyChanged();
- }
- }
-
@Override
public void onAnimationEnd(AnimatedAdapter animatedAdapter) {
if (mConversationListCursor == null) {
@@ -2771,8 +2631,9 @@
}
// Drag and drop is destructive: we remove conversations from the
// current folder.
- final DestructiveAction action = getFolderChange(conversations, dragDropOperations,
- isDestructive, true, true);
+ final DestructiveAction action =
+ getFolderChange(conversations, dragDropOperations, isDestructive,
+ true /* isBatch */, true /* showUndo */, true /* isMoveTo */, folder);
if (isDestructive) {
delete(0, conversations, action);
} else {
@@ -2807,7 +2668,6 @@
}
refreshConversationList();
mSelectedSet.clear();
- return;
}
}
@@ -2821,7 +2681,6 @@
LogUtils.d(LOG_TAG, "AAC.requestDelete: ListFragment is handling delete.");
convListFragment.requestDelete(R.id.change_folder, conversations,
new DroppedInStarredAction(conversations, mFolder, folder));
- return;
}
}
@@ -2842,7 +2701,7 @@
@Override
public void performAction() {
ToastBarOperation undoOp = new ToastBarOperation(mConversations.size(),
- R.id.change_folder, ToastBarOperation.UNDO, true);
+ R.id.change_folder, ToastBarOperation.UNDO, true /* batch */, mInitialFolder);
onUndoAvailable(undoOp);
ArrayList<ConversationOperation> ops = new ArrayList<ConversationOperation>();
ContentValues values = new ContentValues();
@@ -2898,7 +2757,7 @@
* Check if the fragment given here is visible. Checking {@link Fragment#isVisible()} is
* insufficient because that doesn't check if the window is currently in focus or not.
*/
- private final boolean isFragmentVisible(Fragment in) {
+ private boolean isFragmentVisible(Fragment in) {
return in != null && in.isVisible() && mActivity.hasWindowFocus();
}
@@ -2907,9 +2766,8 @@
@Override
public Loader<ConversationCursor> onCreateLoader(int id, Bundle args) {
- Loader<ConversationCursor> result = new ConversationCursorLoader((Activity) mActivity,
+ return new ConversationCursorLoader((Activity) mActivity,
mAccount, mFolder.conversationListUri, mFolder.name);
- return result;
}
@Override
@@ -2956,9 +2814,274 @@
}
/**
+ * Class to perform {@link LoaderManager.LoaderCallbacks} for creating {@link Folder} objects.
+ */
+ private class FolderLoads implements LoaderManager.LoaderCallbacks<ObjectCursor<Folder>> {
+ @Override
+ public Loader<ObjectCursor<Folder>> onCreateLoader(int id, Bundle args) {
+ final String[] everything = UIProvider.FOLDERS_PROJECTION;
+ switch (id) {
+ case LOADER_FOLDER_CURSOR:
+ LogUtils.d(LOG_TAG, "LOADER_FOLDER_CURSOR created");
+ final ObjectCursorLoader<Folder> loader = new
+ ObjectCursorLoader<Folder>(
+ mContext, mFolder.uri, everything, Folder.FACTORY);
+ loader.setUpdateThrottle(mFolderItemUpdateDelayMs);
+ return loader;
+ case LOADER_RECENT_FOLDERS:
+ LogUtils.d(LOG_TAG, "LOADER_RECENT_FOLDERS created");
+ if (mAccount != null && mAccount.recentFolderListUri != null
+ && !mAccount.recentFolderListUri.equals(Uri.EMPTY)) {
+ return new ObjectCursorLoader<Folder>(mContext,
+ mAccount.recentFolderListUri, everything, Folder.FACTORY);
+ }
+ break;
+ case LOADER_ACCOUNT_INBOX:
+ LogUtils.d(LOG_TAG, "LOADER_ACCOUNT_INBOX created");
+ final Uri defaultInbox = Settings.getDefaultInboxUri(mAccount.settings);
+ final Uri inboxUri = defaultInbox.equals(Uri.EMPTY) ?
+ mAccount.folderListUri : defaultInbox;
+ LogUtils.d(LOG_TAG, "Loading the default inbox: %s", inboxUri);
+ if (inboxUri != null) {
+ return new ObjectCursorLoader<Folder>(mContext, inboxUri,
+ everything, Folder.FACTORY);
+ }
+ break;
+ case LOADER_SEARCH:
+ LogUtils.d(LOG_TAG, "LOADER_SEARCH created");
+ return Folder.forSearchResults(mAccount,
+ args.getString(ConversationListContext.EXTRA_SEARCH_QUERY),
+ mActivity.getActivityContext());
+ case LOADER_FIRST_FOLDER:
+ LogUtils.d(LOG_TAG, "LOADER_FIRST_FOLDER created");
+ final Uri folderUri = args.getParcelable(Utils.EXTRA_FOLDER_URI);
+ mConversationToShow = args.getParcelable(Utils.EXTRA_CONVERSATION);
+ if (mConversationToShow != null && mConversationToShow.position < 0){
+ mConversationToShow.position = 0;
+ }
+ return new ObjectCursorLoader<Folder>(mContext, folderUri,
+ everything, Folder.FACTORY);
+ default:
+ LogUtils.wtf(LOG_TAG, "FolderLoads.onCreateLoader(%d) for invalid id", id);
+ return null;
+ }
+ return null;
+ }
+
+ @Override
+ public void onLoadFinished(Loader<ObjectCursor<Folder>> loader, ObjectCursor<Folder> data) {
+ if (data == null) {
+ LogUtils.e(LOG_TAG, "Received null cursor from loader id: %d", loader.getId());
+ }
+ switch (loader.getId()) {
+ case LOADER_FOLDER_CURSOR:
+ if (data != null && data.moveToFirst()) {
+ final Folder folder = data.getModel();
+ setHasFolderChanged(folder);
+ mFolder = folder;
+ mFolderObservable.notifyChanged();
+ } else {
+ LogUtils.d(LOG_TAG, "Unable to get the folder %s",
+ mFolder != null ? mAccount.name : "");
+ }
+ break;
+ case LOADER_RECENT_FOLDERS:
+ // Few recent folders and we are running on a phone? Populate the default
+ // recents. The number of default recent folders is at least 2: every provider
+ // has at least two folders, and the recent folder count never decreases.
+ // Having a single recent folder is an erroneous case, and we can gracefully
+ // recover by populating default recents. The default recents will not stomp on
+ // the existing value: it will be shown in addition to the default folders:
+ // the max number of recent folders is more than 1+num(defaultRecents).
+ if (data != null && data.getCount() <= 1 && !mIsTablet) {
+ final class PopulateDefault extends AsyncTask<Uri, Void, Void> {
+ @Override
+ protected Void doInBackground(Uri... uri) {
+ // Asking for an update on the URI and ignore the result.
+ final ContentResolver resolver = mContext.getContentResolver();
+ resolver.update(uri[0], null, null, null);
+ return null;
+ }
+ }
+ final Uri uri = mAccount.defaultRecentFolderListUri;
+ LogUtils.v(LOG_TAG, "Default recents at %s", uri);
+ new PopulateDefault().execute(uri);
+ break;
+ }
+ LogUtils.v(LOG_TAG, "Reading recent folders from the cursor.");
+ mRecentFolderList.loadFromUiProvider(data);
+ if (isAnimating()) {
+ mRecentsDataUpdated = true;
+ } else {
+ mRecentFolderObservers.notifyChanged();
+ }
+ break;
+ case LOADER_ACCOUNT_INBOX:
+ if (data != null && !data.isClosed() && data.moveToFirst()) {
+ final Folder inbox = data.getModel();
+ onFolderChanged(inbox);
+ // Just want to get the inbox, don't care about updates to it
+ // as this will be tracked by the folder change listener.
+ mActivity.getLoaderManager().destroyLoader(LOADER_ACCOUNT_INBOX);
+ } else {
+ LogUtils.d(LOG_TAG, "Unable to get the account inbox for account %s",
+ mAccount != null ? mAccount.name : "");
+ }
+ break;
+ case LOADER_SEARCH:
+ if (data != null && data.getCount() > 0) {
+ data.moveToFirst();
+ final Folder search = data.getModel();
+ updateFolder(search);
+ mConvListContext = ConversationListContext.forSearchQuery(mAccount, mFolder,
+ mActivity.getIntent()
+ .getStringExtra(UIProvider.SearchQueryParameters.QUERY));
+ showConversationList(mConvListContext);
+ mActivity.invalidateOptionsMenu();
+ mHaveSearchResults = search.totalCount > 0;
+ mActivity.getLoaderManager().destroyLoader(LOADER_SEARCH);
+ } else {
+ LogUtils.e(LOG_TAG, "Null/empty cursor returned by LOADER_SEARCH loader");
+ }
+ break;
+ case LOADER_FIRST_FOLDER:
+ if (data == null || data.getCount() <=0 || !data.moveToFirst()) {
+ return;
+ }
+ final Folder folder = data.getModel();
+ boolean handled = false;
+ if (folder != null) {
+ onFolderChanged(folder);
+ handled = true;
+ }
+ if (mConversationToShow != null) {
+ // Open the conversation.
+ showConversation(mConversationToShow);
+ handled = true;
+ }
+ if (!handled) {
+ // We have an account, but nothing else: load the default inbox.
+ loadAccountInbox();
+ }
+ mConversationToShow = null;
+ // And don't run this anymore.
+ mActivity.getLoaderManager().destroyLoader(LOADER_FIRST_FOLDER);
+ break;
+ }
+ }
+
+ @Override
+ public void onLoaderReset(Loader<ObjectCursor<Folder>> loader) {
+ }
+ }
+
+ /**
+ * Class to perform {@link LoaderManager.LoaderCallbacks} for creating {@link Account} objects.
+ */
+ private class AccountLoads implements LoaderManager.LoaderCallbacks<ObjectCursor<Account>> {
+ final String[] mProjection = UIProvider.ACCOUNTS_PROJECTION;
+ final CursorCreator<Account> mFactory = Account.FACTORY;
+
+ @Override
+ public Loader<ObjectCursor<Account>> onCreateLoader(int id, Bundle args) {
+ switch (id) {
+ case LOADER_ACCOUNT_CURSOR:
+ LogUtils.d(LOG_TAG, "LOADER_ACCOUNT_CURSOR created");
+ return new ObjectCursorLoader<Account>(mContext,
+ MailAppProvider.getAccountsUri(), mProjection, mFactory);
+ case LOADER_ACCOUNT_UPDATE_CURSOR:
+ LogUtils.d(LOG_TAG, "LOADER_ACCOUNT_UPDATE_CURSOR created");
+ return new ObjectCursorLoader<Account>(mContext, mAccount.uri, mProjection,
+ mFactory);
+ default:
+ LogUtils.wtf(LOG_TAG, "Got an id (%d) that I cannot create!", id);
+ break;
+ }
+ return null;
+ }
+
+ @Override
+ public void onLoadFinished(Loader<ObjectCursor<Account>> loader,
+ ObjectCursor<Account> data) {
+ if (data == null) {
+ LogUtils.e(LOG_TAG, "Received null cursor from loader id: %d", loader.getId());
+ }
+ switch (loader.getId()) {
+ case LOADER_ACCOUNT_CURSOR:
+ if (data == null) {
+ // Nothing useful to do if we have no valid data.
+ break;
+ }
+ if (data.getCount() == 0) {
+ // If an empty cursor is returned, the MailAppProvider is indicating that
+ // no accounts have been specified. We want to navigate to the
+ // "add account" activity that will handle the intent returned by the
+ // MailAppProvider
+
+ // If the MailAppProvider believes that all accounts have been loaded,
+ // and the account list is still empty, we want to prompt the user to add
+ // an account.
+ final Bundle extras = data.getExtras();
+ final boolean accountsLoaded =
+ extras.getInt(AccountCursorExtraKeys.ACCOUNTS_LOADED) != 0;
+
+ if (accountsLoaded) {
+ final Intent noAccountIntent = MailAppProvider.getNoAccountIntent
+ (mContext);
+ if (noAccountIntent != null) {
+ mActivity.startActivityForResult(noAccountIntent,
+ ADD_ACCOUNT_REQUEST_CODE);
+ }
+ }
+ } else {
+ final boolean accountListUpdated = accountsUpdated(data);
+ if (!isLoaderInitialized || accountListUpdated) {
+ isLoaderInitialized = updateAccounts(data);
+ }
+ }
+ 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();
+
+ 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
+ mAccount = updatedAccount;
+ LogUtils.d(LOG_TAG, "AbstractActivityController.onLoadFinished(): "
+ + "mAccount = %s", mAccount.uri);
+
+ // Only notify about a settings change if something differs
+ if (!Objects.equal(mAccount.settings, previousSettings)) {
+ mAccountObservers.notifyChanged();
+ }
+ perhapsEnterWaitMode();
+ } else {
+ LogUtils.e(LOG_TAG, "Got update for account: %s with current account:"
+ + " %s", updatedAccount.uri, mAccount.uri);
+ // We need to restart the loader, so the correct account information
+ // will be returned.
+ restartOptionalLoader(LOADER_ACCOUNT_UPDATE_CURSOR, this, Bundle.EMPTY);
+ }
+ }
+ break;
+ }
+ }
+
+ @Override
+ public void onLoaderReset(Loader<ObjectCursor<Account>> loader) {
+ }
+ }
+
+ /**
* Updates controller state based on search results and shows first conversation if required.
*/
- private final void perhapsShowFirstSearchResult() {
+ private void perhapsShowFirstSearchResult() {
if (mCurrentConversation == null) {
// Shown for search results in two-pane mode only.
mHaveSearchResults = Intent.ACTION_SEARCH.equals(mActivity.getIntent().getAction())
@@ -2978,7 +3101,7 @@
* next destructive action..
* @param nextAction the next destructive action to be performed. This can be null.
*/
- private final void destroyPending(DestructiveAction nextAction) {
+ private void destroyPending(DestructiveAction nextAction) {
// If there is a pending action, perform that first.
if (mPendingDestruction != null) {
mPendingDestruction.performAction();
@@ -2990,14 +3113,13 @@
* Register a destructive action with the controller. This performs the previous destructive
* action as a side effect. This method is final because we don't want the child classes to
* embellish this method any more.
- * @param action
+ * @param action the action to register.
*/
- private final void registerDestructiveAction(DestructiveAction action) {
+ private void registerDestructiveAction(DestructiveAction action) {
// TODO(viki): This is not a good idea. The best solution is for clients to request a
// destructive action from the controller and for the controller to own the action. This is
// a half-way solution while refactoring DestructiveAction.
destroyPending(action);
- return;
}
@Override
@@ -3021,7 +3143,7 @@
* @param target the conversations to act upon.
* @return a {@link DestructiveAction} that performs the specified action.
*/
- private final DestructiveAction getDeferredAction(int action, Collection<Conversation> target,
+ private DestructiveAction getDeferredAction(int action, Collection<Conversation> target,
boolean batch) {
return new ConversationAction(action, target, batch);
}
@@ -3040,20 +3162,23 @@
private boolean mIsSelectedSet;
private boolean mShowUndo;
private int mAction;
+ private final Folder mActionFolder;
/**
* Create a new folder destruction object to act on the given conversations.
- * @param target
+ * @param target conversations to act upon.
+ * @param actionFolder the {@link Folder} being acted upon, used for displaying the undo bar
*/
private FolderDestruction(final Collection<Conversation> target,
final Collection<FolderOperation> folders, boolean isDestructive, boolean isBatch,
- boolean showUndo, int action) {
+ boolean showUndo, int action, final Folder actionFolder) {
mTarget = ImmutableList.copyOf(target);
mFolderOps.addAll(folders);
mIsDestructive = isDestructive;
mIsSelectedSet = isBatch;
mShowUndo = showUndo;
mAction = action;
+ mActionFolder = actionFolder;
}
@Override
@@ -3063,7 +3188,7 @@
}
if (mIsDestructive && mShowUndo) {
ToastBarOperation undoOp = new ToastBarOperation(mTarget.size(), mAction,
- ToastBarOperation.UNDO, mIsSelectedSet);
+ ToastBarOperation.UNDO, mIsSelectedSet, mActionFolder);
onUndoAvailable(undoOp);
}
// For each conversation, for each operation, add/ remove the
@@ -3115,19 +3240,18 @@
public final DestructiveAction getFolderChange(Collection<Conversation> target,
Collection<FolderOperation> folders, boolean isDestructive, boolean isBatch,
- boolean showUndo) {
+ boolean showUndo, final boolean isMoveTo, final Folder actionFolder) {
final DestructiveAction da = getDeferredFolderChange(target, folders, isDestructive,
- isBatch, showUndo);
+ isBatch, showUndo, isMoveTo, actionFolder);
registerDestructiveAction(da);
return da;
}
public final DestructiveAction getDeferredFolderChange(Collection<Conversation> target,
Collection<FolderOperation> folders, boolean isDestructive, boolean isBatch,
- boolean showUndo) {
- final DestructiveAction da = new FolderDestruction(target, folders, isDestructive, isBatch,
- showUndo, R.id.change_folder);
- return da;
+ boolean showUndo, final boolean isMoveTo, final Folder actionFolder) {
+ return new FolderDestruction(target, folders, isDestructive, isBatch, showUndo,
+ isMoveTo ? R.id.move_folder : R.id.change_folder, actionFolder);
}
@Override
@@ -3137,7 +3261,7 @@
Collection<FolderOperation> folderOps = new ArrayList<FolderOperation>();
folderOps.add(new FolderOperation(toRemove, false));
return new FolderDestruction(target, folderOps, isDestructive, isBatch,
- showUndo, R.id.remove_folder);
+ showUndo, R.id.remove_folder, mFolder);
}
@Override
@@ -3224,7 +3348,7 @@
false, /* showActionIcon */
actionTextResourceId,
replaceVisibleToast,
- new ToastBarOperation(1, 0, ToastBarOperation.ERROR, false));
+ new ToastBarOperation(1, 0, ToastBarOperation.ERROR, false, folder));
}
private ActionClickedListener getRetryClickedListener(final Folder folder) {
@@ -3373,8 +3497,8 @@
* Sets the listener for the positive action on a confirmation dialog. Since only a single
* confirmation dialog can be shown, this overwrites the previous listener. It is safe to
* unset the listener; in which case action should be set to -1.
- * @param listener
- * @param action
+ * @param listener the listener that will perform the task for this dialog's positive action.
+ * @param action the action that created this dialog.
*/
private void setListener(AlertDialog.OnClickListener listener, final int action){
mDialogListener = listener;
@@ -3410,5 +3534,4 @@
}
mDetachedConvUri = null;
}
-
}
diff --git a/src/com/android/mail/ui/AbstractConversationViewFragment.java b/src/com/android/mail/ui/AbstractConversationViewFragment.java
index 0bb2e57..ba63588 100644
--- a/src/com/android/mail/ui/AbstractConversationViewFragment.java
+++ b/src/com/android/mail/ui/AbstractConversationViewFragment.java
@@ -98,7 +98,6 @@
protected String mBaseUri;
protected Account mAccount;
protected final Map<String, Address> mAddressCache = Maps.newHashMap();
- protected boolean mEnableContentReadySignal;
private MessageCursor mCursor;
private Context mContext;
/**
@@ -203,17 +202,6 @@
// base uri that us guaranteed to be unique for this conversation.
mBaseUri = "x-thread://" + mAccount.name + "/" + mConversation.id;
- // On JB or newer, we use the 'webkitAnimationStart' DOM event to signal load complete
- // Below JB, try to speed up initial render by having the webview do supplemental draws to
- // custom a software canvas.
- // TODO(mindyp):
- //PAGE READINESS SIGNAL FOR JELLYBEAN AND NEWER
- // Notify the app on 'webkitAnimationStart' of a simple dummy element with a simple no-op
- // animation that immediately runs on page load. The app uses this as a signal that the
- // content is loaded and ready to draw, since WebView delays firing this event until the
- // layers are composited and everything is ready to draw.
- // This signal does not seem to be reliable, so just use the old method for now.
- mEnableContentReadySignal = false; //Utils.isRunningJellybeanOrLater();
LogUtils.d(LOG_TAG, "onCreate in ConversationViewFragment (this=%s)", this);
// Not really, we just want to get a crack to store a reference to the change_folder item
setHasOptionsMenu(true);
@@ -473,6 +461,12 @@
return mUserVisible;
}
+ protected void timerMark(String msg) {
+ if (isUserVisible()) {
+ Utils.sConvLoadTimer.mark(msg);
+ }
+ }
+
private class MessageLoaderCallbacks implements LoaderManager.LoaderCallbacks<Cursor> {
@Override
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/AccountChangeListener.java b/src/com/android/mail/ui/AccountChangeListener.java
deleted file mode 100644
index 7690799..0000000
--- a/src/com/android/mail/ui/AccountChangeListener.java
+++ /dev/null
@@ -1,31 +0,0 @@
-/*******************************************************************************
- * Copyright (C) 2012 Google Inc.
- * Licensed to The Android Open Source Project.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- *******************************************************************************/
-package com.android.mail.ui;
-
-import com.android.mail.providers.Account;
-
-/**
- * The callback interface for when a list item has been selected.
- */
-public interface AccountChangeListener {
- /**
- * Handles selecting a folder from within the {@link FolderListFragment}.
- *
- * @param folder the selected folder
- */
- void onAccountChanged(Account account);
-}
diff --git a/src/com/android/mail/ui/AccountController.java b/src/com/android/mail/ui/AccountController.java
index e13b2eb..1d7745c 100644
--- a/src/com/android/mail/ui/AccountController.java
+++ b/src/com/android/mail/ui/AccountController.java
@@ -45,9 +45,31 @@
*/
Account getAccount();
+
+ /**
+ * Registers to receive changes to the list of accounts, and obtain the current list.
+ */
+ void registerAllAccountObserver(DataSetObserver observer);
+
+ /**
+ * Removes a listener from receiving account list changes.
+ */
+ void unregisterAllAccountObserver(DataSetObserver observer);
+
+ /** Returns a list of all accounts currently known. */
+ Account[] getAllAccounts();
+
/**
* Returns an object that can check veiled addresses.
* @return
*/
VeiledAddressMatcher getVeiledAddressMatcher();
+
+ /**
+ * Handles selecting an account from within the {@link FolderListFragment}.
+ *
+ * @param account the account to change to.
+ */
+ void changeAccount(Account account);
+
}
diff --git a/src/com/android/mail/ui/ActionableToastBar.java b/src/com/android/mail/ui/ActionableToastBar.java
index 12279ec..a7e4719 100644
--- a/src/com/android/mail/ui/ActionableToastBar.java
+++ b/src/com/android/mail/ui/ActionableToastBar.java
@@ -18,6 +18,7 @@
import android.animation.Animator;
import android.animation.AnimatorInflater;
import android.content.Context;
+import android.os.Handler;
import android.text.Spanned;
import android.util.AttributeSet;
import android.view.LayoutInflater;
@@ -38,9 +39,14 @@
private boolean mHidden = false;
private Animator mShowAnimation;
private Animator mHideAnimation;
+ private final Runnable mRunnable;
+ private final Handler mFadeOutHandler;
private final int mBottomMarginSizeInConversation;
private final int mBottomMarginSize;
+ /** How long toast will last in ms */
+ private static final long TOAST_LIFETIME = 15*1000L;
+
/** Icon for the description. */
private ImageView mActionDescriptionIcon;
/** The clickable view */
@@ -63,6 +69,15 @@
public ActionableToastBar(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
+ mFadeOutHandler = new Handler();
+ mRunnable = new Runnable() {
+ @Override
+ public void run() {
+ if(!mHidden) {
+ hide(true);
+ }
+ }
+ };
mBottomMarginSize = context.getResources()
.getDimensionPixelSize(R.dimen.toast_bar_bottom_margin);
mBottomMarginSizeInConversation = context.getResources().getDimensionPixelSize(
@@ -113,6 +128,8 @@
if (!mHidden && !replaceVisibleToast) {
return;
}
+ // Remove any running delayed animations first
+ mFadeOutHandler.removeCallbacks(mRunnable);
mOperation = op;
@@ -138,6 +155,9 @@
mHidden = false;
getShowAnimation().start();
+
+ // Set up runnable to execute hide toast once delay is completed
+ mFadeOutHandler.postDelayed(mRunnable, TOAST_LIFETIME);
}
public ToastBarOperation getOperation() {
@@ -149,6 +169,7 @@
*/
public void hide(boolean animate) {
mHidden = true;
+ mFadeOutHandler.removeCallbacks(mRunnable);
if (getVisibility() == View.VISIBLE) {
mActionDescriptionView.setText("");
mActionButton.setOnClickListener(null);
@@ -224,6 +245,13 @@
public boolean isAnimating() {
return mShowAnimation != null && mShowAnimation.isStarted();
}
+
+ @Override
+ public void onDetachedFromWindow() {
+ mFadeOutHandler.removeCallbacks(mRunnable);
+ super.onDetachedFromWindow();
+ }
+
/**
* Classes that wish to perform some action when the action button is clicked
* should implement this interface.
diff --git a/src/com/android/mail/ui/ActivityController.java b/src/com/android/mail/ui/ActivityController.java
index 239185b..d1a1740 100644
--- a/src/com/android/mail/ui/ActivityController.java
+++ b/src/com/android/mail/ui/ActivityController.java
@@ -42,8 +42,7 @@
*/
public interface ActivityController extends LayoutListener,
ModeChangeListener, ConversationListCallbacks,
- FolderChangeListener, AccountChangeListener, LoaderManager.LoaderCallbacks<Cursor>,
- ConversationSetObserver, ConversationListener,
+ FolderChangeListener, ConversationSetObserver, ConversationListener,
FolderListFragment.FolderListSelectionListener, HelpCallback, UndoListener,
ConversationUpdater, ErrorListener, FolderController, AccountController,
ConversationPositionTracker.Callbacks, ConversationListFooterView.FooterViewClickListener,
@@ -217,9 +216,10 @@
public void showWaitForInitialization();
/**
- * Show the folder list associated with the currently selected account.
+ * Load the folder list into the drawer fragment. Only handled by
+ * OnePaneController on account change.
*/
- void showFolderList();
+ void loadFolderList();
/**
* Handle a touch event.
diff --git a/src/com/android/mail/ui/AddableFolderSelectorAdapter.java b/src/com/android/mail/ui/AddableFolderSelectorAdapter.java
index 73ff38e..0888369 100644
--- a/src/com/android/mail/ui/AddableFolderSelectorAdapter.java
+++ b/src/com/android/mail/ui/AddableFolderSelectorAdapter.java
@@ -43,6 +43,8 @@
if (type == UIProvider.FolderType.INBOX || type == UIProvider.FolderType.DEFAULT) {
folder[UIProvider.FOLDER_ID_COLUMN] = folderCursor
.getLong(UIProvider.FOLDER_ID_COLUMN);
+ folder[UIProvider.FOLDER_PERSISTENT_ID_COLUMN] = folderCursor
+ .getString(UIProvider.FOLDER_PERSISTENT_ID_COLUMN);
folder[UIProvider.FOLDER_URI_COLUMN] = folderCursor
.getString(UIProvider.FOLDER_URI_COLUMN);
folder[UIProvider.FOLDER_NAME_COLUMN] = folderCursor
@@ -57,6 +59,8 @@
.getString(UIProvider.FOLDER_CONVERSATION_LIST_URI_COLUMN);
folder[UIProvider.FOLDER_CHILD_FOLDERS_LIST_COLUMN] = folderCursor
.getString(UIProvider.FOLDER_CHILD_FOLDERS_LIST_COLUMN);
+ folder[UIProvider.FOLDER_UNSEEN_COUNT_COLUMN] = folderCursor
+ .getInt(UIProvider.FOLDER_UNSEEN_COUNT_COLUMN);
folder[UIProvider.FOLDER_UNREAD_COUNT_COLUMN] = folderCursor
.getInt(UIProvider.FOLDER_UNREAD_COUNT_COLUMN);
folder[UIProvider.FOLDER_TOTAL_COUNT_COLUMN] = folderCursor
@@ -69,7 +73,9 @@
.getInt(UIProvider.FOLDER_LAST_SYNC_RESULT_COLUMN);
folder[UIProvider.FOLDER_TYPE_COLUMN] = type;
folder[UIProvider.FOLDER_ICON_RES_ID_COLUMN] = folderCursor
- .getLong(UIProvider.FOLDER_ICON_RES_ID_COLUMN);
+ .getInt(UIProvider.FOLDER_ICON_RES_ID_COLUMN);
+ folder[UIProvider.FOLDER_NOTIFICATION_ICON_RES_ID_COLUMN] = folderCursor
+ .getInt(UIProvider.FOLDER_NOTIFICATION_ICON_RES_ID_COLUMN);
folder[UIProvider.FOLDER_BG_COLOR_COLUMN] = folderCursor
.getString(UIProvider.FOLDER_BG_COLOR_COLUMN);
folder[UIProvider.FOLDER_FG_COLOR_COLUMN] = folderCursor
@@ -78,6 +84,8 @@
.getString(UIProvider.FOLDER_LOAD_MORE_URI_COLUMN);
folder[UIProvider.FOLDER_HIERARCHICAL_DESC_COLUMN] = folderCursor
.getString(UIProvider.FOLDER_HIERARCHICAL_DESC_COLUMN);
+ folder[UIProvider.FOLDER_LAST_MESSAGE_TIMESTAMP_COLUMN] = folderCursor
+ .getLong(UIProvider.FOLDER_LAST_MESSAGE_TIMESTAMP_COLUMN);
cursor.addRow(folder);
}
} while (folderCursor.moveToNext());
diff --git a/src/com/android/mail/ui/AnimatedAdapter.java b/src/com/android/mail/ui/AnimatedAdapter.java
index abb1dd6..0a75711 100644
--- a/src/com/android/mail/ui/AnimatedAdapter.java
+++ b/src/com/android/mail/ui/AnimatedAdapter.java
@@ -24,6 +24,7 @@
import android.database.Cursor;
import android.os.Bundle;
import android.os.Handler;
+import android.util.SparseArray;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
@@ -49,6 +50,7 @@
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
+import java.util.List;
import java.util.Map.Entry;
public class AnimatedAdapter extends SimpleCursorAdapter implements
@@ -115,6 +117,9 @@
}
};
+ private final List<ConversationSpecialItemView> mSpecialViews;
+ private final SparseArray<ConversationSpecialItemView> mSpecialViewPositions;
+
private final void setAccount(Account newAccount) {
mAccount = newAccount;
mPriorityMarkersEnabled = mAccount.settings.priorityArrowsEnabled;
@@ -130,6 +135,12 @@
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) {
super(context, -1, cursor, UIProvider.CONVERSATION_PROJECTION, null, 0);
mContext = context;
mBatchConversations = batch;
@@ -146,6 +157,16 @@
context.getResources()
.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());
+
+ for (final ConversationSpecialItemView view : mSpecialViews) {
+ view.setAdapter(this);
+ }
+
+ updateSpecialViews();
}
public void cancelDismissCounter() {
@@ -169,7 +190,10 @@
@Override
public int getCount() {
- final int count = super.getCount();
+ // mSpecialViewPositions only contains the views that are currently being displayed
+ final int specialViewCount = mSpecialViewPositions.size();
+
+ final int count = super.getCount() + specialViewCount;
return mShowFooter ? count + 1 : count;
}
@@ -231,7 +255,7 @@
@Override
public int getItemViewType(int position) {
// Try to recycle views.
- if (mShowFooter && position == super.getCount()) {
+ if (mShowFooter && position == getCount() - 1) {
return TYPE_VIEW_FOOTER;
} else if (hasLeaveBehinds() || isAnimating()) {
// Setting as type -1 means the recycler won't take this view and
@@ -241,6 +265,9 @@
// 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) {
+ // Don't recycle the special views
+ return TYPE_VIEW_DONT_RECYCLE;
}
return TYPE_VIEW_CONVERSATION;
}
@@ -306,11 +333,18 @@
@Override
public View getView(int position, View convertView, ViewGroup parent) {
- if (mShowFooter && position == super.getCount()) {
+ if (mShowFooter && position == getCount() - 1) {
return mFooter;
}
+
+ // Check if this is a special view
+ final View specialView = (View) mSpecialViewPositions.get(position);
+ if (specialView != null) {
+ return specialView;
+ }
+
ConversationCursor cursor = (ConversationCursor) getItem(position);
- Conversation conv = new Conversation(cursor);
+ final Conversation conv = cursor.getConversation();
if (isPositionUndoing(conv.id)) {
return getUndoingView(position, conv, parent, false /* don't show swipe background */);
} if (isPositionUndoingSwipe(conv.id)) {
@@ -347,6 +381,7 @@
return fadeIn;
}
}
+
if (convertView != null && !(convertView instanceof SwipeableConversationItemView)) {
LogUtils.w(LOG_TAG, "Incorrect convert view received; nulling it out");
convertView = newView(mContext, cursor, parent);
@@ -497,10 +532,11 @@
@Override
public long getItemId(int position) {
- if (mShowFooter && position == super.getCount()) {
+ if (mShowFooter && position == getCount() - 1
+ || mSpecialViewPositions.get(position) != null) {
return -1;
}
- return super.getItemId(position);
+ return super.getItemId(position - getPositionOffset(position));
}
private View getDeletingView(int position, Conversation conversation, ViewGroup parent,
@@ -559,10 +595,12 @@
@Override
public Object getItem(int position) {
- if (mShowFooter && position == super.getCount()) {
+ if (mShowFooter && position == getCount() - 1) {
return mFooter;
+ } else if (mSpecialViewPositions.get(position) != null) {
+ return mSpecialViewPositions.get(position);
}
- return super.getItem(position);
+ return super.getItem(position - getPositionOffset(position));
}
private boolean isPositionDeleting(long id) {
@@ -797,4 +835,67 @@
item.cancelFadeOutText();
}
}
+
+ private void updateSpecialViews() {
+ mSpecialViewPositions.clear();
+
+ for (int i = 0; i < mSpecialViews.size(); i++) {
+ final ConversationSpecialItemView specialView = mSpecialViews.get(i);
+ specialView.onUpdate(mAccount.name, mFolder, getConversationCursor());
+
+ if (specialView.getShouldDisplayInList()) {
+ mSpecialViewPositions.put(specialView.getPosition(), specialView);
+ }
+ }
+ }
+
+ @Override
+ public void notifyDataSetChanged() {
+ updateSpecialViews();
+ super.notifyDataSetChanged();
+ }
+
+ @Override
+ public void changeCursor(final Cursor cursor) {
+ super.changeCursor(cursor);
+ updateSpecialViews();
+ }
+
+ @Override
+ public void changeCursorAndColumns(final Cursor c, final String[] from, final int[] to) {
+ super.changeCursorAndColumns(c, from, to);
+ updateSpecialViews();
+ }
+
+ @Override
+ public Cursor swapCursor(final Cursor c) {
+ final Cursor oldCursor = super.swapCursor(c);
+ updateSpecialViews();
+
+ return oldCursor;
+ }
+
+ /**
+ * Gets the offset for the given position in the underlying cursor, based on any special views
+ * that may be above it.
+ */
+ 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);
+ if (specialView.getPosition() <= position) {
+ offset++;
+ }
+ }
+
+ return offset;
+ }
+
+ public void cleanup() {
+ for (final ConversationSpecialItemView view : mSpecialViews) {
+ view.cleanup();
+ }
+ }
}
diff --git a/src/com/android/mail/ui/ControllableActivity.java b/src/com/android/mail/ui/ControllableActivity.java
index 37e70f5..f4a1d87 100644
--- a/src/com/android/mail/ui/ControllableActivity.java
+++ b/src/com/android/mail/ui/ControllableActivity.java
@@ -116,4 +116,9 @@
void stopDragMode();
boolean isAccessibilityEnabled();
+
+ /**
+ * Gets a helper to provide addition features in the conversation list. This may be null.
+ */
+ ConversationListHelper getConversationListHelper();
}
diff --git a/src/com/android/mail/ui/ConversationListFragment.java b/src/com/android/mail/ui/ConversationListFragment.java
index ec57e4a..12d2087 100644
--- a/src/com/android/mail/ui/ConversationListFragment.java
+++ b/src/com/android/mail/ui/ConversationListFragment.java
@@ -43,6 +43,7 @@
import com.android.mail.providers.AccountObserver;
import com.android.mail.providers.Conversation;
import com.android.mail.providers.Folder;
+import com.android.mail.providers.FolderObserver;
import com.android.mail.providers.Settings;
import com.android.mail.providers.UIProvider;
import com.android.mail.providers.UIProvider.AccountCapabilities;
@@ -57,6 +58,7 @@
import com.android.mail.utils.Utils;
import java.util.Collection;
+import java.util.List;
/**
* The conversation list UI component.
@@ -117,7 +119,7 @@
private ConversationListFooterView mFooterView;
private View mEmptyView;
private ErrorListener mErrorListener;
- private DataSetObserver mFolderObserver;
+ private FolderObserver mFolderObserver;
private DataSetObserver mConversationCursorObserver;
private ConversationSelectionSet mSelectedSet;
@@ -140,21 +142,6 @@
super();
}
- // update the pager title strip as the Folder's conversation count changes
- private class FolderObserver extends DataSetObserver {
- @Override
- public void onChanged() {
- if (mActivity == null) {
- return;
- }
- final FolderController controller = mActivity.getFolderController();
- if (controller == null) {
- return;
- }
- onFolderUpdated(controller.getFolder());
- }
- }
-
private class ConversationCursorObserver extends DataSetObserver {
@Override
public void onChanged() {
@@ -264,15 +251,32 @@
null);
mFooterView.setClickListener(mActivity);
final ConversationCursor conversationCursor = getConversationListCursor();
- mListAdapter = new AnimatedAdapter(mActivity.getApplicationContext(),
- conversationCursor, mActivity.getSelectedSet(), mActivity, mListView);
+
+ final ConversationListHelper helper = mActivity.getConversationListHelper();
+ final List<ConversationSpecialItemView> specialItemViews =
+ helper != null ? helper.makeConversationListSpecialViews(getActivity(), mAccount,
+ mActivity.getFolderListSelectionListener()) : null;
+ if (specialItemViews != null) {
+ // Attach to the LoaderManager
+ for (final ConversationSpecialItemView view : specialItemViews) {
+ view.bindLoaderManager(getLoaderManager());
+ }
+ }
+
+ mListAdapter = new AnimatedAdapter(mActivity.getApplicationContext(), conversationCursor,
+ mActivity.getSelectedSet(), mActivity, mListView, specialItemViews);
mListAdapter.addFooter(mFooterView);
mListView.setAdapter(mListAdapter);
mSelectedSet = mActivity.getSelectedSet();
mListView.setSelectionSet(mSelectedSet);
mListAdapter.hideFooter();
- mFolderObserver = new FolderObserver();
- mActivity.getFolderController().registerFolderObserver(mFolderObserver);
+ mFolderObserver = new FolderObserver(){
+ @Override
+ public void onChanged(Folder newFolder) {
+ onFolderUpdated(newFolder);
+ }
+ };
+ mFolderObserver.initialize(mActivity.getFolderController());
mConversationCursorObserver = new ConversationCursorObserver();
mUpdater = mActivity.getConversationUpdater();
mUpdater.registerConversationListObserver(mConversationCursorObserver);
@@ -438,7 +442,7 @@
mActivity.unsetViewModeListener(this);
if (mFolderObserver != null) {
- mActivity.getFolderController().unregisterFolderObserver(mFolderObserver);
+ mFolderObserver.unregisterAndDestroy();
mFolderObserver = null;
}
if (mConversationCursorObserver != null) {
@@ -446,6 +450,7 @@
mConversationCursorObserver = null;
}
mAccountObserver.unregisterAndDestroy();
+ getAnimatedAdapter().cleanup();
super.onDestroyView();
}
@@ -475,7 +480,7 @@
public boolean onItemLongClick(AdapterView<?> parent, View view, int position, long id) {
// Ignore anything that is not a conversation item. Could be a footer.
if (!(view instanceof ConversationItemView)) {
- return true;
+ return false;
}
((ConversationItemView) view).toggleCheckMarkOrBeginDrag();
return true;
@@ -584,30 +589,40 @@
/**
* View the message at the given position.
- * @param position
+ *
+ * @param position The position of the conversation in the list (as opposed to its position in
+ * the cursor)
*/
- protected void viewConversation(int position) {
+ protected void viewConversation(final int position) {
LogUtils.d(LOG_TAG, "ConversationListFragment.viewConversation(%d)", position);
- setSelected(position, true);
- final ConversationCursor cursor = getConversationListCursor();
- if (cursor != null && cursor.moveToPosition(position)) {
- final Conversation conv = new Conversation(cursor);
- conv.position = position;
- mCallbacks.onConversationSelected(conv, false /* inLoaderCallbacks */);
- }
+
+ final ConversationCursor cursor =
+ (ConversationCursor) getAnimatedAdapter().getItem(position);
+ final Conversation conv = cursor.getConversation();
+ /*
+ * The cursor position may be different than the position method parameter because of
+ * special views in the list.
+ */
+ conv.position = cursor.getPosition();
+ setSelected(conv.position, true);
+ mCallbacks.onConversationSelected(conv, false /* inLoaderCallbacks */);
}
/**
* Sets the selected conversation to the position given here.
- * @param position
+ * @param position The position of the conversation in the cursor (as opposed to in the list)
* @param different if the currently selected conversation is different from the one provided
* here. This is a difference in conversations, not a difference in positions. For example, a
* conversation at position 2 can move to position 4 as a result of new mail.
*/
- public void setSelected(int position, boolean different) {
+ public void setSelected(final int cursorPosition, boolean different) {
if (mListView.getChoiceMode() == ListView.CHOICE_MODE_NONE) {
return;
}
+
+ final int position =
+ cursorPosition + getAnimatedAdapter().getPositionOffset(cursorPosition);
+
if (different) {
mListView.smoothScrollToPosition(position);
}
@@ -775,6 +790,11 @@
mListAdapter.notifyDataSetChanged();
}
mConversationCursorHash = newCursorHash;
+
+ if (newCursor != null) {
+ newCursor.markContentsSeen();
+ }
+
// If a current conversation is available, and none is selected in the list, then ask
// the list to select the current conversation.
final Conversation conv = mCallbacks.getCurrentConversation();
diff --git a/src/com/android/mail/ui/ConversationListHelper.java b/src/com/android/mail/ui/ConversationListHelper.java
new file mode 100644
index 0000000..8470be1
--- /dev/null
+++ b/src/com/android/mail/ui/ConversationListHelper.java
@@ -0,0 +1,32 @@
+/*
+ * 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.content.Context;
+
+import com.android.mail.providers.Account;
+import com.android.mail.ui.FolderListFragment.FolderListSelectionListener;
+
+import java.util.List;
+
+public interface ConversationListHelper {
+ /**
+ * Creates a list of newly created special views.
+ */
+ List<ConversationSpecialItemView> makeConversationListSpecialViews(Context context,
+ Account account, FolderListSelectionListener listener);
+}
diff --git a/src/com/android/mail/ui/ConversationSpecialItemView.java b/src/com/android/mail/ui/ConversationSpecialItemView.java
new file mode 100644
index 0000000..8488162
--- /dev/null
+++ b/src/com/android/mail/ui/ConversationSpecialItemView.java
@@ -0,0 +1,49 @@
+/*
+ * 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.widget.BaseAdapter;
+
+import com.android.mail.browse.ConversationCursor;
+import com.android.mail.providers.Folder;
+
+/**
+ * An interface for a view that can be inserted into an {@link AnimatedAdapter} at an arbitrary
+ * point. The methods described here control whether the view gets displayed, and what it displays.
+ */
+public interface ConversationSpecialItemView {
+ /**
+ * Called when there as an update to the information being displayed.
+ *
+ * @param cursor The {@link ConversationCursor}. May be <code>null</code>
+ */
+ void onUpdate(String account, Folder folder, ConversationCursor cursor);
+
+ boolean getShouldDisplayInList();
+
+ int getPosition();
+
+ void setAdapter(BaseAdapter adapter);
+
+ void bindLoaderManager(LoaderManager loaderManager);
+
+ /**
+ * Called when the view is being destroyed.
+ */
+ void cleanup();
+}
diff --git a/src/com/android/mail/ui/ConversationUpdater.java b/src/com/android/mail/ui/ConversationUpdater.java
index fe7f621..55511ac 100644
--- a/src/com/android/mail/ui/ConversationUpdater.java
+++ b/src/com/android/mail/ui/ConversationUpdater.java
@@ -23,7 +23,6 @@
import com.android.mail.browse.ConfirmDialogFragment;
import com.android.mail.browse.ConversationCursor;
-import com.android.mail.browse.ConversationItemView;
import com.android.mail.browse.MessageCursor.ConversationMessage;
import com.android.mail.providers.Conversation;
import com.android.mail.providers.ConversationInfo;
@@ -142,15 +141,17 @@
boolean showUndo);
/**
- * Assign the target conversations to the given folders, and remove them from all other
- * folders that they might be assigned to.
+ * Assign the target conversations to the given folders, and remove them from all other folders
+ * that they might be assigned to.
* @param folders the folders to assign the conversations to.
* @param target the conversations to act upon.
* @param batch whether this is a batch operation
* @param showUndo whether to show the undo bar
+ * @param isMoveTo <code>true</code> if this is a move operation, <code>false</code> if it is
+ * some other type of folder change operation
*/
public void assignFolder(Collection<FolderOperation> folders, Collection<Conversation> target,
- boolean batch, boolean showUndo);
+ boolean batch, boolean showUndo, boolean isMoveTo);
/**
* Refreshes the conversation list, if one exists.
diff --git a/src/com/android/mail/ui/ConversationViewFragment.java b/src/com/android/mail/ui/ConversationViewFragment.java
index 714b6b6..8aae3f3 100644
--- a/src/com/android/mail/ui/ConversationViewFragment.java
+++ b/src/com/android/mail/ui/ConversationViewFragment.java
@@ -83,10 +83,12 @@
import com.android.mail.utils.LogUtils;
import com.android.mail.utils.Utils;
import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import java.util.ArrayList;
import java.util.List;
+import java.util.Map;
import java.util.Set;
@@ -193,6 +195,8 @@
private long mWebViewLoadStartMs;
+ private final Map<String, String> mMessageTransforms = Maps.newHashMap();
+
private final DataSetObserver mLoadedObserver = new DataSetObserver() {
@Override
public void onChanged() {
@@ -386,8 +390,9 @@
final WebChromeClient wcc = new WebChromeClient() {
@Override
public boolean onConsoleMessage(ConsoleMessage consoleMessage) {
- LogUtils.i(LOG_TAG, "JS: %s (%s:%d)", consoleMessage.message(),
- consoleMessage.sourceId(), consoleMessage.lineNumber());
+ LogUtils.i(LOG_TAG, "JS: %s (%s:%d) f=%s", consoleMessage.message(),
+ consoleMessage.sourceId(), consoleMessage.lineNumber(),
+ ConversationViewFragment.this);
return true;
}
};
@@ -589,6 +594,7 @@
LogUtils.i(LOG_TAG,
"SHOWCONV: CVF is user-visible, immediately loading conversation (%s)", this);
reason = LOAD_NOW;
+ timerMark("CVF.showConversation");
} else {
final boolean disableOffscreenLoading = DISABLE_OFFSCREEN_LOADING
|| (mConversation.isRemote
@@ -625,12 +631,6 @@
private void startConversationLoad() {
mWebView.setVisibility(View.VISIBLE);
getLoaderManager().initLoader(MESSAGE_LOADER, Bundle.EMPTY, getMessageLoaderCallbacks());
- if (isUserVisible()) {
- final SubjectDisplayChanger sdc = mActivity.getSubjectDisplayChanger();
- if (sdc != null) {
- sdc.setSubject(mConversation.subject);
- }
- }
// 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.
@@ -638,6 +638,7 @@
}
private void revealConversation() {
+ timerMark("revealing conversation");
dismissLoadingStatus(mOnProgressDismiss);
}
@@ -647,6 +648,7 @@
private void renderConversation(MessageCursor messageCursor) {
final String convHtml = renderMessageBodies(messageCursor, mEnableContentReadySignal);
+ timerMark("rendered conversation");
if (DEBUG_DUMP_CONVERSATION_HTML) {
java.io.FileWriter fw = null;
@@ -787,9 +789,11 @@
mWebView.getSettings().setBlockNetworkImage(!allowNetworkImages);
+ final MailPrefs prefs = MailPrefs.get(getContext());
// If the conversation has specified a base uri, use it here, otherwise use mBaseUri
return mTemplates.endConversation(mBaseUri, mConversation.getBaseUri(mBaseUri), 320,
- mWebView.getViewportWidth(), enableContentReadySignal, isOverviewMode(mAccount));
+ mWebView.getViewportWidth(), enableContentReadySignal, isOverviewMode(mAccount),
+ prefs.shouldMungeTables(), prefs.shouldMungeImages());
}
private void renderSuperCollapsedBlock(int start, int end) {
@@ -814,6 +818,7 @@
mTemplates.appendMessageHtml(msg, expanded, safeForImages,
mWebView.screenPxToWebPx(headerPx), mWebView.screenPxToWebPx(footerPx));
+ timerMark("rendered message");
}
private String renderCollapsedHeaders(MessageCursor cursor,
@@ -960,6 +965,18 @@
"javascript:unblockImages(['%s']);", TextUtils.join("','", messageDomIds));
mWebView.loadUrl(url);
}
+
+ @Override
+ public boolean supportsMessageTransforms() {
+ return true;
+ }
+
+ @Override
+ public String getMessageTransforms(final Message msg) {
+ final String domId = mTemplates.getMessageDomId(msg);
+ return (domId == null) ? null : mMessageTransforms.get(domId);
+ }
+
// END message header callbacks
@Override
@@ -1025,7 +1042,7 @@
}
private static boolean isOverviewMode(Account acct) {
- return acct.settings.conversationViewMode == UIProvider.ConversationViewMode.OVERVIEW;
+ return acct.settings.isOverviewMode();
}
private void setupOverviewMode() {
@@ -1216,6 +1233,13 @@
return 0f;
}
}
+
+ @SuppressWarnings("unused")
+ @JavascriptInterface
+ public void onMessageTransform(String messageDomId, String transformText) {
+ LogUtils.i(LOG_TAG, "TRANSFORM: (%s) %s", messageDomId, transformText);
+ mMessageTransforms.put(messageDomId, transformText);
+ }
}
/**
@@ -1341,6 +1365,7 @@
}
} else {
LogUtils.i(LOG_TAG, "CONV RENDER: initial render. (%s)", this);
+ timerMark("message cursor load finished");
}
// if layout hasn't happened, delay render
@@ -1647,7 +1672,7 @@
mTemplates.appendMessageHtml(msgItem.getMessage(), true /* expanded */,
safeForImages, 0, 0);
final String html = mTemplates.endConversation(mBaseUri,
- mConversation.getBaseUri(mBaseUri), 0, 0, false, false);
+ mConversation.getBaseUri(mBaseUri), 0, 0, false, false, false, false);
mMessageView.loadDataWithBaseURL(mBaseUri, html, "text/html", "utf-8", null);
mMessageViewLoadStartMs = SystemClock.uptimeMillis();
diff --git a/src/com/android/mail/ui/FolderDisplayer.java b/src/com/android/mail/ui/FolderDisplayer.java
index 6f64511..05cfebe 100644
--- a/src/com/android/mail/ui/FolderDisplayer.java
+++ b/src/com/android/mail/ui/FolderDisplayer.java
@@ -21,6 +21,7 @@
import com.google.common.collect.Sets;
import android.content.Context;
+import android.net.Uri;
import com.android.mail.R;
import com.android.mail.providers.Conversation;
@@ -54,9 +55,9 @@
* @param foldersString string containing serialized folders to display.
* @param ignoreFolder (optional) folder to omit from the displayed set
*/
- public void loadConversationFolders(Conversation conv, Folder ignoreFolder) {
+ public void loadConversationFolders(Conversation conv, final Uri ignoreFolderUri) {
mFoldersSortedSet.clear();
- mFoldersSortedSet.addAll(conv.getRawFoldersForDisplay(ignoreFolder));
+ mFoldersSortedSet.addAll(conv.getRawFoldersForDisplay(ignoreFolderUri));
}
/**
diff --git a/src/com/android/mail/ui/FolderItemView.java b/src/com/android/mail/ui/FolderItemView.java
index 3e9d233..8235602 100644
--- a/src/com/android/mail/ui/FolderItemView.java
+++ b/src/com/android/mail/ui/FolderItemView.java
@@ -20,7 +20,9 @@
import android.widget.ImageView;
import android.widget.TextView;
+import com.android.mail.providers.Account;
import com.android.mail.providers.Folder;
+import com.android.mail.providers.UIProvider.FolderType;
import com.android.mail.utils.LogTag;
import com.android.mail.utils.LogUtils;
import com.android.mail.utils.Utils;
@@ -30,6 +32,7 @@
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;
@@ -58,6 +61,7 @@
private Folder mFolder;
private TextView mFolderTextView;
private TextView mUnreadCountTextView;
+ private TextView mUnseenCountTextView;
private DropHandler mDropHandler;
private ImageView mFolderParentIcon;
@@ -107,6 +111,7 @@
}
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();
@@ -118,14 +123,29 @@
mDropHandler = dropHandler;
mFolderTextView.setText(folder.name);
mFolderParentIcon.setVisibility(mFolder.hasChildren ? View.VISIBLE : View.GONE);
- setUnreadCount(Utils.getFolderUnreadDisplayCount(mFolder));
+ if (folder.type == FolderType.INBOX_SECTION && mFolder.unseenCount > 0) {
+ mUnreadCountTextView.setVisibility(View.GONE);
+ setUnseenCount(mFolder.getBackgroundColor(Color.BLACK), mFolder.unseenCount);
+ } else {
+ mUnseenCountTextView.setVisibility(View.GONE);
+ setUnreadCount(Utils.getFolderUnreadDisplayCount(mFolder));
+ }
+ }
+
+ public void bind(Account account, DropHandler dropHandler, int count) {
+ mFolder = null;
+ mDropHandler = dropHandler;
+ mFolderTextView.setText(account.name);
+ mFolderParentIcon.setVisibility(View.GONE);
+ mUnreadCountTextView.setVisibility(View.GONE);
+ setUnseenCount(Color.BLACK, 0);
+ setUnreadCount(count);
}
/**
* Sets the unread count, taking care to hide/show the textview if the count is zero/non-zero.
- * @param count
*/
- private final void setUnreadCount(int count) {
+ private void setUnreadCount(int count) {
mUnreadCountTextView.setVisibility(count > 0 ? View.VISIBLE : View.GONE);
if (count > 0) {
mUnreadCountTextView.setText(Utils.getUnreadCountString(getContext(), count));
@@ -133,6 +153,18 @@
}
/**
+ * Sets the unseen count, taking care to hide/show the textview if the count is zero/non-zero.
+ */
+ private void setUnseenCount(final int color, final int count) {
+ mUnseenCountTextView.setVisibility(count > 0 ? View.VISIBLE : View.GONE);
+ if (count > 0) {
+ mUnseenCountTextView.setBackgroundColor(color);
+ mUnseenCountTextView.setText(
+ getContext().getString(R.string.inbox_unseen_banner, count));
+ }
+ }
+
+ /**
* Used if we detect a problem with the unread count and want to force an override.
* @param count
*/
diff --git a/src/com/android/mail/ui/FolderListFragment.java b/src/com/android/mail/ui/FolderListFragment.java
index 8dd4665..fed9da4 100644
--- a/src/com/android/mail/ui/FolderListFragment.java
+++ b/src/com/android/mail/ui/FolderListFragment.java
@@ -34,12 +34,10 @@
import android.widget.ImageView;
import android.widget.ListAdapter;
import android.widget.ListView;
-import android.widget.TextView;
import com.android.mail.R;
-import com.android.mail.providers.Folder;
-import com.android.mail.providers.RecentFolderObserver;
-import com.android.mail.providers.UIProvider;
+import com.android.mail.adapter.DrawerItem;
+import com.android.mail.providers.*;
import com.android.mail.providers.UIProvider.FolderType;
import com.android.mail.utils.LogTag;
import com.android.mail.utils.LogUtils;
@@ -61,10 +59,14 @@
private Uri mFolderListUri;
/** True if you want a sectioned FolderList, false otherwise. */
private boolean mIsSectioned;
+ /** Is the current device using tablet UI (true if 2-pane, false if 1-pane) */
+ private boolean mIsTabletUI;
/** An {@link ArrayList} of {@link FolderType}s to exclude from displaying. */
private ArrayList<Integer> mExcludedFolderTypes;
- /** Callback into the parent */
- private FolderListSelectionListener mListener;
+ /** Object that changes folders on our behalf. */
+ private FolderListSelectionListener mFolderChanger;
+ /** Object that changes accounts on our behalf */
+ private AccountController mAccountChanger;
/** The currently selected folder (the folder being viewed). This is never null. */
private Uri mSelectedFolderUri = Uri.EMPTY;
@@ -77,16 +79,18 @@
private Folder mParentFolder;
private static final int FOLDER_LOADER_ID = 0;
- public static final int MODE_DEFAULT = 0;
- public static final int MODE_PICK = 1;
/** Key to store {@link #mParentFolder}. */
private static final String ARG_PARENT_FOLDER = "arg-parent-folder";
/** Key to store {@link #mFolderListUri}. */
private static final String ARG_FOLDER_URI = "arg-folder-list-uri";
/** Key to store {@link #mIsSectioned} */
private static final String ARG_IS_SECTIONED = "arg-is-sectioned";
+ /** Key to store {@link #mIsTabletUI} */
+ private static final String ARG_IS_TABLET_UI = "arg-is-tablet-ui";
/** Key to store {@link #mExcludedFolderTypes} */
private static final String ARG_EXCLUDED_FOLDER_TYPES = "arg-excluded-folder-types";
+ /** Should the {@link FolderListFragment} show less labels to begin with? */
+ private static final boolean ARE_ITEMS_COLLAPSED = true;
private static final String BUNDLE_LIST_STATE = "flf-list-state";
private static final String BUNDLE_SELECTED_FOLDER = "flf-selected-folder";
@@ -97,31 +101,24 @@
private View mEmptyView;
/** Observer to wait for changes to the current folder so we can change the selected folder */
private FolderObserver mFolderObserver = null;
+ /** Listen for account changes. */
+ private AccountObserver mAccountObserver = null;
+
+ /** Listen to changes to list of all accounts */
+ private AllAccountObserver mAllAccountObserver = null;
/**
- * Type of currently selected folder: {@link FolderListAdapter.Item#FOLDER_SYSTEM},
- * {@link FolderListAdapter.Item#FOLDER_RECENT} or {@link FolderListAdapter.Item#FOLDER_USER}.
+ * Type of currently selected folder: {@link DrawerItem#FOLDER_SYSTEM},
+ * {@link DrawerItem#FOLDER_RECENT} or {@link DrawerItem#FOLDER_USER}.
*/
- // Setting to NOT_A_FOLDER = leaving uninitialized.
- private int mSelectedFolderType = FolderListAdapter.Item.NOT_A_FOLDER;
+ // Setting to INERT_HEADER = leaving uninitialized.
+ private int mSelectedFolderType = DrawerItem.INERT_HEADER;
private Cursor mFutureData;
private ConversationListCallbacks mConversationListCallback;
+ /** The current account according to the controller */
+ private Account mCurrentAccount;
- /**
- * Listens to folder changes from the controller and updates state accordingly.
- */
- private final class FolderObserver extends DataSetObserver {
- @Override
- public void onChanged() {
- if (mActivity == null) {
- return;
- }
- final FolderController controller = mActivity.getFolderController();
- if (controller == null) {
- return;
- }
- setSelectedFolder(controller.getFolder());
- }
- }
+ /** List of all accounts currently known */
+ private Account[] mAllAccounts;
/**
* Constructor needs to be public to handle orientation changes and activity lifecycle events.
@@ -133,21 +130,24 @@
/**
* Creates a new instance of {@link ConversationListFragment}, initialized
* to display conversation list context.
- * @param isSectioned TODO(viki):
+ * @param isSectioned True if sections should be shown for folder list
+ * @param isTabletUI True if two-pane layout, false if not
*/
public static FolderListFragment newInstance(Folder parentFolder, Uri folderUri,
- boolean isSectioned) {
- return newInstance(parentFolder, folderUri, isSectioned, null);
+ boolean isSectioned, boolean isTabletUI) {
+ return newInstance(parentFolder, folderUri, isSectioned, null, isTabletUI);
}
/**
* Creates a new instance of {@link ConversationListFragment}, initialized
* to display conversation list context.
- * @param isSectioned TODO(viki):
+ * @param isSectioned True if sections should be shown for folder list
* @param excludedFolderTypes A list of {@link FolderType}s to exclude from displaying
+ * @param isTabletUI True if two-pane layout, false if not
*/
public static FolderListFragment newInstance(Folder parentFolder, Uri folderUri,
- boolean isSectioned, final ArrayList<Integer> excludedFolderTypes) {
+ boolean isSectioned, final ArrayList<Integer> excludedFolderTypes,
+ boolean isTabletUI) {
final FolderListFragment fragment = new FolderListFragment();
final Bundle args = new Bundle();
if (parentFolder != null) {
@@ -155,6 +155,7 @@
}
args.putString(ARG_FOLDER_URI, folderUri.toString());
args.putBoolean(ARG_IS_SECTIONED, isSectioned);
+ args.putBoolean(ARG_IS_TABLET_UI, isTabletUI);
if (excludedFolderTypes != null) {
args.putIntegerArrayList(ARG_EXCLUDED_FOLDER_TYPES, excludedFolderTypes);
}
@@ -179,14 +180,38 @@
mConversationListCallback = mActivity.getListHandler();
final FolderController controller = mActivity.getFolderController();
// Listen to folder changes in the future
- mFolderObserver = new FolderObserver();
+ mFolderObserver = new FolderObserver() {
+ @Override
+ public void onChanged(Folder newFolder) {
+ setSelectedFolder(newFolder);
+ }
+ };
if (controller != null) {
// Only register for selected folder updates if we have a controller.
- controller.registerFolderObserver(mFolderObserver);
- mCurrentFolderForUnreadCheck = controller.getFolder();
+ mCurrentFolderForUnreadCheck = mFolderObserver.initialize(controller);
+ }
+ final AccountController accountController = mActivity.getAccountController();
+ mAccountObserver = new AccountObserver() {
+ @Override
+ public void onChanged(Account newAccount) {
+ setSelectedAccount(newAccount);
+ }
+ };
+ if (accountController != null) {
+ // Current account and its observer.
+ mCurrentAccount = mAccountObserver.initialize(accountController);
+ // List of all accounts and its observer.
+ mAllAccountObserver = new AllAccountObserver(){
+ @Override
+ public void onChanged(Account[] allAccounts) {
+ mAllAccounts = allAccounts;
+ }
+ };
+ mAllAccounts = mAllAccountObserver.initialize(accountController);
+ mAccountChanger = accountController;
}
- mListener = mActivity.getFolderListSelectionListener();
+ mFolderChanger = mActivity.getFolderListSelectionListener();
if (mActivity.isFinishing()) {
// Activity is finishing, just bail.
return;
@@ -197,7 +222,12 @@
mCursorAdapter = new HierarchicalFolderListAdapter(null, mParentFolder);
selectedFolder = mActivity.getHierarchyFolder();
} else {
- mCursorAdapter = new FolderListAdapter(R.layout.folder_item, mIsSectioned);
+ // Initiate FLA with accounts and folders collapsed in the list
+ // The second param is for whether folders should be collapsed
+ // The third param is for whether accounts should be collapsed
+ mCursorAdapter = new FolderListAdapter(mIsSectioned,
+ !mIsTabletUI && ARE_ITEMS_COLLAPSED,
+ !mIsTabletUI && ARE_ITEMS_COLLAPSED);
selectedFolder = controller == null ? null : controller.getFolder();
}
// Is the selected folder fresher than the one we have restored from a bundle?
@@ -216,6 +246,7 @@
mFolderListUri = Uri.parse(args.getString(ARG_FOLDER_URI));
mParentFolder = (Folder) args.getParcelable(ARG_PARENT_FOLDER);
mIsSectioned = args.getBoolean(ARG_IS_SECTIONED);
+ mIsTabletUI = args.getBoolean(ARG_IS_TABLET_UI);
mExcludedFolderTypes = args.getIntegerArrayList(ARG_EXCLUDED_FOLDER_TYPES);
final View rootView = inflater.inflate(R.layout.folder_list, null);
mListView = (ListView) rootView.findViewById(android.R.id.list);
@@ -272,31 +303,54 @@
// Clear the adapter.
setListAdapter(null);
if (mFolderObserver != null) {
- FolderController controller = mActivity.getFolderController();
- if (controller != null) {
- controller.unregisterFolderObserver(mFolderObserver);
- mFolderObserver = null;
- }
+ mFolderObserver.unregisterAndDestroy();
+ mFolderObserver = null;
+ }
+ if (mAccountObserver != null) {
+ mAccountObserver.unregisterAndDestroy();
+ mAccountObserver = null;
+ }
+ if (mAllAccountObserver != null) {
+ mAllAccountObserver.unregisterAndDestroy();
+ mAllAccountObserver = null;
}
super.onDestroyView();
}
@Override
public void onListItemClick(ListView l, View v, int position, long id) {
- viewFolder(position);
+ viewFolderOrChangeAccount(position);
}
/**
* Display the conversation list from the folder at the position given.
- * @param position
+ * @param position a zero indexed position into the list.
*/
- private void viewFolder(int position) {
+ private void viewFolderOrChangeAccount(int position) {
final Object item = getListAdapter().getItem(position);
final Folder folder;
- if (item instanceof FolderListAdapter.Item) {
- final FolderListAdapter.Item folderItem = (FolderListAdapter.Item) item;
- folder = mCursorAdapter.getFullFolder(folderItem);
- mSelectedFolderType = folderItem.mFolderType;
+ if (item instanceof DrawerItem) {
+ final DrawerItem folderItem = (DrawerItem) item;
+ // Could be a folder, account, or expand block.
+ final int itemType = mCursorAdapter.getItemType(folderItem);
+ if (itemType == DrawerItem.VIEW_ACCOUNT) {
+ // Account, so switch.
+ folder = null;
+ final Account account = mCursorAdapter.getFullAccount(folderItem);
+ mAccountChanger.changeAccount(account);
+ } else if (itemType == DrawerItem.VIEW_FOLDER) {
+ // Folder type, so change folders only.
+ folder = mCursorAdapter.getFullFolder(folderItem);
+ mSelectedFolderType = folderItem.mFolderType;
+ } else {
+ // Block for expanding/contracting labels/accounts
+ folder = null;
+ if(!folderItem.mIsExpandForAccount) {
+ mCursorAdapter.toggleShowLessFolders();
+ } else {
+ mCursorAdapter.toggleShowLessAccounts();
+ }
+ }
} else if (item instanceof Folder) {
folder = (Folder) item;
} else {
@@ -309,10 +363,7 @@
// update its parent!
folder.parent = folder.equals(mParentFolder) ? null : mParentFolder;
// Go to the conversation list for this folder.
- mListener.onFolderSelected(folder);
- } else {
- LogUtils.d(LOG_TAG, "FolderListFragment unable to get a full fledged folder" +
- " to hand to the listener for position %d", position);
+ mFolderChanger.onFolderSelected(folder);
}
}
@@ -360,8 +411,21 @@
private interface FolderListFragmentCursorAdapter extends ListAdapter {
/** Update the folder list cursor with the cursor given here. */
void setCursor(Cursor cursor);
- /** Get the cursor associated with this adapter **/
- Folder getFullFolder(FolderListAdapter.Item item);
+ /** Toggles showing more accounts or less accounts. */
+ boolean toggleShowLessAccounts();
+ /** Toggles showing more folders or less. */
+ boolean toggleShowLessFolders();
+ /**
+ * Given an item, find the type of the item, which is {@link
+ * DrawerItem#VIEW_FOLDER}, {@link DrawerItem#VIEW_ACCOUNT} or
+ * {@link DrawerItem#VIEW_MORE}
+ * @return the type of the item.
+ */
+ int getItemType(DrawerItem item);
+ /** Get the folder associated with this item. **/
+ Folder getFullFolder(DrawerItem item);
+ /** Get the account associated with this item. **/
+ Account getFullAccount(DrawerItem item);
/** Remove all observers and destroy the object. */
void destroy();
/** Notifies the adapter that the data has changed. */
@@ -380,131 +444,30 @@
}
};
+ /** After given number of accounts, show "more" until expanded. */
+ private static final int MAX_ACCOUNTS = 2;
+ /** After the given number of labels, show "more" until expanded. */
+ private static final int MAX_FOLDERS = 7;
+
private final RecentFolderList mRecentFolders;
/** True if the list is sectioned, false otherwise */
private final boolean mIsSectioned;
- private final LayoutInflater mInflater;
/** All the items */
- private final List<Item> mItemList = new ArrayList<Item>();
+ private final List<DrawerItem> mItemList = new ArrayList<DrawerItem>();
/** Cursor into the folder list. This might be null. */
private Cursor mCursor = null;
-
- /** A union of either a folder or a resource string */
- private class Item {
- public int mPosition;
- public final Folder mFolder;
- public final int mResource;
- /** Either {@link #VIEW_FOLDER} or {@link #VIEW_HEADER} */
- public final int mType;
- /** A normal folder, also a child, if a parent is specified. */
- private static final int VIEW_FOLDER = 0;
- /** A text-label which serves as a header in sectioned lists. */
- private static final int VIEW_HEADER = 1;
-
- /**
- * Either {@link #FOLDER_SYSTEM}, {@link #FOLDER_RECENT} or {@link #FOLDER_USER} when
- * {@link #mType} is {@link #VIEW_FOLDER}, and {@link #NOT_A_FOLDER} otherwise.
- */
- public final int mFolderType;
- private static final int NOT_A_FOLDER = 0;
- private static final int FOLDER_SYSTEM = 1;
- private static final int FOLDER_RECENT = 2;
- private static final int FOLDER_USER = 3;
-
- /**
- * Create a folder item with the given type.
- * @param folder
- * @param folderType one of {@link #FOLDER_SYSTEM}, {@link #FOLDER_RECENT} or
- * {@link #FOLDER_USER}
- */
- private Item(Folder folder, int folderType, int cursorPosition) {
- mFolder = folder;
- mResource = -1;
- mType = VIEW_FOLDER;
- mFolderType = folderType;
- mPosition = cursorPosition;
- }
- /**
- * Create a header item with a string resource.
- * @param resource the string resource: R.string.all_folders_heading
- */
- private Item(int resource) {
- mFolder = null;
- mResource = resource;
- mType = VIEW_HEADER;
- mFolderType = NOT_A_FOLDER;
- }
-
- private final View getView(int position, View convertView, ViewGroup parent) {
- if (mType == VIEW_FOLDER) {
- return getFolderView(position, convertView, parent);
- } else {
- return getHeaderView(position, convertView, parent);
- }
- }
-
- /**
- * Returns a text divider between sections.
- * @param convertView
- * @param parent
- * @return a text header at the given position.
- */
- private final View getHeaderView(int position, View convertView, ViewGroup parent) {
- final TextView headerView;
- if (convertView != null) {
- headerView = (TextView) convertView;
- } else {
- headerView = (TextView) mInflater.inflate(
- R.layout.folder_list_header, parent, false);
- }
- headerView.setText(mResource);
- return headerView;
- }
-
- /**
- * Return a folder: either a parent folder or a normal (child or flat)
- * folder.
- * @param position
- * @param convertView
- * @param parent
- * @return a view showing a folder at the given position.
- */
- private final View getFolderView(int position, View convertView, ViewGroup parent) {
- final FolderItemView folderItemView;
- if (convertView != null) {
- folderItemView = (FolderItemView) convertView;
- } else {
- folderItemView =
- (FolderItemView) mInflater.inflate(R.layout.folder_item, null, false);
- }
- folderItemView.bind(mFolder, mActivity);
- if (mListView != null) {
- final boolean isSelected = (mFolderType == mSelectedFolderType)
- && mFolder.uri.equals(mSelectedFolderUri);
- mListView.setItemChecked(position, isSelected);
- // If this is the current folder, also check to verify that the unread count
- // matches what the action bar shows.
- if (isSelected && (mCurrentFolderForUnreadCheck != null)
- && mFolder.unreadCount != mCurrentFolderForUnreadCheck.unreadCount) {
- folderItemView.overrideUnreadCount(
- mCurrentFolderForUnreadCheck.unreadCount);
- }
- }
- Folder.setFolderBlockColor(mFolder, folderItemView.findViewById(R.id.color_block));
- Folder.setIcon(mFolder, (ImageView) folderItemView.findViewById(R.id.folder_box));
- return folderItemView;
- }
- }
+ /** Watcher for tracking and receiving unread counts for mail */
+ private FolderWatcher mFolderWatcher = null;
+ private boolean mShowLessFolders;
+ private boolean mShowLessAccounts;
/**
* Creates a {@link FolderListAdapter}.This is a flat folder list of all the folders for the
* given account.
- * @param layout
* @param isSectioned TODO(viki):
*/
- public FolderListAdapter(int layout, boolean isSectioned) {
+ public FolderListAdapter(boolean isSectioned, boolean showLess, boolean showLessAccounts) {
super();
- mInflater = LayoutInflater.from(mActivity.getActivityContext());
mIsSectioned = isSectioned;
final RecentFolderController controller = mActivity.getRecentFolderController();
if (controller != null && mIsSectioned) {
@@ -512,22 +475,48 @@
} else {
mRecentFolders = null;
}
+ mFolderWatcher = new FolderWatcher(mActivity, this);
+ mShowLessFolders = showLess;
+ mShowLessAccounts = showLessAccounts;
+ for (int i=0; i < mAllAccounts.length; i++) {
+ final Uri uri = mAllAccounts[i].settings.defaultInbox;
+ mFolderWatcher.startWatching(uri);
+ }
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
- return ((Item) getItem(position)).getView(position, convertView, parent);
+ final DrawerItem item = (DrawerItem) getItem(position);
+ final View view = item.getView(position, convertView, parent);
+ final int type = item.mType;
+ if (mListView!= null) {
+ final boolean isSelected =
+ item.isHighlighted(mCurrentFolderForUnreadCheck, mSelectedFolderType);
+ if (type == DrawerItem.VIEW_FOLDER) {
+ mListView.setItemChecked(position, isSelected);
+ }
+ // If this is the current folder, also check to verify that the unread count
+ // matches what the action bar shows.
+ if (type == DrawerItem.VIEW_FOLDER
+ && isSelected
+ && (mCurrentFolderForUnreadCheck != null)
+ && item.mFolder.unreadCount != mCurrentFolderForUnreadCheck.unreadCount) {
+ ((FolderItemView) view).overrideUnreadCount(
+ mCurrentFolderForUnreadCheck.unreadCount);
+ }
+ }
+ return view;
}
@Override
public int getViewTypeCount() {
- // Headers and folders
- return 2;
+ // Accounts, headers and folders
+ return DrawerItem.getViewTypes();
}
@Override
public int getItemViewType(int position) {
- return ((Item) getItem(position)).mType;
+ return ((DrawerItem) getItem(position)).mType;
}
@Override
@@ -537,22 +526,27 @@
@Override
public boolean isEnabled(int position) {
- // We disallow taps on headers
- return ((Item) getItem(position)).mType != Item.VIEW_HEADER;
+ final DrawerItem item = (DrawerItem) getItem(position);
+ return item.isItemEnabled(getCurrentAccountUri());
+
+ }
+
+ private Uri getCurrentAccountUri() {
+ return mCurrentAccount == null ? Uri.EMPTY : mCurrentAccount.uri;
}
@Override
public boolean areAllItemsEnabled() {
- // The headers are not enabled.
+ // The headers and current accounts are not enabled.
return false;
}
/**
* Returns all the recent folders from the list given here. Safe to call with a null list.
- * @param recentList
+ * @param recentList a list of all recently accessed folders.
* @return a valid list of folders, which are all recent folders.
*/
- private final List<Folder> getRecentFolders(RecentFolderList recentList) {
+ private List<Folder> getRecentFolders(RecentFolderList recentList) {
final List<Folder> folderList = new ArrayList<Folder>();
if (recentList == null) {
return folderList;
@@ -567,21 +561,84 @@
}
/**
- * Recalculates the system, recent and user label lists. Notifies that the data has changed.
- * This method modifies all the three lists on every single invocation.
+ * Toggle boolean for what folders are shown and which ones are
+ * hidden. Redraws list after toggling to show changes.
+ * @return true if folders are hidden, false if all are shown
+ */
+ @Override
+ public boolean toggleShowLessFolders() {
+ mShowLessFolders = !mShowLessFolders;
+ recalculateList();
+ return mShowLessFolders;
+ }
+
+ /**
+ * Toggle boolean for what accounts are shown and which ones are
+ * hidden. Redraws list after toggling to show changes.
+ * @return true if accounts are hidden, false if all are shown
+ */
+ @Override
+ public boolean toggleShowLessAccounts() {
+ mShowLessAccounts = !mShowLessAccounts;
+ recalculateList();
+ return mShowLessAccounts;
+ }
+
+ /**
+ * Responsible for verifying mCursor, adding collapsed view items
+ * when necessary, and notifying the data set has changed.
*/
private void recalculateList() {
if (mCursor == null || mCursor.isClosed() || mCursor.getCount() <= 0
|| !mCursor.moveToFirst()) {
return;
}
+ recalculateListFolders();
+ if(mShowLessFolders) {
+ mItemList.add(new DrawerItem(mActivity, R.string.folder_list_more, false));
+ }
+ // Ask the list to invalidate its views.
+ notifyDataSetChanged();
+ }
+
+ /**
+ * Recalculates the system, recent and user label lists.
+ * This method modifies all the three lists on every single invocation.
+ */
+ private void recalculateListFolders() {
mItemList.clear();
+ if (mAllAccounts != null) {
+ // Add the accounts at the top.
+ // TODO(shahrk): The logic here is messy and will be changed
+ // to properly add/reflect on LRU/MRU account
+ // changes similar to RecentFoldersList
+ if (mShowLessAccounts && mAllAccounts.length > MAX_ACCOUNTS) {
+ mItemList.add(new DrawerItem(
+ mActivity, R.string.folder_list_show_all_accounts, true));
+ final int unreadCount =
+ getFolderUnreadCount(mCurrentAccount.settings.defaultInbox);
+ mItemList.add(new DrawerItem(mActivity, mCurrentAccount, unreadCount));
+ } else {
+ Uri currentAccountUri = getCurrentAccountUri();
+ for (final Account c : mAllAccounts) {
+ if (!currentAccountUri.equals(c.uri)) {
+ final int otherAccountUnreadCount =
+ getFolderUnreadCount(c.settings.defaultInbox);
+ mItemList.add(new DrawerItem(mActivity, c, otherAccountUnreadCount));
+ }
+ }
+ final int accountUnreadCount =
+ getFolderUnreadCount(mCurrentAccount.settings.defaultInbox);
+ mItemList.add(new DrawerItem(mActivity, mCurrentAccount, accountUnreadCount));
+ }
+ }
if (!mIsSectioned) {
// Adapter for a flat list. Everything is a FOLDER_USER, and there are no headers.
do {
final Folder f = Folder.getDeficientDisplayOnlyFolder(mCursor);
if (mExcludedFolderTypes == null || !mExcludedFolderTypes.contains(f.type)) {
- mItemList.add(new Item(f, Item.FOLDER_USER, mCursor.getPosition()));
+ mItemList.add(new DrawerItem(mActivity, f, DrawerItem.FOLDER_USER,
+ mCursor.getPosition()));
}
} while (mCursor.moveToNext());
// Ask the list to invalidate its views.
@@ -589,16 +646,25 @@
return;
}
+ // Tracks how many folders have been added through the rest of the function
+ int folderCount = 0;
// Otherwise, this is an adapter for a sectioned list.
// First add all the system folders.
- final List<Item> userFolderList = new ArrayList<Item>();
+ final List<DrawerItem> userFolderList = new ArrayList<DrawerItem>();
do {
final Folder f = Folder.getDeficientDisplayOnlyFolder(mCursor);
if (mExcludedFolderTypes == null || !mExcludedFolderTypes.contains(f.type)) {
if (f.isProviderFolder()) {
- mItemList.add(new Item(f, Item.FOLDER_SYSTEM, mCursor.getPosition()));
+ mItemList.add(new DrawerItem(mActivity, f, DrawerItem.FOLDER_SYSTEM,
+ mCursor.getPosition()));
+ // Check if show less is enabled and we've passed max folders
+ folderCount++;
+ if(mShowLessFolders && folderCount >= MAX_FOLDERS) {
+ return;
+ }
} else {
- userFolderList.add(new Item(f, Item.FOLDER_USER, mCursor.getPosition()));
+ userFolderList.add(new DrawerItem(
+ mActivity, f, DrawerItem.FOLDER_USER, mCursor.getPosition()));
}
}
} while (mCursor.moveToNext());
@@ -616,20 +682,33 @@
}
if (recentFolderList.size() > 0) {
- mItemList.add(new Item(R.string.recent_folders_heading));
+ mItemList.add(new DrawerItem(mActivity, R.string.recent_folders_heading));
for (Folder f : recentFolderList) {
- mItemList.add(new Item(f, Item.FOLDER_RECENT, -1));
+ mItemList.add(new DrawerItem(mActivity, f, DrawerItem.FOLDER_RECENT, -1));
+ // Check if show less is enabled and we've passed max folders
+ folderCount++;
+ if(mShowLessFolders && folderCount >= MAX_FOLDERS) {
+ return;
+ }
}
}
// If there are user folders, add them and a header.
if (userFolderList.size() > 0) {
- mItemList.add(new Item(R.string.all_folders_heading));
- for (final Item i : userFolderList) {
+ mItemList.add(new DrawerItem(mActivity, R.string.all_folders_heading));
+ for (final DrawerItem i : userFolderList) {
mItemList.add(i);
+ // Check if show less is enabled and we've passed max folders
+ folderCount++;
+ if(mShowLessFolders && folderCount >= MAX_FOLDERS) {
+ return;
+ }
}
}
- // Ask the list to invalidate its views.
- notifyDataSetChanged();
+ }
+
+ private int getFolderUnreadCount(Uri folderUri) {
+ final Folder folder = mFolderWatcher.get(folderUri);
+ return folder != null ? folder.unreadCount : 0;
}
@Override
@@ -654,8 +733,13 @@
}
@Override
- public Folder getFullFolder(Item folderItem) {
- if (folderItem.mFolderType == Item.FOLDER_RECENT) {
+ public int getItemType(DrawerItem item) {
+ return item.mType;
+ }
+
+ @Override
+ public Folder getFullFolder(DrawerItem folderItem) {
+ if (folderItem.mFolderType == DrawerItem.FOLDER_RECENT) {
return folderItem.mFolder;
} else {
int pos = folderItem.mPosition;
@@ -671,6 +755,11 @@
}
}
}
+
+ @Override
+ public Account getFullAccount(DrawerItem item) {
+ return item.mAccount;
+ }
}
private class HierarchicalFolderListAdapter extends ArrayAdapter<Folder>
@@ -699,14 +788,14 @@
@Override
public int getItemViewType(int position) {
- Folder f = getItem(position);
+ final Folder f = getItem(position);
return f.uri.equals(mParentUri) ? PARENT : CHILD;
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
- FolderItemView folderItemView;
- Folder folder = getItem(position);
+ final FolderItemView folderItemView;
+ final Folder folder = getItem(position);
boolean isParent = folder.uri.equals(mParentUri);
if (convertView != null) {
folderItemView = (FolderItemView) convertView;
@@ -726,7 +815,8 @@
folderItemView.overrideUnreadCount(mCurrentFolderForUnreadCheck.unreadCount);
}
}
- Folder.setFolderBlockColor(folder, folderItemView.findViewById(R.id.folder_box));
+ Folder.setFolderBlockColor(folder, folderItemView.findViewById(R.id.color_block));
+ Folder.setIcon(folder, (ImageView) folderItemView.findViewById(R.id.folder_icon));
return folderItemView;
}
@@ -753,7 +843,13 @@
}
@Override
- public Folder getFullFolder(FolderListAdapter.Item folderItem) {
+ public int getItemType(DrawerItem item) {
+ // Always returns folders for now.
+ return DrawerItem.VIEW_FOLDER;
+ }
+
+ @Override
+ public Folder getFullFolder(DrawerItem folderItem) {
int pos = folderItem.mPosition;
if (mCursor == null || mCursor.isClosed()) {
// See if we have a cursor hanging out we can use
@@ -767,6 +863,21 @@
return null;
}
}
+
+ @Override
+ public Account getFullAccount(DrawerItem item) {
+ return null;
+ }
+
+ @Override
+ public boolean toggleShowLessFolders() {
+ return false;
+ }
+
+ @Override
+ public boolean toggleShowLessAccounts() {
+ return false;
+ }
}
/**
@@ -781,7 +892,7 @@
}
// If the current folder changed, we don't have a selected folder type anymore.
if (!folder.uri.equals(mSelectedFolderUri)) {
- mSelectedFolderType = FolderListAdapter.Item.NOT_A_FOLDER;
+ mSelectedFolderType = DrawerItem.INERT_HEADER;
}
mCurrentFolderForUnreadCheck = folder;
mSelectedFolderUri = folder.uri;
@@ -793,15 +904,23 @@
/**
* Sets the selected folder type safely.
- * @param folder
+ * @param folder folder to set to.
*/
private void setSelectedFolderType(Folder folder) {
// If it is set already, assume it is correct.
- if (mSelectedFolderType != FolderListAdapter.Item.NOT_A_FOLDER) {
+ if (mSelectedFolderType != DrawerItem.INERT_HEADER) {
return;
}
- mSelectedFolderType = folder.isProviderFolder() ? FolderListAdapter.Item.FOLDER_SYSTEM
- : FolderListAdapter.Item.FOLDER_USER;
+ mSelectedFolderType = folder.isProviderFolder() ? DrawerItem.FOLDER_SYSTEM
+ : DrawerItem.FOLDER_USER;
+ }
+
+ /**
+ * Sets the current account to the one provided here.
+ * @param account the current account to set to.
+ */
+ private void setSelectedAccount(Account account){
+ mCurrentAccount = account;
}
public interface FolderListSelectionListener {
diff --git a/src/com/android/mail/ui/FolderSelectionActivity.java b/src/com/android/mail/ui/FolderSelectionActivity.java
index c98b0b1..3bee9b0 100644
--- a/src/com/android/mail/ui/FolderSelectionActivity.java
+++ b/src/com/android/mail/ui/FolderSelectionActivity.java
@@ -110,7 +110,7 @@
private void createFolderListFragment(Folder parent, Uri uri) {
final FragmentTransaction fragmentTransaction = getFragmentManager().beginTransaction();
final Fragment fragment = FolderListFragment.newInstance(parent, uri, false,
- getExcludedFolderTypes());
+ getExcludedFolderTypes(), true);
fragmentTransaction.replace(R.id.content_pane, fragment);
fragmentTransaction.commitAllowingStateLoss();
}
@@ -155,7 +155,8 @@
* Create a widget for the specified account and folder
*/
protected void createWidget(int id, Account account, Folder selectedFolder) {
- WidgetProvider.updateWidget(this, id, account, selectedFolder);
+ WidgetProvider.updateWidget(this, id, account, selectedFolder.uri,
+ selectedFolder.conversationListUri, selectedFolder.name);
final Intent result = new Intent();
result.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, id);
setResult(RESULT_OK, result);
@@ -185,7 +186,7 @@
* account, calculate the human readable name of the folder and
* use it as the shortcut name, etc...
*/
- final Intent clickIntent = Utils.createViewFolderIntent(mSelectedFolder,
+ final Intent clickIntent = Utils.createViewFolderIntent(this, mSelectedFolder.uri,
mAccount);
resultIntent.putExtra(Intent.EXTRA_SHORTCUT_INTENT, clickIntent);
resultIntent.putExtra(Intent.EXTRA_SHORTCUT_ICON_RESOURCE,
@@ -370,4 +371,10 @@
// Unsupported
return true;
}
+
+ @Override
+ public ConversationListHelper getConversationListHelper() {
+ // Unsupported
+ return null;
+ }
}
diff --git a/src/com/android/mail/ui/FolderSelectionDialog.java b/src/com/android/mail/ui/FolderSelectionDialog.java
index 412cfa4..dc2a185 100644
--- a/src/com/android/mail/ui/FolderSelectionDialog.java
+++ b/src/com/android/mail/ui/FolderSelectionDialog.java
@@ -52,16 +52,17 @@
public static FolderSelectionDialog getInstance(final Context context, Account account,
final ConversationUpdater updater, Collection<Conversation> target, boolean isBatch,
- Folder currentFolder) {
+ Folder currentFolder, boolean isMoveTo) {
if (sDialogShown) {
return null;
}
- if (account.supportsCapability(UIProvider.AccountCapabilities.MULTIPLE_FOLDERS_PER_CONV)) {
- return new MultiFoldersSelectionDialog(
+ if (isMoveTo || !account.supportsCapability(
+ UIProvider.AccountCapabilities.MULTIPLE_FOLDERS_PER_CONV)) {
+ return new SingleFolderSelectionDialog(
context, account, updater, target, isBatch, currentFolder);
} else {
- return new SingleFolderSelectionDialog(
+ return new MultiFoldersSelectionDialog(
context, account, updater, target, isBatch, currentFolder);
}
}
diff --git a/src/com/android/mail/ui/FolderSelectorAdapter.java b/src/com/android/mail/ui/FolderSelectorAdapter.java
index e5faee0..d5d85af 100644
--- a/src/com/android/mail/ui/FolderSelectorAdapter.java
+++ b/src/com/android/mail/ui/FolderSelectorAdapter.java
@@ -211,9 +211,9 @@
if (view == null) {
view = mInflater.inflate(mLayout, parent, false);
}
- FolderRow row = (FolderRow) getItem(position);
- Folder folder = row.getFolder();
- String folderDisplay = !TextUtils.isEmpty(folder.hierarchicalDesc) ?
+ final FolderRow row = (FolderRow) getItem(position);
+ final Folder folder = row.getFolder();
+ final String folderDisplay = !TextUtils.isEmpty(folder.hierarchicalDesc) ?
folder.hierarchicalDesc : folder.name;
checkBox = (CompoundButton) view.findViewById(R.id.checkbox);
display = (TextView) view.findViewById(R.id.folder_name);
@@ -228,7 +228,7 @@
display.setText(folderDisplay);
}
colorBlock = view.findViewById(R.id.color_block);
- iconView = (ImageView) view.findViewById(R.id.folder_box);
+ iconView = (ImageView) view.findViewById(R.id.folder_icon);
Folder.setFolderBlockColor(folder, colorBlock);
Folder.setIcon(folder, iconView);
return view;
diff --git a/src/com/android/mail/ui/HtmlConversationTemplates.java b/src/com/android/mail/ui/HtmlConversationTemplates.java
index 3420d53..92340b3 100644
--- a/src/com/android/mail/ui/HtmlConversationTemplates.java
+++ b/src/com/android/mail/ui/HtmlConversationTemplates.java
@@ -181,7 +181,8 @@
}
public String endConversation(String docBaseUri, String conversationBaseUri, int viewWidth,
- int viewportWidth, boolean enableContentReadySignal, boolean normalizeMessageWidths) {
+ int viewportWidth, boolean enableContentReadySignal, boolean normalizeMessageWidths,
+ boolean enableMungeTables, boolean enableMungeImages) {
if (!mInProgress) {
throw new IllegalStateException("must call startConversation first");
}
@@ -190,7 +191,8 @@
append(sConversationLower, contentReadyClass, mContext.getString(R.string.hide_elided),
mContext.getString(R.string.show_elided), docBaseUri, conversationBaseUri,
- viewWidth, viewportWidth, enableContentReadySignal, normalizeMessageWidths);
+ viewWidth, viewportWidth, enableContentReadySignal, normalizeMessageWidths,
+ enableMungeTables, enableMungeImages);
mInProgress = false;
diff --git a/src/com/android/mail/ui/MailActionBarView.java b/src/com/android/mail/ui/MailActionBarView.java
index 943d4a1..9e94d33 100644
--- a/src/com/android/mail/ui/MailActionBarView.java
+++ b/src/com/android/mail/ui/MailActionBarView.java
@@ -17,6 +17,23 @@
package com.android.mail.ui;
+import com.android.mail.ConversationListContext;
+import com.android.mail.R;
+import com.android.mail.browse.SnippetTextView;
+import com.android.mail.providers.Account;
+import com.android.mail.providers.AccountObserver;
+import com.android.mail.providers.AllAccountObserver;
+import com.android.mail.providers.Conversation;
+import com.android.mail.providers.Folder;
+import com.android.mail.providers.FolderObserver;
+import com.android.mail.providers.SearchRecentSuggestionsProvider;
+import com.android.mail.providers.UIProvider;
+import com.android.mail.providers.UIProvider.AccountCapabilities;
+import com.android.mail.providers.UIProvider.FolderCapabilities;
+import com.android.mail.utils.LogTag;
+import com.android.mail.utils.LogUtils;
+import com.android.mail.utils.Utils;
+
import android.app.ActionBar;
import android.app.SearchManager;
import android.app.SearchableInfo;
@@ -24,7 +41,6 @@
import android.content.Context;
import android.content.res.Resources;
import android.database.Cursor;
-import android.database.DataSetObserver;
import android.os.Bundle;
import android.os.Handler;
import android.text.SpannableString;
@@ -40,22 +56,6 @@
import android.widget.SearchView.OnQueryTextListener;
import android.widget.SearchView.OnSuggestionListener;
-import com.android.mail.AccountSpinnerAdapter;
-import com.android.mail.ConversationListContext;
-import com.android.mail.R;
-import com.android.mail.browse.SnippetTextView;
-import com.android.mail.providers.Account;
-import com.android.mail.providers.AccountObserver;
-import com.android.mail.providers.Conversation;
-import com.android.mail.providers.Folder;
-import com.android.mail.providers.SearchRecentSuggestionsProvider;
-import com.android.mail.providers.UIProvider;
-import com.android.mail.providers.UIProvider.AccountCapabilities;
-import com.android.mail.providers.UIProvider.FolderCapabilities;
-import com.android.mail.utils.LogTag;
-import com.android.mail.utils.LogUtils;
-import com.android.mail.utils.Utils;
-
/**
* View to manage the various states of the Mail Action Bar.
* <p>
@@ -78,8 +78,6 @@
private int mMode = ViewMode.UNKNOWN;
private MenuItem mSearch;
- private AccountSpinnerAdapter mSpinnerAdapter;
- private MailSpinner mSpinner;
/**
* The account currently being shown
*/
@@ -96,12 +94,12 @@
private MenuItem mRefreshItem;
private MenuItem mFolderSettingsItem;
private View mRefreshActionView;
+ /** True if the current device is a tablet, false otherwise. */
+ private boolean mIsOnTablet;
private boolean mRefreshInProgress;
private Conversation mCurrentConversation;
- /**
- * True if we are running on tablet.
- */
- private final boolean mIsOnTablet;
+ private AllAccountObserver mAllAccountObserver;
+ private boolean mHaveMultipleAccounts = false;
public static final String LOG_TAG = LogTag.getLogTag();
@@ -113,13 +111,12 @@
}
};
private final boolean mShowConversationSubject;
- private DataSetObserver mFolderObserver;
+ private FolderObserver mFolderObserver;
private final AccountObserver mAccountObserver = new AccountObserver() {
@Override
public void onChanged(Account newAccount) {
updateAccount(newAccount);
- mSpinner.setAccount(mAccount);
}
};
@@ -165,9 +162,8 @@
return reports;
}
- /** True if the application has more than one account. */
- private boolean mHasManyAccounts;
-
+ // Created via view inflation.
+ @SuppressWarnings("unused")
public MailActionBarView(Context context) {
this(context, null);
}
@@ -183,18 +179,9 @@
mIsOnTablet = Utils.useTabletUI(r);
}
- // update the pager title strip as the Folder's conversation count changes
- private class FolderObserver extends DataSetObserver {
- @Override
- public void onChanged() {
- onFolderUpdated(mController.getFolder());
- }
- }
-
@Override
protected void onFinishInflate() {
super.onFinishInflate();
-
mSubjectView = (SnippetTextView) findViewById(R.id.conversation_subject);
}
@@ -274,37 +261,37 @@
return modeMenu[mMode];
}
- public void handleRestore(Bundle savedInstanceState) {
- }
-
- public void handleSaveInstanceState(Bundle outState) {
- }
-
public void initialize(ControllableActivity activity, ActivityController callback,
- ViewMode viewMode, ActionBar actionBar, RecentFolderList recentFolders) {
+ ActionBar actionBar) {
mActionBar = actionBar;
mController = callback;
mActivity = activity;
- mFolderObserver = new FolderObserver();
- mController.registerFolderObserver(mFolderObserver);
- // We don't want to include the "Show all folders" menu item on tablet devices
- final Context context = getContext();
- final boolean showAllFolders = !Utils.useTabletUI(context.getResources());
- mSpinnerAdapter = new AccountSpinnerAdapter(activity, context, showAllFolders);
- mSpinner = (MailSpinner) findViewById(R.id.account_spinner);
- mSpinner.setAdapter(mSpinnerAdapter);
- mSpinner.setController(mController);
+ mFolderObserver = new FolderObserver() {
+ @Override
+ public void onChanged(Folder newFolder) {
+ onFolderUpdated(newFolder);
+ }
+ };
+ mFolderObserver.initialize(mController);
+ mAllAccountObserver = new AllAccountObserver() {
+ @Override
+ public void onChanged(Account[] allAccounts) {
+ mHaveMultipleAccounts = (allAccounts.length > 1);
+ }
+ };
+ mAllAccountObserver.initialize(mController);
updateAccount(mAccountObserver.initialize(activity.getAccountController()));
}
private void updateAccount(Account account) {
mAccount = account;
if (mAccount != null) {
- ContentResolver resolver = mActivity.getActivityContext().getContentResolver();
- Bundle bundle = new Bundle(1);
+ final ContentResolver resolver = mActivity.getActivityContext().getContentResolver();
+ final Bundle bundle = new Bundle(1);
bundle.putParcelable(UIProvider.SetCurrentAccountColumns.ACCOUNT, account);
resolver.call(mAccount.uri, UIProvider.AccountCallMethods.SET_CURRENT_ACCOUNT,
mAccount.uri.toString(), bundle);
+ setFolderAndAccount();
}
}
@@ -316,72 +303,34 @@
}
/**
- * Sets the array of accounts to the value provided here.
- * @param accounts
- */
- public void setAccounts(Account[] accounts) {
- mSpinnerAdapter.setAccountArray(accounts);
- mHasManyAccounts = accounts.length > 1;
- enableDisableSpinnner();
- }
-
- /**
- * Changes the spinner state according to the following logic. On phone we always show recent
- * labels: pre-populating if necessary. So on phone we always want to enable the spinner.
- * On tablet, we enable the spinner when the Folder list is NOT visible: In conversation view,
- * and search conversation view.
- */
- private final void enableDisableSpinnner() {
- // Spinner is always shown on phone, and it is enabled by default, so don't mess with it.
- // By default the drawable is set in the XML layout, and the view is enabled.
- if (!mIsOnTablet) {
- return;
- }
- // We do not populate default recent folders on tablet, so we need to check that in the
- // conversation mode we have some recent folders. If we don't have any, then we should
- // disable the spinner.
- final boolean hasRecentsInConvView = ViewMode.isConversationMode(mMode)
- && mSpinnerAdapter.hasRecentFolders();
- // More than one account, OR has recent folders in conversation view.
- final boolean enabled = mHasManyAccounts || hasRecentsInConvView;
- mSpinner.changeEnabledState(enabled);
- }
-
- /**
* Called by the owner of the ActionBar to set the
* folder that is currently being displayed.
*/
public void setFolder(Folder folder) {
setRefreshInProgress(false);
mFolder = folder;
- mSpinner.setFolder(folder);
+ setFolderAndAccount();
mActivity.invalidateOptionsMenu();
}
public void onDestroy() {
if (mFolderObserver != null) {
- mController.unregisterFolderObserver(mFolderObserver);
+ mFolderObserver.unregisterAndDestroy();
mFolderObserver = null;
}
- mSpinnerAdapter.destroy();
+ if (mAllAccountObserver != null) {
+ mAllAccountObserver.unregisterAndDestroy();
+ mAllAccountObserver = null;
+ }
mAccountObserver.unregisterAndDestroy();
}
@Override
public void onViewModeChanged(int newMode) {
mMode = newMode;
- // Always update the options menu and redraw. This will read the new mode and redraw
- // the options menu.
- enableDisableSpinnner();
mActivity.invalidateOptionsMenu();
// Check if we are either on a phone, or in Conversation mode on tablet. For these, the
// recent folders is enabled.
- if (!mIsOnTablet || mMode == ViewMode.CONVERSATION) {
- mSpinnerAdapter.enableRecentFolders();
- } else {
- mSpinnerAdapter.disableRecentFolders();
- }
-
switch (mMode) {
case ViewMode.UNKNOWN:
closeSearchField();
@@ -416,7 +365,7 @@
* Close the search query entry field to avoid keyboard events, and to restore the actionbar
* to non-search mode.
*/
- private final void closeSearchField() {
+ private void closeSearchField() {
if (mSearch == null) {
return;
}
@@ -484,9 +433,9 @@
* Put the ActionBar in List navigation mode. This starts the spinner up if it is missing.
*/
private void showNavList() {
- setTitleModeFlags(ActionBar.DISPLAY_SHOW_CUSTOM);
- mSpinner.setVisibility(View.VISIBLE);
+ setTitleModeFlags(ActionBar.DISPLAY_SHOW_TITLE);
mSubjectView.setVisibility(View.GONE);
+ setFolderAndAccount();
}
/**
@@ -496,24 +445,23 @@
*/
protected void setSnippetMode() {
setTitleModeFlags(ActionBar.DISPLAY_SHOW_CUSTOM);
- mSpinner.setVisibility(View.GONE);
mSubjectView.setVisibility(View.VISIBLE);
-
mSubjectView.addOnLayoutChangeListener(mSnippetLayoutListener);
}
private void setFoldersMode() {
setTitleModeFlags(ActionBar.DISPLAY_SHOW_TITLE);
mActionBar.setTitle(R.string.folders);
- mActionBar.setSubtitle(mAccount.name);
+ if (mHaveMultipleAccounts) {
+ mActionBar.setSubtitle(mAccount.name);
+ }
}
/**
* Set the actionbar mode to empty: no title, no custom content.
*/
protected void setEmptyMode() {
- setTitleModeFlags(ActionBar.DISPLAY_SHOW_CUSTOM);
- mSpinner.setVisibility(View.GONE);
+ setTitleModeFlags(ActionBar.DISPLAY_SHOW_TITLE);
mSubjectView.setVisibility(View.GONE);
}
@@ -571,14 +519,6 @@
setRefreshInProgress(false);
}
- /**
- * Get the query text the user entered in the search widget, or empty string
- * if there is none.
- */
- public String getQuery() {
- return mSearchWidget != null ? mSearchWidget.getQuery().toString() : "";
- }
-
// Next two methods are called when search suggestions are clicked.
@Override
public boolean onSuggestionSelect(int position) {
@@ -630,10 +570,30 @@
}
/**
+ * Uses the current state to update the current folder {@link #mFolder} and the current
+ * account {@link #mAccount} shown in the actionbar.
+ */
+ private void setFolderAndAccount() {
+ // Check if we should be changing the actionbar at all, and back off if not.
+ final boolean isShowingFolderAndAccount =
+ (mActionBar != null && (mIsOnTablet || ViewMode.isListMode(mMode)));
+ if (!isShowingFolderAndAccount) {
+ return;
+ }
+ if (mFolder != null) {
+ mActionBar.setTitle(mFolder.name);
+ }
+ if (mAccount != null && mHaveMultipleAccounts) {
+ mActionBar.setSubtitle(mAccount.name);
+ }
+ // TODO(viki): Show unread count.
+ }
+
+ /**
* Notify that the folder has changed.
*/
public void onFolderUpdated(Folder folder) {
- mSpinner.onFolderUpdated(folder);
+ setFolderAndAccount();
if (folder.isSyncInProgress()) {
onRefreshStarted();
} else {
@@ -672,7 +632,6 @@
private void setTitleModeFlags(int enabledFlags) {
final int mask = ActionBar.DISPLAY_SHOW_TITLE
| ActionBar.DISPLAY_SHOW_CUSTOM | DISPLAY_TITLE_MULTIPLE_LINES;
-
mActionBar.setDisplayOptions(enabledFlags, mask);
}
@@ -745,8 +704,10 @@
Utils.setMenuItemVisibility(menu, R.id.remove_folder, !archiveVisible && mFolder != null
&& mFolder.supportsCapability(FolderCapabilities.CAN_ACCEPT_MOVED_MESSAGES)
&& !mFolder.isProviderFolder());
+ Utils.setMenuItemVisibility(menu, R.id.move_to, mFolder != null
+ && mFolder.supportsCapability(FolderCapabilities.ALLOWS_REMOVE_CONVERSATION));
final MenuItem removeFolder = menu.findItem(R.id.remove_folder);
- if (removeFolder != null) {
+ if (mFolder != null && removeFolder != null) {
removeFolder.setTitle(mActivity.getApplicationContext().getString(
R.string.remove_folder, mFolder.name));
}
diff --git a/src/com/android/mail/ui/MailActivity.java b/src/com/android/mail/ui/MailActivity.java
index 73a4db3..0bc75da 100644
--- a/src/com/android/mail/ui/MailActivity.java
+++ b/src/com/android/mail/ui/MailActivity.java
@@ -67,7 +67,7 @@
* to be static since the {@link ComposeActivity} needs to statically change the account name
* and have the NFC message changed accordingly.
*/
- private static String sAccountName = null;
+ protected static String sAccountName = null;
/**
* Create an NFC message (in the NDEF: Nfc Data Exchange Format) to instruct the recepient to
@@ -404,4 +404,10 @@
mAccessibilityEnabled = enabled;
mController.onAccessibilityStateChanged();
}
+
+ @Override
+ public ConversationListHelper getConversationListHelper() {
+ // Unsupported
+ return null;
+ }
}
diff --git a/src/com/android/mail/ui/MailSpinner.java b/src/com/android/mail/ui/MailSpinner.java
deleted file mode 100644
index d10606f..0000000
--- a/src/com/android/mail/ui/MailSpinner.java
+++ /dev/null
@@ -1,186 +0,0 @@
-/**
- * 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.ui;
-
-import android.content.Context;
-import android.util.AttributeSet;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.view.View.OnClickListener;
-import android.widget.AdapterView;
-import android.widget.AdapterView.OnItemClickListener;
-import android.widget.FrameLayout;
-import android.widget.LinearLayout;
-import android.widget.ListPopupWindow;
-import android.widget.TextView;
-
-import com.android.mail.AccountSpinnerAdapter;
-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;
-
-public class MailSpinner extends FrameLayout implements OnItemClickListener, OnClickListener {
- private static final String LOG_TAG = LogTag.getLogTag();
- private final ListPopupWindow mListPopupWindow;
- private AccountSpinnerAdapter mSpinnerAdapter;
- private Account mAccount;
- private Folder mFolder;
- private ActivityController mController;
- private final TextView mAccountName;
- private final TextView mFolderName;
- private final TextView mFolderCount;
- private final LinearLayout mContainer;
-
- public MailSpinner(Context context) {
- this(context, null);
- }
-
- public MailSpinner(Context context, AttributeSet attrs) {
- this(context, attrs, -1);
- }
-
- public MailSpinner(Context context, AttributeSet attrs, int defStyle) {
- super(context, attrs, defStyle);
- mListPopupWindow = new ListPopupWindow(context);
- mListPopupWindow.setOnItemClickListener(this);
- mListPopupWindow.setAnchorView(this);
- int dropDownWidth = context.getResources().getDimensionPixelSize(
- R.dimen.account_dropdown_dropdownwidth);
- mListPopupWindow.setWidth(dropDownWidth);
- mListPopupWindow.setModal(true);
- addView(LayoutInflater.from(getContext()).inflate(R.layout.account_switch_spinner_item,
- null));
- mAccountName = (TextView) findViewById(R.id.account_second);
- mFolderName = (TextView) findViewById(R.id.account_first);
- mFolderCount = (TextView) findViewById(R.id.account_unread);
- mContainer = (LinearLayout) findViewById(R.id.account_spinner_container);
- mContainer.setOnClickListener(this);
- }
-
- public void setAdapter(AccountSpinnerAdapter adapter) {
- mSpinnerAdapter = adapter;
- mListPopupWindow.setAdapter(mSpinnerAdapter);
- }
-
- /**
- * Changes the enabled state of the spinner. Not called {@link #setEnabled(boolean)} because
- * that is an existing method on views.
- *
- * @param enabled
- */
- public final void changeEnabledState(boolean enabled) {
- setEnabled(enabled);
- if (enabled) {
- mContainer.setBackgroundResource(R.drawable.spinner_ab_holo_light);
- } else {
- mContainer.setBackgroundDrawable(null);
- }
- }
-
- public void setAccount(Account account) {
- mAccount = account;
- if (mAccount != null) {
- mAccountName.setText(mAccount.name);
- }
- }
-
- public void setFolder(Folder f) {
- mFolder = f;
- if (mFolder != null) {
- mFolderName.setText(mFolder.name);
- int unreadCount = Utils.getFolderUnreadDisplayCount(mFolder);
- mFolderCount.setText(Utils.getUnreadCountString(getContext(), unreadCount));
- mFolderCount.setContentDescription(Utils.formatPlural(getContext(),
- R.plurals.unread_mail_count, unreadCount));
-
- if (mSpinnerAdapter != null) {
- // Update the spinner with this current folder, as it could change the recent items
- // that are shown.
- mSpinnerAdapter.setCurrentFolder(mFolder);
- }
- }
- }
-
- @Override
- public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
- LogUtils.d(LOG_TAG, "onNavigationItemSelected(%d, %d) called", position, id);
- final int type = mSpinnerAdapter.getItemViewType(position);
- boolean dismiss = false;
- switch (type) {
- case AccountSpinnerAdapter.TYPE_ACCOUNT:
- // Get the capabilities associated with this account.
- final Account account = (Account) mSpinnerAdapter.getItem(position);
- LogUtils.d(LOG_TAG, "onNavigationItemSelected: Selecting account: %s",
- account.name);
- if (mAccount.uri.equals(account.uri)) {
- // The selected account is the same, let's load the default inbox.
- mController.loadAccountInbox();
- } else {
- // Switching accounts.
- mController.onAccountChanged(account);
- }
- dismiss = true;
- break;
- case AccountSpinnerAdapter.TYPE_FOLDER:
- final Object folder = mSpinnerAdapter.getItem(position);
- assert (folder instanceof Folder);
- LogUtils.d(LOG_TAG, "onNavigationItemSelected: Selecting folder: %s",
- ((Folder)folder).name);
- mController.onFolderChanged((Folder) folder);
- dismiss = true;
- break;
- case AccountSpinnerAdapter.TYPE_ALL_FOLDERS:
- mController.showFolderList();
- dismiss = true;
- break;
- case AccountSpinnerAdapter.TYPE_HEADER:
- LogUtils.e(LOG_TAG, "MailSpinner.onItemClick(): Got unexpected click on header.");
- break;
- default:
- LogUtils.e(LOG_TAG, "MailSpinner.onItemClick(%d): Strange click ignored: type %d.",
- position, type);
- break;
- }
- if (dismiss) {
- mListPopupWindow.dismiss();
- }
- }
-
- public void setController(ActivityController controller) {
- mController = controller;
- }
-
- @Override
- public void onClick(View arg0) {
- if (isEnabled() && !mListPopupWindow.isShowing()) {
- mListPopupWindow.show();
- // Commit any leave behind items.
- mController.commitDestructiveActions(false);
- }
- }
-
- public void dismiss() {
- mListPopupWindow.dismiss();
- }
-
- public void onFolderUpdated(Folder folder) {
- mSpinnerAdapter.onFolderUpdated(folder);
- setFolder(folder);
- }
-}
diff --git a/src/com/android/mail/ui/MultiFoldersSelectionDialog.java b/src/com/android/mail/ui/MultiFoldersSelectionDialog.java
index 41b0778..cc95a48 100644
--- a/src/com/android/mail/ui/MultiFoldersSelectionDialog.java
+++ b/src/com/android/mail/ui/MultiFoldersSelectionDialog.java
@@ -30,7 +30,6 @@
import com.android.mail.ui.FolderSelectorAdapter.FolderRow;
import com.android.mail.utils.Utils;
-import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
@@ -152,7 +151,8 @@
switch (which) {
case DialogInterface.BUTTON_POSITIVE:
if (mUpdater != null) {
- mUpdater.assignFolder(mOperations.values(), mTarget, mBatch, true);
+ mUpdater.assignFolder(mOperations.values(), mTarget, mBatch,
+ true /* showUndo */, false /* isMoveTo */);
}
break;
case DialogInterface.BUTTON_NEGATIVE:
diff --git a/src/com/android/mail/ui/OnePaneController.java b/src/com/android/mail/ui/OnePaneController.java
index e084c5d..fc33b8d 100644
--- a/src/com/android/mail/ui/OnePaneController.java
+++ b/src/com/android/mail/ui/OnePaneController.java
@@ -23,12 +23,15 @@
import android.app.LoaderManager.LoaderCallbacks;
import android.net.Uri;
import android.os.Bundle;
+import android.support.v4.widget.DrawerLayout;
+import android.view.View;
+import android.view.ViewGroup;
+
import com.android.mail.ConversationListContext;
import com.android.mail.R;
import com.android.mail.providers.Account;
import com.android.mail.providers.Conversation;
import com.android.mail.providers.Folder;
-import com.android.mail.providers.Settings;
import com.android.mail.providers.UIProvider;
import com.android.mail.utils.LogUtils;
import com.android.mail.utils.Utils;
@@ -55,6 +58,8 @@
private static final String CONVERSATION_LIST_NEVER_SHOWN_KEY = "conversation-list-never-shown";
/** Key to store {@link #mInbox}. */
private final static String SAVED_INBOX_KEY = "m-inbox";
+ /** Set to true to show sections/recent inbox in drawer, false otherwise*/
+ private final static boolean SECTIONS_AND_RECENT_FOLDERS_ENABLED = true;
private static final int INVALID_ID = -1;
private boolean mConversationListVisible = false;
@@ -65,11 +70,9 @@
private Folder mInbox;
/** Whether a conversation list for this account has ever been shown.*/
private boolean mConversationListNeverShown = true;
+ private DrawerLayout mDrawerContainer;
+ private ViewGroup mDrawerPullout;
- /**
- * @param activity
- * @param viewMode
- */
public OnePaneController(MailActivity activity, ViewMode viewMode) {
super(activity, viewMode);
}
@@ -108,7 +111,8 @@
public void resetActionBarIcon() {
final int mode = mViewMode.getMode();
if (mode == ViewMode.CONVERSATION_LIST
- && inInbox(mAccount, mConvListContext)) {
+ && inInbox(mAccount, mConvListContext)
+ || mViewMode.isWaitingForSync()) {
mActionBarView.removeBackButton();
} else {
mActionBarView.setBackButton();
@@ -117,42 +121,43 @@
/**
* Returns true if the candidate URI is the URI for the default inbox for the given account.
- * @param candidate
- * @param account
- * @return
+ * @param candidate the URI to check
+ * @param account the account whose default Inbox the candidate might be
+ * @return true if the candidate is indeed the default inbox for the given account.
*/
- private static final boolean isDefaultInbox(Uri candidate, Account account) {
- if (candidate == null || account == null) {
- return false;
- }
- final Uri inboxUri = Settings.getDefaultInboxUri(account.settings);
- return candidate.equals(account.settings.defaultInbox);
+ private static boolean isDefaultInbox(Uri candidate, Account account) {
+ return (candidate != null && account != null)
+ && candidate.equals(account.settings.defaultInbox);
}
/**
* Returns true if the user is currently in the conversation list view, viewing the default
* inbox.
- * @return
+ * @return true if user is in conversation list mode, viewing the default inbox.
*/
private static boolean inInbox(final Account account, final ConversationListContext context) {
// If we don't have valid state, then we are not in the inbox.
- if (account == null || context == null || context.folder == null
- || account.settings == null) {
- return false;
- }
- return !ConversationListContext.isSearchResult(context)
+ return !(account == null || context == null || context.folder == null
+ || account.settings == null) && !ConversationListContext.isSearchResult(context)
&& isDefaultInbox(context.folder.uri, account);
}
+ /**
+ * On account change, carry out super implementation, load FolderListFragment
+ * into drawer (to avoid repetitive calls to replaceFragment).
+ */
@Override
- public void onAccountChanged(Account account) {
- super.onAccountChanged(account);
+ public void changeAccount(Account account) {
+ super.changeAccount(account);
mConversationListNeverShown = true;
+ resetAndLoadDrawer();
}
@Override
public boolean onCreate(Bundle savedInstanceState) {
mActivity.setContentView(R.layout.one_pane_activity);
+ mDrawerContainer = (DrawerLayout) mActivity.findViewById(R.id.drawer_container);
+ mDrawerPullout = (ViewGroup) mDrawerContainer.findViewById(R.id.drawer_pullout);
// The parent class sets the correct viewmode and starts the application off.
return super.onCreate(savedInstanceState);
}
@@ -162,10 +167,34 @@
return mConversationListVisible;
}
+ /**
+ * If drawer is open/visible (even partially), close it and replace the
+ * folder fragment upon closing the drawer. Otherwise, the drawer is closed
+ * and we can replace the folder list fragment without concern.
+ */
+ private void resetAndLoadDrawer() {
+ if(mDrawerContainer.isDrawerVisible(mDrawerPullout)) {
+ mDrawerContainer.setDrawerListener(new DrawerLayout.SimpleDrawerListener() {
+ @Override
+ public void onDrawerClosed(View drawerView) {
+ loadFolderList();
+ mDrawerContainer.setDrawerListener(null);
+ }
+ });
+ mDrawerContainer.closeDrawers();
+ } else {
+ loadFolderList();
+ }
+ }
+
@Override
public void onViewModeChanged(int newMode) {
super.onViewModeChanged(newMode);
+ // When view mode changes, we should wait for drawer to close and
+ // repopulate folders.
+ resetAndLoadDrawer();
+
// When entering conversation list mode, hide and clean up any currently visible
// conversation.
if (ViewMode.isListMode(newMode)) {
@@ -196,12 +225,12 @@
// Maintain fragment transaction history so we can get back to the
// fragment used to launch this list.
mLastConversationListTransactionId = replaceFragment(conversationListFragment,
- transition, TAG_CONVERSATION_LIST);
+ transition, TAG_CONVERSATION_LIST, R.id.content_pane);
} else {
// If going to the inbox, clear the folder list transaction history.
mInbox = listContext.folder;
mLastInboxConversationListTransactionId = replaceFragment(conversationListFragment,
- transition, TAG_CONVERSATION_LIST);
+ transition, TAG_CONVERSATION_LIST, R.id.content_pane);
mLastFolderListTransactionId = INVALID_ID;
// If we ever to to the inbox, we want to unset the transation id for any other
@@ -254,7 +283,8 @@
@Override
public void showWaitForInitialization() {
super.showWaitForInitialization();
- replaceFragment(getWaitFragment(), FragmentTransaction.TRANSIT_FRAGMENT_OPEN, TAG_WAIT);
+ replaceFragment(getWaitFragment(), FragmentTransaction.TRANSIT_FRAGMENT_OPEN, TAG_WAIT,
+ R.id.content_pane);
}
@Override
@@ -284,58 +314,105 @@
}
}
+ /**
+ * Loads the FolderListFragment into the drawer pullout FrameLayout.
+ * TODO(shahrk): Clean up and move out drawer calls if necessary
+ */
@Override
- public void showFolderList() {
+ public void loadFolderList() {
if (mAccount == null) {
LogUtils.e(LOG_TAG, "Null account in showFolderList");
return;
}
+
// Null out the currently selected folder; we have nothing selected the
// first time the user enters the folder list
+ // TODO(shahrk): Tweak the function call for nested folders?
setHierarchyFolder(null);
- mViewMode.enterFolderListMode();
- enableCabMode();
- mLastFolderListTransactionId = replaceFragment(
- FolderListFragment.newInstance(null, mAccount.folderListUri, false),
- FragmentTransaction.TRANSIT_FRAGMENT_OPEN, TAG_FOLDER_LIST);
- mConversationListVisible = false;
- onConversationVisibilityChanged(false);
- onConversationListVisibilityChanged(false);
+
+ /*
+ * TODO(shahrk): Drawer addition/support
+ * Take out view mode changes: mViewMode.enterFolderListMode(); enableCabMode();
+ * Adding this will enable back stack to labels: mLastFolderListTransactionId =
+ */
+ replaceFragment(
+ FolderListFragment.newInstance(null, mAccount.folderListUri,
+ SECTIONS_AND_RECENT_FOLDERS_ENABLED, false),
+ FragmentTransaction.TRANSIT_FRAGMENT_OPEN, TAG_FOLDER_LIST,
+ R.id.drawer_pullout);
+
+ /*
+ * TODO(shahrk): Move or remove this
+ * Don't make the list invisible as of right now:
+ * mConversationListVisible = false;
+ * onConversationVisibilityChanged(false);
+ * onConversationListVisibilityChanged(false);
+ */
+ }
+
+ /**
+ * Toggles the drawer pullout. If it was open (Fully extended), the
+ * drawer will be closed. Otherwise, the drawer will be opened. This should
+ * only be called when used with a toggle item. Other cases should be handled
+ * explicitly with just closeDrawers() or openDrawer(View drawerView);
+ */
+ @Override
+ protected void toggleFolderListState() {
+ if(mDrawerContainer.isDrawerOpen(mDrawerPullout)) {
+ mDrawerContainer.closeDrawers();
+ } else {
+ mDrawerContainer.openDrawer(mDrawerPullout);
+ }
+ return;
+ }
+
+ public void onFolderChanged(Folder folder) {
+ mDrawerContainer.closeDrawers();
+ super.onFolderChanged(folder);
}
/**
* Replace the content_pane with the fragment specified here. The tag is specified so that
* the {@link ActivityController} can look up the fragments through the
* {@link android.app.FragmentManager}.
- * @param fragment
- * @param transition
- * @param tag
+ * @param fragment the new fragment to put
+ * @param transition the transition to show
+ * @param tag a tag for the fragment manager.
+ * @param anchor ID of view to replace fragment in
* @return transaction ID returned when the transition is committed.
*/
- private int replaceFragment(Fragment fragment, int transition, String tag) {
+ private int replaceFragment(Fragment fragment, int transition, String tag, int anchor) {
FragmentTransaction fragmentTransaction = mActivity.getFragmentManager().beginTransaction();
fragmentTransaction.addToBackStack(null);
fragmentTransaction.setTransition(transition);
- fragmentTransaction.replace(R.id.content_pane, fragment, tag);
+ fragmentTransaction.replace(anchor, fragment, tag);
return fragmentTransaction.commitAllowingStateLoss();
}
/**
* Back works as follows:
- * 1) If the user is in the folder list view, go back
+ * 1) If the drawer is pulled out (Or mid-drag), close it - handled.
+ * 2) If the user is in the folder list view, go back
* to the account default inbox.
- * 2) If the user is in a conversation list
+ * 3) If the user is in a conversation list
* that is not the inbox AND:
* a) they got there by going through the folder
* list view, go back to the folder list view.
* b) they got there by using some other means (account dropdown), go back to the inbox.
- * 3) If the user is in a conversation, go back to the conversation list they were last in.
- * 4) If the user is in the conversation list for the default account inbox,
+ * 4) If the user is in a conversation, go back to the conversation list they were last in.
+ * 5) If the user is in the conversation list for the default account inbox,
* back exits the app.
*/
@Override
public boolean handleBackPress() {
final int mode = mViewMode.getMode();
+
+ if (mDrawerContainer.isDrawerVisible(mDrawerPullout)) {
+ mDrawerContainer.closeDrawers();
+ return true;
+ }
+
+ //TODO(shahrk): Remove the folder list standalone view
if (mode == ViewMode.FOLDER_LIST) {
final Folder hierarchyFolder = getHierarchyFolder();
final FolderListFragment folderListFragment = getFolderListFragment();
@@ -378,14 +455,15 @@
// showing this folder's children if we are not already
// looking at the child view for this folder.
mLastFolderListTransactionId = replaceFragment(FolderListFragment.newInstance(
- top, top.childFoldersListUri, false),
- FragmentTransaction.TRANSIT_FRAGMENT_OPEN, TAG_FOLDER_LIST);
+ top, top.childFoldersListUri, SECTIONS_AND_RECENT_FOLDERS_ENABLED, false),
+ FragmentTransaction.TRANSIT_FRAGMENT_OPEN, TAG_FOLDER_LIST,
+ R.id.content_pane);
// Show the up affordance when digging into child folders.
mActionBarView.setBackButton();
} else {
// Otherwise, clear the selected folder and go back to whatever the
// last folder list displayed was.
- showFolderList();
+ loadFolderList();
}
}
@@ -418,8 +496,9 @@
// showing this folder's children if we are not already
// looking at the child view for this folder.
mLastFolderListTransactionId = replaceFragment(
- FolderListFragment.newInstance(folder, folder.childFoldersListUri, false),
- FragmentTransaction.TRANSIT_FRAGMENT_OPEN, TAG_FOLDER_LIST);
+ FolderListFragment.newInstance(folder, folder.childFoldersListUri,
+ SECTIONS_AND_RECENT_FOLDERS_ENABLED, false),
+ FragmentTransaction.TRANSIT_FRAGMENT_OPEN, TAG_FOLDER_LIST, R.id.content_pane);
// Show the up affordance when digging into child folders.
mActionBarView.setBackButton();
} else {
@@ -529,7 +608,7 @@
convList != null ? convList.getAnimatedAdapter() : null),
0,
Utils.convertHtmlToPlainText
- (op.getDescription(mActivity.getActivityContext(), mFolder)),
+ (op.getDescription(mActivity.getActivityContext())),
true, /* showActionIcon */
R.string.undo,
true, /* replaceVisibleToast */
@@ -543,7 +622,7 @@
getUndoClickedListener(convList.getAnimatedAdapter()),
0,
Utils.convertHtmlToPlainText
- (op.getDescription(mActivity.getActivityContext(), mFolder)),
+ (op.getDescription(mActivity.getActivityContext())),
true, /* showActionIcon */
R.string.undo,
true, /* replaceVisibleToast */
diff --git a/src/com/android/mail/ui/RecentFolderList.java b/src/com/android/mail/ui/RecentFolderList.java
index 244924f..d05530e 100644
--- a/src/com/android/mail/ui/RecentFolderList.java
+++ b/src/com/android/mail/ui/RecentFolderList.java
@@ -18,10 +18,10 @@
import android.content.ContentValues;
import android.content.Context;
-import android.database.Cursor;
import android.net.Uri;
import android.os.AsyncTask;
+import com.android.mail.content.ObjectCursor;
import com.android.mail.providers.Account;
import com.android.mail.providers.AccountObserver;
import com.android.mail.providers.Folder;
@@ -96,8 +96,8 @@
/**
* Create a new asynchronous task to store the recent folder list. Both the account
* and the folder should be non-null.
- * @param account
- * @param folder
+ * @param account the current account for this folder.
+ * @param folder the folder which is to be stored.
*/
public StoreRecent(Account account, Folder folder) {
assert (account != null && folder != null);
@@ -123,7 +123,7 @@
/**
* Create a Recent Folder List from the given account. This will query the UIProvider to
* retrieve the RecentFolderList from persistent storage (if any).
- * @param context
+ * @param context the context for the activity
*/
public RecentFolderList(Context context) {
mFolderCache = new LruCache<String, RecentFolderListEntry>(
@@ -133,7 +133,7 @@
/**
* Initialize the {@link RecentFolderList} with a controllable activity.
- * @param activity
+ * @param activity the underlying activity
*/
public void initialize(ControllableActivity activity){
setCurrentAccount(mAccountObserver.initialize(activity.getAccountController()));
@@ -141,7 +141,8 @@
/**
* Change the current account. When a cursor over the recent folders for this account is
- * available, the client <b>must</b> call {@link #loadFromUiProvider(Cursor)} with the updated
+ * available, the client <b>must</b> call {@link
+ * #loadFromUiProvider(com.android.mail.content.ObjectCursor)} with the updated
* cursor. Till then, the recent account list will be empty.
* @param account the new current account
*/
@@ -158,7 +159,7 @@
* Load the account information from the UI provider given the cursor over the recent folders.
* @param c a cursor over the recent folders.
*/
- public void loadFromUiProvider(Cursor c) {
+ public void loadFromUiProvider(ObjectCursor<Folder> c) {
if (mAccount == null || c == null) {
LogUtils.e(TAG, "RecentFolderList.loadFromUiProvider: bad input. mAccount=%s,cursor=%s",
mAccount, c);
@@ -173,7 +174,7 @@
// This enables older values to fall off the LRU cache. Also, read all values, just in case
// there are duplicates in the cursor.
do {
- final Folder folder = new Folder(c);
+ final Folder folder = c.getModel();
final RecentFolderListEntry entry = new RecentFolderListEntry(folder);
mFolderCache.putElement(folder.uri.toString(), entry);
LogUtils.v(TAG, "Account %s, Recent: %s", mAccount.name, folder.name);
diff --git a/src/com/android/mail/ui/SecureConversationViewFragment.java b/src/com/android/mail/ui/SecureConversationViewFragment.java
index 61d3579..eebec55 100644
--- a/src/com/android/mail/ui/SecureConversationViewFragment.java
+++ b/src/com/android/mail/ui/SecureConversationViewFragment.java
@@ -21,9 +21,6 @@
import android.database.Cursor;
import android.net.Uri;
import android.os.Bundle;
-import android.text.Html;
-import android.text.SpannedString;
-import android.text.TextUtils;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
@@ -35,17 +32,16 @@
import com.android.mail.R;
import com.android.mail.browse.ConversationViewAdapter;
+import com.android.mail.browse.ConversationViewAdapter.MessageHeaderItem;
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.ConversationViewAdapter.MessageHeaderItem;
-import com.android.mail.browse.MessageCursor.ConversationMessage;
import com.android.mail.browse.MessageHeaderView.MessageHeaderViewCallbacks;
import com.android.mail.providers.Account;
import com.android.mail.providers.Conversation;
import com.android.mail.providers.Message;
-import com.android.mail.providers.UIProvider;
import com.android.mail.utils.LogTag;
import com.android.mail.utils.LogUtils;
@@ -61,7 +57,7 @@
private ConversationMessage mMessage;
private ScrollView mScrollView;
- private WebViewClient mWebViewClient = new AbstractConversationWebViewClient() {
+ private final WebViewClient mWebViewClient = new AbstractConversationWebViewClient() {
@Override
public void onPageFinished(WebView view, String url) {
if (isUserVisible()) {
@@ -99,17 +95,16 @@
super.onActivityCreated(savedInstanceState);
mConversationHeaderView.setCallbacks(this, this);
mConversationHeaderView.setFoldersVisible(false);
- final SubjectDisplayChanger sdc = mActivity.getSubjectDisplayChanger();
- if (sdc != null) {
- sdc.setSubject(mConversation.subject);
- }
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();
}
@@ -165,16 +160,6 @@
}
@Override
- public void setMessageSpacerHeight(MessageHeaderItem item, int newSpacerHeight) {
- // Do nothing.
- }
-
- @Override
- public void setMessageExpanded(MessageHeaderItem item, int newSpacerHeight) {
- // Do nothing.
- }
-
- @Override
public void onConversationViewHeaderHeightChange(int newHeight) {
// Do nothing.
}
@@ -185,12 +170,26 @@
return;
}
if (isUserVisible()) {
- mScrollView.scrollTo(0, 0);
onConversationSeen();
}
}
@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);
}
@@ -201,6 +200,16 @@
}
@Override
+ public boolean supportsMessageTransforms() {
+ return false;
+ }
+
+ @Override
+ public String getMessageTransforms(final Message msg) {
+ return null;
+ }
+
+ @Override
protected void onMessageCursorLoadFinished(Loader<Cursor> loader, MessageCursor newCursor,
MessageCursor oldCursor) {
// ignore cursors that are still loading results
@@ -212,7 +221,7 @@
// Activity is finishing, just bail.
return;
}
- renderMessageBodies(newCursor, mEnableContentReadySignal);
+ renderMessageBodies(newCursor);
}
/**
@@ -220,35 +229,23 @@
* blocks, a conversation header), and return an HTML document with spacer
* divs inserted for all overlays.
*/
- private void renderMessageBodies(MessageCursor messageCursor,
- boolean enableContentReadySignal) {
- final StringBuilder convHtml = new StringBuilder();
- String content;
- if (messageCursor.moveToFirst()) {
- content = messageCursor.getString(UIProvider.MESSAGE_BODY_HTML_COLUMN);
- if (TextUtils.isEmpty(content)) {
- content = messageCursor.getString(UIProvider.MESSAGE_BODY_TEXT_COLUMN);
- if (content != null) {
- content = Html.toHtml(new SpannedString(content));
- }
- }
- convHtml.append(content);
- mMessage = messageCursor.getMessage();
- mWebView.getSettings().setBlockNetworkImage(!mMessage.alwaysShowImages);
- mWebView.loadDataWithBaseURL(mBaseUri, convHtml.toString(), "text/html", "utf-8", null);
- ConversationViewAdapter mAdapter = new ConversationViewAdapter(mActivity, null, null,
- null, null, null, null, null, null);
- MessageHeaderItem item = mAdapter.newMessageHeaderItem(mMessage, true,
- mMessage.alwaysShowImages);
- mMessageHeaderView.initialize(mDateBuilder, this, mAddressCache);
- mMessageHeaderView.setExpandMode(MessageHeaderView.POPUP_MODE);
- mMessageHeaderView.bind(item, false);
- mMessageHeaderView.setMessageDetailsVisibility(View.VISIBLE);
- if (mMessage.hasAttachments) {
- mMessageFooterView.setVisibility(View.VISIBLE);
- mMessageFooterView.initialize(getLoaderManager(), getFragmentManager());
- mMessageFooterView.bind(item, false);
- }
+ private void renderMessageBodies(MessageCursor messageCursor) {
+ if (!messageCursor.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);
}
}
@@ -261,8 +258,4 @@
}
}
- @Override
- public void setMessageDetailsExpanded(MessageHeaderItem i, boolean expanded, int heightbefore) {
- // Do nothing.
- }
}
diff --git a/src/com/android/mail/ui/SingleFolderSelectionDialog.java b/src/com/android/mail/ui/SingleFolderSelectionDialog.java
index 7676732..f7c5771 100644
--- a/src/com/android/mail/ui/SingleFolderSelectionDialog.java
+++ b/src/com/android/mail/ui/SingleFolderSelectionDialog.java
@@ -71,11 +71,12 @@
// Currently, the number of adapters are assumed to match the
// number of headers in the string array.
mAdapter.addSection(new SystemFolderSelectorAdapter(context, foldersCursor,
- R.layout.single_folders_view, null, mCurrentFolder));
+ R.layout.single_folders_view, headers[0], mCurrentFolder));
// TODO(mindyp): we currently do not support frequently moved to
// folders, at headers[1]; need to define what that means.*/
- mAdapter.addSection(new HierarchicalFolderSelectorAdapter(context,
+ // TODO(pwestbro): determine if we need to call filterFolders
+ mAdapter.addSection(new UserFolderHierarchicalFolderSelectorAdapter(context,
AddableFolderSelectorAdapter.filterFolders(foldersCursor),
R.layout.single_folders_view, headers[2], mCurrentFolder));
mBuilder.setAdapter(mAdapter, SingleFolderSelectionDialog.this);
@@ -95,7 +96,7 @@
// Remove the current folder and add the new folder.
ops.add(new FolderOperation(mCurrentFolder, false));
ops.add(new FolderOperation(folder, true));
- mUpdater.assignFolder(ops, mTarget, mBatch, true);
+ mUpdater.assignFolder(ops, mTarget, mBatch, true /* showUndo */, true /* isMoveTo */);
mDialog.dismiss();
}
}
diff --git a/src/com/android/mail/ui/SuppressNotificationReceiver.java b/src/com/android/mail/ui/SuppressNotificationReceiver.java
index 7dba99b..a2d94e7 100644
--- a/src/com/android/mail/ui/SuppressNotificationReceiver.java
+++ b/src/com/android/mail/ui/SuppressNotificationReceiver.java
@@ -35,7 +35,7 @@
/**
- * A simple {@code BroadcastReceiver} which supresses new e-mail notifications for a given folder.
+ * A simple {@code BroadcastReceiver} which suppresses new e-mail notifications for a given folder.
*/
public class SuppressNotificationReceiver extends BroadcastReceiver {
private static final String LOG_TAG = LogTag.getLogTag();
@@ -45,7 +45,7 @@
private String mMimeType;
/**
- * Registers this receiver to supress the new mail notifications for a given folder so
+ * Registers this receiver to suppress the new mail notifications for a given folder so
* that other {@code BroadcastReceiver}s don't receive them.
*/
public boolean activate(Context context, AbstractActivityController controller) {
diff --git a/src/com/android/mail/ui/SwipeableListView.java b/src/com/android/mail/ui/SwipeableListView.java
index 4048119..82d4d33 100644
--- a/src/com/android/mail/ui/SwipeableListView.java
+++ b/src/com/android/mail/ui/SwipeableListView.java
@@ -196,7 +196,8 @@
final Context context = getContext();
final ToastBarOperation undoOp;
- undoOp = new ToastBarOperation(1, mSwipeAction, ToastBarOperation.UNDO, false);
+ undoOp = new ToastBarOperation(1, mSwipeAction, ToastBarOperation.UNDO, false /* batch */,
+ mFolder);
Conversation conv = target.getConversation();
target.getConversation().position = findConversation(target, conv);
final AnimatedAdapter adapter = getAnimatedAdapter();
diff --git a/src/com/android/mail/ui/ToastBarOperation.java b/src/com/android/mail/ui/ToastBarOperation.java
index b56a94d..b47879f 100644
--- a/src/com/android/mail/ui/ToastBarOperation.java
+++ b/src/com/android/mail/ui/ToastBarOperation.java
@@ -33,19 +33,25 @@
private final int mCount;
private final boolean mBatch;
private final int mType;
+ private final Folder mFolder;
/**
* Create a ToastBarOperation
*
* @param count Number of conversations this action would be applied to.
- * @param menuId res id identifying the menu item tapped; used to determine
- * what action was performed
+ * @param menuId res id identifying the menu item tapped; used to determine what action was
+ * performed
+ * @param operationFolder The {@link Folder} upon which the operation was run. This may be
+ * <code>null</code>, but is required in {@link #getDescription(Context)} for certain
+ * actions.
*/
- public ToastBarOperation(int count, int menuId, int type, boolean batch) {
+ public ToastBarOperation(int count, int menuId, int type, boolean batch,
+ final Folder operationFolder) {
mCount = count;
mAction = menuId;
mBatch = batch;
mType = type;
+ mFolder = operationFolder;
}
public int getType() {
@@ -56,11 +62,12 @@
return mBatch;
}
- public ToastBarOperation(Parcel in) {
+ public ToastBarOperation(final Parcel in, final ClassLoader loader) {
mCount = in.readInt();
mAction = in.readInt();
mBatch = in.readInt() != 0;
mType = in.readInt();
+ mFolder = in.readParcelable(loader);
}
@Override
@@ -69,35 +76,44 @@
dest.writeInt(mAction);
dest.writeInt(mBatch ? 1 : 0);
dest.writeInt(mType);
+ dest.writeParcelable(mFolder, 0);
}
- public static final Creator<ToastBarOperation> CREATOR = new Creator<ToastBarOperation>() {
+ public static final ClassLoaderCreator<ToastBarOperation> CREATOR =
+ new ClassLoaderCreator<ToastBarOperation>() {
@Override
- public ToastBarOperation createFromParcel(Parcel source) {
- return new ToastBarOperation(source);
+ public ToastBarOperation createFromParcel(final Parcel source) {
+ return createFromParcel(source, null);
}
@Override
- public ToastBarOperation[] newArray(int size) {
+ public ToastBarOperation[] newArray(final int size) {
return new ToastBarOperation[size];
}
+
+ @Override
+ public ToastBarOperation createFromParcel(final Parcel source, final ClassLoader loader) {
+ return new ToastBarOperation(source, loader);
+ }
};
/**
* Get a string description of the operation that will be performed
* when the user taps the undo bar.
*/
- public String getDescription(Context context, Folder folder) {
+ public String getDescription(Context context) {
int resId = -1;
switch (mAction) {
case R.id.delete:
resId = R.plurals.conversation_deleted;
break;
case R.id.remove_folder:
- return context.getString(R.string.folder_removed, folder.name);
+ return context.getString(R.string.folder_removed, mFolder.name);
case R.id.change_folder:
resId = R.plurals.conversation_folder_changed;
break;
+ case R.id.move_folder:
+ return context.getString(R.string.conversation_folder_moved, mFolder.name);
case R.id.archive:
resId = R.plurals.conversation_archived;
break;
diff --git a/src/com/android/mail/ui/TwoPaneController.java b/src/com/android/mail/ui/TwoPaneController.java
index 8e8be99..d022352 100644
--- a/src/com/android/mail/ui/TwoPaneController.java
+++ b/src/com/android/mail/ui/TwoPaneController.java
@@ -41,29 +41,22 @@
private TwoPaneLayout mLayout;
private Conversation mConversationToShow;
- /**
- * @param activity
- * @param viewMode
- */
public TwoPaneController(MailActivity activity, ViewMode viewMode) {
super(activity, viewMode);
}
/**
* Display the conversation list fragment.
- * @param show
*/
- private void initializeConversationListFragment(boolean show) {
- if (show) {
- if (Intent.ACTION_SEARCH.equals(mActivity.getIntent().getAction())) {
- if (shouldEnterSearchConvMode()) {
- mViewMode.enterSearchResultsConversationMode();
- } else {
- mViewMode.enterSearchResultsListMode();
- }
+ private void initializeConversationListFragment() {
+ if (Intent.ACTION_SEARCH.equals(mActivity.getIntent().getAction())) {
+ if (shouldEnterSearchConvMode()) {
+ mViewMode.enterSearchResultsConversationMode();
} else {
- mViewMode.enterConversationListMode();
+ mViewMode.enterSearchResultsListMode();
}
+ } else {
+ mViewMode.enterConversationListMode();
}
renderConversationList();
}
@@ -112,7 +105,8 @@
private void createFolderListFragment(Folder parent, Uri uri) {
setHierarchyFolder(parent);
// Create a sectioned FolderListFragment.
- FolderListFragment folderListFragment = FolderListFragment.newInstance(parent, uri, true);
+ FolderListFragment folderListFragment = FolderListFragment.newInstance(parent, uri, true,
+ true);
FragmentTransaction fragmentTransaction = mActivity.getFragmentManager().beginTransaction();
if (Utils.useFolderListFragmentTransition(mActivity.getActivityContext())) {
fragmentTransaction.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_OPEN);
@@ -134,11 +128,11 @@
@Override
public void showConversationList(ConversationListContext listContext) {
super.showConversationList(listContext);
- initializeConversationListFragment(true);
+ initializeConversationListFragment();
}
@Override
- public void showFolderList() {
+ public void loadFolderList() {
// On two-pane layouts, showing the folder list takes you to the top level of the
// application, which is the same as pressing the Up button
handleUpPress();
@@ -158,8 +152,7 @@
// notifications upon animation completion:
// (onConversationVisibilityChanged, onConversationListVisibilityChanged)
mViewMode.addListener(mLayout);
- final boolean isParentInitialized = super.onCreate(savedState);
- return isParentInitialized;
+ return super.onCreate(savedState);
}
@Override
@@ -171,8 +164,8 @@
}
@Override
- public void onAccountChanged(Account account) {
- super.onAccountChanged(account);
+ public void changeAccount(Account account) {
+ super.changeAccount(account);
renderFolderList();
}
@@ -196,11 +189,10 @@
createFolderListFragment(folder, folder.childFoldersListUri);
// Show the up affordance when digging into child folders.
mActionBarView.setBackButton();
- super.onFolderSelected(folder);
} else {
setHierarchyFolder(folder);
- super.onFolderSelected(folder);
}
+ super.onFolderSelected(folder);
}
private void goUpFolderHierarchy(Folder current) {
@@ -253,7 +245,7 @@
public void resetActionBarIcon() {
// On two-pane, the back button is only removed in the conversation list mode, and shown
// for every other condition.
- if (mViewMode.isListMode()) {
+ if (mViewMode.isListMode() || mViewMode.isWaitingForSync()) {
mActionBarView.removeBackButton();
} else {
mActionBarView.setBackButton();
@@ -263,7 +255,7 @@
/**
* Enable or disable the CAB mode based on the visibility of the conversation list fragment.
*/
- private final void enableOrDisableCab() {
+ private void enableOrDisableCab() {
if (mLayout.isConversationListCollapsed()) {
disableCabMode();
} else {
@@ -296,7 +288,7 @@
mConversationToShow = conversation;
final int mode = mViewMode.getMode();
- boolean changedMode = false;
+ final boolean changedMode;
if (mode == ViewMode.SEARCH_RESULTS_LIST || mode == ViewMode.SEARCH_RESULTS_CONVERSATION) {
changedMode = mViewMode.enterSearchResultsConversationMode();
} else {
@@ -482,7 +474,7 @@
getUndoClickedListener(convList.getAnimatedAdapter()),
0,
Utils.convertHtmlToPlainText
- (op.getDescription(mActivity.getActivityContext(), mFolder)),
+ (op.getDescription(mActivity.getActivityContext())),
true, /* showActionIcon */
R.string.undo,
true, /* replaceVisibleToast */
@@ -494,7 +486,7 @@
if (convList != null) {
mToastBar.show(getUndoClickedListener(convList.getAnimatedAdapter()), 0,
Utils.convertHtmlToPlainText
- (op.getDescription(mActivity.getActivityContext(), mFolder)),
+ (op.getDescription(mActivity.getActivityContext())),
true, /* showActionIcon */
R.string.undo, true, /* replaceVisibleToast */
op);
diff --git a/src/com/android/mail/ui/UserFolderHierarchicalFolderSelectorAdapter.java b/src/com/android/mail/ui/UserFolderHierarchicalFolderSelectorAdapter.java
new file mode 100644
index 0000000..9a601cf
--- /dev/null
+++ b/src/com/android/mail/ui/UserFolderHierarchicalFolderSelectorAdapter.java
@@ -0,0 +1,42 @@
+/*******************************************************************************
+ * 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.content.Context;
+import android.database.Cursor;
+import com.android.mail.providers.Folder;
+
+import java.util.Set;
+
+public class UserFolderHierarchicalFolderSelectorAdapter extends HierarchicalFolderSelectorAdapter {
+ public UserFolderHierarchicalFolderSelectorAdapter(Context context, Cursor folders, int layout,
+ String header, Folder excludedFolder) {
+ super(context, folders, layout, header, excludedFolder);
+ }
+
+ /**
+ * Return whether the supplied folder meets the requirements to be displayed
+ * in the folder list.
+ */
+ @Override
+ protected boolean meetsRequirements(Folder folder) {
+ if (folder.isProviderFolder()) {
+ return false;
+ }
+ return super.meetsRequirements(folder);
+ }
+}
diff --git a/src/com/android/mail/ui/ViewMode.java b/src/com/android/mail/ui/ViewMode.java
index 220a35b..9644fdc 100644
--- a/src/com/android/mail/ui/ViewMode.java
+++ b/src/com/android/mail/ui/ViewMode.java
@@ -182,6 +182,10 @@
return mode == CONVERSATION || mode == SEARCH_RESULTS_CONVERSATION;
}
+ public boolean isWaitingForSync() {
+ return mMode == WAITING_FOR_ACCOUNT_INITIALIZATION;
+ }
+
/**
* Restoring from a saved state restores only the mode. It does not restore the listeners of
* this object.
diff --git a/src/com/android/mail/utils/AttachmentUtils.java b/src/com/android/mail/utils/AttachmentUtils.java
index 1f776df..3735d63 100644
--- a/src/com/android/mail/utils/AttachmentUtils.java
+++ b/src/com/android/mail/utils/AttachmentUtils.java
@@ -22,18 +22,38 @@
import android.net.ConnectivityManager;
import android.net.NetworkInfo;
import android.net.Uri;
+import android.os.Bundle;
+import android.os.ParcelFileDescriptor;
+import android.os.SystemClock;
import android.text.TextUtils;
import com.android.mail.R;
import com.android.mail.providers.Attachment;
+import com.google.common.collect.ImmutableMap;
+
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.InputStream;
+import java.io.IOException;
+import java.io.File;
import java.text.DecimalFormat;
+import java.text.SimpleDateFormat;
+import java.util.Date;
import java.util.Map;
public class AttachmentUtils {
+ private static final String LOG_TAG = LogTag.getLogTag();
+
private static final int KILO = 1024;
private static final int MEGA = KILO * KILO;
+ /** Any IO reads should be limited to this timeout */
+ private static final long READ_TIMEOUT = 3600 * 1000;
+
+ private static final float MIN_CACHE_THRESHOLD = 0.25f;
+ private static final int MIN_CACHE_AVAILABLE_SPACE_BYTES = 100 * 1024 * 1024;
+
/**
* Singleton map of MIME->friendly description
* @see #getMimeTypeDisplayName(Context, String)
@@ -135,6 +155,105 @@
}
/**
+ * Cache the file specified by the given attachment. This will attempt to use any
+ * {@link ParcelFileDescriptor} in the Bundle parameter
+ * @param context
+ * @param attachment Attachment to be cached
+ * @param attachmentFds optional {@link Bundle} containing {@link ParcelFileDescriptor} if the
+ * caller has opened the files
+ * @return String file path for the cached attachment
+ */
+ // TODO(pwestbro): Once the attachment has a field for the cached path, this method should be
+ // changed to update the attachment, and return a boolean indicating that the attachment has
+ // been cached.
+ public static String cacheAttachmentUri(Context context, Attachment attachment,
+ Bundle attachmentFds) {
+ final File cacheDir = context.getCacheDir();
+
+ final long totalSpace = cacheDir.getTotalSpace();
+ if (attachment.size > 0) {
+ final long usableSpace = cacheDir.getUsableSpace() - attachment.size;
+ if (isLowSpace(totalSpace, usableSpace)) {
+ LogUtils.w(LOG_TAG, "Low memory (%d/%d). Can't cache attachment %s",
+ usableSpace, totalSpace, attachment);
+ return null;
+ }
+ }
+ InputStream inputStream = null;
+ FileOutputStream outputStream = null;
+ File file = null;
+ try {
+ final SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd-kk:mm:ss");
+ file = File.createTempFile(dateFormat.format(new Date()), ".attachment", cacheDir);
+ final ParcelFileDescriptor fileDescriptor = attachmentFds != null
+ && attachment.contentUri != null ? (ParcelFileDescriptor) attachmentFds
+ .getParcelable(attachment.contentUri.toString())
+ : null;
+ if (fileDescriptor != null) {
+ // Get the input stream from the file descriptor
+ inputStream = new FileInputStream(fileDescriptor.getFileDescriptor());
+ } else {
+ // Attempt to open the file
+ inputStream = context.getContentResolver().openInputStream(attachment.contentUri);
+ }
+ outputStream = new FileOutputStream(file);
+ final long now = SystemClock.elapsedRealtime();
+ final byte[] bytes = new byte[1024];
+ while (true) {
+ int len = inputStream.read(bytes);
+ if (len <= 0) {
+ break;
+ }
+ outputStream.write(bytes, 0, len);
+ if (SystemClock.elapsedRealtime() - now > READ_TIMEOUT) {
+ throw new IOException("Timed out reading attachment data");
+ }
+ }
+ outputStream.flush();
+ String cachedFileUri = file.getAbsolutePath();
+ LogUtils.d(LOG_TAG, "Cached %s to %s", attachment.contentUri, cachedFileUri);
+
+ final long usableSpace = cacheDir.getUsableSpace();
+ if (isLowSpace(totalSpace, usableSpace)) {
+ file.delete();
+ LogUtils.w(LOG_TAG, "Low memory (%d/%d). Can't cache attachment %s",
+ usableSpace, totalSpace, attachment);
+ cachedFileUri = null;
+ }
+
+ return cachedFileUri;
+ } catch (IOException e) {
+ // Catch any exception here to allow for unexpected failures during caching se we don't
+ // leave app in inconsistent state as we call this method outside of a transaction for
+ // performance reasons.
+ LogUtils.e(LOG_TAG, e, "Failed to cache attachment %s", attachment);
+ if (file != null) {
+ file.delete();
+ }
+ return null;
+ } finally {
+ try {
+ if (inputStream != null) {
+ inputStream.close();
+ }
+ if (outputStream != null) {
+ outputStream.close();
+ }
+ } catch (IOException e) {
+ LogUtils.w(LOG_TAG, e, "Failed to close stream");
+ }
+ }
+ }
+
+ private static boolean isLowSpace(long totalSpace, long usableSpace) {
+ // For caching attachments we want to enable caching if there is
+ // more than 100MB available, or if 25% of total space is free on devices
+ // where the cache partition is < 400MB.
+ return usableSpace <
+ Math.min(totalSpace * MIN_CACHE_THRESHOLD, MIN_CACHE_AVAILABLE_SPACE_BYTES);
+ }
+
+ /**
* Checks if the attachment can be downloaded with the current network
* connection.
*
diff --git a/src/com/android/mail/utils/LogUtils.java b/src/com/android/mail/utils/LogUtils.java
index 6176ec8..9ec466b 100644
--- a/src/com/android/mail/utils/LogUtils.java
+++ b/src/com/android/mail/utils/LogUtils.java
@@ -60,7 +60,7 @@
* Used to enable/disable logging that we don't want included in
* production releases.
*/
- private static final int MAX_ENABLED_LOG_LEVEL = DEBUG;
+ private static final int MAX_ENABLED_LOG_LEVEL = VERBOSE;
private static Boolean sDebugLoggingEnabledForTests = null;
diff --git a/src/com/android/mail/utils/NotificationActionUtils.java b/src/com/android/mail/utils/NotificationActionUtils.java
index 617959b..2136620 100644
--- a/src/com/android/mail/utils/NotificationActionUtils.java
+++ b/src/com/android/mail/utils/NotificationActionUtils.java
@@ -30,7 +30,6 @@
import android.os.SystemClock;
import android.support.v4.app.NotificationCompat;
import android.support.v4.app.TaskStackBuilder;
-import android.text.TextUtils;
import android.widget.RemoteViews;
import com.android.mail.MailIntentService;
@@ -47,7 +46,7 @@
import com.google.common.collect.ImmutableMap;
import java.util.ArrayList;
-import java.util.HashSet;
+import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Set;
@@ -76,7 +75,8 @@
@Override
public boolean shouldDisplayPrimary(final Folder folder,
final Conversation conversation, final Message message) {
- return folder == null || folder.type == FolderType.INBOX;
+ return folder == null || folder.type == FolderType.INBOX
+ || folder.type == FolderType.INBOX_SECTION;
}
}),
DELETE("delete", true, R.drawable.ic_menu_delete_holo_dark,
@@ -181,31 +181,22 @@
/**
* Adds the appropriate notification actions to the specified
- * {@link android.app.Notification.Builder}
+ * {@link android.support.v4.app.NotificationCompat.Builder}
*
* @param notificationIntent The {@link Intent} used when the notification is clicked
* @param when The value passed into {@link android.app.Notification.Builder#setWhen(long)}.
* This is used for maintaining notification ordering with the undo bar
- * @param notificationActionsString A comma-delimited {@link String} of the actions to display
+ * @param notificationActions A {@link Set} set of the actions to display
*/
public static void addNotificationActions(final Context context,
- final Intent notificationIntent, final Notification.Builder notification,
+ final Intent notificationIntent, final NotificationCompat.Builder notification,
final Account account, final Conversation conversation, final Message message,
final Folder folder, final int notificationId, final long when,
- final String notificationActionsString) {
- final String[] notificationActionsValueArray =
- TextUtils.isEmpty(notificationActionsString) ? new String[0]
- : notificationActionsString.split(",");
- final List<NotificationActionType> notificationActions =
- new ArrayList<NotificationActionType>(notificationActionsValueArray.length);
- for (int i = 0; i < notificationActionsValueArray.length; i++) {
- notificationActions.add(
- NotificationActionType.getActionType(notificationActionsValueArray[i]));
- }
+ final Set<String> notificationActions) {
+ final List<NotificationActionType> sortedActions =
+ getSortedNotificationActions(folder, notificationActions);
- sortNotificationActions(folder, notificationActions);
-
- for (final NotificationActionType notificationAction : notificationActions) {
+ for (final NotificationActionType notificationAction : sortedActions) {
notification.addAction(notificationAction.getActionIconResId(
folder, conversation, message), context.getString(notificationAction
.getDisplayStringResId(folder, conversation, message)),
@@ -218,15 +209,20 @@
* Sorts the notification actions into the appropriate order, based on current label
*
* @param folder The {@link Folder} being notified
- * @param notificationActions The actions to sort
+ * @param notificationActionStrings The action strings to sort
*/
- private static void sortNotificationActions(
- final Folder folder, final List<NotificationActionType> notificationActions) {
- final Set<NotificationActionType> tempActions =
- new HashSet<NotificationActionType>(notificationActions);
- notificationActions.clear();
+ private static List<NotificationActionType> getSortedNotificationActions(
+ final Folder folder, final Collection<String> notificationActionStrings) {
+ final List<NotificationActionType> unsortedActions =
+ new ArrayList<NotificationActionType>(notificationActionStrings.size());
+ for (final String action : notificationActionStrings) {
+ unsortedActions.add(NotificationActionType.getActionType(action));
+ }
- if (folder.type == FolderType.INBOX) {
+ final List<NotificationActionType> sortedActions =
+ new ArrayList<NotificationActionType>(unsortedActions.size());
+
+ if (folder.type == FolderType.INBOX || folder.type == FolderType.INBOX_SECTION) {
// Inbox
/*
* Action 1: Archive, Delete, Mute, Mark read, Add star, Mark important, Reply, Reply
@@ -236,23 +232,23 @@
* Action 2: Reply, Reply all, Forward, Mark important, Add star, Mark read, Mute,
* Delete, Archive
*/
- if (tempActions.contains(NotificationActionType.ARCHIVE_REMOVE_LABEL)) {
- notificationActions.add(NotificationActionType.ARCHIVE_REMOVE_LABEL);
+ if (unsortedActions.contains(NotificationActionType.ARCHIVE_REMOVE_LABEL)) {
+ sortedActions.add(NotificationActionType.ARCHIVE_REMOVE_LABEL);
}
- if (tempActions.contains(NotificationActionType.DELETE)) {
- notificationActions.add(NotificationActionType.DELETE);
+ if (unsortedActions.contains(NotificationActionType.DELETE)) {
+ sortedActions.add(NotificationActionType.DELETE);
}
- if (tempActions.contains(NotificationActionType.MARK_READ)) {
- notificationActions.add(NotificationActionType.MARK_READ);
+ if (unsortedActions.contains(NotificationActionType.MARK_READ)) {
+ sortedActions.add(NotificationActionType.MARK_READ);
}
- if (tempActions.contains(NotificationActionType.REPLY)) {
- notificationActions.add(NotificationActionType.REPLY);
+ if (unsortedActions.contains(NotificationActionType.REPLY)) {
+ sortedActions.add(NotificationActionType.REPLY);
}
- if (tempActions.contains(NotificationActionType.REPLY_ALL)) {
- notificationActions.add(NotificationActionType.REPLY_ALL);
+ if (unsortedActions.contains(NotificationActionType.REPLY_ALL)) {
+ sortedActions.add(NotificationActionType.REPLY_ALL);
}
- if (tempActions.contains(NotificationActionType.FORWARD)) {
- notificationActions.add(NotificationActionType.FORWARD);
+ if (unsortedActions.contains(NotificationActionType.FORWARD)) {
+ sortedActions.add(NotificationActionType.FORWARD);
}
} else if (folder.isProviderFolder()) {
// Gmail system labels
@@ -264,20 +260,20 @@
* Action 2: Reply, Reply all, Forward, Mark important, Add star, Mark read, Mute,
* Delete
*/
- if (tempActions.contains(NotificationActionType.DELETE)) {
- notificationActions.add(NotificationActionType.DELETE);
+ if (unsortedActions.contains(NotificationActionType.DELETE)) {
+ sortedActions.add(NotificationActionType.DELETE);
}
- if (tempActions.contains(NotificationActionType.MARK_READ)) {
- notificationActions.add(NotificationActionType.MARK_READ);
+ if (unsortedActions.contains(NotificationActionType.MARK_READ)) {
+ sortedActions.add(NotificationActionType.MARK_READ);
}
- if (tempActions.contains(NotificationActionType.REPLY)) {
- notificationActions.add(NotificationActionType.REPLY);
+ if (unsortedActions.contains(NotificationActionType.REPLY)) {
+ sortedActions.add(NotificationActionType.REPLY);
}
- if (tempActions.contains(NotificationActionType.REPLY_ALL)) {
- notificationActions.add(NotificationActionType.REPLY_ALL);
+ if (unsortedActions.contains(NotificationActionType.REPLY_ALL)) {
+ sortedActions.add(NotificationActionType.REPLY_ALL);
}
- if (tempActions.contains(NotificationActionType.FORWARD)) {
- notificationActions.add(NotificationActionType.FORWARD);
+ if (unsortedActions.contains(NotificationActionType.FORWARD)) {
+ sortedActions.add(NotificationActionType.FORWARD);
}
} else {
// Gmail user created labels
@@ -288,25 +284,27 @@
/*
* Action 2: Reply, Reply all, Forward, Mark important, Add star, Mark read, Delete
*/
- if (tempActions.contains(NotificationActionType.ARCHIVE_REMOVE_LABEL)) {
- notificationActions.add(NotificationActionType.ARCHIVE_REMOVE_LABEL);
+ if (unsortedActions.contains(NotificationActionType.ARCHIVE_REMOVE_LABEL)) {
+ sortedActions.add(NotificationActionType.ARCHIVE_REMOVE_LABEL);
}
- if (tempActions.contains(NotificationActionType.DELETE)) {
- notificationActions.add(NotificationActionType.DELETE);
+ if (unsortedActions.contains(NotificationActionType.DELETE)) {
+ sortedActions.add(NotificationActionType.DELETE);
}
- if (tempActions.contains(NotificationActionType.MARK_READ)) {
- notificationActions.add(NotificationActionType.MARK_READ);
+ if (unsortedActions.contains(NotificationActionType.MARK_READ)) {
+ sortedActions.add(NotificationActionType.MARK_READ);
}
- if (tempActions.contains(NotificationActionType.REPLY)) {
- notificationActions.add(NotificationActionType.REPLY);
+ if (unsortedActions.contains(NotificationActionType.REPLY)) {
+ sortedActions.add(NotificationActionType.REPLY);
}
- if (tempActions.contains(NotificationActionType.REPLY_ALL)) {
- notificationActions.add(NotificationActionType.REPLY_ALL);
+ if (unsortedActions.contains(NotificationActionType.REPLY_ALL)) {
+ sortedActions.add(NotificationActionType.REPLY_ALL);
}
- if (tempActions.contains(NotificationActionType.FORWARD)) {
- notificationActions.add(NotificationActionType.FORWARD);
+ if (unsortedActions.contains(NotificationActionType.FORWARD)) {
+ sortedActions.add(NotificationActionType.FORWARD);
}
}
+
+ return sortedActions;
}
/**
@@ -327,15 +325,16 @@
// reply activity.
final TaskStackBuilder taskStackBuilder = TaskStackBuilder.create(context);
- final Intent replyIntent = createReplyIntent(context, account, messageUri, false);
- replyIntent.putExtra(ComposeActivity.EXTRA_NOTIFICATION_FOLDER, folder);
+ final Intent intent = createReplyIntent(context, account, messageUri, false);
+ intent.setPackage(context.getPackageName());
+ intent.putExtra(ComposeActivity.EXTRA_NOTIFICATION_FOLDER, folder);
// To make sure that the reply intents one notification don't clobber over
// intents for other notification, force a data uri on the intent
final Uri notificationUri =
- Uri.parse("gmailfrom://gmail-ls/account/" + "reply/" + notificationId);
- replyIntent.setData(notificationUri);
+ Uri.parse("mailfrom://mail/account/" + "reply/" + notificationId);
+ intent.setData(notificationUri);
- taskStackBuilder.addNextIntent(notificationIntent).addNextIntent(replyIntent);
+ taskStackBuilder.addNextIntent(notificationIntent).addNextIntent(intent);
return taskStackBuilder.getPendingIntent(
notificationId, PendingIntent.FLAG_UPDATE_CURRENT);
@@ -344,15 +343,16 @@
// reply activity.
final TaskStackBuilder taskStackBuilder = TaskStackBuilder.create(context);
- final Intent replyIntent = createReplyIntent(context, account, messageUri, true);
- replyIntent.putExtra(ComposeActivity.EXTRA_NOTIFICATION_FOLDER, folder);
+ final Intent intent = createReplyIntent(context, account, messageUri, true);
+ intent.setPackage(context.getPackageName());
+ intent.putExtra(ComposeActivity.EXTRA_NOTIFICATION_FOLDER, folder);
// To make sure that the reply intents one notification don't clobber over
// intents for other notification, force a data uri on the intent
final Uri notificationUri =
- Uri.parse("gmailfrom://gmail-ls/account/" + "replyall/" + notificationId);
- replyIntent.setData(notificationUri);
+ Uri.parse("mailfrom://mail/account/" + "replyall/" + notificationId);
+ intent.setData(notificationUri);
- taskStackBuilder.addNextIntent(notificationIntent).addNextIntent(replyIntent);
+ taskStackBuilder.addNextIntent(notificationIntent).addNextIntent(intent);
return taskStackBuilder.getPendingIntent(
notificationId, PendingIntent.FLAG_UPDATE_CURRENT);
@@ -361,15 +361,16 @@
// reply activity.
final TaskStackBuilder taskStackBuilder = TaskStackBuilder.create(context);
- final Intent replyIntent = createForwardIntent(context, account, messageUri);
- replyIntent.putExtra(ComposeActivity.EXTRA_NOTIFICATION_FOLDER, folder);
+ final Intent intent = createForwardIntent(context, account, messageUri);
+ intent.setPackage(context.getPackageName());
+ intent.putExtra(ComposeActivity.EXTRA_NOTIFICATION_FOLDER, folder);
// To make sure that the reply intents one notification don't clobber over
// intents for other notification, force a data uri on the intent
final Uri notificationUri =
- Uri.parse("gmailfrom://gmail-ls/account/" + "forward/" + notificationId);
- replyIntent.setData(notificationUri);
+ Uri.parse("mailfrom://mail/account/" + "forward/" + notificationId);
+ intent.setData(notificationUri);
- taskStackBuilder.addNextIntent(notificationIntent).addNextIntent(replyIntent);
+ taskStackBuilder.addNextIntent(notificationIntent).addNextIntent(intent);
return taskStackBuilder.getPendingIntent(
notificationId, PendingIntent.FLAG_UPDATE_CURRENT);
@@ -378,6 +379,7 @@
NotificationActionIntentService.ACTION_ARCHIVE_REMOVE_LABEL;
final Intent intent = new Intent(intentAction);
+ intent.setPackage(context.getPackageName());
intent.putExtra(NotificationActionIntentService.EXTRA_NOTIFICATION_ACTION,
notificationAction);
@@ -387,6 +389,7 @@
final String intentAction = NotificationActionIntentService.ACTION_DELETE;
final Intent intent = new Intent(intentAction);
+ intent.setPackage(context.getPackageName());
intent.putExtra(NotificationActionIntentService.EXTRA_NOTIFICATION_ACTION,
notificationAction);
@@ -396,6 +399,7 @@
final String intentAction = NotificationActionIntentService.ACTION_MARK_READ;
final Intent intent = new Intent(intentAction);
+ intent.setPackage(context.getPackageName());
intent.putExtra(NotificationActionIntentService.EXTRA_NOTIFICATION_ACTION,
notificationAction);
@@ -491,7 +495,8 @@
public int getActionTextResId() {
switch (mNotificationActionType) {
case ARCHIVE_REMOVE_LABEL:
- if (mFolder.type == FolderType.INBOX) {
+ if (mFolder.type == FolderType.INBOX
+ || mFolder.type == FolderType.INBOX_SECTION) {
return R.string.notification_action_undo_archive;
} else {
return R.string.notification_action_undo_remove_label;
@@ -566,7 +571,10 @@
undoView.setTextViewText(
R.id.description_text, context.getString(notificationAction.getActionTextResId()));
+ final String packageName = context.getPackageName();
+
final Intent clickIntent = new Intent(NotificationActionIntentService.ACTION_UNDO);
+ clickIntent.setPackage(packageName);
clickIntent.putExtra(NotificationActionIntentService.EXTRA_NOTIFICATION_ACTION,
notificationAction);
final PendingIntent clickPendingIntent = PendingIntent.getService(context, notificationId,
@@ -578,6 +586,7 @@
// When the notification is cleared, we perform the destructive action
final Intent deleteIntent = new Intent(NotificationActionIntentService.ACTION_DESTRUCT);
+ deleteIntent.setPackage(packageName);
deleteIntent.putExtra(NotificationActionIntentService.EXTRA_NOTIFICATION_ACTION,
notificationAction);
final PendingIntent deletePendingIntent = PendingIntent.getService(context,
@@ -661,7 +670,7 @@
switch (destructAction) {
case ARCHIVE_REMOVE_LABEL: {
- if (folder.type == FolderType.INBOX) {
+ if (folder.type == FolderType.INBOX || folder.type == FolderType.INBOX_SECTION) {
// Inbox, so archive
final ContentValues values = new ContentValues(1);
values.put(UIProvider.ConversationOperations.OPERATION_KEY,
@@ -679,12 +688,12 @@
contentResolver.update(uri, values, null, null);
}
- markSeen(context, notificationAction.mAccount.toString(), folder);
+ markSeen(context, folder, conversation);
break;
}
case DELETE: {
contentResolver.delete(uri, null, null);
- markSeen(context, notificationAction.mAccount.toString(), folder);
+ markSeen(context, folder, conversation);
break;
}
default:
@@ -693,10 +702,11 @@
}
}
- private static void markSeen(final Context context, final String account, final Folder folder) {
+ private static void markSeen(
+ final Context context, final Folder folder, final Conversation conversation) {
final Intent intent = new Intent(MailIntentService.ACTION_MARK_SEEN);
- intent.putExtra(MailIntentService.ACCOUNT_EXTRA, account);
intent.putExtra(MailIntentService.FOLDER_EXTRA, folder);
+ intent.putExtra(MailIntentService.CONVERSATION_EXTRA, conversation);
context.startService(intent);
}
@@ -706,7 +716,7 @@
*/
public static void createUndoNotification(final Context context,
final NotificationAction notificationAction) {
- final int notificationId = getNotificationId(
+ final int notificationId = NotificationUtils.getNotificationId(
notificationAction.getAccount().name, notificationAction.getFolder());
final Notification notification =
@@ -722,7 +732,7 @@
public static void cancelUndoNotification(final Context context,
final NotificationAction notificationAction) {
- final int notificationId = getNotificationId(
+ final int notificationId = NotificationUtils.getNotificationId(
notificationAction.getAccount().name, notificationAction.getFolder());
removeUndoNotification(context, notificationId, false);
resendNotifications(context);
@@ -734,7 +744,7 @@
*/
public static void processUndoNotification(final Context context,
final NotificationAction notificationAction) {
- final int notificationId = getNotificationId(
+ final int notificationId = NotificationUtils.getNotificationId(
notificationAction.getAccount().name, notificationAction.getFolder());
removeUndoNotification(context, notificationId, true);
sNotificationTimestamps.delete(notificationId);
@@ -760,13 +770,6 @@
}
}
- public static int getNotificationId(final String account, final Folder folder) {
- // TODO(skennedy): When notifications are fully in UnifiedEmail, remove this method and use
- // the one in Utils
- // 1 == Gmail.NOTIFICATION_ID
- return 1 ^ account.hashCode() ^ folder.hashCode();
- }
-
/**
* Broadcasts an {@link Intent} to inform the app to resend its notifications.
*/
diff --git a/src/com/android/mail/utils/NotificationUtils.java b/src/com/android/mail/utils/NotificationUtils.java
new file mode 100644
index 0000000..dbc0113
--- /dev/null
+++ b/src/com/android/mail/utils/NotificationUtils.java
@@ -0,0 +1,1706 @@
+/*
+ * 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.utils;
+
+import android.app.Notification;
+import android.app.NotificationManager;
+import android.app.PendingIntent;
+import android.content.ContentResolver;
+import android.content.ContentUris;
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.Intent;
+import android.content.res.Resources;
+import android.database.Cursor;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.net.Uri;
+import android.provider.ContactsContract;
+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;
+import android.util.SparseArray;
+
+import com.android.mail.EmailAddress;
+import com.android.mail.MailIntentService;
+import com.android.mail.R;
+import com.android.mail.browse.MessageCursor;
+import com.android.mail.browse.SendersView;
+import com.android.mail.preferences.AccountPreferences;
+import com.android.mail.preferences.FolderPreferences;
+import com.android.mail.preferences.MailPrefs;
+import com.android.mail.providers.Account;
+import com.android.mail.providers.Conversation;
+import com.android.mail.providers.Folder;
+import com.android.mail.providers.Message;
+import com.android.mail.providers.UIProvider;
+import com.android.mail.utils.NotificationActionUtils.NotificationAction;
+import com.google.android.common.html.parser.HTML;
+import com.google.android.common.html.parser.HTML4;
+import com.google.android.common.html.parser.HtmlDocument;
+import com.google.android.common.html.parser.HtmlTree;
+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.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+
+public class NotificationUtils {
+ public static final String LOG_TAG = LogTag.getLogTag();
+
+ /** Contains a list of <(account, label), unread conversations> */
+ private static NotificationMap sActiveNotificationMap = null;
+
+ private static final SparseArray<Bitmap> sNotificationIcons = new SparseArray<Bitmap>();
+
+ 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() {
+ @Override
+ public HtmlTree.PlainTextConverter createInstance() {
+ return new MailMessagePlainTextConverter();
+ }
+ };
+
+ /**
+ * Clears all notifications in response to the user tapping "Clear" in the status bar.
+ */
+ public static void clearAllNotfications(Context context) {
+ LogUtils.v(LOG_TAG, "Clearing all notifications.");
+ final NotificationMap notificationMap = getNotificationMap(context);
+ notificationMap.clear();
+ notificationMap.saveNotificationMap(context);
+ }
+
+ /**
+ * Returns the notification map, creating it if necessary.
+ */
+ private static synchronized NotificationMap getNotificationMap(Context context) {
+ if (sActiveNotificationMap == null) {
+ sActiveNotificationMap = new NotificationMap();
+
+ // populate the map from the cached data
+ sActiveNotificationMap.loadNotificationMap(context);
+ }
+ return sActiveNotificationMap;
+ }
+
+ /**
+ * Class representing the existing notifications, and the number of unread and
+ * unseen conversations that triggered each.
+ */
+ private static class NotificationMap
+ extends ConcurrentHashMap<NotificationKey, Pair<Integer, Integer>> {
+
+ private static final String NOTIFICATION_PART_SEPARATOR = " ";
+ private static final int NUM_NOTIFICATION_PARTS= 4;
+
+ /**
+ * Retuns the unread count for the given NotificationKey.
+ */
+ public Integer getUnread(NotificationKey key) {
+ final Pair<Integer, Integer> value = get(key);
+ return value != null ? value.first : null;
+ }
+
+ /**
+ * Retuns the unread unseen count for the given NotificationKey.
+ */
+ public Integer getUnseen(NotificationKey key) {
+ final Pair<Integer, Integer> value = get(key);
+ return value != null ? value.second : null;
+ }
+
+ /**
+ * Store the unread and unseen value for the given NotificationKey
+ */
+ public void put(NotificationKey key, int unread, int unseen) {
+ final Pair<Integer, Integer> value =
+ new Pair<Integer, Integer>(Integer.valueOf(unread), Integer.valueOf(unseen));
+ put(key, value);
+ }
+
+ /**
+ * Populates the notification map with previously cached data.
+ */
+ public synchronized void loadNotificationMap(final Context context) {
+ final MailPrefs mailPrefs = MailPrefs.get(context);
+ final Set<String> notificationSet = mailPrefs.getActiveNotificationSet();
+ if (notificationSet != null) {
+ for (String notificationEntry : notificationSet) {
+ // Get the parts of the string that make the notification entry
+ final String[] notificationParts =
+ TextUtils.split(notificationEntry, NOTIFICATION_PART_SEPARATOR);
+ if (notificationParts.length == NUM_NOTIFICATION_PARTS) {
+ final Uri accountUri = Uri.parse(notificationParts[0]);
+ final Cursor accountCursor = context.getContentResolver().query(
+ accountUri, UIProvider.ACCOUNTS_PROJECTION, null, null, null);
+ final Account account;
+ try {
+ if (accountCursor.moveToFirst()) {
+ account = new Account(accountCursor);
+ } else {
+ continue;
+ }
+ } finally {
+ accountCursor.close();
+ }
+
+ final Uri folderUri = Uri.parse(notificationParts[1]);
+ final Cursor folderCursor = context.getContentResolver().query(
+ folderUri, UIProvider.FOLDERS_PROJECTION, null, null, null);
+ final Folder folder;
+ try {
+ if (folderCursor.moveToFirst()) {
+ folder = new Folder(folderCursor);
+ } else {
+ continue;
+ }
+ } finally {
+ folderCursor.close();
+ }
+
+ final NotificationKey key = new NotificationKey(account, folder);
+ final Integer unreadValue = Integer.valueOf(notificationParts[2]);
+ final Integer unseenValue = Integer.valueOf(notificationParts[3]);
+ final Pair<Integer, Integer> unreadUnseenValue =
+ new Pair<Integer, Integer>(unreadValue, unseenValue);
+ put(key, unreadUnseenValue);
+ }
+ }
+ }
+ }
+
+ /**
+ * Cache the notification map.
+ */
+ public synchronized void saveNotificationMap(Context context) {
+ final Set<String> notificationSet = Sets.newHashSet();
+ final Set<NotificationKey> keys = keySet();
+ for (NotificationKey key : keys) {
+ final Pair<Integer, Integer> value = get(key);
+ final Integer unreadCount = value.first;
+ final Integer unseenCount = value.second;
+ if (value != null && unreadCount != null && unseenCount != null) {
+ final String[] partValues = new String[] {
+ key.account.uri.toString(), key.folder.uri.toString(),
+ unreadCount.toString(), unseenCount.toString()};
+ notificationSet.add(TextUtils.join(NOTIFICATION_PART_SEPARATOR, partValues));
+ }
+ }
+ final MailPrefs mailPrefs = MailPrefs.get(context);
+ mailPrefs.cacheActiveNotificationSet(notificationSet);
+ }
+ }
+
+ /**
+ * @return the title of this notification with each account and the number of unread and unseen
+ * conversations for it. Also remove any account in the map that has 0 unread.
+ */
+ private static String createNotificationString(NotificationMap notifications) {
+ StringBuilder result = new StringBuilder();
+ int i = 0;
+ Set<NotificationKey> keysToRemove = Sets.newHashSet();
+ for (NotificationKey key : notifications.keySet()) {
+ Integer unread = notifications.getUnread(key);
+ Integer unseen = notifications.getUnseen(key);
+ if (unread == null || unread.intValue() == 0) {
+ keysToRemove.add(key);
+ } else {
+ if (i > 0) result.append(", ");
+ result.append(key.toString() + " (" + unread + ", " + unseen + ")");
+ i++;
+ }
+ }
+
+ for (NotificationKey key : keysToRemove) {
+ notifications.remove(key);
+ }
+
+ return result.toString();
+ }
+
+ /**
+ * Get all notifications for all accounts and cancel them.
+ **/
+ public static void cancelAllNotifications(Context context) {
+ NotificationManager nm = (NotificationManager) context.getSystemService(
+ Context.NOTIFICATION_SERVICE);
+ nm.cancelAll();
+ clearAllNotfications(context);
+ }
+
+ /**
+ * Get all notifications for all accounts, cancel them, and repost.
+ * This happens when locale changes.
+ **/
+ public static void cancelAndResendNotifications(Context context) {
+ resendNotifications(context, true);
+ }
+
+ /**
+ * Get all notifications for all accounts, optionally cancel them, and repost.
+ * This happens when locale changes.
+ **/
+ public static void resendNotifications(Context context, final boolean cancelExisting) {
+ if (cancelExisting) {
+ NotificationManager nm =
+ (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
+ nm.cancelAll();
+ }
+ // Re-validate the notifications.
+ final NotificationMap notificationMap = getNotificationMap(context);
+ final Set<NotificationKey> keys = notificationMap.keySet();
+ for (NotificationKey notification : keys) {
+ final Folder folder = notification.folder;
+ final int notificationId = getNotificationId(notification.account.name, folder);
+
+ final NotificationAction undoableAction =
+ NotificationActionUtils.sUndoNotifications.get(notificationId);
+ if (undoableAction == null) {
+ validateNotifications(context, folder, notification.account, true, false,
+ notification);
+ } else {
+ // Create an undo notification
+ NotificationActionUtils.createUndoNotification(context, undoableAction);
+ }
+ }
+ }
+
+ /**
+ * Validate the notifications for the specified account.
+ */
+ public static void validateAccountNotifications(Context context, String account) {
+ List<NotificationKey> notificationsToCancel = Lists.newArrayList();
+ // Iterate through the notification map to see if there are any entries that correspond to
+ // labels that are not in the sync set.
+ final NotificationMap notificationMap = getNotificationMap(context);
+ Set<NotificationKey> keys = notificationMap.keySet();
+ final AccountPreferences accountPreferences = new AccountPreferences(context, account);
+ final boolean enabled = accountPreferences.areNotificationsEnabled();
+ if (!enabled) {
+ // Cancel all notifications for this account
+ for (NotificationKey notification : keys) {
+ if (notification.account.name.equals(account)) {
+ notificationsToCancel.add(notification);
+ }
+ }
+ } else {
+ // Iterate through the notification map to see if there are any entries that
+ // correspond to labels that are not in the notification set.
+ for (NotificationKey notification : keys) {
+ if (notification.account.name.equals(account)) {
+ // If notification is not enabled for this label, remember this NotificationKey
+ // to later cancel the notification, and remove the entry from the map
+ final Folder folder = notification.folder;
+ final boolean isInbox =
+ notification.account.settings.defaultInbox.equals(folder.uri);
+ final FolderPreferences folderPreferences = new FolderPreferences(
+ context, notification.account.name, folder, isInbox);
+
+ if (!folderPreferences.areNotificationsEnabled()) {
+ notificationsToCancel.add(notification);
+ }
+ }
+ }
+ }
+
+ // Cancel & remove the invalid notifications.
+ if (notificationsToCancel.size() > 0) {
+ NotificationManager nm = (NotificationManager) context.getSystemService(
+ Context.NOTIFICATION_SERVICE);
+ for (NotificationKey notification : notificationsToCancel) {
+ final Folder folder = notification.folder;
+ final int notificationId = getNotificationId(notification.account.name, folder);
+ nm.cancel(notificationId);
+ notificationMap.remove(notification);
+ NotificationActionUtils.sUndoNotifications.remove(notificationId);
+ NotificationActionUtils.sNotificationTimestamps.delete(notificationId);
+ }
+ notificationMap.saveNotificationMap(context);
+ }
+ }
+
+ /**
+ * Display only one notification.
+ */
+ public static void setNewEmailIndicator(Context context, final int unreadCount,
+ final int unseenCount, final Account account, final Folder folder,
+ final boolean getAttention) {
+ boolean ignoreUnobtrusiveSetting = false;
+
+ final int notificationId = getNotificationId(account.name, folder);
+
+ // Update the notification map
+ final NotificationMap notificationMap = getNotificationMap(context);
+ final NotificationKey key = new NotificationKey(account, folder);
+ if (unreadCount == 0) {
+ notificationMap.remove(key);
+ ((NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE))
+ .cancel(notificationId);
+ } else {
+ if (!notificationMap.containsKey(key)) {
+ // This account previously didn't have any unread mail; ignore the "unobtrusive
+ // notifications" setting and play sound and/or vibrate the device even if a
+ // notification already exists (bug 2412348).
+ ignoreUnobtrusiveSetting = true;
+ }
+ notificationMap.put(key, unreadCount, unseenCount);
+ }
+ notificationMap.saveNotificationMap(context);
+
+ if (LogUtils.isLoggable(LOG_TAG, LogUtils.VERBOSE)) {
+ LogUtils.v(LOG_TAG, "New email: %s mapSize: %d getAttention: %b",
+ createNotificationString(notificationMap), notificationMap.size(),
+ getAttention);
+ }
+
+ if (NotificationActionUtils.sUndoNotifications.get(notificationId) == null) {
+ validateNotifications(context, folder, account, getAttention, ignoreUnobtrusiveSetting,
+ key);
+ }
+ }
+
+ /**
+ * Validate the notifications notification.
+ */
+ private static void validateNotifications(Context context, final Folder folder,
+ final Account account, boolean getAttention, boolean ignoreUnobtrusiveSetting,
+ NotificationKey key) {
+
+ NotificationManager nm = (NotificationManager)
+ context.getSystemService(Context.NOTIFICATION_SERVICE);
+
+ final NotificationMap notificationMap = getNotificationMap(context);
+ if (LogUtils.isLoggable(LOG_TAG, LogUtils.VERBOSE)) {
+ LogUtils.v(LOG_TAG, "Validating Notification: %s mapSize: %d folder: %s "
+ + "getAttention: %b", createNotificationString(notificationMap),
+ notificationMap.size(), folder.name, getAttention);
+ }
+ // The number of unread messages for this account and label.
+ final Integer unread = notificationMap.getUnread(key);
+ final int unreadCount = unread != null ? unread.intValue() : 0;
+ final Integer unseen = notificationMap.getUnseen(key);
+ int unseenCount = unseen != null ? unseen.intValue() : 0;
+
+ Cursor cursor = null;
+
+ try {
+ final Uri.Builder uriBuilder = folder.conversationListUri.buildUpon();
+ uriBuilder.appendQueryParameter(
+ UIProvider.SEEN_QUERY_PARAMETER, Boolean.FALSE.toString());
+ cursor = context.getContentResolver().query(uriBuilder.build(),
+ UIProvider.CONVERSATION_PROJECTION, null, null, null);
+ final int cursorUnseenCount = cursor.getCount();
+
+ // Make sure the unseen count matches the number of items in the cursor. But, we don't
+ // want to overwrite a 0 unseen count that was specified in the intent
+ if (unseenCount != 0 && unseenCount != cursorUnseenCount) {
+ LogUtils.d(LOG_TAG,
+ "Unseen count doesn't match cursor count. unseen: %d cursor count: %d",
+ unseenCount, cursorUnseenCount);
+ unseenCount = cursorUnseenCount;
+ }
+
+ // For the purpose of the notifications, the unseen count should be capped at the num of
+ // unread conversations.
+ if (unseenCount > unreadCount) {
+ unseenCount = unreadCount;
+ }
+
+ final int notificationId = getNotificationId(account.name, folder);
+
+ if (unseenCount == 0) {
+ nm.cancel(notificationId);
+ return;
+ }
+
+ // We now have all we need to create the notification and the pending intent
+ PendingIntent clickIntent;
+
+ NotificationCompat.Builder notification = new NotificationCompat.Builder(context);
+ notification.setSmallIcon(R.drawable.stat_notify_email);
+ notification.setTicker(account.name);
+
+ final long when;
+
+ final long oldWhen =
+ NotificationActionUtils.sNotificationTimestamps.get(notificationId);
+ if (oldWhen != 0) {
+ when = oldWhen;
+ } else {
+ when = System.currentTimeMillis();
+ }
+
+ notification.setWhen(when);
+
+ // The timestamp is now stored in the notification, so we can remove it from here
+ NotificationActionUtils.sNotificationTimestamps.delete(notificationId);
+
+ // Dispatch a CLEAR_NEW_MAIL_NOTIFICATIONS intent if the user taps the "X" next to a
+ // notification. Also this intent gets fired when the user taps on a notification as
+ // the AutoCancel flag has been set
+ final Intent cancelNotificationIntent =
+ new Intent(MailIntentService.ACTION_CLEAR_NEW_MAIL_NOTIFICATIONS);
+ cancelNotificationIntent.setPackage(context.getPackageName());
+ cancelNotificationIntent.setData(Utils.appendVersionQueryParameter(context,
+ folder.uri));
+ cancelNotificationIntent.putExtra(MailIntentService.ACCOUNT_EXTRA, account);
+ cancelNotificationIntent.putExtra(MailIntentService.FOLDER_EXTRA, folder);
+
+ notification.setDeleteIntent(PendingIntent.getService(
+ context, notificationId, cancelNotificationIntent, 0));
+
+ // Ensure that the notification is cleared when the user selects it
+ notification.setAutoCancel(true);
+
+ boolean eventInfoConfigured = false;
+
+ final boolean isInbox = account.settings.defaultInbox.equals(folder.uri);
+ final FolderPreferences folderPreferences =
+ new FolderPreferences(context, account.name, folder, isInbox);
+
+ if (isInbox) {
+ final AccountPreferences accountPreferences =
+ new AccountPreferences(context, account.name);
+ moveNotificationSetting(accountPreferences, folderPreferences);
+ }
+
+ if (!folderPreferences.areNotificationsEnabled()) {
+ // Don't notify
+ return;
+ }
+
+ if (unreadCount > 0) {
+ // How can I order this properly?
+ if (cursor.moveToNext()) {
+ Intent notificationIntent = createViewConversationIntent(context, account,
+ folder, null);
+
+ // Launch directly to the conversation, if the
+ // number of unseen conversations == 1
+ if (unseenCount == 1) {
+ notificationIntent = createViewConversationIntent(context, account, folder,
+ cursor);
+ }
+
+ if (notificationIntent == null) {
+ LogUtils.e(LOG_TAG, "Null intent when building notification");
+ return;
+ }
+
+ clickIntent = PendingIntent.getActivity(context, -1, notificationIntent, 0);
+ configureLatestEventInfoFromConversation(context, account, folderPreferences,
+ notification, cursor, clickIntent, notificationIntent,
+ account.name, unreadCount, unseenCount, folder, when);
+ eventInfoConfigured = true;
+ }
+ }
+
+ final boolean vibrate = folderPreferences.isNotificationVibrateEnabled();
+ final String ringtoneUri = folderPreferences.getNotificationRingtoneUri();
+ final boolean notifyOnce = !folderPreferences.isEveryMessageNotificationEnabled();
+
+ if (!ignoreUnobtrusiveSetting && account != null && notifyOnce) {
+ // If the user has "unobtrusive notifications" enabled, only alert the first time
+ // new mail is received in this account. This is the default behavior. See
+ // bugs 2412348 and 2413490.
+ notification.setOnlyAlertOnce(true);
+ }
+
+ if (account != null) {
+ LogUtils.d(LOG_TAG, "Account: %s vibrate: %s", account.name,
+ Boolean.toString(folderPreferences.isNotificationVibrateEnabled()));
+ }
+
+ int defaults = 0;
+
+ /*
+ * We do not want to notify if this is coming back from an Undo notification, hence the
+ * oldWhen check.
+ */
+ if (getAttention && account != null && oldWhen == 0) {
+ final AccountPreferences accountPreferences =
+ new AccountPreferences(context, account.name);
+ if (accountPreferences.areNotificationsEnabled()) {
+ if (vibrate) {
+ defaults |= Notification.DEFAULT_VIBRATE;
+ }
+
+ notification.setSound(TextUtils.isEmpty(ringtoneUri) ? null
+ : Uri.parse(ringtoneUri));
+ LogUtils.d(LOG_TAG, "New email in %s vibrateWhen: %s, playing notification: %s",
+ account.name, vibrate, ringtoneUri);
+ }
+ }
+
+ if (eventInfoConfigured) {
+ defaults |= Notification.DEFAULT_LIGHTS;
+ notification.setDefaults(defaults);
+
+ if (oldWhen != 0) {
+ // We do not want to display the ticker again if we are re-displaying this
+ // notification (like from an Undo notification)
+ notification.setTicker(null);
+ }
+
+ nm.notify(notificationId, notification.build());
+ }
+ } finally {
+ if (cursor != null) {
+ cursor.close();
+ }
+ }
+ }
+
+ /**
+ * @return an {@link Intent} which, if launched, will display the corresponding conversation
+ */
+ private static Intent createViewConversationIntent(final Context context, final Account account,
+ final Folder folder, final Cursor cursor) {
+ if (folder == null || account == null) {
+ LogUtils.e(LOG_TAG, "Null account or folder. account: %s folder: %s",
+ account, folder);
+ return null;
+ }
+
+ final Intent intent;
+
+ if (cursor == null) {
+ intent = Utils.createViewFolderIntent(context, folder.uri, account);
+ } else {
+ // A conversation cursor has been specified, so this intent is intended to be go
+ // directly to the one new conversation
+
+ // Get the Conversation object
+ final Conversation conversation = new Conversation(cursor);
+ intent = Utils.createViewConversationIntent(context, conversation, folder.uri,
+ account);
+ }
+
+ return intent;
+ }
+
+ private static Bitmap getDefaultNotificationIcon(
+ final Context context, final Folder folder, final boolean multipleNew) {
+ final Bitmap icon;
+ if (folder.notificationIconResId != 0) {
+ icon = getIcon(context, folder.notificationIconResId);
+ } else if (multipleNew) {
+ icon = getIcon(context, R.drawable.ic_notification_multiple_mail_holo_dark);
+ } else {
+ icon = getIcon(context, R.drawable.ic_contact_picture);
+ }
+ return icon;
+ }
+
+ private static Bitmap getIcon(final Context context, final int resId) {
+ final Bitmap cachedIcon = sNotificationIcons.get(resId);
+ if (cachedIcon != null) {
+ return cachedIcon;
+ }
+
+ final Bitmap icon = BitmapFactory.decodeResource(context.getResources(), resId);
+ sNotificationIcons.put(resId, icon);
+
+ return icon;
+ }
+
+ private static void configureLatestEventInfoFromConversation(final Context context,
+ final Account account, final FolderPreferences folderPreferences,
+ final NotificationCompat.Builder notification, final Cursor conversationCursor,
+ final PendingIntent clickIntent, final Intent notificationIntent,
+ final String notificationAccount, final int unreadCount, final int unseenCount,
+ final Folder folder, final long when) {
+ final Resources res = context.getResources();
+
+ LogUtils.w(LOG_TAG, "Showing notification with unreadCount of %d and "
+ + "unseenCount of %d", unreadCount, unseenCount);
+
+ String notificationTicker = null;
+
+ // Boolean indicating that this notification is for a non-inbox label.
+ final boolean isInbox = account.settings.defaultInbox.equals(folder.uri);
+
+ // Notification label name for user label notifications.
+ final String notificationLabelName = isInbox ? null : folder.name;
+
+ if (unseenCount > 1) {
+ // Build the string that describes the number of new messages
+ final String newMessagesString = res.getString(R.string.new_messages, unseenCount);
+
+ // Use the default notification icon
+ notification.setLargeIcon(
+ getDefaultNotificationIcon(context, folder, true /* multiple new messages */));
+
+ // The ticker initially start as the new messages string.
+ notificationTicker = newMessagesString;
+
+ // The title of the notification is the new messages string
+ notification.setContentTitle(newMessagesString);
+
+ if (com.android.mail.utils.Utils.isRunningJellybeanOrLater()) {
+ // For a new-style notification
+ final int maxNumDigestItems = context.getResources().getInteger(
+ R.integer.max_num_notification_digest_items);
+
+ // The body of the notification is the account name, or the label name.
+ notification.setSubText(isInbox ? notificationAccount : notificationLabelName);
+
+ final NotificationCompat.InboxStyle digest =
+ new NotificationCompat.InboxStyle(notification);
+
+ digest.setBigContentTitle(newMessagesString);
+
+ int numDigestItems = 0;
+ do {
+ final Conversation conversation = new Conversation(conversationCursor);
+
+ if (!conversation.read) {
+ boolean multipleUnreadThread = false;
+ // TODO(cwren) extract this pattern into a helper
+
+ Cursor cursor = null;
+ MessageCursor messageCursor = null;
+ try {
+ final Uri.Builder uriBuilder = conversation.messageListUri.buildUpon();
+ uriBuilder.appendQueryParameter(
+ UIProvider.LABEL_QUERY_PARAMETER, notificationLabelName);
+ cursor = context.getContentResolver().query(uriBuilder.build(),
+ UIProvider.MESSAGE_PROJECTION, null, null, null);
+ messageCursor = new MessageCursor(cursor);
+
+ String from = "";
+ String fromAddress = "";
+ if (messageCursor.moveToPosition(messageCursor.getCount() - 1)) {
+ final Message message = messageCursor.getMessage();
+ fromAddress = message.getFrom();
+ if (fromAddress == null) {
+ fromAddress = "";
+ }
+ from = getDisplayableSender(fromAddress);
+ }
+ while (messageCursor.moveToPosition(messageCursor.getPosition() - 1)) {
+ final Message message = messageCursor.getMessage();
+ if (!message.read &&
+ !fromAddress.contentEquals(message.getFrom())) {
+ multipleUnreadThread = true;
+ break;
+ }
+ }
+ final SpannableStringBuilder sendersBuilder;
+ if (multipleUnreadThread) {
+ final int sendersLength =
+ res.getInteger(R.integer.swipe_senders_length);
+
+ sendersBuilder = getStyledSenders(context, conversationCursor,
+ sendersLength, notificationAccount);
+ } else {
+ sendersBuilder = new SpannableStringBuilder(from);
+ }
+ final CharSequence digestLine = getSingleMessageInboxLine(context,
+ sendersBuilder.toString(),
+ conversation.subject,
+ conversation.snippet);
+ digest.addLine(digestLine);
+ numDigestItems++;
+ } finally {
+ if (messageCursor != null) {
+ messageCursor.close();
+ }
+ if (cursor != null) {
+ cursor.close();
+ }
+ }
+ }
+ } while (numDigestItems <= maxNumDigestItems && conversationCursor.moveToNext());
+ } else {
+ // The body of the notification is the account name, or the label name.
+ notification.setContentText(
+ isInbox ? notificationAccount : notificationLabelName);
+ }
+ } else {
+ // For notifications for a single new conversation, we want to get the information from
+ // the conversation
+
+ // Move the cursor to the most recent unread conversation
+ seekToLatestUnreadConversation(conversationCursor);
+
+ final Conversation conversation = new Conversation(conversationCursor);
+
+ Cursor cursor = null;
+ MessageCursor messageCursor = null;
+ boolean multipleUnseenThread = false;
+ String from = null;
+ try {
+ final Uri uri = conversation.messageListUri.buildUpon().appendQueryParameter(
+ UIProvider.LABEL_QUERY_PARAMETER, folder.persistentId).build();
+ cursor = context.getContentResolver().query(uri, UIProvider.MESSAGE_PROJECTION,
+ null, null, null);
+ messageCursor = new MessageCursor(cursor);
+ // Use the information from the last sender in the conversation that triggered
+ // this notification.
+
+ String fromAddress = "";
+ if (messageCursor.moveToPosition(messageCursor.getCount() - 1)) {
+ final Message message = messageCursor.getMessage();
+ fromAddress = message.getFrom();
+ from = getDisplayableSender(fromAddress);
+ notification.setLargeIcon(
+ getContactIcon(context, getSenderAddress(fromAddress), folder));
+ }
+
+ // Assume that the last message in this conversation is unread
+ int firstUnseenMessagePos = messageCursor.getPosition();
+ while (messageCursor.moveToPosition(messageCursor.getPosition() - 1)) {
+ final Message message = messageCursor.getMessage();
+ final boolean unseen = !message.seen;
+ if (unseen) {
+ firstUnseenMessagePos = messageCursor.getPosition();
+ if (!multipleUnseenThread
+ && !fromAddress.contentEquals(message.getFrom())) {
+ multipleUnseenThread = true;
+ }
+ }
+ }
+
+ if (Utils.isRunningJellybeanOrLater()) {
+ // For a new-style notification
+
+ if (multipleUnseenThread) {
+ // The title of a single conversation is the list of senders.
+ int sendersLength = res.getInteger(R.integer.swipe_senders_length);
+
+ final SpannableStringBuilder sendersBuilder = getStyledSenders(
+ context, conversationCursor, sendersLength, notificationAccount);
+
+ notification.setContentTitle(sendersBuilder);
+ // For a single new conversation, the ticker is based on the sender's name.
+ notificationTicker = sendersBuilder.toString();
+ } else {
+ // The title of a single message the sender.
+ notification.setContentTitle(from);
+ // For a single new conversation, the ticker is based on the sender's name.
+ notificationTicker = from;
+ }
+
+ // The notification content will be the subject of the conversation.
+ notification.setContentText(
+ getSingleMessageLittleText(context, conversation.subject));
+
+ // The notification subtext will be the subject of the conversation for inbox
+ // notifications, or will based on the the label name for user label
+ // notifications.
+ notification.setSubText(isInbox ? notificationAccount : notificationLabelName);
+
+ if (multipleUnseenThread) {
+ notification.setLargeIcon(
+ getDefaultNotificationIcon(context, folder, true));
+ }
+ final NotificationCompat.BigTextStyle bigText =
+ new NotificationCompat.BigTextStyle(notification);
+
+ // Seek the message cursor to the first unread message
+ final Message message;
+ if (messageCursor.moveToPosition(firstUnseenMessagePos)) {
+ message = messageCursor.getMessage();
+ bigText.bigText(getSingleMessageBigText(context,
+ conversation.subject, message));
+ } else {
+ LogUtils.e(LOG_TAG, "Failed to load message");
+ message = null;
+ }
+
+ if (message != null) {
+ final Set<String> notificationActions =
+ folderPreferences.getNotificationActions();
+
+ final int notificationId = getNotificationId(notificationAccount, folder);
+
+ NotificationActionUtils.addNotificationActions(context, notificationIntent,
+ notification, account, conversation, message, folder,
+ notificationId, when, notificationActions);
+ }
+ } else {
+ // For an old-style notification
+
+ // The title of a single conversation notification is built from both the sender
+ // and subject of the new message.
+ notification.setContentTitle(getSingleMessageNotificationTitle(context,
+ from, conversation.subject));
+
+ // The notification content will be the subject of the conversation for inbox
+ // notifications, or will based on the the label name for user label
+ // notifications.
+ notification.setContentText(
+ isInbox ? notificationAccount : notificationLabelName);
+
+ // For a single new conversation, the ticker is based on the sender's name.
+ notificationTicker = from;
+ }
+ } finally {
+ if (messageCursor != null) {
+ messageCursor.close();
+ }
+ if (cursor != null) {
+ cursor.close();
+ }
+ }
+ }
+
+ // Build the notification ticker
+ if (notificationLabelName != null && notificationTicker != null) {
+ // This is a per label notification, format the ticker with that information
+ notificationTicker = res.getString(R.string.label_notification_ticker,
+ notificationLabelName, notificationTicker);
+ }
+
+ if (notificationTicker != null) {
+ // If we didn't generate a notification ticker, it will default to account name
+ notification.setTicker(notificationTicker);
+ }
+
+ // Set the number in the notification
+ if (unreadCount > 1) {
+ notification.setNumber(unreadCount);
+ }
+
+ notification.setContentIntent(clickIntent);
+ }
+
+ private static SpannableStringBuilder getStyledSenders(final Context context,
+ final Cursor conversationCursor, final int maxLength, final String account) {
+ final Conversation conversation = new Conversation(conversationCursor);
+ final com.android.mail.providers.ConversationInfo conversationInfo =
+ conversation.conversationInfo;
+ final ArrayList<SpannableString> senders = new ArrayList<SpannableString>();
+ if (sNotificationUnreadStyleSpan == null) {
+ sNotificationUnreadStyleSpan = new TextAppearanceSpan(
+ context, R.style.NotificationSendersUnreadTextAppearance);
+ sNotificationReadStyleSpan =
+ new TextAppearanceSpan(context, R.style.NotificationSendersReadTextAppearance);
+ }
+ SendersView.format(context, conversationInfo, "", maxLength, senders, null, null, account,
+ sNotificationUnreadStyleSpan, sNotificationReadStyleSpan, false);
+
+ return ellipsizeStyledSenders(context, senders);
+ }
+
+ private static String sSendersSplitToken = null;
+ private static String sElidedPaddingToken = null;
+
+ private static SpannableStringBuilder ellipsizeStyledSenders(final Context context,
+ ArrayList<SpannableString> styledSenders) {
+ if (sSendersSplitToken == null) {
+ sSendersSplitToken = context.getString(R.string.senders_split_token);
+ sElidedPaddingToken = context.getString(R.string.elided_padding_token);
+ }
+
+ SpannableStringBuilder builder = new SpannableStringBuilder();
+ SpannableString prevSender = null;
+ for (SpannableString sender : styledSenders) {
+ if (sender == null) {
+ LogUtils.e(LOG_TAG, "null sender while iterating over styledSenders");
+ continue;
+ }
+ CharacterStyle[] spans = sender.getSpans(0, sender.length(), CharacterStyle.class);
+ if (SendersView.sElidedString.equals(sender.toString())) {
+ prevSender = sender;
+ sender = copyStyles(spans, sElidedPaddingToken + sender + sElidedPaddingToken);
+ } else if (builder.length() > 0
+ && (prevSender == null || !SendersView.sElidedString.equals(prevSender
+ .toString()))) {
+ prevSender = sender;
+ sender = copyStyles(spans, sSendersSplitToken + sender);
+ } else {
+ prevSender = sender;
+ }
+ builder.append(sender);
+ }
+ return builder;
+ }
+
+ private static SpannableString copyStyles(CharacterStyle[] spans, CharSequence newText) {
+ SpannableString s = new SpannableString(newText);
+ if (spans != null && spans.length > 0) {
+ s.setSpan(spans[0], 0, s.length(), 0);
+ }
+ return s;
+ }
+
+ /**
+ * Seeks the cursor to the position of the most recent unread conversation. If no unread
+ * conversation is found, the position of the cursor will be restored, and false will be
+ * returned.
+ */
+ private static boolean seekToLatestUnreadConversation(final Cursor cursor) {
+ final int initialPosition = cursor.getPosition();
+ do {
+ final Conversation conversation = new Conversation(cursor);
+ if (!conversation.read) {
+ return true;
+ }
+ } while (cursor.moveToNext());
+
+ // Didn't find an unread conversation, reset the position.
+ cursor.moveToPosition(initialPosition);
+ return false;
+ }
+
+ /**
+ * Sets the bigtext for a notification for a single new conversation
+ *
+ * @param context
+ * @param senders Sender of the new message that triggered the notification.
+ * @param subject Subject of the new message that triggered the notification
+ * @param snippet Snippet of the new message that triggered the notification
+ * @return a {@link CharSequence} suitable for use in
+ * {@link android.support.v4.app.NotificationCompat.BigTextStyle}
+ */
+ private static CharSequence getSingleMessageInboxLine(Context context,
+ String senders, String subject, String snippet) {
+ // TODO(cwren) finish this step toward commmon code with getSingleMessageBigText
+
+ final String subjectSnippet = !TextUtils.isEmpty(subject) ? subject : snippet;
+
+ final TextAppearanceSpan notificationPrimarySpan =
+ new TextAppearanceSpan(context, R.style.NotificationPrimaryText);
+
+ if (TextUtils.isEmpty(senders)) {
+ // If the senders are empty, just use the subject/snippet.
+ return subjectSnippet;
+ } else if (TextUtils.isEmpty(subjectSnippet)) {
+ // If the subject/snippet is empty, just use the senders.
+ final SpannableString spannableString = new SpannableString(senders);
+ spannableString.setSpan(notificationPrimarySpan, 0, senders.length(), 0);
+
+ return spannableString;
+ } else {
+ final String formatString = context.getResources().getString(
+ R.string.multiple_new_message_notification_item);
+ final TextAppearanceSpan notificationSecondarySpan =
+ new TextAppearanceSpan(context, R.style.NotificationSecondaryText);
+
+ final String instantiatedString = String.format(formatString, senders, subjectSnippet);
+
+ final SpannableString spannableString = new SpannableString(instantiatedString);
+
+ final boolean isOrderReversed = formatString.indexOf("%2$s") <
+ formatString.indexOf("%1$s");
+ final int primaryOffset =
+ (isOrderReversed ? instantiatedString.lastIndexOf(senders) :
+ instantiatedString.indexOf(senders));
+ final int secondaryOffset =
+ (isOrderReversed ? instantiatedString.lastIndexOf(subjectSnippet) :
+ instantiatedString.indexOf(subjectSnippet));
+ spannableString.setSpan(notificationPrimarySpan,
+ primaryOffset, primaryOffset + senders.length(), 0);
+ spannableString.setSpan(notificationSecondarySpan,
+ secondaryOffset, secondaryOffset + subjectSnippet.length(), 0);
+ return spannableString;
+ }
+ }
+
+ /**
+ * Sets the bigtext for a notification for a single new conversation
+ * @param context
+ * @param subject Subject of the new message that triggered the notification
+ * @return a {@link CharSequence} suitable for use in {@link Notification.ContentText}
+ */
+ private static CharSequence getSingleMessageLittleText(Context context, String subject) {
+ final TextAppearanceSpan notificationSubjectSpan = new TextAppearanceSpan(
+ context, R.style.NotificationPrimaryText);
+
+ final SpannableString spannableString = new SpannableString(subject);
+ spannableString.setSpan(notificationSubjectSpan, 0, subject.length(), 0);
+
+ return spannableString;
+ }
+
+ /**
+ * Sets the bigtext for a notification for a single new conversation
+ *
+ * @param context
+ * @param subject Subject of the new message that triggered the notification
+ * @param message the {@link Message} to be displayed.
+ * @return a {@link CharSequence} suitable for use in
+ * {@link android.support.v4.app.NotificationCompat.BigTextStyle}
+ */
+ private static CharSequence getSingleMessageBigText(Context context, String subject,
+ final Message message) {
+
+ final TextAppearanceSpan notificationSubjectSpan = new TextAppearanceSpan(
+ context, R.style.NotificationPrimaryText);
+
+ final String snippet = getMessageBodyWithoutElidedText(message);
+
+ // Change multiple newlines (with potential white space between), into a single new line
+ final String collapsedSnippet =
+ !TextUtils.isEmpty(snippet) ? snippet.replaceAll("\\n\\s+", "\n") : "";
+
+ if (TextUtils.isEmpty(subject)) {
+ // If the subject is empty, just use the snippet.
+ return snippet;
+ } else if (TextUtils.isEmpty(collapsedSnippet)) {
+ // If the snippet is empty, just use the subject.
+ final SpannableString spannableString = new SpannableString(subject);
+ spannableString.setSpan(notificationSubjectSpan, 0, subject.length(), 0);
+
+ return spannableString;
+ } else {
+ final String notificationBigTextFormat = context.getResources().getString(
+ R.string.single_new_message_notification_big_text);
+
+ // Localizers may change the order of the parameters, look at how the format
+ // string is structured.
+ final boolean isSubjectFirst = notificationBigTextFormat.indexOf("%2$s") >
+ notificationBigTextFormat.indexOf("%1$s");
+ final String bigText =
+ String.format(notificationBigTextFormat, subject, collapsedSnippet);
+ final SpannableString spannableString = new SpannableString(bigText);
+
+ final int subjectOffset =
+ (isSubjectFirst ? bigText.indexOf(subject) : bigText.lastIndexOf(subject));
+ spannableString.setSpan(notificationSubjectSpan,
+ subjectOffset, subjectOffset + subject.length(), 0);
+
+ return spannableString;
+ }
+ }
+
+ /**
+ * Gets the title for a notification for a single new conversation
+ * @param context
+ * @param sender Sender of the new message that triggered the notification.
+ * @param subject Subject of the new message that triggered the notification
+ * @return a {@link CharSequence} suitable for use as a {@link Notification} title.
+ */
+ private static CharSequence getSingleMessageNotificationTitle(Context context,
+ String sender, String subject) {
+
+ if (TextUtils.isEmpty(subject)) {
+ // If the subject is empty, just set the title to the sender's information.
+ return sender;
+ } else {
+ final String notificationTitleFormat = context.getResources().getString(
+ R.string.single_new_message_notification_title);
+
+ // Localizers may change the order of the parameters, look at how the format
+ // string is structured.
+ final boolean isSubjectLast = notificationTitleFormat.indexOf("%2$s") >
+ notificationTitleFormat.indexOf("%1$s");
+ final String titleString = String.format(notificationTitleFormat, sender, subject);
+
+ // Format the string so the subject is using the secondaryText style
+ final SpannableString titleSpannable = new SpannableString(titleString);
+
+ // Find the offset of the subject.
+ final int subjectOffset =
+ isSubjectLast ? titleString.lastIndexOf(subject) : titleString.indexOf(subject);
+ final TextAppearanceSpan notificationSubjectSpan =
+ new TextAppearanceSpan(context, R.style.NotificationSecondaryText);
+ titleSpannable.setSpan(notificationSubjectSpan,
+ subjectOffset, subjectOffset + subject.length(), 0);
+ return titleSpannable;
+ }
+ }
+
+ /**
+ * 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
+ */
+ 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;
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * Uses sender instructions to build a formatted string.
+ *
+ * <p>Sender list instructions contain compact information about the sender list. Most work that
+ * can be done without knowing how much room will be availble for the sender list is done when
+ * creating the instructions.
+ *
+ * <p>The instructions string consists of tokens separated by SENDER_LIST_SEPARATOR. Here are
+ * the tokens, one per line:<ul>
+ * <li><tt>n</tt></li>
+ * <li><em>int</em>, the number of non-draft messages in the conversation</li>
+ * <li><tt>d</tt</li>
+ * <li><em>int</em>, the number of drafts in the conversation</li>
+ * <li><tt>l</tt></li>
+ * <li><em>literal html to be included in the output</em></li>
+ * <li><tt>s</tt> indicates that the message is sending (in the outbox without errors)</li>
+ * <li><tt>f</tt> indicates that the message failed to send (in the outbox with errors)</li>
+ * <li><em>for each message</em><ul>
+ * <li><em>int</em>, 0 for read, 1 for unread</li>
+ * <li><em>int</em>, the priority of the message. Zero is the most important</li>
+ * <li><em>text</em>, the sender text or blank for messages from 'me'</li>
+ * </ul></li>
+ * <li><tt>e</tt> to indicate that one or more messages have been elided</li>
+ *
+ * <p>The instructions indicate how many messages and drafts are in the conversation and then
+ * describe the most important messages in order, indicating the priority of each message and
+ * whether the message is unread.
+ *
+ * @param instructions instructions as described above
+ * @param senderBuilder the SpannableStringBuilder to append to for sender information
+ * @param statusBuilder the SpannableStringBuilder to append to for status
+ * @param maxChars the number of characters available to display the text
+ * @param unreadStyle the CharacterStyle for unread messages, or null
+ * @param draftsStyle the CharacterStyle for draft messages, or null
+ * @param sendingString the string to use when there are messages scheduled to be sent
+ * @param sendFailedString the string to use when there are messages that mailed to send
+ * @param meString the string to use for messages sent by this user
+ * @param draftString the string to use for "Draft"
+ * @param draftPluralString the string to use for "Drafts"
+ * @param showNumMessages false means do not show the message count
+ * @param onlyShowUnread true means the only information from unread messages should be included
+ */
+ public static synchronized void getSenderSnippet(
+ String instructions, SpannableStringBuilder senderBuilder,
+ SpannableStringBuilder statusBuilder, int maxChars,
+ CharacterStyle unreadStyle,
+ CharacterStyle readStyle,
+ CharacterStyle draftsStyle,
+ CharSequence meString, CharSequence draftString, CharSequence draftPluralString,
+ CharSequence sendingString, CharSequence sendFailedString,
+ boolean forceAllUnread, boolean forceAllRead, boolean allowDraft,
+ boolean showNumMessages, boolean onlyShowUnread) {
+ assert !(forceAllUnread && forceAllRead);
+ boolean unreadStatusIsForced = forceAllUnread || forceAllRead;
+ boolean forcedUnreadStatus = forceAllUnread;
+
+ // Measure each fragment. It's ok to iterate over the entire set of fragments because it is
+ // never a long list, even if there are many senders.
+ final Map<Integer, Integer> priorityToLength = sPriorityToLength;
+ priorityToLength.clear();
+
+ int maxFoundPriority = Integer.MIN_VALUE;
+ int numMessages = 0;
+ int numDrafts = 0;
+ CharSequence draftsFragment = "";
+ CharSequence sendingFragment = "";
+ CharSequence sendFailedFragment = "";
+
+ SENDER_LIST_SPLITTER.setString(instructions);
+ int numFragments = 0;
+ String[] fragments = sSenderFragments;
+ int currentSize = fragments.length;
+ while (SENDER_LIST_SPLITTER.hasNext()) {
+ fragments[numFragments++] = SENDER_LIST_SPLITTER.next();
+ if (numFragments == currentSize) {
+ sSenderFragments = new String[2 * currentSize];
+ System.arraycopy(fragments, 0, sSenderFragments, 0, currentSize);
+ currentSize *= 2;
+ fragments = sSenderFragments;
+ }
+ }
+
+ for (int i = 0; i < numFragments;) {
+ String fragment0 = fragments[i++];
+ if ("".equals(fragment0)) {
+ // This should be the final fragment.
+ } else if (Utils.SENDER_LIST_TOKEN_ELIDED.equals(fragment0)) {
+ // ignore
+ } else if (Utils.SENDER_LIST_TOKEN_NUM_MESSAGES.equals(fragment0)) {
+ numMessages = Integer.valueOf(fragments[i++]);
+ } else if (Utils.SENDER_LIST_TOKEN_NUM_DRAFTS.equals(fragment0)) {
+ String numDraftsString = fragments[i++];
+ numDrafts = Integer.parseInt(numDraftsString);
+ draftsFragment = numDrafts == 1 ? draftString :
+ draftPluralString + " (" + numDraftsString + ")";
+ } else if (Utils.SENDER_LIST_TOKEN_LITERAL.equals(fragment0)) {
+ senderBuilder.append(Html.fromHtml(fragments[i++]));
+ return;
+ } else if (Utils.SENDER_LIST_TOKEN_SENDING.equals(fragment0)) {
+ sendingFragment = sendingString;
+ } else if (Utils.SENDER_LIST_TOKEN_SEND_FAILED.equals(fragment0)) {
+ sendFailedFragment = sendFailedString;
+ } else {
+ final String unreadString = fragment0;
+ final boolean unread = unreadStatusIsForced
+ ? forcedUnreadStatus : Integer.parseInt(unreadString) != 0;
+ String priorityString = fragments[i++];
+ CharSequence nameString = fragments[i++];
+ if (nameString.length() == 0) nameString = meString;
+ int priority = Integer.parseInt(priorityString);
+
+ // We want to include this entry if
+ // 1) The onlyShowUnread flags is not set
+ // 2) The above flag is set, and the message is unread
+ if (!onlyShowUnread || unread) {
+ priorityToLength.put(priority, nameString.length());
+ maxFoundPriority = Math.max(maxFoundPriority, priority);
+ }
+ }
+ }
+ final String numMessagesFragment =
+ (numMessages != 0 && showNumMessages) ?
+ " \u00A0" + Integer.toString(numMessages + numDrafts) : "";
+
+ // Don't allocate fixedFragment unless we need it
+ SpannableStringBuilder fixedFragment = null;
+ int fixedFragmentLength = 0;
+ if (draftsFragment.length() != 0 && allowDraft) {
+ if (fixedFragment == null) {
+ fixedFragment = new SpannableStringBuilder();
+ }
+ fixedFragment.append(draftsFragment);
+ if (draftsStyle != null) {
+ fixedFragment.setSpan(
+ CharacterStyle.wrap(draftsStyle),
+ 0, fixedFragment.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
+ }
+ }
+ if (sendingFragment.length() != 0) {
+ if (fixedFragment == null) {
+ fixedFragment = new SpannableStringBuilder();
+ }
+ if (fixedFragment.length() != 0) fixedFragment.append(", ");
+ fixedFragment.append(sendingFragment);
+ }
+ if (sendFailedFragment.length() != 0) {
+ if (fixedFragment == null) {
+ fixedFragment = new SpannableStringBuilder();
+ }
+ if (fixedFragment.length() != 0) fixedFragment.append(", ");
+ fixedFragment.append(sendFailedFragment);
+ }
+
+ if (fixedFragment != null) {
+ fixedFragmentLength = fixedFragment.length();
+ }
+ maxChars -= fixedFragmentLength;
+
+ int maxPriorityToInclude = -1; // inclusive
+ int numCharsUsed = numMessagesFragment.length();
+ int numSendersUsed = 0;
+ while (maxPriorityToInclude < maxFoundPriority) {
+ if (priorityToLength.containsKey(maxPriorityToInclude + 1)) {
+ int length = numCharsUsed + priorityToLength.get(maxPriorityToInclude + 1);
+ if (numCharsUsed > 0) length += 2;
+ // We must show at least two senders if they exist. If we don't have space for both
+ // then we will truncate names.
+ if (length > maxChars && numSendersUsed >= 2) {
+ break;
+ }
+ numCharsUsed = length;
+ numSendersUsed++;
+ }
+ maxPriorityToInclude++;
+ }
+
+ int numCharsToRemovePerWord = 0;
+ if (numCharsUsed > maxChars) {
+ numCharsToRemovePerWord = (numCharsUsed - maxChars) / numSendersUsed;
+ }
+
+ String lastFragment = null;
+ CharacterStyle lastStyle = null;
+ for (int i = 0; i < numFragments;) {
+ String fragment0 = fragments[i++];
+ if ("".equals(fragment0)) {
+ // This should be the final fragment.
+ } else if (Utils.SENDER_LIST_TOKEN_ELIDED.equals(fragment0)) {
+ if (lastFragment != null) {
+ addStyledFragment(senderBuilder, lastFragment, lastStyle, false);
+ senderBuilder.append(" ");
+ addStyledFragment(senderBuilder, "..", lastStyle, true);
+ senderBuilder.append(" ");
+ }
+ lastFragment = null;
+ } else if (Utils.SENDER_LIST_TOKEN_NUM_MESSAGES.equals(fragment0)) {
+ i++;
+ } else if (Utils.SENDER_LIST_TOKEN_NUM_DRAFTS.equals(fragment0)) {
+ i++;
+ } else if (Utils.SENDER_LIST_TOKEN_SENDING.equals(fragment0)) {
+ } else if (Utils.SENDER_LIST_TOKEN_SEND_FAILED.equals(fragment0)) {
+ } else {
+ final String unreadString = fragment0;
+ final String priorityString = fragments[i++];
+ String nameString = fragments[i++];
+ final boolean unread = unreadStatusIsForced
+ ? forcedUnreadStatus : Integer.parseInt(unreadString) != 0;
+
+ // We want to include this entry if
+ // 1) The onlyShowUnread flags is not set
+ // 2) The above flag is set, and the message is unread
+ if (!onlyShowUnread || unread) {
+ if (nameString.length() == 0) {
+ nameString = meString.toString();
+ } else {
+ nameString = Html.fromHtml(nameString).toString();
+ }
+ if (numCharsToRemovePerWord != 0) {
+ nameString = nameString.substring(
+ 0, Math.max(nameString.length() - numCharsToRemovePerWord, 0));
+ }
+ final int priority = Integer.parseInt(priorityString);
+ if (priority <= maxPriorityToInclude) {
+ if (lastFragment != null && !lastFragment.equals(nameString)) {
+ addStyledFragment(
+ senderBuilder, lastFragment.concat(","), lastStyle, false);
+ senderBuilder.append(" ");
+ }
+ lastFragment = nameString;
+ lastStyle = unread ? unreadStyle : readStyle;
+ } else {
+ if (lastFragment != null) {
+ addStyledFragment(senderBuilder, lastFragment, lastStyle, false);
+ // Adjacent spans can cause the TextView in Gmail widget
+ // confused and leads to weird behavior on scrolling.
+ // Our workaround here is to separate the spans by
+ // spaces.
+ senderBuilder.append(" ");
+ addStyledFragment(senderBuilder, "..", lastStyle, true);
+ senderBuilder.append(" ");
+ }
+ lastFragment = null;
+ }
+ }
+ }
+ }
+ if (lastFragment != null) {
+ addStyledFragment(senderBuilder, lastFragment, lastStyle, false);
+ }
+ senderBuilder.append(numMessagesFragment);
+ if (fixedFragmentLength != 0) {
+ statusBuilder.append(fixedFragment);
+ }
+ }
+
+ /**
+ * Clears the notifications for the specified account/folder/conversation.
+ */
+ public static void clearFolderNotification(Context context, Account account, Folder folder) {
+ LogUtils.v(LOG_TAG, "Clearing all notifications for %s/%s", account.name, folder.name);
+ final NotificationMap notificationMap = getNotificationMap(context);
+ final NotificationKey key = new NotificationKey(account, folder);
+ notificationMap.remove(key);
+ notificationMap.saveNotificationMap(context);
+
+ markSeen(context, folder);
+ }
+
+ private static ArrayList<Long> findContacts(Context context, Collection<String> addresses) {
+ ArrayList<String> whereArgs = new ArrayList<String>();
+ StringBuilder whereBuilder = new StringBuilder();
+ String[] questionMarks = new String[addresses.size()];
+
+ whereArgs.addAll(addresses);
+ Arrays.fill(questionMarks, "?");
+ whereBuilder.append(Email.DATA1 + " IN (").
+ append(TextUtils.join(",", questionMarks)).
+ append(")");
+
+ ContentResolver resolver = context.getContentResolver();
+ Cursor c = resolver.query(Email.CONTENT_URI,
+ new String[]{Email.CONTACT_ID}, whereBuilder.toString(),
+ whereArgs.toArray(new String[0]), null);
+
+ ArrayList<Long> contactIds = new ArrayList<Long>();
+ if (c == null) {
+ return contactIds;
+ }
+ try {
+ while (c.moveToNext()) {
+ contactIds.add(c.getLong(0));
+ }
+ } finally {
+ c.close();
+ }
+ return contactIds;
+ }
+
+ private static Bitmap getContactIcon(
+ Context context, String senderAddress, final Folder folder) {
+ if (senderAddress == null) {
+ return null;
+ }
+ Bitmap icon = null;
+ ArrayList<Long> contactIds = findContacts(
+ context, Arrays.asList(new String[] { senderAddress }));
+
+ if (contactIds != null) {
+ // Get the ideal size for this icon.
+ final Resources res = context.getResources();
+ final int idealIconHeight =
+ res.getDimensionPixelSize(android.R.dimen.notification_large_icon_height);
+ final int idealIconWidth =
+ res.getDimensionPixelSize(android.R.dimen.notification_large_icon_width);
+ for (long id : contactIds) {
+ final Uri contactUri =
+ ContentUris.withAppendedId(ContactsContract.Contacts.CONTENT_URI, id);
+ final Uri photoUri = Uri.withAppendedPath(contactUri, Photo.CONTENT_DIRECTORY);
+ final Cursor cursor = context.getContentResolver().query(
+ photoUri, new String[] { Photo.PHOTO }, null, null, null);
+
+ if (cursor != null) {
+ try {
+ if (cursor.moveToFirst()) {
+ byte[] data = cursor.getBlob(0);
+ if (data != null) {
+ icon = BitmapFactory.decodeStream(new ByteArrayInputStream(data));
+ if (icon != null && icon.getHeight() < idealIconHeight) {
+ // We should scale this image to fit the intended size
+ icon = Bitmap.createScaledBitmap(
+ icon, idealIconWidth, idealIconHeight, true);
+ }
+ if (icon != null) {
+ break;
+ }
+ }
+ }
+ } finally {
+ cursor.close();
+ }
+ }
+ }
+ }
+ if (icon == null) {
+ // icon should be the default gmail icon.
+ icon = getDefaultNotificationIcon(context, folder, false /* single new message */);
+ }
+ return icon;
+ }
+
+ private static String getMessageBodyWithoutElidedText(final Message message) {
+ return getMessageBodyWithoutElidedText(message.getBodyAsHtml());
+ }
+
+ public static String getMessageBodyWithoutElidedText(String html) {
+ if (TextUtils.isEmpty(html)) {
+ return "";
+ }
+ // Get the html "tree" for this message body
+ final HtmlTree htmlTree = com.android.mail.utils.Utils.getHtmlTree(html);
+ htmlTree.setPlainTextConverterFactory(MESSAGE_CONVERTER_FACTORY);
+
+ return htmlTree.getPlainText();
+ }
+
+ public static void markSeen(final Context context, final Folder folder) {
+ final Uri uri = folder.uri;
+
+ final ContentValues values = new ContentValues(1);
+ values.put(UIProvider.ConversationColumns.SEEN, 1);
+
+ context.getContentResolver().update(uri, values, null, null);
+ }
+
+ /**
+ * Returns a displayable string representing
+ * the message sender. It has a preference toward showing the name,
+ * but will fall back to the address if that is all that is available.
+ */
+ private static String getDisplayableSender(String sender) {
+ 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 we were unable to tokenize a name or address,
+ // just use whatever was in the sender.
+ if (TextUtils.isEmpty(displayableSender)) {
+ displayableSender = sender;
+ }
+ return displayableSender;
+ }
+
+ /**
+ * Returns only the address portion of a message sender.
+ */
+ private static String getSenderAddress(String sender) {
+ final EmailAddress address = EmailAddress.getEmailAddress(sender);
+
+ String tokenizedAddress = address.getAddress();
+
+ // If we were unable to tokenize a name or address,
+ // just use whatever was in the sender.
+ if (TextUtils.isEmpty(tokenizedAddress)) {
+ tokenizedAddress = sender;
+ }
+ return tokenizedAddress;
+ }
+
+ public static int getNotificationId(final String account, final Folder folder) {
+ return 1 ^ account.hashCode() ^ folder.hashCode();
+ }
+
+ private static class NotificationKey {
+ public final Account account;
+ public final Folder folder;
+
+ public NotificationKey(Account account, Folder folder) {
+ this.account = account;
+ this.folder = folder;
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ if (!(other instanceof NotificationKey)) {
+ return false;
+ }
+ NotificationKey key = (NotificationKey) other;
+ return account.equals(key.account) && folder.equals(key.folder);
+ }
+
+ @Override
+ public String toString() {
+ return account.toString() + " " + folder.name;
+ }
+
+ @Override
+ public int hashCode() {
+ final int accountHashCode = account.hashCode();
+ final int folderHashCode = folder.hashCode();
+ return accountHashCode ^ folderHashCode;
+ }
+ }
+
+ /**
+ * Contains the logic for converting the contents of one HtmlTree into
+ * plaintext.
+ */
+ public static class MailMessagePlainTextConverter extends HtmlTree.DefaultPlainTextConverter {
+ // Strings for parsing html message bodies
+ private static final String ELIDED_TEXT_ELEMENT_NAME = "div";
+ private static final String ELIDED_TEXT_ELEMENT_ATTRIBUTE_NAME = "class";
+ private static final String ELIDED_TEXT_ELEMENT_ATTRIBUTE_CLASS_VALUE = "elided-text";
+
+ private static final HTML.Attribute ELIDED_TEXT_ATTRIBUTE =
+ new HTML.Attribute(ELIDED_TEXT_ELEMENT_ATTRIBUTE_NAME, HTML.Attribute.NO_TYPE);
+
+ private static final HtmlDocument.Node ELIDED_TEXT_REPLACEMENT_NODE =
+ HtmlDocument.createSelfTerminatingTag(HTML4.BR_ELEMENT, null, null, null);
+
+ private int mEndNodeElidedTextBlock = -1;
+
+ @Override
+ public void addNode(HtmlDocument.Node n, int nodeNum, int endNum) {
+ // If we are in the middle of an elided text block, don't add this node
+ if (nodeNum < mEndNodeElidedTextBlock) {
+ return;
+ } else if (nodeNum == mEndNodeElidedTextBlock) {
+ super.addNode(ELIDED_TEXT_REPLACEMENT_NODE, nodeNum, endNum);
+ return;
+ }
+
+ // If this tag starts another elided text block, we want to remember the end
+ if (n instanceof HtmlDocument.Tag) {
+ boolean foundElidedTextTag = false;
+ final HtmlDocument.Tag htmlTag = (HtmlDocument.Tag)n;
+ final HTML.Element htmlElement = htmlTag.getElement();
+ if (ELIDED_TEXT_ELEMENT_NAME.equals(htmlElement.getName())) {
+ // Make sure that the class is what is expected
+ final List<HtmlDocument.TagAttribute> attributes =
+ htmlTag.getAttributes(ELIDED_TEXT_ATTRIBUTE);
+ for (HtmlDocument.TagAttribute attribute : attributes) {
+ if (ELIDED_TEXT_ELEMENT_ATTRIBUTE_CLASS_VALUE.equals(
+ attribute.getValue())) {
+ // Found an "elided-text" div. Remember information about this tag
+ mEndNodeElidedTextBlock = endNum;
+ foundElidedTextTag = true;
+ break;
+ }
+ }
+ }
+
+ if (foundElidedTextTag) {
+ return;
+ }
+ }
+
+ super.addNode(n, nodeNum, endNum);
+ }
+ }
+
+ /**
+ * During account setup in Email, we may not have an inbox yet, so the notification setting had
+ * to be stored in {@link AccountPreferences}. If it is still there, we need to move it to the
+ * {@link FolderPreferences} now.
+ */
+ public static void moveNotificationSetting(final AccountPreferences accountPreferences,
+ final FolderPreferences folderPreferences) {
+ if (accountPreferences.isDefaultInboxNotificationsEnabledSet()) {
+ // If this setting has been changed some other way, don't overwrite it
+ if (!folderPreferences.isNotificationsEnabledSet()) {
+ final boolean notificationsEnabled =
+ accountPreferences.getDefaultInboxNotificationsEnabled();
+
+ folderPreferences.setNotificationsEnabled(notificationsEnabled);
+ }
+
+ accountPreferences.clearDefaultInboxNotificationsEnabled();
+ }
+ }
+}
diff --git a/src/com/android/mail/utils/Observable.java b/src/com/android/mail/utils/Observable.java
new file mode 100644
index 0000000..81e8d83
--- /dev/null
+++ b/src/com/android/mail/utils/Observable.java
@@ -0,0 +1,49 @@
+/*
+ * 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.utils;
+
+import android.database.DataSetObservable;
+import android.database.DataSetObserver;
+
+/**
+ * A Utility class to register observers and return logging and counts for the number of registered
+ * observers.
+ */
+public class Observable extends DataSetObservable {
+ protected static final String LOG_TAG = LogTag.getLogTag();
+ private final String mName;
+
+ public Observable(String name) {
+ mName = name;
+ }
+
+ @Override
+ public void registerObserver(DataSetObserver observer) {
+ final int count = mObservers.size();
+ super.registerObserver(observer);
+ LogUtils.d(LOG_TAG, "IN register(%s)Observer: %s before=%d after=%d",
+ mName, observer, count, mObservers.size());
+ }
+
+ @Override
+ public void unregisterObserver(DataSetObserver observer) {
+ final int count = mObservers.size();
+ super.unregisterObserver(observer);
+ LogUtils.d(LOG_TAG, "IN unregister(%s)Observer: %s before=%d after=%d",
+ mName, observer, count, mObservers.size());
+ }
+}
diff --git a/src/com/android/mail/utils/Utils.java b/src/com/android/mail/utils/Utils.java
index 8e8e860..a0453a5 100644
--- a/src/com/android/mail/utils/Utils.java
+++ b/src/com/android/mail/utils/Utils.java
@@ -26,11 +26,11 @@
import android.app.SearchManager;
import android.content.Context;
import android.content.Intent;
+import android.content.pm.PackageInfo;
import android.content.pm.PackageManager.NameNotFoundException;
import android.content.res.Resources;
import android.database.Cursor;
import android.graphics.Bitmap;
-import android.graphics.BitmapFactory;
import android.graphics.Typeface;
import android.net.Uri;
import android.os.AsyncTask;
@@ -59,12 +59,12 @@
import com.android.mail.R;
import com.android.mail.browse.ConversationCursor;
import com.android.mail.compose.ComposeActivity;
+import com.android.mail.perf.SimpleTimer;
import com.android.mail.providers.Account;
import com.android.mail.providers.Conversation;
import com.android.mail.providers.Folder;
import com.android.mail.providers.UIProvider;
import com.android.mail.providers.UIProvider.EditSettingsExtras;
-import com.android.mail.ui.ControllableActivity;
import com.android.mail.ui.FeedbackEnabledActivity;
import org.json.JSONObject;
@@ -97,7 +97,6 @@
public static final String EXTRA_FOLDER_URI = "folderUri";
public static final String EXTRA_COMPOSE_URI = "composeUri";
public static final String EXTRA_CONVERSATION = "conversationUri";
- public static final String EXTRA_FOLDER = "folder";
/** Extra tag for debugging the blank fragment problem. */
public static final String VIEW_DEBUGGING_TAG = "MailBlankFragment";
@@ -119,8 +118,14 @@
private static final int SCALED_SCREENSHOT_MAX_HEIGHT_WIDTH = 600;
+ private static final String APP_VERSION_QUERY_PARAMETER = "appVersion";
+
private static final String LOG_TAG = LogTag.getLogTag();
+ public static final boolean ENABLE_CONV_LOAD_TIMER = false;
+ public static final SimpleTimer sConvLoadTimer =
+ new SimpleTimer(ENABLE_CONV_LOAD_TIMER).withSessionName("ConvLoadTimer");
+
public static boolean isRunningJellybeanOrLater() {
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN;
}
@@ -661,14 +666,15 @@
* @param account
* @return
*/
- public static Intent createViewConversationIntent(Conversation conversation, Folder folder,
- Account account) {
+ public static Intent createViewConversationIntent(final Context context,
+ Conversation conversation, final Uri folderUri, Account account) {
final Intent intent = new Intent(Intent.ACTION_VIEW);
- intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK |
- Intent.FLAG_ACTIVITY_TASK_ON_HOME);
- intent.setDataAndType(conversation.uri, account.mimeType);
+ intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK
+ | Intent.FLAG_ACTIVITY_TASK_ON_HOME);
+ intent.setDataAndType(appendVersionQueryParameter(context, conversation.uri),
+ account.mimeType);
intent.putExtra(Utils.EXTRA_ACCOUNT, account.serialize());
- intent.putExtra(Utils.EXTRA_FOLDER, Folder.toString(folder));
+ intent.putExtra(Utils.EXTRA_FOLDER_URI, folderUri);
intent.putExtra(Utils.EXTRA_CONVERSATION, conversation);
return intent;
}
@@ -680,18 +686,19 @@
* @param account
* @return
*/
- public static Intent createViewFolderIntent(Folder folder, Account account) {
- if (folder == null || account == null) {
- LogUtils.wtf(
- LOG_TAG, "Utils.createViewFolderIntent(%s,%s): Bad input", folder, account);
+ public static Intent createViewFolderIntent(final Context context, final Uri folderUri,
+ Account account) {
+ if (folderUri == null || account == null) {
+ LogUtils.wtf(LOG_TAG, "Utils.createViewFolderIntent(%s,%s): Bad input", folderUri,
+ account);
return null;
}
final Intent intent = new Intent(Intent.ACTION_VIEW);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK
| Intent.FLAG_ACTIVITY_TASK_ON_HOME);
- intent.setDataAndType(folder.uri, account.mimeType);
+ intent.setDataAndType(appendVersionQueryParameter(context, folderUri), account.mimeType);
intent.putExtra(Utils.EXTRA_ACCOUNT, account.serialize());
- intent.putExtra(Utils.EXTRA_FOLDER, Folder.toString(folder));
+ intent.putExtra(Utils.EXTRA_FOLDER_URI, folderUri);
return intent;
}
@@ -825,17 +832,18 @@
* Show the settings screen for the supplied account.
*/
public static void showFolderSettings(Context context, Account account, Folder folder) {
- if (account == null || folder == null) {
- LogUtils.e(LOG_TAG, "Invalid attempt to show folder settings. account: %s folder: %s",
- account, folder);
- return;
- }
- final Intent settingsIntent = new Intent(Intent.ACTION_EDIT, account.settingsIntentUri);
+ if (account == null || folder == null) {
+ LogUtils.e(LOG_TAG, "Invalid attempt to show folder settings. account: %s folder: %s",
+ account, folder);
+ return;
+ }
+ final Intent settingsIntent = new Intent(Intent.ACTION_EDIT,
+ appendVersionQueryParameter(context, account.settingsIntentUri));
- settingsIntent.putExtra(EditSettingsExtras.EXTRA_ACCOUNT, account);
- settingsIntent.putExtra(EditSettingsExtras.EXTRA_FOLDER, folder);
- settingsIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET);
- context.startActivity(settingsIntent);
+ settingsIntent.putExtra(EditSettingsExtras.EXTRA_ACCOUNT, account);
+ settingsIntent.putExtra(EditSettingsExtras.EXTRA_FOLDER, folder);
+ settingsIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET);
+ context.startActivity(settingsIntent);
}
/**
@@ -859,7 +867,7 @@
*/
public static void sendFeedback(FeedbackEnabledActivity activity, Account account,
boolean reportingProblem) {
- if (activity != null && account != null && account.sendFeedbackIntentUri != null) {
+ if (activity != null && account != null && !isEmpty(account.sendFeedbackIntentUri)) {
final Bundle optionalExtras = new Bundle(2);
optionalExtras.putBoolean(
UIProvider.SendFeedbackExtras.EXTRA_REPORTING_PROBLEM, reportingProblem);
@@ -1228,4 +1236,19 @@
final Intent intent = ComposeActivity.createForwardIntent(context, account, messageUri);
return intent;
}
+
+ public static Uri appendVersionQueryParameter(final Context context, final Uri uri) {
+ int appVersion = 0;
+
+ try {
+ final PackageInfo packageInfo =
+ context.getPackageManager().getPackageInfo(context.getPackageName(), 0);
+ appVersion = packageInfo.versionCode;
+ } catch (final NameNotFoundException e) {
+ LogUtils.wtf(LOG_TAG, e, "Couldn't find our own PackageInfo");
+ }
+
+ return uri.buildUpon().appendQueryParameter(APP_VERSION_QUERY_PARAMETER,
+ Integer.toString(appVersion)).build();
+ }
}
diff --git a/src/com/android/mail/widget/BaseWidgetProvider.java b/src/com/android/mail/widget/BaseWidgetProvider.java
index c02c5ae..d3f3364 100644
--- a/src/com/android/mail/widget/BaseWidgetProvider.java
+++ b/src/com/android/mail/widget/BaseWidgetProvider.java
@@ -25,6 +25,7 @@
import android.content.Intent;
import android.database.Cursor;
import android.net.Uri;
+import android.os.AsyncTask;
import android.os.Bundle;
import android.text.TextUtils;
import android.view.View;
@@ -47,12 +48,14 @@
public abstract class BaseWidgetProvider extends AppWidgetProvider {
public static final String EXTRA_ACCOUNT = "account";
- public static final String EXTRA_FOLDER = "folder";
+ public static final String EXTRA_FOLDER_URI = "folder-uri";
+ public static final String EXTRA_FOLDER_CONVERSATION_LIST_URI = "folder-conversation-list-uri";
+ public static final String EXTRA_FOLDER_DISPLAY_NAME = "folder-display-name";
public static final String EXTRA_UNREAD = "unread";
public static final String EXTRA_UPDATE_ALL_WIDGETS = "update-all-widgets";
public static final String WIDGET_ACCOUNT_PREFIX = "widget-account-";
- static final String ACCOUNT_FOLDER_PREFERENCE_SEPARATOR = " ";
+ public static final String ACCOUNT_FOLDER_PREFERENCE_SEPARATOR = " ";
protected static final String ACTION_UPDATE_WIDGET = "com.android.mail.ACTION_UPDATE_WIDGET";
@@ -118,9 +121,14 @@
if (ACTION_UPDATE_WIDGET.equals(action)) {
final int widgetId = intent.getIntExtra(EXTRA_WIDGET_ID, -1);
final Account account = Account.newinstance(intent.getStringExtra(EXTRA_ACCOUNT));
- Folder folder = Folder.fromString(intent.getStringExtra(EXTRA_FOLDER));
- if (widgetId != -1 && account != null && folder != null) {
- updateWidgetInternal(context, widgetId, account, folder);
+ final Uri folderUri = intent.getParcelableExtra(EXTRA_FOLDER_URI);
+ final Uri folderConversationListUri =
+ intent.getParcelableExtra(EXTRA_FOLDER_CONVERSATION_LIST_URI);
+ final String folderDisplayName = intent.getStringExtra(EXTRA_FOLDER_DISPLAY_NAME);
+
+ if (widgetId != -1 && account != null && folderUri != null) {
+ updateWidgetInternal(context, widgetId, account, folderUri,
+ folderConversationListUri, folderDisplayName);
}
} else if (Utils.ACTION_NOTIFY_DATASET_CHANGED.equals(action)) {
// Receive notification for a certain account.
@@ -173,55 +181,75 @@
super.onUpdate(context, appWidgetManager, appWidgetIds);
// Update each of the widgets with a remote adapter
- ContentResolver resolver = context.getContentResolver();
- for (int i = 0; i < appWidgetIds.length; ++i) {
- // Get the account for this widget from preference
- final String accountFolder = MailPrefs.get(context).getWidgetConfiguration(
- appWidgetIds[i]);
- String accountUri = null;
- Uri folderUri = null;
- if (!TextUtils.isEmpty(accountFolder)) {
- final String[] parsedInfo = TextUtils.split(accountFolder,
- ACCOUNT_FOLDER_PREFERENCE_SEPARATOR);
- if (parsedInfo.length == 2) {
- accountUri = parsedInfo[0];
- folderUri = Uri.parse(parsedInfo[1]);
- } else {
- accountUri = accountFolder;
- folderUri = Uri.EMPTY;
- }
- }
- // account will be null the first time a widget is created. This is
- // OK, as isAccountValid will return false, allowing the widget to
- // be configured.
- // Lookup the account by URI.
- Account account = null;
- if (!TextUtils.isEmpty(accountUri)) {
- account = getAccountObject(context, accountUri);
- }
- if (Utils.isEmpty(folderUri) && account != null) {
- folderUri = account.settings.defaultInbox;
- }
- Folder folder = null;
- if (!Utils.isEmpty(folderUri)) {
- Cursor folderCursor = null;
- try {
- folderCursor = resolver.query(folderUri,
- UIProvider.FOLDERS_PROJECTION, null, null, null);
- if (folderCursor != null) {
+ new BulkUpdateAsyncTask(context, appWidgetIds).execute((Void[]) null);
+ }
+
+ private class BulkUpdateAsyncTask extends AsyncTask<Void, Void, Void> {
+ private final Context mContext;
+ private final int[] mAppWidgetIds;
+
+ public BulkUpdateAsyncTask(final Context context, final int[] appWidgetIds) {
+ mContext = context;
+ mAppWidgetIds = appWidgetIds;
+ }
+
+ @Override
+ protected Void doInBackground(final Void... params) {
+ for (int i = 0; i < mAppWidgetIds.length; ++i) {
+ // Get the account for this widget from preference
+ final String accountFolder = MailPrefs.get(mContext).getWidgetConfiguration(
+ mAppWidgetIds[i]);
+ String accountUri = null;
+ Uri folderUri = null;
+ if (!TextUtils.isEmpty(accountFolder)) {
+ final String[] parsedInfo = TextUtils.split(accountFolder,
+ ACCOUNT_FOLDER_PREFERENCE_SEPARATOR);
+ if (parsedInfo.length == 2) {
+ accountUri = parsedInfo[0];
+ folderUri = Uri.parse(parsedInfo[1]);
+ } else {
+ accountUri = accountFolder;
+ folderUri = Uri.EMPTY;
+ }
+ }
+ // account will be null the first time a widget is created. This is
+ // OK, as isAccountValid will return false, allowing the widget to
+ // be configured.
+
+ // Lookup the account by URI.
+ Account account = null;
+ if (!TextUtils.isEmpty(accountUri)) {
+ account = getAccountObject(mContext, accountUri);
+ }
+ if (Utils.isEmpty(folderUri) && account != null) {
+ folderUri = account.settings.defaultInbox;
+ }
+
+ Folder folder = null;
+
+ if (folderUri != null) {
+ final Cursor folderCursor =
+ mContext.getContentResolver().query(folderUri,
+ UIProvider.FOLDERS_PROJECTION, null, null, null);
+
+ try {
if (folderCursor.moveToFirst()) {
folder = new Folder(folderCursor);
}
- }
- } finally {
- if (folderCursor != null) {
+ } finally {
folderCursor.close();
}
}
+
+ updateWidgetInternal(mContext, mAppWidgetIds[i], account, folderUri,
+ folder == null ? null : folder.conversationListUri, folder == null ? null
+ : folder.name);
}
- updateWidgetInternal(context, appWidgetIds[i], account, folder);
+
+ return null;
}
+
}
protected Account getAccountObject(Context context, String accountUri) {
@@ -248,10 +276,11 @@
* Update the widget appWidgetId with the given account and folder
*/
public static void updateWidget(Context context, int appWidgetId, Account account,
- Folder folder) {
- if (account == null || folder == null) {
+ final Uri folderUri, final Uri folderConversationListUri,
+ final String folderDisplayName) {
+ if (account == null || folderUri == null) {
LogUtils.e(LOG_TAG,
- "Missing account or folder. account: %s folder %s", account, folder);
+ "Missing account or folder. account: %s folder %s", account, folderUri);
return;
}
final Intent updateWidgetIntent = new Intent(ACTION_UPDATE_WIDGET);
@@ -259,17 +288,20 @@
updateWidgetIntent.setType(account.mimeType);
updateWidgetIntent.putExtra(EXTRA_WIDGET_ID, appWidgetId);
updateWidgetIntent.putExtra(EXTRA_ACCOUNT, account.serialize());
- updateWidgetIntent.putExtra(EXTRA_FOLDER, Folder.toString(folder));
+ updateWidgetIntent.putExtra(EXTRA_FOLDER_URI, folderUri);;
+ updateWidgetIntent.putExtra(EXTRA_FOLDER_CONVERSATION_LIST_URI, folderConversationListUri);
+ updateWidgetIntent.putExtra(EXTRA_FOLDER_DISPLAY_NAME, folderDisplayName);
context.sendBroadcast(updateWidgetIntent);
}
protected void updateWidgetInternal(Context context, int appWidgetId, Account account,
- Folder folder) {
+ final Uri folderUri, final Uri folderConversationListUri,
+ final String folderDisplayName) {
RemoteViews remoteViews = new RemoteViews(context.getPackageName(), R.layout.widget);
final boolean isAccountValid = isAccountValid(context, account);
- if (!isAccountValid || folder == null) {
+ if (!isAccountValid || folderUri == null) {
// Widget has not been configured yet
remoteViews.setViewVisibility(R.id.widget_folder, View.GONE);
remoteViews.setViewVisibility(R.id.widget_account, View.GONE);
@@ -291,7 +323,8 @@
remoteViews.setOnClickPendingIntent(R.id.widget_configuration, clickIntent);
} else {
// Set folder to a space here to avoid flicker.
- configureValidAccountWidget(context, remoteViews, appWidgetId, account, folder, " ");
+ configureValidAccountWidget(context, remoteViews, appWidgetId, account, folderUri,
+ folderConversationListUri, folderDisplayName == null ? " " : folderDisplayName);
}
AppWidgetManager.getInstance(context).updateAppWidget(appWidgetId, remoteViews);
@@ -310,9 +343,10 @@
}
protected void configureValidAccountWidget(Context context, RemoteViews remoteViews,
- int appWidgetId, Account account, Folder folder, String folderDisplayName) {
+ int appWidgetId, Account account, final Uri folderUri,
+ final Uri folderConversationListUri, String folderDisplayName) {
WidgetService.configureValidAccountWidget(context, remoteViews, appWidgetId, account,
- folder, folderDisplayName, WidgetService.class);
+ folderUri, folderConversationListUri, folderDisplayName, WidgetService.class);
}
private final void migrateAllLegacyWidgetInformation(Context context) {
@@ -334,5 +368,4 @@
* Abstract method allowing extending classes to perform widget migration
*/
protected abstract void migrateLegacyWidgetInformation(Context context, int widgetId);
-
}
diff --git a/src/com/android/mail/widget/WidgetConversationViewBuilder.java b/src/com/android/mail/widget/WidgetConversationViewBuilder.java
index 6c30570..4a4ae64 100644
--- a/src/com/android/mail/widget/WidgetConversationViewBuilder.java
+++ b/src/com/android/mail/widget/WidgetConversationViewBuilder.java
@@ -17,7 +17,6 @@
package com.android.mail.widget;
import com.android.mail.R;
-import com.android.mail.providers.Account;
import com.android.mail.providers.Conversation;
import com.android.mail.providers.Folder;
import com.android.mail.ui.FolderDisplayer;
@@ -27,6 +26,7 @@
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Typeface;
+import android.net.Uri;
import android.text.Spannable;
import android.text.SpannableStringBuilder;
import android.text.style.AbsoluteSizeSpan;
@@ -66,11 +66,11 @@
* Load Conversation Labels
*/
@Override
- public void loadConversationFolders(Conversation conv, Folder ignoreFolder) {
- super.loadConversationFolders(conv, ignoreFolder);
+ public void loadConversationFolders(Conversation conv, final Uri ignoreFolderUri) {
+ super.loadConversationFolders(conv, ignoreFolderUri);
}
- private int getFolderViewId(int position) {
+ private static int getFolderViewId(int position) {
switch (position) {
case 0:
return R.id.widget_folder_0;
@@ -111,7 +111,7 @@
/*
* Get font sizes and bitmaps from Resources
*/
- public WidgetConversationViewBuilder(Context context, Account account) {
+ public WidgetConversationViewBuilder(Context context) {
mContext = context;
Resources res = context.getResources();
@@ -131,7 +131,7 @@
/*
* Add size, color and style to a given text
*/
- private CharSequence addStyle(CharSequence text, int size, int color) {
+ private static CharSequence addStyle(CharSequence text, int size, int color) {
SpannableStringBuilder builder = new SpannableStringBuilder(text);
builder.setSpan(
new AbsoluteSizeSpan(size), 0, text.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
@@ -145,9 +145,8 @@
/*
* Return the full View
*/
- public RemoteViews getStyledView(CharSequence status, CharSequence date,
- Conversation conversation, Folder currentFolder, SpannableStringBuilder senders,
- String filteredSubject) {
+ public RemoteViews getStyledView(CharSequence date, Conversation conversation,
+ final Uri folderUri, SpannableStringBuilder senders, String filteredSubject) {
final boolean isUnread = !conversation.read;
String snippet = conversation.getSnippet();
@@ -195,7 +194,7 @@
}
if (mContext.getResources().getBoolean(R.bool.display_folder_colors_in_widget)) {
mFolderDisplayer = new WidgetFolderDisplayer(mContext);
- mFolderDisplayer.loadConversationFolders(conversation, currentFolder);
+ mFolderDisplayer.loadConversationFolders(conversation, folderUri);
mFolderDisplayer.displayFolders(remoteViews);
}
diff --git a/src/com/android/mail/widget/WidgetService.java b/src/com/android/mail/widget/WidgetService.java
index fd31bf1..3db1dcf 100644
--- a/src/com/android/mail/widget/WidgetService.java
+++ b/src/com/android/mail/widget/WidgetService.java
@@ -43,8 +43,6 @@
import com.android.mail.preferences.MailPrefs;
import com.android.mail.providers.Account;
import com.android.mail.providers.Conversation;
-import com.android.mail.providers.ConversationInfo;
-import com.android.mail.providers.Folder;
import com.android.mail.providers.UIProvider;
import com.android.mail.providers.UIProvider.ConversationListQueryParameters;
import com.android.mail.utils.AccountUtils;
@@ -68,19 +66,19 @@
return new MailFactory(getApplicationContext(), intent, this);
}
-
protected void configureValidAccountWidget(Context context, RemoteViews remoteViews,
- int appWidgetId, Account account, Folder folder, String folderName) {
- configureValidAccountWidget(context, remoteViews, appWidgetId, account, folder, folderName,
- WidgetService.class);
+ int appWidgetId, Account account, final Uri folderUri,
+ final Uri folderConversationListUri, String folderName) {
+ configureValidAccountWidget(context, remoteViews, appWidgetId, account, folderUri,
+ folderConversationListUri, folderName, WidgetService.class);
}
/**
* Modifies the remoteView for the given account and folder.
*/
public static void configureValidAccountWidget(Context context, RemoteViews remoteViews,
- int appWidgetId, Account account, Folder folder, String folderDisplayName,
- Class<?> widgetService) {
+ int appWidgetId, Account account, final Uri folderUri,
+ final Uri folderConversationListUri, String folderDisplayName, Class<?> widgetService) {
remoteViews.setViewVisibility(R.id.widget_folder, View.VISIBLE);
// If the folder or account name are empty, we don't want to overwrite the valid data that
@@ -107,11 +105,12 @@
remoteViews.setEmptyView(R.id.conversation_list, R.id.empty_conversation_list);
WidgetService.configureValidWidgetIntents(context, remoteViews, appWidgetId, account,
- folder, folderDisplayName, widgetService);
+ folderUri, folderConversationListUri, folderDisplayName, widgetService);
}
public static void configureValidWidgetIntents(Context context, RemoteViews remoteViews,
- int appWidgetId, Account account, Folder folder, String folderDisplayName,
+ int appWidgetId, Account account, final Uri folderUri,
+ final Uri folderConversationListUri, final String folderDisplayName,
Class<?> serviceClass) {
remoteViews.setViewVisibility(R.id.widget_configuration, View.GONE);
@@ -120,11 +119,14 @@
final Intent intent = new Intent(context, serviceClass);
intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId);
intent.putExtra(BaseWidgetProvider.EXTRA_ACCOUNT, account.serialize());
- intent.putExtra(BaseWidgetProvider.EXTRA_FOLDER, Folder.toString(folder));
+ intent.putExtra(BaseWidgetProvider.EXTRA_FOLDER_URI, folderUri);
+ intent.putExtra(BaseWidgetProvider.EXTRA_FOLDER_CONVERSATION_LIST_URI,
+ folderConversationListUri);
+ intent.putExtra(BaseWidgetProvider.EXTRA_FOLDER_DISPLAY_NAME, folderDisplayName);
intent.setData(Uri.parse(intent.toUri(Intent.URI_INTENT_SCHEME)));
remoteViews.setRemoteAdapter(R.id.conversation_list, intent);
// Open mail app when click on header
- final Intent mailIntent = Utils.createViewFolderIntent(folder, account);
+ final Intent mailIntent = Utils.createViewFolderIntent(context, folderUri, account);
PendingIntent clickIntent = PendingIntent.getActivity(context, 0, mailIntent,
PendingIntent.FLAG_UPDATE_CURRENT);
remoteViews.setOnClickPendingIntent(R.id.widget_header, clickIntent);
@@ -159,15 +161,14 @@
* Persists the information about the specified widget.
*/
public static void saveWidgetInformation(Context context, int appWidgetId, Account account,
- Folder folder) {
- MailPrefs.get(context).configureWidget(appWidgetId, account, folder);
+ final String folderUri) {
+ MailPrefs.get(context).configureWidget(appWidgetId, account, folderUri);
}
/**
* Returns true if this widget id has been configured and saved.
*/
- public boolean isWidgetConfigured(Context context, int appWidgetId, Account account,
- Folder folder) {
+ public boolean isWidgetConfigured(Context context, int appWidgetId, Account account) {
return isAccountValid(context, account) &&
MailPrefs.get(context).isWidgetConfigured(appWidgetId);
}
@@ -198,7 +199,9 @@
private final Context mContext;
private final int mAppWidgetId;
private final Account mAccount;
- private Folder mFolder;
+ private final Uri mFolderUri;
+ private final Uri mFolderConversationListUri;
+ private final String mFolderDisplayName;
private final WidgetConversationViewBuilder mWidgetConversationViewBuilder;
private CursorLoader mConversationCursorLoader;
private Cursor mConversationCursor;
@@ -218,9 +221,12 @@
mAppWidgetId = intent.getIntExtra(
AppWidgetManager.EXTRA_APPWIDGET_ID, AppWidgetManager.INVALID_APPWIDGET_ID);
mAccount = Account.newinstance(intent.getStringExtra(WidgetProvider.EXTRA_ACCOUNT));
- mFolder = Folder.fromString(intent.getStringExtra(WidgetProvider.EXTRA_FOLDER));
- mWidgetConversationViewBuilder = new WidgetConversationViewBuilder(context,
- mAccount);
+ mFolderUri = intent.getParcelableExtra(WidgetProvider.EXTRA_FOLDER_URI);
+ mFolderConversationListUri =
+ intent.getParcelableExtra(WidgetProvider.EXTRA_FOLDER_CONVERSATION_LIST_URI);
+ mFolderDisplayName = intent.getStringExtra(WidgetProvider.EXTRA_FOLDER_DISPLAY_NAME);
+
+ mWidgetConversationViewBuilder = new WidgetConversationViewBuilder(context);
mService = service;
}
@@ -228,12 +234,13 @@
public void onCreate() {
// Save the map between widgetId and account to preference
- saveWidgetInformation(mContext, mAppWidgetId, mAccount, mFolder);
+ saveWidgetInformation(mContext, mAppWidgetId, mAccount, mFolderUri.toString());
// If the account of this widget has been removed, we want to update the widget to
// "Tap to configure" mode.
- if (!mService.isWidgetConfigured(mContext, mAppWidgetId, mAccount, mFolder)) {
- BaseWidgetProvider.updateWidget(mContext, mAppWidgetId, mAccount, mFolder);
+ if (!mService.isWidgetConfigured(mContext, mAppWidgetId, mAccount)) {
+ BaseWidgetProvider.updateWidget(mContext, mAppWidgetId, mAccount, mFolderUri,
+ mFolderConversationListUri, mFolderDisplayName);
}
mFolderInformationShown = false;
@@ -244,7 +251,7 @@
// the user made locally, the default policy of the UI provider is to not send
// notifications for. But in this case, since the widget is not using the
// ConversationCursor instance that the UI is using, the widget would not be updated.
- final Uri.Builder builder = mFolder.conversationListUri.buildUpon();
+ final Uri.Builder builder = mFolderConversationListUri.buildUpon();
final String maxConversations = Integer.toString(MAX_CONVERSATIONS_COUNT);
final Uri widgetConversationQueryUri = builder
.appendQueryParameter(ConversationListQueryParameters.LIMIT, maxConversations)
@@ -262,7 +269,7 @@
mConversationCursorLoader.startLoading();
mSendersSplitToken = res.getString(R.string.senders_split_token);
mElidedPaddingToken = res.getString(R.string.elided_padding_token);
- mFolderLoader = new CursorLoader(mContext, mFolder.uri, UIProvider.FOLDERS_PROJECTION,
+ mFolderLoader = new CursorLoader(mContext, mFolderUri, UIProvider.FOLDERS_PROJECTION,
null, null, null);
mFolderLoader.registerListener(FOLDER_LOADER_ID, this);
mFolderUpdateHandler = new FolderUpdateHandler(
@@ -358,14 +365,12 @@
Conversation conversation = new Conversation(mConversationCursor);
// Split the senders and status from the instructions.
SpannableStringBuilder senderBuilder = new SpannableStringBuilder();
- SpannableStringBuilder statusBuilder = new SpannableStringBuilder();
if (conversation.conversationInfo != null) {
ArrayList<SpannableString> senders = new ArrayList<SpannableString>();
SendersView.format(mContext, conversation.conversationInfo, "",
- MAX_SENDERS_LENGTH, senders, null, null, mAccount.name);
- senderBuilder = ellipsizeStyledSenders(conversation.conversationInfo,
- MAX_SENDERS_LENGTH, senders);
+ MAX_SENDERS_LENGTH, senders, null, null, mAccount.name, true);
+ senderBuilder = ellipsizeStyledSenders(senders);
} else {
senderBuilder.append(conversation.senders);
senderBuilder.setSpan(conversation.read ? getReadStyle() : getUnreadStyle(), 0,
@@ -376,13 +381,14 @@
conversation.dateMs);
// Load up our remote view.
- RemoteViews remoteViews = mWidgetConversationViewBuilder.getStyledView(
- statusBuilder, date, conversation, mFolder, senderBuilder,
- filterTag(conversation.subject));
+ RemoteViews remoteViews =
+ mWidgetConversationViewBuilder.getStyledView(date, conversation,
+ mFolderUri, senderBuilder, filterTag(conversation.subject));
// On click intent.
remoteViews.setOnClickFillInIntent(R.id.widget_conversation,
- Utils.createViewConversationIntent(conversation, mFolder, mAccount));
+ Utils.createViewConversationIntent(mContext, conversation, mFolderUri,
+ mAccount));
return remoteViews;
}
@@ -403,7 +409,7 @@
return CharacterStyle.wrap(mReadStyle);
}
- private SpannableStringBuilder ellipsizeStyledSenders(ConversationInfo info, int maxChars,
+ private SpannableStringBuilder ellipsizeStyledSenders(
ArrayList<SpannableString> styledSenders) {
SpannableStringBuilder builder = new SpannableStringBuilder();
SpannableString prevSender = null;
@@ -429,7 +435,7 @@
return builder;
}
- private SpannableString copyStyles(CharacterStyle[] spans, CharSequence newText) {
+ private static SpannableString copyStyles(CharacterStyle[] spans, CharSequence newText) {
SpannableString s = new SpannableString(newText);
if (spans != null && spans.length > 0) {
s.setSpan(spans[0], 0, s.length(), 0);
@@ -445,7 +451,7 @@
view.setTextViewText(
R.id.loading_text, mContext.getText(R.string.view_more_conversations));
view.setOnClickFillInIntent(R.id.widget_loading,
- Utils.createViewFolderIntent(mFolder, mAccount));
+ Utils.createViewFolderIntent(mContext, mFolderUri, mAccount));
return view;
}
@@ -493,8 +499,8 @@
// manager doesn't cache the state of the remote views when doing a partial
// widget update. This causes the folder name to be shown as blank if the state
// of the widget is restored.
- mService.configureValidAccountWidget(
- mContext, remoteViews, mAppWidgetId, mAccount, mFolder, folderName);
+ mService.configureValidAccountWidget(mContext, remoteViews, mAppWidgetId,
+ mAccount, mFolderUri, mFolderConversationListUri, folderName);
appWidgetManager.updateAppWidget(mAppWidgetId, remoteViews);
mFolderInformationShown = true;
}
@@ -539,7 +545,7 @@
* Returns a boolean indicating whether this cursor has valid data.
* Note: This seeks to the first position in the cursor
*/
- private boolean isDataValid(Cursor cursor) {
+ private static boolean isDataValid(Cursor cursor) {
return cursor != null && !cursor.isClosed() && cursor.moveToFirst();
}
diff --git a/tests/src/com/android/mail/EmailAddressTest.java b/tests/src/com/android/mail/EmailAddressTest.java
new file mode 100644
index 0000000..c4e0a94
--- /dev/null
+++ b/tests/src/com/android/mail/EmailAddressTest.java
@@ -0,0 +1,103 @@
+/*
+ * 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;
+
+import android.test.AndroidTestCase;
+import android.test.suitebuilder.annotation.SmallTest;
+
+@SmallTest
+public class EmailAddressTest extends AndroidTestCase {
+
+ public void testNameRegex() {
+ {
+ EmailAddress email = EmailAddress.getEmailAddress("email@gmail.com");
+ assertEquals("", email.getName());
+ }
+
+ {
+ EmailAddress nameKnown = EmailAddress.getEmailAddress("john doe <coolguy@doe.com>");
+ assertEquals("john doe", nameKnown.getName());
+ }
+
+ {
+ EmailAddress withQuotes = EmailAddress
+ .getEmailAddress("\"john doe\" <coolguy@doe.com>");
+ assertEquals("john doe", withQuotes.getName());
+ }
+
+ {
+ EmailAddress noSpace = EmailAddress.getEmailAddress("john doe<coolguy@doe.com>");
+ assertEquals("john doe", noSpace.getName());
+ }
+
+ {
+ EmailAddress noSpaceWithQuotes = EmailAddress
+ .getEmailAddress("\"john doe\"<coolguy@doe.com>");
+ assertEquals("john doe", noSpaceWithQuotes.getName());
+ }
+
+ }
+
+ /**
+ * Test the parsing of email addresses
+ */
+ public void testEmailAddressParsing() {
+ EmailAddress address = EmailAddress.getEmailAddress("test name <test@localhost.com>");
+ assertEquals("test name", address.getName());
+ assertEquals("test@localhost.com", address.getAddress());
+
+ address = EmailAddress.getEmailAddress("\"test name\" <test@localhost.com>");
+ assertEquals("test name", address.getName());
+ assertEquals("test@localhost.com", address.getAddress());
+
+ address = EmailAddress.getEmailAddress("<test@localhost.com>");
+ assertEquals("", address.getName());
+ assertEquals("test@localhost.com", address.getAddress());
+
+ address = EmailAddress.getEmailAddress("test@localhost.com");
+ assertEquals("", address.getName());
+ assertEquals("test@localhost.com", address.getAddress());
+
+ address = EmailAddress.getEmailAddress("O'brian <test@localhost.com>");
+ assertEquals("O'brian", address.getName());
+ assertEquals("test@localhost.com", address.getAddress());
+
+ address = EmailAddress.getEmailAddress("\"O'brian\" <test@localhost.com>");
+ assertEquals("O'brian", address.getName());
+ assertEquals("test@localhost.com", address.getAddress());
+
+ address = EmailAddress.getEmailAddress("\"\\\"O'brian\\\"\" <test@localhost.com>");
+ assertEquals("\"O'brian\"", address.getName());
+ assertEquals("test@localhost.com", address.getAddress());
+
+
+ // Ensure that white space is trimmed from the name
+
+ // Strings that will match the regular expression
+ address = EmailAddress.getEmailAddress("\" \" <test@localhost.com>");
+ assertEquals("", address.getName());
+
+ address = EmailAddress.getEmailAddress("\" test name \" <test@localhost.com>");
+ assertEquals("test name", address.getName());
+
+ // Strings that will fallthrough to the rfc822 tokenizer
+ address = EmailAddress.getEmailAddress("\"\\\" O'brian \\\"\" <test@localhost.com>");
+ assertEquals("\" O'brian \"", address.getName());
+
+ address = EmailAddress.getEmailAddress("\" \\\"O'brian\\\" \" <test@localhost.com>");
+ assertEquals("\"O'brian\"", address.getName());
+ }
+}
diff --git a/tests/src/com/android/mail/browse/SendersFormattingTests.java b/tests/src/com/android/mail/browse/SendersFormattingTests.java
index 2aa625f..2020c05 100644
--- a/tests/src/com/android/mail/browse/SendersFormattingTests.java
+++ b/tests/src/com/android/mail/browse/SendersFormattingTests.java
@@ -43,8 +43,8 @@
conv.addMessage(info);
ArrayList<SpannableString> strings = new ArrayList<SpannableString>();
ArrayList<String> emailDisplays = null;
- SendersView
- .format(getContext(), conv, "", 100, strings, emailDisplays, emailDisplays, null);
+ SendersView.format(getContext(), conv, "", 100, strings, emailDisplays, emailDisplays,
+ null, false);
assertEquals(1, strings.size());
assertEquals(strings.get(0).toString(), "me");
@@ -52,8 +52,8 @@
MessageInfo info2 = new MessageInfo(read, starred, "", -1, null);
strings.clear();
conv2.addMessage(info2);
- SendersView
- .format(getContext(), conv, "", 100, strings, emailDisplays, emailDisplays, null);
+ SendersView.format(getContext(), conv, "", 100, strings, emailDisplays, emailDisplays,
+ null, false);
assertEquals(1, strings.size());
assertEquals(strings.get(0).toString(), "me");
@@ -63,8 +63,8 @@
MessageInfo info4 = new MessageInfo(read, starred, "", -1, null);
conv3.addMessage(info4);
strings.clear();
- SendersView
- .format(getContext(), conv, "", 100, strings, emailDisplays, emailDisplays, null);
+ SendersView.format(getContext(), conv, "", 100, strings, emailDisplays, emailDisplays,
+ null, false);
assertEquals(1, strings.size());
assertEquals(strings.get(0).toString(), "me");
}
@@ -80,8 +80,8 @@
conv.addMessage(info);
MessageInfo info2 = new MessageInfo(read, starred, sender, -1, null);
conv.addMessage(info2);
- SendersView
- .format(getContext(), conv, "", 100, strings, emailDisplays, emailDisplays, null);
+ SendersView.format(getContext(), conv, "", 100, strings, emailDisplays, emailDisplays,
+ null, false);
// We actually don't remove the item, we just set it to null, so count
// just the non-null items.
int count = 0;
diff --git a/tests/src/com/android/mail/providers/AccountTests.java b/tests/src/com/android/mail/providers/AccountTests.java
index dfeadd6..e99e369 100644
--- a/tests/src/com/android/mail/providers/AccountTests.java
+++ b/tests/src/com/android/mail/providers/AccountTests.java
@@ -31,8 +31,6 @@
dest.writeString("foldersList");
dest.writeString("searchUri");
dest.writeString("fromAddresses");
- dest.writeString("saveDraftUri");
- dest.writeString("sendMessageUri");
dest.writeString("expungeMessageUri");
dest.writeString("undoUri");
dest.writeString("settingIntentUri");
@@ -48,8 +46,6 @@
assertEquals(outAccount.uri, account.uri);
assertEquals(outAccount.folderListUri, account.folderListUri);
assertEquals(outAccount.searchUri, account.searchUri);
- assertEquals(outAccount.saveDraftUri, account.saveDraftUri);
- assertEquals(outAccount.sendMessageUri, account.sendMessageUri);
assertEquals(outAccount.expungeMessageUri, account.expungeMessageUri);
}
}
\ No newline at end of file
diff --git a/unified_src/com/android/mail/preferences/PreferenceMigrator.java b/unified_src/com/android/mail/preferences/PreferenceMigrator.java
new file mode 100644
index 0000000..ea7dd97
--- /dev/null
+++ b/unified_src/com/android/mail/preferences/PreferenceMigrator.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright (C) 2012 Google Inc.
+ * Licensed to The Android Open Source Project.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.mail.preferences;
+
+import android.content.Context;
+
+/**
+ * Basic {@link BasePreferenceMigrator} implementation. Projects that extend UnifiedEmail need a
+ * class with the same name and package that actually performs migration (if necessary).
+ */
+public class PreferenceMigrator extends BasePreferenceMigrator {
+ @Override
+ protected void migrate(final Context context, final int oldVersion, final int newVersion) {
+ // Nothing required here.
+ }
+}
diff --git a/unified_src/com/android/mail/providers/protos/boot/AccountReceiver.java b/unified_src/com/android/mail/providers/protos/boot/AccountReceiver.java
deleted file mode 100644
index 9e2412f..0000000
--- a/unified_src/com/android/mail/providers/protos/boot/AccountReceiver.java
+++ /dev/null
@@ -1,34 +0,0 @@
-/**
- * Copyright (c) 2011, 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.providers.protos.boot;
-
-import com.android.mail.providers.protos.mock.MockUiProvider;
-import android.content.BroadcastReceiver;
-import android.content.Context;
-import android.content.Intent;
-
-public class AccountReceiver extends BroadcastReceiver {
- /**
- * Intent used to notify interested parties that the Mail provider has been created.
- */
- public static final String ACTION_PROVIDER_CREATED
- = "com.android.mail.providers.protos.boot.intent.ACTION_PROVIDER_CREATED";
-
- @Override
- public void onReceive(Context context, Intent intent) {
- MockUiProvider.initializeMockProvider();
- }
-}