Home | History | Annotate | Download | only in ui
      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 
     18 package com.android.mail.ui;
     19 
     20 import android.animation.LayoutTransition;
     21 import android.app.Activity;
     22 import android.app.Fragment;
     23 import android.app.LoaderManager;
     24 import android.content.Intent;
     25 import android.content.res.Resources;
     26 import android.database.DataSetObserver;
     27 import android.net.Uri;
     28 import android.os.Bundle;
     29 import android.os.Handler;
     30 import android.os.Parcelable;
     31 import android.support.annotation.IdRes;
     32 import android.support.v4.widget.SwipeRefreshLayout.OnRefreshListener;
     33 import android.view.KeyEvent;
     34 import android.view.LayoutInflater;
     35 import android.view.View;
     36 import android.view.ViewGroup;
     37 import android.widget.AbsListView;
     38 import android.widget.AdapterView;
     39 import android.widget.AdapterView.OnItemLongClickListener;
     40 import android.widget.ListView;
     41 import android.widget.TextView;
     42 
     43 import com.android.mail.ConversationListContext;
     44 import com.android.mail.R;
     45 import com.android.mail.analytics.Analytics;
     46 import com.android.mail.analytics.AnalyticsTimer;
     47 import com.android.mail.browse.ConversationCursor;
     48 import com.android.mail.browse.ConversationItemView;
     49 import com.android.mail.browse.ConversationItemViewModel;
     50 import com.android.mail.browse.ConversationListFooterView;
     51 import com.android.mail.browse.ToggleableItem;
     52 import com.android.mail.providers.Account;
     53 import com.android.mail.providers.AccountObserver;
     54 import com.android.mail.providers.Conversation;
     55 import com.android.mail.providers.Folder;
     56 import com.android.mail.providers.FolderObserver;
     57 import com.android.mail.providers.Settings;
     58 import com.android.mail.providers.UIProvider;
     59 import com.android.mail.providers.UIProvider.AccountCapabilities;
     60 import com.android.mail.providers.UIProvider.ConversationListIcon;
     61 import com.android.mail.providers.UIProvider.FolderCapabilities;
     62 import com.android.mail.providers.UIProvider.Swipe;
     63 import com.android.mail.ui.SwipeableListView.ListItemSwipedListener;
     64 import com.android.mail.ui.SwipeableListView.ListItemsRemovedListener;
     65 import com.android.mail.ui.SwipeableListView.SwipeListener;
     66 import com.android.mail.ui.ViewMode.ModeChangeListener;
     67 import com.android.mail.utils.KeyboardUtils;
     68 import com.android.mail.utils.LogTag;
     69 import com.android.mail.utils.LogUtils;
     70 import com.android.mail.utils.Utils;
     71 import com.android.mail.utils.ViewUtils;
     72 import com.google.common.collect.ImmutableList;
     73 
     74 import java.util.Collection;
     75 import java.util.List;
     76 
     77 import static android.view.View.OnKeyListener;
     78 
     79 /**
     80  * The conversation list UI component.
     81  */
     82 public 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