am 3ef849c6: Merge "Make sure we don\'t create and drop orphans when sorting folders by hierarchy" into jb-ub-mail-ur10
* commit '3ef849c6836a579d42117d333de7c4ecd67d8f16':
Make sure we don't create and drop orphans when sorting folders by hierarchy
diff --git a/proguard.flags b/proguard.flags
index 79a8c83..dd94b99 100644
--- a/proguard.flags
+++ b/proguard.flags
@@ -64,7 +64,6 @@
-keepclasseswithmembers class com.android.mail.browse.ConversationItemView {
*** setAnimatedHeightFraction(...);
- *** setPhotoFlipFraction(...);
}
-keepclasseswithmembers class com.android.mail.ui.MailActivity {
diff --git a/res/drawable-hdpi-v19/btn_default_pressed_holo_light.9.png b/res/drawable-hdpi-v19/btn_default_pressed_holo_light.9.png
new file mode 100644
index 0000000..bf09b6f
--- /dev/null
+++ b/res/drawable-hdpi-v19/btn_default_pressed_holo_light.9.png
Binary files differ
diff --git a/res/drawable-hdpi-v19/list_pressed_holo.9.png b/res/drawable-hdpi-v19/list_pressed_holo.9.png
new file mode 100644
index 0000000..2054530
--- /dev/null
+++ b/res/drawable-hdpi-v19/list_pressed_holo.9.png
Binary files differ
diff --git a/res/drawable-hdpi-v19/notification_bg_normal_pressed.9.png b/res/drawable-hdpi-v19/notification_bg_normal_pressed.9.png
new file mode 100644
index 0000000..c271b11
--- /dev/null
+++ b/res/drawable-hdpi-v19/notification_bg_normal_pressed.9.png
Binary files differ
diff --git a/res/drawable-mdpi-v19/btn_default_pressed_holo_light.9.png b/res/drawable-mdpi-v19/btn_default_pressed_holo_light.9.png
new file mode 100644
index 0000000..c73984e
--- /dev/null
+++ b/res/drawable-mdpi-v19/btn_default_pressed_holo_light.9.png
Binary files differ
diff --git a/res/drawable-mdpi-v19/list_pressed_holo.9.png b/res/drawable-mdpi-v19/list_pressed_holo.9.png
new file mode 100644
index 0000000..061904c
--- /dev/null
+++ b/res/drawable-mdpi-v19/list_pressed_holo.9.png
Binary files differ
diff --git a/res/drawable-mdpi-v19/notification_bg_normal_pressed.9.png b/res/drawable-mdpi-v19/notification_bg_normal_pressed.9.png
new file mode 100644
index 0000000..525120d
--- /dev/null
+++ b/res/drawable-mdpi-v19/notification_bg_normal_pressed.9.png
Binary files differ
diff --git a/res/drawable-v19/conversation_read_selector.xml b/res/drawable-v19/conversation_read_selector.xml
new file mode 100644
index 0000000..3fcf5e6
--- /dev/null
+++ b/res/drawable-v19/conversation_read_selector.xml
@@ -0,0 +1,28 @@
+<?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.
+-->
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:state_pressed="true"
+ android:drawable="@drawable/list_pressed_holo" />
+ <item android:state_selected="true"
+ android:drawable="@drawable/list_checked_holo" />
+ <item android:state_activated="true"
+ android:drawable="@drawable/list_activated_holo" />
+ <item android:state_focused="true"
+ android:drawable="@drawable/list_focused_holo" />
+ <item android:drawable="@drawable/list_read_holo" />
+</selector>
diff --git a/res/drawable-v19/conversation_unread_selector.xml b/res/drawable-v19/conversation_unread_selector.xml
new file mode 100644
index 0000000..a454f8a
--- /dev/null
+++ b/res/drawable-v19/conversation_unread_selector.xml
@@ -0,0 +1,28 @@
+<?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.
+-->
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:state_pressed="true"
+ android:drawable="@drawable/list_pressed_holo" />
+ <item android:state_selected="true"
+ android:drawable="@drawable/list_checked_holo" />
+ <item android:state_activated="true"
+ android:drawable="@drawable/list_activated_holo" />
+ <item android:state_focused="true"
+ android:drawable="@drawable/list_focused_holo" />
+ <item android:drawable="@drawable/list_unread_holo" />
+</selector>
diff --git a/res/drawable-v19/conversation_wide_unread_selector.xml b/res/drawable-v19/conversation_wide_unread_selector.xml
new file mode 100644
index 0000000..39de4ac
--- /dev/null
+++ b/res/drawable-v19/conversation_wide_unread_selector.xml
@@ -0,0 +1,28 @@
+<?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.
+-->
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:state_focused="true"
+ android:drawable="@drawable/list_conversation_wide_unread_focused_holo" />
+ <item android:state_pressed="true"
+ android:drawable="@drawable/list_conversation_wide_unread_pressed_holo" />
+ <item android:state_activated="true"
+ android:drawable="@drawable/list_conversation_wide_unread_pressed_holo" />
+ <item android:state_selected="true"
+ android:drawable="@drawable/list_conversation_wide_unread_selected_holo" />
+ <item android:drawable="@drawable/list_conversation_wide_unread_normal_holo" />
+</selector>
diff --git a/res/drawable-xhdpi-v19/btn_default_pressed_holo_light.9.png b/res/drawable-xhdpi-v19/btn_default_pressed_holo_light.9.png
new file mode 100644
index 0000000..a4ac0c7
--- /dev/null
+++ b/res/drawable-xhdpi-v19/btn_default_pressed_holo_light.9.png
Binary files differ
diff --git a/res/drawable-xhdpi-v19/list_pressed_holo.9.png b/res/drawable-xhdpi-v19/list_pressed_holo.9.png
new file mode 100644
index 0000000..f4af926
--- /dev/null
+++ b/res/drawable-xhdpi-v19/list_pressed_holo.9.png
Binary files differ
diff --git a/res/drawable-xhdpi-v19/notification_bg_normal_pressed.9.png b/res/drawable-xhdpi-v19/notification_bg_normal_pressed.9.png
new file mode 100644
index 0000000..3f054fb
--- /dev/null
+++ b/res/drawable-xhdpi-v19/notification_bg_normal_pressed.9.png
Binary files differ
diff --git a/res/drawable-xxhdpi-v19/btn_default_pressed_holo_light.9.png b/res/drawable-xxhdpi-v19/btn_default_pressed_holo_light.9.png
new file mode 100644
index 0000000..9521603
--- /dev/null
+++ b/res/drawable-xxhdpi-v19/btn_default_pressed_holo_light.9.png
Binary files differ
diff --git a/res/drawable-xxhdpi-v19/list_pressed_holo.9.png b/res/drawable-xxhdpi-v19/list_pressed_holo.9.png
new file mode 100644
index 0000000..1352a17
--- /dev/null
+++ b/res/drawable-xxhdpi-v19/list_pressed_holo.9.png
Binary files differ
diff --git a/res/drawable-xxhdpi-v19/notification_bg_normal_pressed.9.png b/res/drawable-xxhdpi-v19/notification_bg_normal_pressed.9.png
new file mode 100644
index 0000000..936fbe5
--- /dev/null
+++ b/res/drawable-xxhdpi-v19/notification_bg_normal_pressed.9.png
Binary files differ
diff --git a/res/menu/conversation_actions.xml b/res/menu/conversation_actions.xml
index 6f408f2..9c722d8 100644
--- a/res/menu/conversation_actions.xml
+++ b/res/menu/conversation_actions.xml
@@ -111,6 +111,10 @@
android:title="@string/report_phishing"
android:visible="false" />
+ <item android:id="@+id/print"
+ android:title="@string/print"
+ android:visible="false" />
+
<!-- Always available -->
<item android:id="@+id/settings"
android:title="@string/menu_settings" />
diff --git a/res/raw/template_border.html b/res/raw/template_border.html
index 8478cbd..3669d50 100644
--- a/res/raw/template_border.html
+++ b/res/raw/template_border.html
@@ -1 +1,17 @@
+<!--
+ 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.
+-->
<div class="mail-border spacer" style="height: %spx;"></div>
diff --git a/res/raw/template_conversation_lower.html b/res/raw/template_conversation_lower.html
index 9c18cf5..9b08dba 100644
--- a/res/raw/template_conversation_lower.html
+++ b/res/raw/template_conversation_lower.html
@@ -1,3 +1,19 @@
+<!--
+ 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.
+-->
<div id="initial-load-signal" class="%s"></div>
</body>
<script type="text/javascript">
diff --git a/res/raw/template_conversation_upper.html b/res/raw/template_conversation_upper.html
index fb69324..fde8985 100644
--- a/res/raw/template_conversation_upper.html
+++ b/res/raw/template_conversation_upper.html
@@ -1,3 +1,19 @@
+<!--
+ 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.
+-->
<!DOCTYPE html>
<html>
<head>
diff --git a/res/raw/template_message.html b/res/raw/template_message.html
index 8195f7d..8aa8e00 100644
--- a/res/raw/template_message.html
+++ b/res/raw/template_message.html
@@ -1,3 +1,19 @@
+<!--
+ 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.
+-->
<div id="%s" class="mail-message %s">
<div class="mail-message-header spacer" style="height: %spx;"></div>
<div class="mail-message-content collapsible zoom-normal %s" style="display: %s; margin: 16px 0;">%s</div>
diff --git a/res/raw/template_print_conversation_lower.html b/res/raw/template_print_conversation_lower.html
new file mode 100644
index 0000000..6a8c660
--- /dev/null
+++ b/res/raw/template_print_conversation_lower.html
@@ -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.
+-->
+</div>
+</div>
+</body>
+<script type="text/javascript">
+ var elements = document.documentElement.getElementsByClassName("elided-text");
+ var i, elidedElement, toggleElement;
+ for (i = 0; i < elements.length; i++) {
+ elidedElement = elements[i];
+ toggleElement = document.createElement("div");
+ toggleElement.className = "mail-elided-text";
+ toggleElement.innerHTML = "[%s]";
+ elidedElement.style.display = 'none';
+ elidedElement.parentNode.insertBefore(toggleElement, elidedElement);
+ }
+</script>
+</html>
diff --git a/res/raw/template_print_conversation_lower_no_js.html b/res/raw/template_print_conversation_lower_no_js.html
new file mode 100644
index 0000000..e6e704c
--- /dev/null
+++ b/res/raw/template_print_conversation_lower_no_js.html
@@ -0,0 +1,20 @@
+<!--
+ 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.
+-->
+</div>
+</div>
+</body>
+</html>
\ No newline at end of file
diff --git a/res/raw/template_print_conversation_upper.html b/res/raw/template_print_conversation_upper.html
new file mode 100644
index 0000000..8660c83
--- /dev/null
+++ b/res/raw/template_print_conversation_upper.html
@@ -0,0 +1,87 @@
+<!--
+ 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.
+-->
+<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01//EN"
+ "http://www.w3.org/TR/html4/strict.dtd">
+
+<html>
+<head>
+
+ <meta http-equiv=Content-Type content="text/html; charset=UTF-8">
+
+ <meta http-equiv="X-UA-Compatible" content="IE=edge">
+
+
+ <style type="text/css">
+ body, td {
+ font-family:arial,sans-serif;
+ font-size:13px
+ }
+ a:link, a:active {
+ color:#1155CC;
+ text-decoration:none
+ }
+ a:hover {
+ text-decoration:underline;
+ cursor: pointer
+ }
+ a:visited {
+ color:##6611CC
+ }
+ img {
+ border:0px
+ }
+ pre {
+ white-space: pre;
+ white-space: -moz-pre-wrap;
+ white-space: -o-pre-wrap;
+ white-space: pre-wrap;
+ word-wrap: break-word;
+ width: 800px;
+ overflow: auto;
+ }
+ .mail-elided-text {
+ padding:5 0;
+ font-size:10px;
+ color:#888888;
+ }
+ </style>
+</head>
+<body>
+ <div class="bodycontainer">
+ <table width=100%% cellpadding=0 cellspacing=0 border=0>
+ <tr height=14px>
+ <td width=143>
+ <img src="file:///android_res/drawable/logo"
+ width=143 height=59 alt="%s">
+ </td>
+ <td align=right>
+ <font size=-1 color=#777><b>%s <%s></b></font>
+ </td>
+ </tr>
+ </table>
+ <hr>
+ <div class="maincontent">
+ <table width=100%% cellpadding=0 cellspacing=0 border=0>
+ <tr>
+ <td>
+ <font size=+1>
+ <b>%s</b></font><br>
+ <!-- num messages -->
+ <font size=-1 color=#777>%s</font>
+ </td>
+ </tr>
+ </table>
diff --git a/res/raw/template_print_message.html b/res/raw/template_print_message.html
new file mode 100644
index 0000000..ded5075
--- /dev/null
+++ b/res/raw/template_print_message.html
@@ -0,0 +1,50 @@
+<!--
+ 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.
+-->
+<hr>
+<table width=100%% cellpadding=0 cellspacing=0 border=0 class="message">
+ <tr>
+ <td>
+ <!-- author -->
+ <font size=-1><b>%s</b> <%s></font>
+ </td>
+ <td align=right>
+ <!-- date -->
+ <font size=-1>%s</font>
+ </td>
+ </tr>
+ <tr>
+ <td colspan=2>
+ <!-- headers -->
+ <font size=-1 class="recipient">%s</font>
+ </td>
+ </tr>
+ <tr>
+ <td colspan=2>
+ <table width=100%% cellpadding=12 cellspacing=0 border=0>
+ <tr>
+ <td>
+ <!-- body -->
+ <div style="overflow: hidden;">
+ <font size=-1>%s</font>
+ </div>
+ %s
+ </td>
+ </tr>
+ </table>
+ </td>
+ </tr>
+</table>
diff --git a/res/raw/template_super_collapsed.html b/res/raw/template_super_collapsed.html
index 386bbae..5ac9dac 100644
--- a/res/raw/template_super_collapsed.html
+++ b/res/raw/template_super_collapsed.html
@@ -1 +1,17 @@
+<!--
+ 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.
+-->
<div class="mail-super-collapsed-block spacer" index="%s" style="height: %spx;"></div>
diff --git a/res/values/colors.xml b/res/values/colors.xml
index d7f2109..1cdf825 100644
--- a/res/values/colors.xml
+++ b/res/values/colors.xml
@@ -93,10 +93,10 @@
<item>#ad62a7</item>
</array>
<color name="letter_tile_default_color">#d66161</color>
-
<color name="letter_tile_font_color">#ffffff</color>
-
<color name="tile_divider_color">#ffffff</color>
+ <!-- Color.GRAY -->
+ <color name="checkmark_tile_background_color">#ff888888</color>
<!-- Teaser colors -->
diff --git a/res/values/constants.xml b/res/values/constants.xml
index 7b98345..b092c83 100644
--- a/res/values/constants.xml
+++ b/res/values/constants.xml
@@ -119,7 +119,7 @@
<integer name="ap_overflow_max_count">99</integer>
<!-- Duration of the animations for entering/exiting CAB mode -->
- <integer name="conv_item_view_cab_anim_duration">350</integer>
+ <integer name="conv_item_view_cab_anim_duration">250</integer>
<!-- Number of nested folders to display in the conversation list, before hiding the remainder under "Show more" -->
<integer name="nested_folders_collapse_threshold">3</integer>
diff --git a/res/values/strings.xml b/res/values/strings.xml
index a711666..66d093a 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -935,4 +935,25 @@
<string name="show_n_more_folders">Show <xliff:g id="number">%1$s</xliff:g> more folders</string>
<!-- Button in conversation list to hide folders [CHAR LIMIT=50] -->
<string name="hide_folders">Hide folders</string>
+
+ <!-- Menu item text to that when clicked will allow a user to print an email conversation. [CHAR LIMIT=25] -->
+ <string name="print">Print</string>
+ <!-- Number of messages in a conversation [CHAR LIMIT=30]-->
+ <plurals name="num_messages">
+ <item quantity="one"><xliff:g id="count">%1$d</xliff:g> message</item>
+ <item quantity="other"><xliff:g id="count">%1$d</xliff:g> messages</item>
+ </plurals>
+
+ <!-- Display format of an email recipient, displayed in printed message details [CHAR LIMIT=10] -->
+ <string name="address_print_display_format"><xliff:g id="name">%1$s</xliff:g> &lt;<xliff:g id="email">%2$s</xliff:g>&gt;</string>
+ <!-- Shown to display the date of the message [CHAR LIMIT=50] -->
+ <string name="date_message_received_print"><xliff:g id="day_and_date">%1$s</xliff:g> at <xliff:g id="time">%2$s</xliff:g></string>
+ <!-- Shown to display the to recipient(s) of the message if the message is a draft [CHAR LIMIT=20] -->
+ <string name="draft_to_heading">Draft To:\u0020</string>
+
+ <!-- Shown to display in the message header if the message is a draft and there are no "to" addresses in the draft [CHAR LIMIT=10] -->
+ <string name="draft_heading">Draft</string>
+ <!-- Shown to inform the user that the quoted text for this message has been hidden. [CHAR LIMIT=50] -->
+ <string name="quoted_text_hidden_print">Quoted text hidden</string>
+
</resources>
diff --git a/src/com/android/bitmap/AltBitmapCache.java b/src/com/android/bitmap/AltBitmapCache.java
index fb8e915..f519c7b 100644
--- a/src/com/android/bitmap/AltBitmapCache.java
+++ b/src/com/android/bitmap/AltBitmapCache.java
@@ -16,25 +16,40 @@
package com.android.bitmap;
+import com.android.bitmap.DecodeTask.Request;
+import com.android.bitmap.ReusableBitmap.NullReusableBitmap;
import com.android.mail.utils.LogTag;
import com.android.mail.utils.LogUtils;
+import com.android.mail.utils.LruCache;
/**
* This subclass provides custom pool behavior. The pool can be set to block on {@link #poll()} if
* nothing can be returned. This is useful if you know you will incur high costs upon receiving
* nothing from the pool, and you do not want to incur those costs at the critical moment when the
* UI is animating.
+ *
+ * This subclass provides custom cache behavior. Null values can be cached. Later,
+ * when the same key is used to retrieve the value, a {@link NullReusableBitmap} singleton will
+ * be returned.
*/
-public class AltBitmapCache extends AltPooledCache<DecodeTask.Request, ReusableBitmap>
+public class AltBitmapCache extends AltPooledCache<Request, ReusableBitmap>
implements BitmapCache {
private boolean mBlocking = false;
private final Object mLock = new Object();
+ private final LruCache<Request, Void> mNullRequests;
+
private final static boolean DEBUG = false;
private final static String TAG = LogTag.getLogTag();
- public AltBitmapCache(final int targetSizeBytes, final float nonPooledFraction) {
+ private int mDecodeWidth;
+ private int mDecodeHeight;
+
+ public AltBitmapCache(final int targetSizeBytes, final float nonPooledFraction,
+ final int nullCapacity) {
super(targetSizeBytes, nonPooledFraction);
+
+ mNullRequests = new LruCache<Request, Void>(nullCapacity);
}
/**
@@ -78,7 +93,7 @@
LogUtils.d(TAG, "AltBitmapCache: %s notified",
Thread.currentThread().getName());
}
- } catch (InterruptedException e) {
+ } catch (InterruptedException ignored) {
}
Trace.endSection();
}
@@ -95,4 +110,50 @@
mLock.notify();
}
}
+
+ @Override
+ public ReusableBitmap get(final Request key, final boolean incrementRefCount) {
+ if (mNullRequests.containsKey(key)) {
+ return NullReusableBitmap.getInstance();
+ }
+ return super.get(key, incrementRefCount);
+ }
+
+ @Override
+ public ReusableBitmap put(final Request key, final ReusableBitmap value) {
+ if (value == null || value == NullReusableBitmap.getInstance()) {
+ mNullRequests.put(key, null);
+ return null;
+ }
+
+ // Do not allow the pool to be filled with bitmaps that are of the wrong dimensions.
+ if (mDecodeWidth > value.bmp.getWidth() || mDecodeHeight > value.bmp.getHeight()) {
+ if (DEBUG) {
+ LogUtils.d(TAG, "Discarding ReusableBitmap size %d x %d for cache size %d x %d.",
+ value.bmp.getWidth(), value.bmp.getHeight(), mDecodeWidth, mDecodeHeight);
+ }
+ return null;
+ }
+
+ return super.put(key, value);
+ }
+
+ @Override
+ public void setPoolDimensions(final int decodeWidth, final int decodeHeight) {
+ if (mDecodeWidth < decodeWidth || mDecodeHeight < decodeHeight) {
+ clear();
+ mDecodeWidth = decodeWidth;
+ mDecodeHeight = decodeHeight;
+ }
+ }
+
+ @Override
+ public int getDecodeWidth() {
+ return mDecodeWidth;
+ }
+
+ @Override
+ public int getDecodeHeight() {
+ return mDecodeHeight;
+ }
}
diff --git a/src/com/android/bitmap/AltPooledCache.java b/src/com/android/bitmap/AltPooledCache.java
index 0a7d7b4..03a6c50 100644
--- a/src/com/android/bitmap/AltPooledCache.java
+++ b/src/com/android/bitmap/AltPooledCache.java
@@ -67,6 +67,7 @@
@Override
public V get(K key, boolean incrementRefCount) {
+ Trace.beginSection("cache get");
synchronized (mCache) {
V result = mCache.get(key);
if (result == null && mNonPooledCache != null) {
@@ -75,12 +76,14 @@
if (incrementRefCount && result != null) {
result.acquireReference();
}
+ Trace.endSection();
return result;
}
}
@Override
public V put(K key, V value) {
+ Trace.beginSection("cache put");
synchronized (mCache) {
final V prev;
if (value.isEligibleForPooling()) {
@@ -90,22 +93,27 @@
} else {
prev = null;
}
+ Trace.endSection();
return prev;
}
}
@Override
public void offer(V value) {
+ Trace.beginSection("pool offer");
if (value.getRefCount() != 0 || !value.isEligibleForPooling()) {
throw new IllegalArgumentException("unexpected offer of an invalid object: " + value);
}
mPool.offer(value);
+ Trace.endSection();
}
@Override
public V poll() {
+ Trace.beginSection("pool poll");
final V pooled = mPool.poll();
if (pooled != null) {
+ Trace.endSection();
return pooled;
}
@@ -131,11 +139,13 @@
if (DEBUG) System.err.println(
"POOL SCAVENGE FAILED, cache not fully warm yet. szDelta="
+ (mTargetSize-unrefSize));
+ Trace.endSection();
return null;
} else {
mCache.remove(eldestUnref.getKey());
if (DEBUG) System.err.println("POOL SCAVENGE SUCCESS, oldKey="
+ eldestUnref.getKey());
+ Trace.endSection();
return eldestUnref.getValue();
}
}
@@ -209,4 +219,9 @@
}
+ @Override
+ public void clear() {
+ mCache.clear();
+ mPool.clear();
+ }
}
diff --git a/src/com/android/bitmap/BitmapCache.java b/src/com/android/bitmap/BitmapCache.java
index d671c17..fc76c3a 100644
--- a/src/com/android/bitmap/BitmapCache.java
+++ b/src/com/android/bitmap/BitmapCache.java
@@ -19,4 +19,7 @@
public interface BitmapCache extends PooledCache<DecodeTask.Request, ReusableBitmap> {
void setBlocking(boolean blocking);
+ void setPoolDimensions(int decodeWidth, int decodeHeight);
+ int getDecodeWidth();
+ int getDecodeHeight();
}
diff --git a/src/com/android/bitmap/DecodeTask.java b/src/com/android/bitmap/DecodeTask.java
index 2210ec6..d7a3cab 100644
--- a/src/com/android/bitmap/DecodeTask.java
+++ b/src/com/android/bitmap/DecodeTask.java
@@ -54,6 +54,7 @@
public interface Request {
AssetFileDescriptor createFd() throws IOException;
InputStream createInputStream() throws IOException;
+ boolean hasOrientationExif() throws IOException;
}
/**
@@ -65,8 +66,6 @@
* may have been preempted by the scheduler or queued up by a bottlenecked executor.
* <p>
* N.B. this method runs on the UI thread.
- *
- * @param key
*/
void onDecodeBegin(Request key);
void onDecodeComplete(Request key, ReusableBitmap result);
@@ -86,13 +85,17 @@
@Override
protected ReusableBitmap doInBackground(Void... params) {
+ // enqueue the 'onDecodeBegin' signal on the main thread
+ publishProgress();
+
+ return decode();
+ }
+
+ public ReusableBitmap decode() {
if (isCancelled()) {
return null;
}
- // enqueue the 'onDecodeBegin' signal on the main thread
- publishProgress();
-
ReusableBitmap result = null;
AssetFileDescriptor fd = null;
InputStream in = null;
@@ -130,20 +133,26 @@
Trace.endSection();
Trace.beginSection("get orientation");
- if (fd != null) {
- // Creating an input stream from the file descriptor makes it useless afterwards.
- Trace.beginSection("create fd and stream");
- final AssetFileDescriptor orientationFd = mKey.createFd();
- in = orientationFd.createInputStream();
- Trace.endSection();
- }
- final int orientation = Exif.getOrientation(in, byteSize);
- if (fd != null) {
- try {
- // Close the temporary file descriptor.
- in.close();
- } catch (IOException ex) {
+ final int orientation;
+ if (mKey.hasOrientationExif()) {
+ if (fd != null) {
+ // Creating an input stream from the file descriptor makes it useless
+ // afterwards.
+ Trace.beginSection("create fd and stream");
+ final AssetFileDescriptor orientationFd = mKey.createFd();
+ in = orientationFd.createInputStream();
+ Trace.endSection();
}
+ orientation = Exif.getOrientation(in, byteSize);
+ if (fd != null) {
+ try {
+ // Close the temporary file descriptor.
+ in.close();
+ } catch (IOException ignored) {
+ }
+ }
+ } else {
+ orientation = 0;
}
final boolean isNotRotatedOr180 = orientation == 0 || orientation == 180;
Trace.endSection();
@@ -246,6 +255,7 @@
}
}
+ //noinspection PointlessBooleanExpression
if (!CROP_DURING_DECODE || (decodeResult == null && !isCancelled())) {
try {
Trace.beginSection("decode" + mOpts.inSampleSize);
@@ -307,13 +317,13 @@
if (fd != null) {
try {
fd.close();
- } catch (IOException e) {
+ } catch (IOException ignored) {
}
}
if (in != null) {
try {
in.close();
- } catch (IOException e) {
+ } catch (IOException ignored) {
}
}
if (result != null) {
@@ -388,7 +398,7 @@
} else {
try {
in.close();
- } catch (IOException ex) {
+ } catch (IOException ignored) {
}
in = mKey.createInputStream();
}
@@ -414,6 +424,7 @@
// round to the nearest power of two, or just truncate
final boolean stricter = true;
+ //noinspection ConstantConditions
if (stricter) {
result = (int) Math.pow(2, (int) (0.5 + (Math.log(sz) / Math.log(2))));
} else {
diff --git a/src/com/android/bitmap/PooledCache.java b/src/com/android/bitmap/PooledCache.java
index 62381b8..6d6684f 100644
--- a/src/com/android/bitmap/PooledCache.java
+++ b/src/com/android/bitmap/PooledCache.java
@@ -24,4 +24,14 @@
V poll();
String toDebugString();
+ /**
+ * Purge existing Poolables from the pool+cache. Usually, this is done when situations
+ * change and the items in the pool+cache are no longer appropriate. For example,
+ * if the layout changes, the pool+cache may need to hold larger bitmaps.
+ *
+ * <p/>
+ * The existing Poolables will be garbage collected when they are no longer being referenced
+ * by other objects.
+ */
+ void clear();
}
diff --git a/src/com/android/bitmap/ReusableBitmap.java b/src/com/android/bitmap/ReusableBitmap.java
index 2e0199f..dde9bd1 100644
--- a/src/com/android/bitmap/ReusableBitmap.java
+++ b/src/com/android/bitmap/ReusableBitmap.java
@@ -115,4 +115,37 @@
return sb.toString();
}
+ /**
+ * Singleton class to represent a null Bitmap. We don't want to just use a regular
+ * ReusableBitmap with a null bmp field because that will render that ReusableBitmap useless
+ * and unable to be used by another decode process.
+ */
+ public final static class NullReusableBitmap extends ReusableBitmap {
+ private static NullReusableBitmap sInstance;
+
+ /**
+ * Get a singleton.
+ */
+ public static NullReusableBitmap getInstance() {
+ if (sInstance == null) {
+ sInstance = new NullReusableBitmap();
+ }
+ return sInstance;
+ }
+
+ private NullReusableBitmap() {
+ super(null /* bmp */, false /* reusable */);
+ }
+
+ @Override
+ public int getByteCount() {
+ return 0;
+ }
+
+ @Override
+ public void releaseReference() { }
+
+ @Override
+ public void acquireReference() { }
+ }
}
diff --git a/src/com/android/mail/ContactInfo.java b/src/com/android/mail/ContactInfo.java
index b7cecb8..a59dc62 100644
--- a/src/com/android/mail/ContactInfo.java
+++ b/src/com/android/mail/ContactInfo.java
@@ -47,6 +47,6 @@
@Override
public String toString() {
- return "{status=" + status + " photo=" + photo + "}";
+ return "{status=" + status + " photo=" + (photo != null ? photo : photoBytes) + "}";
}
}
diff --git a/src/com/android/mail/FormattedDateBuilder.java b/src/com/android/mail/FormattedDateBuilder.java
index e53b10c..d7c17b2 100644
--- a/src/com/android/mail/FormattedDateBuilder.java
+++ b/src/com/android/mail/FormattedDateBuilder.java
@@ -58,7 +58,7 @@
}
}
- private CharSequence formatLongDayAndDate(long when) {
+ public CharSequence formatLongDayAndDate(long when) {
sb.setLength(0);
DateUtils.formatDateRange(mContext, dateFormatter, when, when,
DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_SHOW_WEEKDAY
@@ -66,7 +66,7 @@
return sb.toString();
}
- private CharSequence formatLongTime(long when) {
+ public CharSequence formatLongTime(long when) {
sb.setLength(0);
DateUtils.formatDateRange(mContext, dateFormatter, when, when,
DateUtils.FORMAT_SHOW_TIME);
diff --git a/src/com/android/mail/SenderInfoLoader.java b/src/com/android/mail/SenderInfoLoader.java
index 5d01ad4..c69e934 100644
--- a/src/com/android/mail/SenderInfoLoader.java
+++ b/src/com/android/mail/SenderInfoLoader.java
@@ -17,6 +17,7 @@
package com.android.mail;
+import com.android.bitmap.Trace;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Maps;
@@ -99,40 +100,53 @@
/**
* Loads contact photos from the ContentProvider.
* @param resolver {@link ContentResolver} to use in queries to the ContentProvider.
- * @param senderSet The email addresses of the sender images to return.
+ * @param emails The email addresses of the sender images to return.
* @param decodeBitmaps If {@code true}, decode the bitmaps and put them into
* {@link ContactInfo}. Otherwise, just put the raw bytes of the photo
* into the {@link ContactInfo}.
- * @return A mapping of email addresses to {@link ContactInfo}s. The {@link ContactInfo} will
- * contain either a byte array or an actual decoded bitmap for the sender image.
+ * @return A mapping of email to {@link ContactInfo}. How to interpret the map:
+ * <ul>
+ * <li>The email is missing from the key set or maps to null - The email was skipped. Try
+ * again.</li>
+ * <li>Either {@link ContactInfo#photoBytes} or {@link ContactInfo#photo} is non-null -
+ * Photo loaded successfully.</li>
+ * <li>Both {@link ContactInfo#photoBytes} and {@link ContactInfo#photo} are null -
+ * Photo load failed.</li>
+ * </ul>
*/
public static ImmutableMap<String, ContactInfo> loadContactPhotos(
- final ContentResolver resolver, final Set<String> senderSet,
- final boolean decodeBitmaps) {
+ final ContentResolver resolver, final Set<String> emails, final boolean decodeBitmaps) {
+ Trace.beginSection("load contact photos util");
Cursor cursor = null;
+ Trace.beginSection("build first query");
Map<String, ContactInfo> results = Maps.newHashMap();
// temporary structures
Map<Long, Pair<String, ContactInfo>> photoIdMap = Maps.newHashMap();
ArrayList<String> photoIdsAsStrings = new ArrayList<String>();
- ArrayList<String> senders = getTruncatedQueryParams(senderSet);
+ ArrayList<String> emailsList = getTruncatedQueryParams(emails);
// Build first query
StringBuilder query = new StringBuilder()
.append(Data.MIMETYPE).append("='").append(Email.CONTENT_ITEM_TYPE)
.append("' AND ").append(Email.DATA).append(" IN (");
- appendQuestionMarks(query, senders);
+ appendQuestionMarks(query, emailsList);
query.append(')');
+ Trace.endSection();
try {
+ Trace.beginSection("query 1");
cursor = resolver.query(Data.CONTENT_URI, DATA_COLS,
- query.toString(), toStringArray(senders), null /* sortOrder */);
+ query.toString(), toStringArray(emailsList), null /* sortOrder */);
+ Trace.endSection();
if (cursor == null) {
+ Trace.endSection();
return null;
}
+ Trace.beginSection("get photo id");
int i = -1;
while (cursor.moveToPosition(++i)) {
String email = cursor.getString(DATA_EMAIL_COLUMN);
@@ -154,11 +168,23 @@
results.put(email, result);
}
cursor.close();
+ Trace.endSection();
+
+ // Put empty ContactInfo for all the emails that didn't map to a contact.
+ // This allows us to differentiate between lookup failed,
+ // and lookup skipped (truncated above).
+ for (String email : emailsList) {
+ if (!results.containsKey(email)) {
+ results.put(email, new ContactInfo(null, null));
+ }
+ }
if (photoIdsAsStrings.isEmpty()) {
+ Trace.endSection();
return ImmutableMap.copyOf(results);
}
+ Trace.beginSection("build second query");
// Build second query: photoIDs->blobs
// based on photo batch-select code in ContactPhotoManager
photoIdsAsStrings = getTruncatedQueryParams(photoIdsAsStrings);
@@ -166,14 +192,19 @@
query.append(Photo._ID).append(" IN (");
appendQuestionMarks(query, photoIdsAsStrings);
query.append(')');
+ Trace.endSection();
+ Trace.beginSection("query 2");
cursor = resolver.query(Data.CONTENT_URI, PHOTO_COLS,
query.toString(), toStringArray(photoIdsAsStrings), null /* sortOrder */);
+ Trace.endSection();
if (cursor == null) {
+ Trace.endSection();
return ImmutableMap.copyOf(results);
}
+ Trace.beginSection("get photo blob");
i = -1;
while (cursor.moveToPosition(++i)) {
byte[] photoBytes = cursor.getBlob(PHOTO_PHOTO_COLUMN);
@@ -187,7 +218,9 @@
ContactInfo prevResult = prev.second;
if (decodeBitmaps) {
+ Trace.beginSection("decode bitmap");
Bitmap photo = BitmapFactory.decodeByteArray(photoBytes, 0, photoBytes.length);
+ Trace.endSection();
// overwrite existing photo-less result
results.put(email,
new ContactInfo(prevResult.contactUri, prevResult.status, photo));
@@ -197,12 +230,14 @@
prevResult.contactUri, prevResult.status, photoBytes));
}
}
+ Trace.endSection();
} finally {
if (cursor != null) {
cursor.close();
}
}
+ Trace.endSection();
return ImmutableMap.copyOf(results);
}
diff --git a/src/com/android/mail/bitmap/AttachmentDrawable.java b/src/com/android/mail/bitmap/AttachmentDrawable.java
index 0252455..49e4008 100644
--- a/src/com/android/mail/bitmap/AttachmentDrawable.java
+++ b/src/com/android/mail/bitmap/AttachmentDrawable.java
@@ -147,11 +147,13 @@
// requests for different renditions of the same attachment
final boolean onlyRenditionChange = (mCurrKey != null && mCurrKey.matches(key));
+ Trace.beginSection("release reference");
if (mBitmap != null && !onlyRenditionChange) {
mBitmap.releaseReference();
// System.out.println("view.bind() decremented ref to old bitmap: " + mBitmap);
mBitmap = null;
}
+ Trace.endSection();
if (mCurrKey != null && SwipeableListView.ENABLE_ATTACHMENT_DECODE_AGGREGATOR) {
mDecodeAggregator.forget(mCurrKey);
}
@@ -169,6 +171,7 @@
setLoadState(LOAD_STATE_UNINITIALIZED);
if (key == null) {
+ invalidateSelf();
Trace.endSection();
return;
}
@@ -200,7 +203,7 @@
return;
}
- if (mBitmap != null) {
+ if (mBitmap != null && mBitmap.bmp != null) {
BitmapUtils
.calculateCroppedSrcRect(mBitmap.getLogicalWidth(), mBitmap.getLogicalHeight(),
bounds.width(), bounds.height(),
diff --git a/src/com/android/mail/bitmap/AttachmentGridDrawable.java b/src/com/android/mail/bitmap/AttachmentGridDrawable.java
index f5d28ed..c1e5789 100644
--- a/src/com/android/mail/bitmap/AttachmentGridDrawable.java
+++ b/src/com/android/mail/bitmap/AttachmentGridDrawable.java
@@ -2,6 +2,7 @@
import android.content.res.Resources;
import android.graphics.Canvas;
+import android.graphics.ColorFilter;
import android.graphics.Paint;
import android.graphics.Paint.Align;
import android.graphics.Rect;
@@ -47,7 +48,7 @@
}
@Override
- protected AttachmentDrawable createDivisionDrawable() {
+ protected AttachmentDrawable createDivisionDrawable(final int i) {
final AttachmentDrawable result = new AttachmentDrawable(mResources, mCache,
mDecodeAggregator, mCoordinates, mPlaceholder, mProgress);
return result;
@@ -105,8 +106,24 @@
}
@Override
+ public void setAlpha(final int alpha) {
+ super.setAlpha(alpha);
+ final int old = mPaint.getAlpha();
+ mPaint.setAlpha(alpha);
+ if (alpha != old) {
+ invalidateSelf();
+ }
+ }
+
+ @Override
+ public void setColorFilter(final ColorFilter cf) {
+ super.setColorFilter(cf);
+ mPaint.setColorFilter(cf);
+ invalidateSelf();
+ }
+
+ @Override
public void setParallaxFraction(float fraction) {
mParallaxFraction = fraction;
}
-
}
diff --git a/src/com/android/mail/bitmap/CompositeDrawable.java b/src/com/android/mail/bitmap/CompositeDrawable.java
index e25bfc7..e372bb5 100644
--- a/src/com/android/mail/bitmap/CompositeDrawable.java
+++ b/src/com/android/mail/bitmap/CompositeDrawable.java
@@ -21,11 +21,13 @@
public abstract class CompositeDrawable<T extends Drawable> extends Drawable
implements Drawable.Callback {
+ public static final int MAX_COMPOSITE_DRAWABLES = 4;
+
protected final List<T> mDrawables;
protected int mCount;
public CompositeDrawable(int maxDivisions) {
- if (maxDivisions >= 4) {
+ if (maxDivisions > MAX_COMPOSITE_DRAWABLES) {
throw new IllegalArgumentException("CompositeDrawable only supports 4 divisions");
}
mDrawables = new ArrayList<T>(maxDivisions);
@@ -35,7 +37,7 @@
mCount = 0;
}
- protected abstract T createDivisionDrawable();
+ protected abstract T createDivisionDrawable(final int i);
public void setCount(int count) {
// zero out the composite bounds, which will propagate to the division drawables
@@ -56,7 +58,7 @@
T result = mDrawables.get(i);
if (result == null) {
Trace.beginSection("create division drawable");
- result = createDivisionDrawable();
+ result = createDivisionDrawable(i);
mDrawables.set(i, result);
result.setCallback(this);
// Make sure drawables created after the bounds were already set have their bounds
@@ -109,6 +111,11 @@
@Override
public void draw(Canvas canvas) {
+ final Rect bounds = getBounds();
+ if (!isVisible() || bounds.isEmpty()) {
+ return;
+ }
+
for (int i = 0; i < mCount; i++) {
mDrawables.get(i).draw(canvas);
}
@@ -132,10 +139,7 @@
public int getOpacity() {
int opacity = PixelFormat.OPAQUE;
for (int i = 0; i < mCount; i++) {
- if (mDrawables.get(i).getOpacity() != PixelFormat.OPAQUE) {
- opacity = PixelFormat.TRANSLUCENT;
- break;
- }
+ opacity = resolveOpacity(opacity, mDrawables.get(i).getOpacity());
}
return opacity;
}
diff --git a/src/com/android/mail/bitmap/ContactCheckableGridDrawable.java b/src/com/android/mail/bitmap/ContactCheckableGridDrawable.java
new file mode 100644
index 0000000..134439d
--- /dev/null
+++ b/src/com/android/mail/bitmap/ContactCheckableGridDrawable.java
@@ -0,0 +1,241 @@
+/*
+ * 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.bitmap;
+
+import android.animation.ValueAnimator;
+import android.animation.ValueAnimator.AnimatorUpdateListener;
+import android.content.res.Resources;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.graphics.Canvas;
+import android.graphics.ColorFilter;
+import android.graphics.Matrix;
+import android.graphics.Paint;
+import android.graphics.PixelFormat;
+import android.graphics.Rect;
+import android.graphics.drawable.Drawable;
+
+import com.android.bitmap.BitmapCache;
+import com.android.mail.R.color;
+import com.android.mail.R.drawable;
+
+/**
+ * Custom FlipDrawable which has a {@link ContactGridDrawable} on the front,
+ * and a {@link CheckmarkDrawable} on the back.
+ */
+public class ContactCheckableGridDrawable extends FlipDrawable implements AnimatorUpdateListener {
+
+ private final ContactGridDrawable mContactGridDrawable;
+ private final CheckmarkDrawable mCheckmarkDrawable;
+
+ private final ValueAnimator mCheckmarkScaleAnimator;
+ private final ValueAnimator mCheckmarkAlphaAnimator;
+
+ private static final int POST_FLIP_DURATION_MS = 150;
+
+ private static final float CHECKMARK_SCALE_BEGIN_VALUE = 0.2f;
+ private static final float CHECKMARK_ALPHA_BEGIN_VALUE = 0f;
+
+ /** Must be <= 1f since the animation value is used as a percentage. */
+ private static final float END_VALUE = 1f;
+
+ public ContactCheckableGridDrawable(final Resources res, final int flipDurationMs) {
+ super(new ContactGridDrawable(res), new CheckmarkDrawable(res), flipDurationMs,
+ 0 /* preFlipDurationMs */, POST_FLIP_DURATION_MS);
+
+ mContactGridDrawable = (ContactGridDrawable) mFront;
+ mCheckmarkDrawable = (CheckmarkDrawable) mBack;
+
+ // We will create checkmark animations that are synchronized with the flipping animation.
+ // The entire delay + duration of the checkmark animation needs to equal the entire
+ // duration of the flip animation (where delay is 0).
+
+ // The checkmark animation is in effect only when the back drawable is being shown.
+ // For the flip animation duration <pre>[_][]|[][_]<post>
+ // The checkmark animation will be |--delay--|-duration-|
+
+ // Need delay to skip the first half of the flip duration.
+ final long animationDelay = mPreFlipDurationMs + mFlipDurationMs / 2;
+ // Actual duration is the second half of the flip duration.
+ final long animationDuration = mFlipDurationMs / 2 + mPostFlipDurationMs;
+
+ mCheckmarkScaleAnimator = ValueAnimator.ofFloat(CHECKMARK_SCALE_BEGIN_VALUE, END_VALUE)
+ .setDuration(animationDuration);
+ mCheckmarkScaleAnimator.setStartDelay(animationDelay);
+ mCheckmarkScaleAnimator.addUpdateListener(this);
+
+ mCheckmarkAlphaAnimator = ValueAnimator.ofFloat(CHECKMARK_ALPHA_BEGIN_VALUE, END_VALUE)
+ .setDuration(animationDuration);
+ mCheckmarkAlphaAnimator.setStartDelay(animationDelay);
+ mCheckmarkAlphaAnimator.addUpdateListener(this);
+ }
+
+ @Override
+ public void reset(final boolean side) {
+ super.reset(side);
+ if (mCheckmarkScaleAnimator == null) {
+ // Call from super's constructor. Not yet initialized.
+ return;
+ }
+ mCheckmarkScaleAnimator.cancel();
+ mCheckmarkAlphaAnimator.cancel();
+ mCheckmarkDrawable.setScaleAnimatorValue(side ? CHECKMARK_SCALE_BEGIN_VALUE : END_VALUE);
+ mCheckmarkDrawable.setAlphaAnimatorValue(side ? CHECKMARK_ALPHA_BEGIN_VALUE : END_VALUE);
+ }
+
+ @Override
+ public void flip() {
+ super.flip();
+ // Keep the checkmark animators in sync with the flip animator.
+ if (mCheckmarkScaleAnimator.isStarted()) {
+ mCheckmarkScaleAnimator.reverse();
+ mCheckmarkAlphaAnimator.reverse();
+ } else {
+ if (!getSideFlippingTowards() /* front to back */) {
+ mCheckmarkScaleAnimator.start();
+ mCheckmarkAlphaAnimator.start();
+ } else /* back to front */ {
+ mCheckmarkScaleAnimator.reverse();
+ mCheckmarkAlphaAnimator.reverse();
+ }
+ }
+ }
+
+ public ContactDrawable getOrCreateDrawable(final int i) {
+ return mContactGridDrawable.getOrCreateDrawable(i);
+ }
+
+ public void setBitmapCache(final BitmapCache cache) {
+ mContactGridDrawable.setBitmapCache(cache);
+ }
+
+ public void setContactResolver(final ContactResolver contactResolver) {
+ mContactGridDrawable.setContactResolver(contactResolver);
+ }
+
+ public int getCount() {
+ return mContactGridDrawable.getCount();
+ }
+
+ public void setCount(final int count) {
+ mContactGridDrawable.setCount(count);
+ // Side effect needs to happen here too.
+ setBounds(0, 0, 0, 0);
+ }
+
+ @Override
+ public void onAnimationUpdate(final ValueAnimator animation) {
+ //noinspection ConstantConditions
+ final float value = (Float) animation.getAnimatedValue();
+
+ if (animation == mCheckmarkScaleAnimator) {
+ mCheckmarkDrawable.setScaleAnimatorValue(value);
+ } else if (animation == mCheckmarkAlphaAnimator) {
+ mCheckmarkDrawable.setAlphaAnimatorValue(value);
+ }
+ }
+
+ /**
+ * Meant to be used as the with a FlipDrawable. The animator driving this Drawable should be
+ * more or less in sync with the containing FlipDrawable's flip animator.
+ */
+ private static class CheckmarkDrawable extends Drawable {
+
+ private static Bitmap CHECKMARK;
+ private static int sBackgroundColor;
+
+ private final Paint mPaint;
+
+ private float mScaleFraction;
+ private float mAlphaFraction;
+
+ private static final Matrix sMatrix = new Matrix();
+
+ public CheckmarkDrawable(final Resources res) {
+ if (CHECKMARK == null) {
+ CHECKMARK = BitmapFactory.decodeResource(res, drawable.ic_avatar_check);
+ sBackgroundColor = res.getColor(color.checkmark_tile_background_color);
+ }
+ mPaint = new Paint();
+ mPaint.setAntiAlias(true);
+ mPaint.setFilterBitmap(true);
+ mPaint.setColor(sBackgroundColor);
+ }
+
+ @Override
+ public void draw(final Canvas canvas) {
+ final Rect bounds = getBounds();
+ if (!isVisible() || bounds.isEmpty()) {
+ return;
+ }
+
+ canvas.drawRect(getBounds(), mPaint);
+
+ // Scale the checkmark.
+ sMatrix.reset();
+ sMatrix.setScale(mScaleFraction, mScaleFraction, CHECKMARK.getWidth() / 2,
+ CHECKMARK.getHeight() / 2);
+ sMatrix.postTranslate(bounds.centerX() - CHECKMARK.getWidth() / 2,
+ bounds.centerY() - CHECKMARK.getHeight() / 2);
+
+ // Fade the checkmark.
+ final int oldAlpha = mPaint.getAlpha();
+ // Interpolate the alpha.
+ mPaint.setAlpha((int) (oldAlpha * mAlphaFraction));
+ canvas.drawBitmap(CHECKMARK, sMatrix, mPaint);
+ // Restore the alpha.
+ mPaint.setAlpha(oldAlpha);
+ }
+
+ @Override
+ public void setAlpha(final int alpha) {
+ mPaint.setAlpha(alpha);
+ }
+
+ @Override
+ public void setColorFilter(final ColorFilter cf) {
+ mPaint.setColorFilter(cf);
+ }
+
+ @Override
+ public int getOpacity() {
+ // Always a gray background.
+ return PixelFormat.OPAQUE;
+ }
+
+ /**
+ * Set value as a fraction from 0f to 1f.
+ */
+ public void setScaleAnimatorValue(final float value) {
+ final float old = mScaleFraction;
+ mScaleFraction = value;
+ if (old != mScaleFraction) {
+ invalidateSelf();
+ }
+ }
+
+ /**
+ * Set value as a fraction from 0f to 1f.
+ */
+ public void setAlphaAnimatorValue(final float value) {
+ final float old = mAlphaFraction;
+ mAlphaFraction = value;
+ if (old != mAlphaFraction) {
+ invalidateSelf();
+ }
+ }
+ }
+}
diff --git a/src/com/android/mail/bitmap/ContactDrawable.java b/src/com/android/mail/bitmap/ContactDrawable.java
new file mode 100644
index 0000000..34aa682
--- /dev/null
+++ b/src/com/android/mail/bitmap/ContactDrawable.java
@@ -0,0 +1,252 @@
+/*
+ * 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.bitmap;
+
+import android.content.res.Resources;
+import android.content.res.TypedArray;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.graphics.Canvas;
+import android.graphics.ColorFilter;
+import android.graphics.Paint;
+import android.graphics.Paint.Align;
+import android.graphics.Rect;
+import android.graphics.Typeface;
+import android.graphics.drawable.Drawable;
+
+import com.android.bitmap.BitmapCache;
+import com.android.bitmap.DecodeTask.Request;
+import com.android.bitmap.ReusableBitmap;
+import com.android.mail.R;
+
+/**
+ * A drawable that encapsulates all the functionality needed to display a contact image,
+ * including request creation/cancelling and data unbinding/re-binding. While no contact images
+ * can be shown, a default letter tile will be shown instead.
+ *
+ * <p/>
+ * The actual contact resolving and decoding is handled by {@link ContactResolver}.
+ */
+public class ContactDrawable extends Drawable {
+
+ private final BitmapCache mCache;
+ private final ContactResolver mContactResolver;
+
+ private ContactRequest mContactRequest;
+ private ReusableBitmap mBitmap;
+ private final Paint mPaint;
+ private int mScale;
+
+ /** Letter tile */
+ private static TypedArray sColors;
+ private static int sDefaultColor;
+ private static int sTileLetterFontSize;
+ private static int sTileLetterFontSizeSmall;
+ private static int sTileFontColor;
+ private static Bitmap DEFAULT_AVATAR;
+ /** Reusable components to avoid new allocations */
+ private static final Paint sPaint = new Paint();
+ private static final Rect sRect = new Rect();
+ private static final char[] sFirstChar = new char[1];
+
+ /** This should match the total number of colors defined in colors.xml for letter_tile_color */
+ private static final int NUM_OF_TILE_COLORS = 8;
+
+ public ContactDrawable(final Resources res, final BitmapCache cache,
+ final ContactResolver contactResolver) {
+ mCache = cache;
+ mContactResolver = contactResolver;
+ mPaint = new Paint();
+ mPaint.setFilterBitmap(true);
+ mPaint.setDither(true);
+
+ if (sColors == null) {
+ sColors = res.obtainTypedArray(R.array.letter_tile_colors);
+ sDefaultColor = res.getColor(R.color.letter_tile_default_color);
+ sTileLetterFontSize = res.getDimensionPixelSize(R.dimen.tile_letter_font_size);
+ sTileLetterFontSizeSmall = res
+ .getDimensionPixelSize(R.dimen.tile_letter_font_size_small);
+ sTileFontColor = res.getColor(R.color.letter_tile_font_color);
+ DEFAULT_AVATAR = BitmapFactory.decodeResource(res, R.drawable.ic_generic_man);
+
+ sPaint.setTypeface(Typeface.create("sans-serif-light", Typeface.NORMAL));
+ sPaint.setTextAlign(Align.CENTER);
+ sPaint.setAntiAlias(true);
+ }
+ }
+
+ @Override
+ public void draw(final Canvas canvas) {
+ final Rect bounds = getBounds();
+ if (!isVisible() || bounds.isEmpty()) {
+ return;
+ }
+
+ if (mBitmap != null && mBitmap.bmp != null) {
+ // Draw sender image.
+ drawBitmap(mBitmap.bmp, mBitmap.getLogicalWidth(), mBitmap.getLogicalHeight(), canvas);
+ } else {
+ // Draw letter tile.
+ drawLetterTile(canvas);
+ }
+ }
+
+ /**
+ * Draw the bitmap onto the canvas at the current bounds taking into account the current scale.
+ */
+ private void drawBitmap(final Bitmap bitmap, final int width, final int height,
+ final Canvas canvas) {
+ final Rect bounds = getBounds();
+
+ if (mScale != ContactGridDrawable.SCALE_TYPE_HALF) {
+ sRect.set(0, 0, width, height);
+ } else {
+ // For skinny bounds, draw the middle two quarters.
+ sRect.set(width / 4, 0, width / 4 * 3, height);
+ }
+ canvas.drawBitmap(bitmap, sRect, bounds, mPaint);
+ }
+
+ private void drawLetterTile(final Canvas canvas) {
+ if (mContactRequest == null) {
+ return;
+ }
+
+ // Draw background color.
+ final String email = mContactRequest.getEmail();
+ sPaint.setColor(pickColor(email));
+ sPaint.setAlpha(mPaint.getAlpha());
+ canvas.drawRect(getBounds(), sPaint);
+
+ // Draw letter/digit or generic avatar.
+ final String displayName = mContactRequest.getDisplayName();
+ final char firstChar = displayName.charAt(0);
+ final Rect bounds = getBounds();
+ if (isEnglishLetterOrDigit(firstChar)) {
+ // Draw letter or digit.
+ sFirstChar[0] = Character.toUpperCase(firstChar);
+ sPaint.setTextSize(mScale == ContactGridDrawable.SCALE_TYPE_ONE ? sTileLetterFontSize
+ : sTileLetterFontSizeSmall);
+ sPaint.getTextBounds(sFirstChar, 0, 1, sRect);
+ sPaint.setColor(sTileFontColor);
+ canvas.drawText(sFirstChar, 0, 1, bounds.centerX(),
+ bounds.centerY() + sRect.height() / 2, sPaint);
+ } else {
+ drawBitmap(DEFAULT_AVATAR, DEFAULT_AVATAR.getWidth(), DEFAULT_AVATAR.getHeight(),
+ canvas);
+ }
+ }
+
+ private static int pickColor(final String email) {
+ // String.hashCode() implementation is not supposed to change across java versions, so
+ // this should guarantee the same email address always maps to the same color.
+ // The email should already have been normalized by the ContactRequest.
+ final int color = Math.abs(email.hashCode()) % NUM_OF_TILE_COLORS;
+ return sColors.getColor(color, sDefaultColor);
+ }
+
+ private static boolean isEnglishLetterOrDigit(final char c) {
+ return ('A' <= c && c <= 'Z') || ('a' <= c && c <= 'z') || ('0' <= c && c <= '9');
+ }
+
+ @Override
+ public void setAlpha(final int alpha) {
+ mPaint.setAlpha(alpha);
+ }
+
+ @Override
+ public void setColorFilter(final ColorFilter cf) {
+ mPaint.setColorFilter(cf);
+ }
+
+ @Override
+ public int getOpacity() {
+ return 0;
+ }
+
+ public void setDecodeDimensions(final int decodeWidth, final int decodeHeight) {
+ mCache.setPoolDimensions(decodeWidth, decodeHeight);
+ }
+
+ public void setScale(final int scale) {
+ mScale = scale;
+ }
+
+ public void unbind() {
+ setImage(null);
+ }
+
+ public void bind(final String name, final String email) {
+ setImage(new ContactRequest(name, email));
+ }
+
+ private void setImage(final ContactRequest contactRequest) {
+ if (mContactRequest != null && mContactRequest.equals(contactRequest)) {
+ return;
+ }
+
+ if (mBitmap != null) {
+ mBitmap.releaseReference();
+ mBitmap = null;
+ }
+
+ mContactResolver.remove(mContactRequest, this);
+ mContactRequest = contactRequest;
+
+ if (contactRequest == null) {
+ invalidateSelf();
+ return;
+ }
+
+ final ReusableBitmap cached = mCache.get(contactRequest, true /* incrementRefCount */);
+ if (cached != null) {
+ setBitmap(cached);
+ } else {
+ decode();
+ }
+ }
+
+ private void setBitmap(final ReusableBitmap bmp) {
+ if (mBitmap != null && mBitmap != bmp) {
+ mBitmap.releaseReference();
+ }
+ mBitmap = bmp;
+ invalidateSelf();
+ }
+
+ private void decode() {
+ if (mContactRequest == null) {
+ return;
+ }
+ // Add to batch.
+ mContactResolver.add(mContactRequest, this);
+ }
+
+ public void onDecodeComplete(final Request key, final ReusableBitmap result) {
+ final ContactRequest request = (ContactRequest) key;
+ // Remove from batch.
+ mContactResolver.remove(request, this);
+ if (request.equals(mContactRequest)) {
+ setBitmap(result);
+ } else {
+ // if the requests don't match (i.e. this request is stale), decrement the
+ // ref count to allow the bitmap to be pooled
+ if (result != null) {
+ result.releaseReference();
+ }
+ }
+ }
+}
diff --git a/src/com/android/mail/bitmap/ContactGridDrawable.java b/src/com/android/mail/bitmap/ContactGridDrawable.java
new file mode 100644
index 0000000..590f806
--- /dev/null
+++ b/src/com/android/mail/bitmap/ContactGridDrawable.java
@@ -0,0 +1,162 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.mail.bitmap;
+
+import android.content.res.Resources;
+import android.graphics.Canvas;
+import android.graphics.ColorFilter;
+import android.graphics.Paint;
+import android.graphics.Rect;
+
+import com.android.bitmap.BitmapCache;
+import com.android.mail.R;
+
+/**
+ * A 2x2 grid of contact drawables. Adds horizontal and vertical dividers.
+ */
+public class ContactGridDrawable extends CompositeDrawable<ContactDrawable> {
+
+ public static final int SCALE_TYPE_ONE = 0;
+ public static final int SCALE_TYPE_HALF = 1;
+ public static final int SCALE_TYPE_QUARTER = 2;
+
+ private static final int MAX_CONTACTS_COUNT = 4;
+
+ private ContactResolver mContactResolver;
+ private BitmapCache mCache;
+ private final Resources mRes;
+ private Paint mPaint;
+
+ private static int sDividerWidth = -1;
+ private static int sDividerColor;
+
+ public ContactGridDrawable(final Resources res) {
+ super(MAX_CONTACTS_COUNT);
+
+ if (sDividerWidth == -1) {
+ sDividerWidth = res.getDimensionPixelSize(R.dimen.tile_divider_width);
+ sDividerColor = res.getColor(R.color.tile_divider_color);
+ }
+
+ mRes = res;
+ mPaint = new Paint();
+ mPaint.setStrokeWidth(sDividerWidth);
+ mPaint.setColor(sDividerColor);
+ }
+
+ @Override
+ protected ContactDrawable createDivisionDrawable(final int i) {
+ final ContactDrawable drawable = new ContactDrawable(mRes, mCache, mContactResolver);
+ drawable.setScale(calculateScale(i));
+ return drawable;
+ }
+
+ @Override
+ public void setCount(final int count) {
+ super.setCount(count);
+
+ for (int i = 0; i < mCount; i++) {
+ final ContactDrawable drawable = mDrawables.get(i);
+ if (drawable != null) {
+ drawable.setScale(calculateScale(i));
+ }
+ }
+ }
+
+ /**
+ * Given which section a drawable is in, calculate its scale based on the current total count.
+ * @param i The section, indexed by 0.
+ */
+ private int calculateScale(final int i) {
+ switch (mCount) {
+ case 1:
+ // 1 bitmap: passthrough
+ return SCALE_TYPE_ONE;
+ case 2:
+ // 2 bitmaps split vertically
+ return SCALE_TYPE_HALF;
+ case 3:
+ // 1st is tall on the left, 2nd/3rd stacked vertically on the right
+ return i == 0 ? SCALE_TYPE_HALF : SCALE_TYPE_QUARTER;
+ case 4:
+ // 4 bitmaps in a 2x2 grid
+ return SCALE_TYPE_QUARTER;
+ default:
+ return SCALE_TYPE_ONE;
+ }
+ }
+
+ @Override
+ public void draw(final Canvas canvas) {
+ super.draw(canvas);
+
+ final Rect bounds = getBounds();
+ if (!isVisible() || bounds.isEmpty()) {
+ return;
+ }
+
+ // Draw horizontal and vertical dividers.
+ switch (mCount) {
+ case 1:
+ // 1 bitmap: passthrough
+ break;
+ case 2:
+ // 2 bitmaps split vertically
+ canvas.drawLine(bounds.centerX(), bounds.top, bounds.centerX(), bounds.bottom,
+ mPaint);
+ break;
+ case 3:
+ // 1st is tall on the left, 2nd/3rd stacked vertically on the right
+ canvas.drawLine(bounds.centerX(), bounds.top, bounds.centerX(), bounds.bottom,
+ mPaint);
+ canvas.drawLine(bounds.centerX(), bounds.centerY(), bounds.right, bounds.centerY(),
+ mPaint);
+ break;
+ case 4:
+ // 4 bitmaps in a 2x2 grid
+ canvas.drawLine(bounds.centerX(), bounds.top, bounds.centerX(), bounds.bottom,
+ mPaint);
+ canvas.drawLine(bounds.left, bounds.centerY(), bounds.right, bounds.centerY(),
+ mPaint);
+ break;
+ }
+ }
+
+ @Override
+ public void setAlpha(final int alpha) {
+ super.setAlpha(alpha);
+ final int old = mPaint.getAlpha();
+ mPaint.setAlpha(alpha);
+ if (alpha != old) {
+ invalidateSelf();
+ }
+ }
+
+ @Override
+ public void setColorFilter(final ColorFilter cf) {
+ super.setColorFilter(cf);
+ mPaint.setColorFilter(cf);
+ invalidateSelf();
+ }
+
+ public void setBitmapCache(final BitmapCache cache) {
+ mCache = cache;
+ }
+
+ public void setContactResolver(final ContactResolver contactResolver) {
+ mContactResolver = contactResolver;
+ }
+}
diff --git a/src/com/android/mail/bitmap/ContactRequest.java b/src/com/android/mail/bitmap/ContactRequest.java
new file mode 100644
index 0000000..b0bd2a0
--- /dev/null
+++ b/src/com/android/mail/bitmap/ContactRequest.java
@@ -0,0 +1,167 @@
+/*
+ * 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.bitmap;
+
+import android.content.res.AssetFileDescriptor;
+import android.text.TextUtils;
+
+import com.android.bitmap.DecodeTask;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+
+/**
+ * A request object for contact images. ContactRequests have a destination because multiple
+ * ContactRequests can share the same decoded data.
+ */
+public class ContactRequest implements DecodeTask.Request {
+
+ private final String mName;
+ private final String mEmail;
+
+ public byte[] bytes;
+
+ public ContactRequest(final String name, final String email) {
+ mName = name;
+ mEmail = normalizeEmail(email);
+ }
+
+ private String normalizeEmail(final String email) {
+ if (TextUtils.isEmpty(email)) {
+ throw new IllegalArgumentException("Email must not be empty.");
+ }
+ // todo: b/10258788
+ return email;
+ }
+
+ @Override
+ public boolean equals(final Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+
+ final ContactRequest that = (ContactRequest) o;
+
+ // Only count email, so we can pull results out of the cache that are from other contact
+ // requests.
+ //noinspection RedundantIfStatement
+ if (mEmail != null ? !mEmail.equals(that.mEmail) : that.mEmail != null) {
+ return false;
+ }
+
+ return true;
+ }
+
+ @Override
+ public int hashCode() {
+ // Only count email, so we can pull results out of the cache that are from other contact
+ // requests.
+ return mEmail != null ? mEmail.hashCode() : 0;
+ }
+
+ @Override
+ public String toString() {
+ return "[" + super.toString() + " mName=" + mName + " mEmail=" + mEmail + "]";
+ }
+
+ @Override
+ public AssetFileDescriptor createFd() throws IOException {
+ return null;
+ }
+
+ @Override
+ public InputStream createInputStream() throws IOException {
+ return new ByteArrayInputStream(bytes);
+ }
+
+ @Override
+ public boolean hasOrientationExif() throws IOException {
+ return false;
+ }
+
+ public String getEmail() {
+ return mEmail;
+ }
+
+ public String getDisplayName() {
+ return !TextUtils.isEmpty(mName) ? mName : mEmail;
+ }
+
+ /**
+ * This ContactRequest wrapper provides implementations of equals() and hashcode() that
+ * include the destination. We need to put multiple ContactRequests in a set,
+ * but its implementations of equals() and hashcode() don't include the destination.
+ */
+ public static class ContactRequestHolder {
+
+ public final ContactRequest contactRequest;
+ public final ContactDrawable destination;
+
+ public ContactRequestHolder(final ContactRequest contactRequest,
+ final ContactDrawable destination) {
+ this.contactRequest = contactRequest;
+ this.destination = destination;
+ }
+
+ @Override
+ public boolean equals(final Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+
+ final ContactRequestHolder that = (ContactRequestHolder) o;
+
+ if (contactRequest != null ? !contactRequest.equals(that.contactRequest)
+ : that.contactRequest != null) {
+ return false;
+ }
+ //noinspection RedundantIfStatement
+ if (destination != null ? !destination.equals(that.destination)
+ : that.destination != null) {
+ return false;
+ }
+
+ return true;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = contactRequest != null ? contactRequest.hashCode() : 0;
+ result = 31 * result + (destination != null ? destination.hashCode() : 0);
+ return result;
+ }
+
+ @Override
+ public String toString() {
+ return contactRequest.toString();
+ }
+
+ public String getEmail() {
+ return contactRequest.getEmail();
+ }
+
+ public String getDisplayName() {
+ return contactRequest.getDisplayName();
+ }
+ }
+}
diff --git a/src/com/android/mail/bitmap/ContactResolver.java b/src/com/android/mail/bitmap/ContactResolver.java
new file mode 100644
index 0000000..b3d1fd5
--- /dev/null
+++ b/src/com/android/mail/bitmap/ContactResolver.java
@@ -0,0 +1,260 @@
+/*
+ * 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.bitmap;
+
+import android.content.ContentResolver;
+import android.os.AsyncTask;
+import android.os.AsyncTask.Status;
+import android.os.Handler;
+
+import com.android.bitmap.BitmapCache;
+import com.android.bitmap.DecodeTask;
+import com.android.bitmap.ReusableBitmap;
+import com.android.ex.photo.util.Trace;
+import com.android.mail.ContactInfo;
+import com.android.mail.SenderInfoLoader;
+import com.android.mail.bitmap.ContactRequest.ContactRequestHolder;
+import com.android.mail.utils.LogTag;
+import com.android.mail.utils.LogUtils;
+import com.google.common.collect.ImmutableMap;
+
+import java.util.HashSet;
+import java.util.LinkedHashSet;
+import java.util.Set;
+import java.util.concurrent.Executor;
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.ThreadPoolExecutor;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Batches up ContactRequests so we can efficiently query the contacts provider. Kicks off a
+ * ContactResolverTask to query for contact images in the background.
+ */
+public class ContactResolver implements Runnable {
+
+ private static final String TAG = LogTag.getLogTag();
+
+ private final ContentResolver mResolver;
+ private final BitmapCache mCache;
+ /** Insertion ordered set allows us to work from the top down. */
+ private final LinkedHashSet<ContactRequestHolder> mBatch;
+
+ private final Handler mHandler = new Handler();
+ private ContactResolverTask mTask;
+
+
+ /** Size 1 pool mostly to make systrace output traces on one line. */
+ private static final Executor SMALL_POOL_EXECUTOR = new ThreadPoolExecutor(1, 1,
+ 1, TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>());
+ private static final Executor EXECUTOR = SMALL_POOL_EXECUTOR;
+
+ public ContactResolver(final ContentResolver resolver, final BitmapCache cache) {
+ mResolver = resolver;
+ mCache = cache;
+ mBatch = new LinkedHashSet<ContactRequestHolder>();
+ }
+
+ @Override
+ public void run() {
+ // Start to process a new batch.
+ if (mBatch.isEmpty()) {
+ return;
+ }
+
+ if (mTask != null && mTask.getStatus() == Status.RUNNING) {
+ LogUtils.d(TAG, "ContactResolver << batch skip");
+ return;
+ }
+
+ Trace.beginSection("ContactResolver run");
+ LogUtils.d(TAG, "ContactResolver >> batch start");
+
+ // Make a copy of the batch.
+ LinkedHashSet<ContactRequestHolder> batch = new LinkedHashSet<ContactRequestHolder>(mBatch);
+
+ if (mTask != null) {
+ mTask.cancel(true);
+ }
+
+ mTask = new ContactResolverTask(batch, mResolver, mCache, this);
+ mTask.executeOnExecutor(EXECUTOR);
+ Trace.endSection();
+ }
+
+ public void add(final ContactRequest request, final ContactDrawable drawable) {
+ mBatch.add(new ContactRequestHolder(request, drawable));
+ notifyBatchReady();
+ }
+
+ public void remove(final ContactRequest request, final ContactDrawable drawable) {
+ mBatch.remove(new ContactRequestHolder(request, drawable));
+ }
+
+ /**
+ * A layout pass traverses the whole tree during a single iteration of the event loop. That
+ * means that every ContactDrawable on the screen will add its ContactRequest to the batch in
+ * a single iteration of the event loop.
+ *
+ * <p/>
+ * We take advantage of this by posting a Runnable (happens to be this object) at the end of
+ * the event queue. Every time something is added to the batch as part of the same layout pass,
+ * the Runnable is moved to the back of the queue. When the next layout pass occurs,
+ * it is placed in the event loop behind this Runnable. That allows us to process the batch
+ * that was added previously.
+ */
+ private void notifyBatchReady() {
+ LogUtils.d(TAG, "ContactResolver > batch %d", mBatch.size());
+ mHandler.removeCallbacks(this);
+ mHandler.post(this);
+ }
+
+ /**
+ * This is not a very traditional AsyncTask, in the sense that we do not care about what gets
+ * returned in doInBackground(). Instead, we signal traditional "return values" through
+ * publishProgress().
+ *
+ * <p/>
+ * The reason we do this is because this task is responsible for decoding an entire batch of
+ * ContactRequests. But, we do not want to have to wait to decode all of them before updating
+ * any views. So we must do all the work in doInBackground(),
+ * but upon finishing each individual task, we need to jump out to the UI thread and update
+ * that view.
+ */
+ private static class ContactResolverTask extends AsyncTask<Void, Result, Void> {
+
+ private final Set<ContactRequestHolder> mContactRequests;
+ private final ContentResolver mResolver;
+ private final BitmapCache mCache;
+ private final ContactResolver mCallback;
+
+ public ContactResolverTask(final Set<ContactRequestHolder> contactRequests,
+ final ContentResolver resolver, final BitmapCache cache,
+ final ContactResolver callback) {
+ mContactRequests = contactRequests;
+ mResolver = resolver;
+ mCache = cache;
+ mCallback = callback;
+ }
+
+ @Override
+ protected Void doInBackground(final Void... params) {
+ Trace.beginSection("set up");
+ final Set<String> emails = new HashSet<String>(mContactRequests.size());
+ for (ContactRequestHolder request : mContactRequests) {
+ final String email = request.getEmail();
+ emails.add(email);
+ }
+ Trace.endSection();
+
+ Trace.beginSection("load contact photo bytes");
+ // Query the contacts provider for the current batch of emails.
+ ImmutableMap<String, ContactInfo> contactInfos = SenderInfoLoader
+ .loadContactPhotos(mResolver, emails, false /* decodeBitmaps */);
+ Trace.endSection();
+
+ for (ContactRequestHolder request : mContactRequests) {
+ Trace.beginSection("decode");
+ final String email = request.getEmail();
+ if (contactInfos == null) {
+ // Query failed.
+ LogUtils.d(TAG, "ContactResolver -- failed %s", email);
+ publishProgress(new Result(request, null));
+ Trace.endSection();
+ continue;
+ }
+
+ final ContactInfo contactInfo = contactInfos.get(email);
+ if (contactInfo == null) {
+ // Request skipped. Try again next batch.
+ LogUtils.d(TAG, "ContactResolver = skipped %s", email);
+ Trace.endSection();
+ continue;
+ }
+
+ // Query attempted.
+ final byte[] photo = contactInfo.photoBytes;
+ if (photo == null) {
+ // No photo bytes found.
+ LogUtils.d(TAG, "ContactResolver -- failed %s", email);
+ publishProgress(new Result(request, null));
+ Trace.endSection();
+ continue;
+ }
+
+ // Query succeeded. Photo bytes found.
+ request.contactRequest.bytes = photo;
+
+ // Start decode.
+ LogUtils.d(TAG, "ContactResolver ++ found %s", email);
+ final ReusableBitmap result;
+ final int width = mCache.getDecodeWidth();
+ final int height = mCache.getDecodeHeight();
+ // Synchronously decode the photo bytes. We are already in a background
+ // thread, and we want decodes to finish in order. The decodes are blazing
+ // fast so we don't need to kick off multiple threads.
+ result = new DecodeTask(request.contactRequest, width, height, width, height, null,
+ mCache).decode();
+ request.contactRequest.bytes = null;
+
+ // Decode success.
+ publishProgress(new Result(request, result));
+ Trace.endSection();
+ }
+
+ return null;
+ }
+
+ /**
+ * We use progress updates to jump to the UI thread so we can decode the batch
+ * incrementally.
+ */
+ @Override
+ protected void onProgressUpdate(final Result... values) {
+ final ContactRequestHolder request = values[0].request;
+ final ReusableBitmap bitmap = values[0].bitmap;
+
+ // DecodeTask does not add null results to the cache.
+ if (bitmap == null) {
+ // Cache null result.
+ mCache.put(request.contactRequest, null);
+ }
+
+ request.destination.onDecodeComplete(request.contactRequest, bitmap);
+ }
+
+ @Override
+ protected void onPostExecute(final Void aVoid) {
+ // Batch completed. Start next batch.
+ mCallback.notifyBatchReady();
+ }
+ }
+
+ /**
+ * Wrapper for the ContactRequest and its decoded bitmap. This class is used to pass results
+ * to onProgressUpdate().
+ */
+ private static class Result {
+ public final ContactRequestHolder request;
+ public final ReusableBitmap bitmap;
+
+ private Result(final ContactRequestHolder request, final ReusableBitmap bitmap) {
+ this.request = request;
+ this.bitmap = bitmap;
+ }
+ }
+}
diff --git a/src/com/android/mail/bitmap/FlipDrawable.java b/src/com/android/mail/bitmap/FlipDrawable.java
new file mode 100644
index 0000000..6cc7b26
--- /dev/null
+++ b/src/com/android/mail/bitmap/FlipDrawable.java
@@ -0,0 +1,269 @@
+/*
+ * 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.bitmap;
+
+import android.animation.ValueAnimator;
+import android.animation.ValueAnimator.AnimatorUpdateListener;
+import android.graphics.Canvas;
+import android.graphics.ColorFilter;
+import android.graphics.Rect;
+import android.graphics.drawable.Drawable;
+
+import com.android.mail.utils.LogUtils;
+
+/**
+ * A drawable that wraps two other drawables and allows flipping between them. The flipping
+ * animation is a 2D rotation around the y axis.
+ *
+ * <p/>
+ * The 3 durations are: (best viewed in documentation form)
+ * <pre>
+ * <pre>[_][]|[][_]<post>
+ * | | |
+ * V V V
+ * <pre>< flip ><post>
+ * </pre>
+ */
+public class FlipDrawable extends Drawable implements Drawable.Callback {
+
+ /**
+ * The inner drawables.
+ */
+ protected final Drawable mFront;
+ protected final Drawable mBack;
+
+ protected final int mFlipDurationMs;
+ protected final int mPreFlipDurationMs;
+ protected final int mPostFlipDurationMs;
+ private final ValueAnimator mFlipAnimator;
+
+ private static final float END_VALUE = 2f;
+
+ /**
+ * From 0f to END_VALUE. Determines the flip progress between mFront and mBack. 0f means
+ * mFront is fully shown, while END_VALUE means mBack is fully shown.
+ */
+ private float mFlipFraction = 0f;
+
+ /**
+ * True if flipping towards front, false if flipping towards back.
+ */
+ private boolean mFlipToSide = true;
+
+ /**
+ * Create a new FlipDrawable. The front is fully shown by default.
+ *
+ * <p/>
+ * The 3 durations are: (best viewed in documentation form)
+ * <pre>
+ * <pre>[_][]|[][_]<post>
+ * | | |
+ * V V V
+ * <pre>< flip ><post>
+ * </pre>
+ *
+ * @param front The front drawable.
+ * @param back The back drawable.
+ * @param flipDurationMs The duration of the actual flip. This duration includes both
+ * animating away one side and showing the other.
+ * @param preFlipDurationMs The duration before the actual flip begins. Subclasses can use this
+ * to add flourish.
+ * @param postFlipDurationMs The duration after the actual flip begins. Subclasses can use this
+ * to add flourish.
+ */
+ public FlipDrawable(final Drawable front, final Drawable back, final int flipDurationMs,
+ final int preFlipDurationMs, final int postFlipDurationMs) {
+ if (front == null || back == null) {
+ throw new IllegalArgumentException("Front and back drawables must not be null.");
+ }
+ mFront = front;
+ mBack = back;
+
+ mFront.setCallback(this);
+ mBack.setCallback(this);
+
+ mFlipDurationMs = flipDurationMs;
+ mPreFlipDurationMs = preFlipDurationMs;
+ mPostFlipDurationMs = postFlipDurationMs;
+
+ mFlipAnimator = ValueAnimator.ofFloat(0f, END_VALUE)
+ .setDuration(mPreFlipDurationMs + mFlipDurationMs + mPostFlipDurationMs);
+ mFlipAnimator.addUpdateListener(new AnimatorUpdateListener() {
+ @Override
+ public void onAnimationUpdate(final ValueAnimator animation) {
+ final float old = mFlipFraction;
+ //noinspection ConstantConditions
+ mFlipFraction = (Float) animation.getAnimatedValue();
+ if (old != mFlipFraction) {
+ invalidateSelf();
+ }
+ }
+ });
+
+ reset(true);
+ }
+
+ @Override
+ protected void onBoundsChange(final Rect bounds) {
+ super.onBoundsChange(bounds);
+ if (bounds.isEmpty()) {
+ mFront.setBounds(0, 0, 0, 0);
+ mBack.setBounds(0, 0, 0, 0);
+ } else {
+ mFront.setBounds(bounds);
+ mBack.setBounds(bounds);
+ }
+ }
+
+ @Override
+ public void draw(final Canvas canvas) {
+ final Rect bounds = getBounds();
+ if (!isVisible() || bounds.isEmpty()) {
+ return;
+ }
+
+ final Drawable inner = getSideShown() /* == front */ ? mFront : mBack;
+
+ final float totalDurationMs = mPreFlipDurationMs + mFlipDurationMs + mPostFlipDurationMs;
+
+ final float scaleX;
+ if (mFlipFraction / 2 <= mPreFlipDurationMs / totalDurationMs) {
+ // During pre-flip.
+ scaleX = 1;
+ } else if (mFlipFraction / 2 >= (totalDurationMs - mPostFlipDurationMs) / totalDurationMs) {
+ // During post-flip.
+ scaleX = 1;
+ } else {
+ // During flip.
+ final float flipFraction = mFlipFraction / 2;
+ final float flipMiddle = (mPreFlipDurationMs / totalDurationMs
+ + (totalDurationMs - mPostFlipDurationMs) / totalDurationMs) / 2;
+ final float distFraction = Math.abs(flipFraction - flipMiddle);
+ final float multiplier = 1 / (flipMiddle - (mPreFlipDurationMs / totalDurationMs));
+ scaleX = distFraction * multiplier;
+ }
+
+ canvas.save();
+ // The flip is a simple 1 dimensional scale.
+ canvas.scale(scaleX, 1, bounds.exactCenterX(), bounds.exactCenterY());
+ inner.draw(canvas);
+ canvas.restore();
+ }
+
+ @Override
+ public void setAlpha(final int alpha) {
+ mFront.setAlpha(alpha);
+ mBack.setAlpha(alpha);
+ }
+
+ @Override
+ public void setColorFilter(final ColorFilter cf) {
+ mFront.setColorFilter(cf);
+ mBack.setColorFilter(cf);
+ }
+
+ @Override
+ public int getOpacity() {
+ return resolveOpacity(mFront.getOpacity(), mBack.getOpacity());
+ }
+
+ @Override
+ protected boolean onLevelChange(final int level) {
+ return mFront.setLevel(level) || mBack.setLevel(level);
+ }
+
+ @Override
+ public void invalidateDrawable(final Drawable who) {
+ invalidateSelf();
+ }
+
+ @Override
+ public void scheduleDrawable(final Drawable who, final Runnable what, final long when) {
+ scheduleSelf(what, when);
+ }
+
+ @Override
+ public void unscheduleDrawable(final Drawable who, final Runnable what) {
+ unscheduleSelf(what);
+ }
+
+ /**
+ * Stop animating the flip and reset to one side.
+ * @param side Pass true if reset to front, false if reset to back.
+ */
+ public void reset(final boolean side) {
+ final float old = mFlipFraction;
+ mFlipAnimator.cancel();
+ mFlipFraction = side ? 0f : 2f;
+ mFlipToSide = side;
+ if (mFlipFraction != old) {
+ invalidateSelf();
+ }
+ }
+
+ /**
+ * Returns true if the front is shown. Returns false if the back is shown.
+ */
+ public boolean getSideShown() {
+ final float totalDurationMs = mPreFlipDurationMs + mFlipDurationMs + mPostFlipDurationMs;
+ final float middleFraction = (mPreFlipDurationMs / totalDurationMs
+ + (totalDurationMs - mPostFlipDurationMs) / totalDurationMs) / 2;
+ return mFlipFraction / 2 < middleFraction;
+ }
+
+ /**
+ * Returns true if the front is being flipped towards. Returns false if the back is being
+ * flipped towards.
+ */
+ public boolean getSideFlippingTowards() {
+ return mFlipToSide;
+ }
+
+ /**
+ * Starts an animated flip to the other side. If a flip animation is currently started,
+ * it will be reversed.
+ */
+ public void flip() {
+ mFlipToSide = !mFlipToSide;
+ if (mFlipAnimator.isStarted()) {
+ mFlipAnimator.reverse();
+ } else {
+ if (!mFlipToSide /* front to back */) {
+ mFlipAnimator.start();
+ } else /* back to front */ {
+ mFlipAnimator.reverse();
+ }
+ }
+ }
+
+ /**
+ * Start an animated flip to a side. This works regardless of whether a flip animation is
+ * currently started.
+ * @param side Pass true if flip to front, false if flip to back.
+ */
+ public void flipTo(final boolean side) {
+ if (mFlipToSide != side) {
+ flip();
+ }
+ }
+
+ /**
+ * Returns whether flipping is in progress.
+ */
+ public boolean isFlipping() {
+ return mFlipAnimator.isStarted();
+ }
+}
diff --git a/src/com/android/mail/bitmap/ImageAttachmentRequest.java b/src/com/android/mail/bitmap/ImageAttachmentRequest.java
index 6c58772..b680db5 100644
--- a/src/com/android/mail/bitmap/ImageAttachmentRequest.java
+++ b/src/com/android/mail/bitmap/ImageAttachmentRequest.java
@@ -1,6 +1,5 @@
package com.android.mail.bitmap;
-import android.content.ContentResolver;
import android.content.Context;
import android.content.res.AssetFileDescriptor;
import android.database.Cursor;
@@ -25,6 +24,9 @@
private final int mRendition;
public final int mDestW;
+ private Uri mCachedUri;
+ private String mCachedMimeType;
+
public ImageAttachmentRequest(final Context context, final String lookupUri,
final int rendition, final int destW) {
mContext = context;
@@ -78,26 +80,40 @@
@Override
public AssetFileDescriptor createFd() throws IOException {
- AssetFileDescriptor result = null;
+ if (mCachedUri == null) {
+ cacheValues();
+ }
+ return mContext.getContentResolver().openAssetFileDescriptor(mCachedUri, "r");
+ }
+
+ private void cacheValues() throws IOException {
Cursor cursor = null;
- final ContentResolver cr = mContext.getContentResolver();
try {
- cursor = cr.query(Uri.parse(mLookupUri), UIProvider.ATTACHMENT_PROJECTION, null, null,
- null);
+ cursor = mContext.getContentResolver().query(Uri.parse(mLookupUri),
+ UIProvider.ATTACHMENT_PROJECTION, null, null, null);
if (cursor != null && cursor.moveToFirst()) {
final Attachment a = new Attachment(cursor);
- result = cr.openAssetFileDescriptor(a.getUriForRendition(mRendition), "r");
+ mCachedUri = a.getUriForRendition(mRendition);
+ final String mimeType = a.getContentType();
+ mCachedMimeType = mimeType != null ? mimeType.toLowerCase() : null;
}
} finally {
if (cursor != null) {
cursor.close();
}
}
- return result;
}
@Override
public InputStream createInputStream() throws IOException {
return null;
}
+
+ @Override
+ public boolean hasOrientationExif() throws IOException {
+ if (mCachedUri == null) {
+ cacheValues();
+ }
+ return mCachedMimeType == null || mCachedMimeType.equals("image/jpeg");
+ }
}
diff --git a/src/com/android/mail/browse/ConversationCursor.java b/src/com/android/mail/browse/ConversationCursor.java
index 6c15e88..7a3ca0f 100644
--- a/src/com/android/mail/browse/ConversationCursor.java
+++ b/src/com/android/mail/browse/ConversationCursor.java
@@ -165,6 +165,8 @@
private final Handler mMainThreadHandler = new Handler(Looper.getMainLooper());
+ private final boolean mCachingEnabled;
+
private void setCursor(UnderlyingCursorWrapper cursor) {
// If we have an existing underlying cursor, make sure it's closed
if (mUnderlyingCursor != null) {
@@ -193,6 +195,9 @@
mName = name;
qProjection = UIProvider.CONVERSATION_PROJECTION;
mCursorObserver = new CursorObserver(new Handler(Looper.getMainLooper()));
+
+ // Disable caching on low memory devices
+ mCachingEnabled = !Utils.isLowRamDevice(activity);
}
/**
@@ -357,7 +362,7 @@
* notes on thread safety.
*/
private int mCachePos;
- private boolean mCachingEnabled = true;
+ private boolean mCachingEnabled;
private final NewCursorUpdateObserver mCursorUpdateObserver;
private boolean mUpdateObserverRegistered = false;
@@ -370,9 +375,11 @@
private boolean mCursorUpdated = false;
- public UnderlyingCursorWrapper(Cursor result) {
+ public UnderlyingCursorWrapper(Cursor result, boolean cachingEnabled) {
super(result);
+ mCachingEnabled = cachingEnabled;
+
// Register the content observer immediately, as we want to make sure that we don't miss
// any updates
mCursorUpdateObserver =
@@ -639,7 +646,8 @@
uri, time, result.getCount());
}
System.gc();
- return new UnderlyingCursorWrapper(result);
+
+ return new UnderlyingCursorWrapper(result, mCachingEnabled);
}
static boolean offUiThread() {
@@ -1946,6 +1954,15 @@
}
@Override
+ public Uri getNotificationUri() {
+ if (mUnderlyingCursor == null) {
+ return null;
+ } else {
+ return mUnderlyingCursor.getNotificationUri();
+ }
+ }
+
+ @Override
public void setNotificationUri(ContentResolver cr, Uri uri) {
throw new UnsupportedOperationException();
}
diff --git a/src/com/android/mail/browse/ConversationItemView.java b/src/com/android/mail/browse/ConversationItemView.java
index 0d2c3fb..16b9933 100644
--- a/src/com/android/mail/browse/ConversationItemView.java
+++ b/src/com/android/mail/browse/ConversationItemView.java
@@ -18,8 +18,6 @@
package com.android.mail.browse;
import android.animation.Animator;
-import android.animation.Animator.AnimatorListener;
-import android.animation.AnimatorListenerAdapter;
import android.animation.AnimatorSet;
import android.animation.ObjectAnimator;
import android.content.ClipData;
@@ -31,7 +29,6 @@
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.LinearGradient;
-import android.graphics.Matrix;
import android.graphics.Paint;
import android.graphics.Point;
import android.graphics.Rect;
@@ -60,7 +57,6 @@
import android.view.ViewGroup;
import android.view.ViewParent;
import android.view.animation.DecelerateInterpolator;
-import android.view.animation.LinearInterpolator;
import android.widget.AbsListView;
import android.widget.AbsListView.OnScrollListener;
import android.widget.TextView;
@@ -71,11 +67,10 @@
import com.android.mail.analytics.Analytics;
import com.android.mail.bitmap.AttachmentDrawable;
import com.android.mail.bitmap.AttachmentGridDrawable;
+import com.android.mail.bitmap.ContactCheckableGridDrawable;
+import com.android.mail.bitmap.ContactDrawable;
import com.android.mail.browse.ConversationItemViewModel.SenderFragment;
import com.android.mail.perf.Timer;
-import com.android.mail.photomanager.ContactPhotoManager;
-import com.android.mail.photomanager.ContactPhotoManager.ContactIdentifier;
-import com.android.mail.photomanager.PhotoManager.PhotoIdentifier;
import com.android.mail.providers.Address;
import com.android.mail.providers.Attachment;
import com.android.mail.providers.Conversation;
@@ -86,9 +81,9 @@
import com.android.mail.providers.UIProvider.ConversationListIcon;
import com.android.mail.providers.UIProvider.FolderType;
import com.android.mail.ui.AnimatedAdapter;
-import com.android.mail.ui.AnimatedAdapter.ConversationListListener;
import com.android.mail.ui.ControllableActivity;
import com.android.mail.ui.ConversationSelectionSet;
+import com.android.mail.ui.ConversationSetObserver;
import com.android.mail.ui.DividedImageCanvas;
import com.android.mail.ui.DividedImageCanvas.InvalidateCallback;
import com.android.mail.ui.FolderDisplayer;
@@ -101,13 +96,12 @@
import com.android.mail.utils.LogUtils;
import com.android.mail.utils.Utils;
import com.google.common.annotations.VisibleForTesting;
-import com.google.common.collect.Lists;
import java.util.ArrayList;
-import java.util.List;
public class ConversationItemView extends View
- implements SwipeableItemView, ToggleableItem, InvalidateCallback, OnScrollListener {
+ implements SwipeableItemView, ToggleableItem, InvalidateCallback, OnScrollListener,
+ ConversationSetObserver {
// Timer.
private static int sLayoutCount = 0;
@@ -124,7 +118,6 @@
// Static bitmaps.
private static Bitmap STAR_OFF;
private static Bitmap STAR_ON;
- private static Bitmap CHECK;
private static Bitmap ATTACHMENT;
private static Bitmap ONLY_TO_ME;
private static Bitmap TO_ME_AND_OTHERS;
@@ -199,15 +192,12 @@
private float mAnimatedHeightFraction = 1.0f;
private final String mAccount;
private ControllableActivity mActivity;
- private ConversationListListener mConversationListListener;
private final TextView mSubjectTextView;
private final TextView mSendersTextView;
private int mGadgetMode;
private boolean mAttachmentPreviewsEnabled;
private boolean mParallaxSpeedAlternative;
private boolean mParallaxDirectionAlternative;
- private final DividedImageCanvas mContactImagesHolder;
- private static ContactPhotoManager sContactPhotoManager;
private static int sFoldersLeftPadding;
private static TextAppearanceSpan sSubjectTextUnreadSpan;
@@ -217,19 +207,9 @@
private static int sScrollSlop;
private static CharacterStyle sActivatedTextSpan;
+ private final ContactCheckableGridDrawable mSendersImageView;
private final AttachmentGridDrawable mAttachmentsView;
- private final Matrix mPhotoFlipMatrix = new Matrix();
- private final Matrix mCheckMatrix = new Matrix();
-
- private final CabAnimator mPhotoFlipAnimator;
-
- /**
- * The conversation id, if this conversation was selected the last time we were in a selection
- * mode. This is reset after any animations complete upon exiting the selection mode.
- */
- private long mLastSelectedId = -1;
-
/** The resource id of the color to use to override the background. */
private int mBackgroundOverrideResId = -1;
/** The bitmap to use, or <code>null</code> for the default */
@@ -257,19 +237,6 @@
sCheckBackgroundPaint.setColor(Color.GRAY);
}
- public static void setScrollStateChanged(final int scrollState) {
- if (sContactPhotoManager == null) {
- return;
- }
- final boolean flinging = scrollState == OnScrollListener.SCROLL_STATE_FLING;
-
- if (flinging) {
- sContactPhotoManager.pause();
- } else {
- sContactPhotoManager.resume();
- }
- }
-
/**
* Handles displaying folders in a conversation header view.
*/
@@ -420,7 +387,6 @@
// Initialize static bitmaps.
STAR_OFF = BitmapFactory.decodeResource(res, R.drawable.ic_btn_star_off);
STAR_ON = BitmapFactory.decodeResource(res, R.drawable.ic_btn_star_on);
- CHECK = BitmapFactory.decodeResource(res, R.drawable.ic_avatar_check);
ATTACHMENT = BitmapFactory.decodeResource(res, R.drawable.ic_attachment_holo_light);
ONLY_TO_ME = BitmapFactory.decodeResource(res, R.drawable.ic_email_caret_double);
TO_ME_AND_OTHERS = BitmapFactory.decodeResource(res, R.drawable.ic_email_caret_single);
@@ -466,24 +432,10 @@
sElidedPaddingToken = res.getString(R.string.elided_padding_token);
sScrollSlop = res.getInteger(R.integer.swipeScrollSlop);
sFoldersLeftPadding = res.getDimensionPixelOffset(R.dimen.folders_left_padding);
- sContactPhotoManager = ContactPhotoManager.createContactPhotoManager(context);
sOverflowCountMax = res.getInteger(integer.ap_overflow_max_count);
- sCabAnimationDuration =
- res.getInteger(R.integer.conv_item_view_cab_anim_duration);
+ sCabAnimationDuration = res.getInteger(R.integer.conv_item_view_cab_anim_duration);
}
- mPhotoFlipAnimator = new CabAnimator("photoFlipFraction", 0, 2,
- sCabAnimationDuration) {
- @Override
- public void invalidateArea() {
- final int left = mCoordinates.contactImagesX;
- final int right = left + mContactImagesHolder.getWidth();
- final int top = mCoordinates.contactImagesY;
- final int bottom = top + mContactImagesHolder.getHeight();
- invalidate(left, top, right, bottom);
- }
- };
-
mSendersTextView = new TextView(mContext);
mSendersTextView.setIncludeFontPadding(false);
@@ -491,27 +443,16 @@
mSubjectTextView.setEllipsize(TextUtils.TruncateAt.END);
mSubjectTextView.setIncludeFontPadding(false);
- mContactImagesHolder = new DividedImageCanvas(context, new InvalidateCallback() {
- @Override
- public void invalidate() {
- if (mCoordinates == null) {
- return;
- }
- ConversationItemView.this.invalidate(mCoordinates.contactImagesX,
- mCoordinates.contactImagesY,
- mCoordinates.contactImagesX + mCoordinates.contactImagesWidth,
- mCoordinates.contactImagesY + mCoordinates.contactImagesHeight);
- }
- });
-
mAttachmentsView = new AttachmentGridDrawable(res, PLACEHOLDER, PROGRESS_BAR);
mAttachmentsView.setCallback(this);
+ mSendersImageView = new ContactCheckableGridDrawable(res, sCabAnimationDuration);
+ mSendersImageView.setCallback(this);
+
Utils.traceEndSection();
}
public void bind(final Conversation conversation, final ControllableActivity activity,
- final ConversationListListener conversationListListener,
final ConversationSelectionSet set, final Folder folder,
final int checkboxOrSenderImage, final boolean showAttachmentPreviews,
final boolean parallaxSpeedAlternative, final boolean parallaxDirectionAlternative,
@@ -519,31 +460,28 @@
final AnimatedAdapter adapter) {
Utils.traceBeginSection("CIVC.bind");
bind(ConversationItemViewModel.forConversation(mAccount, conversation), activity,
- conversationListListener, null /* conversationItemAreaClickListener */, set, folder,
- checkboxOrSenderImage, showAttachmentPreviews, parallaxSpeedAlternative,
- parallaxDirectionAlternative, swipeEnabled, priorityArrowEnabled, adapter,
- -1 /* backgroundOverrideResId */,
- null /* photoBitmap */);
+ null /* conversationItemAreaClickListener */,
+ set, folder, checkboxOrSenderImage, showAttachmentPreviews,
+ parallaxSpeedAlternative, parallaxDirectionAlternative, swipeEnabled,
+ priorityArrowEnabled, adapter, -1 /* backgroundOverrideResId */, null /* photoBitmap */);
Utils.traceEndSection();
}
public void bindAd(final ConversationItemViewModel conversationItemViewModel,
final ControllableActivity activity,
- final ConversationListListener conversationListListener,
final ConversationItemAreaClickListener conversationItemAreaClickListener,
final Folder folder, final int checkboxOrSenderImage, final AnimatedAdapter adapter,
final int backgroundOverrideResId, final Bitmap photoBitmap) {
Utils.traceBeginSection("CIVC.bindAd");
- bind(conversationItemViewModel, activity, conversationListListener,
- conversationItemAreaClickListener, null /* set */, folder, checkboxOrSenderImage,
- false /* attachment previews */, false /* parallax */, false /* parallax */,
- true /* swipeEnabled */, false /* priorityArrowEnabled */, adapter,
- backgroundOverrideResId, photoBitmap);
+ bind(conversationItemViewModel, activity, conversationItemAreaClickListener, null /* set */,
+ folder, checkboxOrSenderImage, false /* attachment previews */,
+ false /* parallax */, false /* parallax */, true /* swipeEnabled */,
+ false /* priorityArrowEnabled */,
+ adapter, backgroundOverrideResId, photoBitmap);
Utils.traceEndSection();
}
private void bind(final ConversationItemViewModel header, final ControllableActivity activity,
- final ConversationListListener conversationListListener,
final ConversationItemAreaClickListener conversationItemAreaClickListener,
final ConversationSelectionSet set, final Folder folder,
final int checkboxOrSenderImage, final boolean showAttachmentPreviews,
@@ -555,28 +493,25 @@
mConversationItemAreaClickListener = conversationItemAreaClickListener;
if (mHeader != null) {
+ Utils.traceBeginSection("unbind");
+ final boolean newlyBound = header.conversation.id != mHeader.conversation.id;
// If this was previously bound to a different conversation, remove any contact photo
// manager requests.
- if (header.conversation.id != mHeader.conversation.id ||
- (mHeader.displayableSenderNames != null && !mHeader.displayableSenderNames
- .equals(header.displayableSenderNames))) {
- ArrayList<String> divisionIds = mContactImagesHolder.getDivisionIds();
- if (divisionIds != null) {
- mContactImagesHolder.reset();
- for (int pos = 0; pos < divisionIds.size(); pos++) {
- sContactPhotoManager.removePhoto(ContactPhotoManager.generateHash(
- mContactImagesHolder, pos, divisionIds.get(pos)));
- }
+ if (newlyBound || (mHeader.displayableSenderNames != null && !mHeader
+ .displayableSenderNames.equals(
+ header.displayableSenderNames))) {
+ for (int i = 0; i < mSendersImageView.getCount(); i++) {
+ mSendersImageView.getOrCreateDrawable(i).unbind();
}
+ mSendersImageView.setCount(0);
}
// If this was previously bound to a different conversation,
// remove any attachment preview manager requests.
- if (header.conversation.id != mHeader.conversation.id
- || header.conversation.attachmentPreviewsCount
- != mHeader.conversation.attachmentPreviewsCount
- || !header.conversation.getAttachmentPreviewUris()
- .equals(mHeader.conversation.getAttachmentPreviewUris())) {
+ if (newlyBound || header.conversation.attachmentPreviewsCount
+ != mHeader.conversation.attachmentPreviewsCount || !header.conversation
+ .getAttachmentPreviewUris().equals(
+ mHeader.conversation.getAttachmentPreviewUris())) {
// unbind the attachments view (releasing bitmap references)
// (this also cancels all async tasks)
@@ -587,22 +522,31 @@
mAttachmentsView.setCount(0);
}
- if (header.conversation.id != mHeader.conversation.id) {
+ if (newlyBound) {
// Stop the photo flip animation
- mPhotoFlipAnimator.stopAnimation();
+ final boolean showSenders = !isSelected();
+ mSendersImageView.reset(showSenders);
}
+ Utils.traceEndSection();
}
mCoordinates = null;
mHeader = header;
mActivity = activity;
- mConversationListListener = conversationListListener;
mSelectedConversationSet = set;
+ if (mSelectedConversationSet != null) {
+ mSelectedConversationSet.addObserver(this);
+ }
mDisplayedFolder = folder;
mStarEnabled = folder != null && !folder.isTrash();
mSwipeEnabled = swipeEnabled;
mAdapter = adapter;
- mAttachmentsView.setBitmapCache(mAdapter.getBitmapCache());
- mAttachmentsView.setDecodeAggregator(mAdapter.getDecodeAggregator());
+
+ Utils.traceBeginSection("drawables");
+ mAttachmentsView.setBitmapCache(mAdapter.getAttachmentPreviewsCache());
+ mAttachmentsView.setDecodeAggregator(mAdapter.getAttachmentPreviewsDecodeAggregator());
+ mSendersImageView.setBitmapCache(mAdapter.getSendersImagesCache());
+ mSendersImageView.setContactResolver(mAdapter.getContactResolver());
+ Utils.traceEndSection();
if (checkboxOrSenderImage == ConversationListIcon.SENDER_IMAGE) {
mGadgetMode = ConversationItemViewCoordinates.GADGET_CONTACT_PHOTO;
@@ -614,12 +558,14 @@
mParallaxSpeedAlternative = parallaxSpeedAlternative;
mParallaxDirectionAlternative = parallaxDirectionAlternative;
+ Utils.traceBeginSection("folder displayer");
// Initialize folder displayer.
if (mHeader.folderDisplayer == null) {
mHeader.folderDisplayer = new ConversationItemFolderDisplayer(mContext);
} else {
mHeader.folderDisplayer.reset();
}
+ Utils.traceEndSection();
final int ignoreFolderType;
if (mDisplayedFolder.isInbox()) {
@@ -628,16 +574,21 @@
ignoreFolderType = -1;
}
+ Utils.traceBeginSection("load folders");
mHeader.folderDisplayer.loadConversationFolders(mHeader.conversation,
mDisplayedFolder.folderUri, ignoreFolderType);
+ Utils.traceEndSection();
if (mHeader.dateOverrideText == null) {
+ Utils.traceBeginSection("relative time");
mHeader.dateText = DateUtils.getRelativeTimeSpanString(mContext,
mHeader.conversation.dateMs);
+ Utils.traceEndSection();
} else {
mHeader.dateText = mHeader.dateOverrideText;
}
+ Utils.traceBeginSection("config setup");
mConfig = new ConversationItemViewCoordinates.Config()
.withGadget(mGadgetMode)
.withAttachmentPreviews(getAttachmentPreviewsMode());
@@ -650,6 +601,7 @@
if (mHeader.conversation.color != 0) {
mConfig.showColorBlock();
}
+
// Personal level.
mHeader.personalLevelBitmap = null;
if (true) { // TODO: hook this up to a setting
@@ -671,15 +623,20 @@
if (mHeader.personalLevelBitmap != null) {
mConfig.showPersonalIndicator();
}
+ Utils.traceEndSection();
+ Utils.traceBeginSection("overflow");
mAttachmentsView.setOverflowText(null);
+ Utils.traceEndSection();
+ Utils.traceBeginSection("content description");
setContentDescription();
+ Utils.traceEndSection();
requestLayout();
}
@Override
- public void invalidateDrawable(Drawable who) {
+ public void invalidateDrawable(final Drawable who) {
boolean handled = false;
if (mCoordinates != null) {
if (mAttachmentsView.equals(who)) {
@@ -687,6 +644,11 @@
r.offset(mCoordinates.attachmentPreviewsX, mCoordinates.attachmentPreviewsY);
ConversationItemView.this.invalidate(r.left, r.top, r.right, r.bottom);
handled = true;
+ } else if (mSendersImageView.equals(who)) {
+ final Rect r = new Rect(who.getBounds());
+ r.offset(mCoordinates.contactImagesX, mCoordinates.contactImagesY);
+ ConversationItemView.this.invalidate(r.left, r.top, r.right, r.bottom);
+ handled = true;
}
}
if (!handled) {
@@ -760,12 +722,14 @@
Utils.traceEndSection();
// Subject.
+ Utils.traceBeginSection("subject");
createSubject(mHeader.unread);
if (!mHeader.isLayoutValid()) {
setContentDescription();
}
mHeader.validate();
+ Utils.traceEndSection();
pauseTimer(PERF_TAG_LAYOUT);
if (sTimer != null && ++sLayoutCount >= PERF_LAYOUT_ITERATIONS) {
@@ -915,34 +879,36 @@
// FIXME(ath): maybe move this to bind(). the only dependency on layout is on tile W/H, which
// is immutable.
private void loadSenderImages() {
- if (mGadgetMode == ConversationItemViewCoordinates.GADGET_CONTACT_PHOTO
- && mHeader.displayableSenderEmails != null
- && mHeader.displayableSenderEmails.size() > 0) {
- if (mCoordinates.contactImagesWidth <= 0 || mCoordinates.contactImagesHeight <= 0) {
- LogUtils.w(LOG_TAG,
- "Contact image width(%d) or height(%d) is 0 for mode: (%d).",
- mCoordinates.contactImagesWidth, mCoordinates.contactImagesHeight,
- mCoordinates.getMode());
- return;
- }
-
- int size = mHeader.displayableSenderEmails.size();
- final List<Object> keys = Lists.newArrayListWithCapacity(size);
- for (int i = 0; i < DividedImageCanvas.MAX_DIVISIONS && i < size; i++) {
- keys.add(mHeader.displayableSenderEmails.get(i));
- }
-
- mContactImagesHolder.setDimensions(mCoordinates.contactImagesWidth,
- mCoordinates.contactImagesHeight);
- mContactImagesHolder.setDivisionIds(keys);
- String emailAddress;
- for (int i = 0; i < DividedImageCanvas.MAX_DIVISIONS && i < size; i++) {
- emailAddress = mHeader.displayableSenderEmails.get(i);
- PhotoIdentifier photoIdentifier = new ContactIdentifier(
- mHeader.displayableSenderNames.get(i), emailAddress, i);
- sContactPhotoManager.loadThumbnail(photoIdentifier, mContactImagesHolder);
- }
+ if (mGadgetMode != ConversationItemViewCoordinates.GADGET_CONTACT_PHOTO
+ || mHeader.displayableSenderEmails == null
+ || mHeader.displayableSenderEmails.size() <= 0) {
+ return;
}
+ if (mCoordinates.contactImagesWidth <= 0 || mCoordinates.contactImagesHeight <= 0) {
+ LogUtils.w(LOG_TAG,
+ "Contact image width(%d) or height(%d) is 0 for mode: (%d).",
+ mCoordinates.contactImagesWidth, mCoordinates.contactImagesHeight,
+ mCoordinates.getMode());
+ return;
+ }
+
+ Utils.traceBeginSection("load sender images");
+ final int count = mHeader.displayableSenderEmails.size();
+
+ mSendersImageView.setCount(count);
+ mSendersImageView
+ .setBounds(0, 0, mCoordinates.contactImagesWidth, mCoordinates.contactImagesHeight);
+
+ for (int i = 0; i < DividedImageCanvas.MAX_DIVISIONS && i < count; i++) {
+ Utils.traceBeginSection("load single sender image");
+ final ContactDrawable drawable = mSendersImageView.getOrCreateDrawable(i);
+ drawable.setDecodeDimensions(mCoordinates.contactImagesWidth,
+ mCoordinates.contactImagesHeight);
+ drawable.bind(mHeader.displayableSenderNames.get(i),
+ mHeader.displayableSenderEmails.get(i));
+ Utils.traceEndSection();
+ }
+ Utils.traceEndSection();
}
private void loadAttachmentPreviews() {
@@ -1390,7 +1356,9 @@
// Contact photo
if (mGadgetMode == ConversationItemViewCoordinates.GADGET_CONTACT_PHOTO) {
canvas.save();
- drawContactImageArea(canvas);
+ Utils.traceBeginSection("draw senders image");
+ drawSendersImage(canvas);
+ Utils.traceEndSection();
canvas.restore();
}
@@ -1499,113 +1467,19 @@
Utils.traceEndSection();
}
- /**
- * Draws the contact images or check, in the correct animated state.
- */
- private void drawContactImageArea(final Canvas canvas) {
- if (isSelected()) {
- mLastSelectedId = mHeader.conversation.id;
-
- // Since this is selected, we draw the checkbox if the animation is not running, or if
- // it's running, and is past the half-way point
- if (mPhotoFlipAnimator.getValue() > 1 || !mPhotoFlipAnimator.isStarted()) {
- // Flash in the check
- drawCheckbox(canvas);
- } else {
- // Flip out the contact photo
- drawContactImages(canvas);
- }
- } else {
- if ((mConversationListListener.isExitingSelectionMode()
- && mLastSelectedId == mHeader.conversation.id)
- || mPhotoFlipAnimator.isStarted()) {
- // Animate back to the photo
- if (!mPhotoFlipAnimator.isStarted()) {
- mPhotoFlipAnimator.startAnimation(true /* reverse */);
- }
-
- if (mPhotoFlipAnimator.getValue() > 1) {
- // Flash out the check
- drawCheckbox(canvas);
- } else {
- // Flip in the contact photo
- drawContactImages(canvas);
- }
- } else {
- mLastSelectedId = -1; // We don't care anymore
- mPhotoFlipAnimator.stopAnimation(); // It's not running, but we want to reset state
-
- // Contact photos
- drawContactImages(canvas);
- }
+ private void drawSendersImage(final Canvas canvas) {
+ if (!mSendersImageView.isFlipping()) {
+ final boolean showSenders = !isSelected();
+ mSendersImageView.reset(showSenders);
}
- }
-
- private void drawContactImages(final Canvas canvas) {
- // mPhotoFlipFraction goes from 0 to 1
- final float value = mPhotoFlipAnimator.getValue();
-
- final float scale = 1f - value;
- final float xOffset = mContactImagesHolder.getWidth() * value / 2;
-
- mPhotoFlipMatrix.reset();
- mPhotoFlipMatrix.postScale(scale, 1);
-
- final float x = mCoordinates.contactImagesX + xOffset;
- final float y = mCoordinates.contactImagesY;
-
- canvas.translate(x, y);
-
+ canvas.translate(mCoordinates.contactImagesX, mCoordinates.contactImagesY);
if (mPhotoBitmap == null) {
- mContactImagesHolder.draw(canvas, mPhotoFlipMatrix);
+ mSendersImageView.draw(canvas);
} else {
canvas.drawBitmap(mPhotoBitmap, null, mPhotoRect, sPaint);
}
}
- private void drawCheckbox(final Canvas canvas) {
- // mPhotoFlipFraction goes from 1 to 2
-
- // Draw the background
- canvas.save();
- canvas.translate(mCoordinates.contactImagesX, mCoordinates.contactImagesY);
- canvas.drawRect(0, 0, mCoordinates.contactImagesWidth, mCoordinates.contactImagesHeight,
- sCheckBackgroundPaint);
- canvas.restore();
-
- final int x = mCoordinates.contactImagesX
- + (mCoordinates.contactImagesWidth - CHECK.getWidth()) / 2;
- final int y = mCoordinates.contactImagesY
- + (mCoordinates.contactImagesHeight - CHECK.getHeight()) / 2;
-
- final float value = mPhotoFlipAnimator.getValue();
- final float scale;
-
- if (!mPhotoFlipAnimator.isStarted()) {
- // We're not animating
- scale = 1;
- } else if (value < 1.9) {
- // 1.0 to 1.9 will scale 0 to 1
- scale = (value - 1f) / 0.9f;
- } else if (value < 1.95) {
- // 1.9 to 1.95 will scale 1 to 19/18
- scale = (value - 1f) / 0.9f;
- } else {
- // 1.95 to 2.0 will scale 19/18 to 1
- scale = (0.95f - (value - 1.95f)) / 0.9f;
- }
-
- final float xOffset = CHECK.getWidth() * (1f - scale) / 2f;
- final float yOffset = CHECK.getHeight() * (1f - scale) / 2f;
-
- mCheckMatrix.reset();
- mCheckMatrix.postScale(scale, scale);
-
- canvas.translate(x + xOffset, y + yOffset);
-
- canvas.drawBitmap(CHECK, mCheckMatrix, sPaint);
- }
-
private void drawAttachmentPreviews(Canvas canvas) {
canvas.translate(mCoordinates.attachmentPreviewsX, mCoordinates.attachmentPreviewsY);
final float fraction;
@@ -1684,13 +1558,13 @@
return toggleSelectedState(null);
}
- private boolean toggleSelectedState(String sourceOpt) {
+ private boolean toggleSelectedState(final String sourceOpt) {
if (mHeader != null && mHeader.conversation != null && mSelectedConversationSet != null) {
mSelected = !mSelected;
setSelected(mSelected);
- Conversation conv = mHeader.conversation;
+ final Conversation conv = mHeader.conversation;
// Set the list position of this item in the conversation
- SwipeableListView listView = getListView();
+ final SwipeableListView listView = getListView();
try {
conv.position = mSelected && listView != null ? listView.getPositionForView(this)
@@ -1709,9 +1583,8 @@
listView.commitDestructiveActions(true);
}
- final boolean reverse = !mSelected;
- mPhotoFlipAnimator.startAnimation(reverse);
- mPhotoFlipAnimator.invalidateArea();
+ final boolean front = !mSelected;
+ mSendersImageView.flipTo(front);
// We update the background after the checked state has changed
// now that we have a selected background asset. Setting the background
@@ -1725,6 +1598,17 @@
return false;
}
+ @Override
+ public void onSetEmpty() {
+ mSendersImageView.flipTo(true);
+ }
+
+ @Override
+ public void onSetPopulated(final ConversationSelectionSet set) { }
+
+ @Override
+ public void onSetChanged(final ConversationSelectionSet set) { }
+
/**
* Toggle the star on this view and update the conversation.
*/
@@ -1774,13 +1658,13 @@
}
if (mStarEnabled) {
- if (mIsExpansiveTablet) {
+ if (mCoordinates.getMode() == ConversationItemViewCoordinates.WIDE_MODE) {
// Just check that we're left of the star's touch area
if (x >= mCoordinates.starX - sStarTouchSlop) {
return false;
}
} else {
- // We're on a phone or non-expansive tablet
+ // We're on a single pane device with the more condensed layout
// We allow touches all the way to the right edge, so no x check is necessary
@@ -1796,7 +1680,8 @@
}
private boolean isTouchInStar(float x, float y) {
- if (mHeader.infoIcon != null && !mIsExpansiveTablet) {
+ if (mHeader.infoIcon != null
+ && mCoordinates.getMode() != ConversationItemViewCoordinates.WIDE_MODE) {
// We have an info icon, and it's above the star
// We allow touches everywhere below the top of the subject text
if (y < mCoordinates.subjectY) {
@@ -2172,124 +2057,6 @@
return sScrollSlop;
}
- private abstract class CabAnimator {
- private ObjectAnimator mAnimator = null;
-
- private final String mPropertyName;
-
- private float mValue;
-
- private final float mStartValue;
- private final float mEndValue;
-
- private final long mDuration;
-
- private boolean mReversing = false;
-
- public CabAnimator(final String propertyName, final float startValue, final float endValue,
- final long duration) {
- mPropertyName = propertyName;
-
- mStartValue = startValue;
- mEndValue = endValue;
-
- mDuration = duration;
- }
-
- private ObjectAnimator createAnimator() {
- final ObjectAnimator animator = ObjectAnimator.ofFloat(ConversationItemView.this,
- mPropertyName, mStartValue, mEndValue);
- animator.setDuration(mDuration);
- animator.setInterpolator(new LinearInterpolator());
- animator.addListener(new AnimatorListenerAdapter() {
- @Override
- public void onAnimationEnd(final Animator animation) {
- invalidateArea();
- }
- });
- animator.addListener(mAnimatorListener);
- return animator;
- }
-
- private final AnimatorListener mAnimatorListener = new AnimatorListener() {
- @Override
- public void onAnimationStart(final Animator animation) {
- // Do nothing
- }
-
- @Override
- public void onAnimationEnd(final Animator animation) {
- if (mReversing) {
- mReversing = false;
- // We no longer want to track whether we were last selected,
- // since we no longer are selected
- mLastSelectedId = -1;
- }
- }
-
- @Override
- public void onAnimationCancel(final Animator animation) {
- // Do nothing
- }
-
- @Override
- public void onAnimationRepeat(final Animator animation) {
- // Do nothing
- }
- };
-
- public abstract void invalidateArea();
-
- public void setValue(final float fraction) {
- if (mValue == fraction) {
- return;
- }
- mValue = fraction;
- invalidateArea();
- }
-
- public float getValue() {
- return mValue;
- }
-
- /**
- * @param reverse <code>true</code> to animate in reverse
- */
- public void startAnimation(final boolean reverse) {
- if (mAnimator != null) {
- mAnimator.cancel();
- }
-
- mAnimator = createAnimator();
- mReversing = reverse;
-
- if (reverse) {
- mAnimator.reverse();
- } else {
- mAnimator.start();
- }
- }
-
- public void stopAnimation() {
- if (mAnimator != null) {
- mAnimator.cancel();
- mAnimator = null;
- }
-
- mReversing = false;
-
- setValue(0);
- }
-
- public boolean isStarted() {
- return mAnimator != null && mAnimator.isStarted();
- }
- }
-
- public void setPhotoFlipFraction(final float fraction) {
- mPhotoFlipAnimator.setValue(fraction);
- }
-
public String getAccount() {
return mAccount;
}
diff --git a/src/com/android/mail/browse/EmlMessageViewFragment.java b/src/com/android/mail/browse/EmlMessageViewFragment.java
index d1dab2f..c0f7f9b 100644
--- a/src/com/android/mail/browse/EmlMessageViewFragment.java
+++ b/src/com/android/mail/browse/EmlMessageViewFragment.java
@@ -19,6 +19,7 @@
import android.app.Fragment;
import android.app.LoaderManager;
+import android.content.Context;
import android.content.CursorLoader;
import android.content.Loader;
import android.database.Cursor;
@@ -236,6 +237,11 @@
return mAccountUri;
}
+ @Override
+ public Context getContext() {
+ return getActivity().getApplicationContext();
+ }
+
// End SecureConversationViewControllerCallbacks
private class MessageLoadCallbacks
diff --git a/src/com/android/mail/browse/MessageCursor.java b/src/com/android/mail/browse/MessageCursor.java
index 5a4146a..8437d87 100644
--- a/src/com/android/mail/browse/MessageCursor.java
+++ b/src/com/android/mail/browse/MessageCursor.java
@@ -73,6 +73,10 @@
return m;
}
+ public Conversation getConversation() {
+ return mController != null ? mController.getConversation() : null;
+ }
+
// Is the conversation starred?
public boolean isConversationStarred() {
int pos = -1;
diff --git a/src/com/android/mail/browse/MessageHeaderDetailsDialogFragment.java b/src/com/android/mail/browse/MessageHeaderDetailsDialogFragment.java
index 92a4f3c..62099fc 100644
--- a/src/com/android/mail/browse/MessageHeaderDetailsDialogFragment.java
+++ b/src/com/android/mail/browse/MessageHeaderDetailsDialogFragment.java
@@ -19,15 +19,16 @@
import android.app.AlertDialog;
import android.app.Dialog;
+import android.app.DialogFragment;
import android.content.Context;
import android.os.Bundle;
-import android.app.DialogFragment;
import android.view.LayoutInflater;
import android.view.View;
import com.android.mail.R;
import com.android.mail.providers.Account;
import com.android.mail.providers.Address;
+import com.android.mail.utils.Utils;
import java.util.HashMap;
import java.util.Map;
@@ -93,7 +94,7 @@
private static void addAddressesToBundle(
Bundle addresses, Map<String, Address> addressCache, String[] emails) {
for (final String email : emails) {
- addresses.putParcelable(email, MessageHeaderView.getAddress(addressCache, email));
+ addresses.putParcelable(email, Utils.getAddress(addressCache, email));
}
}
diff --git a/src/com/android/mail/browse/MessageHeaderView.java b/src/com/android/mail/browse/MessageHeaderView.java
index ed47bf5..1c3aa33 100644
--- a/src/com/android/mail/browse/MessageHeaderView.java
+++ b/src/com/android/mail/browse/MessageHeaderView.java
@@ -495,23 +495,7 @@
}
public Address getAddress(String emailStr) {
- return getAddress(mAddressCache, emailStr);
- }
-
- public static Address getAddress(Map<String, Address> cache, String emailStr) {
- Address addr = null;
- synchronized (cache) {
- if (cache != null) {
- addr = cache.get(emailStr);
- }
- if (addr == null) {
- addr = Address.getEmailAddress(emailStr);
- if (cache != null) {
- cache.put(emailStr, addr);
- }
- }
- }
- return addr;
+ return Utils.getAddress(mAddressCache, emailStr);
}
private void updateSpacerHeight() {
@@ -758,7 +742,7 @@
final int len = Math.min(maxToCopy, rawAddrs.length);
boolean first = true;
for (int i = 0; i < len; i++) {
- final Address email = getAddress(mAddressCache, rawAddrs[i]);
+ final Address email = Utils.getAddress(mAddressCache, rawAddrs[i]);
final String emailAddress = email.getAddress();
final String name;
if (mMatcher != null && mMatcher.isVeiledAddress(emailAddress)) {
@@ -1326,7 +1310,7 @@
}
final String[] formattedEmails = new String[emails.length];
for (int i = 0; i < emails.length; i++) {
- final Address email = getAddress(addressCache, emails[i]);
+ final Address email = Utils.getAddress(addressCache, emails[i]);
String name = email.getName();
final String address = email.getAddress();
// Check if the address here is a veiled address. If it is, we need to display an
diff --git a/src/com/android/mail/browse/SwipeableConversationItemView.java b/src/com/android/mail/browse/SwipeableConversationItemView.java
index cd17c2f..919a5aa 100644
--- a/src/com/android/mail/browse/SwipeableConversationItemView.java
+++ b/src/com/android/mail/browse/SwipeableConversationItemView.java
@@ -28,7 +28,6 @@
import com.android.mail.providers.Conversation;
import com.android.mail.providers.Folder;
import com.android.mail.ui.AnimatedAdapter;
-import com.android.mail.ui.AnimatedAdapter.ConversationListListener;
import com.android.mail.ui.ControllableActivity;
import com.android.mail.ui.ConversationSelectionSet;
@@ -56,15 +55,14 @@
}
public void bind(final Conversation conversation, final ControllableActivity activity,
- final ConversationListListener conversationListListener,
final ConversationSelectionSet set, final Folder folder,
final int checkboxOrSenderImage, final boolean showAttachmentPreviews,
final boolean parallaxSpeedAlternative, final boolean parallaxDirectionAlternative,
final boolean swipeEnabled, final boolean priorityArrowsEnabled,
final AnimatedAdapter animatedAdapter) {
- mConversationItemView.bind(conversation, activity, conversationListListener, set, folder,
- checkboxOrSenderImage, showAttachmentPreviews, parallaxSpeedAlternative,
- parallaxDirectionAlternative, swipeEnabled, priorityArrowsEnabled, animatedAdapter);
+ mConversationItemView.bind(conversation, activity, set, folder, checkboxOrSenderImage,
+ showAttachmentPreviews, parallaxSpeedAlternative, parallaxDirectionAlternative,
+ swipeEnabled, priorityArrowsEnabled, animatedAdapter);
}
public void startUndoAnimation(AnimatorListener listener, boolean swipe) {
diff --git a/src/com/android/mail/photo/MailPhotoViewActivity.java b/src/com/android/mail/photo/MailPhotoViewActivity.java
index c25e45f..0562568 100644
--- a/src/com/android/mail/photo/MailPhotoViewActivity.java
+++ b/src/com/android/mail/photo/MailPhotoViewActivity.java
@@ -76,7 +76,8 @@
public static void startMailPhotoViewActivity(final Context context, final Uri imageListUri,
final int photoIndex) {
final Intents.PhotoViewIntentBuilder builder =
- Intents.newPhotoViewIntentBuilder(context, MailPhotoViewActivity.class);
+ Intents.newPhotoViewIntentBuilder(context,
+ "com.android.mail.photo.MailPhotoViewActivity");
builder
.setPhotosUri(imageListUri.toString())
.setProjection(UIProvider.ATTACHMENT_PROJECTION)
@@ -96,7 +97,8 @@
public static void startMailPhotoViewActivity(final Context context, final Uri imageListUri,
final String initialPhotoUri) {
final Intents.PhotoViewIntentBuilder builder =
- Intents.newPhotoViewIntentBuilder(context, MailPhotoViewActivity.class);
+ Intents.newPhotoViewIntentBuilder(context,
+ "com.android.mail.photo.MailPhotoViewActivity");
builder
.setPhotosUri(imageListUri.toString())
.setProjection(UIProvider.ATTACHMENT_PROJECTION)
diff --git a/src/com/android/mail/photomanager/ContactPhotoManager.java b/src/com/android/mail/photomanager/ContactPhotoManager.java
deleted file mode 100644
index 06184b0..0000000
--- a/src/com/android/mail/photomanager/ContactPhotoManager.java
+++ /dev/null
@@ -1,218 +0,0 @@
-/*
- * 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.photomanager;
-
-import android.content.ContentResolver;
-import android.content.Context;
-import android.text.TextUtils;
-import android.util.LruCache;
-
-import com.android.mail.ContactInfo;
-import com.android.mail.SenderInfoLoader;
-import com.android.mail.ui.ImageCanvas;
-import com.android.mail.utils.LogUtils;
-import com.google.common.base.Objects;
-import com.google.common.collect.ImmutableMap;
-
-import java.util.Collection;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.Map;
-import java.util.Set;
-
-/**
- * Asynchronously loads contact photos and maintains a cache of photos.
- */
-public class ContactPhotoManager extends PhotoManager {
- public static final String CONTACT_PHOTO_SERVICE = "contactPhotos";
-
- /**
- * An LRU cache for photo ids mapped to contact addresses.
- */
- private final LruCache<String, Long> mPhotoIdCache;
- private final LetterTileProvider mLetterTileProvider;
-
- /** Cache size for {@link #mPhotoIdCache}. Starting with 500 entries. */
- private static final int PHOTO_ID_CACHE_SIZE = 500;
-
- /**
- * Requests the singleton instance with data bound from the available authenticators. This
- * method can safely be called from the UI thread.
- */
- public static ContactPhotoManager getInstance(Context context) {
- Context applicationContext = context.getApplicationContext();
- ContactPhotoManager service =
- (ContactPhotoManager) applicationContext.getSystemService(CONTACT_PHOTO_SERVICE);
- if (service == null) {
- service = createContactPhotoManager(applicationContext);
- LogUtils.e(TAG, "No contact photo service in context: " + applicationContext);
- }
- return service;
- }
-
- public static synchronized ContactPhotoManager createContactPhotoManager(Context context) {
- return new ContactPhotoManager(context);
- }
-
- public static int generateHash(ImageCanvas view, int pos, Object key) {
- return Objects.hashCode(view, pos, key);
- }
-
- private ContactPhotoManager(Context context) {
- super(context);
- mPhotoIdCache = new LruCache<String, Long>(PHOTO_ID_CACHE_SIZE);
- mLetterTileProvider = new LetterTileProvider(context);
- }
-
- @Override
- protected DefaultImageProvider getDefaultImageProvider() {
- return mLetterTileProvider;
- }
-
- @Override
- protected int getHash(PhotoIdentifier id, ImageCanvas view) {
- final ContactIdentifier contactId = (ContactIdentifier) id;
- return generateHash(view, contactId.pos, contactId.getKey());
- }
-
- @Override
- protected PhotoLoaderThread getLoaderThread(ContentResolver contentResolver) {
- return new ContactPhotoLoaderThread(contentResolver);
- }
-
- @Override
- public void clear() {
- super.clear();
- mPhotoIdCache.evictAll();
- }
-
- public static class ContactIdentifier extends PhotoIdentifier {
- public final String name;
- public final String emailAddress;
- public final int pos;
-
- public ContactIdentifier(String name, String emailAddress, int pos) {
- this.name = name;
- this.emailAddress = emailAddress;
- this.pos = pos;
- }
-
- @Override
- public boolean isValid() {
- return !TextUtils.isEmpty(emailAddress);
- }
-
- @Override
- public Object getKey() {
- return emailAddress;
- }
-
- @Override
- public int hashCode() {
- int hash = 17;
- hash = 31 * hash + (emailAddress != null ? emailAddress.hashCode() : 0);
- hash = 31 * hash + (name != null ? name.hashCode() : 0);
- hash = 31 * hash + pos;
- return hash;
- }
-
- @Override
- public boolean equals(Object obj) {
- if (this == obj)
- return true;
- if (obj == null)
- return false;
- if (getClass() != obj.getClass())
- return false;
- ContactIdentifier other = (ContactIdentifier) obj;
- return Objects.equal(emailAddress, other.emailAddress)
- && Objects.equal(name, other.name) && Objects.equal(pos, other.pos);
- }
-
- @Override
- public String toString() {
- final StringBuilder sb = new StringBuilder("{");
- sb.append(super.toString());
- sb.append(" name=");
- sb.append(name);
- sb.append(" email=");
- sb.append(emailAddress);
- sb.append(" pos=");
- sb.append(pos);
- sb.append("}");
- return sb.toString();
- }
-
- @Override
- public int compareTo(PhotoIdentifier another) {
- return 0;
- }
- }
-
- public class ContactPhotoLoaderThread extends PhotoLoaderThread {
- public ContactPhotoLoaderThread(ContentResolver resolver) {
- super(resolver);
- }
-
- @Override
- protected Map<String, BitmapHolder> loadPhotos(Collection<Request> requests) {
- Map<String, BitmapHolder> photos = new HashMap<String, BitmapHolder>(requests.size());
-
- Set<String> addresses = new HashSet<String>();
- Set<Long> photoIds = new HashSet<Long>();
- HashMap<Long, String> photoIdMap = new HashMap<Long, String>();
-
- Long match;
- String emailAddress;
- for (Request request : requests) {
- emailAddress = (String) request.getKey();
- match = mPhotoIdCache.get(emailAddress);
- if (match != null) {
- photoIds.add(match);
- photoIdMap.put(match, emailAddress);
- } else {
- addresses.add(emailAddress);
- }
- }
-
- // get the Map of email addresses to ContactInfo
- ImmutableMap<String, ContactInfo> emailAddressToContactInfoMap =
- SenderInfoLoader.loadContactPhotos(
- getResolver(), addresses, false /* decodeBitmaps */);
-
- // Put all entries into photos map: a mapping of email addresses to photoBytes.
- // If there is no ContactInfo, it means we couldn't get a photo for this
- // address so just put null in for the bytes so that the crazy caching
- // works properly and we don't get an infinite loop of GC churn.
- if (emailAddressToContactInfoMap != null) {
- for (final String address : addresses) {
- final ContactInfo info = emailAddressToContactInfoMap.get(address);
- photos.put(address,
- new BitmapHolder(info != null ? info.photoBytes : null, -1, -1));
- }
- } else {
- // Still need to set a null result for all addresses, otherwise we end
- // up in the loop where photo manager attempts to load these again.
- for (final String address: addresses) {
- photos.put(address, new BitmapHolder(null, -1, -1));
- }
- }
-
- return photos;
- }
- }
-}
diff --git a/src/com/android/mail/photomanager/LetterTileProvider.java b/src/com/android/mail/photomanager/LetterTileProvider.java
index bc28223..0cb7505 100644
--- a/src/com/android/mail/photomanager/LetterTileProvider.java
+++ b/src/com/android/mail/photomanager/LetterTileProvider.java
@@ -29,12 +29,8 @@
import android.text.TextUtils;
import com.android.mail.R;
-import com.android.mail.photomanager.ContactPhotoManager.ContactIdentifier;
-import com.android.mail.photomanager.PhotoManager.DefaultImageProvider;
-import com.android.mail.photomanager.PhotoManager.PhotoIdentifier;
-import com.android.mail.ui.DividedImageCanvas;
-import com.android.mail.ui.ImageCanvas;
import com.android.mail.ui.ImageCanvas.Dimensions;
+import com.android.mail.utils.BitmapUtil;
import com.android.mail.utils.LogTag;
import com.android.mail.utils.LogUtils;
@@ -46,7 +42,8 @@
* tile. If there is no English alphabet character (or digit), it creates a
* bitmap with the default contact avatar.
*/
-public class LetterTileProvider implements DefaultImageProvider {
+@Deprecated
+public class LetterTileProvider {
private static final String TAG = LogTag.getLogTag();
private final Bitmap mDefaultBitmap;
private final Bitmap[] mBitmapBackgroundCache;
@@ -89,30 +86,6 @@
mDefaultColor = res.getColor(R.color.letter_tile_default_color);
}
- @Override
- public void applyDefaultImage(PhotoIdentifier id, ImageCanvas view, int extent) {
- ContactIdentifier contactIdentifier = (ContactIdentifier) id;
- DividedImageCanvas dividedImageView = (DividedImageCanvas) view;
-
- final String displayName = contactIdentifier.name;
- final String address = (String) contactIdentifier.getKey();
-
- // don't apply again if existing letter is there (and valid)
- if (dividedImageView.hasImageFor(address)) {
- return;
- }
-
- dividedImageView.getDesiredDimensions(address, mDims);
-
- final Bitmap bitmap = getLetterTile(mDims, displayName, address);
-
- if (bitmap == null) {
- return;
- }
-
- dividedImageView.addDivisionImage(bitmap, address);
- }
-
public Bitmap getLetterTile(final Dimensions dimensions, final String displayName,
final String address) {
final String display = !TextUtils.isEmpty(displayName) ? displayName : address;
diff --git a/src/com/android/mail/photomanager/MemInfoReader.java b/src/com/android/mail/photomanager/MemInfoReader.java
deleted file mode 100644
index f53c60c..0000000
--- a/src/com/android/mail/photomanager/MemInfoReader.java
+++ /dev/null
@@ -1,109 +0,0 @@
-/*
- * 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.photomanager;
-
-import android.os.StrictMode;
-
-import java.io.FileInputStream;
-
-public class MemInfoReader {
- byte[] mBuffer = new byte[1024];
-
- private long mTotalSize;
- private long mFreeSize;
- private long mCachedSize;
-
- private static boolean matchText(byte[] buffer, int index, String text) {
- int N = text.length();
- if ((index + N) >= buffer.length) {
- return false;
- }
- for (int i = 0; i < N; i++) {
- if (buffer[index + i] != text.charAt(i)) {
- return false;
- }
- }
- return true;
- }
-
- private static long extractMemValue(byte[] buffer, int index) {
- while (index < buffer.length && buffer[index] != '\n') {
- if (buffer[index] >= '0' && buffer[index] <= '9') {
- int start = index;
- index++;
- while (index < buffer.length && buffer[index] >= '0' && buffer[index] <= '9') {
- index++;
- }
- String str = new String(buffer, 0, start, index - start);
- return ((long) Integer.parseInt(str)) * 1024;
- }
- index++;
- }
- return 0;
- }
-
- public void readMemInfo() {
- // Permit disk reads here, as /proc/meminfo isn't really "on
- // disk" and should be fast. TODO: make BlockGuard ignore
- // /proc/ and /sys/ files perhaps?
- StrictMode.ThreadPolicy savedPolicy = StrictMode.allowThreadDiskReads();
- try {
- mTotalSize = 0;
- mFreeSize = 0;
- mCachedSize = 0;
- FileInputStream is = new FileInputStream("/proc/meminfo");
- int len = is.read(mBuffer);
- is.close();
- final int BUFLEN = mBuffer.length;
- int count = 0;
- for (int i = 0; i < len && count < 3; i++) {
- if (matchText(mBuffer, i, "MemTotal")) {
- i += 8;
- mTotalSize = extractMemValue(mBuffer, i);
- count++;
- } else if (matchText(mBuffer, i, "MemFree")) {
- i += 7;
- mFreeSize = extractMemValue(mBuffer, i);
- count++;
- } else if (matchText(mBuffer, i, "Cached")) {
- i += 6;
- mCachedSize = extractMemValue(mBuffer, i);
- count++;
- }
- while (i < BUFLEN && mBuffer[i] != '\n') {
- i++;
- }
- }
- } catch (java.io.FileNotFoundException e) {
- } catch (java.io.IOException e) {
- } finally {
- StrictMode.setThreadPolicy(savedPolicy);
- }
- }
-
- public long getTotalSize() {
- return mTotalSize;
- }
-
- public long getFreeSize() {
- return mFreeSize;
- }
-
- public long getCachedSize() {
- return mCachedSize;
- }
-}
diff --git a/src/com/android/mail/photomanager/MemoryUtils.java b/src/com/android/mail/photomanager/MemoryUtils.java
deleted file mode 100644
index 4992026..0000000
--- a/src/com/android/mail/photomanager/MemoryUtils.java
+++ /dev/null
@@ -1,39 +0,0 @@
-/*
- * 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.photomanager;
-
-public class MemoryUtils {
- private MemoryUtils() {
- }
-
- public static final int LARGE_RAM_THRESHOLD = 640 * 1024 * 1024;
- private static long sTotalMemorySize = -1;
-
- public static long getTotalMemorySize() {
- if (sTotalMemorySize < 0) {
- MemInfoReader reader = new MemInfoReader();
- reader.readMemInfo();
-
- // getTotalSize() returns the "MemTotal" value from /proc/meminfo.
- // Because the linux kernel doesn't see all the RAM on the system
- // (e.g. GPU takes some),
- // this is usually smaller than the actual RAM size.
- sTotalMemorySize = reader.getTotalSize();
- }
- return sTotalMemorySize;
- }
-}
diff --git a/src/com/android/mail/photomanager/PhotoManager.java b/src/com/android/mail/photomanager/PhotoManager.java
deleted file mode 100644
index 94f809d..0000000
--- a/src/com/android/mail/photomanager/PhotoManager.java
+++ /dev/null
@@ -1,993 +0,0 @@
-/*
- * 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.photomanager;
-
-import android.content.ComponentCallbacks2;
-import android.content.ContentResolver;
-import android.content.Context;
-import android.content.res.Configuration;
-import android.graphics.Bitmap;
-import android.os.Handler;
-import android.os.Handler.Callback;
-import android.os.HandlerThread;
-import android.os.Message;
-import android.os.Process;
-import android.util.LruCache;
-
-import com.android.mail.ui.ImageCanvas;
-import com.android.mail.utils.LogUtils;
-import com.android.mail.utils.Utils;
-import com.google.common.base.Objects;
-import com.google.common.collect.Lists;
-
-import java.util.Collection;
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.List;
-import java.util.Map;
-import java.util.PriorityQueue;
-import java.util.concurrent.atomic.AtomicInteger;
-
-/**
- * Asynchronously loads photos and maintains a cache of photos
- */
-public abstract class PhotoManager implements ComponentCallbacks2, Callback {
- /**
- * Get the default image provider that draws while the photo is being
- * loaded.
- */
- protected abstract DefaultImageProvider getDefaultImageProvider();
-
- /**
- * Generate a hashcode unique to each request.
- */
- protected abstract int getHash(PhotoIdentifier id, ImageCanvas view);
-
- /**
- * Return a specific implementation of PhotoLoaderThread.
- */
- protected abstract PhotoLoaderThread getLoaderThread(ContentResolver contentResolver);
-
- /**
- * Subclasses can implement this method to alert callbacks that images finished loading.
- * @param request The original request made.
- * @param success True if we successfully loaded the image from cache. False if we fell back
- * to the default image.
- */
- protected void onImageDrawn(final Request request, final boolean success) {
- // Subclasses can choose to do something about this
- }
-
- /**
- * Subclasses can implement this method to alert callbacks that images started loading.
- * @param request The original request made.
- */
- protected void onImageLoadStarted(final Request request) {
- // Subclasses can choose to do something about this
- }
-
- /**
- * Subclasses can implement this method to determine whether a previously loaded bitmap can
- * be reused for a new canvas size.
- * @param prevWidth The width of the previously loaded bitmap.
- * @param prevHeight The height of the previously loaded bitmap.
- * @param newWidth The width of the canvas this request is drawing on.
- * @param newHeight The height of the canvas this request is drawing on.
- * @return
- */
- protected boolean isSizeCompatible(int prevWidth, int prevHeight, int newWidth, int newHeight) {
- return true;
- }
-
- protected final Context getContext() {
- return mContext;
- }
-
- static final String TAG = "PhotoManager";
- static final boolean DEBUG = false; // Don't submit with true
- static final boolean DEBUG_SIZES = false; // Don't submit with true
-
- private static final String LOADER_THREAD_NAME = "PhotoLoader";
-
- /**
- * Type of message sent by the UI thread to itself to indicate that some photos
- * need to be loaded.
- */
- private static final int MESSAGE_REQUEST_LOADING = 1;
-
- /**
- * Type of message sent by the loader thread to indicate that some photos have
- * been loaded.
- */
- private static final int MESSAGE_PHOTOS_LOADED = 2;
-
- /**
- * Type of message sent by the loader thread to indicate that
- */
- private static final int MESSAGE_PHOTO_LOADING = 3;
-
- public interface DefaultImageProvider {
- /**
- * Applies the default avatar to the DividedImageView. Extent is an
- * indicator for the size (width or height). If darkTheme is set, the
- * avatar is one that looks better on dark background
- * @param id
- */
- public void applyDefaultImage(PhotoIdentifier id, ImageCanvas view, int extent);
- }
-
- /**
- * Maintains the state of a particular photo.
- */
- protected static class BitmapHolder {
- byte[] bytes;
- int width;
- int height;
-
- volatile boolean fresh;
-
- public BitmapHolder(byte[] bytes, int width, int height) {
- this.bytes = bytes;
- this.width = width;
- this.height = height;
- this.fresh = true;
- }
-
- @Override
- public String toString() {
- final StringBuilder sb = new StringBuilder("{");
- sb.append(super.toString());
- sb.append(" bytes=");
- sb.append(bytes);
- sb.append(" size=");
- sb.append(bytes == null ? 0 : bytes.length);
- sb.append(" width=");
- sb.append(width);
- sb.append(" height=");
- sb.append(height);
- sb.append(" fresh=");
- sb.append(fresh);
- sb.append("}");
- return sb.toString();
- }
- }
-
- // todo:ath caches should be member vars
- /**
- * An LRU cache for bitmap holders. The cache contains bytes for photos just
- * as they come from the database. Each holder has a soft reference to the
- * actual bitmap. The keys are decided by the implementation.
- */
- private static final LruCache<Object, BitmapHolder> sBitmapHolderCache;
-
- /**
- * Level 2 LRU cache for bitmaps. This is a smaller cache that holds
- * the most recently used bitmaps to save time on decoding
- * them from bytes (the bytes are stored in {@link #sBitmapHolderCache}.
- * The keys are decided by the implementation.
- */
- private static final LruCache<BitmapIdentifier, Bitmap> sBitmapCache;
-
- /** Cache size for {@link #sBitmapHolderCache} for devices with "large" RAM. */
- private static final int HOLDER_CACHE_SIZE = 2000000;
-
- /** Cache size for {@link #sBitmapCache} for devices with "large" RAM. */
- private static final int BITMAP_CACHE_SIZE = 1024 * 1024 * 8; // 8MB
-
- /** For debug: How many times we had to reload cached photo for a stale entry */
- private static final AtomicInteger sStaleCacheOverwrite = new AtomicInteger();
-
- /** For debug: How many times we had to reload cached photo for a fresh entry. Should be 0. */
- private static final AtomicInteger sFreshCacheOverwrite = new AtomicInteger();
-
- static {
- final float cacheSizeAdjustment =
- (MemoryUtils.getTotalMemorySize() >= MemoryUtils.LARGE_RAM_THRESHOLD) ?
- 1.0f : 0.5f;
- final int holderCacheSize = (int) (cacheSizeAdjustment * HOLDER_CACHE_SIZE);
- sBitmapHolderCache = new LruCache<Object, BitmapHolder>(holderCacheSize) {
- @Override protected int sizeOf(Object key, BitmapHolder value) {
- return value.bytes != null ? value.bytes.length : 0;
- }
-
- @Override protected void entryRemoved(
- boolean evicted, Object key, BitmapHolder oldValue, BitmapHolder newValue) {
- if (DEBUG) dumpStats();
- }
- };
- final int bitmapCacheSize = (int) (cacheSizeAdjustment * BITMAP_CACHE_SIZE);
- sBitmapCache = new LruCache<BitmapIdentifier, Bitmap>(bitmapCacheSize) {
- @Override protected int sizeOf(BitmapIdentifier key, Bitmap value) {
- return value.getByteCount();
- }
-
- @Override protected void entryRemoved(
- boolean evicted, BitmapIdentifier key, Bitmap oldValue, Bitmap newValue) {
- if (DEBUG) dumpStats();
- }
- };
- LogUtils.i(TAG, "Cache adj: " + cacheSizeAdjustment);
- if (DEBUG) {
- LogUtils.d(TAG, "Cache size: " + btk(sBitmapHolderCache.maxSize())
- + " + " + btk(sBitmapCache.maxSize()));
- }
- }
-
- /**
- * A map from ImageCanvas hashcode to the corresponding photo ID or uri,
- * encapsulated in a request. The request may swapped out before the photo
- * loading request is started.
- */
- private final Map<Integer, Request> mPendingRequests = Collections.synchronizedMap(
- new HashMap<Integer, Request>());
-
- /**
- * Handler for messages sent to the UI thread.
- */
- private final Handler mMainThreadHandler = new Handler(this);
-
- /**
- * Thread responsible for loading photos from the database. Created upon
- * the first request.
- */
- private PhotoLoaderThread mLoaderThread;
-
- /**
- * A gate to make sure we only send one instance of MESSAGE_PHOTOS_NEEDED at a time.
- */
- private boolean mLoadingRequested;
-
- /**
- * Flag indicating if the image loading is paused.
- */
- private boolean mPaused;
-
- private final Context mContext;
-
- public PhotoManager(Context context) {
- mContext = context;
- }
-
- public void loadThumbnail(PhotoIdentifier id, ImageCanvas view) {
- loadThumbnail(id, view, null);
- }
-
- /**
- * Load an image
- *
- * @param dimensions Preferred dimensions
- */
- public void loadThumbnail(final PhotoIdentifier id, final ImageCanvas view,
- final ImageCanvas.Dimensions dimensions) {
- Utils.traceBeginSection("Load thumbnail");
- final DefaultImageProvider defaultProvider = getDefaultImageProvider();
- final Request request = new Request(id, defaultProvider, view, dimensions);
- final int hashCode = request.hashCode();
-
- if (!id.isValid()) {
- // No photo is needed
- request.applyDefaultImage();
- onImageDrawn(request, false);
- mPendingRequests.remove(hashCode);
- } else if (mPendingRequests.containsKey(hashCode)) {
- LogUtils.d(TAG, "load request dropped for %s", id);
- } else {
- if (DEBUG) LogUtils.v(TAG, "loadPhoto request: %s", id.getKey());
- loadPhoto(hashCode, request);
- }
- Utils.traceEndSection();
- }
-
- private void loadPhoto(int hashCode, Request request) {
- if (DEBUG) {
- LogUtils.v(TAG, "NEW IMAGE REQUEST key=%s r=%s thread=%s",
- request.getKey(),
- request,
- Thread.currentThread());
- }
-
- boolean loaded = loadCachedPhoto(request, false);
- if (loaded) {
- if (DEBUG) {
- LogUtils.v(TAG, "image request, cache hit. request queue size=%s",
- mPendingRequests.size());
- }
- } else {
- if (DEBUG) {
- LogUtils.d(TAG, "image request, cache miss: key=%s", request.getKey());
- }
- mPendingRequests.put(hashCode, request);
- if (!mPaused) {
- // Send a request to start loading photos
- requestLoading();
- }
- }
- }
-
- /**
- * Remove photo from the supplied image view. This also cancels current pending load request
- * inside this photo manager.
- */
- public void removePhoto(int hashcode) {
- Request r = mPendingRequests.remove(hashcode);
- if (r != null) {
- LogUtils.d(TAG, "removed request %s", r.getKey());
- }
- }
-
- private void ensureLoaderThread() {
- if (mLoaderThread == null) {
- mLoaderThread = getLoaderThread(mContext.getContentResolver());
- mLoaderThread.start();
- }
- }
-
- /**
- * Checks if the photo is present in cache. If so, sets the photo on the view.
- *
- * @param request Determines which image to load from cache.
- * @param afterLoaderThreadFinished Pass true if calling after the LoaderThread has run. Pass
- * false if the Loader Thread hasn't made any attempts to
- * load images yet.
- * @return false if the photo needs to be (re)loaded from the provider.
- */
- private boolean loadCachedPhoto(final Request request,
- final boolean afterLoaderThreadFinished) {
- Utils.traceBeginSection("Load cached photo");
- final Bitmap cached = getCachedPhoto(request.bitmapKey);
- if (cached != null) {
- if (DEBUG) {
- LogUtils.v(TAG, "%s, key=%s decodedSize=%s thread=%s",
- afterLoaderThreadFinished ? "DECODED IMG READ"
- : "DECODED IMG CACHE HIT",
- request.getKey(),
- cached.getByteCount(),
- Thread.currentThread());
- }
- if (request.getView().getGeneration() == request.viewGeneration) {
- request.getView().drawImage(cached, request.getKey());
- onImageDrawn(request, true);
- }
- Utils.traceEndSection();
- return true;
- }
-
- // We couldn't load the requested image, so try to load a replacement.
- // This removes the flicker from SIMPLE to BEST transition.
- final Object replacementKey = request.getPhotoIdentifier().getKeyToShowInsteadOfDefault();
- if (replacementKey != null) {
- final BitmapIdentifier replacementBitmapKey = new BitmapIdentifier(replacementKey,
- request.bitmapKey.w, request.bitmapKey.h);
- final Bitmap cachedReplacement = getCachedPhoto(replacementBitmapKey);
- if (cachedReplacement != null) {
- if (DEBUG) {
- LogUtils.v(TAG, "%s, key=%s decodedSize=%s thread=%s",
- afterLoaderThreadFinished ? "DECODED IMG READ"
- : "DECODED IMG CACHE HIT",
- replacementKey,
- cachedReplacement.getByteCount(),
- Thread.currentThread());
- }
- if (request.getView().getGeneration() == request.viewGeneration) {
- request.getView().drawImage(cachedReplacement, request.getKey());
- onImageDrawn(request, true);
- }
- Utils.traceEndSection();
- return false;
- }
- }
-
- // We couldn't load any image, so draw a default image
- request.applyDefaultImage();
-
- final BitmapHolder holder = sBitmapHolderCache.get(request.getKey());
- // Check if we loaded null bytes, which means we meant to not draw anything.
- if (holder != null && holder.bytes == null) {
- onImageDrawn(request, holder.fresh);
- Utils.traceEndSection();
- return holder.fresh;
- }
- Utils.traceEndSection();
- return false;
- }
-
- /**
- * Takes care of retrieving the Bitmap from both the decoded and holder caches.
- */
- private static Bitmap getCachedPhoto(BitmapIdentifier bitmapKey) {
- Utils.traceBeginSection("Get cached photo");
- final Bitmap cached = sBitmapCache.get(bitmapKey);
- Utils.traceEndSection();
- return cached;
- }
-
- /**
- * Temporarily stops loading photos from the database.
- */
- public void pause() {
- LogUtils.d(TAG, "%s paused.", getClass().getName());
- mPaused = true;
- }
-
- /**
- * Resumes loading photos from the database.
- */
- public void resume() {
- LogUtils.d(TAG, "%s resumed.", getClass().getName());
- mPaused = false;
- if (DEBUG) dumpStats();
- if (!mPendingRequests.isEmpty()) {
- requestLoading();
- }
- }
-
- /**
- * Sends a message to this thread itself to start loading images. If the current
- * view contains multiple image views, all of those image views will get a chance
- * to request their respective photos before any of those requests are executed.
- * This allows us to load images in bulk.
- */
- private void requestLoading() {
- if (!mLoadingRequested) {
- mLoadingRequested = true;
- mMainThreadHandler.sendEmptyMessage(MESSAGE_REQUEST_LOADING);
- }
- }
-
- /**
- * Processes requests on the main thread.
- */
- @Override
- public boolean handleMessage(final Message msg) {
- switch (msg.what) {
- case MESSAGE_REQUEST_LOADING: {
- mLoadingRequested = false;
- if (!mPaused) {
- ensureLoaderThread();
- mLoaderThread.requestLoading();
- }
- return true;
- }
-
- case MESSAGE_PHOTOS_LOADED: {
- processLoadedImages();
- if (DEBUG) dumpStats();
- return true;
- }
-
- case MESSAGE_PHOTO_LOADING: {
- final int hashcode = msg.arg1;
- final Request request = mPendingRequests.get(hashcode);
- onImageLoadStarted(request);
- return true;
- }
- }
- return false;
- }
-
- /**
- * Goes over pending loading requests and displays loaded photos. If some of the
- * photos still haven't been loaded, sends another request for image loading.
- */
- private void processLoadedImages() {
- Utils.traceBeginSection("process loaded images");
- final List<Integer> toRemove = Lists.newArrayList();
- for (final Integer hash : mPendingRequests.keySet()) {
- final Request request = mPendingRequests.get(hash);
- final boolean loaded = loadCachedPhoto(request, true);
- // Request can go through multiple attempts if the LoaderThread fails to load any
- // images for it, or if the images it loads are evicted from the cache before we
- // could access them in the main thread.
- if (loaded || request.attempts > 2) {
- toRemove.add(hash);
- }
- }
- for (final Integer key : toRemove) {
- mPendingRequests.remove(key);
- }
-
- if (!mPaused && !mPendingRequests.isEmpty()) {
- LogUtils.d(TAG, "Finished loading batch. %d still have to be loaded.",
- mPendingRequests.size());
- requestLoading();
- }
- Utils.traceEndSection();
- }
-
- /**
- * Stores the supplied bitmap in cache.
- */
- private static void cacheBitmapHolder(final String cacheKey, final BitmapHolder holder) {
- if (DEBUG) {
- BitmapHolder prev = sBitmapHolderCache.get(cacheKey);
- if (prev != null && prev.bytes != null) {
- LogUtils.d(TAG, "Overwriting cache: key=" + cacheKey
- + (prev.fresh ? " FRESH" : " stale"));
- if (prev.fresh) {
- sFreshCacheOverwrite.incrementAndGet();
- } else {
- sStaleCacheOverwrite.incrementAndGet();
- }
- }
- LogUtils.d(TAG, "Caching data: key=" + cacheKey + ", "
- + (holder.bytes == null ? "<null>" : btk(holder.bytes.length)));
- }
-
- sBitmapHolderCache.put(cacheKey, holder);
- }
-
- protected static void cacheBitmap(final BitmapIdentifier bitmapKey, final Bitmap bitmap) {
- sBitmapCache.put(bitmapKey, bitmap);
- }
-
- // ComponentCallbacks2
- @Override
- public void onConfigurationChanged(Configuration newConfig) {
- }
-
- // ComponentCallbacks2
- @Override
- public void onLowMemory() {
- }
-
- // ComponentCallbacks2
- @Override
- public void onTrimMemory(int level) {
- if (DEBUG) LogUtils.d(TAG, "onTrimMemory: " + level);
- if (level >= ComponentCallbacks2.TRIM_MEMORY_MODERATE) {
- // Clear the caches. Note all pending requests will be removed too.
- clear();
- }
- }
-
- public void clear() {
- if (DEBUG) LogUtils.d(TAG, "clear");
- mPendingRequests.clear();
- sBitmapHolderCache.evictAll();
- sBitmapCache.evictAll();
- }
-
- /**
- * Dump cache stats on logcat.
- */
- private static void dumpStats() {
- if (!DEBUG) {
- return;
- }
- int numHolders = 0;
- int rawBytes = 0;
- int bitmapBytes = 0;
- int numBitmaps = 0;
- for (BitmapHolder h : sBitmapHolderCache.snapshot().values()) {
- numHolders++;
- if (h.bytes != null) {
- rawBytes += h.bytes.length;
- numBitmaps++;
- }
- }
- LogUtils.d(TAG,
- "L1: " + btk(rawBytes) + " + " + btk(bitmapBytes) + " = "
- + btk(rawBytes + bitmapBytes) + ", " + numHolders + " holders, "
- + numBitmaps + " bitmaps, avg: " + btk(safeDiv(rawBytes, numBitmaps)));
- LogUtils.d(TAG, "L1 Stats: %s, overwrite: fresh=%s stale=%s", sBitmapHolderCache,
- sFreshCacheOverwrite.get(), sStaleCacheOverwrite.get());
-
- numBitmaps = 0;
- bitmapBytes = 0;
- for (Bitmap b : sBitmapCache.snapshot().values()) {
- numBitmaps++;
- bitmapBytes += b.getByteCount();
- }
- LogUtils.d(TAG, "L2: " + btk(bitmapBytes) + ", " + numBitmaps + " bitmaps" + ", avg: "
- + btk(safeDiv(bitmapBytes, numBitmaps)));
- // We don't get from L2 cache, so L2 stats is meaningless.
- }
-
- /** Converts bytes to K bytes, rounding up. Used only for debug log. */
- private static String btk(int bytes) {
- return ((bytes + 1023) / 1024) + "K";
- }
-
- private static final int safeDiv(int dividend, int divisor) {
- return (divisor == 0) ? 0 : (dividend / divisor);
- }
-
- public static abstract class PhotoIdentifier implements Comparable<PhotoIdentifier> {
- /**
- * If this returns false, the PhotoManager will not attempt to load the
- * bitmap. Instead, the default image provider will be used.
- */
- public abstract boolean isValid();
-
- /**
- * Identifies this request.
- */
- public abstract Object getKey();
-
- /**
- * Replacement key to try to load from cache instead of drawing the default image. This
- * is useful when we've already loaded a SIMPLE rendition, and are now loading the BEST
- * rendition. We want the BEST image to appear seamlessly on top of the existing SIMPLE
- * image.
- */
- public Object getKeyToShowInsteadOfDefault() {
- return null;
- }
- }
-
- /**
- * The thread that performs loading of photos from the database.
- */
- protected abstract class PhotoLoaderThread extends HandlerThread implements Callback {
-
- /**
- * Return photos mapped from {@link Request#getKey()} to the photo for
- * that request.
- */
- protected abstract Map<String, BitmapHolder> loadPhotos(Collection<Request> requests);
-
- private static final int MESSAGE_LOAD_PHOTOS = 0;
-
- private final ContentResolver mResolver;
-
- private Handler mLoaderThreadHandler;
-
- public PhotoLoaderThread(ContentResolver resolver) {
- super(LOADER_THREAD_NAME, Process.THREAD_PRIORITY_BACKGROUND);
- mResolver = resolver;
- }
-
- protected ContentResolver getResolver() {
- return mResolver;
- }
-
- public void ensureHandler() {
- if (mLoaderThreadHandler == null) {
- mLoaderThreadHandler = new Handler(getLooper(), this);
- }
- }
-
- /**
- * Sends a message to this thread to load requested photos. Cancels a preloading
- * request, if any: we don't want preloading to impede loading of the photos
- * we need to display now.
- */
- public void requestLoading() {
- ensureHandler();
- mLoaderThreadHandler.sendEmptyMessage(MESSAGE_LOAD_PHOTOS);
- }
-
- /**
- * Receives the above message, loads photos and then sends a message
- * to the main thread to process them.
- */
- @Override
- public boolean handleMessage(Message msg) {
- switch (msg.what) {
- case MESSAGE_LOAD_PHOTOS:
- loadPhotosInBackground();
- break;
- }
- return true;
- }
-
- /**
- * Subclasses may specify the maximum number of requests to be given at a time to
- * #loadPhotos(). For batch count N, the UI will be updated with up to N images at a time.
- *
- * @return A positive integer if you would like to limit the number of
- * items in a single batch.
- */
- protected int getMaxBatchCount() {
- return -1;
- }
-
- private void loadPhotosInBackground() {
- Utils.traceBeginSection("pre processing");
- final Collection<Request> loadRequests = new HashSet<PhotoManager.Request>();
- final Collection<Request> decodeRequests = new HashSet<PhotoManager.Request>();
- final PriorityQueue<Request> requests;
- synchronized (mPendingRequests) {
- requests = new PriorityQueue<Request>(mPendingRequests.values());
- }
-
- int batchCount = 0;
- int maxBatchCount = getMaxBatchCount();
- while (!requests.isEmpty()) {
- Request request = requests.poll();
- final BitmapHolder holder = sBitmapHolderCache
- .get(request.getKey());
- if (holder == null || holder.bytes == null || !holder.fresh || !isSizeCompatible(
- holder.width, holder.height, request.bitmapKey.w, request.bitmapKey.h)) {
- loadRequests.add(request);
- decodeRequests.add(request);
- batchCount++;
-
- final Message msg = Message.obtain();
- msg.what = MESSAGE_PHOTO_LOADING;
- msg.arg1 = request.hashCode();
- mMainThreadHandler.sendMessage(msg);
- } else {
- // Even if the image load is already done, this particular decode configuration
- // may not yet have run. Be sure to add it to the queue.
- if (sBitmapCache.get(request.bitmapKey) == null) {
- decodeRequests.add(request);
- }
- }
- request.attempts++;
- if (maxBatchCount > 0 && batchCount >= maxBatchCount) {
- break;
- }
- }
- Utils.traceEndSection();
-
- Utils.traceBeginSection("load photos");
- // Ask subclass to do the actual loading
- final Map<String, BitmapHolder> photosMap = loadPhotos(loadRequests);
- Utils.traceEndSection();
-
- if (DEBUG) {
- LogUtils.d(TAG,
- "worker thread completed read request batch. inputN=%s outputN=%s",
- loadRequests.size(),
- photosMap.size());
- }
- Utils.traceBeginSection("post processing");
- for (String cacheKey : photosMap.keySet()) {
- if (DEBUG) {
- LogUtils.d(TAG,
- "worker thread completed read request key=%s byteCount=%s thread=%s",
- cacheKey,
- photosMap.get(cacheKey) == null ? 0
- : photosMap.get(cacheKey).bytes.length,
- Thread.currentThread());
- }
- cacheBitmapHolder(cacheKey, photosMap.get(cacheKey));
- }
-
- for (Request r : decodeRequests) {
- if (sBitmapCache.get(r.bitmapKey) != null) {
- continue;
- }
-
- final Object cacheKey = r.getKey();
- final BitmapHolder holder = sBitmapHolderCache.get(cacheKey);
- if (holder == null || holder.bytes == null || !holder.fresh || !isSizeCompatible(
- holder.width, holder.height, r.bitmapKey.w, r.bitmapKey.h)) {
- continue;
- }
-
- final int w = r.bitmapKey.w;
- final int h = r.bitmapKey.h;
- final byte[] src = holder.bytes;
-
- if (w == 0 || h == 0) {
- LogUtils.e(TAG, new Error(), "bad dimensions for request=%s w/h=%s/%s",
- r, w, h);
- }
-
- final Bitmap decoded = BitmapUtil.decodeByteArrayWithCenterCrop(src, w, h);
- if (DEBUG) {
- LogUtils.i(TAG,
- "worker thread completed decode bmpKey=%s decoded=%s holder=%s",
- r.bitmapKey, decoded, holder);
- }
-
- if (decoded != null) {
- cacheBitmap(r.bitmapKey, decoded);
- }
- }
- Utils.traceEndSection();
-
- mMainThreadHandler.sendEmptyMessage(MESSAGE_PHOTOS_LOADED);
- }
-
- protected String createInQuery(String value, int itemCount) {
- // Build first query
- StringBuilder query = new StringBuilder().append(value + " IN (");
- appendQuestionMarks(query, itemCount);
- query.append(')');
- return query.toString();
- }
-
- protected void appendQuestionMarks(StringBuilder query, int itemCount) {
- boolean first = true;
- for (int i = 0; i < itemCount; i++) {
- if (first) {
- first = false;
- } else {
- query.append(',');
- }
- query.append('?');
- }
- }
- }
-
- /**
- * An object to uniquely identify a combination of (Request + decoded size). Multiple requests
- * may require the same src image, but want to decode it into different sizes.
- */
- public static final class BitmapIdentifier {
- public final Object key;
- public final int w;
- public final int h;
-
- // OK to be static as long as all Requests are created on the same
- // thread
- private static final ImageCanvas.Dimensions sWorkDims = new ImageCanvas.Dimensions();
-
- public static BitmapIdentifier getBitmapKey(PhotoIdentifier id, ImageCanvas view,
- ImageCanvas.Dimensions dimensions) {
- final int width;
- final int height;
- if (dimensions != null) {
- width = dimensions.width;
- height = dimensions.height;
- } else {
- view.getDesiredDimensions(id.getKey(), sWorkDims);
- width = sWorkDims.width;
- height = sWorkDims.height;
- }
- return new BitmapIdentifier(id.getKey(), width, height);
- }
-
- public BitmapIdentifier(Object key, int w, int h) {
- this.key = key;
- this.w = w;
- this.h = h;
- }
-
- @Override
- public int hashCode() {
- int hash = 19;
- hash = 31 * hash + key.hashCode();
- hash = 31 * hash + w;
- hash = 31 * hash + h;
- return hash;
- }
-
- @Override
- public boolean equals(Object obj) {
- if (obj == null || obj.getClass() != getClass()) {
- return false;
- } else if (obj == this) {
- return true;
- }
- final BitmapIdentifier o = (BitmapIdentifier) obj;
- return Objects.equal(key, o.key) && w == o.w && h == o.h;
- }
-
- @Override
- public String toString() {
- final StringBuilder sb = new StringBuilder("{");
- sb.append(super.toString());
- sb.append(" key=");
- sb.append(key);
- sb.append(" w=");
- sb.append(w);
- sb.append(" h=");
- sb.append(h);
- sb.append("}");
- return sb.toString();
- }
- }
-
- /**
- * A holder for a contact photo request.
- */
- public final class Request implements Comparable<Request> {
- private final int mRequestedExtent;
- private final DefaultImageProvider mDefaultProvider;
- private final PhotoIdentifier mPhotoIdentifier;
- private final ImageCanvas mView;
- public final BitmapIdentifier bitmapKey;
- public final int viewGeneration;
- public int attempts;
-
- private Request(final PhotoIdentifier photoIdentifier,
- final DefaultImageProvider defaultProvider, final ImageCanvas view,
- final ImageCanvas.Dimensions dimensions) {
- mPhotoIdentifier = photoIdentifier;
- mRequestedExtent = -1;
- mDefaultProvider = defaultProvider;
- mView = view;
- viewGeneration = view.getGeneration();
-
- bitmapKey = BitmapIdentifier.getBitmapKey(photoIdentifier, mView, dimensions);
- }
-
- public ImageCanvas getView() {
- return mView;
- }
-
- public PhotoIdentifier getPhotoIdentifier() {
- return mPhotoIdentifier;
- }
-
- /**
- * @see PhotoIdentifier#getKey()
- */
- public Object getKey() {
- return mPhotoIdentifier.getKey();
- }
-
- @Override
- public int hashCode() {
- return getHash(mPhotoIdentifier, mView);
- }
-
- @Override
- public boolean equals(Object obj) {
- if (this == obj) return true;
- if (obj == null) return false;
- if (getClass() != obj.getClass()) return false;
- final Request that = (Request) obj;
- if (mRequestedExtent != that.mRequestedExtent) return false;
- if (!Objects.equal(mPhotoIdentifier, that.mPhotoIdentifier)) return false;
- if (!Objects.equal(mView, that.mView)) return false;
- // Don't compare equality of mDarkTheme because it is only used in the default contact
- // photo case. When the contact does have a photo, the contact photo is the same
- // regardless of mDarkTheme, so we shouldn't need to put the photo request on the queue
- // twice.
- return true;
- }
-
- @Override
- public String toString() {
- final StringBuilder sb = new StringBuilder("{");
- sb.append(super.toString());
- sb.append(" key=");
- sb.append(getKey());
- sb.append(" id=");
- sb.append(mPhotoIdentifier);
- sb.append(" mView=");
- sb.append(mView);
- sb.append(" mExtent=");
- sb.append(mRequestedExtent);
- sb.append(" bitmapKey=");
- sb.append(bitmapKey);
- sb.append(" viewGeneration=");
- sb.append(viewGeneration);
- sb.append("}");
- return sb.toString();
- }
-
- public void applyDefaultImage() {
- if (mView.getGeneration() != viewGeneration) {
- // This can legitimately happen when an ImageCanvas is reused and re-purposed to
- // house a new set of images (e.g. by ListView recycling).
- // Ignore this now-stale request.
- if (DEBUG) {
- LogUtils.d(TAG,
- "ImageCanvas skipping applyDefaultImage; no longer contains" +
- " item=%s canvas=%s", getKey(), mView);
- }
- }
- mDefaultProvider.applyDefaultImage(mPhotoIdentifier, mView, mRequestedExtent);
- }
-
- @Override
- public int compareTo(Request another) {
- // Hold off on loading Requests which have failed before so it don't hold up others
- if (attempts - another.attempts != 0) {
- return attempts - another.attempts;
- }
- return mPhotoIdentifier.compareTo(another.mPhotoIdentifier);
- }
- }
-}
diff --git a/src/com/android/mail/preferences/MailPrefs.java b/src/com/android/mail/preferences/MailPrefs.java
index c83e6e8..0c1b374 100644
--- a/src/com/android/mail/preferences/MailPrefs.java
+++ b/src/com/android/mail/preferences/MailPrefs.java
@@ -23,6 +23,7 @@
import com.android.mail.providers.Account;
import com.android.mail.providers.UIProvider;
import com.android.mail.utils.LogUtils;
+import com.android.mail.utils.Utils;
import com.android.mail.widget.BaseWidgetProvider;
import com.google.common.collect.ImmutableSet;
@@ -40,7 +41,7 @@
*/
public final class MailPrefs extends VersionedPrefs {
- public static final boolean SHOW_EXPERIMENTAL_PREFS = false;
+ public static final boolean SHOW_EXPERIMENTAL_PREFS = true;
private static final String PREFS_NAME = "UnifiedEmail";
@@ -385,7 +386,6 @@
editor.apply();
}
-
public void setShowSenderImages(boolean enable) {
getEditor().putBoolean(PreferenceKeys.SHOW_SENDER_IMAGES, enable).apply();
notifyBackupPreferenceChanged();
diff --git a/src/com/android/mail/print/HtmlPrintTemplates.java b/src/com/android/mail/print/HtmlPrintTemplates.java
new file mode 100644
index 0000000..27dfba3
--- /dev/null
+++ b/src/com/android/mail/print/HtmlPrintTemplates.java
@@ -0,0 +1,124 @@
+/**
+ * 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.print;
+
+import android.content.Context;
+import android.content.res.Resources;
+
+import com.android.mail.R;
+import com.android.mail.ui.AbstractHtmlTemplates;
+import com.android.mail.utils.LogTag;
+import com.android.mail.utils.LogUtils;
+
+/**
+ * Renders data into very simple string-substitution HTML templates for printing conversations.
+ */
+public class HtmlPrintTemplates extends AbstractHtmlTemplates {
+
+ private static final String TAG = LogTag.getLogTag();
+
+ private final String mConversationUpper;
+ private final String mMessage;
+ private final String mConversationLower;
+ private final String mConversationLowerNoJs;
+
+ public HtmlPrintTemplates(Context context) {
+ super(context);
+
+ mConversationUpper = readTemplate(R.raw.template_print_conversation_upper);
+ mMessage = readTemplate(R.raw.template_print_message);
+ mConversationLower = readTemplate(R.raw.template_print_conversation_lower);
+ mConversationLowerNoJs = readTemplate(R.raw.template_print_conversation_lower_no_js);
+ }
+
+ /**
+ * Start building the html for a printed conversation. Can only be called once
+ * until {@link #endPrintConversation()} or {@link #endPrintConversationNoJavascript()}
+ * is called.
+ */
+ public void startPrintConversation(String accountName, String accountAddress,
+ String subject, int numMessages) {
+ if (mInProgress) {
+ throw new IllegalStateException("Should not call startPrintConversation twice");
+ }
+
+ reset();
+
+ final Resources res = mContext.getResources();
+ final String numMessageString = res.getQuantityString(
+ R.plurals.num_messages, numMessages, numMessages);
+ append(mConversationUpper, mContext.getString(R.string.app_name),
+ accountName == null ? "" : accountName,
+ accountAddress, subject, numMessageString);
+
+ mInProgress = true;
+ }
+
+ /**
+ * Add a message to the html for this printed conversation.
+ */
+ public void appendMessage(String senderName, String senderAddress, String date,
+ String recipients, String bodyHtml, String attachments) {
+ append(mMessage, senderName, senderAddress, date, recipients, bodyHtml, attachments);
+ }
+
+ /**
+ * Adds the end of the printed conversation to the html. NOTE: this method
+ * includes JavaScript. If you need a version without JavaScript,
+ * use {@link #endPrintConversationNoJavascript()}.<br/><br/>
+ *
+ * One example where we use JavaScript is to hide quoted text.
+ *
+ * @return a {@link String} containing the html for the conversation.
+ */
+ public String endPrintConversation() {
+ if (!mInProgress) {
+ throw new IllegalStateException("must call startConversation first");
+ }
+
+ append(mConversationLower, mContext.getString(R.string.quoted_text_hidden_print));
+
+ mInProgress = false;
+
+ LogUtils.d(TAG, "rendered conversation of %d bytes, buffer capacity=%d",
+ mBuilder.length() << 1, mBuilder.capacity() << 1);
+
+ return emit();
+ }
+
+ /**
+ * Adds the end of the printed conversation to the html. NOTE: this method
+ * does not include any JavaScript. If you need a version with JavaScript,
+ * use {@link #endPrintConversation()}.
+ * @return a {@link String} containing the html for the conversation.
+ */
+ public String endPrintConversationNoJavascript() {
+ if (!mInProgress) {
+ throw new IllegalStateException("must call startConversation first");
+ }
+
+ append(mConversationLowerNoJs);
+
+ mInProgress = false;
+
+ LogUtils.d(TAG, "rendered conversation of %d bytes, buffer capacity=%d",
+ mBuilder.length() << 1, mBuilder.capacity() << 1);
+
+ return emit();
+ }
+}
diff --git a/src/com/android/mail/print/Printer.java b/src/com/android/mail/print/Printer.java
new file mode 100644
index 0000000..7744c37
--- /dev/null
+++ b/src/com/android/mail/print/Printer.java
@@ -0,0 +1,158 @@
+/**
+ * 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.print;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.text.TextUtils;
+
+import com.android.mail.FormattedDateBuilder;
+import com.android.mail.R;
+import com.android.mail.browse.ConversationMessage;
+import com.android.mail.browse.MessageCursor;
+import com.android.mail.providers.Account;
+import com.android.mail.providers.Address;
+import com.android.mail.providers.Conversation;
+import com.android.mail.providers.UIProvider;
+import com.android.mail.utils.Utils;
+
+import java.util.Map;
+
+/**
+ * Static class that provides a {@link #print} function to build a print html document.
+ */
+public class Printer {
+ private static final String DIV_START = "<div>";
+ private static final String REPLY_TO_DIV_START = "<div class=\"replyto\">";
+ private static final String DIV_END = "</div>";
+
+ /**
+ * Builds an html document that is suitable for printing and returns it as a {@link String}.
+ */
+ public static String print(Context context, Account account,
+ MessageCursor cursor, Map<String, Address> addressCache, boolean useJavascript) {
+ final HtmlPrintTemplates templates = new HtmlPrintTemplates(context);
+ final FormattedDateBuilder dateBuilder = new FormattedDateBuilder(context);
+
+ if (!cursor.moveToFirst()) {
+ throw new IllegalStateException("trying to print without a conversation");
+ }
+
+ // TODO - remove account name(not account.name which is email address) or get it somehow
+ final Conversation conversation = cursor.getConversation();
+ templates.startPrintConversation("", account.name,
+ conversation.subject, conversation.getNumMessages());
+
+ // for each message in the conversation, add message html
+ final Resources res = context.getResources();
+ do {
+ final ConversationMessage message = cursor.getMessage();
+ final Address fromAddress = Utils.getAddress(addressCache, message.getFrom());
+ final long when = message.dateReceivedMs;
+ final String date = res.getString(R.string.date_message_received_print,
+ dateBuilder.formatLongDayAndDate(when), dateBuilder.formatLongTime(when));
+
+
+ templates.appendMessage(fromAddress.getName(), fromAddress.getAddress(), date,
+ renderRecipients(res, addressCache, message), message.getBodyAsHtml(),
+ ""); // TODO - attachment html
+ } while (cursor.moveToNext());
+
+ // only include JavaScript if specifically requested
+ return useJavascript ?
+ templates.endPrintConversation() : templates.endPrintConversationNoJavascript();
+ }
+
+ /**
+ * Builds html for the message header. Specifically, the (optional) lists of
+ * reply-to, to, cc, and bcc.
+ */
+ private static String renderRecipients(Resources res, Map<String, Address> addressCache,
+ ConversationMessage message) {
+ final StringBuilder recipients = new StringBuilder();
+
+ // reply-to
+ final String replyTo = renderEmailList(res, message.getReplyToAddresses(), addressCache);
+ buildEmailDiv(res, recipients, replyTo, REPLY_TO_DIV_START, DIV_END,
+ R.string.replyto_heading);
+
+ // to
+ // To has special semantics since the message can be a draft.
+ // If it is a draft and there are no to addresses, we just print "Draft".
+ // If it is a draft and there are to addresses, we print "Draft To: "
+ // If not a draft, we just use "To: ".
+ final boolean isDraft = message.draftType != UIProvider.DraftType.NOT_A_DRAFT;
+ final String to = renderEmailList(res, message.getToAddresses(), addressCache);
+ if (isDraft && to == null) {
+ recipients.append(DIV_START).append(res.getString(R.string.draft_heading))
+ .append(DIV_END);
+ } else {
+ buildEmailDiv(res, recipients, to, DIV_START, DIV_END,
+ isDraft ? R.string.draft_to_heading : R.string.to_heading);
+ }
+
+ // cc
+ final String cc = renderEmailList(res, message.getCcAddresses(), addressCache);
+ buildEmailDiv(res, recipients, cc, DIV_START, DIV_END,
+ R.string.cc_heading);
+
+ // bcc
+ final String bcc = renderEmailList(res, message.getBccAddresses(), addressCache);
+ buildEmailDiv(res, recipients, bcc, DIV_START, DIV_END,
+ R.string.bcc_heading);
+
+ return recipients.toString();
+ }
+
+ /**
+ * Appends an html div containing a list of emails based on the passed in data.
+ */
+ private static void buildEmailDiv(Resources res, StringBuilder recipients, String emailList,
+ String divStart, String divEnd, int headingId) {
+ if (emailList != null) {
+ recipients.append(divStart).append(res.getString(headingId))
+ .append(emailList).append(divEnd);
+ }
+ }
+
+ /**
+ * Builds and returns a list of comma-separated emails of the form "Name <email>".
+ * If the email does not contain a name, "email" is used instead.
+ */
+ private static String renderEmailList(Resources resources, String[] emails,
+ Map<String, Address> addressCache) {
+ if (emails == null || emails.length == 0) {
+ return null;
+ }
+ final String[] formattedEmails = new String[emails.length];
+ for (int i = 0; i < emails.length; i++) {
+ final Address email = Utils.getAddress(addressCache, emails[i]);
+ final String name = email.getName();
+ final String address = email.getAddress();
+
+ if (TextUtils.isEmpty(name)) {
+ formattedEmails[i] = address;
+ } else {
+ formattedEmails[i] = resources.getString(R.string.address_print_display_format,
+ name, address);
+ }
+ }
+
+ return TextUtils.join(", ", formattedEmails);
+ }
+}
diff --git a/src/com/android/mail/ui/AbstractConversationViewFragment.java b/src/com/android/mail/ui/AbstractConversationViewFragment.java
index 0023403..473d158 100644
--- a/src/com/android/mail/ui/AbstractConversationViewFragment.java
+++ b/src/com/android/mail/ui/AbstractConversationViewFragment.java
@@ -24,6 +24,7 @@
import android.content.Loader;
import android.database.Cursor;
import android.net.Uri;
+import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.view.Menu;
@@ -326,6 +327,9 @@
} else if (itemId == R.id.show_original) {
showUntransformedConversation();
handled = true;
+ } else if (itemId == R.id.print) {
+ printConversation();
+ handled = true;
}
return handled;
}
@@ -335,6 +339,7 @@
// Only show option if we support message transforms and message has been transformed.
Utils.setMenuItemVisibility(menu, R.id.show_original, supportsMessageTransforms() &&
mHasConversationBeenTransformed && !mHasConversationTransformBeenReverted);
+ Utils.setMenuItemVisibility(menu, R.id.print, Utils.isRunningKitkatOrLater());
}
abstract boolean supportsMessageTransforms();
@@ -634,4 +639,6 @@
return (mAccount.enableMessageTransforms > 0) &&
!mHasConversationTransformBeenReverted;
}
+
+ protected abstract void printConversation();
}
diff --git a/src/com/android/mail/ui/AbstractHtmlTemplates.java b/src/com/android/mail/ui/AbstractHtmlTemplates.java
new file mode 100644
index 0000000..25c3bc5
--- /dev/null
+++ b/src/com/android/mail/ui/AbstractHtmlTemplates.java
@@ -0,0 +1,95 @@
+/**
+ * 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.content.res.Resources;
+
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.util.Formatter;
+
+/**
+ * Abstract class to support common functionality for both
+ * {@link com.android.mail.ui.HtmlConversationTemplates} and
+ * {@link com.android.mail.print.HtmlPrintTemplates}.
+ *
+ * Renders data into very simple string-substitution HTML templates.
+ *
+ * Templates should be UTF-8 encoded HTML with '%s' placeholders to be substituted upon render.
+ * Plain-jane string substitution with '%s' is slightly faster than typed substitution.
+ */
+public abstract class AbstractHtmlTemplates {
+ // TODO: refine. too expensive to iterate over cursor and pre-calculate total. so either
+ // estimate it, or defer assembly until the end when size is known (deferring increases
+ // working set size vs. estimation but is exact).
+ private static final int BUFFER_SIZE_CHARS = 64 * 1024;
+
+ protected Context mContext;
+ protected Formatter mFormatter;
+ protected StringBuilder mBuilder;
+ protected boolean mInProgress = false;
+
+ public AbstractHtmlTemplates(Context context) {
+ mContext = context;
+ }
+
+ public String emit() {
+ final String out = mFormatter.toString();
+ // release the builder memory ASAP
+ mFormatter = null;
+ mBuilder = null;
+ return out;
+ }
+
+ public void reset() {
+ mBuilder = new StringBuilder(BUFFER_SIZE_CHARS);
+ mFormatter = new Formatter(mBuilder, null /* no localization */);
+ }
+
+ protected String readTemplate(int id) throws Resources.NotFoundException {
+ final StringBuilder out = new StringBuilder();
+ InputStreamReader in = null;
+ try {
+ try {
+ in = new InputStreamReader(
+ mContext.getResources().openRawResource(id), "UTF-8");
+ final char[] buf = new char[4096];
+ int chars;
+
+ while ((chars=in.read(buf)) > 0) {
+ out.append(buf, 0, chars);
+ }
+
+ return out.toString();
+
+ } finally {
+ if (in != null) {
+ in.close();
+ }
+ }
+ } catch (IOException e) {
+ throw new Resources.NotFoundException("Unable to open template id="
+ + Integer.toHexString(id) + " exception=" + e.getMessage());
+ }
+ }
+
+ protected void append(String template, Object... args) {
+ mFormatter.format(template, args);
+ }
+}
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/AnimatedAdapter.java b/src/com/android/mail/ui/AnimatedAdapter.java
index 936fc97..e891249 100644
--- a/src/com/android/mail/ui/AnimatedAdapter.java
+++ b/src/com/android/mail/ui/AnimatedAdapter.java
@@ -40,6 +40,7 @@
import com.android.bitmap.DecodeAggregator;
import com.android.mail.R;
import com.android.mail.analytics.Analytics;
+import com.android.mail.bitmap.ContactResolver;
import com.android.mail.browse.ConversationCursor;
import com.android.mail.browse.ConversationItemView;
import com.android.mail.browse.ConversationItemViewCoordinates.CoordinatesCache;
@@ -91,17 +92,6 @@
private final Handler mHandler;
protected long mLastLeaveBehind = -1;
- private final BitmapCache mBitmapCache;
- private final DecodeAggregator mDecodeAggregator;
-
- public interface ConversationListListener {
- /**
- * @return <code>true</code> if the list is just exiting selection mode (so animations may
- * be required), <code>false</code> otherwise
- */
- boolean isExitingSelectionMode();
- }
-
private final AnimatorListener mAnimatorListener = new AnimatorListenerAdapter() {
@Override
@@ -182,7 +172,6 @@
/** True if priority inbox markers are enabled, false otherwise. */
private boolean mPriorityMarkersEnabled;
private final ControllableActivity mActivity;
- private final ConversationListListener mConversationListListener;
private final AccountObserver mAccountListener = new AccountObserver() {
@Override
public void onChanged(Account newAccount) {
@@ -245,29 +234,42 @@
private static final String LOG_TAG = LogTag.getLogTag();
private static final int INCREASE_WAIT_COUNT = 2;
- private static final int BITMAP_CACHE_TARGET_SIZE_BYTES = 0; // TODO: enable cache
+ private final BitmapCache mAttachmentPreviewsCache;
+ private final DecodeAggregator mAttachmentPreviewsDecodeAggregator;
+ private final BitmapCache mSendersImagesCache;
+ private final ContactResolver mContactResolver;
+
+ private static final int ATTACHMENT_PREVIEWS_CACHE_TARGET_SIZE_BYTES = 0; // TODO: enable cache
+ /** 339KB cache fits 10 bitmaps at 33856 bytes each. */
+ private static final int SENDERS_IMAGES_CACHE_TARGET_SIZE_BYTES = 1024 * 339;
/**
* This is the fractional portion of the total cache size above that's dedicated to non-pooled
* bitmaps. (This is basically the portion of cache dedicated to GIFs.)
*/
- private static final float BITMAP_CACHE_NON_POOLED_FRACTION = 0.1f;
+ private static final float ATTACHMENT_PREVIEWS_CACHE_NON_POOLED_FRACTION = 0.1f;
+ private static final float SENDERS_IMAGES_PREVIEWS_CACHE_NON_POOLED_FRACTION = 0f;
+ /** Each string has upper estimate of 50 bytes, so this cache would be 5KB. */
+ private static final int SENDERS_IMAGES_PREVIEWS_CACHE_NULL_CAPACITY = 100;
public AnimatedAdapter(Context context, ConversationCursor cursor,
ConversationSelectionSet batch, ControllableActivity activity,
- final ConversationListListener conversationListListener, SwipeableListView listView,
- final List<ConversationSpecialItemView> specialViews) {
+ SwipeableListView listView, final List<ConversationSpecialItemView> specialViews) {
super(context, -1, cursor, UIProvider.CONVERSATION_PROJECTION, null, 0);
mContext = context;
mBatchConversations = batch;
setAccount(mAccountListener.initialize(activity.getAccountController()));
mActivity = activity;
- mConversationListListener = conversationListListener;
mShowFooter = false;
mListView = listView;
- mBitmapCache = new AltBitmapCache(BITMAP_CACHE_TARGET_SIZE_BYTES,
- BITMAP_CACHE_NON_POOLED_FRACTION);
- mDecodeAggregator = new DecodeAggregator();
+ mAttachmentPreviewsCache = new AltBitmapCache(ATTACHMENT_PREVIEWS_CACHE_TARGET_SIZE_BYTES,
+ ATTACHMENT_PREVIEWS_CACHE_NON_POOLED_FRACTION, 0);
+ mAttachmentPreviewsDecodeAggregator = new DecodeAggregator();
+ mSendersImagesCache = new AltBitmapCache(Utils.isLowRamDevice(mContext) ?
+ 0 : SENDERS_IMAGES_CACHE_TARGET_SIZE_BYTES,
+ SENDERS_IMAGES_PREVIEWS_CACHE_NON_POOLED_FRACTION,
+ SENDERS_IMAGES_PREVIEWS_CACHE_NULL_CAPACITY);
+ mContactResolver = new ContactResolver(mContext.getContentResolver(), mSendersImagesCache);
mHandler = new Handler();
if (sDismissAllShortDelay == -1) {
@@ -376,10 +378,10 @@
if (view == null) {
view = new SwipeableConversationItemView(context, mAccount.name);
}
- view.bind(conv, mActivity, mConversationListListener, mBatchConversations, mFolder,
- getCheckboxSetting(), getAttachmentPreviewsSetting(),
- getParallaxSpeedAlternativeSetting(), getParallaxDirectionAlternativeSetting(),
- mSwipeEnabled, mPriorityMarkersEnabled, this);
+ view.bind(conv, mActivity, mBatchConversations, mFolder, getCheckboxSetting(),
+ getAttachmentPreviewsSetting(), getParallaxSpeedAlternativeSetting(),
+ getParallaxDirectionAlternativeSetting(), mSwipeEnabled, mPriorityMarkersEnabled,
+ this);
return view;
}
@@ -764,10 +766,10 @@
SwipeableConversationItemView view = (SwipeableConversationItemView) super.getView(
position, null, parent);
view.reset();
- view.bind(conversation, mActivity, mConversationListListener, mBatchConversations, mFolder,
- getCheckboxSetting(), getAttachmentPreviewsSetting(),
- getParallaxSpeedAlternativeSetting(), getParallaxDirectionAlternativeSetting(),
- mSwipeEnabled, mPriorityMarkersEnabled, this);
+ view.bind(conversation, mActivity, mBatchConversations, mFolder, getCheckboxSetting(),
+ getAttachmentPreviewsSetting(), getParallaxSpeedAlternativeSetting(),
+ getParallaxDirectionAlternativeSetting(), mSwipeEnabled, mPriorityMarkersEnabled,
+ this);
mAnimatingViews.put(conversation.id, view);
return view;
}
@@ -1068,12 +1070,20 @@
return oldCursor;
}
- public BitmapCache getBitmapCache() {
- return mBitmapCache;
+ public BitmapCache getAttachmentPreviewsCache() {
+ return mAttachmentPreviewsCache;
}
- public DecodeAggregator getDecodeAggregator() {
- return mDecodeAggregator;
+ public DecodeAggregator getAttachmentPreviewsDecodeAggregator() {
+ return mAttachmentPreviewsDecodeAggregator;
+ }
+
+ public BitmapCache getSendersImagesCache() {
+ return mSendersImagesCache;
+ }
+
+ public ContactResolver getContactResolver() {
+ return mContactResolver;
}
/**
@@ -1129,7 +1139,7 @@
public void onScrollStateChanged(final int scrollState) {
final boolean scrolling = scrollState != OnScrollListener.SCROLL_STATE_IDLE;
- mBitmapCache.setBlocking(scrolling);
+ mAttachmentPreviewsCache.setBlocking(scrolling);
}
public int getViewMode() {
diff --git a/src/com/android/mail/ui/ConversationListFragment.java b/src/com/android/mail/ui/ConversationListFragment.java
index 95f0dc0..2abd2bc 100644
--- a/src/com/android/mail/ui/ConversationListFragment.java
+++ b/src/com/android/mail/ui/ConversationListFragment.java
@@ -56,7 +56,6 @@
import com.android.mail.providers.UIProvider.FolderCapabilities;
import com.android.mail.providers.UIProvider.FolderType;
import com.android.mail.providers.UIProvider.Swipe;
-import com.android.mail.ui.AnimatedAdapter.ConversationListListener;
import com.android.mail.ui.SwipeableListView.ListItemSwipedListener;
import com.android.mail.ui.SwipeableListView.ListItemsRemovedListener;
import com.android.mail.ui.ViewMode.ModeChangeListener;
@@ -300,8 +299,7 @@
}
mListAdapter = new AnimatedAdapter(mActivity.getApplicationContext(), conversationCursor,
- mActivity.getSelectedSet(), mActivity, mConversationListListener, mListView,
- specialItemViews);
+ mActivity.getSelectedSet(), mActivity, mListView, specialItemViews);
mListAdapter.addFooter(mFooterView);
mListView.setAdapter(mListAdapter);
mSelectedSet = mActivity.getSelectedSet();
@@ -682,15 +680,6 @@
mCallbacks.onConversationSelected(conv, false /* inLoaderCallbacks */);
}
- private final ConversationListListener mConversationListListener =
- new ConversationListListener() {
- @Override
- public boolean isExitingSelectionMode() {
- return System.currentTimeMillis() <
- (mSelectionModeExitedTimestamp + sSelectionModeAnimationDuration);
- }
- };
-
/**
* Sets the selected conversation to the position given here.
* @param cursorPosition The position of the conversation in the cursor (as opposed to
diff --git a/src/com/android/mail/ui/ConversationViewFragment.java b/src/com/android/mail/ui/ConversationViewFragment.java
index 98cbaae..51484b1 100644
--- a/src/com/android/mail/ui/ConversationViewFragment.java
+++ b/src/com/android/mail/ui/ConversationViewFragment.java
@@ -27,6 +27,9 @@
import android.os.AsyncTask;
import android.os.Bundle;
import android.os.SystemClock;
+import android.print.PrintAttributes;
+import android.print.PrintJob;
+import android.print.PrintManager;
import android.text.TextUtils;
import android.view.LayoutInflater;
import android.view.ScaleGestureDetector;
@@ -63,6 +66,7 @@
import com.android.mail.browse.SuperCollapsedBlock;
import com.android.mail.browse.WebViewContextMenu;
import com.android.mail.content.ObjectCursor;
+import com.android.mail.print.Printer;
import com.android.mail.providers.Account;
import com.android.mail.providers.Address;
import com.android.mail.providers.Conversation;
@@ -512,6 +516,7 @@
timerMark("CVF.showConversation");
} else {
final boolean disableOffscreenLoading = DISABLE_OFFSCREEN_LOADING
+ || Utils.isLowRamDevice(getContext())
|| (mConversation != null && (mConversation.isRemote
|| mConversation.getNumMessages() > mMaxAutoLoadMessages));
@@ -576,8 +581,7 @@
if (DEBUG_DUMP_CONVERSATION_HTML) {
java.io.FileWriter fw = null;
try {
- fw = new java.io.FileWriter("/sdcard/conv" + mConversation.id
- + ".html");
+ fw = new java.io.FileWriter(getSdCardFilePath());
fw.write(convHtml);
} catch (java.io.IOException e) {
e.printStackTrace();
@@ -602,6 +606,10 @@
mWebViewLoadStartMs = SystemClock.uptimeMillis();
}
+ protected String getSdCardFilePath() {
+ return "/sdcard/conv" + mConversation.id + ".html";
+ }
+
/**
* Populate the adapter with overlay views (message headers, super-collapsed blocks, a
* conversation header), and return an HTML document with spacer divs inserted for all overlays.
@@ -1570,4 +1578,17 @@
}
}
+
+ protected void printConversation() {
+ // TODO - offscreen webview stuff so that we don't clobber
+ final String convHtml =
+ Printer.print(getContext(), mAccount, getMessageCursor(),
+ mAddressCache, true /* userJavascript */);
+ mWebView.loadDataWithBaseURL(mBaseUri, convHtml, "text/html", "utf-8", null);
+ final PrintManager printManager =
+ (PrintManager) getContext().getSystemService(Context.PRINT_SERVICE);
+ printManager.print(getConversation().subject,
+ mWebView.createPrintDocumentAdapter(),
+ new PrintAttributes.Builder().build());
+ }
}
diff --git a/src/com/android/mail/ui/HtmlConversationTemplates.java b/src/com/android/mail/ui/HtmlConversationTemplates.java
index d368454..104d0c0 100644
--- a/src/com/android/mail/ui/HtmlConversationTemplates.java
+++ b/src/com/android/mail/ui/HtmlConversationTemplates.java
@@ -1,4 +1,4 @@
-/*
+/**
* Copyright (C) 2012 Google Inc.
* Licensed to The Android Open Source Project.
*
@@ -18,7 +18,6 @@
package com.android.mail.ui;
import android.content.Context;
-import android.content.res.Resources.NotFoundException;
import com.android.mail.R;
import com.android.mail.utils.LogTag;
@@ -26,30 +25,17 @@
import com.android.mail.utils.Utils;
import com.google.common.annotations.VisibleForTesting;
-import java.io.IOException;
-import java.io.InputStreamReader;
-import java.util.Formatter;
import java.util.regex.Pattern;
/**
* Renders data into very simple string-substitution HTML templates for conversation view.
- *
- * Templates should be UTF-8 encoded HTML with '%s' placeholders to be substituted upon render.
- * Plain-jane string substitution with '%s' is slightly faster than typed substitution.
- *
*/
-public class HtmlConversationTemplates {
+public class HtmlConversationTemplates extends AbstractHtmlTemplates {
/**
* Prefix applied to a message id for use as a div id
*/
public static final String MESSAGE_PREFIX = "m";
- public static final int MESSAGE_PREFIX_LENGTH = MESSAGE_PREFIX.length();
-
- // TODO: refine. too expensive to iterate over cursor and pre-calculate total. so either
- // estimate it, or defer assembly until the end when size is known (deferring increases
- // working set size vs. estimation but is exact).
- private static final int BUFFER_SIZE_CHARS = 64 * 1024;
private static final String TAG = LogTag.getLogTag();
@@ -92,13 +78,8 @@
private static String sConversationUpper;
private static String sConversationLower;
- private Context mContext;
- private Formatter mFormatter;
- private StringBuilder mBuilder;
- private boolean mInProgress = false;
-
public HtmlConversationTemplates(Context context) {
- mContext = context;
+ super(context);
// The templates are small (~2KB total in ICS MR2), so it's okay to load them once and keep
// them in memory.
@@ -178,7 +159,8 @@
public void startConversation(int sideMargin, int conversationHeaderHeight) {
if (mInProgress) {
- throw new IllegalStateException("must call startConversation first");
+ throw new IllegalStateException(
+ "Should not call start conversation until end conversation has been called");
}
reset();
@@ -209,49 +191,4 @@
return emit();
}
-
- public String emit() {
- String out = mFormatter.toString();
- // release the builder memory ASAP
- mFormatter = null;
- mBuilder = null;
- return out;
- }
-
- public void reset() {
- mBuilder = new StringBuilder(BUFFER_SIZE_CHARS);
- mFormatter = new Formatter(mBuilder, null /* no localization */);
- }
-
- private String readTemplate(int id) throws NotFoundException {
- StringBuilder out = new StringBuilder();
- InputStreamReader in = null;
- try {
- try {
- in = new InputStreamReader(
- mContext.getResources().openRawResource(id), "UTF-8");
- char[] buf = new char[4096];
- int chars;
-
- while ((chars=in.read(buf)) > 0) {
- out.append(buf, 0, chars);
- }
-
- return out.toString();
-
- } finally {
- if (in != null) {
- in.close();
- }
- }
- } catch (IOException e) {
- throw new NotFoundException("Unable to open template id=" + Integer.toHexString(id)
- + " exception=" + e.getMessage());
- }
- }
-
- private void append(String template, Object... args) {
- mFormatter.format(template, args);
- }
-
}
diff --git a/src/com/android/mail/ui/SecureConversationViewController.java b/src/com/android/mail/ui/SecureConversationViewController.java
index 1f9b13e..4a89bd2 100644
--- a/src/com/android/mail/ui/SecureConversationViewController.java
+++ b/src/com/android/mail/ui/SecureConversationViewController.java
@@ -183,6 +183,10 @@
mConversationHeaderView.setSubject(subject);
}
+ public void printConversation() {
+ // TODO - implement this
+ }
+
// Start MessageHeaderViewCallbacks implementations
@Override
diff --git a/src/com/android/mail/ui/SecureConversationViewControllerCallbacks.java b/src/com/android/mail/ui/SecureConversationViewControllerCallbacks.java
index 2e4f5d9..0284a49 100644
--- a/src/com/android/mail/ui/SecureConversationViewControllerCallbacks.java
+++ b/src/com/android/mail/ui/SecureConversationViewControllerCallbacks.java
@@ -18,6 +18,7 @@
package com.android.mail.ui;
import android.app.Fragment;
+import android.content.Context;
import android.net.Uri;
import android.os.Handler;
@@ -46,4 +47,5 @@
public String getBaseUri();
public boolean isViewOnlyMode();
public Uri getAccountUri();
+ public Context getContext();
}
diff --git a/src/com/android/mail/ui/SecureConversationViewFragment.java b/src/com/android/mail/ui/SecureConversationViewFragment.java
index 14937b7..729a774 100644
--- a/src/com/android/mail/ui/SecureConversationViewFragment.java
+++ b/src/com/android/mail/ui/SecureConversationViewFragment.java
@@ -261,4 +261,8 @@
public boolean supportsMessageTransforms() {
return false;
}
+
+ protected void printConversation() {
+ mViewController.printConversation();
+ }
}
diff --git a/src/com/android/mail/ui/SwipeableListView.java b/src/com/android/mail/ui/SwipeableListView.java
index 627a375..2c0ca14 100644
--- a/src/com/android/mail/ui/SwipeableListView.java
+++ b/src/com/android/mail/ui/SwipeableListView.java
@@ -419,7 +419,6 @@
if (adapter != null) {
adapter.onScrollStateChanged(scrollState);
}
- ConversationItemView.setScrollStateChanged(scrollState);
}
}
diff --git a/src/com/android/mail/photomanager/BitmapUtil.java b/src/com/android/mail/utils/BitmapUtil.java
similarity index 93%
rename from src/com/android/mail/photomanager/BitmapUtil.java
rename to src/com/android/mail/utils/BitmapUtil.java
index 9c2ab2b..6f61f56 100644
--- a/src/com/android/mail/photomanager/BitmapUtil.java
+++ b/src/com/android/mail/utils/BitmapUtil.java
@@ -14,18 +14,17 @@
* limitations under the License.
*/
-package com.android.mail.photomanager;
+package com.android.mail.utils;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Matrix;
-import com.android.mail.utils.LogUtils;
-
/**
* Provides static functions to decode bitmaps at the optimal size
*/
public class BitmapUtil {
+ private static final String TAG = LogTag.getLogTag();
private static final boolean DEBUG = false;
private BitmapUtil() {
@@ -53,7 +52,7 @@
opts.inJustDecodeBounds = false;
return BitmapFactory.decodeByteArray(src, 0, src.length, opts);
} catch (Throwable t) {
- LogUtils.w(PhotoManager.TAG, t, "unable to decode image");
+ LogUtils.w(TAG, t, "BitmapUtils unable to decode image");
return null;
}
}
@@ -73,7 +72,7 @@
return centerCrop(decoded, w, h);
} catch (Throwable t) {
- LogUtils.w(PhotoManager.TAG, t, "unable to crop image");
+ LogUtils.w(TAG, t, "BitmapUtils unable to crop image");
return null;
}
}
@@ -161,13 +160,13 @@
final Bitmap cropped = Bitmap.createBitmap(src, srcX, srcY, srcCroppedW, srcCroppedH, m,
true /* filter */);
- if (DEBUG) LogUtils.i(PhotoManager.TAG,
- "IN centerCrop, srcW/H=%s/%s desiredW/H=%s/%s srcX/Y=%s/%s" +
+ if (DEBUG) LogUtils.i(TAG,
+ "BitmapUtils IN centerCrop, srcW/H=%s/%s desiredW/H=%s/%s srcX/Y=%s/%s" +
" innerW/H=%s/%s scale=%s resultW/H=%s/%s",
srcWidth, srcHeight, w, h, srcX, srcY, srcCroppedW, srcCroppedH, scale,
cropped.getWidth(), cropped.getHeight());
if (DEBUG && (w != cropped.getWidth() || h != cropped.getHeight())) {
- LogUtils.e(PhotoManager.TAG, new Error(), "last center crop violated assumptions.");
+ LogUtils.e(TAG, new Error(), "BitmapUtils last center crop violated assumptions.");
}
return cropped;
diff --git a/src/com/android/mail/utils/LogUtils.java b/src/com/android/mail/utils/LogUtils.java
index 5d4f434..3203422 100644
--- a/src/com/android/mail/utils/LogUtils.java
+++ b/src/com/android/mail/utils/LogUtils.java
@@ -64,7 +64,7 @@
* production releases. This should be set to DEBUG for production releases, and VERBOSE for
* internal builds.
*/
- private static final int MAX_ENABLED_LOG_LEVEL = DEBUG;
+ private static final int MAX_ENABLED_LOG_LEVEL = VERBOSE;
private static Boolean sDebugLoggingEnabledForTests = null;
diff --git a/src/com/android/mail/utils/Utils.java b/src/com/android/mail/utils/Utils.java
index de21172..e6a0312 100644
--- a/src/com/android/mail/utils/Utils.java
+++ b/src/com/android/mail/utils/Utils.java
@@ -16,12 +16,14 @@
package com.android.mail.utils;
+import com.android.mail.providers.Address;
import com.google.android.mail.common.html.parser.HtmlDocument;
import com.google.android.mail.common.html.parser.HtmlParser;
import com.google.android.mail.common.html.parser.HtmlTree;
import com.google.android.mail.common.html.parser.HtmlTreeBuilder;
import com.google.common.collect.Maps;
+import android.app.ActivityManager;
import android.app.Fragment;
import android.app.SearchManager;
import android.content.Context;
@@ -146,6 +148,21 @@
}
/**
+ * @return Whether we are running on a low memory device. This is used to disable certain
+ * memory intensive features in the app.
+ */
+ public static boolean isLowRamDevice(Context context) {
+ // TODO: use SDK_INT to check if device is KitKat or greater.
+ if (Build.VERSION.CODENAME.startsWith("K")) {
+ final ActivityManager am = (ActivityManager) context.getSystemService(
+ Context.ACTIVITY_SERVICE);
+ return am.isLowRamDevice();
+ } else {
+ return false;
+ }
+ }
+
+ /**
* Sets WebView in a restricted mode suitable for email use.
*
* @param webView The WebView to restrict
@@ -710,7 +727,7 @@
/**
* Create an intent to show a conversation.
* @param conversation Conversation to open.
- * @param folder
+ * @param folderUri
* @param account
* @return
*/
@@ -733,7 +750,7 @@
/**
* Create an intent to open a folder.
*
- * @param folder Folder to open.
+ * @param folderUri Folder to open.
* @param account
* @return
*/
@@ -963,18 +980,22 @@
final Bitmap drawingCache = rootView.getDrawingCache();
// Null check to avoid NPE discovered from monkey crash:
if (drawingCache != null) {
- final Bitmap originalBitmap = drawingCache.copy(Bitmap.Config.RGB_565, false);
- double originalHeight = originalBitmap.getHeight();
- double originalWidth = originalBitmap.getWidth();
- int newHeight = SCALED_SCREENSHOT_MAX_HEIGHT_WIDTH;
- int newWidth = SCALED_SCREENSHOT_MAX_HEIGHT_WIDTH;
- double scaleX, scaleY;
- scaleX = newWidth / originalWidth;
- scaleY = newHeight / originalHeight;
- final double scale = Math.min(scaleX, scaleY);
- newWidth = (int)Math.round(originalWidth * scale);
- newHeight = (int)Math.round(originalHeight * scale);
- return Bitmap.createScaledBitmap(originalBitmap, newWidth, newHeight, true);
+ try {
+ final Bitmap originalBitmap = drawingCache.copy(Bitmap.Config.RGB_565, false);
+ double originalHeight = originalBitmap.getHeight();
+ double originalWidth = originalBitmap.getWidth();
+ int newHeight = SCALED_SCREENSHOT_MAX_HEIGHT_WIDTH;
+ int newWidth = SCALED_SCREENSHOT_MAX_HEIGHT_WIDTH;
+ double scaleX, scaleY;
+ scaleX = newWidth / originalWidth;
+ scaleY = newHeight / originalHeight;
+ final double scale = Math.min(scaleX, scaleY);
+ newWidth = (int)Math.round(originalWidth * scale);
+ newHeight = (int)Math.round(originalHeight * scale);
+ return Bitmap.createScaledBitmap(originalBitmap, newWidth, newHeight, true);
+ } catch (OutOfMemoryError e) {
+ LogUtils.e(LOG_TAG, e, "OOME when attempting to scale screenshot");
+ }
}
}
return null;
@@ -1023,8 +1044,8 @@
*
* @param type MIME data type to normalize
* @return normalized MIME data type, or null if the input was null
- * @see {@link #setType}
- * @see {@link #setTypeAndNormalize}
+ * @see {@link android.content.Intent#setType}
+ * @see {@link android.content.Intent#setTypeAndNormalize}
*/
public static String normalizeMimeType(String type) {
if (type == null) {
@@ -1041,7 +1062,7 @@
}
/**
- * (copied from {@link Uri#normalize()} for pre-J)
+ * (copied from {@link android.net.Uri#normalizeScheme()} for pre-J)
*
* Return a normalized representation of this Uri.
*
@@ -1063,7 +1084,7 @@
*
* @return normalized Uri (never null)
* @see {@link android.content.Intent#setData}
- * @see {@link #setNormalizedData}
+ * @see {@link android.content.Intent#setNormalizedData}
*/
public static Uri normalizeUri(Uri uri) {
String scheme = uri.getScheme();
@@ -1409,4 +1430,20 @@
}
}
+
+ public static Address getAddress(Map<String, Address> cache, String emailStr) {
+ Address addr = null;
+ synchronized (cache) {
+ if (cache != null) {
+ addr = cache.get(emailStr);
+ }
+ if (addr == null) {
+ addr = Address.getEmailAddress(emailStr);
+ if (cache != null) {
+ cache.put(emailStr, addr);
+ }
+ }
+ }
+ return addr;
+ }
}