/*
* 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.app.FragmentManager;
import android.app.LoaderManager;
import android.content.Context;
import android.support.annotation.IntDef;
import android.support.v4.text.BidiFormatter;
import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewParent;
import android.widget.BaseAdapter;
import com.android.emailcommon.mail.Address;
import com.android.mail.ContactInfoSource;
import com.android.mail.FormattedDateBuilder;
import com.android.mail.R;
import com.android.mail.browse.ConversationFooterView.ConversationFooterCallbacks;
import com.android.mail.browse.ConversationViewHeader.ConversationViewHeaderCallbacks;
import com.android.mail.browse.MessageFooterView.MessageFooterCallbacks;
import com.android.mail.browse.MessageHeaderView.MessageHeaderViewCallbacks;
import com.android.mail.browse.SuperCollapsedBlock.OnClickListener;
import com.android.mail.providers.Conversation;
import com.android.mail.providers.UIProvider;
import com.android.mail.ui.ControllableActivity;
import com.android.mail.ui.ConversationUpdater;
import com.android.mail.utils.LogTag;
import com.android.mail.utils.LogUtils;
import com.android.mail.utils.VeiledAddressMatcher;
import com.google.common.base.Objects;
import com.google.common.collect.Lists;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* A specialized adapter that contains overlay views to draw on top of the underlying conversation
* WebView. Each independently drawn overlay view gets its own item in this adapter, and indices
* in this adapter do not necessarily line up with cursor indices. For example, an expanded
* message may have a header and footer, and since they are not drawn coupled together, they each
* get an adapter item.
*
* 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 {
private static final String LOG_TAG = LogTag.getLogTag();
private static final String OVERLAY_ITEM_ROOT_TAG = "overlay_item_root";
private final Context mContext;
private final FormattedDateBuilder mDateBuilder;
private final ConversationAccountController mAccountController;
private final LoaderManager mLoaderManager;
private final FragmentManager mFragmentManager;
private final MessageHeaderViewCallbacks mMessageCallbacks;
private final MessageFooterCallbacks mFooterCallbacks;
private final ContactInfoSource mContactInfoSource;
private final ConversationViewHeaderCallbacks mConversationCallbacks;
private final ConversationFooterCallbacks mConversationFooterCallbacks;
private final ConversationUpdater mConversationUpdater;
private final OnClickListener mSuperCollapsedListener;
private final Map mAddressCache;
private final LayoutInflater mInflater;
private final List mItems;
private final VeiledAddressMatcher mMatcher;
@Retention(RetentionPolicy.SOURCE)
@IntDef({
VIEW_TYPE_CONVERSATION_HEADER,
VIEW_TYPE_CONVERSATION_FOOTER,
VIEW_TYPE_MESSAGE_HEADER,
VIEW_TYPE_MESSAGE_FOOTER,
VIEW_TYPE_SUPER_COLLAPSED_BLOCK,
VIEW_TYPE_AD_HEADER,
VIEW_TYPE_AD_SENDER_HEADER,
VIEW_TYPE_AD_FOOTER
})
public @interface ConversationViewType {}
public static final int VIEW_TYPE_CONVERSATION_HEADER = 0;
public static final int VIEW_TYPE_CONVERSATION_FOOTER = 1;
public static final int VIEW_TYPE_MESSAGE_HEADER = 2;
public static final int VIEW_TYPE_MESSAGE_FOOTER = 3;
public static final int VIEW_TYPE_SUPER_COLLAPSED_BLOCK = 4;
public static final int VIEW_TYPE_AD_HEADER = 5;
public static final int VIEW_TYPE_AD_SENDER_HEADER = 6;
public static final int VIEW_TYPE_AD_FOOTER = 7;
public static final int VIEW_TYPE_COUNT = 8;
private final BidiFormatter mBidiFormatter;
private final View.OnKeyListener mOnKeyListener;
public class ConversationHeaderItem extends ConversationOverlayItem {
public final Conversation mConversation;
private ConversationHeaderItem(Conversation conv) {
mConversation = conv;
}
@Override
public @ConversationViewType int getType() {
return VIEW_TYPE_CONVERSATION_HEADER;
}
@Override
public View createView(Context context, LayoutInflater inflater, ViewGroup parent) {
final ConversationViewHeader v = (ConversationViewHeader) inflater.inflate(
R.layout.conversation_view_header, parent, false);
v.setCallbacks(
mConversationCallbacks, mAccountController, mConversationUpdater);
v.setSubject(mConversation.subject);
if (mAccountController.getAccount().supportsCapability(
UIProvider.AccountCapabilities.MULTIPLE_FOLDERS_PER_CONV)) {
v.setFolders(mConversation);
}
v.setStarred(mConversation.starred);
v.setTag(OVERLAY_ITEM_ROOT_TAG);
return v;
}
@Override
public void bindView(View v, boolean measureOnly) {
ConversationViewHeader header = (ConversationViewHeader) v;
header.bind(this);
}
@Override
public boolean isContiguous() {
return true;
}
@Override
public View.OnKeyListener getOnKeyListener() {
return mOnKeyListener;
}
public ConversationViewAdapter getAdapter() {
return ConversationViewAdapter.this;
}
}
public class ConversationFooterItem extends ConversationOverlayItem {
private MessageHeaderItem mLastMessageHeaderItem;
public ConversationFooterItem(MessageHeaderItem lastMessageHeaderItem) {
setLastMessageHeaderItem(lastMessageHeaderItem);
}
@Override
public @ConversationViewType int getType() {
return VIEW_TYPE_CONVERSATION_FOOTER;
}
@Override
public View createView(Context context, LayoutInflater inflater, ViewGroup parent) {
final ConversationFooterView v = (ConversationFooterView)
inflater.inflate(R.layout.conversation_footer, parent, false);
v.setAccountController(mAccountController);
v.setConversationFooterCallbacks(mConversationFooterCallbacks);
v.setTag(OVERLAY_ITEM_ROOT_TAG);
// Register the onkey listener for all relevant views
registerOnKeyListeners(v, v.findViewById(R.id.reply_button),
v.findViewById(R.id.reply_all_button), v.findViewById(R.id.forward_button));
return v;
}
@Override
public void bindView(View v, boolean measureOnly) {
((ConversationFooterView) v).bind(this);
mRootView = v;
}
@Override
public void rebindView(View view) {
((ConversationFooterView) view).rebind(this);
mRootView = view;
}
@Override
public View getFocusableView() {
return mRootView.findViewById(R.id.reply_button);
}
@Override
public boolean isContiguous() {
return true;
}
@Override
public View.OnKeyListener getOnKeyListener() {
return mOnKeyListener;
}
public MessageHeaderItem getLastMessageHeaderItem() {
return mLastMessageHeaderItem;
}
public void setLastMessageHeaderItem(MessageHeaderItem lastMessageHeaderItem) {
mLastMessageHeaderItem = lastMessageHeaderItem;
}
}
public static class MessageHeaderItem extends ConversationOverlayItem {
private final ConversationViewAdapter mAdapter;
private ConversationMessage mMessage;
// view state variables
private boolean mExpanded;
public boolean detailsExpanded;
private boolean mShowImages;
// cached values to speed up re-rendering during view recycling
private CharSequence mTimestampShort;
private CharSequence mTimestampLong;
private CharSequence mTimestampFull;
private long mTimestampMs;
private final FormattedDateBuilder mDateBuilder;
public CharSequence recipientSummaryText;
MessageHeaderItem(ConversationViewAdapter adapter, FormattedDateBuilder dateBuilder,
ConversationMessage message, boolean expanded, boolean showImages) {
mAdapter = adapter;
mDateBuilder = dateBuilder;
mMessage = message;
mExpanded = expanded;
mShowImages = showImages;
detailsExpanded = false;
}
public ConversationMessage getMessage() {
return mMessage;
}
@Override
public @ConversationViewType int getType() {
return VIEW_TYPE_MESSAGE_HEADER;
}
@Override
public View createView(Context context, LayoutInflater inflater, ViewGroup parent) {
final MessageHeaderView v = (MessageHeaderView) inflater.inflate(
R.layout.conversation_message_header, parent, false);
v.initialize(mAdapter.mAccountController,
mAdapter.mAddressCache);
v.setCallbacks(mAdapter.mMessageCallbacks);
v.setContactInfoSource(mAdapter.mContactInfoSource);
v.setVeiledMatcher(mAdapter.mMatcher);
v.setTag(OVERLAY_ITEM_ROOT_TAG);
// Register the onkey listener for all relevant views
registerOnKeyListeners(v, v.findViewById(R.id.upper_header),
v.findViewById(R.id.hide_details), v.findViewById(R.id.edit_draft),
v.findViewById(R.id.reply), v.findViewById(R.id.reply_all),
v.findViewById(R.id.overflow), v.findViewById(R.id.send_date));
return v;
}
@Override
public void bindView(View v, boolean measureOnly) {
final MessageHeaderView header = (MessageHeaderView) v;
header.bind(this, measureOnly);
mRootView = v;
}
@Override
public View getFocusableView() {
return mRootView.findViewById(R.id.upper_header);
}
@Override
public void onModelUpdated(View v) {
final MessageHeaderView header = (MessageHeaderView) v;
header.refresh();
}
@Override
public boolean isContiguous() {
return !isExpanded();
}
@Override
public View.OnKeyListener getOnKeyListener() {
return mAdapter.getOnKeyListener();
}
@Override
public boolean isExpanded() {
return mExpanded;
}
public void setExpanded(boolean expanded) {
if (mExpanded != expanded) {
mExpanded = expanded;
}
}
public boolean getShowImages() {
return mShowImages;
}
public void setShowImages(boolean showImages) {
mShowImages = showImages;
}
@Override
public boolean canBecomeSnapHeader() {
return isExpanded();
}
@Override
public boolean canPushSnapHeader() {
return true;
}
@Override
public boolean belongsToMessage(ConversationMessage message) {
return Objects.equal(mMessage, message);
}
@Override
public void setMessage(ConversationMessage message) {
mMessage = message;
// setMessage signifies an in-place update to the message, so let's clear out recipient
// summary text so the view will refresh it on the next render.
recipientSummaryText = null;
}
public CharSequence getTimestampShort() {
ensureTimestamps();
return mTimestampShort;
}
public CharSequence getTimestampLong() {
ensureTimestamps();
return mTimestampLong;
}
public CharSequence getTimestampFull() {
ensureTimestamps();
return mTimestampFull;
}
private void ensureTimestamps() {
if (mMessage.dateReceivedMs != mTimestampMs) {
mTimestampMs = mMessage.dateReceivedMs;
mTimestampShort = mDateBuilder.formatShortDateTime(mTimestampMs);
mTimestampLong = mDateBuilder.formatLongDateTime(mTimestampMs);
mTimestampFull = mDateBuilder.formatFullDateTime(mTimestampMs);
}
}
public ConversationViewAdapter getAdapter() {
return mAdapter;
}
@Override
public void rebindView(View view) {
final MessageHeaderView header = (MessageHeaderView) view;
header.rebind(this);
mRootView = view;
}
}
public static class MessageFooterItem extends ConversationOverlayItem {
private final ConversationViewAdapter mAdapter;
/**
* 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.
*/
private final MessageHeaderItem mHeaderItem;
private MessageFooterItem(ConversationViewAdapter adapter, MessageHeaderItem item) {
mAdapter = adapter;
mHeaderItem = item;
}
@Override
public @ConversationViewType int getType() {
return VIEW_TYPE_MESSAGE_FOOTER;
}
@Override
public View createView(Context context, LayoutInflater inflater, ViewGroup parent) {
final MessageFooterView v = (MessageFooterView) inflater.inflate(
R.layout.conversation_message_footer, parent, false);
v.initialize(mAdapter.mLoaderManager, mAdapter.mFragmentManager,
mAdapter.mAccountController, mAdapter.mFooterCallbacks);
v.setTag(OVERLAY_ITEM_ROOT_TAG);
// Register the onkey listener for all relevant views
registerOnKeyListeners(v, v.findViewById(R.id.view_entire_message_prompt));
return v;
}
@Override
public void bindView(View v, boolean measureOnly) {
final MessageFooterView attachmentsView = (MessageFooterView) v;
attachmentsView.bind(mHeaderItem, measureOnly);
mRootView = v;
}
@Override
public boolean isContiguous() {
return true;
}
@Override
public View.OnKeyListener getOnKeyListener() {
return mAdapter.getOnKeyListener();
}
@Override
public boolean isExpanded() {
return mHeaderItem.isExpanded();
}
@Override
public int getGravity() {
// attachments are top-aligned within their spacer area
// Attachments should stay near the body they belong to, even when zoomed far in.
return Gravity.TOP;
}
@Override
public int getHeight() {
// a footer may change height while its view does not exist because it is offscreen
// (but the header is onscreen and thus collapsible)
if (!mHeaderItem.isExpanded()) {
return 0;
}
return super.getHeight();
}
public MessageHeaderItem getHeaderItem() {
return mHeaderItem;
}
}
public class SuperCollapsedBlockItem extends ConversationOverlayItem {
private final int mStart;
private final int mEnd;
private final boolean mHasDraft;
private SuperCollapsedBlockItem(int start, int end, boolean hasDraft) {
mStart = start;
mEnd = end;
mHasDraft = hasDraft;
}
@Override
public @ConversationViewType int getType() {
return VIEW_TYPE_SUPER_COLLAPSED_BLOCK;
}
@Override
public View createView(Context context, LayoutInflater inflater, ViewGroup parent) {
final SuperCollapsedBlock v = (SuperCollapsedBlock) inflater.inflate(
R.layout.super_collapsed_block, parent, false);
v.initialize(mSuperCollapsedListener);
v.setOnKeyListener(mOnKeyListener);
v.setTag(OVERLAY_ITEM_ROOT_TAG);
// Register the onkey listener for all relevant views
registerOnKeyListeners(v);
return v;
}
@Override
public void bindView(View v, boolean measureOnly) {
final SuperCollapsedBlock scb = (SuperCollapsedBlock) v;
scb.bind(this);
mRootView = v;
}
@Override
public boolean isContiguous() {
return true;
}
@Override
public View.OnKeyListener getOnKeyListener() {
return mOnKeyListener;
}
@Override
public boolean isExpanded() {
return false;
}
public int getStart() {
return mStart;
}
public int getEnd() {
return mEnd;
}
public boolean hasDraft() {
return mHasDraft;
}
@Override
public boolean canPushSnapHeader() {
return true;
}
}
public ConversationViewAdapter(ControllableActivity controllableActivity,
ConversationAccountController accountController,
LoaderManager loaderManager,
MessageHeaderViewCallbacks messageCallbacks,
MessageFooterCallbacks footerCallbacks,
ContactInfoSource contactInfoSource,
ConversationViewHeaderCallbacks convCallbacks,
ConversationFooterCallbacks convFooterCallbacks,
ConversationUpdater conversationUpdater,
OnClickListener scbListener,
Map addressCache,
FormattedDateBuilder dateBuilder,
BidiFormatter bidiFormatter,
View.OnKeyListener onKeyListener) {
mContext = controllableActivity.getActivityContext();
mDateBuilder = dateBuilder;
mAccountController = accountController;
mLoaderManager = loaderManager;
mFragmentManager = controllableActivity.getFragmentManager();
mMessageCallbacks = messageCallbacks;
mFooterCallbacks = footerCallbacks;
mContactInfoSource = contactInfoSource;
mConversationCallbacks = convCallbacks;
mConversationFooterCallbacks = convFooterCallbacks;
mConversationUpdater = conversationUpdater;
mSuperCollapsedListener = scbListener;
mAddressCache = addressCache;
mInflater = LayoutInflater.from(mContext);
mItems = Lists.newArrayList();
mMatcher = controllableActivity.getAccountController().getVeiledAddressMatcher();
mBidiFormatter = bidiFormatter;
mOnKeyListener = onKeyListener;
}
@Override
public int getCount() {
return mItems.size();
}
@Override
public @ConversationViewType int getItemViewType(int position) {
return mItems.get(position).getType();
}
@Override
public int getViewTypeCount() {
return VIEW_TYPE_COUNT;
}
@Override
public ConversationOverlayItem getItem(int position) {
return mItems.get(position);
}
@Override
public long getItemId(int position) {
return position; // TODO: ensure this works well enough
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
return getView(getItem(position), convertView, parent, false /* measureOnly */);
}
public View getView(ConversationOverlayItem item, View convertView, ViewGroup parent,
boolean measureOnly) {
final View v;
if (convertView == null) {
v = item.createView(mContext, mInflater, parent);
} else {
v = convertView;
}
item.bindView(v, measureOnly);
return v;
}
public LayoutInflater getLayoutInflater() {
return mInflater;
}
public FormattedDateBuilder getDateBuilder() {
return mDateBuilder;
}
public int addItem(ConversationOverlayItem item) {
final int pos = mItems.size();
item.setPosition(pos);
mItems.add(item);
return pos;
}
public void clear() {
mItems.clear();
notifyDataSetChanged();
}
public int addConversationHeader(Conversation conv) {
return addItem(new ConversationHeaderItem(conv));
}
public int addConversationFooter(MessageHeaderItem headerItem) {
return addItem(new ConversationFooterItem(headerItem));
}
public int addMessageHeader(ConversationMessage msg, boolean expanded, boolean showImages) {
return addItem(new MessageHeaderItem(this, mDateBuilder, msg, expanded, showImages));
}
public int addMessageFooter(MessageHeaderItem headerItem) {
return addItem(new MessageFooterItem(this, headerItem));
}
public static MessageHeaderItem newMessageHeaderItem(ConversationViewAdapter adapter,
FormattedDateBuilder dateBuilder, ConversationMessage message,
boolean expanded, boolean showImages) {
return new MessageHeaderItem(adapter, dateBuilder, message, expanded, showImages);
}
public static MessageFooterItem newMessageFooterItem(
ConversationViewAdapter adapter, MessageHeaderItem headerItem) {
return new MessageFooterItem(adapter, headerItem);
}
public int addSuperCollapsedBlock(int start, int end, boolean hasDraft) {
return addItem(new SuperCollapsedBlockItem(start, end, hasDraft));
}
public void replaceSuperCollapsedBlock(SuperCollapsedBlockItem blockToRemove,
Collection replacements) {
final int pos = mItems.indexOf(blockToRemove);
if (pos == -1) {
return;
}
mItems.remove(pos);
mItems.addAll(pos, replacements);
// update position for all items
for (int i = 0, size = mItems.size(); i < size; i++) {
mItems.get(i).setPosition(i);
}
}
public void updateItemsForMessage(ConversationMessage message,
List affectedPositions) {
for (int i = 0, len = mItems.size(); i < len; i++) {
final ConversationOverlayItem item = mItems.get(i);
if (item.belongsToMessage(message)) {
item.setMessage(message);
affectedPositions.add(i);
}
}
}
/**
* Remove and return the {@link ConversationFooterItem} from the adapter.
*/
public ConversationFooterItem removeFooterItem() {
final int count = mItems.size();
if (count < 4) {
LogUtils.e(LOG_TAG, "not enough items in the adapter. count: %s", count);
return null;
}
final ConversationFooterItem item = (ConversationFooterItem) mItems.remove(count - 1);
if (item == null) {
LogUtils.e(LOG_TAG, "removed wrong overlay item: %s", item);
return null;
}
return item;
}
public ConversationFooterItem getFooterItem() {
final int count = mItems.size();
if (count < 4) {
LogUtils.e(LOG_TAG, "not enough items in the adapter. count: %s", count);
return null;
}
final ConversationOverlayItem item = mItems.get(count - 1);
try {
return (ConversationFooterItem) item;
} catch (ClassCastException e) {
LogUtils.e(LOG_TAG, "Last item is not a conversation footer. type: %s", item.getType());
return null;
}
}
/**
* Returns true if the item before this one is of type
* {@link #VIEW_TYPE_SUPER_COLLAPSED_BLOCK}.
*/
public boolean isPreviousItemSuperCollapsed(ConversationOverlayItem item) {
// super-collapsed will be the item just before the header
final int position = item.getPosition() - 1;
final int count = mItems.size();
return !(position < 0 || position >= count)
&& mItems.get(position).getType() == VIEW_TYPE_SUPER_COLLAPSED_BLOCK;
}
// This should be a safe call since all containers should have at least a conv header and a
// message header.
public boolean focusFirstMessageHeader() {
if (mItems.size() > 1) {
final View v = mItems.get(1).getFocusableView();
if (v != null && v.isShown() && v.isFocusable()) {
v.requestFocus();
return true;
}
}
return false;
}
/**
* Find the next view that should grab focus with respect to the current position.
*/
public View getNextOverlayView(View curr, boolean isDown, Set scraps) {
// First find the root view of the overlay item
while (curr.getTag() != OVERLAY_ITEM_ROOT_TAG) {
final ViewParent parent = curr.getParent();
if (parent != null && parent instanceof View) {
curr = (View) parent;
} else {
return null;
}
}
// Find the position of the root view
for (int i = 0; i < mItems.size(); i++) {
if (mItems.get(i).mRootView == curr) {
// Found view, now find the next applicable view
if (isDown && i >= 0) {
while (++i < mItems.size()) {
final ConversationOverlayItem item = mItems.get(i);
final View next = item.getFocusableView();
if (item.mRootView != null && !scraps.contains(item.mRootView) &&
next != null && next.isFocusable()) {
return next;
}
}
} else {
while (--i >= 0) {
final ConversationOverlayItem item = mItems.get(i);
final View next = item.getFocusableView();
if (item.mRootView != null && !scraps.contains(item.mRootView) &&
next != null && next.isFocusable()) {
return next;
}
}
}
return null;
}
}
return null;
}
public BidiFormatter getBidiFormatter() {
return mBidiFormatter;
}
public View.OnKeyListener getOnKeyListener() {
return mOnKeyListener;
}
}