1/*
2 * Copyright (C) 2012 Google Inc.
3 * Licensed to The Android Open Source Project.
4 *
5 * Licensed under the Apache License, Version 2.0 (the "License");
6 * you may not use this file except in compliance with the License.
7 * You may obtain a copy of the License at
8 *
9 *      http://www.apache.org/licenses/LICENSE-2.0
10 *
11 * Unless required by applicable law or agreed to in writing, software
12 * distributed under the License is distributed on an "AS IS" BASIS,
13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 * See the License for the specific language governing permissions and
15 * limitations under the License.
16 */
17
18package com.android.mail.ui;
19
20import android.animation.LayoutTransition;
21import android.app.Activity;
22import android.app.Fragment;
23import android.app.LoaderManager;
24import android.content.Intent;
25import android.content.res.Resources;
26import android.database.DataSetObserver;
27import android.net.Uri;
28import android.os.Bundle;
29import android.os.Handler;
30import android.os.Parcelable;
31import android.support.annotation.IdRes;
32import android.support.v4.widget.SwipeRefreshLayout.OnRefreshListener;
33import android.view.KeyEvent;
34import android.view.LayoutInflater;
35import android.view.View;
36import android.view.ViewGroup;
37import android.widget.AbsListView;
38import android.widget.AdapterView;
39import android.widget.AdapterView.OnItemLongClickListener;
40import android.widget.ListView;
41import android.widget.TextView;
42
43import com.android.mail.ConversationListContext;
44import com.android.mail.R;
45import com.android.mail.analytics.Analytics;
46import com.android.mail.analytics.AnalyticsTimer;
47import com.android.mail.browse.ConversationCursor;
48import com.android.mail.browse.ConversationItemView;
49import com.android.mail.browse.ConversationItemViewModel;
50import com.android.mail.browse.ConversationListFooterView;
51import com.android.mail.browse.ToggleableItem;
52import com.android.mail.providers.Account;
53import com.android.mail.providers.AccountObserver;
54import com.android.mail.providers.Conversation;
55import com.android.mail.providers.Folder;
56import com.android.mail.providers.FolderObserver;
57import com.android.mail.providers.Settings;
58import com.android.mail.providers.UIProvider;
59import com.android.mail.providers.UIProvider.AccountCapabilities;
60import com.android.mail.providers.UIProvider.ConversationListIcon;
61import com.android.mail.providers.UIProvider.FolderCapabilities;
62import com.android.mail.providers.UIProvider.Swipe;
63import com.android.mail.ui.SwipeableListView.ListItemSwipedListener;
64import com.android.mail.ui.SwipeableListView.ListItemsRemovedListener;
65import com.android.mail.ui.SwipeableListView.SwipeListener;
66import com.android.mail.ui.ViewMode.ModeChangeListener;
67import com.android.mail.utils.KeyboardUtils;
68import com.android.mail.utils.LogTag;
69import com.android.mail.utils.LogUtils;
70import com.android.mail.utils.Utils;
71import com.android.mail.utils.ViewUtils;
72import com.google.common.collect.ImmutableList;
73
74import java.util.Collection;
75import java.util.List;
76
77import static android.view.View.OnKeyListener;
78
79/**
80 * The conversation list UI component.
81 */
82public final class ConversationListFragment extends Fragment implements
83        OnItemLongClickListener, ModeChangeListener, ListItemSwipedListener, OnRefreshListener,
84        SwipeListener, OnKeyListener, AdapterView.OnItemClickListener, View.OnClickListener,
85        AbsListView.OnScrollListener {
86    /** Key used to pass data to {@link ConversationListFragment}. */
87    private static final String CONVERSATION_LIST_KEY = "conversation-list";
88    /** Key used to keep track of the scroll state of the list. */
89    private static final String LIST_STATE_KEY = "list-state";
90
91    private static final String LOG_TAG = LogTag.getLogTag();
92    /** Key used to save the ListView choice mode, since ListView doesn't save it automatically! */
93    private static final String CHOICE_MODE_KEY = "choice-mode-key";
94
95    // True if we are on a tablet device
96    private static boolean mTabletDevice;
97
98    // Delay before displaying the loading view.
99    private static int LOADING_DELAY_MS;
100    // Minimum amount of time to keep the loading view displayed.
101    private static int MINIMUM_LOADING_DURATION;
102
103    /**
104     * Frequency of update of timestamps. Initialized in
105     * {@link #onCreate(Bundle)} and final afterwards.
106     */
107    private static int TIMESTAMP_UPDATE_INTERVAL = 0;
108
109    private ControllableActivity mActivity;
110
111    // Control state.
112    private ConversationListCallbacks mCallbacks;
113
114    private final Handler mHandler = new Handler();
115
116    // The internal view objects.
117    private SwipeableListView mListView;
118
119    private View mSearchHeaderView;
120    private TextView mSearchResultCountTextView;
121
122    /**
123     * Current Account being viewed
124     */
125    private Account mAccount;
126    /**
127     * Current folder being viewed.
128     */
129    private Folder mFolder;
130
131    /**
132     * A simple method to update the timestamps of conversations periodically.
133     */
134    private Runnable mUpdateTimestampsRunnable = null;
135
136    private ConversationListContext mViewContext;
137
138    private AnimatedAdapter mListAdapter;
139
140    private ConversationListFooterView mFooterView;
141    private ConversationListEmptyView mEmptyView;
142    private View mSecurityHoldView;
143    private TextView mSecurityHoldText;
144    private View mSecurityHoldButton;
145    private View mLoadingView;
146    private ErrorListener mErrorListener;
147    private FolderObserver mFolderObserver;
148    private DataSetObserver mConversationCursorObserver;
149
150    private ConversationCheckedSet mCheckedSet;
151    private final AccountObserver mAccountObserver = new AccountObserver() {
152        @Override
153        public void onChanged(Account newAccount) {
154            mAccount = newAccount;
155            setSwipeAction();
156        }
157    };
158    private ConversationUpdater mUpdater;
159    /** Hash of the Conversation Cursor we last obtained from the controller. */
160    private int mConversationCursorHash;
161    // The number of items in the last known ConversationCursor
162    private int mConversationCursorLastCount;
163    // State variable to keep track if we just loaded a new list, used for analytics only
164    // True if NO DATA has returned, false if we either partially or fully loaded the data
165    private boolean mInitialCursorLoading;
166
167    private @IdRes int mNextFocusStartId;
168    // Tracks if a onKey event was initiated from the listview (received ACTION_DOWN before
169    // ACTION_UP). If not, the listview only receives ACTION_UP.
170    private boolean mKeyInitiatedFromList;
171
172    // Default color id for what background should be while idle
173    private int mDefaultListBackgroundColor;
174
175    /** Duration, in milliseconds, of the CAB mode (peek icon) animation. */
176    private static long sSelectionModeAnimationDuration = -1;
177
178    // Let's ensure that we are only showing one out of the three views at once
179    private void showListView() {
180        setupEmptyIcon(false);
181        mListView.setVisibility(View.VISIBLE);
182        mEmptyView.setVisibility(View.INVISIBLE);
183        mLoadingView.setVisibility(View.INVISIBLE);
184        mSecurityHoldView.setVisibility(View.INVISIBLE);
185    }
186
187    private void showSecurityHoldView() {
188        setupEmptyIcon(false);
189        mListView.setVisibility(View.INVISIBLE);
190        mEmptyView.setVisibility(View.INVISIBLE);
191        mLoadingView.setVisibility(View.INVISIBLE);
192        setupSecurityHoldView();
193        mSecurityHoldView.setVisibility(View.VISIBLE);
194    }
195
196    private void showEmptyView() {
197        // If the callbacks didn't set up the empty icon, then we should show it in the empty view.
198        final boolean shouldShowIcon = !setupEmptyIcon(true);
199        mEmptyView.setupEmptyText(mFolder, mViewContext.searchQuery,
200                mListAdapter.getBidiFormatter(), shouldShowIcon);
201        mListView.setVisibility(View.INVISIBLE);
202        mEmptyView.setVisibility(View.VISIBLE);
203        mLoadingView.setVisibility(View.INVISIBLE);
204        mSecurityHoldView.setVisibility(View.INVISIBLE);
205    }
206
207    private void showLoadingView() {
208        setupEmptyIcon(false);
209        mListView.setVisibility(View.INVISIBLE);
210        mEmptyView.setVisibility(View.INVISIBLE);
211        mLoadingView.setVisibility(View.VISIBLE);
212        mSecurityHoldView.setVisibility(View.INVISIBLE);
213    }
214
215    private boolean setupEmptyIcon(boolean isEmpty) {
216        return mCallbacks != null && mCallbacks.setupEmptyIconView(mFolder, isEmpty);
217    }
218
219    private void setupSecurityHoldView() {
220        mSecurityHoldText.setText(getString(R.string.security_hold_required_text,
221                mAccount.getDisplayName()));
222    }
223
224    private final Runnable mLoadingViewRunnable = new FragmentRunnable("LoadingRunnable", this) {
225        @Override
226        public void go() {
227            if (!isCursorReadyToShow()) {
228                mCanTakeDownLoadingView = false;
229                showLoadingView();
230                mHandler.removeCallbacks(mHideLoadingRunnable);
231                mHandler.postDelayed(mHideLoadingRunnable, MINIMUM_LOADING_DURATION);
232            }
233            mLoadingViewPending = false;
234        }
235    };
236
237    private final Runnable mHideLoadingRunnable = new FragmentRunnable("CancelLoading", this) {
238        @Override
239        public void go() {
240            mCanTakeDownLoadingView = true;
241            if (isCursorReadyToShow()) {
242                hideLoadingViewAndShowContents();
243            }
244        }
245    };
246
247    // Keep track of if we are waiting for the loading view. This variable is also used to check
248    // if the cursor corresponding to the current folder loaded (either partially or completely).
249    private boolean mLoadingViewPending;
250    private boolean mCanTakeDownLoadingView;
251
252    /**
253     * If <code>true</code>, we have restored (or attempted to restore) the list's scroll position
254     * from when we were last on this conversation list.
255     */
256    private boolean mScrollPositionRestored = false;
257    private MailSwipeRefreshLayout mSwipeRefreshWidget;
258
259    /**
260     * Constructor needs to be public to handle orientation changes and activity
261     * lifecycle events.
262     */
263    public ConversationListFragment() {
264        super();
265    }
266
267    @Override
268    public void onBeginSwipe() {
269        mSwipeRefreshWidget.setEnabled(false);
270    }
271
272    @Override
273    public void onEndSwipe() {
274        mSwipeRefreshWidget.setEnabled(true);
275    }
276
277    private class ConversationCursorObserver extends DataSetObserver {
278        @Override
279        public void onChanged() {
280            onConversationListStatusUpdated();
281        }
282    }
283
284    /**
285     * Creates a new instance of {@link ConversationListFragment}, initialized
286     * to display conversation list context.
287     */
288    public static ConversationListFragment newInstance(ConversationListContext viewContext) {
289        final ConversationListFragment fragment = new ConversationListFragment();
290        final Bundle args = new Bundle(1);
291        args.putBundle(CONVERSATION_LIST_KEY, viewContext.toBundle());
292        fragment.setArguments(args);
293        return fragment;
294    }
295
296    /**
297     * Show the header if the current conversation list is showing search
298     * results.
299     */
300    private void updateSearchResultHeader(int count) {
301        if (mActivity == null || mSearchHeaderView == null) {
302            return;
303        }
304        mSearchResultCountTextView.setText(
305                getResources().getString(R.string.search_results_loaded, count));
306    }
307
308    @Override
309    public void onActivityCreated(Bundle savedState) {
310        super.onActivityCreated(savedState);
311        mLoadingViewPending = false;
312        mCanTakeDownLoadingView = true;
313        if (sSelectionModeAnimationDuration < 0) {
314            sSelectionModeAnimationDuration = getResources().getInteger(
315                    R.integer.conv_item_view_cab_anim_duration);
316        }
317
318        // Strictly speaking, we get back an android.app.Activity from
319        // getActivity. However, the
320        // only activity creating a ConversationListContext is a MailActivity
321        // which is of type
322        // ControllableActivity, so this cast should be safe. If this cast
323        // fails, some other
324        // activity is creating ConversationListFragments. This activity must be
325        // of type
326        // ControllableActivity.
327        final Activity activity = getActivity();
328        if (!(activity instanceof ControllableActivity)) {
329            LogUtils.e(LOG_TAG, "ConversationListFragment expects only a ControllableActivity to"
330                    + "create it. Cannot proceed.");
331        }
332        mActivity = (ControllableActivity) activity;
333        // Since we now have a controllable activity, load the account from it,
334        // and register for
335        // future account changes.
336        mAccount = mAccountObserver.initialize(mActivity.getAccountController());
337        mCallbacks = mActivity.getListHandler();
338        mErrorListener = mActivity.getErrorListener();
339        // Start off with the current state of the folder being viewed.
340        final LayoutInflater inflater = LayoutInflater.from(mActivity.getActivityContext());
341        mFooterView = (ConversationListFooterView) inflater.inflate(
342                R.layout.conversation_list_footer_view, null);
343        mFooterView.setClickListener(mActivity);
344        final ConversationCursor conversationCursor = getConversationListCursor();
345        final LoaderManager manager = getLoaderManager();
346
347        // TODO: These special views are always created, doesn't matter whether they will
348        // be shown or not, as we add more views this will get more expensive. Given these are
349        // tips that are only shown once to the user, we should consider creating these on demand.
350        final ConversationListHelper helper = mActivity.getConversationListHelper();
351        final List<ConversationSpecialItemView> specialItemViews = helper != null ?
352                ImmutableList.copyOf(helper.makeConversationListSpecialViews(
353                        activity, mActivity, mAccount))
354                : null;
355        if (specialItemViews != null) {
356            // Attach to the LoaderManager
357            for (final ConversationSpecialItemView view : specialItemViews) {
358                view.bindFragment(manager, savedState);
359            }
360        }
361
362        mListAdapter = new AnimatedAdapter(mActivity.getApplicationContext(), conversationCursor,
363                mActivity.getCheckedSet(), mActivity, mListView, specialItemViews);
364        mListAdapter.addFooter(mFooterView);
365        // Show search result header only if we are in search mode
366        final boolean showSearchHeader = ConversationListContext.isSearchResult(mViewContext);
367        if (showSearchHeader) {
368            mSearchHeaderView = inflater.inflate(R.layout.search_results_view, null);
369            mSearchResultCountTextView = (TextView)
370                    mSearchHeaderView.findViewById(R.id.search_result_count_view);
371            mListAdapter.addHeader(mSearchHeaderView);
372        }
373
374        mListView.setAdapter(mListAdapter);
375        mCheckedSet = mActivity.getCheckedSet();
376        mListView.setCheckedSet(mCheckedSet);
377        mListAdapter.setFooterVisibility(false);
378        mFolderObserver = new FolderObserver(){
379            @Override
380            public void onChanged(Folder newFolder) {
381                onFolderUpdated(newFolder);
382            }
383        };
384        mFolderObserver.initialize(mActivity.getFolderController());
385        mConversationCursorObserver = new ConversationCursorObserver();
386        mUpdater = mActivity.getConversationUpdater();
387        mUpdater.registerConversationListObserver(mConversationCursorObserver);
388        mTabletDevice = Utils.useTabletUI(mActivity.getApplicationContext().getResources());
389
390        // Shadow mods to TL require background changes and scroll listening to avoid overdraw
391        mDefaultListBackgroundColor =
392                getResources().getColor(R.color.conversation_list_background_color);
393        getView().setBackgroundColor(mDefaultListBackgroundColor);
394        mListView.setOnScrollListener(this);
395
396        // The onViewModeChanged callback doesn't get called when the mode
397        // object is created, so
398        // force setting the mode manually this time around.
399        onViewModeChanged(mActivity.getViewMode().getMode());
400        mActivity.getViewMode().addListener(this);
401        if (mActivity.getListHandler().shouldPreventListSwipesEntirely()) {
402            mListView.preventSwipesEntirely();
403        } else {
404            mListView.stopPreventingSwipes();
405        }
406
407        if (mActivity.isFinishing()) {
408            // Activity is finishing, just bail.
409            return;
410        }
411        mConversationCursorHash = (conversationCursor == null) ? 0 : conversationCursor.hashCode();
412        // Belt and suspenders here; make sure we do any necessary sync of the
413        // ConversationCursor
414        if (conversationCursor != null && conversationCursor.isRefreshReady()) {
415            conversationCursor.sync();
416        }
417
418        // On a phone we never highlight a conversation, so the default is to select none.
419        // On a tablet, we highlight a SINGLE conversation in landscape conversation view.
420        int choice = getDefaultChoiceMode(mTabletDevice);
421        if (savedState != null) {
422            // Restore the choice mode if it was set earlier, or NONE if creating a fresh view.
423            // Choice mode here represents the current conversation only. CAB mode does not rely on
424            // the platform: checked state is a local variable {@link ConversationItemView#mChecked}
425            choice = savedState.getInt(CHOICE_MODE_KEY, choice);
426            if (savedState.containsKey(LIST_STATE_KEY)) {
427                // TODO: find a better way to unset the selected item when restoring
428                mListView.clearChoices();
429            }
430        }
431        setChoiceMode(choice);
432
433        // Show list and start loading list.
434        showList();
435        ToastBarOperation pendingOp = mActivity.getPendingToastOperation();
436        if (pendingOp != null) {
437            // Clear the pending operation
438            mActivity.setPendingToastOperation(null);
439            mActivity.onUndoAvailable(pendingOp);
440        }
441    }
442
443    /**
444     * Returns the default choice mode for the list based on whether the list is displayed on tablet
445     * or not.
446     * @param isTablet
447     * @return
448     */
449    private final static int getDefaultChoiceMode(boolean isTablet) {
450        return isTablet ? ListView.CHOICE_MODE_SINGLE : ListView.CHOICE_MODE_NONE;
451    }
452
453    public AnimatedAdapter getAnimatedAdapter() {
454        return mListAdapter;
455    }
456
457    @Override
458    public void onCreate(Bundle savedState) {
459        super.onCreate(savedState);
460
461        // Initialize fragment constants from resources
462        final Resources res = getResources();
463        TIMESTAMP_UPDATE_INTERVAL = res.getInteger(R.integer.timestamp_update_interval);
464        LOADING_DELAY_MS = res.getInteger(R.integer.conversationview_show_loading_delay);
465        MINIMUM_LOADING_DURATION = res.getInteger(R.integer.conversationview_min_show_loading);
466        mUpdateTimestampsRunnable = new Runnable() {
467            @Override
468            public void run() {
469                mListView.invalidateViews();
470                mHandler.postDelayed(mUpdateTimestampsRunnable, TIMESTAMP_UPDATE_INTERVAL);
471            }
472        };
473
474        // Get the context from the arguments
475        final Bundle args = getArguments();
476        mViewContext = ConversationListContext.forBundle(args.getBundle(CONVERSATION_LIST_KEY));
477        mAccount = mViewContext.account;
478
479        setRetainInstance(false);
480    }
481
482    @Override
483    public String toString() {
484        final String s = super.toString();
485        if (mViewContext == null) {
486            return s;
487        }
488        final StringBuilder sb = new StringBuilder(s);
489        sb.setLength(sb.length() - 1);
490        sb.append(" mListAdapter=");
491        sb.append(mListAdapter);
492        sb.append(" folder=");
493        sb.append(mViewContext.folder);
494        if (mListView != null) {
495            sb.append(" selectedPos=");
496            sb.append(mListView.getSelectedConversationPosDebug());
497            sb.append(" listSelectedPos=");
498            sb.append(mListView.getSelectedItemPosition());
499            sb.append(" isListInTouchMode=");
500            sb.append(mListView.isInTouchMode());
501        }
502        sb.append("}");
503        return sb.toString();
504    }
505
506    @Override
507    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedState) {
508        View rootView = inflater.inflate(R.layout.conversation_list, null);
509        mEmptyView = (ConversationListEmptyView) rootView.findViewById(R.id.empty_view);
510        mSecurityHoldView = rootView.findViewById(R.id.security_hold_view);
511        mSecurityHoldText = (TextView) rootView.findViewById(R.id.security_hold_text);
512        mSecurityHoldButton = rootView.findViewById(R.id.security_hold_button);
513        mSecurityHoldButton.setOnClickListener(this);
514        mLoadingView = rootView.findViewById(R.id.conversation_list_loading_view);
515        mListView = (SwipeableListView) rootView.findViewById(R.id.conversation_list_view);
516        mListView.setHeaderDividersEnabled(false);
517        mListView.setOnItemLongClickListener(this);
518        mListView.enableSwipe(mAccount.supportsCapability(AccountCapabilities.UNDO));
519        mListView.setListItemSwipedListener(this);
520        mListView.setSwipeListener(this);
521        mListView.setOnKeyListener(this);
522        mListView.setOnItemClickListener(this);
523
524        // For tablets, the default left focus is the mini-drawer
525        if (mTabletDevice && mNextFocusStartId == 0) {
526            mNextFocusStartId = R.id.mini_drawer;
527        }
528        setNextFocusStartOnList();
529
530        // enable animateOnLayout (equivalent of setLayoutTransition) only for >=JB (b/14302062)
531        if (Utils.isRunningJellybeanOrLater()) {
532            ((ViewGroup) rootView.findViewById(R.id.conversation_list_parent_frame))
533                    .setLayoutTransition(new LayoutTransition());
534        }
535
536        // By default let's show the list view
537        showListView();
538
539        if (savedState != null && savedState.containsKey(LIST_STATE_KEY)) {
540            mListView.onRestoreInstanceState(savedState.getParcelable(LIST_STATE_KEY));
541        }
542        mSwipeRefreshWidget =
543                (MailSwipeRefreshLayout) rootView.findViewById(R.id.swipe_refresh_widget);
544        mSwipeRefreshWidget.setColorScheme(R.color.swipe_refresh_color1,
545                R.color.swipe_refresh_color2,
546                R.color.swipe_refresh_color3, R.color.swipe_refresh_color4);
547        mSwipeRefreshWidget.setOnRefreshListener(this);
548        mSwipeRefreshWidget.setScrollableChild(mListView);
549
550        return rootView;
551    }
552
553    /**
554     * Sets the choice mode of the list view
555     */
556    private final void setChoiceMode(int choiceMode) {
557        mListView.setChoiceMode(choiceMode);
558    }
559
560    /**
561     * Tell the list to select nothing.
562     */
563    public final void setChoiceNone() {
564        // On a phone, the default choice mode is already none, so nothing to do.
565        if (!mTabletDevice) {
566            return;
567        }
568        clearChoicesAndActivated();
569        setChoiceMode(ListView.CHOICE_MODE_NONE);
570    }
571
572    /**
573     * Tell the list to get out of selecting none.
574     */
575    public final void revertChoiceMode() {
576        // On a phone, the default choice mode is always none, so nothing to do.
577        if (!mTabletDevice) {
578            return;
579        }
580        setChoiceMode(getDefaultChoiceMode(mTabletDevice));
581    }
582
583    @Override
584    public void onDestroy() {
585        super.onDestroy();
586    }
587
588    @Override
589    public void onDestroyView() {
590
591        // Clear the list's adapter
592        mListAdapter.destroy();
593        mListView.setAdapter(null);
594
595        mActivity.getViewMode().removeListener(this);
596        if (mFolderObserver != null) {
597            mFolderObserver.unregisterAndDestroy();
598            mFolderObserver = null;
599        }
600        if (mConversationCursorObserver != null) {
601            mUpdater.unregisterConversationListObserver(mConversationCursorObserver);
602            mConversationCursorObserver = null;
603        }
604        mAccountObserver.unregisterAndDestroy();
605        getAnimatedAdapter().cleanup();
606        super.onDestroyView();
607    }
608
609    /**
610     * There are three binary variables, which determine what we do with a
611     * message. checkbEnabled: Whether check boxes are enabled or not (forced
612     * true on tablet) cabModeOn: Whether CAB mode is currently on or not.
613     * pressType: long or short tap (There is a third possibility: phone or
614     * tablet, but they have <em>identical</em> behavior) The matrix of
615     * possibilities is:
616     * <p>
617     * Long tap: Always toggle selection of conversation. If CAB mode is not
618     * started, then start it.
619     * <pre>
620     *              | Checkboxes | No Checkboxes
621     *    ----------+------------+---------------
622     *    CAB mode  |   Select   |     Select
623     *    List mode |   Select   |     Select
624     *
625     * </pre>
626     *
627     * Reference: http://b/issue?id=6392199
628     * <p>
629     * {@inheritDoc}
630     */
631    @Override
632    public boolean onItemLongClick(AdapterView<?> parent, View view, int position, long id) {
633        // Ignore anything that is not a conversation item. Could be a footer.
634        if (!(view instanceof ConversationItemView)) {
635            return false;
636        }
637        return ((ConversationItemView) view).toggleCheckedState("long_press");
638    }
639
640    /**
641     * See the comment for
642     * {@link #onItemLongClick(AdapterView, View, int, long)}.
643     * <p>
644     * Short tap behavior:
645     *
646     * <pre>
647     *              | Checkboxes | No Checkboxes
648     *    ----------+------------+---------------
649     *    CAB mode  |    Peek    |     Select
650     *    List mode |    Peek    |      Peek
651     * </pre>
652     *
653     * Reference: http://b/issue?id=6392199
654     * <p>
655     * {@inheritDoc}
656     */
657    @Override
658    public void onItemClick(AdapterView<?> adapterView, View view, int position, long id) {
659        onListItemSelected(view, position);
660    }
661
662    private void onListItemSelected(View view, int position) {
663        if (view instanceof ToggleableItem) {
664            final boolean showSenderImage =
665                    (mAccount.settings.convListIcon == ConversationListIcon.SENDER_IMAGE);
666            final boolean inCabMode = !mCheckedSet.isEmpty();
667            if (!showSenderImage && inCabMode) {
668                ((ToggleableItem) view).toggleCheckedState();
669            } else {
670                if (inCabMode) {
671                    // this is a peek.
672                    Analytics.getInstance().sendEvent("peek", null, null, mCheckedSet.size());
673                }
674                AnalyticsTimer.getInstance().trackStart(AnalyticsTimer.OPEN_CONV_VIEW_FROM_LIST);
675                viewConversation(position);
676            }
677        } else {
678            // Ignore anything that is not a conversation item. Could be a footer.
679            // If we are using a keyboard, the highlighted item is the parent;
680            // otherwise, this is a direct call from the ConverationItemView
681            return;
682        }
683        // When a new list item is clicked, commit any existing leave behind
684        // items. Wait until we have opened the desired conversation to cause
685        // any position changes.
686        commitDestructiveActions(Utils.useTabletUI(mActivity.getActivityContext().getResources()));
687    }
688
689    @Override
690    public boolean onKey(View view, int keyCode, KeyEvent keyEvent) {
691        if (view instanceof  SwipeableListView) {
692            SwipeableListView list = (SwipeableListView) view;
693            // Don't need to handle ENTER because it's auto-handled as a "click".
694            if (KeyboardUtils.isKeycodeDirectionEnd(keyCode, ViewUtils.isViewRtl(list))) {
695                if (keyEvent.getAction() == KeyEvent.ACTION_UP) {
696                    if (mKeyInitiatedFromList) {
697                        int currentPos = list.getSelectedItemPosition();
698                        if (currentPos < 0) {
699                            // Find the activated item if the focused item is non-existent.
700                            // This can happen when the user transitions from touch mode.
701                            currentPos = list.getCheckedItemPosition();
702                        }
703                        if (currentPos >= 0) {
704                            // We don't use onListItemSelected because right arrow should always
705                            // view the conversation even in CAB/no_sender_image mode.
706                            viewConversation(currentPos);
707                            commitDestructiveActions(Utils.useTabletUI(
708                                    mActivity.getActivityContext().getResources()));
709                        }
710                    }
711                    mKeyInitiatedFromList = false;
712                } else if (keyEvent.getAction() == KeyEvent.ACTION_DOWN) {
713                    mKeyInitiatedFromList = true;
714                }
715                return true;
716            } else if ((keyCode == KeyEvent.KEYCODE_DPAD_UP ||
717                    keyCode == KeyEvent.KEYCODE_DPAD_DOWN) &&
718                    keyEvent.getAction() == KeyEvent.ACTION_UP) {
719                final int position = list.getSelectedItemPosition();
720                if (position >= 0) {
721                    final Object item = getAnimatedAdapter().getItem(position);
722                    if (item != null && item instanceof ConversationCursor) {
723                        final Conversation conv = ((ConversationCursor) item).getConversation();
724                        mCallbacks.onConversationFocused(conv);
725                    }
726                }
727            }
728        }
729        return false;
730    }
731
732    @Override
733    public void onResume() {
734        super.onResume();
735
736        if (!isCursorReadyToShow()) {
737            // If the cursor got reset, let's reset the analytics state variable and show the list
738            // view since we are waiting for load again
739            mInitialCursorLoading = true;
740            showListView();
741        }
742
743        final ConversationCursor conversationCursor = getConversationListCursor();
744        if (conversationCursor != null) {
745            conversationCursor.handleNotificationActions();
746
747            restoreLastScrolledPosition();
748        }
749
750        mCheckedSet.addObserver(mConversationSetObserver);
751    }
752
753    @Override
754    public void onPause() {
755        super.onPause();
756
757        mCheckedSet.removeObserver(mConversationSetObserver);
758
759        saveLastScrolledPosition();
760    }
761
762    @Override
763    public void onSaveInstanceState(Bundle outState) {
764        super.onSaveInstanceState(outState);
765        if (mListView != null) {
766            outState.putParcelable(LIST_STATE_KEY, mListView.onSaveInstanceState());
767            outState.putInt(CHOICE_MODE_KEY, mListView.getChoiceMode());
768        }
769
770        if (mListAdapter != null) {
771            mListAdapter.saveSpecialItemInstanceState(outState);
772        }
773    }
774
775    @Override
776    public void onStart() {
777        super.onStart();
778        mHandler.postDelayed(mUpdateTimestampsRunnable, TIMESTAMP_UPDATE_INTERVAL);
779        Analytics.getInstance().sendView("ConversationListFragment");
780    }
781
782    @Override
783    public void onStop() {
784        super.onStop();
785        mHandler.removeCallbacks(mUpdateTimestampsRunnable);
786    }
787
788    @Override
789    public void onViewModeChanged(int newMode) {
790        if (mTabletDevice) {
791            if (ViewMode.isListMode(newMode)) {
792                // There are no checked conversations when in conversation list mode.
793                clearChoicesAndActivated();
794            }
795        }
796    }
797
798    public boolean isAnimating() {
799        final AnimatedAdapter adapter = getAnimatedAdapter();
800        if (adapter != null && adapter.isAnimating()) {
801            return true;
802        }
803        final boolean isScrolling = (mListView != null && mListView.isScrolling());
804        if (isScrolling) {
805            LogUtils.i(LOG_TAG, "CLF.isAnimating=true due to scrolling");
806        }
807        return isScrolling;
808    }
809
810    protected void clearChoicesAndActivated() {
811        final int currentChecked = mListView.getCheckedItemPosition();
812        if (currentChecked != ListView.INVALID_POSITION) {
813            mListView.setItemChecked(currentChecked, false);
814        }
815    }
816
817    /**
818     * Handles a request to show a new conversation list, either from a search
819     * query or for viewing a folder. This will initiate a data load, and hence
820     * must be called on the UI thread.
821     */
822    private void showList() {
823        mInitialCursorLoading = true;
824        onFolderUpdated(mActivity.getFolderController().getFolder());
825        onConversationListStatusUpdated();
826
827        // try to get an order-of-magnitude sense for message count within folders
828        // (N.B. this count currently isn't working for search folders, since their counts stream
829        // in over time in pieces.)
830        final Folder f = mViewContext.folder;
831        if (f != null) {
832            final long countLog;
833            if (f.totalCount > 0) {
834                countLog = (long) Math.log10(f.totalCount);
835            } else {
836                countLog = 0;
837            }
838            Analytics.getInstance().sendEvent("view_folder", f.getTypeDescription(),
839                    Long.toString(countLog), f.totalCount);
840        }
841    }
842
843    /**
844     * View the message at the given position.
845     *
846     * @param position The position of the conversation in the list (as opposed to its position
847     *        in the cursor)
848     */
849    private void viewConversation(final int position) {
850        LogUtils.d(LOG_TAG, "ConversationListFragment.viewConversation(%d)", position);
851
852        final Object item = getAnimatedAdapter().getItem(position);
853        if (item != null && item instanceof ConversationCursor) {
854            final ConversationCursor cursor = (ConversationCursor) item;
855            final Conversation conv = cursor.getConversation();
856        /*
857         * The cursor position may be different than the position method parameter because of
858         * special views in the list.
859         */
860            conv.position = cursor.getPosition();
861            setActivated(conv, true);
862            mCallbacks.onConversationSelected(conv, false /* inLoaderCallbacks */);
863        } else {
864            LogUtils.e(LOG_TAG,
865                    "unable to open conv at cursor pos=%s item=%s getPositionOffset=%s",
866                    position, item, getAnimatedAdapter().getPositionOffset(position));
867        }
868    }
869
870    /**
871     * Sets the checked conversation to the position given here.
872     * @param conversation the activated conversation.
873     * @param different if the currently checked conversation is different from the one provided
874     * here.  This is a difference in conversations, not a difference in positions. For example, a
875     * conversation at position 2 can move to position 4 as a result of new mail.
876     */
877    public void setActivated(final Conversation conversation, boolean different) {
878        if (mListView.getChoiceMode() == ListView.CHOICE_MODE_NONE || conversation == null) {
879            return;
880        }
881
882        final int cursorPosition = conversation.position;
883        final int position = cursorPosition + mListAdapter.getPositionOffset(cursorPosition);
884        setRawActivated(position, different);
885        setRawSelected(conversation, position);
886    }
887
888    /**
889     * Set the selected conversation (used by the framework to indicate current focus in the list).
890     * @param conversation the selected conversation.
891     */
892    public void setSelected(final Conversation conversation) {
893        if (mListView.getChoiceMode() == ListView.CHOICE_MODE_NONE || conversation == null) {
894            return;
895        }
896
897        final int cursorPosition = conversation.position;
898        final int position = cursorPosition + mListAdapter.getPositionOffset(cursorPosition);
899        setRawSelected(conversation, position);
900    }
901
902    /**
903     * Set the selected conversation (used by the framework to indicate current focus in the list).
904     * @param position The position of the item in the list
905     */
906    private void setRawSelected(Conversation conversation, final int position) {
907        final View selectedView = mListView.getChildAt(
908                position - mListView.getFirstVisiblePosition());
909        // Don't do anything if the view is already selected.
910        if (!(selectedView != null && selectedView.isSelected())) {
911            final int firstVisible = mListView.getFirstVisiblePosition();
912            final int lastVisible = mListView.getLastVisiblePosition();
913            // Check if the view is off the screen
914            if (selectedView == null || position < firstVisible || position > lastVisible) {
915                mListView.setSelection(position);
916            } else {
917                // If the view is on screen, we call setSelectionFromTop with a top offset. This
918                // prevents the list from stupidly scrolling the item to the top because
919                // setSelection calls setSelectionFromTop with y = 0.
920                mListView.setSelectionFromTop(position, selectedView.getTop());
921            }
922            mListView.setSelectedConversation(conversation);
923        }
924    }
925
926    /**
927     * Sets the activated conversation to the position given here.
928     * @param position The position of the item in the list
929     * @param different if the currently activated conversation is different from the one provided
930     * here.  This is a difference in conversations, not a difference in positions. For example, a
931     * conversation at position 2 can move to position 4 as a result of new mail.
932     */
933    public void setRawActivated(final int position, final boolean different) {
934        if (mListView.getChoiceMode() == ListView.CHOICE_MODE_NONE) {
935            return;
936        }
937
938        if (different) {
939            mListView.smoothScrollToPosition(position);
940        }
941        // Internally setItemChecked will set the activated bit if the item does not implement
942        // the Checkable interface. We use checked state to indicated CAB selection mode.
943        mListView.setItemChecked(position, true);
944    }
945
946    /**
947     * Returns the cursor associated with the conversation list.
948     * @return
949     */
950    private ConversationCursor getConversationListCursor() {
951        return mCallbacks != null ? mCallbacks.getConversationListCursor() : null;
952    }
953
954    /**
955     * Request a refresh of the list. No sync is carried out and none is
956     * promised.
957     */
958    public void requestListRefresh() {
959        mListAdapter.notifyDataSetChanged();
960    }
961
962    /**
963     * Change the UI to delete the conversations provided and then call the
964     * {@link DestructiveAction} provided here <b>after</b> the UI has been
965     * updated.
966     * @param conversations
967     * @param action
968     */
969    public void requestDelete(int actionId, final Collection<Conversation> conversations,
970            final DestructiveAction action) {
971        for (Conversation conv : conversations) {
972            conv.localDeleteOnUpdate = true;
973        }
974        final ListItemsRemovedListener listener = new ListItemsRemovedListener() {
975            @Override
976            public void onListItemsRemoved() {
977                action.performAction();
978            }
979        };
980        if (mListView.getSwipeAction() == actionId) {
981            if (!mListView.destroyItems(conversations, listener)) {
982                // The listView failed to destroy the items, perform the action manually
983                LogUtils.e(LOG_TAG, "ConversationListFragment.requestDelete: " +
984                        "listView failed to destroy items.");
985                action.performAction();
986            }
987            return;
988        }
989        // Delete the local delete items (all for now) and when done,
990        // update...
991        mListAdapter.delete(conversations, listener);
992    }
993
994    public void onFolderUpdated(Folder folder) {
995        if (!isCursorReadyToShow()) {
996            // Wait a bit before showing either the empty or loading view. If the messages are
997            // actually local, it's disorienting to see this appear on every folder transition.
998            // If they aren't, then it will likely take more than 200 milliseconds to load, and
999            // then we'll see the loading view.
1000            if (!mLoadingViewPending) {
1001                mHandler.postDelayed(mLoadingViewRunnable, LOADING_DELAY_MS);
1002                mLoadingViewPending = true;
1003            }
1004        }
1005
1006        mFolder = folder;
1007        setSwipeAction();
1008
1009        // Update enabled state of swipe to refresh.
1010        mSwipeRefreshWidget.setEnabled(!ConversationListContext.isSearchResult(mViewContext));
1011
1012        if (mFolder == null) {
1013            return;
1014        }
1015        mListAdapter.setFolder(mFolder);
1016        mFooterView.setFolder(mFolder);
1017        if (!mFolder.wasSyncSuccessful()) {
1018            mErrorListener.onError(mFolder, false);
1019        }
1020
1021        // Update the sync status bar with sync results if needed
1022        checkSyncStatus();
1023
1024        // Blow away conversation items cache.
1025        ConversationItemViewModel.onFolderUpdated(mFolder);
1026    }
1027
1028    /**
1029     * Updates the footer visibility and updates the conversation cursor
1030     */
1031    public void onConversationListStatusUpdated() {
1032        // Also change the cursor here.
1033        onCursorUpdated();
1034
1035        if (isCursorReadyToShow() && mCanTakeDownLoadingView) {
1036            hideLoadingViewAndShowContents();
1037        }
1038    }
1039
1040    private void hideLoadingViewAndShowContents() {
1041        final ConversationCursor cursor = getConversationListCursor();
1042        final boolean showFooter = mFooterView.updateStatus(cursor);
1043        // Update the sync status bar with sync results if needed
1044        checkSyncStatus();
1045        mListAdapter.setFooterVisibility(showFooter);
1046        mLoadingViewPending = false;
1047        mHandler.removeCallbacks(mLoadingViewRunnable);
1048
1049        // Even though cursor might be empty, the list adapter might have teasers/footers.
1050        // So we check the list adapter count if the cursor is fully/partially loaded.
1051        if (mAccount.securityHold != 0) {
1052            showSecurityHoldView();
1053        } else if (mListAdapter.getCount() == 0) {
1054            showEmptyView();
1055        } else {
1056            showListView();
1057        }
1058    }
1059
1060    private void setSwipeAction() {
1061        int swipeSetting = Settings.getSwipeSetting(mAccount.settings);
1062        if (swipeSetting == Swipe.DISABLED
1063                || !mAccount.supportsCapability(AccountCapabilities.UNDO)
1064                || (mFolder != null && mFolder.isTrash())) {
1065            mListView.enableSwipe(false);
1066        } else {
1067            final int action;
1068            mListView.enableSwipe(true);
1069            if (mFolder == null) {
1070                action = R.id.remove_folder;
1071            } else {
1072                switch (swipeSetting) {
1073                    // Try to respect user's setting as best as we can and default to doing nothing
1074                    case Swipe.DELETE:
1075                        // Delete in Outbox means discard failed message and put it in draft
1076                        if (mFolder.isType(UIProvider.FolderType.OUTBOX)) {
1077                            action = R.id.discard_outbox;
1078                        } else {
1079                            action = R.id.delete;
1080                        }
1081                        break;
1082                    case Swipe.ARCHIVE:
1083                        // Special case spam since it shouldn't remove spam folder label on swipe
1084                        if (mAccount.supportsCapability(AccountCapabilities.ARCHIVE)
1085                                && !mFolder.isSpam()) {
1086                            if (mFolder.supportsCapability(FolderCapabilities.ARCHIVE)) {
1087                                action = R.id.archive;
1088                                break;
1089                            } else if (mFolder.supportsCapability
1090                                    (FolderCapabilities.CAN_ACCEPT_MOVED_MESSAGES)) {
1091                                action = R.id.remove_folder;
1092                                break;
1093                            }
1094                        }
1095
1096                        /*
1097                         * If we get here, we don't support archive, on either the account or the
1098                         * folder, so we want to fall through to swipe doing nothing
1099                         */
1100                        //$FALL-THROUGH$
1101                    default:
1102                        mListView.enableSwipe(false);
1103                        action = 0; // Use default value so setSwipeAction essentially has no effect
1104                        break;
1105                }
1106            }
1107            mListView.setSwipeAction(action);
1108        }
1109        mListView.setCurrentAccount(mAccount);
1110        mListView.setCurrentFolder(mFolder);
1111    }
1112
1113    /**
1114     * Changes the conversation cursor in the list and sets checked position if none is set.
1115     */
1116    private void onCursorUpdated() {
1117        if (mCallbacks == null || mListAdapter == null) {
1118            return;
1119        }
1120        // Check against the previous cursor here and see if they are the same. If they are, then
1121        // do a notifyDataSetChanged.
1122        final ConversationCursor newCursor = mCallbacks.getConversationListCursor();
1123
1124        if (newCursor == null && mListAdapter.getCursor() != null) {
1125            // We're losing our cursor, so save our scroll position
1126            saveLastScrolledPosition();
1127        }
1128
1129        mListAdapter.swapCursor(newCursor);
1130        // When the conversation cursor is *updated*, we get back the same instance. In that
1131        // situation, CursorAdapter.swapCursor() silently returns, without forcing a
1132        // notifyDataSetChanged(). So let's force a call to notifyDataSetChanged, since an updated
1133        // cursor means that the dataset has changed.
1134        final int newCursorHash = (newCursor == null) ? 0 : newCursor.hashCode();
1135        if (mConversationCursorHash == newCursorHash && mConversationCursorHash != 0) {
1136            mListAdapter.notifyDataSetChanged();
1137        }
1138        mConversationCursorHash = newCursorHash;
1139
1140        updateAnalyticsData(newCursor);
1141        if (newCursor != null) {
1142            final int newCursorCount = newCursor.getCount();
1143            updateSearchResultHeader(newCursorCount);
1144            if (newCursorCount > 0) {
1145                newCursor.markContentsSeen();
1146                restoreLastScrolledPosition();
1147            }
1148        }
1149
1150        // If a current conversation is available, and none is activated in the list, then ask
1151        // the list to select the current conversation.
1152        final Conversation conv = mCallbacks.getCurrentConversation();
1153        final boolean currentConvIsPeeking = mCallbacks.isCurrentConversationJustPeeking();
1154        if (conv != null && !currentConvIsPeeking) {
1155            if (mListView.getChoiceMode() != ListView.CHOICE_MODE_NONE
1156                    && mListView.getCheckedItemPosition() == -1) {
1157                setActivated(conv, true);
1158            }
1159        }
1160    }
1161
1162    public void commitDestructiveActions(boolean animate) {
1163        if (mListView != null) {
1164            mListView.commitDestructiveActions(animate);
1165
1166        }
1167    }
1168
1169    @Override
1170    public void onListItemSwiped(Collection<Conversation> conversations) {
1171        mUpdater.showNextConversation(conversations);
1172    }
1173
1174    private void checkSyncStatus() {
1175        if (mFolder != null && mFolder.isSyncInProgress()) {
1176            LogUtils.d(LOG_TAG, "CLF.checkSyncStatus still syncing");
1177            // Still syncing, ignore
1178        } else {
1179            // Finished syncing:
1180            LogUtils.d(LOG_TAG, "CLF.checkSyncStatus done syncing");
1181            mSwipeRefreshWidget.setRefreshing(false);
1182        }
1183    }
1184
1185    /**
1186     * Displays the indefinite progress bar indicating a sync is in progress.  This
1187     * should only be called if user manually requested a sync, and not for background syncs.
1188     */
1189    protected void showSyncStatusBar() {
1190        mSwipeRefreshWidget.setRefreshing(true);
1191    }
1192
1193    /**
1194     * Clears all items in the list.
1195     */
1196    public void clear() {
1197        mListView.setAdapter(null);
1198    }
1199
1200    private final ConversationSetObserver mConversationSetObserver = new ConversationSetObserver() {
1201        @Override
1202        public void onSetPopulated(final ConversationCheckedSet set) {
1203            // Disable the swipe to refresh widget.
1204            mSwipeRefreshWidget.setEnabled(false);
1205        }
1206
1207        @Override
1208        public void onSetEmpty() {
1209            mSwipeRefreshWidget.setEnabled(true);
1210        }
1211
1212        @Override
1213        public void onSetChanged(final ConversationCheckedSet set) {
1214            // Do nothing
1215        }
1216    };
1217
1218    private void saveLastScrolledPosition() {
1219        if (mFolder == null || mFolder.conversationListUri == null ||
1220                mListAdapter.getCursor() == null) {
1221            // If you save your scroll position in an empty list, you're gonna have a bad time
1222            return;
1223        }
1224
1225        final Parcelable savedState = mListView.onSaveInstanceState();
1226
1227        mActivity.getListHandler().setConversationListScrollPosition(
1228                mFolder.conversationListUri.toString(), savedState);
1229    }
1230
1231    private void restoreLastScrolledPosition() {
1232        // Scroll to our previous position, if necessary
1233        if (!mScrollPositionRestored && mFolder != null) {
1234            final String key = mFolder.conversationListUri.toString();
1235            final Parcelable savedState = mActivity.getListHandler()
1236                    .getConversationListScrollPosition(key);
1237            if (savedState != null) {
1238                mListView.onRestoreInstanceState(savedState);
1239            }
1240            mScrollPositionRestored = true;
1241        }
1242    }
1243
1244    /* (non-Javadoc)
1245     * @see android.support.v4.widget.SwipeRefreshLayout.OnRefreshListener#onRefresh()
1246     */
1247    @Override
1248    public void onRefresh() {
1249        Analytics.getInstance().sendEvent(Analytics.EVENT_CATEGORY_MENU_ITEM, "swipe_refresh", null,
1250                0);
1251
1252        // This will call back to showSyncStatusBar():
1253        mActivity.getFolderController().requestFolderRefresh();
1254
1255        // Clear list adapter state out of an abundance of caution.
1256        // There is a class of bugs where an animation that should have finished doesn't (maybe
1257        // it didn't start, or it didn't finish), and the list gets stuck pretty much forever.
1258        // Clearing the state here is in line with user expectation for 'refresh'.
1259        getAnimatedAdapter().clearAnimationState();
1260        // possibly act on the now-cleared state
1261        mActivity.onAnimationEnd(mListAdapter);
1262    }
1263
1264    /**
1265     * Extracted function that handles Analytics state and logging updates for each new cursor
1266     * @param newCursor the new cursor pointer
1267     */
1268    private void updateAnalyticsData(ConversationCursor newCursor) {
1269        if (newCursor != null) {
1270            // Check if the initial data returned yet
1271            if (mInitialCursorLoading) {
1272                // This marks the very first time the cursor with the data the user sees returned.
1273                // We either have a cursor in LOADING state with cursor's count > 0, OR the cursor
1274                // completed loading.
1275                // Use this point to log the appropriate timing information that depends on when
1276                // the conversation list view finishes loading
1277                if (isCursorReadyToShow()) {
1278                    if (newCursor.getCount() == 0) {
1279                        Analytics.getInstance().sendEvent("empty_state", "post_label_change",
1280                                mFolder.getTypeDescription(), 0);
1281                    }
1282                    AnalyticsTimer.getInstance().logDuration(AnalyticsTimer.COLD_START_LAUNCHER,
1283                            true /* isDestructive */, "cold_start_to_list", "from_launcher", null);
1284                    // Don't need null checks because the activity, controller, and folder cannot
1285                    // be null in this case
1286                    if (mActivity.getFolderController().getFolder().isSearch()) {
1287                        AnalyticsTimer.getInstance().logDuration(AnalyticsTimer.SEARCH_TO_LIST,
1288                                true /* isDestructive */, "search_to_list", null, null);
1289                    }
1290
1291                    mInitialCursorLoading = false;
1292                }
1293            } else {
1294                // Log the appropriate events that happen after the initial cursor is loaded
1295                if (newCursor.getCount() == 0 && mConversationCursorLastCount > 0) {
1296                    Analytics.getInstance().sendEvent("empty_state", "post_delete",
1297                            mFolder.getTypeDescription(), 0);
1298                }
1299            }
1300
1301            // We save the count here because for folders that are empty, multiple successful
1302            // cursor loads will occur with size of 0. Thus we don't want to emit any false
1303            // positive post_delete events.
1304            mConversationCursorLastCount = newCursor.getCount();
1305        } else {
1306            mConversationCursorLastCount = 0;
1307        }
1308    }
1309
1310    /**
1311     * Helper function to determine if the current cursor is ready to populate the UI
1312     * Since we extracted the functionality into a static function in ConversationCursor,
1313     * this function remains for the sole purpose of readability.
1314     * @return
1315     */
1316    private boolean isCursorReadyToShow() {
1317        return ConversationCursor.isCursorReadyToShow(getConversationListCursor());
1318    }
1319
1320    public SwipeableListView getListView() {
1321        return mListView;
1322    }
1323
1324    public void setNextFocusStartId(@IdRes int id) {
1325        mNextFocusStartId = id;
1326        setNextFocusStartOnList();
1327    }
1328
1329    private void setNextFocusStartOnList() {
1330        if (mListView != null && mNextFocusStartId != 0) {
1331            // Since we manually handle right navigation from the list, let's just always set both
1332            // the default left and right navigation to the left id so that whenever the framework
1333            // handles one of these directions, it will go to the left side regardless of RTL.
1334            mListView.setNextFocusLeftId(mNextFocusStartId);
1335            mListView.setNextFocusRightId(mNextFocusStartId);
1336        }
1337    }
1338
1339    public void onClick(View view) {
1340        if (view == mSecurityHoldButton) {
1341            final String accountSecurityUri = mAccount.accountSecurityUri;
1342            Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(accountSecurityUri));
1343            startActivity(intent);
1344        }
1345    }
1346
1347    @Override
1348    public final void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount,
1349            int totalItemCount) {
1350        mListView.onScroll(view, firstVisibleItem, visibleItemCount, totalItemCount);
1351    }
1352
1353    /**
1354     * Used with SwipeableListView to change conv_list backgrounds to work around shadow elevation
1355     * issues causing and overdraw problems due to static backgrounds.
1356     *
1357     * @param view
1358     * @param scrollState
1359     */
1360    @Override
1361    public void onScrollStateChanged(final AbsListView view, final int scrollState) {
1362        mListView.onScrollStateChanged(view, scrollState);
1363
1364        final View rootView = getView();
1365
1366        // It seems that the list view is reading the scroll state, but the onCreateView has not
1367        // yet finished and the root view is null, so check that
1368        if (rootView != null) {
1369            // If not scrolling, assign default background - white for tablet, transparent for phone
1370            if (scrollState == AbsListView.OnScrollListener.SCROLL_STATE_IDLE) {
1371                rootView.setBackgroundColor(mDefaultListBackgroundColor);
1372
1373                // Otherwise, list is scrolling, so remove background (corresponds to 0 input)
1374            } else {
1375                rootView.setBackgroundResource(0);
1376            }
1377        }
1378    }
1379}
1380