true
, we have restored (or attempted to restore) the list's scroll position
* from when we were last on this conversation list.
*/
private boolean mScrollPositionRestored = false;
/**
* Constructor needs to be public to handle orientation changes and activity
* lifecycle events.
*/
public ConversationListFragment() {
super();
}
private class ConversationCursorObserver extends DataSetObserver {
@Override
public void onChanged() {
onConversationListStatusUpdated();
}
}
/**
* Creates a new instance of {@link ConversationListFragment}, initialized
* to display conversation list context.
*/
public static ConversationListFragment newInstance(ConversationListContext viewContext) {
final ConversationListFragment fragment = new ConversationListFragment();
final Bundle args = new Bundle(1);
args.putBundle(CONVERSATION_LIST_KEY, viewContext.toBundle());
fragment.setArguments(args);
return fragment;
}
/**
* Show the header if the current conversation list is showing search
* results.
*/
void configureSearchResultHeader() {
if (mActivity == null) {
return;
}
// Only show the header if the context is for a search result
final Resources res = getResources();
final boolean showHeader = ConversationListContext.isSearchResult(mViewContext);
// TODO(viki): This code contains intimate understanding of the view.
// Much of this logic
// needs to reside in a separate class that handles the text view in
// isolation. Then,
// that logic can be reused in other fragments.
if (showHeader) {
mSearchStatusTextView.setText(res.getString(R.string.search_results_searching_header));
// Initially reset the count
mSearchResultCountTextView.setText("");
}
mSearchStatusView.setVisibility(showHeader ? View.VISIBLE : View.GONE);
int marginTop = showHeader ? (int) res.getDimension(R.dimen.notification_view_height) : 0;
MarginLayoutParams layoutParams = (MarginLayoutParams) mListView.getLayoutParams();
layoutParams.topMargin = marginTop;
mListView.setLayoutParams(layoutParams);
}
/**
* Show the header if the current conversation list is showing search
* results.
*/
private void updateSearchResultHeader(int count) {
if (mActivity == null) {
return;
}
// Only show the header if the context is for a search result
final Resources res = getResources();
final boolean showHeader = ConversationListContext.isSearchResult(mViewContext);
if (showHeader) {
mSearchStatusTextView.setText(res.getString(R.string.search_results_header));
mSearchResultCountTextView
.setText(res.getString(R.string.search_results_loaded, count));
}
}
/**
* Initializes all internal state for a rendering.
*/
private void initializeUiForFirstDisplay() {
// TODO(mindyp): find some way to make the notification container more
// re-usable.
// TODO(viki): refactor according to comment in
// configureSearchResultHandler()
mSearchStatusView = mActivity.findViewById(R.id.search_status_view);
mSearchStatusTextView = (TextView) mActivity.findViewById(R.id.search_status_text_view);
mSearchResultCountTextView = (TextView) mActivity
.findViewById(R.id.search_result_count_view);
}
@Override
public void onActivityCreated(Bundle savedState) {
super.onActivityCreated(savedState);
if (sSelectionModeAnimationDuration < 0) {
sSelectionModeAnimationDuration = getResources().getInteger(
R.integer.conv_item_view_cab_anim_duration);
}
// Strictly speaking, we get back an android.app.Activity from
// getActivity. However, the
// only activity creating a ConversationListContext is a MailActivity
// which is of type
// ControllableActivity, so this cast should be safe. If this cast
// fails, some other
// activity is creating ConversationListFragments. This activity must be
// of type
// ControllableActivity.
final Activity activity = getActivity();
if (!(activity instanceof ControllableActivity)) {
LogUtils.e(LOG_TAG, "ConversationListFragment expects only a ControllableActivity to"
+ "create it. Cannot proceed.");
}
mActivity = (ControllableActivity) activity;
// Since we now have a controllable activity, load the account from it,
// and register for
// future account changes.
mAccount = mAccountObserver.initialize(mActivity.getAccountController());
mCallbacks = mActivity.getListHandler();
mErrorListener = mActivity.getErrorListener();
// Start off with the current state of the folder being viewed.
Context activityContext = mActivity.getActivityContext();
mFooterView = (ConversationListFooterView) LayoutInflater.from(
activityContext).inflate(R.layout.conversation_list_footer_view,
null);
mFooterView.setClickListener(mActivity);
mConversationListView.setActivity(mActivity);
final ConversationCursor conversationCursor = getConversationListCursor();
final LoaderManager manager = getLoaderManager();
// TODO: These special views are always created, doesn't matter whether they will
// be shown or not, as we add more views this will get more expensive. Given these are
// tips that are only shown once to the user, we should consider creating these on demand.
final ConversationListHelper helper = mActivity.getConversationListHelper();
final List* Long tap: Always toggle selection of conversation. If CAB mode is not * started, then start it. *
* | Checkboxes | No Checkboxes * ----------+------------+--------------- * CAB mode | Select | Select * List mode | Select | Select * ** * Reference: http://b/issue?id=6392199 *
* {@inheritDoc} */ @Override public boolean onItemLongClick(AdapterView> parent, View view, int position, long id) { // Ignore anything that is not a conversation item. Could be a footer. if (!(view instanceof ConversationItemView)) { return false; } return ((ConversationItemView) view).toggleSelectedStateOrBeginDrag(); } /** * See the comment for * {@link #onItemLongClick(AdapterView, View, int, long)}. *
* Short tap behavior: * *
* | Checkboxes | No Checkboxes * ----------+------------+--------------- * CAB mode | Peek | Select * List mode | Peek | Peek ** * Reference: http://b/issue?id=6392199 *
* {@inheritDoc}
*/
@Override
public void onListItemClick(ListView l, View view, int position, long id) {
if (view instanceof ToggleableItem) {
final boolean showSenderImage =
(mAccount.settings.convListIcon == ConversationListIcon.SENDER_IMAGE);
final boolean inCabMode = !mSelectedSet.isEmpty();
if (!showSenderImage && inCabMode) {
((ToggleableItem) view).toggleSelectedState();
} else {
if (inCabMode) {
// this is a peek.
Analytics.getInstance().sendEvent("peek", null, null, mSelectedSet.size());
}
viewConversation(position);
}
} else {
// Ignore anything that is not a conversation item. Could be a footer.
// If we are using a keyboard, the highlighted item is the parent;
// otherwise, this is a direct call from the ConverationItemView
return;
}
// When a new list item is clicked, commit any existing leave behind
// items. Wait until we have opened the desired conversation to cause
// any position changes.
commitDestructiveActions(Utils.useTabletUI(mActivity.getActivityContext().getResources()));
}
@Override
public void onResume() {
super.onResume();
final ConversationCursor conversationCursor = getConversationListCursor();
if (conversationCursor != null) {
conversationCursor.handleNotificationActions();
restoreLastScrolledPosition();
}
mSelectedSet.addObserver(mConversationSetObserver);
}
@Override
public void onPause() {
super.onPause();
mSelectedSet.removeObserver(mConversationSetObserver);
saveLastScrolledPosition();
}
@Override
public void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
if (mListView != null) {
outState.putParcelable(LIST_STATE_KEY, mListView.onSaveInstanceState());
outState.putInt(CHOICE_MODE_KEY, mListView.getChoiceMode());
}
if (mListAdapter != null) {
mListAdapter.saveSpecialItemInstanceState(outState);
}
}
@Override
public void onStart() {
super.onStart();
mHandler.postDelayed(mUpdateTimestampsRunnable, TIMESTAMP_UPDATE_INTERVAL);
Analytics.getInstance().sendView(getClass().getName());
}
@Override
public void onStop() {
super.onStop();
mHandler.removeCallbacks(mUpdateTimestampsRunnable);
}
@Override
public void onViewModeChanged(int newMode) {
if (mTabletDevice) {
if (ViewMode.isListMode(newMode)) {
// There are no selected conversations when in conversation list mode.
clearChoicesAndActivated();
}
}
if (mFooterView != null) {
mFooterView.onViewModeChanged(newMode);
}
}
public boolean isAnimating() {
final AnimatedAdapter adapter = getAnimatedAdapter();
return (adapter != null && adapter.isAnimating()) ||
(mListView != null && mListView.isScrolling());
}
private void clearChoicesAndActivated() {
final int currentSelected = mListView.getCheckedItemPosition();
if (currentSelected != ListView.INVALID_POSITION) {
mListView.setItemChecked(mListView.getCheckedItemPosition(), false);
}
}
/**
* Handles a request to show a new conversation list, either from a search
* query or for viewing a folder. This will initiate a data load, and hence
* must be called on the UI thread.
*/
private void showList() {
mListView.setEmptyView(null);
onFolderUpdated(mActivity.getFolderController().getFolder());
onConversationListStatusUpdated();
}
/**
* View the message at the given position.
*
* @param position The position of the conversation in the list (as opposed to its position
* in the cursor)
*/
private void viewConversation(final int position) {
LogUtils.d(LOG_TAG, "ConversationListFragment.viewConversation(%d)", position);
final ConversationCursor cursor =
(ConversationCursor) getAnimatedAdapter().getItem(position);
if (cursor == null) {
LogUtils.e(LOG_TAG,
"unable to open conv at cursor pos=%s cursor=%s getPositionOffset=%s",
position, cursor, getAnimatedAdapter().getPositionOffset(position));
return;
}
final Conversation conv = cursor.getConversation();
/*
* The cursor position may be different than the position method parameter because of
* special views in the list.
*/
conv.position = cursor.getPosition();
setSelected(conv.position, true);
mCallbacks.onConversationSelected(conv, false /* inLoaderCallbacks */);
}
private final ConversationListListener mConversationListListener =
new ConversationListListener() {
@Override
public boolean isExitingSelectionMode() {
return System.currentTimeMillis() <
(mSelectionModeExitedTimestamp + sSelectionModeAnimationDuration);
}
};
/**
* Sets the selected conversation to the position given here.
* @param cursorPosition The position of the conversation in the cursor (as opposed to
* in the list)
* @param different if the currently selected conversation is different from the one provided
* here. This is a difference in conversations, not a difference in positions. For example, a
* conversation at position 2 can move to position 4 as a result of new mail.
*/
public void setSelected(final int cursorPosition, boolean different) {
if (mListView.getChoiceMode() == ListView.CHOICE_MODE_NONE) {
return;
}
final int position =
cursorPosition + getAnimatedAdapter().getPositionOffset(cursorPosition);
setRawSelected(position, different);
}
/**
* Sets the selected conversation to the position given here.
* @param position The position of the item in the list
* @param different if the currently selected conversation is different from the one provided
* here. This is a difference in conversations, not a difference in positions. For example, a
* conversation at position 2 can move to position 4 as a result of new mail.
*/
public void setRawSelected(final int position, final boolean different) {
if (mListView.getChoiceMode() == ListView.CHOICE_MODE_NONE) {
return;
}
if (different) {
mListView.smoothScrollToPosition(position);
}
mListView.setItemChecked(position, true);
}
/**
* Returns the cursor associated with the conversation list.
* @return
*/
private ConversationCursor getConversationListCursor() {
return mCallbacks != null ? mCallbacks.getConversationListCursor() : null;
}
/**
* Request a refresh of the list. No sync is carried out and none is
* promised.
*/
public void requestListRefresh() {
mListAdapter.notifyDataSetChanged();
}
/**
* Change the UI to delete the conversations provided and then call the
* {@link DestructiveAction} provided here after the UI has been
* updated.
* @param conversations
* @param action
*/
public void requestDelete(int actionId, final Collection