blob: f6e40f4c4d599bf1d0b83fd584de00b52ee838c2 [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 Aggarwalb9e1a352012-01-24 15:23:38 -080024import android.content.res.Resources;
Mindy Pereira6d8e7fe2012-07-26 16:02:49 -070025import android.database.DataSetObserver;
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -080026import android.os.Bundle;
27import android.os.Handler;
Scott Kennedyf77806e2013-08-30 11:38:15 -070028import android.os.Parcelable;
Mindy Pereira4bb435c2013-11-13 14:21:15 -080029import android.support.v4.widget.SwipeRefreshLayout.OnRefreshListener;
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;
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -080034import android.widget.AdapterView;
35import android.widget.AdapterView.OnItemLongClickListener;
36import android.widget.ListView;
37import android.widget.TextView;
38
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -080039import com.android.mail.ConversationListContext;
Marc Blankf3626952012-02-28 17:06:05 -080040import com.android.mail.R;
Andy Huang761522c2013-08-08 13:09:11 -070041import com.android.mail.analytics.Analytics;
Vikram Aggarwal80aeac52012-02-07 15:27:20 -080042import com.android.mail.browse.ConversationCursor;
Vikram Aggarwald247dc92012-02-10 15:49:01 -080043import com.android.mail.browse.ConversationItemView;
Mindy Pereira12fe37a2012-08-15 10:02:57 -070044import com.android.mail.browse.ConversationItemViewModel;
Mindy Pereira6681f6b2012-03-09 13:55:54 -080045import com.android.mail.browse.ConversationListFooterView;
Alice Yang64273142013-04-10 18:26:56 -070046import com.android.mail.browse.ToggleableItem;
Vikram Aggarwal80aeac52012-02-07 15:27:20 -080047import com.android.mail.providers.Account;
Vikram Aggarwal7c401b72012-08-13 16:43:47 -070048import com.android.mail.providers.AccountObserver;
Vikram Aggarwald247dc92012-02-10 15:49:01 -080049import com.android.mail.providers.Conversation;
Mindy Pereira4f166de2012-02-14 13:40:58 -080050import com.android.mail.providers.Folder;
Vikram Aggarwal50ff0e52013-03-14 13:58:02 -070051import com.android.mail.providers.FolderObserver;
Vikram Aggarwal7d816002012-04-17 17:06:41 -070052import com.android.mail.providers.Settings;
Vikram Aggarwal80aeac52012-02-07 15:27:20 -080053import com.android.mail.providers.UIProvider;
Mindy Pereira4e812f42012-07-30 16:52:49 -070054import com.android.mail.providers.UIProvider.AccountCapabilities;
Scott Kennedy5dce1462013-08-05 11:26:40 -070055import com.android.mail.providers.UIProvider.ConversationListIcon;
Mindy Pereira4e812f42012-07-30 16:52:49 -070056import com.android.mail.providers.UIProvider.FolderCapabilities;
mindyp77c3e1b2012-09-11 14:05:02 -070057import com.android.mail.providers.UIProvider.FolderType;
Mindy Pereirae58222b2012-07-25 14:33:18 -070058import com.android.mail.providers.UIProvider.Swipe;
mindyp9365a822012-09-12 09:09:09 -070059import com.android.mail.ui.SwipeableListView.ListItemSwipedListener;
60import com.android.mail.ui.SwipeableListView.ListItemsRemovedListener;
Andrew Sapperstein7c411aa2014-02-10 12:37:21 -080061import com.android.mail.ui.SwipeableListView.SwipeListener;
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -080062import com.android.mail.ui.ViewMode.ModeChangeListener;
Paul Westbrookb334c902012-06-25 11:42:46 -070063import com.android.mail.utils.LogTag;
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -080064import com.android.mail.utils.LogUtils;
Vikram Aggarwalfa131a22012-02-02 13:56:22 -080065import com.android.mail.utils.Utils;
Andrew Sapperstein6c570db2013-08-06 17:21:36 -070066import com.google.common.collect.ImmutableList;
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -080067
Mindy Pereiraf6a6b502012-03-15 15:24:26 -070068import java.util.Collection;
Scott Kennedy7c8325d2013-02-28 10:46:10 -080069import java.util.List;
Mindy Pereirafe06bea2012-02-16 08:15:14 -080070
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -080071/**
72 * The conversation list UI component.
73 */
Vikram Aggarwald247dc92012-02-10 15:49:01 -080074public final class ConversationListFragment extends ListFragment implements
Andrew Sapperstein7c411aa2014-02-10 12:37:21 -080075 OnItemLongClickListener, ModeChangeListener, ListItemSwipedListener, OnRefreshListener,
76 SwipeListener {
Vikram Aggarwal649b9ea2012-08-27 12:15:20 -070077 /** Key used to pass data to {@link ConversationListFragment}. */
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -080078 private static final String CONVERSATION_LIST_KEY = "conversation-list";
Vikram Aggarwal649b9ea2012-08-27 12:15:20 -070079 /** Key used to keep track of the scroll state of the list. */
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -080080 private static final String LIST_STATE_KEY = "list-state";
Vikram Aggarwal649b9ea2012-08-27 12:15:20 -070081
Paul Westbrookb334c902012-06-25 11:42:46 -070082 private static final String LOG_TAG = LogTag.getLogTag();
Vikram Aggarwala91d00b2013-01-18 12:00:37 -080083 /** Key used to save the ListView choice mode, since ListView doesn't save it automatically! */
84 private static final String CHOICE_MODE_KEY = "choice-mode-key";
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -080085
Vikram Aggarwald7a12cd2012-02-03 09:36:20 -080086 // True if we are on a tablet device
87 private static boolean mTabletDevice;
88
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -080089 /**
mindyp9365a822012-09-12 09:09:09 -070090 * Frequency of update of timestamps. Initialized in
91 * {@link #onCreate(Bundle)} and final afterwards.
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -080092 */
93 private static int TIMESTAMP_UPDATE_INTERVAL = 0;
94
Alice Yang0d74a662013-03-25 14:01:24 -070095 private static long NO_NEW_MESSAGE_DURATION = 1 * DateUtils.SECOND_IN_MILLIS;
96
Vikram Aggarwal80aeac52012-02-07 15:27:20 -080097 private ControllableActivity mActivity;
Vikram Aggarwal80aeac52012-02-07 15:27:20 -080098
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -080099 // Control state.
100 private ConversationListCallbacks mCallbacks;
Vikram Aggarwal80aeac52012-02-07 15:27:20 -0800101
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -0800102 private final Handler mHandler = new Handler();
Vikram Aggarwal80aeac52012-02-07 15:27:20 -0800103
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -0800104 // The internal view objects.
Mindy Pereiraf6a6b502012-03-15 15:24:26 -0700105 private SwipeableListView mListView;
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -0800106
107 private TextView mSearchResultCountTextView;
108 private TextView mSearchStatusTextView;
109
110 private View mSearchStatusView;
Vikram Aggarwal80aeac52012-02-07 15:27:20 -0800111
Vikram Aggarwal80aeac52012-02-07 15:27:20 -0800112 /**
113 * Current Account being viewed
114 */
Vikram Aggarwald247dc92012-02-10 15:49:01 -0800115 private Account mAccount;
Vikram Aggarwal80aeac52012-02-07 15:27:20 -0800116 /**
Mindy Pereira30fd47b2012-03-09 09:24:00 -0800117 * Current folder being viewed.
Vikram Aggarwal80aeac52012-02-07 15:27:20 -0800118 */
Mindy Pereira4f166de2012-02-14 13:40:58 -0800119 private Folder mFolder;
Vikram Aggarwal80aeac52012-02-07 15:27:20 -0800120
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -0800121 /**
122 * A simple method to update the timestamps of conversations periodically.
123 */
124 private Runnable mUpdateTimestampsRunnable = null;
125
126 private ConversationListContext mViewContext;
127
Vikram Aggarwal80aeac52012-02-07 15:27:20 -0800128 private AnimatedAdapter mListAdapter;
Vikram Aggarwal7d602882012-02-07 15:01:20 -0800129
Mindy Pereira6681f6b2012-03-09 13:55:54 -0800130 private ConversationListFooterView mFooterView;
Andrew Sappersteina44b0ed2014-02-12 18:56:37 -0800131 private ConversationListEmptyView mEmptyView;
Andrew Sappersteinc2c9dc12012-07-02 18:17:32 -0700132 private ErrorListener mErrorListener;
Vikram Aggarwal50ff0e52013-03-14 13:58:02 -0700133 private FolderObserver mFolderObserver;
Vikram Aggarwala91d00b2013-01-18 12:00:37 -0800134 private DataSetObserver mConversationCursorObserver;
Vikram Aggarwald247dc92012-02-10 15:49:01 -0800135
Vikram Aggarwal4f1cc0b2012-08-02 15:16:30 -0700136 private ConversationSelectionSet mSelectedSet;
Vikram Aggarwal7c401b72012-08-13 16:43:47 -0700137 private final AccountObserver mAccountObserver = new AccountObserver() {
138 @Override
139 public void onChanged(Account newAccount) {
140 mAccount = newAccount;
141 setSwipeAction();
142 }
143 };
mindyp9365a822012-09-12 09:09:09 -0700144 private ConversationUpdater mUpdater;
Vikram Aggarwal81a4f082012-09-28 09:19:04 -0700145 /** Hash of the Conversation Cursor we last obtained from the controller. */
146 private int mConversationCursorHash;
Vikram Aggarwal4f1cc0b2012-08-02 15:16:30 -0700147
Scott Kennedy1fea6a32013-07-09 15:58:51 -0700148 /** Duration, in milliseconds, of the CAB mode (peek icon) animation. */
149 private static long sSelectionModeAnimationDuration = -1;
Scott Kennedy1fea6a32013-07-09 15:58:51 -0700150 /** The time at which we last exited CAB mode. */
151 private long mSelectionModeExitedTimestamp = -1;
152
Vikram Aggarwal6c511582012-02-27 10:59:47 -0800153 /**
Scott Kennedyf77806e2013-08-30 11:38:15 -0700154 * If <code>true</code>, we have restored (or attempted to restore) the list's scroll position
155 * from when we were last on this conversation list.
156 */
157 private boolean mScrollPositionRestored = false;
Andrew Sappersteinb6910bd2014-02-13 09:52:19 -0800158 private MailSwipeRefreshLayout mSwipeRefreshWidget;
Scott Kennedyf77806e2013-08-30 11:38:15 -0700159
160 /**
mindyp9365a822012-09-12 09:09:09 -0700161 * Constructor needs to be public to handle orientation changes and activity
162 * lifecycle events.
Vikram Aggarwal6c511582012-02-27 10:59:47 -0800163 */
Paul Westbrookefc9f122012-02-21 11:14:49 -0800164 public ConversationListFragment() {
Vikram Aggarwald247dc92012-02-10 15:49:01 -0800165 super();
Vikram Aggarwald247dc92012-02-10 15:49:01 -0800166 }
167
Andrew Sapperstein7c411aa2014-02-10 12:37:21 -0800168 @Override
169 public void onBeginSwipe() {
170 mSwipeRefreshWidget.setEnabled(false);
171 }
172
173 @Override
174 public void onEndSwipe() {
175 mSwipeRefreshWidget.setEnabled(true);
176 }
177
Vikram Aggarwala91d00b2013-01-18 12:00:37 -0800178 private class ConversationCursorObserver extends DataSetObserver {
Mindy Pereira70a70c92012-08-02 08:39:45 -0700179 @Override
180 public void onChanged() {
Mindy Pereira70a70c92012-08-02 08:39:45 -0700181 onConversationListStatusUpdated();
182 }
183 }
184
Vikram Aggarwal7d602882012-02-07 15:01:20 -0800185 /**
mindyp9365a822012-09-12 09:09:09 -0700186 * Creates a new instance of {@link ConversationListFragment}, initialized
187 * to display conversation list context.
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -0800188 */
189 public static ConversationListFragment newInstance(ConversationListContext viewContext) {
Paul Westbrooke2be97a2013-05-21 02:12:09 -0700190 final ConversationListFragment fragment = new ConversationListFragment();
191 final Bundle args = new Bundle(1);
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -0800192 args.putBundle(CONVERSATION_LIST_KEY, viewContext.toBundle());
193 fragment.setArguments(args);
194 return fragment;
195 }
196
197 /**
mindyp9365a822012-09-12 09:09:09 -0700198 * Show the header if the current conversation list is showing search
199 * results.
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -0800200 */
Mindy Pereira967ede62012-03-22 09:29:09 -0700201 void configureSearchResultHeader() {
Mindy Pereira755fd6e2012-03-21 15:15:44 -0700202 if (mActivity == null) {
203 return;
204 }
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -0800205 // Only show the header if the context is for a search result
206 final Resources res = getResources();
Vikram Aggarwalae4ea992012-08-07 09:56:02 -0700207 final boolean showHeader = ConversationListContext.isSearchResult(mViewContext);
mindyp9365a822012-09-12 09:09:09 -0700208 // TODO(viki): This code contains intimate understanding of the view.
209 // Much of this logic
210 // needs to reside in a separate class that handles the text view in
211 // isolation. Then,
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -0800212 // that logic can be reused in other fragments.
213 if (showHeader) {
214 mSearchStatusTextView.setText(res.getString(R.string.search_results_searching_header));
215 // Initially reset the count
216 mSearchResultCountTextView.setText("");
217 }
218 mSearchStatusView.setVisibility(showHeader ? View.VISIBLE : View.GONE);
Mindy Pereira4bb435c2013-11-13 14:21:15 -0800219 int paddingTop = showHeader ? (int) res.getDimension(R.dimen.notification_view_height) : 0;
220 mListView.setPadding(mListView.getPaddingLeft(), paddingTop, mListView.getPaddingRight(),
221 mListView.getPaddingBottom());
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -0800222 }
223
Vikram Aggarwal80aeac52012-02-07 15:27:20 -0800224 /**
mindyp9365a822012-09-12 09:09:09 -0700225 * Show the header if the current conversation list is showing search
226 * results.
Mindy Pereira68f2e222012-03-07 10:36:54 -0800227 */
228 private void updateSearchResultHeader(int count) {
Mindy Pereira71a8f292012-07-26 16:30:48 -0700229 if (mActivity == null) {
230 return;
231 }
Mindy Pereira68f2e222012-03-07 10:36:54 -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);
Mindy Pereira68f2e222012-03-07 10:36:54 -0800235 if (showHeader) {
236 mSearchStatusTextView.setText(res.getString(R.string.search_results_header));
237 mSearchResultCountTextView
238 .setText(res.getString(R.string.search_results_loaded, count));
239 }
240 }
241
242 /**
Vikram Aggarwal80aeac52012-02-07 15:27:20 -0800243 * Initializes all internal state for a rendering.
244 */
245 private void initializeUiForFirstDisplay() {
mindyp9365a822012-09-12 09:09:09 -0700246 // TODO(mindyp): find some way to make the notification container more
247 // re-usable.
248 // TODO(viki): refactor according to comment in
249 // configureSearchResultHandler()
Vikram Aggarwal80aeac52012-02-07 15:27:20 -0800250 mSearchStatusView = mActivity.findViewById(R.id.search_status_view);
251 mSearchStatusTextView = (TextView) mActivity.findViewById(R.id.search_status_text_view);
mindyp9365a822012-09-12 09:09:09 -0700252 mSearchResultCountTextView = (TextView) mActivity
253 .findViewById(R.id.search_result_count_view);
Vikram Aggarwal80aeac52012-02-07 15:27:20 -0800254 }
255
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -0800256 @Override
Vikram Aggarwal0509bba2013-01-29 17:11:30 -0800257 public void onActivityCreated(Bundle savedState) {
258 super.onActivityCreated(savedState);
Scott Kennedy1fea6a32013-07-09 15:58:51 -0700259
260 if (sSelectionModeAnimationDuration < 0) {
261 sSelectionModeAnimationDuration = getResources().getInteger(
262 R.integer.conv_item_view_cab_anim_duration);
263 }
264
mindyp9365a822012-09-12 09:09:09 -0700265 // Strictly speaking, we get back an android.app.Activity from
266 // getActivity. However, the
267 // only activity creating a ConversationListContext is a MailActivity
268 // which is of type
269 // ControllableActivity, so this cast should be safe. If this cast
270 // fails, some other
271 // activity is creating ConversationListFragments. This activity must be
272 // of type
Vikram Aggarwal80aeac52012-02-07 15:27:20 -0800273 // ControllableActivity.
274 final Activity activity = getActivity();
mindyp9365a822012-09-12 09:09:09 -0700275 if (!(activity instanceof ControllableActivity)) {
276 LogUtils.e(LOG_TAG, "ConversationListFragment expects only a ControllableActivity to"
277 + "create it. Cannot proceed.");
Vikram Aggarwal80aeac52012-02-07 15:27:20 -0800278 }
279 mActivity = (ControllableActivity) activity;
mindyp9365a822012-09-12 09:09:09 -0700280 // Since we now have a controllable activity, load the account from it,
281 // and register for
Vikram Aggarwal7c401b72012-08-13 16:43:47 -0700282 // future account changes.
283 mAccount = mAccountObserver.initialize(mActivity.getAccountController());
Vikram Aggarwal7d602882012-02-07 15:01:20 -0800284 mCallbacks = mActivity.getListHandler();
Andrew Sappersteinc2c9dc12012-07-02 18:17:32 -0700285 mErrorListener = mActivity.getErrorListener();
Mindy Pereira278fd222012-07-26 15:23:10 -0700286 // Start off with the current state of the folder being viewed.
Alice Yang20323162013-04-17 10:37:41 -0700287 Context activityContext = mActivity.getActivityContext();
Mindy Pereira6681f6b2012-03-09 13:55:54 -0800288 mFooterView = (ConversationListFooterView) LayoutInflater.from(
Alice Yang20323162013-04-17 10:37:41 -0700289 activityContext).inflate(R.layout.conversation_list_footer_view,
Mindy Pereira6681f6b2012-03-09 13:55:54 -0800290 null);
Paul Westbrook4969e0c2012-08-20 14:38:39 -0700291 mFooterView.setClickListener(mActivity);
Vikram Aggarwal81a4f082012-09-28 09:19:04 -0700292 final ConversationCursor conversationCursor = getConversationListCursor();
Vikram Aggarwal37a20ca2013-06-06 11:19:49 -0700293 final LoaderManager manager = getLoaderManager();
294
Alice Yangc5567732013-07-29 18:34:51 -0700295 // TODO: These special views are always created, doesn't matter whether they will
296 // be shown or not, as we add more views this will get more expensive. Given these are
297 // tips that are only shown once to the user, we should consider creating these on demand.
Scott Kennedy7c8325d2013-02-28 10:46:10 -0800298 final ConversationListHelper helper = mActivity.getConversationListHelper();
Mark Wei2102b2c2013-05-02 17:15:30 -0700299 final List<ConversationSpecialItemView> specialItemViews = helper != null ?
300 ImmutableList.copyOf(helper.makeConversationListSpecialViews(
Scott Kennedy103319a2013-07-26 13:35:35 -0700301 activity, mActivity, mAccount))
Mark Wei2102b2c2013-05-02 17:15:30 -0700302 : null;
Scott Kennedy7c8325d2013-02-28 10:46:10 -0800303 if (specialItemViews != null) {
304 // Attach to the LoaderManager
305 for (final ConversationSpecialItemView view : specialItemViews) {
Scott Kennedy32ddb842013-08-28 17:38:22 -0700306 view.bindFragment(manager, savedState);
Scott Kennedy7c8325d2013-02-28 10:46:10 -0800307 }
308 }
309
310 mListAdapter = new AnimatedAdapter(mActivity.getApplicationContext(), conversationCursor,
Scott Kennedy6af70772013-09-20 19:27:56 -0400311 mActivity.getSelectedSet(), mActivity, mListView, specialItemViews);
Mindy Pereira6681f6b2012-03-09 13:55:54 -0800312 mListAdapter.addFooter(mFooterView);
Mindy Pereira6f4a6af2012-02-29 11:48:52 -0800313 mListView.setAdapter(mListAdapter);
Vikram Aggarwal4f1cc0b2012-08-02 15:16:30 -0700314 mSelectedSet = mActivity.getSelectedSet();
315 mListView.setSelectionSet(mSelectedSet);
Vikram Aggarwal37a20ca2013-06-06 11:19:49 -0700316 mListAdapter.setFooterVisibility(false);
Vikram Aggarwal50ff0e52013-03-14 13:58:02 -0700317 mFolderObserver = new FolderObserver(){
318 @Override
319 public void onChanged(Folder newFolder) {
320 onFolderUpdated(newFolder);
321 }
322 };
323 mFolderObserver.initialize(mActivity.getFolderController());
Vikram Aggarwala91d00b2013-01-18 12:00:37 -0800324 mConversationCursorObserver = new ConversationCursorObserver();
mindyp9365a822012-09-12 09:09:09 -0700325 mUpdater = mActivity.getConversationUpdater();
Vikram Aggarwala91d00b2013-01-18 12:00:37 -0800326 mUpdater.registerConversationListObserver(mConversationCursorObserver);
Vikram Aggarwalbcb16b92013-01-28 18:05:03 -0800327 mTabletDevice = Utils.useTabletUI(mActivity.getApplicationContext().getResources());
Vikram Aggarwal80aeac52012-02-07 15:27:20 -0800328 initializeUiForFirstDisplay();
Mindy Pereiradc8963b2012-03-28 17:51:05 -0700329 configureSearchResultHeader();
mindyp9365a822012-09-12 09:09:09 -0700330 // The onViewModeChanged callback doesn't get called when the mode
331 // object is created, so
Vikram Aggarwalfa131a22012-02-02 13:56:22 -0800332 // force setting the mode manually this time around.
Mindy Pereiraf96ec322012-03-02 14:06:33 -0800333 onViewModeChanged(mActivity.getViewMode().getMode());
Mindy Pereirab5901be2012-08-09 17:21:53 -0700334 mActivity.getViewMode().addListener(this);
Paul Westbrook0e3fd9d2012-04-20 02:02:23 -0700335
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -0800336 if (mActivity.isFinishing()) {
337 // Activity is finishing, just bail.
338 return;
339 }
Vikram Aggarwal81a4f082012-09-28 09:19:04 -0700340 mConversationCursorHash = (conversationCursor == null) ? 0 : conversationCursor.hashCode();
341 // Belt and suspenders here; make sure we do any necessary sync of the
342 // ConversationCursor
343 if (conversationCursor != null && conversationCursor.isRefreshReady()) {
344 conversationCursor.sync();
345 }
346
Vikram Aggarwal0509bba2013-01-29 17:11:30 -0800347 // On a phone we never highlight a conversation, so the default is to select none.
348 // On a tablet, we highlight a SINGLE conversation in landscape conversation view.
349 int choice = getDefaultChoiceMode(mTabletDevice);
350 if (savedState != null) {
351 // Restore the choice mode if it was set earlier, or NONE if creating a fresh view.
352 // Choice mode here represents the current conversation only. CAB mode does not rely on
353 // the platform: checked state is a local variable {@link ConversationItemView#mChecked}
354 choice = savedState.getInt(CHOICE_MODE_KEY, choice);
355 if (savedState.containsKey(LIST_STATE_KEY)) {
356 // TODO: find a better way to unset the selected item when restoring
357 mListView.clearChoices();
358 }
359 }
360 setChoiceMode(choice);
361
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -0800362 // Show list and start loading list.
363 showList();
Mindy Pereira4765c5c2012-07-19 11:58:22 -0700364 ToastBarOperation pendingOp = mActivity.getPendingToastOperation();
365 if (pendingOp != null) {
366 // Clear the pending operation
367 mActivity.setPendingToastOperation(null);
368 mActivity.onUndoAvailable(pendingOp);
369 }
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -0800370 }
371
Vikram Aggarwal0509bba2013-01-29 17:11:30 -0800372 /**
373 * Returns the default choice mode for the list based on whether the list is displayed on tablet
374 * or not.
375 * @param isTablet
376 * @return
377 */
378 private final static int getDefaultChoiceMode(boolean isTablet) {
379 return isTablet ? ListView.CHOICE_MODE_SINGLE : ListView.CHOICE_MODE_NONE;
380 }
381
Mindy Pereira967ede62012-03-22 09:29:09 -0700382 public AnimatedAdapter getAnimatedAdapter() {
383 return mListAdapter;
384 }
385
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -0800386 @Override
Vikram Aggarwald247dc92012-02-10 15:49:01 -0800387 public void onCreate(Bundle savedState) {
Vikram Aggarwald247dc92012-02-10 15:49:01 -0800388 super.onCreate(savedState);
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -0800389
390 // Initialize fragment constants from resources
Vikram Aggarwal6c511582012-02-27 10:59:47 -0800391 final Resources res = getResources();
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -0800392 TIMESTAMP_UPDATE_INTERVAL = res.getInteger(R.integer.timestamp_update_interval);
mindyp9365a822012-09-12 09:09:09 -0700393 mUpdateTimestampsRunnable = new Runnable() {
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -0800394 @Override
395 public void run() {
396 mListView.invalidateViews();
397 mHandler.postDelayed(mUpdateTimestampsRunnable, TIMESTAMP_UPDATE_INTERVAL);
398 }
399 };
400
Vikram Aggarwal80aeac52012-02-07 15:27:20 -0800401 // Get the context from the arguments
Vikram Aggarwal6c511582012-02-27 10:59:47 -0800402 final Bundle args = getArguments();
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -0800403 mViewContext = ConversationListContext.forBundle(args.getBundle(CONVERSATION_LIST_KEY));
Mindy Pereira3982e232012-02-29 15:00:34 -0800404 mAccount = mViewContext.account;
Paul Westbrook80ecd892012-08-16 13:37:39 -0700405
Vikram Aggarwal9a49c9b2012-08-31 10:49:33 -0700406 setRetainInstance(false);
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -0800407 }
408
409 @Override
Andy Huang9e4ca792013-02-28 14:33:43 -0800410 public String toString() {
411 final String s = super.toString();
412 if (mViewContext == null) {
413 return s;
414 }
Andy Huangc1922a92013-05-13 14:33:05 -0700415 final StringBuilder sb = new StringBuilder(s);
416 sb.setLength(sb.length() - 1);
417 sb.append(" mListAdapter=");
418 sb.append(mListAdapter);
419 sb.append(" folder=");
420 sb.append(mViewContext.folder);
421 sb.append("}");
422 return sb.toString();
Andy Huang9e4ca792013-02-28 14:33:43 -0800423 }
424
425 @Override
Vikram Aggarwal9a49c9b2012-08-31 10:49:33 -0700426 public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedState) {
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -0800427 View rootView = inflater.inflate(R.layout.conversation_list, null);
Andrew Sappersteina44b0ed2014-02-12 18:56:37 -0800428 mEmptyView = (ConversationListEmptyView) rootView.findViewById(R.id.empty_view);
Mindy Pereiraf6a6b502012-03-15 15:24:26 -0700429 mListView = (SwipeableListView) rootView.findViewById(android.R.id.list);
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -0800430 mListView.setHeaderDividersEnabled(false);
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -0800431 mListView.setOnItemLongClickListener(this);
Mindy Pereira4e812f42012-07-30 16:52:49 -0700432 mListView.enableSwipe(mAccount.supportsCapability(AccountCapabilities.UNDO));
Andrew Sapperstein7c411aa2014-02-10 12:37:21 -0800433 mListView.setListItemSwipedListener(this);
434 mListView.setSwipeListener(this);
Marc Blank2af0cf32012-03-01 19:24:13 -0800435
Vikram Aggarwal0509bba2013-01-29 17:11:30 -0800436 if (savedState != null && savedState.containsKey(LIST_STATE_KEY)) {
437 mListView.onRestoreInstanceState(savedState.getParcelable(LIST_STATE_KEY));
Vikram Aggarwal9a49c9b2012-08-31 10:49:33 -0700438 }
Andrew Sappersteinb6910bd2014-02-13 09:52:19 -0800439 mSwipeRefreshWidget =
440 (MailSwipeRefreshLayout) rootView.findViewById(R.id.swipe_refresh_widget);
Mindy Pereira4bb435c2013-11-13 14:21:15 -0800441 mSwipeRefreshWidget.setColorScheme(R.color.swipe_refresh_color1,
442 R.color.swipe_refresh_color2,
443 R.color.swipe_refresh_color3, R.color.swipe_refresh_color4);
444 mSwipeRefreshWidget.setOnRefreshListener(this);
Andrew Sappersteinb6910bd2014-02-13 09:52:19 -0800445 mSwipeRefreshWidget.setScrollableChild(mListView);
Alice Yang0d74a662013-03-25 14:01:24 -0700446
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -0800447 return rootView;
448 }
449
Vikram Aggarwala91d00b2013-01-18 12:00:37 -0800450 /**
451 * Sets the choice mode of the list view
Vikram Aggarwala91d00b2013-01-18 12:00:37 -0800452 */
453 private final void setChoiceMode(int choiceMode) {
454 mListView.setChoiceMode(choiceMode);
455 }
456
457 /**
458 * Tell the list to select nothing.
459 */
460 public final void setChoiceNone() {
Vikram Aggarwal0509bba2013-01-29 17:11:30 -0800461 // On a phone, the default choice mode is already none, so nothing to do.
462 if (!mTabletDevice) {
463 return;
464 }
Andy Huangb0be3fc2013-05-02 15:48:05 -0700465 clearChoicesAndActivated();
Vikram Aggarwala91d00b2013-01-18 12:00:37 -0800466 setChoiceMode(ListView.CHOICE_MODE_NONE);
467 }
468
469 /**
470 * Tell the list to get out of selecting none.
471 */
472 public final void revertChoiceMode() {
Vikram Aggarwal0509bba2013-01-29 17:11:30 -0800473 // On a phone, the default choice mode is always none, so nothing to do.
474 if (!mTabletDevice) {
475 return;
476 }
477 setChoiceMode(getDefaultChoiceMode(mTabletDevice));
Vikram Aggarwala91d00b2013-01-18 12:00:37 -0800478 }
479
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -0800480 @Override
Andy Huang7517e3b2012-08-20 12:30:08 -0700481 public void onDestroy() {
Andy Huang7517e3b2012-08-20 12:30:08 -0700482 super.onDestroy();
483 }
484
485 @Override
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -0800486 public void onDestroyView() {
Andy Huang7517e3b2012-08-20 12:30:08 -0700487
Vikram Aggarwal7c401b72012-08-13 16:43:47 -0700488 // Clear the list's adapter
489 mListAdapter.destroy();
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -0800490 mListView.setAdapter(null);
491
Andrew Sapperstein6c570db2013-08-06 17:21:36 -0700492 mActivity.getViewMode().removeListener(this);
Mindy Pereira71a8f292012-07-26 16:30:48 -0700493 if (mFolderObserver != null) {
Vikram Aggarwal50ff0e52013-03-14 13:58:02 -0700494 mFolderObserver.unregisterAndDestroy();
Mindy Pereira71a8f292012-07-26 16:30:48 -0700495 mFolderObserver = null;
496 }
Vikram Aggarwala91d00b2013-01-18 12:00:37 -0800497 if (mConversationCursorObserver != null) {
498 mUpdater.unregisterConversationListObserver(mConversationCursorObserver);
499 mConversationCursorObserver = null;
Mindy Pereira70a70c92012-08-02 08:39:45 -0700500 }
Vikram Aggarwal7c401b72012-08-13 16:43:47 -0700501 mAccountObserver.unregisterAndDestroy();
Scott Kennedy7c8325d2013-02-28 10:46:10 -0800502 getAnimatedAdapter().cleanup();
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -0800503 super.onDestroyView();
504 }
505
Vikram Aggarwal9730ea02012-08-02 12:46:19 -0700506 /**
mindyp9365a822012-09-12 09:09:09 -0700507 * There are three binary variables, which determine what we do with a
508 * message. checkbEnabled: Whether check boxes are enabled or not (forced
509 * true on tablet) cabModeOn: Whether CAB mode is currently on or not.
510 * pressType: long or short tap (There is a third possibility: phone or
511 * tablet, but they have <em>identical</em> behavior) The matrix of
512 * possibilities is:
513 * <p>
514 * Long tap: Always toggle selection of conversation. If CAB mode is not
515 * started, then start it.
Vikram Aggarwal9730ea02012-08-02 12:46:19 -0700516 * <pre>
517 * | Checkboxes | No Checkboxes
518 * ----------+------------+---------------
519 * CAB mode | Select | Select
520 * List mode | Select | Select
521 *
Vikram Aggarwal9730ea02012-08-02 12:46:19 -0700522 * </pre>
mindyp9365a822012-09-12 09:09:09 -0700523 *
Vikram Aggarwal9730ea02012-08-02 12:46:19 -0700524 * Reference: http://b/issue?id=6392199
525 * <p>
526 * {@inheritDoc}
527 */
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -0800528 @Override
529 public boolean onItemLongClick(AdapterView<?> parent, View view, int position, long id) {
Mindy Pereira1e231a62012-03-23 12:42:15 -0700530 // Ignore anything that is not a conversation item. Could be a footer.
531 if (!(view instanceof ConversationItemView)) {
Scott Kennedy7c8325d2013-02-28 10:46:10 -0800532 return false;
Mindy Pereira1e231a62012-03-23 12:42:15 -0700533 }
Scott Kennedy955a7662013-08-06 17:12:57 -0700534 return ((ConversationItemView) view).toggleSelectedStateOrBeginDrag();
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -0800535 }
536
Vikram Aggarwal4f1cc0b2012-08-02 15:16:30 -0700537 /**
mindyp9365a822012-09-12 09:09:09 -0700538 * See the comment for
539 * {@link #onItemLongClick(AdapterView, View, int, long)}.
540 * <p>
541 * Short tap behavior:
542 *
Vikram Aggarwal4f1cc0b2012-08-02 15:16:30 -0700543 * <pre>
544 * | Checkboxes | No Checkboxes
545 * ----------+------------+---------------
Scott Kennedy9ba7fba2013-07-30 17:41:26 -0700546 * CAB mode | Peek | Select
Vikram Aggarwal4f1cc0b2012-08-02 15:16:30 -0700547 * List mode | Peek | Peek
548 * </pre>
mindyp9365a822012-09-12 09:09:09 -0700549 *
Vikram Aggarwal4f1cc0b2012-08-02 15:16:30 -0700550 * Reference: http://b/issue?id=6392199
551 * <p>
552 * {@inheritDoc}
553 */
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -0800554 @Override
Vikram Aggarwal54452ae2012-03-13 15:29:00 -0700555 public void onListItemClick(ListView l, View view, int position, long id) {
Scott Kennedy0e8dc842013-09-10 11:13:53 -0700556 if (view instanceof ToggleableItem) {
Scott Kennedy5dce1462013-08-05 11:26:40 -0700557 final boolean showSenderImage =
558 (mAccount.settings.convListIcon == ConversationListIcon.SENDER_IMAGE);
Andy Huang042a5302013-08-13 12:39:08 -0700559 final boolean inCabMode = !mSelectedSet.isEmpty();
560 if (!showSenderImage && inCabMode) {
Vikram Aggarwal37a20ca2013-06-06 11:19:49 -0700561 ((ToggleableItem) view).toggleSelectedState();
562 } else {
Andy Huang042a5302013-08-13 12:39:08 -0700563 if (inCabMode) {
564 // this is a peek.
565 Analytics.getInstance().sendEvent("peek", null, null, mSelectedSet.size());
566 }
Scott Kennedy8afccad2013-07-28 19:09:11 -0700567 viewConversation(position);
Vikram Aggarwal37a20ca2013-06-06 11:19:49 -0700568 }
Vikram Aggarwal4f1cc0b2012-08-02 15:16:30 -0700569 } else {
Vikram Aggarwal37a20ca2013-06-06 11:19:49 -0700570 // Ignore anything that is not a conversation item. Could be a footer.
571 // If we are using a keyboard, the highlighted item is the parent;
572 // otherwise, this is a direct call from the ConverationItemView
573 return;
Vikram Aggarwal4f1cc0b2012-08-02 15:16:30 -0700574 }
mindyp9365a822012-09-12 09:09:09 -0700575 // When a new list item is clicked, commit any existing leave behind
mindyp8694fe92012-09-25 11:07:16 -0700576 // items. Wait until we have opened the desired conversation to cause
577 // any position changes.
Vikram Aggarwalbcb16b92013-01-28 18:05:03 -0800578 commitDestructiveActions(Utils.useTabletUI(mActivity.getActivityContext().getResources()));
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -0800579 }
580
581 @Override
Scott Kennedy08a079c2013-02-22 10:13:10 -0800582 public void onResume() {
583 super.onResume();
584
585 final ConversationCursor conversationCursor = getConversationListCursor();
586 if (conversationCursor != null) {
587 conversationCursor.handleNotificationActions();
Scott Kennedyf77806e2013-08-30 11:38:15 -0700588
589 restoreLastScrolledPosition();
Scott Kennedy08a079c2013-02-22 10:13:10 -0800590 }
Scott Kennedy1fea6a32013-07-09 15:58:51 -0700591
592 mSelectedSet.addObserver(mConversationSetObserver);
Scott Kennedy08a079c2013-02-22 10:13:10 -0800593 }
594
595 @Override
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -0800596 public void onPause() {
597 super.onPause();
Scott Kennedy1fea6a32013-07-09 15:58:51 -0700598
599 mSelectedSet.removeObserver(mConversationSetObserver);
Scott Kennedyf77806e2013-08-30 11:38:15 -0700600
601 saveLastScrolledPosition();
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -0800602 }
603
604 @Override
605 public void onSaveInstanceState(Bundle outState) {
606 super.onSaveInstanceState(outState);
607 if (mListView != null) {
608 outState.putParcelable(LIST_STATE_KEY, mListView.onSaveInstanceState());
Vikram Aggarwala91d00b2013-01-18 12:00:37 -0800609 outState.putInt(CHOICE_MODE_KEY, mListView.getChoiceMode());
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -0800610 }
Scott Kennedy32ddb842013-08-28 17:38:22 -0700611
612 if (mListAdapter != null) {
613 mListAdapter.saveSpecialItemInstanceState(outState);
614 }
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -0800615 }
616
617 @Override
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -0800618 public void onStart() {
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -0800619 super.onStart();
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -0800620 mHandler.postDelayed(mUpdateTimestampsRunnable, TIMESTAMP_UPDATE_INTERVAL);
Andy Huanga90c33b2013-11-25 17:02:05 -0800621 Analytics.getInstance().sendView("ConversationListFragment");
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -0800622 }
623
624 @Override
625 public void onStop() {
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -0800626 super.onStop();
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -0800627 mHandler.removeCallbacks(mUpdateTimestampsRunnable);
628 }
629
Vikram Aggarwal80aeac52012-02-07 15:27:20 -0800630 @Override
Vikram Aggarwalfa131a22012-02-02 13:56:22 -0800631 public void onViewModeChanged(int newMode) {
Vikram Aggarwalfa131a22012-02-02 13:56:22 -0800632 if (mTabletDevice) {
Andy Huangb0be3fc2013-05-02 15:48:05 -0700633 if (ViewMode.isListMode(newMode)) {
Vikram Aggarwal0509bba2013-01-29 17:11:30 -0800634 // There are no selected conversations when in conversation list mode.
Andy Huangb0be3fc2013-05-02 15:48:05 -0700635 clearChoicesAndActivated();
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -0800636 }
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -0800637 }
Mindy Pereirab5901be2012-08-09 17:21:53 -0700638 if (mFooterView != null) {
639 mFooterView.onViewModeChanged(newMode);
640 }
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -0800641 }
Mindy Pereirab5901be2012-08-09 17:21:53 -0700642
Andy Huang48ccbc52013-06-05 20:30:47 -0700643 public boolean isAnimating() {
644 final AnimatedAdapter adapter = getAnimatedAdapter();
645 return (adapter != null && adapter.isAnimating()) ||
646 (mListView != null && mListView.isScrolling());
647 }
648
Andy Huangb0be3fc2013-05-02 15:48:05 -0700649 private void clearChoicesAndActivated() {
650 final int currentSelected = mListView.getCheckedItemPosition();
Andy Huangb0be3fc2013-05-02 15:48:05 -0700651 if (currentSelected != ListView.INVALID_POSITION) {
Andy Huangf0aebd32013-06-17 15:00:15 -0700652 mListView.setItemChecked(mListView.getCheckedItemPosition(), false);
Andy Huangb0be3fc2013-05-02 15:48:05 -0700653 }
654 }
655
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -0800656 /**
mindyp9365a822012-09-12 09:09:09 -0700657 * Handles a request to show a new conversation list, either from a search
658 * query or for viewing a folder. This will initiate a data load, and hence
659 * must be called on the UI thread.
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -0800660 */
661 private void showList() {
Vikram Aggarwal80aeac52012-02-07 15:27:20 -0800662 mListView.setEmptyView(null);
Andy Huang7591d2f2012-07-26 16:48:06 -0700663 onFolderUpdated(mActivity.getFolderController().getFolder());
mindypca8ca2d2012-09-11 17:38:34 -0700664 onConversationListStatusUpdated();
Andy Huanga90c33b2013-11-25 17:02:05 -0800665
666 // try to get an order-of-magnitude sense for message count within folders
667 // (N.B. this count currently isn't working for search folders, since their counts stream
668 // in over time in pieces.)
669 final Folder f = mViewContext.folder;
670 if (f != null) {
671 final long countLog;
672 if (f.totalCount > 0) {
673 countLog = (long) Math.log10(f.totalCount);
674 } else {
675 countLog = 0;
676 }
677 Analytics.getInstance().sendEvent("view_folder", f.getTypeDescription(),
678 Long.toString(countLog), f.totalCount);
679 }
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -0800680 }
681
Scott Kennedy8afccad2013-07-28 19:09:11 -0700682 /**
683 * View the message at the given position.
684 *
685 * @param position The position of the conversation in the list (as opposed to its position
686 * in the cursor)
687 */
688 private void viewConversation(final int position) {
689 LogUtils.d(LOG_TAG, "ConversationListFragment.viewConversation(%d)", position);
690
691 final ConversationCursor cursor =
692 (ConversationCursor) getAnimatedAdapter().getItem(position);
693
694 if (cursor == null) {
695 LogUtils.e(LOG_TAG,
696 "unable to open conv at cursor pos=%s cursor=%s getPositionOffset=%s",
697 position, cursor, getAnimatedAdapter().getPositionOffset(position));
698 return;
699 }
700
701 final Conversation conv = cursor.getConversation();
702 /*
703 * The cursor position may be different than the position method parameter because of
704 * special views in the list.
705 */
706 conv.position = cursor.getPosition();
707 setSelected(conv.position, true);
708 mCallbacks.onConversationSelected(conv, false /* inLoaderCallbacks */);
709 }
710
Vikram Aggarwala91d00b2013-01-18 12:00:37 -0800711 /**
712 * Sets the selected conversation to the position given here.
Alice Yang0d74a662013-03-25 14:01:24 -0700713 * @param cursorPosition The position of the conversation in the cursor (as opposed to
714 * in the list)
Vikram Aggarwala91d00b2013-01-18 12:00:37 -0800715 * @param different if the currently selected conversation is different from the one provided
716 * here. This is a difference in conversations, not a difference in positions. For example, a
717 * conversation at position 2 can move to position 4 as a result of new mail.
718 */
Scott Kennedy7c8325d2013-02-28 10:46:10 -0800719 public void setSelected(final int cursorPosition, boolean different) {
Vikram Aggarwala91d00b2013-01-18 12:00:37 -0800720 if (mListView.getChoiceMode() == ListView.CHOICE_MODE_NONE) {
721 return;
722 }
Scott Kennedy7c8325d2013-02-28 10:46:10 -0800723
724 final int position =
725 cursorPosition + getAnimatedAdapter().getPositionOffset(cursorPosition);
726
Scott Kennedye0d01fb2013-08-19 19:07:54 -0700727 setRawSelected(position, different);
728 }
729
730 /**
731 * Sets the selected conversation to the position given here.
732 * @param position The position of the item in the list
733 * @param different if the currently selected conversation is different from the one provided
734 * here. This is a difference in conversations, not a difference in positions. For example, a
735 * conversation at position 2 can move to position 4 as a result of new mail.
736 */
737 public void setRawSelected(final int position, final boolean different) {
738 if (mListView.getChoiceMode() == ListView.CHOICE_MODE_NONE) {
739 return;
740 }
741
mindype21f8862012-10-01 09:32:03 -0700742 if (different) {
743 mListView.smoothScrollToPosition(position);
744 }
745 mListView.setItemChecked(position, true);
746 }
747
Vikram Aggarwal3d7ca9d2012-05-11 14:40:36 -0700748 /**
Vikram Aggarwala91d00b2013-01-18 12:00:37 -0800749 * Returns the cursor associated with the conversation list.
750 * @return
Vikram Aggarwal3d7ca9d2012-05-11 14:40:36 -0700751 */
Mindy Pereira967ede62012-03-22 09:29:09 -0700752 private ConversationCursor getConversationListCursor() {
753 return mCallbacks != null ? mCallbacks.getConversationListCursor() : null;
Mindy Pereirafe06bea2012-02-16 08:15:14 -0800754 }
755
756 /**
mindyp9365a822012-09-12 09:09:09 -0700757 * Request a refresh of the list. No sync is carried out and none is
758 * promised.
Vikram Aggarwal54452ae2012-03-13 15:29:00 -0700759 */
760 public void requestListRefresh() {
761 mListAdapter.notifyDataSetChanged();
762 }
763
Vikram Aggarwal75daee52012-04-30 11:13:09 -0700764 /**
Vikram Aggarwal7f602f72012-04-30 16:04:06 -0700765 * Change the UI to delete the conversations provided and then call the
mindyp9365a822012-09-12 09:09:09 -0700766 * {@link DestructiveAction} provided here <b>after</b> the UI has been
767 * updated.
Vikram Aggarwal7f602f72012-04-30 16:04:06 -0700768 * @param conversations
Vikram Aggarwal75daee52012-04-30 11:13:09 -0700769 * @param action
770 */
Vikram Aggarwala8e43182012-09-13 12:55:10 -0700771 public void requestDelete(int actionId, final Collection<Conversation> conversations,
Vikram Aggarwal669947b2013-01-10 17:05:56 -0800772 final DestructiveAction action) {
Mindy Pereiraacf60392012-04-06 09:11:00 -0700773 for (Conversation conv : conversations) {
774 conv.localDeleteOnUpdate = true;
775 }
Vikram Aggarwala8e43182012-09-13 12:55:10 -0700776 final ListItemsRemovedListener listener = new ListItemsRemovedListener() {
mindyp9365a822012-09-12 09:09:09 -0700777 @Override
778 public void onListItemsRemoved() {
779 action.performAction();
780 }
Vikram Aggarwala8e43182012-09-13 12:55:10 -0700781 };
782 final SwipeableListView listView = (SwipeableListView) getListView();
783 if (listView.getSwipeAction() == actionId) {
Paul Westbrookcec3e0b2013-02-04 22:50:22 -0800784 if (!listView.destroyItems(conversations, listener)) {
785 // The listView failed to destroy the items, perform the action manually
786 LogUtils.e(LOG_TAG, "ConversationListFragment.requestDelete: " +
787 "listView failed to destroy items.");
788 action.performAction();
789 }
Vikram Aggarwala8e43182012-09-13 12:55:10 -0700790 return;
791 }
792 // Delete the local delete items (all for now) and when done,
793 // update...
794 mListAdapter.delete(conversations, listener);
Mindy Pereiraacf60392012-04-06 09:11:00 -0700795 }
Mindy Pereiraa3911aa2012-03-09 13:26:26 -0800796
Mindy Pereira11dd5ef2012-03-10 15:10:18 -0800797 public void onFolderUpdated(Folder folder) {
Mindy Pereiradc8963b2012-03-28 17:51:05 -0700798 mFolder = folder;
Mindy Pereira06642fa2012-07-12 16:23:27 -0700799 setSwipeAction();
Mindy Pereira4bb435c2013-11-13 14:21:15 -0800800
801 // Update enabled state of swipe to refresh.
802 mSwipeRefreshWidget.setEnabled(!ConversationListContext.isSearchResult(mViewContext));
803
Mindy Pereira82cea482012-03-27 16:45:00 -0700804 if (mFolder == null) {
805 return;
806 }
Mindy Pereira4584a0d2012-03-13 14:42:14 -0700807 mListAdapter.setFolder(mFolder);
Mindy Pereira70a70c92012-08-02 08:39:45 -0700808 mFooterView.setFolder(mFolder);
Vikram Aggarwal41b9e8f2012-09-25 10:15:04 -0700809 if (!mFolder.wasSyncSuccessful()) {
Mindy Pereira70a70c92012-08-02 08:39:45 -0700810 mErrorListener.onError(mFolder, false);
811 }
Paul Westbrookc01f5d92012-09-25 17:22:25 -0700812
813 // Notify of changes to the Folder.
814 onFolderStatusUpdated();
815
Mindy Pereira12fe37a2012-08-15 10:02:57 -0700816 // Blow away conversation items cache.
817 ConversationItemViewModel.onFolderUpdated(mFolder);
Mindy Pereira70a70c92012-08-02 08:39:45 -0700818 }
819
Vikram Aggarwal81a4f082012-09-28 09:19:04 -0700820 /**
821 * Updates the footer visibility and updates the conversation cursor
822 */
Mindy Pereira70a70c92012-08-02 08:39:45 -0700823 public void onConversationListStatusUpdated() {
Paul Westbrook9a70e912012-08-17 15:53:20 -0700824 final ConversationCursor cursor = getConversationListCursor();
Tony Mantler8f28e112013-09-18 14:35:01 -0700825 final boolean showFooter = mFooterView.updateStatus(cursor);
Paul Westbrookc01f5d92012-09-25 17:22:25 -0700826 // Update the folder status, in case the cursor could affect it.
827 onFolderStatusUpdated();
828 mListAdapter.setFooterVisibility(showFooter);
829
830 // Also change the cursor here.
831 onCursorUpdated();
832 }
833
834 private void onFolderStatusUpdated() {
Alice Yang0d74a662013-03-25 14:01:24 -0700835 // Update the sync status bar with sync results if needed
Alice Yang486e63e2013-04-05 13:01:50 -0700836 checkSyncStatus();
Alice Yang0d74a662013-03-25 14:01:24 -0700837
Paul Westbrookc01f5d92012-09-25 17:22:25 -0700838 final ConversationCursor cursor = getConversationListCursor();
Paul Westbrook9a70e912012-08-17 15:53:20 -0700839 Bundle extras = cursor != null ? cursor.getExtras() : Bundle.EMPTY;
Andrew Sapperstein427df9d2013-05-15 12:05:09 -0700840 int errorStatus = extras.containsKey(UIProvider.CursorExtraKeys.EXTRA_ERROR) ?
Mindy Pereira70a70c92012-08-02 08:39:45 -0700841 extras.getInt(UIProvider.CursorExtraKeys.EXTRA_ERROR)
842 : UIProvider.LastSyncResult.SUCCESS;
Andrew Sapperstein427df9d2013-05-15 12:05:09 -0700843 int cursorStatus = extras.getInt(UIProvider.CursorExtraKeys.EXTRA_STATUS);
Alice Yang0d74a662013-03-25 14:01:24 -0700844 // We want to update the UI with this information if either we are loaded or complete, or
Paul Westbrookc01f5d92012-09-25 17:22:25 -0700845 // we have a folder with a non-0 count.
846 final int folderCount = mFolder != null ? mFolder.totalCount : 0;
Andrew Sapperstein427df9d2013-05-15 12:05:09 -0700847 if (errorStatus == UIProvider.LastSyncResult.SUCCESS
848 && (cursorStatus == UIProvider.CursorStatus.LOADED
849 || cursorStatus == UIProvider.CursorStatus.COMPLETE) || folderCount > 0) {
Paul Westbrookc01f5d92012-09-25 17:22:25 -0700850 updateSearchResultHeader(folderCount);
851 if (folderCount == 0) {
Andrew Sappersteina44b0ed2014-02-12 18:56:37 -0800852 mEmptyView.setupEmptyView(
853 mFolder, mViewContext.searchQuery, mListAdapter.getBidiFormatter());
Mindy Pereira6cc95532012-08-15 16:04:56 -0700854 mListView.setEmptyView(mEmptyView);
855 }
Mindy Pereira70a70c92012-08-02 08:39:45 -0700856 }
Mindy Pereiraa3911aa2012-03-09 13:26:26 -0800857 }
Mindy Pereiraf6a6b502012-03-15 15:24:26 -0700858
Mindy Pereira06642fa2012-07-12 16:23:27 -0700859 private void setSwipeAction() {
Vikram Aggarwal7c401b72012-08-13 16:43:47 -0700860 int swipeSetting = Settings.getSwipeSetting(mAccount.settings);
Mindy Pereirae58222b2012-07-25 14:33:18 -0700861 if (swipeSetting == Swipe.DISABLED
Mindy Pereirab8073372012-08-09 14:00:10 -0700862 || !mAccount.supportsCapability(AccountCapabilities.UNDO)
863 || (mFolder != null && mFolder.isTrash())) {
Mindy Pereirae58222b2012-07-25 14:33:18 -0700864 mListView.enableSwipe(false);
865 } else {
Scott Kennedy3b965d72013-06-25 14:36:55 -0700866 final int action;
Rohan Shah7f98d0d2013-02-15 13:21:09 -0800867 mListView.enableSwipe(true);
mindyp77c3e1b2012-09-11 14:05:02 -0700868 if (ConversationListContext.isSearchResult(mViewContext)
Scott Kennedy8c1058e2013-03-20 13:40:20 -0700869 || (mFolder != null && mFolder.isType(FolderType.SPAM))) {
Mindy Pereirae58222b2012-07-25 14:33:18 -0700870 action = R.id.delete;
Mindy Pereira4e812f42012-07-30 16:52:49 -0700871 } else if (mFolder == null) {
Mindy Pereira01f30502012-08-14 10:30:51 -0700872 action = R.id.remove_folder;
Mindy Pereira4e812f42012-07-30 16:52:49 -0700873 } else {
874 // We have enough information to respect user settings.
875 switch (swipeSetting) {
876 case Swipe.ARCHIVE:
877 if (mAccount.supportsCapability(AccountCapabilities.ARCHIVE)) {
878 if (mFolder.supportsCapability(FolderCapabilities.ARCHIVE)) {
879 action = R.id.archive;
880 break;
881 } else if (mFolder.supportsCapability
882 (FolderCapabilities.CAN_ACCEPT_MOVED_MESSAGES)) {
Mindy Pereira01f30502012-08-14 10:30:51 -0700883 action = R.id.remove_folder;
Mindy Pereira4e812f42012-07-30 16:52:49 -0700884 break;
885 }
886 }
Scott Kennedy3b965d72013-06-25 14:36:55 -0700887
888 /*
889 * If we get here, we don't support archive, on either the account or the
890 * folder, so we want to fall through into the delete case.
891 */
892 //$FALL-THROUGH$
Mindy Pereira4e812f42012-07-30 16:52:49 -0700893 case Swipe.DELETE:
894 default:
895 action = R.id.delete;
896 break;
897 }
Mindy Pereirae58222b2012-07-25 14:33:18 -0700898 }
899 mListView.setSwipeAction(action);
900 }
Mark Wei6126d722013-04-24 21:09:43 -0700901 mListView.setCurrentAccount(mAccount);
Mindy Pereira06642fa2012-07-12 16:23:27 -0700902 mListView.setCurrentFolder(mFolder);
903 }
904
Vikram Aggarwal17f373e2012-09-17 15:49:32 -0700905 /**
906 * Changes the conversation cursor in the list and sets selected position if none is set.
907 */
908 private void onCursorUpdated() {
909 if (mCallbacks == null || mListAdapter == null) {
910 return;
911 }
Vikram Aggarwal81a4f082012-09-28 09:19:04 -0700912 // Check against the previous cursor here and see if they are the same. If they are, then
913 // do a notifyDataSetChanged.
914 final ConversationCursor newCursor = mCallbacks.getConversationListCursor();
Scott Kennedyf77806e2013-08-30 11:38:15 -0700915
916 if (newCursor == null && mListAdapter.getCursor() != null) {
917 // We're losing our cursor, so save our scroll position
918 saveLastScrolledPosition();
919 }
920
Vikram Aggarwal81a4f082012-09-28 09:19:04 -0700921 mListAdapter.swapCursor(newCursor);
922 // When the conversation cursor is *updated*, we get back the same instance. In that
923 // situation, CursorAdapter.swapCursor() silently returns, without forcing a
924 // notifyDataSetChanged(). So let's force a call to notifyDataSetChanged, since an updated
925 // cursor means that the dataset has changed.
926 final int newCursorHash = (newCursor == null) ? 0 : newCursor.hashCode();
927 if (mConversationCursorHash == newCursorHash && mConversationCursorHash != 0) {
928 mListAdapter.notifyDataSetChanged();
929 }
930 mConversationCursorHash = newCursorHash;
Scott Kennedyb1e21482013-03-15 13:40:38 -0700931
Scott Kennedy4b9b3952013-03-20 17:20:59 -0700932 if (newCursor != null && newCursor.getCount() > 0) {
Scott Kennedyb1e21482013-03-15 13:40:38 -0700933 newCursor.markContentsSeen();
Scott Kennedyf77806e2013-08-30 11:38:15 -0700934 restoreLastScrolledPosition();
Scott Kennedyb1e21482013-03-15 13:40:38 -0700935 }
936
Vikram Aggarwal17f373e2012-09-17 15:49:32 -0700937 // If a current conversation is available, and none is selected in the list, then ask
938 // the list to select the current conversation.
939 final Conversation conv = mCallbacks.getCurrentConversation();
Scott Kennedyf77806e2013-08-30 11:38:15 -0700940 if (conv != null) {
941 if (mListView.getChoiceMode() != ListView.CHOICE_MODE_NONE
942 && mListView.getCheckedItemPosition() == -1) {
943 setSelected(conv.position, true);
944 }
Mindy Pereira967ede62012-03-22 09:29:09 -0700945 }
Mindy Pereira21ab4902012-03-19 18:48:03 -0700946 }
Mindy Pereira1e2573b2012-04-17 14:34:36 -0700947
mindypc6adce32012-08-22 18:46:42 -0700948 public void commitDestructiveActions(boolean animate) {
Mindy Pereira8937bf12012-07-23 14:05:02 -0700949 if (mListView != null) {
mindypc6adce32012-08-22 18:46:42 -0700950 mListView.commitDestructiveActions(animate);
Vikram Aggarwal7d816002012-04-17 17:06:41 -0700951
952 }
953 }
954
mindyp9365a822012-09-12 09:09:09 -0700955 @Override
956 public void onListItemSwiped(Collection<Conversation> conversations) {
957 mUpdater.showNextConversation(conversations);
958 }
Alice Yang0d74a662013-03-25 14:01:24 -0700959
Alice Yang486e63e2013-04-05 13:01:50 -0700960 private void checkSyncStatus() {
Alice Yang76d20652013-04-24 02:32:48 -0700961 if (mFolder != null && mFolder.isSyncInProgress()) {
Alice Yang03752f32013-05-05 15:05:16 -0700962 LogUtils.d(LOG_TAG, "CLF.checkSyncStatus still syncing");
Alice Yang486e63e2013-04-05 13:01:50 -0700963 // Still syncing, ignore
964 } else {
965 // Finished syncing:
Alice Yang03752f32013-05-05 15:05:16 -0700966 LogUtils.d(LOG_TAG, "CLF.checkSyncStatus done syncing");
Mindy Pereira4bb435c2013-11-13 14:21:15 -0800967 mSwipeRefreshWidget.setRefreshing(false);
Alice Yang486e63e2013-04-05 13:01:50 -0700968 }
969 }
970
Alice Yang0d74a662013-03-25 14:01:24 -0700971 /**
Alice Yang486e63e2013-04-05 13:01:50 -0700972 * Displays the indefinite progress bar indicating a sync is in progress. This
973 * should only be called if user manually requested a sync, and not for background syncs.
Alice Yang0d74a662013-03-25 14:01:24 -0700974 */
975 protected void showSyncStatusBar() {
Mindy Pereira4bb435c2013-11-13 14:21:15 -0800976 mSwipeRefreshWidget.setRefreshing(true);
Alice Yang0d74a662013-03-25 14:01:24 -0700977 }
Rohan Shahd4f22872013-04-19 17:06:37 -0700978
979 /**
980 * Clears all items in the list.
981 */
982 public void clear() {
983 mListView.setAdapter(null);
984 }
Scott Kennedy1fea6a32013-07-09 15:58:51 -0700985
986 private final ConversationSetObserver mConversationSetObserver = new ConversationSetObserver() {
987 @Override
988 public void onSetPopulated(final ConversationSelectionSet set) {
Mindy Pereira4bb435c2013-11-13 14:21:15 -0800989 // Disable the swipe to refresh widget.
990 mSwipeRefreshWidget.setEnabled(false);
Scott Kennedy1fea6a32013-07-09 15:58:51 -0700991 }
992
993 @Override
994 public void onSetEmpty() {
995 mSelectionModeExitedTimestamp = System.currentTimeMillis();
Mindy Pereira4bb435c2013-11-13 14:21:15 -0800996 mSwipeRefreshWidget.setEnabled(true);
Scott Kennedy1fea6a32013-07-09 15:58:51 -0700997 }
998
999 @Override
1000 public void onSetChanged(final ConversationSelectionSet set) {
Scott Kennedy8afccad2013-07-28 19:09:11 -07001001 // Do nothing
Scott Kennedy1fea6a32013-07-09 15:58:51 -07001002 }
1003 };
Scott Kennedyf77806e2013-08-30 11:38:15 -07001004
1005 private void saveLastScrolledPosition() {
1006 if (mListAdapter.getCursor() == null) {
1007 // If you save your scroll position in an empty list, you're gonna have a bad time
1008 return;
1009 }
1010
1011 final Parcelable savedState = mListView.onSaveInstanceState();
1012
1013 mActivity.getListHandler().setConversationListScrollPosition(
1014 mFolder.conversationListUri.toString(), savedState);
1015 }
1016
1017 private void restoreLastScrolledPosition() {
1018 // Scroll to our previous position, if necessary
Scott Kennedy599c3cf2013-09-25 15:17:19 -07001019 if (!mScrollPositionRestored && mFolder != null) {
1020 final String key = mFolder.conversationListUri.toString();
Scott Kennedyf77806e2013-08-30 11:38:15 -07001021 final Parcelable savedState = mActivity.getListHandler()
Scott Kennedy599c3cf2013-09-25 15:17:19 -07001022 .getConversationListScrollPosition(key);
Scott Kennedyf77806e2013-08-30 11:38:15 -07001023 if (savedState != null) {
1024 mListView.onRestoreInstanceState(savedState);
1025 }
1026 mScrollPositionRestored = true;
1027 }
1028 }
Mindy Pereira4bb435c2013-11-13 14:21:15 -08001029
1030 /* (non-Javadoc)
1031 * @see android.support.v4.widget.SwipeRefreshLayout.OnRefreshListener#onRefresh()
1032 */
1033 @Override
1034 public void onRefresh() {
1035 Analytics.getInstance().sendEvent(Analytics.EVENT_CATEGORY_MENU_ITEM, "swipe_refresh", null,
1036 0);
1037
1038 // This will call back to showSyncStatusBar():
1039 mActivity.getFolderController().requestFolderRefresh();
1040 }
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -08001041}