blob: 7711f0dae60a5ed068313db1ccbb7cd92dd73322 [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
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -080020import android.app.Activity;
21import android.app.ListFragment;
Vikram Aggarwal37a20ca2013-06-06 11:19:49 -070022import android.app.LoaderManager;
Alice Yang20323162013-04-17 10:37:41 -070023import android.content.Context;
Vikram Aggarwal37a20ca2013-06-06 11:19:49 -070024import android.content.Loader;
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -080025import android.content.res.Resources;
Mindy Pereira6d8e7fe2012-07-26 16:02:49 -070026import android.database.DataSetObserver;
Vikram Aggarwal37a20ca2013-06-06 11:19:49 -070027import android.net.Uri;
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -080028import android.os.Bundle;
29import android.os.Handler;
Alice Yang0d74a662013-03-25 14:01:24 -070030import android.text.format.DateUtils;
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -080031import android.view.LayoutInflater;
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -080032import android.view.View;
33import android.view.ViewGroup;
34import android.view.ViewGroup.MarginLayoutParams;
35import 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;
Vikram Aggarwal80aeac52012-02-07 15:27:20 -080043import com.android.mail.browse.ConversationCursor;
Vikram Aggarwald247dc92012-02-10 15:49:01 -080044import com.android.mail.browse.ConversationItemView;
Mindy Pereira12fe37a2012-08-15 10:02:57 -070045import com.android.mail.browse.ConversationItemViewModel;
Mindy Pereira6681f6b2012-03-09 13:55:54 -080046import com.android.mail.browse.ConversationListFooterView;
Alice Yang64273142013-04-10 18:26:56 -070047import com.android.mail.browse.ToggleableItem;
Vikram Aggarwal37a20ca2013-06-06 11:19:49 -070048import com.android.mail.content.ObjectCursor;
49import com.android.mail.content.ObjectCursorLoader;
Vikram Aggarwal80aeac52012-02-07 15:27:20 -080050import com.android.mail.providers.Account;
Vikram Aggarwal7c401b72012-08-13 16:43:47 -070051import com.android.mail.providers.AccountObserver;
Vikram Aggarwald247dc92012-02-10 15:49:01 -080052import com.android.mail.providers.Conversation;
Mindy Pereira4f166de2012-02-14 13:40:58 -080053import com.android.mail.providers.Folder;
Vikram Aggarwal50ff0e52013-03-14 13:58:02 -070054import com.android.mail.providers.FolderObserver;
Vikram Aggarwal7d816002012-04-17 17:06:41 -070055import com.android.mail.providers.Settings;
Vikram Aggarwal80aeac52012-02-07 15:27:20 -080056import com.android.mail.providers.UIProvider;
Mindy Pereira4e812f42012-07-30 16:52:49 -070057import com.android.mail.providers.UIProvider.AccountCapabilities;
Scott Kennedy5dce1462013-08-05 11:26:40 -070058import com.android.mail.providers.UIProvider.ConversationListIcon;
Mindy Pereira4e812f42012-07-30 16:52:49 -070059import com.android.mail.providers.UIProvider.FolderCapabilities;
mindyp77c3e1b2012-09-11 14:05:02 -070060import com.android.mail.providers.UIProvider.FolderType;
Mindy Pereirae58222b2012-07-25 14:33:18 -070061import com.android.mail.providers.UIProvider.Swipe;
Scott Kennedy1fea6a32013-07-09 15:58:51 -070062import com.android.mail.ui.AnimatedAdapter.ConversationListListener;
mindyp9365a822012-09-12 09:09:09 -070063import com.android.mail.ui.SwipeableListView.ListItemSwipedListener;
64import com.android.mail.ui.SwipeableListView.ListItemsRemovedListener;
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -080065import com.android.mail.ui.ViewMode.ModeChangeListener;
Paul Westbrookb334c902012-06-25 11:42:46 -070066import com.android.mail.utils.LogTag;
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -080067import com.android.mail.utils.LogUtils;
Vikram Aggarwalfa131a22012-02-02 13:56:22 -080068import com.android.mail.utils.Utils;
Andrew Sapperstein6c570db2013-08-06 17:21:36 -070069import com.google.common.collect.ImmutableList;
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -080070
Mindy Pereiraf6a6b502012-03-15 15:24:26 -070071import java.util.Collection;
Scott Kennedy7c8325d2013-02-28 10:46:10 -080072import java.util.List;
Mindy Pereirafe06bea2012-02-16 08:15:14 -080073
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -080074/**
75 * The conversation list UI component.
76 */
Vikram Aggarwald247dc92012-02-10 15:49:01 -080077public final class ConversationListFragment extends ListFragment implements
mindyp9365a822012-09-12 09:09:09 -070078 OnItemLongClickListener, ModeChangeListener, ListItemSwipedListener {
Vikram Aggarwal649b9ea2012-08-27 12:15:20 -070079 /** Key used to pass data to {@link ConversationListFragment}. */
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -080080 private static final String CONVERSATION_LIST_KEY = "conversation-list";
Vikram Aggarwal649b9ea2012-08-27 12:15:20 -070081 /** Key used to keep track of the scroll state of the list. */
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -080082 private static final String LIST_STATE_KEY = "list-state";
Vikram Aggarwal649b9ea2012-08-27 12:15:20 -070083
Paul Westbrookb334c902012-06-25 11:42:46 -070084 private static final String LOG_TAG = LogTag.getLogTag();
Vikram Aggarwala91d00b2013-01-18 12:00:37 -080085 /** Key used to save the ListView choice mode, since ListView doesn't save it automatically! */
86 private static final String CHOICE_MODE_KEY = "choice-mode-key";
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -080087
Vikram Aggarwald7a12cd2012-02-03 09:36:20 -080088 // True if we are on a tablet device
89 private static boolean mTabletDevice;
90
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -080091 /**
mindyp9365a822012-09-12 09:09:09 -070092 * Frequency of update of timestamps. Initialized in
93 * {@link #onCreate(Bundle)} and final afterwards.
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -080094 */
95 private static int TIMESTAMP_UPDATE_INTERVAL = 0;
96
Alice Yang0d74a662013-03-25 14:01:24 -070097 private static long NO_NEW_MESSAGE_DURATION = 1 * DateUtils.SECOND_IN_MILLIS;
98
Vikram Aggarwal80aeac52012-02-07 15:27:20 -080099 private ControllableActivity mActivity;
Vikram Aggarwal80aeac52012-02-07 15:27:20 -0800100
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -0800101 // Control state.
102 private ConversationListCallbacks mCallbacks;
Vikram Aggarwal80aeac52012-02-07 15:27:20 -0800103
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -0800104 private final Handler mHandler = new Handler();
Vikram Aggarwal80aeac52012-02-07 15:27:20 -0800105
Alice Yang486e63e2013-04-05 13:01:50 -0700106 private ConversationListView mConversationListView;
107
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -0800108 // The internal view objects.
Mindy Pereiraf6a6b502012-03-15 15:24:26 -0700109 private SwipeableListView mListView;
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -0800110
111 private TextView mSearchResultCountTextView;
112 private TextView mSearchStatusTextView;
113
114 private View mSearchStatusView;
Vikram Aggarwal80aeac52012-02-07 15:27:20 -0800115
Vikram Aggarwal80aeac52012-02-07 15:27:20 -0800116 /**
117 * Current Account being viewed
118 */
Vikram Aggarwald247dc92012-02-10 15:49:01 -0800119 private Account mAccount;
Vikram Aggarwal80aeac52012-02-07 15:27:20 -0800120 /**
Mindy Pereira30fd47b2012-03-09 09:24:00 -0800121 * Current folder being viewed.
Vikram Aggarwal80aeac52012-02-07 15:27:20 -0800122 */
Mindy Pereira4f166de2012-02-14 13:40:58 -0800123 private Folder mFolder;
Vikram Aggarwal80aeac52012-02-07 15:27:20 -0800124
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -0800125 /**
126 * A simple method to update the timestamps of conversations periodically.
127 */
128 private Runnable mUpdateTimestampsRunnable = null;
129
130 private ConversationListContext mViewContext;
131
Vikram Aggarwal80aeac52012-02-07 15:27:20 -0800132 private AnimatedAdapter mListAdapter;
Vikram Aggarwal7d602882012-02-07 15:01:20 -0800133
Mindy Pereira6681f6b2012-03-09 13:55:54 -0800134 private ConversationListFooterView mFooterView;
Mindy Pereira6cc95532012-08-15 16:04:56 -0700135 private View mEmptyView;
Andrew Sappersteinc2c9dc12012-07-02 18:17:32 -0700136 private ErrorListener mErrorListener;
Vikram Aggarwal50ff0e52013-03-14 13:58:02 -0700137 private FolderObserver mFolderObserver;
Vikram Aggarwala91d00b2013-01-18 12:00:37 -0800138 private DataSetObserver mConversationCursorObserver;
Vikram Aggarwald247dc92012-02-10 15:49:01 -0800139
Vikram Aggarwal4f1cc0b2012-08-02 15:16:30 -0700140 private ConversationSelectionSet mSelectedSet;
Vikram Aggarwal7c401b72012-08-13 16:43:47 -0700141 private final AccountObserver mAccountObserver = new AccountObserver() {
142 @Override
143 public void onChanged(Account newAccount) {
144 mAccount = newAccount;
145 setSwipeAction();
146 }
147 };
mindyp9365a822012-09-12 09:09:09 -0700148 private ConversationUpdater mUpdater;
Vikram Aggarwal81a4f082012-09-28 09:19:04 -0700149 /** Hash of the Conversation Cursor we last obtained from the controller. */
150 private int mConversationCursorHash;
Vikram Aggarwal4f1cc0b2012-08-02 15:16:30 -0700151
Scott Kennedy1fea6a32013-07-09 15:58:51 -0700152 /** Duration, in milliseconds, of the CAB mode (peek icon) animation. */
153 private static long sSelectionModeAnimationDuration = -1;
Scott Kennedy1fea6a32013-07-09 15:58:51 -0700154 /** The time at which we last exited CAB mode. */
155 private long mSelectionModeExitedTimestamp = -1;
156
Vikram Aggarwal6c511582012-02-27 10:59:47 -0800157 /**
Vikram Aggarwal37a20ca2013-06-06 11:19:49 -0700158 * If the current list is for a folder with children, this set of loader callbacks will
159 * create a loader for all the child folders, and will return an {@link ObjectCursor} over the
160 * list.
161 */
162 private final class ChildFolderLoads
163 implements LoaderManager.LoaderCallbacks<ObjectCursor<Folder>> {
164 /** Load all child folders for the current folder. */
165 private static final int LOADER_CHIDREN = 0;
166 public static final String CHILD_URI = "arg-child-uri";
167 private final String[] projection = UIProvider.FOLDERS_PROJECTION;
168
169 @Override
170 public Loader<ObjectCursor<Folder>> onCreateLoader(int id, Bundle args) {
171 if (id != LOADER_CHIDREN) {
172 throw new IllegalStateException("ChildFolderLoads loading ID=" + id);
173 }
174 final Uri childUri = Uri.parse(args.getString(CHILD_URI));
175 return new ObjectCursorLoader<Folder>(
176 getActivity(), childUri, projection, Folder.FACTORY);
177 }
178
179 @Override
180 public void onLoadFinished(Loader<ObjectCursor<Folder>> loader, ObjectCursor<Folder> data) {
181 if (data != null && data.getCount() >= 0 && mListAdapter != null) {
182 mListAdapter.updateNestedFolders(data);
183 }
184 }
185
186 @Override
187 public void onLoaderReset(Loader<ObjectCursor<Folder>> loader) {
188 // Do nothing.
189 }
190 }
191
192 /** Callbacks to handle creating a loader and receiving child folders from it. */
193 private final ChildFolderLoads mChildCallback = new ChildFolderLoads();
194
195 /**
196 * Include all the folders at the cursor provided here in the conversation list.
197 * @param cursor The cursor containing child folders for the current folder.
198 */
199 private void showChildFolders(ObjectCursor<Folder> cursor) {
200
201 }
202
203 /**
mindyp9365a822012-09-12 09:09:09 -0700204 * Constructor needs to be public to handle orientation changes and activity
205 * lifecycle events.
Vikram Aggarwal6c511582012-02-27 10:59:47 -0800206 */
Paul Westbrookefc9f122012-02-21 11:14:49 -0800207 public ConversationListFragment() {
Vikram Aggarwald247dc92012-02-10 15:49:01 -0800208 super();
Vikram Aggarwald247dc92012-02-10 15:49:01 -0800209 }
210
Vikram Aggarwala91d00b2013-01-18 12:00:37 -0800211 private class ConversationCursorObserver extends DataSetObserver {
Mindy Pereira70a70c92012-08-02 08:39:45 -0700212 @Override
213 public void onChanged() {
Mindy Pereira70a70c92012-08-02 08:39:45 -0700214 onConversationListStatusUpdated();
215 }
216 }
217
Vikram Aggarwal7d602882012-02-07 15:01:20 -0800218 /**
mindyp9365a822012-09-12 09:09:09 -0700219 * Creates a new instance of {@link ConversationListFragment}, initialized
220 * to display conversation list context.
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -0800221 */
222 public static ConversationListFragment newInstance(ConversationListContext viewContext) {
Paul Westbrooke2be97a2013-05-21 02:12:09 -0700223 final ConversationListFragment fragment = new ConversationListFragment();
224 final Bundle args = new Bundle(1);
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -0800225 args.putBundle(CONVERSATION_LIST_KEY, viewContext.toBundle());
226 fragment.setArguments(args);
227 return fragment;
228 }
229
230 /**
mindyp9365a822012-09-12 09:09:09 -0700231 * Show the header if the current conversation list is showing search
232 * results.
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -0800233 */
Mindy Pereira967ede62012-03-22 09:29:09 -0700234 void configureSearchResultHeader() {
Mindy Pereira755fd6e2012-03-21 15:15:44 -0700235 if (mActivity == null) {
236 return;
237 }
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -0800238 // Only show the header if the context is for a search result
239 final Resources res = getResources();
Vikram Aggarwalae4ea992012-08-07 09:56:02 -0700240 final boolean showHeader = ConversationListContext.isSearchResult(mViewContext);
mindyp9365a822012-09-12 09:09:09 -0700241 // TODO(viki): This code contains intimate understanding of the view.
242 // Much of this logic
243 // needs to reside in a separate class that handles the text view in
244 // isolation. Then,
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -0800245 // that logic can be reused in other fragments.
246 if (showHeader) {
247 mSearchStatusTextView.setText(res.getString(R.string.search_results_searching_header));
248 // Initially reset the count
249 mSearchResultCountTextView.setText("");
250 }
251 mSearchStatusView.setVisibility(showHeader ? View.VISIBLE : View.GONE);
252 int marginTop = showHeader ? (int) res.getDimension(R.dimen.notification_view_height) : 0;
253 MarginLayoutParams layoutParams = (MarginLayoutParams) mListView.getLayoutParams();
254 layoutParams.topMargin = marginTop;
255 mListView.setLayoutParams(layoutParams);
256 }
257
Vikram Aggarwal80aeac52012-02-07 15:27:20 -0800258 /**
mindyp9365a822012-09-12 09:09:09 -0700259 * Show the header if the current conversation list is showing search
260 * results.
Mindy Pereira68f2e222012-03-07 10:36:54 -0800261 */
262 private void updateSearchResultHeader(int count) {
Mindy Pereira71a8f292012-07-26 16:30:48 -0700263 if (mActivity == null) {
264 return;
265 }
Mindy Pereira68f2e222012-03-07 10:36:54 -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);
Mindy Pereira68f2e222012-03-07 10:36:54 -0800269 if (showHeader) {
270 mSearchStatusTextView.setText(res.getString(R.string.search_results_header));
271 mSearchResultCountTextView
272 .setText(res.getString(R.string.search_results_loaded, count));
273 }
274 }
275
276 /**
Vikram Aggarwal80aeac52012-02-07 15:27:20 -0800277 * Initializes all internal state for a rendering.
278 */
279 private void initializeUiForFirstDisplay() {
mindyp9365a822012-09-12 09:09:09 -0700280 // TODO(mindyp): find some way to make the notification container more
281 // re-usable.
282 // TODO(viki): refactor according to comment in
283 // configureSearchResultHandler()
Vikram Aggarwal80aeac52012-02-07 15:27:20 -0800284 mSearchStatusView = mActivity.findViewById(R.id.search_status_view);
285 mSearchStatusTextView = (TextView) mActivity.findViewById(R.id.search_status_text_view);
mindyp9365a822012-09-12 09:09:09 -0700286 mSearchResultCountTextView = (TextView) mActivity
287 .findViewById(R.id.search_result_count_view);
Vikram Aggarwal80aeac52012-02-07 15:27:20 -0800288 }
289
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -0800290 @Override
Vikram Aggarwal0509bba2013-01-29 17:11:30 -0800291 public void onActivityCreated(Bundle savedState) {
292 super.onActivityCreated(savedState);
Scott Kennedy1fea6a32013-07-09 15:58:51 -0700293
294 if (sSelectionModeAnimationDuration < 0) {
295 sSelectionModeAnimationDuration = getResources().getInteger(
296 R.integer.conv_item_view_cab_anim_duration);
297 }
298
mindyp9365a822012-09-12 09:09:09 -0700299 // Strictly speaking, we get back an android.app.Activity from
300 // getActivity. However, the
301 // only activity creating a ConversationListContext is a MailActivity
302 // which is of type
303 // ControllableActivity, so this cast should be safe. If this cast
304 // fails, some other
305 // activity is creating ConversationListFragments. This activity must be
306 // of type
Vikram Aggarwal80aeac52012-02-07 15:27:20 -0800307 // ControllableActivity.
308 final Activity activity = getActivity();
mindyp9365a822012-09-12 09:09:09 -0700309 if (!(activity instanceof ControllableActivity)) {
310 LogUtils.e(LOG_TAG, "ConversationListFragment expects only a ControllableActivity to"
311 + "create it. Cannot proceed.");
Vikram Aggarwal80aeac52012-02-07 15:27:20 -0800312 }
313 mActivity = (ControllableActivity) activity;
mindyp9365a822012-09-12 09:09:09 -0700314 // Since we now have a controllable activity, load the account from it,
315 // and register for
Vikram Aggarwal7c401b72012-08-13 16:43:47 -0700316 // future account changes.
317 mAccount = mAccountObserver.initialize(mActivity.getAccountController());
Vikram Aggarwal7d602882012-02-07 15:01:20 -0800318 mCallbacks = mActivity.getListHandler();
Andrew Sappersteinc2c9dc12012-07-02 18:17:32 -0700319 mErrorListener = mActivity.getErrorListener();
Mindy Pereira278fd222012-07-26 15:23:10 -0700320 // Start off with the current state of the folder being viewed.
Alice Yang20323162013-04-17 10:37:41 -0700321 Context activityContext = mActivity.getActivityContext();
Mindy Pereira6681f6b2012-03-09 13:55:54 -0800322 mFooterView = (ConversationListFooterView) LayoutInflater.from(
Alice Yang20323162013-04-17 10:37:41 -0700323 activityContext).inflate(R.layout.conversation_list_footer_view,
Mindy Pereira6681f6b2012-03-09 13:55:54 -0800324 null);
Paul Westbrook4969e0c2012-08-20 14:38:39 -0700325 mFooterView.setClickListener(mActivity);
Alice Yangc36fa722013-05-05 01:23:46 -0700326 mConversationListView.setActivity(mActivity);
Vikram Aggarwal81a4f082012-09-28 09:19:04 -0700327 final ConversationCursor conversationCursor = getConversationListCursor();
Vikram Aggarwal37a20ca2013-06-06 11:19:49 -0700328 final LoaderManager manager = getLoaderManager();
329
330 // If this a parent folder, load all the child folders.
331 if (mViewContext.folder.hasChildren) {
332 final Uri childUri = mViewContext.folder.childFoldersListUri;
333 final Bundle args = new Bundle();
334 args.putString(ChildFolderLoads.CHILD_URI, childUri.toString());
335 manager.initLoader(ChildFolderLoads.LOADER_CHIDREN, args, mChildCallback);
336 }
Scott Kennedy7c8325d2013-02-28 10:46:10 -0800337
Alice Yangc5567732013-07-29 18:34:51 -0700338 // TODO: These special views are always created, doesn't matter whether they will
339 // be shown or not, as we add more views this will get more expensive. Given these are
340 // tips that are only shown once to the user, we should consider creating these on demand.
Scott Kennedy7c8325d2013-02-28 10:46:10 -0800341 final ConversationListHelper helper = mActivity.getConversationListHelper();
Mark Wei2102b2c2013-05-02 17:15:30 -0700342 final List<ConversationSpecialItemView> specialItemViews = helper != null ?
343 ImmutableList.copyOf(helper.makeConversationListSpecialViews(
Scott Kennedy103319a2013-07-26 13:35:35 -0700344 activity, mActivity, mAccount))
Mark Wei2102b2c2013-05-02 17:15:30 -0700345 : null;
Scott Kennedy7c8325d2013-02-28 10:46:10 -0800346 if (specialItemViews != null) {
347 // Attach to the LoaderManager
348 for (final ConversationSpecialItemView view : specialItemViews) {
Vikram Aggarwal37a20ca2013-06-06 11:19:49 -0700349 view.bindLoaderManager(manager);
Scott Kennedy7c8325d2013-02-28 10:46:10 -0800350 }
351 }
352
353 mListAdapter = new AnimatedAdapter(mActivity.getApplicationContext(), conversationCursor,
Scott Kennedy1fea6a32013-07-09 15:58:51 -0700354 mActivity.getSelectedSet(), mActivity, mConversationListListener, mListView,
355 specialItemViews, null);
Mindy Pereira6681f6b2012-03-09 13:55:54 -0800356 mListAdapter.addFooter(mFooterView);
Mindy Pereira6f4a6af2012-02-29 11:48:52 -0800357 mListView.setAdapter(mListAdapter);
Vikram Aggarwal4f1cc0b2012-08-02 15:16:30 -0700358 mSelectedSet = mActivity.getSelectedSet();
359 mListView.setSelectionSet(mSelectedSet);
Vikram Aggarwal37a20ca2013-06-06 11:19:49 -0700360 mListAdapter.setFooterVisibility(false);
Vikram Aggarwal50ff0e52013-03-14 13:58:02 -0700361 mFolderObserver = new FolderObserver(){
362 @Override
363 public void onChanged(Folder newFolder) {
364 onFolderUpdated(newFolder);
365 }
366 };
367 mFolderObserver.initialize(mActivity.getFolderController());
Vikram Aggarwala91d00b2013-01-18 12:00:37 -0800368 mConversationCursorObserver = new ConversationCursorObserver();
mindyp9365a822012-09-12 09:09:09 -0700369 mUpdater = mActivity.getConversationUpdater();
Vikram Aggarwala91d00b2013-01-18 12:00:37 -0800370 mUpdater.registerConversationListObserver(mConversationCursorObserver);
Vikram Aggarwalbcb16b92013-01-28 18:05:03 -0800371 mTabletDevice = Utils.useTabletUI(mActivity.getApplicationContext().getResources());
Vikram Aggarwal80aeac52012-02-07 15:27:20 -0800372 initializeUiForFirstDisplay();
Mindy Pereiradc8963b2012-03-28 17:51:05 -0700373 configureSearchResultHeader();
mindyp9365a822012-09-12 09:09:09 -0700374 // The onViewModeChanged callback doesn't get called when the mode
375 // object is created, so
Vikram Aggarwalfa131a22012-02-02 13:56:22 -0800376 // force setting the mode manually this time around.
Mindy Pereiraf96ec322012-03-02 14:06:33 -0800377 onViewModeChanged(mActivity.getViewMode().getMode());
Mindy Pereirab5901be2012-08-09 17:21:53 -0700378 mActivity.getViewMode().addListener(this);
Paul Westbrook0e3fd9d2012-04-20 02:02:23 -0700379
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -0800380 if (mActivity.isFinishing()) {
381 // Activity is finishing, just bail.
382 return;
383 }
Vikram Aggarwal81a4f082012-09-28 09:19:04 -0700384 mConversationCursorHash = (conversationCursor == null) ? 0 : conversationCursor.hashCode();
385 // Belt and suspenders here; make sure we do any necessary sync of the
386 // ConversationCursor
387 if (conversationCursor != null && conversationCursor.isRefreshReady()) {
388 conversationCursor.sync();
389 }
390
Vikram Aggarwal0509bba2013-01-29 17:11:30 -0800391 // On a phone we never highlight a conversation, so the default is to select none.
392 // On a tablet, we highlight a SINGLE conversation in landscape conversation view.
393 int choice = getDefaultChoiceMode(mTabletDevice);
394 if (savedState != null) {
395 // Restore the choice mode if it was set earlier, or NONE if creating a fresh view.
396 // Choice mode here represents the current conversation only. CAB mode does not rely on
397 // the platform: checked state is a local variable {@link ConversationItemView#mChecked}
398 choice = savedState.getInt(CHOICE_MODE_KEY, choice);
399 if (savedState.containsKey(LIST_STATE_KEY)) {
400 // TODO: find a better way to unset the selected item when restoring
401 mListView.clearChoices();
402 }
403 }
404 setChoiceMode(choice);
405
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -0800406 // Show list and start loading list.
407 showList();
Mindy Pereira4765c5c2012-07-19 11:58:22 -0700408 ToastBarOperation pendingOp = mActivity.getPendingToastOperation();
409 if (pendingOp != null) {
410 // Clear the pending operation
411 mActivity.setPendingToastOperation(null);
412 mActivity.onUndoAvailable(pendingOp);
413 }
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -0800414 }
415
Vikram Aggarwal0509bba2013-01-29 17:11:30 -0800416 /**
417 * Returns the default choice mode for the list based on whether the list is displayed on tablet
418 * or not.
419 * @param isTablet
420 * @return
421 */
422 private final static int getDefaultChoiceMode(boolean isTablet) {
423 return isTablet ? ListView.CHOICE_MODE_SINGLE : ListView.CHOICE_MODE_NONE;
424 }
425
Mindy Pereira967ede62012-03-22 09:29:09 -0700426 public AnimatedAdapter getAnimatedAdapter() {
427 return mListAdapter;
428 }
429
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -0800430 @Override
Vikram Aggarwald247dc92012-02-10 15:49:01 -0800431 public void onCreate(Bundle savedState) {
Vikram Aggarwald247dc92012-02-10 15:49:01 -0800432 super.onCreate(savedState);
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -0800433
434 // Initialize fragment constants from resources
Vikram Aggarwal6c511582012-02-27 10:59:47 -0800435 final Resources res = getResources();
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -0800436 TIMESTAMP_UPDATE_INTERVAL = res.getInteger(R.integer.timestamp_update_interval);
mindyp9365a822012-09-12 09:09:09 -0700437 mUpdateTimestampsRunnable = new Runnable() {
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -0800438 @Override
439 public void run() {
440 mListView.invalidateViews();
441 mHandler.postDelayed(mUpdateTimestampsRunnable, TIMESTAMP_UPDATE_INTERVAL);
442 }
443 };
444
Vikram Aggarwal80aeac52012-02-07 15:27:20 -0800445 // Get the context from the arguments
Vikram Aggarwal6c511582012-02-27 10:59:47 -0800446 final Bundle args = getArguments();
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -0800447 mViewContext = ConversationListContext.forBundle(args.getBundle(CONVERSATION_LIST_KEY));
Mindy Pereira3982e232012-02-29 15:00:34 -0800448 mAccount = mViewContext.account;
Paul Westbrook80ecd892012-08-16 13:37:39 -0700449
Vikram Aggarwal9a49c9b2012-08-31 10:49:33 -0700450 setRetainInstance(false);
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -0800451 }
452
453 @Override
Andy Huang9e4ca792013-02-28 14:33:43 -0800454 public String toString() {
455 final String s = super.toString();
456 if (mViewContext == null) {
457 return s;
458 }
Andy Huangc1922a92013-05-13 14:33:05 -0700459 final StringBuilder sb = new StringBuilder(s);
460 sb.setLength(sb.length() - 1);
461 sb.append(" mListAdapter=");
462 sb.append(mListAdapter);
463 sb.append(" folder=");
464 sb.append(mViewContext.folder);
465 sb.append("}");
466 return sb.toString();
Andy Huang9e4ca792013-02-28 14:33:43 -0800467 }
468
469 @Override
Vikram Aggarwal9a49c9b2012-08-31 10:49:33 -0700470 public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedState) {
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -0800471 View rootView = inflater.inflate(R.layout.conversation_list, null);
Mindy Pereira6cc95532012-08-15 16:04:56 -0700472 mEmptyView = rootView.findViewById(R.id.empty_view);
Alice Yang486e63e2013-04-05 13:01:50 -0700473 mConversationListView =
474 (ConversationListView) rootView.findViewById(R.id.conversation_list);
Alice Yang20323162013-04-17 10:37:41 -0700475 mConversationListView.setConversationContext(mViewContext);
Mindy Pereiraf6a6b502012-03-15 15:24:26 -0700476 mListView = (SwipeableListView) rootView.findViewById(android.R.id.list);
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -0800477 mListView.setHeaderDividersEnabled(false);
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -0800478 mListView.setOnItemLongClickListener(this);
Mindy Pereira4e812f42012-07-30 16:52:49 -0700479 mListView.enableSwipe(mAccount.supportsCapability(AccountCapabilities.UNDO));
mindyp9365a822012-09-12 09:09:09 -0700480 mListView.setSwipedListener(this);
Marc Blank2af0cf32012-03-01 19:24:13 -0800481
Vikram Aggarwal0509bba2013-01-29 17:11:30 -0800482 if (savedState != null && savedState.containsKey(LIST_STATE_KEY)) {
483 mListView.onRestoreInstanceState(savedState.getParcelable(LIST_STATE_KEY));
Vikram Aggarwal9a49c9b2012-08-31 10:49:33 -0700484 }
Alice Yang0d74a662013-03-25 14:01:24 -0700485
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -0800486 return rootView;
487 }
488
Vikram Aggarwala91d00b2013-01-18 12:00:37 -0800489 /**
490 * Sets the choice mode of the list view
491 * @param choiceMode ListView#
492 */
493 private final void setChoiceMode(int choiceMode) {
494 mListView.setChoiceMode(choiceMode);
495 }
496
497 /**
498 * Tell the list to select nothing.
499 */
500 public final void setChoiceNone() {
Vikram Aggarwal0509bba2013-01-29 17:11:30 -0800501 // On a phone, the default choice mode is already none, so nothing to do.
502 if (!mTabletDevice) {
503 return;
504 }
Andy Huangb0be3fc2013-05-02 15:48:05 -0700505 clearChoicesAndActivated();
Vikram Aggarwala91d00b2013-01-18 12:00:37 -0800506 setChoiceMode(ListView.CHOICE_MODE_NONE);
507 }
508
509 /**
510 * Tell the list to get out of selecting none.
511 */
512 public final void revertChoiceMode() {
Vikram Aggarwal0509bba2013-01-29 17:11:30 -0800513 // On a phone, the default choice mode is always none, so nothing to do.
514 if (!mTabletDevice) {
515 return;
516 }
517 setChoiceMode(getDefaultChoiceMode(mTabletDevice));
Vikram Aggarwala91d00b2013-01-18 12:00:37 -0800518 }
519
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -0800520 @Override
Andy Huang7517e3b2012-08-20 12:30:08 -0700521 public void onDestroy() {
Andy Huang7517e3b2012-08-20 12:30:08 -0700522 super.onDestroy();
523 }
524
525 @Override
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -0800526 public void onDestroyView() {
Andy Huang7517e3b2012-08-20 12:30:08 -0700527
Vikram Aggarwal7c401b72012-08-13 16:43:47 -0700528 // Clear the list's adapter
529 mListAdapter.destroy();
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -0800530 mListView.setAdapter(null);
531
Andrew Sapperstein6c570db2013-08-06 17:21:36 -0700532 mActivity.getViewMode().removeListener(this);
Mindy Pereira71a8f292012-07-26 16:30:48 -0700533 if (mFolderObserver != null) {
Vikram Aggarwal50ff0e52013-03-14 13:58:02 -0700534 mFolderObserver.unregisterAndDestroy();
Mindy Pereira71a8f292012-07-26 16:30:48 -0700535 mFolderObserver = null;
536 }
Vikram Aggarwala91d00b2013-01-18 12:00:37 -0800537 if (mConversationCursorObserver != null) {
538 mUpdater.unregisterConversationListObserver(mConversationCursorObserver);
539 mConversationCursorObserver = null;
Mindy Pereira70a70c92012-08-02 08:39:45 -0700540 }
Vikram Aggarwal7c401b72012-08-13 16:43:47 -0700541 mAccountObserver.unregisterAndDestroy();
Scott Kennedy7c8325d2013-02-28 10:46:10 -0800542 getAnimatedAdapter().cleanup();
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -0800543 super.onDestroyView();
544 }
545
Vikram Aggarwal9730ea02012-08-02 12:46:19 -0700546 /**
mindyp9365a822012-09-12 09:09:09 -0700547 * There are three binary variables, which determine what we do with a
548 * message. checkbEnabled: Whether check boxes are enabled or not (forced
549 * true on tablet) cabModeOn: Whether CAB mode is currently on or not.
550 * pressType: long or short tap (There is a third possibility: phone or
551 * tablet, but they have <em>identical</em> behavior) The matrix of
552 * possibilities is:
553 * <p>
554 * Long tap: Always toggle selection of conversation. If CAB mode is not
555 * started, then start it.
Vikram Aggarwal9730ea02012-08-02 12:46:19 -0700556 * <pre>
557 * | Checkboxes | No Checkboxes
558 * ----------+------------+---------------
559 * CAB mode | Select | Select
560 * List mode | Select | Select
561 *
Vikram Aggarwal9730ea02012-08-02 12:46:19 -0700562 * </pre>
mindyp9365a822012-09-12 09:09:09 -0700563 *
Vikram Aggarwal9730ea02012-08-02 12:46:19 -0700564 * Reference: http://b/issue?id=6392199
565 * <p>
566 * {@inheritDoc}
567 */
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -0800568 @Override
569 public boolean onItemLongClick(AdapterView<?> parent, View view, int position, long id) {
Mindy Pereira1e231a62012-03-23 12:42:15 -0700570 // Ignore anything that is not a conversation item. Could be a footer.
571 if (!(view instanceof ConversationItemView)) {
Scott Kennedy7c8325d2013-02-28 10:46:10 -0800572 return false;
Mindy Pereira1e231a62012-03-23 12:42:15 -0700573 }
Scott Kennedy955a7662013-08-06 17:12:57 -0700574 return ((ConversationItemView) view).toggleSelectedStateOrBeginDrag();
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -0800575 }
576
Vikram Aggarwal4f1cc0b2012-08-02 15:16:30 -0700577 /**
mindyp9365a822012-09-12 09:09:09 -0700578 * See the comment for
579 * {@link #onItemLongClick(AdapterView, View, int, long)}.
580 * <p>
581 * Short tap behavior:
582 *
Vikram Aggarwal4f1cc0b2012-08-02 15:16:30 -0700583 * <pre>
584 * | Checkboxes | No Checkboxes
585 * ----------+------------+---------------
Scott Kennedy9ba7fba2013-07-30 17:41:26 -0700586 * CAB mode | Peek | Select
Vikram Aggarwal4f1cc0b2012-08-02 15:16:30 -0700587 * List mode | Peek | Peek
588 * </pre>
mindyp9365a822012-09-12 09:09:09 -0700589 *
Vikram Aggarwal4f1cc0b2012-08-02 15:16:30 -0700590 * Reference: http://b/issue?id=6392199
591 * <p>
592 * {@inheritDoc}
593 */
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -0800594 @Override
Vikram Aggarwal54452ae2012-03-13 15:29:00 -0700595 public void onListItemClick(ListView l, View view, int position, long id) {
Vikram Aggarwal37a20ca2013-06-06 11:19:49 -0700596 if (view instanceof NestedFolderView) {
597 final FolderSelector selector = mActivity.getFolderSelector();
598 selector.onFolderSelected(((NestedFolderView) view).getFolder());
599 } else if (view instanceof ToggleableItem) {
Scott Kennedy5dce1462013-08-05 11:26:40 -0700600 final boolean showSenderImage =
601 (mAccount.settings.convListIcon == ConversationListIcon.SENDER_IMAGE);
602 if (!showSenderImage && !mSelectedSet.isEmpty()) {
Vikram Aggarwal37a20ca2013-06-06 11:19:49 -0700603 ((ToggleableItem) view).toggleSelectedState();
604 } else {
Scott Kennedy8afccad2013-07-28 19:09:11 -0700605 viewConversation(position);
Vikram Aggarwal37a20ca2013-06-06 11:19:49 -0700606 }
Vikram Aggarwal4f1cc0b2012-08-02 15:16:30 -0700607 } else {
Vikram Aggarwal37a20ca2013-06-06 11:19:49 -0700608 // Ignore anything that is not a conversation item. Could be a footer.
609 // If we are using a keyboard, the highlighted item is the parent;
610 // otherwise, this is a direct call from the ConverationItemView
611 return;
Vikram Aggarwal4f1cc0b2012-08-02 15:16:30 -0700612 }
mindyp9365a822012-09-12 09:09:09 -0700613 // When a new list item is clicked, commit any existing leave behind
mindyp8694fe92012-09-25 11:07:16 -0700614 // items. Wait until we have opened the desired conversation to cause
615 // any position changes.
Vikram Aggarwalbcb16b92013-01-28 18:05:03 -0800616 commitDestructiveActions(Utils.useTabletUI(mActivity.getActivityContext().getResources()));
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -0800617 }
618
619 @Override
Scott Kennedy08a079c2013-02-22 10:13:10 -0800620 public void onResume() {
621 super.onResume();
622
623 final ConversationCursor conversationCursor = getConversationListCursor();
624 if (conversationCursor != null) {
625 conversationCursor.handleNotificationActions();
626 }
Scott Kennedy1fea6a32013-07-09 15:58:51 -0700627
628 mSelectedSet.addObserver(mConversationSetObserver);
Scott Kennedy08a079c2013-02-22 10:13:10 -0800629 }
630
631 @Override
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -0800632 public void onPause() {
633 super.onPause();
Scott Kennedy1fea6a32013-07-09 15:58:51 -0700634
635 mSelectedSet.removeObserver(mConversationSetObserver);
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -0800636 }
637
638 @Override
639 public void onSaveInstanceState(Bundle outState) {
640 super.onSaveInstanceState(outState);
641 if (mListView != null) {
642 outState.putParcelable(LIST_STATE_KEY, mListView.onSaveInstanceState());
Vikram Aggarwala91d00b2013-01-18 12:00:37 -0800643 outState.putInt(CHOICE_MODE_KEY, mListView.getChoiceMode());
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -0800644 }
645 }
646
647 @Override
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -0800648 public void onStart() {
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -0800649 super.onStart();
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -0800650 mHandler.postDelayed(mUpdateTimestampsRunnable, TIMESTAMP_UPDATE_INTERVAL);
Andy Huang761522c2013-08-08 13:09:11 -0700651 Analytics.getInstance().sendView(getClass().getName());
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -0800652 }
653
654 @Override
655 public void onStop() {
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -0800656 super.onStop();
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -0800657 mHandler.removeCallbacks(mUpdateTimestampsRunnable);
658 }
659
Vikram Aggarwal80aeac52012-02-07 15:27:20 -0800660 @Override
Vikram Aggarwalfa131a22012-02-02 13:56:22 -0800661 public void onViewModeChanged(int newMode) {
Vikram Aggarwalfa131a22012-02-02 13:56:22 -0800662 if (mTabletDevice) {
Andy Huangb0be3fc2013-05-02 15:48:05 -0700663 if (ViewMode.isListMode(newMode)) {
Vikram Aggarwal0509bba2013-01-29 17:11:30 -0800664 // There are no selected conversations when in conversation list mode.
Andy Huangb0be3fc2013-05-02 15:48:05 -0700665 clearChoicesAndActivated();
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -0800666 }
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -0800667 }
Mindy Pereirab5901be2012-08-09 17:21:53 -0700668 if (mFooterView != null) {
669 mFooterView.onViewModeChanged(newMode);
670 }
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -0800671 }
Mindy Pereirab5901be2012-08-09 17:21:53 -0700672
Andy Huang48ccbc52013-06-05 20:30:47 -0700673 public boolean isAnimating() {
674 final AnimatedAdapter adapter = getAnimatedAdapter();
675 return (adapter != null && adapter.isAnimating()) ||
676 (mListView != null && mListView.isScrolling());
677 }
678
Andy Huangb0be3fc2013-05-02 15:48:05 -0700679 private void clearChoicesAndActivated() {
680 final int currentSelected = mListView.getCheckedItemPosition();
Andy Huangb0be3fc2013-05-02 15:48:05 -0700681 if (currentSelected != ListView.INVALID_POSITION) {
Andy Huangf0aebd32013-06-17 15:00:15 -0700682 mListView.setItemChecked(mListView.getCheckedItemPosition(), false);
Andy Huangb0be3fc2013-05-02 15:48:05 -0700683 }
684 }
685
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -0800686 /**
mindyp9365a822012-09-12 09:09:09 -0700687 * Handles a request to show a new conversation list, either from a search
688 * query or for viewing a folder. This will initiate a data load, and hence
689 * must be called on the UI thread.
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -0800690 */
691 private void showList() {
Vikram Aggarwal80aeac52012-02-07 15:27:20 -0800692 mListView.setEmptyView(null);
Andy Huang7591d2f2012-07-26 16:48:06 -0700693 onFolderUpdated(mActivity.getFolderController().getFolder());
mindypca8ca2d2012-09-11 17:38:34 -0700694 onConversationListStatusUpdated();
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -0800695 }
696
Scott Kennedy8afccad2013-07-28 19:09:11 -0700697 /**
698 * View the message at the given position.
699 *
700 * @param position The position of the conversation in the list (as opposed to its position
701 * in the cursor)
702 */
703 private void viewConversation(final int position) {
704 LogUtils.d(LOG_TAG, "ConversationListFragment.viewConversation(%d)", position);
705
706 final ConversationCursor cursor =
707 (ConversationCursor) getAnimatedAdapter().getItem(position);
708
709 if (cursor == null) {
710 LogUtils.e(LOG_TAG,
711 "unable to open conv at cursor pos=%s cursor=%s getPositionOffset=%s",
712 position, cursor, getAnimatedAdapter().getPositionOffset(position));
713 return;
714 }
715
716 final Conversation conv = cursor.getConversation();
717 /*
718 * The cursor position may be different than the position method parameter because of
719 * special views in the list.
720 */
721 conv.position = cursor.getPosition();
722 setSelected(conv.position, true);
723 mCallbacks.onConversationSelected(conv, false /* inLoaderCallbacks */);
724 }
725
Scott Kennedy1fea6a32013-07-09 15:58:51 -0700726 private final ConversationListListener mConversationListListener =
727 new ConversationListListener() {
728 @Override
Scott Kennedy1fea6a32013-07-09 15:58:51 -0700729 public boolean isExitingSelectionMode() {
730 return System.currentTimeMillis() <
731 (mSelectionModeExitedTimestamp + sSelectionModeAnimationDuration);
732 }
733 };
Vikram Aggarwal3d7ca9d2012-05-11 14:40:36 -0700734
Vikram Aggarwala91d00b2013-01-18 12:00:37 -0800735 /**
736 * Sets the selected conversation to the position given here.
Alice Yang0d74a662013-03-25 14:01:24 -0700737 * @param cursorPosition The position of the conversation in the cursor (as opposed to
738 * in the list)
Vikram Aggarwala91d00b2013-01-18 12:00:37 -0800739 * @param different if the currently selected conversation is different from the one provided
740 * here. This is a difference in conversations, not a difference in positions. For example, a
741 * conversation at position 2 can move to position 4 as a result of new mail.
742 */
Scott Kennedy7c8325d2013-02-28 10:46:10 -0800743 public void setSelected(final int cursorPosition, boolean different) {
Vikram Aggarwala91d00b2013-01-18 12:00:37 -0800744 if (mListView.getChoiceMode() == ListView.CHOICE_MODE_NONE) {
745 return;
746 }
Scott Kennedy7c8325d2013-02-28 10:46:10 -0800747
748 final int position =
749 cursorPosition + getAnimatedAdapter().getPositionOffset(cursorPosition);
750
mindype21f8862012-10-01 09:32:03 -0700751 if (different) {
752 mListView.smoothScrollToPosition(position);
753 }
754 mListView.setItemChecked(position, true);
755 }
756
Vikram Aggarwal3d7ca9d2012-05-11 14:40:36 -0700757 /**
Vikram Aggarwala91d00b2013-01-18 12:00:37 -0800758 * Returns the cursor associated with the conversation list.
759 * @return
Vikram Aggarwal3d7ca9d2012-05-11 14:40:36 -0700760 */
Mindy Pereira967ede62012-03-22 09:29:09 -0700761 private ConversationCursor getConversationListCursor() {
762 return mCallbacks != null ? mCallbacks.getConversationListCursor() : null;
Mindy Pereirafe06bea2012-02-16 08:15:14 -0800763 }
764
765 /**
mindyp9365a822012-09-12 09:09:09 -0700766 * Request a refresh of the list. No sync is carried out and none is
767 * promised.
Vikram Aggarwal54452ae2012-03-13 15:29:00 -0700768 */
769 public void requestListRefresh() {
770 mListAdapter.notifyDataSetChanged();
771 }
772
Vikram Aggarwal75daee52012-04-30 11:13:09 -0700773 /**
Vikram Aggarwal7f602f72012-04-30 16:04:06 -0700774 * Change the UI to delete the conversations provided and then call the
mindyp9365a822012-09-12 09:09:09 -0700775 * {@link DestructiveAction} provided here <b>after</b> the UI has been
776 * updated.
Vikram Aggarwal7f602f72012-04-30 16:04:06 -0700777 * @param conversations
Vikram Aggarwal75daee52012-04-30 11:13:09 -0700778 * @param action
779 */
Vikram Aggarwala8e43182012-09-13 12:55:10 -0700780 public void requestDelete(int actionId, final Collection<Conversation> conversations,
Vikram Aggarwal669947b2013-01-10 17:05:56 -0800781 final DestructiveAction action) {
Mindy Pereiraacf60392012-04-06 09:11:00 -0700782 for (Conversation conv : conversations) {
783 conv.localDeleteOnUpdate = true;
784 }
Vikram Aggarwala8e43182012-09-13 12:55:10 -0700785 final ListItemsRemovedListener listener = new ListItemsRemovedListener() {
mindyp9365a822012-09-12 09:09:09 -0700786 @Override
787 public void onListItemsRemoved() {
788 action.performAction();
789 }
Vikram Aggarwala8e43182012-09-13 12:55:10 -0700790 };
791 final SwipeableListView listView = (SwipeableListView) getListView();
792 if (listView.getSwipeAction() == actionId) {
Paul Westbrookcec3e0b2013-02-04 22:50:22 -0800793 if (!listView.destroyItems(conversations, listener)) {
794 // The listView failed to destroy the items, perform the action manually
795 LogUtils.e(LOG_TAG, "ConversationListFragment.requestDelete: " +
796 "listView failed to destroy items.");
797 action.performAction();
798 }
Vikram Aggarwala8e43182012-09-13 12:55:10 -0700799 return;
800 }
801 // Delete the local delete items (all for now) and when done,
802 // update...
803 mListAdapter.delete(conversations, listener);
Mindy Pereiraacf60392012-04-06 09:11:00 -0700804 }
Mindy Pereiraa3911aa2012-03-09 13:26:26 -0800805
Mindy Pereira11dd5ef2012-03-10 15:10:18 -0800806 public void onFolderUpdated(Folder folder) {
Mindy Pereiradc8963b2012-03-28 17:51:05 -0700807 mFolder = folder;
Mindy Pereira06642fa2012-07-12 16:23:27 -0700808 setSwipeAction();
Mindy Pereira82cea482012-03-27 16:45:00 -0700809 if (mFolder == null) {
810 return;
811 }
Mindy Pereira4584a0d2012-03-13 14:42:14 -0700812 mListAdapter.setFolder(mFolder);
Mindy Pereira70a70c92012-08-02 08:39:45 -0700813 mFooterView.setFolder(mFolder);
Vikram Aggarwal41b9e8f2012-09-25 10:15:04 -0700814 if (!mFolder.wasSyncSuccessful()) {
Mindy Pereira70a70c92012-08-02 08:39:45 -0700815 mErrorListener.onError(mFolder, false);
816 }
Paul Westbrookc01f5d92012-09-25 17:22:25 -0700817
818 // Notify of changes to the Folder.
819 onFolderStatusUpdated();
820
Mindy Pereira12fe37a2012-08-15 10:02:57 -0700821 // Blow away conversation items cache.
822 ConversationItemViewModel.onFolderUpdated(mFolder);
Mindy Pereira70a70c92012-08-02 08:39:45 -0700823 }
824
Vikram Aggarwal81a4f082012-09-28 09:19:04 -0700825 /**
826 * Updates the footer visibility and updates the conversation cursor
827 */
Mindy Pereira70a70c92012-08-02 08:39:45 -0700828 public void onConversationListStatusUpdated() {
Paul Westbrook9a70e912012-08-17 15:53:20 -0700829 final ConversationCursor cursor = getConversationListCursor();
830 final boolean showFooter = cursor != null && mFooterView.updateStatus(cursor);
Paul Westbrookc01f5d92012-09-25 17:22:25 -0700831 // Update the folder status, in case the cursor could affect it.
832 onFolderStatusUpdated();
833 mListAdapter.setFooterVisibility(showFooter);
834
835 // Also change the cursor here.
836 onCursorUpdated();
837 }
838
839 private void onFolderStatusUpdated() {
Alice Yang0d74a662013-03-25 14:01:24 -0700840 // Update the sync status bar with sync results if needed
Alice Yang486e63e2013-04-05 13:01:50 -0700841 checkSyncStatus();
Alice Yang0d74a662013-03-25 14:01:24 -0700842
Paul Westbrookc01f5d92012-09-25 17:22:25 -0700843 final ConversationCursor cursor = getConversationListCursor();
Paul Westbrook9a70e912012-08-17 15:53:20 -0700844 Bundle extras = cursor != null ? cursor.getExtras() : Bundle.EMPTY;
Andrew Sapperstein427df9d2013-05-15 12:05:09 -0700845 int errorStatus = extras.containsKey(UIProvider.CursorExtraKeys.EXTRA_ERROR) ?
Mindy Pereira70a70c92012-08-02 08:39:45 -0700846 extras.getInt(UIProvider.CursorExtraKeys.EXTRA_ERROR)
847 : UIProvider.LastSyncResult.SUCCESS;
Andrew Sapperstein427df9d2013-05-15 12:05:09 -0700848 int cursorStatus = extras.getInt(UIProvider.CursorExtraKeys.EXTRA_STATUS);
Alice Yang0d74a662013-03-25 14:01:24 -0700849 // We want to update the UI with this information if either we are loaded or complete, or
Paul Westbrookc01f5d92012-09-25 17:22:25 -0700850 // we have a folder with a non-0 count.
851 final int folderCount = mFolder != null ? mFolder.totalCount : 0;
Andrew Sapperstein427df9d2013-05-15 12:05:09 -0700852 if (errorStatus == UIProvider.LastSyncResult.SUCCESS
853 && (cursorStatus == UIProvider.CursorStatus.LOADED
854 || cursorStatus == UIProvider.CursorStatus.COMPLETE) || folderCount > 0) {
Paul Westbrookc01f5d92012-09-25 17:22:25 -0700855 updateSearchResultHeader(folderCount);
856 if (folderCount == 0) {
Mindy Pereira6cc95532012-08-15 16:04:56 -0700857 mListView.setEmptyView(mEmptyView);
858 }
Mindy Pereira70a70c92012-08-02 08:39:45 -0700859 }
Mindy Pereiraa3911aa2012-03-09 13:26:26 -0800860 }
Mindy Pereiraf6a6b502012-03-15 15:24:26 -0700861
Mindy Pereira06642fa2012-07-12 16:23:27 -0700862 private void setSwipeAction() {
Vikram Aggarwal7c401b72012-08-13 16:43:47 -0700863 int swipeSetting = Settings.getSwipeSetting(mAccount.settings);
Mindy Pereirae58222b2012-07-25 14:33:18 -0700864 if (swipeSetting == Swipe.DISABLED
Mindy Pereirab8073372012-08-09 14:00:10 -0700865 || !mAccount.supportsCapability(AccountCapabilities.UNDO)
866 || (mFolder != null && mFolder.isTrash())) {
Mindy Pereirae58222b2012-07-25 14:33:18 -0700867 mListView.enableSwipe(false);
868 } else {
Scott Kennedy3b965d72013-06-25 14:36:55 -0700869 final int action;
Rohan Shah7f98d0d2013-02-15 13:21:09 -0800870 mListView.enableSwipe(true);
mindyp77c3e1b2012-09-11 14:05:02 -0700871 if (ConversationListContext.isSearchResult(mViewContext)
Scott Kennedy8c1058e2013-03-20 13:40:20 -0700872 || (mFolder != null && mFolder.isType(FolderType.SPAM))) {
Mindy Pereirae58222b2012-07-25 14:33:18 -0700873 action = R.id.delete;
Mindy Pereira4e812f42012-07-30 16:52:49 -0700874 } else if (mFolder == null) {
Mindy Pereira01f30502012-08-14 10:30:51 -0700875 action = R.id.remove_folder;
Mindy Pereira4e812f42012-07-30 16:52:49 -0700876 } else {
877 // We have enough information to respect user settings.
878 switch (swipeSetting) {
879 case Swipe.ARCHIVE:
880 if (mAccount.supportsCapability(AccountCapabilities.ARCHIVE)) {
881 if (mFolder.supportsCapability(FolderCapabilities.ARCHIVE)) {
882 action = R.id.archive;
883 break;
884 } else if (mFolder.supportsCapability
885 (FolderCapabilities.CAN_ACCEPT_MOVED_MESSAGES)) {
Mindy Pereira01f30502012-08-14 10:30:51 -0700886 action = R.id.remove_folder;
Mindy Pereira4e812f42012-07-30 16:52:49 -0700887 break;
888 }
889 }
Scott Kennedy3b965d72013-06-25 14:36:55 -0700890
891 /*
892 * If we get here, we don't support archive, on either the account or the
893 * folder, so we want to fall through into the delete case.
894 */
895 //$FALL-THROUGH$
Mindy Pereira4e812f42012-07-30 16:52:49 -0700896 case Swipe.DELETE:
897 default:
898 action = R.id.delete;
899 break;
900 }
Mindy Pereirae58222b2012-07-25 14:33:18 -0700901 }
902 mListView.setSwipeAction(action);
903 }
Mark Wei6126d722013-04-24 21:09:43 -0700904 mListView.setCurrentAccount(mAccount);
Mindy Pereira06642fa2012-07-12 16:23:27 -0700905 mListView.setCurrentFolder(mFolder);
906 }
907
Vikram Aggarwal17f373e2012-09-17 15:49:32 -0700908 /**
909 * Changes the conversation cursor in the list and sets selected position if none is set.
910 */
911 private void onCursorUpdated() {
912 if (mCallbacks == null || mListAdapter == null) {
913 return;
914 }
Vikram Aggarwal81a4f082012-09-28 09:19:04 -0700915 // Check against the previous cursor here and see if they are the same. If they are, then
916 // do a notifyDataSetChanged.
917 final ConversationCursor newCursor = mCallbacks.getConversationListCursor();
918 mListAdapter.swapCursor(newCursor);
919 // When the conversation cursor is *updated*, we get back the same instance. In that
920 // situation, CursorAdapter.swapCursor() silently returns, without forcing a
921 // notifyDataSetChanged(). So let's force a call to notifyDataSetChanged, since an updated
922 // cursor means that the dataset has changed.
923 final int newCursorHash = (newCursor == null) ? 0 : newCursor.hashCode();
924 if (mConversationCursorHash == newCursorHash && mConversationCursorHash != 0) {
925 mListAdapter.notifyDataSetChanged();
926 }
927 mConversationCursorHash = newCursorHash;
Scott Kennedyb1e21482013-03-15 13:40:38 -0700928
Scott Kennedy4b9b3952013-03-20 17:20:59 -0700929 if (newCursor != null && newCursor.getCount() > 0) {
Scott Kennedyb1e21482013-03-15 13:40:38 -0700930 newCursor.markContentsSeen();
931 }
932
Vikram Aggarwal17f373e2012-09-17 15:49:32 -0700933 // If a current conversation is available, and none is selected in the list, then ask
934 // the list to select the current conversation.
935 final Conversation conv = mCallbacks.getCurrentConversation();
936 if (conv == null) {
937 return;
938 }
Vikram Aggarwala91d00b2013-01-18 12:00:37 -0800939 if (mListView.getChoiceMode() != ListView.CHOICE_MODE_NONE
940 && mListView.getCheckedItemPosition() == -1) {
941 setSelected(conv.position, true);
Mindy Pereira967ede62012-03-22 09:29:09 -0700942 }
Mindy Pereira21ab4902012-03-19 18:48:03 -0700943 }
Mindy Pereira1e2573b2012-04-17 14:34:36 -0700944
mindypc6adce32012-08-22 18:46:42 -0700945 public void commitDestructiveActions(boolean animate) {
Mindy Pereira8937bf12012-07-23 14:05:02 -0700946 if (mListView != null) {
mindypc6adce32012-08-22 18:46:42 -0700947 mListView.commitDestructiveActions(animate);
Vikram Aggarwal7d816002012-04-17 17:06:41 -0700948
949 }
950 }
951
mindyp9365a822012-09-12 09:09:09 -0700952 @Override
953 public void onListItemSwiped(Collection<Conversation> conversations) {
954 mUpdater.showNextConversation(conversations);
955 }
Alice Yang0d74a662013-03-25 14:01:24 -0700956
Alice Yang486e63e2013-04-05 13:01:50 -0700957 private void checkSyncStatus() {
Alice Yang76d20652013-04-24 02:32:48 -0700958 if (mFolder != null && mFolder.isSyncInProgress()) {
Alice Yang03752f32013-05-05 15:05:16 -0700959 LogUtils.d(LOG_TAG, "CLF.checkSyncStatus still syncing");
Alice Yang486e63e2013-04-05 13:01:50 -0700960 // Still syncing, ignore
961 } else {
962 // Finished syncing:
Alice Yang03752f32013-05-05 15:05:16 -0700963 LogUtils.d(LOG_TAG, "CLF.checkSyncStatus done syncing");
Alice Yang486e63e2013-04-05 13:01:50 -0700964 mConversationListView.onSyncFinished();
965 }
966 }
967
Alice Yang0d74a662013-03-25 14:01:24 -0700968 /**
Alice Yang486e63e2013-04-05 13:01:50 -0700969 * Displays the indefinite progress bar indicating a sync is in progress. This
970 * should only be called if user manually requested a sync, and not for background syncs.
Alice Yang0d74a662013-03-25 14:01:24 -0700971 */
972 protected void showSyncStatusBar() {
Alice Yang486e63e2013-04-05 13:01:50 -0700973 mConversationListView.showSyncStatusBar();
Alice Yang0d74a662013-03-25 14:01:24 -0700974 }
Rohan Shahd4f22872013-04-19 17:06:37 -0700975
976 /**
977 * Clears all items in the list.
978 */
979 public void clear() {
980 mListView.setAdapter(null);
981 }
Scott Kennedy1fea6a32013-07-09 15:58:51 -0700982
983 private final ConversationSetObserver mConversationSetObserver = new ConversationSetObserver() {
984 @Override
985 public void onSetPopulated(final ConversationSelectionSet set) {
Scott Kennedy8afccad2013-07-28 19:09:11 -0700986 // Do nothing
Scott Kennedy1fea6a32013-07-09 15:58:51 -0700987 }
988
989 @Override
990 public void onSetEmpty() {
991 mSelectionModeExitedTimestamp = System.currentTimeMillis();
Scott Kennedy1fea6a32013-07-09 15:58:51 -0700992 }
993
994 @Override
995 public void onSetChanged(final ConversationSelectionSet set) {
Scott Kennedy8afccad2013-07-28 19:09:11 -0700996 // Do nothing
Scott Kennedy1fea6a32013-07-09 15:58:51 -0700997 }
998 };
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -0800999}