blob: 6f057aac829c398fb6051d9df16f5cd56f60a8d6 [file] [log] [blame]
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -08001/*
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
Jin Cao5cf73b82014-04-25 11:13:51 -070020import android.animation.LayoutTransition;
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -080021import android.app.Activity;
22import android.app.ListFragment;
Vikram Aggarwal37a20ca2013-06-06 11:19:49 -070023import android.app.LoaderManager;
Alice Yang20323162013-04-17 10:37:41 -070024import android.content.Context;
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -080025import android.content.res.Resources;
Mindy Pereira6d8e7fe2012-07-26 16:02:49 -070026import android.database.DataSetObserver;
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -080027import android.os.Bundle;
28import android.os.Handler;
Scott Kennedyf77806e2013-08-30 11:38:15 -070029import android.os.Parcelable;
Mindy Pereira4bb435c2013-11-13 14:21:15 -080030import android.support.v4.widget.SwipeRefreshLayout.OnRefreshListener;
Alice Yang0d74a662013-03-25 14:01:24 -070031import android.text.format.DateUtils;
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -080032import android.view.LayoutInflater;
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -080033import android.view.View;
34import android.view.ViewGroup;
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -080035import android.widget.AdapterView;
36import android.widget.AdapterView.OnItemLongClickListener;
37import android.widget.ListView;
38import android.widget.TextView;
39
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -080040import com.android.mail.ConversationListContext;
Marc Blankf3626952012-02-28 17:06:05 -080041import com.android.mail.R;
Andy Huang761522c2013-08-08 13:09:11 -070042import com.android.mail.analytics.Analytics;
Jin Cao72953f22014-04-15 18:23:37 -070043import com.android.mail.analytics.AnalyticsTimer;
Vikram Aggarwal80aeac52012-02-07 15:27:20 -080044import com.android.mail.browse.ConversationCursor;
Vikram Aggarwald247dc92012-02-10 15:49:01 -080045import com.android.mail.browse.ConversationItemView;
Mindy Pereira12fe37a2012-08-15 10:02:57 -070046import com.android.mail.browse.ConversationItemViewModel;
Mindy Pereira6681f6b2012-03-09 13:55:54 -080047import com.android.mail.browse.ConversationListFooterView;
Alice Yang64273142013-04-10 18:26:56 -070048import com.android.mail.browse.ToggleableItem;
Vikram Aggarwal80aeac52012-02-07 15:27:20 -080049import com.android.mail.providers.Account;
Vikram Aggarwal7c401b72012-08-13 16:43:47 -070050import com.android.mail.providers.AccountObserver;
Vikram Aggarwald247dc92012-02-10 15:49:01 -080051import com.android.mail.providers.Conversation;
Mindy Pereira4f166de2012-02-14 13:40:58 -080052import com.android.mail.providers.Folder;
Vikram Aggarwal50ff0e52013-03-14 13:58:02 -070053import com.android.mail.providers.FolderObserver;
Vikram Aggarwal7d816002012-04-17 17:06:41 -070054import com.android.mail.providers.Settings;
Vikram Aggarwal80aeac52012-02-07 15:27:20 -080055import com.android.mail.providers.UIProvider;
Mindy Pereira4e812f42012-07-30 16:52:49 -070056import com.android.mail.providers.UIProvider.AccountCapabilities;
Scott Kennedy5dce1462013-08-05 11:26:40 -070057import com.android.mail.providers.UIProvider.ConversationListIcon;
Mindy Pereira4e812f42012-07-30 16:52:49 -070058import com.android.mail.providers.UIProvider.FolderCapabilities;
Mindy Pereirae58222b2012-07-25 14:33:18 -070059import com.android.mail.providers.UIProvider.Swipe;
mindyp9365a822012-09-12 09:09:09 -070060import com.android.mail.ui.SwipeableListView.ListItemSwipedListener;
61import com.android.mail.ui.SwipeableListView.ListItemsRemovedListener;
Andrew Sapperstein7c411aa2014-02-10 12:37:21 -080062import com.android.mail.ui.SwipeableListView.SwipeListener;
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -080063import com.android.mail.ui.ViewMode.ModeChangeListener;
Paul Westbrookb334c902012-06-25 11:42:46 -070064import com.android.mail.utils.LogTag;
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -080065import com.android.mail.utils.LogUtils;
Vikram Aggarwalfa131a22012-02-02 13:56:22 -080066import com.android.mail.utils.Utils;
Andrew Sapperstein6c570db2013-08-06 17:21:36 -070067import com.google.common.collect.ImmutableList;
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -080068
Mindy Pereiraf6a6b502012-03-15 15:24:26 -070069import java.util.Collection;
Scott Kennedy7c8325d2013-02-28 10:46:10 -080070import java.util.List;
Mindy Pereirafe06bea2012-02-16 08:15:14 -080071
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -080072/**
73 * The conversation list UI component.
74 */
Vikram Aggarwald247dc92012-02-10 15:49:01 -080075public final class ConversationListFragment extends ListFragment implements
Andrew Sapperstein7c411aa2014-02-10 12:37:21 -080076 OnItemLongClickListener, ModeChangeListener, ListItemSwipedListener, OnRefreshListener,
77 SwipeListener {
Vikram Aggarwal649b9ea2012-08-27 12:15:20 -070078 /** Key used to pass data to {@link ConversationListFragment}. */
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -080079 private static final String CONVERSATION_LIST_KEY = "conversation-list";
Vikram Aggarwal649b9ea2012-08-27 12:15:20 -070080 /** Key used to keep track of the scroll state of the list. */
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -080081 private static final String LIST_STATE_KEY = "list-state";
Vikram Aggarwal649b9ea2012-08-27 12:15:20 -070082
Paul Westbrookb334c902012-06-25 11:42:46 -070083 private static final String LOG_TAG = LogTag.getLogTag();
Vikram Aggarwala91d00b2013-01-18 12:00:37 -080084 /** Key used to save the ListView choice mode, since ListView doesn't save it automatically! */
85 private static final String CHOICE_MODE_KEY = "choice-mode-key";
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -080086
Vikram Aggarwald7a12cd2012-02-03 09:36:20 -080087 // True if we are on a tablet device
88 private static boolean mTabletDevice;
89
Martin Hibdonaaca6132014-03-24 11:28:14 -070090 // Delay before displaying the loading view.
Andy Huang0ef5cb22014-04-09 19:22:39 -070091 private static int LOADING_DELAY_MS;
Martin Hibdon4cdc4852014-04-04 15:46:12 -070092 // Minimum amount of time to keep the loading view displayed.
Andy Huang0ef5cb22014-04-09 19:22:39 -070093 private static int MINIMUM_LOADING_DURATION;
Martin Hibdonaaca6132014-03-24 11:28:14 -070094
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -080095 /**
mindyp9365a822012-09-12 09:09:09 -070096 * Frequency of update of timestamps. Initialized in
97 * {@link #onCreate(Bundle)} and final afterwards.
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -080098 */
99 private static int TIMESTAMP_UPDATE_INTERVAL = 0;
100
Alice Yang0d74a662013-03-25 14:01:24 -0700101 private static long NO_NEW_MESSAGE_DURATION = 1 * DateUtils.SECOND_IN_MILLIS;
102
Vikram Aggarwal80aeac52012-02-07 15:27:20 -0800103 private ControllableActivity mActivity;
Vikram Aggarwal80aeac52012-02-07 15:27:20 -0800104
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -0800105 // Control state.
106 private ConversationListCallbacks mCallbacks;
Vikram Aggarwal80aeac52012-02-07 15:27:20 -0800107
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -0800108 private final Handler mHandler = new Handler();
Vikram Aggarwal80aeac52012-02-07 15:27:20 -0800109
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -0800110 // The internal view objects.
Mindy Pereiraf6a6b502012-03-15 15:24:26 -0700111 private SwipeableListView mListView;
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -0800112
113 private TextView mSearchResultCountTextView;
114 private TextView mSearchStatusTextView;
115
116 private View mSearchStatusView;
Vikram Aggarwal80aeac52012-02-07 15:27:20 -0800117
Vikram Aggarwal80aeac52012-02-07 15:27:20 -0800118 /**
119 * Current Account being viewed
120 */
Vikram Aggarwald247dc92012-02-10 15:49:01 -0800121 private Account mAccount;
Vikram Aggarwal80aeac52012-02-07 15:27:20 -0800122 /**
Mindy Pereira30fd47b2012-03-09 09:24:00 -0800123 * Current folder being viewed.
Vikram Aggarwal80aeac52012-02-07 15:27:20 -0800124 */
Mindy Pereira4f166de2012-02-14 13:40:58 -0800125 private Folder mFolder;
Vikram Aggarwal80aeac52012-02-07 15:27:20 -0800126
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -0800127 /**
128 * A simple method to update the timestamps of conversations periodically.
129 */
130 private Runnable mUpdateTimestampsRunnable = null;
131
132 private ConversationListContext mViewContext;
133
Vikram Aggarwal80aeac52012-02-07 15:27:20 -0800134 private AnimatedAdapter mListAdapter;
Vikram Aggarwal7d602882012-02-07 15:01:20 -0800135
Mindy Pereira6681f6b2012-03-09 13:55:54 -0800136 private ConversationListFooterView mFooterView;
Andrew Sappersteina44b0ed2014-02-12 18:56:37 -0800137 private ConversationListEmptyView mEmptyView;
Martin Hibdonaaca6132014-03-24 11:28:14 -0700138 private View mLoadingView;
Andrew Sappersteinc2c9dc12012-07-02 18:17:32 -0700139 private ErrorListener mErrorListener;
Vikram Aggarwal50ff0e52013-03-14 13:58:02 -0700140 private FolderObserver mFolderObserver;
Vikram Aggarwala91d00b2013-01-18 12:00:37 -0800141 private DataSetObserver mConversationCursorObserver;
Vikram Aggarwald247dc92012-02-10 15:49:01 -0800142
Vikram Aggarwal4f1cc0b2012-08-02 15:16:30 -0700143 private ConversationSelectionSet mSelectedSet;
Vikram Aggarwal7c401b72012-08-13 16:43:47 -0700144 private final AccountObserver mAccountObserver = new AccountObserver() {
145 @Override
146 public void onChanged(Account newAccount) {
147 mAccount = newAccount;
148 setSwipeAction();
149 }
150 };
mindyp9365a822012-09-12 09:09:09 -0700151 private ConversationUpdater mUpdater;
Vikram Aggarwal81a4f082012-09-28 09:19:04 -0700152 /** Hash of the Conversation Cursor we last obtained from the controller. */
153 private int mConversationCursorHash;
Jin Cao343cf452014-04-18 14:05:25 -0700154 // The number of items in the last known ConversationCursor
155 private int mConversationCursorLastCount;
156 // State variable to keep track if we just loaded a new list
Jin Cao8bf9c212014-04-25 15:00:53 -0700157 // True if NO DATA has returned, false if we either partially or fully loaded the data
Jin Caofaa91982014-04-24 16:52:26 -0700158 private boolean mInitialCursorLoading;
Vikram Aggarwal4f1cc0b2012-08-02 15:16:30 -0700159
Scott Kennedy1fea6a32013-07-09 15:58:51 -0700160 /** Duration, in milliseconds, of the CAB mode (peek icon) animation. */
161 private static long sSelectionModeAnimationDuration = -1;
Scott Kennedy1fea6a32013-07-09 15:58:51 -0700162 /** The time at which we last exited CAB mode. */
163 private long mSelectionModeExitedTimestamp = -1;
164
Jin Cao856e1312014-04-09 17:36:14 -0700165 // Let's ensure that we are only showing one out of the three views at once
166 private void showListView() {
167 mListView.setVisibility(View.VISIBLE);
Jin Cao90c61672014-04-23 12:57:32 -0700168 mEmptyView.setVisibility(View.INVISIBLE);
169 mLoadingView.setVisibility(View.INVISIBLE);
Jin Cao856e1312014-04-09 17:36:14 -0700170 }
171
172 private void showEmptyView() {
Jin Cao3f386e82014-04-18 11:29:54 -0700173 mEmptyView.setupEmptyView(
174 mFolder, mViewContext.searchQuery, mListAdapter.getBidiFormatter());
Jin Cao90c61672014-04-23 12:57:32 -0700175 mListView.setVisibility(View.INVISIBLE);
Jin Cao856e1312014-04-09 17:36:14 -0700176 mEmptyView.setVisibility(View.VISIBLE);
Jin Cao90c61672014-04-23 12:57:32 -0700177 mLoadingView.setVisibility(View.INVISIBLE);
Jin Cao856e1312014-04-09 17:36:14 -0700178 }
179
180 private void showLoadingView() {
Jin Cao90c61672014-04-23 12:57:32 -0700181 mListView.setVisibility(View.INVISIBLE);
182 mEmptyView.setVisibility(View.INVISIBLE);
Jin Cao856e1312014-04-09 17:36:14 -0700183 mLoadingView.setVisibility(View.VISIBLE);
184 }
185
Martin Hibdon4cdc4852014-04-04 15:46:12 -0700186 private final Runnable mLoadingViewRunnable = new FragmentRunnable("LoadingRunnable", this) {
Martin Hibdonaaca6132014-03-24 11:28:14 -0700187 @Override
188 public void go() {
Jin Cao8bf9c212014-04-25 15:00:53 -0700189 if (mInitialCursorLoading) {
Martin Hibdon4cdc4852014-04-04 15:46:12 -0700190 mCanTakeDownLoadingView = false;
Jin Cao90c61672014-04-23 12:57:32 -0700191 showLoadingView();
Martin Hibdon4cdc4852014-04-04 15:46:12 -0700192 mHandler.removeCallbacks(mHideLoadingRunnable);
193 mHandler.postDelayed(mHideLoadingRunnable, MINIMUM_LOADING_DURATION);
Martin Hibdonaaca6132014-03-24 11:28:14 -0700194 }
195 mLoadingViewPending = false;
196 }
197 };
198
Martin Hibdon4cdc4852014-04-04 15:46:12 -0700199 private final Runnable mHideLoadingRunnable = new FragmentRunnable("CancelLoading", this) {
200 @Override
201 public void go() {
202 mCanTakeDownLoadingView = true;
Jin Cao8bf9c212014-04-25 15:00:53 -0700203 if (!mInitialCursorLoading) {
Martin Hibdon4cdc4852014-04-04 15:46:12 -0700204 hideLoadingViewAndShowContents();
205 }
206 }
207 };
208
Jin Cao7f4170a2014-04-23 13:55:47 -0700209 // Keep track of if we are waiting for the loading view. This variable is also used to check
210 // if the cursor corresponding to the current folder loaded (either partially or completely).
Martin Hibdonaaca6132014-03-24 11:28:14 -0700211 private boolean mLoadingViewPending;
Martin Hibdon4cdc4852014-04-04 15:46:12 -0700212 private boolean mCanTakeDownLoadingView;
Martin Hibdonaaca6132014-03-24 11:28:14 -0700213
Vikram Aggarwal6c511582012-02-27 10:59:47 -0800214 /**
Scott Kennedyf77806e2013-08-30 11:38:15 -0700215 * If <code>true</code>, we have restored (or attempted to restore) the list's scroll position
216 * from when we were last on this conversation list.
217 */
218 private boolean mScrollPositionRestored = false;
Andrew Sappersteinb6910bd2014-02-13 09:52:19 -0800219 private MailSwipeRefreshLayout mSwipeRefreshWidget;
Scott Kennedyf77806e2013-08-30 11:38:15 -0700220
221 /**
mindyp9365a822012-09-12 09:09:09 -0700222 * Constructor needs to be public to handle orientation changes and activity
223 * lifecycle events.
Vikram Aggarwal6c511582012-02-27 10:59:47 -0800224 */
Paul Westbrookefc9f122012-02-21 11:14:49 -0800225 public ConversationListFragment() {
Vikram Aggarwald247dc92012-02-10 15:49:01 -0800226 super();
Vikram Aggarwald247dc92012-02-10 15:49:01 -0800227 }
228
Andrew Sapperstein7c411aa2014-02-10 12:37:21 -0800229 @Override
230 public void onBeginSwipe() {
231 mSwipeRefreshWidget.setEnabled(false);
232 }
233
234 @Override
235 public void onEndSwipe() {
236 mSwipeRefreshWidget.setEnabled(true);
237 }
238
Vikram Aggarwala91d00b2013-01-18 12:00:37 -0800239 private class ConversationCursorObserver extends DataSetObserver {
Mindy Pereira70a70c92012-08-02 08:39:45 -0700240 @Override
241 public void onChanged() {
Mindy Pereira70a70c92012-08-02 08:39:45 -0700242 onConversationListStatusUpdated();
243 }
244 }
245
Vikram Aggarwal7d602882012-02-07 15:01:20 -0800246 /**
mindyp9365a822012-09-12 09:09:09 -0700247 * Creates a new instance of {@link ConversationListFragment}, initialized
248 * to display conversation list context.
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -0800249 */
250 public static ConversationListFragment newInstance(ConversationListContext viewContext) {
Paul Westbrooke2be97a2013-05-21 02:12:09 -0700251 final ConversationListFragment fragment = new ConversationListFragment();
252 final Bundle args = new Bundle(1);
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -0800253 args.putBundle(CONVERSATION_LIST_KEY, viewContext.toBundle());
254 fragment.setArguments(args);
255 return fragment;
256 }
257
258 /**
mindyp9365a822012-09-12 09:09:09 -0700259 * Show the header if the current conversation list is showing search
260 * results.
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -0800261 */
Mindy Pereira967ede62012-03-22 09:29:09 -0700262 void configureSearchResultHeader() {
Mindy Pereira755fd6e2012-03-21 15:15:44 -0700263 if (mActivity == null) {
264 return;
265 }
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -0800266 // Only show the header if the context is for a search result
267 final Resources res = getResources();
Vikram Aggarwalae4ea992012-08-07 09:56:02 -0700268 final boolean showHeader = ConversationListContext.isSearchResult(mViewContext);
mindyp9365a822012-09-12 09:09:09 -0700269 // TODO(viki): This code contains intimate understanding of the view.
270 // Much of this logic
271 // needs to reside in a separate class that handles the text view in
272 // isolation. Then,
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -0800273 // that logic can be reused in other fragments.
Jin Cao94bf9c92014-04-21 16:41:16 -0700274 mSearchStatusView.setVisibility(showHeader ? View.INVISIBLE : View.GONE);
Mindy Pereira4bb435c2013-11-13 14:21:15 -0800275 int paddingTop = showHeader ? (int) res.getDimension(R.dimen.notification_view_height) : 0;
276 mListView.setPadding(mListView.getPaddingLeft(), paddingTop, mListView.getPaddingRight(),
277 mListView.getPaddingBottom());
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -0800278 }
279
Vikram Aggarwal80aeac52012-02-07 15:27:20 -0800280 /**
mindyp9365a822012-09-12 09:09:09 -0700281 * Show the header if the current conversation list is showing search
282 * results.
Mindy Pereira68f2e222012-03-07 10:36:54 -0800283 */
284 private void updateSearchResultHeader(int count) {
Mindy Pereira71a8f292012-07-26 16:30:48 -0700285 if (mActivity == null) {
286 return;
287 }
Mindy Pereira68f2e222012-03-07 10:36:54 -0800288 // Only show the header if the context is for a search result
289 final Resources res = getResources();
Vikram Aggarwalae4ea992012-08-07 09:56:02 -0700290 final boolean showHeader = ConversationListContext.isSearchResult(mViewContext);
Mindy Pereira68f2e222012-03-07 10:36:54 -0800291 if (showHeader) {
292 mSearchStatusTextView.setText(res.getString(R.string.search_results_header));
293 mSearchResultCountTextView
294 .setText(res.getString(R.string.search_results_loaded, count));
Jin Cao94bf9c92014-04-21 16:41:16 -0700295 mSearchStatusView.setVisibility(View.VISIBLE);
Mindy Pereira68f2e222012-03-07 10:36:54 -0800296 }
297 }
298
299 /**
Vikram Aggarwal80aeac52012-02-07 15:27:20 -0800300 * Initializes all internal state for a rendering.
301 */
302 private void initializeUiForFirstDisplay() {
mindyp9365a822012-09-12 09:09:09 -0700303 // TODO(mindyp): find some way to make the notification container more
304 // re-usable.
305 // TODO(viki): refactor according to comment in
306 // configureSearchResultHandler()
Vikram Aggarwal80aeac52012-02-07 15:27:20 -0800307 mSearchStatusView = mActivity.findViewById(R.id.search_status_view);
308 mSearchStatusTextView = (TextView) mActivity.findViewById(R.id.search_status_text_view);
mindyp9365a822012-09-12 09:09:09 -0700309 mSearchResultCountTextView = (TextView) mActivity
310 .findViewById(R.id.search_result_count_view);
Vikram Aggarwal80aeac52012-02-07 15:27:20 -0800311 }
312
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -0800313 @Override
Vikram Aggarwal0509bba2013-01-29 17:11:30 -0800314 public void onActivityCreated(Bundle savedState) {
315 super.onActivityCreated(savedState);
Martin Hibdonaaca6132014-03-24 11:28:14 -0700316 mLoadingViewPending = false;
Martin Hibdon4cdc4852014-04-04 15:46:12 -0700317 mCanTakeDownLoadingView = true;
Scott Kennedy1fea6a32013-07-09 15:58:51 -0700318 if (sSelectionModeAnimationDuration < 0) {
319 sSelectionModeAnimationDuration = getResources().getInteger(
320 R.integer.conv_item_view_cab_anim_duration);
321 }
322
mindyp9365a822012-09-12 09:09:09 -0700323 // Strictly speaking, we get back an android.app.Activity from
324 // getActivity. However, the
325 // only activity creating a ConversationListContext is a MailActivity
326 // which is of type
327 // ControllableActivity, so this cast should be safe. If this cast
328 // fails, some other
329 // activity is creating ConversationListFragments. This activity must be
330 // of type
Vikram Aggarwal80aeac52012-02-07 15:27:20 -0800331 // ControllableActivity.
332 final Activity activity = getActivity();
mindyp9365a822012-09-12 09:09:09 -0700333 if (!(activity instanceof ControllableActivity)) {
334 LogUtils.e(LOG_TAG, "ConversationListFragment expects only a ControllableActivity to"
335 + "create it. Cannot proceed.");
Vikram Aggarwal80aeac52012-02-07 15:27:20 -0800336 }
337 mActivity = (ControllableActivity) activity;
mindyp9365a822012-09-12 09:09:09 -0700338 // Since we now have a controllable activity, load the account from it,
339 // and register for
Vikram Aggarwal7c401b72012-08-13 16:43:47 -0700340 // future account changes.
341 mAccount = mAccountObserver.initialize(mActivity.getAccountController());
Vikram Aggarwal7d602882012-02-07 15:01:20 -0800342 mCallbacks = mActivity.getListHandler();
Andrew Sappersteinc2c9dc12012-07-02 18:17:32 -0700343 mErrorListener = mActivity.getErrorListener();
Mindy Pereira278fd222012-07-26 15:23:10 -0700344 // Start off with the current state of the folder being viewed.
Alice Yang20323162013-04-17 10:37:41 -0700345 Context activityContext = mActivity.getActivityContext();
Mindy Pereira6681f6b2012-03-09 13:55:54 -0800346 mFooterView = (ConversationListFooterView) LayoutInflater.from(
Alice Yang20323162013-04-17 10:37:41 -0700347 activityContext).inflate(R.layout.conversation_list_footer_view,
Mindy Pereira6681f6b2012-03-09 13:55:54 -0800348 null);
Paul Westbrook4969e0c2012-08-20 14:38:39 -0700349 mFooterView.setClickListener(mActivity);
Vikram Aggarwal81a4f082012-09-28 09:19:04 -0700350 final ConversationCursor conversationCursor = getConversationListCursor();
Vikram Aggarwal37a20ca2013-06-06 11:19:49 -0700351 final LoaderManager manager = getLoaderManager();
352
Alice Yangc5567732013-07-29 18:34:51 -0700353 // TODO: These special views are always created, doesn't matter whether they will
354 // be shown or not, as we add more views this will get more expensive. Given these are
355 // tips that are only shown once to the user, we should consider creating these on demand.
Scott Kennedy7c8325d2013-02-28 10:46:10 -0800356 final ConversationListHelper helper = mActivity.getConversationListHelper();
Mark Wei2102b2c2013-05-02 17:15:30 -0700357 final List<ConversationSpecialItemView> specialItemViews = helper != null ?
358 ImmutableList.copyOf(helper.makeConversationListSpecialViews(
Scott Kennedy103319a2013-07-26 13:35:35 -0700359 activity, mActivity, mAccount))
Mark Wei2102b2c2013-05-02 17:15:30 -0700360 : null;
Scott Kennedy7c8325d2013-02-28 10:46:10 -0800361 if (specialItemViews != null) {
362 // Attach to the LoaderManager
363 for (final ConversationSpecialItemView view : specialItemViews) {
Scott Kennedy32ddb842013-08-28 17:38:22 -0700364 view.bindFragment(manager, savedState);
Scott Kennedy7c8325d2013-02-28 10:46:10 -0800365 }
366 }
367
368 mListAdapter = new AnimatedAdapter(mActivity.getApplicationContext(), conversationCursor,
Scott Kennedy6af70772013-09-20 19:27:56 -0400369 mActivity.getSelectedSet(), mActivity, mListView, specialItemViews);
Mindy Pereira6681f6b2012-03-09 13:55:54 -0800370 mListAdapter.addFooter(mFooterView);
Mindy Pereira6f4a6af2012-02-29 11:48:52 -0800371 mListView.setAdapter(mListAdapter);
Vikram Aggarwal4f1cc0b2012-08-02 15:16:30 -0700372 mSelectedSet = mActivity.getSelectedSet();
373 mListView.setSelectionSet(mSelectedSet);
Vikram Aggarwal37a20ca2013-06-06 11:19:49 -0700374 mListAdapter.setFooterVisibility(false);
Vikram Aggarwal50ff0e52013-03-14 13:58:02 -0700375 mFolderObserver = new FolderObserver(){
376 @Override
377 public void onChanged(Folder newFolder) {
378 onFolderUpdated(newFolder);
379 }
380 };
381 mFolderObserver.initialize(mActivity.getFolderController());
Vikram Aggarwala91d00b2013-01-18 12:00:37 -0800382 mConversationCursorObserver = new ConversationCursorObserver();
mindyp9365a822012-09-12 09:09:09 -0700383 mUpdater = mActivity.getConversationUpdater();
Vikram Aggarwala91d00b2013-01-18 12:00:37 -0800384 mUpdater.registerConversationListObserver(mConversationCursorObserver);
Vikram Aggarwalbcb16b92013-01-28 18:05:03 -0800385 mTabletDevice = Utils.useTabletUI(mActivity.getApplicationContext().getResources());
Vikram Aggarwal80aeac52012-02-07 15:27:20 -0800386 initializeUiForFirstDisplay();
Mindy Pereiradc8963b2012-03-28 17:51:05 -0700387 configureSearchResultHeader();
mindyp9365a822012-09-12 09:09:09 -0700388 // The onViewModeChanged callback doesn't get called when the mode
389 // object is created, so
Vikram Aggarwalfa131a22012-02-02 13:56:22 -0800390 // force setting the mode manually this time around.
Mindy Pereiraf96ec322012-03-02 14:06:33 -0800391 onViewModeChanged(mActivity.getViewMode().getMode());
Mindy Pereirab5901be2012-08-09 17:21:53 -0700392 mActivity.getViewMode().addListener(this);
Paul Westbrook0e3fd9d2012-04-20 02:02:23 -0700393
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -0800394 if (mActivity.isFinishing()) {
395 // Activity is finishing, just bail.
396 return;
397 }
Vikram Aggarwal81a4f082012-09-28 09:19:04 -0700398 mConversationCursorHash = (conversationCursor == null) ? 0 : conversationCursor.hashCode();
399 // Belt and suspenders here; make sure we do any necessary sync of the
400 // ConversationCursor
401 if (conversationCursor != null && conversationCursor.isRefreshReady()) {
402 conversationCursor.sync();
403 }
404
Vikram Aggarwal0509bba2013-01-29 17:11:30 -0800405 // On a phone we never highlight a conversation, so the default is to select none.
406 // On a tablet, we highlight a SINGLE conversation in landscape conversation view.
407 int choice = getDefaultChoiceMode(mTabletDevice);
408 if (savedState != null) {
409 // Restore the choice mode if it was set earlier, or NONE if creating a fresh view.
410 // Choice mode here represents the current conversation only. CAB mode does not rely on
411 // the platform: checked state is a local variable {@link ConversationItemView#mChecked}
412 choice = savedState.getInt(CHOICE_MODE_KEY, choice);
413 if (savedState.containsKey(LIST_STATE_KEY)) {
414 // TODO: find a better way to unset the selected item when restoring
415 mListView.clearChoices();
416 }
417 }
418 setChoiceMode(choice);
419
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -0800420 // Show list and start loading list.
421 showList();
Mindy Pereira4765c5c2012-07-19 11:58:22 -0700422 ToastBarOperation pendingOp = mActivity.getPendingToastOperation();
423 if (pendingOp != null) {
424 // Clear the pending operation
425 mActivity.setPendingToastOperation(null);
426 mActivity.onUndoAvailable(pendingOp);
427 }
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -0800428 }
429
Vikram Aggarwal0509bba2013-01-29 17:11:30 -0800430 /**
431 * Returns the default choice mode for the list based on whether the list is displayed on tablet
432 * or not.
433 * @param isTablet
434 * @return
435 */
436 private final static int getDefaultChoiceMode(boolean isTablet) {
437 return isTablet ? ListView.CHOICE_MODE_SINGLE : ListView.CHOICE_MODE_NONE;
438 }
439
Mindy Pereira967ede62012-03-22 09:29:09 -0700440 public AnimatedAdapter getAnimatedAdapter() {
441 return mListAdapter;
442 }
443
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -0800444 @Override
Vikram Aggarwald247dc92012-02-10 15:49:01 -0800445 public void onCreate(Bundle savedState) {
Vikram Aggarwald247dc92012-02-10 15:49:01 -0800446 super.onCreate(savedState);
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -0800447
448 // Initialize fragment constants from resources
Vikram Aggarwal6c511582012-02-27 10:59:47 -0800449 final Resources res = getResources();
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -0800450 TIMESTAMP_UPDATE_INTERVAL = res.getInteger(R.integer.timestamp_update_interval);
Andy Huang0ef5cb22014-04-09 19:22:39 -0700451 LOADING_DELAY_MS = res.getInteger(R.integer.conversationview_show_loading_delay);
452 MINIMUM_LOADING_DURATION = res.getInteger(R.integer.conversationview_min_show_loading);
mindyp9365a822012-09-12 09:09:09 -0700453 mUpdateTimestampsRunnable = new Runnable() {
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -0800454 @Override
455 public void run() {
456 mListView.invalidateViews();
457 mHandler.postDelayed(mUpdateTimestampsRunnable, TIMESTAMP_UPDATE_INTERVAL);
458 }
459 };
460
Vikram Aggarwal80aeac52012-02-07 15:27:20 -0800461 // Get the context from the arguments
Vikram Aggarwal6c511582012-02-27 10:59:47 -0800462 final Bundle args = getArguments();
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -0800463 mViewContext = ConversationListContext.forBundle(args.getBundle(CONVERSATION_LIST_KEY));
Mindy Pereira3982e232012-02-29 15:00:34 -0800464 mAccount = mViewContext.account;
Paul Westbrook80ecd892012-08-16 13:37:39 -0700465
Vikram Aggarwal9a49c9b2012-08-31 10:49:33 -0700466 setRetainInstance(false);
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -0800467 }
468
469 @Override
Andy Huang9e4ca792013-02-28 14:33:43 -0800470 public String toString() {
471 final String s = super.toString();
472 if (mViewContext == null) {
473 return s;
474 }
Andy Huangc1922a92013-05-13 14:33:05 -0700475 final StringBuilder sb = new StringBuilder(s);
476 sb.setLength(sb.length() - 1);
477 sb.append(" mListAdapter=");
478 sb.append(mListAdapter);
479 sb.append(" folder=");
480 sb.append(mViewContext.folder);
481 sb.append("}");
482 return sb.toString();
Andy Huang9e4ca792013-02-28 14:33:43 -0800483 }
484
485 @Override
Vikram Aggarwal9a49c9b2012-08-31 10:49:33 -0700486 public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedState) {
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -0800487 View rootView = inflater.inflate(R.layout.conversation_list, null);
Andrew Sappersteina44b0ed2014-02-12 18:56:37 -0800488 mEmptyView = (ConversationListEmptyView) rootView.findViewById(R.id.empty_view);
Andy Huang0ef5cb22014-04-09 19:22:39 -0700489 mLoadingView = rootView.findViewById(R.id.background_view);
Andy Huangc96efcc2014-04-09 15:30:42 -0700490 mLoadingView.setVisibility(View.GONE);
Andy Huang0ef5cb22014-04-09 19:22:39 -0700491 mLoadingView.findViewById(R.id.loading_progress).setVisibility(View.VISIBLE);
Mindy Pereiraf6a6b502012-03-15 15:24:26 -0700492 mListView = (SwipeableListView) rootView.findViewById(android.R.id.list);
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -0800493 mListView.setHeaderDividersEnabled(false);
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -0800494 mListView.setOnItemLongClickListener(this);
Mindy Pereira4e812f42012-07-30 16:52:49 -0700495 mListView.enableSwipe(mAccount.supportsCapability(AccountCapabilities.UNDO));
Andrew Sapperstein7c411aa2014-02-10 12:37:21 -0800496 mListView.setListItemSwipedListener(this);
497 mListView.setSwipeListener(this);
Marc Blank2af0cf32012-03-01 19:24:13 -0800498
Jin Cao5cf73b82014-04-25 11:13:51 -0700499 // enable animateOnLayout (equivalent of setLayoutTransition) only for >=JB (b/14302062)
500 if (Utils.isRunningJellybeanOrLater()) {
501 ((ViewGroup) rootView.findViewById(R.id.conversation_list_parent_frame))
502 .setLayoutTransition(new LayoutTransition());
503 }
504
Jin Cao856e1312014-04-09 17:36:14 -0700505 // By default let's show the list view
506 showListView();
507
Vikram Aggarwal0509bba2013-01-29 17:11:30 -0800508 if (savedState != null && savedState.containsKey(LIST_STATE_KEY)) {
509 mListView.onRestoreInstanceState(savedState.getParcelable(LIST_STATE_KEY));
Vikram Aggarwal9a49c9b2012-08-31 10:49:33 -0700510 }
Andrew Sappersteinb6910bd2014-02-13 09:52:19 -0800511 mSwipeRefreshWidget =
512 (MailSwipeRefreshLayout) rootView.findViewById(R.id.swipe_refresh_widget);
Mindy Pereira4bb435c2013-11-13 14:21:15 -0800513 mSwipeRefreshWidget.setColorScheme(R.color.swipe_refresh_color1,
514 R.color.swipe_refresh_color2,
515 R.color.swipe_refresh_color3, R.color.swipe_refresh_color4);
516 mSwipeRefreshWidget.setOnRefreshListener(this);
Andrew Sappersteinb6910bd2014-02-13 09:52:19 -0800517 mSwipeRefreshWidget.setScrollableChild(mListView);
Alice Yang0d74a662013-03-25 14:01:24 -0700518
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -0800519 return rootView;
520 }
521
Vikram Aggarwala91d00b2013-01-18 12:00:37 -0800522 /**
523 * Sets the choice mode of the list view
Vikram Aggarwala91d00b2013-01-18 12:00:37 -0800524 */
525 private final void setChoiceMode(int choiceMode) {
526 mListView.setChoiceMode(choiceMode);
527 }
528
529 /**
530 * Tell the list to select nothing.
531 */
532 public final void setChoiceNone() {
Vikram Aggarwal0509bba2013-01-29 17:11:30 -0800533 // On a phone, the default choice mode is already none, so nothing to do.
534 if (!mTabletDevice) {
535 return;
536 }
Andy Huangb0be3fc2013-05-02 15:48:05 -0700537 clearChoicesAndActivated();
Vikram Aggarwala91d00b2013-01-18 12:00:37 -0800538 setChoiceMode(ListView.CHOICE_MODE_NONE);
539 }
540
541 /**
542 * Tell the list to get out of selecting none.
543 */
544 public final void revertChoiceMode() {
Vikram Aggarwal0509bba2013-01-29 17:11:30 -0800545 // On a phone, the default choice mode is always none, so nothing to do.
546 if (!mTabletDevice) {
547 return;
548 }
549 setChoiceMode(getDefaultChoiceMode(mTabletDevice));
Vikram Aggarwala91d00b2013-01-18 12:00:37 -0800550 }
551
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -0800552 @Override
Andy Huang7517e3b2012-08-20 12:30:08 -0700553 public void onDestroy() {
Andy Huang7517e3b2012-08-20 12:30:08 -0700554 super.onDestroy();
555 }
556
557 @Override
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -0800558 public void onDestroyView() {
Andy Huang7517e3b2012-08-20 12:30:08 -0700559
Vikram Aggarwal7c401b72012-08-13 16:43:47 -0700560 // Clear the list's adapter
561 mListAdapter.destroy();
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -0800562 mListView.setAdapter(null);
563
Andrew Sapperstein6c570db2013-08-06 17:21:36 -0700564 mActivity.getViewMode().removeListener(this);
Mindy Pereira71a8f292012-07-26 16:30:48 -0700565 if (mFolderObserver != null) {
Vikram Aggarwal50ff0e52013-03-14 13:58:02 -0700566 mFolderObserver.unregisterAndDestroy();
Mindy Pereira71a8f292012-07-26 16:30:48 -0700567 mFolderObserver = null;
568 }
Vikram Aggarwala91d00b2013-01-18 12:00:37 -0800569 if (mConversationCursorObserver != null) {
570 mUpdater.unregisterConversationListObserver(mConversationCursorObserver);
571 mConversationCursorObserver = null;
Mindy Pereira70a70c92012-08-02 08:39:45 -0700572 }
Vikram Aggarwal7c401b72012-08-13 16:43:47 -0700573 mAccountObserver.unregisterAndDestroy();
Scott Kennedy7c8325d2013-02-28 10:46:10 -0800574 getAnimatedAdapter().cleanup();
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -0800575 super.onDestroyView();
576 }
577
Vikram Aggarwal9730ea02012-08-02 12:46:19 -0700578 /**
mindyp9365a822012-09-12 09:09:09 -0700579 * There are three binary variables, which determine what we do with a
580 * message. checkbEnabled: Whether check boxes are enabled or not (forced
581 * true on tablet) cabModeOn: Whether CAB mode is currently on or not.
582 * pressType: long or short tap (There is a third possibility: phone or
583 * tablet, but they have <em>identical</em> behavior) The matrix of
584 * possibilities is:
585 * <p>
586 * Long tap: Always toggle selection of conversation. If CAB mode is not
587 * started, then start it.
Vikram Aggarwal9730ea02012-08-02 12:46:19 -0700588 * <pre>
589 * | Checkboxes | No Checkboxes
590 * ----------+------------+---------------
591 * CAB mode | Select | Select
592 * List mode | Select | Select
593 *
Vikram Aggarwal9730ea02012-08-02 12:46:19 -0700594 * </pre>
mindyp9365a822012-09-12 09:09:09 -0700595 *
Vikram Aggarwal9730ea02012-08-02 12:46:19 -0700596 * Reference: http://b/issue?id=6392199
597 * <p>
598 * {@inheritDoc}
599 */
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -0800600 @Override
601 public boolean onItemLongClick(AdapterView<?> parent, View view, int position, long id) {
Mindy Pereira1e231a62012-03-23 12:42:15 -0700602 // Ignore anything that is not a conversation item. Could be a footer.
603 if (!(view instanceof ConversationItemView)) {
Scott Kennedy7c8325d2013-02-28 10:46:10 -0800604 return false;
Mindy Pereira1e231a62012-03-23 12:42:15 -0700605 }
Scott Kennedy955a7662013-08-06 17:12:57 -0700606 return ((ConversationItemView) view).toggleSelectedStateOrBeginDrag();
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -0800607 }
608
Vikram Aggarwal4f1cc0b2012-08-02 15:16:30 -0700609 /**
mindyp9365a822012-09-12 09:09:09 -0700610 * See the comment for
611 * {@link #onItemLongClick(AdapterView, View, int, long)}.
612 * <p>
613 * Short tap behavior:
614 *
Vikram Aggarwal4f1cc0b2012-08-02 15:16:30 -0700615 * <pre>
616 * | Checkboxes | No Checkboxes
617 * ----------+------------+---------------
Scott Kennedy9ba7fba2013-07-30 17:41:26 -0700618 * CAB mode | Peek | Select
Vikram Aggarwal4f1cc0b2012-08-02 15:16:30 -0700619 * List mode | Peek | Peek
620 * </pre>
mindyp9365a822012-09-12 09:09:09 -0700621 *
Vikram Aggarwal4f1cc0b2012-08-02 15:16:30 -0700622 * Reference: http://b/issue?id=6392199
623 * <p>
624 * {@inheritDoc}
625 */
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -0800626 @Override
Vikram Aggarwal54452ae2012-03-13 15:29:00 -0700627 public void onListItemClick(ListView l, View view, int position, long id) {
Scott Kennedy0e8dc842013-09-10 11:13:53 -0700628 if (view instanceof ToggleableItem) {
Scott Kennedy5dce1462013-08-05 11:26:40 -0700629 final boolean showSenderImage =
630 (mAccount.settings.convListIcon == ConversationListIcon.SENDER_IMAGE);
Andy Huang042a5302013-08-13 12:39:08 -0700631 final boolean inCabMode = !mSelectedSet.isEmpty();
632 if (!showSenderImage && inCabMode) {
Vikram Aggarwal37a20ca2013-06-06 11:19:49 -0700633 ((ToggleableItem) view).toggleSelectedState();
634 } else {
Andy Huang042a5302013-08-13 12:39:08 -0700635 if (inCabMode) {
636 // this is a peek.
637 Analytics.getInstance().sendEvent("peek", null, null, mSelectedSet.size());
638 }
Jin Cao72953f22014-04-15 18:23:37 -0700639 AnalyticsTimer.getInstance().trackStart(AnalyticsTimer.OPEN_CONV_VIEW_FROM_LIST);
Scott Kennedy8afccad2013-07-28 19:09:11 -0700640 viewConversation(position);
Vikram Aggarwal37a20ca2013-06-06 11:19:49 -0700641 }
Vikram Aggarwal4f1cc0b2012-08-02 15:16:30 -0700642 } else {
Vikram Aggarwal37a20ca2013-06-06 11:19:49 -0700643 // Ignore anything that is not a conversation item. Could be a footer.
644 // If we are using a keyboard, the highlighted item is the parent;
645 // otherwise, this is a direct call from the ConverationItemView
646 return;
Vikram Aggarwal4f1cc0b2012-08-02 15:16:30 -0700647 }
mindyp9365a822012-09-12 09:09:09 -0700648 // When a new list item is clicked, commit any existing leave behind
mindyp8694fe92012-09-25 11:07:16 -0700649 // items. Wait until we have opened the desired conversation to cause
650 // any position changes.
Vikram Aggarwalbcb16b92013-01-28 18:05:03 -0800651 commitDestructiveActions(Utils.useTabletUI(mActivity.getActivityContext().getResources()));
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -0800652 }
653
654 @Override
Scott Kennedy08a079c2013-02-22 10:13:10 -0800655 public void onResume() {
656 super.onResume();
657
658 final ConversationCursor conversationCursor = getConversationListCursor();
Jin Caofa934272014-05-01 13:38:21 -0700659 if (!isCursorReadyToShow(conversationCursor)) {
660 mInitialCursorLoading = true;
661
662 // Let's show the list view when we resume and are waiting for load again
663 showListView();
664 }
665
Scott Kennedy08a079c2013-02-22 10:13:10 -0800666 if (conversationCursor != null) {
667 conversationCursor.handleNotificationActions();
Scott Kennedyf77806e2013-08-30 11:38:15 -0700668
669 restoreLastScrolledPosition();
Scott Kennedy08a079c2013-02-22 10:13:10 -0800670 }
Scott Kennedy1fea6a32013-07-09 15:58:51 -0700671
672 mSelectedSet.addObserver(mConversationSetObserver);
Scott Kennedy08a079c2013-02-22 10:13:10 -0800673 }
674
675 @Override
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -0800676 public void onPause() {
677 super.onPause();
Scott Kennedy1fea6a32013-07-09 15:58:51 -0700678
679 mSelectedSet.removeObserver(mConversationSetObserver);
Scott Kennedyf77806e2013-08-30 11:38:15 -0700680
681 saveLastScrolledPosition();
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -0800682 }
683
684 @Override
685 public void onSaveInstanceState(Bundle outState) {
686 super.onSaveInstanceState(outState);
687 if (mListView != null) {
688 outState.putParcelable(LIST_STATE_KEY, mListView.onSaveInstanceState());
Vikram Aggarwala91d00b2013-01-18 12:00:37 -0800689 outState.putInt(CHOICE_MODE_KEY, mListView.getChoiceMode());
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -0800690 }
Scott Kennedy32ddb842013-08-28 17:38:22 -0700691
692 if (mListAdapter != null) {
693 mListAdapter.saveSpecialItemInstanceState(outState);
694 }
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -0800695 }
696
697 @Override
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -0800698 public void onStart() {
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -0800699 super.onStart();
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -0800700 mHandler.postDelayed(mUpdateTimestampsRunnable, TIMESTAMP_UPDATE_INTERVAL);
Andy Huanga90c33b2013-11-25 17:02:05 -0800701 Analytics.getInstance().sendView("ConversationListFragment");
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -0800702 }
703
704 @Override
705 public void onStop() {
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -0800706 super.onStop();
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -0800707 mHandler.removeCallbacks(mUpdateTimestampsRunnable);
708 }
709
Vikram Aggarwal80aeac52012-02-07 15:27:20 -0800710 @Override
Vikram Aggarwalfa131a22012-02-02 13:56:22 -0800711 public void onViewModeChanged(int newMode) {
Vikram Aggarwalfa131a22012-02-02 13:56:22 -0800712 if (mTabletDevice) {
Andy Huangb0be3fc2013-05-02 15:48:05 -0700713 if (ViewMode.isListMode(newMode)) {
Vikram Aggarwal0509bba2013-01-29 17:11:30 -0800714 // There are no selected conversations when in conversation list mode.
Andy Huangb0be3fc2013-05-02 15:48:05 -0700715 clearChoicesAndActivated();
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -0800716 }
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -0800717 }
Mindy Pereirab5901be2012-08-09 17:21:53 -0700718 if (mFooterView != null) {
719 mFooterView.onViewModeChanged(newMode);
720 }
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -0800721 }
Mindy Pereirab5901be2012-08-09 17:21:53 -0700722
Andy Huang48ccbc52013-06-05 20:30:47 -0700723 public boolean isAnimating() {
724 final AnimatedAdapter adapter = getAnimatedAdapter();
725 return (adapter != null && adapter.isAnimating()) ||
726 (mListView != null && mListView.isScrolling());
727 }
728
Andy Huangb0be3fc2013-05-02 15:48:05 -0700729 private void clearChoicesAndActivated() {
730 final int currentSelected = mListView.getCheckedItemPosition();
Andy Huangb0be3fc2013-05-02 15:48:05 -0700731 if (currentSelected != ListView.INVALID_POSITION) {
Andy Huangf0aebd32013-06-17 15:00:15 -0700732 mListView.setItemChecked(mListView.getCheckedItemPosition(), false);
Andy Huangb0be3fc2013-05-02 15:48:05 -0700733 }
734 }
735
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -0800736 /**
mindyp9365a822012-09-12 09:09:09 -0700737 * Handles a request to show a new conversation list, either from a search
738 * query or for viewing a folder. This will initiate a data load, and hence
739 * must be called on the UI thread.
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -0800740 */
741 private void showList() {
Jin Caofaa91982014-04-24 16:52:26 -0700742 mInitialCursorLoading = true;
Andy Huang7591d2f2012-07-26 16:48:06 -0700743 onFolderUpdated(mActivity.getFolderController().getFolder());
mindypca8ca2d2012-09-11 17:38:34 -0700744 onConversationListStatusUpdated();
Andy Huanga90c33b2013-11-25 17:02:05 -0800745
746 // try to get an order-of-magnitude sense for message count within folders
747 // (N.B. this count currently isn't working for search folders, since their counts stream
748 // in over time in pieces.)
749 final Folder f = mViewContext.folder;
750 if (f != null) {
751 final long countLog;
752 if (f.totalCount > 0) {
753 countLog = (long) Math.log10(f.totalCount);
754 } else {
755 countLog = 0;
756 }
757 Analytics.getInstance().sendEvent("view_folder", f.getTypeDescription(),
758 Long.toString(countLog), f.totalCount);
759 }
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -0800760 }
761
Scott Kennedy8afccad2013-07-28 19:09:11 -0700762 /**
763 * View the message at the given position.
764 *
765 * @param position The position of the conversation in the list (as opposed to its position
766 * in the cursor)
767 */
768 private void viewConversation(final int position) {
769 LogUtils.d(LOG_TAG, "ConversationListFragment.viewConversation(%d)", position);
770
771 final ConversationCursor cursor =
772 (ConversationCursor) getAnimatedAdapter().getItem(position);
773
774 if (cursor == null) {
775 LogUtils.e(LOG_TAG,
776 "unable to open conv at cursor pos=%s cursor=%s getPositionOffset=%s",
777 position, cursor, getAnimatedAdapter().getPositionOffset(position));
778 return;
779 }
780
781 final Conversation conv = cursor.getConversation();
782 /*
783 * The cursor position may be different than the position method parameter because of
784 * special views in the list.
785 */
786 conv.position = cursor.getPosition();
787 setSelected(conv.position, true);
788 mCallbacks.onConversationSelected(conv, false /* inLoaderCallbacks */);
789 }
790
Vikram Aggarwala91d00b2013-01-18 12:00:37 -0800791 /**
792 * Sets the selected conversation to the position given here.
Alice Yang0d74a662013-03-25 14:01:24 -0700793 * @param cursorPosition The position of the conversation in the cursor (as opposed to
794 * in the list)
Vikram Aggarwala91d00b2013-01-18 12:00:37 -0800795 * @param different if the currently selected conversation is different from the one provided
796 * here. This is a difference in conversations, not a difference in positions. For example, a
797 * conversation at position 2 can move to position 4 as a result of new mail.
798 */
Scott Kennedy7c8325d2013-02-28 10:46:10 -0800799 public void setSelected(final int cursorPosition, boolean different) {
Vikram Aggarwala91d00b2013-01-18 12:00:37 -0800800 if (mListView.getChoiceMode() == ListView.CHOICE_MODE_NONE) {
801 return;
802 }
Scott Kennedy7c8325d2013-02-28 10:46:10 -0800803
804 final int position =
805 cursorPosition + getAnimatedAdapter().getPositionOffset(cursorPosition);
806
Scott Kennedye0d01fb2013-08-19 19:07:54 -0700807 setRawSelected(position, different);
808 }
809
810 /**
811 * Sets the selected conversation to the position given here.
812 * @param position The position of the item in the list
813 * @param different if the currently selected conversation is different from the one provided
814 * here. This is a difference in conversations, not a difference in positions. For example, a
815 * conversation at position 2 can move to position 4 as a result of new mail.
816 */
817 public void setRawSelected(final int position, final boolean different) {
818 if (mListView.getChoiceMode() == ListView.CHOICE_MODE_NONE) {
819 return;
820 }
821
mindype21f8862012-10-01 09:32:03 -0700822 if (different) {
823 mListView.smoothScrollToPosition(position);
824 }
825 mListView.setItemChecked(position, true);
826 }
827
Vikram Aggarwal3d7ca9d2012-05-11 14:40:36 -0700828 /**
Vikram Aggarwala91d00b2013-01-18 12:00:37 -0800829 * Returns the cursor associated with the conversation list.
830 * @return
Vikram Aggarwal3d7ca9d2012-05-11 14:40:36 -0700831 */
Mindy Pereira967ede62012-03-22 09:29:09 -0700832 private ConversationCursor getConversationListCursor() {
833 return mCallbacks != null ? mCallbacks.getConversationListCursor() : null;
Mindy Pereirafe06bea2012-02-16 08:15:14 -0800834 }
835
836 /**
mindyp9365a822012-09-12 09:09:09 -0700837 * Request a refresh of the list. No sync is carried out and none is
838 * promised.
Vikram Aggarwal54452ae2012-03-13 15:29:00 -0700839 */
840 public void requestListRefresh() {
841 mListAdapter.notifyDataSetChanged();
842 }
843
Vikram Aggarwal75daee52012-04-30 11:13:09 -0700844 /**
Vikram Aggarwal7f602f72012-04-30 16:04:06 -0700845 * Change the UI to delete the conversations provided and then call the
mindyp9365a822012-09-12 09:09:09 -0700846 * {@link DestructiveAction} provided here <b>after</b> the UI has been
847 * updated.
Vikram Aggarwal7f602f72012-04-30 16:04:06 -0700848 * @param conversations
Vikram Aggarwal75daee52012-04-30 11:13:09 -0700849 * @param action
850 */
Vikram Aggarwala8e43182012-09-13 12:55:10 -0700851 public void requestDelete(int actionId, final Collection<Conversation> conversations,
Vikram Aggarwal669947b2013-01-10 17:05:56 -0800852 final DestructiveAction action) {
Mindy Pereiraacf60392012-04-06 09:11:00 -0700853 for (Conversation conv : conversations) {
854 conv.localDeleteOnUpdate = true;
855 }
Vikram Aggarwala8e43182012-09-13 12:55:10 -0700856 final ListItemsRemovedListener listener = new ListItemsRemovedListener() {
mindyp9365a822012-09-12 09:09:09 -0700857 @Override
858 public void onListItemsRemoved() {
859 action.performAction();
860 }
Vikram Aggarwala8e43182012-09-13 12:55:10 -0700861 };
862 final SwipeableListView listView = (SwipeableListView) getListView();
863 if (listView.getSwipeAction() == actionId) {
Paul Westbrookcec3e0b2013-02-04 22:50:22 -0800864 if (!listView.destroyItems(conversations, listener)) {
865 // The listView failed to destroy the items, perform the action manually
866 LogUtils.e(LOG_TAG, "ConversationListFragment.requestDelete: " +
867 "listView failed to destroy items.");
868 action.performAction();
869 }
Vikram Aggarwala8e43182012-09-13 12:55:10 -0700870 return;
871 }
872 // Delete the local delete items (all for now) and when done,
873 // update...
874 mListAdapter.delete(conversations, listener);
Mindy Pereiraacf60392012-04-06 09:11:00 -0700875 }
Mindy Pereiraa3911aa2012-03-09 13:26:26 -0800876
Mindy Pereira11dd5ef2012-03-10 15:10:18 -0800877 public void onFolderUpdated(Folder folder) {
Jin Cao8bf9c212014-04-25 15:00:53 -0700878 if (mInitialCursorLoading) {
Jin Caofaa91982014-04-24 16:52:26 -0700879 // Wait a bit before showing either the empty or loading view. If the messages are
880 // actually local, it's disorienting to see this appear on every folder transition.
881 // If they aren't, then it will likely take more than 200 milliseconds to load, and
882 // then we'll see the loading view.
883 if (!mLoadingViewPending) {
884 mHandler.postDelayed(mLoadingViewRunnable, LOADING_DELAY_MS);
885 mLoadingViewPending = true;
886 }
887 }
888
Mindy Pereiradc8963b2012-03-28 17:51:05 -0700889 mFolder = folder;
Mindy Pereira06642fa2012-07-12 16:23:27 -0700890 setSwipeAction();
Mindy Pereira4bb435c2013-11-13 14:21:15 -0800891
892 // Update enabled state of swipe to refresh.
893 mSwipeRefreshWidget.setEnabled(!ConversationListContext.isSearchResult(mViewContext));
894
Mindy Pereira82cea482012-03-27 16:45:00 -0700895 if (mFolder == null) {
896 return;
897 }
Mindy Pereira4584a0d2012-03-13 14:42:14 -0700898 mListAdapter.setFolder(mFolder);
Mindy Pereira70a70c92012-08-02 08:39:45 -0700899 mFooterView.setFolder(mFolder);
Vikram Aggarwal41b9e8f2012-09-25 10:15:04 -0700900 if (!mFolder.wasSyncSuccessful()) {
Mindy Pereira70a70c92012-08-02 08:39:45 -0700901 mErrorListener.onError(mFolder, false);
902 }
Paul Westbrookc01f5d92012-09-25 17:22:25 -0700903
904 // Notify of changes to the Folder.
905 onFolderStatusUpdated();
906
Mindy Pereira12fe37a2012-08-15 10:02:57 -0700907 // Blow away conversation items cache.
908 ConversationItemViewModel.onFolderUpdated(mFolder);
Mindy Pereira70a70c92012-08-02 08:39:45 -0700909 }
910
Vikram Aggarwal81a4f082012-09-28 09:19:04 -0700911 /**
912 * Updates the footer visibility and updates the conversation cursor
913 */
Mindy Pereira70a70c92012-08-02 08:39:45 -0700914 public void onConversationListStatusUpdated() {
Paul Westbrookc01f5d92012-09-25 17:22:25 -0700915 // Also change the cursor here.
916 onCursorUpdated();
Jin Caofaa91982014-04-24 16:52:26 -0700917
Jin Cao8bf9c212014-04-25 15:00:53 -0700918 if (!mInitialCursorLoading && mCanTakeDownLoadingView) {
Jin Caofaa91982014-04-24 16:52:26 -0700919 hideLoadingViewAndShowContents();
920 }
Paul Westbrookc01f5d92012-09-25 17:22:25 -0700921 }
922
Martin Hibdon4cdc4852014-04-04 15:46:12 -0700923 private void hideLoadingViewAndShowContents() {
Martin Hibdon4cdc4852014-04-04 15:46:12 -0700924 final ConversationCursor cursor = getConversationListCursor();
Jin Cao3f386e82014-04-18 11:29:54 -0700925 if (cursor != null && cursor.getCount() == 0) {
926 showEmptyView();
927 } else {
928 showListView();
929 }
Martin Hibdon4cdc4852014-04-04 15:46:12 -0700930 final boolean showFooter = mFooterView.updateStatus(cursor);
931 // Update the folder status, in case the cursor could affect it.
932 onFolderStatusUpdated();
933 mListAdapter.setFooterVisibility(showFooter);
934 mLoadingViewPending = false;
935 mHandler.removeCallbacks(mLoadingViewRunnable);
936 }
937
Paul Westbrookc01f5d92012-09-25 17:22:25 -0700938 private void onFolderStatusUpdated() {
Alice Yang0d74a662013-03-25 14:01:24 -0700939 // Update the sync status bar with sync results if needed
Alice Yang486e63e2013-04-05 13:01:50 -0700940 checkSyncStatus();
Alice Yang0d74a662013-03-25 14:01:24 -0700941
Paul Westbrookc01f5d92012-09-25 17:22:25 -0700942 final ConversationCursor cursor = getConversationListCursor();
Paul Westbrook9a70e912012-08-17 15:53:20 -0700943 Bundle extras = cursor != null ? cursor.getExtras() : Bundle.EMPTY;
Andrew Sapperstein427df9d2013-05-15 12:05:09 -0700944 int errorStatus = extras.containsKey(UIProvider.CursorExtraKeys.EXTRA_ERROR) ?
Mindy Pereira70a70c92012-08-02 08:39:45 -0700945 extras.getInt(UIProvider.CursorExtraKeys.EXTRA_ERROR)
946 : UIProvider.LastSyncResult.SUCCESS;
Andrew Sapperstein427df9d2013-05-15 12:05:09 -0700947 int cursorStatus = extras.getInt(UIProvider.CursorExtraKeys.EXTRA_STATUS);
Alice Yang0d74a662013-03-25 14:01:24 -0700948 // We want to update the UI with this information if either we are loaded or complete, or
Paul Westbrookc01f5d92012-09-25 17:22:25 -0700949 // we have a folder with a non-0 count.
950 final int folderCount = mFolder != null ? mFolder.totalCount : 0;
Andrew Sapperstein427df9d2013-05-15 12:05:09 -0700951 if (errorStatus == UIProvider.LastSyncResult.SUCCESS
952 && (cursorStatus == UIProvider.CursorStatus.LOADED
953 || cursorStatus == UIProvider.CursorStatus.COMPLETE) || folderCount > 0) {
Paul Westbrookc01f5d92012-09-25 17:22:25 -0700954 updateSearchResultHeader(folderCount);
Mindy Pereira70a70c92012-08-02 08:39:45 -0700955 }
Mindy Pereiraa3911aa2012-03-09 13:26:26 -0800956 }
Mindy Pereiraf6a6b502012-03-15 15:24:26 -0700957
Mindy Pereira06642fa2012-07-12 16:23:27 -0700958 private void setSwipeAction() {
Vikram Aggarwal7c401b72012-08-13 16:43:47 -0700959 int swipeSetting = Settings.getSwipeSetting(mAccount.settings);
Mindy Pereirae58222b2012-07-25 14:33:18 -0700960 if (swipeSetting == Swipe.DISABLED
Mindy Pereirab8073372012-08-09 14:00:10 -0700961 || !mAccount.supportsCapability(AccountCapabilities.UNDO)
962 || (mFolder != null && mFolder.isTrash())) {
Mindy Pereirae58222b2012-07-25 14:33:18 -0700963 mListView.enableSwipe(false);
964 } else {
Scott Kennedy3b965d72013-06-25 14:36:55 -0700965 final int action;
Rohan Shah7f98d0d2013-02-15 13:21:09 -0800966 mListView.enableSwipe(true);
Jin Caod4e02e42014-03-26 13:04:40 -0700967 if (mFolder == null) {
Mindy Pereira01f30502012-08-14 10:30:51 -0700968 action = R.id.remove_folder;
Mindy Pereira4e812f42012-07-30 16:52:49 -0700969 } else {
Mindy Pereira4e812f42012-07-30 16:52:49 -0700970 switch (swipeSetting) {
Jin Caod4e02e42014-03-26 13:04:40 -0700971 // Try to respect user's setting as best as we can and default to doing nothing
972 case Swipe.DELETE:
973 action = R.id.delete;
974 break;
Mindy Pereira4e812f42012-07-30 16:52:49 -0700975 case Swipe.ARCHIVE:
Jin Caod4e02e42014-03-26 13:04:40 -0700976 // Special case spam since it shouldn't remove spam folder label on swipe
977 if (mAccount.supportsCapability(AccountCapabilities.ARCHIVE)
978 && !mFolder.isSpam()) {
Mindy Pereira4e812f42012-07-30 16:52:49 -0700979 if (mFolder.supportsCapability(FolderCapabilities.ARCHIVE)) {
980 action = R.id.archive;
981 break;
982 } else if (mFolder.supportsCapability
983 (FolderCapabilities.CAN_ACCEPT_MOVED_MESSAGES)) {
Mindy Pereira01f30502012-08-14 10:30:51 -0700984 action = R.id.remove_folder;
Mindy Pereira4e812f42012-07-30 16:52:49 -0700985 break;
986 }
987 }
Scott Kennedy3b965d72013-06-25 14:36:55 -0700988
989 /*
990 * If we get here, we don't support archive, on either the account or the
Jin Cao3432b632014-03-28 13:35:44 -0700991 * folder, so we want to fall through to swipe doing nothing
Scott Kennedy3b965d72013-06-25 14:36:55 -0700992 */
993 //$FALL-THROUGH$
Mindy Pereira4e812f42012-07-30 16:52:49 -0700994 default:
Jin Cao3432b632014-03-28 13:35:44 -0700995 mListView.enableSwipe(false);
996 action = 0; // Use default value so setSwipeAction essentially has no effect
Mindy Pereira4e812f42012-07-30 16:52:49 -0700997 break;
998 }
Mindy Pereirae58222b2012-07-25 14:33:18 -0700999 }
1000 mListView.setSwipeAction(action);
1001 }
Mark Wei6126d722013-04-24 21:09:43 -07001002 mListView.setCurrentAccount(mAccount);
Mindy Pereira06642fa2012-07-12 16:23:27 -07001003 mListView.setCurrentFolder(mFolder);
1004 }
1005
Vikram Aggarwal17f373e2012-09-17 15:49:32 -07001006 /**
1007 * Changes the conversation cursor in the list and sets selected position if none is set.
1008 */
1009 private void onCursorUpdated() {
1010 if (mCallbacks == null || mListAdapter == null) {
1011 return;
1012 }
Vikram Aggarwal81a4f082012-09-28 09:19:04 -07001013 // Check against the previous cursor here and see if they are the same. If they are, then
1014 // do a notifyDataSetChanged.
1015 final ConversationCursor newCursor = mCallbacks.getConversationListCursor();
Scott Kennedyf77806e2013-08-30 11:38:15 -07001016
1017 if (newCursor == null && mListAdapter.getCursor() != null) {
1018 // We're losing our cursor, so save our scroll position
1019 saveLastScrolledPosition();
1020 }
1021
Vikram Aggarwal81a4f082012-09-28 09:19:04 -07001022 mListAdapter.swapCursor(newCursor);
1023 // When the conversation cursor is *updated*, we get back the same instance. In that
1024 // situation, CursorAdapter.swapCursor() silently returns, without forcing a
1025 // notifyDataSetChanged(). So let's force a call to notifyDataSetChanged, since an updated
1026 // cursor means that the dataset has changed.
1027 final int newCursorHash = (newCursor == null) ? 0 : newCursor.hashCode();
1028 if (mConversationCursorHash == newCursorHash && mConversationCursorHash != 0) {
1029 mListAdapter.notifyDataSetChanged();
1030 }
1031 mConversationCursorHash = newCursorHash;
Scott Kennedyb1e21482013-03-15 13:40:38 -07001032
Jin Cao8bf9c212014-04-25 15:00:53 -07001033 updateAnalyticsData(newCursor);
Jin Cao779dd602014-04-22 16:16:28 -07001034
Scott Kennedy4b9b3952013-03-20 17:20:59 -07001035 if (newCursor != null && newCursor.getCount() > 0) {
Scott Kennedyb1e21482013-03-15 13:40:38 -07001036 newCursor.markContentsSeen();
Scott Kennedyf77806e2013-08-30 11:38:15 -07001037 restoreLastScrolledPosition();
Scott Kennedyb1e21482013-03-15 13:40:38 -07001038 }
1039
Vikram Aggarwal17f373e2012-09-17 15:49:32 -07001040 // If a current conversation is available, and none is selected in the list, then ask
1041 // the list to select the current conversation.
1042 final Conversation conv = mCallbacks.getCurrentConversation();
Scott Kennedyf77806e2013-08-30 11:38:15 -07001043 if (conv != null) {
1044 if (mListView.getChoiceMode() != ListView.CHOICE_MODE_NONE
1045 && mListView.getCheckedItemPosition() == -1) {
1046 setSelected(conv.position, true);
1047 }
Mindy Pereira967ede62012-03-22 09:29:09 -07001048 }
Mindy Pereira21ab4902012-03-19 18:48:03 -07001049 }
Mindy Pereira1e2573b2012-04-17 14:34:36 -07001050
mindypc6adce32012-08-22 18:46:42 -07001051 public void commitDestructiveActions(boolean animate) {
Mindy Pereira8937bf12012-07-23 14:05:02 -07001052 if (mListView != null) {
mindypc6adce32012-08-22 18:46:42 -07001053 mListView.commitDestructiveActions(animate);
Vikram Aggarwal7d816002012-04-17 17:06:41 -07001054
1055 }
1056 }
1057
mindyp9365a822012-09-12 09:09:09 -07001058 @Override
1059 public void onListItemSwiped(Collection<Conversation> conversations) {
1060 mUpdater.showNextConversation(conversations);
1061 }
Alice Yang0d74a662013-03-25 14:01:24 -07001062
Alice Yang486e63e2013-04-05 13:01:50 -07001063 private void checkSyncStatus() {
Alice Yang76d20652013-04-24 02:32:48 -07001064 if (mFolder != null && mFolder.isSyncInProgress()) {
Alice Yang03752f32013-05-05 15:05:16 -07001065 LogUtils.d(LOG_TAG, "CLF.checkSyncStatus still syncing");
Alice Yang486e63e2013-04-05 13:01:50 -07001066 // Still syncing, ignore
1067 } else {
1068 // Finished syncing:
Alice Yang03752f32013-05-05 15:05:16 -07001069 LogUtils.d(LOG_TAG, "CLF.checkSyncStatus done syncing");
Mindy Pereira4bb435c2013-11-13 14:21:15 -08001070 mSwipeRefreshWidget.setRefreshing(false);
Alice Yang486e63e2013-04-05 13:01:50 -07001071 }
1072 }
1073
Alice Yang0d74a662013-03-25 14:01:24 -07001074 /**
Alice Yang486e63e2013-04-05 13:01:50 -07001075 * Displays the indefinite progress bar indicating a sync is in progress. This
1076 * should only be called if user manually requested a sync, and not for background syncs.
Alice Yang0d74a662013-03-25 14:01:24 -07001077 */
1078 protected void showSyncStatusBar() {
Mindy Pereira4bb435c2013-11-13 14:21:15 -08001079 mSwipeRefreshWidget.setRefreshing(true);
Alice Yang0d74a662013-03-25 14:01:24 -07001080 }
Rohan Shahd4f22872013-04-19 17:06:37 -07001081
1082 /**
1083 * Clears all items in the list.
1084 */
1085 public void clear() {
1086 mListView.setAdapter(null);
1087 }
Scott Kennedy1fea6a32013-07-09 15:58:51 -07001088
1089 private final ConversationSetObserver mConversationSetObserver = new ConversationSetObserver() {
1090 @Override
1091 public void onSetPopulated(final ConversationSelectionSet set) {
Mindy Pereira4bb435c2013-11-13 14:21:15 -08001092 // Disable the swipe to refresh widget.
1093 mSwipeRefreshWidget.setEnabled(false);
Scott Kennedy1fea6a32013-07-09 15:58:51 -07001094 }
1095
1096 @Override
1097 public void onSetEmpty() {
1098 mSelectionModeExitedTimestamp = System.currentTimeMillis();
Mindy Pereira4bb435c2013-11-13 14:21:15 -08001099 mSwipeRefreshWidget.setEnabled(true);
Scott Kennedy1fea6a32013-07-09 15:58:51 -07001100 }
1101
1102 @Override
1103 public void onSetChanged(final ConversationSelectionSet set) {
Scott Kennedy8afccad2013-07-28 19:09:11 -07001104 // Do nothing
Scott Kennedy1fea6a32013-07-09 15:58:51 -07001105 }
1106 };
Scott Kennedyf77806e2013-08-30 11:38:15 -07001107
1108 private void saveLastScrolledPosition() {
1109 if (mListAdapter.getCursor() == null) {
1110 // If you save your scroll position in an empty list, you're gonna have a bad time
1111 return;
1112 }
1113
1114 final Parcelable savedState = mListView.onSaveInstanceState();
1115
1116 mActivity.getListHandler().setConversationListScrollPosition(
1117 mFolder.conversationListUri.toString(), savedState);
1118 }
1119
1120 private void restoreLastScrolledPosition() {
1121 // Scroll to our previous position, if necessary
Scott Kennedy599c3cf2013-09-25 15:17:19 -07001122 if (!mScrollPositionRestored && mFolder != null) {
1123 final String key = mFolder.conversationListUri.toString();
Scott Kennedyf77806e2013-08-30 11:38:15 -07001124 final Parcelable savedState = mActivity.getListHandler()
Scott Kennedy599c3cf2013-09-25 15:17:19 -07001125 .getConversationListScrollPosition(key);
Scott Kennedyf77806e2013-08-30 11:38:15 -07001126 if (savedState != null) {
1127 mListView.onRestoreInstanceState(savedState);
1128 }
1129 mScrollPositionRestored = true;
1130 }
1131 }
Mindy Pereira4bb435c2013-11-13 14:21:15 -08001132
1133 /* (non-Javadoc)
1134 * @see android.support.v4.widget.SwipeRefreshLayout.OnRefreshListener#onRefresh()
1135 */
1136 @Override
1137 public void onRefresh() {
1138 Analytics.getInstance().sendEvent(Analytics.EVENT_CATEGORY_MENU_ITEM, "swipe_refresh", null,
1139 0);
1140
1141 // This will call back to showSyncStatusBar():
1142 mActivity.getFolderController().requestFolderRefresh();
1143 }
Jin Cao343cf452014-04-18 14:05:25 -07001144
1145 /**
Jin Cao8bf9c212014-04-25 15:00:53 -07001146 * Extracted function that handles Analytics state and logging updates for each new cursor
1147 * @param newCursor the new cursor pointer
Jin Cao343cf452014-04-18 14:05:25 -07001148 */
1149 private void updateAnalyticsData(ConversationCursor newCursor) {
Jin Cao8bf9c212014-04-25 15:00:53 -07001150 if (newCursor != null) {
1151 // Check if the initial data returned yet
1152 if (mInitialCursorLoading) {
1153 // This marks the very first time the cursor with the data the user sees returned.
1154 // We either have a cursor in LOADING state with cursor's count > 0, OR the cursor
1155 // completed loading.
1156 // Use this point to log the appropriate timing information that depends on when
1157 // the conversation list view finishes loading
Jin Caofa934272014-05-01 13:38:21 -07001158 if (isCursorReadyToShow(newCursor)) {
Jin Cao8bf9c212014-04-25 15:00:53 -07001159 if (newCursor.getCount() == 0) {
1160 Analytics.getInstance().sendEvent("empty_state", "post_label_change",
1161 mFolder.getTypeDescription(), 0);
1162 }
1163 AnalyticsTimer.getInstance().logDuration(AnalyticsTimer.COLD_START_LAUNCHER,
1164 true /* isDestructive */, "cold_start_to_list", "from_launcher", null);
1165 // Don't need null checks because the activity, controller, and folder cannot
1166 // be null in this case
1167 if (mActivity.getFolderController().getFolder().isSearch()) {
1168 AnalyticsTimer.getInstance().logDuration(AnalyticsTimer.SEARCH_TO_LIST,
1169 true /* isDestructive */, "search_to_list", null, null);
1170 }
Jin Cao779dd602014-04-22 16:16:28 -07001171
Jin Cao8bf9c212014-04-25 15:00:53 -07001172 mInitialCursorLoading = false;
1173 }
1174 } else {
1175 // Log the appropriate events that happen after the initial cursor is loaded
1176 if (newCursor.getCount() == 0 && mConversationCursorLastCount > 0) {
1177 Analytics.getInstance().sendEvent("empty_state", "post_delete",
1178 mFolder.getTypeDescription(), 0);
1179 }
Jin Cao779dd602014-04-22 16:16:28 -07001180 }
Jin Caofaa91982014-04-24 16:52:26 -07001181
Jin Cao8bf9c212014-04-25 15:00:53 -07001182 // We save the count here because for folders that are empty, multiple successful
1183 // cursor loads will occur with size of 0. Thus we don't want to emit any false
1184 // positive post_delete events.
1185 mConversationCursorLastCount = newCursor.getCount();
1186 } else {
1187 mConversationCursorLastCount = 0;
1188 }
Jin Cao343cf452014-04-18 14:05:25 -07001189 }
Jin Caofa934272014-05-01 13:38:21 -07001190
1191 /**
1192 * Helper function to determine if the given cursor is ready to populate the UI
1193 * @param cursor
1194 * @return
1195 */
1196 private boolean isCursorReadyToShow(ConversationCursor cursor) {
1197 if (cursor == null) {
1198 return false;
1199 }
1200 final int status = cursor.getExtras().getInt(
1201 UIProvider.CursorExtraKeys.EXTRA_STATUS);
1202 return (cursor.getCount() > 0 || !UIProvider.CursorStatus.isWaitingForResults(status));
1203 }
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -08001204}