summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--proguard.flags1
-rw-r--r--res/drawable-hdpi/spinner_holo.pngbin0 -> 738 bytes
-rw-r--r--res/layout/conversation_attachment_previews.xml29
-rw-r--r--res/layout/conversation_item_view_normal.xml18
-rw-r--r--res/layout/conversation_item_view_normal_spacious.xml21
-rw-r--r--res/layout/conversation_item_view_wide.xml21
-rw-r--r--res/values/colors.xml3
-rw-r--r--res/values/constants.xml3
-rw-r--r--res/values/dimen.xml17
-rw-r--r--res/values/strings.xml2
-rw-r--r--src/com/android/mail/browse/ConversationItemView.java536
-rw-r--r--src/com/android/mail/browse/ConversationItemViewCoordinates.java151
-rw-r--r--src/com/android/mail/browse/ConversationItemViewModel.java3
-rw-r--r--src/com/android/mail/browse/MessageAttachmentTile.java12
-rw-r--r--src/com/android/mail/photo/MailPhotoViewActivity.java23
-rw-r--r--src/com/android/mail/photomanager/AttachmentPreviewsManager.java352
-rw-r--r--src/com/android/mail/photomanager/BitmapUtil.java101
-rw-r--r--src/com/android/mail/photomanager/ContactPhotoManager.java71
-rw-r--r--src/com/android/mail/photomanager/LetterTileProvider.java2
-rw-r--r--src/com/android/mail/photomanager/PhotoManager.java417
-rw-r--r--src/com/android/mail/preferences/MailPrefs.java1
-rw-r--r--src/com/android/mail/providers/Attachment.java62
-rw-r--r--src/com/android/mail/providers/Conversation.java82
-rw-r--r--src/com/android/mail/providers/UIProvider.java76
-rw-r--r--src/com/android/mail/ui/DividedImageCanvas.java168
-rw-r--r--src/com/android/mail/ui/FolderSelectionActivity.java1
-rw-r--r--src/com/android/mail/ui/ImageCanvas.java21
-rw-r--r--src/com/android/mail/ui/SwipeableListView.java23
28 files changed, 1802 insertions, 415 deletions
diff --git a/proguard.flags b/proguard.flags
index 033b29e21..5d904420e 100644
--- a/proguard.flags
+++ b/proguard.flags
@@ -60,6 +60,7 @@
-keepclasseswithmembers class com.android.mail.browse.ConversationItemView {
*** setAnimatedHeightFraction(...);
+ *** setAnimatedProgressFraction(...);
}
-keepclasseswithmembers class com.android.mail.ui.MailActivity {
diff --git a/res/drawable-hdpi/spinner_holo.png b/res/drawable-hdpi/spinner_holo.png
new file mode 100644
index 000000000..396ce71f6
--- /dev/null
+++ b/res/drawable-hdpi/spinner_holo.png
Binary files differ
diff --git a/res/layout/conversation_attachment_previews.xml b/res/layout/conversation_attachment_previews.xml
new file mode 100644
index 000000000..eb7ce2591
--- /dev/null
+++ b/res/layout/conversation_attachment_previews.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/attachment_previews"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="@dimen/attachment_preview_margin_top"
+ android:layout_marginLeft="@dimen/attachment_preview_margin_side"
+ android:layout_marginRight="@dimen/attachment_preview_margin_side"
+ android:visibility="gone" >
+ <!--todo:markwei get font color, typeface, and size from channah-->
+ <TextView
+ android:id="@+id/ap_overflow"
+ android:layout_width="@dimen/ap_overflow_count_diameter"
+ android:layout_height="@dimen/ap_overflow_count_diameter"
+ android:layout_marginRight="@dimen/ap_margin_side"
+ android:layout_marginBottom="@dimen/ap_margin_side"
+ android:layout_gravity="bottom|right"
+ android:includeFontPadding="false"
+ android:textStyle="bold"
+ android:textSize="12sp"/>
+ <!--todo:markwei get actual spinner asset from channah-->
+ <ImageView
+ android:id="@+id/ap_progress_bar"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center"
+ android:src="@drawable/spinner_holo" />
+</FrameLayout> \ No newline at end of file
diff --git a/res/layout/conversation_item_view_normal.xml b/res/layout/conversation_item_view_normal.xml
index 311f5c6a2..149dca085 100644
--- a/res/layout/conversation_item_view_normal.xml
+++ b/res/layout/conversation_item_view_normal.xml
@@ -164,16 +164,20 @@
</LinearLayout>
</LinearLayout>
+ <!-- There are 12dp bottom margins here for when there are no folders -->
- <ImageView
- android:id="@+id/attachment_previews"
- android:layout_width="match_parent"
- android:layout_height="0dp"
- android:layout_marginTop="@dimen/attachment_preview_margin_top"
- android:layout_marginLeft="@dimen/attachment_preview_margin_side"
- android:layout_marginRight="@dimen/attachment_preview_margin_side"
+ <include
+ layout="@layout/conversation_attachment_previews"/>
+
+ <!-- Margin between attachment previews and folders. 12dp bottom margins match above -->
+ <View
+ android:id="@+id/attachment_previews_bottom_margin"
+ android:layout_width="0dp"
+ android:layout_height="@dimen/attachment_preview_margin_bottom"
+ android:layout_marginBottom="12dp"
android:visibility="gone" />
+ <!-- Folders should be 0dp below the subject, so we cancel out the 12dp bottom margin above -->
<TextView
android:id="@+id/folders"
android:layout_width="match_parent"
diff --git a/res/layout/conversation_item_view_normal_spacious.xml b/res/layout/conversation_item_view_normal_spacious.xml
index 0b806277a..25218e624 100644
--- a/res/layout/conversation_item_view_normal_spacious.xml
+++ b/res/layout/conversation_item_view_normal_spacious.xml
@@ -164,16 +164,23 @@
</LinearLayout>
</LinearLayout>
+ <!-- There are 16dp bottom margins here for when there are no labels -->
- <ImageView
- android:id="@+id/attachment_previews"
- android:layout_width="match_parent"
- android:layout_height="0dp"
- android:layout_marginTop="@dimen/attachment_preview_margin_top"
- android:layout_marginLeft="@dimen/attachment_preview_margin_side"
- android:layout_marginRight="@dimen/attachment_preview_margin_side"
+ <include
+ layout="@layout/conversation_attachment_previews"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="@dimen/attachment_preview_margin_top_spacious"/>
+
+ <!-- Margin between attachment previews and folders. 16dp bottom margins match above -->
+ <View
+ android:id="@+id/attachment_previews_bottom_margin"
+ android:layout_width="0dp"
+ android:layout_height="@dimen/attachment_preview_margin_bottom"
+ android:layout_marginBottom="16dp"
android:visibility="gone" />
+ <!-- Labels should be 0dp below the subject, so we cancel out the 16dp bottom margin -->
<TextView
android:id="@+id/folders"
android:layout_width="match_parent"
diff --git a/res/layout/conversation_item_view_wide.xml b/res/layout/conversation_item_view_wide.xml
index 3acda636e..6061e21c1 100644
--- a/res/layout/conversation_item_view_wide.xml
+++ b/res/layout/conversation_item_view_wide.xml
@@ -157,16 +157,23 @@
android:layout_gravity="top|right" />
</FrameLayout>
+ <!-- There are 10dp bottom margins here for when there are no folders -->
- <ImageView
- android:id="@+id/attachment_previews"
- android:layout_width="match_parent"
- android:layout_height="0dp"
- android:layout_marginTop="@dimen/attachment_preview_margin_top"
- android:layout_marginLeft="@dimen/attachment_preview_margin_side"
- android:layout_marginRight="@dimen/attachment_preview_margin_side"
+ <include
+ layout="@layout/conversation_attachment_previews"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="@dimen/attachment_preview_margin_top_wide"/>
+
+ <!-- Margin between attachment previews and folders. 10dp bottom margins match above -->
+ <View
+ android:id="@+id/attachment_previews_bottom_margin"
+ android:layout_width="0dp"
+ android:layout_height="@dimen/attachment_preview_margin_bottom_wide"
+ android:layout_marginBottom="10dp"
android:visibility="gone" />
+ <!-- Folders should be 0dp below the subject, so we cancel out the 10dp bottom margin above -->
<TextView
android:id="@+id/folders"
android:layout_width="match_parent"
diff --git a/res/values/colors.xml b/res/values/colors.xml
index c3972fd92..4a7261004 100644
--- a/res/values/colors.xml
+++ b/res/values/colors.xml
@@ -32,6 +32,9 @@
<color name="date_text_color">@color/dark_gray_text_color</color>
<color name="message_info_text_color">@color/gray_text_color</color>
<color name="subject_text_color">#333333</color>
+ <!--todo:markwei get overflow badge and count color from channah-->
+ <color name="ap_overflow_badge_color">#aaeeeeee</color>
+ <color name="ap_overflow_text_color">@android:color/tertiary_text_light</color>
<!-- a 'checked' item is in the conversation selection set. also the 'pressed' color. -->
<!-- this is holo_blue_light @ 20% opacity -->
<color name="checked_item_background_color">#cfe9f3</color>
diff --git a/res/values/constants.xml b/res/values/constants.xml
index f05a3405f..517ae8dd3 100644
--- a/res/values/constants.xml
+++ b/res/values/constants.xml
@@ -114,4 +114,7 @@
<!-- Number of menu items to hide from the ActionBar by subtracting from actionbar_max_items in non-cab mode -->
<integer name="actionbar_hidden_non_cab_items_no_physical_button">1</integer>
+
+ <!-- Max overflow count to show for attachment previews -->
+ <integer name="ap_overflow_max_count">99</integer>
</resources>
diff --git a/res/values/dimen.xml b/res/values/dimen.xml
index 32230ff69..1937ae5f5 100644
--- a/res/values/dimen.xml
+++ b/res/values/dimen.xml
@@ -109,11 +109,24 @@
<dimen name="tile_letter_font_size_small">16dp</dimen>
<dimen name="tile_divider_width">1dp</dimen>
<dimen name="sync_status_bar_height">40dip</dimen>
+
<dimen name="attachment_preview_height_tall">124dip</dimen>
<dimen name="attachment_preview_height_tall_wide">168dip</dimen>
<dimen name="attachment_preview_height_short">28dip</dimen>
- <dimen name="attachment_preview_margin_top">8dip</dimen>
- <dimen name="attachment_preview_margin_top_wide">12dip</dimen>
+ <!-- Margin of 8dp with 12dp cancelled out -->
+ <dimen name="attachment_preview_margin_top">-4dip</dimen>
+ <!-- Margin of 8dp with 16dp cancelled out -->
+ <dimen name="attachment_preview_margin_top_spacious">-8dip</dimen>
+ <!-- Margin of 12dp with 10dp cancelled out -->
+ <dimen name="attachment_preview_margin_top_wide">2dip</dimen>
<dimen name="attachment_preview_margin_side">16dip</dimen>
+ <dimen name="attachment_preview_margin_bottom">4dp</dimen>
+ <dimen name="attachment_preview_margin_bottom_wide">8dp</dimen>
+
+ <!--todo:markwei get the diameter from channah-->
+ <dimen name="ap_overflow_count_diameter">20dp</dimen>
+ <!--todo:markwei channah wanted 8dp but only 4dp fits read previews-->
+ <dimen name="ap_margin_side">4dp</dimen>
+
<dimen name="folder_minimum_width">48dip</dimen>
</resources>
diff --git a/res/values/strings.xml b/res/values/strings.xml
index 642358c0b..a674dd202 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -346,6 +346,8 @@
<!-- Displayed in Conversation Header View and Widget in the form of "subject - snippet"
[CHAR LIMIT=5] -->
<string name="subject_and_snippet"><xliff:g>%s</xliff:g> \u2014 <xliff:g>%s</xliff:g></string>
+ <!-- Displayed in attachment previews in conversation list [CHAR LIMIT=1] -->
+ <string name="ap_overflow_format">+<xliff:g>%d</xliff:g></string>
<!-- Displayed in browse list item when the list item is a draft message instead of showing the subject [CHAR LIMIT=100] -->
<plurals name="draft">
<!-- Title of the screen when there is exactly one draft -->
diff --git a/src/com/android/mail/browse/ConversationItemView.java b/src/com/android/mail/browse/ConversationItemView.java
index d1d0fbc83..8f7dcc05b 100644
--- a/src/com/android/mail/browse/ConversationItemView.java
+++ b/src/com/android/mail/browse/ConversationItemView.java
@@ -18,6 +18,7 @@
package com.android.mail.browse;
import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
import android.animation.AnimatorSet;
import android.animation.ObjectAnimator;
import android.content.ClipData;
@@ -30,6 +31,7 @@ import android.graphics.Canvas;
import android.graphics.LinearGradient;
import android.graphics.Paint;
import android.graphics.Point;
+import android.graphics.Rect;
import android.graphics.Shader;
import android.graphics.Typeface;
import android.graphics.drawable.Drawable;
@@ -56,18 +58,30 @@ import android.view.View;
import android.view.ViewGroup;
import android.view.ViewParent;
import android.view.animation.DecelerateInterpolator;
+import android.view.animation.LinearInterpolator;
import android.widget.TextView;
import com.android.mail.R;
+import com.android.mail.R.color;
+import com.android.mail.R.drawable;
+import com.android.mail.R.integer;
+import com.android.mail.R.string;
import com.android.mail.browse.ConversationItemViewModel.SenderFragment;
import com.android.mail.perf.Timer;
+import com.android.mail.photo.MailPhotoViewActivity;
+import com.android.mail.photomanager.AttachmentPreviewsManager;
+import com.android.mail.photomanager.AttachmentPreviewsManager.AttachmentPreviewsDividedImageCanvas;
+import com.android.mail.photomanager.AttachmentPreviewsManager.AttachmentPreviewsManagerCallback;
import com.android.mail.photomanager.ContactPhotoManager;
import com.android.mail.photomanager.ContactPhotoManager.ContactIdentifier;
+import com.android.mail.photomanager.AttachmentPreviewsManager.AttachmentPreviewIdentifier;
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;
import com.android.mail.providers.Folder;
import com.android.mail.providers.UIProvider;
+import com.android.mail.providers.UIProvider.AttachmentRendition;
import com.android.mail.providers.UIProvider.ConversationColumns;
import com.android.mail.providers.UIProvider.ConversationListIcon;
import com.android.mail.providers.UIProvider.FolderType;
@@ -77,6 +91,7 @@ import com.android.mail.ui.ConversationSelectionSet;
import com.android.mail.ui.DividedImageCanvas;
import com.android.mail.ui.DividedImageCanvas.InvalidateCallback;
import com.android.mail.ui.FolderDisplayer;
+import com.android.mail.ui.ImageCanvas;
import com.android.mail.ui.SwipeableItemView;
import com.android.mail.ui.SwipeableListView;
import com.android.mail.ui.ViewMode;
@@ -85,11 +100,14 @@ import com.android.mail.utils.LogTag;
import com.android.mail.utils.LogUtils;
import com.android.mail.utils.Utils;
import com.google.common.annotations.VisibleForTesting;
+import com.google.common.collect.Lists;
import java.util.ArrayList;
+import java.util.List;
public class ConversationItemView extends View implements SwipeableItemView, ToggleableItem,
- InvalidateCallback {
+ InvalidateCallback, AttachmentPreviewsManagerCallback {
+
// Timer.
private static int sLayoutCount = 0;
private static Timer sTimer; // Create the sTimer here if you need to do
@@ -117,15 +135,19 @@ public class ConversationItemView extends View implements SwipeableItemView, Tog
private static Bitmap STATE_CALENDAR_INVITE;
private static Bitmap VISIBLE_CONVERSATION_CARET;
private static Drawable RIGHT_EDGE_TABLET;
+ private static Bitmap PROGRESS_BAR;
private static String sSendersSplitToken;
private static String sElidedPaddingToken;
+ private static String sOverflowCountFormat;
// Static colors.
private static int sActivatedTextColor;
private static int sSendersTextColorRead;
private static int sSendersTextColorUnread;
private static int sDateTextColor;
+ private static int sOverflowBadgeColor;
+ private static int sOverflowTextColor;
private static int sStarTouchSlop;
private static int sSenderImageTouchSlop;
@Deprecated
@@ -133,11 +155,16 @@ public class ConversationItemView extends View implements SwipeableItemView, Tog
private static int sShrinkAnimationDuration;
private static int sSlideAnimationDuration;
private static int sAnimatingBackgroundColor;
+ // todo:markwei get duration from channah
+ private static int sProgressAnimationDuration;
+ private static int sOverflowCountMax;
// Static paints.
private static TextPaint sPaint = new TextPaint();
private static TextPaint sFoldersPaint = new TextPaint();
+ private static Rect sRect = new Rect();
+
// Backgrounds for different states.
private final SparseArray<Drawable> mBackgrounds = new SparseArray<Drawable>();
@@ -145,9 +172,12 @@ public class ConversationItemView extends View implements SwipeableItemView, Tog
private int mViewWidth = -1;
/** The view mode at which we calculated mViewWidth previously. */
private int mPreviousMode;
+
private int mDateX;
private int mPaperclipX;
private int mSendersWidth;
+ private int mOverflowX;
+ private int mOverflowY;
/** Whether we are on a tablet device or not */
private final boolean mTabletDevice;
@@ -180,8 +210,8 @@ public class ConversationItemView extends View implements SwipeableItemView, Tog
private final TextView mSendersTextView;
private int mGadgetMode;
private final DividedImageCanvas mContactImagesHolder;
- private int mAttachmentPreviewMode;
- private final DividedImageCanvas mAttachmentPreviewsCanvas;
+ private static ContactPhotoManager sContactPhotoManager;
+
private static int sFoldersLeftPadding;
private static TextAppearanceSpan sSubjectTextUnreadSpan;
@@ -190,14 +220,37 @@ public class ConversationItemView extends View implements SwipeableItemView, Tog
private static ForegroundColorSpan sSnippetTextReadSpan;
private static int sScrollSlop;
private static CharacterStyle sActivatedTextSpan;
- private static ContactPhotoManager sContactPhotoManager;
- private static ContactPhotoManager sAttachmentPreviewsManager;
+
+ private final AttachmentPreviewsDividedImageCanvas mAttachmentPreviewsCanvas;
+ private static AttachmentPreviewsManager sAttachmentPreviewsManager;
+ /**
+ * Animates the mAnimatedProgressFraction field to make the progress bars spin. Cancelling
+ * this animator does not remove the progress bars.
+ */
+ private final ObjectAnimator mProgressAnimator;
+ private float mAnimatedProgressFraction;
+ private boolean[] mImagesLoaded = new boolean[0];
+ private static final boolean CONVLIST_ATTACHMENT_PREVIEWS_ENABLED = true;
static {
sPaint.setAntiAlias(true);
sFoldersPaint.setAntiAlias(true);
}
+ public static void setPhotoManagersPaused(boolean shouldPause) {
+ if (sContactPhotoManager == null) {
+ return;
+ }
+
+ if (shouldPause) {
+ sContactPhotoManager.pause();
+ sAttachmentPreviewsManager.pause();
+ } else {
+ sContactPhotoManager.resume();
+ sAttachmentPreviewsManager.resume();
+ }
+ }
+
/**
* Handles displaying folders in a conversation header view.
*/
@@ -341,6 +394,7 @@ public class ConversationItemView extends View implements SwipeableItemView, Tog
public ConversationItemView(Context context, String account) {
super(context);
+ Utils.traceBeginSection("CIVC constructor");
setClickable(true);
setLongClickable(true);
mContext = context.getApplicationContext();
@@ -374,8 +428,9 @@ public class ConversationItemView extends View implements SwipeableItemView, Tog
BitmapFactory.decodeResource(res, R.drawable.ic_badge_invite_holo_light);
VISIBLE_CONVERSATION_CARET = BitmapFactory.decodeResource(res,
R.drawable.ic_carrot_holo);
-
RIGHT_EDGE_TABLET = res.getDrawable(R.drawable.list_edge_tablet);
+// todo:markwei get actual spinner asset from channah
+ PROGRESS_BAR = BitmapFactory.decodeResource(res, drawable.spinner_holo);
// Initialize colors.
sActivatedTextColor = res.getColor(R.color.senders_text_color_read);
@@ -391,6 +446,8 @@ public class ConversationItemView extends View implements SwipeableItemView, Tog
sSnippetTextReadSpan =
new ForegroundColorSpan(res.getColor(R.color.snippet_text_color_read));
sDateTextColor = res.getColor(R.color.date_text_color);
+ sOverflowBadgeColor = res.getColor(color.ap_overflow_badge_color);
+ sOverflowTextColor = res.getColor(color.ap_overflow_text_color);
sStarTouchSlop = res.getDimensionPixelSize(R.dimen.star_touch_slop);
sSenderImageTouchSlop = res.getDimensionPixelSize(R.dimen.sender_image_touch_slop);
sStandardScaledDimen = res.getDimensionPixelSize(R.dimen.standard_scaled_dimen);
@@ -399,11 +456,15 @@ public class ConversationItemView extends View implements SwipeableItemView, Tog
// Initialize static color.
sSendersSplitToken = res.getString(R.string.senders_split_token);
sElidedPaddingToken = res.getString(R.string.elided_padding_token);
+ sOverflowCountFormat = res.getString(string.ap_overflow_format);
sAnimatingBackgroundColor = res.getColor(R.color.animating_item_background_color);
sScrollSlop = res.getInteger(R.integer.swipeScrollSlop);
sFoldersLeftPadding = res.getDimensionPixelOffset(R.dimen.folders_left_padding);
sContactPhotoManager = ContactPhotoManager.createContactPhotoManager(context);
- sAttachmentPreviewsManager = ContactPhotoManager.createContactPhotoManager(context);
+ sAttachmentPreviewsManager = new AttachmentPreviewsManager(context);
+ // todo:markwei get animation duration from channah
+ sProgressAnimationDuration = 1000;
+ sOverflowCountMax = res.getInteger(integer.ap_overflow_max_count);
}
mSendersTextView = new TextView(mContext);
@@ -425,29 +486,73 @@ public class ConversationItemView extends View implements SwipeableItemView, Tog
mCoordinates.contactImagesY + mCoordinates.contactImagesHeight);
}
});
- mAttachmentPreviewsCanvas = new DividedImageCanvas(context, this);
+ mAttachmentPreviewsCanvas = new AttachmentPreviewsDividedImageCanvas(context,
+ new InvalidateCallback() {
+ @Override
+ public void invalidate() {
+ if (mCoordinates == null) {
+ return;
+ }
+ ConversationItemView.this.invalidate(
+ mCoordinates.attachmentPreviewsX, mCoordinates.attachmentPreviewsY,
+ mCoordinates.attachmentPreviewsX
+ + mCoordinates.attachmentPreviewsWidth,
+ mCoordinates.attachmentPreviewsY
+ + mCoordinates.attachmentPreviewsHeight);
+ }
+ });
+
+ mProgressAnimator = createProgressAnimator();
+ Utils.traceEndSection();
}
public void bind(Conversation conversation, ControllableActivity activity,
ConversationSelectionSet set, Folder folder, int checkboxOrSenderImage,
boolean swipeEnabled, boolean priorityArrowEnabled, AnimatedAdapter adapter) {
+ Utils.traceBeginSection("CIVC.bind");
bind(ConversationItemViewModel.forConversation(mAccount, conversation), activity, set,
folder, checkboxOrSenderImage, swipeEnabled, priorityArrowEnabled, adapter);
+ Utils.traceEndSection();
}
private void bind(ConversationItemViewModel header, ControllableActivity activity,
ConversationSelectionSet set, Folder folder, int checkboxOrSenderImage,
boolean swipeEnabled, boolean priorityArrowEnabled, AnimatedAdapter adapter) {
- // If this was previously bound to a conversation, remove any contact
- // photo manager requests.
- // TODO:MARKWEI attachment previews
if (mHeader != null) {
- final ArrayList<String> divisionIds = mContactImagesHolder.getDivisionIds();
- if (divisionIds != null) {
- mContactImagesHolder.reset();
- for (int pos = 0; pos < divisionIds.size(); pos++) {
- sContactPhotoManager.removePhoto(DividedImageCanvas.generateHash(
- mContactImagesHolder, pos, divisionIds.get(pos)));
+ // If this was previously bound to a different conversation, remove any contact photo
+ // manager requests.
+ if (header.conversation.id != mHeader.conversation.id || !header.displayableSenderNames
+ .equals(mHeader.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 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())) {
+ ArrayList<String> divisionIds = mAttachmentPreviewsCanvas.getDivisionIds();
+ if (divisionIds != null) {
+ mAttachmentPreviewsCanvas.reset();
+ for (int pos = 0; pos < divisionIds.size(); pos++) {
+ String uri = divisionIds.get(pos);
+ for (int rendition : AttachmentRendition.PREFERRED_RENDITIONS) {
+ AttachmentPreviewIdentifier id = new AttachmentPreviewIdentifier(uri,
+ rendition, 0, 0);
+ sAttachmentPreviewsManager
+ .removePhoto(AttachmentPreviewsManager.generateHash(
+ mAttachmentPreviewsCanvas, id.getKey()));
+ }
+ }
}
}
}
@@ -459,13 +564,7 @@ public class ConversationItemView extends View implements SwipeableItemView, Tog
mStarEnabled = folder != null && !folder.isTrash();
mSwipeEnabled = swipeEnabled;
mAdapter = adapter;
- if (mHeader.conversation.getAttachmentsCount() == 0) {
- mAttachmentPreviewMode = ConversationItemViewCoordinates.ATTACHMENT_PREVIEW_NONE;
- } else {
- mAttachmentPreviewMode = mHeader.conversation.read ?
- ConversationItemViewCoordinates.ATTACHMENT_PREVIEW_SHORT
- : ConversationItemViewCoordinates.ATTACHMENT_PREVIEW_TALL;
- }
+ mImagesLoaded = new boolean[mHeader.conversation.getAttachmentPreviewUris().size()];
if (checkboxOrSenderImage == ConversationListIcon.SENDER_IMAGE) {
mGadgetMode = ConversationItemViewCoordinates.GADGET_CONTACT_PHOTO;
@@ -495,7 +594,7 @@ public class ConversationItemView extends View implements SwipeableItemView, Tog
mConfig = new ConversationItemViewCoordinates.Config()
.withGadget(mGadgetMode)
- .withAttachmentPreviews(mAttachmentPreviewMode);
+ .withAttachmentPreviews(getAttachmentPreviewsMode());
if (header.folderDisplayer.hasVisibleFolders()) {
mConfig.showFolders();
}
@@ -527,6 +626,9 @@ public class ConversationItemView extends View implements SwipeableItemView, Tog
mConfig.showPersonalIndicator();
}
+ int overflowCount = Math.min(getOverflowCount(), sOverflowCountMax);
+ mHeader.overflowText = String.format(sOverflowCountFormat, overflowCount);
+
setContentDescription();
requestLayout();
}
@@ -552,6 +654,7 @@ public class ConversationItemView extends View implements SwipeableItemView, Tog
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ Utils.traceBeginSection("CIVC.measure");
final int wSize = MeasureSpec.getSize(widthMeasureSpec);
final int currentMode = mActivity.getViewMode().getMode();
@@ -565,11 +668,6 @@ public class ConversationItemView extends View implements SwipeableItemView, Tog
Resources res = getResources();
mHeader.standardScaledDimen = res.getDimensionPixelOffset(R.dimen.standard_scaled_dimen);
- if (mHeader.standardScaledDimen != sStandardScaledDimen) {
- // Large Text has been toggle on/off. Update the static dimens.
- sStandardScaledDimen = mHeader.standardScaledDimen;
- ConversationItemViewCoordinates.refreshConversationDimens(mContext);
- }
mCoordinates = ConversationItemViewCoordinates.forConfig(mContext, mConfig,
mAdapter.getCoordinatesCache());
@@ -577,6 +675,7 @@ public class ConversationItemView extends View implements SwipeableItemView, Tog
final int h = (mAnimatedHeightFraction != 1.0f) ?
Math.round(mAnimatedHeightFraction * mCoordinates.height) : mCoordinates.height;
setMeasuredDimension(mConfig.getWidth(), h);
+ Utils.traceEndSection();
}
@Override
@@ -615,6 +714,7 @@ public class ConversationItemView extends View implements SwipeableItemView, Tog
@Override
public void setBackgroundResource(int resourceId) {
+ Utils.traceBeginSection("set background resource");
Drawable drawable = mBackgrounds.get(resourceId);
if (drawable == null) {
drawable = getResources().getDrawable(resourceId);
@@ -623,6 +723,7 @@ public class ConversationItemView extends View implements SwipeableItemView, Tog
if (getBackground() != drawable) {
super.setBackgroundDrawable(drawable);
}
+ Utils.traceEndSection();
}
private void calculateTextsAndBitmaps() {
@@ -677,7 +778,7 @@ public class ConversationItemView extends View implements SwipeableItemView, Tog
}
}
- if (mAttachmentPreviewMode != ConversationItemViewCoordinates.ATTACHMENT_PREVIEW_NONE) {
+ if (isAttachmentPreviewsEnabled()) {
loadAttachmentPreviews();
}
@@ -702,6 +803,30 @@ public class ConversationItemView extends View implements SwipeableItemView, Tog
pauseTimer(PERF_TAG_CALCULATE_TEXTS_BITMAPS);
}
+ private boolean isAttachmentPreviewsEnabled() {
+ return CONVLIST_ATTACHMENT_PREVIEWS_ENABLED
+ && mHeader.conversation.attachmentPreviewsCount != 0;
+ }
+
+ private boolean getOverflowCountVisible() {
+ return isAttachmentPreviewsEnabled() && getOverflowCount() > 0;
+ }
+
+ private int getOverflowCount() {
+ return mHeader.conversation.attachmentPreviewsCount - mHeader.conversation
+ .getAttachmentPreviewUris().size();
+ }
+
+ private int getAttachmentPreviewsMode() {
+ if (isAttachmentPreviewsEnabled()) {
+ return mHeader.conversation.read
+ ? ConversationItemViewCoordinates.ATTACHMENT_PREVIEW_READ
+ : ConversationItemViewCoordinates.ATTACHMENT_PREVIEW_UNREAD;
+ } else {
+ return ConversationItemViewCoordinates.ATTACHMENT_PREVIEW_NONE;
+ }
+ }
+
// FIXME(ath): maybe move this to bind(). the only dependency on layout is on tile W/H, which
// is immutable.
private void loadSenderImages() {
@@ -715,10 +840,16 @@ public class ConversationItemView extends View implements SwipeableItemView, Tog
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(mHeader.displayableSenderEmails);
- int size = mHeader.displayableSenderEmails.size();
+ mContactImagesHolder.setDivisionIds(keys);
String emailAddress;
for (int i = 0; i < DividedImageCanvas.MAX_DIVISIONS && i < size; i++) {
emailAddress = mHeader.displayableSenderEmails.get(i);
@@ -730,29 +861,122 @@ public class ConversationItemView extends View implements SwipeableItemView, Tog
}
private void loadAttachmentPreviews() {
- if (mAttachmentPreviewMode != ConversationItemViewCoordinates.ATTACHMENT_PREVIEW_NONE) {
- final int attachmentPreviewsHeight = ConversationItemViewCoordinates
- .getAttachmentPreviewsHeight(mContext, mAttachmentPreviewMode);
- if (mCoordinates.attachmentPreviewsWidth <= 0 || attachmentPreviewsHeight <= 0) {
- LogUtils.w(LOG_TAG,
- "Attachment preview width(%d) or height(%d) is 0 for mode: (%d,%d).",
- mCoordinates.attachmentPreviewsWidth, attachmentPreviewsHeight,
- mCoordinates.getMode(), mAttachmentPreviewMode);
- return;
+ if (!isAttachmentPreviewsEnabled()) {
+ return;
+ }
+ if (mCoordinates.attachmentPreviewsWidth <= 0
+ || mCoordinates.attachmentPreviewsHeight <= 0) {
+ LogUtils.w(LOG_TAG,
+ "Attachment preview width(%d) or height(%d) is 0 for mode: (%d,%d).",
+ mCoordinates.attachmentPreviewsWidth, mCoordinates.attachmentPreviewsHeight,
+ mCoordinates.getMode(), getAttachmentPreviewsMode());
+ return;
+ }
+ Utils.traceBeginSection("attachment previews");
+
+ Utils.traceBeginSection("Setup load attachment previews");
+
+ LogUtils.d(LOG_TAG,
+ "loadAttachmentPreviews: ###############################################");
+ LogUtils.d(LOG_TAG,
+ "loadAttachmentPreviews: Loading attachment previews for conversation %s",
+ mHeader.conversation);
+
+ // Get list of attachments and states from conversation
+ final ArrayList<String> attachmentUris = mHeader.conversation.getAttachmentPreviewUris();
+ final int previewStates = mHeader.conversation.attachmentPreviewStates;
+ final int displayCount = Math.min(attachmentUris.size(), DividedImageCanvas.MAX_DIVISIONS);
+ Utils.traceEndSection();
+
+ final List<AttachmentPreviewIdentifier> ids = Lists.newArrayListWithCapacity(displayCount);
+ final List<Object> keys = Lists.newArrayListWithCapacity(displayCount);
+ // First pass: Create and set the rendition on each load request
+ for (int i = 0; i < displayCount; i++) {
+ Utils.traceBeginSection("finding rendition of attachment preview");
+ final String uri = attachmentUris.get(i);
+
+ // Find the rendition to load based on availability.
+ LogUtils.d(LOG_TAG, "loadAttachmentPreviews: state [BEST, SIMPLE] is [%s, %s] for %s ",
+ Attachment.getPreviewState(previewStates, i, AttachmentRendition.BEST),
+ Attachment.getPreviewState(previewStates, i, AttachmentRendition.SIMPLE),
+ uri);
+ int bestAvailableRendition = -1;
+ // BEST first, else use less preferred renditions
+ for (int rendition : AttachmentRendition.PREFERRED_RENDITIONS) {
+ if (Attachment.getPreviewState(previewStates, i, rendition)) {
+ bestAvailableRendition = rendition;
+ break;
+ }
}
- mAttachmentPreviewsCanvas.setDimensions(mCoordinates.attachmentPreviewsWidth,
- attachmentPreviewsHeight);
- ArrayList<String> attachments = mHeader.conversation.getAttachments();
- mAttachmentPreviewsCanvas.setDivisionIds(attachments);
- int size = attachments.size();
- for (int i = 0; i < DividedImageCanvas.MAX_DIVISIONS && i < size; i++) {
- String attachment = attachments.get(i);
- PhotoIdentifier photoIdentifier = new ContactIdentifier(
- attachment, attachment, i);
- sAttachmentPreviewsManager.loadThumbnail(
- photoIdentifier, mAttachmentPreviewsCanvas);
+
+ final AttachmentPreviewIdentifier photoIdentifier = new AttachmentPreviewIdentifier(uri,
+ bestAvailableRendition, mHeader.conversation.id, i);
+ ids.add(photoIdentifier);
+ keys.add(photoIdentifier.getKey());
+ Utils.traceEndSection();
+ }
+
+ Utils.traceBeginSection("preparing divided image canvas");
+ // Prepare the canvas.
+ mAttachmentPreviewsCanvas.setDimensions(mCoordinates.attachmentPreviewsWidth,
+ mCoordinates.attachmentPreviewsHeight);
+ mAttachmentPreviewsCanvas.setDivisionIds(keys);
+ Utils.traceEndSection();
+
+ // Second pass: Find the dimensions to load and start the load request
+ final ImageCanvas.Dimensions canvasDimens = new ImageCanvas.Dimensions();
+ for (int i = 0; i < displayCount; i++) {
+ final PhotoIdentifier photoIdentifier = ids.get(i);
+ final Object key = keys.get(i);
+ mAttachmentPreviewsCanvas.getDesiredDimensions(key, canvasDimens);
+ if (i < mImagesLoaded.length) {
+ // We want to show default progress image
+ mImagesLoaded[i] = false;
+ if (!mProgressAnimator.isStarted()) {
+ LogUtils.d(LOG_TAG, "progress animator: >> started");
+ mProgressAnimator.setCurrentPlayTime(
+ (long) (sProgressAnimationDuration * mAnimatedProgressFraction));
+ mProgressAnimator.start();
+ }
}
+ LogUtils.d(LOG_TAG, "loadAttachmentPreviews: start loading %s", photoIdentifier);
+ sAttachmentPreviewsManager
+ .loadThumbnail(photoIdentifier, mAttachmentPreviewsCanvas, canvasDimens, this);
}
+ Utils.traceEndSection();
+ }
+
+ @Override
+ public void onImageDrawn(Object key, boolean success) {
+ Utils.traceBeginSection("on image drawn");
+ String uri = AttachmentPreviewsManager.transformKeyToUri(key);
+ int index = mHeader.conversation.getAttachmentPreviewUris().indexOf(uri);
+
+ if (index < 0 || index >= mImagesLoaded.length) {
+ Utils.traceEndSection();
+ return;
+ }
+
+ LogUtils.d(LOG_TAG,
+ "loadAttachmentPreviews: <= onImageDrawn callback [%b] on index %d for %s", success,
+ index, key);
+ // We want to hide the spinning progress bar when we draw something.
+ mImagesLoaded[index] = success;
+
+ if (mProgressAnimator.isStarted() && areAllImagesLoaded()) {
+ LogUtils.d(LOG_TAG, "progress animator: << stopped");
+ mProgressAnimator.cancel();
+ }
+ Utils.traceEndSection();
+ }
+
+ private boolean areAllImagesLoaded() {
+ for (int i = 0; i < mImagesLoaded.length; i++) {
+ if (!mImagesLoaded[i]) {
+ return false;
+ }
+ }
+ return true;
}
private static int makeExactSpecForSize(int size) {
@@ -902,6 +1126,19 @@ public class ConversationItemView extends View implements SwipeableItemView, Tog
mSendersWidth = 0;
}
+ String overflowText = mHeader.overflowText != null ? mHeader.overflowText : "";
+ sPaint.setTextSize(mCoordinates.overflowFontSize);
+ sPaint.setTypeface(mCoordinates.overflowTypeface);
+
+ sPaint.getTextBounds(overflowText, 0, overflowText.length(), sRect);
+
+ final int overflowWidth = (int) sPaint.measureText(overflowText);
+ final int overflowHeight = sRect.height();
+ mOverflowX = mCoordinates.overflowXEnd - mCoordinates.overflowDiameter / 2
+ - overflowWidth / 2;
+ mOverflowY = mCoordinates.overflowYEnd - mCoordinates.overflowDiameter / 2
+ + overflowHeight / 2;
+
pauseTimer(PERF_TAG_CALCULATE_COORDINATES);
}
@@ -1091,12 +1328,14 @@ public class ConversationItemView extends View implements SwipeableItemView, Tog
@Override
protected void onDraw(Canvas canvas) {
Utils.traceBeginSection("CIVC.draw");
+
// Contact photo
if (mGadgetMode == ConversationItemViewCoordinates.GADGET_CONTACT_PHOTO) {
canvas.save();
drawContactImages(canvas);
canvas.restore();
}
+
// Senders.
boolean isUnread = mHeader.unread;
// Old style senders; apply text colors/ sizes/ styling.
@@ -1174,10 +1413,36 @@ public class ConversationItemView extends View implements SwipeableItemView, Tog
}
// Attachment previews
- if (mAttachmentPreviewMode != ConversationItemViewCoordinates.ATTACHMENT_PREVIEW_NONE) {
+ if (isAttachmentPreviewsEnabled()) {
canvas.save();
drawAttachmentPreviews(canvas);
canvas.restore();
+
+ // Overflow badge and count
+ if (getOverflowCountVisible() && areAllImagesLoaded()) {
+ float radius = mCoordinates.overflowDiameter / 2;
+ // todo:markwei get color of overflow badge from channah
+ sPaint.setColor(sOverflowBadgeColor);
+ canvas.drawCircle(mCoordinates.overflowXEnd - radius,
+ mCoordinates.overflowYEnd - radius, radius, sPaint);
+
+ sPaint.setTextSize(mCoordinates.overflowFontSize);
+ sPaint.setTypeface(mCoordinates.overflowTypeface);
+ sPaint.setColor(sOverflowTextColor);
+ drawText(canvas, mHeader.overflowText, mOverflowX, mOverflowY, sPaint);
+ }
+
+ // Progress bar
+ if (mProgressAnimator.isRunning()) {
+ final int count = mImagesLoaded.length;
+ for (int i = 0; i < count; i++) {
+ if (!mImagesLoaded[i]) {
+ canvas.save();
+ drawProgressBar(canvas, i, count);
+ canvas.restore();
+ }
+ }
+ }
}
// right-side edge effect when in tablet conversation mode and the list is not collapsed
@@ -1217,6 +1482,50 @@ public class ConversationItemView extends View implements SwipeableItemView, Tog
mSendersTextView.draw(canvas);
}
+ /**
+ * Draws the specified progress bar on the canvas.
+ * @param canvas The canvas to draw on.
+ * @param index If drawing multiple progress bars, this determines which one we are drawing.
+ * @param total Whether we are drawing multiple progress bars.
+ */
+ private void drawProgressBar(Canvas canvas, int index, int total) {
+ int progressBarX = getProgressBarX(index, total);
+ if (progressBarX == -1) {
+ return;
+ }
+
+ // We want to rotate counter-clockwise, because that's the direction the asset faces
+ canvas.rotate(360 - mAnimatedProgressFraction * 360,
+ progressBarX + mCoordinates.progressBarWidth / 2,
+ mCoordinates.progressBarY + mCoordinates.progressBarHeight / 2);
+
+ canvas.drawBitmap(PROGRESS_BAR, progressBarX, mCoordinates.progressBarY, null);
+ }
+
+ /**
+ * @see com.android.mail.browse.ConversationItemView#drawProgressBar
+ */
+ private void invalidateProgressBar(int index, int total) {
+ int progressBarX = getProgressBarX(index, total);
+ if (progressBarX == -1) {
+ return;
+ }
+
+ invalidate(progressBarX, mCoordinates.progressBarY,
+ progressBarX + mCoordinates.progressBarWidth,
+ mCoordinates.progressBarY + mCoordinates.progressBarHeight);
+ }
+
+ private int getProgressBarX(int index, int total) {
+ if (mCoordinates == null) {
+ return -1;
+ }
+ int sectionWidth = mCoordinates.attachmentPreviewsWidth / total;
+ int sectionOffset = index * sectionWidth;
+ return mCoordinates.attachmentPreviewsX + sectionOffset + sectionWidth / 2
+ - mCoordinates.progressBarWidth / 2;
+ }
+
private Bitmap getStarBitmap() {
return mHeader.conversation.starred ? STAR_ON : STAR_OFF;
}
@@ -1297,16 +1606,49 @@ public class ConversationItemView extends View implements SwipeableItemView, Tog
}
}
- private boolean isTouchInContactPhoto(float x) {
+ public void viewAttachmentPreview(int index) {
+ Uri imageListUri = mHeader.conversation.attachmentPreviewsListUri;
+ LogUtils.d(LOG_TAG,
+ "ConversationItemView: tapped on attachment preview %d, "
+ + "opening photoviewer for image list uri %s",
+ index, imageListUri);
+ MailPhotoViewActivity
+ .startMailPhotoViewActivity(mActivity.getActivityContext(), imageListUri, index);
+ }
+
+ private boolean isTouchInContactPhoto(float x, float y) {
// Everything before the right edge of contact photo
- return (mHeader.gadgetMode == ConversationItemViewCoordinates.GADGET_CONTACT_PHOTO
- && x < (mCoordinates.contactImagesX + mCoordinates.contactImagesWidth
- + sSenderImageTouchSlop));
+ return mHeader.gadgetMode == ConversationItemViewCoordinates.GADGET_CONTACT_PHOTO
+ && x < mCoordinates.contactImagesX + mCoordinates.contactImagesWidth
+ + sSenderImageTouchSlop
+ && (!isAttachmentPreviewsEnabled() || y < mCoordinates.attachmentPreviewsY);
}
private boolean isTouchInStar(float x, float y) {
// Everything after the star and include a touch slop.
- return mStarEnabled && x > mCoordinates.starX - sStarTouchSlop;
+ return mStarEnabled
+ && x > mCoordinates.starX - sStarTouchSlop
+ && (!isAttachmentPreviewsEnabled() || y < mCoordinates.attachmentPreviewsY);
+ }
+
+ /**
+ * If the touch is in the attachment previews, return the index of the attachment under that
+ * point (for multiple previews). Return -1 if the touch is outside of the previews.
+ */
+ private int getAttachmentPreviewsIndexForTouch(float x, float y) {
+ if (!isAttachmentPreviewsEnabled()) {
+ return -1;
+ }
+ if (y > mCoordinates.attachmentPreviewsY
+ && y < mCoordinates.attachmentPreviewsY + mCoordinates.attachmentPreviewsHeight
+ && x > mCoordinates.attachmentPreviewsX
+ && x < mCoordinates.attachmentPreviewsX + mCoordinates.attachmentPreviewsWidth) {
+ int eachWidth = mCoordinates.attachmentPreviewsWidth / mHeader.conversation
+ .getAttachmentPreviewUris().size();
+ int offset = (int) (x - mCoordinates.attachmentPreviewsX);
+ return offset / eachWidth;
+ }
+ return -1;
}
@Override
@@ -1323,6 +1665,7 @@ public class ConversationItemView extends View implements SwipeableItemView, Tog
}
private boolean onTouchEventNoSwipe(MotionEvent event) {
+ Utils.traceBeginSection("on touch event no swipe");
boolean handled = false;
int x = (int) event.getX();
@@ -1331,7 +1674,8 @@ public class ConversationItemView extends View implements SwipeableItemView, Tog
mLastTouchY = y;
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
- if (isTouchInContactPhoto(x) || isTouchInStar(x, y)) {
+ if (isTouchInContactPhoto(x, y) || isTouchInStar(x, y)
+ || getAttachmentPreviewsIndexForTouch(x, y) > -1) {
mDownEvent = true;
handled = true;
}
@@ -1343,12 +1687,16 @@ public class ConversationItemView extends View implements SwipeableItemView, Tog
case MotionEvent.ACTION_UP:
if (mDownEvent) {
- if (isTouchInContactPhoto(x)) {
+ int index;
+ if (isTouchInContactPhoto(x, y)) {
// Touch on the check mark
toggleSelectedState();
} else if (isTouchInStar(x, y)) {
// Touch on the star
toggleStar();
+ } else if ((index = getAttachmentPreviewsIndexForTouch(x, y)) > -1) {
+ // Touch on an attachment preview
+ viewAttachmentPreview(index);
}
handled = true;
}
@@ -1359,6 +1707,7 @@ public class ConversationItemView extends View implements SwipeableItemView, Tog
handled = super.onTouchEvent(event);
}
+ Utils.traceEndSection();
return handled;
}
@@ -1367,31 +1716,45 @@ public class ConversationItemView extends View implements SwipeableItemView, Tog
*/
@Override
public boolean onTouchEvent(MotionEvent event) {
+ Utils.traceBeginSection("on touch event");
int x = (int) event.getX();
int y = (int) event.getY();
mLastTouchX = x;
mLastTouchY = y;
if (!mSwipeEnabled) {
+ Utils.traceEndSection();
return onTouchEventNoSwipe(event);
}
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
- if (isTouchInContactPhoto(x) || isTouchInStar(x, y)) {
+ if (isTouchInContactPhoto(x, y) || isTouchInStar(x, y)
+ || getAttachmentPreviewsIndexForTouch(x, y) > -1) {
mDownEvent = true;
+ Utils.traceEndSection();
return true;
}
break;
case MotionEvent.ACTION_UP:
if (mDownEvent) {
- if (isTouchInContactPhoto(x)) {
+ int index;
+ if (isTouchInContactPhoto(x, y)) {
// Touch on the check mark
+ Utils.traceEndSection();
mDownEvent = false;
toggleSelectedState();
+ Utils.traceEndSection();
return true;
} else if (isTouchInStar(x, y)) {
// Touch on the star
mDownEvent = false;
toggleStar();
+ Utils.traceEndSection();
+ return true;
+ } else if ((index = getAttachmentPreviewsIndexForTouch(x, y)) > -1) {
+ // Touch on an attachment preview
+ mDownEvent = false;
+ viewAttachmentPreview(index);
+ Utils.traceEndSection();
return true;
}
}
@@ -1400,8 +1763,10 @@ public class ConversationItemView extends View implements SwipeableItemView, Tog
// Let View try to handle it as well.
boolean handled = super.onTouchEvent(event);
if (event.getAction() == MotionEvent.ACTION_DOWN) {
+ Utils.traceEndSection();
return true;
}
+ Utils.traceEndSection();
return handled;
}
@@ -1430,9 +1795,16 @@ public class ConversationItemView extends View implements SwipeableItemView, Tog
* can be reused.
*/
public void reset() {
+ Utils.traceBeginSection("reset");
setAlpha(1f);
setTranslationX(0f);
mAnimatedHeightFraction = 1.0f;
+ LogUtils.d(LOG_TAG, "progress animator: cancelling after %dms", sProgressAnimationDuration);
+ if (mProgressAnimator.isStarted()) {
+ LogUtils.d(LOG_TAG, "progress animator: << stopped");
+ mProgressAnimator.cancel();
+ }
+ Utils.traceEndSection();
}
@SuppressWarnings("deprecation")
@@ -1525,6 +1897,44 @@ public class ConversationItemView extends View implements SwipeableItemView, Tog
requestLayout();
}
+ private ObjectAnimator createProgressAnimator() {
+ ObjectAnimator animator = ObjectAnimator.ofFloat(this, "animatedProgressFraction", 0f, 1.0f)
+ .setDuration(sProgressAnimationDuration);
+ animator.setInterpolator(new LinearInterpolator());
+ animator.setRepeatCount(ObjectAnimator.INFINITE);
+ animator.setRepeatMode(ObjectAnimator.RESTART);
+ animator.addListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ invalidateAll();
+ }
+
+ @Override
+ public void onAnimationCancel(Animator animation) {
+ invalidateAll();
+ }
+
+ private void invalidateAll() {
+ int count = mHeader.conversation.getAttachmentPreviewUris().size();
+ for (int i = 0; i < count; i++) {
+ invalidateProgressBar(i, count);
+ }
+ }
+ });
+ return animator;
+ }
+
+ // Used by animator
+ public void setAnimatedProgressFraction(float fraction) {
+ mAnimatedProgressFraction = fraction;
+ final int count = mImagesLoaded.length;
+ for (int i = 0; i < count; i++) {
+ if (!mImagesLoaded[i]) {
+ invalidateProgressBar(i, count);
+ }
+ }
+ }
+
@Override
public SwipeableView getSwipeableView() {
return SwipeableView.from(this);
diff --git a/src/com/android/mail/browse/ConversationItemViewCoordinates.java b/src/com/android/mail/browse/ConversationItemViewCoordinates.java
index 85b016651..34b5596b3 100644
--- a/src/com/android/mail/browse/ConversationItemViewCoordinates.java
+++ b/src/com/android/mail/browse/ConversationItemViewCoordinates.java
@@ -28,9 +28,12 @@ import android.view.View.MeasureSpec;
import android.view.ViewGroup;
import android.view.ViewGroup.LayoutParams;
import android.view.ViewParent;
+import android.widget.FrameLayout;
import android.widget.TextView;
import com.android.mail.R;
+import com.android.mail.R.dimen;
+import com.android.mail.R.id;
import com.android.mail.ui.ViewMode;
import com.android.mail.utils.Utils;
import com.google.common.base.Objects;
@@ -56,10 +59,9 @@ public class ConversationItemViewCoordinates {
static final int GADGET_CHECKBOX = 2;
// Attachment previews modes
- static final int ATTACHMENT_PREVIEW_MODE_COUNT = 3;
static final int ATTACHMENT_PREVIEW_NONE = 0;
- static final int ATTACHMENT_PREVIEW_TALL = 1;
- static final int ATTACHMENT_PREVIEW_SHORT = 2;
+ static final int ATTACHMENT_PREVIEW_UNREAD = 1;
+ static final int ATTACHMENT_PREVIEW_READ = 2;
// For combined views
private static int COLOR_BLOCK_WIDTH = -1;
@@ -167,13 +169,6 @@ public class ConversationItemViewCoordinates {
final int height;
- // Attachments view
- static int sAttachmentPreviewsHeights[];
- static int sAttachmentPreviewsMarginTops[];
- final int attachmentPreviewsX;
- final int attachmentPreviewsY;
- final int attachmentPreviewsWidth;
-
// Checkmark.
final int checkmarkX;
final int checkmarkY;
@@ -181,6 +176,7 @@ public class ConversationItemViewCoordinates {
// Star.
final int starX;
final int starY;
+ final int starWidth;
// Senders.
final int sendersX;
@@ -241,6 +237,24 @@ public class ConversationItemViewCoordinates {
final int contactImagesX;
final int contactImagesY;
+ // Attachment previews
+ final int attachmentPreviewsX;
+ final int attachmentPreviewsY;
+ final int attachmentPreviewsWidth;
+ final int attachmentPreviewsHeight;
+
+ // Attachment previews overflow badge and count
+ final int overflowXEnd;
+ final int overflowYEnd;
+ final int overflowDiameter;
+ final float overflowFontSize;
+ final Typeface overflowTypeface;
+
+ // Attachment previews progress bar
+ final int progressBarY;
+ final int progressBarWidth;
+ final int progressBarHeight;
+
/**
* The smallest item width for which we use the "wide" layout.
*/
@@ -255,6 +269,7 @@ public class ConversationItemViewCoordinates {
private final int mFolderMinimumWidth;
private ConversationItemViewCoordinates(Context context, Config config) {
+ Utils.traceBeginSection("CIV coordinates constructor");
final Resources res = context.getResources();
mFolderCellWidth = res.getDimensionPixelSize(R.dimen.folder_cell_width);
mMinListWidthForWide = res.getDimensionPixelSize(R.dimen.list_min_width_is_wide);
@@ -276,22 +291,27 @@ public class ConversationItemViewCoordinates {
}
final ViewGroup view = (ViewGroup) LayoutInflater.from(context).inflate(layoutId, null);
- final TextView folders = (TextView) view.findViewById(R.id.folders);
- folders.setVisibility(config.areFoldersVisible() ? View.VISIBLE : View.GONE);
-
// Show/hide optional views before measure/layout call
+
View attachmentPreviews = null;
if (config.getAttachmentPreviewMode() != ATTACHMENT_PREVIEW_NONE) {
attachmentPreviews = view.findViewById(R.id.attachment_previews);
- if (attachmentPreviews != null) {
- LayoutParams params = attachmentPreviews.getLayoutParams();
- attachmentPreviews.setVisibility(View.VISIBLE);
- params.height = getAttachmentPreviewsHeight(
- context, config.getAttachmentPreviewMode());
- attachmentPreviews.setLayoutParams(params);
- }
+ LayoutParams params = attachmentPreviews.getLayoutParams();
+ attachmentPreviews.setVisibility(View.VISIBLE);
+ params.height = getAttachmentPreviewsHeight(context, config.getAttachmentPreviewMode());
+ attachmentPreviews.setLayoutParams(params);
}
+ final TextView folders = (TextView) view.findViewById(R.id.folders);
+ folders.setVisibility(config.areFoldersVisible() ? View.VISIBLE : View.GONE);
+
+ // Add margin between attachment previews and folders
+ View attachmentPreviewsBottomMargin = view
+ .findViewById(R.id.attachment_previews_bottom_margin);
+ attachmentPreviewsBottomMargin.setVisibility(
+ attachmentPreviews != null && config.areFoldersVisible() ? View.VISIBLE
+ : View.GONE);
+
View contactImagesView = view.findViewById(R.id.contact_image);
View checkmark = view.findViewById(R.id.checkmark);
@@ -351,6 +371,7 @@ public class ConversationItemViewCoordinates {
final View star = view.findViewById(R.id.star);
starX = getX(star);
starY = getY(star);
+ starWidth = star.getWidth();
final TextView senders = (TextView) view.findViewById(R.id.senders);
final int sendersTopAdjust = getLatinTopAdjustment(senders);
@@ -440,16 +461,50 @@ public class ConversationItemViewCoordinates {
paperclipPaddingLeft = paperclip.getPaddingLeft();
if (attachmentPreviews != null) {
- attachmentPreviewsX = getX(attachmentPreviews);
- attachmentPreviewsY = getY(attachmentPreviews);
- attachmentPreviewsWidth = attachmentPreviews.getWidth();
+ attachmentPreviewsX = getAttachmentPreviewsX(attachmentPreviews,
+ config.mAttachmentPreviewMode);
+ attachmentPreviewsY = getY(attachmentPreviews) + sendersTopAdjust;
+ final int attachmentPreviewsXEnd;
+ if (isWide()) {
+ attachmentPreviewsXEnd = subjectX + subjectWidth;
+ } else {
+ attachmentPreviewsXEnd = starX + starWidth;
+ }
+
+ attachmentPreviewsWidth = attachmentPreviewsXEnd - attachmentPreviewsX;
+ attachmentPreviewsHeight = attachmentPreviews.getHeight();
+
+ // We only care about the right and bottom of the overflow count
+ final TextView overflow = (TextView) view.findViewById(id.ap_overflow);
+ FrameLayout.LayoutParams params = (FrameLayout.LayoutParams) overflow.getLayoutParams();
+ overflowXEnd = attachmentPreviewsX + attachmentPreviewsWidth - params.rightMargin;
+ overflowYEnd = attachmentPreviewsY + attachmentPreviewsHeight - params.bottomMargin;
+ overflowDiameter = overflow.getWidth();
+ overflowFontSize = overflow.getTextSize();
+ overflowTypeface = overflow.getTypeface();
+
+ final View progressBar = view.findViewById(id.ap_progress_bar);
+ progressBarWidth = progressBar.getWidth();
+ progressBarHeight = progressBar.getHeight();
+ progressBarY = attachmentPreviewsY + attachmentPreviewsHeight / 2
+ - progressBarHeight / 2;
} else {
attachmentPreviewsX = 0;
attachmentPreviewsY = 0;
attachmentPreviewsWidth = 0;
+ attachmentPreviewsHeight = 0;
+ overflowXEnd = 0;
+ overflowYEnd = 0;
+ overflowDiameter = 0;
+ overflowFontSize = 0;
+ overflowTypeface = null;
+ progressBarY = 0;
+ progressBarWidth = 0;
+ progressBarHeight = 0;
}
height = view.getHeight() + (isWide() ? 0 : sendersTopAdjust);
+ Utils.traceEndSection();
}
public int getMode() {
@@ -490,48 +545,24 @@ public class ConversationItemViewCoordinates {
}
}
- /**
- * Returns a value array multiplied by the specified density.
- */
- public static int[] getDensityDependentArray(int[] values, float density) {
- int result[] = new int[values.length];
- for (int i = 0; i < values.length; ++i) {
- result[i] = (int) (values[i] * density);
+ private int getAttachmentPreviewsX(View attachmentPreviews, int attachmentPreviewMode) {
+ if (isWide() || attachmentPreviewMode == ATTACHMENT_PREVIEW_READ) {
+ return subjectX;
}
- return result;
+ return getX(attachmentPreviews);
}
- /**
- * Refreshes the conversation heights array.
- */
- @Deprecated
- // TODO: heights are now dynamic and should be members of this class. the fixed attachment
- // heights can still be stored in a dimensional array, but should only be used as input into
- // forConfig's measure/layout
- public static void refreshConversationDimens(Context context) {
+ private int getAttachmentPreviewsHeight(Context context, int attachmentPreviewMode) {
Resources res = context.getResources();
-
- // Attachment previews height
- sAttachmentPreviewsHeights = new int[ATTACHMENT_PREVIEW_MODE_COUNT];
- sAttachmentPreviewsHeights[ATTACHMENT_PREVIEW_TALL] = 0;
- sAttachmentPreviewsHeights[ATTACHMENT_PREVIEW_TALL] = (int) res.getDimension(
- R.dimen.attachment_preview_height_tall);
- sAttachmentPreviewsHeights[ATTACHMENT_PREVIEW_SHORT] = (int) res.getDimension(
- R.dimen.attachment_preview_height_short);
-
- // Attachment previews margin top
- sAttachmentPreviewsMarginTops = new int[MODE_COUNT];
- sAttachmentPreviewsMarginTops[NORMAL_MODE] = (int) res.getDimension(
- R.dimen.attachment_preview_margin_top);
- sAttachmentPreviewsMarginTops[WIDE_MODE] = (int) res.getDimension(
- R.dimen.attachment_preview_margin_top_wide);
- }
-
- public static int getAttachmentPreviewsHeight(Context context, int attachmentPreviewMode) {
- if (sAttachmentPreviewsHeights == null) {
- refreshConversationDimens(context);
+ switch (attachmentPreviewMode) {
+ case ATTACHMENT_PREVIEW_UNREAD:
+ return (int) (isWide() ? res.getDimension(dimen.attachment_preview_height_tall_wide)
+ : res.getDimension(dimen.attachment_preview_height_tall));
+ case ATTACHMENT_PREVIEW_READ:
+ return (int) res.getDimension(dimen.attachment_preview_height_short);
+ default:
+ return 0;
}
- return sAttachmentPreviewsHeights[attachmentPreviewMode];
}
/**
diff --git a/src/com/android/mail/browse/ConversationItemViewModel.java b/src/com/android/mail/browse/ConversationItemViewModel.java
index 303ec5902..ad6c0d857 100644
--- a/src/com/android/mail/browse/ConversationItemViewModel.java
+++ b/src/com/android/mail/browse/ConversationItemViewModel.java
@@ -86,6 +86,9 @@ public class ConversationItemViewModel {
boolean hasDraftMessage;
+ // Attachment Previews overflow
+ String overflowText;
+
// View Width
public int viewWidth;
diff --git a/src/com/android/mail/browse/MessageAttachmentTile.java b/src/com/android/mail/browse/MessageAttachmentTile.java
index ece218353..693df5b29 100644
--- a/src/com/android/mail/browse/MessageAttachmentTile.java
+++ b/src/com/android/mail/browse/MessageAttachmentTile.java
@@ -154,14 +154,8 @@ public class MessageAttachmentTile extends AttachmentTile implements OnClickList
@Override
public void viewAttachment() {
if (ImageUtils.isImageMimeType(Utils.normalizeMimeType(mAttachment.getContentType()))) {
- final PhotoViewIntentBuilder builder =
- Intents.newPhotoViewIntentBuilder(getContext(), MailPhotoViewActivity.class);
- builder
- .setPhotosUri(mAttachmentsListUri.toString())
- .setProjection(UIProvider.ATTACHMENT_PROJECTION)
- .setPhotoIndex(mPhotoIndex);
-
- getContext().startActivity(builder.build());
+ MailPhotoViewActivity
+ .startMailPhotoViewActivity(getContext(), mAttachmentsListUri, mPhotoIndex);
return;
}
@@ -213,7 +207,7 @@ public class MessageAttachmentTile extends AttachmentTile implements OnClickList
/**
* Given two child views, figure out whose index is closest to the specified
- * index. (generated by markwei)
+ * index.
*/
public static class ViewIndexDistanceComparator implements Comparator<View>{
final private int mIndex;
diff --git a/src/com/android/mail/photo/MailPhotoViewActivity.java b/src/com/android/mail/photo/MailPhotoViewActivity.java
index eb2b4b45e..3bfd3d73f 100644
--- a/src/com/android/mail/photo/MailPhotoViewActivity.java
+++ b/src/com/android/mail/photo/MailPhotoViewActivity.java
@@ -18,7 +18,9 @@
package com.android.mail.photo;
import android.app.ActionBar;
+import android.content.Context;
import android.database.Cursor;
+import android.net.Uri;
import android.os.Bundle;
import android.os.Parcelable;
import android.support.v4.app.Fragment;
@@ -30,12 +32,14 @@ import android.view.Window;
import android.widget.ImageView;
import android.widget.TextView;
+import com.android.ex.photo.Intents;
import com.android.ex.photo.PhotoViewActivity;
import com.android.ex.photo.fragments.PhotoViewFragment;
import com.android.ex.photo.views.ProgressBarWrapper;
import com.android.mail.R;
import com.android.mail.browse.AttachmentActionHandler;
import com.android.mail.providers.Attachment;
+import com.android.mail.providers.UIProvider;
import com.android.mail.providers.UIProvider.AttachmentDestination;
import com.android.mail.providers.UIProvider.AttachmentState;
import com.android.mail.utils.AttachmentUtils;
@@ -57,6 +61,25 @@ public class MailPhotoViewActivity extends PhotoViewActivity {
private AttachmentActionHandler mActionHandler;
private Menu mMenu;
+ /**
+ * Start a new MailPhotoViewActivity to view the given images.
+ * @param imageListUri The uri to query for the images that you want to view. The resulting
+ * cursor must have the columns as defined in
+ * {@link com.android.ex.photo.provider.PhotoContract.PhotoViewColumns}
+ * @param photoIndex The index of the photo to show first
+ */
+ public static void startMailPhotoViewActivity(Context context, Uri imageListUri,
+ int photoIndex) {
+ final Intents.PhotoViewIntentBuilder builder =
+ Intents.newPhotoViewIntentBuilder(context, MailPhotoViewActivity.class);
+ builder
+ .setPhotosUri(imageListUri.toString())
+ .setProjection(UIProvider.ATTACHMENT_PROJECTION)
+ .setPhotoIndex(photoIndex);
+
+ context.startActivity(builder.build());
+ }
+
@Override
protected void onCreate(Bundle savedInstanceState) {
requestWindowFeature(Window.FEATURE_PROGRESS);
diff --git a/src/com/android/mail/photomanager/AttachmentPreviewsManager.java b/src/com/android/mail/photomanager/AttachmentPreviewsManager.java
new file mode 100644
index 000000000..dc584f34e
--- /dev/null
+++ b/src/com/android/mail/photomanager/AttachmentPreviewsManager.java
@@ -0,0 +1,352 @@
+package com.android.mail.photomanager;
+
+import com.android.mail.photomanager.BitmapUtil.InputStreamFactory;
+import com.android.mail.providers.Attachment;
+import com.android.mail.providers.UIProvider;
+import com.android.mail.providers.UIProvider.AttachmentRendition;
+import com.android.mail.ui.DividedImageCanvas;
+import com.android.mail.ui.ImageCanvas.Dimensions;
+import com.android.mail.utils.Utils;
+import com.google.common.base.Objects;
+
+import android.content.ContentResolver;
+import android.content.Context;
+import android.database.Cursor;
+import android.graphics.Bitmap;
+import android.net.Uri;
+import android.text.TextUtils;
+import android.util.Pair;
+
+import com.android.mail.ui.ImageCanvas;
+import com.android.mail.utils.LogUtils;
+import com.google.common.primitives.Longs;
+
+import java.io.FileNotFoundException;
+import java.io.InputStream;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Asynchronously loads attachment image previews and maintains a cache of
+ * photos.
+ */
+public class AttachmentPreviewsManager extends PhotoManager {
+
+ private static final DefaultImageProvider sDefaultImageProvider
+ = new AttachmentPreviewsDefaultProvider();
+ private final Map<Object, AttachmentPreviewsManagerCallback> mCallbacks;
+
+ public static int generateHash(ImageCanvas view, Object key) {
+ return Objects.hashCode(view, key);
+ }
+
+ public static String transformKeyToUri(Object key) {
+ return (String) ((Pair)key).second;
+ }
+
+ public AttachmentPreviewsManager(Context context) {
+ super(context);
+ mCallbacks = new HashMap<Object, AttachmentPreviewsManagerCallback>();
+ }
+
+ public void loadThumbnail(PhotoIdentifier id, ImageCanvas view, Dimensions dimensions,
+ AttachmentPreviewsManagerCallback callback) {
+ mCallbacks.put(id.getKey(), callback);
+ super.loadThumbnail(id, view, dimensions);
+ }
+
+ @Override
+ protected DefaultImageProvider getDefaultImageProvider() {
+ return sDefaultImageProvider;
+ }
+
+ @Override
+ protected int getHash(PhotoIdentifier id, ImageCanvas view) {
+ return generateHash(view, id.getKey());
+ }
+
+ @Override
+ protected PhotoLoaderThread getLoaderThread(ContentResolver contentResolver) {
+ return new AttachmentPreviewsLoaderThread(contentResolver);
+ }
+
+ @Override
+ protected void onImageDrawn(Request request, boolean success) {
+ Object key = request.getKey();
+ if (mCallbacks.containsKey(key)) {
+ AttachmentPreviewsManagerCallback callback = mCallbacks.get(key);
+ callback.onImageDrawn(request.getKey(), success);
+
+ if (success) {
+ mCallbacks.remove(key);
+ }
+ }
+ }
+
+ @Override
+ protected boolean isSizeCompatible(int prevWidth, int prevHeight, int newWidth, int newHeight) {
+ float ratio = (float) newWidth / prevWidth;
+ boolean previousRequestSmaller = newWidth > prevWidth
+ || newWidth > prevWidth * ratio
+ || newHeight > prevHeight * ratio;
+ return !previousRequestSmaller;
+ }
+
+ public static class AttachmentPreviewIdentifier extends PhotoIdentifier {
+ public final String uri;
+ public final int rendition;
+ // conversationId and index used for sorting requests
+ long conversationId;
+ public int index;
+
+ /**
+ * <RENDITION, URI>
+ */
+ private Pair<Integer, String> mKey;
+
+ public AttachmentPreviewIdentifier(String uri, int rendition, long conversationId,
+ int index) {
+ this.uri = uri;
+ this.rendition = rendition;
+ this.conversationId = conversationId;
+ this.index = index;
+ mKey = new Pair<Integer, String>(rendition, uri) {
+ @Override
+ public String toString() {
+ return "<" + first + ", " + second + ">";
+ }
+ };
+ }
+
+ @Override
+ public boolean isValid() {
+ return !TextUtils.isEmpty(uri) && rendition >= AttachmentRendition.SIMPLE;
+ }
+
+ @Override
+ public Object getKey() {
+ return mKey;
+ }
+
+ @Override
+ public Object getKeyToShowInsteadOfDefault() {
+ return new AttachmentPreviewIdentifier(uri, rendition - 1, conversationId, index)
+ .getKey();
+ }
+
+ @Override
+ public int hashCode() {
+ int hash = 17;
+ hash = 31 * hash + (uri != null ? uri.hashCode() : 0);
+ hash = 31 * hash + rendition;
+ hash = 31 * hash + Longs.hashCode(conversationId);
+ hash = 31 * hash + index;
+ return hash;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+
+ AttachmentPreviewIdentifier that = (AttachmentPreviewIdentifier) o;
+
+ if (rendition != that.rendition) {
+ return false;
+ }
+ if (uri != null ? !uri.equals(that.uri) : that.uri != null) {
+ return false;
+ }
+ if (conversationId != that.conversationId) {
+ return false;
+ }
+ if (index != that.index) {
+ return false;
+ }
+
+ return true;
+ }
+
+ @Override
+ public String toString() {
+ return mKey.toString();
+ }
+
+ @Override
+ public int compareTo(PhotoIdentifier another) {
+ if (another instanceof AttachmentPreviewIdentifier) {
+ AttachmentPreviewIdentifier anotherId = (AttachmentPreviewIdentifier) another;
+ // We want to load SIMPLE images first because they are super fast
+ if (rendition - anotherId.rendition != 0) {
+ return rendition - anotherId.rendition;
+ }
+
+ // Load images from later messages first (later messages appear on top of the list)
+ if (anotherId.conversationId - conversationId != 0) {
+ return (anotherId.conversationId - conversationId) > 0 ? 1 : -1;
+ }
+
+ // Load images from left to right
+ if (index - anotherId.index != 0) {
+ return index - anotherId.index;
+ }
+
+ return 0;
+ } else {
+ return -1;
+ }
+ }
+ }
+
+ protected class AttachmentPreviewsLoaderThread extends PhotoLoaderThread {
+
+ public AttachmentPreviewsLoaderThread(ContentResolver resolver) {
+ super(resolver);
+ }
+
+ @Override
+ protected int getMaxBatchCount() {
+ return 1;
+ }
+
+ @Override
+ protected Map<String, BitmapHolder> loadPhotos(Collection<Request> requests) {
+ final Map<String, BitmapHolder> photos = new HashMap<String, BitmapHolder>(
+ requests.size());
+
+ LogUtils.d(TAG, "AttachmentPreviewsManager: starting batch load. Count: %d",
+ requests.size());
+ for (final Request request : requests) {
+ Utils.traceBeginSection("Setup load photo");
+ final AttachmentPreviewIdentifier id = (AttachmentPreviewIdentifier) request
+ .getPhotoIdentifier();
+ final Uri uri = Uri.parse(id.uri);
+ // Get the attachment for this preview
+ final Cursor cursor = getResolver()
+ .query(uri, UIProvider.ATTACHMENT_PROJECTION, null, null, null);
+ if (cursor == null) {
+ Utils.traceEndSection();
+ continue;
+ }
+ Attachment attachment = null;
+ try {
+ LogUtils.d(TAG, "AttachmentPreviewsManager: found %d attachments for uri %s",
+ cursor.getCount(), uri);
+ if (cursor.moveToFirst()) {
+ attachment = new Attachment(cursor);
+ }
+ } finally {
+ cursor.close();
+ }
+
+ if (attachment == null) {
+ LogUtils.d(TAG, "AttachmentPreviewsManager: attachment not found for uri %s",
+ uri);
+ Utils.traceEndSection();
+ continue;
+ }
+
+ // Determine whether we load the SIMPLE or BEST image for this preview
+ final Uri contentUri;
+ if (id.rendition == UIProvider.AttachmentRendition.BEST) {
+ contentUri = attachment.contentUri;
+ } else if (id.rendition == AttachmentRendition.SIMPLE) {
+ contentUri = attachment.thumbnailUri;
+ } else {
+ LogUtils.d(TAG,
+ "AttachmentPreviewsManager: Cannot load rendition %d for uri %s",
+ id.rendition, uri);
+ Utils.traceEndSection();
+ continue;
+ }
+
+ LogUtils.d(TAG, "AttachmentPreviewsManager: attachments has contentUri %s",
+ contentUri);
+ final InputStreamFactory factory = new InputStreamFactory() {
+ @Override
+ public InputStream newInputStream() {
+ try {
+ return getResolver().openInputStream(contentUri);
+ } catch (FileNotFoundException e) {
+ LogUtils.e(TAG,
+ "AttachmentPreviewsManager: file not found for attachment %s."
+ + " This may be due to the attachment not being "
+ + "downloaded yet. But this shouldn't happen because "
+ + "we check the state of the attachment downloads "
+ + "before attempting to load it.",
+ contentUri);
+ return null;
+ }
+ }
+ };
+ Utils.traceEndSection();
+
+ Utils.traceBeginSection("Decode stream and crop");
+ // todo:markwei read EXIF data for orientation
+ // Crop it. I've seen that in real-world situations,
+ // a 5.5MB image will be cropped down to about a 200KB image,
+ // so this is definitely worth it.
+ final Bitmap bitmap = BitmapUtil
+ .decodeStreamWithCenterCrop(factory, request.bitmapKey.w,
+ request.bitmapKey.h);
+ Utils.traceEndSection();
+
+ if (bitmap == null) {
+ LogUtils.w(TAG, "Unable to decode bitmap for contentUri %s", contentUri);
+ continue;
+ }
+ cacheBitmap(request.bitmapKey, bitmap);
+ LogUtils.d(TAG,
+ "AttachmentPreviewsManager: finished loading attachment cropped size %db",
+ bitmap.getByteCount());
+ }
+
+ return photos;
+ }
+ }
+
+ public static class AttachmentPreviewsDividedImageCanvas extends DividedImageCanvas {
+ public AttachmentPreviewsDividedImageCanvas(Context context, InvalidateCallback callback) {
+ super(context, callback);
+ }
+
+ @Override
+ protected void drawVerticalDivider(int width, int height) {
+ return; // do not draw vertical dividers
+ }
+
+ @Override
+ protected boolean isPartialBitmapComplete() {
+ return true; // images may not be loaded at the same time
+ }
+
+ @Override
+ protected String transformKeyToDivisionId(Object key) {
+ return transformKeyToUri(key);
+ }
+ }
+
+ public static class AttachmentPreviewsDefaultProvider implements DefaultImageProvider {
+
+ /**
+ * All we need to do is clear the section. The ConversationItemView will draw the
+ * progress bar.
+ */
+ @Override
+ public void applyDefaultImage(PhotoIdentifier id, ImageCanvas view, int extent) {
+ AttachmentPreviewsDividedImageCanvas dividedImageCanvas
+ = (AttachmentPreviewsDividedImageCanvas) view;
+ dividedImageCanvas.clearDivisionImage(id.getKey());
+ }
+ }
+
+ public interface AttachmentPreviewsManagerCallback {
+
+ public void onImageDrawn(Object key, boolean success);
+ }
+}
diff --git a/src/com/android/mail/photomanager/BitmapUtil.java b/src/com/android/mail/photomanager/BitmapUtil.java
index 5b013ca8f..611174964 100644
--- a/src/com/android/mail/photomanager/BitmapUtil.java
+++ b/src/com/android/mail/photomanager/BitmapUtil.java
@@ -21,6 +21,8 @@ import android.graphics.Matrix;
import com.android.mail.utils.LogUtils;
+import java.io.InputStream;
+
/**
* Provides static functions to decode bitmaps at the optimal size
*/
@@ -97,15 +99,15 @@ public class BitmapUtil {
}
/**
- * Decode an image into a Bitmap, using sub-sampling if the desired dimensions call for it.
- * Also applies a center-crop a la {@link android.widget.ImageView.ScaleType#CENTER_CROP}.
+ * Decode an image into a Bitmap, using sub-sampling if the hinted dimensions call for it.
+ * Does not crop to fit the hinted dimensions.
*
* @param src an encoded image
- * @param w desired width in px
- * @param h desired height in px
- * @return an exactly-sized decoded Bitmap that is center-cropped.
+ * @param w hint width in px
+ * @param h hint height in px
+ * @return a decoded Bitmap that is not exactly sized to the hinted dimensions.
*/
- public static Bitmap decodeByteArrayWithCenterCrop(byte[] src, int w, int h) {
+ public static Bitmap decodeByteArray(byte[] src, int w, int h) {
try {
// calculate sample size based on w/h
final BitmapFactory.Options opts = new BitmapFactory.Options();
@@ -116,10 +118,44 @@ public class BitmapUtil {
}
opts.inSampleSize = Math.min(opts.outWidth / w, opts.outHeight / h);
opts.inJustDecodeBounds = false;
- final Bitmap decoded = BitmapFactory.decodeByteArray(src, 0, src.length, opts);
+ return BitmapFactory.decodeByteArray(src, 0, src.length, opts);
+ } catch (Throwable t) {
+ LogUtils.w(PhotoManager.TAG, t, "unable to decode image");
+ return null;
+ }
+ }
+ /**
+ * Decode an input stream into a Bitmap, using sub-sampling if the hinted dimensions call for
+ * it. Does not crop to fit the hinted dimensions.
+ *
+ * @param factory a factory to retrieve fresh input streams from.
+ * @param w hint width in px
+ * @param h hint height in px
+ * @return a decoded Bitmap that is not exactly sized to the hinted dimensions.
+ */
+ public static Bitmap decodeStream(InputStreamFactory factory, int w, int h) {
+ try {
+ // calculate sample size based on w/h
+ final BitmapFactory.Options opts = new BitmapFactory.Options();
+ opts.inJustDecodeBounds = true;
+ InputStream src = factory.newInputStream();
+ BitmapFactory.decodeStream(src, null, opts);
+ if (src != null) {
+ src.close();
+ }
- return centerCrop(decoded, w, h);
+ if (opts.mCancel || opts.outWidth == -1 || opts.outHeight == -1) {
+ return null;
+ }
+ opts.inSampleSize = Math.min(opts.outWidth / w, opts.outHeight / h);
+ opts.inJustDecodeBounds = false;
+ src = factory.newInputStream();
+ Bitmap bitmap = BitmapFactory.decodeStream(src, null, opts);
+ if (src != null) {
+ src.close();
+ }
+ return bitmap;
} catch (Throwable t) {
LogUtils.w(PhotoManager.TAG, t, "unable to decode image");
return null;
@@ -127,6 +163,51 @@ public class BitmapUtil {
}
/**
+ * Decode an image into a Bitmap, using sub-sampling if the desired dimensions call for it.
+ * Also applies a center-crop a la {@link android.widget.ImageView.ScaleType#CENTER_CROP}.
+ *
+ * @param src an encoded image
+ * @param w desired width in px
+ * @param h desired height in px
+ * @return an exactly-sized decoded Bitmap that is center-cropped.
+ */
+ public static Bitmap decodeByteArrayWithCenterCrop(byte[] src, int w, int h) {
+ try {
+ final Bitmap decoded = decodeByteArray(src, w, h);
+ return centerCrop(decoded, w, h);
+
+ } catch (Throwable t) {
+ LogUtils.w(PhotoManager.TAG, t, "unable to crop image");
+ return null;
+ }
+ }
+
+ /**
+ * Decode an input stream into a Bitmap, using sub-sampling if the desired dimensions call
+ * for it. Also applies a center-crop a la {@link android.widget.ImageView
+ * .ScaleType#CENTER_CROP}.
+ *
+ * @param factory a factory to retrieve fresh input streams from.
+ * @param w desired width in px
+ * @param h desired height in px
+ * @return an exactly-sized decoded Bitmap that is center-cropped.
+ */
+ public static Bitmap decodeStreamWithCenterCrop(InputStreamFactory factory, int w, int h) {
+ try {
+ final Bitmap decoded = decodeStream(factory, w, h);
+ //todo:markwei don't always CENTER_CROP
+ final Bitmap cropped = centerCrop(decoded, w, h);
+ LogUtils.d(PhotoManager.TAG, "Full decoded bitmap size %d bytes, cropped size %d bytes",
+ decoded.getByteCount(), cropped.getByteCount());
+ return cropped;
+
+ } catch (Throwable t) {
+ LogUtils.w(PhotoManager.TAG, t, "unable to crop image");
+ return null;
+ }
+ }
+
+ /**
* Returns a new Bitmap copy with a center-crop effect a la
* {@link android.widget.ImageView.ScaleType#CENTER_CROP}. May return the input bitmap if no
* scaling is necessary.
@@ -173,4 +254,8 @@ public class BitmapUtil {
return cropped;
}
+
+ public interface InputStreamFactory {
+ InputStream newInputStream();
+ }
}
diff --git a/src/com/android/mail/photomanager/ContactPhotoManager.java b/src/com/android/mail/photomanager/ContactPhotoManager.java
index 1b6eb44b8..c612f31bd 100644
--- a/src/com/android/mail/photomanager/ContactPhotoManager.java
+++ b/src/com/android/mail/photomanager/ContactPhotoManager.java
@@ -49,32 +49,9 @@ public class ContactPhotoManager extends PhotoManager {
/** Cache size for {@link #mPhotoIdCache}. Starting with 500 entries. */
private static final int PHOTO_ID_CACHE_SIZE = 500;
- private ContactPhotoManager(Context context) {
- super(context);
- mPhotoIdCache = new LruCache<String, Long>(PHOTO_ID_CACHE_SIZE);
- mLetterTileProvider = new LetterTileProvider(context);
- }
-
- @Override
- public DefaultImageProvider getDefaultImageProvider() {
- return mLetterTileProvider;
- }
-
- @Override
- public long getHash(PhotoIdentifier id, ImageCanvas view) {
- final ContactIdentifier contact = (ContactIdentifier) id;
- return Objects.hashCode(view, contact.pos, contact.emailAddress);
- }
-
- @Override
- public PhotoLoaderThread getLoaderThread(ContentResolver contentResolver) {
- return new ContactPhotoLoaderThread(contentResolver);
- }
-
/**
- * Requests the singleton instance of {@link AccountTypeManager} with data
- * bound from the available authenticators. This method can safely be called
- * from the UI thread.
+ * 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();
@@ -91,13 +68,39 @@ public class ContactPhotoManager extends PhotoManager {
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 implements PhotoIdentifier {
+ public static class ContactIdentifier extends PhotoIdentifier {
public final String name;
public final String emailAddress;
public final int pos;
@@ -114,7 +117,7 @@ public class ContactPhotoManager extends PhotoManager {
}
@Override
- public String getKey() {
+ public Object getKey() {
return emailAddress;
}
@@ -153,6 +156,11 @@ public class ContactPhotoManager extends PhotoManager {
sb.append("}");
return sb.toString();
}
+
+ @Override
+ public int compareTo(PhotoIdentifier another) {
+ return 0;
+ }
}
public class ContactPhotoLoaderThread extends PhotoLoaderThread {
@@ -161,8 +169,8 @@ public class ContactPhotoManager extends PhotoManager {
}
@Override
- protected Map<String, byte[]> loadPhotos(Collection<Request> requests) {
- Map<String, byte[]> photos = new HashMap<String, byte[]>(requests.size());
+ 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>();
@@ -171,7 +179,7 @@ public class ContactPhotoManager extends PhotoManager {
Long match;
String emailAddress;
for (Request request : requests) {
- emailAddress = request.getKey();
+ emailAddress = (String) request.getKey();
match = mPhotoIdCache.get(emailAddress);
if (match != null) {
photoIds.add(match);
@@ -193,7 +201,8 @@ public class ContactPhotoManager extends PhotoManager {
if (emailAddressToContactInfoMap != null) {
for (final String address : addresses) {
final ContactInfo info = emailAddressToContactInfoMap.get(address);
- photos.put(address, info != null ? info.photoBytes : null);
+ 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
diff --git a/src/com/android/mail/photomanager/LetterTileProvider.java b/src/com/android/mail/photomanager/LetterTileProvider.java
index 39730b5eb..bc282230e 100644
--- a/src/com/android/mail/photomanager/LetterTileProvider.java
+++ b/src/com/android/mail/photomanager/LetterTileProvider.java
@@ -95,7 +95,7 @@ public class LetterTileProvider implements DefaultImageProvider {
DividedImageCanvas dividedImageView = (DividedImageCanvas) view;
final String displayName = contactIdentifier.name;
- final String address = contactIdentifier.emailAddress;
+ final String address = (String) contactIdentifier.getKey();
// don't apply again if existing letter is there (and valid)
if (dividedImageView.hasImageFor(address)) {
diff --git a/src/com/android/mail/photomanager/PhotoManager.java b/src/com/android/mail/photomanager/PhotoManager.java
index 46502061f..57d2fa136 100644
--- a/src/com/android/mail/photomanager/PhotoManager.java
+++ b/src/com/android/mail/photomanager/PhotoManager.java
@@ -30,8 +30,8 @@ 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.ImmutableList;
import com.google.common.collect.Lists;
import java.util.Collection;
@@ -40,6 +40,7 @@ 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;
/**
@@ -50,17 +51,44 @@ public abstract class PhotoManager implements ComponentCallbacks2, Callback {
* Get the default image provider that draws while the photo is being
* loaded.
*/
- public abstract DefaultImageProvider getDefaultImageProvider();
+ protected abstract DefaultImageProvider getDefaultImageProvider();
/**
* Generate a hashcode unique to each request.
*/
- public abstract long getHash(PhotoIdentifier id, ImageCanvas view);
+ protected abstract int getHash(PhotoIdentifier id, ImageCanvas view);
/**
* Return a specific implementation of PhotoLoaderThread.
*/
- public abstract PhotoLoaderThread getLoaderThread(ContentResolver contentResolver);
+ protected abstract PhotoLoaderThread getLoaderThread(ContentResolver contentResolver);
+
+ /**
+ * Subclasses can implement this method to alert callbacks of the images' loading progress.
+ * @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(Request request, boolean success) {
+ // 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
@@ -93,13 +121,17 @@ public abstract class PhotoManager implements ComponentCallbacks2, Callback {
/**
* Maintains the state of a particular photo.
*/
- private static class BitmapHolder {
+ protected static class BitmapHolder {
byte[] bytes;
+ int width;
+ int height;
volatile boolean fresh;
- public BitmapHolder(byte[] bytes) {
+ public BitmapHolder(byte[] bytes, int width, int height) {
this.bytes = bytes;
+ this.width = width;
+ this.height = height;
this.fresh = true;
}
@@ -111,6 +143,10 @@ public abstract class PhotoManager implements ComponentCallbacks2, Callback {
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("}");
@@ -137,7 +173,7 @@ public abstract class PhotoManager implements ComponentCallbacks2, Callback {
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 = 36864 * 48; // 1728K
+ 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();
@@ -149,25 +185,25 @@ public abstract class PhotoManager implements ComponentCallbacks2, Callback {
final float cacheSizeAdjustment =
(MemoryUtils.getTotalMemorySize() >= MemoryUtils.LARGE_RAM_THRESHOLD) ?
1.0f : 0.5f;
- 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();
+ 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, BitmapIdentifier key, Bitmap oldValue, Bitmap newValue) {
+ boolean evicted, Object key, BitmapHolder oldValue, BitmapHolder newValue) {
if (DEBUG) dumpStats();
}
};
- 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;
+ 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, Object key, BitmapHolder oldValue, BitmapHolder newValue) {
+ boolean evicted, BitmapIdentifier key, Bitmap oldValue, Bitmap newValue) {
if (DEBUG) dumpStats();
}
};
@@ -183,8 +219,8 @@ public abstract class PhotoManager implements ComponentCallbacks2, Callback {
* encapsulated in a request. The request may swapped out before the photo
* loading request is started.
*/
- private final Map<Long, Request> mPendingRequests = Collections.synchronizedMap(
- new HashMap<Long, Request>());
+ private final Map<Integer, Request> mPendingRequests = Collections.synchronizedMap(
+ new HashMap<Integer, Request>());
/**
* Handler for messages sent to the UI thread.
@@ -214,31 +250,53 @@ public abstract class PhotoManager implements ComponentCallbacks2, Callback {
}
public void loadThumbnail(PhotoIdentifier id, ImageCanvas view) {
- long hashCode = getHash(id, view);
+ loadThumbnail(id, view, null);
+ }
+
+ /**
+ * Load an image
+ *
+ * @param dimensions Preferred dimensions
+ */
+ public void loadThumbnail(PhotoIdentifier id, ImageCanvas view,
+ ImageCanvas.Dimensions dimensions) {
+ Utils.traceBeginSection("Load thumbnail");
DefaultImageProvider defaultProvider = getDefaultImageProvider();
+ Request request = new Request(id, defaultProvider, view, dimensions);
+ int hashCode = request.hashCode();
+
if (!id.isValid()) {
// No photo is needed
- defaultProvider.applyDefaultImage(id, view, -1);
+ 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.create(id, defaultProvider, view));
+ if (DEBUG) LogUtils.v(TAG, "loadPhoto request: %s", id.getKey());
+ loadPhoto(hashCode, request);
}
+ Utils.traceEndSection();
}
- private void loadPhoto(Long hashCode, Request request) {
- if (DEBUG) LogUtils.v(TAG, "NEW IMAGE REQUEST key=%s r=%s thread=%s",
- request.getKey(),
- request,
- Thread.currentThread());
+ 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());
+ 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());
+ 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
@@ -251,14 +309,14 @@ public abstract class PhotoManager implements ComponentCallbacks2, Callback {
* Remove photo from the supplied image view. This also cancels current pending load request
* inside this photo manager.
*/
- public void removePhoto(Long hash) {
- Request r = mPendingRequests.get(hash);
+ public void removePhoto(int hashcode) {
+ Request r = mPendingRequests.remove(hashcode);
if (r != null) {
- mPendingRequests.remove(hash);
+ LogUtils.d(TAG, "removed request %s", r.getKey());
}
}
- public void ensureLoaderThread() {
+ private void ensureLoaderThread() {
if (mLoaderThread == null) {
mLoaderThread = getLoaderThread(mContext.getContentResolver());
mLoaderThread.start();
@@ -268,42 +326,82 @@ public abstract class PhotoManager implements ComponentCallbacks2, Callback {
/**
* 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 static boolean loadCachedPhoto(Request request, boolean fadeIn) {
- final Bitmap decoded = sBitmapCache.get(request.bitmapKey);
- if (decoded != null) {
- if (DEBUG) LogUtils.v(TAG, "%s, key=%s decodedSize=%s r=%s thread=%s",
- fadeIn ? "DECODED IMG READ" : "DECODED IMG CACHE HIT",
- request.getKey(),
- decoded.getByteCount(),
- request,
- Thread.currentThread());
+ private boolean loadCachedPhoto(Request request, 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(decoded, request.getKey());
+ onImageDrawn(request, true);
+ request.getView().drawImage(cached, request.getKey());
}
+ Utils.traceEndSection();
return true;
}
- BitmapHolder holder = sBitmapHolderCache.get(request.getKey());
- if (holder == null) {
- // The bitmap has not been loaded ==> show default avatar
- request.applyDefaultImage();
- return false;
+ // 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) {
+ onImageDrawn(request, true);
+ request.getView().drawImage(cachedReplacement, request.getKey());
+ }
+ Utils.traceEndSection();
+ return false;
+ }
}
- if (holder.bytes == null) {
- request.applyDefaultImage();
+ // 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;
}
-
- // Requests that were enqueued and erroneously read too early may get here.
- // The worker thread will eventually decode the bitmap and come right back here,
- // so just sit tight.
+ Utils.traceEndSection();
return false;
}
/**
+ * Takes care of retrieving the Bitmap from both the decoded and holder caches.
+ */
+ private 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() {
@@ -365,15 +463,16 @@ public abstract class PhotoManager implements ComponentCallbacks2, Callback {
* photos still haven't been loaded, sends another request for image loading.
*/
private void processLoadedImages() {
- final List<Long> toRemove = Lists.newArrayList();
- for (Long hash : mPendingRequests.keySet()) {
+ Utils.traceBeginSection("process loaded images");
+ final List<Integer> toRemove = Lists.newArrayList();
+ for (Integer hash : mPendingRequests.keySet()) {
Request request = mPendingRequests.get(hash);
boolean loaded = loadCachedPhoto(request, true);
if (loaded) {
toRemove.add(hash);
}
}
- for (Long key : toRemove) {
+ for (Integer key : toRemove) {
mPendingRequests.remove(key);
}
@@ -381,18 +480,21 @@ public abstract class PhotoManager implements ComponentCallbacks2, Callback {
//softenCache();
if (!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 cacheBitmap(final Object key, final byte[] bytes) {
+ private static void cacheBitmapHolder(final String cacheKey, final BitmapHolder holder) {
if (DEBUG) {
- BitmapHolder prev = sBitmapHolderCache.get(key);
+ BitmapHolder prev = sBitmapHolderCache.get(cacheKey);
if (prev != null && prev.bytes != null) {
- LogUtils.d(TAG, "Overwriting cache: key=" + key
+ LogUtils.d(TAG, "Overwriting cache: key=" + cacheKey
+ (prev.fresh ? " FRESH" : " stale"));
if (prev.fresh) {
sFreshCacheOverwrite.incrementAndGet();
@@ -400,12 +502,15 @@ public abstract class PhotoManager implements ComponentCallbacks2, Callback {
sStaleCacheOverwrite.incrementAndGet();
}
}
- LogUtils.d(TAG, "Caching data: key=" + key + ", "
- + (bytes == null ? "<null>" : btk(bytes.length)));
+ LogUtils.d(TAG, "Caching data: key=" + cacheKey + ", "
+ + (holder.bytes == null ? "<null>" : btk(holder.bytes.length)));
}
- BitmapHolder holder = new BitmapHolder(bytes);
- sBitmapHolderCache.put(key, holder);
+ sBitmapHolderCache.put(cacheKey, holder);
+ }
+
+ protected static void cacheBitmap(final BitmapIdentifier bitmapKey, final Bitmap bitmap) {
+ sBitmapCache.put(bitmapKey, bitmap);
}
// ComponentCallbacks2
@@ -480,13 +585,27 @@ public abstract class PhotoManager implements ComponentCallbacks2, Callback {
return (divisor == 0) ? 0 : (dividend / divisor);
}
- public interface PhotoIdentifier {
+ 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 boolean isValid();
- public String getKey();
+ 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;
+ }
}
/**
@@ -498,7 +617,7 @@ public abstract class PhotoManager implements ComponentCallbacks2, Callback {
* Return photos mapped from {@link Request#getKey()} to the photo for
* that request.
*/
- protected abstract Map<String, byte[]> loadPhotos(Collection<Request> requests);
+ protected abstract Map<String, BitmapHolder> loadPhotos(Collection<Request> requests);
private static final int MESSAGE_LOAD_PHOTOS = 0;
@@ -545,18 +664,37 @@ public abstract class PhotoManager implements ComponentCallbacks2, Callback {
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 List<Request> requests;
+ final PriorityQueue<Request> requests;
synchronized (mPendingRequests) {
- requests = ImmutableList.copyOf(mPendingRequests.values());
+ requests = new PriorityQueue<Request>(mPendingRequests.values());
}
- for (Request request : requests) {
- final BitmapHolder holder = sBitmapHolderCache.get(request.getKey());
- if (holder == null || holder.bytes == null || !holder.fresh) {
+
+ 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++;
} 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.
@@ -564,19 +702,34 @@ public abstract class PhotoManager implements ComponentCallbacks2, Callback {
decodeRequests.add(request);
}
}
+ if (maxBatchCount > 0 && batchCount >= maxBatchCount) {
+ break;
+ }
}
- final Map<String, byte[]> photosMap = loadPhotos(loadRequests);
- if (DEBUG) LogUtils.d(TAG,
- "worker thread completed read request batch. inputN=%s outputN=%s",
- loadRequests.size(),
- photosMap.size());
- for (String key : photosMap.keySet()) {
- if (DEBUG) LogUtils.d(TAG,
- "worker thread completed read request key=%s byteCount=%s thread=%s",
- key,
- photosMap.get(key) == null ? 0 : photosMap.get(key).length,
- Thread.currentThread());
- cacheBitmap(key, photosMap.get(key));
+ 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) {
@@ -584,9 +737,10 @@ public abstract class PhotoManager implements ComponentCallbacks2, Callback {
continue;
}
- final String key = r.getKey();
- final BitmapHolder holder = sBitmapHolderCache.get(key);
- if (holder == null || holder.bytes == null || !holder.fresh) {
+ 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;
}
@@ -600,12 +754,17 @@ public abstract class PhotoManager implements ComponentCallbacks2, Callback {
}
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 (DEBUG) {
+ LogUtils.i(TAG,
+ "worker thread completed decode bmpKey=%s decoded=%s holder=%s",
+ r.bitmapKey, decoded, holder);
+ }
- sBitmapCache.put(r.bitmapKey, decoded);
+ if (decoded != null) {
+ cacheBitmap(r.bitmapKey, decoded);
+ }
}
+ Utils.traceEndSection();
mMainThreadHandler.sendEmptyMessage(MESSAGE_PHOTOS_LOADED);
}
@@ -640,6 +799,25 @@ public abstract class PhotoManager implements ComponentCallbacks2, Callback {
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;
@@ -684,43 +862,43 @@ public abstract class PhotoManager implements ComponentCallbacks2, Callback {
/**
* A holder for a contact photo request.
*/
- public static final class 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;
- // OK to be static as long as all Requests are created on the same thread
- private static final ImageCanvas.Dimensions sWorkDims = new ImageCanvas.Dimensions();
- private Request(PhotoIdentifier photoIdentifier, int requestedExtent,
- DefaultImageProvider defaultProvider, ImageCanvas view) {
+ private Request(PhotoIdentifier photoIdentifier, DefaultImageProvider defaultProvider,
+ ImageCanvas view, ImageCanvas.Dimensions dimensions) {
mPhotoIdentifier = photoIdentifier;
- mRequestedExtent = requestedExtent;
+ mRequestedExtent = -1;
mDefaultProvider = defaultProvider;
mView = view;
viewGeneration = view.getGeneration();
- final String key = getKey();
- // TODO: consider having the client pass in the desired width/height, which would be
- // faster and more direct.
- mView.getDesiredDimensions(key, sWorkDims);
- bitmapKey = new BitmapIdentifier(key, sWorkDims.width, sWorkDims.height);
+ bitmapKey = BitmapIdentifier.getBitmapKey(photoIdentifier, mView, dimensions);
}
public ImageCanvas getView() {
return mView;
}
- public static Request create(
- PhotoIdentifier id, DefaultImageProvider defaultProvider, ImageCanvas view) {
- return new Request(id, -1, defaultProvider, view);
+ public PhotoIdentifier getPhotoIdentifier() {
+ return mPhotoIdentifier;
+ }
+
+ /**
+ * @see PhotoIdentifier#getKey()
+ */
+ public Object getKey() {
+ return mPhotoIdentifier.getKey();
}
@Override
public int hashCode() {
- return Objects.hashCode(mRequestedExtent, mPhotoIdentifier, mView);
+ return getHash(mPhotoIdentifier, mView);
}
@Override
@@ -759,22 +937,23 @@ public abstract class PhotoManager implements ComponentCallbacks2, Callback {
return sb.toString();
}
- public String getKey() {
- return mPhotoIdentifier.getKey();
- }
-
public void applyDefaultImage() {
- final String key = getKey();
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", key, mView);
- return;
+ 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) {
+ return mPhotoIdentifier.compareTo(another.mPhotoIdentifier);
+ }
}
}
diff --git a/src/com/android/mail/preferences/MailPrefs.java b/src/com/android/mail/preferences/MailPrefs.java
index 90b4a152e..985dda4e0 100644
--- a/src/com/android/mail/preferences/MailPrefs.java
+++ b/src/com/android/mail/preferences/MailPrefs.java
@@ -87,7 +87,6 @@ public final class MailPrefs extends VersionedPrefs {
.add(DISPLAY_IMAGES)
.add(DISPLAY_IMAGES_PATTERNS)
.build();
-
}
public static final class ConversationListSwipeActions {
diff --git a/src/com/android/mail/providers/Attachment.java b/src/com/android/mail/providers/Attachment.java
index b6699a843..309b24283 100644
--- a/src/com/android/mail/providers/Attachment.java
+++ b/src/com/android/mail/providers/Attachment.java
@@ -31,6 +31,7 @@ import com.android.emailcommon.mail.Part;
import com.android.mail.browse.MessageAttachmentBar;
import com.android.mail.providers.UIProvider.AttachmentColumns;
import com.android.mail.providers.UIProvider.AttachmentDestination;
+import com.android.mail.providers.UIProvider.AttachmentRendition;
import com.android.mail.providers.UIProvider.AttachmentState;
import com.android.mail.utils.LogTag;
import com.android.mail.utils.LogUtils;
@@ -52,6 +53,7 @@ import java.util.Collection;
import java.util.List;
public class Attachment implements Parcelable {
+ public static final int MAX_ATTACHMENT_PREVIEWS = 2;
public static final String LOG_TAG = LogTag.getLogTag();
/**
* Workaround for b/8070022 so that appending a null partId to the end of a
@@ -65,7 +67,7 @@ public class Attachment implements Parcelable {
public String partId;
/**
- * Attachment file name. See {@link AttachmentColumns#NAME}.
+ * Attachment file name. See {@link AttachmentColumns#NAME} Use {@link #setName(String)}.
*/
private String name;
@@ -84,7 +86,7 @@ public class Attachment implements Parcelable {
public Uri uri;
/**
- * MIME type of the file.
+ * MIME type of the file. Use {@link #getContentType()} and {@link #setContentType(String)}.
*
* @see AttachmentColumns#CONTENT_TYPE
*/
@@ -92,7 +94,7 @@ public class Attachment implements Parcelable {
private String inferredContentType;
/**
- * @see AttachmentColumns#STATE
+ * @see AttachmentColumns#STATE. Use {@link #setState(int)}
*/
public int state;
@@ -529,9 +531,11 @@ public class Attachment implements Parcelable {
private static final String LOCAL_FILE = "LOCAL_FILE";
public String toJoinedString() {
- return TextUtils.join("|", Lists.newArrayList(
+ return TextUtils.join(UIProvider.ATTACHMENT_INFO_DELIMITER, Lists.newArrayList(
partId == null ? "" : partId,
- name == null ? "" : name.replaceAll("[|\n]", ""),
+ name == null ? ""
+ : name.replaceAll("[" + UIProvider.ATTACHMENT_INFO_DELIMITER
+ + UIProvider.ATTACHMENT_INFO_SEPARATOR + "]", ""),
getContentType(),
String.valueOf(size),
getContentType(),
@@ -539,6 +543,54 @@ public class Attachment implements Parcelable {
contentUri));
}
+ /**
+ * For use with {@link UIProvider.ConversationColumns#ATTACHMENT_PREVIEW_STATES}.
+ *
+ * @param previewStates The packed int describing the states of multiple attachments.
+ * @param attachmentIndex The index of the attachment to update.
+ * @param rendition The rendition of that attachment to update.
+ * @param downloaded Whether that specific rendition is downloaded.
+ * @return A packed int describing the updated downloaded states of the multiple attachments.
+ */
+ public static int updatePreviewStates(int previewStates, int attachmentIndex, int rendition,
+ boolean downloaded) {
+ // find the bit that describes that specific attachment index and rendition
+ int shift = attachmentIndex * 2 + rendition;
+ int mask = 1 << shift;
+ // update the packed int at that bit
+ if (downloaded) {
+ // turns that bit into a 1
+ return previewStates | mask;
+ } else {
+ // turns that bit into a 0
+ return previewStates & ~mask;
+ }
+ }
+
+ /**
+ * For use with {@link UIProvider.ConversationColumns#ATTACHMENT_PREVIEW_STATES}.
+ *
+ * @param previewStates The packed int describing the states of multiple attachments.
+ * @param attachmentIndex The index of the attachment.
+ * @param rendition The rendition of the attachment.
+ * @return The downloaded state of that particular rendition of that particular attachment.
+ */
+ public static boolean getPreviewState(int previewStates, int attachmentIndex, int rendition) {
+ // find the bit that describes that specific attachment index
+ int shift = attachmentIndex * 2;
+ int mask = 1 << shift;
+
+ if (rendition == AttachmentRendition.SIMPLE) {
+ // implicit shift of 0 finds the SIMPLE rendition bit
+ return (previewStates & mask) != 0;
+ } else if (rendition == AttachmentRendition.BEST) {
+ // shift of 1 finds the BEST rendition bit
+ return (previewStates & (mask << 1)) != 0;
+ } else {
+ return false;
+ }
+ }
+
public static final Creator<Attachment> CREATOR = new Creator<Attachment>() {
@Override
public Attachment createFromParcel(Parcel source) {
diff --git a/src/com/android/mail/providers/Conversation.java b/src/com/android/mail/providers/Conversation.java
index 6e9546b8e..e936ed571 100644
--- a/src/com/android/mail/providers/Conversation.java
+++ b/src/com/android/mail/providers/Conversation.java
@@ -33,6 +33,7 @@ import com.android.mail.providers.UIProvider.ConversationColumns;
import com.android.mail.ui.ConversationCursorLoader;
import com.android.mail.utils.LogTag;
import com.android.mail.utils.LogUtils;
+import com.android.mail.utils.Utils;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Lists;
@@ -74,6 +75,27 @@ public class Conversation implements Parcelable {
*/
public boolean hasAttachments;
/**
+ * Union of attachmentPreviewUri0 and attachmentPreviewUri1
+ */
+ public transient ArrayList<String> attachmentPreviews;
+ /**
+ * @see UIProvider.ConversationColumns#ATTACHMENT_PREVIEW_URI0
+ */
+ public String attachmentPreviewUri0;
+ /**
+ * @see UIProvider.ConversationColumns#ATTACHMENT_PREVIEW_URI1
+ */
+ public String attachmentPreviewUri1;
+ /**
+ * @see UIProvider.ConversationColumns#ATTACHMENT_PREVIEW_STATES
+ */
+ public int attachmentPreviewStates;
+ /**
+ * @see UIProvider.ConversationColumns#ATTACHMENT_PREVIEWS_COUNT
+ */
+ public int attachmentPreviewsCount;
+ public Uri attachmentPreviewsListUri;
+ /**
* @see UIProvider.ConversationColumns#MESSAGE_LIST_URI
*/
public Uri messageListUri;
@@ -216,6 +238,11 @@ public class Conversation implements Parcelable {
dest.writeParcelable(conversationInfo, 0);
dest.writeParcelable(conversationBaseUri, 0);
dest.writeInt(isRemote ? 1 : 0);
+ dest.writeString(attachmentPreviewUri0);
+ dest.writeString(attachmentPreviewUri1);
+ dest.writeInt(attachmentPreviewStates);
+ dest.writeInt(attachmentPreviewsCount);
+ dest.writeParcelable(attachmentPreviewsListUri, 0);
}
private Conversation(Parcel in, ClassLoader loader) {
@@ -247,6 +274,12 @@ public class Conversation implements Parcelable {
conversationInfo = in.readParcelable(loader);
conversationBaseUri = in.readParcelable(null);
isRemote = in.readInt() != 0;
+ attachmentPreviews = null;
+ attachmentPreviewUri0 = in.readString();
+ attachmentPreviewUri1 = in.readString();
+ attachmentPreviewStates = in.readInt();
+ attachmentPreviewsCount = in.readInt();
+ attachmentPreviewsListUri = in.readParcelable(null);
}
@Override
@@ -330,6 +363,17 @@ public class Conversation implements Parcelable {
numDrafts = cursor.getInt(UIProvider.CONVERSATION_NUM_DRAFTS_COLUMN);
}
isRemote = cursor.getInt(UIProvider.CONVERSATION_REMOTE_COLUMN) != 0;
+ attachmentPreviews = null;
+ attachmentPreviewUri0 = cursor.getString(
+ UIProvider.CONVERSATION_ATTACHMENT_PREVIEW_URI0_COLUMN);
+ attachmentPreviewUri1 = cursor.getString(
+ UIProvider.CONVERSATION_ATTACHMENT_PREVIEW_URI1_COLUMN);
+ attachmentPreviewStates = cursor.getInt(
+ UIProvider.CONVERSATION_ATTACHMENT_PREVIEW_STATES_COLUMN);
+ attachmentPreviewsCount = cursor.getInt(
+ UIProvider.CONVERSATION_ATTACHMENT_PREVIEWS_COUNT_COLUMN);
+ attachmentPreviewsListUri = Utils.getValidUri(cursor
+ .getString(UIProvider.CONVERSATION_ATTACHMENT_PREVIEWS_LIST_URI_COLUMN));
}
}
@@ -368,18 +412,25 @@ public class Conversation implements Parcelable {
numMessages = other.numMessages;
numDrafts = other.numDrafts;
isRemote = other.isRemote;
+ attachmentPreviews = null;
+ attachmentPreviewUri0 = other.attachmentPreviewUri0;
+ attachmentPreviewUri1 = other.attachmentPreviewUri1;
+ attachmentPreviewStates = other.attachmentPreviewStates;
+ attachmentPreviewsCount = other.attachmentPreviewsCount;
+ attachmentPreviewsListUri = other.attachmentPreviewsListUri;
}
public Conversation() {
}
- public static Conversation create(long id, Uri uri, String subject, long dateMs,
- String snippet, boolean hasAttachment, Uri messageListUri, String senders,
+ public static Conversation create(long id, Uri uri, String subject, long dateMs, String snippet,
+ boolean hasAttachment, Uri messageListUri, String senders,
int numMessages, int numDrafts, int sendingState, int priority, boolean read,
boolean seen, boolean starred, FolderList rawFolders, int convFlags, int personalLevel,
boolean spam, boolean phishing, boolean muted, Uri accountUri,
- ConversationInfo conversationInfo, Uri conversationBase, boolean isRemote) {
-
+ ConversationInfo conversationInfo, Uri conversationBase, boolean isRemote,
+ String attachmentPreviewUri0, String attachmentPreviewUri1, int attachmentPreviewStates,
+ int attachmentPreviewsCount, Uri attachmentPreviewsListUri) {
final Conversation conversation = new Conversation();
conversation.id = id;
@@ -408,6 +459,12 @@ public class Conversation implements Parcelable {
conversation.conversationInfo = conversationInfo;
conversation.conversationBaseUri = conversationBase;
conversation.isRemote = isRemote;
+ conversation.attachmentPreviews = null;
+ conversation.attachmentPreviewUri0 = attachmentPreviewUri0;
+ conversation.attachmentPreviewUri1 = attachmentPreviewUri1;
+ conversation.attachmentPreviewStates = attachmentPreviewStates;
+ conversation.attachmentPreviewsCount = attachmentPreviewsCount;
+ conversation.attachmentPreviewsListUri = attachmentPreviewsListUri;
return conversation;
}
@@ -621,12 +678,17 @@ public class Conversation implements Parcelable {
return conversationBaseUri != null ? conversationBaseUri.toString() : defaultValue;
}
- public int getAttachmentsCount() {
- return getAttachments().size();
- }
-
- public ArrayList<String> getAttachments() {
- return Lists.newArrayList();
+ public ArrayList<String> getAttachmentPreviewUris() {
+ if (attachmentPreviews == null) {
+ attachmentPreviews = Lists.newArrayListWithCapacity(2);
+ if (!TextUtils.isEmpty(attachmentPreviewUri0)) {
+ attachmentPreviews.add(attachmentPreviewUri0);
+ }
+ if (!TextUtils.isEmpty(attachmentPreviewUri1)) {
+ attachmentPreviews.add(attachmentPreviewUri1);
+ }
+ }
+ return attachmentPreviews;
}
/**
diff --git a/src/com/android/mail/providers/UIProvider.java b/src/com/android/mail/providers/UIProvider.java
index 0d530bd6d..c6565bfd2 100644
--- a/src/com/android/mail/providers/UIProvider.java
+++ b/src/com/android/mail/providers/UIProvider.java
@@ -35,6 +35,7 @@ import com.google.common.collect.ImmutableMap;
import java.util.ArrayList;
import java.util.Map;
+import java.util.regex.Pattern;
public class UIProvider {
public static final String EMAIL_SEPARATOR = ",";
@@ -914,7 +915,12 @@ public class UIProvider {
ConversationColumns.ACCOUNT_URI,
ConversationColumns.SENDER_INFO,
ConversationColumns.CONVERSATION_BASE_URI,
- ConversationColumns.REMOTE
+ ConversationColumns.REMOTE,
+ ConversationColumns.ATTACHMENT_PREVIEW_URI0,
+ ConversationColumns.ATTACHMENT_PREVIEW_URI1,
+ ConversationColumns.ATTACHMENT_PREVIEW_STATES,
+ ConversationColumns.ATTACHMENT_PREVIEWS_COUNT,
+ ConversationColumns.ATTACHMENT_PREVIEWS_LIST_URI
};
/**
@@ -952,6 +958,11 @@ public class UIProvider {
public static final int CONVERSATION_SENDER_INFO_COLUMN = 23;
public static final int CONVERSATION_BASE_URI_COLUMN = 24;
public static final int CONVERSATION_REMOTE_COLUMN = 25;
+ public static final int CONVERSATION_ATTACHMENT_PREVIEW_URI0_COLUMN = 26;
+ public static final int CONVERSATION_ATTACHMENT_PREVIEW_URI1_COLUMN = 27;
+ public static final int CONVERSATION_ATTACHMENT_PREVIEW_STATES_COLUMN = 28;
+ public static final int CONVERSATION_ATTACHMENT_PREVIEWS_COUNT_COLUMN = 29;
+ public static final int CONVERSATION_ATTACHMENT_PREVIEWS_LIST_URI_COLUMN = 30;
public static final class ConversationSendingState {
public static final int OTHER = 0;
@@ -1131,6 +1142,40 @@ public class UIProvider {
*/
public static final String CONVERSATION_BASE_URI = "conversationBaseUri";
+ /**
+ * This string column contains the uri of the first attachment preview of the first unread
+ * message, denoted by UNREAD_MESSAGE_ID.
+ */
+ public static final String ATTACHMENT_PREVIEW_URI0 = "attachmentPreviewUri0";
+
+ /**
+ * This string column contains the uri of the second attachment preview of the first unread
+ * message, denoted by UNREAD_MESSAGE_ID.
+ */
+ public static final String ATTACHMENT_PREVIEW_URI1 = "attachmentPreviewUri1";
+
+ /**
+ * This int column contains the states of the attachment previews of the first unread
+ * message, the same message used for the snippet. The states is a packed int,
+ * where the first and second bits represent the SIMPLE and BEST state of the first
+ * attachment preview, while the third and fourth bits represent those states for the
+ * second attachment preview. For each bit, a one means that rendition of that attachment
+ * preview is downloaded.
+ */
+ public static final String ATTACHMENT_PREVIEW_STATES = "attachmentPreviewStates";
+
+ /**
+ * This int column contains the total count of images in the first unread message. The
+ * total count may be higher than the number of ATTACHMENT_PREVIEW_URI columns.
+ */
+ public static final String ATTACHMENT_PREVIEWS_COUNT = "attachmentPreviewsCount";
+
+ /**
+ * This String column contains the image list uri for the first unread message. We can
+ * pass this uri to the MailPhotoViewActivity to view all images.
+ */
+ public static final String ATTACHMENT_PREVIEWS_LIST_URI = "attachmentPreviewsListUri";
+
private ConversationColumns() {
}
}
@@ -1745,6 +1790,14 @@ public class UIProvider {
public static final int ATTACHMENT_PREVIEW_INTENT_COLUMN = 9;
public static final int ATTACHMENT_SUPPORTS_DOWNLOAD_AGAIN_COLUMN = 10;
+ /** Separates attachment info parts in strings in the database. */
+ public static final String ATTACHMENT_INFO_SEPARATOR = "\n"; // use to join
+ public static final Pattern ATTACHMENT_INFO_SEPARATOR_PATTERN =
+ Pattern.compile(ATTACHMENT_INFO_SEPARATOR); // use to split
+ public static final String ATTACHMENT_INFO_DELIMITER = "|"; // use to join
+ // use to split
+ public static final Pattern ATTACHMENT_INFO_DELIMITER_PATTERN = Pattern.compile("\\|");
+
/**
* Valid states for the {@link AttachmentColumns#STATE} column.
*
@@ -1932,12 +1985,29 @@ public class UIProvider {
private static final String SIMPLE_STRING = "SIMPLE";
private static final String BEST_STRING = "BEST";
+ /**
+ * Prefer renditions in this order.
+ */
+ public static final int[] PREFERRED_RENDITIONS = new int[]{BEST, SIMPLE};
+
public static int parseRendition(String rendition) {
- return TextUtils.equals(rendition, SIMPLE_STRING) ? SIMPLE : BEST;
+ if (TextUtils.equals(rendition, SIMPLE_STRING)) {
+ return SIMPLE;
+ } else if (TextUtils.equals(rendition, BEST_STRING)) {
+ return BEST;
+ }
+
+ throw new IllegalArgumentException(String.format("Unknown rendition %s", rendition));
}
public static String toString(int rendition) {
- return rendition == BEST ? BEST_STRING : SIMPLE_STRING;
+ if (rendition == BEST) {
+ return BEST_STRING;
+ } else if (rendition == SIMPLE) {
+ return SIMPLE_STRING;
+ }
+
+ throw new IllegalArgumentException(String.format("Unknown rendition %d", rendition));
}
}
diff --git a/src/com/android/mail/ui/DividedImageCanvas.java b/src/com/android/mail/ui/DividedImageCanvas.java
index 2338189ca..e40ff12cf 100644
--- a/src/com/android/mail/ui/DividedImageCanvas.java
+++ b/src/com/android/mail/ui/DividedImageCanvas.java
@@ -21,10 +21,12 @@ import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Paint;
+import android.graphics.PorterDuff.Mode;
+import android.graphics.PorterDuffXfermode;
import android.graphics.Rect;
import com.android.mail.R;
-import com.google.common.base.Objects;
+import com.android.mail.utils.Utils;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
@@ -45,8 +47,8 @@ import java.util.Map;
public class DividedImageCanvas implements ImageCanvas {
public static final int MAX_DIVISIONS = 4;
- private final Map<String, Integer> mDivisionMap =
- Maps.newHashMapWithExpectedSize(MAX_DIVISIONS);
+ private final Map<String, Integer> mDivisionMap = Maps
+ .newHashMapWithExpectedSize(MAX_DIVISIONS);
private Bitmap mDividedBitmap;
private Canvas mCanvas;
private int mWidth;
@@ -65,11 +67,17 @@ public class DividedImageCanvas implements ImageCanvas {
private int mGeneration;
private static final Paint sPaint = new Paint();
+ private static final Paint sClearPaint = new Paint();
+ private static final Rect sSrc = new Rect();
private static final Rect sDest = new Rect();
private static int sDividerLineWidth = -1;
private static int sDividerColor;
+ static {
+ sClearPaint.setXfermode(new PorterDuffXfermode(Mode.CLEAR));
+ }
+
public DividedImageCanvas(Context context, InvalidateCallback callback) {
mContext = context;
mCallback = callback;
@@ -102,41 +110,72 @@ public class DividedImageCanvas implements ImageCanvas {
/**
* Set the id associated with each quadrant. The quadrants are laid out:
* TopLeft, TopRight, Bottom Left, Bottom Right
- * @param divisionIds
+ * @param keys
*/
- public void setDivisionIds(List<String> divisionIds) {
- if (divisionIds.size() > MAX_DIVISIONS) {
- throw new IllegalArgumentException("too many divisionIds: " + divisionIds);
+ public void setDivisionIds(List<Object> keys) {
+ if (keys.size() > MAX_DIVISIONS) {
+ throw new IllegalArgumentException("too many divisionIds: " + keys);
}
- mDivisionMap.clear();
- mDivisionImages.clear();
- int i = 0;
- for (String id : divisionIds) {
- mDivisionMap.put(id, i);
- mDivisionImages.add(null);
- i++;
+
+ boolean needClear = getDivisionCount() != keys.size();
+ if (!needClear) {
+ for (int i = 0; i < keys.size(); i++) {
+ String divisionId = transformKeyToDivisionId(keys.get(i));
+ // different item or different place
+ if (!mDivisionMap.containsKey(divisionId) || mDivisionMap.get(divisionId) != i) {
+ needClear = true;
+ break;
+ }
+ }
+ }
+
+ if (needClear) {
+ mDivisionMap.clear();
+ mDivisionImages.clear();
+ int i = 0;
+ for (Object key : keys) {
+ String divisionId = transformKeyToDivisionId(key);
+ mDivisionMap.put(divisionId, i);
+ mDivisionImages.add(null);
+ i++;
+ }
}
}
- private static void draw(Bitmap b, Canvas c, int left, int top, int right, int bottom) {
+ private void draw(Bitmap b, int left, int top, int right, int bottom) {
if (b != null) {
+ // Some times we load taller images compared to the destination rect on the canvas
+ int srcTop = 0;
+ int srcBottom = b.getHeight();
+ int destHeight = bottom - top;
+ if (b.getHeight() > bottom - top) {
+ srcTop = b.getHeight() / 2 - destHeight/2;
+ srcBottom = b.getHeight() / 2 + destHeight/2;
+ }
+
+// todo:markwei do not scale very small bitmaps
// l t r b
+ sSrc.set(0, srcTop, b.getWidth(), srcBottom);
sDest.set(left, top, right, bottom);
- c.drawBitmap(b, null, sDest, sPaint);
+ mCanvas.drawBitmap(b, sSrc, sDest, sPaint);
+ } else {
+ // clear
+ mCanvas.drawRect(left, top, right, bottom, sClearPaint);
}
}
/**
* Get the desired dimensions and scale for the bitmap to be placed in the
* location corresponding to id. Caller must allocate the Dimensions object.
- * @param id
+ * @param key
* @param outDim a {@link ImageCanvas.Dimensions} object to write results into
*/
@Override
- public void getDesiredDimensions(Object id, Dimensions outDim) {
+ public void getDesiredDimensions(Object key, Dimensions outDim) {
+ Utils.traceBeginSection("get desired dimensions");
int w = 0, h = 0;
float scale = 0;
- final Integer pos = mDivisionMap.get(id);
+ final Integer pos = mDivisionMap.get(transformKeyToDivisionId(key));
if (pos != null && pos >= 0) {
final int size = mDivisionMap.size();
switch (size) {
@@ -175,21 +214,32 @@ public class DividedImageCanvas implements ImageCanvas {
outDim.width = w;
outDim.height = h;
outDim.scale = scale;
+ Utils.traceEndSection();
}
@Override
- public void drawImage(Bitmap b, Object id) {
- addDivisionImage(b, id);
+ public void drawImage(Bitmap b, Object key) {
+ addDivisionImage(b, key);
}
/**
* Add a bitmap to this view in the quadrant matching its id.
* @param b Bitmap
- * @param id Id to look for that was previously set in setDivisionIds.
+ * @param key Id to look for that was previously set in setDivisionIds.
*/
- public void addDivisionImage(Bitmap b, Object id) {
- final Integer pos = mDivisionMap.get(id);
- if (pos != null && pos >= 0 && b != null) {
+ public void addDivisionImage(Bitmap b, Object key) {
+ if (b != null) {
+ addOrClearDivisionImage(b, key);
+ }
+ }
+
+ public void clearDivisionImage(Object key) {
+ addOrClearDivisionImage(null, key);
+ }
+ private void addOrClearDivisionImage(Bitmap b, Object key) {
+ Utils.traceBeginSection("add or clear division image");
+ final Integer pos = mDivisionMap.get(transformKeyToDivisionId(key));
+ if (pos != null && pos >= 0) {
mDivisionImages.set(pos, b);
boolean complete = false;
final int width = mWidth;
@@ -202,20 +252,21 @@ public class DividedImageCanvas implements ImageCanvas {
break;
case 1:
// Draw the bitmap filling the entire canvas.
- draw(mDivisionImages.get(0), mCanvas, 0, 0, width, height);
+ draw(mDivisionImages.get(0), 0, 0, width, height);
complete = true;
break;
case 2:
// Draw 2 bitmaps split vertically down the middle
switch (pos) {
case 0:
- draw(mDivisionImages.get(0), mCanvas, 0, 0, width / 2, height);
+ draw(mDivisionImages.get(0), 0, 0, width / 2, height);
break;
case 1:
- draw(mDivisionImages.get(1), mCanvas, width / 2, 0, width, height);
+ draw(mDivisionImages.get(1), width / 2, 0, width, height);
break;
}
- complete = mDivisionImages.get(0) != null && mDivisionImages.get(1) != null;
+ complete = mDivisionImages.get(0) != null && mDivisionImages.get(1) != null
+ || isPartialBitmapComplete();
if (complete) {
// Draw dividers
drawVerticalDivider(width, height);
@@ -227,18 +278,17 @@ public class DividedImageCanvas implements ImageCanvas {
// position.
switch (pos) {
case 0:
- draw(mDivisionImages.get(0), mCanvas, 0, 0, width / 2, height);
+ draw(mDivisionImages.get(0), 0, 0, width / 2, height);
break;
case 1:
- draw(mDivisionImages.get(1), mCanvas, width / 2, 0, width, height / 2);
+ draw(mDivisionImages.get(1), width / 2, 0, width, height / 2);
break;
case 2:
- draw(mDivisionImages.get(2), mCanvas, width / 2, height / 2, width,
- height);
+ draw(mDivisionImages.get(2), width / 2, height / 2, width, height);
break;
}
complete = mDivisionImages.get(0) != null && mDivisionImages.get(1) != null
- && mDivisionImages.get(2) != null;
+ && mDivisionImages.get(2) != null || isPartialBitmapComplete();
if (complete) {
// Draw dividers
drawVerticalDivider(width, height);
@@ -249,21 +299,21 @@ public class DividedImageCanvas implements ImageCanvas {
// Draw all 4 bitmaps in a grid
switch (pos) {
case 0:
- draw(mDivisionImages.get(0), mCanvas, 0, 0, width / 2, height / 2);
+ draw(mDivisionImages.get(0), 0, 0, width / 2, height / 2);
break;
case 1:
- draw(mDivisionImages.get(1), mCanvas, width / 2, 0, width, height / 2);
+ draw(mDivisionImages.get(1), width / 2, 0, width, height / 2);
break;
case 2:
- draw(mDivisionImages.get(2), mCanvas, 0, height / 2, width / 2, height);
+ draw(mDivisionImages.get(2), 0, height / 2, width / 2, height);
break;
case 3:
- draw(mDivisionImages.get(3), mCanvas, width / 2, height / 2, width,
- height);
+ draw(mDivisionImages.get(3), width / 2, height / 2, width, height);
break;
}
complete = mDivisionImages.get(0) != null && mDivisionImages.get(1) != null
- && mDivisionImages.get(2) != null && mDivisionImages.get(3) != null;
+ && mDivisionImages.get(2) != null && mDivisionImages.get(3) != null
+ || isPartialBitmapComplete();
if (complete) {
// Draw dividers
drawVerticalDivider(width, height);
@@ -277,10 +327,11 @@ public class DividedImageCanvas implements ImageCanvas {
mCallback.invalidate();
}
}
+ Utils.traceEndSection();
}
- public boolean hasImageFor(Object id) {
- final Integer pos = mDivisionMap.get(id);
+ public boolean hasImageFor(Object key) {
+ final Integer pos = mDivisionMap.get(transformKeyToDivisionId(key));
return pos != null && mDivisionImages.get(pos) != null;
}
@@ -298,21 +349,31 @@ public class DividedImageCanvas implements ImageCanvas {
sPaint.setColor(sDividerColor);
}
- private void drawVerticalDivider(int width, int height) {
+ protected void drawVerticalDivider(int width, int height) {
int x1 = width / 2, y1 = 0, x2 = width/2, y2 = height;
setupPaint();
mCanvas.drawLine(x1, y1, x2, y2, sPaint);
}
- private void drawHorizontalDivider(int x1, int y1, int x2, int y2) {
+ protected void drawHorizontalDivider(int x1, int y1, int x2, int y2) {
setupPaint();
mCanvas.drawLine(x1, y1, x2, y2, sPaint);
}
+ protected boolean isPartialBitmapComplete() {
+ return false;
+ }
+
+ protected String transformKeyToDivisionId(Object key) {
+ return key.toString();
+ }
+
/**
* Draw the contents of the DividedImageCanvas to the supplied canvas.
*/
public void draw(Canvas canvas) {
+ // todo:markwei we can see the old image behind transparency regions. Should we also
+ // "clear" the canvas? ath
if (mDividedBitmap != null && mBitmapValid) {
canvas.drawBitmap(mDividedBitmap, 0, 0, null);
}
@@ -339,16 +400,20 @@ public class DividedImageCanvas implements ImageCanvas {
* @param height
*/
public void setDimensions(int width, int height) {
+ Utils.traceBeginSection("set dimensions");
if (mWidth == width && mHeight == height) {
+ Utils.traceEndSection();
return;
}
mWidth = width;
mHeight = height;
+ // todo:ath this bitmap is creating a GC which is killing CIV.loadAttachmentPreviews()
mDividedBitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
mCanvas = new Canvas(mDividedBitmap);
mBitmapValid = true;
+ Utils.traceEndSection();
}
/**
@@ -378,24 +443,9 @@ public class DividedImageCanvas implements ImageCanvas {
}
/**
- * Generate a unique hashcode to use for the request for an image to put in
- * the specified position of the DividedImageCanvas.
- */
- public static long generateHash(DividedImageCanvas contactImagesHolder, int i, String address) {
- return Objects.hashCode(contactImagesHolder, i, address);
- }
-
- /**
* Get the division ids currently associated with this DivisionImageCanvas.
*/
public ArrayList<String> getDivisionIds() {
return Lists.newArrayList(mDivisionMap.keySet());
}
-
- @Deprecated
- @Override
- public Bitmap loadImage(byte[] bytes, Object id) {
- // TODO: remove me soon.
- return null;
- }
}
diff --git a/src/com/android/mail/ui/FolderSelectionActivity.java b/src/com/android/mail/ui/FolderSelectionActivity.java
index f490bc7fe..743afc799 100644
--- a/src/com/android/mail/ui/FolderSelectionActivity.java
+++ b/src/com/android/mail/ui/FolderSelectionActivity.java
@@ -464,5 +464,4 @@ public class FolderSelectionActivity extends Activity implements OnClickListener
// Unsupported
return null;
}
-
}
diff --git a/src/com/android/mail/ui/ImageCanvas.java b/src/com/android/mail/ui/ImageCanvas.java
index 850df2fe6..d6660400c 100644
--- a/src/com/android/mail/ui/ImageCanvas.java
+++ b/src/com/android/mail/ui/ImageCanvas.java
@@ -46,15 +46,12 @@ public interface ImageCanvas {
height = h;
scale = s;
}
- }
- /**
- * Draw the image, given an optional {@link PhotoIdentifier#getKey()} id.
- *
- * @deprecated use {@link #drawImage(Bitmap, Object)} instead.
- */
- @Deprecated
- Bitmap loadImage(byte[] bytes, Object id);
+ @Override
+ public String toString() {
+ return String.format("Dimens [%d x %d]", width, height);
+ }
+ }
/**
* Draw/composite the given Bitmap corresponding with the key 'id'. It will be sized according
@@ -62,9 +59,9 @@ public interface ImageCanvas {
* decode request was made.
*
* @param decoded an exactly-sized, decoded bitmap to display
- * @param id
+ * @param key
*/
- void drawImage(Bitmap decoded, Object id);
+ void drawImage(Bitmap decoded, Object key);
/**
* Reset all state associated with this view so that it can be reused.
@@ -74,10 +71,10 @@ public interface ImageCanvas {
/**
* Outputs the desired dimensions that the object with key 'id' would like to be drawn to.
*
- * @param id
+ * @param key
* @param outDim caller-allocated {@link Dimensions} object to house the result
*/
- void getDesiredDimensions(Object id, Dimensions outDim);
+ void getDesiredDimensions(Object key, Dimensions outDim);
/**
* Return an arbitrary integer to associate with any asynchronous requests for images that
diff --git a/src/com/android/mail/ui/SwipeableListView.java b/src/com/android/mail/ui/SwipeableListView.java
index 657051ff0..80cf6defc 100644
--- a/src/com/android/mail/ui/SwipeableListView.java
+++ b/src/com/android/mail/ui/SwipeableListView.java
@@ -53,6 +53,10 @@ public class SwipeableListView extends ListView implements Callback, OnScrollLis
private boolean mEnableSwipe = false;
public static final String LOG_TAG = LogTag.getLogTag();
+ /**
+ * Set to false to prevent the FLING scroll state from pausing the photo manager loaders.
+ */
+ private final static boolean SCROLL_PAUSE_ENABLE = true;
private ConversationSelectionSet mConvSelectionSet;
private int mSwipeAction;
@@ -364,19 +368,14 @@ public class SwipeableListView extends ListView implements Callback, OnScrollLis
}
@Override
- public void onScroll(AbsListView arg0, int arg1, int arg2, int arg3) {
- // Do nothing.
+ public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount,
+ int totalItemCount) {
}
@Override
- public void onScrollStateChanged(AbsListView arg0, int scrollState) {
- switch (scrollState) {
- case OnScrollListener.SCROLL_STATE_IDLE:
- mScrolling = false;
- break;
- default:
- mScrolling = true;
- }
+ public void onScrollStateChanged(AbsListView view, int scrollState) {
+ mScrolling = scrollState != OnScrollListener.SCROLL_STATE_IDLE;
+
if (!mScrolling) {
final Context c = getContext();
if (c instanceof ControllableActivity) {
@@ -386,6 +385,10 @@ public class SwipeableListView extends ListView implements Callback, OnScrollLis
LogUtils.wtf(LOG_TAG, "unexpected context=%s", c);
}
}
+
+ if (SCROLL_PAUSE_ENABLE) {
+ ConversationItemView.setPhotoManagersPaused(mScrolling);
+ }
}
public boolean isScrolling() {