blob: e497d4272093e7f5c38fa8efd11061d8c022df0f [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;
22import android.content.res.Resources;
Mindy Pereira6d8e7fe2012-07-26 16:02:49 -070023import android.database.DataSetObserver;
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -080024import android.os.Bundle;
25import android.os.Handler;
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -080026import android.view.LayoutInflater;
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -080027import android.view.View;
28import android.view.ViewGroup;
29import android.view.ViewGroup.MarginLayoutParams;
30import android.widget.AdapterView;
31import android.widget.AdapterView.OnItemLongClickListener;
32import android.widget.ListView;
33import android.widget.TextView;
34
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -080035import com.android.mail.ConversationListContext;
Marc Blankf3626952012-02-28 17:06:05 -080036import com.android.mail.R;
mindypc03b05a2012-09-28 14:23:48 -070037import com.android.mail.browse.ToggleableItem;
Vikram Aggarwal80aeac52012-02-07 15:27:20 -080038import com.android.mail.browse.ConversationCursor;
Vikram Aggarwald247dc92012-02-10 15:49:01 -080039import com.android.mail.browse.ConversationItemView;
Mindy Pereira12fe37a2012-08-15 10:02:57 -070040import com.android.mail.browse.ConversationItemViewModel;
Mindy Pereira6681f6b2012-03-09 13:55:54 -080041import com.android.mail.browse.ConversationListFooterView;
Vikram Aggarwal80aeac52012-02-07 15:27:20 -080042import com.android.mail.providers.Account;
Vikram Aggarwal7c401b72012-08-13 16:43:47 -070043import com.android.mail.providers.AccountObserver;
Vikram Aggarwald247dc92012-02-10 15:49:01 -080044import com.android.mail.providers.Conversation;
Mindy Pereira4f166de2012-02-14 13:40:58 -080045import com.android.mail.providers.Folder;
Vikram Aggarwal7d816002012-04-17 17:06:41 -070046import com.android.mail.providers.Settings;
Vikram Aggarwal80aeac52012-02-07 15:27:20 -080047import com.android.mail.providers.UIProvider;
Mindy Pereira4e812f42012-07-30 16:52:49 -070048import com.android.mail.providers.UIProvider.AccountCapabilities;
49import com.android.mail.providers.UIProvider.FolderCapabilities;
mindyp77c3e1b2012-09-11 14:05:02 -070050import com.android.mail.providers.UIProvider.FolderType;
Mindy Pereirae58222b2012-07-25 14:33:18 -070051import com.android.mail.providers.UIProvider.Swipe;
mindyp9365a822012-09-12 09:09:09 -070052import com.android.mail.ui.SwipeableListView.ListItemSwipedListener;
53import com.android.mail.ui.SwipeableListView.ListItemsRemovedListener;
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -080054import com.android.mail.ui.ViewMode.ModeChangeListener;
Paul Westbrookb334c902012-06-25 11:42:46 -070055import com.android.mail.utils.LogTag;
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -080056import com.android.mail.utils.LogUtils;
Vikram Aggarwalfa131a22012-02-02 13:56:22 -080057import com.android.mail.utils.Utils;
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -080058
Mindy Pereiraf6a6b502012-03-15 15:24:26 -070059import java.util.Collection;
Mindy Pereirafe06bea2012-02-16 08:15:14 -080060
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -080061/**
62 * The conversation list UI component.
63 */
Vikram Aggarwald247dc92012-02-10 15:49:01 -080064public final class ConversationListFragment extends ListFragment implements
mindyp9365a822012-09-12 09:09:09 -070065 OnItemLongClickListener, ModeChangeListener, ListItemSwipedListener {
Vikram Aggarwal649b9ea2012-08-27 12:15:20 -070066 /** Key used to pass data to {@link ConversationListFragment}. */
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -080067 private static final String CONVERSATION_LIST_KEY = "conversation-list";
Vikram Aggarwal649b9ea2012-08-27 12:15:20 -070068 /** Key used to keep track of the scroll state of the list. */
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -080069 private static final String LIST_STATE_KEY = "list-state";
Vikram Aggarwal649b9ea2012-08-27 12:15:20 -070070
Paul Westbrookb334c902012-06-25 11:42:46 -070071 private static final String LOG_TAG = LogTag.getLogTag();
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -080072
Vikram Aggarwald7a12cd2012-02-03 09:36:20 -080073 // True if we are on a tablet device
74 private static boolean mTabletDevice;
75
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -080076 /**
mindyp9365a822012-09-12 09:09:09 -070077 * Frequency of update of timestamps. Initialized in
78 * {@link #onCreate(Bundle)} and final afterwards.
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -080079 */
80 private static int TIMESTAMP_UPDATE_INTERVAL = 0;
81
Vikram Aggarwal80aeac52012-02-07 15:27:20 -080082 private ControllableActivity mActivity;
Vikram Aggarwal80aeac52012-02-07 15:27:20 -080083
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -080084 // Control state.
85 private ConversationListCallbacks mCallbacks;
Vikram Aggarwal80aeac52012-02-07 15:27:20 -080086
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -080087 private final Handler mHandler = new Handler();
Vikram Aggarwal80aeac52012-02-07 15:27:20 -080088
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -080089 // The internal view objects.
Mindy Pereiraf6a6b502012-03-15 15:24:26 -070090 private SwipeableListView mListView;
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -080091
92 private TextView mSearchResultCountTextView;
93 private TextView mSearchStatusTextView;
94
95 private View mSearchStatusView;
Vikram Aggarwal80aeac52012-02-07 15:27:20 -080096
Vikram Aggarwal80aeac52012-02-07 15:27:20 -080097 /**
98 * Current Account being viewed
99 */
Vikram Aggarwald247dc92012-02-10 15:49:01 -0800100 private Account mAccount;
Vikram Aggarwal80aeac52012-02-07 15:27:20 -0800101 /**
Mindy Pereira30fd47b2012-03-09 09:24:00 -0800102 * Current folder being viewed.
Vikram Aggarwal80aeac52012-02-07 15:27:20 -0800103 */
Mindy Pereira4f166de2012-02-14 13:40:58 -0800104 private Folder mFolder;
Vikram Aggarwal80aeac52012-02-07 15:27:20 -0800105
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -0800106 /**
107 * A simple method to update the timestamps of conversations periodically.
108 */
109 private Runnable mUpdateTimestampsRunnable = null;
110
111 private ConversationListContext mViewContext;
112
Vikram Aggarwal80aeac52012-02-07 15:27:20 -0800113 private AnimatedAdapter mListAdapter;
Vikram Aggarwal7d602882012-02-07 15:01:20 -0800114
Mindy Pereira6681f6b2012-03-09 13:55:54 -0800115 private ConversationListFooterView mFooterView;
Mindy Pereira6cc95532012-08-15 16:04:56 -0700116 private View mEmptyView;
Andrew Sappersteinc2c9dc12012-07-02 18:17:32 -0700117 private ErrorListener mErrorListener;
Mindy Pereirae3c7b0b2012-07-26 16:28:34 -0700118 private DataSetObserver mFolderObserver;
Mindy Pereira70a70c92012-08-02 08:39:45 -0700119 private DataSetObserver mConversationListStatusObserver;
Vikram Aggarwald247dc92012-02-10 15:49:01 -0800120
Vikram Aggarwal4f1cc0b2012-08-02 15:16:30 -0700121 private ConversationSelectionSet mSelectedSet;
Vikram Aggarwal7c401b72012-08-13 16:43:47 -0700122 private final AccountObserver mAccountObserver = new AccountObserver() {
123 @Override
124 public void onChanged(Account newAccount) {
125 mAccount = newAccount;
126 setSwipeAction();
127 }
128 };
mindyp9365a822012-09-12 09:09:09 -0700129 private ConversationUpdater mUpdater;
Vikram Aggarwal81a4f082012-09-28 09:19:04 -0700130 /** Hash of the Conversation Cursor we last obtained from the controller. */
131 private int mConversationCursorHash;
Vikram Aggarwal4f1cc0b2012-08-02 15:16:30 -0700132
Vikram Aggarwal6c511582012-02-27 10:59:47 -0800133 /**
mindyp9365a822012-09-12 09:09:09 -0700134 * Constructor needs to be public to handle orientation changes and activity
135 * lifecycle events.
Vikram Aggarwal6c511582012-02-27 10:59:47 -0800136 */
Paul Westbrookefc9f122012-02-21 11:14:49 -0800137 public ConversationListFragment() {
Vikram Aggarwald247dc92012-02-10 15:49:01 -0800138 super();
Vikram Aggarwald247dc92012-02-10 15:49:01 -0800139 }
140
Mindy Pereira6d8e7fe2012-07-26 16:02:49 -0700141 // update the pager title strip as the Folder's conversation count changes
142 private class FolderObserver extends DataSetObserver {
143 @Override
144 public void onChanged() {
Vikram Aggarwal81a4f082012-09-28 09:19:04 -0700145 if (mActivity == null) {
146 return;
147 }
148 final FolderController controller = mActivity.getFolderController();
149 if (controller == null) {
150 return;
151 }
152 onFolderUpdated(controller.getFolder());
Mindy Pereira6d8e7fe2012-07-26 16:02:49 -0700153 }
154 }
155
Mindy Pereira70a70c92012-08-02 08:39:45 -0700156 private class ConversationListStatusObserver extends DataSetObserver {
157 @Override
158 public void onChanged() {
Mindy Pereira70a70c92012-08-02 08:39:45 -0700159 onConversationListStatusUpdated();
160 }
161 }
162
Vikram Aggarwal7d602882012-02-07 15:01:20 -0800163 /**
mindyp9365a822012-09-12 09:09:09 -0700164 * Creates a new instance of {@link ConversationListFragment}, initialized
165 * to display conversation list context.
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -0800166 */
167 public static ConversationListFragment newInstance(ConversationListContext viewContext) {
168 ConversationListFragment fragment = new ConversationListFragment();
169 Bundle args = new Bundle();
170 args.putBundle(CONVERSATION_LIST_KEY, viewContext.toBundle());
171 fragment.setArguments(args);
172 return fragment;
173 }
174
175 /**
mindyp9365a822012-09-12 09:09:09 -0700176 * Show the header if the current conversation list is showing search
177 * results.
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -0800178 */
Mindy Pereira967ede62012-03-22 09:29:09 -0700179 void configureSearchResultHeader() {
Mindy Pereira755fd6e2012-03-21 15:15:44 -0700180 if (mActivity == null) {
181 return;
182 }
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -0800183 // Only show the header if the context is for a search result
184 final Resources res = getResources();
Vikram Aggarwalae4ea992012-08-07 09:56:02 -0700185 final boolean showHeader = ConversationListContext.isSearchResult(mViewContext);
mindyp9365a822012-09-12 09:09:09 -0700186 // TODO(viki): This code contains intimate understanding of the view.
187 // Much of this logic
188 // needs to reside in a separate class that handles the text view in
189 // isolation. Then,
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -0800190 // that logic can be reused in other fragments.
191 if (showHeader) {
192 mSearchStatusTextView.setText(res.getString(R.string.search_results_searching_header));
193 // Initially reset the count
194 mSearchResultCountTextView.setText("");
195 }
196 mSearchStatusView.setVisibility(showHeader ? View.VISIBLE : View.GONE);
197 int marginTop = showHeader ? (int) res.getDimension(R.dimen.notification_view_height) : 0;
198 MarginLayoutParams layoutParams = (MarginLayoutParams) mListView.getLayoutParams();
199 layoutParams.topMargin = marginTop;
200 mListView.setLayoutParams(layoutParams);
201 }
202
Vikram Aggarwal80aeac52012-02-07 15:27:20 -0800203 /**
mindyp9365a822012-09-12 09:09:09 -0700204 * Show the header if the current conversation list is showing search
205 * results.
Mindy Pereira68f2e222012-03-07 10:36:54 -0800206 */
207 private void updateSearchResultHeader(int count) {
Mindy Pereira71a8f292012-07-26 16:30:48 -0700208 if (mActivity == null) {
209 return;
210 }
Mindy Pereira68f2e222012-03-07 10:36:54 -0800211 // Only show the header if the context is for a search result
212 final Resources res = getResources();
Vikram Aggarwalae4ea992012-08-07 09:56:02 -0700213 final boolean showHeader = ConversationListContext.isSearchResult(mViewContext);
Mindy Pereira68f2e222012-03-07 10:36:54 -0800214 if (showHeader) {
215 mSearchStatusTextView.setText(res.getString(R.string.search_results_header));
216 mSearchResultCountTextView
217 .setText(res.getString(R.string.search_results_loaded, count));
218 }
219 }
220
221 /**
Vikram Aggarwal80aeac52012-02-07 15:27:20 -0800222 * Initializes all internal state for a rendering.
223 */
224 private void initializeUiForFirstDisplay() {
mindyp9365a822012-09-12 09:09:09 -0700225 // TODO(mindyp): find some way to make the notification container more
226 // re-usable.
227 // TODO(viki): refactor according to comment in
228 // configureSearchResultHandler()
Vikram Aggarwal80aeac52012-02-07 15:27:20 -0800229 mSearchStatusView = mActivity.findViewById(R.id.search_status_view);
230 mSearchStatusTextView = (TextView) mActivity.findViewById(R.id.search_status_text_view);
mindyp9365a822012-09-12 09:09:09 -0700231 mSearchResultCountTextView = (TextView) mActivity
232 .findViewById(R.id.search_result_count_view);
Vikram Aggarwal80aeac52012-02-07 15:27:20 -0800233 }
234
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -0800235 @Override
236 public void onActivityCreated(Bundle savedInstanceState) {
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -0800237 super.onActivityCreated(savedInstanceState);
mindyp9365a822012-09-12 09:09:09 -0700238 // Strictly speaking, we get back an android.app.Activity from
239 // getActivity. However, the
240 // only activity creating a ConversationListContext is a MailActivity
241 // which is of type
242 // ControllableActivity, so this cast should be safe. If this cast
243 // fails, some other
244 // activity is creating ConversationListFragments. This activity must be
245 // of type
Vikram Aggarwal80aeac52012-02-07 15:27:20 -0800246 // ControllableActivity.
247 final Activity activity = getActivity();
mindyp9365a822012-09-12 09:09:09 -0700248 if (!(activity instanceof ControllableActivity)) {
249 LogUtils.e(LOG_TAG, "ConversationListFragment expects only a ControllableActivity to"
250 + "create it. Cannot proceed.");
Vikram Aggarwal80aeac52012-02-07 15:27:20 -0800251 }
252 mActivity = (ControllableActivity) activity;
mindyp9365a822012-09-12 09:09:09 -0700253 // Since we now have a controllable activity, load the account from it,
254 // and register for
Vikram Aggarwal7c401b72012-08-13 16:43:47 -0700255 // future account changes.
256 mAccount = mAccountObserver.initialize(mActivity.getAccountController());
Vikram Aggarwal7d602882012-02-07 15:01:20 -0800257 mCallbacks = mActivity.getListHandler();
Andrew Sappersteinc2c9dc12012-07-02 18:17:32 -0700258 mErrorListener = mActivity.getErrorListener();
Mindy Pereira278fd222012-07-26 15:23:10 -0700259 // Start off with the current state of the folder being viewed.
Mindy Pereira6681f6b2012-03-09 13:55:54 -0800260 mFooterView = (ConversationListFooterView) LayoutInflater.from(
261 mActivity.getActivityContext()).inflate(R.layout.conversation_list_footer_view,
262 null);
Paul Westbrook4969e0c2012-08-20 14:38:39 -0700263 mFooterView.setClickListener(mActivity);
Vikram Aggarwal81a4f082012-09-28 09:19:04 -0700264 final ConversationCursor conversationCursor = getConversationListCursor();
Mindy Pereira6d8e7fe2012-07-26 16:02:49 -0700265 mListAdapter = new AnimatedAdapter(mActivity.getApplicationContext(), -1,
Vikram Aggarwal81a4f082012-09-28 09:19:04 -0700266 conversationCursor, mActivity.getSelectedSet(), mActivity, mListView);
Mindy Pereira6681f6b2012-03-09 13:55:54 -0800267 mListAdapter.addFooter(mFooterView);
Mindy Pereira6f4a6af2012-02-29 11:48:52 -0800268 mListView.setAdapter(mListAdapter);
Vikram Aggarwal4f1cc0b2012-08-02 15:16:30 -0700269 mSelectedSet = mActivity.getSelectedSet();
270 mListView.setSelectionSet(mSelectedSet);
Mindy Pereiradc8963b2012-03-28 17:51:05 -0700271 mListAdapter.hideFooter();
Mindy Pereirae3c7b0b2012-07-26 16:28:34 -0700272 mFolderObserver = new FolderObserver();
Andy Huang7591d2f2012-07-26 16:48:06 -0700273 mActivity.getFolderController().registerFolderObserver(mFolderObserver);
Mindy Pereira70a70c92012-08-02 08:39:45 -0700274 mConversationListStatusObserver = new ConversationListStatusObserver();
mindyp9365a822012-09-12 09:09:09 -0700275 mUpdater = mActivity.getConversationUpdater();
276 mUpdater.registerConversationListObserver(mConversationListStatusObserver);
Vikram Aggarwalfa131a22012-02-02 13:56:22 -0800277 mTabletDevice = Utils.useTabletUI(mActivity.getApplicationContext());
Vikram Aggarwal80aeac52012-02-07 15:27:20 -0800278 initializeUiForFirstDisplay();
Mindy Pereiradc8963b2012-03-28 17:51:05 -0700279 configureSearchResultHeader();
mindyp9365a822012-09-12 09:09:09 -0700280 // The onViewModeChanged callback doesn't get called when the mode
281 // object is created, so
Vikram Aggarwalfa131a22012-02-02 13:56:22 -0800282 // force setting the mode manually this time around.
Mindy Pereiraf96ec322012-03-02 14:06:33 -0800283 onViewModeChanged(mActivity.getViewMode().getMode());
Mindy Pereirab5901be2012-08-09 17:21:53 -0700284 mActivity.getViewMode().addListener(this);
Paul Westbrook0e3fd9d2012-04-20 02:02:23 -0700285
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -0800286 if (mActivity.isFinishing()) {
287 // Activity is finishing, just bail.
288 return;
289 }
Vikram Aggarwal81a4f082012-09-28 09:19:04 -0700290 mConversationCursorHash = (conversationCursor == null) ? 0 : conversationCursor.hashCode();
291 // Belt and suspenders here; make sure we do any necessary sync of the
292 // ConversationCursor
293 if (conversationCursor != null && conversationCursor.isRefreshReady()) {
294 conversationCursor.sync();
295 }
296
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -0800297 // Show list and start loading list.
298 showList();
Mindy Pereira4765c5c2012-07-19 11:58:22 -0700299 ToastBarOperation pendingOp = mActivity.getPendingToastOperation();
300 if (pendingOp != null) {
301 // Clear the pending operation
302 mActivity.setPendingToastOperation(null);
303 mActivity.onUndoAvailable(pendingOp);
304 }
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -0800305 }
306
Mindy Pereira967ede62012-03-22 09:29:09 -0700307 public AnimatedAdapter getAnimatedAdapter() {
308 return mListAdapter;
309 }
310
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -0800311 @Override
Vikram Aggarwald247dc92012-02-10 15:49:01 -0800312 public void onCreate(Bundle savedState) {
Vikram Aggarwald247dc92012-02-10 15:49:01 -0800313 super.onCreate(savedState);
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -0800314
315 // Initialize fragment constants from resources
Vikram Aggarwal6c511582012-02-27 10:59:47 -0800316 final Resources res = getResources();
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -0800317 TIMESTAMP_UPDATE_INTERVAL = res.getInteger(R.integer.timestamp_update_interval);
mindyp9365a822012-09-12 09:09:09 -0700318 mUpdateTimestampsRunnable = new Runnable() {
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -0800319 @Override
320 public void run() {
321 mListView.invalidateViews();
322 mHandler.postDelayed(mUpdateTimestampsRunnable, TIMESTAMP_UPDATE_INTERVAL);
323 }
324 };
325
Vikram Aggarwal80aeac52012-02-07 15:27:20 -0800326 // Get the context from the arguments
Vikram Aggarwal6c511582012-02-27 10:59:47 -0800327 final Bundle args = getArguments();
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -0800328 mViewContext = ConversationListContext.forBundle(args.getBundle(CONVERSATION_LIST_KEY));
Mindy Pereira3982e232012-02-29 15:00:34 -0800329 mAccount = mViewContext.account;
Paul Westbrook80ecd892012-08-16 13:37:39 -0700330
Vikram Aggarwal9a49c9b2012-08-31 10:49:33 -0700331 setRetainInstance(false);
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -0800332 }
333
334 @Override
Vikram Aggarwal9a49c9b2012-08-31 10:49:33 -0700335 public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedState) {
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -0800336 View rootView = inflater.inflate(R.layout.conversation_list, null);
Mindy Pereira6cc95532012-08-15 16:04:56 -0700337 mEmptyView = rootView.findViewById(R.id.empty_view);
Mindy Pereiraf6a6b502012-03-15 15:24:26 -0700338 mListView = (SwipeableListView) rootView.findViewById(android.R.id.list);
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -0800339 mListView.setHeaderDividersEnabled(false);
Vikram Aggarwal17f373e2012-09-17 15:49:32 -0700340 // Choice mode here represents the current conversation only. CAB mode does not rely on the
341 // platform: it is a local variable within conversation items.
342 mListView.setChoiceMode(ListView.CHOICE_MODE_SINGLE);
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -0800343 mListView.setOnItemLongClickListener(this);
Mindy Pereira4e812f42012-07-30 16:52:49 -0700344 mListView.enableSwipe(mAccount.supportsCapability(AccountCapabilities.UNDO));
mindyp9365a822012-09-12 09:09:09 -0700345 mListView.setSwipedListener(this);
Marc Blank2af0cf32012-03-01 19:24:13 -0800346
Vikram Aggarwal9a49c9b2012-08-31 10:49:33 -0700347 // Restore the list state
348 if (savedState != null && savedState.containsKey(LIST_STATE_KEY)) {
349 mListView.onRestoreInstanceState(savedState.getParcelable(LIST_STATE_KEY));
350 // TODO: find a better way to unset the selected item when restoring
351 mListView.clearChoices();
352 }
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -0800353 return rootView;
354 }
355
356 @Override
Andy Huang7517e3b2012-08-20 12:30:08 -0700357 public void onDestroy() {
Andy Huang7517e3b2012-08-20 12:30:08 -0700358 super.onDestroy();
359 }
360
361 @Override
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -0800362 public void onDestroyView() {
Andy Huang7517e3b2012-08-20 12:30:08 -0700363
Vikram Aggarwal7c401b72012-08-13 16:43:47 -0700364 // Clear the list's adapter
365 mListAdapter.destroy();
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -0800366 mListView.setAdapter(null);
367
Vikram Aggarwald7a12cd2012-02-03 09:36:20 -0800368 mActivity.unsetViewModeListener(this);
Mindy Pereira71a8f292012-07-26 16:30:48 -0700369 if (mFolderObserver != null) {
Andy Huang7591d2f2012-07-26 16:48:06 -0700370 mActivity.getFolderController().unregisterFolderObserver(mFolderObserver);
Mindy Pereira71a8f292012-07-26 16:30:48 -0700371 mFolderObserver = null;
372 }
Mindy Pereira70a70c92012-08-02 08:39:45 -0700373 if (mConversationListStatusObserver != null) {
mindyp9365a822012-09-12 09:09:09 -0700374 mUpdater.unregisterConversationListObserver(mConversationListStatusObserver);
Mindy Pereira70a70c92012-08-02 08:39:45 -0700375 mConversationListStatusObserver = null;
376 }
Vikram Aggarwal7c401b72012-08-13 16:43:47 -0700377 mAccountObserver.unregisterAndDestroy();
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -0800378 super.onDestroyView();
379 }
380
Vikram Aggarwal9730ea02012-08-02 12:46:19 -0700381 /**
mindyp9365a822012-09-12 09:09:09 -0700382 * There are three binary variables, which determine what we do with a
383 * message. checkbEnabled: Whether check boxes are enabled or not (forced
384 * true on tablet) cabModeOn: Whether CAB mode is currently on or not.
385 * pressType: long or short tap (There is a third possibility: phone or
386 * tablet, but they have <em>identical</em> behavior) The matrix of
387 * possibilities is:
388 * <p>
389 * Long tap: Always toggle selection of conversation. If CAB mode is not
390 * started, then start it.
Vikram Aggarwal9730ea02012-08-02 12:46:19 -0700391 * <pre>
392 * | Checkboxes | No Checkboxes
393 * ----------+------------+---------------
394 * CAB mode | Select | Select
395 * List mode | Select | Select
396 *
Vikram Aggarwal9730ea02012-08-02 12:46:19 -0700397 * </pre>
mindyp9365a822012-09-12 09:09:09 -0700398 *
Vikram Aggarwal9730ea02012-08-02 12:46:19 -0700399 * Reference: http://b/issue?id=6392199
400 * <p>
401 * {@inheritDoc}
402 */
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -0800403 @Override
404 public boolean onItemLongClick(AdapterView<?> parent, View view, int position, long id) {
Mindy Pereira1e231a62012-03-23 12:42:15 -0700405 // Ignore anything that is not a conversation item. Could be a footer.
406 if (!(view instanceof ConversationItemView)) {
407 return true;
408 }
mindypbad1a932012-08-23 10:25:22 -0700409 ((ConversationItemView) view).toggleCheckMarkOrBeginDrag();
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -0800410 return true;
411 }
412
Vikram Aggarwal4f1cc0b2012-08-02 15:16:30 -0700413 /**
mindyp9365a822012-09-12 09:09:09 -0700414 * See the comment for
415 * {@link #onItemLongClick(AdapterView, View, int, long)}.
416 * <p>
417 * Short tap behavior:
418 *
Vikram Aggarwal4f1cc0b2012-08-02 15:16:30 -0700419 * <pre>
420 * | Checkboxes | No Checkboxes
421 * ----------+------------+---------------
422 * CAB mode | Peek | Select
423 * List mode | Peek | Peek
424 * </pre>
mindyp9365a822012-09-12 09:09:09 -0700425 *
Vikram Aggarwal4f1cc0b2012-08-02 15:16:30 -0700426 * Reference: http://b/issue?id=6392199
427 * <p>
428 * {@inheritDoc}
429 */
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -0800430 @Override
Vikram Aggarwal54452ae2012-03-13 15:29:00 -0700431 public void onListItemClick(ListView l, View view, int position, long id) {
Mindy Pereira1e231a62012-03-23 12:42:15 -0700432 // Ignore anything that is not a conversation item. Could be a footer.
mindyp8694fe92012-09-25 11:07:16 -0700433 // If we are using a keyboard, the highlighted item is the parent;
434 // otherwise, this is a direct call from the ConverationItemView
mindypc03b05a2012-09-28 14:23:48 -0700435 if (!(view instanceof ToggleableItem)) {
Mindy Pereira1e231a62012-03-23 12:42:15 -0700436 return;
437 }
Paul Westbrook7ed53772013-01-23 10:19:55 -0800438 if (mAccount.settings.hideCheckboxes && !mSelectedSet.isEmpty()) {
mindypc03b05a2012-09-28 14:23:48 -0700439 ToggleableItem v = (ToggleableItem) view;
440 v.toggleCheckMarkOrBeginDrag();
Vikram Aggarwal4f1cc0b2012-08-02 15:16:30 -0700441 } else {
442 viewConversation(position);
443 }
mindyp9365a822012-09-12 09:09:09 -0700444 // When a new list item is clicked, commit any existing leave behind
mindyp8694fe92012-09-25 11:07:16 -0700445 // items. Wait until we have opened the desired conversation to cause
446 // any position changes.
mindyp9365a822012-09-12 09:09:09 -0700447 commitDestructiveActions(Utils.useTabletUI(mActivity.getActivityContext()));
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -0800448 }
449
450 @Override
451 public void onPause() {
452 super.onPause();
453 }
454
455 @Override
456 public void onSaveInstanceState(Bundle outState) {
457 super.onSaveInstanceState(outState);
458 if (mListView != null) {
459 outState.putParcelable(LIST_STATE_KEY, mListView.onSaveInstanceState());
460 }
461 }
462
463 @Override
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -0800464 public void onStart() {
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -0800465 super.onStart();
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -0800466 mHandler.postDelayed(mUpdateTimestampsRunnable, TIMESTAMP_UPDATE_INTERVAL);
467 }
468
469 @Override
470 public void onStop() {
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -0800471 super.onStop();
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -0800472 mHandler.removeCallbacks(mUpdateTimestampsRunnable);
473 }
474
Vikram Aggarwal80aeac52012-02-07 15:27:20 -0800475 @Override
Vikram Aggarwalfa131a22012-02-02 13:56:22 -0800476 public void onViewModeChanged(int newMode) {
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -0800477 // Change the divider based on view mode.
Vikram Aggarwalfa131a22012-02-02 13:56:22 -0800478 if (mTabletDevice) {
479 if (newMode == ViewMode.CONVERSATION) {
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -0800480 mListView.setBackgroundResource(R.drawable.panel_conversation_leftstroke);
Mindy Pereirade1c8592012-08-10 14:54:31 -0700481 } else if (newMode == ViewMode.CONVERSATION_LIST
482 || newMode == ViewMode.SEARCH_RESULTS_LIST) {
Mindy Pereirab5901be2012-08-09 17:21:53 -0700483 // There are no selected conversations when in conversation
484 // list mode.
Mindy Pereira62c5af82012-03-05 10:08:38 -0800485 mListView.clearChoices();
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -0800486 mListView.setBackgroundDrawable(null);
487 }
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -0800488 } else {
489 mListView.setBackgroundDrawable(null);
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -0800490 }
Mindy Pereirab5901be2012-08-09 17:21:53 -0700491 if (mFooterView != null) {
492 mFooterView.onViewModeChanged(newMode);
493 }
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -0800494 }
Mindy Pereirab5901be2012-08-09 17:21:53 -0700495
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -0800496 /**
mindyp9365a822012-09-12 09:09:09 -0700497 * Handles a request to show a new conversation list, either from a search
498 * query or for viewing a folder. This will initiate a data load, and hence
499 * must be called on the UI thread.
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -0800500 */
501 private void showList() {
Vikram Aggarwal80aeac52012-02-07 15:27:20 -0800502 mListView.setEmptyView(null);
Andy Huang7591d2f2012-07-26 16:48:06 -0700503 onFolderUpdated(mActivity.getFolderController().getFolder());
mindypca8ca2d2012-09-11 17:38:34 -0700504 onConversationListStatusUpdated();
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -0800505 }
506
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -0800507 /**
508 * View the message at the given position.
509 * @param position
510 */
Mindy Pereirafbe40192012-03-20 10:40:45 -0700511 protected void viewConversation(int position) {
Vikram Aggarwalc7694222012-04-23 13:37:01 -0700512 LogUtils.d(LOG_TAG, "ConversationListFragment.viewConversation(%d)", position);
Vikram Aggarwal3d7ca9d2012-05-11 14:40:36 -0700513 setSelected(position);
514 final ConversationCursor cursor = getConversationListCursor();
515 if (cursor != null && cursor.moveToPosition(position)) {
516 final Conversation conv = new Conversation(cursor);
517 conv.position = position;
Andy Huang1ee96b22012-08-24 20:19:53 -0700518 mCallbacks.onConversationSelected(conv, false /* inLoaderCallbacks */);
Vikram Aggarwal3d7ca9d2012-05-11 14:40:36 -0700519 }
520 }
521
mindype21f8862012-10-01 09:32:03 -0700522
523 public void setSelected(int position, boolean different) {
524 if (different) {
525 mListView.smoothScrollToPosition(position);
526 }
527 mListView.setItemChecked(position, true);
528 }
529
Vikram Aggarwal3d7ca9d2012-05-11 14:40:36 -0700530 /**
mindyp9365a822012-09-12 09:09:09 -0700531 * Sets the selected position (the highlighted conversation) to the position
532 * provided here.
Vikram Aggarwal3d7ca9d2012-05-11 14:40:36 -0700533 * @param position
534 */
535 protected final void setSelected(int position) {
mindype21f8862012-10-01 09:32:03 -0700536 setSelected(position, true);
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -0800537 }
Vikram Aggarwald247dc92012-02-10 15:49:01 -0800538
Mindy Pereira967ede62012-03-22 09:29:09 -0700539 private ConversationCursor getConversationListCursor() {
540 return mCallbacks != null ? mCallbacks.getConversationListCursor() : null;
Mindy Pereirafe06bea2012-02-16 08:15:14 -0800541 }
542
543 /**
mindyp9365a822012-09-12 09:09:09 -0700544 * Request a refresh of the list. No sync is carried out and none is
545 * promised.
Vikram Aggarwal54452ae2012-03-13 15:29:00 -0700546 */
547 public void requestListRefresh() {
548 mListAdapter.notifyDataSetChanged();
549 }
550
Vikram Aggarwal75daee52012-04-30 11:13:09 -0700551 /**
Vikram Aggarwal7f602f72012-04-30 16:04:06 -0700552 * Change the UI to delete the conversations provided and then call the
mindyp9365a822012-09-12 09:09:09 -0700553 * {@link DestructiveAction} provided here <b>after</b> the UI has been
554 * updated.
Vikram Aggarwal7f602f72012-04-30 16:04:06 -0700555 * @param conversations
Vikram Aggarwal75daee52012-04-30 11:13:09 -0700556 * @param action
557 */
Vikram Aggarwala8e43182012-09-13 12:55:10 -0700558 public void requestDelete(int actionId, final Collection<Conversation> conversations,
Vikram Aggarwal669947b2013-01-10 17:05:56 -0800559 final DestructiveAction action) {
Mindy Pereiraacf60392012-04-06 09:11:00 -0700560 for (Conversation conv : conversations) {
561 conv.localDeleteOnUpdate = true;
562 }
Vikram Aggarwala8e43182012-09-13 12:55:10 -0700563 final ListItemsRemovedListener listener = new ListItemsRemovedListener() {
mindyp9365a822012-09-12 09:09:09 -0700564 @Override
565 public void onListItemsRemoved() {
566 action.performAction();
567 }
Vikram Aggarwala8e43182012-09-13 12:55:10 -0700568 };
569 final SwipeableListView listView = (SwipeableListView) getListView();
570 if (listView.getSwipeAction() == actionId) {
mindyp4e77a7d2012-11-27 09:03:32 -0800571 listView.destroyItems(conversations, listener);
Vikram Aggarwala8e43182012-09-13 12:55:10 -0700572 return;
573 }
574 // Delete the local delete items (all for now) and when done,
575 // update...
576 mListAdapter.delete(conversations, listener);
Mindy Pereiraacf60392012-04-06 09:11:00 -0700577 }
Mindy Pereiraa3911aa2012-03-09 13:26:26 -0800578
Mindy Pereira11dd5ef2012-03-10 15:10:18 -0800579 public void onFolderUpdated(Folder folder) {
Mindy Pereiradc8963b2012-03-28 17:51:05 -0700580 mFolder = folder;
Mindy Pereira06642fa2012-07-12 16:23:27 -0700581 setSwipeAction();
Mindy Pereira82cea482012-03-27 16:45:00 -0700582 if (mFolder == null) {
583 return;
584 }
Mindy Pereira4584a0d2012-03-13 14:42:14 -0700585 mListAdapter.setFolder(mFolder);
Mindy Pereira70a70c92012-08-02 08:39:45 -0700586 mFooterView.setFolder(mFolder);
Vikram Aggarwal41b9e8f2012-09-25 10:15:04 -0700587 if (!mFolder.wasSyncSuccessful()) {
Mindy Pereira70a70c92012-08-02 08:39:45 -0700588 mErrorListener.onError(mFolder, false);
589 }
Paul Westbrookc01f5d92012-09-25 17:22:25 -0700590
591 // Notify of changes to the Folder.
592 onFolderStatusUpdated();
593
Mindy Pereira12fe37a2012-08-15 10:02:57 -0700594 // Blow away conversation items cache.
595 ConversationItemViewModel.onFolderUpdated(mFolder);
Mindy Pereira70a70c92012-08-02 08:39:45 -0700596 }
597
Vikram Aggarwal81a4f082012-09-28 09:19:04 -0700598 /**
599 * Updates the footer visibility and updates the conversation cursor
600 */
Mindy Pereira70a70c92012-08-02 08:39:45 -0700601 public void onConversationListStatusUpdated() {
Paul Westbrook9a70e912012-08-17 15:53:20 -0700602 final ConversationCursor cursor = getConversationListCursor();
603 final boolean showFooter = cursor != null && mFooterView.updateStatus(cursor);
Paul Westbrookc01f5d92012-09-25 17:22:25 -0700604 // Update the folder status, in case the cursor could affect it.
605 onFolderStatusUpdated();
606 mListAdapter.setFooterVisibility(showFooter);
607
608 // Also change the cursor here.
609 onCursorUpdated();
610 }
611
612 private void onFolderStatusUpdated() {
613 final ConversationCursor cursor = getConversationListCursor();
Paul Westbrook9a70e912012-08-17 15:53:20 -0700614 Bundle extras = cursor != null ? cursor.getExtras() : Bundle.EMPTY;
Mindy Pereira70a70c92012-08-02 08:39:45 -0700615 int error = extras.containsKey(UIProvider.CursorExtraKeys.EXTRA_ERROR) ?
616 extras.getInt(UIProvider.CursorExtraKeys.EXTRA_ERROR)
617 : UIProvider.LastSyncResult.SUCCESS;
Mindy Pereira6cc95532012-08-15 16:04:56 -0700618 int status = extras.getInt(UIProvider.CursorExtraKeys.EXTRA_STATUS);
Paul Westbrookc01f5d92012-09-25 17:22:25 -0700619 // We want to update the UI with this informaion if either we are loaded or complete, or
620 // we have a folder with a non-0 count.
621 final int folderCount = mFolder != null ? mFolder.totalCount : 0;
Mindy Pereira6cc95532012-08-15 16:04:56 -0700622 if (error == UIProvider.LastSyncResult.SUCCESS
623 && (status == UIProvider.CursorStatus.LOADED
Paul Westbrookc01f5d92012-09-25 17:22:25 -0700624 || status == UIProvider.CursorStatus.COMPLETE) || folderCount > 0) {
625 updateSearchResultHeader(folderCount);
626 if (folderCount == 0) {
Mindy Pereira6cc95532012-08-15 16:04:56 -0700627 mListView.setEmptyView(mEmptyView);
628 }
Mindy Pereira70a70c92012-08-02 08:39:45 -0700629 }
Mindy Pereiraa3911aa2012-03-09 13:26:26 -0800630 }
Mindy Pereiraf6a6b502012-03-15 15:24:26 -0700631
Mindy Pereira06642fa2012-07-12 16:23:27 -0700632 private void setSwipeAction() {
Vikram Aggarwal7c401b72012-08-13 16:43:47 -0700633 int swipeSetting = Settings.getSwipeSetting(mAccount.settings);
Mindy Pereirae58222b2012-07-25 14:33:18 -0700634 if (swipeSetting == Swipe.DISABLED
Mindy Pereirab8073372012-08-09 14:00:10 -0700635 || !mAccount.supportsCapability(AccountCapabilities.UNDO)
636 || (mFolder != null && mFolder.isTrash())) {
Mindy Pereirae58222b2012-07-25 14:33:18 -0700637 mListView.enableSwipe(false);
638 } else {
639 int action;
mindyp77c3e1b2012-09-11 14:05:02 -0700640 if (ConversationListContext.isSearchResult(mViewContext)
641 || (mFolder != null && mFolder.type == FolderType.SPAM)) {
Mindy Pereirae58222b2012-07-25 14:33:18 -0700642 action = R.id.delete;
Mindy Pereira4e812f42012-07-30 16:52:49 -0700643 } else if (mFolder == null) {
Mindy Pereira01f30502012-08-14 10:30:51 -0700644 action = R.id.remove_folder;
Mindy Pereira4e812f42012-07-30 16:52:49 -0700645 } else {
646 // We have enough information to respect user settings.
647 switch (swipeSetting) {
648 case Swipe.ARCHIVE:
649 if (mAccount.supportsCapability(AccountCapabilities.ARCHIVE)) {
650 if (mFolder.supportsCapability(FolderCapabilities.ARCHIVE)) {
651 action = R.id.archive;
652 break;
653 } else if (mFolder.supportsCapability
654 (FolderCapabilities.CAN_ACCEPT_MOVED_MESSAGES)) {
Mindy Pereira01f30502012-08-14 10:30:51 -0700655 action = R.id.remove_folder;
Mindy Pereira4e812f42012-07-30 16:52:49 -0700656 break;
657 }
658 }
659 case Swipe.DELETE:
660 default:
661 action = R.id.delete;
662 break;
663 }
Mindy Pereirae58222b2012-07-25 14:33:18 -0700664 }
665 mListView.setSwipeAction(action);
666 }
Mindy Pereira06642fa2012-07-12 16:23:27 -0700667 mListView.setCurrentFolder(mFolder);
668 }
669
Vikram Aggarwal17f373e2012-09-17 15:49:32 -0700670 /**
671 * Changes the conversation cursor in the list and sets selected position if none is set.
672 */
673 private void onCursorUpdated() {
674 if (mCallbacks == null || mListAdapter == null) {
675 return;
676 }
Vikram Aggarwal81a4f082012-09-28 09:19:04 -0700677 // Check against the previous cursor here and see if they are the same. If they are, then
678 // do a notifyDataSetChanged.
679 final ConversationCursor newCursor = mCallbacks.getConversationListCursor();
680 mListAdapter.swapCursor(newCursor);
681 // When the conversation cursor is *updated*, we get back the same instance. In that
682 // situation, CursorAdapter.swapCursor() silently returns, without forcing a
683 // notifyDataSetChanged(). So let's force a call to notifyDataSetChanged, since an updated
684 // cursor means that the dataset has changed.
685 final int newCursorHash = (newCursor == null) ? 0 : newCursor.hashCode();
686 if (mConversationCursorHash == newCursorHash && mConversationCursorHash != 0) {
687 mListAdapter.notifyDataSetChanged();
688 }
689 mConversationCursorHash = newCursorHash;
Vikram Aggarwal17f373e2012-09-17 15:49:32 -0700690 // If a current conversation is available, and none is selected in the list, then ask
691 // the list to select the current conversation.
692 final Conversation conv = mCallbacks.getCurrentConversation();
693 if (conv == null) {
694 return;
695 }
696 if (mListView.getCheckedItemPosition() == -1) {
Vikram Aggarwal17f373e2012-09-17 15:49:32 -0700697 setSelected(conv.position);
Mindy Pereira967ede62012-03-22 09:29:09 -0700698 }
Mindy Pereira21ab4902012-03-19 18:48:03 -0700699 }
Mindy Pereira1e2573b2012-04-17 14:34:36 -0700700
mindypc6adce32012-08-22 18:46:42 -0700701 public void commitDestructiveActions(boolean animate) {
Mindy Pereira8937bf12012-07-23 14:05:02 -0700702 if (mListView != null) {
mindypc6adce32012-08-22 18:46:42 -0700703 mListView.commitDestructiveActions(animate);
Vikram Aggarwal7d816002012-04-17 17:06:41 -0700704
705 }
706 }
707
mindyp9365a822012-09-12 09:09:09 -0700708 @Override
709 public void onListItemSwiped(Collection<Conversation> conversations) {
710 mUpdater.showNextConversation(conversations);
711 }
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -0800712}