blob: b11cadd08259040b8f2dd0316ae773fe984620cb [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
Mark Wei2102b2c2013-05-02 17:15:30 -070020import com.google.common.collect.ImmutableList;
21
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -080022import android.app.Activity;
23import android.app.ListFragment;
Vikram Aggarwal37a20ca2013-06-06 11:19:49 -070024import android.app.LoaderManager;
Alice Yang20323162013-04-17 10:37:41 -070025import android.content.Context;
Vikram Aggarwal37a20ca2013-06-06 11:19:49 -070026import android.content.Loader;
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -080027import android.content.res.Resources;
Mindy Pereira6d8e7fe2012-07-26 16:02:49 -070028import android.database.DataSetObserver;
Vikram Aggarwal37a20ca2013-06-06 11:19:49 -070029import android.net.Uri;
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -080030import android.os.Bundle;
31import android.os.Handler;
Alice Yang0d74a662013-03-25 14:01:24 -070032import android.text.format.DateUtils;
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -080033import android.view.LayoutInflater;
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -080034import android.view.View;
35import android.view.ViewGroup;
36import android.view.ViewGroup.MarginLayoutParams;
37import android.widget.AdapterView;
38import android.widget.AdapterView.OnItemLongClickListener;
39import android.widget.ListView;
40import android.widget.TextView;
41
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -080042import com.android.mail.ConversationListContext;
Marc Blankf3626952012-02-28 17:06:05 -080043import com.android.mail.R;
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 Aggarwal37a20ca2013-06-06 11:19:49 -070049import com.android.mail.content.ObjectCursor;
50import com.android.mail.content.ObjectCursorLoader;
Vikram Aggarwal80aeac52012-02-07 15:27:20 -080051import com.android.mail.providers.Account;
Vikram Aggarwal7c401b72012-08-13 16:43:47 -070052import com.android.mail.providers.AccountObserver;
Vikram Aggarwald247dc92012-02-10 15:49:01 -080053import com.android.mail.providers.Conversation;
Mindy Pereira4f166de2012-02-14 13:40:58 -080054import com.android.mail.providers.Folder;
Vikram Aggarwal50ff0e52013-03-14 13:58:02 -070055import com.android.mail.providers.FolderObserver;
Vikram Aggarwal7d816002012-04-17 17:06:41 -070056import com.android.mail.providers.Settings;
Vikram Aggarwal80aeac52012-02-07 15:27:20 -080057import com.android.mail.providers.UIProvider;
Mindy Pereira4e812f42012-07-30 16:52:49 -070058import com.android.mail.providers.UIProvider.AccountCapabilities;
Alice Yang64273142013-04-10 18:26:56 -070059import com.android.mail.providers.UIProvider.ConversationListIcon;
Mindy Pereira4e812f42012-07-30 16:52:49 -070060import com.android.mail.providers.UIProvider.FolderCapabilities;
mindyp77c3e1b2012-09-11 14:05:02 -070061import com.android.mail.providers.UIProvider.FolderType;
Mindy Pereirae58222b2012-07-25 14:33:18 -070062import com.android.mail.providers.UIProvider.Swipe;
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;
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -080069
Mindy Pereiraf6a6b502012-03-15 15:24:26 -070070import java.util.Collection;
Scott Kennedy7c8325d2013-02-28 10:46:10 -080071import java.util.List;
Mindy Pereirafe06bea2012-02-16 08:15:14 -080072
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -080073/**
74 * The conversation list UI component.
75 */
Vikram Aggarwald247dc92012-02-10 15:49:01 -080076public final class ConversationListFragment extends ListFragment implements
mindyp9365a822012-09-12 09:09:09 -070077 OnItemLongClickListener, ModeChangeListener, ListItemSwipedListener {
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
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -080090 /**
mindyp9365a822012-09-12 09:09:09 -070091 * Frequency of update of timestamps. Initialized in
92 * {@link #onCreate(Bundle)} and final afterwards.
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -080093 */
94 private static int TIMESTAMP_UPDATE_INTERVAL = 0;
95
Alice Yang0d74a662013-03-25 14:01:24 -070096 private static long NO_NEW_MESSAGE_DURATION = 1 * DateUtils.SECOND_IN_MILLIS;
97
Vikram Aggarwal80aeac52012-02-07 15:27:20 -080098 private ControllableActivity mActivity;
Vikram Aggarwal80aeac52012-02-07 15:27:20 -080099
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -0800100 // Control state.
101 private ConversationListCallbacks mCallbacks;
Vikram Aggarwal80aeac52012-02-07 15:27:20 -0800102
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -0800103 private final Handler mHandler = new Handler();
Vikram Aggarwal80aeac52012-02-07 15:27:20 -0800104
Alice Yang486e63e2013-04-05 13:01:50 -0700105 private ConversationListView mConversationListView;
106
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -0800107 // The internal view objects.
Mindy Pereiraf6a6b502012-03-15 15:24:26 -0700108 private SwipeableListView mListView;
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -0800109
110 private TextView mSearchResultCountTextView;
111 private TextView mSearchStatusTextView;
112
113 private View mSearchStatusView;
Vikram Aggarwal80aeac52012-02-07 15:27:20 -0800114
Vikram Aggarwal80aeac52012-02-07 15:27:20 -0800115 /**
116 * Current Account being viewed
117 */
Vikram Aggarwald247dc92012-02-10 15:49:01 -0800118 private Account mAccount;
Vikram Aggarwal80aeac52012-02-07 15:27:20 -0800119 /**
Mindy Pereira30fd47b2012-03-09 09:24:00 -0800120 * Current folder being viewed.
Vikram Aggarwal80aeac52012-02-07 15:27:20 -0800121 */
Mindy Pereira4f166de2012-02-14 13:40:58 -0800122 private Folder mFolder;
Vikram Aggarwal80aeac52012-02-07 15:27:20 -0800123
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -0800124 /**
125 * A simple method to update the timestamps of conversations periodically.
126 */
127 private Runnable mUpdateTimestampsRunnable = null;
128
129 private ConversationListContext mViewContext;
130
Vikram Aggarwal80aeac52012-02-07 15:27:20 -0800131 private AnimatedAdapter mListAdapter;
Vikram Aggarwal7d602882012-02-07 15:01:20 -0800132
Mindy Pereira6681f6b2012-03-09 13:55:54 -0800133 private ConversationListFooterView mFooterView;
Mindy Pereira6cc95532012-08-15 16:04:56 -0700134 private View mEmptyView;
Andrew Sappersteinc2c9dc12012-07-02 18:17:32 -0700135 private ErrorListener mErrorListener;
Vikram Aggarwal50ff0e52013-03-14 13:58:02 -0700136 private FolderObserver mFolderObserver;
Vikram Aggarwala91d00b2013-01-18 12:00:37 -0800137 private DataSetObserver mConversationCursorObserver;
Vikram Aggarwald247dc92012-02-10 15:49:01 -0800138
Vikram Aggarwal4f1cc0b2012-08-02 15:16:30 -0700139 private ConversationSelectionSet mSelectedSet;
Vikram Aggarwal7c401b72012-08-13 16:43:47 -0700140 private final AccountObserver mAccountObserver = new AccountObserver() {
141 @Override
142 public void onChanged(Account newAccount) {
143 mAccount = newAccount;
144 setSwipeAction();
145 }
146 };
mindyp9365a822012-09-12 09:09:09 -0700147 private ConversationUpdater mUpdater;
Vikram Aggarwal81a4f082012-09-28 09:19:04 -0700148 /** Hash of the Conversation Cursor we last obtained from the controller. */
149 private int mConversationCursorHash;
Vikram Aggarwal4f1cc0b2012-08-02 15:16:30 -0700150
Vikram Aggarwal6c511582012-02-27 10:59:47 -0800151 /**
Vikram Aggarwal37a20ca2013-06-06 11:19:49 -0700152 * If the current list is for a folder with children, this set of loader callbacks will
153 * create a loader for all the child folders, and will return an {@link ObjectCursor} over the
154 * list.
155 */
156 private final class ChildFolderLoads
157 implements LoaderManager.LoaderCallbacks<ObjectCursor<Folder>> {
158 /** Load all child folders for the current folder. */
159 private static final int LOADER_CHIDREN = 0;
160 public static final String CHILD_URI = "arg-child-uri";
161 private final String[] projection = UIProvider.FOLDERS_PROJECTION;
162
163 @Override
164 public Loader<ObjectCursor<Folder>> onCreateLoader(int id, Bundle args) {
165 if (id != LOADER_CHIDREN) {
166 throw new IllegalStateException("ChildFolderLoads loading ID=" + id);
167 }
168 final Uri childUri = Uri.parse(args.getString(CHILD_URI));
169 return new ObjectCursorLoader<Folder>(
170 getActivity(), childUri, projection, Folder.FACTORY);
171 }
172
173 @Override
174 public void onLoadFinished(Loader<ObjectCursor<Folder>> loader, ObjectCursor<Folder> data) {
175 if (data != null && data.getCount() >= 0 && mListAdapter != null) {
176 mListAdapter.updateNestedFolders(data);
177 }
178 }
179
180 @Override
181 public void onLoaderReset(Loader<ObjectCursor<Folder>> loader) {
182 // Do nothing.
183 }
184 }
185
186 /** Callbacks to handle creating a loader and receiving child folders from it. */
187 private final ChildFolderLoads mChildCallback = new ChildFolderLoads();
188
189 /**
190 * Include all the folders at the cursor provided here in the conversation list.
191 * @param cursor The cursor containing child folders for the current folder.
192 */
193 private void showChildFolders(ObjectCursor<Folder> cursor) {
194
195 }
196
197 /**
mindyp9365a822012-09-12 09:09:09 -0700198 * Constructor needs to be public to handle orientation changes and activity
199 * lifecycle events.
Vikram Aggarwal6c511582012-02-27 10:59:47 -0800200 */
Paul Westbrookefc9f122012-02-21 11:14:49 -0800201 public ConversationListFragment() {
Vikram Aggarwald247dc92012-02-10 15:49:01 -0800202 super();
Vikram Aggarwald247dc92012-02-10 15:49:01 -0800203 }
204
Vikram Aggarwala91d00b2013-01-18 12:00:37 -0800205 private class ConversationCursorObserver extends DataSetObserver {
Mindy Pereira70a70c92012-08-02 08:39:45 -0700206 @Override
207 public void onChanged() {
Mindy Pereira70a70c92012-08-02 08:39:45 -0700208 onConversationListStatusUpdated();
209 }
210 }
211
Vikram Aggarwal7d602882012-02-07 15:01:20 -0800212 /**
mindyp9365a822012-09-12 09:09:09 -0700213 * Creates a new instance of {@link ConversationListFragment}, initialized
214 * to display conversation list context.
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -0800215 */
216 public static ConversationListFragment newInstance(ConversationListContext viewContext) {
217 ConversationListFragment fragment = new ConversationListFragment();
218 Bundle args = new Bundle();
219 args.putBundle(CONVERSATION_LIST_KEY, viewContext.toBundle());
220 fragment.setArguments(args);
221 return fragment;
222 }
223
224 /**
mindyp9365a822012-09-12 09:09:09 -0700225 * Show the header if the current conversation list is showing search
226 * results.
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -0800227 */
Mindy Pereira967ede62012-03-22 09:29:09 -0700228 void configureSearchResultHeader() {
Mindy Pereira755fd6e2012-03-21 15:15:44 -0700229 if (mActivity == null) {
230 return;
231 }
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -0800232 // Only show the header if the context is for a search result
233 final Resources res = getResources();
Vikram Aggarwalae4ea992012-08-07 09:56:02 -0700234 final boolean showHeader = ConversationListContext.isSearchResult(mViewContext);
mindyp9365a822012-09-12 09:09:09 -0700235 // TODO(viki): This code contains intimate understanding of the view.
236 // Much of this logic
237 // needs to reside in a separate class that handles the text view in
238 // isolation. Then,
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -0800239 // that logic can be reused in other fragments.
240 if (showHeader) {
241 mSearchStatusTextView.setText(res.getString(R.string.search_results_searching_header));
242 // Initially reset the count
243 mSearchResultCountTextView.setText("");
244 }
245 mSearchStatusView.setVisibility(showHeader ? View.VISIBLE : View.GONE);
246 int marginTop = showHeader ? (int) res.getDimension(R.dimen.notification_view_height) : 0;
247 MarginLayoutParams layoutParams = (MarginLayoutParams) mListView.getLayoutParams();
248 layoutParams.topMargin = marginTop;
249 mListView.setLayoutParams(layoutParams);
250 }
251
Vikram Aggarwal80aeac52012-02-07 15:27:20 -0800252 /**
mindyp9365a822012-09-12 09:09:09 -0700253 * Show the header if the current conversation list is showing search
254 * results.
Mindy Pereira68f2e222012-03-07 10:36:54 -0800255 */
256 private void updateSearchResultHeader(int count) {
Mindy Pereira71a8f292012-07-26 16:30:48 -0700257 if (mActivity == null) {
258 return;
259 }
Mindy Pereira68f2e222012-03-07 10:36:54 -0800260 // Only show the header if the context is for a search result
261 final Resources res = getResources();
Vikram Aggarwalae4ea992012-08-07 09:56:02 -0700262 final boolean showHeader = ConversationListContext.isSearchResult(mViewContext);
Mindy Pereira68f2e222012-03-07 10:36:54 -0800263 if (showHeader) {
264 mSearchStatusTextView.setText(res.getString(R.string.search_results_header));
265 mSearchResultCountTextView
266 .setText(res.getString(R.string.search_results_loaded, count));
267 }
268 }
269
270 /**
Vikram Aggarwal80aeac52012-02-07 15:27:20 -0800271 * Initializes all internal state for a rendering.
272 */
273 private void initializeUiForFirstDisplay() {
mindyp9365a822012-09-12 09:09:09 -0700274 // TODO(mindyp): find some way to make the notification container more
275 // re-usable.
276 // TODO(viki): refactor according to comment in
277 // configureSearchResultHandler()
Vikram Aggarwal80aeac52012-02-07 15:27:20 -0800278 mSearchStatusView = mActivity.findViewById(R.id.search_status_view);
279 mSearchStatusTextView = (TextView) mActivity.findViewById(R.id.search_status_text_view);
mindyp9365a822012-09-12 09:09:09 -0700280 mSearchResultCountTextView = (TextView) mActivity
281 .findViewById(R.id.search_result_count_view);
Vikram Aggarwal80aeac52012-02-07 15:27:20 -0800282 }
283
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -0800284 @Override
Vikram Aggarwal0509bba2013-01-29 17:11:30 -0800285 public void onActivityCreated(Bundle savedState) {
286 super.onActivityCreated(savedState);
mindyp9365a822012-09-12 09:09:09 -0700287 // Strictly speaking, we get back an android.app.Activity from
288 // getActivity. However, the
289 // only activity creating a ConversationListContext is a MailActivity
290 // which is of type
291 // ControllableActivity, so this cast should be safe. If this cast
292 // fails, some other
293 // activity is creating ConversationListFragments. This activity must be
294 // of type
Vikram Aggarwal80aeac52012-02-07 15:27:20 -0800295 // ControllableActivity.
296 final Activity activity = getActivity();
mindyp9365a822012-09-12 09:09:09 -0700297 if (!(activity instanceof ControllableActivity)) {
298 LogUtils.e(LOG_TAG, "ConversationListFragment expects only a ControllableActivity to"
299 + "create it. Cannot proceed.");
Vikram Aggarwal80aeac52012-02-07 15:27:20 -0800300 }
301 mActivity = (ControllableActivity) activity;
mindyp9365a822012-09-12 09:09:09 -0700302 // Since we now have a controllable activity, load the account from it,
303 // and register for
Vikram Aggarwal7c401b72012-08-13 16:43:47 -0700304 // future account changes.
305 mAccount = mAccountObserver.initialize(mActivity.getAccountController());
Vikram Aggarwal7d602882012-02-07 15:01:20 -0800306 mCallbacks = mActivity.getListHandler();
Andrew Sappersteinc2c9dc12012-07-02 18:17:32 -0700307 mErrorListener = mActivity.getErrorListener();
Mindy Pereira278fd222012-07-26 15:23:10 -0700308 // Start off with the current state of the folder being viewed.
Alice Yang20323162013-04-17 10:37:41 -0700309 Context activityContext = mActivity.getActivityContext();
Mindy Pereira6681f6b2012-03-09 13:55:54 -0800310 mFooterView = (ConversationListFooterView) LayoutInflater.from(
Alice Yang20323162013-04-17 10:37:41 -0700311 activityContext).inflate(R.layout.conversation_list_footer_view,
Mindy Pereira6681f6b2012-03-09 13:55:54 -0800312 null);
Paul Westbrook4969e0c2012-08-20 14:38:39 -0700313 mFooterView.setClickListener(mActivity);
Alice Yangc36fa722013-05-05 01:23:46 -0700314 mConversationListView.setActivity(mActivity);
Vikram Aggarwal81a4f082012-09-28 09:19:04 -0700315 final ConversationCursor conversationCursor = getConversationListCursor();
Vikram Aggarwal37a20ca2013-06-06 11:19:49 -0700316 final LoaderManager manager = getLoaderManager();
317
318 // If this a parent folder, load all the child folders.
319 if (mViewContext.folder.hasChildren) {
320 final Uri childUri = mViewContext.folder.childFoldersListUri;
321 final Bundle args = new Bundle();
322 args.putString(ChildFolderLoads.CHILD_URI, childUri.toString());
323 manager.initLoader(ChildFolderLoads.LOADER_CHIDREN, args, mChildCallback);
324 }
Scott Kennedy7c8325d2013-02-28 10:46:10 -0800325
326 final ConversationListHelper helper = mActivity.getConversationListHelper();
Mark Wei2102b2c2013-05-02 17:15:30 -0700327 final List<ConversationSpecialItemView> specialItemViews = helper != null ?
328 ImmutableList.copyOf(helper.makeConversationListSpecialViews(
Vikram Aggarwal37a20ca2013-06-06 11:19:49 -0700329 activity, mAccount, mActivity.getFolderSelector()))
Mark Wei2102b2c2013-05-02 17:15:30 -0700330 : null;
Scott Kennedy7c8325d2013-02-28 10:46:10 -0800331 if (specialItemViews != null) {
332 // Attach to the LoaderManager
333 for (final ConversationSpecialItemView view : specialItemViews) {
Vikram Aggarwal37a20ca2013-06-06 11:19:49 -0700334 view.bindLoaderManager(manager);
Scott Kennedy7c8325d2013-02-28 10:46:10 -0800335 }
336 }
337
338 mListAdapter = new AnimatedAdapter(mActivity.getApplicationContext(), conversationCursor,
Vikram Aggarwal37a20ca2013-06-06 11:19:49 -0700339 mActivity.getSelectedSet(), mActivity, mListView, specialItemViews, null);
Mindy Pereira6681f6b2012-03-09 13:55:54 -0800340 mListAdapter.addFooter(mFooterView);
Mindy Pereira6f4a6af2012-02-29 11:48:52 -0800341 mListView.setAdapter(mListAdapter);
Vikram Aggarwal4f1cc0b2012-08-02 15:16:30 -0700342 mSelectedSet = mActivity.getSelectedSet();
343 mListView.setSelectionSet(mSelectedSet);
Vikram Aggarwal37a20ca2013-06-06 11:19:49 -0700344 mListAdapter.setFooterVisibility(false);
Vikram Aggarwal50ff0e52013-03-14 13:58:02 -0700345 mFolderObserver = new FolderObserver(){
346 @Override
347 public void onChanged(Folder newFolder) {
348 onFolderUpdated(newFolder);
349 }
350 };
351 mFolderObserver.initialize(mActivity.getFolderController());
Vikram Aggarwala91d00b2013-01-18 12:00:37 -0800352 mConversationCursorObserver = new ConversationCursorObserver();
mindyp9365a822012-09-12 09:09:09 -0700353 mUpdater = mActivity.getConversationUpdater();
Vikram Aggarwala91d00b2013-01-18 12:00:37 -0800354 mUpdater.registerConversationListObserver(mConversationCursorObserver);
Vikram Aggarwalbcb16b92013-01-28 18:05:03 -0800355 mTabletDevice = Utils.useTabletUI(mActivity.getApplicationContext().getResources());
Vikram Aggarwal80aeac52012-02-07 15:27:20 -0800356 initializeUiForFirstDisplay();
Mindy Pereiradc8963b2012-03-28 17:51:05 -0700357 configureSearchResultHeader();
mindyp9365a822012-09-12 09:09:09 -0700358 // The onViewModeChanged callback doesn't get called when the mode
359 // object is created, so
Vikram Aggarwalfa131a22012-02-02 13:56:22 -0800360 // force setting the mode manually this time around.
Mindy Pereiraf96ec322012-03-02 14:06:33 -0800361 onViewModeChanged(mActivity.getViewMode().getMode());
Mindy Pereirab5901be2012-08-09 17:21:53 -0700362 mActivity.getViewMode().addListener(this);
Paul Westbrook0e3fd9d2012-04-20 02:02:23 -0700363
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -0800364 if (mActivity.isFinishing()) {
365 // Activity is finishing, just bail.
366 return;
367 }
Vikram Aggarwal81a4f082012-09-28 09:19:04 -0700368 mConversationCursorHash = (conversationCursor == null) ? 0 : conversationCursor.hashCode();
369 // Belt and suspenders here; make sure we do any necessary sync of the
370 // ConversationCursor
371 if (conversationCursor != null && conversationCursor.isRefreshReady()) {
372 conversationCursor.sync();
373 }
374
Vikram Aggarwal0509bba2013-01-29 17:11:30 -0800375 // On a phone we never highlight a conversation, so the default is to select none.
376 // On a tablet, we highlight a SINGLE conversation in landscape conversation view.
377 int choice = getDefaultChoiceMode(mTabletDevice);
378 if (savedState != null) {
379 // Restore the choice mode if it was set earlier, or NONE if creating a fresh view.
380 // Choice mode here represents the current conversation only. CAB mode does not rely on
381 // the platform: checked state is a local variable {@link ConversationItemView#mChecked}
382 choice = savedState.getInt(CHOICE_MODE_KEY, choice);
383 if (savedState.containsKey(LIST_STATE_KEY)) {
384 // TODO: find a better way to unset the selected item when restoring
385 mListView.clearChoices();
386 }
387 }
388 setChoiceMode(choice);
389
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -0800390 // Show list and start loading list.
391 showList();
Mindy Pereira4765c5c2012-07-19 11:58:22 -0700392 ToastBarOperation pendingOp = mActivity.getPendingToastOperation();
393 if (pendingOp != null) {
394 // Clear the pending operation
395 mActivity.setPendingToastOperation(null);
396 mActivity.onUndoAvailable(pendingOp);
397 }
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -0800398 }
399
Vikram Aggarwal0509bba2013-01-29 17:11:30 -0800400 /**
401 * Returns the default choice mode for the list based on whether the list is displayed on tablet
402 * or not.
403 * @param isTablet
404 * @return
405 */
406 private final static int getDefaultChoiceMode(boolean isTablet) {
407 return isTablet ? ListView.CHOICE_MODE_SINGLE : ListView.CHOICE_MODE_NONE;
408 }
409
Mindy Pereira967ede62012-03-22 09:29:09 -0700410 public AnimatedAdapter getAnimatedAdapter() {
411 return mListAdapter;
412 }
413
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -0800414 @Override
Vikram Aggarwald247dc92012-02-10 15:49:01 -0800415 public void onCreate(Bundle savedState) {
Vikram Aggarwald247dc92012-02-10 15:49:01 -0800416 super.onCreate(savedState);
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -0800417
418 // Initialize fragment constants from resources
Vikram Aggarwal6c511582012-02-27 10:59:47 -0800419 final Resources res = getResources();
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -0800420 TIMESTAMP_UPDATE_INTERVAL = res.getInteger(R.integer.timestamp_update_interval);
mindyp9365a822012-09-12 09:09:09 -0700421 mUpdateTimestampsRunnable = new Runnable() {
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -0800422 @Override
423 public void run() {
424 mListView.invalidateViews();
425 mHandler.postDelayed(mUpdateTimestampsRunnable, TIMESTAMP_UPDATE_INTERVAL);
426 }
427 };
428
Vikram Aggarwal80aeac52012-02-07 15:27:20 -0800429 // Get the context from the arguments
Vikram Aggarwal6c511582012-02-27 10:59:47 -0800430 final Bundle args = getArguments();
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -0800431 mViewContext = ConversationListContext.forBundle(args.getBundle(CONVERSATION_LIST_KEY));
Mindy Pereira3982e232012-02-29 15:00:34 -0800432 mAccount = mViewContext.account;
Paul Westbrook80ecd892012-08-16 13:37:39 -0700433
Vikram Aggarwal9a49c9b2012-08-31 10:49:33 -0700434 setRetainInstance(false);
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -0800435 }
436
437 @Override
Andy Huang9e4ca792013-02-28 14:33:43 -0800438 public String toString() {
439 final String s = super.toString();
440 if (mViewContext == null) {
441 return s;
442 }
Andy Huangc1922a92013-05-13 14:33:05 -0700443 final StringBuilder sb = new StringBuilder(s);
444 sb.setLength(sb.length() - 1);
445 sb.append(" mListAdapter=");
446 sb.append(mListAdapter);
447 sb.append(" folder=");
448 sb.append(mViewContext.folder);
449 sb.append("}");
450 return sb.toString();
Andy Huang9e4ca792013-02-28 14:33:43 -0800451 }
452
453 @Override
Vikram Aggarwal9a49c9b2012-08-31 10:49:33 -0700454 public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedState) {
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -0800455 View rootView = inflater.inflate(R.layout.conversation_list, null);
Mindy Pereira6cc95532012-08-15 16:04:56 -0700456 mEmptyView = rootView.findViewById(R.id.empty_view);
Alice Yang486e63e2013-04-05 13:01:50 -0700457 mConversationListView =
458 (ConversationListView) rootView.findViewById(R.id.conversation_list);
Alice Yang20323162013-04-17 10:37:41 -0700459 mConversationListView.setConversationContext(mViewContext);
Mindy Pereiraf6a6b502012-03-15 15:24:26 -0700460 mListView = (SwipeableListView) rootView.findViewById(android.R.id.list);
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -0800461 mListView.setHeaderDividersEnabled(false);
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -0800462 mListView.setOnItemLongClickListener(this);
Mindy Pereira4e812f42012-07-30 16:52:49 -0700463 mListView.enableSwipe(mAccount.supportsCapability(AccountCapabilities.UNDO));
mindyp9365a822012-09-12 09:09:09 -0700464 mListView.setSwipedListener(this);
Marc Blank2af0cf32012-03-01 19:24:13 -0800465
Vikram Aggarwal0509bba2013-01-29 17:11:30 -0800466 if (savedState != null && savedState.containsKey(LIST_STATE_KEY)) {
467 mListView.onRestoreInstanceState(savedState.getParcelable(LIST_STATE_KEY));
Vikram Aggarwal9a49c9b2012-08-31 10:49:33 -0700468 }
Alice Yang0d74a662013-03-25 14:01:24 -0700469
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -0800470 return rootView;
471 }
472
Vikram Aggarwala91d00b2013-01-18 12:00:37 -0800473 /**
474 * Sets the choice mode of the list view
475 * @param choiceMode ListView#
476 */
477 private final void setChoiceMode(int choiceMode) {
478 mListView.setChoiceMode(choiceMode);
479 }
480
481 /**
482 * Tell the list to select nothing.
483 */
484 public final void setChoiceNone() {
Vikram Aggarwal0509bba2013-01-29 17:11:30 -0800485 // On a phone, the default choice mode is already none, so nothing to do.
486 if (!mTabletDevice) {
487 return;
488 }
Andy Huangb0be3fc2013-05-02 15:48:05 -0700489 clearChoicesAndActivated();
Vikram Aggarwala91d00b2013-01-18 12:00:37 -0800490 setChoiceMode(ListView.CHOICE_MODE_NONE);
491 }
492
493 /**
494 * Tell the list to get out of selecting none.
495 */
496 public final void revertChoiceMode() {
Vikram Aggarwal0509bba2013-01-29 17:11:30 -0800497 // On a phone, the default choice mode is always none, so nothing to do.
498 if (!mTabletDevice) {
499 return;
500 }
501 setChoiceMode(getDefaultChoiceMode(mTabletDevice));
Vikram Aggarwala91d00b2013-01-18 12:00:37 -0800502 }
503
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -0800504 @Override
Andy Huang7517e3b2012-08-20 12:30:08 -0700505 public void onDestroy() {
Andy Huang7517e3b2012-08-20 12:30:08 -0700506 super.onDestroy();
507 }
508
509 @Override
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -0800510 public void onDestroyView() {
Andy Huang7517e3b2012-08-20 12:30:08 -0700511
Vikram Aggarwal7c401b72012-08-13 16:43:47 -0700512 // Clear the list's adapter
513 mListAdapter.destroy();
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -0800514 mListView.setAdapter(null);
515
Vikram Aggarwald7a12cd2012-02-03 09:36:20 -0800516 mActivity.unsetViewModeListener(this);
Mindy Pereira71a8f292012-07-26 16:30:48 -0700517 if (mFolderObserver != null) {
Vikram Aggarwal50ff0e52013-03-14 13:58:02 -0700518 mFolderObserver.unregisterAndDestroy();
Mindy Pereira71a8f292012-07-26 16:30:48 -0700519 mFolderObserver = null;
520 }
Vikram Aggarwala91d00b2013-01-18 12:00:37 -0800521 if (mConversationCursorObserver != null) {
522 mUpdater.unregisterConversationListObserver(mConversationCursorObserver);
523 mConversationCursorObserver = null;
Mindy Pereira70a70c92012-08-02 08:39:45 -0700524 }
Vikram Aggarwal7c401b72012-08-13 16:43:47 -0700525 mAccountObserver.unregisterAndDestroy();
Scott Kennedy7c8325d2013-02-28 10:46:10 -0800526 getAnimatedAdapter().cleanup();
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -0800527 super.onDestroyView();
528 }
529
Vikram Aggarwal9730ea02012-08-02 12:46:19 -0700530 /**
mindyp9365a822012-09-12 09:09:09 -0700531 * There are three binary variables, which determine what we do with a
532 * message. checkbEnabled: Whether check boxes are enabled or not (forced
533 * true on tablet) cabModeOn: Whether CAB mode is currently on or not.
534 * pressType: long or short tap (There is a third possibility: phone or
535 * tablet, but they have <em>identical</em> behavior) The matrix of
536 * possibilities is:
537 * <p>
538 * Long tap: Always toggle selection of conversation. If CAB mode is not
539 * started, then start it.
Vikram Aggarwal9730ea02012-08-02 12:46:19 -0700540 * <pre>
541 * | Checkboxes | No Checkboxes
542 * ----------+------------+---------------
543 * CAB mode | Select | Select
544 * List mode | Select | Select
545 *
Vikram Aggarwal9730ea02012-08-02 12:46:19 -0700546 * </pre>
mindyp9365a822012-09-12 09:09:09 -0700547 *
Vikram Aggarwal9730ea02012-08-02 12:46:19 -0700548 * Reference: http://b/issue?id=6392199
549 * <p>
550 * {@inheritDoc}
551 */
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -0800552 @Override
553 public boolean onItemLongClick(AdapterView<?> parent, View view, int position, long id) {
Mindy Pereira1e231a62012-03-23 12:42:15 -0700554 // Ignore anything that is not a conversation item. Could be a footer.
555 if (!(view instanceof ConversationItemView)) {
Scott Kennedy7c8325d2013-02-28 10:46:10 -0800556 return false;
Mindy Pereira1e231a62012-03-23 12:42:15 -0700557 }
Alice Yang4758e982013-04-24 20:31:34 -0700558 ((ConversationItemView) view).toggleSelectedStateOrBeginDrag();
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -0800559 return true;
560 }
561
Vikram Aggarwal4f1cc0b2012-08-02 15:16:30 -0700562 /**
mindyp9365a822012-09-12 09:09:09 -0700563 * See the comment for
564 * {@link #onItemLongClick(AdapterView, View, int, long)}.
565 * <p>
566 * Short tap behavior:
567 *
Vikram Aggarwal4f1cc0b2012-08-02 15:16:30 -0700568 * <pre>
569 * | Checkboxes | No Checkboxes
570 * ----------+------------+---------------
571 * CAB mode | Peek | Select
572 * List mode | Peek | Peek
573 * </pre>
mindyp9365a822012-09-12 09:09:09 -0700574 *
Vikram Aggarwal4f1cc0b2012-08-02 15:16:30 -0700575 * Reference: http://b/issue?id=6392199
576 * <p>
577 * {@inheritDoc}
578 */
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -0800579 @Override
Vikram Aggarwal54452ae2012-03-13 15:29:00 -0700580 public void onListItemClick(ListView l, View view, int position, long id) {
Vikram Aggarwal37a20ca2013-06-06 11:19:49 -0700581 if (view instanceof NestedFolderView) {
582 final FolderSelector selector = mActivity.getFolderSelector();
583 selector.onFolderSelected(((NestedFolderView) view).getFolder());
584 } else if (view instanceof ToggleableItem) {
585 final boolean showSenderImage =
586 (mAccount.settings.convListIcon == ConversationListIcon.SENDER_IMAGE);
587 if (!showSenderImage && !mSelectedSet.isEmpty()) {
588 ((ToggleableItem) view).toggleSelectedState();
589 } else {
590 viewConversation(position);
591 }
Vikram Aggarwal4f1cc0b2012-08-02 15:16:30 -0700592 } else {
Vikram Aggarwal37a20ca2013-06-06 11:19:49 -0700593 // Ignore anything that is not a conversation item. Could be a footer.
594 // If we are using a keyboard, the highlighted item is the parent;
595 // otherwise, this is a direct call from the ConverationItemView
596 return;
Vikram Aggarwal4f1cc0b2012-08-02 15:16:30 -0700597 }
mindyp9365a822012-09-12 09:09:09 -0700598 // When a new list item is clicked, commit any existing leave behind
mindyp8694fe92012-09-25 11:07:16 -0700599 // items. Wait until we have opened the desired conversation to cause
600 // any position changes.
Vikram Aggarwalbcb16b92013-01-28 18:05:03 -0800601 commitDestructiveActions(Utils.useTabletUI(mActivity.getActivityContext().getResources()));
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -0800602 }
603
604 @Override
Scott Kennedy08a079c2013-02-22 10:13:10 -0800605 public void onResume() {
606 super.onResume();
607
608 final ConversationCursor conversationCursor = getConversationListCursor();
609 if (conversationCursor != null) {
610 conversationCursor.handleNotificationActions();
611 }
612 }
613
614 @Override
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -0800615 public void onPause() {
616 super.onPause();
617 }
618
619 @Override
620 public void onSaveInstanceState(Bundle outState) {
621 super.onSaveInstanceState(outState);
622 if (mListView != null) {
623 outState.putParcelable(LIST_STATE_KEY, mListView.onSaveInstanceState());
Vikram Aggarwala91d00b2013-01-18 12:00:37 -0800624 outState.putInt(CHOICE_MODE_KEY, mListView.getChoiceMode());
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -0800625 }
626 }
627
628 @Override
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -0800629 public void onStart() {
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -0800630 super.onStart();
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -0800631 mHandler.postDelayed(mUpdateTimestampsRunnable, TIMESTAMP_UPDATE_INTERVAL);
632 }
633
634 @Override
635 public void onStop() {
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -0800636 super.onStop();
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -0800637 mHandler.removeCallbacks(mUpdateTimestampsRunnable);
638 }
639
Vikram Aggarwal80aeac52012-02-07 15:27:20 -0800640 @Override
Vikram Aggarwalfa131a22012-02-02 13:56:22 -0800641 public void onViewModeChanged(int newMode) {
Vikram Aggarwalfa131a22012-02-02 13:56:22 -0800642 if (mTabletDevice) {
Andy Huangb0be3fc2013-05-02 15:48:05 -0700643 if (ViewMode.isListMode(newMode)) {
Vikram Aggarwal0509bba2013-01-29 17:11:30 -0800644 // There are no selected conversations when in conversation list mode.
Andy Huangb0be3fc2013-05-02 15:48:05 -0700645 clearChoicesAndActivated();
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -0800646 }
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -0800647 }
Mindy Pereirab5901be2012-08-09 17:21:53 -0700648 if (mFooterView != null) {
649 mFooterView.onViewModeChanged(newMode);
650 }
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -0800651 }
Mindy Pereirab5901be2012-08-09 17:21:53 -0700652
Andy Huangb0be3fc2013-05-02 15:48:05 -0700653 private void clearChoicesAndActivated() {
654 final int currentSelected = mListView.getCheckedItemPosition();
655 mListView.clearChoices();
656 // We use the activated state to show the blue highlight on tablet. Clearing the choices
657 // removes the checked state, but doesn't do anything to the activated state. We must
658 // manually clear that.
659 if (currentSelected != ListView.INVALID_POSITION) {
660 final View v = mListView.getChildAt(currentSelected);
661 if (v != null) {
662 v.setActivated(false);
663 }
664 }
665 }
666
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -0800667 /**
mindyp9365a822012-09-12 09:09:09 -0700668 * Handles a request to show a new conversation list, either from a search
669 * query or for viewing a folder. This will initiate a data load, and hence
670 * must be called on the UI thread.
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -0800671 */
672 private void showList() {
Vikram Aggarwal80aeac52012-02-07 15:27:20 -0800673 mListView.setEmptyView(null);
Andy Huang7591d2f2012-07-26 16:48:06 -0700674 onFolderUpdated(mActivity.getFolderController().getFolder());
mindypca8ca2d2012-09-11 17:38:34 -0700675 onConversationListStatusUpdated();
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -0800676 }
677
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -0800678 /**
679 * View the message at the given position.
Scott Kennedy7c8325d2013-02-28 10:46:10 -0800680 *
681 * @param position The position of the conversation in the list (as opposed to its position in
682 * the cursor)
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -0800683 */
Scott Kennedy7c8325d2013-02-28 10:46:10 -0800684 protected void viewConversation(final int position) {
Vikram Aggarwalc7694222012-04-23 13:37:01 -0700685 LogUtils.d(LOG_TAG, "ConversationListFragment.viewConversation(%d)", position);
Scott Kennedy7c8325d2013-02-28 10:46:10 -0800686
687 final ConversationCursor cursor =
688 (ConversationCursor) getAnimatedAdapter().getItem(position);
Andy Huang97b10962013-04-02 13:33:32 -0700689
690 if (cursor == null) {
691 LogUtils.e(LOG_TAG,
692 "unable to open conv at cursor pos=%s cursor=%s getPositionOffset=%s",
693 position, cursor, getAnimatedAdapter().getPositionOffset(position));
694 return;
695 }
696
Scott Kennedy7c8325d2013-02-28 10:46:10 -0800697 final Conversation conv = cursor.getConversation();
698 /*
699 * The cursor position may be different than the position method parameter because of
700 * special views in the list.
701 */
702 conv.position = cursor.getPosition();
703 setSelected(conv.position, true);
704 mCallbacks.onConversationSelected(conv, false /* inLoaderCallbacks */);
Vikram Aggarwal3d7ca9d2012-05-11 14:40:36 -0700705 }
706
Vikram Aggarwala91d00b2013-01-18 12:00:37 -0800707 /**
708 * Sets the selected conversation to the position given here.
Alice Yang0d74a662013-03-25 14:01:24 -0700709 * @param cursorPosition The position of the conversation in the cursor (as opposed to
710 * in the list)
Vikram Aggarwala91d00b2013-01-18 12:00:37 -0800711 * @param different if the currently selected conversation is different from the one provided
712 * here. This is a difference in conversations, not a difference in positions. For example, a
713 * conversation at position 2 can move to position 4 as a result of new mail.
714 */
Scott Kennedy7c8325d2013-02-28 10:46:10 -0800715 public void setSelected(final int cursorPosition, boolean different) {
Vikram Aggarwala91d00b2013-01-18 12:00:37 -0800716 if (mListView.getChoiceMode() == ListView.CHOICE_MODE_NONE) {
717 return;
718 }
Scott Kennedy7c8325d2013-02-28 10:46:10 -0800719
720 final int position =
721 cursorPosition + getAnimatedAdapter().getPositionOffset(cursorPosition);
722
mindype21f8862012-10-01 09:32:03 -0700723 if (different) {
724 mListView.smoothScrollToPosition(position);
725 }
726 mListView.setItemChecked(position, true);
727 }
728
Vikram Aggarwal3d7ca9d2012-05-11 14:40:36 -0700729 /**
Vikram Aggarwala91d00b2013-01-18 12:00:37 -0800730 * Returns the cursor associated with the conversation list.
731 * @return
Vikram Aggarwal3d7ca9d2012-05-11 14:40:36 -0700732 */
Mindy Pereira967ede62012-03-22 09:29:09 -0700733 private ConversationCursor getConversationListCursor() {
734 return mCallbacks != null ? mCallbacks.getConversationListCursor() : null;
Mindy Pereirafe06bea2012-02-16 08:15:14 -0800735 }
736
737 /**
mindyp9365a822012-09-12 09:09:09 -0700738 * Request a refresh of the list. No sync is carried out and none is
739 * promised.
Vikram Aggarwal54452ae2012-03-13 15:29:00 -0700740 */
741 public void requestListRefresh() {
742 mListAdapter.notifyDataSetChanged();
743 }
744
Vikram Aggarwal75daee52012-04-30 11:13:09 -0700745 /**
Vikram Aggarwal7f602f72012-04-30 16:04:06 -0700746 * Change the UI to delete the conversations provided and then call the
mindyp9365a822012-09-12 09:09:09 -0700747 * {@link DestructiveAction} provided here <b>after</b> the UI has been
748 * updated.
Vikram Aggarwal7f602f72012-04-30 16:04:06 -0700749 * @param conversations
Vikram Aggarwal75daee52012-04-30 11:13:09 -0700750 * @param action
751 */
Vikram Aggarwala8e43182012-09-13 12:55:10 -0700752 public void requestDelete(int actionId, final Collection<Conversation> conversations,
Vikram Aggarwal669947b2013-01-10 17:05:56 -0800753 final DestructiveAction action) {
Mindy Pereiraacf60392012-04-06 09:11:00 -0700754 for (Conversation conv : conversations) {
755 conv.localDeleteOnUpdate = true;
756 }
Vikram Aggarwala8e43182012-09-13 12:55:10 -0700757 final ListItemsRemovedListener listener = new ListItemsRemovedListener() {
mindyp9365a822012-09-12 09:09:09 -0700758 @Override
759 public void onListItemsRemoved() {
760 action.performAction();
761 }
Vikram Aggarwala8e43182012-09-13 12:55:10 -0700762 };
763 final SwipeableListView listView = (SwipeableListView) getListView();
764 if (listView.getSwipeAction() == actionId) {
Paul Westbrookcec3e0b2013-02-04 22:50:22 -0800765 if (!listView.destroyItems(conversations, listener)) {
766 // The listView failed to destroy the items, perform the action manually
767 LogUtils.e(LOG_TAG, "ConversationListFragment.requestDelete: " +
768 "listView failed to destroy items.");
769 action.performAction();
770 }
Vikram Aggarwala8e43182012-09-13 12:55:10 -0700771 return;
772 }
773 // Delete the local delete items (all for now) and when done,
774 // update...
775 mListAdapter.delete(conversations, listener);
Mindy Pereiraacf60392012-04-06 09:11:00 -0700776 }
Mindy Pereiraa3911aa2012-03-09 13:26:26 -0800777
Mindy Pereira11dd5ef2012-03-10 15:10:18 -0800778 public void onFolderUpdated(Folder folder) {
Mindy Pereiradc8963b2012-03-28 17:51:05 -0700779 mFolder = folder;
Mindy Pereira06642fa2012-07-12 16:23:27 -0700780 setSwipeAction();
Mindy Pereira82cea482012-03-27 16:45:00 -0700781 if (mFolder == null) {
782 return;
783 }
Mindy Pereira4584a0d2012-03-13 14:42:14 -0700784 mListAdapter.setFolder(mFolder);
Mindy Pereira70a70c92012-08-02 08:39:45 -0700785 mFooterView.setFolder(mFolder);
Vikram Aggarwal41b9e8f2012-09-25 10:15:04 -0700786 if (!mFolder.wasSyncSuccessful()) {
Mindy Pereira70a70c92012-08-02 08:39:45 -0700787 mErrorListener.onError(mFolder, false);
788 }
Paul Westbrookc01f5d92012-09-25 17:22:25 -0700789
790 // Notify of changes to the Folder.
791 onFolderStatusUpdated();
792
Mindy Pereira12fe37a2012-08-15 10:02:57 -0700793 // Blow away conversation items cache.
794 ConversationItemViewModel.onFolderUpdated(mFolder);
Mindy Pereira70a70c92012-08-02 08:39:45 -0700795 }
796
Vikram Aggarwal81a4f082012-09-28 09:19:04 -0700797 /**
798 * Updates the footer visibility and updates the conversation cursor
799 */
Mindy Pereira70a70c92012-08-02 08:39:45 -0700800 public void onConversationListStatusUpdated() {
Paul Westbrook9a70e912012-08-17 15:53:20 -0700801 final ConversationCursor cursor = getConversationListCursor();
802 final boolean showFooter = cursor != null && mFooterView.updateStatus(cursor);
Paul Westbrookc01f5d92012-09-25 17:22:25 -0700803 // Update the folder status, in case the cursor could affect it.
804 onFolderStatusUpdated();
805 mListAdapter.setFooterVisibility(showFooter);
806
807 // Also change the cursor here.
808 onCursorUpdated();
809 }
810
811 private void onFolderStatusUpdated() {
Alice Yang0d74a662013-03-25 14:01:24 -0700812 // Update the sync status bar with sync results if needed
Alice Yang486e63e2013-04-05 13:01:50 -0700813 checkSyncStatus();
Alice Yang0d74a662013-03-25 14:01:24 -0700814
Paul Westbrookc01f5d92012-09-25 17:22:25 -0700815 final ConversationCursor cursor = getConversationListCursor();
Paul Westbrook9a70e912012-08-17 15:53:20 -0700816 Bundle extras = cursor != null ? cursor.getExtras() : Bundle.EMPTY;
Andrew Sapperstein427df9d2013-05-15 12:05:09 -0700817 int errorStatus = extras.containsKey(UIProvider.CursorExtraKeys.EXTRA_ERROR) ?
Mindy Pereira70a70c92012-08-02 08:39:45 -0700818 extras.getInt(UIProvider.CursorExtraKeys.EXTRA_ERROR)
819 : UIProvider.LastSyncResult.SUCCESS;
Andrew Sapperstein427df9d2013-05-15 12:05:09 -0700820 int cursorStatus = extras.getInt(UIProvider.CursorExtraKeys.EXTRA_STATUS);
Alice Yang0d74a662013-03-25 14:01:24 -0700821 // We want to update the UI with this information if either we are loaded or complete, or
Paul Westbrookc01f5d92012-09-25 17:22:25 -0700822 // we have a folder with a non-0 count.
823 final int folderCount = mFolder != null ? mFolder.totalCount : 0;
Andrew Sapperstein427df9d2013-05-15 12:05:09 -0700824 if (errorStatus == UIProvider.LastSyncResult.SUCCESS
825 && (cursorStatus == UIProvider.CursorStatus.LOADED
826 || cursorStatus == UIProvider.CursorStatus.COMPLETE) || folderCount > 0) {
Paul Westbrookc01f5d92012-09-25 17:22:25 -0700827 updateSearchResultHeader(folderCount);
828 if (folderCount == 0) {
Mindy Pereira6cc95532012-08-15 16:04:56 -0700829 mListView.setEmptyView(mEmptyView);
830 }
Mindy Pereira70a70c92012-08-02 08:39:45 -0700831 }
Mindy Pereiraa3911aa2012-03-09 13:26:26 -0800832 }
Mindy Pereiraf6a6b502012-03-15 15:24:26 -0700833
Mindy Pereira06642fa2012-07-12 16:23:27 -0700834 private void setSwipeAction() {
Vikram Aggarwal7c401b72012-08-13 16:43:47 -0700835 int swipeSetting = Settings.getSwipeSetting(mAccount.settings);
Mindy Pereirae58222b2012-07-25 14:33:18 -0700836 if (swipeSetting == Swipe.DISABLED
Mindy Pereirab8073372012-08-09 14:00:10 -0700837 || !mAccount.supportsCapability(AccountCapabilities.UNDO)
838 || (mFolder != null && mFolder.isTrash())) {
Mindy Pereirae58222b2012-07-25 14:33:18 -0700839 mListView.enableSwipe(false);
840 } else {
841 int action;
Rohan Shah7f98d0d2013-02-15 13:21:09 -0800842 mListView.enableSwipe(true);
mindyp77c3e1b2012-09-11 14:05:02 -0700843 if (ConversationListContext.isSearchResult(mViewContext)
Scott Kennedy8c1058e2013-03-20 13:40:20 -0700844 || (mFolder != null && mFolder.isType(FolderType.SPAM))) {
Mindy Pereirae58222b2012-07-25 14:33:18 -0700845 action = R.id.delete;
Mindy Pereira4e812f42012-07-30 16:52:49 -0700846 } else if (mFolder == null) {
Mindy Pereira01f30502012-08-14 10:30:51 -0700847 action = R.id.remove_folder;
Mindy Pereira4e812f42012-07-30 16:52:49 -0700848 } else {
849 // We have enough information to respect user settings.
850 switch (swipeSetting) {
851 case Swipe.ARCHIVE:
852 if (mAccount.supportsCapability(AccountCapabilities.ARCHIVE)) {
853 if (mFolder.supportsCapability(FolderCapabilities.ARCHIVE)) {
854 action = R.id.archive;
855 break;
856 } else if (mFolder.supportsCapability
857 (FolderCapabilities.CAN_ACCEPT_MOVED_MESSAGES)) {
Mindy Pereira01f30502012-08-14 10:30:51 -0700858 action = R.id.remove_folder;
Mindy Pereira4e812f42012-07-30 16:52:49 -0700859 break;
860 }
861 }
862 case Swipe.DELETE:
863 default:
864 action = R.id.delete;
865 break;
866 }
Mindy Pereirae58222b2012-07-25 14:33:18 -0700867 }
868 mListView.setSwipeAction(action);
869 }
Mark Wei6126d722013-04-24 21:09:43 -0700870 mListView.setCurrentAccount(mAccount);
Mindy Pereira06642fa2012-07-12 16:23:27 -0700871 mListView.setCurrentFolder(mFolder);
872 }
873
Vikram Aggarwal17f373e2012-09-17 15:49:32 -0700874 /**
875 * Changes the conversation cursor in the list and sets selected position if none is set.
876 */
877 private void onCursorUpdated() {
878 if (mCallbacks == null || mListAdapter == null) {
879 return;
880 }
Vikram Aggarwal81a4f082012-09-28 09:19:04 -0700881 // Check against the previous cursor here and see if they are the same. If they are, then
882 // do a notifyDataSetChanged.
883 final ConversationCursor newCursor = mCallbacks.getConversationListCursor();
884 mListAdapter.swapCursor(newCursor);
885 // When the conversation cursor is *updated*, we get back the same instance. In that
886 // situation, CursorAdapter.swapCursor() silently returns, without forcing a
887 // notifyDataSetChanged(). So let's force a call to notifyDataSetChanged, since an updated
888 // cursor means that the dataset has changed.
889 final int newCursorHash = (newCursor == null) ? 0 : newCursor.hashCode();
890 if (mConversationCursorHash == newCursorHash && mConversationCursorHash != 0) {
891 mListAdapter.notifyDataSetChanged();
892 }
893 mConversationCursorHash = newCursorHash;
Scott Kennedyb1e21482013-03-15 13:40:38 -0700894
Scott Kennedy4b9b3952013-03-20 17:20:59 -0700895 if (newCursor != null && newCursor.getCount() > 0) {
Scott Kennedyb1e21482013-03-15 13:40:38 -0700896 newCursor.markContentsSeen();
897 }
898
Vikram Aggarwal17f373e2012-09-17 15:49:32 -0700899 // If a current conversation is available, and none is selected in the list, then ask
900 // the list to select the current conversation.
901 final Conversation conv = mCallbacks.getCurrentConversation();
902 if (conv == null) {
903 return;
904 }
Vikram Aggarwala91d00b2013-01-18 12:00:37 -0800905 if (mListView.getChoiceMode() != ListView.CHOICE_MODE_NONE
906 && mListView.getCheckedItemPosition() == -1) {
907 setSelected(conv.position, true);
Mindy Pereira967ede62012-03-22 09:29:09 -0700908 }
Mindy Pereira21ab4902012-03-19 18:48:03 -0700909 }
Mindy Pereira1e2573b2012-04-17 14:34:36 -0700910
mindypc6adce32012-08-22 18:46:42 -0700911 public void commitDestructiveActions(boolean animate) {
Mindy Pereira8937bf12012-07-23 14:05:02 -0700912 if (mListView != null) {
mindypc6adce32012-08-22 18:46:42 -0700913 mListView.commitDestructiveActions(animate);
Vikram Aggarwal7d816002012-04-17 17:06:41 -0700914
915 }
916 }
917
mindyp9365a822012-09-12 09:09:09 -0700918 @Override
919 public void onListItemSwiped(Collection<Conversation> conversations) {
920 mUpdater.showNextConversation(conversations);
921 }
Alice Yang0d74a662013-03-25 14:01:24 -0700922
Alice Yang486e63e2013-04-05 13:01:50 -0700923 private void checkSyncStatus() {
Alice Yang76d20652013-04-24 02:32:48 -0700924 if (mFolder != null && mFolder.isSyncInProgress()) {
Alice Yang03752f32013-05-05 15:05:16 -0700925 LogUtils.d(LOG_TAG, "CLF.checkSyncStatus still syncing");
Alice Yang486e63e2013-04-05 13:01:50 -0700926 // Still syncing, ignore
927 } else {
928 // Finished syncing:
Alice Yang03752f32013-05-05 15:05:16 -0700929 LogUtils.d(LOG_TAG, "CLF.checkSyncStatus done syncing");
Alice Yang486e63e2013-04-05 13:01:50 -0700930 mConversationListView.onSyncFinished();
931 }
932 }
933
Alice Yang0d74a662013-03-25 14:01:24 -0700934 /**
Alice Yang486e63e2013-04-05 13:01:50 -0700935 * Displays the indefinite progress bar indicating a sync is in progress. This
936 * should only be called if user manually requested a sync, and not for background syncs.
Alice Yang0d74a662013-03-25 14:01:24 -0700937 */
938 protected void showSyncStatusBar() {
Alice Yang486e63e2013-04-05 13:01:50 -0700939 mConversationListView.showSyncStatusBar();
Alice Yang0d74a662013-03-25 14:01:24 -0700940 }
Rohan Shahd4f22872013-04-19 17:06:37 -0700941
942 /**
943 * Clears all items in the list.
944 */
945 public void clear() {
946 mListView.setAdapter(null);
947 }
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -0800948}