blob: 81e417829c68a610d82871d19ce65b9710f1737d [file] [log] [blame]
Vikram Aggarwal4a5c5302012-01-12 15:07:13 -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 Aggarwal1ddcf0f2012-01-13 11:45:02 -080020import android.app.ActionBar;
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -080021import android.app.ActionBar.LayoutParams;
Vikram Aggarwala55b36c2012-01-13 11:45:02 -080022import android.app.Activity;
Vikram Aggarwal54452ae2012-03-13 15:29:00 -070023import android.app.AlertDialog;
Vikram Aggarwala55b36c2012-01-13 11:45:02 -080024import android.app.Dialog;
Andrew Sapperstein00179f12012-08-09 15:15:40 -070025import android.app.DialogFragment;
Vikram Aggarwal6902dcf2012-04-11 08:57:42 -070026import android.app.Fragment;
Paul Westbrook2d50bcd2012-04-10 11:53:47 -070027import android.app.FragmentManager;
Andy Huangf9a73482012-03-13 15:54:02 -070028import android.app.LoaderManager;
Vikram Aggarwale620a7a2012-03-28 13:16:14 -070029import android.app.SearchManager;
Andy Huang839ada22012-07-20 15:48:40 -070030import android.content.ContentProviderOperation;
Vikram Aggarwal80aeac52012-02-07 15:27:20 -080031import android.content.ContentResolver;
Mindy Pereira6c2663d2012-07-20 15:37:29 -070032import android.content.ContentValues;
Vikram Aggarwala55b36c2012-01-13 11:45:02 -080033import android.content.Context;
Vikram Aggarwal7dedb952012-02-16 16:10:23 -080034import android.content.CursorLoader;
Vikram Aggarwal54452ae2012-03-13 15:29:00 -070035import android.content.DialogInterface;
Vikram Aggarwala55b36c2012-01-13 11:45:02 -080036import android.content.Intent;
Vikram Aggarwal7dedb952012-02-16 16:10:23 -080037import android.content.Loader;
Vikram Aggarwal80aeac52012-02-07 15:27:20 -080038import android.database.Cursor;
Andy Huang632721e2012-04-11 16:57:26 -070039import android.database.DataSetObservable;
40import android.database.DataSetObserver;
Paul Westbrook23b74b92012-02-29 11:36:12 -080041import android.net.Uri;
Vikram Aggarwal27d89ad2012-06-12 13:38:40 -070042import android.os.AsyncTask;
Vikram Aggarwala55b36c2012-01-13 11:45:02 -080043import android.os.Bundle;
Mindy Pereira21ab4902012-03-19 18:48:03 -070044import android.os.Handler;
Vikram Aggarwale620a7a2012-03-28 13:16:14 -070045import android.provider.SearchRecentSuggestions;
Vikram Aggarwal649b9ea2012-08-27 12:15:20 -070046import android.text.TextUtils;
Mindy Pereiraacf60392012-04-06 09:11:00 -070047import android.view.DragEvent;
Vikram Aggarwala55b36c2012-01-13 11:45:02 -080048import android.view.KeyEvent;
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -080049import android.view.LayoutInflater;
Vikram Aggarwala55b36c2012-01-13 11:45:02 -080050import android.view.Menu;
Mindy Pereira28d5f722012-02-15 12:32:40 -080051import android.view.MenuInflater;
Vikram Aggarwala55b36c2012-01-13 11:45:02 -080052import android.view.MenuItem;
53import android.view.MotionEvent;
Mindy Pereirad33674992012-06-25 16:26:30 -070054import android.view.View;
Vikram Aggarwala8e43182012-09-13 12:55:10 -070055import android.widget.ListView;
Mindy Pereirafd0c2972012-03-27 13:50:39 -070056import android.widget.Toast;
Vikram Aggarwala55b36c2012-01-13 11:45:02 -080057
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -080058import com.android.mail.ConversationListContext;
Andy Huangf9a73482012-03-13 15:54:02 -070059import com.android.mail.R;
Mindy Pereira967ede62012-03-22 09:29:09 -070060import com.android.mail.browse.ConversationCursor;
Vikram Aggarwala8e43182012-09-13 12:55:10 -070061import com.android.mail.browse.ConversationItemView;
Paul Westbrookbf232c32012-04-18 03:17:41 -070062import com.android.mail.browse.ConversationPagerController;
Vikram Aggarwalcc754b12012-08-30 14:04:21 -070063import com.android.mail.browse.ConversationCursor.ConversationOperation;
Andy Huang839ada22012-07-20 15:48:40 -070064import com.android.mail.browse.MessageCursor.ConversationMessage;
Marc Blank7c9f6ac2012-04-02 13:27:19 -070065import com.android.mail.browse.SelectedConversationsActionMenu;
Andy Huang991f4532012-08-14 13:32:55 -070066import com.android.mail.browse.SyncErrorDialogFragment;
Mindy Pereira9b875682012-02-15 18:10:54 -080067import com.android.mail.compose.ComposeActivity;
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -080068import com.android.mail.providers.Account;
Mindy Pereira9b875682012-02-15 18:10:54 -080069import com.android.mail.providers.Conversation;
Andy Huang839ada22012-07-20 15:48:40 -070070import com.android.mail.providers.ConversationInfo;
Vikram Aggarwal4a5c5302012-01-12 15:07:13 -080071import com.android.mail.providers.Folder;
Vikram Aggarwal7a5d95a2012-07-27 16:24:54 -070072import com.android.mail.providers.FolderWatcher;
Paul Westbrookc2074c42012-03-22 15:26:58 -070073import com.android.mail.providers.MailAppProvider;
Mindy Pereiradac00542012-03-01 10:50:33 -080074import com.android.mail.providers.Settings;
Vikram Aggarwale620a7a2012-03-28 13:16:14 -070075import com.android.mail.providers.SuggestionsProvider;
Vikram Aggarwal80aeac52012-02-07 15:27:20 -080076import com.android.mail.providers.UIProvider;
Mindy Pereira3cb28b52012-05-24 15:26:39 -070077import com.android.mail.providers.UIProvider.AccountCapabilities;
Paul Westbrook2388c5d2012-03-25 12:29:11 -070078import com.android.mail.providers.UIProvider.AccountCursorExtraKeys;
Mindy Pereirac9d59182012-03-22 16:06:46 -070079import com.android.mail.providers.UIProvider.ConversationColumns;
Vikram Aggarwal54452ae2012-03-13 15:29:00 -070080import com.android.mail.providers.UIProvider.FolderCapabilities;
Andrew Sappersteinc2c9dc12012-07-02 18:17:32 -070081import com.android.mail.ui.ActionableToastBar.ActionClickedListener;
Andy Huang839ada22012-07-20 15:48:40 -070082import com.android.mail.utils.ContentProviderTask;
Paul Westbrookb334c902012-06-25 11:42:46 -070083import com.android.mail.utils.LogTag;
Mindy Pereira5cb0c3e2012-02-22 15:22:47 -080084import com.android.mail.utils.LogUtils;
Vikram Aggarwalfa131a22012-02-02 13:56:22 -080085import com.android.mail.utils.Utils;
Paul Westbrookca08fc12012-07-31 12:01:15 -070086import com.google.common.base.Objects;
Paul Westbrook77eee622012-07-10 13:41:57 -070087import com.google.common.collect.ImmutableList;
Mindy Pereiraacf60392012-04-06 09:11:00 -070088import com.google.common.collect.Lists;
Marc Blank167faa82012-03-21 13:11:53 -070089import com.google.common.collect.Sets;
Vikram Aggarwal4a5c5302012-01-12 15:07:13 -080090
Marc Blank167faa82012-03-21 13:11:53 -070091import java.util.ArrayList;
Mindy Pereirafbe40192012-03-20 10:40:45 -070092import java.util.Collection;
Andy Huang839ada22012-07-20 15:48:40 -070093import java.util.Collections;
Mindy Pereira8db7e402012-07-13 10:32:47 -070094import java.util.HashMap;
Vikram Aggarwalcc754b12012-08-30 14:04:21 -070095import java.util.List;
Paul Westbrook23b74b92012-02-29 11:36:12 -080096import java.util.Set;
Marc Blankbf128eb2012-04-18 15:58:45 -070097import java.util.TimerTask;
Paul Westbrook23b74b92012-02-29 11:36:12 -080098
99
Vikram Aggarwal4a5c5302012-01-12 15:07:13 -0800100/**
Mindy Pereira161f50d2012-02-28 15:47:19 -0800101 * This is an abstract implementation of the Activity Controller. This class
102 * knows how to respond to menu items, state changes, layout changes, etc. It
103 * weaves together the views and listeners, dispatching actions to the
104 * respective underlying classes.
Vikram Aggarwala55b36c2012-01-13 11:45:02 -0800105 * <p>
Mindy Pereira161f50d2012-02-28 15:47:19 -0800106 * Even though this class is abstract, it should provide default implementations
107 * for most, if not all the methods in the ActivityController interface. This
108 * makes the task of the subclasses easier: OnePaneActivityController and
109 * TwoPaneActivityController can be concise when the common functionality is in
110 * AbstractActivityController.
111 * </p>
112 * <p>
113 * In the Gmail codebase, this was called BaseActivityController
114 * </p>
Vikram Aggarwal4a5c5302012-01-12 15:07:13 -0800115 */
Vikram Aggarwal531488e2012-05-29 16:36:52 -0700116public abstract class AbstractActivityController implements ActivityController {
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -0800117 // Keys for serialization of various information in Bundles.
Vikram Aggarwalcabd3f22012-04-19 10:14:41 -0700118 /** Tag for {@link #mAccount} */
Vikram Aggarwal6c511582012-02-27 10:59:47 -0800119 private static final String SAVED_ACCOUNT = "saved-account";
Vikram Aggarwalcabd3f22012-04-19 10:14:41 -0700120 /** Tag for {@link #mFolder} */
Mindy Pereira5e478d22012-03-26 18:04:58 -0700121 private static final String SAVED_FOLDER = "saved-folder";
Vikram Aggarwalcabd3f22012-04-19 10:14:41 -0700122 /** Tag for {@link #mCurrentConversation} */
Mindy Pereira26f23fc2012-03-27 10:26:04 -0700123 private static final String SAVED_CONVERSATION = "saved-conversation";
Vikram Aggarwalcabd3f22012-04-19 10:14:41 -0700124 /** Tag for {@link #mSelectedSet} */
125 private static final String SAVED_SELECTED_SET = "saved-selected-set";
Vikram Aggarwal649b9ea2012-08-27 12:15:20 -0700126 /** Tag for {@link ActionableToastBar#getOperation()} */
Mindy Pereirad33674992012-06-25 16:26:30 -0700127 private static final String SAVED_TOAST_BAR_OP = "saved-toast-bar-op";
Vikram Aggarwal649b9ea2012-08-27 12:15:20 -0700128 /** Tag for {@link #mFolderListFolder} */
129 private static final String SAVED_HIERARCHICAL_FOLDER = "saved-hierarchical-folder";
Vikram Aggarwalf3341402012-08-07 10:09:38 -0700130 /** Tag for {@link ConversationListContext#searchQuery} */
131 private static final String SAVED_QUERY = "saved-query";
Mindy Pereira5cb0c3e2012-02-22 15:22:47 -0800132
Vikram Aggarwal6902dcf2012-04-11 08:57:42 -0700133 /** Tag used when loading a wait fragment */
134 protected static final String TAG_WAIT = "wait-fragment";
135 /** Tag used when loading a conversation list fragment. */
Paul Westbrookbf232c32012-04-18 03:17:41 -0700136 public static final String TAG_CONVERSATION_LIST = "tag-conversation-list";
Vikram Aggarwal6902dcf2012-04-11 08:57:42 -0700137 /** Tag used when loading a folder list fragment. */
138 protected static final String TAG_FOLDER_LIST = "tag-folder-list";
139
Vikram Aggarwald7a12cd2012-02-03 09:36:20 -0800140 protected Account mAccount;
Mindy Pereira12a4d802012-06-22 09:24:43 -0700141 protected Folder mFolder;
Vikram Aggarwal69b5c302012-09-05 11:11:13 -0700142 /** True when {@link #mFolder} is first shown to the user. */
143 private boolean mFolderChanged = false;
Andy Huang6681e542012-06-14 14:36:45 -0700144 protected MailActionBarView mActionBarView;
Vikram Aggarwal7c401b72012-08-13 16:43:47 -0700145 protected final ControllableActivity mActivity;
Vikram Aggarwala55b36c2012-01-13 11:45:02 -0800146 protected final Context mContext;
Vikram Aggarwal6902dcf2012-04-11 08:57:42 -0700147 private final FragmentManager mFragmentManager;
Vikram Aggarwalec5cbf72012-03-08 15:10:35 -0800148 protected final RecentFolderList mRecentFolderList;
Vikram Aggarwal80aeac52012-02-07 15:27:20 -0800149 protected ConversationListContext mConvListContext;
Mindy Pereira9b875682012-02-15 18:10:54 -0800150 protected Conversation mCurrentConversation;
Vikram Aggarwald7a12cd2012-02-03 09:36:20 -0800151
Paul Westbrook6ead20d2012-03-19 14:48:14 -0700152 /** A {@link android.content.BroadcastReceiver} that suppresses new e-mail notifications. */
153 private SuppressNotificationReceiver mNewEmailReceiver = null;
154
Mindy Pereirafbe40192012-03-20 10:40:45 -0700155 protected Handler mHandler = new Handler();
Mindy Pereirafa995b42012-07-25 12:06:13 -0700156
Vikram Aggarwalfa131a22012-02-02 13:56:22 -0800157 /**
Mindy Pereira161f50d2012-02-28 15:47:19 -0800158 * The current mode of the application. All changes in mode are initiated by
159 * the activity controller. View mode changes are propagated to classes that
160 * attach themselves as listeners of view mode changes.
Vikram Aggarwalfa131a22012-02-02 13:56:22 -0800161 */
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -0800162 protected final ViewMode mViewMode;
Vikram Aggarwal80aeac52012-02-07 15:27:20 -0800163 protected ContentResolver mResolver;
Vikram Aggarwal7dedb952012-02-16 16:10:23 -0800164 protected boolean isLoaderInitialized = false;
Mindy Pereirab7b33e02012-02-21 15:32:19 -0800165 private AsyncRefreshTask mAsyncRefreshTask;
Mindy Pereira5cb0c3e2012-02-22 15:22:47 -0800166
Andy Huang4e0158f2012-08-07 21:06:01 -0700167 private boolean mDestroyed;
168
Andy Huang1ee96b22012-08-24 20:19:53 -0700169 /**
170 * Are we in a point in the Activity/Fragment lifecycle where it's safe to execute fragment
171 * transactions? (including back stack manipulation)
172 * <p>
173 * Per docs in {@link FragmentManager#beginTransaction()}, this flag starts out true, switches
174 * to false after {@link Activity#onSaveInstanceState}, and becomes true again in both onStart
175 * and onResume.
176 */
177 private boolean mSafeToModifyFragments = true;
178
Paul Westbrook23b74b92012-02-29 11:36:12 -0800179 private final Set<Uri> mCurrentAccountUris = Sets.newHashSet();
Mindy Pereira967ede62012-03-22 09:29:09 -0700180 protected ConversationCursor mConversationListCursor;
Andy Huang632721e2012-04-11 16:57:26 -0700181 private final DataSetObservable mConversationListObservable = new DataSetObservable() {
182 @Override
183 public void registerObserver(DataSetObserver observer) {
184 final int count = mObservers.size();
185 super.registerObserver(observer);
Vikram Aggarwal7c401b72012-08-13 16:43:47 -0700186 LogUtils.d(LOG_TAG, "IN AAC.register(List)Observer: %s before=%d after=%d", observer,
Andy Huang632721e2012-04-11 16:57:26 -0700187 count, mObservers.size());
188 }
189 @Override
190 public void unregisterObserver(DataSetObserver observer) {
191 final int count = mObservers.size();
192 super.unregisterObserver(observer);
Vikram Aggarwal7c401b72012-08-13 16:43:47 -0700193 LogUtils.d(LOG_TAG, "IN AAC.unregister(List)Observer: %s before=%d after=%d", observer,
Andy Huang632721e2012-04-11 16:57:26 -0700194 count, mObservers.size());
195 }
196 };
Marc Blankbf128eb2012-04-18 15:58:45 -0700197
Marc Blankbf128eb2012-04-18 15:58:45 -0700198 private RefreshTimerTask mConversationListRefreshTask;
Marc Blanke1d1b072012-04-13 17:29:16 -0700199
Vikram Aggarwal7c401b72012-08-13 16:43:47 -0700200 /** Listeners that are interested in changes to the current account. */
201 private final DataSetObservable mAccountObservers = new DataSetObservable() {
202 @Override
203 public void registerObserver(DataSetObserver observer) {
204 final int count = mObservers.size();
205 super.registerObserver(observer);
206 LogUtils.d(LOG_TAG, "IN AAC.register(Account)Observer: %s before=%d after=%d",
207 observer, count, mObservers.size());
208 }
209 @Override
210 public void unregisterObserver(DataSetObserver observer) {
211 final int count = mObservers.size();
212 super.unregisterObserver(observer);
213 LogUtils.d(LOG_TAG, "IN AAC.unregister(Account)Observer: %s before=%d after=%d",
214 observer, count, mObservers.size());
215 }
216 };
Vikram Aggarwal7d816002012-04-17 17:06:41 -0700217
Vikram Aggarwal58cad2e2012-08-28 16:18:23 -0700218 /** Listeners that are interested in changes to the recent folders. */
219 private final DataSetObservable mRecentFolderObservers = new DataSetObservable() {
220 @Override
221 public void registerObserver(DataSetObserver observer) {
222 final int count = mObservers.size();
223 super.registerObserver(observer);
224 LogUtils.d(LOG_TAG, "IN AAC.register(RecentFolder)Observer: %s before=%d after=%d",
225 observer, count, mObservers.size());
226 }
227 @Override
228 public void unregisterObserver(DataSetObserver observer) {
229 final int count = mObservers.size();
230 super.unregisterObserver(observer);
231 LogUtils.d(LOG_TAG, "IN AAC.unregister(RecentFolder)Observer: %s before=%d after=%d",
232 observer, count, mObservers.size());
233 }
234 };
235
Mindy Pereira967ede62012-03-22 09:29:09 -0700236 /**
237 * Selected conversations, if any.
238 */
Andy Huang4556a442012-03-30 16:42:05 -0700239 private final ConversationSelectionSet mSelectedSet = new ConversationSelectionSet();
Mindy Pereiradac00542012-03-01 10:50:33 -0800240
Paul Westbrookc7a070f2012-04-12 01:46:41 -0700241 private final int mFolderItemUpdateDelayMs;
242
Vikram Aggarwal135fd022012-04-11 14:44:15 -0700243 /** Keeps track of selected and unselected conversations */
Paul Westbrook937c94f2012-08-16 13:01:18 -0700244 final protected ConversationPositionTracker mTracker;
Vikram Aggarwalaf1ee0c2012-04-12 17:13:13 -0700245
Vikram Aggarwale128fc22012-04-04 12:33:34 -0700246 /**
247 * Action menu associated with the selected set.
248 */
249 SelectedConversationsActionMenu mCabActionMenu;
Andrew Sappersteinc2c9dc12012-07-02 18:17:32 -0700250 protected ActionableToastBar mToastBar;
Andy Huang632721e2012-04-11 16:57:26 -0700251 protected ConversationPagerController mPagerController;
Mindy Pereiraab486362012-03-21 18:18:53 -0700252
Andy Huangb1c34dc2012-04-17 16:36:19 -0700253 // this is split out from the general loader dispatcher because its loader doesn't return a
254 // basic Cursor
255 private final ConversationListLoaderCallbacks mListCursorCallbacks =
256 new ConversationListLoaderCallbacks();
257
Andy Huang090db1e2012-07-25 13:25:28 -0700258 private final DataSetObservable mFolderObservable = new DataSetObservable();
259
Paul Westbrookb334c902012-06-25 11:42:46 -0700260 protected static final String LOG_TAG = LogTag.getLogTag();
Vikram Aggarwalfa4b47e2012-03-09 13:02:46 -0800261 /** Constants used to differentiate between the types of loaders. */
262 private static final int LOADER_ACCOUNT_CURSOR = 0;
Vikram Aggarwalfa4b47e2012-03-09 13:02:46 -0800263 private static final int LOADER_FOLDER_CURSOR = 2;
264 private static final int LOADER_RECENT_FOLDERS = 3;
Mindy Pereira967ede62012-03-22 09:29:09 -0700265 private static final int LOADER_CONVERSATION_LIST = 4;
Mindy Pereiraab486362012-03-21 18:18:53 -0700266 private static final int LOADER_ACCOUNT_INBOX = 5;
267 private static final int LOADER_SEARCH = 6;
Paul Westbrook2d50bcd2012-04-10 11:53:47 -0700268 private static final int LOADER_ACCOUNT_UPDATE_CURSOR = 7;
Vikram Aggarwal7a5d95a2012-07-27 16:24:54 -0700269 /**
270 * Guaranteed to be the last loader ID used by the activity. Loaders are owned by Activity or
271 * fragments, and within an activity, loader IDs need to be unique. A hack to ensure that the
272 * {@link FolderWatcher} can create its folder loaders without clashing with the IDs of those
273 * of the {@link AbstractActivityController}. Currently, the {@link FolderWatcher} is the only
274 * other class that uses this activity's LoaderManager. If another class needs activity-level
275 * loaders, consider consolidating the loaders in a central location: a UI-less fragment
276 * perhaps.
277 */
278 public static final int LAST_LOADER_ID = 100;
Vikram Aggarwala55b36c2012-01-13 11:45:02 -0800279
Paul Westbrook2388c5d2012-03-25 12:29:11 -0700280 private static final int ADD_ACCOUNT_REQUEST_CODE = 1;
Paul Westbrook122f7c22012-08-20 17:50:31 -0700281 private static final int REAUTHENTICATE_REQUEST_CODE = 2;
Paul Westbrook2388c5d2012-03-25 12:29:11 -0700282
Vikram Aggarwale8a85322012-04-24 09:01:38 -0700283 /** The pending destructive action to be carried out before swapping the conversation cursor.*/
284 private DestructiveAction mPendingDestruction;
Andrew Sappersteinc2c9dc12012-07-02 18:17:32 -0700285 protected AsyncRefreshTask mFolderSyncTask;
Mindy Pereira49e5dbe2012-07-12 11:47:54 -0700286 // Task for setting any share intents for the account to enabled.
287 // This gets cancelled if the user kills the app before it finishes, and
288 // will just run the next time the user opens the app.
289 private AsyncTask<String, Void, Void> mEnableShareIntents;
Mindy Pereirac975e842012-07-16 09:15:00 -0700290 private Folder mFolderListFolder;
mindyp5390fca2012-08-22 12:12:25 -0700291 private boolean mIsDragHappening;
mindypead50392012-08-23 11:03:53 -0700292 private int mShowUndoBarDelay;
Andrew Sapperstein00179f12012-08-09 15:15:40 -0700293 public static final String SYNC_ERROR_DIALOG_FRAGMENT_TAG = "SyncErrorDialogFragment";
Vikram Aggarwale8a85322012-04-24 09:01:38 -0700294
Vikram Aggarwala55b36c2012-01-13 11:45:02 -0800295 public AbstractActivityController(MailActivity activity, ViewMode viewMode) {
296 mActivity = activity;
Vikram Aggarwal6902dcf2012-04-11 08:57:42 -0700297 mFragmentManager = mActivity.getFragmentManager();
Vikram Aggarwala55b36c2012-01-13 11:45:02 -0800298 mViewMode = viewMode;
299 mContext = activity.getApplicationContext();
Vikram Aggarwal025eba82012-05-08 10:45:30 -0700300 mRecentFolderList = new RecentFolderList(mContext);
Paul Westbrook937c94f2012-08-16 13:01:18 -0700301 mTracker = new ConversationPositionTracker(this);
Mindy Pereira967ede62012-03-22 09:29:09 -0700302 // Allow the fragment to observe changes to its own selection set. No other object is
303 // aware of the selected set.
304 mSelectedSet.addObserver(this);
Paul Westbrookc7a070f2012-04-12 01:46:41 -0700305
306 mFolderItemUpdateDelayMs =
307 mContext.getResources().getInteger(R.integer.folder_item_refresh_delay_ms);
mindypead50392012-08-23 11:03:53 -0700308 mShowUndoBarDelay =
309 mContext.getResources().getInteger(R.integer.show_undo_bar_delay_ms);
Vikram Aggarwala55b36c2012-01-13 11:45:02 -0800310 }
Vikram Aggarwal4a5c5302012-01-12 15:07:13 -0800311
312 @Override
Vikram Aggarwale9a81032012-02-22 13:15:35 -0800313 public Account getCurrentAccount() {
314 return mAccount;
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -0800315 }
316
317 @Override
318 public ConversationListContext getCurrentListContext() {
Vikram Aggarwale9a81032012-02-22 13:15:35 -0800319 return mConvListContext;
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -0800320 }
321
322 @Override
Vikram Aggarwal4a5c5302012-01-12 15:07:13 -0800323 public String getHelpContext() {
Paul Westbrook30745b62012-08-19 14:10:32 -0700324 final int mode = mViewMode.getMode();
325 final int helpContextResId;
326 switch (mode) {
327 case ViewMode.WAITING_FOR_ACCOUNT_INITIALIZATION:
328 helpContextResId = R.string.wait_help_context;
329 break;
330 default:
331 helpContextResId = R.string.main_help_context;
332 }
333 return mContext.getString(helpContextResId);
Vikram Aggarwal4a5c5302012-01-12 15:07:13 -0800334 }
335
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -0800336 @Override
Vikram Aggarwalef7c9922012-04-23 12:35:04 -0700337 public final ConversationCursor getConversationListCursor() {
Mindy Pereira967ede62012-03-22 09:29:09 -0700338 return mConversationListCursor;
339 }
340
Vikram Aggarwal6902dcf2012-04-11 08:57:42 -0700341 /**
Vikram Aggarwal34a65012012-04-17 12:39:06 -0700342 * Check if the fragment is attached to an activity and has a root view.
343 * @param in
344 * @return true if the fragment is valid, false otherwise
345 */
346 private static final boolean isValidFragment(Fragment in) {
347 if (in == null || in.getActivity() == null || in.getView() == null) {
348 return false;
349 }
350 return true;
351 }
352
353 /**
Vikram Aggarwal49e0e992012-09-21 13:53:15 -0700354 * Get the conversation list fragment for this activity. If the conversation list fragment is
355 * not attached, this method returns null.
Andy Huang839ada22012-07-20 15:48:40 -0700356 *
Vikram Aggarwal49e0e992012-09-21 13:53:15 -0700357 * Caution! This method returns the {@link ConversationListFragment} after the fragment has been
358 * added, <b>and</b> after the {@link FragmentManager} has run through its queue to add the
359 * fragment. There is a non-trivial amount of time after the fragment is instantiated and before
360 * this call returns a non-null value, depending on the {@link FragmentManager}. If you
361 * need the fragment immediately after adding it, consider making the fragment an observer of
362 * the controller and perform the task immediately on {@link Fragment#onActivityCreated(Bundle)}
Vikram Aggarwal6902dcf2012-04-11 08:57:42 -0700363 */
364 protected ConversationListFragment getConversationListFragment() {
365 final Fragment fragment = mFragmentManager.findFragmentByTag(TAG_CONVERSATION_LIST);
Vikram Aggarwal34a65012012-04-17 12:39:06 -0700366 if (isValidFragment(fragment)) {
367 return (ConversationListFragment) fragment;
Andy Huang9585d782012-04-16 19:45:04 -0700368 }
Vikram Aggarwal34a65012012-04-17 12:39:06 -0700369 return null;
Vikram Aggarwal6902dcf2012-04-11 08:57:42 -0700370 }
371
372 /**
Vikram Aggarwal6902dcf2012-04-11 08:57:42 -0700373 * Returns the folder list fragment attached with this activity. If no such fragment is attached
374 * this method returns null.
Andy Huang839ada22012-07-20 15:48:40 -0700375 *
Vikram Aggarwal49e0e992012-09-21 13:53:15 -0700376 * Caution! This method returns the {@link FolderListFragment} after the fragment has been
377 * added, <b>and</b> after the {@link FragmentManager} has run through its queue to add the
378 * fragment. There is a non-trivial amount of time after the fragment is instantiated and before
379 * this call returns a non-null value, depending on the {@link FragmentManager}. If you
380 * need the fragment immediately after adding it, consider making the fragment an observer of
381 * the controller and perform the task immediately on {@link Fragment#onActivityCreated(Bundle)}
Vikram Aggarwal6902dcf2012-04-11 08:57:42 -0700382 */
383 protected FolderListFragment getFolderListFragment() {
384 final Fragment fragment = mFragmentManager.findFragmentByTag(TAG_FOLDER_LIST);
Vikram Aggarwal34a65012012-04-17 12:39:06 -0700385 if (isValidFragment(fragment)) {
386 return (FolderListFragment) fragment;
Andy Huang9585d782012-04-16 19:45:04 -0700387 }
Vikram Aggarwal34a65012012-04-17 12:39:06 -0700388 return null;
Vikram Aggarwal6902dcf2012-04-11 08:57:42 -0700389 }
390
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -0800391 /**
Mindy Pereira161f50d2012-02-28 15:47:19 -0800392 * Initialize the action bar. This is not visible to OnePaneController and
393 * TwoPaneController so they cannot override this behavior.
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -0800394 */
Vikram Aggarwalabd24d82012-04-26 13:23:14 -0700395 private void initializeActionBar() {
396 final ActionBar actionBar = mActivity.getActionBar();
Andy Huang5895f7b2012-06-01 17:07:20 -0700397 if (actionBar == null) {
398 return;
Vikram Aggarwalabd24d82012-04-26 13:23:14 -0700399 }
Andy Huang5895f7b2012-06-01 17:07:20 -0700400
401 // be sure to inherit from the ActionBar theme when inflating
402 final LayoutInflater inflater = LayoutInflater.from(actionBar.getThemedContext());
Mindy Pereira82faec72012-06-14 17:21:50 -0700403 final boolean isSearch = mActivity.getIntent() != null
404 && Intent.ACTION_SEARCH.equals(mActivity.getIntent().getAction());
405 mActionBarView = (MailActionBarView) inflater.inflate(
406 isSearch ? R.layout.search_actionbar_view : R.layout.actionbar_view, null);
Andy Huang5895f7b2012-06-01 17:07:20 -0700407 mActionBarView.initialize(mActivity, this, mViewMode, actionBar, mRecentFolderList);
Vikram Aggarwalabd24d82012-04-26 13:23:14 -0700408 }
409
410 /**
411 * Attach the action bar to the activity.
412 */
413 private void attachActionBar() {
414 final ActionBar actionBar = mActivity.getActionBar();
415 if (actionBar != null && mActionBarView != null) {
Vikram Aggarwalec5cbf72012-03-08 15:10:35 -0800416 actionBar.setCustomView(mActionBarView, new ActionBar.LayoutParams(
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -0800417 LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT));
Vikram Aggarwal649b9ea2012-08-27 12:15:20 -0700418 // Show a custom view and home icon, but remove the title
419 final int mask = ActionBar.DISPLAY_SHOW_CUSTOM | ActionBar.DISPLAY_SHOW_TITLE
420 | ActionBar.DISPLAY_SHOW_HOME;
421 final int enabled = ActionBar.DISPLAY_SHOW_CUSTOM | ActionBar.DISPLAY_SHOW_HOME;
422 actionBar.setDisplayOptions(enabled, mask);
Vikram Aggarwalabd24d82012-04-26 13:23:14 -0700423 mActionBarView.attach();
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -0800424 }
Vikram Aggarwalabd24d82012-04-26 13:23:14 -0700425 mViewMode.addListener(mActionBarView);
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -0800426 }
427
428 /**
Mindy Pereira161f50d2012-02-28 15:47:19 -0800429 * Returns whether the conversation list fragment is visible or not.
430 * Different layouts will have their own notion on the visibility of
431 * fragments, so this method needs to be overriden.
432 *
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -0800433 */
434 protected abstract boolean isConversationListVisible();
435
Vikram Aggarwaleb7d3292012-04-20 17:07:20 -0700436 /**
437 * Switch the current account to the one provided as an argument to the method.
Vikram Aggarwal66bc6ed2012-07-31 09:59:55 -0700438 * @param account new account
439 * @param shouldReloadInbox whether the default inbox should be reloaded.
Vikram Aggarwaleb7d3292012-04-20 17:07:20 -0700440 */
Vikram Aggarwal66bc6ed2012-07-31 09:59:55 -0700441 private void switchAccount(Account account, boolean shouldReloadInbox){
Vikram Aggarwaleb7d3292012-04-20 17:07:20 -0700442 // Current account is different from the new account, restart loaders and show
443 // the account Inbox.
444 mAccount = account;
Vikram Aggarwaledc137c2012-04-24 13:40:58 -0700445 LogUtils.d(LOG_TAG, "AbstractActivityController.switchAccount(): mAccount = %s",
446 mAccount.uri);
Vikram Aggarwaleb7d3292012-04-20 17:07:20 -0700447 cancelRefreshTask();
Vikram Aggarwal6a147ad2012-08-30 14:33:57 -0700448 mAccountObservers.notifyChanged();
Vikram Aggarwal66bc6ed2012-07-31 09:59:55 -0700449 if (shouldReloadInbox) {
450 loadAccountInbox();
451 }
Vikram Aggarwaleb7d3292012-04-20 17:07:20 -0700452 restartOptionalLoader(LOADER_RECENT_FOLDERS);
453 mActivity.invalidateOptionsMenu();
454 disableNotificationsOnAccountChange(mAccount);
455 restartOptionalLoader(LOADER_ACCOUNT_UPDATE_CURSOR);
456 MailAppProvider.getInstance().setLastViewedAccount(mAccount.uri.toString());
mindyp1234aca2012-09-05 15:59:11 -0700457 if (mAccount != null && !Uri.EMPTY.equals(mAccount.settings.setupIntentUri)) {
458 // Launch the intent!
459 Intent intent = new Intent(Intent.ACTION_EDIT);
460 intent.setData(mAccount.settings.setupIntentUri);
461 mActivity.startActivity(intent);
462 }
Vikram Aggarwaleb7d3292012-04-20 17:07:20 -0700463 }
464
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -0800465 @Override
Mindy Pereira28e0c342012-02-17 15:05:13 -0800466 public void onAccountChanged(Account account) {
Vikram Aggarwal60069912012-07-24 14:26:09 -0700467 // Is the account or account settings different from the existing account?
Vikram Aggarwal66bc6ed2012-07-31 09:59:55 -0700468 final boolean firstLoad = mAccount == null;
Andy Huang3825f3d2012-08-29 16:44:12 -0700469 LogUtils.d(LOG_TAG, "onAccountChanged (%s) called. firstLoad=%s", account, firstLoad);
Vikram Aggarwal66bc6ed2012-07-31 09:59:55 -0700470 final boolean accountChanged = firstLoad || !account.uri.equals(mAccount.uri);
471 final boolean settingsChanged = firstLoad || !account.settings.equals(mAccount.settings);
472 if (accountChanged || settingsChanged) {
Mindy Pereirafa20c1a2012-07-23 13:00:02 -0700473 if (account != null) {
Mindy Pereirafa995b42012-07-25 12:06:13 -0700474 final String accountName = account.name;
475 mHandler.post(new Runnable() {
476 @Override
477 public void run() {
478 MailActivity.setForegroundNdef(MailActivity.getMailtoNdef(accountName));
479 }
480 });
Mindy Pereirafa20c1a2012-07-23 13:00:02 -0700481 }
mindypc6adce32012-08-22 18:46:42 -0700482 if (accountChanged) {
483 commitDestructiveActions(false);
484 }
Vikram Aggarwal66bc6ed2012-07-31 09:59:55 -0700485 switchAccount(account, accountChanged);
Mindy Pereira28d5f722012-02-15 12:32:40 -0800486 }
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -0800487 }
488
Vikram Aggarwale6340bc2012-03-26 15:57:09 -0700489 /**
Vikram Aggarwal7c401b72012-08-13 16:43:47 -0700490 * Adds a listener interested in change in the current account. If a class is storing a
491 * reference to the current account, it should listen on changes, so it can receive updates to
492 * settings. Must happen in the UI thread.
493 */
Mindy Pereiraefe3d252012-03-01 14:20:44 -0800494 @Override
Vikram Aggarwal7c401b72012-08-13 16:43:47 -0700495 public void registerAccountObserver(DataSetObserver obs) {
496 mAccountObservers.registerObserver(obs);
Mindy Pereiraefe3d252012-03-01 14:20:44 -0800497 }
498
Vikram Aggarwal7d816002012-04-17 17:06:41 -0700499 /**
Vikram Aggarwal7c401b72012-08-13 16:43:47 -0700500 * Removes a listener from receiving current account changes.
Vikram Aggarwal7d816002012-04-17 17:06:41 -0700501 * Must happen in the UI thread.
502 */
Vikram Aggarwal7c401b72012-08-13 16:43:47 -0700503 @Override
504 public void unregisterAccountObserver(DataSetObserver obs) {
505 mAccountObservers.unregisterObserver(obs);
Vikram Aggarwal7d816002012-04-17 17:06:41 -0700506 }
507
Vikram Aggarwal7c401b72012-08-13 16:43:47 -0700508 @Override
509 public Account getAccount() {
510 return mAccount;
Vikram Aggarwal7d816002012-04-17 17:06:41 -0700511 }
512
Mindy Pereirae0828392012-03-08 10:38:40 -0800513 private void fetchSearchFolder(Intent intent) {
Vikram Aggarwal2b703c62012-09-18 13:54:15 -0700514 final Bundle args = new Bundle();
Mindy Pereiraab486362012-03-21 18:18:53 -0700515 args.putString(ConversationListContext.EXTRA_SEARCH_QUERY, intent
Mindy Pereirae0828392012-03-08 10:38:40 -0800516 .getStringExtra(ConversationListContext.EXTRA_SEARCH_QUERY));
Mindy Pereiraab486362012-03-21 18:18:53 -0700517 mActivity.getLoaderManager().restartLoader(LOADER_SEARCH, args, this);
Mindy Pereirae0828392012-03-08 10:38:40 -0800518 }
519
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -0800520 @Override
Mindy Pereira28e0c342012-02-17 15:05:13 -0800521 public void onFolderChanged(Folder folder) {
Vikram Aggarwalf3341402012-08-07 10:09:38 -0700522 changeFolder(folder, null);
523 }
524
525 /**
Vikram Aggarwal203dc002012-08-23 13:59:04 -0700526 * Sets the folder state without changing view mode and without creating a list fragment, if
527 * possible.
528 * @param folder
529 */
530 private void setListContext(Folder folder, String query) {
531 updateFolder(folder);
532 if (query != null) {
533 mConvListContext = ConversationListContext.forSearchQuery(mAccount, mFolder, query);
534 } else {
535 mConvListContext = ConversationListContext.forFolder(mAccount, mFolder);
536 }
Vikram Aggarwal203dc002012-08-23 13:59:04 -0700537 cancelRefreshTask();
538 }
539
540 /**
Vikram Aggarwalf3341402012-08-07 10:09:38 -0700541 * Changes the folder to the value provided here. This causes the view mode to change.
542 * @param folder the folder to change to
543 * @param query if non-null, this represents the search string that the folder represents.
544 */
545 private void changeFolder(Folder folder, String query) {
Vikram Aggarwal203dc002012-08-23 13:59:04 -0700546 if (!Objects.equal(mFolder, folder)) {
547 commitDestructiveActions(false);
548 }
Mindy Pereiraa1f99982012-07-16 16:32:15 -0700549 if (folder != null && !folder.equals(mFolder)
550 || (mViewMode.getMode() != ViewMode.CONVERSATION_LIST)) {
Vikram Aggarwal203dc002012-08-23 13:59:04 -0700551 setListContext(folder, query);
Mindy Pereira28e0c342012-02-17 15:05:13 -0800552 showConversationList(mConvListContext);
Mindy Pereira28e0c342012-02-17 15:05:13 -0800553 }
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -0800554 }
555
Mindy Pereira13c12a62012-05-31 15:41:08 -0700556 @Override
Mindy Pereira505df5f2012-06-19 17:57:17 -0700557 public void onFolderSelected(Folder folder) {
Mindy Pereira13c12a62012-05-31 15:41:08 -0700558 onFolderChanged(folder);
559 }
560
Vikram Aggarwal792ccba2012-03-27 13:46:57 -0700561 /**
562 * Update the recent folders. This only needs to be done once when accessing a new folder.
563 */
Paul Westbrook7ebdfd02012-03-21 15:55:30 -0700564 private void updateRecentFolderList() {
Mindy Pereiraab486362012-03-21 18:18:53 -0700565 if (mFolder != null) {
Marc Blank2675dbc2012-04-03 10:17:13 -0700566 mRecentFolderList.touchFolder(mFolder, mAccount);
Mindy Pereiraab486362012-03-21 18:18:53 -0700567 }
Paul Westbrook7ebdfd02012-03-21 15:55:30 -0700568 }
569
Vikram Aggarwal58cad2e2012-08-28 16:18:23 -0700570 /**
571 * Adds a listener interested in change in the recent folders. If a class is storing a
572 * reference to the recent folders, it should listen on changes, so it can receive updates.
573 * Must happen in the UI thread.
574 */
575 @Override
576 public void registerRecentFolderObserver(DataSetObserver obs) {
577 mRecentFolderObservers.registerObserver(obs);
578 }
579
580 /**
581 * Removes a listener from receiving recent folder changes.
582 * Must happen in the UI thread.
583 */
584 @Override
585 public void unregisterRecentFolderObserver(DataSetObserver obs) {
586 mRecentFolderObservers.unregisterObserver(obs);
587 }
588
589 @Override
590 public RecentFolderList getRecentFolders() {
591 return mRecentFolderList;
592 }
593
Mindy Pereiraab486362012-03-21 18:18:53 -0700594 // TODO(mindyp): set this up to store a copy of the folder as a transient
595 // field in the account.
Vikram Aggarwaleb7d3292012-04-20 17:07:20 -0700596 @Override
597 public void loadAccountInbox() {
Vikram Aggarwal94c94de2012-04-04 15:38:28 -0700598 restartOptionalLoader(LOADER_ACCOUNT_INBOX);
Mindy Pereiraf6acdad2012-03-15 11:21:13 -0700599 }
600
Vikram Aggarwal69b5c302012-09-05 11:11:13 -0700601 /**
Vikram Aggarwald00229d2012-09-20 12:31:44 -0700602 * Marks the {@link #mFolderChanged} value if the newFolder is different from the existing
603 * {@link #mFolder}. This should be called immediately <b>before</b> assigning newFolder to
604 * mFolder.
605 * @param newFolder
606 */
607 private final void setHasFolderChanged(final Folder newFolder) {
608 // We should never try to assign a null folder. But in the rare event that we do, we should
609 // only set the bit when we have a valid folder, and null is not valid.
610 if (newFolder == null) {
611 return;
612 }
613 // If the previous folder was null, or if the two folders represent different data, then we
614 // consider that the folder has changed.
615 if (mFolder == null || !newFolder.uri.equals(mFolder.uri)) {
616 mFolderChanged = true;
617 }
618 }
619
620 /**
Vikram Aggarwal69b5c302012-09-05 11:11:13 -0700621 * Sets the current folder if it is different from the object provided here. This method does
622 * NOT notify the folder observers that a change has happened. Observers are notified when we
623 * get an updated folder from the loaders, which will happen as a consequence of this method
624 * (since this method starts/restarts the loaders).
625 * @param folder The folder to assign
626 */
Mindy Pereira11e35962012-06-01 14:49:46 -0700627 private void updateFolder(Folder folder) {
Vikram Aggarwal1672ff82012-09-21 10:15:22 -0700628 if (folder == null || !folder.isInitialized()) {
629 LogUtils.e(LOG_TAG, new Error(), "AAC.setFolder(%s): Bad input", folder);
630 return;
631 }
632 if (folder.equals(mFolder)) {
633 LogUtils.d(LOG_TAG, "AAC.setFolder(%s): Input matches mFolder", folder);
634 return;
635 }
636 final boolean wasNull = mFolder == null;
637 LogUtils.d(LOG_TAG, "AbstractActivityController.setFolder(%s)", folder.name);
638 final LoaderManager lm = mActivity.getLoaderManager();
639 // updateFolder is called from AAC.onLoadFinished() on folder changes. We need to
640 // ensure that the folder is different from the previous folder before marking the
641 // folder changed.
642 setHasFolderChanged(folder);
643 mFolder = folder;
Vikram Aggarwal69b5c302012-09-05 11:11:13 -0700644
Vikram Aggarwal1672ff82012-09-21 10:15:22 -0700645 // We do not need to notify folder observers yet. Instead we start the loaders and
646 // when the load finishes, we will get an updated folder. Then, we notify the
647 // folderObservers in onLoadFinished.
648 mActionBarView.setFolder(mFolder);
Andy Huangb1c34dc2012-04-17 16:36:19 -0700649
Vikram Aggarwal1672ff82012-09-21 10:15:22 -0700650 // Only when we switch from one folder to another do we want to restart the
651 // folder and conversation list loaders (to trigger onCreateLoader).
652 // The first time this runs when the activity is [re-]initialized, we want to re-use the
653 // previous loader's instance and data upon configuration change (e.g. rotation).
654 // If there was not already an instance of the loader, init it.
655 if (lm.getLoader(LOADER_FOLDER_CURSOR) == null) {
656 lm.initLoader(LOADER_FOLDER_CURSOR, null, this);
657 } else {
658 lm.restartLoader(LOADER_FOLDER_CURSOR, null, this);
659 }
660 // In this case, we are starting from no folder, which would occur
661 // the first time the app was launched or on orientation changes.
662 // We want to attach to an existing loader, if available.
663 if (wasNull || lm.getLoader(LOADER_CONVERSATION_LIST) == null) {
664 lm.initLoader(LOADER_CONVERSATION_LIST, null, mListCursorCallbacks);
665 } else {
666 // However, if there was an existing folder AND we have changed
667 // folders, we want to restart the loader to get the information
668 // for the newly selected folder
669 lm.destroyLoader(LOADER_CONVERSATION_LIST);
670 lm.initLoader(LOADER_CONVERSATION_LIST, null, mListCursorCallbacks);
Mindy Pereira5cb0c3e2012-02-22 15:22:47 -0800671 }
672 }
673
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -0800674 @Override
Andy Huang090db1e2012-07-25 13:25:28 -0700675 public Folder getFolder() {
676 return mFolder;
677 }
678
679 @Override
Mindy Pereirac975e842012-07-16 09:15:00 -0700680 public Folder getHierarchyFolder() {
681 return mFolderListFolder;
682 }
683
684 @Override
685 public void setHierarchyFolder(Folder folder) {
686 mFolderListFolder = folder;
Mindy Pereira23aadfd2012-05-25 11:24:33 -0700687 }
688
689 @Override
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -0800690 public void onActivityResult(int requestCode, int resultCode, Intent data) {
Paul Westbrook122f7c22012-08-20 17:50:31 -0700691 switch (requestCode) {
692 case ADD_ACCOUNT_REQUEST_CODE:
693 // We were waiting for the user to create an account
694 if (resultCode == Activity.RESULT_OK) {
695 // restart the loader to get the updated list of accounts
696 mActivity.getLoaderManager().initLoader(
697 LOADER_ACCOUNT_CURSOR, null, this);
698 } else {
699 // The user failed to create an account, just exit the app
700 mActivity.finish();
701 }
702 break;
703 case REAUTHENTICATE_REQUEST_CODE:
704 if (resultCode == Activity.RESULT_OK) {
705 // The user successfully authenticated, attempt to refresh the list
706 final Uri refreshUri = mFolder != null ? mFolder.refreshUri : null;
707 if (refreshUri != null) {
708 startAsyncRefreshTask(refreshUri);
709 }
710 }
711 break;
Paul Westbrook2388c5d2012-03-25 12:29:11 -0700712 }
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -0800713 }
714
Vikram Aggarwal69b5c302012-09-05 11:11:13 -0700715 /**
716 * Inform the conversation cursor that there has been a visibility change.
717 * @param visible
718 */
719 protected synchronized void informCursorVisiblity(boolean visible) {
720 if (mConversationListCursor != null) {
721 Utils.setConversationCursorVisibility(mConversationListCursor, visible, mFolderChanged);
722 // We have informed the cursor. Subsequent visibility changes should not tell it that
723 // the folder has changed.
724 mFolderChanged = false;
725 }
726 }
727
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -0800728 @Override
mindyp3bcf1802012-09-09 11:17:00 -0700729 public void onConversationLoadError() {
730 // Jump back to conversation list view. this can happen if connectivity is interrupted while
731 // looking at a live label.
732 // {@link FragmentTransaction} cannot be committed inside {@link
733 // LoaderManager.LoaderCallbacks<D>#onLoadFinished(Loader<D>, D)}. So we post a {@link
734 // Runnable} to run later.
735 mHandler.post(new Runnable() {
736 @Override
737 public void run() {
738 showConversationList(mConvListContext);
739 }
740 });
741 }
742
743 @Override
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -0800744 public void onConversationListVisibilityChanged(boolean visible) {
Vikram Aggarwal69b5c302012-09-05 11:11:13 -0700745 informCursorVisiblity(visible);
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -0800746 }
747
Vikram Aggarwal4a5c5302012-01-12 15:07:13 -0800748 /**
Vikram Aggarwal7dd054e2012-05-21 14:43:10 -0700749 * Called when a conversation is visible. Child classes must call the super class implementation
750 * before performing local computation.
Vikram Aggarwal4a5c5302012-01-12 15:07:13 -0800751 */
752 @Override
753 public void onConversationVisibilityChanged(boolean visible) {
Vikram Aggarwal4a5c5302012-01-12 15:07:13 -0800754 }
755
756 @Override
Vikram Aggarwald7a12cd2012-02-03 09:36:20 -0800757 public boolean onCreate(Bundle savedState) {
Vikram Aggarwalabd24d82012-04-26 13:23:14 -0700758 initializeActionBar();
Vikram Aggarwala55b36c2012-01-13 11:45:02 -0800759 // Allow shortcut keys to function for the ActionBar and menus.
760 mActivity.setDefaultKeyMode(Activity.DEFAULT_KEYS_SHORTCUT);
Vikram Aggarwal80aeac52012-02-07 15:27:20 -0800761 mResolver = mActivity.getContentResolver();
Paul Westbrook6ead20d2012-03-19 14:48:14 -0700762 mNewEmailReceiver = new SuppressNotificationReceiver();
Vikram Aggarwal7c401b72012-08-13 16:43:47 -0700763 mRecentFolderList.initialize(mActivity);
Paul Westbrook6ead20d2012-03-19 14:48:14 -0700764
Mindy Pereira161f50d2012-02-28 15:47:19 -0800765 // All the individual UI components listen for ViewMode changes. This
Mindy Pereirab849dfb2012-03-07 18:13:15 -0800766 // simplifies the amount of logic in the AbstractActivityController, but increases the
767 // possibility of timing-related bugs.
Vikram Aggarwala55b36c2012-01-13 11:45:02 -0800768 mViewMode.addListener(this);
Andy Huang632721e2012-04-11 16:57:26 -0700769 mPagerController = new ConversationPagerController(mActivity, this);
Andrew Sappersteinc2c9dc12012-07-02 18:17:32 -0700770 mToastBar = (ActionableToastBar) mActivity.findViewById(R.id.toast_bar);
Vikram Aggarwalada51782012-04-26 14:18:31 -0700771 attachActionBar();
Andy Huang632721e2012-04-11 16:57:26 -0700772
773 final Intent intent = mActivity.getIntent();
Vikram Aggarwalabd24d82012-04-26 13:23:14 -0700774 // Immediately handle a clean launch with intent, and any state restoration
Andy Huang632721e2012-04-11 16:57:26 -0700775 // that does not rely on restored fragments or loader data
776 // any state restoration that relies on those can be done later in
777 // onRestoreInstanceState, once fragments are up and loader data is re-delivered
778 if (savedState != null) {
779 if (savedState.containsKey(SAVED_ACCOUNT)) {
780 setAccount((Account) savedState.getParcelable(SAVED_ACCOUNT));
Andy Huang632721e2012-04-11 16:57:26 -0700781 }
782 if (savedState.containsKey(SAVED_FOLDER)) {
Vikram Aggarwalf3341402012-08-07 10:09:38 -0700783 final Folder folder = (Folder) savedState.getParcelable(SAVED_FOLDER);
Vikram Aggarwal203dc002012-08-23 13:59:04 -0700784 final String query = savedState.getString(SAVED_QUERY, null);
Vikram Aggarwal649b9ea2012-08-27 12:15:20 -0700785 setListContext(folder, query);
Andy Huang632721e2012-04-11 16:57:26 -0700786 }
Vikram Aggarwal649b9ea2012-08-27 12:15:20 -0700787 mViewMode.handleRestore(savedState);
Andy Huang632721e2012-04-11 16:57:26 -0700788 } else if (intent != null) {
789 handleIntent(intent);
790 }
Andy Huang632721e2012-04-11 16:57:26 -0700791 // Create the accounts loader; this loads the account switch spinner.
792 mActivity.getLoaderManager().initLoader(LOADER_ACCOUNT_CURSOR, null, this);
Andy Huang632721e2012-04-11 16:57:26 -0700793 return true;
Andy Huangb1c34dc2012-04-17 16:36:19 -0700794 }
795
796 @Override
Andy Huang1ee96b22012-08-24 20:19:53 -0700797 public void onStart() {
798 mSafeToModifyFragments = true;
799 }
800
801 @Override
Andrew Sapperstein00179f12012-08-09 15:15:40 -0700802 public void onRestart() {
803 DialogFragment fragment = (DialogFragment)
804 mFragmentManager.findFragmentByTag(SYNC_ERROR_DIALOG_FRAGMENT_TAG);
805 if (fragment != null) {
806 fragment.dismiss();
807 }
mindypea04f932012-08-27 14:17:59 -0700808 // When the user places the app in the background by pressing "home",
809 // dismiss the toast bar. However, since there is no way to determine if
810 // home was pressed, just dismiss any existing toast bar when restarting
811 // the app.
812 if (mToastBar != null) {
813 mToastBar.hide(false);
814 }
Andrew Sapperstein00179f12012-08-09 15:15:40 -0700815 }
816
817 @Override
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -0800818 public Dialog onCreateDialog(int id, Bundle bundle) {
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -0800819 return null;
Vikram Aggarwala55b36c2012-01-13 11:45:02 -0800820 }
821
822 @Override
Vikram Aggarwal4eb52712012-06-19 16:24:50 -0700823 public final boolean onCreateOptionsMenu(Menu menu) {
Vikram Aggarwale5e917c2012-09-20 16:27:41 -0700824 final MenuInflater inflater = mActivity.getMenuInflater();
Mindy Pereiraf5acda42012-02-15 20:13:59 -0800825 inflater.inflate(mActionBarView.getOptionsMenuId(), menu);
Mindy Pereira68f2e222012-03-07 10:36:54 -0800826 mActionBarView.onCreateOptionsMenu(menu);
Mindy Pereira28d5f722012-02-15 12:32:40 -0800827 return true;
Vikram Aggarwala55b36c2012-01-13 11:45:02 -0800828 }
829
830 @Override
Vikram Aggarwal4eb52712012-06-19 16:24:50 -0700831 public final boolean onKeyDown(int keyCode, KeyEvent event) {
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -0800832 // TODO(viki): Auto-generated method stub
833 return false;
834 }
835
836 @Override
Vikram Aggarwal4eb52712012-06-19 16:24:50 -0700837 public final boolean onOptionsItemSelected(MenuItem item) {
Vikram Aggarwal54452ae2012-03-13 15:29:00 -0700838 final int id = item.getItemId();
Vikram Aggarwal7dd054e2012-05-21 14:43:10 -0700839 LogUtils.d(LOG_TAG, "AbstractController.onOptionsItemSelected(%d) called.", id);
Mindy Pereira9b875682012-02-15 18:10:54 -0800840 boolean handled = true;
Mindy Pereiraba68fda2012-05-24 15:53:06 -0700841 final Collection<Conversation> target = Conversation.listOf(mCurrentConversation);
Vikram Aggarwal112cd162012-06-18 10:51:13 -0700842 final Settings settings = (mAccount == null) ? null : mAccount.settings;
Mindy Pereira8937bf12012-07-23 14:05:02 -0700843 // The user is choosing a new action; commit whatever they had been doing before.
mindypc6adce32012-08-22 18:46:42 -0700844 commitDestructiveActions(true);
Mindy Pereira28d5f722012-02-15 12:32:40 -0800845 switch (id) {
Mindy Pereiraba68fda2012-05-24 15:53:06 -0700846 case R.id.archive: {
847 final boolean showDialog = (settings != null && settings.confirmArchive);
848 confirmAndDelete(target, showDialog, R.plurals.confirm_archive_conversation,
849 getAction(R.id.archive, target));
850 break;
851 }
Mindy Pereira01f30502012-08-14 10:30:51 -0700852 case R.id.remove_folder:
Vikram Aggarwala8e43182012-09-13 12:55:10 -0700853 delete(R.id.remove_folder, target,
854 getRemoveFolder(target, mFolder, true, false, true));
Mindy Pereira01f30502012-08-14 10:30:51 -0700855 break;
Mindy Pereiraba68fda2012-05-24 15:53:06 -0700856 case R.id.delete: {
857 final boolean showDialog = (settings != null && settings.confirmDelete);
858 confirmAndDelete(target, showDialog, R.plurals.confirm_delete_conversation,
859 getAction(R.id.delete, target));
860 break;
861 }
Vikram Aggarwala8e43182012-09-13 12:55:10 -0700862 case R.id.discard_drafts: {
Paul Westbrookef362542012-08-27 14:53:32 -0700863 final boolean showDialog = (settings != null && settings.confirmDelete);
864 confirmAndDelete(target, showDialog, R.plurals.confirm_discard_drafts_conversation,
865 getAction(R.id.discard_drafts, target));
866 break;
Vikram Aggarwala8e43182012-09-13 12:55:10 -0700867 }
Mindy Pereiraba68fda2012-05-24 15:53:06 -0700868 case R.id.mark_important:
Vikram Aggarwal531488e2012-05-29 16:36:52 -0700869 updateConversation(Conversation.listOf(mCurrentConversation),
870 ConversationColumns.PRIORITY, UIProvider.ConversationPriority.HIGH);
Mindy Pereiraba68fda2012-05-24 15:53:06 -0700871 break;
872 case R.id.mark_not_important:
Mindy Pereira0d03ef82012-08-15 09:05:48 -0700873 if (mFolder != null && mFolder.isImportantOnly()) {
Vikram Aggarwala8e43182012-09-13 12:55:10 -0700874 delete(R.id.mark_not_important, target,
875 getAction(R.id.mark_not_important, target));
Mindy Pereira0d03ef82012-08-15 09:05:48 -0700876 } else {
877 updateConversation(Conversation.listOf(mCurrentConversation),
878 ConversationColumns.PRIORITY, UIProvider.ConversationPriority.LOW);
879 }
Mindy Pereiraba68fda2012-05-24 15:53:06 -0700880 break;
881 case R.id.mute:
Vikram Aggarwala8e43182012-09-13 12:55:10 -0700882 delete(R.id.mute, target, getAction(R.id.mute, target));
Mindy Pereiraba68fda2012-05-24 15:53:06 -0700883 break;
884 case R.id.report_spam:
Vikram Aggarwala8e43182012-09-13 12:55:10 -0700885 delete(R.id.report_spam, target, getAction(R.id.report_spam, target));
Mindy Pereiraba68fda2012-05-24 15:53:06 -0700886 break;
Paul Westbrook77eee622012-07-10 13:41:57 -0700887 case R.id.mark_not_spam:
888 // Currently, since spam messages are only shown in list with other spam messages,
889 // marking a message not as spam is a destructive action
Vikram Aggarwala8e43182012-09-13 12:55:10 -0700890 delete(R.id.mark_not_spam, target, getAction(R.id.mark_not_spam, target));
Paul Westbrook77eee622012-07-10 13:41:57 -0700891 break;
Paul Westbrook76b20622012-07-12 11:45:43 -0700892 case R.id.report_phishing:
Vikram Aggarwala8e43182012-09-13 12:55:10 -0700893 delete(R.id.report_phishing, target, getAction(R.id.report_phishing, target));
Paul Westbrook76b20622012-07-12 11:45:43 -0700894 break;
Mindy Pereiraf5acda42012-02-15 20:13:59 -0800895 case android.R.id.home:
896 onUpPressed();
897 break;
Mindy Pereira9b875682012-02-15 18:10:54 -0800898 case R.id.compose:
899 ComposeActivity.compose(mActivity.getActivityContext(), mAccount);
900 break;
Mindy Pereira28d5f722012-02-15 12:32:40 -0800901 case R.id.show_all_folders:
902 showFolderList();
903 break;
Mindy Pereira28e0c342012-02-17 15:05:13 -0800904 case R.id.refresh:
905 requestFolderRefresh();
906 break;
Mindy Pereira1f936682012-03-02 11:30:33 -0800907 case R.id.settings:
908 Utils.showSettings(mActivity.getActivityContext(), mAccount);
Paul Westbrook2861b6a2012-02-15 15:25:34 -0800909 break;
Paul Westbrooke5503552012-03-28 00:35:57 -0700910 case R.id.folder_options:
911 Utils.showFolderSettings(mActivity.getActivityContext(), mAccount, mFolder);
912 break;
Paul Westbrook94e440d2012-02-24 11:03:47 -0800913 case R.id.help_info_menu_item:
Paul Westbrook30745b62012-08-19 14:10:32 -0700914 Utils.showHelp(mActivity.getActivityContext(), mAccount, getHelpContext());
Paul Westbrook94e440d2012-02-24 11:03:47 -0800915 break;
Vikram Aggarwal54452ae2012-03-13 15:29:00 -0700916 case R.id.feedback_menu_item:
Paul Westbrook17beb0b2012-08-20 13:34:37 -0700917 Utils.sendFeedback(mActivity.getActivityContext(), mAccount, false);
Mindy Pereirafbe40192012-03-20 10:40:45 -0700918 break;
Paul Westbrook18babd22012-04-09 22:17:08 -0700919 case R.id.manage_folders_item:
920 Utils.showManageFolder(mActivity.getActivityContext(), mAccount);
921 break;
Vikram Aggarwald503df42012-05-11 10:13:35 -0700922 case R.id.change_folder:
Vikram Aggarwal49e0e992012-09-21 13:53:15 -0700923 if (mAccount.supportsCapability(
924 UIProvider.AccountCapabilities.MULTIPLE_FOLDERS_PER_CONV)) {
mindypa7e15452012-09-18 14:22:11 -0700925 new MultiFoldersSelectionDialog(mActivity.getActivityContext(), mAccount, this,
926 Conversation.listOf(mCurrentConversation), false, mFolder).show();
927 } else {
928 new SingleFolderSelectionDialog(mActivity.getActivityContext(), mAccount, this,
929 Conversation.listOf(mCurrentConversation), false, mFolder).show();
930 }
Vikram Aggarwald503df42012-05-11 10:13:35 -0700931 break;
Mindy Pereira9b875682012-02-15 18:10:54 -0800932 default:
933 handled = false;
934 break;
Mindy Pereira28d5f722012-02-15 12:32:40 -0800935 }
Mindy Pereira9b875682012-02-15 18:10:54 -0800936 return handled;
Vikram Aggarwala55b36c2012-01-13 11:45:02 -0800937 }
938
Vikram Aggarwal531488e2012-05-29 16:36:52 -0700939 @Override
Mindy Pereira6c2663d2012-07-20 15:37:29 -0700940 public void updateConversation(Collection<Conversation> target, ContentValues values) {
941 mConversationListCursor.updateValues(mContext, target, values);
942 refreshConversationList();
943 }
944
945 @Override
Vikram Aggarwal531488e2012-05-29 16:36:52 -0700946 public void updateConversation(Collection <Conversation> target, String columnName,
947 boolean value) {
948 mConversationListCursor.updateBoolean(mContext, target, columnName, value);
Vikram Aggarwal75daee52012-04-30 11:13:09 -0700949 refreshConversationList();
Vikram Aggarwal54452ae2012-03-13 15:29:00 -0700950 }
951
Vikram Aggarwal531488e2012-05-29 16:36:52 -0700952 @Override
Mindy Pereira6c2663d2012-07-20 15:37:29 -0700953 public void updateConversation(Collection <Conversation> target, String columnName,
954 int value) {
Vikram Aggarwal531488e2012-05-29 16:36:52 -0700955 mConversationListCursor.updateInt(mContext, target, columnName, value);
Vikram Aggarwal75daee52012-04-30 11:13:09 -0700956 refreshConversationList();
Mindy Pereirac9d59182012-03-22 16:06:46 -0700957 }
958
Vikram Aggarwal531488e2012-05-29 16:36:52 -0700959 @Override
960 public void updateConversation(Collection <Conversation> target, String columnName,
961 String value) {
962 mConversationListCursor.updateString(mContext, target, columnName, value);
Vikram Aggarwal75daee52012-04-30 11:13:09 -0700963 refreshConversationList();
Vikram Aggarwal54452ae2012-03-13 15:29:00 -0700964 }
965
Andy Huang839ada22012-07-20 15:48:40 -0700966 @Override
967 public void markConversationMessagesUnread(Conversation conv, Set<Uri> unreadMessageUris,
Vikram Aggarwal4a878b62012-07-31 15:09:25 -0700968 String originalConversationInfo) {
Andy Huang8f6b0062012-07-31 15:36:31 -0700969 // The only caller of this method is the conversation view, from where marking unread should
970 // *always* take you back to list mode.
971 showConversation(null);
972
Andy Huang839ada22012-07-20 15:48:40 -0700973 // locally mark conversation unread (the provider is supposed to propagate message unread
974 // to conversation unread)
975 conv.read = false;
976
Andy Huang28e31e22012-07-26 16:33:15 -0700977 // only do a granular 'mark unread' if a subset of messages are unread
978 final int unreadCount = (unreadMessageUris == null) ? 0 : unreadMessageUris.size();
Mindy Pereira0972e072012-08-01 17:43:06 -0700979 final int numMessages = conv.getNumMessages();
980 final boolean subsetIsUnread = (numMessages > 1 && unreadCount > 0
981 && unreadCount < numMessages);
Andy Huang28e31e22012-07-26 16:33:15 -0700982
983 if (!subsetIsUnread) {
Vikram Aggarwal66bc2aa2012-08-02 10:47:03 -0700984 // Conversations are neither marked read, nor viewed, and we don't want to show
985 // the next conversation.
986 markConversationsRead(Collections.singletonList(conv), false, false, false);
Andy Huang839ada22012-07-20 15:48:40 -0700987 } else {
Andy Huangdaa06ab2012-07-24 10:46:44 -0700988 mConversationListCursor.setConversationColumn(conv.uri, ConversationColumns.READ, 0);
Andy Huang839ada22012-07-20 15:48:40 -0700989
Mindy Pereira7b6d03d2012-07-30 13:03:41 -0700990 // locally update conversation's conversationInfo to revert to original version
Andy Huang28e31e22012-07-26 16:33:15 -0700991 if (originalConversationInfo != null) {
992 mConversationListCursor.setConversationColumn(conv.uri,
993 ConversationColumns.CONVERSATION_INFO, originalConversationInfo);
994 }
Andy Huang839ada22012-07-20 15:48:40 -0700995
996 // applyBatch with each CPO as an UPDATE op on each affected message uri
997 final ArrayList<ContentProviderOperation> ops = Lists.newArrayList();
998 String authority = null;
999 for (Uri messageUri : unreadMessageUris) {
1000 if (authority == null) {
1001 authority = messageUri.getAuthority();
1002 }
1003 ops.add(ContentProviderOperation.newUpdate(messageUri)
1004 .withValue(UIProvider.MessageColumns.READ, 0)
1005 .build());
1006 }
1007
1008 new ContentProviderTask() {
1009 @Override
1010 protected void onPostExecute(Result result) {
1011 // TODO: handle errors?
1012 }
1013 }.run(mResolver, authority, ops);
Andy Huang839ada22012-07-20 15:48:40 -07001014 }
Andy Huang839ada22012-07-20 15:48:40 -07001015 }
1016
1017 @Override
Vikram Aggarwal66bc2aa2012-08-02 10:47:03 -07001018 public void markConversationsRead(Collection<Conversation> targets, boolean read,
1019 boolean viewed) {
1020 // We want to show the next conversation if we are marking unread.
1021 markConversationsRead(targets, read, viewed, true);
Andy Huang8f6b0062012-07-31 15:36:31 -07001022 }
1023
1024 private void markConversationsRead(Collection<Conversation> targets, boolean read,
Vikram Aggarwal66bc2aa2012-08-02 10:47:03 -07001025 boolean markViewed, boolean showNext) {
Vikram Aggarwalcc754b12012-08-30 14:04:21 -07001026 // Auto-advance if requested and the current conversation is being marked unread
Andy Huang8f6b0062012-07-31 15:36:31 -07001027 if (showNext && !read) {
1028 showNextConversation(targets);
1029 }
Vikram Aggarwalcc754b12012-08-30 14:04:21 -07001030 final int size = targets.size();
1031 final List<ConversationOperation> opList = new ArrayList<ConversationOperation>(size);
1032 for (final Conversation target : targets) {
1033 final ContentValues value = new ContentValues();
1034 value.put(ConversationColumns.READ, read);
Vikram Aggarwal66bc2aa2012-08-02 10:47:03 -07001035 if (markViewed) {
Vikram Aggarwalcc754b12012-08-30 14:04:21 -07001036 value.put(ConversationColumns.VIEWED, true);
Vikram Aggarwal66bc2aa2012-08-02 10:47:03 -07001037 }
Vikram Aggarwal192fac12012-07-25 16:44:55 -07001038 final ConversationInfo info = target.conversationInfo;
Andy Huang839ada22012-07-20 15:48:40 -07001039 if (info != null) {
Mindy Pereira5f424372012-07-30 11:49:55 -07001040 info.markRead(read);
Vikram Aggarwalcc754b12012-08-30 14:04:21 -07001041 value.put(ConversationColumns.CONVERSATION_INFO, ConversationInfo.toString(info));
Andy Huang839ada22012-07-20 15:48:40 -07001042 }
Vikram Aggarwalcc754b12012-08-30 14:04:21 -07001043 opList.add(mConversationListCursor.getOperationForConversation(
1044 target, ConversationOperation.UPDATE, value));
1045 // Update the local conversation objects so they immediately change state.
1046 target.read = read;
Andy Huangcd5c5ee2012-08-12 19:03:51 -07001047 if (markViewed) {
Vikram Aggarwalcc754b12012-08-30 14:04:21 -07001048 target.markViewed();
Andy Huangcd5c5ee2012-08-12 19:03:51 -07001049 }
Andy Huang839ada22012-07-20 15:48:40 -07001050 }
Vikram Aggarwalcc754b12012-08-30 14:04:21 -07001051 mConversationListCursor.updateBulkValues(mContext, opList);
Andy Huang839ada22012-07-20 15:48:40 -07001052 }
1053
Andy Huang8f6b0062012-07-31 15:36:31 -07001054 /**
1055 * Auto-advance to a different conversation if the currently visible conversation in
1056 * conversation mode is affected (deleted, marked unread, etc.).
1057 *
1058 * <p>Does nothing if outside of conversation mode.
1059 *
1060 * @param target the set of conversations being deleted/marked unread
1061 */
mindyp9365a822012-09-12 09:09:09 -07001062 @Override
1063 public void showNextConversation(Collection<Conversation> target) {
Andy Huang8f6b0062012-07-31 15:36:31 -07001064 final boolean currentConversationInView = (mViewMode.getMode() == ViewMode.CONVERSATION)
1065 && Conversation.contains(target, mCurrentConversation);
1066 if (currentConversationInView) {
1067 final Conversation next = mTracker.getNextConversation(
1068 Settings.getAutoAdvanceSetting(mAccount.settings), target);
1069 LogUtils.d(LOG_TAG, "showNextConversation: showing %s next.", next);
1070 showConversation(next);
1071 }
1072 }
1073
Andy Huang839ada22012-07-20 15:48:40 -07001074 @Override
1075 public void starMessage(ConversationMessage msg, boolean starred) {
1076 if (msg.starred == starred) {
1077 return;
1078 }
1079
1080 msg.starred = starred;
1081
1082 // locally propagate the change to the owning conversation
1083 // (figure the provider will properly propagate the change when it commits it)
1084 //
1085 // when unstarring, only propagate the change if this was the only message starred
1086 final boolean conversationStarred = starred || msg.isConversationStarred();
1087 if (conversationStarred != msg.conversation.starred) {
1088 msg.conversation.starred = conversationStarred;
Andy Huangdaa06ab2012-07-24 10:46:44 -07001089 mConversationListCursor.setConversationColumn(msg.conversation.uri,
Andy Huang839ada22012-07-20 15:48:40 -07001090 ConversationColumns.STARRED, conversationStarred);
1091 }
1092
1093 final ContentValues values = new ContentValues(1);
1094 values.put(UIProvider.MessageColumns.STARRED, starred ? 1 : 0);
1095
1096 new ContentProviderTask.UpdateTask() {
1097 @Override
1098 protected void onPostExecute(Result result) {
1099 // TODO: handle errors?
1100 }
1101 }.run(mResolver, msg.uri, values, null /* selection*/, null /* selectionArgs */);
1102 }
1103
Mindy Pereira28e0c342012-02-17 15:05:13 -08001104 private void requestFolderRefresh() {
1105 if (mFolder != null) {
Mindy Pereirab7b33e02012-02-21 15:32:19 -08001106 if (mAsyncRefreshTask != null) {
1107 mAsyncRefreshTask.cancel(true);
1108 }
Paul Westbrook7e2a2a12012-06-27 13:52:40 -07001109 mAsyncRefreshTask = new AsyncRefreshTask(mContext, mFolder.refreshUri);
Mindy Pereirab7b33e02012-02-21 15:32:19 -08001110 mAsyncRefreshTask.execute();
Mindy Pereira28e0c342012-02-17 15:05:13 -08001111 }
1112 }
1113
Mindy Pereirafbe40192012-03-20 10:40:45 -07001114 /**
1115 * Confirm (based on user's settings) and delete a conversation from the conversation list and
1116 * from the database.
Vikram Aggarwal3d7ca9d2012-05-11 14:40:36 -07001117 * @param target the conversations to act upon
1118 * @param showDialog true if a confirmation dialog is to be shown, false otherwise.
1119 * @param confirmResource the resource ID of the string that is shown in the confirmation dialog
1120 * @param action the action to perform after animating the deletion of the conversations.
Mindy Pereirafbe40192012-03-20 10:40:45 -07001121 */
Vikram Aggarwal7f602f72012-04-30 16:04:06 -07001122 protected void confirmAndDelete(final Collection<Conversation> target, boolean showDialog,
1123 int confirmResource, final DestructiveAction action) {
Mindy Pereirafbe40192012-03-20 10:40:45 -07001124 if (showDialog) {
1125 final AlertDialog.OnClickListener onClick = new AlertDialog.OnClickListener() {
1126 @Override
1127 public void onClick(DialogInterface dialog, int which) {
Vikram Aggarwala8e43182012-09-13 12:55:10 -07001128 delete(0, target, action);
Mindy Pereirafbe40192012-03-20 10:40:45 -07001129 }
1130 };
Vikram Aggarwal3d7ca9d2012-05-11 14:40:36 -07001131 final CharSequence message = Utils.formatPlural(mContext, confirmResource,
1132 target.size());
Mindy Pereirafbe40192012-03-20 10:40:45 -07001133 new AlertDialog.Builder(mActivity.getActivityContext()).setMessage(message)
1134 .setPositiveButton(R.string.ok, onClick)
1135 .setNegativeButton(R.string.cancel, null)
1136 .create().show();
1137 } else {
Vikram Aggarwala8e43182012-09-13 12:55:10 -07001138 delete(0, target, action);
Mindy Pereirafbe40192012-03-20 10:40:45 -07001139 }
1140 }
1141
Vikram Aggarwal4f4782b2012-05-30 08:39:09 -07001142 @Override
Vikram Aggarwala8e43182012-09-13 12:55:10 -07001143 public void delete(int actionId, final Collection<Conversation> target,
1144 final DestructiveAction action) {
Vikram Aggarwal192fac12012-07-25 16:44:55 -07001145 // Order of events is critical! The Conversation View Fragment must be notified
1146 // of the next conversation with showConversation(next) *before* the conversation list
1147 // fragment has a chance to delete the conversation, animating it away.
1148
Vikram Aggarwald503df42012-05-11 10:13:35 -07001149 // Update the conversation fragment if the current conversation is deleted.
Andy Huang8f6b0062012-07-31 15:36:31 -07001150 showNextConversation(target);
Vikram Aggarwal192fac12012-07-25 16:44:55 -07001151 // The conversation list deletes and performs the action if it exists.
1152 final ConversationListFragment convListFragment = getConversationListFragment();
1153 if (convListFragment != null) {
1154 LogUtils.d(LOG_TAG, "AAC.requestDelete: ListFragment is handling delete.");
Vikram Aggarwala8e43182012-09-13 12:55:10 -07001155 convListFragment.requestDelete(actionId, target, action);
Vikram Aggarwal192fac12012-07-25 16:44:55 -07001156 return;
1157 }
Vikram Aggarwald503df42012-05-11 10:13:35 -07001158 // No visible UI element handled it on our behalf. Perform the action ourself.
1159 action.performAction();
1160 }
1161
1162 /**
1163 * Requests that the action be performed and the UI state is updated to reflect the new change.
1164 * @param target
1165 * @param action
1166 */
Vikram Aggarwal4f4782b2012-05-30 08:39:09 -07001167 private void requestUpdate(final Collection<Conversation> target,
Vikram Aggarwald503df42012-05-11 10:13:35 -07001168 final DestructiveAction action) {
1169 action.performAction();
1170 refreshConversationList();
Vikram Aggarwal75daee52012-04-30 11:13:09 -07001171 }
Vikram Aggarwal6fbc87a2012-03-15 15:24:00 -07001172
1173 @Override
Vikram Aggarwala55b36c2012-01-13 11:45:02 -08001174 public void onPrepareDialog(int id, Dialog dialog, Bundle bundle) {
1175 // TODO(viki): Auto-generated method stub
Vikram Aggarwala55b36c2012-01-13 11:45:02 -08001176 }
1177
1178 @Override
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -08001179 public boolean onPrepareOptionsMenu(Menu menu) {
Andy Huangd736a382012-08-29 13:08:58 -07001180 return mActionBarView.onPrepareOptionsMenu(menu);
Vikram Aggarwala55b36c2012-01-13 11:45:02 -08001181 }
1182
Mindy Pereira68f2e222012-03-07 10:36:54 -08001183 @Override
1184 public void onPause() {
1185 isLoaderInitialized = false;
Paul Westbrook6ead20d2012-03-19 14:48:14 -07001186 enableNotifications();
Paul Westbrook94e440d2012-02-24 11:03:47 -08001187 }
1188
Vikram Aggarwala55b36c2012-01-13 11:45:02 -08001189 @Override
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -08001190 public void onResume() {
Paul Westbrook6ead20d2012-03-19 14:48:14 -07001191 // Register the receiver that will prevent the status receiver from
1192 // displaying its notification icon as long as we're running.
1193 // The SupressNotificationReceiver will block the broadcast if we're looking at the folder
1194 // that the notification was received for.
1195 disableNotifications();
Andy Huang1ee96b22012-08-24 20:19:53 -07001196
1197 mSafeToModifyFragments = true;
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -08001198 }
1199
1200 @Override
1201 public void onSaveInstanceState(Bundle outState) {
Vikram Aggarwal649b9ea2012-08-27 12:15:20 -07001202 mViewMode.handleSaveInstanceState(outState);
Vikram Aggarwal6c511582012-02-27 10:59:47 -08001203 if (mAccount != null) {
1204 LogUtils.d(LOG_TAG, "Saving the account now");
1205 outState.putParcelable(SAVED_ACCOUNT, mAccount);
1206 }
Mindy Pereira5e478d22012-03-26 18:04:58 -07001207 if (mFolder != null) {
1208 outState.putParcelable(SAVED_FOLDER, mFolder);
Vikram Aggarwal8b152632012-02-03 14:58:45 -08001209 }
Vikram Aggarwalf3341402012-08-07 10:09:38 -07001210 // If this is a search activity, let's store the search query term as well.
1211 if (ConversationListContext.isSearchResult(mConvListContext)) {
1212 outState.putString(SAVED_QUERY, mConvListContext.searchQuery);
1213 }
Vikram Aggarwal49e0e992012-09-21 13:53:15 -07001214 if (mCurrentConversation != null && mViewMode.isConversationMode()) {
Mindy Pereira26f23fc2012-03-27 10:26:04 -07001215 outState.putParcelable(SAVED_CONVERSATION, mCurrentConversation);
1216 }
Andy Huang4556a442012-03-30 16:42:05 -07001217 if (!mSelectedSet.isEmpty()) {
Vikram Aggarwalcabd3f22012-04-19 10:14:41 -07001218 outState.putParcelable(SAVED_SELECTED_SET, mSelectedSet);
Andy Huang4556a442012-03-30 16:42:05 -07001219 }
Mindy Pereirad33674992012-06-25 16:26:30 -07001220 if (mToastBar.getVisibility() == View.VISIBLE) {
1221 outState.putParcelable(SAVED_TOAST_BAR_OP, mToastBar.getOperation());
1222 }
Vikram Aggarwal649b9ea2012-08-27 12:15:20 -07001223 final ConversationListFragment convListFragment = getConversationListFragment();
Mindy Pereirad33674992012-06-25 16:26:30 -07001224 if (convListFragment != null) {
Vikram Aggarwal649b9ea2012-08-27 12:15:20 -07001225 convListFragment.getAnimatedAdapter().onSaveInstanceState(outState);
Mindy Pereirad33674992012-06-25 16:26:30 -07001226 }
Andy Huang1ee96b22012-08-24 20:19:53 -07001227 mSafeToModifyFragments = false;
Vikram Aggarwal649b9ea2012-08-27 12:15:20 -07001228 outState.putString(SAVED_HIERARCHICAL_FOLDER,
1229 (mFolderListFolder != null) ? Folder.toString(mFolderListFolder) : null);
Andy Huang1ee96b22012-08-24 20:19:53 -07001230 }
1231
1232 /**
1233 * @see #mSafeToModifyFragments
1234 */
1235 protected boolean safeToModifyFragments() {
1236 return mSafeToModifyFragments;
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -08001237 }
1238
1239 @Override
Mindy Pereira68f2e222012-03-07 10:36:54 -08001240 public void onSearchRequested(String query) {
1241 Intent intent = new Intent();
1242 intent.setAction(Intent.ACTION_SEARCH);
1243 intent.putExtra(ConversationListContext.EXTRA_SEARCH_QUERY, query);
1244 intent.putExtra(Utils.EXTRA_ACCOUNT, mAccount);
1245 intent.setComponent(mActivity.getComponentName());
Vikram Aggarwalb17cbc02012-04-06 15:41:46 -07001246 mActionBarView.collapseSearch();
Mindy Pereira68f2e222012-03-07 10:36:54 -08001247 mActivity.startActivity(intent);
Vikram Aggarwala55b36c2012-01-13 11:45:02 -08001248 }
1249
1250 @Override
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -08001251 public void onStop() {
Mindy Pereira49e5dbe2012-07-12 11:47:54 -07001252 if (mEnableShareIntents != null) {
1253 mEnableShareIntents.cancel(true);
1254 }
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -08001255 }
1256
Andy Huang632721e2012-04-11 16:57:26 -07001257 @Override
1258 public void onDestroy() {
1259 // unregister the ViewPager's observer on the conversation cursor
1260 mPagerController.onDestroy();
Mindy Pereira641de652012-08-02 15:21:50 -07001261 mActionBarView.onDestroy();
Vikram Aggarwal7c401b72012-08-13 16:43:47 -07001262 mRecentFolderList.destroy();
Andy Huang4e0158f2012-08-07 21:06:01 -07001263 mDestroyed = true;
Andy Huang632721e2012-04-11 16:57:26 -07001264 }
1265
Vikram Aggarwalfa131a22012-02-02 13:56:22 -08001266 /**
Mindy Pereira161f50d2012-02-28 15:47:19 -08001267 * {@inheritDoc} Subclasses must override this to listen to mode changes
1268 * from the ViewMode. Subclasses <b>must</b> call the parent's
1269 * onViewModeChanged since the parent will handle common state changes.
Vikram Aggarwalfa131a22012-02-02 13:56:22 -08001270 */
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -08001271 @Override
Vikram Aggarwalfa131a22012-02-02 13:56:22 -08001272 public void onViewModeChanged(int newMode) {
Vikram Aggarwal49e0e992012-09-21 13:53:15 -07001273 // When we step away from the conversation mode, we don't have a current conversation
1274 // anymore. Let's blank it out so clients calling getCurrentConversation are not misled.
1275 if (!ViewMode.isConversationMode(newMode)) {
1276 setCurrentConversation(null);
1277 }
Vikram Aggarwala55b36c2012-01-13 11:45:02 -08001278 }
1279
Andy Huang3825f3d2012-08-29 16:44:12 -07001280 public void disablePagerUpdates() {
1281 mPagerController.stopListening();
1282 }
1283
Andy Huang4e0158f2012-08-07 21:06:01 -07001284 public boolean isDestroyed() {
1285 return mDestroyed;
1286 }
1287
mindyp54f120f2012-08-28 13:10:33 -07001288 @Override
1289 public void commitDestructiveActions(boolean animate) {
mindypc6adce32012-08-22 18:46:42 -07001290 ConversationListFragment fragment = getConversationListFragment();
Mindy Pereira1e2573b2012-04-17 14:34:36 -07001291 if (fragment != null) {
mindypc6adce32012-08-22 18:46:42 -07001292 fragment.commitDestructiveActions(animate);
Mindy Pereira1e2573b2012-04-17 14:34:36 -07001293 }
1294 }
1295
Vikram Aggarwala55b36c2012-01-13 11:45:02 -08001296 @Override
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -08001297 public void onWindowFocusChanged(boolean hasFocus) {
Vikram Aggarwal69b5c302012-09-05 11:11:13 -07001298 final ConversationListFragment convList = getConversationListFragment();
Paul Westbrook9f119c72012-04-24 16:10:59 -07001299 if (hasFocus && convList != null && convList.isVisible()) {
1300 // The conversation list is visible.
Vikram Aggarwal69b5c302012-09-05 11:11:13 -07001301 informCursorVisiblity(true);
Paul Westbrook9f119c72012-04-24 16:10:59 -07001302 }
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -08001303 }
1304
Vikram Aggarwalca716f12012-08-20 11:11:48 -07001305 /**
1306 * Set the account, and carry out all the account-related changes that rely on this.
1307 * @param account
1308 */
1309 // TODO(viki): Two different methods do the same thing. Resolve
1310 // {@link #setAccount(Account)} and {@link #switchAccount(Account, boolean)}
Mindy Pereira75181e82012-04-18 08:17:13 -07001311 private void setAccount(Account account) {
Andy Huangb9ca9792012-05-18 15:31:49 -07001312 if (account == null) {
1313 LogUtils.w(LOG_TAG, new Error(),
1314 "AAC ignoring null (presumably invalid) account restoration");
1315 return;
1316 }
Andy Huangb1148412012-05-19 00:16:30 -07001317 LogUtils.d(LOG_TAG, "AbstractActivityController.setAccount(): account = %s", account.uri);
Vikram Aggarwal91e87372012-05-18 15:36:04 -07001318 mAccount = account;
Vikram Aggarwalca716f12012-08-20 11:11:48 -07001319 // Only change AAC state here. Do *not* modify any other object's state. The object
1320 // should listen on account changes.
1321 restartOptionalLoader(LOADER_RECENT_FOLDERS);
1322 mActivity.invalidateOptionsMenu();
1323 disableNotificationsOnAccountChange(mAccount);
1324 restartOptionalLoader(LOADER_ACCOUNT_UPDATE_CURSOR);
1325 MailAppProvider.getInstance().setLastViewedAccount(mAccount.uri.toString());
1326
Vikram Aggarwal91e87372012-05-18 15:36:04 -07001327 if (account.settings == null) {
1328 LogUtils.w(LOG_TAG, new Error(), "AAC ignoring account with null settings.");
1329 return;
1330 }
Vikram Aggarwal7c401b72012-08-13 16:43:47 -07001331 mAccountObservers.notifyChanged();
Mindy Pereira75181e82012-04-18 08:17:13 -07001332 }
1333
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -08001334 /**
Mindy Pereira161f50d2012-02-28 15:47:19 -08001335 * Restore the state from the previous bundle. Subclasses should call this
1336 * method from the parent class, since it performs important UI
1337 * initialization.
1338 *
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -08001339 * @param savedState
1340 */
Andy Huang632721e2012-04-11 16:57:26 -07001341 @Override
1342 public void onRestoreInstanceState(Bundle savedState) {
1343 LogUtils.d(LOG_TAG, "IN AAC.onRestoreInstanceState");
1344 if (savedState.containsKey(SAVED_CONVERSATION)) {
1345 // Open the conversation.
Paul Westbrook534e4a22012-04-25 03:46:29 -07001346 final Conversation conversation =
1347 (Conversation)savedState.getParcelable(SAVED_CONVERSATION);
1348 if (conversation != null && conversation.position < 0) {
1349 // Set the position to 0 on this conversation, as we don't know where it is
1350 // in the list
1351 conversation.position = 0;
1352 }
Andy Huanged4fdf02012-07-26 17:12:50 -07001353 showConversation(conversation);
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -08001354 }
Mindy Pereira967ede62012-03-22 09:29:09 -07001355
Mindy Pereirad33674992012-06-25 16:26:30 -07001356 if (savedState.containsKey(SAVED_TOAST_BAR_OP)) {
1357 ToastBarOperation op = ((ToastBarOperation) savedState
1358 .getParcelable(SAVED_TOAST_BAR_OP));
1359 if (op != null) {
1360 if (op.getType() == ToastBarOperation.UNDO) {
1361 onUndoAvailable(op);
Andrew Sapperstein9d7519d2012-07-16 14:03:53 -07001362 } else if (op.getType() == ToastBarOperation.ERROR) {
1363 onError(mFolder, true);
Mindy Pereirad33674992012-06-25 16:26:30 -07001364 }
1365 }
1366 }
Vikram Aggarwal649b9ea2012-08-27 12:15:20 -07001367 final String folderString = savedState.getString(SAVED_HIERARCHICAL_FOLDER, null);
1368 if (!TextUtils.isEmpty(folderString)) {
1369 mFolderListFolder = Folder.fromString(folderString);
1370 }
1371 final ConversationListFragment convListFragment = getConversationListFragment();
Mindy Pereirad33674992012-06-25 16:26:30 -07001372 if (convListFragment != null) {
Vikram Aggarwal649b9ea2012-08-27 12:15:20 -07001373 convListFragment.getAnimatedAdapter().onRestoreInstanceState(savedState);
Mindy Pereirad33674992012-06-25 16:26:30 -07001374 }
Mindy Pereira967ede62012-03-22 09:29:09 -07001375 /**
1376 * Restore the state of selected conversations. This needs to be done after the correct mode
1377 * is set and the action bar is fully initialized. If not, several key pieces of state
1378 * information will be missing, and the split views may not be initialized correctly.
1379 * @param savedState
1380 */
Andy Huang4556a442012-03-30 16:42:05 -07001381 restoreSelectedConversations(savedState);
Andy Huang632721e2012-04-11 16:57:26 -07001382 }
1383
Vikram Aggarwal649b9ea2012-08-27 12:15:20 -07001384 /**
1385 * Handle an intent to open the app. This method is called only when there is no saved state,
1386 * so we need to set state that wasn't set before. It is correct to change the viewmode here
1387 * since it has not been previously set.
1388 * @param intent
1389 */
Andy Huang632721e2012-04-11 16:57:26 -07001390 private void handleIntent(Intent intent) {
1391 boolean handled = false;
1392 if (Intent.ACTION_VIEW.equals(intent.getAction())) {
1393 if (intent.hasExtra(Utils.EXTRA_ACCOUNT)) {
Vikram Aggarwal649b9ea2012-08-27 12:15:20 -07001394 setAccount(Account.newinstance(intent.getStringExtra(Utils.EXTRA_ACCOUNT)));
Andy Huang632721e2012-04-11 16:57:26 -07001395 }
Andy Huangb9ca9792012-05-18 15:31:49 -07001396 if (mAccount == null) {
1397 return;
Andy Huang632721e2012-04-11 16:57:26 -07001398 }
Vikram Aggarwal1a249e02012-08-03 16:19:33 -07001399 final boolean isConversationMode = intent.hasExtra(Utils.EXTRA_CONVERSATION);
Vikram Aggarwal649b9ea2012-08-27 12:15:20 -07001400 if (isConversationMode && mViewMode.getMode() == ViewMode.UNKNOWN) {
Vikram Aggarwal1a249e02012-08-03 16:19:33 -07001401 mViewMode.enterConversationMode();
1402 } else {
1403 mViewMode.enterConversationListMode();
1404 }
Vikram Aggarwal1672ff82012-09-21 10:15:22 -07001405 final Folder folder = intent.hasExtra(Utils.EXTRA_FOLDER) ?
1406 Folder.fromString(intent.getStringExtra(Utils.EXTRA_FOLDER)) : null;
Andy Huang632721e2012-04-11 16:57:26 -07001407 if (folder != null) {
1408 onFolderChanged(folder);
1409 handled = true;
1410 }
1411
Vikram Aggarwal1a249e02012-08-03 16:19:33 -07001412 if (isConversationMode) {
Andy Huang632721e2012-04-11 16:57:26 -07001413 // Open the conversation.
1414 LogUtils.d(LOG_TAG, "SHOW THE CONVERSATION at %s",
1415 intent.getParcelableExtra(Utils.EXTRA_CONVERSATION));
Paul Westbrooka9161912012-04-24 10:10:04 -07001416 final Conversation conversation =
1417 (Conversation)intent.getParcelableExtra(Utils.EXTRA_CONVERSATION);
1418 if (conversation != null && conversation.position < 0) {
1419 // Set the position to 0 on this conversation, as we don't know where it is
1420 // in the list
1421 conversation.position = 0;
1422 }
Andy Huang980aaea2012-07-26 17:22:19 -07001423 showConversation(conversation);
Andy Huang632721e2012-04-11 16:57:26 -07001424 handled = true;
1425 }
1426
1427 if (!handled) {
Vikram Aggarwal0c3c2052012-09-21 11:06:28 -07001428 // We have an account, but nothing else: load the default inbox.
Andy Huang632721e2012-04-11 16:57:26 -07001429 loadAccountInbox();
1430 }
1431 } else if (Intent.ACTION_SEARCH.equals(intent.getAction())) {
1432 if (intent.hasExtra(Utils.EXTRA_ACCOUNT)) {
1433 // Save this search query for future suggestions.
1434 final String query = intent.getStringExtra(SearchManager.QUERY);
1435 final String authority = mContext.getString(R.string.suggestions_authority);
Vikram Aggarwal2b703c62012-09-18 13:54:15 -07001436 final SearchRecentSuggestions suggestions = new SearchRecentSuggestions(
Andy Huang632721e2012-04-11 16:57:26 -07001437 mContext, authority, SuggestionsProvider.MODE);
1438 suggestions.saveRecentQuery(query, null);
Mindy Pereiraac254822012-06-18 10:46:43 -07001439 if (Utils.showTwoPaneSearchResults(mActivity.getActivityContext())) {
1440 mViewMode.enterSearchResultsConversationMode();
1441 } else {
1442 mViewMode.enterSearchResultsListMode();
1443 }
Andy Huang632721e2012-04-11 16:57:26 -07001444 setAccount((Account) intent.getParcelableExtra(Utils.EXTRA_ACCOUNT));
Andy Huang632721e2012-04-11 16:57:26 -07001445 restartOptionalLoader(LOADER_RECENT_FOLDERS);
Andy Huang632721e2012-04-11 16:57:26 -07001446 fetchSearchFolder(intent);
1447 } else {
1448 LogUtils.e(LOG_TAG, "Missing account extra from search intent. Finishing");
1449 mActivity.finish();
1450 }
1451 }
1452 if (mAccount != null) {
1453 restartOptionalLoader(LOADER_ACCOUNT_UPDATE_CURSOR);
1454 }
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -08001455 }
1456
Andy Huang4556a442012-03-30 16:42:05 -07001457 /**
1458 * Copy any selected conversations stored in the saved bundle into our selection set,
1459 * triggering {@link ConversationSetObserver} callbacks as our selection set changes.
1460 *
1461 */
Vikram Aggarwal4eb52712012-06-19 16:24:50 -07001462 private final void restoreSelectedConversations(Bundle savedState) {
Mindy Pereira967ede62012-03-22 09:29:09 -07001463 if (savedState == null) {
Andy Huang4556a442012-03-30 16:42:05 -07001464 mSelectedSet.clear();
Mindy Pereira967ede62012-03-22 09:29:09 -07001465 return;
1466 }
Vikram Aggarwalcabd3f22012-04-19 10:14:41 -07001467 final ConversationSelectionSet selectedSet = savedState.getParcelable(SAVED_SELECTED_SET);
Andy Huang4556a442012-03-30 16:42:05 -07001468 if (selectedSet == null || selectedSet.isEmpty()) {
1469 mSelectedSet.clear();
Mindy Pereira967ede62012-03-22 09:29:09 -07001470 return;
1471 }
Andy Huang632721e2012-04-11 16:57:26 -07001472
1473 // putAll will take care of calling our registered onSetPopulated method
Vikram Aggarwalcabd3f22012-04-19 10:14:41 -07001474 mSelectedSet.putAll(selectedSet);
Mindy Pereira967ede62012-03-22 09:29:09 -07001475 }
1476
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -08001477 @Override
Andy Huang5895f7b2012-06-01 17:07:20 -07001478 public SubjectDisplayChanger getSubjectDisplayChanger() {
1479 return mActionBarView;
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -08001480 }
1481
Vikram Aggarwal17f373e2012-09-17 15:49:32 -07001482 private final void showConversation(Conversation conversation) {
Andy Huang1ee96b22012-08-24 20:19:53 -07001483 showConversation(conversation, false /* inLoaderCallbacks */);
1484 }
1485
Vikram Aggarwalec5cbf72012-03-08 15:10:35 -08001486 /**
Vikram Aggarwal49e0e992012-09-21 13:53:15 -07001487 * Show the conversation provided in the arguments. It is safe to pass a null conversation
1488 * object, which is a signal to back out of conversation view mode.
1489 * Child classes must call super.showConversation() <b>before</b> their own implementations.
1490 * @param conversation
1491 * @param inLoaderCallbacks true if the method is called as a result of
1492 * {@link #onLoadFinished(Loader, Cursor)}
Vikram Aggarwalec5cbf72012-03-08 15:10:35 -08001493 */
Andy Huang1ee96b22012-08-24 20:19:53 -07001494 protected void showConversation(Conversation conversation, boolean inLoaderCallbacks) {
Vikram Aggarwalc67182d2012-04-03 14:35:06 -07001495 // Set the current conversation just in case it wasn't already set.
1496 setCurrentConversation(conversation);
Vikram Aggarwal0f142732012-08-24 09:39:34 -07001497 // Add the folder that we were viewing to the recent folders list.
1498 // TODO: this may need to be fine tuned. If this is the signal that is indicating that
1499 // the list is shown to the user, this could fire in one pane if the user goes directly
1500 // to a conversation
1501 updateRecentFolderList();
Vikram Aggarwalec5cbf72012-03-08 15:10:35 -08001502 }
1503
Vikram Aggarwale128fc22012-04-04 12:33:34 -07001504 /**
Paul Westbrook2d50bcd2012-04-10 11:53:47 -07001505 * Children can override this method, but they must call super.showWaitForInitialization().
1506 * {@inheritDoc}
1507 */
1508 @Override
1509 public void showWaitForInitialization() {
1510 mViewMode.enterWaitingForInitializationMode();
1511 }
1512
1513 @Override
1514 public void hideWaitForInitialization() {
1515 }
1516
1517 @Override
1518 public void updateWaitMode() {
1519 final FragmentManager manager = mActivity.getFragmentManager();
1520 final WaitFragment waitFragment =
Vikram Aggarwal6902dcf2012-04-11 08:57:42 -07001521 (WaitFragment)manager.findFragmentByTag(TAG_WAIT);
Paul Westbrook2d50bcd2012-04-10 11:53:47 -07001522 if (waitFragment != null) {
1523 waitFragment.updateAccount(mAccount);
1524 }
1525 }
1526
Vikram Aggarwal48b2a6c2012-05-29 14:09:27 -07001527 /**
1528 * Returns true if we are waiting for the account to sync, and cannot show any folders or
1529 * conversation for the current account yet.
Andy Huang839ada22012-07-20 15:48:40 -07001530 *
Vikram Aggarwal48b2a6c2012-05-29 14:09:27 -07001531 */
Paul Westbrook2d50bcd2012-04-10 11:53:47 -07001532 public boolean inWaitMode() {
1533 final FragmentManager manager = mActivity.getFragmentManager();
1534 final WaitFragment waitFragment =
Vikram Aggarwal6902dcf2012-04-11 08:57:42 -07001535 (WaitFragment)manager.findFragmentByTag(TAG_WAIT);
Paul Westbrook2d50bcd2012-04-10 11:53:47 -07001536 if (waitFragment != null) {
1537 final Account fragmentAccount = waitFragment.getAccount();
1538 return fragmentAccount.uri.equals(mAccount.uri) &&
1539 mViewMode.getMode() == ViewMode.WAITING_FOR_ACCOUNT_INITIALIZATION;
1540 }
1541 return false;
1542 }
1543
1544 /**
Vikram Aggarwale128fc22012-04-04 12:33:34 -07001545 * Children can override this method, but they must call super.showConversationList().
1546 * {@inheritDoc}
1547 */
1548 @Override
1549 public void showConversationList(ConversationListContext listContext) {
1550 }
1551
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -08001552 @Override
Vikram Aggarwal17f373e2012-09-17 15:49:32 -07001553 public final void onConversationSelected(Conversation conversation, boolean inLoaderCallbacks) {
mindypaa55bc92012-08-24 09:49:56 -07001554 // Only animate destructive actions if we are going to be showing the
1555 // conversation list when we show the next conversation.
1556 commitDestructiveActions(Utils.useTabletUI(mContext));
Andy Huang1ee96b22012-08-24 20:19:53 -07001557 showConversation(conversation, inLoaderCallbacks);
1558 }
1559
1560 @Override
1561 public Conversation getCurrentConversation() {
1562 return mCurrentConversation;
Vikram Aggarwal7d602882012-02-07 15:01:20 -08001563 }
Mindy Pereira555140c2012-02-15 14:55:29 -08001564
Vikram Aggarwalc67182d2012-04-03 14:35:06 -07001565 /**
1566 * Set the current conversation. This is the conversation on which all actions are performed.
1567 * Do not modify mCurrentConversation except through this method, which makes it easy to
1568 * perform common actions associated with changing the current conversation.
1569 * @param conversation
1570 */
Andy Huang632721e2012-04-11 16:57:26 -07001571 @Override
1572 public void setCurrentConversation(Conversation conversation) {
Vikram Aggarwal17f373e2012-09-17 15:49:32 -07001573 // Must be the first call because this sets conversation.position if a cursor is available.
Vikram Aggarwal135fd022012-04-11 14:44:15 -07001574 mTracker.initialize(mCurrentConversation);
Vikram Aggarwal17f373e2012-09-17 15:49:32 -07001575 mCurrentConversation = conversation;
Andy Huang7d646122012-09-05 19:41:44 -07001576
1577 if (mCurrentConversation != null) {
Yorke Leef807ba72012-09-20 17:18:05 -07001578 mActionBarView.setCurrentConversation(mCurrentConversation);
Andy Huang7d646122012-09-05 19:41:44 -07001579 getSubjectDisplayChanger().setSubject(mCurrentConversation.subject);
Yorke Leef807ba72012-09-20 17:18:05 -07001580 mActivity.invalidateOptionsMenu();
Andy Huang7d646122012-09-05 19:41:44 -07001581 }
Mindy Pereira5040f1a2012-03-20 10:14:06 -07001582 }
1583
Vikram Aggarwala9b93f32012-02-23 14:51:58 -08001584 /**
1585 * {@inheritDoc}
1586 */
Mindy Pereira555140c2012-02-15 14:55:29 -08001587 @Override
Vikram Aggarwal7dedb952012-02-16 16:10:23 -08001588 public Loader<Cursor> onCreateLoader(int id, Bundle args) {
Vikram Aggarwalfa4b47e2012-03-09 13:02:46 -08001589 switch (id) {
1590 case LOADER_ACCOUNT_CURSOR:
Paul Westbrookc2074c42012-03-22 15:26:58 -07001591 return new CursorLoader(mContext, MailAppProvider.getAccountsUri(),
Vikram Aggarwalfa4b47e2012-03-09 13:02:46 -08001592 UIProvider.ACCOUNTS_PROJECTION, null, null, null);
1593 case LOADER_FOLDER_CURSOR:
Paul Westbrookc7a070f2012-04-12 01:46:41 -07001594 final CursorLoader loader = new CursorLoader(mContext, mFolder.uri,
1595 UIProvider.FOLDERS_PROJECTION, null, null, null);
1596 loader.setUpdateThrottle(mFolderItemUpdateDelayMs);
1597 return loader;
Vikram Aggarwalfa4b47e2012-03-09 13:02:46 -08001598 case LOADER_RECENT_FOLDERS:
Paul Westbrook91d10502012-04-13 12:01:39 -07001599 if (mAccount != null && mAccount.recentFolderListUri != null) {
Paul Westbrookea4ee202012-03-12 14:12:33 -07001600 return new CursorLoader(mContext, mAccount.recentFolderListUri,
1601 UIProvider.FOLDERS_PROJECTION, null, null, null);
1602 }
1603 break;
Mindy Pereiraab486362012-03-21 18:18:53 -07001604 case LOADER_ACCOUNT_INBOX:
Vikram Aggarwal025eba82012-05-08 10:45:30 -07001605 final Uri defaultInbox = Settings.getDefaultInboxUri(mAccount.settings);
Vikram Aggarwal1e57e672012-05-07 14:48:24 -07001606 final Uri inboxUri = defaultInbox.equals(Uri.EMPTY) ?
1607 mAccount.folderListUri : defaultInbox;
Paul Westbrook7496e822012-04-24 09:50:54 -07001608 LogUtils.d(LOG_TAG, "Loading the default inbox: %s", inboxUri);
Paul Westbrook1220b6d2012-04-10 00:48:00 -07001609 if (inboxUri != null) {
1610 return new CursorLoader(mContext, inboxUri, UIProvider.FOLDERS_PROJECTION, null,
1611 null, null);
1612 }
1613 break;
Mindy Pereiraab486362012-03-21 18:18:53 -07001614 case LOADER_SEARCH:
1615 return Folder.forSearchResults(mAccount,
1616 args.getString(ConversationListContext.EXTRA_SEARCH_QUERY),
1617 mActivity.getActivityContext());
Paul Westbrook2d50bcd2012-04-10 11:53:47 -07001618 case LOADER_ACCOUNT_UPDATE_CURSOR:
1619 return new CursorLoader(mContext, mAccount.uri, UIProvider.ACCOUNTS_PROJECTION,
1620 null, null, null);
Vikram Aggarwalfa4b47e2012-03-09 13:02:46 -08001621 default:
Paul Westbrookad6a2752012-04-04 16:58:13 -07001622 LogUtils.wtf(LOG_TAG, "Loader returned unexpected id: %d", id);
Mindy Pereira5cb0c3e2012-02-22 15:22:47 -08001623 }
1624 return null;
Vikram Aggarwal7dedb952012-02-16 16:10:23 -08001625 }
1626
Paul Westbrookb1f573c2012-04-06 11:38:28 -07001627 @Override
1628 public void onLoaderReset(Loader<Cursor> loader) {
1629
1630 }
1631
Andy Huangf9a73482012-03-13 15:54:02 -07001632 /**
1633 * {@link LoaderManager} currently has a bug in
1634 * {@link LoaderManager#restartLoader(int, Bundle, android.app.LoaderManager.LoaderCallbacks)}
1635 * where, if a previous onCreateLoader returned a null loader, this method will NPE. Work around
1636 * this bug by destroying any loaders that may have been created as null (essentially because
1637 * they are optional loads, and may not apply to a particular account).
1638 * <p>
1639 * A simple null check before restarting a loader will not work, because that would not
1640 * give the controller a chance to invalidate UI corresponding the prior loader result.
1641 *
1642 * @param id loader ID to safely restart
Andy Huangf9a73482012-03-13 15:54:02 -07001643 */
Vikram Aggarwal94c94de2012-04-04 15:38:28 -07001644 private void restartOptionalLoader(int id) {
Andy Huangf9a73482012-03-13 15:54:02 -07001645 final LoaderManager lm = mActivity.getLoaderManager();
1646 lm.destroyLoader(id);
Vikram Aggarwal94c94de2012-04-04 15:38:28 -07001647 lm.restartLoader(id, Bundle.EMPTY, this);
1648 }
1649
Andy Huang632721e2012-04-11 16:57:26 -07001650 @Override
1651 public void registerConversationListObserver(DataSetObserver observer) {
1652 mConversationListObservable.registerObserver(observer);
1653 }
1654
1655 @Override
1656 public void unregisterConversationListObserver(DataSetObserver observer) {
1657 mConversationListObservable.unregisterObserver(observer);
1658 }
1659
Andy Huang090db1e2012-07-25 13:25:28 -07001660 @Override
1661 public void registerFolderObserver(DataSetObserver observer) {
1662 mFolderObservable.registerObserver(observer);
1663 }
1664
1665 @Override
1666 public void unregisterFolderObserver(DataSetObserver observer) {
1667 mFolderObservable.unregisterObserver(observer);
1668 }
1669
Vikram Aggarwal60069912012-07-24 14:26:09 -07001670 /**
1671 * Returns true if the number of accounts is different, or if the current account has been
1672 * removed from the device
1673 * @param accountCursor
1674 * @return
1675 */
Paul Westbrook23b74b92012-02-29 11:36:12 -08001676 private boolean accountsUpdated(Cursor accountCursor) {
1677 // Check to see if the current account hasn't been set, or the account cursor is empty
1678 if (mAccount == null || !accountCursor.moveToFirst()) {
Vikram Aggarwal6c511582012-02-27 10:59:47 -08001679 return true;
Paul Westbrook23b74b92012-02-29 11:36:12 -08001680 }
1681
1682 // Check to see if the number of accounts are different, from the number we saw on the last
1683 // updated
1684 if (mCurrentAccountUris.size() != accountCursor.getCount()) {
1685 return true;
1686 }
1687
1688 // Check to see if the account list is different or if the current account is not found in
1689 // the cursor.
1690 boolean foundCurrentAccount = false;
Vikram Aggarwal7dedb952012-02-16 16:10:23 -08001691 do {
Paul Westbrook23b74b92012-02-29 11:36:12 -08001692 final Uri accountUri =
1693 Uri.parse(accountCursor.getString(UIProvider.ACCOUNT_URI_COLUMN));
1694 if (!foundCurrentAccount && mAccount.uri.equals(accountUri)) {
1695 foundCurrentAccount = true;
1696 }
Vikram Aggarwal60069912012-07-24 14:26:09 -07001697 // Is there a new account that we do not know about?
Paul Westbrook23b74b92012-02-29 11:36:12 -08001698 if (!mCurrentAccountUris.contains(accountUri)) {
1699 return true;
1700 }
Vikram Aggarwal7dedb952012-02-16 16:10:23 -08001701 } while (accountCursor.moveToNext());
Paul Westbrook23b74b92012-02-29 11:36:12 -08001702
1703 // As long as we found the current account, the list hasn't been updated
1704 return !foundCurrentAccount;
Vikram Aggarwal7dedb952012-02-16 16:10:23 -08001705 }
1706
1707 /**
Vikram Aggarwal60069912012-07-24 14:26:09 -07001708 * Updates accounts for the app. If the current account is missing, the first
1709 * account in the list is set to the current account (we <em>have</em> to choose something).
Mindy Pereira161f50d2012-02-28 15:47:19 -08001710 *
Vikram Aggarwal6c511582012-02-27 10:59:47 -08001711 * @param accounts cursor into the AccountCache
Vikram Aggarwal7dedb952012-02-16 16:10:23 -08001712 * @return true if the update was successful, false otherwise
1713 */
Vikram Aggarwal60069912012-07-24 14:26:09 -07001714 private boolean updateAccounts(Cursor accounts) {
Vikram Aggarwal7dedb952012-02-16 16:10:23 -08001715 if (accounts == null || !accounts.moveToFirst()) {
1716 return false;
1717 }
Paul Westbrook23b74b92012-02-29 11:36:12 -08001718
Vikram Aggarwala9b93f32012-02-23 14:51:58 -08001719 final Account[] allAccounts = Account.getAllAccounts(accounts);
Vikram Aggarwal60069912012-07-24 14:26:09 -07001720 // A match for the current account's URI in the list of accounts.
1721 Account currentFromList = null;
Paul Westbrook23b74b92012-02-29 11:36:12 -08001722
Vikram Aggarwal636c1a12012-09-22 16:02:40 -07001723 // Save the uris for the accounts and find the current account in the updated cursor.
Paul Westbrook23b74b92012-02-29 11:36:12 -08001724 mCurrentAccountUris.clear();
Vikram Aggarwal636c1a12012-09-22 16:02:40 -07001725 for (final Account account : allAccounts) {
Vikram Aggarwal60069912012-07-24 14:26:09 -07001726 LogUtils.d(LOG_TAG, "updateAccounts(%s)", account);
Paul Westbrook23b74b92012-02-29 11:36:12 -08001727 mCurrentAccountUris.add(account.uri);
Vikram Aggarwal60069912012-07-24 14:26:09 -07001728 if (mAccount != null && account.uri.equals(mAccount.uri)) {
1729 currentFromList = account;
1730 }
Paul Westbrook23b74b92012-02-29 11:36:12 -08001731 }
1732
Vikram Aggarwal60069912012-07-24 14:26:09 -07001733 // 1. current account is already set and is in allAccounts:
1734 // 1a. It has changed -> load the updated account.
1735 // 2b. It is unchanged -> no-op
Andy Huang0d647352012-03-21 21:48:16 -07001736 // 2. current account is set and is not in allAccounts -> pick first (acct was deleted?)
Vikram Aggarwal60069912012-07-24 14:26:09 -07001737 // 3. saved preference has an account -> pick that one
Andy Huang0d647352012-03-21 21:48:16 -07001738 // 4. otherwise just pick first
1739
Vikram Aggarwal60069912012-07-24 14:26:09 -07001740 boolean accountChanged = false;
1741 /// Assume case 4, initialize to first account, and see if we can find anything better.
1742 Account newAccount = allAccounts[0];
1743 if (currentFromList != null) {
1744 // Case 1: Current account exists but has changed
1745 if (!currentFromList.equals(mAccount)) {
1746 newAccount = currentFromList;
1747 accountChanged = true;
Andy Huang0d647352012-03-21 21:48:16 -07001748 }
Vikram Aggarwal60069912012-07-24 14:26:09 -07001749 // Case 1b: else, current account is unchanged: nothing to do.
Paul Westbrook23b74b92012-02-29 11:36:12 -08001750 } else {
Vikram Aggarwal60069912012-07-24 14:26:09 -07001751 // Case 2: Current account is not in allAccounts, the account needs to change.
1752 accountChanged = true;
1753 if (mAccount == null) {
1754 // Case 3: Check for last viewed account, and check if it exists in the list.
1755 final String lastAccountUri = MailAppProvider.getInstance().getLastViewedAccount();
1756 if (lastAccountUri != null) {
1757 for (final Account account : allAccounts) {
1758 if (lastAccountUri.equals(account.uri.toString())) {
1759 newAccount = account;
1760 break;
1761 }
Andy Huang0d647352012-03-21 21:48:16 -07001762 }
1763 }
1764 }
Paul Westbrook23b74b92012-02-29 11:36:12 -08001765 }
Vikram Aggarwal60069912012-07-24 14:26:09 -07001766 if (accountChanged) {
1767 onAccountChanged(newAccount);
1768 }
1769 // Whether we have updated the current account or not, we need to update the list of
1770 // accounts in the ActionBar.
Vikram Aggarwala9b93f32012-02-23 14:51:58 -08001771 mActionBarView.setAccounts(allAccounts);
1772 return (allAccounts.length > 0);
Vikram Aggarwal7dedb952012-02-16 16:10:23 -08001773 }
1774
Paul Westbrook6ead20d2012-03-19 14:48:14 -07001775 private void disableNotifications() {
1776 mNewEmailReceiver.activate(mContext, this);
1777 }
1778
1779 private void enableNotifications() {
1780 mNewEmailReceiver.deactivate();
1781 }
1782
1783 private void disableNotificationsOnAccountChange(Account account) {
1784 // If the new mail suppression receiver is activated for a different account, we want to
1785 // activate it for the new account.
1786 if (mNewEmailReceiver.activated() &&
1787 !mNewEmailReceiver.notificationsDisabledForAccount(account)) {
1788 // Deactivate the current receiver, otherwise multiple receivers may be registered.
1789 mNewEmailReceiver.deactivate();
1790 mNewEmailReceiver.activate(mContext, this);
1791 }
1792 }
1793
Vikram Aggarwala9b93f32012-02-23 14:51:58 -08001794 /**
1795 * {@inheritDoc}
1796 */
Vikram Aggarwal7dedb952012-02-16 16:10:23 -08001797 @Override
1798 public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
Mindy Pereira5cb0c3e2012-02-22 15:22:47 -08001799 // We want to reinitialize only if we haven't ever been initialized, or
1800 // if the current account has vanished.
Paul Westbrooke3e84292012-03-05 16:19:30 -08001801 if (data == null) {
Vikram Aggarwalfa4b47e2012-03-09 13:02:46 -08001802 LogUtils.e(LOG_TAG, "Received null cursor from loader id: %d", loader.getId());
Paul Westbrooke3e84292012-03-05 16:19:30 -08001803 }
Vikram Aggarwalfa4b47e2012-03-09 13:02:46 -08001804 switch (loader.getId()) {
1805 case LOADER_ACCOUNT_CURSOR:
Vikram Aggarwal636c1a12012-09-22 16:02:40 -07001806 if (data == null) {
1807 // Nothing useful to do if we have no valid data.
1808 break;
1809 }
1810 if (data.getCount() == 0) {
Paul Westbrook2388c5d2012-03-25 12:29:11 -07001811 // If an empty cursor is returned, the MailAppProvider is indicating that
1812 // no accounts have been specified. We want to navigate to the "add account"
1813 // activity that will handle the intent returned by the MailAppProvider
1814
1815 // If the MailAppProvider believes that all accounts have been loaded, and the
1816 // account list is still empty, we want to prompt the user to add an account
1817 final Bundle extras = data.getExtras();
1818 final boolean accountsLoaded =
1819 extras.getInt(AccountCursorExtraKeys.ACCOUNTS_LOADED) != 0;
1820
1821 if (accountsLoaded) {
1822 final Intent noAccountIntent = MailAppProvider.getNoAccountIntent(mContext);
1823 if (noAccountIntent != null) {
1824 mActivity.startActivityForResult(noAccountIntent,
1825 ADD_ACCOUNT_REQUEST_CODE);
1826 }
1827 }
1828 } else {
1829 final boolean accountListUpdated = accountsUpdated(data);
1830 if (!isLoaderInitialized || accountListUpdated) {
Vikram Aggarwal60069912012-07-24 14:26:09 -07001831 isLoaderInitialized = updateAccounts(data);
Paul Westbrook2388c5d2012-03-25 12:29:11 -07001832 }
Vikram Aggarwalfa4b47e2012-03-09 13:02:46 -08001833 }
1834 break;
Paul Westbrook2d50bcd2012-04-10 11:53:47 -07001835 case LOADER_ACCOUNT_UPDATE_CURSOR:
1836 // We have gotten an update for current account.
1837
Vikram Aggarwal60069912012-07-24 14:26:09 -07001838 // Make sure that this is an update for the current account
Paul Westbrook2d50bcd2012-04-10 11:53:47 -07001839 if (data != null && data.moveToFirst()) {
1840 final Account updatedAccount = new Account(data);
1841
1842 if (updatedAccount.uri.equals(mAccount.uri)) {
Paul Westbrookca08fc12012-07-31 12:01:15 -07001843 // Keep a reference to the previous settings object
1844 final Settings previousSettings = mAccount.settings;
1845
Vikram Aggarwal7d816002012-04-17 17:06:41 -07001846 // Update the controller's reference to the current account
Paul Westbrook2d50bcd2012-04-10 11:53:47 -07001847 mAccount = updatedAccount;
Vikram Aggarwaledc137c2012-04-24 13:40:58 -07001848 LogUtils.d(LOG_TAG, "AbstractActivityController.onLoadFinished(): "
1849 + "mAccount = %s", mAccount.uri);
Paul Westbrookca08fc12012-07-31 12:01:15 -07001850
1851 // Only notify about a settings change if something differs
1852 if (!Objects.equal(mAccount.settings, previousSettings)) {
Vikram Aggarwal7c401b72012-08-13 16:43:47 -07001853 mAccountObservers.notifyChanged();
Paul Westbrookca08fc12012-07-31 12:01:15 -07001854 }
Paul Westbrook2d50bcd2012-04-10 11:53:47 -07001855
1856 // Got an update for the current account
1857 final boolean inWaitingMode = inWaitMode();
1858 if (!updatedAccount.isAccountIntialized() && !inWaitingMode) {
1859 // Transition to waiting mode
1860 showWaitForInitialization();
Mindy Pereira830bdaf2012-07-11 12:59:55 -07001861 } else if (updatedAccount.isAccountIntialized()) {
1862 if (inWaitingMode) {
1863 // Dismiss waiting mode
1864 hideWaitForInitialization();
1865 }
Paul Westbrook2d50bcd2012-04-10 11:53:47 -07001866 } else if (!updatedAccount.isAccountIntialized() && inWaitingMode) {
1867 // Update the WaitFragment's account object
1868 updateWaitMode();
1869 }
1870 } else {
1871 LogUtils.e(LOG_TAG, "Got update for account: %s with current account: %s",
1872 updatedAccount.uri, mAccount.uri);
1873 // We need to restart the loader, so the correct account information will
1874 // be returned
1875 restartOptionalLoader(LOADER_ACCOUNT_UPDATE_CURSOR);
1876 }
1877 }
1878 break;
Vikram Aggarwalfa4b47e2012-03-09 13:02:46 -08001879 case LOADER_FOLDER_CURSOR:
Vikram Aggarwalfa4b47e2012-03-09 13:02:46 -08001880 // Check status of the cursor.
Marc Blankfd9d0b82012-04-23 16:01:51 -07001881 if (data != null && data.moveToFirst()) {
Andy Huang090db1e2012-07-25 13:25:28 -07001882 final Folder folder = new Folder(data);
Marc Blankfd9d0b82012-04-23 16:01:51 -07001883 LogUtils.d(LOG_TAG, "FOLDER STATUS = %d", folder.syncStatus);
Vikram Aggarwald00229d2012-09-20 12:31:44 -07001884 setHasFolderChanged(folder);
Andy Huang090db1e2012-07-25 13:25:28 -07001885 mFolder = folder;
1886 mFolderObservable.notifyChanged();
Paul Westbrookc808fac2012-02-22 16:42:18 -08001887 } else {
Marc Blankfd9d0b82012-04-23 16:01:51 -07001888 LogUtils.d(LOG_TAG, "Unable to get the folder %s",
1889 mFolder != null ? mAccount.name : "");
Mindy Pereira11dd5ef2012-03-10 15:10:18 -08001890 }
Vikram Aggarwalfa4b47e2012-03-09 13:02:46 -08001891 break;
Vikram Aggarwalfa4b47e2012-03-09 13:02:46 -08001892 case LOADER_RECENT_FOLDERS:
Vikram Aggarwalf9ba8712012-09-23 11:42:39 -07001893 // Few recent folders and we are running on a phone? Populate the default recents.
1894 // The number of default recent folders is at least 2: every provider has at
1895 // least two folders, and the recent folder count never decreases. Having a single
1896 // recent folder is an erroneous case, and we can gracefully recover by populating
1897 // default recents. The default recents will not stomp on the existing value: it
1898 // will be shown in addition to the default folders: the max number of recent
1899 // folders is more than 1+num(defaultRecents).
1900 if (data != null && data.getCount() <= 1 && !Utils.useTabletUI(mContext)) {
Vikram Aggarwal27d89ad2012-06-12 13:38:40 -07001901 final class PopulateDefault extends AsyncTask<Uri, Void, Void> {
1902 @Override
1903 protected Void doInBackground(Uri... uri) {
1904 // Asking for an update on the URI and ignore the result.
1905 final ContentResolver resolver = mContext.getContentResolver();
1906 resolver.update(uri[0], null, null, null);
1907 return null;
1908 }
1909 }
1910 final Uri uri = mAccount.defaultRecentFolderListUri;
1911 LogUtils.v(LOG_TAG, "Default recents at %s", uri);
1912 new PopulateDefault().execute(uri);
1913 break;
1914 }
1915 LogUtils.v(LOG_TAG, "Reading recent folders from the cursor.");
Vikram Aggarwalfa4b47e2012-03-09 13:02:46 -08001916 mRecentFolderList.loadFromUiProvider(data);
Vikram Aggarwal58cad2e2012-08-28 16:18:23 -07001917 mRecentFolderObservers.notifyChanged();
Vikram Aggarwalfa4b47e2012-03-09 13:02:46 -08001918 break;
Mindy Pereiraab486362012-03-21 18:18:53 -07001919 case LOADER_ACCOUNT_INBOX:
Marc Blankfd9d0b82012-04-23 16:01:51 -07001920 if (data != null && !data.isClosed() && data.moveToFirst()) {
Mindy Pereira0e88e9f2012-03-25 13:47:41 -07001921 Folder inbox = new Folder(data);
1922 onFolderChanged(inbox);
Mindy Pereirab4a43282012-03-23 16:20:03 -07001923 // Just want to get the inbox, don't care about updates to it
1924 // as this will be tracked by the folder change listener.
1925 mActivity.getLoaderManager().destroyLoader(LOADER_ACCOUNT_INBOX);
Mindy Pereira5ba33802012-03-26 16:30:11 -07001926 } else {
1927 LogUtils.d(LOG_TAG, "Unable to get the account inbox for account %s",
1928 mAccount != null ? mAccount.name : "");
Mindy Pereirab4a43282012-03-23 16:20:03 -07001929 }
Mindy Pereiraab486362012-03-21 18:18:53 -07001930 break;
1931 case LOADER_SEARCH:
Paul Westbrookc4845c52012-08-29 21:48:43 -07001932 if (data != null && data.getCount() > 0) {
1933 data.moveToFirst();
Vikram Aggarwal2b703c62012-09-18 13:54:15 -07001934 final Folder search = new Folder(data);
Paul Westbrookc4845c52012-08-29 21:48:43 -07001935 updateFolder(search);
1936 mConvListContext = ConversationListContext.forSearchQuery(mAccount, mFolder,
1937 mActivity.getIntent()
1938 .getStringExtra(UIProvider.SearchQueryParameters.QUERY));
1939 showConversationList(mConvListContext);
1940 mActivity.invalidateOptionsMenu();
1941 mActivity.getLoaderManager().destroyLoader(LOADER_SEARCH);
1942 } else {
1943 LogUtils.e(LOG_TAG, "Null or empty cursor returned by LOADER_SEARCH loader");
1944 }
Mindy Pereiraab486362012-03-21 18:18:53 -07001945 break;
Vikram Aggarwal7dedb952012-02-16 16:10:23 -08001946 }
1947 }
1948
Vikram Aggarwalc7694222012-04-23 13:37:01 -07001949 /**
1950 * Destructive actions on Conversations. This class should only be created by controllers, and
1951 * clients should only require {@link DestructiveAction}s, not specific implementations of the.
1952 * Only the controllers should know what kind of destructive actions are being created.
1953 */
Mindy Pereirade3e74a2012-07-24 09:43:10 -07001954 public class ConversationAction implements DestructiveAction {
Vikram Aggarwalacaa3c02012-04-24 12:45:27 -07001955 /**
1956 * The action to be performed. This is specified as the resource ID of the menu item
1957 * corresponding to this action: R.id.delete, R.id.report_spam, etc.
1958 */
Vikram Aggarwal7f602f72012-04-30 16:04:06 -07001959 private final int mAction;
Vikram Aggarwalbc67bb12012-04-30 14:05:35 -07001960 /** The action will act upon these conversations */
Paul Westbrook77eee622012-07-10 13:41:57 -07001961 private final Collection<Conversation> mTarget;
Vikram Aggarwal7f602f72012-04-30 16:04:06 -07001962 /** Whether this destructive action has already been performed */
1963 private boolean mCompleted;
Vikram Aggarwalc4113952012-05-11 14:14:56 -07001964 /** Whether this is an action on the currently selected set. */
1965 private final boolean mIsSelectedSet;
Vikram Aggarwale8a85322012-04-24 09:01:38 -07001966
Mindy Pereirafbe40192012-03-20 10:40:45 -07001967 /**
1968 * Create a listener object. action is one of four constants: R.id.y_button (archive),
1969 * R.id.delete , R.id.mute, and R.id.report_spam.
1970 * @param action
Vikram Aggarwalbc67bb12012-04-30 14:05:35 -07001971 * @param target Conversation that we want to apply the action to.
Vikram Aggarwalc4113952012-05-11 14:14:56 -07001972 * @param isBatch whether the conversations are in the currently selected batch set.
1973 */
1974 public ConversationAction(int action, Collection<Conversation> target, boolean isBatch) {
Mindy Pereirafbe40192012-03-20 10:40:45 -07001975 mAction = action;
Paul Westbrook77eee622012-07-10 13:41:57 -07001976 mTarget = ImmutableList.copyOf(target);
Vikram Aggarwalc4113952012-05-11 14:14:56 -07001977 mIsSelectedSet = isBatch;
Mindy Pereirafbe40192012-03-20 10:40:45 -07001978 }
1979
Vikram Aggarwalacaa3c02012-04-24 12:45:27 -07001980 /**
1981 * The action common to child classes. This performs the action specified in the constructor
1982 * on the conversations given here.
Vikram Aggarwalacaa3c02012-04-24 12:45:27 -07001983 */
Vikram Aggarwalbc67bb12012-04-30 14:05:35 -07001984 @Override
1985 public void performAction() {
Vikram Aggarwal7f602f72012-04-30 16:04:06 -07001986 if (isPerformed()) {
1987 return;
1988 }
Mindy Pereira3cb28b52012-05-24 15:26:39 -07001989 boolean undoEnabled = mAccount.supportsCapability(AccountCapabilities.UNDO);
Vikram Aggarwal7dd054e2012-05-21 14:43:10 -07001990
1991 // Are we destroying the currently shown conversation? Show the next one.
1992 if (LogUtils.isLoggable(LOG_TAG, LogUtils.DEBUG)){
Vikram Aggarwal8742a612012-08-13 10:22:50 -07001993 LogUtils.d(LOG_TAG, "ConversationAction.performAction():"
1994 + "\nmTarget=%s\nCurrent=%s",
Vikram Aggarwal7dd054e2012-05-21 14:43:10 -07001995 Conversation.toString(mTarget), mCurrentConversation);
1996 }
Vikram Aggarwal7dd054e2012-05-21 14:43:10 -07001997
Paul Westbrooke1221d22012-08-19 11:09:07 -07001998 if (mConversationListCursor == null) {
1999 LogUtils.e(LOG_TAG, "null ConversationCursor in ConversationAction.performAction():"
2000 + "\nmTarget=%s\nCurrent=%s",
2001 Conversation.toString(mTarget), mCurrentConversation);
2002 return;
2003 }
2004
Mindy Pereirafbe40192012-03-20 10:40:45 -07002005 switch (mAction) {
Mindy Pereira0692baf2012-03-23 17:34:31 -07002006 case R.id.archive:
Vikram Aggarwal7dd054e2012-05-21 14:43:10 -07002007 LogUtils.d(LOG_TAG, "Archiving");
Vikram Aggarwalbc67bb12012-04-30 14:05:35 -07002008 mConversationListCursor.archive(mContext, mTarget);
Mindy Pereirafbe40192012-03-20 10:40:45 -07002009 break;
2010 case R.id.delete:
Vikram Aggarwal7dd054e2012-05-21 14:43:10 -07002011 LogUtils.d(LOG_TAG, "Deleting");
Vikram Aggarwalbc67bb12012-04-30 14:05:35 -07002012 mConversationListCursor.delete(mContext, mTarget);
Mindy Pereira695d6962012-06-18 13:02:10 -07002013 if (mFolder.supportsCapability(FolderCapabilities.DELETE_ACTION_FINAL)) {
Marc Blank386243f2012-05-25 10:40:59 -07002014 undoEnabled = false;
2015 }
Mindy Pereirafbe40192012-03-20 10:40:45 -07002016 break;
2017 case R.id.mute:
Vikram Aggarwal7dd054e2012-05-21 14:43:10 -07002018 LogUtils.d(LOG_TAG, "Muting");
Vikram Aggarwalbc67bb12012-04-30 14:05:35 -07002019 if (mFolder.supportsCapability(FolderCapabilities.DESTRUCTIVE_MUTE)) {
Vikram Aggarwal7f602f72012-04-30 16:04:06 -07002020 for (Conversation c : mTarget) {
2021 c.localDeleteOnUpdate = true;
2022 }
Vikram Aggarwalbc67bb12012-04-30 14:05:35 -07002023 }
2024 mConversationListCursor.mute(mContext, mTarget);
Mindy Pereirafbe40192012-03-20 10:40:45 -07002025 break;
2026 case R.id.report_spam:
Vikram Aggarwal7dd054e2012-05-21 14:43:10 -07002027 LogUtils.d(LOG_TAG, "Reporting spam");
Vikram Aggarwalbc67bb12012-04-30 14:05:35 -07002028 mConversationListCursor.reportSpam(mContext, mTarget);
2029 break;
Paul Westbrook77eee622012-07-10 13:41:57 -07002030 case R.id.mark_not_spam:
2031 LogUtils.d(LOG_TAG, "Marking not spam");
2032 mConversationListCursor.reportNotSpam(mContext, mTarget);
2033 break;
Paul Westbrook76b20622012-07-12 11:45:43 -07002034 case R.id.report_phishing:
2035 LogUtils.d(LOG_TAG, "Reporting phishing");
2036 mConversationListCursor.reportPhishing(mContext, mTarget);
2037 break;
Vikram Aggarwalbc67bb12012-04-30 14:05:35 -07002038 case R.id.remove_star:
Vikram Aggarwal7dd054e2012-05-21 14:43:10 -07002039 LogUtils.d(LOG_TAG, "Removing star");
Vikram Aggarwalbc67bb12012-04-30 14:05:35 -07002040 // Star removal is destructive in the Starred folder.
2041 mConversationListCursor.updateBoolean(mContext, mTarget,
2042 ConversationColumns.STARRED, false);
2043 break;
2044 case R.id.mark_not_important:
Vikram Aggarwal7dd054e2012-05-21 14:43:10 -07002045 LogUtils.d(LOG_TAG, "Marking not-important");
Mindy Pereira445be212012-08-15 08:50:10 -07002046 // Marking not important is destructive in a mailbox
2047 // containing only important messages
Mindy Pereira0d03ef82012-08-15 09:05:48 -07002048 if (mFolder != null && mFolder.isImportantOnly()) {
Mindy Pereira445be212012-08-15 08:50:10 -07002049 for (Conversation conv : mTarget) {
2050 conv.localDeleteOnUpdate = true;
2051 }
2052 }
Vikram Aggarwalbc67bb12012-04-30 14:05:35 -07002053 mConversationListCursor.updateInt(mContext, mTarget,
2054 ConversationColumns.PRIORITY, UIProvider.ConversationPriority.LOW);
Mindy Pereirafbe40192012-03-20 10:40:45 -07002055 break;
Paul Westbrookef362542012-08-27 14:53:32 -07002056 case R.id.discard_drafts:
2057 LogUtils.d(LOG_TAG, "Discarding draft messages");
2058 // Discarding draft messages is destructive in a "draft" mailbox
2059 if (mFolder != null && mFolder.isDraft()) {
2060 for (Conversation conv : mTarget) {
2061 conv.localDeleteOnUpdate = true;
2062 }
2063 }
2064 mConversationListCursor.discardDrafts(mContext, mTarget);
2065 // We don't support undoing discarding drafts
2066 undoEnabled = false;
2067 break;
Vikram Aggarwal3d7ca9d2012-05-11 14:40:36 -07002068 }
2069 if (undoEnabled) {
mindypead50392012-08-23 11:03:53 -07002070 mHandler.postDelayed(new Runnable() {
2071 @Override
2072 public void run() {
2073 onUndoAvailable(new ToastBarOperation(mTarget.size(), mAction,
mindypa59283a2012-09-11 17:49:06 -07002074 ToastBarOperation.UNDO, mIsSelectedSet));
mindypead50392012-08-23 11:03:53 -07002075 }
2076 }, mShowUndoBarDelay);
Vikram Aggarwal3d7ca9d2012-05-11 14:40:36 -07002077 }
Vikram Aggarwalbc67bb12012-04-30 14:05:35 -07002078 refreshConversationList();
Vikram Aggarwalc4113952012-05-11 14:14:56 -07002079 if (mIsSelectedSet) {
Vikram Aggarwalc4113952012-05-11 14:14:56 -07002080 mSelectedSet.clear();
2081 }
Mindy Pereirafbe40192012-03-20 10:40:45 -07002082 }
Vikram Aggarwal7f602f72012-04-30 16:04:06 -07002083
2084 /**
2085 * Returns true if this action has been performed, false otherwise.
Andy Huang839ada22012-07-20 15:48:40 -07002086 *
Vikram Aggarwal7f602f72012-04-30 16:04:06 -07002087 */
2088 private synchronized boolean isPerformed() {
2089 if (mCompleted) {
2090 return true;
2091 }
2092 mCompleted = true;
2093 return false;
2094 }
Mindy Pereirafbe40192012-03-20 10:40:45 -07002095 }
Mindy Pereirae5f4dc02012-03-21 16:08:53 -07002096
Vikram Aggarwal3d7ca9d2012-05-11 14:40:36 -07002097 /**
2098 * Get a destructive action for a menu action.
2099 * This is a temporary method, to control the profusion of {@link DestructiveAction} classes
2100 * that are created. Please do not copy this paradigm.
2101 * @param action the resource ID of the menu action: R.id.delete, for example
2102 * @param target the conversations to act upon.
2103 * @return a {@link DestructiveAction} that performs the specified action.
2104 */
Vikram Aggarwal531488e2012-05-29 16:36:52 -07002105 private final DestructiveAction getAction(int action, Collection<Conversation> target) {
Vikram Aggarwal3d7ca9d2012-05-11 14:40:36 -07002106 final DestructiveAction da = new ConversationAction(action, target, false);
2107 registerDestructiveAction(da);
2108 return da;
2109 }
2110
Vikram Aggarwald503df42012-05-11 10:13:35 -07002111 // Called from the FolderSelectionDialog after a user is done selecting folders to assign the
2112 // conversations to.
Mindy Pereirae5f4dc02012-03-21 16:08:53 -07002113 @Override
Mindy Pereira8db7e402012-07-13 10:32:47 -07002114 public final void assignFolder(Collection<FolderOperation> folderOps,
2115 Collection<Conversation> target, boolean batch, boolean showUndo) {
2116 // Actions are destructive only when the current folder can be assigned
2117 // to (which is the same as being able to un-assign a conversation from the folder) and
2118 // when the list of folders contains the current folder.
2119 final boolean isDestructive = mFolder
2120 .supportsCapability(FolderCapabilities.CAN_ACCEPT_MOVED_MESSAGES)
2121 && FolderOperation.isDestructive(folderOps, mFolder);
Vikram Aggarwald503df42012-05-11 10:13:35 -07002122 LogUtils.d(LOG_TAG, "onFolderChangesCommit: isDestructive = %b", isDestructive);
2123 if (isDestructive) {
2124 for (final Conversation c : target) {
2125 c.localDeleteOnUpdate = true;
Mindy Pereira6778f462012-03-23 18:01:55 -07002126 }
Mindy Pereirae5f4dc02012-03-21 16:08:53 -07002127 }
mindypc84759c2012-08-29 09:51:53 -07002128 final DestructiveAction folderChange;
Vikram Aggarwald503df42012-05-11 10:13:35 -07002129 // Update the UI elements depending no their visibility and availability
Vikram Aggarwal3d7ca9d2012-05-11 14:40:36 -07002130 // TODO(viki): Consolidate this into a single method requestDelete.
Vikram Aggarwald503df42012-05-11 10:13:35 -07002131 if (isDestructive) {
mindypc84759c2012-08-29 09:51:53 -07002132 folderChange = getDeferredFolderChange(target, folderOps, isDestructive,
2133 batch, showUndo);
Vikram Aggarwala8e43182012-09-13 12:55:10 -07002134 delete(0, target, folderChange);
Vikram Aggarwal6902dcf2012-04-11 08:57:42 -07002135 } else {
mindypc84759c2012-08-29 09:51:53 -07002136 folderChange = getFolderChange(target, folderOps, isDestructive,
2137 batch, showUndo);
Vikram Aggarwald503df42012-05-11 10:13:35 -07002138 requestUpdate(target, folderChange);
Mindy Pereirae5f4dc02012-03-21 16:08:53 -07002139 }
2140 }
2141
Mindy Pereira967ede62012-03-22 09:29:09 -07002142 @Override
Vikram Aggarwal3d7ca9d2012-05-11 14:40:36 -07002143 public final void onRefreshRequired() {
mindyp5390fca2012-08-22 12:12:25 -07002144 if (isAnimating() || isDragging()) {
Mindy Pereira69e88dd2012-08-10 09:30:18 -07002145 LogUtils.d(LOG_TAG, "onRefreshRequired: delay until animating done");
2146 return;
2147 }
Marc Blankbf128eb2012-04-18 15:58:45 -07002148 // Refresh the query in the background
Paul Westbrookcff1aea2012-08-10 11:51:00 -07002149 if (mConversationListCursor.isRefreshRequired()) {
2150 mConversationListCursor.refresh();
2151 }
Marc Blankbf128eb2012-04-18 15:58:45 -07002152 }
2153
mindyp5390fca2012-08-22 12:12:25 -07002154 @Override
2155 public void startDragMode() {
2156 mIsDragHappening = true;
2157 }
2158
2159 @Override
2160 public void stopDragMode() {
2161 mIsDragHappening = false;
2162 if (mConversationListCursor.isRefreshReady()) {
2163 LogUtils.d(LOG_TAG, "Stopped animating: try sync");
2164 onRefreshReady();
2165 }
2166
2167 if (mConversationListCursor.isRefreshRequired()) {
2168 LogUtils.d(LOG_TAG, "Stopped animating: refresh");
2169 mConversationListCursor.refresh();
2170 }
2171 }
2172
2173 private boolean isDragging() {
2174 return mIsDragHappening;
2175 }
2176
Mindy Pereira69e88dd2012-08-10 09:30:18 -07002177 private boolean isAnimating() {
2178 boolean isAnimating = false;
2179 ConversationListFragment convListFragment = getConversationListFragment();
2180 if (convListFragment != null) {
2181 AnimatedAdapter adapter = convListFragment.getAnimatedAdapter();
2182 if (adapter != null) {
2183 isAnimating = adapter.isAnimating();
2184 }
2185 }
2186 return isAnimating;
2187 }
2188
Marc Blankbf128eb2012-04-18 15:58:45 -07002189 /**
2190 * Called when the {@link ConversationCursor} is changed or has new data in it.
2191 * <p>
2192 * {@inheritDoc}
2193 */
2194 @Override
Vikram Aggarwal3d7ca9d2012-05-11 14:40:36 -07002195 public final void onRefreshReady() {
Paul Westbrookcff1aea2012-08-10 11:51:00 -07002196 if (!isAnimating()) {
Marc Blankbf128eb2012-04-18 15:58:45 -07002197 // Swap cursors
2198 mConversationListCursor.sync();
Marc Blankbf128eb2012-04-18 15:58:45 -07002199 }
Paul Westbrook937c94f2012-08-16 13:01:18 -07002200 mTracker.onCursorUpdated();
Marc Blankbf128eb2012-04-18 15:58:45 -07002201 }
2202
2203 @Override
Vikram Aggarwal3d7ca9d2012-05-11 14:40:36 -07002204 public final void onDataSetChanged() {
Paul Westbrook9f119c72012-04-24 16:10:59 -07002205 updateConversationListFragment();
Andy Huang632721e2012-04-11 16:57:26 -07002206 mConversationListObservable.notifyChanged();
Paul Westbrooka13b3742012-09-07 16:35:06 -07002207 mSelectedSet.validateAgainstCursor(mConversationListCursor);
Marc Blankbf128eb2012-04-18 15:58:45 -07002208 }
2209
Vikram Aggarwal3d7ca9d2012-05-11 14:40:36 -07002210 /**
2211 * If the Conversation List Fragment is visible, updates the fragment.
2212 */
2213 private final void updateConversationListFragment() {
Marc Blankbf128eb2012-04-18 15:58:45 -07002214 final ConversationListFragment convList = getConversationListFragment();
2215 if (convList != null) {
Vikram Aggarwal75daee52012-04-30 11:13:09 -07002216 refreshConversationList();
Paul Westbrook9f119c72012-04-24 16:10:59 -07002217 if (convList.isVisible()) {
Vikram Aggarwal69b5c302012-09-05 11:11:13 -07002218 informCursorVisiblity(true);
Paul Westbrook9f119c72012-04-24 16:10:59 -07002219 }
Marc Blankbf128eb2012-04-18 15:58:45 -07002220 }
2221 }
2222
2223 /**
2224 * This class handles throttled refresh of the conversation list
2225 */
2226 static class RefreshTimerTask extends TimerTask {
2227 final Handler mHandler;
2228 final AbstractActivityController mController;
2229
2230 RefreshTimerTask(AbstractActivityController controller, Handler handler) {
2231 mHandler = handler;
2232 mController = controller;
2233 }
2234
2235 @Override
2236 public void run() {
2237 mHandler.post(new Runnable() {
2238 @Override
2239 public void run() {
2240 LogUtils.d(LOG_TAG, "Delay done... calling onRefreshRequired");
2241 mController.onRefreshRequired();
2242 }});
2243 }
2244 }
2245
2246 /**
2247 * Cancel the refresh task, if it's running
2248 */
2249 private void cancelRefreshTask () {
2250 if (mConversationListRefreshTask != null) {
2251 mConversationListRefreshTask.cancel();
2252 mConversationListRefreshTask = null;
2253 }
2254 }
2255
2256 @Override
Paul Westbrookcff1aea2012-08-10 11:51:00 -07002257 public void onAnimationEnd(AnimatedAdapter animatedAdapter) {
Paul Westbrook026139c2012-09-19 22:35:37 -07002258 if (mConversationListCursor == null) {
2259 LogUtils.e(LOG_TAG, "null ConversationCursor in onAnimationEnd");
2260 return;
2261 }
Paul Westbrookcff1aea2012-08-10 11:51:00 -07002262 if (mConversationListCursor.isRefreshReady()) {
mindyp52544862012-08-20 12:05:36 -07002263 LogUtils.d(LOG_TAG, "Stopped animating: try sync");
Paul Westbrookcff1aea2012-08-10 11:51:00 -07002264 onRefreshReady();
Marc Blankbf128eb2012-04-18 15:58:45 -07002265 }
Marc Blankbf128eb2012-04-18 15:58:45 -07002266
Paul Westbrookcff1aea2012-08-10 11:51:00 -07002267 if (mConversationListCursor.isRefreshRequired()) {
mindyp52544862012-08-20 12:05:36 -07002268 LogUtils.d(LOG_TAG, "Stopped animating: refresh");
Paul Westbrookcff1aea2012-08-10 11:51:00 -07002269 mConversationListCursor.refresh();
2270 }
Marc Blankbf128eb2012-04-18 15:58:45 -07002271 }
2272
2273 @Override
Mindy Pereira967ede62012-03-22 09:29:09 -07002274 public void onSetEmpty() {
Mindy Pereira967ede62012-03-22 09:29:09 -07002275 }
2276
2277 @Override
2278 public void onSetPopulated(ConversationSelectionSet set) {
Vikram Aggarwal6902dcf2012-04-11 08:57:42 -07002279 final ConversationListFragment convList = getConversationListFragment();
2280 if (convList == null) {
2281 return;
2282 }
Vikram Aggarwal7c401b72012-08-13 16:43:47 -07002283 mCabActionMenu = new SelectedConversationsActionMenu(mActivity, set, mFolder,
Vikram Aggarwal531488e2012-05-29 16:36:52 -07002284 (SwipeableListView) convList.getListView());
Vikram Aggarwale128fc22012-04-04 12:33:34 -07002285 enableCabMode();
Mindy Pereira967ede62012-03-22 09:29:09 -07002286 }
2287
Mindy Pereira967ede62012-03-22 09:29:09 -07002288 @Override
2289 public void onSetChanged(ConversationSelectionSet set) {
2290 // Do nothing. We don't care about changes to the set.
2291 }
2292
2293 @Override
2294 public ConversationSelectionSet getSelectedSet() {
2295 return mSelectedSet;
2296 }
2297
Vikram Aggarwale128fc22012-04-04 12:33:34 -07002298 /**
2299 * Disable the Contextual Action Bar (CAB). The selected set is not changed.
2300 */
2301 protected void disableCabMode() {
Mindy Pereira8937bf12012-07-23 14:05:02 -07002302 // Commit any previous destructive actions when entering/ exiting CAB mode.
mindypc6adce32012-08-22 18:46:42 -07002303 commitDestructiveActions(true);
Vikram Aggarwale128fc22012-04-04 12:33:34 -07002304 if (mCabActionMenu != null) {
2305 mCabActionMenu.deactivate();
2306 }
2307 }
2308
2309 /**
2310 * Re-enable the CAB menu if required. The selection set is not changed.
2311 */
2312 protected void enableCabMode() {
2313 if (mCabActionMenu != null) {
2314 mCabActionMenu.activate();
2315 }
2316 }
2317
Vikram Aggarwal4eb52712012-06-19 16:24:50 -07002318 /**
2319 * Unselect conversations and exit CAB mode.
2320 */
2321 protected final void exitCabMode() {
2322 mSelectedSet.clear();
2323 }
2324
Mindy Pereira967ede62012-03-22 09:29:09 -07002325 @Override
Mindy Pereirafd0c2972012-03-27 13:50:39 -07002326 public void startSearch() {
Vikram Aggarwal35f19d72012-04-24 13:24:48 -07002327 if (mAccount == null) {
2328 // We cannot search if there is no account. Drop the request to the floor.
2329 LogUtils.d(LOG_TAG, "AbstractActivityController.startSearch(): null account");
2330 return;
2331 }
Mindy Pereirafd0c2972012-03-27 13:50:39 -07002332 if (mAccount.supportsCapability(UIProvider.AccountCapabilities.LOCAL_SEARCH)
2333 | mAccount.supportsCapability(UIProvider.AccountCapabilities.SERVER_SEARCH)) {
Vikram Aggarwal70f130e2012-04-03 12:32:14 -07002334 onSearchRequested(mActionBarView.getQuery());
Mindy Pereirafd0c2972012-03-27 13:50:39 -07002335 } else {
2336 Toast.makeText(mActivity.getActivityContext(), mActivity.getActivityContext()
Mindy Pereiraa46c2992012-03-27 14:12:39 -07002337 .getString(R.string.search_unsupported), Toast.LENGTH_SHORT).show();
Mindy Pereirafd0c2972012-03-27 13:50:39 -07002338 }
2339 }
Mindy Pereiraacf60392012-04-06 09:11:00 -07002340
Vikram Aggarwal0dda5732012-04-06 11:20:16 -07002341 @Override
2342 public void exitSearchMode() {
2343 if (mViewMode.getMode() == ViewMode.SEARCH_RESULTS_LIST) {
2344 mActivity.finish();
2345 }
2346 }
2347
Mindy Pereiraacf60392012-04-06 09:11:00 -07002348 /**
2349 * Supports dragging conversations to a folder.
2350 */
2351 @Override
2352 public boolean supportsDrag(DragEvent event, Folder folder) {
2353 return (folder != null
2354 && event != null
2355 && event.getClipDescription() != null
2356 && folder.supportsCapability
2357 (UIProvider.FolderCapabilities.CAN_ACCEPT_MOVED_MESSAGES)
2358 && folder.supportsCapability
2359 (UIProvider.FolderCapabilities.CAN_HOLD_MAIL)
2360 && !mFolder.uri.equals(folder.uri));
2361 }
2362
2363 /**
Mindy Pereira6c2663d2012-07-20 15:37:29 -07002364 * Handles dropping conversations to a folder.
Mindy Pereiraacf60392012-04-06 09:11:00 -07002365 */
2366 @Override
2367 public void handleDrop(DragEvent event, final Folder folder) {
Mindy Pereiraacf60392012-04-06 09:11:00 -07002368 if (!supportsDrag(event, folder)) {
2369 return;
2370 }
Mindy Pereiraacf60392012-04-06 09:11:00 -07002371 final Collection<Conversation> conversations = mSelectedSet.values();
mindypa8492632012-09-24 09:27:54 -07002372 final ArrayList<FolderOperation> dragDropOperations = new ArrayList<FolderOperation>();
2373 // Add the drop target folder.
2374 dragDropOperations.add(new FolderOperation(folder, true));
2375 // Remove the current folder unless the user is viewing "all".
2376 // That operation should just add the new folder.
2377 boolean isDestructive = !mFolder.isViewAll()
2378 && mFolder.supportsCapability
2379 (UIProvider.FolderCapabilities.CAN_ACCEPT_MOVED_MESSAGES);
2380 if (isDestructive) {
2381 dragDropOperations.add(new FolderOperation(mFolder, false));
2382 }
Mindy Pereira8db7e402012-07-13 10:32:47 -07002383 // Drag and drop is destructive: we remove conversations from the
2384 // current folder.
mindypa8492632012-09-24 09:27:54 -07002385 final DestructiveAction action = getFolderChange(conversations, dragDropOperations,
2386 isDestructive, true, true);
2387 if (isDestructive) {
2388 delete(0, conversations, action);
2389 } else {
2390 action.performAction();
2391 }
Mindy Pereiraacf60392012-04-06 09:11:00 -07002392 }
Mindy Pereira0963ef82012-04-10 11:43:01 -07002393
2394 @Override
Mindy Pereira0963ef82012-04-10 11:43:01 -07002395 public void onTouchEvent(MotionEvent event) {
2396 if (event.getAction() == MotionEvent.ACTION_DOWN) {
Andrew Sappersteinc2c9dc12012-07-02 18:17:32 -07002397 if (mToastBar != null && !mToastBar.isEventInToastBar(event)) {
2398 mToastBar.hide(true);
Mindy Pereira0963ef82012-04-10 11:43:01 -07002399 }
2400 }
2401 }
Andy Huangb1c34dc2012-04-17 16:36:19 -07002402
Andy Huang632721e2012-04-11 16:57:26 -07002403 @Override
2404 public void onConversationSeen(Conversation conv) {
2405 mPagerController.onConversationSeen(conv);
2406 }
2407
Andy Huangb1c34dc2012-04-17 16:36:19 -07002408 private class ConversationListLoaderCallbacks implements
2409 LoaderManager.LoaderCallbacks<ConversationCursor> {
2410
2411 @Override
2412 public Loader<ConversationCursor> onCreateLoader(int id, Bundle args) {
2413 Loader<ConversationCursor> result = new ConversationCursorLoader((Activity) mActivity,
Paul Westbrook9a70e912012-08-17 15:53:20 -07002414 mAccount, mFolder.conversationListUri, mFolder.name);
Andy Huangb1c34dc2012-04-17 16:36:19 -07002415 return result;
2416 }
2417
2418 @Override
2419 public void onLoadFinished(Loader<ConversationCursor> loader, ConversationCursor data) {
Andy Huang632721e2012-04-11 16:57:26 -07002420 LogUtils.d(LOG_TAG, "IN AAC.ConversationCursor.onLoadFinished, data=%s loader=%s",
2421 data, loader);
Vikram Aggarwale8a85322012-04-24 09:01:38 -07002422 // Clear our all pending destructive actions before swapping the conversation cursor
2423 destroyPending(null);
Andy Huangb1c34dc2012-04-17 16:36:19 -07002424 mConversationListCursor = data;
Paul Westbrookbf232c32012-04-18 03:17:41 -07002425 mConversationListCursor.addListener(AbstractActivityController.this);
Andy Huangb1c34dc2012-04-17 16:36:19 -07002426
Paul Westbrook937c94f2012-08-16 13:01:18 -07002427 mTracker.onCursorUpdated();
2428
Andy Huange3df1ad2012-04-24 17:15:23 -07002429 mConversationListObservable.notifyChanged();
Paul Westbrook9f119c72012-04-24 16:10:59 -07002430
Vikram Aggarwal17f373e2012-09-17 15:49:32 -07002431 final ConversationListFragment convList = getConversationListFragment();
2432 if (convList != null && convList.isVisible()) {
2433 // The conversation list is already listening to list changes and gets notified
2434 // in the mConversationListObservable.notifyChanged() line above. We only need to
2435 // check and inform the cursor of the change in visibility here.
2436 informCursorVisiblity(true);
Andy Huangb1c34dc2012-04-17 16:36:19 -07002437 }
2438 // Shown for search results in two-pane mode only.
2439 if (shouldShowFirstConversation()) {
2440 if (mConversationListCursor.getCount() > 0) {
2441 mConversationListCursor.moveToPosition(0);
Andy Huangb1c34dc2012-04-17 16:36:19 -07002442 final Conversation conv = new Conversation(mConversationListCursor);
2443 conv.position = 0;
Andy Huang1ee96b22012-08-24 20:19:53 -07002444 onConversationSelected(conv, true /* checkSafeToModifyFragments */);
Andy Huangb1c34dc2012-04-17 16:36:19 -07002445 }
2446 }
2447 }
2448
2449 @Override
2450 public void onLoaderReset(Loader<ConversationCursor> loader) {
Paul Westbrook9a70e912012-08-17 15:53:20 -07002451 LogUtils.d(LOG_TAG, "IN AAC.ConversationCursor.onLoaderReset, data=%s loader=%s",
2452 mConversationListCursor, loader);
2453
2454 if (mConversationListCursor != null) {
2455 // Unregister the listener
2456 mConversationListCursor.removeListener(AbstractActivityController.this);
2457 mConversationListCursor = null;
2458
2459 // Inform anyone who is interested about the change
2460 mTracker.onCursorUpdated();
2461 mConversationListObservable.notifyChanged();
Andy Huangb1c34dc2012-04-17 16:36:19 -07002462 }
Andy Huangb1c34dc2012-04-17 16:36:19 -07002463 }
Paul Westbrookbf232c32012-04-18 03:17:41 -07002464 }
Andy Huangb1c34dc2012-04-17 16:36:19 -07002465
Vikram Aggarwale8a85322012-04-24 09:01:38 -07002466 /**
2467 * Destroy the pending {@link DestructiveAction} till now and assign the given action as the
2468 * next destructive action..
2469 * @param nextAction the next destructive action to be performed. This can be null.
2470 */
2471 private final void destroyPending(DestructiveAction nextAction) {
2472 // If there is a pending action, perform that first.
2473 if (mPendingDestruction != null) {
2474 mPendingDestruction.performAction();
2475 }
2476 mPendingDestruction = nextAction;
2477 }
2478
2479 /**
2480 * Register a destructive action with the controller. This performs the previous destructive
Vikram Aggarwalacaa3c02012-04-24 12:45:27 -07002481 * action as a side effect. This method is final because we don't want the child classes to
Vikram Aggarwal3d7ca9d2012-05-11 14:40:36 -07002482 * embellish this method any more.
Vikram Aggarwale8a85322012-04-24 09:01:38 -07002483 * @param action
2484 */
Vikram Aggarwal3d7ca9d2012-05-11 14:40:36 -07002485 private final void registerDestructiveAction(DestructiveAction action) {
Vikram Aggarwale8a85322012-04-24 09:01:38 -07002486 // TODO(viki): This is not a good idea. The best solution is for clients to request a
2487 // destructive action from the controller and for the controller to own the action. This is
2488 // a half-way solution while refactoring DestructiveAction.
2489 destroyPending(action);
2490 return;
2491 }
2492
Vikram Aggarwal531488e2012-05-29 16:36:52 -07002493 @Override
2494 public final DestructiveAction getBatchAction(int action) {
Vikram Aggarwalc4113952012-05-11 14:14:56 -07002495 final DestructiveAction da = new ConversationAction(action, mSelectedSet.values(), true);
Vikram Aggarwale8a85322012-04-24 09:01:38 -07002496 registerDestructiveAction(da);
2497 return da;
2498 }
Vikram Aggarwal41e6e712012-04-24 11:22:57 -07002499
Mindy Pereirade3e74a2012-07-24 09:43:10 -07002500 @Override
2501 public final DestructiveAction getDeferredBatchAction(int action) {
2502 final DestructiveAction da = new ConversationAction(action, mSelectedSet.values(), true);
2503 return da;
2504 }
2505
Vikram Aggarwalbc67bb12012-04-30 14:05:35 -07002506 /**
2507 * Class to change the folders that are assigned to a set of conversations. This is destructive
2508 * because the user can remove the current folder from the conversation, in which case it has
2509 * to be animated away from the current folder.
2510 */
Vikram Aggarwal41e6e712012-04-24 11:22:57 -07002511 private class FolderDestruction implements DestructiveAction {
Paul Westbrook77eee622012-07-10 13:41:57 -07002512 private final Collection<Conversation> mTarget;
Mindy Pereira8db7e402012-07-13 10:32:47 -07002513 private final ArrayList<FolderOperation> mFolderOps = new ArrayList<FolderOperation>();
Vikram Aggarwal41e6e712012-04-24 11:22:57 -07002514 private final boolean mIsDestructive;
Vikram Aggarwal7f602f72012-04-30 16:04:06 -07002515 /** Whether this destructive action has already been performed */
2516 private boolean mCompleted;
Mindy Pereiraf3a45562012-05-24 16:30:19 -07002517 private boolean mIsSelectedSet;
Mindy Pereira06642fa2012-07-12 16:23:27 -07002518 private boolean mShowUndo;
Mindy Pereira01f30502012-08-14 10:30:51 -07002519 private int mAction;
Vikram Aggarwal41e6e712012-04-24 11:22:57 -07002520
2521 /**
2522 * Create a new folder destruction object to act on the given conversations.
2523 * @param target
2524 */
Vikram Aggarwal7f602f72012-04-30 16:04:06 -07002525 private FolderDestruction(final Collection<Conversation> target,
Mindy Pereira8db7e402012-07-13 10:32:47 -07002526 final Collection<FolderOperation> folders, boolean isDestructive, boolean isBatch,
Mindy Pereira01f30502012-08-14 10:30:51 -07002527 boolean showUndo, int action) {
Paul Westbrook77eee622012-07-10 13:41:57 -07002528 mTarget = ImmutableList.copyOf(target);
Mindy Pereira8db7e402012-07-13 10:32:47 -07002529 mFolderOps.addAll(folders);
Vikram Aggarwal41e6e712012-04-24 11:22:57 -07002530 mIsDestructive = isDestructive;
Mindy Pereiraf3a45562012-05-24 16:30:19 -07002531 mIsSelectedSet = isBatch;
Mindy Pereira06642fa2012-07-12 16:23:27 -07002532 mShowUndo = showUndo;
Mindy Pereira01f30502012-08-14 10:30:51 -07002533 mAction = action;
Vikram Aggarwal41e6e712012-04-24 11:22:57 -07002534 }
2535
Vikram Aggarwal41e6e712012-04-24 11:22:57 -07002536 @Override
2537 public void performAction() {
Vikram Aggarwal7f602f72012-04-30 16:04:06 -07002538 if (isPerformed()) {
2539 return;
2540 }
Mindy Pereira06642fa2012-07-12 16:23:27 -07002541 if (mIsDestructive && mShowUndo) {
Mindy Pereirad33674992012-06-25 16:26:30 -07002542 ToastBarOperation undoOp = new ToastBarOperation(mTarget.size(),
mindypa59283a2012-09-11 17:49:06 -07002543 mAction, ToastBarOperation.UNDO, mIsSelectedSet);
Vikram Aggarwal41e6e712012-04-24 11:22:57 -07002544 onUndoAvailable(undoOp);
2545 }
Mindy Pereira8db7e402012-07-13 10:32:47 -07002546 // For each conversation, for each operation, add/ remove the
2547 // appropriate folders.
mindyp389f0b22012-08-29 11:12:54 -07002548 ArrayList<String> updatedTargetFolders = new ArrayList<String>(mTarget.size());
Mindy Pereira8db7e402012-07-13 10:32:47 -07002549 for (Conversation target : mTarget) {
2550 HashMap<Uri, Folder> targetFolders = Folder
Mindy Pereira68f83842012-07-27 09:43:31 -07002551 .hashMapForFolders(target.getRawFolders());
Mindy Pereira01f30502012-08-14 10:30:51 -07002552 if (mIsDestructive) {
2553 target.localDeleteOnUpdate = true;
2554 }
Mindy Pereira8db7e402012-07-13 10:32:47 -07002555 for (FolderOperation op : mFolderOps) {
2556 if (op.mAdd) {
2557 targetFolders.put(op.mFolder.uri, op.mFolder);
2558 } else {
2559 targetFolders.remove(op.mFolder.uri);
2560 }
2561 }
mindyp389f0b22012-08-29 11:12:54 -07002562 updatedTargetFolders.add(Folder.getSerializedFolderString(targetFolders.values()));
2563 }
2564 if (mConversationListCursor != null) {
2565 mConversationListCursor.updateStrings(mContext, mTarget,
2566 Conversation.UPDATE_FOLDER_COLUMN, updatedTargetFolders);
Mindy Pereira8db7e402012-07-13 10:32:47 -07002567 }
Vikram Aggarwal7f602f72012-04-30 16:04:06 -07002568 refreshConversationList();
Mindy Pereiraf3a45562012-05-24 16:30:19 -07002569 if (mIsSelectedSet) {
2570 mSelectedSet.clear();
2571 }
Vikram Aggarwal7f602f72012-04-30 16:04:06 -07002572 }
Mindy Pereira8db7e402012-07-13 10:32:47 -07002573
Vikram Aggarwal7f602f72012-04-30 16:04:06 -07002574 /**
2575 * Returns true if this action has been performed, false otherwise.
Andy Huang839ada22012-07-20 15:48:40 -07002576 *
Vikram Aggarwal7f602f72012-04-30 16:04:06 -07002577 */
2578 private synchronized boolean isPerformed() {
2579 if (mCompleted) {
2580 return true;
Vikram Aggarwal41e6e712012-04-24 11:22:57 -07002581 }
Vikram Aggarwal7f602f72012-04-30 16:04:06 -07002582 mCompleted = true;
2583 return false;
Vikram Aggarwal41e6e712012-04-24 11:22:57 -07002584 }
2585 }
Vikram Aggarwal7f602f72012-04-30 16:04:06 -07002586
mindypc84759c2012-08-29 09:51:53 -07002587 public final DestructiveAction getFolderChange(Collection<Conversation> target,
2588 Collection<FolderOperation> folders, boolean isDestructive, boolean isBatch,
2589 boolean showUndo) {
2590 final DestructiveAction da = getDeferredFolderChange(target, folders, isDestructive,
2591 isBatch, showUndo);
2592 registerDestructiveAction(da);
2593 return da;
2594 }
2595
2596 public final DestructiveAction getDeferredFolderChange(Collection<Conversation> target,
Mindy Pereira8db7e402012-07-13 10:32:47 -07002597 Collection<FolderOperation> folders, boolean isDestructive, boolean isBatch,
2598 boolean showUndo) {
Mindy Pereira06642fa2012-07-12 16:23:27 -07002599 final DestructiveAction da = new FolderDestruction(target, folders, isDestructive, isBatch,
Mindy Pereira01f30502012-08-14 10:30:51 -07002600 showUndo, R.id.change_folder);
Mindy Pereira01f30502012-08-14 10:30:51 -07002601 return da;
2602 }
2603
2604 @Override
2605 public final DestructiveAction getDeferredRemoveFolder(Collection<Conversation> target,
2606 Folder toRemove, boolean isDestructive, boolean isBatch,
2607 boolean showUndo) {
2608 Collection<FolderOperation> folderOps = new ArrayList<FolderOperation>();
2609 folderOps.add(new FolderOperation(toRemove, false));
2610 return new FolderDestruction(target, folderOps, isDestructive, isBatch,
2611 showUndo, R.id.remove_folder);
2612 }
2613
2614 private final DestructiveAction getRemoveFolder(Collection<Conversation> target,
mindypc84759c2012-08-29 09:51:53 -07002615 Folder toRemove, boolean isDestructive, boolean isBatch, boolean showUndo) {
2616 DestructiveAction da = getDeferredRemoveFolder(target, toRemove, isDestructive, isBatch,
2617 showUndo);
Vikram Aggarwal41e6e712012-04-24 11:22:57 -07002618 registerDestructiveAction(da);
2619 return da;
2620 }
Vikram Aggarwal75daee52012-04-30 11:13:09 -07002621
Vikram Aggarwal4f4782b2012-05-30 08:39:09 -07002622 @Override
2623 public final void refreshConversationList() {
Vikram Aggarwal75daee52012-04-30 11:13:09 -07002624 final ConversationListFragment convList = getConversationListFragment();
2625 if (convList == null) {
2626 return;
2627 }
2628 convList.requestListRefresh();
2629 }
Andrew Sappersteinc2c9dc12012-07-02 18:17:32 -07002630
2631 protected final ActionClickedListener getUndoClickedListener(
2632 final AnimatedAdapter listAdapter) {
2633 return new ActionClickedListener() {
2634 @Override
2635 public void onActionClicked() {
2636 if (mAccount.undoUri != null) {
2637 // NOTE: We might want undo to return the messages affected, in which case
2638 // the resulting cursor might be interesting...
2639 // TODO: Use UIProvider.SEQUENCE_QUERY_PARAMETER to indicate the set of
2640 // commands to undo
2641 if (mConversationListCursor != null) {
2642 mConversationListCursor.undo(
2643 mActivity.getActivityContext(), mAccount.undoUri);
2644 }
2645 if (listAdapter != null) {
2646 listAdapter.setUndo(true);
2647 }
2648 }
2649 }
2650 };
2651 }
2652
Andrew Sapperstein9d7519d2012-07-16 14:03:53 -07002653 protected final void showErrorToast(final Folder folder, boolean replaceVisibleToast) {
Andrew Sappersteinc2c9dc12012-07-02 18:17:32 -07002654 mToastBar.setConversationMode(false);
Andrew Sapperstein00179f12012-08-09 15:15:40 -07002655
2656 ActionClickedListener listener = null;
2657 int actionTextResourceId;
2658 final int lastSyncResult = folder.lastSyncResult;
2659 switch (lastSyncResult) {
2660 case UIProvider.LastSyncResult.CONNECTION_ERROR:
2661 listener = getRetryClickedListener(folder);
2662 actionTextResourceId = R.string.retry;
2663 break;
2664 case UIProvider.LastSyncResult.AUTH_ERROR:
2665 listener = getSignInClickedListener();
2666 actionTextResourceId = R.string.signin;
2667 break;
2668 case UIProvider.LastSyncResult.SECURITY_ERROR:
2669 return; // Currently we do nothing for security errors.
2670 case UIProvider.LastSyncResult.STORAGE_ERROR:
2671 listener = getStorageErrorClickedListener();
2672 actionTextResourceId = R.string.info;
2673 break;
2674 case UIProvider.LastSyncResult.INTERNAL_ERROR:
2675 listener = getInternalErrorClickedListener();
2676 actionTextResourceId = R.string.report;
2677 break;
2678 default:
2679 return;
2680 }
Andrew Sappersteinc2c9dc12012-07-02 18:17:32 -07002681 mToastBar.show(
Andrew Sapperstein00179f12012-08-09 15:15:40 -07002682 listener,
Andrew Sapperstein5d420962012-07-12 16:43:10 -07002683 R.drawable.ic_alert_white,
Andrew Sappersteinc2c9dc12012-07-02 18:17:32 -07002684 Utils.getSyncStatusText(mActivity.getActivityContext(),
Andrew Sapperstein00179f12012-08-09 15:15:40 -07002685 lastSyncResult),
Andrew Sappersteinc2c9dc12012-07-02 18:17:32 -07002686 false, /* showActionIcon */
Andrew Sapperstein00179f12012-08-09 15:15:40 -07002687 actionTextResourceId,
Andrew Sapperstein9d7519d2012-07-16 14:03:53 -07002688 replaceVisibleToast,
mindypa59283a2012-09-11 17:49:06 -07002689 new ToastBarOperation(1, 0, ToastBarOperation.ERROR, false));
Andrew Sappersteinc2c9dc12012-07-02 18:17:32 -07002690 }
2691
Andrew Sapperstein00179f12012-08-09 15:15:40 -07002692 private ActionClickedListener getRetryClickedListener(final Folder folder) {
Andrew Sappersteinc2c9dc12012-07-02 18:17:32 -07002693 return new ActionClickedListener() {
2694 @Override
2695 public void onActionClicked() {
2696 final Uri uri = folder.refreshUri;
2697
2698 if (uri != null) {
Paul Westbrook4969e0c2012-08-20 14:38:39 -07002699 startAsyncRefreshTask(uri);
Andrew Sappersteinc2c9dc12012-07-02 18:17:32 -07002700 }
2701 }
2702 };
2703 }
Andrew Sapperstein00179f12012-08-09 15:15:40 -07002704
2705 private ActionClickedListener getSignInClickedListener() {
2706 return new ActionClickedListener() {
2707 @Override
2708 public void onActionClicked() {
Paul Westbrook122f7c22012-08-20 17:50:31 -07002709 promptUserForAuthentication(mAccount);
Andrew Sapperstein00179f12012-08-09 15:15:40 -07002710 }
2711 };
2712 }
2713
2714 private ActionClickedListener getStorageErrorClickedListener() {
2715 return new ActionClickedListener() {
2716 @Override
2717 public void onActionClicked() {
Paul Westbrook4969e0c2012-08-20 14:38:39 -07002718 showStorageErrorDialog();
Andrew Sapperstein00179f12012-08-09 15:15:40 -07002719 }
2720 };
2721 }
2722
Paul Westbrook4969e0c2012-08-20 14:38:39 -07002723 private void showStorageErrorDialog() {
2724 DialogFragment fragment = (DialogFragment)
2725 mFragmentManager.findFragmentByTag(SYNC_ERROR_DIALOG_FRAGMENT_TAG);
2726 if (fragment == null) {
2727 fragment = SyncErrorDialogFragment.newInstance();
2728 }
2729 fragment.show(mFragmentManager, SYNC_ERROR_DIALOG_FRAGMENT_TAG);
2730 }
2731
Andrew Sapperstein00179f12012-08-09 15:15:40 -07002732 private ActionClickedListener getInternalErrorClickedListener() {
2733 return new ActionClickedListener() {
2734 @Override
2735 public void onActionClicked() {
Paul Westbrook17beb0b2012-08-20 13:34:37 -07002736 Utils.sendFeedback(
2737 mActivity.getActivityContext(), mAccount, true /* reportingProblem */);
Andrew Sapperstein00179f12012-08-09 15:15:40 -07002738 }
2739 };
2740 }
Paul Westbrook4969e0c2012-08-20 14:38:39 -07002741
2742 @Override
2743 public void onFooterViewErrorActionClick(Folder folder, int errorStatus) {
2744 Uri uri = null;
2745 switch (errorStatus) {
2746 case UIProvider.LastSyncResult.CONNECTION_ERROR:
2747 if (folder != null && folder.refreshUri != null) {
2748 uri = folder.refreshUri;
2749 }
2750 break;
2751 case UIProvider.LastSyncResult.AUTH_ERROR:
Paul Westbrook122f7c22012-08-20 17:50:31 -07002752 promptUserForAuthentication(mAccount);
Paul Westbrook4969e0c2012-08-20 14:38:39 -07002753 return;
2754 case UIProvider.LastSyncResult.SECURITY_ERROR:
2755 return; // Currently we do nothing for security errors.
2756 case UIProvider.LastSyncResult.STORAGE_ERROR:
2757 showStorageErrorDialog();
2758 return;
2759 case UIProvider.LastSyncResult.INTERNAL_ERROR:
2760 Utils.sendFeedback(
2761 mActivity.getActivityContext(), mAccount, true /* reportingProblem */);
2762 return;
2763 default:
2764 return;
2765 }
2766
2767 if (uri != null) {
2768 startAsyncRefreshTask(uri);
2769 }
2770 }
2771
2772 @Override
2773 public void onFooterViewLoadMoreClick(Folder folder) {
2774 if (folder != null && folder.loadMoreUri != null) {
2775 startAsyncRefreshTask(folder.loadMoreUri);
2776 }
2777 }
2778
2779 private void startAsyncRefreshTask(Uri uri) {
2780 if (mFolderSyncTask != null) {
2781 mFolderSyncTask.cancel(true);
2782 }
2783 mFolderSyncTask = new AsyncRefreshTask(mActivity.getActivityContext(), uri);
2784 mFolderSyncTask.execute();
2785 }
Paul Westbrook122f7c22012-08-20 17:50:31 -07002786
2787 private void promptUserForAuthentication(Account account) {
Paul Westbrook429399b2012-08-24 11:19:17 -07002788 if (account != null && !Utils.isEmpty(account.reauthenticationIntentUri)) {
Paul Westbrook122f7c22012-08-20 17:50:31 -07002789 final Intent authenticationIntent =
2790 new Intent(Intent.ACTION_VIEW, account.reauthenticationIntentUri);
2791 mActivity.startActivityForResult(authenticationIntent, REAUTHENTICATE_REQUEST_CODE);
2792 }
2793 }
Vikram Aggarwal4a5c5302012-01-12 15:07:13 -08002794}