blob: 1a0ca5a1fb58f4b855eaadc8630d872ddba50a98 [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;
Mindy Pereirafd0c2972012-03-27 13:50:39 -070055import android.widget.Toast;
Vikram Aggarwala55b36c2012-01-13 11:45:02 -080056
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -080057import com.android.mail.ConversationListContext;
Andy Huangf9a73482012-03-13 15:54:02 -070058import com.android.mail.R;
Mindy Pereira967ede62012-03-22 09:29:09 -070059import com.android.mail.browse.ConversationCursor;
mindyp84f7d322012-10-01 17:14:40 -070060import com.android.mail.browse.ConversationItemView;
mindypca87de42012-09-28 15:02:39 -070061import com.android.mail.browse.ConversationItemViewModel;
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;
Scott Kennedy0d0f8b02012-10-12 15:18:18 -070078import com.android.mail.providers.UIProvider.AccountColumns;
Paul Westbrook2388c5d2012-03-25 12:29:11 -070079import com.android.mail.providers.UIProvider.AccountCursorExtraKeys;
Scott Kennedy0d0f8b02012-10-12 15:18:18 -070080import com.android.mail.providers.UIProvider.AutoAdvance;
Mindy Pereirac9d59182012-03-22 16:06:46 -070081import com.android.mail.providers.UIProvider.ConversationColumns;
Paul Westbrook5109c512012-11-05 11:00:30 -080082import com.android.mail.providers.UIProvider.ConversationOperations;
Vikram Aggarwal54452ae2012-03-13 15:29:00 -070083import com.android.mail.providers.UIProvider.FolderCapabilities;
Andrew Sappersteinc2c9dc12012-07-02 18:17:32 -070084import com.android.mail.ui.ActionableToastBar.ActionClickedListener;
Andy Huang839ada22012-07-20 15:48:40 -070085import com.android.mail.utils.ContentProviderTask;
Paul Westbrookb334c902012-06-25 11:42:46 -070086import com.android.mail.utils.LogTag;
Mindy Pereira5cb0c3e2012-02-22 15:22:47 -080087import com.android.mail.utils.LogUtils;
Vikram Aggarwalfa131a22012-02-02 13:56:22 -080088import com.android.mail.utils.Utils;
Scott Kennedy0d0f8b02012-10-12 15:18:18 -070089
Paul Westbrookca08fc12012-07-31 12:01:15 -070090import com.google.common.base.Objects;
Paul Westbrook77eee622012-07-10 13:41:57 -070091import com.google.common.collect.ImmutableList;
Mindy Pereiraacf60392012-04-06 09:11:00 -070092import com.google.common.collect.Lists;
Marc Blank167faa82012-03-21 13:11:53 -070093import com.google.common.collect.Sets;
Vikram Aggarwal4a5c5302012-01-12 15:07:13 -080094
Marc Blank167faa82012-03-21 13:11:53 -070095import java.util.ArrayList;
Mindy Pereirafbe40192012-03-20 10:40:45 -070096import java.util.Collection;
Andy Huang839ada22012-07-20 15:48:40 -070097import java.util.Collections;
Mindy Pereira8db7e402012-07-13 10:32:47 -070098import java.util.HashMap;
Vikram Aggarwalcc754b12012-08-30 14:04:21 -070099import java.util.List;
Paul Westbrook23b74b92012-02-29 11:36:12 -0800100import java.util.Set;
Marc Blankbf128eb2012-04-18 15:58:45 -0700101import java.util.TimerTask;
Paul Westbrook23b74b92012-02-29 11:36:12 -0800102
103
Vikram Aggarwal4a5c5302012-01-12 15:07:13 -0800104/**
Mindy Pereira161f50d2012-02-28 15:47:19 -0800105 * This is an abstract implementation of the Activity Controller. This class
106 * knows how to respond to menu items, state changes, layout changes, etc. It
107 * weaves together the views and listeners, dispatching actions to the
108 * respective underlying classes.
Vikram Aggarwala55b36c2012-01-13 11:45:02 -0800109 * <p>
Mindy Pereira161f50d2012-02-28 15:47:19 -0800110 * Even though this class is abstract, it should provide default implementations
111 * for most, if not all the methods in the ActivityController interface. This
112 * makes the task of the subclasses easier: OnePaneActivityController and
113 * TwoPaneActivityController can be concise when the common functionality is in
114 * AbstractActivityController.
115 * </p>
116 * <p>
117 * In the Gmail codebase, this was called BaseActivityController
118 * </p>
Vikram Aggarwal4a5c5302012-01-12 15:07:13 -0800119 */
Vikram Aggarwal531488e2012-05-29 16:36:52 -0700120public abstract class AbstractActivityController implements ActivityController {
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -0800121 // Keys for serialization of various information in Bundles.
Vikram Aggarwalcabd3f22012-04-19 10:14:41 -0700122 /** Tag for {@link #mAccount} */
Vikram Aggarwal6c511582012-02-27 10:59:47 -0800123 private static final String SAVED_ACCOUNT = "saved-account";
Vikram Aggarwalcabd3f22012-04-19 10:14:41 -0700124 /** Tag for {@link #mFolder} */
Mindy Pereira5e478d22012-03-26 18:04:58 -0700125 private static final String SAVED_FOLDER = "saved-folder";
Vikram Aggarwalcabd3f22012-04-19 10:14:41 -0700126 /** Tag for {@link #mCurrentConversation} */
Mindy Pereira26f23fc2012-03-27 10:26:04 -0700127 private static final String SAVED_CONVERSATION = "saved-conversation";
Vikram Aggarwalcabd3f22012-04-19 10:14:41 -0700128 /** Tag for {@link #mSelectedSet} */
129 private static final String SAVED_SELECTED_SET = "saved-selected-set";
Vikram Aggarwal649b9ea2012-08-27 12:15:20 -0700130 /** Tag for {@link ActionableToastBar#getOperation()} */
Mindy Pereirad33674992012-06-25 16:26:30 -0700131 private static final String SAVED_TOAST_BAR_OP = "saved-toast-bar-op";
Vikram Aggarwal649b9ea2012-08-27 12:15:20 -0700132 /** Tag for {@link #mFolderListFolder} */
133 private static final String SAVED_HIERARCHICAL_FOLDER = "saved-hierarchical-folder";
Vikram Aggarwalf3341402012-08-07 10:09:38 -0700134 /** Tag for {@link ConversationListContext#searchQuery} */
135 private static final String SAVED_QUERY = "saved-query";
Mindy Pereira5cb0c3e2012-02-22 15:22:47 -0800136
Vikram Aggarwal6902dcf2012-04-11 08:57:42 -0700137 /** Tag used when loading a wait fragment */
138 protected static final String TAG_WAIT = "wait-fragment";
139 /** Tag used when loading a conversation list fragment. */
Paul Westbrookbf232c32012-04-18 03:17:41 -0700140 public static final String TAG_CONVERSATION_LIST = "tag-conversation-list";
Vikram Aggarwal6902dcf2012-04-11 08:57:42 -0700141 /** Tag used when loading a folder list fragment. */
142 protected static final String TAG_FOLDER_LIST = "tag-folder-list";
143
Vikram Aggarwald7a12cd2012-02-03 09:36:20 -0800144 protected Account mAccount;
Mindy Pereira12a4d802012-06-22 09:24:43 -0700145 protected Folder mFolder;
Vikram Aggarwal69b5c302012-09-05 11:11:13 -0700146 /** True when {@link #mFolder} is first shown to the user. */
147 private boolean mFolderChanged = false;
Andy Huang6681e542012-06-14 14:36:45 -0700148 protected MailActionBarView mActionBarView;
Vikram Aggarwal7c401b72012-08-13 16:43:47 -0700149 protected final ControllableActivity mActivity;
Vikram Aggarwala55b36c2012-01-13 11:45:02 -0800150 protected final Context mContext;
Vikram Aggarwal6902dcf2012-04-11 08:57:42 -0700151 private final FragmentManager mFragmentManager;
Vikram Aggarwalec5cbf72012-03-08 15:10:35 -0800152 protected final RecentFolderList mRecentFolderList;
Vikram Aggarwal80aeac52012-02-07 15:27:20 -0800153 protected ConversationListContext mConvListContext;
Mindy Pereira9b875682012-02-15 18:10:54 -0800154 protected Conversation mCurrentConversation;
Vikram Aggarwald7a12cd2012-02-03 09:36:20 -0800155
Paul Westbrook6ead20d2012-03-19 14:48:14 -0700156 /** A {@link android.content.BroadcastReceiver} that suppresses new e-mail notifications. */
157 private SuppressNotificationReceiver mNewEmailReceiver = null;
158
Mindy Pereirafbe40192012-03-20 10:40:45 -0700159 protected Handler mHandler = new Handler();
Mindy Pereirafa995b42012-07-25 12:06:13 -0700160
Vikram Aggarwalfa131a22012-02-02 13:56:22 -0800161 /**
Mindy Pereira161f50d2012-02-28 15:47:19 -0800162 * The current mode of the application. All changes in mode are initiated by
163 * the activity controller. View mode changes are propagated to classes that
164 * attach themselves as listeners of view mode changes.
Vikram Aggarwalfa131a22012-02-02 13:56:22 -0800165 */
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -0800166 protected final ViewMode mViewMode;
Vikram Aggarwal80aeac52012-02-07 15:27:20 -0800167 protected ContentResolver mResolver;
Vikram Aggarwal7dedb952012-02-16 16:10:23 -0800168 protected boolean isLoaderInitialized = false;
Mindy Pereirab7b33e02012-02-21 15:32:19 -0800169 private AsyncRefreshTask mAsyncRefreshTask;
Mindy Pereira5cb0c3e2012-02-22 15:22:47 -0800170
Andy Huang4e0158f2012-08-07 21:06:01 -0700171 private boolean mDestroyed;
172
Andy Huang1ee96b22012-08-24 20:19:53 -0700173 /**
174 * Are we in a point in the Activity/Fragment lifecycle where it's safe to execute fragment
175 * transactions? (including back stack manipulation)
176 * <p>
177 * Per docs in {@link FragmentManager#beginTransaction()}, this flag starts out true, switches
178 * to false after {@link Activity#onSaveInstanceState}, and becomes true again in both onStart
179 * and onResume.
180 */
181 private boolean mSafeToModifyFragments = true;
182
Paul Westbrook23b74b92012-02-29 11:36:12 -0800183 private final Set<Uri> mCurrentAccountUris = Sets.newHashSet();
Mindy Pereira967ede62012-03-22 09:29:09 -0700184 protected ConversationCursor mConversationListCursor;
Andy Huang632721e2012-04-11 16:57:26 -0700185 private final DataSetObservable mConversationListObservable = new DataSetObservable() {
186 @Override
187 public void registerObserver(DataSetObserver observer) {
188 final int count = mObservers.size();
189 super.registerObserver(observer);
Vikram Aggarwal7c401b72012-08-13 16:43:47 -0700190 LogUtils.d(LOG_TAG, "IN AAC.register(List)Observer: %s before=%d after=%d", observer,
Andy Huang632721e2012-04-11 16:57:26 -0700191 count, mObservers.size());
192 }
193 @Override
194 public void unregisterObserver(DataSetObserver observer) {
195 final int count = mObservers.size();
196 super.unregisterObserver(observer);
Vikram Aggarwal7c401b72012-08-13 16:43:47 -0700197 LogUtils.d(LOG_TAG, "IN AAC.unregister(List)Observer: %s before=%d after=%d", observer,
Andy Huang632721e2012-04-11 16:57:26 -0700198 count, mObservers.size());
199 }
200 };
Marc Blankbf128eb2012-04-18 15:58:45 -0700201
Marc Blankbf128eb2012-04-18 15:58:45 -0700202 private RefreshTimerTask mConversationListRefreshTask;
Marc Blanke1d1b072012-04-13 17:29:16 -0700203
Vikram Aggarwal7c401b72012-08-13 16:43:47 -0700204 /** Listeners that are interested in changes to the current account. */
205 private final DataSetObservable mAccountObservers = new DataSetObservable() {
206 @Override
207 public void registerObserver(DataSetObserver observer) {
208 final int count = mObservers.size();
209 super.registerObserver(observer);
210 LogUtils.d(LOG_TAG, "IN AAC.register(Account)Observer: %s before=%d after=%d",
211 observer, count, mObservers.size());
212 }
213 @Override
214 public void unregisterObserver(DataSetObserver observer) {
215 final int count = mObservers.size();
216 super.unregisterObserver(observer);
217 LogUtils.d(LOG_TAG, "IN AAC.unregister(Account)Observer: %s before=%d after=%d",
218 observer, count, mObservers.size());
219 }
220 };
Vikram Aggarwal7d816002012-04-17 17:06:41 -0700221
Vikram Aggarwal58cad2e2012-08-28 16:18:23 -0700222 /** Listeners that are interested in changes to the recent folders. */
223 private final DataSetObservable mRecentFolderObservers = new DataSetObservable() {
224 @Override
225 public void registerObserver(DataSetObserver observer) {
226 final int count = mObservers.size();
227 super.registerObserver(observer);
228 LogUtils.d(LOG_TAG, "IN AAC.register(RecentFolder)Observer: %s before=%d after=%d",
229 observer, count, mObservers.size());
230 }
231 @Override
232 public void unregisterObserver(DataSetObserver observer) {
233 final int count = mObservers.size();
234 super.unregisterObserver(observer);
235 LogUtils.d(LOG_TAG, "IN AAC.unregister(RecentFolder)Observer: %s before=%d after=%d",
236 observer, count, mObservers.size());
237 }
238 };
239
Mindy Pereira967ede62012-03-22 09:29:09 -0700240 /**
241 * Selected conversations, if any.
242 */
Andy Huang4556a442012-03-30 16:42:05 -0700243 private final ConversationSelectionSet mSelectedSet = new ConversationSelectionSet();
Mindy Pereiradac00542012-03-01 10:50:33 -0800244
Paul Westbrookc7a070f2012-04-12 01:46:41 -0700245 private final int mFolderItemUpdateDelayMs;
246
Vikram Aggarwal135fd022012-04-11 14:44:15 -0700247 /** Keeps track of selected and unselected conversations */
Paul Westbrook937c94f2012-08-16 13:01:18 -0700248 final protected ConversationPositionTracker mTracker;
Vikram Aggarwalaf1ee0c2012-04-12 17:13:13 -0700249
Vikram Aggarwale128fc22012-04-04 12:33:34 -0700250 /**
251 * Action menu associated with the selected set.
252 */
253 SelectedConversationsActionMenu mCabActionMenu;
Andrew Sappersteinc2c9dc12012-07-02 18:17:32 -0700254 protected ActionableToastBar mToastBar;
Andy Huang632721e2012-04-11 16:57:26 -0700255 protected ConversationPagerController mPagerController;
Mindy Pereiraab486362012-03-21 18:18:53 -0700256
Andy Huangb1c34dc2012-04-17 16:36:19 -0700257 // this is split out from the general loader dispatcher because its loader doesn't return a
258 // basic Cursor
259 private final ConversationListLoaderCallbacks mListCursorCallbacks =
260 new ConversationListLoaderCallbacks();
261
Andy Huang090db1e2012-07-25 13:25:28 -0700262 private final DataSetObservable mFolderObservable = new DataSetObservable();
263
Paul Westbrookb334c902012-06-25 11:42:46 -0700264 protected static final String LOG_TAG = LogTag.getLogTag();
Vikram Aggarwalfa4b47e2012-03-09 13:02:46 -0800265 /** Constants used to differentiate between the types of loaders. */
266 private static final int LOADER_ACCOUNT_CURSOR = 0;
Vikram Aggarwalfa4b47e2012-03-09 13:02:46 -0800267 private static final int LOADER_FOLDER_CURSOR = 2;
268 private static final int LOADER_RECENT_FOLDERS = 3;
Mindy Pereira967ede62012-03-22 09:29:09 -0700269 private static final int LOADER_CONVERSATION_LIST = 4;
Mindy Pereiraab486362012-03-21 18:18:53 -0700270 private static final int LOADER_ACCOUNT_INBOX = 5;
271 private static final int LOADER_SEARCH = 6;
Paul Westbrook2d50bcd2012-04-10 11:53:47 -0700272 private static final int LOADER_ACCOUNT_UPDATE_CURSOR = 7;
Vikram Aggarwal7a5d95a2012-07-27 16:24:54 -0700273 /**
274 * Guaranteed to be the last loader ID used by the activity. Loaders are owned by Activity or
275 * fragments, and within an activity, loader IDs need to be unique. A hack to ensure that the
276 * {@link FolderWatcher} can create its folder loaders without clashing with the IDs of those
277 * of the {@link AbstractActivityController}. Currently, the {@link FolderWatcher} is the only
278 * other class that uses this activity's LoaderManager. If another class needs activity-level
279 * loaders, consider consolidating the loaders in a central location: a UI-less fragment
280 * perhaps.
281 */
282 public static final int LAST_LOADER_ID = 100;
Vikram Aggarwala55b36c2012-01-13 11:45:02 -0800283
Paul Westbrook2388c5d2012-03-25 12:29:11 -0700284 private static final int ADD_ACCOUNT_REQUEST_CODE = 1;
Paul Westbrook122f7c22012-08-20 17:50:31 -0700285 private static final int REAUTHENTICATE_REQUEST_CODE = 2;
Paul Westbrook2388c5d2012-03-25 12:29:11 -0700286
Vikram Aggarwale8a85322012-04-24 09:01:38 -0700287 /** The pending destructive action to be carried out before swapping the conversation cursor.*/
288 private DestructiveAction mPendingDestruction;
Andrew Sappersteinc2c9dc12012-07-02 18:17:32 -0700289 protected AsyncRefreshTask mFolderSyncTask;
Mindy Pereira49e5dbe2012-07-12 11:47:54 -0700290 // Task for setting any share intents for the account to enabled.
291 // This gets cancelled if the user kills the app before it finishes, and
292 // will just run the next time the user opens the app.
293 private AsyncTask<String, Void, Void> mEnableShareIntents;
Mindy Pereirac975e842012-07-16 09:15:00 -0700294 private Folder mFolderListFolder;
mindyp5390fca2012-08-22 12:12:25 -0700295 private boolean mIsDragHappening;
mindypead50392012-08-23 11:03:53 -0700296 private int mShowUndoBarDelay;
mindyp6f54e1b2012-10-09 09:54:08 -0700297 private boolean mRecentsDataUpdated;
Vikram Aggarwala3f43d42012-10-25 16:21:30 -0700298 /** A wait fragment we added, if any. */
299 private WaitFragment mWaitFragment;
Vikram Aggarwalf0ef2302012-11-07 14:53:34 -0800300 /** True if we have results from a search query */
301 private boolean mHaveSearchResults = false;
Andrew Sapperstein00179f12012-08-09 15:15:40 -0700302 public static final String SYNC_ERROR_DIALOG_FRAGMENT_TAG = "SyncErrorDialogFragment";
Vikram Aggarwale8a85322012-04-24 09:01:38 -0700303
Vikram Aggarwala55b36c2012-01-13 11:45:02 -0800304 public AbstractActivityController(MailActivity activity, ViewMode viewMode) {
305 mActivity = activity;
Vikram Aggarwal6902dcf2012-04-11 08:57:42 -0700306 mFragmentManager = mActivity.getFragmentManager();
Vikram Aggarwala55b36c2012-01-13 11:45:02 -0800307 mViewMode = viewMode;
308 mContext = activity.getApplicationContext();
Vikram Aggarwal025eba82012-05-08 10:45:30 -0700309 mRecentFolderList = new RecentFolderList(mContext);
Paul Westbrook937c94f2012-08-16 13:01:18 -0700310 mTracker = new ConversationPositionTracker(this);
Mindy Pereira967ede62012-03-22 09:29:09 -0700311 // Allow the fragment to observe changes to its own selection set. No other object is
312 // aware of the selected set.
313 mSelectedSet.addObserver(this);
Paul Westbrookc7a070f2012-04-12 01:46:41 -0700314
315 mFolderItemUpdateDelayMs =
316 mContext.getResources().getInteger(R.integer.folder_item_refresh_delay_ms);
mindypead50392012-08-23 11:03:53 -0700317 mShowUndoBarDelay =
318 mContext.getResources().getInteger(R.integer.show_undo_bar_delay_ms);
Vikram Aggarwala55b36c2012-01-13 11:45:02 -0800319 }
Vikram Aggarwal4a5c5302012-01-12 15:07:13 -0800320
321 @Override
Vikram Aggarwale9a81032012-02-22 13:15:35 -0800322 public Account getCurrentAccount() {
323 return mAccount;
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -0800324 }
325
326 @Override
327 public ConversationListContext getCurrentListContext() {
Vikram Aggarwale9a81032012-02-22 13:15:35 -0800328 return mConvListContext;
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -0800329 }
330
331 @Override
Vikram Aggarwal4a5c5302012-01-12 15:07:13 -0800332 public String getHelpContext() {
Paul Westbrook30745b62012-08-19 14:10:32 -0700333 final int mode = mViewMode.getMode();
334 final int helpContextResId;
335 switch (mode) {
336 case ViewMode.WAITING_FOR_ACCOUNT_INITIALIZATION:
337 helpContextResId = R.string.wait_help_context;
338 break;
339 default:
340 helpContextResId = R.string.main_help_context;
341 }
342 return mContext.getString(helpContextResId);
Vikram Aggarwal4a5c5302012-01-12 15:07:13 -0800343 }
344
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -0800345 @Override
Vikram Aggarwalef7c9922012-04-23 12:35:04 -0700346 public final ConversationCursor getConversationListCursor() {
Mindy Pereira967ede62012-03-22 09:29:09 -0700347 return mConversationListCursor;
348 }
349
Vikram Aggarwal6902dcf2012-04-11 08:57:42 -0700350 /**
Vikram Aggarwal34a65012012-04-17 12:39:06 -0700351 * Check if the fragment is attached to an activity and has a root view.
352 * @param in
353 * @return true if the fragment is valid, false otherwise
354 */
355 private static final boolean isValidFragment(Fragment in) {
356 if (in == null || in.getActivity() == null || in.getView() == null) {
357 return false;
358 }
359 return true;
360 }
361
362 /**
Vikram Aggarwal49e0e992012-09-21 13:53:15 -0700363 * Get the conversation list fragment for this activity. If the conversation list fragment is
364 * not attached, this method returns null.
Andy Huang839ada22012-07-20 15:48:40 -0700365 *
Vikram Aggarwal49e0e992012-09-21 13:53:15 -0700366 * Caution! This method returns the {@link ConversationListFragment} after the fragment has been
367 * added, <b>and</b> after the {@link FragmentManager} has run through its queue to add the
368 * fragment. There is a non-trivial amount of time after the fragment is instantiated and before
369 * this call returns a non-null value, depending on the {@link FragmentManager}. If you
370 * need the fragment immediately after adding it, consider making the fragment an observer of
371 * the controller and perform the task immediately on {@link Fragment#onActivityCreated(Bundle)}
Vikram Aggarwal6902dcf2012-04-11 08:57:42 -0700372 */
373 protected ConversationListFragment getConversationListFragment() {
374 final Fragment fragment = mFragmentManager.findFragmentByTag(TAG_CONVERSATION_LIST);
Vikram Aggarwal34a65012012-04-17 12:39:06 -0700375 if (isValidFragment(fragment)) {
376 return (ConversationListFragment) fragment;
Andy Huang9585d782012-04-16 19:45:04 -0700377 }
Vikram Aggarwal34a65012012-04-17 12:39:06 -0700378 return null;
Vikram Aggarwal6902dcf2012-04-11 08:57:42 -0700379 }
380
381 /**
Vikram Aggarwal6902dcf2012-04-11 08:57:42 -0700382 * Returns the folder list fragment attached with this activity. If no such fragment is attached
383 * this method returns null.
Andy Huang839ada22012-07-20 15:48:40 -0700384 *
Vikram Aggarwal49e0e992012-09-21 13:53:15 -0700385 * Caution! This method returns the {@link FolderListFragment} after the fragment has been
386 * added, <b>and</b> after the {@link FragmentManager} has run through its queue to add the
387 * fragment. There is a non-trivial amount of time after the fragment is instantiated and before
388 * this call returns a non-null value, depending on the {@link FragmentManager}. If you
389 * need the fragment immediately after adding it, consider making the fragment an observer of
390 * the controller and perform the task immediately on {@link Fragment#onActivityCreated(Bundle)}
Vikram Aggarwal6902dcf2012-04-11 08:57:42 -0700391 */
392 protected FolderListFragment getFolderListFragment() {
393 final Fragment fragment = mFragmentManager.findFragmentByTag(TAG_FOLDER_LIST);
Vikram Aggarwal34a65012012-04-17 12:39:06 -0700394 if (isValidFragment(fragment)) {
395 return (FolderListFragment) fragment;
Andy Huang9585d782012-04-16 19:45:04 -0700396 }
Vikram Aggarwal34a65012012-04-17 12:39:06 -0700397 return null;
Vikram Aggarwal6902dcf2012-04-11 08:57:42 -0700398 }
399
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -0800400 /**
Mindy Pereira161f50d2012-02-28 15:47:19 -0800401 * Initialize the action bar. This is not visible to OnePaneController and
402 * TwoPaneController so they cannot override this behavior.
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -0800403 */
Vikram Aggarwalabd24d82012-04-26 13:23:14 -0700404 private void initializeActionBar() {
405 final ActionBar actionBar = mActivity.getActionBar();
Andy Huang5895f7b2012-06-01 17:07:20 -0700406 if (actionBar == null) {
407 return;
Vikram Aggarwalabd24d82012-04-26 13:23:14 -0700408 }
Andy Huang5895f7b2012-06-01 17:07:20 -0700409
410 // be sure to inherit from the ActionBar theme when inflating
411 final LayoutInflater inflater = LayoutInflater.from(actionBar.getThemedContext());
Mindy Pereira82faec72012-06-14 17:21:50 -0700412 final boolean isSearch = mActivity.getIntent() != null
413 && Intent.ACTION_SEARCH.equals(mActivity.getIntent().getAction());
414 mActionBarView = (MailActionBarView) inflater.inflate(
415 isSearch ? R.layout.search_actionbar_view : R.layout.actionbar_view, null);
Andy Huang5895f7b2012-06-01 17:07:20 -0700416 mActionBarView.initialize(mActivity, this, mViewMode, actionBar, mRecentFolderList);
Vikram Aggarwalabd24d82012-04-26 13:23:14 -0700417 }
418
419 /**
420 * Attach the action bar to the activity.
421 */
422 private void attachActionBar() {
423 final ActionBar actionBar = mActivity.getActionBar();
424 if (actionBar != null && mActionBarView != null) {
Vikram Aggarwalec5cbf72012-03-08 15:10:35 -0800425 actionBar.setCustomView(mActionBarView, new ActionBar.LayoutParams(
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -0800426 LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT));
Vikram Aggarwal649b9ea2012-08-27 12:15:20 -0700427 // Show a custom view and home icon, but remove the title
428 final int mask = ActionBar.DISPLAY_SHOW_CUSTOM | ActionBar.DISPLAY_SHOW_TITLE
429 | ActionBar.DISPLAY_SHOW_HOME;
430 final int enabled = ActionBar.DISPLAY_SHOW_CUSTOM | ActionBar.DISPLAY_SHOW_HOME;
431 actionBar.setDisplayOptions(enabled, mask);
Vikram Aggarwalabd24d82012-04-26 13:23:14 -0700432 mActionBarView.attach();
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -0800433 }
Vikram Aggarwalabd24d82012-04-26 13:23:14 -0700434 mViewMode.addListener(mActionBarView);
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -0800435 }
436
437 /**
Mindy Pereira161f50d2012-02-28 15:47:19 -0800438 * Returns whether the conversation list fragment is visible or not.
439 * Different layouts will have their own notion on the visibility of
440 * fragments, so this method needs to be overriden.
441 *
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -0800442 */
443 protected abstract boolean isConversationListVisible();
444
Vikram Aggarwaleb7d3292012-04-20 17:07:20 -0700445 /**
Vikram Aggarwal34f7b232012-10-17 13:32:23 -0700446 * If required, starts wait mode for the current account.
Vikram Aggarwal34f7b232012-10-17 13:32:23 -0700447 */
448 final void perhapsEnterWaitMode() {
449 // If the account is not initialized, then show the wait fragment, since nothing can be
450 // shown.
451 if (mAccount.isAccountInitializationRequired()) {
452 showWaitForInitialization();
453 return;
454 }
455
456 final boolean inWaitingMode = inWaitMode();
457 final boolean isSyncRequired = mAccount.isAccountSyncRequired();
458 if (isSyncRequired) {
459 if (inWaitingMode) {
460 // Update the WaitFragment's account object
461 updateWaitMode();
462 } else {
463 // Transition to waiting mode
464 showWaitForInitialization();
465 }
466 } else if (inWaitingMode) {
467 // Dismiss waiting mode
468 hideWaitForInitialization();
469 }
470 }
471
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -0800472 @Override
Mindy Pereira28e0c342012-02-17 15:05:13 -0800473 public void onAccountChanged(Account account) {
Vikram Aggarwal60069912012-07-24 14:26:09 -0700474 // Is the account or account settings different from the existing account?
Vikram Aggarwal66bc6ed2012-07-31 09:59:55 -0700475 final boolean firstLoad = mAccount == null;
476 final boolean accountChanged = firstLoad || !account.uri.equals(mAccount.uri);
Vikram Aggarwal472e5362012-11-05 14:17:23 -0800477 // If nothing has changed, return early without wasting any more time.
478 if (!accountChanged && !account.settingsDiffer(mAccount)) {
479 return;
480 }
481 // We also don't want to do anything if the new account is null
482 if (account == null) {
483 LogUtils.e(LOG_TAG, "AAC.onAccountChanged(null) called.");
484 return;
485 }
486 final String accountName = account.name;
487 mHandler.post(new Runnable() {
488 @Override
489 public void run() {
490 MailActivity.setForegroundNdef(MailActivity.getMailtoNdef(accountName));
Mindy Pereirafa20c1a2012-07-23 13:00:02 -0700491 }
Vikram Aggarwal472e5362012-11-05 14:17:23 -0800492 });
493 if (accountChanged) {
494 commitDestructiveActions(false);
495 }
496 // Change the account here
497 setAccount(account);
498 // And carry out associated actions.
499 cancelRefreshTask();
500 if (accountChanged) {
501 loadAccountInbox();
502 }
503 // Check if we need to force setting up an account before proceeding.
504 if (mAccount != null && !Uri.EMPTY.equals(mAccount.settings.setupIntentUri)) {
505 // Launch the intent!
506 final Intent intent = new Intent(Intent.ACTION_EDIT);
507 intent.setData(mAccount.settings.setupIntentUri);
508 mActivity.startActivity(intent);
Mindy Pereira28d5f722012-02-15 12:32:40 -0800509 }
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -0800510 }
511
Vikram Aggarwale6340bc2012-03-26 15:57:09 -0700512 /**
Vikram Aggarwal7c401b72012-08-13 16:43:47 -0700513 * Adds a listener interested in change in the current account. If a class is storing a
514 * reference to the current account, it should listen on changes, so it can receive updates to
515 * settings. Must happen in the UI thread.
516 */
Mindy Pereiraefe3d252012-03-01 14:20:44 -0800517 @Override
Vikram Aggarwal7c401b72012-08-13 16:43:47 -0700518 public void registerAccountObserver(DataSetObserver obs) {
519 mAccountObservers.registerObserver(obs);
Mindy Pereiraefe3d252012-03-01 14:20:44 -0800520 }
521
Vikram Aggarwal7d816002012-04-17 17:06:41 -0700522 /**
Vikram Aggarwal7c401b72012-08-13 16:43:47 -0700523 * Removes a listener from receiving current account changes.
Vikram Aggarwal7d816002012-04-17 17:06:41 -0700524 * Must happen in the UI thread.
525 */
Vikram Aggarwal7c401b72012-08-13 16:43:47 -0700526 @Override
527 public void unregisterAccountObserver(DataSetObserver obs) {
528 mAccountObservers.unregisterObserver(obs);
Vikram Aggarwal7d816002012-04-17 17:06:41 -0700529 }
530
Vikram Aggarwal7c401b72012-08-13 16:43:47 -0700531 @Override
532 public Account getAccount() {
533 return mAccount;
Vikram Aggarwal7d816002012-04-17 17:06:41 -0700534 }
535
Mindy Pereirae0828392012-03-08 10:38:40 -0800536 private void fetchSearchFolder(Intent intent) {
Vikram Aggarwal2b703c62012-09-18 13:54:15 -0700537 final Bundle args = new Bundle();
Mindy Pereiraab486362012-03-21 18:18:53 -0700538 args.putString(ConversationListContext.EXTRA_SEARCH_QUERY, intent
Mindy Pereirae0828392012-03-08 10:38:40 -0800539 .getStringExtra(ConversationListContext.EXTRA_SEARCH_QUERY));
Mindy Pereiraab486362012-03-21 18:18:53 -0700540 mActivity.getLoaderManager().restartLoader(LOADER_SEARCH, args, this);
Mindy Pereirae0828392012-03-08 10:38:40 -0800541 }
542
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -0800543 @Override
Mindy Pereira28e0c342012-02-17 15:05:13 -0800544 public void onFolderChanged(Folder folder) {
Vikram Aggarwalf3341402012-08-07 10:09:38 -0700545 changeFolder(folder, null);
546 }
547
548 /**
Vikram Aggarwal203dc002012-08-23 13:59:04 -0700549 * Sets the folder state without changing view mode and without creating a list fragment, if
550 * possible.
551 * @param folder
552 */
553 private void setListContext(Folder folder, String query) {
554 updateFolder(folder);
555 if (query != null) {
556 mConvListContext = ConversationListContext.forSearchQuery(mAccount, mFolder, query);
557 } else {
558 mConvListContext = ConversationListContext.forFolder(mAccount, mFolder);
559 }
Vikram Aggarwal203dc002012-08-23 13:59:04 -0700560 cancelRefreshTask();
561 }
562
563 /**
Vikram Aggarwalf3341402012-08-07 10:09:38 -0700564 * Changes the folder to the value provided here. This causes the view mode to change.
565 * @param folder the folder to change to
566 * @param query if non-null, this represents the search string that the folder represents.
567 */
Vikram Aggarwal93dc2022012-11-05 13:36:57 -0800568 private final void changeFolder(Folder folder, String query) {
Vikram Aggarwal203dc002012-08-23 13:59:04 -0700569 if (!Objects.equal(mFolder, folder)) {
570 commitDestructiveActions(false);
571 }
Mindy Pereiraa1f99982012-07-16 16:32:15 -0700572 if (folder != null && !folder.equals(mFolder)
573 || (mViewMode.getMode() != ViewMode.CONVERSATION_LIST)) {
Vikram Aggarwal203dc002012-08-23 13:59:04 -0700574 setListContext(folder, query);
Mindy Pereira28e0c342012-02-17 15:05:13 -0800575 showConversationList(mConvListContext);
Mindy Pereira28e0c342012-02-17 15:05:13 -0800576 }
Vikram Aggarwal93dc2022012-11-05 13:36:57 -0800577 resetActionBarIcon();
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -0800578 }
579
Mindy Pereira13c12a62012-05-31 15:41:08 -0700580 @Override
Mindy Pereira505df5f2012-06-19 17:57:17 -0700581 public void onFolderSelected(Folder folder) {
Mindy Pereira13c12a62012-05-31 15:41:08 -0700582 onFolderChanged(folder);
583 }
584
Vikram Aggarwal792ccba2012-03-27 13:46:57 -0700585 /**
586 * Update the recent folders. This only needs to be done once when accessing a new folder.
587 */
Paul Westbrook7ebdfd02012-03-21 15:55:30 -0700588 private void updateRecentFolderList() {
Mindy Pereiraab486362012-03-21 18:18:53 -0700589 if (mFolder != null) {
Marc Blank2675dbc2012-04-03 10:17:13 -0700590 mRecentFolderList.touchFolder(mFolder, mAccount);
Mindy Pereiraab486362012-03-21 18:18:53 -0700591 }
Paul Westbrook7ebdfd02012-03-21 15:55:30 -0700592 }
593
Vikram Aggarwal58cad2e2012-08-28 16:18:23 -0700594 /**
595 * Adds a listener interested in change in the recent folders. If a class is storing a
596 * reference to the recent folders, it should listen on changes, so it can receive updates.
597 * Must happen in the UI thread.
598 */
599 @Override
600 public void registerRecentFolderObserver(DataSetObserver obs) {
601 mRecentFolderObservers.registerObserver(obs);
602 }
603
604 /**
605 * Removes a listener from receiving recent folder changes.
606 * Must happen in the UI thread.
607 */
608 @Override
609 public void unregisterRecentFolderObserver(DataSetObserver obs) {
610 mRecentFolderObservers.unregisterObserver(obs);
611 }
612
613 @Override
614 public RecentFolderList getRecentFolders() {
615 return mRecentFolderList;
616 }
617
Mindy Pereiraab486362012-03-21 18:18:53 -0700618 // TODO(mindyp): set this up to store a copy of the folder as a transient
619 // field in the account.
Vikram Aggarwaleb7d3292012-04-20 17:07:20 -0700620 @Override
621 public void loadAccountInbox() {
Vikram Aggarwal94c94de2012-04-04 15:38:28 -0700622 restartOptionalLoader(LOADER_ACCOUNT_INBOX);
Mindy Pereiraf6acdad2012-03-15 11:21:13 -0700623 }
624
Vikram Aggarwal69b5c302012-09-05 11:11:13 -0700625 /**
Vikram Aggarwald00229d2012-09-20 12:31:44 -0700626 * Marks the {@link #mFolderChanged} value if the newFolder is different from the existing
627 * {@link #mFolder}. This should be called immediately <b>before</b> assigning newFolder to
628 * mFolder.
629 * @param newFolder
630 */
631 private final void setHasFolderChanged(final Folder newFolder) {
632 // We should never try to assign a null folder. But in the rare event that we do, we should
633 // only set the bit when we have a valid folder, and null is not valid.
634 if (newFolder == null) {
635 return;
636 }
637 // If the previous folder was null, or if the two folders represent different data, then we
638 // consider that the folder has changed.
639 if (mFolder == null || !newFolder.uri.equals(mFolder.uri)) {
640 mFolderChanged = true;
641 }
642 }
643
644 /**
Vikram Aggarwal69b5c302012-09-05 11:11:13 -0700645 * Sets the current folder if it is different from the object provided here. This method does
646 * NOT notify the folder observers that a change has happened. Observers are notified when we
647 * get an updated folder from the loaders, which will happen as a consequence of this method
648 * (since this method starts/restarts the loaders).
649 * @param folder The folder to assign
650 */
Mindy Pereira11e35962012-06-01 14:49:46 -0700651 private void updateFolder(Folder folder) {
Vikram Aggarwal1672ff82012-09-21 10:15:22 -0700652 if (folder == null || !folder.isInitialized()) {
653 LogUtils.e(LOG_TAG, new Error(), "AAC.setFolder(%s): Bad input", folder);
654 return;
655 }
656 if (folder.equals(mFolder)) {
657 LogUtils.d(LOG_TAG, "AAC.setFolder(%s): Input matches mFolder", folder);
658 return;
659 }
660 final boolean wasNull = mFolder == null;
661 LogUtils.d(LOG_TAG, "AbstractActivityController.setFolder(%s)", folder.name);
662 final LoaderManager lm = mActivity.getLoaderManager();
663 // updateFolder is called from AAC.onLoadFinished() on folder changes. We need to
664 // ensure that the folder is different from the previous folder before marking the
665 // folder changed.
666 setHasFolderChanged(folder);
667 mFolder = folder;
Vikram Aggarwal69b5c302012-09-05 11:11:13 -0700668
Vikram Aggarwal1672ff82012-09-21 10:15:22 -0700669 // We do not need to notify folder observers yet. Instead we start the loaders and
670 // when the load finishes, we will get an updated folder. Then, we notify the
671 // folderObservers in onLoadFinished.
672 mActionBarView.setFolder(mFolder);
Andy Huangb1c34dc2012-04-17 16:36:19 -0700673
Vikram Aggarwal1672ff82012-09-21 10:15:22 -0700674 // Only when we switch from one folder to another do we want to restart the
675 // folder and conversation list loaders (to trigger onCreateLoader).
676 // The first time this runs when the activity is [re-]initialized, we want to re-use the
677 // previous loader's instance and data upon configuration change (e.g. rotation).
678 // If there was not already an instance of the loader, init it.
679 if (lm.getLoader(LOADER_FOLDER_CURSOR) == null) {
680 lm.initLoader(LOADER_FOLDER_CURSOR, null, this);
681 } else {
682 lm.restartLoader(LOADER_FOLDER_CURSOR, null, this);
683 }
684 // In this case, we are starting from no folder, which would occur
685 // the first time the app was launched or on orientation changes.
686 // We want to attach to an existing loader, if available.
687 if (wasNull || lm.getLoader(LOADER_CONVERSATION_LIST) == null) {
688 lm.initLoader(LOADER_CONVERSATION_LIST, null, mListCursorCallbacks);
689 } else {
690 // However, if there was an existing folder AND we have changed
691 // folders, we want to restart the loader to get the information
692 // for the newly selected folder
693 lm.destroyLoader(LOADER_CONVERSATION_LIST);
694 lm.initLoader(LOADER_CONVERSATION_LIST, null, mListCursorCallbacks);
Mindy Pereira5cb0c3e2012-02-22 15:22:47 -0800695 }
696 }
697
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -0800698 @Override
Andy Huang090db1e2012-07-25 13:25:28 -0700699 public Folder getFolder() {
700 return mFolder;
701 }
702
703 @Override
Mindy Pereirac975e842012-07-16 09:15:00 -0700704 public Folder getHierarchyFolder() {
705 return mFolderListFolder;
706 }
707
708 @Override
709 public void setHierarchyFolder(Folder folder) {
710 mFolderListFolder = folder;
Mindy Pereira23aadfd2012-05-25 11:24:33 -0700711 }
712
713 @Override
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -0800714 public void onActivityResult(int requestCode, int resultCode, Intent data) {
Paul Westbrook122f7c22012-08-20 17:50:31 -0700715 switch (requestCode) {
716 case ADD_ACCOUNT_REQUEST_CODE:
717 // We were waiting for the user to create an account
718 if (resultCode == Activity.RESULT_OK) {
719 // restart the loader to get the updated list of accounts
720 mActivity.getLoaderManager().initLoader(
721 LOADER_ACCOUNT_CURSOR, null, this);
722 } else {
723 // The user failed to create an account, just exit the app
724 mActivity.finish();
725 }
726 break;
727 case REAUTHENTICATE_REQUEST_CODE:
728 if (resultCode == Activity.RESULT_OK) {
729 // The user successfully authenticated, attempt to refresh the list
730 final Uri refreshUri = mFolder != null ? mFolder.refreshUri : null;
731 if (refreshUri != null) {
732 startAsyncRefreshTask(refreshUri);
733 }
734 }
735 break;
Paul Westbrook2388c5d2012-03-25 12:29:11 -0700736 }
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -0800737 }
738
Vikram Aggarwal69b5c302012-09-05 11:11:13 -0700739 /**
740 * Inform the conversation cursor that there has been a visibility change.
741 * @param visible
742 */
743 protected synchronized void informCursorVisiblity(boolean visible) {
744 if (mConversationListCursor != null) {
745 Utils.setConversationCursorVisibility(mConversationListCursor, visible, mFolderChanged);
746 // We have informed the cursor. Subsequent visibility changes should not tell it that
747 // the folder has changed.
748 mFolderChanged = false;
749 }
750 }
751
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -0800752 @Override
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -0800753 public void onConversationListVisibilityChanged(boolean visible) {
Vikram Aggarwal69b5c302012-09-05 11:11:13 -0700754 informCursorVisiblity(visible);
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -0800755 }
756
Vikram Aggarwal4a5c5302012-01-12 15:07:13 -0800757 /**
Vikram Aggarwal7dd054e2012-05-21 14:43:10 -0700758 * Called when a conversation is visible. Child classes must call the super class implementation
759 * before performing local computation.
Vikram Aggarwal4a5c5302012-01-12 15:07:13 -0800760 */
761 @Override
762 public void onConversationVisibilityChanged(boolean visible) {
Vikram Aggarwal4a5c5302012-01-12 15:07:13 -0800763 }
764
765 @Override
Vikram Aggarwald7a12cd2012-02-03 09:36:20 -0800766 public boolean onCreate(Bundle savedState) {
Vikram Aggarwalabd24d82012-04-26 13:23:14 -0700767 initializeActionBar();
Vikram Aggarwala55b36c2012-01-13 11:45:02 -0800768 // Allow shortcut keys to function for the ActionBar and menus.
769 mActivity.setDefaultKeyMode(Activity.DEFAULT_KEYS_SHORTCUT);
Vikram Aggarwal80aeac52012-02-07 15:27:20 -0800770 mResolver = mActivity.getContentResolver();
Paul Westbrook6ead20d2012-03-19 14:48:14 -0700771 mNewEmailReceiver = new SuppressNotificationReceiver();
Vikram Aggarwal7c401b72012-08-13 16:43:47 -0700772 mRecentFolderList.initialize(mActivity);
Paul Westbrook6ead20d2012-03-19 14:48:14 -0700773
Mindy Pereira161f50d2012-02-28 15:47:19 -0800774 // All the individual UI components listen for ViewMode changes. This
Mindy Pereirab849dfb2012-03-07 18:13:15 -0800775 // simplifies the amount of logic in the AbstractActivityController, but increases the
776 // possibility of timing-related bugs.
Vikram Aggarwala55b36c2012-01-13 11:45:02 -0800777 mViewMode.addListener(this);
Andy Huang632721e2012-04-11 16:57:26 -0700778 mPagerController = new ConversationPagerController(mActivity, this);
Andrew Sappersteinc2c9dc12012-07-02 18:17:32 -0700779 mToastBar = (ActionableToastBar) mActivity.findViewById(R.id.toast_bar);
Vikram Aggarwalada51782012-04-26 14:18:31 -0700780 attachActionBar();
Mark Wei9eb1c9a2012-10-01 12:54:50 -0700781 FolderSelectionDialog.setDialogDismissed();
Andy Huang632721e2012-04-11 16:57:26 -0700782
783 final Intent intent = mActivity.getIntent();
Vikram Aggarwalabd24d82012-04-26 13:23:14 -0700784 // Immediately handle a clean launch with intent, and any state restoration
Andy Huang632721e2012-04-11 16:57:26 -0700785 // that does not rely on restored fragments or loader data
786 // any state restoration that relies on those can be done later in
787 // onRestoreInstanceState, once fragments are up and loader data is re-delivered
788 if (savedState != null) {
789 if (savedState.containsKey(SAVED_ACCOUNT)) {
790 setAccount((Account) savedState.getParcelable(SAVED_ACCOUNT));
Andy Huang632721e2012-04-11 16:57:26 -0700791 }
792 if (savedState.containsKey(SAVED_FOLDER)) {
Andy Huang2bc8bc12012-11-12 17:24:25 -0800793 final Folder folder = savedState.getParcelable(SAVED_FOLDER);
Vikram Aggarwal203dc002012-08-23 13:59:04 -0700794 final String query = savedState.getString(SAVED_QUERY, null);
Vikram Aggarwal649b9ea2012-08-27 12:15:20 -0700795 setListContext(folder, query);
Andy Huang632721e2012-04-11 16:57:26 -0700796 }
Vikram Aggarwal649b9ea2012-08-27 12:15:20 -0700797 mViewMode.handleRestore(savedState);
Andy Huang632721e2012-04-11 16:57:26 -0700798 } else if (intent != null) {
799 handleIntent(intent);
800 }
Andy Huang632721e2012-04-11 16:57:26 -0700801 // Create the accounts loader; this loads the account switch spinner.
802 mActivity.getLoaderManager().initLoader(LOADER_ACCOUNT_CURSOR, null, this);
Andy Huang632721e2012-04-11 16:57:26 -0700803 return true;
Andy Huangb1c34dc2012-04-17 16:36:19 -0700804 }
805
806 @Override
Andy Huang1ee96b22012-08-24 20:19:53 -0700807 public void onStart() {
808 mSafeToModifyFragments = true;
809 }
810
811 @Override
Andrew Sapperstein00179f12012-08-09 15:15:40 -0700812 public void onRestart() {
813 DialogFragment fragment = (DialogFragment)
814 mFragmentManager.findFragmentByTag(SYNC_ERROR_DIALOG_FRAGMENT_TAG);
815 if (fragment != null) {
816 fragment.dismiss();
817 }
mindypea04f932012-08-27 14:17:59 -0700818 // When the user places the app in the background by pressing "home",
819 // dismiss the toast bar. However, since there is no way to determine if
820 // home was pressed, just dismiss any existing toast bar when restarting
821 // the app.
822 if (mToastBar != null) {
823 mToastBar.hide(false);
824 }
Andrew Sapperstein00179f12012-08-09 15:15:40 -0700825 }
826
827 @Override
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -0800828 public Dialog onCreateDialog(int id, Bundle bundle) {
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -0800829 return null;
Vikram Aggarwala55b36c2012-01-13 11:45:02 -0800830 }
831
832 @Override
Vikram Aggarwal4eb52712012-06-19 16:24:50 -0700833 public final boolean onCreateOptionsMenu(Menu menu) {
Vikram Aggarwale5e917c2012-09-20 16:27:41 -0700834 final MenuInflater inflater = mActivity.getMenuInflater();
Mindy Pereiraf5acda42012-02-15 20:13:59 -0800835 inflater.inflate(mActionBarView.getOptionsMenuId(), menu);
Mindy Pereira68f2e222012-03-07 10:36:54 -0800836 mActionBarView.onCreateOptionsMenu(menu);
Mindy Pereira28d5f722012-02-15 12:32:40 -0800837 return true;
Vikram Aggarwala55b36c2012-01-13 11:45:02 -0800838 }
839
840 @Override
Vikram Aggarwal4eb52712012-06-19 16:24:50 -0700841 public final boolean onKeyDown(int keyCode, KeyEvent event) {
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -0800842 // TODO(viki): Auto-generated method stub
843 return false;
844 }
845
846 @Override
Vikram Aggarwal4eb52712012-06-19 16:24:50 -0700847 public final boolean onOptionsItemSelected(MenuItem item) {
Vikram Aggarwal54452ae2012-03-13 15:29:00 -0700848 final int id = item.getItemId();
Vikram Aggarwal7dd054e2012-05-21 14:43:10 -0700849 LogUtils.d(LOG_TAG, "AbstractController.onOptionsItemSelected(%d) called.", id);
Mindy Pereira9b875682012-02-15 18:10:54 -0800850 boolean handled = true;
Mindy Pereiraba68fda2012-05-24 15:53:06 -0700851 final Collection<Conversation> target = Conversation.listOf(mCurrentConversation);
Vikram Aggarwal112cd162012-06-18 10:51:13 -0700852 final Settings settings = (mAccount == null) ? null : mAccount.settings;
mindyp84f7d322012-10-01 17:14:40 -0700853 // The user is choosing a new action; commit whatever they had been
854 // doing before.
mindypc6adce32012-08-22 18:46:42 -0700855 commitDestructiveActions(true);
Mindy Pereira28d5f722012-02-15 12:32:40 -0800856 switch (id) {
Mindy Pereiraba68fda2012-05-24 15:53:06 -0700857 case R.id.archive: {
858 final boolean showDialog = (settings != null && settings.confirmArchive);
859 confirmAndDelete(target, showDialog, R.plurals.confirm_archive_conversation,
mindypf0656a12012-10-01 08:30:57 -0700860 getDeferredAction(R.id.archive, target, false));
Mindy Pereiraba68fda2012-05-24 15:53:06 -0700861 break;
862 }
Mindy Pereira01f30502012-08-14 10:30:51 -0700863 case R.id.remove_folder:
Vikram Aggarwala8e43182012-09-13 12:55:10 -0700864 delete(R.id.remove_folder, target,
mindyp84f7d322012-10-01 17:14:40 -0700865 getDeferredRemoveFolder(target, mFolder, true, false, true));
Mindy Pereira01f30502012-08-14 10:30:51 -0700866 break;
Mindy Pereiraba68fda2012-05-24 15:53:06 -0700867 case R.id.delete: {
868 final boolean showDialog = (settings != null && settings.confirmDelete);
869 confirmAndDelete(target, showDialog, R.plurals.confirm_delete_conversation,
mindypf0656a12012-10-01 08:30:57 -0700870 getDeferredAction(R.id.delete, target, false));
Mindy Pereiraba68fda2012-05-24 15:53:06 -0700871 break;
872 }
Vikram Aggarwala8e43182012-09-13 12:55:10 -0700873 case R.id.discard_drafts: {
Paul Westbrookef362542012-08-27 14:53:32 -0700874 final boolean showDialog = (settings != null && settings.confirmDelete);
875 confirmAndDelete(target, showDialog, R.plurals.confirm_discard_drafts_conversation,
mindypf0656a12012-10-01 08:30:57 -0700876 getDeferredAction(R.id.discard_drafts, target, false));
Paul Westbrookef362542012-08-27 14:53:32 -0700877 break;
Vikram Aggarwala8e43182012-09-13 12:55:10 -0700878 }
Mindy Pereiraba68fda2012-05-24 15:53:06 -0700879 case R.id.mark_important:
Vikram Aggarwal531488e2012-05-29 16:36:52 -0700880 updateConversation(Conversation.listOf(mCurrentConversation),
881 ConversationColumns.PRIORITY, UIProvider.ConversationPriority.HIGH);
Mindy Pereiraba68fda2012-05-24 15:53:06 -0700882 break;
883 case R.id.mark_not_important:
Mindy Pereira0d03ef82012-08-15 09:05:48 -0700884 if (mFolder != null && mFolder.isImportantOnly()) {
Vikram Aggarwala8e43182012-09-13 12:55:10 -0700885 delete(R.id.mark_not_important, target,
mindyp84f7d322012-10-01 17:14:40 -0700886 getDeferredAction(R.id.mark_not_important, target, false));
Mindy Pereira0d03ef82012-08-15 09:05:48 -0700887 } else {
888 updateConversation(Conversation.listOf(mCurrentConversation),
889 ConversationColumns.PRIORITY, UIProvider.ConversationPriority.LOW);
890 }
Mindy Pereiraba68fda2012-05-24 15:53:06 -0700891 break;
892 case R.id.mute:
mindyp84f7d322012-10-01 17:14:40 -0700893 delete(R.id.mute, target, getDeferredAction(R.id.mute, target, false));
Mindy Pereiraba68fda2012-05-24 15:53:06 -0700894 break;
895 case R.id.report_spam:
mindyp84f7d322012-10-01 17:14:40 -0700896 delete(R.id.report_spam, target,
897 getDeferredAction(R.id.report_spam, target, false));
Mindy Pereiraba68fda2012-05-24 15:53:06 -0700898 break;
Paul Westbrook77eee622012-07-10 13:41:57 -0700899 case R.id.mark_not_spam:
mindyp84f7d322012-10-01 17:14:40 -0700900 // Currently, since spam messages are only shown in list with
901 // other spam messages,
Paul Westbrook77eee622012-07-10 13:41:57 -0700902 // marking a message not as spam is a destructive action
mindyp84f7d322012-10-01 17:14:40 -0700903 delete(R.id.mark_not_spam, target,
904 getDeferredAction(R.id.mark_not_spam, target, false));
Paul Westbrook77eee622012-07-10 13:41:57 -0700905 break;
Paul Westbrook76b20622012-07-12 11:45:43 -0700906 case R.id.report_phishing:
mindyp84f7d322012-10-01 17:14:40 -0700907 delete(R.id.report_phishing, target,
908 getDeferredAction(R.id.report_phishing, target, false));
Paul Westbrook76b20622012-07-12 11:45:43 -0700909 break;
Mindy Pereiraf5acda42012-02-15 20:13:59 -0800910 case android.R.id.home:
911 onUpPressed();
912 break;
Mindy Pereira9b875682012-02-15 18:10:54 -0800913 case R.id.compose:
914 ComposeActivity.compose(mActivity.getActivityContext(), mAccount);
915 break;
Mindy Pereira28d5f722012-02-15 12:32:40 -0800916 case R.id.show_all_folders:
917 showFolderList();
918 break;
Mindy Pereira28e0c342012-02-17 15:05:13 -0800919 case R.id.refresh:
920 requestFolderRefresh();
921 break;
Mindy Pereira1f936682012-03-02 11:30:33 -0800922 case R.id.settings:
923 Utils.showSettings(mActivity.getActivityContext(), mAccount);
Paul Westbrook2861b6a2012-02-15 15:25:34 -0800924 break;
Paul Westbrooke5503552012-03-28 00:35:57 -0700925 case R.id.folder_options:
926 Utils.showFolderSettings(mActivity.getActivityContext(), mAccount, mFolder);
927 break;
Paul Westbrook94e440d2012-02-24 11:03:47 -0800928 case R.id.help_info_menu_item:
Paul Westbrook30745b62012-08-19 14:10:32 -0700929 Utils.showHelp(mActivity.getActivityContext(), mAccount, getHelpContext());
Paul Westbrook94e440d2012-02-24 11:03:47 -0800930 break;
Vikram Aggarwal54452ae2012-03-13 15:29:00 -0700931 case R.id.feedback_menu_item:
Paul Westbrook17beb0b2012-08-20 13:34:37 -0700932 Utils.sendFeedback(mActivity.getActivityContext(), mAccount, false);
Mindy Pereirafbe40192012-03-20 10:40:45 -0700933 break;
Paul Westbrook18babd22012-04-09 22:17:08 -0700934 case R.id.manage_folders_item:
935 Utils.showManageFolder(mActivity.getActivityContext(), mAccount);
936 break;
Vikram Aggarwald503df42012-05-11 10:13:35 -0700937 case R.id.change_folder:
Mark Wei8f98ac02012-10-01 17:05:08 -0700938 final FolderSelectionDialog dialog = FolderSelectionDialog.getInstance(
939 mActivity.getActivityContext(), mAccount, this,
940 Conversation.listOf(mCurrentConversation), false, mFolder);
Mark Wei9eb1c9a2012-10-01 12:54:50 -0700941 if (dialog != null) {
942 dialog.show();
mindypa7e15452012-09-18 14:22:11 -0700943 }
Vikram Aggarwald503df42012-05-11 10:13:35 -0700944 break;
Mindy Pereira9b875682012-02-15 18:10:54 -0800945 default:
946 handled = false;
947 break;
Mindy Pereira28d5f722012-02-15 12:32:40 -0800948 }
Mindy Pereira9b875682012-02-15 18:10:54 -0800949 return handled;
Vikram Aggarwala55b36c2012-01-13 11:45:02 -0800950 }
951
Vikram Aggarwal531488e2012-05-29 16:36:52 -0700952 @Override
Mindy Pereira6c2663d2012-07-20 15:37:29 -0700953 public void updateConversation(Collection<Conversation> target, ContentValues values) {
954 mConversationListCursor.updateValues(mContext, target, values);
955 refreshConversationList();
956 }
957
958 @Override
Vikram Aggarwal531488e2012-05-29 16:36:52 -0700959 public void updateConversation(Collection <Conversation> target, String columnName,
960 boolean value) {
961 mConversationListCursor.updateBoolean(mContext, target, columnName, value);
Vikram Aggarwal75daee52012-04-30 11:13:09 -0700962 refreshConversationList();
Vikram Aggarwal54452ae2012-03-13 15:29:00 -0700963 }
964
Vikram Aggarwal531488e2012-05-29 16:36:52 -0700965 @Override
Mindy Pereira6c2663d2012-07-20 15:37:29 -0700966 public void updateConversation(Collection <Conversation> target, String columnName,
967 int value) {
Vikram Aggarwal531488e2012-05-29 16:36:52 -0700968 mConversationListCursor.updateInt(mContext, target, columnName, value);
Vikram Aggarwal75daee52012-04-30 11:13:09 -0700969 refreshConversationList();
Mindy Pereirac9d59182012-03-22 16:06:46 -0700970 }
971
Vikram Aggarwal531488e2012-05-29 16:36:52 -0700972 @Override
973 public void updateConversation(Collection <Conversation> target, String columnName,
974 String value) {
975 mConversationListCursor.updateString(mContext, target, columnName, value);
Vikram Aggarwal75daee52012-04-30 11:13:09 -0700976 refreshConversationList();
Vikram Aggarwal54452ae2012-03-13 15:29:00 -0700977 }
978
Andy Huang839ada22012-07-20 15:48:40 -0700979 @Override
980 public void markConversationMessagesUnread(Conversation conv, Set<Uri> unreadMessageUris,
Vikram Aggarwal4a878b62012-07-31 15:09:25 -0700981 String originalConversationInfo) {
Andy Huang8f6b0062012-07-31 15:36:31 -0700982 // The only caller of this method is the conversation view, from where marking unread should
983 // *always* take you back to list mode.
984 showConversation(null);
985
Andy Huang839ada22012-07-20 15:48:40 -0700986 // locally mark conversation unread (the provider is supposed to propagate message unread
987 // to conversation unread)
988 conv.read = false;
989
Paul Westbrook1faf93d2012-10-16 08:58:07 -0700990 if (mConversationListCursor == null) {
991 LogUtils.e(LOG_TAG, "null ConversationCursor in markConversationMessagesUnread");
992 return;
993 }
994
Andy Huang28e31e22012-07-26 16:33:15 -0700995 // only do a granular 'mark unread' if a subset of messages are unread
996 final int unreadCount = (unreadMessageUris == null) ? 0 : unreadMessageUris.size();
Mindy Pereira0972e072012-08-01 17:43:06 -0700997 final int numMessages = conv.getNumMessages();
998 final boolean subsetIsUnread = (numMessages > 1 && unreadCount > 0
999 && unreadCount < numMessages);
Andy Huang28e31e22012-07-26 16:33:15 -07001000
1001 if (!subsetIsUnread) {
Vikram Aggarwal66bc2aa2012-08-02 10:47:03 -07001002 // Conversations are neither marked read, nor viewed, and we don't want to show
1003 // the next conversation.
1004 markConversationsRead(Collections.singletonList(conv), false, false, false);
Andy Huang839ada22012-07-20 15:48:40 -07001005 } else {
Andy Huangdaa06ab2012-07-24 10:46:44 -07001006 mConversationListCursor.setConversationColumn(conv.uri, ConversationColumns.READ, 0);
Andy Huang839ada22012-07-20 15:48:40 -07001007
Mindy Pereira7b6d03d2012-07-30 13:03:41 -07001008 // locally update conversation's conversationInfo to revert to original version
Andy Huang28e31e22012-07-26 16:33:15 -07001009 if (originalConversationInfo != null) {
1010 mConversationListCursor.setConversationColumn(conv.uri,
1011 ConversationColumns.CONVERSATION_INFO, originalConversationInfo);
1012 }
Andy Huang839ada22012-07-20 15:48:40 -07001013
1014 // applyBatch with each CPO as an UPDATE op on each affected message uri
1015 final ArrayList<ContentProviderOperation> ops = Lists.newArrayList();
1016 String authority = null;
1017 for (Uri messageUri : unreadMessageUris) {
1018 if (authority == null) {
1019 authority = messageUri.getAuthority();
1020 }
1021 ops.add(ContentProviderOperation.newUpdate(messageUri)
1022 .withValue(UIProvider.MessageColumns.READ, 0)
1023 .build());
1024 }
1025
1026 new ContentProviderTask() {
1027 @Override
1028 protected void onPostExecute(Result result) {
1029 // TODO: handle errors?
1030 }
1031 }.run(mResolver, authority, ops);
Andy Huang839ada22012-07-20 15:48:40 -07001032 }
Andy Huang839ada22012-07-20 15:48:40 -07001033 }
1034
1035 @Override
Vikram Aggarwal66bc2aa2012-08-02 10:47:03 -07001036 public void markConversationsRead(Collection<Conversation> targets, boolean read,
1037 boolean viewed) {
1038 // We want to show the next conversation if we are marking unread.
1039 markConversationsRead(targets, read, viewed, true);
Andy Huang8f6b0062012-07-31 15:36:31 -07001040 }
1041
Scott Kennedy0d0f8b02012-10-12 15:18:18 -07001042 private void markConversationsRead(final Collection<Conversation> targets, final boolean read,
1043 final boolean markViewed, final boolean showNext) {
Vikram Aggarwalcc754b12012-08-30 14:04:21 -07001044 // Auto-advance if requested and the current conversation is being marked unread
Andy Huang8f6b0062012-07-31 15:36:31 -07001045 if (showNext && !read) {
Scott Kennedy0d0f8b02012-10-12 15:18:18 -07001046 final Runnable operation = new Runnable() {
1047 @Override
1048 public void run() {
1049 markConversationsRead(targets, read, markViewed, showNext);
1050 }
1051 };
1052
1053 if (!showNextConversation(targets, operation)) {
1054 // This method will be called again if the user selects an autoadvance option
1055 return;
1056 }
Andy Huang8f6b0062012-07-31 15:36:31 -07001057 }
Scott Kennedy0d0f8b02012-10-12 15:18:18 -07001058
Vikram Aggarwalcc754b12012-08-30 14:04:21 -07001059 final int size = targets.size();
1060 final List<ConversationOperation> opList = new ArrayList<ConversationOperation>(size);
1061 for (final Conversation target : targets) {
1062 final ContentValues value = new ContentValues();
1063 value.put(ConversationColumns.READ, read);
Paul Westbrook5109c512012-11-05 11:00:30 -08001064
1065 // The mark read/unread/viewed operations do not show an undo bar
1066 value.put(ConversationOperations.Parameters.SUPPRESS_UNDO, true);
Vikram Aggarwal66bc2aa2012-08-02 10:47:03 -07001067 if (markViewed) {
Vikram Aggarwalcc754b12012-08-30 14:04:21 -07001068 value.put(ConversationColumns.VIEWED, true);
Vikram Aggarwal66bc2aa2012-08-02 10:47:03 -07001069 }
Vikram Aggarwal192fac12012-07-25 16:44:55 -07001070 final ConversationInfo info = target.conversationInfo;
Andy Huang839ada22012-07-20 15:48:40 -07001071 if (info != null) {
mindyp7f55c682012-10-04 11:38:27 -07001072 boolean changed = info.markRead(read);
1073 if (changed) {
1074 value.put(ConversationColumns.CONVERSATION_INFO,
1075 ConversationInfo.toString(info));
1076 }
Andy Huang839ada22012-07-20 15:48:40 -07001077 }
Vikram Aggarwalcc754b12012-08-30 14:04:21 -07001078 opList.add(mConversationListCursor.getOperationForConversation(
1079 target, ConversationOperation.UPDATE, value));
1080 // Update the local conversation objects so they immediately change state.
1081 target.read = read;
Andy Huangcd5c5ee2012-08-12 19:03:51 -07001082 if (markViewed) {
Vikram Aggarwalcc754b12012-08-30 14:04:21 -07001083 target.markViewed();
Andy Huangcd5c5ee2012-08-12 19:03:51 -07001084 }
Andy Huang839ada22012-07-20 15:48:40 -07001085 }
Vikram Aggarwalcc754b12012-08-30 14:04:21 -07001086 mConversationListCursor.updateBulkValues(mContext, opList);
Andy Huang839ada22012-07-20 15:48:40 -07001087 }
1088
Andy Huang8f6b0062012-07-31 15:36:31 -07001089 /**
1090 * Auto-advance to a different conversation if the currently visible conversation in
1091 * conversation mode is affected (deleted, marked unread, etc.).
1092 *
Scott Kennedy0d0f8b02012-10-12 15:18:18 -07001093 * <p>Does nothing if outside of conversation mode.</p>
Andy Huang8f6b0062012-07-31 15:36:31 -07001094 *
1095 * @param target the set of conversations being deleted/marked unread
1096 */
mindyp9365a822012-09-12 09:09:09 -07001097 @Override
Scott Kennedy0d0f8b02012-10-12 15:18:18 -07001098 public void showNextConversation(final Collection<Conversation> target) {
1099 showNextConversation(target, null);
1100 }
1101
1102 /**
1103 * Auto-advance to a different conversation if the currently visible conversation in
1104 * conversation mode is affected (deleted, marked unread, etc.).
1105 *
1106 * <p>Does nothing if outside of conversation mode.</p>
1107 *
1108 * @param target the set of conversations being deleted/marked unread
1109 * @return <code>false</code> if we aborted because the user has not yet specified a default
1110 * action, <code>true</code> otherwise
1111 */
1112 private boolean showNextConversation(final Collection<Conversation> target,
1113 final Runnable operation) {
Andy Huang8f6b0062012-07-31 15:36:31 -07001114 final boolean currentConversationInView = (mViewMode.getMode() == ViewMode.CONVERSATION)
1115 && Conversation.contains(target, mCurrentConversation);
Scott Kennedy0d0f8b02012-10-12 15:18:18 -07001116
Andy Huang8f6b0062012-07-31 15:36:31 -07001117 if (currentConversationInView) {
Scott Kennedy0d0f8b02012-10-12 15:18:18 -07001118 final int autoAdvanceSetting = mAccount.settings.getAutoAdvanceSetting();
1119
1120 if (autoAdvanceSetting == AutoAdvance.UNSET && Utils.useTabletUI(mContext)) {
1121 displayAutoAdvanceDialogAndPerformAction(operation);
1122 return false;
1123 } else {
1124 // If we don't have one set, but we're here, just take the default
1125 final int autoAdvance = (autoAdvanceSetting == AutoAdvance.UNSET) ? Settings
1126 .getAutoAdvanceSetting(null)
1127 : autoAdvanceSetting;
1128
1129 final Conversation next = mTracker.getNextConversation(autoAdvance, target);
1130 LogUtils.d(LOG_TAG, "showNextConversation: showing %s next.", next);
1131 showConversation(next);
1132 return true;
1133 }
Andy Huang8f6b0062012-07-31 15:36:31 -07001134 }
Scott Kennedy0d0f8b02012-10-12 15:18:18 -07001135
1136 return true;
1137 }
1138
1139 /**
1140 * Displays a the auto-advance dialog, and when the user makes a selection, the preference is
1141 * stored, and the specified operation is run.
1142 */
1143 private void displayAutoAdvanceDialogAndPerformAction(final Runnable operation) {
1144 final String[] autoAdvanceDisplayOptions =
1145 mContext.getResources().getStringArray(R.array.prefEntries_autoAdvance);
1146 final String[] autoAdvanceOptionValues =
1147 mContext.getResources().getStringArray(R.array.prefValues_autoAdvance);
1148
1149 final String defaultValue = mContext.getString(R.string.prefDefault_autoAdvance);
1150 int initialIndex = 0;
1151 for (int i = 0; i < autoAdvanceOptionValues.length; i++) {
1152 if (defaultValue.equals(autoAdvanceOptionValues[i])) {
1153 initialIndex = i;
1154 break;
1155 }
1156 }
1157
1158 final DialogInterface.OnClickListener listClickListener =
1159 new DialogInterface.OnClickListener() {
1160 @Override
1161 public void onClick(DialogInterface dialog, int whichItem) {
1162 final String autoAdvanceValue = autoAdvanceOptionValues[whichItem];
1163 final int autoAdvanceValueInt =
1164 UIProvider.AutoAdvance.getAutoAdvanceInt(autoAdvanceValue);
1165 mAccount.settings.setAutoAdvanceSetting(autoAdvanceValueInt);
1166
1167 // Save the user's setting
1168 final ContentValues values = new ContentValues(1);
1169 values.put(AccountColumns.SettingsColumns.AUTO_ADVANCE, autoAdvanceValue);
1170
1171 final ContentResolver resolver = mContext.getContentResolver();
1172 resolver.update(mAccount.updateSettingsUri, values, null, null);
1173
1174 // Dismiss the dialog, as clicking the items in the list doesn't close the
1175 // dialog.
1176 dialog.dismiss();
1177 if (operation != null) {
1178 operation.run();
1179 }
1180 }
1181 };
1182
1183 new AlertDialog.Builder(mActivity.getActivityContext()).setTitle(
1184 R.string.auto_advance_help_title)
1185 .setSingleChoiceItems(autoAdvanceDisplayOptions, initialIndex, listClickListener)
1186 .setPositiveButton(null, null)
1187 .create()
1188 .show();
Andy Huang8f6b0062012-07-31 15:36:31 -07001189 }
1190
Andy Huang839ada22012-07-20 15:48:40 -07001191 @Override
1192 public void starMessage(ConversationMessage msg, boolean starred) {
1193 if (msg.starred == starred) {
1194 return;
1195 }
1196
1197 msg.starred = starred;
1198
1199 // locally propagate the change to the owning conversation
1200 // (figure the provider will properly propagate the change when it commits it)
1201 //
1202 // when unstarring, only propagate the change if this was the only message starred
1203 final boolean conversationStarred = starred || msg.isConversationStarred();
Andy Huangcd12e822012-11-08 19:50:57 -08001204 final Conversation conv = msg.getConversation();
1205 if (conversationStarred != conv.starred) {
1206 conv.starred = conversationStarred;
1207 mConversationListCursor.setConversationColumn(conv.uri,
Andy Huang839ada22012-07-20 15:48:40 -07001208 ConversationColumns.STARRED, conversationStarred);
1209 }
1210
1211 final ContentValues values = new ContentValues(1);
1212 values.put(UIProvider.MessageColumns.STARRED, starred ? 1 : 0);
1213
1214 new ContentProviderTask.UpdateTask() {
1215 @Override
1216 protected void onPostExecute(Result result) {
1217 // TODO: handle errors?
1218 }
1219 }.run(mResolver, msg.uri, values, null /* selection*/, null /* selectionArgs */);
1220 }
1221
Mindy Pereira28e0c342012-02-17 15:05:13 -08001222 private void requestFolderRefresh() {
1223 if (mFolder != null) {
Mindy Pereirab7b33e02012-02-21 15:32:19 -08001224 if (mAsyncRefreshTask != null) {
1225 mAsyncRefreshTask.cancel(true);
1226 }
Paul Westbrook7e2a2a12012-06-27 13:52:40 -07001227 mAsyncRefreshTask = new AsyncRefreshTask(mContext, mFolder.refreshUri);
Mindy Pereirab7b33e02012-02-21 15:32:19 -08001228 mAsyncRefreshTask.execute();
Mindy Pereira28e0c342012-02-17 15:05:13 -08001229 }
1230 }
1231
Mindy Pereirafbe40192012-03-20 10:40:45 -07001232 /**
1233 * Confirm (based on user's settings) and delete a conversation from the conversation list and
1234 * from the database.
Vikram Aggarwal3d7ca9d2012-05-11 14:40:36 -07001235 * @param target the conversations to act upon
1236 * @param showDialog true if a confirmation dialog is to be shown, false otherwise.
1237 * @param confirmResource the resource ID of the string that is shown in the confirmation dialog
1238 * @param action the action to perform after animating the deletion of the conversations.
Mindy Pereirafbe40192012-03-20 10:40:45 -07001239 */
Vikram Aggarwal7f602f72012-04-30 16:04:06 -07001240 protected void confirmAndDelete(final Collection<Conversation> target, boolean showDialog,
1241 int confirmResource, final DestructiveAction action) {
Mindy Pereirafbe40192012-03-20 10:40:45 -07001242 if (showDialog) {
1243 final AlertDialog.OnClickListener onClick = new AlertDialog.OnClickListener() {
1244 @Override
1245 public void onClick(DialogInterface dialog, int which) {
mindypf0656a12012-10-01 08:30:57 -07001246 if (which == DialogInterface.BUTTON_POSITIVE) {
1247 delete(0, target, action);
1248 }
Mindy Pereirafbe40192012-03-20 10:40:45 -07001249 }
1250 };
Vikram Aggarwal3d7ca9d2012-05-11 14:40:36 -07001251 final CharSequence message = Utils.formatPlural(mContext, confirmResource,
1252 target.size());
Mindy Pereirafbe40192012-03-20 10:40:45 -07001253 new AlertDialog.Builder(mActivity.getActivityContext()).setMessage(message)
1254 .setPositiveButton(R.string.ok, onClick)
1255 .setNegativeButton(R.string.cancel, null)
1256 .create().show();
1257 } else {
Vikram Aggarwala8e43182012-09-13 12:55:10 -07001258 delete(0, target, action);
Mindy Pereirafbe40192012-03-20 10:40:45 -07001259 }
1260 }
1261
Vikram Aggarwal4f4782b2012-05-30 08:39:09 -07001262 @Override
Scott Kennedy0d0f8b02012-10-12 15:18:18 -07001263 public void delete(final int actionId, final Collection<Conversation> target,
mindyp84f7d322012-10-01 17:14:40 -07001264 final Collection<ConversationItemView> targetViews, final DestructiveAction action) {
1265 // Order of events is critical! The Conversation View Fragment must be
1266 // notified of the next conversation with showConversation(next) *before* the
1267 // conversation list
Vikram Aggarwal192fac12012-07-25 16:44:55 -07001268 // fragment has a chance to delete the conversation, animating it away.
1269
mindyp84f7d322012-10-01 17:14:40 -07001270 // Update the conversation fragment if the current conversation is
1271 // deleted.
Scott Kennedy0d0f8b02012-10-12 15:18:18 -07001272 final Runnable operation = new Runnable() {
1273 @Override
1274 public void run() {
1275 delete(actionId, target, targetViews, action);
1276 }
1277 };
1278
1279 if (!showNextConversation(target, operation)) {
1280 // This method will be called again if the user selects an autoadvance option
1281 return;
1282 }
1283
Vikram Aggarwal192fac12012-07-25 16:44:55 -07001284 // The conversation list deletes and performs the action if it exists.
1285 final ConversationListFragment convListFragment = getConversationListFragment();
1286 if (convListFragment != null) {
1287 LogUtils.d(LOG_TAG, "AAC.requestDelete: ListFragment is handling delete.");
mindyp84f7d322012-10-01 17:14:40 -07001288 convListFragment.requestDelete(actionId, target, targetViews, action);
Vikram Aggarwal192fac12012-07-25 16:44:55 -07001289 return;
1290 }
mindyp84f7d322012-10-01 17:14:40 -07001291 // No visible UI element handled it on our behalf. Perform the action
1292 // ourself.
Vikram Aggarwald503df42012-05-11 10:13:35 -07001293 action.performAction();
1294 }
1295
mindyp84f7d322012-10-01 17:14:40 -07001296 @Override
1297 public void delete(int actionId, final Collection<Conversation> target,
1298 final DestructiveAction action) {
1299 delete(actionId, target, null, action);
1300 }
1301
Vikram Aggarwald503df42012-05-11 10:13:35 -07001302 /**
1303 * Requests that the action be performed and the UI state is updated to reflect the new change.
1304 * @param target
1305 * @param action
1306 */
Vikram Aggarwal4f4782b2012-05-30 08:39:09 -07001307 private void requestUpdate(final Collection<Conversation> target,
Vikram Aggarwald503df42012-05-11 10:13:35 -07001308 final DestructiveAction action) {
1309 action.performAction();
1310 refreshConversationList();
Vikram Aggarwal75daee52012-04-30 11:13:09 -07001311 }
Vikram Aggarwal6fbc87a2012-03-15 15:24:00 -07001312
1313 @Override
Vikram Aggarwala55b36c2012-01-13 11:45:02 -08001314 public void onPrepareDialog(int id, Dialog dialog, Bundle bundle) {
1315 // TODO(viki): Auto-generated method stub
Vikram Aggarwala55b36c2012-01-13 11:45:02 -08001316 }
1317
1318 @Override
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -08001319 public boolean onPrepareOptionsMenu(Menu menu) {
Andy Huangd736a382012-08-29 13:08:58 -07001320 return mActionBarView.onPrepareOptionsMenu(menu);
Vikram Aggarwala55b36c2012-01-13 11:45:02 -08001321 }
1322
Mindy Pereira68f2e222012-03-07 10:36:54 -08001323 @Override
1324 public void onPause() {
1325 isLoaderInitialized = false;
Paul Westbrook6ead20d2012-03-19 14:48:14 -07001326 enableNotifications();
Paul Westbrook94e440d2012-02-24 11:03:47 -08001327 }
1328
Vikram Aggarwala55b36c2012-01-13 11:45:02 -08001329 @Override
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -08001330 public void onResume() {
Paul Westbrook6ead20d2012-03-19 14:48:14 -07001331 // Register the receiver that will prevent the status receiver from
1332 // displaying its notification icon as long as we're running.
1333 // The SupressNotificationReceiver will block the broadcast if we're looking at the folder
1334 // that the notification was received for.
1335 disableNotifications();
Andy Huang1ee96b22012-08-24 20:19:53 -07001336
1337 mSafeToModifyFragments = true;
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -08001338 }
1339
1340 @Override
1341 public void onSaveInstanceState(Bundle outState) {
Vikram Aggarwal649b9ea2012-08-27 12:15:20 -07001342 mViewMode.handleSaveInstanceState(outState);
Vikram Aggarwal6c511582012-02-27 10:59:47 -08001343 if (mAccount != null) {
1344 LogUtils.d(LOG_TAG, "Saving the account now");
1345 outState.putParcelable(SAVED_ACCOUNT, mAccount);
1346 }
Mindy Pereira5e478d22012-03-26 18:04:58 -07001347 if (mFolder != null) {
1348 outState.putParcelable(SAVED_FOLDER, mFolder);
Vikram Aggarwal8b152632012-02-03 14:58:45 -08001349 }
Vikram Aggarwalf3341402012-08-07 10:09:38 -07001350 // If this is a search activity, let's store the search query term as well.
1351 if (ConversationListContext.isSearchResult(mConvListContext)) {
1352 outState.putString(SAVED_QUERY, mConvListContext.searchQuery);
1353 }
Vikram Aggarwal49e0e992012-09-21 13:53:15 -07001354 if (mCurrentConversation != null && mViewMode.isConversationMode()) {
Mindy Pereira26f23fc2012-03-27 10:26:04 -07001355 outState.putParcelable(SAVED_CONVERSATION, mCurrentConversation);
1356 }
Andy Huang4556a442012-03-30 16:42:05 -07001357 if (!mSelectedSet.isEmpty()) {
Vikram Aggarwalcabd3f22012-04-19 10:14:41 -07001358 outState.putParcelable(SAVED_SELECTED_SET, mSelectedSet);
Andy Huang4556a442012-03-30 16:42:05 -07001359 }
Mindy Pereirad33674992012-06-25 16:26:30 -07001360 if (mToastBar.getVisibility() == View.VISIBLE) {
1361 outState.putParcelable(SAVED_TOAST_BAR_OP, mToastBar.getOperation());
1362 }
Vikram Aggarwal649b9ea2012-08-27 12:15:20 -07001363 final ConversationListFragment convListFragment = getConversationListFragment();
Mindy Pereirad33674992012-06-25 16:26:30 -07001364 if (convListFragment != null) {
Vikram Aggarwal649b9ea2012-08-27 12:15:20 -07001365 convListFragment.getAnimatedAdapter().onSaveInstanceState(outState);
Mindy Pereirad33674992012-06-25 16:26:30 -07001366 }
Andy Huang1ee96b22012-08-24 20:19:53 -07001367 mSafeToModifyFragments = false;
Vikram Aggarwal649b9ea2012-08-27 12:15:20 -07001368 outState.putString(SAVED_HIERARCHICAL_FOLDER,
1369 (mFolderListFolder != null) ? Folder.toString(mFolderListFolder) : null);
Andy Huang1ee96b22012-08-24 20:19:53 -07001370 }
1371
1372 /**
1373 * @see #mSafeToModifyFragments
1374 */
1375 protected boolean safeToModifyFragments() {
1376 return mSafeToModifyFragments;
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -08001377 }
1378
1379 @Override
Mindy Pereira68f2e222012-03-07 10:36:54 -08001380 public void onSearchRequested(String query) {
1381 Intent intent = new Intent();
1382 intent.setAction(Intent.ACTION_SEARCH);
1383 intent.putExtra(ConversationListContext.EXTRA_SEARCH_QUERY, query);
1384 intent.putExtra(Utils.EXTRA_ACCOUNT, mAccount);
1385 intent.setComponent(mActivity.getComponentName());
Vikram Aggarwalb17cbc02012-04-06 15:41:46 -07001386 mActionBarView.collapseSearch();
Mindy Pereira68f2e222012-03-07 10:36:54 -08001387 mActivity.startActivity(intent);
Vikram Aggarwala55b36c2012-01-13 11:45:02 -08001388 }
1389
1390 @Override
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -08001391 public void onStop() {
Mindy Pereira49e5dbe2012-07-12 11:47:54 -07001392 if (mEnableShareIntents != null) {
1393 mEnableShareIntents.cancel(true);
1394 }
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -08001395 }
1396
Andy Huang632721e2012-04-11 16:57:26 -07001397 @Override
1398 public void onDestroy() {
1399 // unregister the ViewPager's observer on the conversation cursor
1400 mPagerController.onDestroy();
Mindy Pereira641de652012-08-02 15:21:50 -07001401 mActionBarView.onDestroy();
Vikram Aggarwal7c401b72012-08-13 16:43:47 -07001402 mRecentFolderList.destroy();
Andy Huang4e0158f2012-08-07 21:06:01 -07001403 mDestroyed = true;
Andy Huang632721e2012-04-11 16:57:26 -07001404 }
1405
Vikram Aggarwalfa131a22012-02-02 13:56:22 -08001406 /**
Vikram Aggarwal93dc2022012-11-05 13:36:57 -08001407 * Set the Action Bar icon according to the mode. The Action Bar icon can contain a back button
1408 * or not. The individual controller is responsible for changing the icon based on the mode.
1409 */
1410 protected abstract void resetActionBarIcon();
1411
1412 /**
Mindy Pereira161f50d2012-02-28 15:47:19 -08001413 * {@inheritDoc} Subclasses must override this to listen to mode changes
1414 * from the ViewMode. Subclasses <b>must</b> call the parent's
1415 * onViewModeChanged since the parent will handle common state changes.
Vikram Aggarwalfa131a22012-02-02 13:56:22 -08001416 */
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -08001417 @Override
Vikram Aggarwalfa131a22012-02-02 13:56:22 -08001418 public void onViewModeChanged(int newMode) {
Vikram Aggarwal49e0e992012-09-21 13:53:15 -07001419 // When we step away from the conversation mode, we don't have a current conversation
1420 // anymore. Let's blank it out so clients calling getCurrentConversation are not misled.
1421 if (!ViewMode.isConversationMode(newMode)) {
1422 setCurrentConversation(null);
1423 }
Vikram Aggarwal93dc2022012-11-05 13:36:57 -08001424 // If the viewmode is not set, preserve existing icon.
1425 if (newMode != ViewMode.UNKNOWN) {
1426 resetActionBarIcon();
1427 }
Vikram Aggarwala55b36c2012-01-13 11:45:02 -08001428 }
1429
Andy Huang3825f3d2012-08-29 16:44:12 -07001430 public void disablePagerUpdates() {
1431 mPagerController.stopListening();
1432 }
1433
Andy Huang4e0158f2012-08-07 21:06:01 -07001434 public boolean isDestroyed() {
1435 return mDestroyed;
1436 }
1437
mindyp54f120f2012-08-28 13:10:33 -07001438 @Override
1439 public void commitDestructiveActions(boolean animate) {
mindypc6adce32012-08-22 18:46:42 -07001440 ConversationListFragment fragment = getConversationListFragment();
Mindy Pereira1e2573b2012-04-17 14:34:36 -07001441 if (fragment != null) {
mindypc6adce32012-08-22 18:46:42 -07001442 fragment.commitDestructiveActions(animate);
Mindy Pereira1e2573b2012-04-17 14:34:36 -07001443 }
1444 }
1445
Vikram Aggarwala55b36c2012-01-13 11:45:02 -08001446 @Override
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -08001447 public void onWindowFocusChanged(boolean hasFocus) {
Vikram Aggarwal69b5c302012-09-05 11:11:13 -07001448 final ConversationListFragment convList = getConversationListFragment();
Paul Westbrook9f119c72012-04-24 16:10:59 -07001449 if (hasFocus && convList != null && convList.isVisible()) {
1450 // The conversation list is visible.
Vikram Aggarwal69b5c302012-09-05 11:11:13 -07001451 informCursorVisiblity(true);
Paul Westbrook9f119c72012-04-24 16:10:59 -07001452 }
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -08001453 }
1454
Vikram Aggarwalca716f12012-08-20 11:11:48 -07001455 /**
1456 * Set the account, and carry out all the account-related changes that rely on this.
1457 * @param account
1458 */
Mindy Pereira75181e82012-04-18 08:17:13 -07001459 private void setAccount(Account account) {
Andy Huangb9ca9792012-05-18 15:31:49 -07001460 if (account == null) {
1461 LogUtils.w(LOG_TAG, new Error(),
1462 "AAC ignoring null (presumably invalid) account restoration");
1463 return;
1464 }
Andy Huangb1148412012-05-19 00:16:30 -07001465 LogUtils.d(LOG_TAG, "AbstractActivityController.setAccount(): account = %s", account.uri);
Vikram Aggarwal91e87372012-05-18 15:36:04 -07001466 mAccount = account;
Vikram Aggarwalca716f12012-08-20 11:11:48 -07001467 // Only change AAC state here. Do *not* modify any other object's state. The object
1468 // should listen on account changes.
1469 restartOptionalLoader(LOADER_RECENT_FOLDERS);
1470 mActivity.invalidateOptionsMenu();
1471 disableNotificationsOnAccountChange(mAccount);
1472 restartOptionalLoader(LOADER_ACCOUNT_UPDATE_CURSOR);
1473 MailAppProvider.getInstance().setLastViewedAccount(mAccount.uri.toString());
1474
Vikram Aggarwal91e87372012-05-18 15:36:04 -07001475 if (account.settings == null) {
1476 LogUtils.w(LOG_TAG, new Error(), "AAC ignoring account with null settings.");
1477 return;
1478 }
Vikram Aggarwal7c401b72012-08-13 16:43:47 -07001479 mAccountObservers.notifyChanged();
Vikram Aggarwal34f7b232012-10-17 13:32:23 -07001480 perhapsEnterWaitMode();
Mindy Pereira75181e82012-04-18 08:17:13 -07001481 }
1482
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -08001483 /**
Mindy Pereira161f50d2012-02-28 15:47:19 -08001484 * Restore the state from the previous bundle. Subclasses should call this
1485 * method from the parent class, since it performs important UI
1486 * initialization.
1487 *
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -08001488 * @param savedState
1489 */
Andy Huang632721e2012-04-11 16:57:26 -07001490 @Override
1491 public void onRestoreInstanceState(Bundle savedState) {
1492 LogUtils.d(LOG_TAG, "IN AAC.onRestoreInstanceState");
1493 if (savedState.containsKey(SAVED_CONVERSATION)) {
1494 // Open the conversation.
Andy Huang2bc8bc12012-11-12 17:24:25 -08001495 final Conversation conversation = savedState.getParcelable(SAVED_CONVERSATION);
Paul Westbrook534e4a22012-04-25 03:46:29 -07001496 if (conversation != null && conversation.position < 0) {
1497 // Set the position to 0 on this conversation, as we don't know where it is
1498 // in the list
1499 conversation.position = 0;
1500 }
Andy Huanged4fdf02012-07-26 17:12:50 -07001501 showConversation(conversation);
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -08001502 }
Mindy Pereira967ede62012-03-22 09:29:09 -07001503
Mindy Pereirad33674992012-06-25 16:26:30 -07001504 if (savedState.containsKey(SAVED_TOAST_BAR_OP)) {
Andy Huang2bc8bc12012-11-12 17:24:25 -08001505 ToastBarOperation op = savedState.getParcelable(SAVED_TOAST_BAR_OP);
Mindy Pereirad33674992012-06-25 16:26:30 -07001506 if (op != null) {
1507 if (op.getType() == ToastBarOperation.UNDO) {
1508 onUndoAvailable(op);
Andrew Sapperstein9d7519d2012-07-16 14:03:53 -07001509 } else if (op.getType() == ToastBarOperation.ERROR) {
1510 onError(mFolder, true);
Mindy Pereirad33674992012-06-25 16:26:30 -07001511 }
1512 }
1513 }
Vikram Aggarwal649b9ea2012-08-27 12:15:20 -07001514 final String folderString = savedState.getString(SAVED_HIERARCHICAL_FOLDER, null);
1515 if (!TextUtils.isEmpty(folderString)) {
1516 mFolderListFolder = Folder.fromString(folderString);
1517 }
1518 final ConversationListFragment convListFragment = getConversationListFragment();
Mindy Pereirad33674992012-06-25 16:26:30 -07001519 if (convListFragment != null) {
Vikram Aggarwal649b9ea2012-08-27 12:15:20 -07001520 convListFragment.getAnimatedAdapter().onRestoreInstanceState(savedState);
Mindy Pereirad33674992012-06-25 16:26:30 -07001521 }
Mindy Pereira967ede62012-03-22 09:29:09 -07001522 /**
1523 * Restore the state of selected conversations. This needs to be done after the correct mode
1524 * is set and the action bar is fully initialized. If not, several key pieces of state
1525 * information will be missing, and the split views may not be initialized correctly.
1526 * @param savedState
1527 */
Andy Huang4556a442012-03-30 16:42:05 -07001528 restoreSelectedConversations(savedState);
Andy Huang632721e2012-04-11 16:57:26 -07001529 }
1530
Vikram Aggarwal649b9ea2012-08-27 12:15:20 -07001531 /**
1532 * Handle an intent to open the app. This method is called only when there is no saved state,
1533 * so we need to set state that wasn't set before. It is correct to change the viewmode here
1534 * since it has not been previously set.
1535 * @param intent
1536 */
Andy Huang632721e2012-04-11 16:57:26 -07001537 private void handleIntent(Intent intent) {
1538 boolean handled = false;
1539 if (Intent.ACTION_VIEW.equals(intent.getAction())) {
1540 if (intent.hasExtra(Utils.EXTRA_ACCOUNT)) {
Vikram Aggarwal649b9ea2012-08-27 12:15:20 -07001541 setAccount(Account.newinstance(intent.getStringExtra(Utils.EXTRA_ACCOUNT)));
Andy Huang632721e2012-04-11 16:57:26 -07001542 }
Andy Huangb9ca9792012-05-18 15:31:49 -07001543 if (mAccount == null) {
1544 return;
Andy Huang632721e2012-04-11 16:57:26 -07001545 }
Vikram Aggarwal1a249e02012-08-03 16:19:33 -07001546 final boolean isConversationMode = intent.hasExtra(Utils.EXTRA_CONVERSATION);
Vikram Aggarwal649b9ea2012-08-27 12:15:20 -07001547 if (isConversationMode && mViewMode.getMode() == ViewMode.UNKNOWN) {
Vikram Aggarwal1a249e02012-08-03 16:19:33 -07001548 mViewMode.enterConversationMode();
1549 } else {
1550 mViewMode.enterConversationListMode();
1551 }
Vikram Aggarwal1672ff82012-09-21 10:15:22 -07001552 final Folder folder = intent.hasExtra(Utils.EXTRA_FOLDER) ?
1553 Folder.fromString(intent.getStringExtra(Utils.EXTRA_FOLDER)) : null;
Andy Huang632721e2012-04-11 16:57:26 -07001554 if (folder != null) {
1555 onFolderChanged(folder);
1556 handled = true;
1557 }
1558
Vikram Aggarwal1a249e02012-08-03 16:19:33 -07001559 if (isConversationMode) {
Andy Huang632721e2012-04-11 16:57:26 -07001560 // Open the conversation.
1561 LogUtils.d(LOG_TAG, "SHOW THE CONVERSATION at %s",
1562 intent.getParcelableExtra(Utils.EXTRA_CONVERSATION));
Paul Westbrooka9161912012-04-24 10:10:04 -07001563 final Conversation conversation =
Andy Huang2bc8bc12012-11-12 17:24:25 -08001564 intent.getParcelableExtra(Utils.EXTRA_CONVERSATION);
Paul Westbrooka9161912012-04-24 10:10:04 -07001565 if (conversation != null && conversation.position < 0) {
1566 // Set the position to 0 on this conversation, as we don't know where it is
1567 // in the list
1568 conversation.position = 0;
1569 }
Andy Huang980aaea2012-07-26 17:22:19 -07001570 showConversation(conversation);
Andy Huang632721e2012-04-11 16:57:26 -07001571 handled = true;
1572 }
1573
1574 if (!handled) {
Vikram Aggarwal0c3c2052012-09-21 11:06:28 -07001575 // We have an account, but nothing else: load the default inbox.
Andy Huang632721e2012-04-11 16:57:26 -07001576 loadAccountInbox();
1577 }
1578 } else if (Intent.ACTION_SEARCH.equals(intent.getAction())) {
1579 if (intent.hasExtra(Utils.EXTRA_ACCOUNT)) {
Vikram Aggarwalf0ef2302012-11-07 14:53:34 -08001580 mHaveSearchResults = false;
Andy Huang632721e2012-04-11 16:57:26 -07001581 // Save this search query for future suggestions.
1582 final String query = intent.getStringExtra(SearchManager.QUERY);
1583 final String authority = mContext.getString(R.string.suggestions_authority);
Vikram Aggarwal2b703c62012-09-18 13:54:15 -07001584 final SearchRecentSuggestions suggestions = new SearchRecentSuggestions(
Andy Huang632721e2012-04-11 16:57:26 -07001585 mContext, authority, SuggestionsProvider.MODE);
1586 suggestions.saveRecentQuery(query, null);
Vikram Aggarwalf0ef2302012-11-07 14:53:34 -08001587 setAccount((Account) intent.getParcelableExtra(Utils.EXTRA_ACCOUNT));
1588 fetchSearchFolder(intent);
1589 if (shouldEnterSearchConvMode()) {
Mindy Pereiraac254822012-06-18 10:46:43 -07001590 mViewMode.enterSearchResultsConversationMode();
1591 } else {
1592 mViewMode.enterSearchResultsListMode();
1593 }
Andy Huang632721e2012-04-11 16:57:26 -07001594 } else {
1595 LogUtils.e(LOG_TAG, "Missing account extra from search intent. Finishing");
1596 mActivity.finish();
1597 }
1598 }
1599 if (mAccount != null) {
1600 restartOptionalLoader(LOADER_ACCOUNT_UPDATE_CURSOR);
1601 }
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -08001602 }
1603
Andy Huang4556a442012-03-30 16:42:05 -07001604 /**
Vikram Aggarwalf0ef2302012-11-07 14:53:34 -08001605 * Returns true if we should enter conversation mode with search.
1606 */
1607 protected final boolean shouldEnterSearchConvMode() {
1608 return mHaveSearchResults && Utils.showTwoPaneSearchResults(mActivity.getActivityContext());
1609 }
1610
1611 /**
Andy Huang4556a442012-03-30 16:42:05 -07001612 * Copy any selected conversations stored in the saved bundle into our selection set,
1613 * triggering {@link ConversationSetObserver} callbacks as our selection set changes.
1614 *
1615 */
Vikram Aggarwal4eb52712012-06-19 16:24:50 -07001616 private final void restoreSelectedConversations(Bundle savedState) {
Mindy Pereira967ede62012-03-22 09:29:09 -07001617 if (savedState == null) {
Andy Huang4556a442012-03-30 16:42:05 -07001618 mSelectedSet.clear();
Mindy Pereira967ede62012-03-22 09:29:09 -07001619 return;
1620 }
Vikram Aggarwalcabd3f22012-04-19 10:14:41 -07001621 final ConversationSelectionSet selectedSet = savedState.getParcelable(SAVED_SELECTED_SET);
Andy Huang4556a442012-03-30 16:42:05 -07001622 if (selectedSet == null || selectedSet.isEmpty()) {
1623 mSelectedSet.clear();
Mindy Pereira967ede62012-03-22 09:29:09 -07001624 return;
1625 }
Andy Huang632721e2012-04-11 16:57:26 -07001626
1627 // putAll will take care of calling our registered onSetPopulated method
Vikram Aggarwalcabd3f22012-04-19 10:14:41 -07001628 mSelectedSet.putAll(selectedSet);
Mindy Pereira967ede62012-03-22 09:29:09 -07001629 }
1630
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -08001631 @Override
Andy Huang5895f7b2012-06-01 17:07:20 -07001632 public SubjectDisplayChanger getSubjectDisplayChanger() {
1633 return mActionBarView;
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -08001634 }
1635
Vikram Aggarwal17f373e2012-09-17 15:49:32 -07001636 private final void showConversation(Conversation conversation) {
Andy Huang1ee96b22012-08-24 20:19:53 -07001637 showConversation(conversation, false /* inLoaderCallbacks */);
1638 }
1639
Vikram Aggarwalec5cbf72012-03-08 15:10:35 -08001640 /**
Vikram Aggarwal49e0e992012-09-21 13:53:15 -07001641 * Show the conversation provided in the arguments. It is safe to pass a null conversation
1642 * object, which is a signal to back out of conversation view mode.
1643 * Child classes must call super.showConversation() <b>before</b> their own implementations.
1644 * @param conversation
1645 * @param inLoaderCallbacks true if the method is called as a result of
1646 * {@link #onLoadFinished(Loader, Cursor)}
Vikram Aggarwalec5cbf72012-03-08 15:10:35 -08001647 */
Andy Huang1ee96b22012-08-24 20:19:53 -07001648 protected void showConversation(Conversation conversation, boolean inLoaderCallbacks) {
Vikram Aggarwalc67182d2012-04-03 14:35:06 -07001649 // Set the current conversation just in case it wasn't already set.
1650 setCurrentConversation(conversation);
Vikram Aggarwal0f142732012-08-24 09:39:34 -07001651 // Add the folder that we were viewing to the recent folders list.
1652 // TODO: this may need to be fine tuned. If this is the signal that is indicating that
1653 // the list is shown to the user, this could fire in one pane if the user goes directly
1654 // to a conversation
1655 updateRecentFolderList();
Vikram Aggarwalec5cbf72012-03-08 15:10:35 -08001656 }
1657
Vikram Aggarwale128fc22012-04-04 12:33:34 -07001658 /**
Paul Westbrook2d50bcd2012-04-10 11:53:47 -07001659 * Children can override this method, but they must call super.showWaitForInitialization().
1660 * {@inheritDoc}
1661 */
1662 @Override
1663 public void showWaitForInitialization() {
1664 mViewMode.enterWaitingForInitializationMode();
Vikram Aggarwala3f43d42012-10-25 16:21:30 -07001665 mWaitFragment = WaitFragment.newInstance(mAccount);
Paul Westbrook2d50bcd2012-04-10 11:53:47 -07001666 }
1667
Vikram Aggarwaldd6a7ce2012-10-22 15:45:57 -07001668 private void updateWaitMode() {
Paul Westbrook2d50bcd2012-04-10 11:53:47 -07001669 final FragmentManager manager = mActivity.getFragmentManager();
1670 final WaitFragment waitFragment =
Vikram Aggarwal6902dcf2012-04-11 08:57:42 -07001671 (WaitFragment)manager.findFragmentByTag(TAG_WAIT);
Paul Westbrook2d50bcd2012-04-10 11:53:47 -07001672 if (waitFragment != null) {
1673 waitFragment.updateAccount(mAccount);
1674 }
1675 }
1676
Vikram Aggarwala3f43d42012-10-25 16:21:30 -07001677 /**
1678 * Remove the "Waiting for Initialization" fragment. Child classes are free to override this
1679 * method, though they must call the parent implementation <b>after</b> they do anything.
1680 */
1681 protected void hideWaitForInitialization() {
1682 mWaitFragment = null;
1683 }
1684
1685 /**
1686 * Use the instance variable and the wait fragment's tag to get the wait fragment. This is
1687 * far superior to using the value of mWaitFragment, which might be invalid or might refer
1688 * to a fragment after it has been destroyed.
1689 * @return
1690 */
1691 protected final WaitFragment getWaitFragment() {
1692 final FragmentManager manager = mActivity.getFragmentManager();
1693 final WaitFragment waitFrag = (WaitFragment) manager.findFragmentByTag(TAG_WAIT);
1694 if (waitFrag != null) {
1695 // The Fragment Manager knows better, so use its instance.
1696 mWaitFragment = waitFrag;
1697 }
1698 return mWaitFragment;
1699 }
Vikram Aggarwaldd6a7ce2012-10-22 15:45:57 -07001700
Vikram Aggarwal48b2a6c2012-05-29 14:09:27 -07001701 /**
1702 * Returns true if we are waiting for the account to sync, and cannot show any folders or
1703 * conversation for the current account yet.
Vikram Aggarwal48b2a6c2012-05-29 14:09:27 -07001704 */
Vikram Aggarwaldd6a7ce2012-10-22 15:45:57 -07001705 private boolean inWaitMode() {
Paul Westbrook2d50bcd2012-04-10 11:53:47 -07001706 final FragmentManager manager = mActivity.getFragmentManager();
Vikram Aggarwala3f43d42012-10-25 16:21:30 -07001707 final WaitFragment waitFragment = getWaitFragment();
Paul Westbrook2d50bcd2012-04-10 11:53:47 -07001708 if (waitFragment != null) {
1709 final Account fragmentAccount = waitFragment.getAccount();
Paul Westbrook339004b2012-11-05 17:13:51 -08001710 return fragmentAccount != null && fragmentAccount.uri.equals(mAccount.uri) &&
Paul Westbrook2d50bcd2012-04-10 11:53:47 -07001711 mViewMode.getMode() == ViewMode.WAITING_FOR_ACCOUNT_INITIALIZATION;
1712 }
1713 return false;
1714 }
1715
1716 /**
Vikram Aggarwale128fc22012-04-04 12:33:34 -07001717 * Children can override this method, but they must call super.showConversationList().
1718 * {@inheritDoc}
1719 */
1720 @Override
1721 public void showConversationList(ConversationListContext listContext) {
1722 }
1723
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -08001724 @Override
Vikram Aggarwal17f373e2012-09-17 15:49:32 -07001725 public final void onConversationSelected(Conversation conversation, boolean inLoaderCallbacks) {
mindypaa55bc92012-08-24 09:49:56 -07001726 // Only animate destructive actions if we are going to be showing the
1727 // conversation list when we show the next conversation.
1728 commitDestructiveActions(Utils.useTabletUI(mContext));
Andy Huang1ee96b22012-08-24 20:19:53 -07001729 showConversation(conversation, inLoaderCallbacks);
1730 }
1731
1732 @Override
1733 public Conversation getCurrentConversation() {
1734 return mCurrentConversation;
Vikram Aggarwal7d602882012-02-07 15:01:20 -08001735 }
Mindy Pereira555140c2012-02-15 14:55:29 -08001736
Vikram Aggarwalc67182d2012-04-03 14:35:06 -07001737 /**
1738 * Set the current conversation. This is the conversation on which all actions are performed.
1739 * Do not modify mCurrentConversation except through this method, which makes it easy to
1740 * perform common actions associated with changing the current conversation.
1741 * @param conversation
1742 */
Andy Huang632721e2012-04-11 16:57:26 -07001743 @Override
1744 public void setCurrentConversation(Conversation conversation) {
Vikram Aggarwal17f373e2012-09-17 15:49:32 -07001745 // Must be the first call because this sets conversation.position if a cursor is available.
Andy Huang8883f222012-11-12 19:25:00 -08001746 mTracker.initialize(conversation);
Vikram Aggarwal17f373e2012-09-17 15:49:32 -07001747 mCurrentConversation = conversation;
Andy Huang7d646122012-09-05 19:41:44 -07001748
1749 if (mCurrentConversation != null) {
Yorke Leef807ba72012-09-20 17:18:05 -07001750 mActionBarView.setCurrentConversation(mCurrentConversation);
Andy Huang7d646122012-09-05 19:41:44 -07001751 getSubjectDisplayChanger().setSubject(mCurrentConversation.subject);
Yorke Leef807ba72012-09-20 17:18:05 -07001752 mActivity.invalidateOptionsMenu();
Andy Huang7d646122012-09-05 19:41:44 -07001753 }
Mindy Pereira5040f1a2012-03-20 10:14:06 -07001754 }
1755
Vikram Aggarwala9b93f32012-02-23 14:51:58 -08001756 /**
1757 * {@inheritDoc}
1758 */
Mindy Pereira555140c2012-02-15 14:55:29 -08001759 @Override
Vikram Aggarwal7dedb952012-02-16 16:10:23 -08001760 public Loader<Cursor> onCreateLoader(int id, Bundle args) {
Vikram Aggarwalfa4b47e2012-03-09 13:02:46 -08001761 switch (id) {
1762 case LOADER_ACCOUNT_CURSOR:
Paul Westbrookc2074c42012-03-22 15:26:58 -07001763 return new CursorLoader(mContext, MailAppProvider.getAccountsUri(),
Vikram Aggarwalfa4b47e2012-03-09 13:02:46 -08001764 UIProvider.ACCOUNTS_PROJECTION, null, null, null);
1765 case LOADER_FOLDER_CURSOR:
Paul Westbrookc7a070f2012-04-12 01:46:41 -07001766 final CursorLoader loader = new CursorLoader(mContext, mFolder.uri,
1767 UIProvider.FOLDERS_PROJECTION, null, null, null);
1768 loader.setUpdateThrottle(mFolderItemUpdateDelayMs);
1769 return loader;
Vikram Aggarwalfa4b47e2012-03-09 13:02:46 -08001770 case LOADER_RECENT_FOLDERS:
Paul Westbrook91d10502012-04-13 12:01:39 -07001771 if (mAccount != null && mAccount.recentFolderListUri != null) {
Paul Westbrookea4ee202012-03-12 14:12:33 -07001772 return new CursorLoader(mContext, mAccount.recentFolderListUri,
1773 UIProvider.FOLDERS_PROJECTION, null, null, null);
1774 }
1775 break;
Mindy Pereiraab486362012-03-21 18:18:53 -07001776 case LOADER_ACCOUNT_INBOX:
Vikram Aggarwal025eba82012-05-08 10:45:30 -07001777 final Uri defaultInbox = Settings.getDefaultInboxUri(mAccount.settings);
Vikram Aggarwal1e57e672012-05-07 14:48:24 -07001778 final Uri inboxUri = defaultInbox.equals(Uri.EMPTY) ?
1779 mAccount.folderListUri : defaultInbox;
Paul Westbrook7496e822012-04-24 09:50:54 -07001780 LogUtils.d(LOG_TAG, "Loading the default inbox: %s", inboxUri);
Paul Westbrook1220b6d2012-04-10 00:48:00 -07001781 if (inboxUri != null) {
1782 return new CursorLoader(mContext, inboxUri, UIProvider.FOLDERS_PROJECTION, null,
1783 null, null);
1784 }
1785 break;
Mindy Pereiraab486362012-03-21 18:18:53 -07001786 case LOADER_SEARCH:
1787 return Folder.forSearchResults(mAccount,
1788 args.getString(ConversationListContext.EXTRA_SEARCH_QUERY),
1789 mActivity.getActivityContext());
Paul Westbrook2d50bcd2012-04-10 11:53:47 -07001790 case LOADER_ACCOUNT_UPDATE_CURSOR:
1791 return new CursorLoader(mContext, mAccount.uri, UIProvider.ACCOUNTS_PROJECTION,
1792 null, null, null);
Vikram Aggarwalfa4b47e2012-03-09 13:02:46 -08001793 default:
Paul Westbrookad6a2752012-04-04 16:58:13 -07001794 LogUtils.wtf(LOG_TAG, "Loader returned unexpected id: %d", id);
Mindy Pereira5cb0c3e2012-02-22 15:22:47 -08001795 }
1796 return null;
Vikram Aggarwal7dedb952012-02-16 16:10:23 -08001797 }
1798
Paul Westbrookb1f573c2012-04-06 11:38:28 -07001799 @Override
1800 public void onLoaderReset(Loader<Cursor> loader) {
1801
1802 }
1803
Andy Huangf9a73482012-03-13 15:54:02 -07001804 /**
1805 * {@link LoaderManager} currently has a bug in
1806 * {@link LoaderManager#restartLoader(int, Bundle, android.app.LoaderManager.LoaderCallbacks)}
1807 * where, if a previous onCreateLoader returned a null loader, this method will NPE. Work around
1808 * this bug by destroying any loaders that may have been created as null (essentially because
1809 * they are optional loads, and may not apply to a particular account).
1810 * <p>
1811 * A simple null check before restarting a loader will not work, because that would not
1812 * give the controller a chance to invalidate UI corresponding the prior loader result.
1813 *
1814 * @param id loader ID to safely restart
Andy Huangf9a73482012-03-13 15:54:02 -07001815 */
Vikram Aggarwal94c94de2012-04-04 15:38:28 -07001816 private void restartOptionalLoader(int id) {
Andy Huangf9a73482012-03-13 15:54:02 -07001817 final LoaderManager lm = mActivity.getLoaderManager();
1818 lm.destroyLoader(id);
Vikram Aggarwal94c94de2012-04-04 15:38:28 -07001819 lm.restartLoader(id, Bundle.EMPTY, this);
1820 }
1821
Andy Huang632721e2012-04-11 16:57:26 -07001822 @Override
1823 public void registerConversationListObserver(DataSetObserver observer) {
1824 mConversationListObservable.registerObserver(observer);
1825 }
1826
1827 @Override
1828 public void unregisterConversationListObserver(DataSetObserver observer) {
1829 mConversationListObservable.unregisterObserver(observer);
1830 }
1831
Andy Huang090db1e2012-07-25 13:25:28 -07001832 @Override
1833 public void registerFolderObserver(DataSetObserver observer) {
1834 mFolderObservable.registerObserver(observer);
1835 }
1836
1837 @Override
1838 public void unregisterFolderObserver(DataSetObserver observer) {
1839 mFolderObservable.unregisterObserver(observer);
1840 }
1841
Andy Huang9d3fd922012-09-26 22:23:58 -07001842 @Override
1843 public void registerConversationLoadedObserver(DataSetObserver observer) {
1844 mPagerController.registerConversationLoadedObserver(observer);
1845 }
1846
1847 @Override
1848 public void unregisterConversationLoadedObserver(DataSetObserver observer) {
1849 mPagerController.unregisterConversationLoadedObserver(observer);
1850 }
1851
Vikram Aggarwal60069912012-07-24 14:26:09 -07001852 /**
1853 * Returns true if the number of accounts is different, or if the current account has been
1854 * removed from the device
1855 * @param accountCursor
1856 * @return
1857 */
Paul Westbrook23b74b92012-02-29 11:36:12 -08001858 private boolean accountsUpdated(Cursor accountCursor) {
1859 // Check to see if the current account hasn't been set, or the account cursor is empty
1860 if (mAccount == null || !accountCursor.moveToFirst()) {
Vikram Aggarwal6c511582012-02-27 10:59:47 -08001861 return true;
Paul Westbrook23b74b92012-02-29 11:36:12 -08001862 }
1863
1864 // Check to see if the number of accounts are different, from the number we saw on the last
1865 // updated
1866 if (mCurrentAccountUris.size() != accountCursor.getCount()) {
1867 return true;
1868 }
1869
1870 // Check to see if the account list is different or if the current account is not found in
1871 // the cursor.
1872 boolean foundCurrentAccount = false;
Vikram Aggarwal7dedb952012-02-16 16:10:23 -08001873 do {
Paul Westbrook23b74b92012-02-29 11:36:12 -08001874 final Uri accountUri =
1875 Uri.parse(accountCursor.getString(UIProvider.ACCOUNT_URI_COLUMN));
1876 if (!foundCurrentAccount && mAccount.uri.equals(accountUri)) {
1877 foundCurrentAccount = true;
1878 }
Vikram Aggarwal60069912012-07-24 14:26:09 -07001879 // Is there a new account that we do not know about?
Paul Westbrook23b74b92012-02-29 11:36:12 -08001880 if (!mCurrentAccountUris.contains(accountUri)) {
1881 return true;
1882 }
Vikram Aggarwal7dedb952012-02-16 16:10:23 -08001883 } while (accountCursor.moveToNext());
Paul Westbrook23b74b92012-02-29 11:36:12 -08001884
1885 // As long as we found the current account, the list hasn't been updated
1886 return !foundCurrentAccount;
Vikram Aggarwal7dedb952012-02-16 16:10:23 -08001887 }
1888
1889 /**
Vikram Aggarwal60069912012-07-24 14:26:09 -07001890 * Updates accounts for the app. If the current account is missing, the first
1891 * account in the list is set to the current account (we <em>have</em> to choose something).
Mindy Pereira161f50d2012-02-28 15:47:19 -08001892 *
Vikram Aggarwal6c511582012-02-27 10:59:47 -08001893 * @param accounts cursor into the AccountCache
Vikram Aggarwal7dedb952012-02-16 16:10:23 -08001894 * @return true if the update was successful, false otherwise
1895 */
Vikram Aggarwal60069912012-07-24 14:26:09 -07001896 private boolean updateAccounts(Cursor accounts) {
Vikram Aggarwal7dedb952012-02-16 16:10:23 -08001897 if (accounts == null || !accounts.moveToFirst()) {
1898 return false;
1899 }
Paul Westbrook23b74b92012-02-29 11:36:12 -08001900
Vikram Aggarwala9b93f32012-02-23 14:51:58 -08001901 final Account[] allAccounts = Account.getAllAccounts(accounts);
Vikram Aggarwal60069912012-07-24 14:26:09 -07001902 // A match for the current account's URI in the list of accounts.
1903 Account currentFromList = null;
Paul Westbrook23b74b92012-02-29 11:36:12 -08001904
Vikram Aggarwal636c1a12012-09-22 16:02:40 -07001905 // Save the uris for the accounts and find the current account in the updated cursor.
Paul Westbrook23b74b92012-02-29 11:36:12 -08001906 mCurrentAccountUris.clear();
Vikram Aggarwal636c1a12012-09-22 16:02:40 -07001907 for (final Account account : allAccounts) {
Vikram Aggarwal60069912012-07-24 14:26:09 -07001908 LogUtils.d(LOG_TAG, "updateAccounts(%s)", account);
Paul Westbrook23b74b92012-02-29 11:36:12 -08001909 mCurrentAccountUris.add(account.uri);
Vikram Aggarwal60069912012-07-24 14:26:09 -07001910 if (mAccount != null && account.uri.equals(mAccount.uri)) {
1911 currentFromList = account;
1912 }
Paul Westbrook23b74b92012-02-29 11:36:12 -08001913 }
1914
Vikram Aggarwal60069912012-07-24 14:26:09 -07001915 // 1. current account is already set and is in allAccounts:
1916 // 1a. It has changed -> load the updated account.
1917 // 2b. It is unchanged -> no-op
Andy Huang0d647352012-03-21 21:48:16 -07001918 // 2. current account is set and is not in allAccounts -> pick first (acct was deleted?)
Vikram Aggarwal60069912012-07-24 14:26:09 -07001919 // 3. saved preference has an account -> pick that one
Andy Huang0d647352012-03-21 21:48:16 -07001920 // 4. otherwise just pick first
1921
Vikram Aggarwal60069912012-07-24 14:26:09 -07001922 boolean accountChanged = false;
1923 /// Assume case 4, initialize to first account, and see if we can find anything better.
1924 Account newAccount = allAccounts[0];
1925 if (currentFromList != null) {
1926 // Case 1: Current account exists but has changed
1927 if (!currentFromList.equals(mAccount)) {
1928 newAccount = currentFromList;
1929 accountChanged = true;
Andy Huang0d647352012-03-21 21:48:16 -07001930 }
Vikram Aggarwal60069912012-07-24 14:26:09 -07001931 // Case 1b: else, current account is unchanged: nothing to do.
Paul Westbrook23b74b92012-02-29 11:36:12 -08001932 } else {
Vikram Aggarwal60069912012-07-24 14:26:09 -07001933 // Case 2: Current account is not in allAccounts, the account needs to change.
1934 accountChanged = true;
1935 if (mAccount == null) {
1936 // Case 3: Check for last viewed account, and check if it exists in the list.
1937 final String lastAccountUri = MailAppProvider.getInstance().getLastViewedAccount();
1938 if (lastAccountUri != null) {
1939 for (final Account account : allAccounts) {
1940 if (lastAccountUri.equals(account.uri.toString())) {
1941 newAccount = account;
1942 break;
1943 }
Andy Huang0d647352012-03-21 21:48:16 -07001944 }
1945 }
1946 }
Paul Westbrook23b74b92012-02-29 11:36:12 -08001947 }
Vikram Aggarwal60069912012-07-24 14:26:09 -07001948 if (accountChanged) {
1949 onAccountChanged(newAccount);
1950 }
1951 // Whether we have updated the current account or not, we need to update the list of
1952 // accounts in the ActionBar.
Vikram Aggarwala9b93f32012-02-23 14:51:58 -08001953 mActionBarView.setAccounts(allAccounts);
1954 return (allAccounts.length > 0);
Vikram Aggarwal7dedb952012-02-16 16:10:23 -08001955 }
1956
Paul Westbrook6ead20d2012-03-19 14:48:14 -07001957 private void disableNotifications() {
1958 mNewEmailReceiver.activate(mContext, this);
1959 }
1960
1961 private void enableNotifications() {
1962 mNewEmailReceiver.deactivate();
1963 }
1964
1965 private void disableNotificationsOnAccountChange(Account account) {
1966 // If the new mail suppression receiver is activated for a different account, we want to
1967 // activate it for the new account.
1968 if (mNewEmailReceiver.activated() &&
1969 !mNewEmailReceiver.notificationsDisabledForAccount(account)) {
1970 // Deactivate the current receiver, otherwise multiple receivers may be registered.
1971 mNewEmailReceiver.deactivate();
1972 mNewEmailReceiver.activate(mContext, this);
1973 }
1974 }
1975
Vikram Aggarwala9b93f32012-02-23 14:51:58 -08001976 /**
1977 * {@inheritDoc}
1978 */
Vikram Aggarwal7dedb952012-02-16 16:10:23 -08001979 @Override
1980 public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
Mindy Pereira5cb0c3e2012-02-22 15:22:47 -08001981 // We want to reinitialize only if we haven't ever been initialized, or
1982 // if the current account has vanished.
Paul Westbrooke3e84292012-03-05 16:19:30 -08001983 if (data == null) {
Vikram Aggarwalfa4b47e2012-03-09 13:02:46 -08001984 LogUtils.e(LOG_TAG, "Received null cursor from loader id: %d", loader.getId());
Paul Westbrooke3e84292012-03-05 16:19:30 -08001985 }
Vikram Aggarwalfa4b47e2012-03-09 13:02:46 -08001986 switch (loader.getId()) {
1987 case LOADER_ACCOUNT_CURSOR:
Vikram Aggarwal636c1a12012-09-22 16:02:40 -07001988 if (data == null) {
1989 // Nothing useful to do if we have no valid data.
1990 break;
1991 }
1992 if (data.getCount() == 0) {
Paul Westbrook2388c5d2012-03-25 12:29:11 -07001993 // If an empty cursor is returned, the MailAppProvider is indicating that
1994 // no accounts have been specified. We want to navigate to the "add account"
1995 // activity that will handle the intent returned by the MailAppProvider
1996
1997 // If the MailAppProvider believes that all accounts have been loaded, and the
1998 // account list is still empty, we want to prompt the user to add an account
1999 final Bundle extras = data.getExtras();
2000 final boolean accountsLoaded =
2001 extras.getInt(AccountCursorExtraKeys.ACCOUNTS_LOADED) != 0;
2002
2003 if (accountsLoaded) {
2004 final Intent noAccountIntent = MailAppProvider.getNoAccountIntent(mContext);
2005 if (noAccountIntent != null) {
2006 mActivity.startActivityForResult(noAccountIntent,
2007 ADD_ACCOUNT_REQUEST_CODE);
2008 }
2009 }
2010 } else {
2011 final boolean accountListUpdated = accountsUpdated(data);
2012 if (!isLoaderInitialized || accountListUpdated) {
Vikram Aggarwal60069912012-07-24 14:26:09 -07002013 isLoaderInitialized = updateAccounts(data);
Paul Westbrook2388c5d2012-03-25 12:29:11 -07002014 }
Vikram Aggarwalfa4b47e2012-03-09 13:02:46 -08002015 }
2016 break;
Paul Westbrook2d50bcd2012-04-10 11:53:47 -07002017 case LOADER_ACCOUNT_UPDATE_CURSOR:
2018 // We have gotten an update for current account.
2019
Vikram Aggarwal60069912012-07-24 14:26:09 -07002020 // Make sure that this is an update for the current account
Paul Westbrook2d50bcd2012-04-10 11:53:47 -07002021 if (data != null && data.moveToFirst()) {
2022 final Account updatedAccount = new Account(data);
2023
2024 if (updatedAccount.uri.equals(mAccount.uri)) {
Paul Westbrookca08fc12012-07-31 12:01:15 -07002025 // Keep a reference to the previous settings object
2026 final Settings previousSettings = mAccount.settings;
2027
Vikram Aggarwal7d816002012-04-17 17:06:41 -07002028 // Update the controller's reference to the current account
Paul Westbrook2d50bcd2012-04-10 11:53:47 -07002029 mAccount = updatedAccount;
Vikram Aggarwaledc137c2012-04-24 13:40:58 -07002030 LogUtils.d(LOG_TAG, "AbstractActivityController.onLoadFinished(): "
2031 + "mAccount = %s", mAccount.uri);
Paul Westbrookca08fc12012-07-31 12:01:15 -07002032
2033 // Only notify about a settings change if something differs
2034 if (!Objects.equal(mAccount.settings, previousSettings)) {
Vikram Aggarwal7c401b72012-08-13 16:43:47 -07002035 mAccountObservers.notifyChanged();
Paul Westbrookca08fc12012-07-31 12:01:15 -07002036 }
Vikram Aggarwal34f7b232012-10-17 13:32:23 -07002037 perhapsEnterWaitMode();
Paul Westbrook2d50bcd2012-04-10 11:53:47 -07002038 } else {
2039 LogUtils.e(LOG_TAG, "Got update for account: %s with current account: %s",
2040 updatedAccount.uri, mAccount.uri);
2041 // We need to restart the loader, so the correct account information will
2042 // be returned
2043 restartOptionalLoader(LOADER_ACCOUNT_UPDATE_CURSOR);
2044 }
2045 }
2046 break;
Vikram Aggarwalfa4b47e2012-03-09 13:02:46 -08002047 case LOADER_FOLDER_CURSOR:
Vikram Aggarwalfa4b47e2012-03-09 13:02:46 -08002048 // Check status of the cursor.
Marc Blankfd9d0b82012-04-23 16:01:51 -07002049 if (data != null && data.moveToFirst()) {
Andy Huang090db1e2012-07-25 13:25:28 -07002050 final Folder folder = new Folder(data);
Marc Blankfd9d0b82012-04-23 16:01:51 -07002051 LogUtils.d(LOG_TAG, "FOLDER STATUS = %d", folder.syncStatus);
Vikram Aggarwald00229d2012-09-20 12:31:44 -07002052 setHasFolderChanged(folder);
Andy Huang090db1e2012-07-25 13:25:28 -07002053 mFolder = folder;
2054 mFolderObservable.notifyChanged();
Paul Westbrookc808fac2012-02-22 16:42:18 -08002055 } else {
Marc Blankfd9d0b82012-04-23 16:01:51 -07002056 LogUtils.d(LOG_TAG, "Unable to get the folder %s",
2057 mFolder != null ? mAccount.name : "");
Mindy Pereira11dd5ef2012-03-10 15:10:18 -08002058 }
Vikram Aggarwalfa4b47e2012-03-09 13:02:46 -08002059 break;
Vikram Aggarwalfa4b47e2012-03-09 13:02:46 -08002060 case LOADER_RECENT_FOLDERS:
Vikram Aggarwalf9ba8712012-09-23 11:42:39 -07002061 // Few recent folders and we are running on a phone? Populate the default recents.
2062 // The number of default recent folders is at least 2: every provider has at
2063 // least two folders, and the recent folder count never decreases. Having a single
2064 // recent folder is an erroneous case, and we can gracefully recover by populating
2065 // default recents. The default recents will not stomp on the existing value: it
2066 // will be shown in addition to the default folders: the max number of recent
2067 // folders is more than 1+num(defaultRecents).
2068 if (data != null && data.getCount() <= 1 && !Utils.useTabletUI(mContext)) {
Vikram Aggarwal27d89ad2012-06-12 13:38:40 -07002069 final class PopulateDefault extends AsyncTask<Uri, Void, Void> {
2070 @Override
2071 protected Void doInBackground(Uri... uri) {
2072 // Asking for an update on the URI and ignore the result.
2073 final ContentResolver resolver = mContext.getContentResolver();
2074 resolver.update(uri[0], null, null, null);
2075 return null;
2076 }
2077 }
2078 final Uri uri = mAccount.defaultRecentFolderListUri;
2079 LogUtils.v(LOG_TAG, "Default recents at %s", uri);
2080 new PopulateDefault().execute(uri);
2081 break;
2082 }
2083 LogUtils.v(LOG_TAG, "Reading recent folders from the cursor.");
mindyp6f54e1b2012-10-09 09:54:08 -07002084 loadRecentFolders(data);
Vikram Aggarwalfa4b47e2012-03-09 13:02:46 -08002085 break;
Mindy Pereiraab486362012-03-21 18:18:53 -07002086 case LOADER_ACCOUNT_INBOX:
Marc Blankfd9d0b82012-04-23 16:01:51 -07002087 if (data != null && !data.isClosed() && data.moveToFirst()) {
Mindy Pereira0e88e9f2012-03-25 13:47:41 -07002088 Folder inbox = new Folder(data);
2089 onFolderChanged(inbox);
Mindy Pereirab4a43282012-03-23 16:20:03 -07002090 // Just want to get the inbox, don't care about updates to it
2091 // as this will be tracked by the folder change listener.
2092 mActivity.getLoaderManager().destroyLoader(LOADER_ACCOUNT_INBOX);
Mindy Pereira5ba33802012-03-26 16:30:11 -07002093 } else {
2094 LogUtils.d(LOG_TAG, "Unable to get the account inbox for account %s",
2095 mAccount != null ? mAccount.name : "");
Mindy Pereirab4a43282012-03-23 16:20:03 -07002096 }
Mindy Pereiraab486362012-03-21 18:18:53 -07002097 break;
2098 case LOADER_SEARCH:
Paul Westbrookc4845c52012-08-29 21:48:43 -07002099 if (data != null && data.getCount() > 0) {
2100 data.moveToFirst();
Vikram Aggarwal2b703c62012-09-18 13:54:15 -07002101 final Folder search = new Folder(data);
Paul Westbrookc4845c52012-08-29 21:48:43 -07002102 updateFolder(search);
2103 mConvListContext = ConversationListContext.forSearchQuery(mAccount, mFolder,
2104 mActivity.getIntent()
2105 .getStringExtra(UIProvider.SearchQueryParameters.QUERY));
2106 showConversationList(mConvListContext);
2107 mActivity.invalidateOptionsMenu();
Vikram Aggarwalf0ef2302012-11-07 14:53:34 -08002108 mHaveSearchResults = search.totalCount > 0;
Paul Westbrookc4845c52012-08-29 21:48:43 -07002109 mActivity.getLoaderManager().destroyLoader(LOADER_SEARCH);
2110 } else {
2111 LogUtils.e(LOG_TAG, "Null or empty cursor returned by LOADER_SEARCH loader");
2112 }
Mindy Pereiraab486362012-03-21 18:18:53 -07002113 break;
Vikram Aggarwal7dedb952012-02-16 16:10:23 -08002114 }
2115 }
2116
mindyp6f54e1b2012-10-09 09:54:08 -07002117
Vikram Aggarwalc7694222012-04-23 13:37:01 -07002118 /**
2119 * Destructive actions on Conversations. This class should only be created by controllers, and
2120 * clients should only require {@link DestructiveAction}s, not specific implementations of the.
2121 * Only the controllers should know what kind of destructive actions are being created.
2122 */
Mindy Pereirade3e74a2012-07-24 09:43:10 -07002123 public class ConversationAction implements DestructiveAction {
Vikram Aggarwalacaa3c02012-04-24 12:45:27 -07002124 /**
2125 * The action to be performed. This is specified as the resource ID of the menu item
2126 * corresponding to this action: R.id.delete, R.id.report_spam, etc.
2127 */
Vikram Aggarwal7f602f72012-04-30 16:04:06 -07002128 private final int mAction;
Vikram Aggarwalbc67bb12012-04-30 14:05:35 -07002129 /** The action will act upon these conversations */
Paul Westbrook77eee622012-07-10 13:41:57 -07002130 private final Collection<Conversation> mTarget;
Vikram Aggarwal7f602f72012-04-30 16:04:06 -07002131 /** Whether this destructive action has already been performed */
2132 private boolean mCompleted;
Vikram Aggarwalc4113952012-05-11 14:14:56 -07002133 /** Whether this is an action on the currently selected set. */
2134 private final boolean mIsSelectedSet;
Vikram Aggarwale8a85322012-04-24 09:01:38 -07002135
Mindy Pereirafbe40192012-03-20 10:40:45 -07002136 /**
2137 * Create a listener object. action is one of four constants: R.id.y_button (archive),
2138 * R.id.delete , R.id.mute, and R.id.report_spam.
2139 * @param action
Vikram Aggarwalbc67bb12012-04-30 14:05:35 -07002140 * @param target Conversation that we want to apply the action to.
Vikram Aggarwalc4113952012-05-11 14:14:56 -07002141 * @param isBatch whether the conversations are in the currently selected batch set.
2142 */
2143 public ConversationAction(int action, Collection<Conversation> target, boolean isBatch) {
Mindy Pereirafbe40192012-03-20 10:40:45 -07002144 mAction = action;
Paul Westbrook77eee622012-07-10 13:41:57 -07002145 mTarget = ImmutableList.copyOf(target);
Vikram Aggarwalc4113952012-05-11 14:14:56 -07002146 mIsSelectedSet = isBatch;
Mindy Pereirafbe40192012-03-20 10:40:45 -07002147 }
2148
Vikram Aggarwalacaa3c02012-04-24 12:45:27 -07002149 /**
2150 * The action common to child classes. This performs the action specified in the constructor
2151 * on the conversations given here.
Vikram Aggarwalacaa3c02012-04-24 12:45:27 -07002152 */
Vikram Aggarwalbc67bb12012-04-30 14:05:35 -07002153 @Override
2154 public void performAction() {
Vikram Aggarwal7f602f72012-04-30 16:04:06 -07002155 if (isPerformed()) {
2156 return;
2157 }
Mindy Pereira3cb28b52012-05-24 15:26:39 -07002158 boolean undoEnabled = mAccount.supportsCapability(AccountCapabilities.UNDO);
Vikram Aggarwal7dd054e2012-05-21 14:43:10 -07002159
2160 // Are we destroying the currently shown conversation? Show the next one.
2161 if (LogUtils.isLoggable(LOG_TAG, LogUtils.DEBUG)){
Vikram Aggarwal8742a612012-08-13 10:22:50 -07002162 LogUtils.d(LOG_TAG, "ConversationAction.performAction():"
2163 + "\nmTarget=%s\nCurrent=%s",
Vikram Aggarwal7dd054e2012-05-21 14:43:10 -07002164 Conversation.toString(mTarget), mCurrentConversation);
2165 }
Vikram Aggarwal7dd054e2012-05-21 14:43:10 -07002166
Paul Westbrooke1221d22012-08-19 11:09:07 -07002167 if (mConversationListCursor == null) {
2168 LogUtils.e(LOG_TAG, "null ConversationCursor in ConversationAction.performAction():"
2169 + "\nmTarget=%s\nCurrent=%s",
2170 Conversation.toString(mTarget), mCurrentConversation);
2171 return;
2172 }
2173
Mindy Pereirafbe40192012-03-20 10:40:45 -07002174 switch (mAction) {
Mindy Pereira0692baf2012-03-23 17:34:31 -07002175 case R.id.archive:
Vikram Aggarwal7dd054e2012-05-21 14:43:10 -07002176 LogUtils.d(LOG_TAG, "Archiving");
Vikram Aggarwalbc67bb12012-04-30 14:05:35 -07002177 mConversationListCursor.archive(mContext, mTarget);
Mindy Pereirafbe40192012-03-20 10:40:45 -07002178 break;
2179 case R.id.delete:
Vikram Aggarwal7dd054e2012-05-21 14:43:10 -07002180 LogUtils.d(LOG_TAG, "Deleting");
Vikram Aggarwalbc67bb12012-04-30 14:05:35 -07002181 mConversationListCursor.delete(mContext, mTarget);
Mindy Pereira695d6962012-06-18 13:02:10 -07002182 if (mFolder.supportsCapability(FolderCapabilities.DELETE_ACTION_FINAL)) {
Marc Blank386243f2012-05-25 10:40:59 -07002183 undoEnabled = false;
2184 }
Mindy Pereirafbe40192012-03-20 10:40:45 -07002185 break;
2186 case R.id.mute:
Vikram Aggarwal7dd054e2012-05-21 14:43:10 -07002187 LogUtils.d(LOG_TAG, "Muting");
Vikram Aggarwalbc67bb12012-04-30 14:05:35 -07002188 if (mFolder.supportsCapability(FolderCapabilities.DESTRUCTIVE_MUTE)) {
Vikram Aggarwal7f602f72012-04-30 16:04:06 -07002189 for (Conversation c : mTarget) {
2190 c.localDeleteOnUpdate = true;
2191 }
Vikram Aggarwalbc67bb12012-04-30 14:05:35 -07002192 }
2193 mConversationListCursor.mute(mContext, mTarget);
Mindy Pereirafbe40192012-03-20 10:40:45 -07002194 break;
2195 case R.id.report_spam:
Vikram Aggarwal7dd054e2012-05-21 14:43:10 -07002196 LogUtils.d(LOG_TAG, "Reporting spam");
Vikram Aggarwalbc67bb12012-04-30 14:05:35 -07002197 mConversationListCursor.reportSpam(mContext, mTarget);
2198 break;
Paul Westbrook77eee622012-07-10 13:41:57 -07002199 case R.id.mark_not_spam:
2200 LogUtils.d(LOG_TAG, "Marking not spam");
2201 mConversationListCursor.reportNotSpam(mContext, mTarget);
2202 break;
Paul Westbrook76b20622012-07-12 11:45:43 -07002203 case R.id.report_phishing:
2204 LogUtils.d(LOG_TAG, "Reporting phishing");
2205 mConversationListCursor.reportPhishing(mContext, mTarget);
2206 break;
Vikram Aggarwalbc67bb12012-04-30 14:05:35 -07002207 case R.id.remove_star:
Vikram Aggarwal7dd054e2012-05-21 14:43:10 -07002208 LogUtils.d(LOG_TAG, "Removing star");
Vikram Aggarwalbc67bb12012-04-30 14:05:35 -07002209 // Star removal is destructive in the Starred folder.
2210 mConversationListCursor.updateBoolean(mContext, mTarget,
2211 ConversationColumns.STARRED, false);
2212 break;
2213 case R.id.mark_not_important:
Vikram Aggarwal7dd054e2012-05-21 14:43:10 -07002214 LogUtils.d(LOG_TAG, "Marking not-important");
Mindy Pereira445be212012-08-15 08:50:10 -07002215 // Marking not important is destructive in a mailbox
2216 // containing only important messages
Mindy Pereira0d03ef82012-08-15 09:05:48 -07002217 if (mFolder != null && mFolder.isImportantOnly()) {
Mindy Pereira445be212012-08-15 08:50:10 -07002218 for (Conversation conv : mTarget) {
2219 conv.localDeleteOnUpdate = true;
2220 }
2221 }
Vikram Aggarwalbc67bb12012-04-30 14:05:35 -07002222 mConversationListCursor.updateInt(mContext, mTarget,
2223 ConversationColumns.PRIORITY, UIProvider.ConversationPriority.LOW);
Mindy Pereirafbe40192012-03-20 10:40:45 -07002224 break;
Paul Westbrookef362542012-08-27 14:53:32 -07002225 case R.id.discard_drafts:
2226 LogUtils.d(LOG_TAG, "Discarding draft messages");
2227 // Discarding draft messages is destructive in a "draft" mailbox
2228 if (mFolder != null && mFolder.isDraft()) {
2229 for (Conversation conv : mTarget) {
2230 conv.localDeleteOnUpdate = true;
2231 }
2232 }
2233 mConversationListCursor.discardDrafts(mContext, mTarget);
2234 // We don't support undoing discarding drafts
2235 undoEnabled = false;
2236 break;
Vikram Aggarwal3d7ca9d2012-05-11 14:40:36 -07002237 }
2238 if (undoEnabled) {
mindypead50392012-08-23 11:03:53 -07002239 mHandler.postDelayed(new Runnable() {
2240 @Override
2241 public void run() {
2242 onUndoAvailable(new ToastBarOperation(mTarget.size(), mAction,
mindypa59283a2012-09-11 17:49:06 -07002243 ToastBarOperation.UNDO, mIsSelectedSet));
mindypead50392012-08-23 11:03:53 -07002244 }
2245 }, mShowUndoBarDelay);
Vikram Aggarwal3d7ca9d2012-05-11 14:40:36 -07002246 }
Vikram Aggarwalbc67bb12012-04-30 14:05:35 -07002247 refreshConversationList();
Vikram Aggarwalc4113952012-05-11 14:14:56 -07002248 if (mIsSelectedSet) {
Vikram Aggarwalc4113952012-05-11 14:14:56 -07002249 mSelectedSet.clear();
2250 }
Mindy Pereirafbe40192012-03-20 10:40:45 -07002251 }
Vikram Aggarwal7f602f72012-04-30 16:04:06 -07002252
2253 /**
2254 * Returns true if this action has been performed, false otherwise.
Andy Huang839ada22012-07-20 15:48:40 -07002255 *
Vikram Aggarwal7f602f72012-04-30 16:04:06 -07002256 */
2257 private synchronized boolean isPerformed() {
2258 if (mCompleted) {
2259 return true;
2260 }
2261 mCompleted = true;
2262 return false;
2263 }
Mindy Pereirafbe40192012-03-20 10:40:45 -07002264 }
Mindy Pereirae5f4dc02012-03-21 16:08:53 -07002265
Vikram Aggarwal3d7ca9d2012-05-11 14:40:36 -07002266 /**
2267 * Get a destructive action for a menu action.
2268 * This is a temporary method, to control the profusion of {@link DestructiveAction} classes
2269 * that are created. Please do not copy this paradigm.
2270 * @param action the resource ID of the menu action: R.id.delete, for example
2271 * @param target the conversations to act upon.
2272 * @return a {@link DestructiveAction} that performs the specified action.
2273 */
Vikram Aggarwal531488e2012-05-29 16:36:52 -07002274 private final DestructiveAction getAction(int action, Collection<Conversation> target) {
Vikram Aggarwal3d7ca9d2012-05-11 14:40:36 -07002275 final DestructiveAction da = new ConversationAction(action, target, false);
2276 registerDestructiveAction(da);
2277 return da;
2278 }
2279
Vikram Aggarwald503df42012-05-11 10:13:35 -07002280 // Called from the FolderSelectionDialog after a user is done selecting folders to assign the
2281 // conversations to.
Mindy Pereirae5f4dc02012-03-21 16:08:53 -07002282 @Override
Mindy Pereira8db7e402012-07-13 10:32:47 -07002283 public final void assignFolder(Collection<FolderOperation> folderOps,
2284 Collection<Conversation> target, boolean batch, boolean showUndo) {
2285 // Actions are destructive only when the current folder can be assigned
2286 // to (which is the same as being able to un-assign a conversation from the folder) and
2287 // when the list of folders contains the current folder.
2288 final boolean isDestructive = mFolder
2289 .supportsCapability(FolderCapabilities.CAN_ACCEPT_MOVED_MESSAGES)
2290 && FolderOperation.isDestructive(folderOps, mFolder);
Vikram Aggarwald503df42012-05-11 10:13:35 -07002291 LogUtils.d(LOG_TAG, "onFolderChangesCommit: isDestructive = %b", isDestructive);
2292 if (isDestructive) {
2293 for (final Conversation c : target) {
2294 c.localDeleteOnUpdate = true;
Mindy Pereira6778f462012-03-23 18:01:55 -07002295 }
Mindy Pereirae5f4dc02012-03-21 16:08:53 -07002296 }
mindypc84759c2012-08-29 09:51:53 -07002297 final DestructiveAction folderChange;
Vikram Aggarwald503df42012-05-11 10:13:35 -07002298 // Update the UI elements depending no their visibility and availability
Vikram Aggarwal3d7ca9d2012-05-11 14:40:36 -07002299 // TODO(viki): Consolidate this into a single method requestDelete.
Vikram Aggarwald503df42012-05-11 10:13:35 -07002300 if (isDestructive) {
mindypc84759c2012-08-29 09:51:53 -07002301 folderChange = getDeferredFolderChange(target, folderOps, isDestructive,
2302 batch, showUndo);
Vikram Aggarwala8e43182012-09-13 12:55:10 -07002303 delete(0, target, folderChange);
Vikram Aggarwal6902dcf2012-04-11 08:57:42 -07002304 } else {
mindypc84759c2012-08-29 09:51:53 -07002305 folderChange = getFolderChange(target, folderOps, isDestructive,
2306 batch, showUndo);
Vikram Aggarwald503df42012-05-11 10:13:35 -07002307 requestUpdate(target, folderChange);
Mindy Pereirae5f4dc02012-03-21 16:08:53 -07002308 }
2309 }
2310
Mindy Pereira967ede62012-03-22 09:29:09 -07002311 @Override
Vikram Aggarwal3d7ca9d2012-05-11 14:40:36 -07002312 public final void onRefreshRequired() {
mindyp5390fca2012-08-22 12:12:25 -07002313 if (isAnimating() || isDragging()) {
Mindy Pereira69e88dd2012-08-10 09:30:18 -07002314 LogUtils.d(LOG_TAG, "onRefreshRequired: delay until animating done");
2315 return;
2316 }
Marc Blankbf128eb2012-04-18 15:58:45 -07002317 // Refresh the query in the background
Paul Westbrookcff1aea2012-08-10 11:51:00 -07002318 if (mConversationListCursor.isRefreshRequired()) {
2319 mConversationListCursor.refresh();
2320 }
Marc Blankbf128eb2012-04-18 15:58:45 -07002321 }
2322
mindyp5390fca2012-08-22 12:12:25 -07002323 @Override
2324 public void startDragMode() {
2325 mIsDragHappening = true;
2326 }
2327
2328 @Override
2329 public void stopDragMode() {
2330 mIsDragHappening = false;
2331 if (mConversationListCursor.isRefreshReady()) {
2332 LogUtils.d(LOG_TAG, "Stopped animating: try sync");
2333 onRefreshReady();
2334 }
2335
2336 if (mConversationListCursor.isRefreshRequired()) {
2337 LogUtils.d(LOG_TAG, "Stopped animating: refresh");
2338 mConversationListCursor.refresh();
2339 }
2340 }
2341
2342 private boolean isDragging() {
2343 return mIsDragHappening;
2344 }
2345
mindyp6f54e1b2012-10-09 09:54:08 -07002346 @Override
2347 public boolean isAnimating() {
Mindy Pereira69e88dd2012-08-10 09:30:18 -07002348 boolean isAnimating = false;
2349 ConversationListFragment convListFragment = getConversationListFragment();
2350 if (convListFragment != null) {
2351 AnimatedAdapter adapter = convListFragment.getAnimatedAdapter();
2352 if (adapter != null) {
2353 isAnimating = adapter.isAnimating();
2354 }
2355 }
2356 return isAnimating;
2357 }
2358
Marc Blankbf128eb2012-04-18 15:58:45 -07002359 /**
2360 * Called when the {@link ConversationCursor} is changed or has new data in it.
2361 * <p>
2362 * {@inheritDoc}
2363 */
2364 @Override
Vikram Aggarwal3d7ca9d2012-05-11 14:40:36 -07002365 public final void onRefreshReady() {
mindyp5c1d8352012-11-05 10:12:44 -08002366 LogUtils.d(LOG_TAG, "Received refresh ready callback for folder %s",
2367 mFolder != null ? mFolder.id : "-1");
Paul Westbrookcff1aea2012-08-10 11:51:00 -07002368 if (!isAnimating()) {
Marc Blankbf128eb2012-04-18 15:58:45 -07002369 // Swap cursors
2370 mConversationListCursor.sync();
Marc Blankbf128eb2012-04-18 15:58:45 -07002371 }
Paul Westbrook937c94f2012-08-16 13:01:18 -07002372 mTracker.onCursorUpdated();
Vikram Aggarwalf0ef2302012-11-07 14:53:34 -08002373 perhapsShowFirstSearchResult();
Marc Blankbf128eb2012-04-18 15:58:45 -07002374 }
2375
2376 @Override
Vikram Aggarwal3d7ca9d2012-05-11 14:40:36 -07002377 public final void onDataSetChanged() {
Paul Westbrook9f119c72012-04-24 16:10:59 -07002378 updateConversationListFragment();
Andy Huang632721e2012-04-11 16:57:26 -07002379 mConversationListObservable.notifyChanged();
Paul Westbrooka13b3742012-09-07 16:35:06 -07002380 mSelectedSet.validateAgainstCursor(mConversationListCursor);
Marc Blankbf128eb2012-04-18 15:58:45 -07002381 }
2382
Vikram Aggarwal3d7ca9d2012-05-11 14:40:36 -07002383 /**
2384 * If the Conversation List Fragment is visible, updates the fragment.
2385 */
2386 private final void updateConversationListFragment() {
Marc Blankbf128eb2012-04-18 15:58:45 -07002387 final ConversationListFragment convList = getConversationListFragment();
2388 if (convList != null) {
Vikram Aggarwal75daee52012-04-30 11:13:09 -07002389 refreshConversationList();
Paul Westbrook9f119c72012-04-24 16:10:59 -07002390 if (convList.isVisible()) {
Vikram Aggarwal69b5c302012-09-05 11:11:13 -07002391 informCursorVisiblity(true);
Paul Westbrook9f119c72012-04-24 16:10:59 -07002392 }
Marc Blankbf128eb2012-04-18 15:58:45 -07002393 }
2394 }
2395
2396 /**
2397 * This class handles throttled refresh of the conversation list
2398 */
2399 static class RefreshTimerTask extends TimerTask {
2400 final Handler mHandler;
2401 final AbstractActivityController mController;
2402
2403 RefreshTimerTask(AbstractActivityController controller, Handler handler) {
2404 mHandler = handler;
2405 mController = controller;
2406 }
2407
2408 @Override
2409 public void run() {
2410 mHandler.post(new Runnable() {
2411 @Override
2412 public void run() {
2413 LogUtils.d(LOG_TAG, "Delay done... calling onRefreshRequired");
2414 mController.onRefreshRequired();
2415 }});
2416 }
2417 }
2418
2419 /**
2420 * Cancel the refresh task, if it's running
2421 */
2422 private void cancelRefreshTask () {
2423 if (mConversationListRefreshTask != null) {
2424 mConversationListRefreshTask.cancel();
2425 mConversationListRefreshTask = null;
2426 }
2427 }
2428
mindyp6f54e1b2012-10-09 09:54:08 -07002429 private void loadRecentFolders(Cursor data) {
2430 mRecentFolderList.loadFromUiProvider(data);
2431 if (isAnimating()) {
2432 mRecentsDataUpdated = true;
2433 } else {
2434 mRecentFolderObservers.notifyChanged();
2435 }
2436 }
2437
Marc Blankbf128eb2012-04-18 15:58:45 -07002438 @Override
Paul Westbrookcff1aea2012-08-10 11:51:00 -07002439 public void onAnimationEnd(AnimatedAdapter animatedAdapter) {
Paul Westbrook026139c2012-09-19 22:35:37 -07002440 if (mConversationListCursor == null) {
2441 LogUtils.e(LOG_TAG, "null ConversationCursor in onAnimationEnd");
2442 return;
2443 }
Paul Westbrookcff1aea2012-08-10 11:51:00 -07002444 if (mConversationListCursor.isRefreshReady()) {
mindyp52544862012-08-20 12:05:36 -07002445 LogUtils.d(LOG_TAG, "Stopped animating: try sync");
Paul Westbrookcff1aea2012-08-10 11:51:00 -07002446 onRefreshReady();
Marc Blankbf128eb2012-04-18 15:58:45 -07002447 }
Marc Blankbf128eb2012-04-18 15:58:45 -07002448
Paul Westbrookcff1aea2012-08-10 11:51:00 -07002449 if (mConversationListCursor.isRefreshRequired()) {
mindyp52544862012-08-20 12:05:36 -07002450 LogUtils.d(LOG_TAG, "Stopped animating: refresh");
Paul Westbrookcff1aea2012-08-10 11:51:00 -07002451 mConversationListCursor.refresh();
2452 }
mindyp6f54e1b2012-10-09 09:54:08 -07002453 if (mRecentsDataUpdated) {
2454 mRecentsDataUpdated = false;
2455 mRecentFolderObservers.notifyChanged();
2456 }
2457 FolderListFragment frag = this.getFolderListFragment();
2458 if (frag != null) {
2459 frag.onAnimationEnd();
2460 }
Marc Blankbf128eb2012-04-18 15:58:45 -07002461 }
2462
2463 @Override
Mindy Pereira967ede62012-03-22 09:29:09 -07002464 public void onSetEmpty() {
Mindy Pereira967ede62012-03-22 09:29:09 -07002465 }
2466
2467 @Override
2468 public void onSetPopulated(ConversationSelectionSet set) {
Vikram Aggarwal6902dcf2012-04-11 08:57:42 -07002469 final ConversationListFragment convList = getConversationListFragment();
2470 if (convList == null) {
2471 return;
2472 }
Vikram Aggarwal7c401b72012-08-13 16:43:47 -07002473 mCabActionMenu = new SelectedConversationsActionMenu(mActivity, set, mFolder,
Vikram Aggarwal531488e2012-05-29 16:36:52 -07002474 (SwipeableListView) convList.getListView());
Vikram Aggarwale128fc22012-04-04 12:33:34 -07002475 enableCabMode();
Mindy Pereira967ede62012-03-22 09:29:09 -07002476 }
2477
Mindy Pereira967ede62012-03-22 09:29:09 -07002478 @Override
2479 public void onSetChanged(ConversationSelectionSet set) {
2480 // Do nothing. We don't care about changes to the set.
2481 }
2482
2483 @Override
2484 public ConversationSelectionSet getSelectedSet() {
2485 return mSelectedSet;
2486 }
2487
Vikram Aggarwale128fc22012-04-04 12:33:34 -07002488 /**
2489 * Disable the Contextual Action Bar (CAB). The selected set is not changed.
2490 */
2491 protected void disableCabMode() {
Mindy Pereira8937bf12012-07-23 14:05:02 -07002492 // Commit any previous destructive actions when entering/ exiting CAB mode.
mindypc6adce32012-08-22 18:46:42 -07002493 commitDestructiveActions(true);
Vikram Aggarwale128fc22012-04-04 12:33:34 -07002494 if (mCabActionMenu != null) {
2495 mCabActionMenu.deactivate();
2496 }
2497 }
2498
2499 /**
2500 * Re-enable the CAB menu if required. The selection set is not changed.
2501 */
2502 protected void enableCabMode() {
2503 if (mCabActionMenu != null) {
2504 mCabActionMenu.activate();
2505 }
2506 }
2507
Vikram Aggarwal4eb52712012-06-19 16:24:50 -07002508 /**
2509 * Unselect conversations and exit CAB mode.
2510 */
2511 protected final void exitCabMode() {
2512 mSelectedSet.clear();
2513 }
2514
Mindy Pereira967ede62012-03-22 09:29:09 -07002515 @Override
Mindy Pereirafd0c2972012-03-27 13:50:39 -07002516 public void startSearch() {
Vikram Aggarwal35f19d72012-04-24 13:24:48 -07002517 if (mAccount == null) {
2518 // We cannot search if there is no account. Drop the request to the floor.
2519 LogUtils.d(LOG_TAG, "AbstractActivityController.startSearch(): null account");
2520 return;
2521 }
Mindy Pereirafd0c2972012-03-27 13:50:39 -07002522 if (mAccount.supportsCapability(UIProvider.AccountCapabilities.LOCAL_SEARCH)
2523 | mAccount.supportsCapability(UIProvider.AccountCapabilities.SERVER_SEARCH)) {
Vikram Aggarwal70f130e2012-04-03 12:32:14 -07002524 onSearchRequested(mActionBarView.getQuery());
Mindy Pereirafd0c2972012-03-27 13:50:39 -07002525 } else {
2526 Toast.makeText(mActivity.getActivityContext(), mActivity.getActivityContext()
Mindy Pereiraa46c2992012-03-27 14:12:39 -07002527 .getString(R.string.search_unsupported), Toast.LENGTH_SHORT).show();
Mindy Pereirafd0c2972012-03-27 13:50:39 -07002528 }
2529 }
Mindy Pereiraacf60392012-04-06 09:11:00 -07002530
Vikram Aggarwal0dda5732012-04-06 11:20:16 -07002531 @Override
2532 public void exitSearchMode() {
2533 if (mViewMode.getMode() == ViewMode.SEARCH_RESULTS_LIST) {
2534 mActivity.finish();
2535 }
2536 }
2537
Mindy Pereiraacf60392012-04-06 09:11:00 -07002538 /**
2539 * Supports dragging conversations to a folder.
2540 */
2541 @Override
2542 public boolean supportsDrag(DragEvent event, Folder folder) {
2543 return (folder != null
2544 && event != null
2545 && event.getClipDescription() != null
2546 && folder.supportsCapability
2547 (UIProvider.FolderCapabilities.CAN_ACCEPT_MOVED_MESSAGES)
2548 && folder.supportsCapability
2549 (UIProvider.FolderCapabilities.CAN_HOLD_MAIL)
2550 && !mFolder.uri.equals(folder.uri));
2551 }
2552
2553 /**
Mindy Pereira6c2663d2012-07-20 15:37:29 -07002554 * Handles dropping conversations to a folder.
Mindy Pereiraacf60392012-04-06 09:11:00 -07002555 */
2556 @Override
2557 public void handleDrop(DragEvent event, final Folder folder) {
Mindy Pereiraacf60392012-04-06 09:11:00 -07002558 if (!supportsDrag(event, folder)) {
2559 return;
2560 }
Mindy Pereiraacf60392012-04-06 09:11:00 -07002561 final Collection<Conversation> conversations = mSelectedSet.values();
mindypa8492632012-09-24 09:27:54 -07002562 final ArrayList<FolderOperation> dragDropOperations = new ArrayList<FolderOperation>();
2563 // Add the drop target folder.
2564 dragDropOperations.add(new FolderOperation(folder, true));
2565 // Remove the current folder unless the user is viewing "all".
2566 // That operation should just add the new folder.
2567 boolean isDestructive = !mFolder.isViewAll()
2568 && mFolder.supportsCapability
2569 (UIProvider.FolderCapabilities.CAN_ACCEPT_MOVED_MESSAGES);
2570 if (isDestructive) {
2571 dragDropOperations.add(new FolderOperation(mFolder, false));
2572 }
Mindy Pereira8db7e402012-07-13 10:32:47 -07002573 // Drag and drop is destructive: we remove conversations from the
2574 // current folder.
mindypa8492632012-09-24 09:27:54 -07002575 final DestructiveAction action = getFolderChange(conversations, dragDropOperations,
2576 isDestructive, true, true);
2577 if (isDestructive) {
2578 delete(0, conversations, action);
2579 } else {
2580 action.performAction();
2581 }
Mindy Pereiraacf60392012-04-06 09:11:00 -07002582 }
Mindy Pereira0963ef82012-04-10 11:43:01 -07002583
2584 @Override
Mindy Pereira0963ef82012-04-10 11:43:01 -07002585 public void onTouchEvent(MotionEvent event) {
2586 if (event.getAction() == MotionEvent.ACTION_DOWN) {
Andrew Sappersteinc2c9dc12012-07-02 18:17:32 -07002587 if (mToastBar != null && !mToastBar.isEventInToastBar(event)) {
Mark Weid243d452012-10-31 16:24:08 -07002588 hideOrRepositionToastBar(true);
Mindy Pereira0963ef82012-04-10 11:43:01 -07002589 }
2590 }
2591 }
Andy Huangb1c34dc2012-04-17 16:36:19 -07002592
Mark Weid243d452012-10-31 16:24:08 -07002593 protected abstract void hideOrRepositionToastBar(boolean animated);
2594
Andy Huang632721e2012-04-11 16:57:26 -07002595 @Override
2596 public void onConversationSeen(Conversation conv) {
2597 mPagerController.onConversationSeen(conv);
2598 }
2599
Andy Huang9d3fd922012-09-26 22:23:58 -07002600 @Override
2601 public boolean isInitialConversationLoading() {
2602 return mPagerController.isInitialConversationLoading();
2603 }
2604
Andy Huangb1c34dc2012-04-17 16:36:19 -07002605 private class ConversationListLoaderCallbacks implements
2606 LoaderManager.LoaderCallbacks<ConversationCursor> {
2607
2608 @Override
2609 public Loader<ConversationCursor> onCreateLoader(int id, Bundle args) {
2610 Loader<ConversationCursor> result = new ConversationCursorLoader((Activity) mActivity,
Paul Westbrook9a70e912012-08-17 15:53:20 -07002611 mAccount, mFolder.conversationListUri, mFolder.name);
Andy Huangb1c34dc2012-04-17 16:36:19 -07002612 return result;
2613 }
2614
2615 @Override
2616 public void onLoadFinished(Loader<ConversationCursor> loader, ConversationCursor data) {
Andy Huang632721e2012-04-11 16:57:26 -07002617 LogUtils.d(LOG_TAG, "IN AAC.ConversationCursor.onLoadFinished, data=%s loader=%s",
2618 data, loader);
Vikram Aggarwale8a85322012-04-24 09:01:38 -07002619 // Clear our all pending destructive actions before swapping the conversation cursor
2620 destroyPending(null);
Andy Huangb1c34dc2012-04-17 16:36:19 -07002621 mConversationListCursor = data;
Paul Westbrookbf232c32012-04-18 03:17:41 -07002622 mConversationListCursor.addListener(AbstractActivityController.this);
Andy Huangb1c34dc2012-04-17 16:36:19 -07002623
Paul Westbrook937c94f2012-08-16 13:01:18 -07002624 mTracker.onCursorUpdated();
Andy Huange3df1ad2012-04-24 17:15:23 -07002625 mConversationListObservable.notifyChanged();
Paul Westbrook9f119c72012-04-24 16:10:59 -07002626
Vikram Aggarwal17f373e2012-09-17 15:49:32 -07002627 final ConversationListFragment convList = getConversationListFragment();
2628 if (convList != null && convList.isVisible()) {
2629 // The conversation list is already listening to list changes and gets notified
2630 // in the mConversationListObservable.notifyChanged() line above. We only need to
2631 // check and inform the cursor of the change in visibility here.
2632 informCursorVisiblity(true);
Andy Huangb1c34dc2012-04-17 16:36:19 -07002633 }
Vikram Aggarwalf0ef2302012-11-07 14:53:34 -08002634 perhapsShowFirstSearchResult();
Andy Huangb1c34dc2012-04-17 16:36:19 -07002635 }
2636
2637 @Override
2638 public void onLoaderReset(Loader<ConversationCursor> loader) {
Paul Westbrook9a70e912012-08-17 15:53:20 -07002639 LogUtils.d(LOG_TAG, "IN AAC.ConversationCursor.onLoaderReset, data=%s loader=%s",
2640 mConversationListCursor, loader);
2641
2642 if (mConversationListCursor != null) {
2643 // Unregister the listener
2644 mConversationListCursor.removeListener(AbstractActivityController.this);
2645 mConversationListCursor = null;
2646
2647 // Inform anyone who is interested about the change
2648 mTracker.onCursorUpdated();
2649 mConversationListObservable.notifyChanged();
Andy Huangb1c34dc2012-04-17 16:36:19 -07002650 }
Andy Huangb1c34dc2012-04-17 16:36:19 -07002651 }
Paul Westbrookbf232c32012-04-18 03:17:41 -07002652 }
Andy Huangb1c34dc2012-04-17 16:36:19 -07002653
Vikram Aggarwale8a85322012-04-24 09:01:38 -07002654 /**
Vikram Aggarwalf0ef2302012-11-07 14:53:34 -08002655 * Updates controller state based on search results and shows first conversation if required.
2656 */
2657 private final void perhapsShowFirstSearchResult() {
mindypd27009a2012-11-27 11:54:18 -08002658 if (mCurrentConversation == null) {
2659 // Shown for search results in two-pane mode only.
2660 mHaveSearchResults = Intent.ACTION_SEARCH.equals(mActivity.getIntent().getAction())
2661 && mConversationListCursor.getCount() > 0;
2662 if (!shouldShowFirstConversation()) {
2663 return;
2664 }
2665 mConversationListCursor.moveToPosition(0);
2666 final Conversation conv = new Conversation(mConversationListCursor);
2667 conv.position = 0;
2668 onConversationSelected(conv, true /* checkSafeToModifyFragments */);
Vikram Aggarwalf0ef2302012-11-07 14:53:34 -08002669 }
Vikram Aggarwalf0ef2302012-11-07 14:53:34 -08002670 }
2671
2672 /**
Vikram Aggarwale8a85322012-04-24 09:01:38 -07002673 * Destroy the pending {@link DestructiveAction} till now and assign the given action as the
2674 * next destructive action..
2675 * @param nextAction the next destructive action to be performed. This can be null.
2676 */
2677 private final void destroyPending(DestructiveAction nextAction) {
2678 // If there is a pending action, perform that first.
2679 if (mPendingDestruction != null) {
2680 mPendingDestruction.performAction();
2681 }
2682 mPendingDestruction = nextAction;
2683 }
2684
2685 /**
2686 * Register a destructive action with the controller. This performs the previous destructive
Vikram Aggarwalacaa3c02012-04-24 12:45:27 -07002687 * 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 -07002688 * embellish this method any more.
Vikram Aggarwale8a85322012-04-24 09:01:38 -07002689 * @param action
2690 */
Vikram Aggarwal3d7ca9d2012-05-11 14:40:36 -07002691 private final void registerDestructiveAction(DestructiveAction action) {
Vikram Aggarwale8a85322012-04-24 09:01:38 -07002692 // TODO(viki): This is not a good idea. The best solution is for clients to request a
2693 // destructive action from the controller and for the controller to own the action. This is
2694 // a half-way solution while refactoring DestructiveAction.
2695 destroyPending(action);
2696 return;
2697 }
2698
Vikram Aggarwal531488e2012-05-29 16:36:52 -07002699 @Override
2700 public final DestructiveAction getBatchAction(int action) {
Vikram Aggarwalc4113952012-05-11 14:14:56 -07002701 final DestructiveAction da = new ConversationAction(action, mSelectedSet.values(), true);
Vikram Aggarwale8a85322012-04-24 09:01:38 -07002702 registerDestructiveAction(da);
2703 return da;
2704 }
Vikram Aggarwal41e6e712012-04-24 11:22:57 -07002705
Mindy Pereirade3e74a2012-07-24 09:43:10 -07002706 @Override
2707 public final DestructiveAction getDeferredBatchAction(int action) {
mindypf0656a12012-10-01 08:30:57 -07002708 return getDeferredAction(action, mSelectedSet.values(), true);
2709 }
2710
2711 /**
2712 * Get a destructive action for a menu action. This is a temporary method,
2713 * to control the profusion of {@link DestructiveAction} classes that are
2714 * created. Please do not copy this paradigm.
2715 * @param action the resource ID of the menu action: R.id.delete, for
2716 * example
2717 * @param target the conversations to act upon.
2718 * @return a {@link DestructiveAction} that performs the specified action.
2719 */
2720 @Override
2721 public DestructiveAction getDeferredAction(int action, Collection<Conversation> target,
2722 boolean batch) {
2723 final DestructiveAction da = new ConversationAction(action, target, batch);
Mindy Pereirade3e74a2012-07-24 09:43:10 -07002724 return da;
2725 }
2726
Vikram Aggarwalbc67bb12012-04-30 14:05:35 -07002727 /**
2728 * Class to change the folders that are assigned to a set of conversations. This is destructive
2729 * because the user can remove the current folder from the conversation, in which case it has
2730 * to be animated away from the current folder.
2731 */
Vikram Aggarwal41e6e712012-04-24 11:22:57 -07002732 private class FolderDestruction implements DestructiveAction {
Paul Westbrook77eee622012-07-10 13:41:57 -07002733 private final Collection<Conversation> mTarget;
Mindy Pereira8db7e402012-07-13 10:32:47 -07002734 private final ArrayList<FolderOperation> mFolderOps = new ArrayList<FolderOperation>();
Vikram Aggarwal41e6e712012-04-24 11:22:57 -07002735 private final boolean mIsDestructive;
Vikram Aggarwal7f602f72012-04-30 16:04:06 -07002736 /** Whether this destructive action has already been performed */
2737 private boolean mCompleted;
Mindy Pereiraf3a45562012-05-24 16:30:19 -07002738 private boolean mIsSelectedSet;
Mindy Pereira06642fa2012-07-12 16:23:27 -07002739 private boolean mShowUndo;
Mindy Pereira01f30502012-08-14 10:30:51 -07002740 private int mAction;
Vikram Aggarwal41e6e712012-04-24 11:22:57 -07002741
2742 /**
2743 * Create a new folder destruction object to act on the given conversations.
2744 * @param target
2745 */
Vikram Aggarwal7f602f72012-04-30 16:04:06 -07002746 private FolderDestruction(final Collection<Conversation> target,
Mindy Pereira8db7e402012-07-13 10:32:47 -07002747 final Collection<FolderOperation> folders, boolean isDestructive, boolean isBatch,
Mindy Pereira01f30502012-08-14 10:30:51 -07002748 boolean showUndo, int action) {
Paul Westbrook77eee622012-07-10 13:41:57 -07002749 mTarget = ImmutableList.copyOf(target);
Mindy Pereira8db7e402012-07-13 10:32:47 -07002750 mFolderOps.addAll(folders);
Vikram Aggarwal41e6e712012-04-24 11:22:57 -07002751 mIsDestructive = isDestructive;
Mindy Pereiraf3a45562012-05-24 16:30:19 -07002752 mIsSelectedSet = isBatch;
Mindy Pereira06642fa2012-07-12 16:23:27 -07002753 mShowUndo = showUndo;
Mindy Pereira01f30502012-08-14 10:30:51 -07002754 mAction = action;
Vikram Aggarwal41e6e712012-04-24 11:22:57 -07002755 }
2756
Vikram Aggarwal41e6e712012-04-24 11:22:57 -07002757 @Override
2758 public void performAction() {
Vikram Aggarwal7f602f72012-04-30 16:04:06 -07002759 if (isPerformed()) {
2760 return;
2761 }
Mindy Pereira06642fa2012-07-12 16:23:27 -07002762 if (mIsDestructive && mShowUndo) {
Mindy Pereirad33674992012-06-25 16:26:30 -07002763 ToastBarOperation undoOp = new ToastBarOperation(mTarget.size(),
mindypa59283a2012-09-11 17:49:06 -07002764 mAction, ToastBarOperation.UNDO, mIsSelectedSet);
Vikram Aggarwal41e6e712012-04-24 11:22:57 -07002765 onUndoAvailable(undoOp);
2766 }
Mindy Pereira8db7e402012-07-13 10:32:47 -07002767 // For each conversation, for each operation, add/ remove the
2768 // appropriate folders.
mindyp389f0b22012-08-29 11:12:54 -07002769 ArrayList<String> updatedTargetFolders = new ArrayList<String>(mTarget.size());
Mindy Pereira8db7e402012-07-13 10:32:47 -07002770 for (Conversation target : mTarget) {
2771 HashMap<Uri, Folder> targetFolders = Folder
Mindy Pereira68f83842012-07-27 09:43:31 -07002772 .hashMapForFolders(target.getRawFolders());
Mindy Pereira01f30502012-08-14 10:30:51 -07002773 if (mIsDestructive) {
2774 target.localDeleteOnUpdate = true;
2775 }
Mindy Pereira8db7e402012-07-13 10:32:47 -07002776 for (FolderOperation op : mFolderOps) {
2777 if (op.mAdd) {
2778 targetFolders.put(op.mFolder.uri, op.mFolder);
2779 } else {
2780 targetFolders.remove(op.mFolder.uri);
2781 }
2782 }
mindyp389f0b22012-08-29 11:12:54 -07002783 updatedTargetFolders.add(Folder.getSerializedFolderString(targetFolders.values()));
2784 }
2785 if (mConversationListCursor != null) {
2786 mConversationListCursor.updateStrings(mContext, mTarget,
2787 Conversation.UPDATE_FOLDER_COLUMN, updatedTargetFolders);
Mindy Pereira8db7e402012-07-13 10:32:47 -07002788 }
Vikram Aggarwal7f602f72012-04-30 16:04:06 -07002789 refreshConversationList();
Mindy Pereiraf3a45562012-05-24 16:30:19 -07002790 if (mIsSelectedSet) {
2791 mSelectedSet.clear();
2792 }
Vikram Aggarwal7f602f72012-04-30 16:04:06 -07002793 }
Mindy Pereira8db7e402012-07-13 10:32:47 -07002794
Vikram Aggarwal7f602f72012-04-30 16:04:06 -07002795 /**
2796 * Returns true if this action has been performed, false otherwise.
Andy Huang839ada22012-07-20 15:48:40 -07002797 *
Vikram Aggarwal7f602f72012-04-30 16:04:06 -07002798 */
2799 private synchronized boolean isPerformed() {
2800 if (mCompleted) {
2801 return true;
Vikram Aggarwal41e6e712012-04-24 11:22:57 -07002802 }
Vikram Aggarwal7f602f72012-04-30 16:04:06 -07002803 mCompleted = true;
2804 return false;
Vikram Aggarwal41e6e712012-04-24 11:22:57 -07002805 }
2806 }
Vikram Aggarwal7f602f72012-04-30 16:04:06 -07002807
mindypc84759c2012-08-29 09:51:53 -07002808 public final DestructiveAction getFolderChange(Collection<Conversation> target,
2809 Collection<FolderOperation> folders, boolean isDestructive, boolean isBatch,
2810 boolean showUndo) {
2811 final DestructiveAction da = getDeferredFolderChange(target, folders, isDestructive,
2812 isBatch, showUndo);
2813 registerDestructiveAction(da);
2814 return da;
2815 }
2816
2817 public final DestructiveAction getDeferredFolderChange(Collection<Conversation> target,
Mindy Pereira8db7e402012-07-13 10:32:47 -07002818 Collection<FolderOperation> folders, boolean isDestructive, boolean isBatch,
2819 boolean showUndo) {
Mindy Pereira06642fa2012-07-12 16:23:27 -07002820 final DestructiveAction da = new FolderDestruction(target, folders, isDestructive, isBatch,
Mindy Pereira01f30502012-08-14 10:30:51 -07002821 showUndo, R.id.change_folder);
Mindy Pereira01f30502012-08-14 10:30:51 -07002822 return da;
2823 }
2824
2825 @Override
2826 public final DestructiveAction getDeferredRemoveFolder(Collection<Conversation> target,
2827 Folder toRemove, boolean isDestructive, boolean isBatch,
2828 boolean showUndo) {
2829 Collection<FolderOperation> folderOps = new ArrayList<FolderOperation>();
2830 folderOps.add(new FolderOperation(toRemove, false));
2831 return new FolderDestruction(target, folderOps, isDestructive, isBatch,
2832 showUndo, R.id.remove_folder);
2833 }
2834
2835 private final DestructiveAction getRemoveFolder(Collection<Conversation> target,
mindypc84759c2012-08-29 09:51:53 -07002836 Folder toRemove, boolean isDestructive, boolean isBatch, boolean showUndo) {
2837 DestructiveAction da = getDeferredRemoveFolder(target, toRemove, isDestructive, isBatch,
2838 showUndo);
Vikram Aggarwal41e6e712012-04-24 11:22:57 -07002839 registerDestructiveAction(da);
2840 return da;
2841 }
Vikram Aggarwal75daee52012-04-30 11:13:09 -07002842
Vikram Aggarwal4f4782b2012-05-30 08:39:09 -07002843 @Override
2844 public final void refreshConversationList() {
Vikram Aggarwal75daee52012-04-30 11:13:09 -07002845 final ConversationListFragment convList = getConversationListFragment();
2846 if (convList == null) {
2847 return;
2848 }
2849 convList.requestListRefresh();
2850 }
Andrew Sappersteinc2c9dc12012-07-02 18:17:32 -07002851
2852 protected final ActionClickedListener getUndoClickedListener(
2853 final AnimatedAdapter listAdapter) {
2854 return new ActionClickedListener() {
2855 @Override
2856 public void onActionClicked() {
2857 if (mAccount.undoUri != null) {
2858 // NOTE: We might want undo to return the messages affected, in which case
2859 // the resulting cursor might be interesting...
2860 // TODO: Use UIProvider.SEQUENCE_QUERY_PARAMETER to indicate the set of
2861 // commands to undo
2862 if (mConversationListCursor != null) {
2863 mConversationListCursor.undo(
2864 mActivity.getActivityContext(), mAccount.undoUri);
2865 }
2866 if (listAdapter != null) {
2867 listAdapter.setUndo(true);
2868 }
2869 }
2870 }
2871 };
2872 }
2873
Vikram Aggarwal41b9e8f2012-09-25 10:15:04 -07002874 /**
2875 * Shows an error toast in the bottom when a folder was not fetched successfully.
2876 * @param folder the folder which could not be fetched.
2877 * @param replaceVisibleToast if true, this should replace any currently visible toast.
2878 */
Andrew Sapperstein9d7519d2012-07-16 14:03:53 -07002879 protected final void showErrorToast(final Folder folder, boolean replaceVisibleToast) {
Andrew Sappersteinc2c9dc12012-07-02 18:17:32 -07002880 mToastBar.setConversationMode(false);
Andrew Sapperstein00179f12012-08-09 15:15:40 -07002881
Vikram Aggarwal41b9e8f2012-09-25 10:15:04 -07002882 final ActionClickedListener listener;
2883 final int actionTextResourceId;
Andrew Sapperstein00179f12012-08-09 15:15:40 -07002884 final int lastSyncResult = folder.lastSyncResult;
Vikram Aggarwal41b9e8f2012-09-25 10:15:04 -07002885 switch (lastSyncResult & 0x0f) {
Andrew Sapperstein00179f12012-08-09 15:15:40 -07002886 case UIProvider.LastSyncResult.CONNECTION_ERROR:
Vikram Aggarwal41b9e8f2012-09-25 10:15:04 -07002887 // The sync request that caused this failure.
2888 final int syncRequest = lastSyncResult >> 4;
2889 // Show: User explicitly pressed the refresh button and there is no connection
2890 // Show: The first time the user enters the app and there is no connection
2891 // TODO(viki): Implement this.
2892 // Reference: http://b/7202801
2893 final boolean showToast = (syncRequest & UIProvider.SyncStatus.USER_REFRESH) != 0;
2894 // Don't show: Already in the app; user switches to a synced label
2895 // Don't show: In a live label and a background sync fails
2896 final boolean avoidToast = !showToast && (folder.syncWindow > 0
2897 || (syncRequest & UIProvider.SyncStatus.BACKGROUND_SYNC) != 0);
2898 if (avoidToast) {
2899 return;
2900 }
Andrew Sapperstein00179f12012-08-09 15:15:40 -07002901 listener = getRetryClickedListener(folder);
2902 actionTextResourceId = R.string.retry;
2903 break;
2904 case UIProvider.LastSyncResult.AUTH_ERROR:
2905 listener = getSignInClickedListener();
2906 actionTextResourceId = R.string.signin;
2907 break;
2908 case UIProvider.LastSyncResult.SECURITY_ERROR:
2909 return; // Currently we do nothing for security errors.
2910 case UIProvider.LastSyncResult.STORAGE_ERROR:
2911 listener = getStorageErrorClickedListener();
2912 actionTextResourceId = R.string.info;
2913 break;
2914 case UIProvider.LastSyncResult.INTERNAL_ERROR:
2915 listener = getInternalErrorClickedListener();
2916 actionTextResourceId = R.string.report;
2917 break;
2918 default:
2919 return;
2920 }
Vikram Aggarwal41b9e8f2012-09-25 10:15:04 -07002921 mToastBar.show(listener,
Andrew Sapperstein5d420962012-07-12 16:43:10 -07002922 R.drawable.ic_alert_white,
Vikram Aggarwal41b9e8f2012-09-25 10:15:04 -07002923 Utils.getSyncStatusText(mActivity.getActivityContext(), lastSyncResult),
Andrew Sappersteinc2c9dc12012-07-02 18:17:32 -07002924 false, /* showActionIcon */
Andrew Sapperstein00179f12012-08-09 15:15:40 -07002925 actionTextResourceId,
Andrew Sapperstein9d7519d2012-07-16 14:03:53 -07002926 replaceVisibleToast,
mindypa59283a2012-09-11 17:49:06 -07002927 new ToastBarOperation(1, 0, ToastBarOperation.ERROR, false));
Andrew Sappersteinc2c9dc12012-07-02 18:17:32 -07002928 }
2929
Andrew Sapperstein00179f12012-08-09 15:15:40 -07002930 private ActionClickedListener getRetryClickedListener(final Folder folder) {
Andrew Sappersteinc2c9dc12012-07-02 18:17:32 -07002931 return new ActionClickedListener() {
2932 @Override
2933 public void onActionClicked() {
2934 final Uri uri = folder.refreshUri;
2935
2936 if (uri != null) {
Paul Westbrook4969e0c2012-08-20 14:38:39 -07002937 startAsyncRefreshTask(uri);
Andrew Sappersteinc2c9dc12012-07-02 18:17:32 -07002938 }
2939 }
2940 };
2941 }
Andrew Sapperstein00179f12012-08-09 15:15:40 -07002942
2943 private ActionClickedListener getSignInClickedListener() {
2944 return new ActionClickedListener() {
2945 @Override
2946 public void onActionClicked() {
Paul Westbrook122f7c22012-08-20 17:50:31 -07002947 promptUserForAuthentication(mAccount);
Andrew Sapperstein00179f12012-08-09 15:15:40 -07002948 }
2949 };
2950 }
2951
2952 private ActionClickedListener getStorageErrorClickedListener() {
2953 return new ActionClickedListener() {
2954 @Override
2955 public void onActionClicked() {
Paul Westbrook4969e0c2012-08-20 14:38:39 -07002956 showStorageErrorDialog();
Andrew Sapperstein00179f12012-08-09 15:15:40 -07002957 }
2958 };
2959 }
2960
Paul Westbrook4969e0c2012-08-20 14:38:39 -07002961 private void showStorageErrorDialog() {
2962 DialogFragment fragment = (DialogFragment)
2963 mFragmentManager.findFragmentByTag(SYNC_ERROR_DIALOG_FRAGMENT_TAG);
2964 if (fragment == null) {
2965 fragment = SyncErrorDialogFragment.newInstance();
2966 }
2967 fragment.show(mFragmentManager, SYNC_ERROR_DIALOG_FRAGMENT_TAG);
2968 }
2969
Andrew Sapperstein00179f12012-08-09 15:15:40 -07002970 private ActionClickedListener getInternalErrorClickedListener() {
2971 return new ActionClickedListener() {
2972 @Override
2973 public void onActionClicked() {
Paul Westbrook17beb0b2012-08-20 13:34:37 -07002974 Utils.sendFeedback(
2975 mActivity.getActivityContext(), mAccount, true /* reportingProblem */);
Andrew Sapperstein00179f12012-08-09 15:15:40 -07002976 }
2977 };
2978 }
Paul Westbrook4969e0c2012-08-20 14:38:39 -07002979
2980 @Override
2981 public void onFooterViewErrorActionClick(Folder folder, int errorStatus) {
2982 Uri uri = null;
2983 switch (errorStatus) {
2984 case UIProvider.LastSyncResult.CONNECTION_ERROR:
2985 if (folder != null && folder.refreshUri != null) {
2986 uri = folder.refreshUri;
2987 }
2988 break;
2989 case UIProvider.LastSyncResult.AUTH_ERROR:
Paul Westbrook122f7c22012-08-20 17:50:31 -07002990 promptUserForAuthentication(mAccount);
Paul Westbrook4969e0c2012-08-20 14:38:39 -07002991 return;
2992 case UIProvider.LastSyncResult.SECURITY_ERROR:
2993 return; // Currently we do nothing for security errors.
2994 case UIProvider.LastSyncResult.STORAGE_ERROR:
2995 showStorageErrorDialog();
2996 return;
2997 case UIProvider.LastSyncResult.INTERNAL_ERROR:
2998 Utils.sendFeedback(
2999 mActivity.getActivityContext(), mAccount, true /* reportingProblem */);
3000 return;
3001 default:
3002 return;
3003 }
3004
3005 if (uri != null) {
3006 startAsyncRefreshTask(uri);
3007 }
3008 }
3009
3010 @Override
3011 public void onFooterViewLoadMoreClick(Folder folder) {
3012 if (folder != null && folder.loadMoreUri != null) {
3013 startAsyncRefreshTask(folder.loadMoreUri);
3014 }
3015 }
3016
3017 private void startAsyncRefreshTask(Uri uri) {
3018 if (mFolderSyncTask != null) {
3019 mFolderSyncTask.cancel(true);
3020 }
3021 mFolderSyncTask = new AsyncRefreshTask(mActivity.getActivityContext(), uri);
3022 mFolderSyncTask.execute();
3023 }
Paul Westbrook122f7c22012-08-20 17:50:31 -07003024
3025 private void promptUserForAuthentication(Account account) {
Paul Westbrook429399b2012-08-24 11:19:17 -07003026 if (account != null && !Utils.isEmpty(account.reauthenticationIntentUri)) {
Paul Westbrook122f7c22012-08-20 17:50:31 -07003027 final Intent authenticationIntent =
3028 new Intent(Intent.ACTION_VIEW, account.reauthenticationIntentUri);
3029 mActivity.startActivityForResult(authenticationIntent, REAUTHENTICATE_REQUEST_CODE);
3030 }
3031 }
mindypca87de42012-09-28 15:02:39 -07003032
3033 @Override
3034 public void onAccessibilityStateChanged() {
3035 // Clear the cache of objects.
3036 ConversationItemViewModel.onAccessibilityUpdated();
3037 // Re-render the list if it exists.
3038 ConversationListFragment frag = getConversationListFragment();
3039 if (frag != null) {
3040 AnimatedAdapter adapter = frag.getAnimatedAdapter();
3041 if (adapter != null) {
3042 adapter.notifyDataSetInvalidated();
3043 }
3044 }
3045 }
Vikram Aggarwal4a5c5302012-01-12 15:07:13 -08003046}