super-collapsed blocks
Expanding a block inserts HTML for all bodies in that block, as
was done in ICS. This could be improved to be more lazy.
The block's layout has a mostly useless wrapper for the 1px
bottom white padding, because ConversationContainer does not yet
support margins on child overlays.
The overlay container now listens to adapter changes, which
was necessary to update on super-collapsed expansion. This
should also fix crashes and strange layouts on draft deletion.
Also fixed monkey NPE on url click while I was in there.
Bug: 6325429
Bug: 6260673
Bug: 6258859
Change-Id: I77347b58bbec49b4b5b58a2b3de7e5e9f291ca9c
diff --git a/assets/script.js b/assets/script.js
index da380e6..4e0ba2c 100644
--- a/assets/script.js
+++ b/assets/script.js
@@ -193,6 +193,26 @@
}
setMessageHeaderSpacerHeight(messageDomId, spacerHeight);
}
+
+function replaceSuperCollapsedBlock(startIndex) {
+ var parent, block, header;
+
+ block = document.querySelector(".mail-super-collapsed-block[index='" + startIndex + "']");
+ if (!block) {
+ console.log("can't expand super collapsed block at index: " + startIndex);
+ return;
+ }
+ parent = block.parentNode;
+ block.innerHTML = window.mail.getTempMessageBodies();
+
+ header = block.firstChild;
+ while (header) {
+ parent.insertBefore(header, block);
+ header = block.firstChild;
+ }
+ parent.removeChild(block);
+ measurePositions();
+}
// END Java->JavaScript handlers
collapseQuotedText();
diff --git a/res/layout/super_collapsed_block.xml b/res/layout/super_collapsed_block.xml
new file mode 100644
index 0000000..44444f5
--- /dev/null
+++ b/res/layout/super_collapsed_block.xml
@@ -0,0 +1,57 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2011 Google Inc.
+ Licensed to The Android Open Source Project.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<com.android.mail.browse.SuperCollapsedBlock
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/super_collapsed_block"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:paddingBottom="@dimen/message_header_vertical_margin">
+ <RelativeLayout
+ android:id="@+id/super_collapsed_background"
+ android:layout_width="match_parent"
+ android:layout_height="@dimen/super_collapsed_height"
+ android:paddingRight="@dimen/message_header_padding_right">
+ <View
+ android:id="@+id/super_collapsed_icon"
+ android:layout_width="@dimen/contact_photo_width"
+ android:layout_height="match_parent"
+ android:layout_alignParentLeft="true"
+ android:background="@drawable/super_collapsed_icon" />
+ <TextView
+ android:id="@+id/super_collapsed_count"
+ android:layout_width="@dimen/message_header_action_button_width"
+ android:layout_height="wrap_content"
+ android:layout_alignParentRight="true"
+ android:layout_centerVertical="true"
+ android:gravity="center_horizontal"
+ android:textSize="14sp"
+ android:textStyle="bold"
+ android:textColor="@color/message_sender_name" />
+ <TextView
+ android:id="@+id/super_collapsed_label"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_toRightOf="@id/super_collapsed_icon"
+ android:layout_toLeftOf="@id/super_collapsed_count"
+ android:layout_centerVertical="true"
+ android:paddingLeft="@dimen/message_header_inner_side_padding"
+ android:text="@string/show_messages_read"
+ android:textSize="14sp"
+ android:textColor="@color/message_sender_name" />
+ </RelativeLayout>
+</com.android.mail.browse.SuperCollapsedBlock>
diff --git a/res/raw/template_super_collapsed.html b/res/raw/template_super_collapsed.html
index 386bbae..e46ead2 100644
--- a/res/raw/template_super_collapsed.html
+++ b/res/raw/template_super_collapsed.html
@@ -1 +1 @@
-<div class="mail-super-collapsed-block spacer" index="%s" style="height: %spx;"></div>
+<div class="mail-super-collapsed-block" index="%s" style="height: %spx;"></div>
diff --git a/res/values-sw600dp/dimen.xml b/res/values-sw600dp/dimen.xml
index 4adc6fd..997a440 100644
--- a/res/values-sw600dp/dimen.xml
+++ b/res/values-sw600dp/dimen.xml
@@ -27,4 +27,18 @@
<dimen name="spinner_frame_width">180dp</dimen>
<dimen name="conversation_header_side_padding">0dip</dimen>
<dimen name="conversation_header_vertical_padding">12dip</dimen>
+ <dimen name="conversation_page_side_margin">16dip</dimen>
+ <dimen name="conversation_page_gutter">0dip</dimen>
+ <dimen name="conversation_side_padding">16dip</dimen>
+ <dimen name="message_header_inner_side_padding">16dip</dimen>
+ <dimen name="contact_photo_width">64sp</dimen>
+ <dimen name="contact_photo_height">64sp</dimen>
+ <dimen name="message_details_header_padding_left">14dip</dimen>
+ <dimen name="message_details_header_padding_right">4dip</dimen>
+ <dimen name="message_details_header_date_margin_right">64dip</dimen>
+ <dimen name="message_header_height">64sp</dimen>
+ <dimen name="message_header_padding_right">4dip</dimen>
+ <dimen name="message_header_action_button_width">56dip</dimen>
+ <dimen name="message_header_title_container_margin_right">0dip</dimen>
+ <dimen name="super_collapsed_height">32sp</dimen>
</resources>
diff --git a/res/values/dimen.xml b/res/values/dimen.xml
index 78f7467..131db0e 100644
--- a/res/values/dimen.xml
+++ b/res/values/dimen.xml
@@ -59,6 +59,7 @@
<dimen name="message_header_action_button_width">48dip</dimen>
<dimen name="message_header_title_container_margin_right">16dip</dimen>
<dimen name="message_header_padding_right">0dip</dimen>
+ <dimen name="super_collapsed_height">24sp</dimen>
<dimen name="notification_view_height">36dip</dimen>
<dimen name="contact_photo_width">48sp</dimen>
<dimen name="contact_photo_height">48sp</dimen>
diff --git a/src/com/android/mail/browse/ConversationContainer.java b/src/com/android/mail/browse/ConversationContainer.java
index 531d31c..ce0840e 100644
--- a/src/com/android/mail/browse/ConversationContainer.java
+++ b/src/com/android/mail/browse/ConversationContainer.java
@@ -18,6 +18,7 @@
package com.android.mail.browse;
import android.content.Context;
+import android.database.DataSetObserver;
import android.util.AttributeSet;
import android.util.SparseArray;
import android.view.MotionEvent;
@@ -30,7 +31,6 @@
import android.widget.ScrollView;
import com.android.mail.R;
-import com.android.mail.browse.ConversationViewAdapter.ConversationItem;
import com.android.mail.browse.ScrollNotifier.ScrollListener;
import com.android.mail.utils.DequeMap;
import com.android.mail.utils.LogUtils;
@@ -118,8 +118,12 @@
* its child array. So we remove any child from this list immediately and queue up a task to
* detach it later. Since nobody other than the detach task references that view in the
* meantime, we don't need any further checks or synchronization.
+ * <p>
+ * We keep {@link OverlayView} wrappers instead of bare views so that when it's time to dispose
+ * of all views (on data set or adapter change), we can at least recycle them into the typed
+ * scrap piles for later reuse.
*/
- private final SparseArray<View> mOverlayViews;
+ private final SparseArray<OverlayView> mOverlayViews;
private final float mDensity;
@@ -127,6 +131,8 @@
private boolean mDisableLayoutTracing;
+ private final DataSetObserver mAdapterObserver = new AdapterObserver();
+
/**
* Child views of this container should implement this interface to be notified when they are
* being detached.
@@ -140,6 +146,16 @@
void onDetachedFromParent();
}
+ private static class OverlayView {
+ public View view;
+ int itemType;
+
+ public OverlayView(View view, int itemType) {
+ this.view = view;
+ this.itemType = itemType;
+ }
+ }
+
public ConversationContainer(Context c) {
this(c, null);
}
@@ -147,7 +163,7 @@
public ConversationContainer(Context c, AttributeSet attrs) {
super(c, attrs);
- mOverlayViews = new SparseArray<View>();
+ mOverlayViews = new SparseArray<OverlayView>();
mDensity = c.getResources().getDisplayMetrics().density;
mScale = mDensity;
@@ -170,14 +186,31 @@
}
public void setOverlayAdapter(ConversationViewAdapter a) {
+ if (mOverlayAdapter != null) {
+ mOverlayAdapter.unregisterDataSetObserver(mAdapterObserver);
+ clearOverlays();
+ }
mOverlayAdapter = a;
- // TODO: register/unregister for dataset notifications on the new/old adapter
+ if (mOverlayAdapter != null) {
+ mOverlayAdapter.registerDataSetObserver(mAdapterObserver);
+ }
}
public Adapter getOverlayAdapter() {
return mOverlayAdapter;
}
+ private void clearOverlays() {
+ for (int i = 0, len = mOverlayViews.size(); i < len; i++) {
+ detachOverlay(mOverlayViews.valueAt(i));
+ }
+ mOverlayViews.clear();
+ }
+
+ private void onDataSetChanged() {
+ clearOverlays();
+ }
+
private void forwardFakeMotionEvent(MotionEvent original, int newAction) {
MotionEvent newEvent = MotionEvent.obtain(original);
newEvent.setAction(newAction);
@@ -295,7 +328,7 @@
final int spacerBottomY = getOverlayBottom(spacerIndex);
// always place at least one overlay per spacer
- ConversationItem adapterItem = mOverlayAdapter.getItem(adapterIndex);
+ ConversationOverlayItem adapterItem = mOverlayAdapter.getItem(adapterIndex);
int overlayBottomY = spacerBottomY;
int overlayTopY = overlayBottomY - adapterItem.getHeight();
@@ -360,8 +393,8 @@
child.measure(childWidthSpec, childHeightSpec);
}
- private void onOverlayScrolledOff(final int adapterIndex, final View overlayView,
- final int itemType, int overlayTop, int overlayBottom) {
+ private void onOverlayScrolledOff(final int adapterIndex, final OverlayView overlay,
+ int overlayTop, int overlayBottom) {
// detach the view asynchronously, as scroll notification can happen during a draw, when
// it's not safe to remove children
@@ -371,13 +404,13 @@
post(new Runnable() {
@Override
public void run() {
- detachOverlay(overlayView, itemType);
+ detachOverlay(overlay);
}
});
// push it out of view immediately
// otherwise this scrolled-off header will continue to draw until the runnable runs
- layoutOverlay(overlayView, overlayTop, overlayBottom);
+ layoutOverlay(overlay.view, overlayTop, overlayBottom);
}
public View getScrapView(int type) {
@@ -388,11 +421,11 @@
mScrapViews.add(type, v);
}
- private void detachOverlay(View overlayView, int itemType) {
- detachViewFromParent(overlayView);
- mScrapViews.add(itemType, overlayView);
- if (overlayView instanceof DetachListener) {
- ((DetachListener) overlayView).onDetachedFromParent();
+ private void detachOverlay(OverlayView overlay) {
+ detachViewFromParent(overlay.view);
+ mScrapViews.add(overlay.itemType, overlay.view);
+ if (overlay.view instanceof DetachListener) {
+ ((DetachListener) overlay.view).onDetachedFromParent();
}
}
@@ -444,12 +477,13 @@
}
private void positionOverlay(int adapterIndex, int overlayTopY, int overlayBottomY) {
- View overlayView = mOverlayViews.get(adapterIndex);
- final ConversationItem item = mOverlayAdapter.getItem(adapterIndex);
+ final OverlayView overlay = mOverlayViews.get(adapterIndex);
+ final ConversationOverlayItem item = mOverlayAdapter.getItem(adapterIndex);
// is the overlay visible and does it have non-zero height?
if (overlayTopY != overlayBottomY && overlayBottomY > mOffsetY
&& overlayTopY < mOffsetY + getHeight()) {
+ View overlayView = overlay != null ? overlay.view : null;
// show and/or move overlay
if (overlayView == null) {
overlayView = addOverlayView(adapterIndex);
@@ -470,10 +504,9 @@
layoutOverlay(overlayView, overlayTopY, overlayTopY + overlayView.getMeasuredHeight());
} else {
// hide overlay
- if (overlayView != null) {
+ if (overlay != null) {
traceLayout("hide overlay %d", adapterIndex);
- onOverlayScrolledOff(adapterIndex, overlayView, item.getType(), overlayTopY,
- overlayBottomY);
+ onOverlayScrolledOff(adapterIndex, overlay, overlayTopY, overlayBottomY);
} else {
traceLayout("ignore non-visible overlay %d", adapterIndex);
}
@@ -493,7 +526,7 @@
final View convertView = mScrapViews.poll(itemType);
View view = mOverlayAdapter.getView(adapterIndex, convertView, this);
- mOverlayViews.put(adapterIndex, view);
+ mOverlayViews.put(adapterIndex, new OverlayView(view, itemType));
// Only re-attach if the view had previously been added to a view hierarchy.
// Since external components can contribute to the scrap heap (addScrapView), we can't
@@ -540,4 +573,11 @@
LogUtils.i(TAG, msg, params);
}
+ private class AdapterObserver extends DataSetObserver {
+ @Override
+ public void onChanged() {
+ onDataSetChanged();
+ }
+ }
+
}
diff --git a/src/com/android/mail/browse/ConversationOverlayItem.java b/src/com/android/mail/browse/ConversationOverlayItem.java
new file mode 100644
index 0000000..bc87147
--- /dev/null
+++ b/src/com/android/mail/browse/ConversationOverlayItem.java
@@ -0,0 +1,89 @@
+/*
+ * Copyright (C) 2012 Google Inc.
+ * Licensed to The Android Open Source Project.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.mail.browse;
+
+import android.content.Context;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.Adapter;
+import android.widget.CursorAdapter;
+
+import com.android.mail.utils.LogUtils;
+
+public abstract class ConversationOverlayItem {
+ private int mHeight; // in px
+ private boolean mNeedsMeasure;
+
+ public static final String LOG_TAG = new LogUtils().getLogTag();
+
+ /**
+ * @see Adapter#getItemViewType(int)
+ */
+ public abstract int getType();
+ /**
+ * Inflate and perform one-time initialization on a view for later binding.
+ */
+ public abstract View createView(Context context, LayoutInflater inflater,
+ ViewGroup parent);
+ /**
+ * @see CursorAdapter#bindView(View, Context, android.database.Cursor)
+ */
+ public abstract void bindView(View v);
+ /**
+ * Returns true if this overlay view is meant to be positioned right on top of the overlay
+ * below. This special positioning allows {@link ConversationContainer} to stack overlays
+ * together even when zoomed into a conversation, when the overlay spacers spread farther
+ * apart.
+ */
+ public abstract boolean isContiguous();
+
+ /**
+ * This method's behavior is critical and requires some 'splainin.
+ * <p>
+ * Subclasses that return a zero-size height to the {@link ConversationContainer} will
+ * cause the scrolling/recycling logic there to remove any matching view from the container.
+ * The item should switch to returning a non-zero height when its view should re-appear.
+ * <p>
+ * It's imperative that this method stay in sync with the current height of the HTML spacer
+ * that matches this overlay.
+ */
+ public int getHeight() {
+ return mHeight;
+ }
+
+ public void setHeight(int h) {
+ LogUtils.i(LOG_TAG, "IN setHeight=%dpx of overlay item: %s", h, this);
+ if (mHeight != h) {
+ mHeight = h;
+ mNeedsMeasure = true;
+ }
+ }
+
+ public boolean isMeasurementValid() {
+ return !mNeedsMeasure;
+ }
+
+ public void markMeasurementValid() {
+ mNeedsMeasure = false;
+ }
+
+ public void invalidateMeasurement() {
+ mNeedsMeasure = true;
+ }
+}
diff --git a/src/com/android/mail/browse/ConversationViewAdapter.java b/src/com/android/mail/browse/ConversationViewAdapter.java
index 6862622..c1fd1e4 100644
--- a/src/com/android/mail/browse/ConversationViewAdapter.java
+++ b/src/com/android/mail/browse/ConversationViewAdapter.java
@@ -22,22 +22,21 @@
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
-import android.widget.Adapter;
import android.widget.BaseAdapter;
-import android.widget.CursorAdapter;
import com.android.mail.FormattedDateBuilder;
import com.android.mail.R;
import com.android.mail.browse.ConversationViewHeader.ConversationViewHeaderCallbacks;
import com.android.mail.browse.MessageHeaderView.MessageHeaderViewCallbacks;
+import com.android.mail.browse.SuperCollapsedBlock.OnClickListener;
import com.android.mail.providers.Account;
import com.android.mail.providers.Address;
import com.android.mail.providers.Conversation;
import com.android.mail.providers.Message;
import com.android.mail.providers.UIProvider;
-import com.android.mail.utils.LogUtils;
import com.google.common.collect.Lists;
+import java.util.Collection;
import java.util.List;
import java.util.Map;
@@ -48,8 +47,8 @@
* message may have a header and footer, and since they are not drawn coupled together, they each
* get an adapter item.
* <p>
- * Each item in this adapter is a {@link ConversationItem} to expose enough information to
- * {@link ConversationContainer} so that it can position overlays properly.
+ * Each item in this adapter is a {@link ConversationOverlayItem} to expose enough information
+ * to {@link ConversationContainer} so that it can position overlays properly.
*
*/
public class ConversationViewAdapter extends BaseAdapter {
@@ -60,80 +59,20 @@
private final LoaderManager mLoaderManager;
private final MessageHeaderViewCallbacks mMessageCallbacks;
private ConversationViewHeaderCallbacks mConversationCallbacks;
+ private OnClickListener mSuperCollapsedListener;
private Map<String, Address> mAddressCache;
private final LayoutInflater mInflater;
private boolean mDefaultReplyAll;
- private final List<ConversationItem> mItems;
+ private final List<ConversationOverlayItem> mItems;
public static final int VIEW_TYPE_CONVERSATION_HEADER = 0;
public static final int VIEW_TYPE_MESSAGE_HEADER = 1;
public static final int VIEW_TYPE_MESSAGE_FOOTER = 2;
- public static final int VIEW_TYPE_COUNT = 3;
+ public static final int VIEW_TYPE_SUPER_COLLAPSED_BLOCK = 3;
+ public static final int VIEW_TYPE_COUNT = 4;
- public static final String LOG_TAG = new LogUtils().getLogTag();
-
- public static abstract class ConversationItem {
- private int mHeight; // in px
- private boolean mNeedsMeasure;
-
- /**
- * @see Adapter#getItemViewType(int)
- */
- public abstract int getType();
- /**
- * Inflate and perform one-time initialization on a view for later binding.
- */
- public abstract View createView(Context context, LayoutInflater inflater,
- ViewGroup parent);
- /**
- * @see CursorAdapter#bindView(View, Context, android.database.Cursor)
- */
- public abstract void bindView(View v);
- /**
- * Returns true if this overlay view is meant to be positioned right on top of the overlay
- * below. This special positioning allows {@link ConversationContainer} to stack overlays
- * together even when zoomed into a conversation, when the overlay spacers spread farther
- * apart.
- */
- public abstract boolean isContiguous();
-
- /**
- * This method's behavior is critical and requires some 'splainin.
- * <p>
- * Subclasses that return a zero-size height to the {@link ConversationContainer} will
- * cause the scrolling/recycling logic there to remove any matching view from the container.
- * The item should switch to returning a non-zero height when its view should re-appear.
- * <p>
- * It's imperative that this method stay in sync with the current height of the HTML spacer
- * that matches this overlay.
- */
- public int getHeight() {
- return mHeight;
- }
-
- public void setHeight(int h) {
- LogUtils.i(LOG_TAG, "IN setHeight=%dpx of overlay item: %s", h, this);
- if (mHeight != h) {
- mHeight = h;
- mNeedsMeasure = true;
- }
- }
-
- public boolean isMeasurementValid() {
- return !mNeedsMeasure;
- }
-
- public void markMeasurementValid() {
- mNeedsMeasure = false;
- }
-
- public void invalidateMeasurement() {
- mNeedsMeasure = true;
- }
- }
-
- public class ConversationHeaderItem extends ConversationItem {
+ public class ConversationHeaderItem extends ConversationOverlayItem {
public final Conversation mConversation;
private ConversationHeaderItem(Conversation conv) {
@@ -172,7 +111,7 @@
}
- public class MessageHeaderItem extends ConversationItem {
+ public class MessageHeaderItem extends ConversationOverlayItem {
public final Message message;
// view state variables
@@ -227,7 +166,7 @@
}
}
- public class MessageFooterItem extends ConversationItem {
+ public class MessageFooterItem extends ConversationOverlayItem {
/**
* A footer can only exist if there is a matching header. Requiring a header allows a
* footer to stay in sync with the expanded state of the header.
@@ -273,15 +212,60 @@
}
}
+ public class SuperCollapsedBlockItem extends ConversationOverlayItem {
+
+ private final int mStart;
+ private int mEnd;
+
+ private SuperCollapsedBlockItem(int start, int end) {
+ mStart = start;
+ mEnd = end;
+ }
+
+ @Override
+ public int getType() {
+ return VIEW_TYPE_SUPER_COLLAPSED_BLOCK;
+ }
+
+ @Override
+ public View createView(Context context, LayoutInflater inflater, ViewGroup parent) {
+ final SuperCollapsedBlock scb = (SuperCollapsedBlock) inflater.inflate(
+ R.layout.super_collapsed_block, parent, false);
+ scb.initialize(mSuperCollapsedListener);
+ return scb;
+ }
+
+ @Override
+ public void bindView(View v) {
+ final SuperCollapsedBlock scb = (SuperCollapsedBlock) v;
+ scb.bind(this);
+ }
+
+ @Override
+ public boolean isContiguous() {
+ return true;
+ }
+
+ public int getStart() {
+ return mStart;
+ }
+
+ public int getEnd() {
+ return mEnd;
+ }
+ }
+
public ConversationViewAdapter(Context context, Account account, LoaderManager loaderManager,
MessageHeaderViewCallbacks messageCallbacks,
- ConversationViewHeaderCallbacks convCallbacks, Map<String, Address> addressCache) {
+ ConversationViewHeaderCallbacks convCallbacks,
+ SuperCollapsedBlock.OnClickListener scbListener, Map<String, Address> addressCache) {
mContext = context;
mDateBuilder = new FormattedDateBuilder(context);
mAccount = account;
mLoaderManager = loaderManager;
mMessageCallbacks = messageCallbacks;
mConversationCallbacks = convCallbacks;
+ mSuperCollapsedListener = scbListener;
mAddressCache = addressCache;
mInflater = LayoutInflater.from(context);
@@ -308,7 +292,7 @@
}
@Override
- public ConversationItem getItem(int position) {
+ public ConversationOverlayItem getItem(int position) {
return mItems.get(position);
}
@@ -319,8 +303,11 @@
@Override
public View getView(int position, View convertView, ViewGroup parent) {
+ return getView(getItem(position), convertView, parent);
+ }
+
+ public View getView(ConversationOverlayItem item, View convertView, ViewGroup parent) {
final View v;
- final ConversationItem item = getItem(position);
if (convertView == null) {
v = item.createView(mContext, mInflater, parent);
@@ -332,7 +319,7 @@
return v;
}
- public int addItem(ConversationItem item) {
+ public int addItem(ConversationOverlayItem item) {
final int pos = mItems.size();
mItems.add(item);
notifyDataSetChanged();
@@ -356,4 +343,28 @@
return addItem(new MessageFooterItem(headerItem));
}
+ public MessageHeaderItem newMessageHeaderItem(Message message, boolean expanded) {
+ return new MessageHeaderItem(message, expanded);
+ }
+
+ public MessageFooterItem newMessageFooterItem(MessageHeaderItem headerItem) {
+ return new MessageFooterItem(headerItem);
+ }
+
+ public int addSuperCollapsedBlock(int start, int end) {
+ return addItem(new SuperCollapsedBlockItem(start, end));
+ }
+
+ public void replaceSuperCollapsedBlock(SuperCollapsedBlockItem blockToRemove,
+ Collection<ConversationOverlayItem> replacements) {
+ final int pos = mItems.indexOf(blockToRemove);
+ if (pos == -1) {
+ return;
+ }
+
+ mItems.remove(pos);
+ mItems.addAll(pos, replacements);
+ notifyDataSetChanged();
+ }
+
}
diff --git a/src/com/android/mail/browse/SuperCollapsedBlock.java b/src/com/android/mail/browse/SuperCollapsedBlock.java
new file mode 100644
index 0000000..f7fc98e
--- /dev/null
+++ b/src/com/android/mail/browse/SuperCollapsedBlock.java
@@ -0,0 +1,150 @@
+/*
+ * Copyright (C) 2012 Google Inc.
+ * Licensed to The Android Open Source Project.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.mail.browse;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.graphics.Shader.TileMode;
+import android.graphics.drawable.BitmapDrawable;
+import android.util.AttributeSet;
+import android.view.View;
+import android.widget.FrameLayout;
+import android.widget.TextView;
+
+import com.android.mail.R;
+import com.android.mail.browse.ConversationViewAdapter.SuperCollapsedBlockItem;
+import com.android.mail.utils.LogUtils;
+
+/**
+ * A header block that expands to a list of collapsed message headers. Will notify a listener on tap
+ * so the listener can hide the block and reveal the corresponding collapsed message headers.
+ *
+ */
+public class SuperCollapsedBlock extends FrameLayout implements View.OnClickListener,
+ HeaderBlock {
+
+ public interface OnClickListener {
+ /**
+ * Handle a click on a super-collapsed block.
+ *
+ */
+ void onSuperCollapsedClick(SuperCollapsedBlockItem item);
+ }
+
+ private SuperCollapsedBlockItem mModel;
+ private OnClickListener mClick;
+ private View mIconView;
+ private TextView mCountView;
+ private View mBackgroundView;
+
+ private static final String LOG_TAG = new LogUtils().getLogTag();
+
+ public SuperCollapsedBlock(Context context) {
+ this(context, null);
+ }
+
+ public SuperCollapsedBlock(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ setActivated(false);
+ setOnClickListener(this);
+ }
+
+ public void initialize(OnClickListener onClick) {
+ mClick = onClick;
+ }
+
+ @Override
+ protected void onFinishInflate() {
+ super.onFinishInflate();
+
+ mIconView = findViewById(R.id.super_collapsed_icon);
+ mCountView = (TextView) findViewById(R.id.super_collapsed_count);
+ mBackgroundView = findViewById(R.id.super_collapsed_background);
+
+ // Work around Honeycomb bug where BitmapDrawable's tileMode is unreliable in XML (5160739)
+ BitmapDrawable bd = (BitmapDrawable) getResources().getDrawable(
+ R.drawable.header_convo_view_thread_bg_holo);
+ bd.setTileModeXY(TileMode.REPEAT, TileMode.REPEAT);
+ mBackgroundView.setBackgroundDrawable(bd);
+ }
+
+ public void bind(SuperCollapsedBlockItem item) {
+ mModel = item;
+ setCount(item.getEnd() - item.getStart() + 1);
+ }
+
+ public void setCount(int count) {
+ mCountView.setText(Integer.toString(count));
+ mIconView.getBackground().setLevel(count);
+ }
+
+ @Override
+ public void onClick(final View v) {
+ ((TextView) findViewById(R.id.super_collapsed_label)).setText(
+ R.string.loading_conversation);
+ mCountView.setVisibility(GONE);
+
+ if (mClick != null) {
+ getHandler().post(new Runnable() {
+ @Override
+ public void run() {
+ mClick.onSuperCollapsedClick(mModel);
+ }
+ });
+ }
+ }
+
+ public static int getCannedHeight(Context context) {
+ Resources r = context.getResources();
+ // Rather than try to measure the height a super-collapsed block, just add up the known
+ // vertical dimension components.
+ return r.getDimensionPixelSize(R.dimen.super_collapsed_height)
+ + r.getDimensionPixelOffset(R.dimen.message_header_vertical_margin);
+ }
+
+ @Override
+ public boolean canSnap() {
+ return false;
+ }
+
+ @Override
+ public MessageHeaderView getSnapView() {
+ return null;
+ }
+
+ @Override
+ public void setMarginBottom(int height) {
+ // no-op. should never have a matching body.
+
+ // sanity check
+ if (height != 0) {
+ LogUtils.d(LOG_TAG, "super-collapsed block yielded unexpected body height: %d", height);
+ }
+ }
+
+ @Override
+ public void updateContactInfo() {
+ // no-op
+ }
+
+ @Override
+ public void setStarDisplay(boolean starred) {
+ // no-op
+ }
+
+}
diff --git a/src/com/android/mail/ui/ConversationViewFragment.java b/src/com/android/mail/ui/ConversationViewFragment.java
index 6cf2e69..914cf9c 100644
--- a/src/com/android/mail/ui/ConversationViewFragment.java
+++ b/src/com/android/mail/ui/ConversationViewFragment.java
@@ -34,7 +34,6 @@
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
-import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.webkit.ConsoleMessage;
@@ -45,14 +44,17 @@
import com.android.mail.R;
import com.android.mail.browse.ConversationContainer;
+import com.android.mail.browse.ConversationOverlayItem;
import com.android.mail.browse.ConversationViewAdapter;
-import com.android.mail.browse.ConversationViewAdapter.ConversationItem;
+import com.android.mail.browse.ConversationViewAdapter.MessageFooterItem;
import com.android.mail.browse.ConversationViewAdapter.MessageHeaderItem;
+import com.android.mail.browse.ConversationViewAdapter.SuperCollapsedBlockItem;
import com.android.mail.browse.ConversationViewHeader;
import com.android.mail.browse.ConversationWebView;
import com.android.mail.browse.MessageCursor;
import com.android.mail.browse.MessageFooterView;
import com.android.mail.browse.MessageHeaderView.MessageHeaderViewCallbacks;
+import com.android.mail.browse.SuperCollapsedBlock;
import com.android.mail.providers.Account;
import com.android.mail.providers.Address;
import com.android.mail.providers.Conversation;
@@ -65,8 +67,10 @@
import com.android.mail.providers.UIProvider.FolderCapabilities;
import com.android.mail.utils.LogUtils;
import com.android.mail.utils.Utils;
+import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
+import java.util.List;
import java.util.Map;
@@ -76,7 +80,8 @@
public final class ConversationViewFragment extends Fragment implements
LoaderManager.LoaderCallbacks<Cursor>,
ConversationViewHeader.ConversationViewHeaderCallbacks,
- MessageHeaderViewCallbacks {
+ MessageHeaderViewCallbacks,
+ SuperCollapsedBlock.OnClickListener {
private static final String LOG_TAG = new LogUtils().getLogTag();
@@ -117,6 +122,14 @@
private final Map<String, Address> mAddressCache = Maps.newHashMap();
+ /**
+ * Temporary string containing the message bodies of the messages within a super-collapsed
+ * block, for one-time use during block expansion. We cannot easily pass the body HTML
+ * into JS without problematic escaping, so hold onto it momentarily and signal JS to fetch it
+ * using {@link MailJsBridge}.
+ */
+ private String mTempBodiesHtml;
+
private static final String ARG_ACCOUNT = "account";
private static final String ARG_CONVERSATION = "conversation";
private static final String ARG_FOLDER = "folder";
@@ -165,7 +178,7 @@
mTemplates = new HtmlConversationTemplates(mContext);
mAdapter = new ConversationViewAdapter(mActivity.getActivityContext(), mAccount,
- getLoaderManager(), this, this, mAddressCache);
+ getLoaderManager(), this, this, this, mAddressCache);
mConversationContainer.setOverlayAdapter(mAdapter);
mDensity = getResources().getDisplayMetrics().density;
@@ -227,6 +240,8 @@
@Override
public void onDestroyView() {
super.onDestroyView();
+ mConversationContainer.setOverlayAdapter(null);
+ mAdapter = null;
mViewsCreated = false;
}
@@ -357,27 +372,44 @@
mTemplates.startConversation(convHeaderDp);
+ int collapsedStart = -1;
+ Message prevCollapsedMsg = null;
+ boolean prevSafeForImages = false;
+
while (messageCursor.moveToPosition(++pos)) {
final Message msg = messageCursor.getMessage();
+
// TODO: save/restore 'show pics' state
final boolean safeForImages = msg.alwaysShowImages /* || savedStateSaysSafe */;
allowNetworkImages |= safeForImages;
final boolean expanded = !msg.read || msg.starred || messageCursor.isLast();
- final int headerPos = mAdapter.addMessageHeader(msg, expanded);
- final MessageHeaderItem headerItem = (MessageHeaderItem) mAdapter.getItem(headerPos);
+ if (!expanded) {
+ // contribute to a super-collapsed block that will be emitted just before the next
+ // expanded header
+ if (collapsedStart < 0) {
+ collapsedStart = pos;
+ }
+ prevCollapsedMsg = msg;
+ prevSafeForImages = safeForImages;
+ continue;
+ }
- final int footerPos = mAdapter.addMessageFooter(headerItem);
+ // resolve any deferred decisions on previous collapsed items
+ if (collapsedStart >= 0) {
+ if (pos - collapsedStart == 1) {
+ // special-case for a single collapsed message: no need to super-collapse it
+ renderMessage(prevCollapsedMsg, false /* expanded */,
+ prevSafeForImages);
+ } else {
+ renderSuperCollapsedBlock(collapsedStart, pos - 1);
+ }
+ prevCollapsedMsg = null;
+ collapsedStart = -1;
+ }
- // Measure item header and footer heights to allocate spacers in HTML
- // But since the views themselves don't exist yet, render each item temporarily into
- // a host view for measurement.
- final int headerDp = measureOverlayHeight(headerPos);
- final int footerDp = measureOverlayHeight(footerPos);
-
- mTemplates.appendMessageHtml(msg, expanded, safeForImages, 1.0f, headerDp,
- footerDp);
+ renderMessage(msg, expanded, safeForImages);
}
// Re-enable attachment loaders
@@ -388,24 +420,76 @@
return mTemplates.endConversation(mBaseUri, 320);
}
+ private void renderSuperCollapsedBlock(int start, int end) {
+ final int blockPos = mAdapter.addSuperCollapsedBlock(start, end);
+ final int blockDp = measureOverlayHeight(blockPos);
+ mTemplates.appendSuperCollapsedHtml(start, blockDp);
+ }
+
+ private void renderMessage(Message msg, boolean expanded, boolean safeForImages) {
+ final int headerPos = mAdapter.addMessageHeader(msg, expanded);
+ final MessageHeaderItem headerItem = (MessageHeaderItem) mAdapter.getItem(headerPos);
+
+ final int footerPos = mAdapter.addMessageFooter(headerItem);
+
+ // Measure item header and footer heights to allocate spacers in HTML
+ // But since the views themselves don't exist yet, render each item temporarily into
+ // a host view for measurement.
+ final int headerDp = measureOverlayHeight(headerPos);
+ final int footerDp = measureOverlayHeight(footerPos);
+
+ mTemplates.appendMessageHtml(msg, expanded, safeForImages, 1.0f, headerDp,
+ footerDp);
+ }
+
+ private String renderCollapsedHeaders(MessageCursor cursor,
+ SuperCollapsedBlockItem blockToReplace) {
+ final List<ConversationOverlayItem> replacements = Lists.newArrayList();
+
+ mTemplates.reset();
+
+ for (int i = blockToReplace.getStart(), end = blockToReplace.getEnd(); i <= end; i++) {
+ cursor.moveToPosition(i);
+ final Message msg = cursor.getMessage();
+ final MessageHeaderItem header = mAdapter.newMessageHeaderItem(msg,
+ false /* expanded */);
+ final MessageFooterItem footer = mAdapter.newMessageFooterItem(header);
+
+ final int headerDp = measureOverlayHeight(header);
+ final int footerDp = measureOverlayHeight(footer);
+
+ mTemplates.appendMessageHtml(msg, false /* expanded */, msg.alwaysShowImages, 1.0f,
+ headerDp, footerDp);
+ replacements.add(header);
+ replacements.add(footer);
+ }
+
+ mAdapter.replaceSuperCollapsedBlock(blockToReplace, replacements);
+
+ return mTemplates.emit();
+ }
+
+ private int measureOverlayHeight(int position) {
+ return measureOverlayHeight(mAdapter.getItem(position));
+ }
+
/**
- * Measure the height of an adapter view by rendering the data in the adapter into a temporary
- * host view, and asking the adapter item to immediately measure itself. This method will reuse
+ * Measure the height of an adapter view by rendering and adapter item into a temporary
+ * host view, and asking the view to immediately measure itself. This method will reuse
* a previous adapter view from {@link ConversationContainer}'s scrap views if one was generated
* earlier.
* <p>
- * After measuring the height, this method also saves the height in the {@link ConversationItem}
- * for later use in overlay positioning.
+ * After measuring the height, this method also saves the height in the
+ * {@link ConversationOverlayItem} for later use in overlay positioning.
*
- * @param position index into the adapter
+ * @param convItem adapter item with data to render and measure
* @return height in dp of the rendered view
*/
- private int measureOverlayHeight(int position) {
- final ConversationItem convItem = mAdapter.getItem(position);
+ private int measureOverlayHeight(ConversationOverlayItem convItem) {
final int type = convItem.getType();
final View convertView = mConversationContainer.getScrapView(type);
- final View hostView = mAdapter.getView(position, convertView, mConversationContainer);
+ final View hostView = mAdapter.getView(convItem, convertView, mConversationContainer);
if (convertView == null) {
mConversationContainer.addScrapView(type, hostView);
}
@@ -471,6 +555,16 @@
}
// END message header callbacks
+ @Override
+ public void onSuperCollapsedClick(SuperCollapsedBlockItem item) {
+ if (mCursor == null || !mViewsCreated) {
+ return;
+ }
+
+ mTempBodiesHtml = renderCollapsedHeaders(mCursor, item);
+ mWebView.loadUrl("javascript:replaceSuperCollapsedBlock(" + item.getStart() + ")");
+ }
+
private static class MessageLoader extends CursorLoader {
private boolean mDeliveredFirstResults = false;
@@ -533,17 +627,22 @@
@Override
public boolean shouldOverrideUrlLoading(WebView view, String url) {
+ final Activity activity = getActivity();
+ if (!mViewsCreated || activity == null) {
+ return false;
+ }
+
boolean result = false;
final Uri uri = Uri.parse(url);
Intent intent = new Intent(Intent.ACTION_VIEW, uri);
- intent.putExtra(Browser.EXTRA_APPLICATION_ID, getActivity().getPackageName());
+ intent.putExtra(Browser.EXTRA_APPLICATION_ID, activity.getPackageName());
intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET);
// FIXME: give provider a chance to customize url intents?
// Utils.addGoogleUriAccountIntentExtras(mContext, uri, mAccount, intent);
try {
- mActivity.getActivityContext().startActivity(intent);
+ activity.startActivity(intent);
result = true;
} catch (ActivityNotFoundException ex) {
// If no application can handle the URL, assume that the
@@ -564,18 +663,38 @@
@SuppressWarnings("unused")
public void onWebContentGeometryChange(final String[] overlayBottomStrs) {
- mHandler.post(new Runnable() {
- @Override
- public void run() {
- if (!mViewsCreated) {
- LogUtils.d(LOG_TAG, "ignoring webContentGeometryChange because views" +
- " are gone, %s", ConversationViewFragment.this);
- return;
- }
+ try {
+ mHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ if (!mViewsCreated) {
+ LogUtils.d(LOG_TAG, "ignoring webContentGeometryChange because views" +
+ " are gone, %s", ConversationViewFragment.this);
+ return;
+ }
- mConversationContainer.onGeometryChange(parseInts(overlayBottomStrs));
+ mConversationContainer.onGeometryChange(parseInts(overlayBottomStrs));
+ }
+ });
+ } catch (Throwable t) {
+ LogUtils.e(LOG_TAG, t, "Error in MailJsBridge.onWebContentGeometryChange");
+ }
+ }
+
+ @SuppressWarnings("unused")
+ public String getTempMessageBodies() {
+ try {
+ if (!mViewsCreated) {
+ return "";
}
- });
+
+ final String s = mTempBodiesHtml;
+ mTempBodiesHtml = null;
+ return s;
+ } catch (Throwable t) {
+ LogUtils.e(LOG_TAG, t, "Error in MailJsBridge.getTempMessageBodies");
+ return "";
+ }
}
}
diff --git a/src/com/android/mail/ui/HtmlConversationTemplates.java b/src/com/android/mail/ui/HtmlConversationTemplates.java
index 7fae644..5123988 100644
--- a/src/com/android/mail/ui/HtmlConversationTemplates.java
+++ b/src/com/android/mail/ui/HtmlConversationTemplates.java
@@ -202,7 +202,7 @@
return emit();
}
- private String emit() {
+ public String emit() {
String out = mFormatter.toString();
// release the builder memory ASAP
mFormatter = null;