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;
private MailSwipeRefreshLayout mSwipeRefreshWidget;
/**
* Constructor needs to be public to handle orientation changes and activity
* lifecycle events.
*/
public ConversationListFragment() {
super();
}
@Override
public void onBeginSwipe() {
mSwipeRefreshWidget.setEnabled(false);
}
@Override
public void onEndSwipe() {
mSwipeRefreshWidget.setEnabled(true);
}
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.
*/
private void updateSearchResultHeader(int count) {
if (mActivity == null || mSearchHeaderView == null) {
return;
}
mSearchResultCountTextView.setText(
getResources().getString(R.string.search_results_loaded, count));
}
@Override
public void onActivityCreated(Bundle savedState) {
super.onActivityCreated(savedState);
mLoadingViewPending = false;
mCanTakeDownLoadingView = true;
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.
final LayoutInflater inflater = LayoutInflater.from(mActivity.getActivityContext());
mFooterView = (ConversationListFooterView) inflater.inflate(
R.layout.conversation_list_footer_view, null);
mFooterView.setClickListener(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 onItemClick(AdapterView> adapterView, View view, int position, long id) {
onListItemSelected(view, position);
}
private void onListItemSelected(View view, int position) {
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());
}
AnalyticsTimer.getInstance().trackStart(AnalyticsTimer.OPEN_CONV_VIEW_FROM_LIST);
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 boolean onKey(View view, int keyCode, KeyEvent keyEvent) {
SwipeableListView list = (SwipeableListView) view;
// Don't need to handle ENTER because it's auto-handled as a "click".
if (keyCode == KeyEvent.KEYCODE_DPAD_RIGHT) {
if (keyEvent.getAction() == KeyEvent.ACTION_UP) {
if (mKeyInitiatedFromList) {
onListItemSelected(list.getSelectedView(), list.getSelectedItemPosition());
}
mKeyInitiatedFromList = false;
} else if (keyEvent.getAction() == KeyEvent.ACTION_DOWN) {
mKeyInitiatedFromList = true;
}
return true;
} else if (keyEvent.getAction() == KeyEvent.ACTION_UP) {
if (keyCode == KeyEvent.KEYCODE_DPAD_UP || keyCode == KeyEvent.KEYCODE_DPAD_DOWN) {
final int position = list.getSelectedItemPosition();
final Object item = getAnimatedAdapter().getItem(position);
if (item != null && item instanceof ConversationCursor) {
final Conversation conv = ((ConversationCursor) item).getConversation();
mCallbacks.onConversationFocused(conv);
}
}
}
return false;
}
@Override
public void onResume() {
super.onResume();
if (!isCursorReadyToShow()) {
// If the cursor got reset, let's reset the analytics state variable and show the list
// view since we are waiting for load again
mInitialCursorLoading = true;
showListView();
}
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("ConversationListFragment");
}
@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);
}
// Set default navigation
if (ViewMode.isListMode(newMode)) {
mListView.setNextFocusRightId(R.id.conversation_list_view);
mListView.requestFocus();
} else if (ViewMode.isConversationMode(newMode)) {
// This would only happen in two_pane
mListView.setNextFocusRightId(R.id.conversation_pager);
}
}
public boolean isAnimating() {
final AnimatedAdapter adapter = getAnimatedAdapter();
if (adapter != null && adapter.isAnimating()) {
return true;
}
final boolean isScrolling = (mListView != null && mListView.isScrolling());
if (isScrolling) {
LogUtils.i(LOG_TAG, "CLF.isAnimating=true due to scrolling");
}
return 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() {
mInitialCursorLoading = true;
onFolderUpdated(mActivity.getFolderController().getFolder());
onConversationListStatusUpdated();
// try to get an order-of-magnitude sense for message count within folders
// (N.B. this count currently isn't working for search folders, since their counts stream
// in over time in pieces.)
final Folder f = mViewContext.folder;
if (f != null) {
final long countLog;
if (f.totalCount > 0) {
countLog = (long) Math.log10(f.totalCount);
} else {
countLog = 0;
}
Analytics.getInstance().sendEvent("view_folder", f.getTypeDescription(),
Long.toString(countLog), f.totalCount);
}
}
/**
* 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 */);
}
/**
* 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