blob: 1e3886bde3dd76033d005d58d37e3129cdb75b77 [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 Aggarwal6cadbfc2012-12-27 09:17:05 -080036import android.content.DialogInterface.OnClickListener;
Vikram Aggarwala55b36c2012-01-13 11:45:02 -080037import android.content.Intent;
Vikram Aggarwal7dedb952012-02-16 16:10:23 -080038import android.content.Loader;
Vikram Aggarwalbcb16b92013-01-28 18:05:03 -080039import android.content.res.Resources;
Vikram Aggarwal80aeac52012-02-07 15:27:20 -080040import android.database.Cursor;
Andy Huang632721e2012-04-11 16:57:26 -070041import android.database.DataSetObservable;
42import android.database.DataSetObserver;
Paul Westbrook23b74b92012-02-29 11:36:12 -080043import android.net.Uri;
Vikram Aggarwal27d89ad2012-06-12 13:38:40 -070044import android.os.AsyncTask;
Vikram Aggarwala55b36c2012-01-13 11:45:02 -080045import android.os.Bundle;
Mindy Pereira21ab4902012-03-19 18:48:03 -070046import android.os.Handler;
Vikram Aggarwale620a7a2012-03-28 13:16:14 -070047import android.provider.SearchRecentSuggestions;
Vikram Aggarwal649b9ea2012-08-27 12:15:20 -070048import android.text.TextUtils;
Mindy Pereiraacf60392012-04-06 09:11:00 -070049import android.view.DragEvent;
Vikram Aggarwala55b36c2012-01-13 11:45:02 -080050import android.view.KeyEvent;
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -080051import android.view.LayoutInflater;
Vikram Aggarwala55b36c2012-01-13 11:45:02 -080052import android.view.Menu;
Mindy Pereira28d5f722012-02-15 12:32:40 -080053import android.view.MenuInflater;
Vikram Aggarwala55b36c2012-01-13 11:45:02 -080054import android.view.MenuItem;
55import android.view.MotionEvent;
Mindy Pereirad33674992012-06-25 16:26:30 -070056import android.view.View;
Mindy Pereirafd0c2972012-03-27 13:50:39 -070057import android.widget.Toast;
Vikram Aggarwala55b36c2012-01-13 11:45:02 -080058
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -080059import com.android.mail.ConversationListContext;
Andy Huangf9a73482012-03-13 15:54:02 -070060import com.android.mail.R;
Vikram Aggarwal6cadbfc2012-12-27 09:17:05 -080061import com.android.mail.browse.ConfirmDialogFragment;
Mindy Pereira967ede62012-03-22 09:29:09 -070062import com.android.mail.browse.ConversationCursor;
Yu Ping Hu7c909c72013-01-18 11:58:01 -080063import com.android.mail.browse.ConversationCursor.ConversationOperation;
mindypca87de42012-09-28 15:02:39 -070064import com.android.mail.browse.ConversationItemViewModel;
Paul Westbrookbf232c32012-04-18 03:17:41 -070065import com.android.mail.browse.ConversationPagerController;
Andy Huang839ada22012-07-20 15:48:40 -070066import com.android.mail.browse.MessageCursor.ConversationMessage;
Marc Blank7c9f6ac2012-04-02 13:27:19 -070067import com.android.mail.browse.SelectedConversationsActionMenu;
Andy Huang991f4532012-08-14 13:32:55 -070068import com.android.mail.browse.SyncErrorDialogFragment;
Mindy Pereira9b875682012-02-15 18:10:54 -080069import com.android.mail.compose.ComposeActivity;
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -080070import com.android.mail.providers.Account;
Mindy Pereira9b875682012-02-15 18:10:54 -080071import com.android.mail.providers.Conversation;
Andy Huang839ada22012-07-20 15:48:40 -070072import com.android.mail.providers.ConversationInfo;
Vikram Aggarwal4a5c5302012-01-12 15:07:13 -080073import com.android.mail.providers.Folder;
Vikram Aggarwal7a5d95a2012-07-27 16:24:54 -070074import com.android.mail.providers.FolderWatcher;
Paul Westbrookc2074c42012-03-22 15:26:58 -070075import com.android.mail.providers.MailAppProvider;
Mindy Pereiradac00542012-03-01 10:50:33 -080076import com.android.mail.providers.Settings;
Vikram Aggarwale620a7a2012-03-28 13:16:14 -070077import com.android.mail.providers.SuggestionsProvider;
Vikram Aggarwal80aeac52012-02-07 15:27:20 -080078import com.android.mail.providers.UIProvider;
Mindy Pereira3cb28b52012-05-24 15:26:39 -070079import com.android.mail.providers.UIProvider.AccountCapabilities;
Scott Kennedy0d0f8b02012-10-12 15:18:18 -070080import com.android.mail.providers.UIProvider.AccountColumns;
Paul Westbrook2388c5d2012-03-25 12:29:11 -070081import com.android.mail.providers.UIProvider.AccountCursorExtraKeys;
Scott Kennedy0d0f8b02012-10-12 15:18:18 -070082import com.android.mail.providers.UIProvider.AutoAdvance;
Mindy Pereirac9d59182012-03-22 16:06:46 -070083import com.android.mail.providers.UIProvider.ConversationColumns;
Paul Westbrook5109c512012-11-05 11:00:30 -080084import com.android.mail.providers.UIProvider.ConversationOperations;
Vikram Aggarwal54452ae2012-03-13 15:29:00 -070085import com.android.mail.providers.UIProvider.FolderCapabilities;
Andrew Sappersteinc2c9dc12012-07-02 18:17:32 -070086import com.android.mail.ui.ActionableToastBar.ActionClickedListener;
Andy Huang839ada22012-07-20 15:48:40 -070087import com.android.mail.utils.ContentProviderTask;
Paul Westbrookb334c902012-06-25 11:42:46 -070088import com.android.mail.utils.LogTag;
Mindy Pereira5cb0c3e2012-02-22 15:22:47 -080089import com.android.mail.utils.LogUtils;
Vikram Aggarwalfa131a22012-02-02 13:56:22 -080090import com.android.mail.utils.Utils;
Vikram Aggarwal69a6cdf2013-01-08 16:05:17 -080091import com.android.mail.utils.VeiledAddressMatcher;
Scott Kennedy0d0f8b02012-10-12 15:18:18 -070092
Paul Westbrookca08fc12012-07-31 12:01:15 -070093import com.google.common.base.Objects;
Paul Westbrook77eee622012-07-10 13:41:57 -070094import com.google.common.collect.ImmutableList;
Mindy Pereiraacf60392012-04-06 09:11:00 -070095import com.google.common.collect.Lists;
Marc Blank167faa82012-03-21 13:11:53 -070096import com.google.common.collect.Sets;
Vikram Aggarwal4a5c5302012-01-12 15:07:13 -080097
Marc Blank167faa82012-03-21 13:11:53 -070098import java.util.ArrayList;
Mindy Pereirafbe40192012-03-20 10:40:45 -070099import java.util.Collection;
Andy Huang839ada22012-07-20 15:48:40 -0700100import java.util.Collections;
Mindy Pereira8db7e402012-07-13 10:32:47 -0700101import java.util.HashMap;
Vikram Aggarwalcc754b12012-08-30 14:04:21 -0700102import java.util.List;
Paul Westbrook23b74b92012-02-29 11:36:12 -0800103import java.util.Set;
Marc Blankbf128eb2012-04-18 15:58:45 -0700104import java.util.TimerTask;
Paul Westbrook23b74b92012-02-29 11:36:12 -0800105
106
Vikram Aggarwal4a5c5302012-01-12 15:07:13 -0800107/**
Mindy Pereira161f50d2012-02-28 15:47:19 -0800108 * This is an abstract implementation of the Activity Controller. This class
109 * knows how to respond to menu items, state changes, layout changes, etc. It
110 * weaves together the views and listeners, dispatching actions to the
111 * respective underlying classes.
Vikram Aggarwala55b36c2012-01-13 11:45:02 -0800112 * <p>
Mindy Pereira161f50d2012-02-28 15:47:19 -0800113 * Even though this class is abstract, it should provide default implementations
114 * for most, if not all the methods in the ActivityController interface. This
115 * makes the task of the subclasses easier: OnePaneActivityController and
116 * TwoPaneActivityController can be concise when the common functionality is in
117 * AbstractActivityController.
118 * </p>
119 * <p>
120 * In the Gmail codebase, this was called BaseActivityController
121 * </p>
Vikram Aggarwal4a5c5302012-01-12 15:07:13 -0800122 */
Vikram Aggarwal531488e2012-05-29 16:36:52 -0700123public abstract class AbstractActivityController implements ActivityController {
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -0800124 // Keys for serialization of various information in Bundles.
Vikram Aggarwalcabd3f22012-04-19 10:14:41 -0700125 /** Tag for {@link #mAccount} */
Vikram Aggarwal6c511582012-02-27 10:59:47 -0800126 private static final String SAVED_ACCOUNT = "saved-account";
Vikram Aggarwalcabd3f22012-04-19 10:14:41 -0700127 /** Tag for {@link #mFolder} */
Mindy Pereira5e478d22012-03-26 18:04:58 -0700128 private static final String SAVED_FOLDER = "saved-folder";
Vikram Aggarwalcabd3f22012-04-19 10:14:41 -0700129 /** Tag for {@link #mCurrentConversation} */
Mindy Pereira26f23fc2012-03-27 10:26:04 -0700130 private static final String SAVED_CONVERSATION = "saved-conversation";
Vikram Aggarwalcabd3f22012-04-19 10:14:41 -0700131 /** Tag for {@link #mSelectedSet} */
132 private static final String SAVED_SELECTED_SET = "saved-selected-set";
Vikram Aggarwal649b9ea2012-08-27 12:15:20 -0700133 /** Tag for {@link ActionableToastBar#getOperation()} */
Mindy Pereirad33674992012-06-25 16:26:30 -0700134 private static final String SAVED_TOAST_BAR_OP = "saved-toast-bar-op";
Vikram Aggarwal649b9ea2012-08-27 12:15:20 -0700135 /** Tag for {@link #mFolderListFolder} */
136 private static final String SAVED_HIERARCHICAL_FOLDER = "saved-hierarchical-folder";
Vikram Aggarwalf3341402012-08-07 10:09:38 -0700137 /** Tag for {@link ConversationListContext#searchQuery} */
138 private static final String SAVED_QUERY = "saved-query";
Vikram Aggarwal6cadbfc2012-12-27 09:17:05 -0800139 /** Tag for {@link #mDialogAction} */
140 private static final String SAVED_ACTION = "saved-action";
Vikram Aggarwalb8c31712013-01-03 17:03:19 -0800141 /** Tag for {@link #mDialogFromSelectedSet} */
142 private static final String SAVED_ACTION_FROM_SELECTED = "saved-action-from-selected";
Vikram Aggarwala91d00b2013-01-18 12:00:37 -0800143 /** Tag for {@link #mDetachedConvUri} */
144 private static final String SAVED_DETACHED_CONV_URI = "saved-detached-conv-uri";
Mindy Pereira5cb0c3e2012-02-22 15:22:47 -0800145
Vikram Aggarwal6902dcf2012-04-11 08:57:42 -0700146 /** Tag used when loading a wait fragment */
147 protected static final String TAG_WAIT = "wait-fragment";
148 /** Tag used when loading a conversation list fragment. */
Paul Westbrookbf232c32012-04-18 03:17:41 -0700149 public static final String TAG_CONVERSATION_LIST = "tag-conversation-list";
Vikram Aggarwal6902dcf2012-04-11 08:57:42 -0700150 /** Tag used when loading a folder list fragment. */
151 protected static final String TAG_FOLDER_LIST = "tag-folder-list";
152
Vikram Aggarwald7a12cd2012-02-03 09:36:20 -0800153 protected Account mAccount;
Mindy Pereira12a4d802012-06-22 09:24:43 -0700154 protected Folder mFolder;
Vikram Aggarwal69b5c302012-09-05 11:11:13 -0700155 /** True when {@link #mFolder} is first shown to the user. */
156 private boolean mFolderChanged = false;
Andy Huang6681e542012-06-14 14:36:45 -0700157 protected MailActionBarView mActionBarView;
Vikram Aggarwal7c401b72012-08-13 16:43:47 -0700158 protected final ControllableActivity mActivity;
Vikram Aggarwala55b36c2012-01-13 11:45:02 -0800159 protected final Context mContext;
Vikram Aggarwal6902dcf2012-04-11 08:57:42 -0700160 private final FragmentManager mFragmentManager;
Vikram Aggarwalec5cbf72012-03-08 15:10:35 -0800161 protected final RecentFolderList mRecentFolderList;
Vikram Aggarwal80aeac52012-02-07 15:27:20 -0800162 protected ConversationListContext mConvListContext;
Mindy Pereira9b875682012-02-15 18:10:54 -0800163 protected Conversation mCurrentConversation;
Vikram Aggarwala91d00b2013-01-18 12:00:37 -0800164 /**
165 * The hash of {@link #mCurrentConversation} in detached mode. 0 if we are not in detached mode.
166 */
167 private Uri mDetachedConvUri;
Vikram Aggarwald7a12cd2012-02-03 09:36:20 -0800168
Paul Westbrook6ead20d2012-03-19 14:48:14 -0700169 /** A {@link android.content.BroadcastReceiver} that suppresses new e-mail notifications. */
170 private SuppressNotificationReceiver mNewEmailReceiver = null;
171
Mindy Pereirafbe40192012-03-20 10:40:45 -0700172 protected Handler mHandler = new Handler();
Mindy Pereirafa995b42012-07-25 12:06:13 -0700173
Vikram Aggarwalfa131a22012-02-02 13:56:22 -0800174 /**
Mindy Pereira161f50d2012-02-28 15:47:19 -0800175 * The current mode of the application. All changes in mode are initiated by
176 * the activity controller. View mode changes are propagated to classes that
177 * attach themselves as listeners of view mode changes.
Vikram Aggarwalfa131a22012-02-02 13:56:22 -0800178 */
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -0800179 protected final ViewMode mViewMode;
Vikram Aggarwal80aeac52012-02-07 15:27:20 -0800180 protected ContentResolver mResolver;
Vikram Aggarwal7dedb952012-02-16 16:10:23 -0800181 protected boolean isLoaderInitialized = false;
Mindy Pereirab7b33e02012-02-21 15:32:19 -0800182 private AsyncRefreshTask mAsyncRefreshTask;
Mindy Pereira5cb0c3e2012-02-22 15:22:47 -0800183
Andy Huang4e0158f2012-08-07 21:06:01 -0700184 private boolean mDestroyed;
185
Vikram Aggarwalbcb16b92013-01-28 18:05:03 -0800186 /** True if running on tablet */
187 private final boolean mIsTablet;
188
Andy Huang1ee96b22012-08-24 20:19:53 -0700189 /**
190 * Are we in a point in the Activity/Fragment lifecycle where it's safe to execute fragment
191 * transactions? (including back stack manipulation)
192 * <p>
193 * Per docs in {@link FragmentManager#beginTransaction()}, this flag starts out true, switches
194 * to false after {@link Activity#onSaveInstanceState}, and becomes true again in both onStart
195 * and onResume.
196 */
197 private boolean mSafeToModifyFragments = true;
198
Paul Westbrook23b74b92012-02-29 11:36:12 -0800199 private final Set<Uri> mCurrentAccountUris = Sets.newHashSet();
Mindy Pereira967ede62012-03-22 09:29:09 -0700200 protected ConversationCursor mConversationListCursor;
Andy Huang632721e2012-04-11 16:57:26 -0700201 private final DataSetObservable mConversationListObservable = new DataSetObservable() {
202 @Override
203 public void registerObserver(DataSetObserver observer) {
204 final int count = mObservers.size();
205 super.registerObserver(observer);
Vikram Aggarwal7c401b72012-08-13 16:43:47 -0700206 LogUtils.d(LOG_TAG, "IN AAC.register(List)Observer: %s before=%d after=%d", observer,
Andy Huang632721e2012-04-11 16:57:26 -0700207 count, mObservers.size());
208 }
209 @Override
210 public void unregisterObserver(DataSetObserver observer) {
211 final int count = mObservers.size();
212 super.unregisterObserver(observer);
Vikram Aggarwal7c401b72012-08-13 16:43:47 -0700213 LogUtils.d(LOG_TAG, "IN AAC.unregister(List)Observer: %s before=%d after=%d", observer,
Andy Huang632721e2012-04-11 16:57:26 -0700214 count, mObservers.size());
215 }
216 };
Marc Blankbf128eb2012-04-18 15:58:45 -0700217
Yu Ping Hu7c909c72013-01-18 11:58:01 -0800218 /**
219 * Interface for actions that are deferred until after a load completes. This is for handling
220 * user actions which affect cursors (e.g. marking messages read or unread) that happen before
221 * that cursor is loaded.
222 */
223 private interface LoadFinishedCallback {
224 void onLoadFinished();
225 }
226
227 /** The deferred actions to execute when mConversationListCursor load completes. */
228 private final ArrayList<LoadFinishedCallback> mConversationListLoadFinishedCallbacks =
229 new ArrayList<LoadFinishedCallback>();
230
Marc Blankbf128eb2012-04-18 15:58:45 -0700231 private RefreshTimerTask mConversationListRefreshTask;
Marc Blanke1d1b072012-04-13 17:29:16 -0700232
Vikram Aggarwal7c401b72012-08-13 16:43:47 -0700233 /** Listeners that are interested in changes to the current account. */
234 private final DataSetObservable mAccountObservers = new DataSetObservable() {
235 @Override
236 public void registerObserver(DataSetObserver observer) {
237 final int count = mObservers.size();
238 super.registerObserver(observer);
239 LogUtils.d(LOG_TAG, "IN AAC.register(Account)Observer: %s before=%d after=%d",
240 observer, count, mObservers.size());
241 }
242 @Override
243 public void unregisterObserver(DataSetObserver observer) {
244 final int count = mObservers.size();
245 super.unregisterObserver(observer);
246 LogUtils.d(LOG_TAG, "IN AAC.unregister(Account)Observer: %s before=%d after=%d",
247 observer, count, mObservers.size());
248 }
249 };
Vikram Aggarwal7d816002012-04-17 17:06:41 -0700250
Vikram Aggarwal58cad2e2012-08-28 16:18:23 -0700251 /** Listeners that are interested in changes to the recent folders. */
252 private final DataSetObservable mRecentFolderObservers = new DataSetObservable() {
253 @Override
254 public void registerObserver(DataSetObserver observer) {
255 final int count = mObservers.size();
256 super.registerObserver(observer);
257 LogUtils.d(LOG_TAG, "IN AAC.register(RecentFolder)Observer: %s before=%d after=%d",
258 observer, count, mObservers.size());
259 }
260 @Override
261 public void unregisterObserver(DataSetObserver observer) {
262 final int count = mObservers.size();
263 super.unregisterObserver(observer);
264 LogUtils.d(LOG_TAG, "IN AAC.unregister(RecentFolder)Observer: %s before=%d after=%d",
265 observer, count, mObservers.size());
266 }
267 };
268
Mindy Pereira967ede62012-03-22 09:29:09 -0700269 /**
270 * Selected conversations, if any.
271 */
Andy Huang4556a442012-03-30 16:42:05 -0700272 private final ConversationSelectionSet mSelectedSet = new ConversationSelectionSet();
Mindy Pereiradac00542012-03-01 10:50:33 -0800273
Paul Westbrookc7a070f2012-04-12 01:46:41 -0700274 private final int mFolderItemUpdateDelayMs;
275
Vikram Aggarwal135fd022012-04-11 14:44:15 -0700276 /** Keeps track of selected and unselected conversations */
Paul Westbrook937c94f2012-08-16 13:01:18 -0700277 final protected ConversationPositionTracker mTracker;
Vikram Aggarwalaf1ee0c2012-04-12 17:13:13 -0700278
Vikram Aggarwale128fc22012-04-04 12:33:34 -0700279 /**
280 * Action menu associated with the selected set.
281 */
282 SelectedConversationsActionMenu mCabActionMenu;
Andrew Sappersteinc2c9dc12012-07-02 18:17:32 -0700283 protected ActionableToastBar mToastBar;
Andy Huang632721e2012-04-11 16:57:26 -0700284 protected ConversationPagerController mPagerController;
Mindy Pereiraab486362012-03-21 18:18:53 -0700285
Andy Huangb1c34dc2012-04-17 16:36:19 -0700286 // this is split out from the general loader dispatcher because its loader doesn't return a
287 // basic Cursor
288 private final ConversationListLoaderCallbacks mListCursorCallbacks =
289 new ConversationListLoaderCallbacks();
290
Andy Huang090db1e2012-07-25 13:25:28 -0700291 private final DataSetObservable mFolderObservable = new DataSetObservable();
292
Vikram Aggarwal69a6cdf2013-01-08 16:05:17 -0800293 /**
294 * Matched addresses that must be shielded from users because they are temporary. Even though
295 * this is instantiated from settings, this matcher is valid for all accounts, and is expected
296 * to live past the life of an account.
297 */
298 private final VeiledAddressMatcher mVeiledMatcher;
299
Paul Westbrookb334c902012-06-25 11:42:46 -0700300 protected static final String LOG_TAG = LogTag.getLogTag();
Vikram Aggarwalfa4b47e2012-03-09 13:02:46 -0800301 /** Constants used to differentiate between the types of loaders. */
302 private static final int LOADER_ACCOUNT_CURSOR = 0;
Vikram Aggarwalfa4b47e2012-03-09 13:02:46 -0800303 private static final int LOADER_FOLDER_CURSOR = 2;
304 private static final int LOADER_RECENT_FOLDERS = 3;
Mindy Pereira967ede62012-03-22 09:29:09 -0700305 private static final int LOADER_CONVERSATION_LIST = 4;
Mindy Pereiraab486362012-03-21 18:18:53 -0700306 private static final int LOADER_ACCOUNT_INBOX = 5;
307 private static final int LOADER_SEARCH = 6;
Paul Westbrook2d50bcd2012-04-10 11:53:47 -0700308 private static final int LOADER_ACCOUNT_UPDATE_CURSOR = 7;
Vikram Aggarwal7a5d95a2012-07-27 16:24:54 -0700309 /**
310 * Guaranteed to be the last loader ID used by the activity. Loaders are owned by Activity or
311 * fragments, and within an activity, loader IDs need to be unique. A hack to ensure that the
312 * {@link FolderWatcher} can create its folder loaders without clashing with the IDs of those
313 * of the {@link AbstractActivityController}. Currently, the {@link FolderWatcher} is the only
314 * other class that uses this activity's LoaderManager. If another class needs activity-level
315 * loaders, consider consolidating the loaders in a central location: a UI-less fragment
316 * perhaps.
317 */
318 public static final int LAST_LOADER_ID = 100;
Vikram Aggarwala55b36c2012-01-13 11:45:02 -0800319
Paul Westbrook2388c5d2012-03-25 12:29:11 -0700320 private static final int ADD_ACCOUNT_REQUEST_CODE = 1;
Paul Westbrook122f7c22012-08-20 17:50:31 -0700321 private static final int REAUTHENTICATE_REQUEST_CODE = 2;
Paul Westbrook2388c5d2012-03-25 12:29:11 -0700322
Vikram Aggarwale8a85322012-04-24 09:01:38 -0700323 /** The pending destructive action to be carried out before swapping the conversation cursor.*/
324 private DestructiveAction mPendingDestruction;
Andrew Sappersteinc2c9dc12012-07-02 18:17:32 -0700325 protected AsyncRefreshTask mFolderSyncTask;
Mindy Pereira49e5dbe2012-07-12 11:47:54 -0700326 // Task for setting any share intents for the account to enabled.
327 // This gets cancelled if the user kills the app before it finishes, and
328 // will just run the next time the user opens the app.
329 private AsyncTask<String, Void, Void> mEnableShareIntents;
Mindy Pereirac975e842012-07-16 09:15:00 -0700330 private Folder mFolderListFolder;
mindyp5390fca2012-08-22 12:12:25 -0700331 private boolean mIsDragHappening;
mindypead50392012-08-23 11:03:53 -0700332 private int mShowUndoBarDelay;
mindyp6f54e1b2012-10-09 09:54:08 -0700333 private boolean mRecentsDataUpdated;
Vikram Aggarwala3f43d42012-10-25 16:21:30 -0700334 /** A wait fragment we added, if any. */
335 private WaitFragment mWaitFragment;
Vikram Aggarwalf0ef2302012-11-07 14:53:34 -0800336 /** True if we have results from a search query */
337 private boolean mHaveSearchResults = false;
Vikram Aggarwal6cadbfc2012-12-27 09:17:05 -0800338 /** If a confirmation dialog is being show, the listener for the positive action. */
339 private OnClickListener mDialogListener;
340 /**
341 * If a confirmation dialog is being show, the resource of the action: R.id.delete, etc. This
342 * is used to create a new {@link #mDialogListener} on orientation changes.
343 */
344 private int mDialogAction = -1;
Vikram Aggarwalb8c31712013-01-03 17:03:19 -0800345 /**
346 * If a confirmation dialog is being shown, this is true if the dialog acts on the selected set
347 * and false if it acts on the currently selected conversation
348 */
349 private boolean mDialogFromSelectedSet;
Vikram Aggarwal6cadbfc2012-12-27 09:17:05 -0800350
Andrew Sapperstein00179f12012-08-09 15:15:40 -0700351 public static final String SYNC_ERROR_DIALOG_FRAGMENT_TAG = "SyncErrorDialogFragment";
Vikram Aggarwale8a85322012-04-24 09:01:38 -0700352
Vikram Aggarwala55b36c2012-01-13 11:45:02 -0800353 public AbstractActivityController(MailActivity activity, ViewMode viewMode) {
354 mActivity = activity;
Vikram Aggarwal6902dcf2012-04-11 08:57:42 -0700355 mFragmentManager = mActivity.getFragmentManager();
Vikram Aggarwala55b36c2012-01-13 11:45:02 -0800356 mViewMode = viewMode;
357 mContext = activity.getApplicationContext();
Vikram Aggarwal025eba82012-05-08 10:45:30 -0700358 mRecentFolderList = new RecentFolderList(mContext);
Paul Westbrook937c94f2012-08-16 13:01:18 -0700359 mTracker = new ConversationPositionTracker(this);
Mindy Pereira967ede62012-03-22 09:29:09 -0700360 // Allow the fragment to observe changes to its own selection set. No other object is
361 // aware of the selected set.
362 mSelectedSet.addObserver(this);
Paul Westbrookc7a070f2012-04-12 01:46:41 -0700363
Vikram Aggarwalbcb16b92013-01-28 18:05:03 -0800364 final Resources r = mContext.getResources();
365 mFolderItemUpdateDelayMs = r.getInteger(R.integer.folder_item_refresh_delay_ms);
366 mShowUndoBarDelay = r.getInteger(R.integer.show_undo_bar_delay_ms);
Vikram Aggarwal69a6cdf2013-01-08 16:05:17 -0800367 mVeiledMatcher = VeiledAddressMatcher.newInstance(activity.getResources());
Vikram Aggarwalbcb16b92013-01-28 18:05:03 -0800368 mIsTablet = Utils.useTabletUI(r);
Vikram Aggarwala55b36c2012-01-13 11:45:02 -0800369 }
Vikram Aggarwal4a5c5302012-01-12 15:07:13 -0800370
371 @Override
Vikram Aggarwale9a81032012-02-22 13:15:35 -0800372 public Account getCurrentAccount() {
373 return mAccount;
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -0800374 }
375
376 @Override
377 public ConversationListContext getCurrentListContext() {
Vikram Aggarwale9a81032012-02-22 13:15:35 -0800378 return mConvListContext;
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -0800379 }
380
381 @Override
Vikram Aggarwal4a5c5302012-01-12 15:07:13 -0800382 public String getHelpContext() {
Paul Westbrook30745b62012-08-19 14:10:32 -0700383 final int mode = mViewMode.getMode();
384 final int helpContextResId;
385 switch (mode) {
386 case ViewMode.WAITING_FOR_ACCOUNT_INITIALIZATION:
387 helpContextResId = R.string.wait_help_context;
388 break;
389 default:
390 helpContextResId = R.string.main_help_context;
391 }
392 return mContext.getString(helpContextResId);
Vikram Aggarwal4a5c5302012-01-12 15:07:13 -0800393 }
394
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -0800395 @Override
Vikram Aggarwalef7c9922012-04-23 12:35:04 -0700396 public final ConversationCursor getConversationListCursor() {
Mindy Pereira967ede62012-03-22 09:29:09 -0700397 return mConversationListCursor;
398 }
399
Vikram Aggarwal6902dcf2012-04-11 08:57:42 -0700400 /**
Vikram Aggarwal34a65012012-04-17 12:39:06 -0700401 * Check if the fragment is attached to an activity and has a root view.
402 * @param in
403 * @return true if the fragment is valid, false otherwise
404 */
405 private static final boolean isValidFragment(Fragment in) {
406 if (in == null || in.getActivity() == null || in.getView() == null) {
407 return false;
408 }
409 return true;
410 }
411
412 /**
Vikram Aggarwal49e0e992012-09-21 13:53:15 -0700413 * Get the conversation list fragment for this activity. If the conversation list fragment is
414 * not attached, this method returns null.
Andy Huang839ada22012-07-20 15:48:40 -0700415 *
Vikram Aggarwal49e0e992012-09-21 13:53:15 -0700416 * Caution! This method returns the {@link ConversationListFragment} after the fragment has been
417 * added, <b>and</b> after the {@link FragmentManager} has run through its queue to add the
418 * fragment. There is a non-trivial amount of time after the fragment is instantiated and before
419 * this call returns a non-null value, depending on the {@link FragmentManager}. If you
420 * need the fragment immediately after adding it, consider making the fragment an observer of
421 * the controller and perform the task immediately on {@link Fragment#onActivityCreated(Bundle)}
Vikram Aggarwal6902dcf2012-04-11 08:57:42 -0700422 */
423 protected ConversationListFragment getConversationListFragment() {
424 final Fragment fragment = mFragmentManager.findFragmentByTag(TAG_CONVERSATION_LIST);
Vikram Aggarwal34a65012012-04-17 12:39:06 -0700425 if (isValidFragment(fragment)) {
426 return (ConversationListFragment) fragment;
Andy Huang9585d782012-04-16 19:45:04 -0700427 }
Vikram Aggarwal34a65012012-04-17 12:39:06 -0700428 return null;
Vikram Aggarwal6902dcf2012-04-11 08:57:42 -0700429 }
430
431 /**
Vikram Aggarwal6902dcf2012-04-11 08:57:42 -0700432 * Returns the folder list fragment attached with this activity. If no such fragment is attached
433 * this method returns null.
Andy Huang839ada22012-07-20 15:48:40 -0700434 *
Vikram Aggarwal49e0e992012-09-21 13:53:15 -0700435 * Caution! This method returns the {@link FolderListFragment} after the fragment has been
436 * added, <b>and</b> after the {@link FragmentManager} has run through its queue to add the
437 * fragment. There is a non-trivial amount of time after the fragment is instantiated and before
438 * this call returns a non-null value, depending on the {@link FragmentManager}. If you
439 * need the fragment immediately after adding it, consider making the fragment an observer of
440 * the controller and perform the task immediately on {@link Fragment#onActivityCreated(Bundle)}
Vikram Aggarwal6902dcf2012-04-11 08:57:42 -0700441 */
442 protected FolderListFragment getFolderListFragment() {
443 final Fragment fragment = mFragmentManager.findFragmentByTag(TAG_FOLDER_LIST);
Vikram Aggarwal34a65012012-04-17 12:39:06 -0700444 if (isValidFragment(fragment)) {
445 return (FolderListFragment) fragment;
Andy Huang9585d782012-04-16 19:45:04 -0700446 }
Vikram Aggarwal34a65012012-04-17 12:39:06 -0700447 return null;
Vikram Aggarwal6902dcf2012-04-11 08:57:42 -0700448 }
449
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -0800450 /**
Mindy Pereira161f50d2012-02-28 15:47:19 -0800451 * Initialize the action bar. This is not visible to OnePaneController and
452 * TwoPaneController so they cannot override this behavior.
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -0800453 */
Vikram Aggarwalabd24d82012-04-26 13:23:14 -0700454 private void initializeActionBar() {
455 final ActionBar actionBar = mActivity.getActionBar();
Andy Huang5895f7b2012-06-01 17:07:20 -0700456 if (actionBar == null) {
457 return;
Vikram Aggarwalabd24d82012-04-26 13:23:14 -0700458 }
Andy Huang5895f7b2012-06-01 17:07:20 -0700459
460 // be sure to inherit from the ActionBar theme when inflating
461 final LayoutInflater inflater = LayoutInflater.from(actionBar.getThemedContext());
Mindy Pereira82faec72012-06-14 17:21:50 -0700462 final boolean isSearch = mActivity.getIntent() != null
463 && Intent.ACTION_SEARCH.equals(mActivity.getIntent().getAction());
464 mActionBarView = (MailActionBarView) inflater.inflate(
465 isSearch ? R.layout.search_actionbar_view : R.layout.actionbar_view, null);
Andy Huang5895f7b2012-06-01 17:07:20 -0700466 mActionBarView.initialize(mActivity, this, mViewMode, actionBar, mRecentFolderList);
Vikram Aggarwalabd24d82012-04-26 13:23:14 -0700467 }
468
469 /**
470 * Attach the action bar to the activity.
471 */
472 private void attachActionBar() {
473 final ActionBar actionBar = mActivity.getActionBar();
474 if (actionBar != null && mActionBarView != null) {
Vikram Aggarwalec5cbf72012-03-08 15:10:35 -0800475 actionBar.setCustomView(mActionBarView, new ActionBar.LayoutParams(
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -0800476 LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT));
Vikram Aggarwal649b9ea2012-08-27 12:15:20 -0700477 // Show a custom view and home icon, but remove the title
478 final int mask = ActionBar.DISPLAY_SHOW_CUSTOM | ActionBar.DISPLAY_SHOW_TITLE
479 | ActionBar.DISPLAY_SHOW_HOME;
480 final int enabled = ActionBar.DISPLAY_SHOW_CUSTOM | ActionBar.DISPLAY_SHOW_HOME;
481 actionBar.setDisplayOptions(enabled, mask);
Vikram Aggarwalabd24d82012-04-26 13:23:14 -0700482 mActionBarView.attach();
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -0800483 }
Vikram Aggarwalabd24d82012-04-26 13:23:14 -0700484 mViewMode.addListener(mActionBarView);
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -0800485 }
486
487 /**
Mindy Pereira161f50d2012-02-28 15:47:19 -0800488 * Returns whether the conversation list fragment is visible or not.
489 * Different layouts will have their own notion on the visibility of
490 * fragments, so this method needs to be overriden.
491 *
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -0800492 */
493 protected abstract boolean isConversationListVisible();
494
Vikram Aggarwaleb7d3292012-04-20 17:07:20 -0700495 /**
Vikram Aggarwal34f7b232012-10-17 13:32:23 -0700496 * If required, starts wait mode for the current account.
Vikram Aggarwal34f7b232012-10-17 13:32:23 -0700497 */
498 final void perhapsEnterWaitMode() {
499 // If the account is not initialized, then show the wait fragment, since nothing can be
500 // shown.
501 if (mAccount.isAccountInitializationRequired()) {
502 showWaitForInitialization();
503 return;
504 }
505
506 final boolean inWaitingMode = inWaitMode();
507 final boolean isSyncRequired = mAccount.isAccountSyncRequired();
508 if (isSyncRequired) {
509 if (inWaitingMode) {
510 // Update the WaitFragment's account object
511 updateWaitMode();
512 } else {
513 // Transition to waiting mode
514 showWaitForInitialization();
515 }
516 } else if (inWaitingMode) {
517 // Dismiss waiting mode
518 hideWaitForInitialization();
519 }
520 }
521
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -0800522 @Override
Mindy Pereira28e0c342012-02-17 15:05:13 -0800523 public void onAccountChanged(Account account) {
Vikram Aggarwal60069912012-07-24 14:26:09 -0700524 // Is the account or account settings different from the existing account?
Vikram Aggarwal66bc6ed2012-07-31 09:59:55 -0700525 final boolean firstLoad = mAccount == null;
526 final boolean accountChanged = firstLoad || !account.uri.equals(mAccount.uri);
Vikram Aggarwal472e5362012-11-05 14:17:23 -0800527 // If nothing has changed, return early without wasting any more time.
528 if (!accountChanged && !account.settingsDiffer(mAccount)) {
529 return;
530 }
531 // We also don't want to do anything if the new account is null
532 if (account == null) {
533 LogUtils.e(LOG_TAG, "AAC.onAccountChanged(null) called.");
534 return;
535 }
536 final String accountName = account.name;
537 mHandler.post(new Runnable() {
538 @Override
539 public void run() {
Vikram Aggarwalf6c00b82013-01-03 10:02:50 -0800540 MailActivity.setNfcMessage(accountName);
Mindy Pereirafa20c1a2012-07-23 13:00:02 -0700541 }
Vikram Aggarwal472e5362012-11-05 14:17:23 -0800542 });
543 if (accountChanged) {
544 commitDestructiveActions(false);
545 }
546 // Change the account here
547 setAccount(account);
548 // And carry out associated actions.
549 cancelRefreshTask();
550 if (accountChanged) {
551 loadAccountInbox();
552 }
553 // Check if we need to force setting up an account before proceeding.
554 if (mAccount != null && !Uri.EMPTY.equals(mAccount.settings.setupIntentUri)) {
555 // Launch the intent!
556 final Intent intent = new Intent(Intent.ACTION_EDIT);
557 intent.setData(mAccount.settings.setupIntentUri);
558 mActivity.startActivity(intent);
Mindy Pereira28d5f722012-02-15 12:32:40 -0800559 }
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -0800560 }
561
Vikram Aggarwale6340bc2012-03-26 15:57:09 -0700562 /**
Vikram Aggarwal7c401b72012-08-13 16:43:47 -0700563 * Adds a listener interested in change in the current account. If a class is storing a
564 * reference to the current account, it should listen on changes, so it can receive updates to
565 * settings. Must happen in the UI thread.
566 */
Mindy Pereiraefe3d252012-03-01 14:20:44 -0800567 @Override
Vikram Aggarwal7c401b72012-08-13 16:43:47 -0700568 public void registerAccountObserver(DataSetObserver obs) {
569 mAccountObservers.registerObserver(obs);
Mindy Pereiraefe3d252012-03-01 14:20:44 -0800570 }
571
Vikram Aggarwal7d816002012-04-17 17:06:41 -0700572 /**
Vikram Aggarwal7c401b72012-08-13 16:43:47 -0700573 * Removes a listener from receiving current account changes.
Vikram Aggarwal7d816002012-04-17 17:06:41 -0700574 * Must happen in the UI thread.
575 */
Vikram Aggarwal7c401b72012-08-13 16:43:47 -0700576 @Override
577 public void unregisterAccountObserver(DataSetObserver obs) {
578 mAccountObservers.unregisterObserver(obs);
Vikram Aggarwal7d816002012-04-17 17:06:41 -0700579 }
580
Vikram Aggarwal7c401b72012-08-13 16:43:47 -0700581 @Override
582 public Account getAccount() {
583 return mAccount;
Vikram Aggarwal7d816002012-04-17 17:06:41 -0700584 }
585
Mindy Pereirae0828392012-03-08 10:38:40 -0800586 private void fetchSearchFolder(Intent intent) {
Vikram Aggarwal2b703c62012-09-18 13:54:15 -0700587 final Bundle args = new Bundle();
Mindy Pereiraab486362012-03-21 18:18:53 -0700588 args.putString(ConversationListContext.EXTRA_SEARCH_QUERY, intent
Mindy Pereirae0828392012-03-08 10:38:40 -0800589 .getStringExtra(ConversationListContext.EXTRA_SEARCH_QUERY));
Mindy Pereiraab486362012-03-21 18:18:53 -0700590 mActivity.getLoaderManager().restartLoader(LOADER_SEARCH, args, this);
Mindy Pereirae0828392012-03-08 10:38:40 -0800591 }
592
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -0800593 @Override
Mindy Pereira28e0c342012-02-17 15:05:13 -0800594 public void onFolderChanged(Folder folder) {
Vikram Aggarwalf3341402012-08-07 10:09:38 -0700595 changeFolder(folder, null);
596 }
597
598 /**
Vikram Aggarwal203dc002012-08-23 13:59:04 -0700599 * Sets the folder state without changing view mode and without creating a list fragment, if
600 * possible.
601 * @param folder
602 */
603 private void setListContext(Folder folder, String query) {
604 updateFolder(folder);
605 if (query != null) {
606 mConvListContext = ConversationListContext.forSearchQuery(mAccount, mFolder, query);
607 } else {
608 mConvListContext = ConversationListContext.forFolder(mAccount, mFolder);
609 }
Vikram Aggarwal203dc002012-08-23 13:59:04 -0700610 cancelRefreshTask();
611 }
612
613 /**
Vikram Aggarwalf3341402012-08-07 10:09:38 -0700614 * Changes the folder to the value provided here. This causes the view mode to change.
615 * @param folder the folder to change to
616 * @param query if non-null, this represents the search string that the folder represents.
617 */
Vikram Aggarwal93dc2022012-11-05 13:36:57 -0800618 private final void changeFolder(Folder folder, String query) {
Vikram Aggarwal203dc002012-08-23 13:59:04 -0700619 if (!Objects.equal(mFolder, folder)) {
620 commitDestructiveActions(false);
621 }
Mindy Pereiraa1f99982012-07-16 16:32:15 -0700622 if (folder != null && !folder.equals(mFolder)
623 || (mViewMode.getMode() != ViewMode.CONVERSATION_LIST)) {
Vikram Aggarwal203dc002012-08-23 13:59:04 -0700624 setListContext(folder, query);
Mindy Pereira28e0c342012-02-17 15:05:13 -0800625 showConversationList(mConvListContext);
Mindy Pereira28e0c342012-02-17 15:05:13 -0800626 }
Vikram Aggarwal93dc2022012-11-05 13:36:57 -0800627 resetActionBarIcon();
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -0800628 }
629
Mindy Pereira13c12a62012-05-31 15:41:08 -0700630 @Override
Mindy Pereira505df5f2012-06-19 17:57:17 -0700631 public void onFolderSelected(Folder folder) {
Mindy Pereira13c12a62012-05-31 15:41:08 -0700632 onFolderChanged(folder);
633 }
634
Vikram Aggarwal792ccba2012-03-27 13:46:57 -0700635 /**
636 * Update the recent folders. This only needs to be done once when accessing a new folder.
637 */
Paul Westbrook7ebdfd02012-03-21 15:55:30 -0700638 private void updateRecentFolderList() {
Mindy Pereiraab486362012-03-21 18:18:53 -0700639 if (mFolder != null) {
Marc Blank2675dbc2012-04-03 10:17:13 -0700640 mRecentFolderList.touchFolder(mFolder, mAccount);
Mindy Pereiraab486362012-03-21 18:18:53 -0700641 }
Paul Westbrook7ebdfd02012-03-21 15:55:30 -0700642 }
643
Vikram Aggarwal58cad2e2012-08-28 16:18:23 -0700644 /**
645 * Adds a listener interested in change in the recent folders. If a class is storing a
646 * reference to the recent folders, it should listen on changes, so it can receive updates.
647 * Must happen in the UI thread.
648 */
649 @Override
650 public void registerRecentFolderObserver(DataSetObserver obs) {
651 mRecentFolderObservers.registerObserver(obs);
652 }
653
654 /**
655 * Removes a listener from receiving recent folder changes.
656 * Must happen in the UI thread.
657 */
658 @Override
659 public void unregisterRecentFolderObserver(DataSetObserver obs) {
660 mRecentFolderObservers.unregisterObserver(obs);
661 }
662
663 @Override
664 public RecentFolderList getRecentFolders() {
665 return mRecentFolderList;
666 }
667
Mindy Pereiraab486362012-03-21 18:18:53 -0700668 // TODO(mindyp): set this up to store a copy of the folder as a transient
669 // field in the account.
Vikram Aggarwaleb7d3292012-04-20 17:07:20 -0700670 @Override
671 public void loadAccountInbox() {
Vikram Aggarwal94c94de2012-04-04 15:38:28 -0700672 restartOptionalLoader(LOADER_ACCOUNT_INBOX);
Mindy Pereiraf6acdad2012-03-15 11:21:13 -0700673 }
674
Vikram Aggarwal69b5c302012-09-05 11:11:13 -0700675 /**
Vikram Aggarwald00229d2012-09-20 12:31:44 -0700676 * Marks the {@link #mFolderChanged} value if the newFolder is different from the existing
677 * {@link #mFolder}. This should be called immediately <b>before</b> assigning newFolder to
678 * mFolder.
679 * @param newFolder
680 */
681 private final void setHasFolderChanged(final Folder newFolder) {
682 // We should never try to assign a null folder. But in the rare event that we do, we should
683 // only set the bit when we have a valid folder, and null is not valid.
684 if (newFolder == null) {
685 return;
686 }
687 // If the previous folder was null, or if the two folders represent different data, then we
688 // consider that the folder has changed.
689 if (mFolder == null || !newFolder.uri.equals(mFolder.uri)) {
690 mFolderChanged = true;
691 }
692 }
693
694 /**
Vikram Aggarwal69b5c302012-09-05 11:11:13 -0700695 * Sets the current folder if it is different from the object provided here. This method does
696 * NOT notify the folder observers that a change has happened. Observers are notified when we
697 * get an updated folder from the loaders, which will happen as a consequence of this method
698 * (since this method starts/restarts the loaders).
699 * @param folder The folder to assign
700 */
Mindy Pereira11e35962012-06-01 14:49:46 -0700701 private void updateFolder(Folder folder) {
Vikram Aggarwal1672ff82012-09-21 10:15:22 -0700702 if (folder == null || !folder.isInitialized()) {
703 LogUtils.e(LOG_TAG, new Error(), "AAC.setFolder(%s): Bad input", folder);
704 return;
705 }
706 if (folder.equals(mFolder)) {
707 LogUtils.d(LOG_TAG, "AAC.setFolder(%s): Input matches mFolder", folder);
708 return;
709 }
710 final boolean wasNull = mFolder == null;
711 LogUtils.d(LOG_TAG, "AbstractActivityController.setFolder(%s)", folder.name);
712 final LoaderManager lm = mActivity.getLoaderManager();
713 // updateFolder is called from AAC.onLoadFinished() on folder changes. We need to
714 // ensure that the folder is different from the previous folder before marking the
715 // folder changed.
716 setHasFolderChanged(folder);
717 mFolder = folder;
Vikram Aggarwal69b5c302012-09-05 11:11:13 -0700718
Vikram Aggarwal1672ff82012-09-21 10:15:22 -0700719 // We do not need to notify folder observers yet. Instead we start the loaders and
720 // when the load finishes, we will get an updated folder. Then, we notify the
721 // folderObservers in onLoadFinished.
722 mActionBarView.setFolder(mFolder);
Andy Huangb1c34dc2012-04-17 16:36:19 -0700723
Vikram Aggarwal1672ff82012-09-21 10:15:22 -0700724 // Only when we switch from one folder to another do we want to restart the
725 // folder and conversation list loaders (to trigger onCreateLoader).
726 // The first time this runs when the activity is [re-]initialized, we want to re-use the
727 // previous loader's instance and data upon configuration change (e.g. rotation).
728 // If there was not already an instance of the loader, init it.
729 if (lm.getLoader(LOADER_FOLDER_CURSOR) == null) {
730 lm.initLoader(LOADER_FOLDER_CURSOR, null, this);
731 } else {
732 lm.restartLoader(LOADER_FOLDER_CURSOR, null, this);
733 }
734 // In this case, we are starting from no folder, which would occur
735 // the first time the app was launched or on orientation changes.
736 // We want to attach to an existing loader, if available.
737 if (wasNull || lm.getLoader(LOADER_CONVERSATION_LIST) == null) {
738 lm.initLoader(LOADER_CONVERSATION_LIST, null, mListCursorCallbacks);
739 } else {
740 // However, if there was an existing folder AND we have changed
741 // folders, we want to restart the loader to get the information
742 // for the newly selected folder
743 lm.destroyLoader(LOADER_CONVERSATION_LIST);
744 lm.initLoader(LOADER_CONVERSATION_LIST, null, mListCursorCallbacks);
Mindy Pereira5cb0c3e2012-02-22 15:22:47 -0800745 }
746 }
747
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -0800748 @Override
Andy Huang090db1e2012-07-25 13:25:28 -0700749 public Folder getFolder() {
750 return mFolder;
751 }
752
753 @Override
Mindy Pereirac975e842012-07-16 09:15:00 -0700754 public Folder getHierarchyFolder() {
755 return mFolderListFolder;
756 }
757
758 @Override
759 public void setHierarchyFolder(Folder folder) {
760 mFolderListFolder = folder;
Mindy Pereira23aadfd2012-05-25 11:24:33 -0700761 }
762
763 @Override
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -0800764 public void onActivityResult(int requestCode, int resultCode, Intent data) {
Paul Westbrook122f7c22012-08-20 17:50:31 -0700765 switch (requestCode) {
766 case ADD_ACCOUNT_REQUEST_CODE:
767 // We were waiting for the user to create an account
768 if (resultCode == Activity.RESULT_OK) {
769 // restart the loader to get the updated list of accounts
770 mActivity.getLoaderManager().initLoader(
771 LOADER_ACCOUNT_CURSOR, null, this);
772 } else {
773 // The user failed to create an account, just exit the app
774 mActivity.finish();
775 }
776 break;
777 case REAUTHENTICATE_REQUEST_CODE:
778 if (resultCode == Activity.RESULT_OK) {
779 // The user successfully authenticated, attempt to refresh the list
780 final Uri refreshUri = mFolder != null ? mFolder.refreshUri : null;
781 if (refreshUri != null) {
782 startAsyncRefreshTask(refreshUri);
783 }
784 }
785 break;
Paul Westbrook2388c5d2012-03-25 12:29:11 -0700786 }
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -0800787 }
788
Vikram Aggarwal69b5c302012-09-05 11:11:13 -0700789 /**
790 * Inform the conversation cursor that there has been a visibility change.
791 * @param visible
792 */
793 protected synchronized void informCursorVisiblity(boolean visible) {
794 if (mConversationListCursor != null) {
795 Utils.setConversationCursorVisibility(mConversationListCursor, visible, mFolderChanged);
796 // We have informed the cursor. Subsequent visibility changes should not tell it that
797 // the folder has changed.
798 mFolderChanged = false;
799 }
800 }
801
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -0800802 @Override
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -0800803 public void onConversationListVisibilityChanged(boolean visible) {
Vikram Aggarwal69b5c302012-09-05 11:11:13 -0700804 informCursorVisiblity(visible);
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -0800805 }
806
Vikram Aggarwal4a5c5302012-01-12 15:07:13 -0800807 /**
Vikram Aggarwal7dd054e2012-05-21 14:43:10 -0700808 * Called when a conversation is visible. Child classes must call the super class implementation
809 * before performing local computation.
Vikram Aggarwal4a5c5302012-01-12 15:07:13 -0800810 */
811 @Override
812 public void onConversationVisibilityChanged(boolean visible) {
Vikram Aggarwal4a5c5302012-01-12 15:07:13 -0800813 }
814
815 @Override
Vikram Aggarwald7a12cd2012-02-03 09:36:20 -0800816 public boolean onCreate(Bundle savedState) {
Vikram Aggarwalabd24d82012-04-26 13:23:14 -0700817 initializeActionBar();
Vikram Aggarwala55b36c2012-01-13 11:45:02 -0800818 // Allow shortcut keys to function for the ActionBar and menus.
819 mActivity.setDefaultKeyMode(Activity.DEFAULT_KEYS_SHORTCUT);
Vikram Aggarwal80aeac52012-02-07 15:27:20 -0800820 mResolver = mActivity.getContentResolver();
Paul Westbrook6ead20d2012-03-19 14:48:14 -0700821 mNewEmailReceiver = new SuppressNotificationReceiver();
Vikram Aggarwal7c401b72012-08-13 16:43:47 -0700822 mRecentFolderList.initialize(mActivity);
Vikram Aggarwal69a6cdf2013-01-08 16:05:17 -0800823 mVeiledMatcher.initialize(this);
Paul Westbrook6ead20d2012-03-19 14:48:14 -0700824
Mindy Pereira161f50d2012-02-28 15:47:19 -0800825 // All the individual UI components listen for ViewMode changes. This
Mindy Pereirab849dfb2012-03-07 18:13:15 -0800826 // simplifies the amount of logic in the AbstractActivityController, but increases the
827 // possibility of timing-related bugs.
Vikram Aggarwala55b36c2012-01-13 11:45:02 -0800828 mViewMode.addListener(this);
Andy Huang632721e2012-04-11 16:57:26 -0700829 mPagerController = new ConversationPagerController(mActivity, this);
Andrew Sappersteinc2c9dc12012-07-02 18:17:32 -0700830 mToastBar = (ActionableToastBar) mActivity.findViewById(R.id.toast_bar);
Vikram Aggarwalada51782012-04-26 14:18:31 -0700831 attachActionBar();
Mark Wei9eb1c9a2012-10-01 12:54:50 -0700832 FolderSelectionDialog.setDialogDismissed();
Andy Huang632721e2012-04-11 16:57:26 -0700833
834 final Intent intent = mActivity.getIntent();
Vikram Aggarwalabd24d82012-04-26 13:23:14 -0700835 // Immediately handle a clean launch with intent, and any state restoration
Andy Huang632721e2012-04-11 16:57:26 -0700836 // that does not rely on restored fragments or loader data
837 // any state restoration that relies on those can be done later in
838 // onRestoreInstanceState, once fragments are up and loader data is re-delivered
839 if (savedState != null) {
840 if (savedState.containsKey(SAVED_ACCOUNT)) {
841 setAccount((Account) savedState.getParcelable(SAVED_ACCOUNT));
Andy Huang632721e2012-04-11 16:57:26 -0700842 }
843 if (savedState.containsKey(SAVED_FOLDER)) {
Andy Huang2bc8bc12012-11-12 17:24:25 -0800844 final Folder folder = savedState.getParcelable(SAVED_FOLDER);
Vikram Aggarwal203dc002012-08-23 13:59:04 -0700845 final String query = savedState.getString(SAVED_QUERY, null);
Vikram Aggarwal649b9ea2012-08-27 12:15:20 -0700846 setListContext(folder, query);
Andy Huang632721e2012-04-11 16:57:26 -0700847 }
Vikram Aggarwal6cadbfc2012-12-27 09:17:05 -0800848 if (savedState.containsKey(SAVED_ACTION)) {
849 mDialogAction = savedState.getInt(SAVED_ACTION);
850 }
Vikram Aggarwala91d00b2013-01-18 12:00:37 -0800851 mDialogFromSelectedSet = savedState.getBoolean(SAVED_ACTION_FROM_SELECTED, false);
Vikram Aggarwal649b9ea2012-08-27 12:15:20 -0700852 mViewMode.handleRestore(savedState);
Andy Huang632721e2012-04-11 16:57:26 -0700853 } else if (intent != null) {
854 handleIntent(intent);
855 }
Andy Huang632721e2012-04-11 16:57:26 -0700856 // Create the accounts loader; this loads the account switch spinner.
857 mActivity.getLoaderManager().initLoader(LOADER_ACCOUNT_CURSOR, null, this);
Andy Huang632721e2012-04-11 16:57:26 -0700858 return true;
Andy Huangb1c34dc2012-04-17 16:36:19 -0700859 }
860
861 @Override
Andy Huang1ee96b22012-08-24 20:19:53 -0700862 public void onStart() {
863 mSafeToModifyFragments = true;
864 }
865
866 @Override
Andrew Sapperstein00179f12012-08-09 15:15:40 -0700867 public void onRestart() {
868 DialogFragment fragment = (DialogFragment)
869 mFragmentManager.findFragmentByTag(SYNC_ERROR_DIALOG_FRAGMENT_TAG);
870 if (fragment != null) {
871 fragment.dismiss();
872 }
mindypea04f932012-08-27 14:17:59 -0700873 // When the user places the app in the background by pressing "home",
874 // dismiss the toast bar. However, since there is no way to determine if
875 // home was pressed, just dismiss any existing toast bar when restarting
876 // the app.
877 if (mToastBar != null) {
878 mToastBar.hide(false);
879 }
Andrew Sapperstein00179f12012-08-09 15:15:40 -0700880 }
881
882 @Override
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -0800883 public Dialog onCreateDialog(int id, Bundle bundle) {
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -0800884 return null;
Vikram Aggarwala55b36c2012-01-13 11:45:02 -0800885 }
886
887 @Override
Vikram Aggarwal4eb52712012-06-19 16:24:50 -0700888 public final boolean onCreateOptionsMenu(Menu menu) {
Vikram Aggarwale5e917c2012-09-20 16:27:41 -0700889 final MenuInflater inflater = mActivity.getMenuInflater();
Mindy Pereiraf5acda42012-02-15 20:13:59 -0800890 inflater.inflate(mActionBarView.getOptionsMenuId(), menu);
Mindy Pereira68f2e222012-03-07 10:36:54 -0800891 mActionBarView.onCreateOptionsMenu(menu);
Mindy Pereira28d5f722012-02-15 12:32:40 -0800892 return true;
Vikram Aggarwala55b36c2012-01-13 11:45:02 -0800893 }
894
895 @Override
Vikram Aggarwal4eb52712012-06-19 16:24:50 -0700896 public final boolean onKeyDown(int keyCode, KeyEvent event) {
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -0800897 return false;
898 }
899
mindyp17a8e782012-11-29 14:56:17 -0800900 public abstract boolean doesActionChangeConversationListVisibility(int action);
901
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -0800902 @Override
Vikram Aggarwal4eb52712012-06-19 16:24:50 -0700903 public final boolean onOptionsItemSelected(MenuItem item) {
Vikram Aggarwal54452ae2012-03-13 15:29:00 -0700904 final int id = item.getItemId();
Vikram Aggarwal7dd054e2012-05-21 14:43:10 -0700905 LogUtils.d(LOG_TAG, "AbstractController.onOptionsItemSelected(%d) called.", id);
Mindy Pereira9b875682012-02-15 18:10:54 -0800906 boolean handled = true;
Mindy Pereiraba68fda2012-05-24 15:53:06 -0700907 final Collection<Conversation> target = Conversation.listOf(mCurrentConversation);
Vikram Aggarwal112cd162012-06-18 10:51:13 -0700908 final Settings settings = (mAccount == null) ? null : mAccount.settings;
mindyp84f7d322012-10-01 17:14:40 -0700909 // The user is choosing a new action; commit whatever they had been
mindyp17a8e782012-11-29 14:56:17 -0800910 // doing before. Don't animate if we are launching a new screen.
911 commitDestructiveActions(!doesActionChangeConversationListVisibility(id));
Mindy Pereira28d5f722012-02-15 12:32:40 -0800912 switch (id) {
Mindy Pereiraba68fda2012-05-24 15:53:06 -0700913 case R.id.archive: {
914 final boolean showDialog = (settings != null && settings.confirmArchive);
Vikram Aggarwalb8c31712013-01-03 17:03:19 -0800915 confirmAndDelete(id, target, showDialog, R.plurals.confirm_archive_conversation);
Mindy Pereiraba68fda2012-05-24 15:53:06 -0700916 break;
917 }
Mindy Pereira01f30502012-08-14 10:30:51 -0700918 case R.id.remove_folder:
Vikram Aggarwala8e43182012-09-13 12:55:10 -0700919 delete(R.id.remove_folder, target,
mindyp84f7d322012-10-01 17:14:40 -0700920 getDeferredRemoveFolder(target, mFolder, true, false, true));
Mindy Pereira01f30502012-08-14 10:30:51 -0700921 break;
Mindy Pereiraba68fda2012-05-24 15:53:06 -0700922 case R.id.delete: {
923 final boolean showDialog = (settings != null && settings.confirmDelete);
Vikram Aggarwalb8c31712013-01-03 17:03:19 -0800924 confirmAndDelete(id, target, showDialog, R.plurals.confirm_delete_conversation);
Mindy Pereiraba68fda2012-05-24 15:53:06 -0700925 break;
926 }
Vikram Aggarwala8e43182012-09-13 12:55:10 -0700927 case R.id.discard_drafts: {
Paul Westbrookef362542012-08-27 14:53:32 -0700928 final boolean showDialog = (settings != null && settings.confirmDelete);
Vikram Aggarwalb8c31712013-01-03 17:03:19 -0800929 confirmAndDelete(id, target, showDialog,
930 R.plurals.confirm_discard_drafts_conversation);
Paul Westbrookef362542012-08-27 14:53:32 -0700931 break;
Vikram Aggarwala8e43182012-09-13 12:55:10 -0700932 }
Mindy Pereiraba68fda2012-05-24 15:53:06 -0700933 case R.id.mark_important:
Vikram Aggarwal531488e2012-05-29 16:36:52 -0700934 updateConversation(Conversation.listOf(mCurrentConversation),
935 ConversationColumns.PRIORITY, UIProvider.ConversationPriority.HIGH);
Mindy Pereiraba68fda2012-05-24 15:53:06 -0700936 break;
937 case R.id.mark_not_important:
Mindy Pereira0d03ef82012-08-15 09:05:48 -0700938 if (mFolder != null && mFolder.isImportantOnly()) {
Vikram Aggarwala8e43182012-09-13 12:55:10 -0700939 delete(R.id.mark_not_important, target,
mindyp84f7d322012-10-01 17:14:40 -0700940 getDeferredAction(R.id.mark_not_important, target, false));
Mindy Pereira0d03ef82012-08-15 09:05:48 -0700941 } else {
942 updateConversation(Conversation.listOf(mCurrentConversation),
943 ConversationColumns.PRIORITY, UIProvider.ConversationPriority.LOW);
944 }
Mindy Pereiraba68fda2012-05-24 15:53:06 -0700945 break;
946 case R.id.mute:
mindyp84f7d322012-10-01 17:14:40 -0700947 delete(R.id.mute, target, getDeferredAction(R.id.mute, target, false));
Mindy Pereiraba68fda2012-05-24 15:53:06 -0700948 break;
949 case R.id.report_spam:
mindyp84f7d322012-10-01 17:14:40 -0700950 delete(R.id.report_spam, target,
951 getDeferredAction(R.id.report_spam, target, false));
Mindy Pereiraba68fda2012-05-24 15:53:06 -0700952 break;
Paul Westbrook77eee622012-07-10 13:41:57 -0700953 case R.id.mark_not_spam:
mindyp84f7d322012-10-01 17:14:40 -0700954 // Currently, since spam messages are only shown in list with
955 // other spam messages,
Paul Westbrook77eee622012-07-10 13:41:57 -0700956 // marking a message not as spam is a destructive action
mindyp84f7d322012-10-01 17:14:40 -0700957 delete(R.id.mark_not_spam, target,
958 getDeferredAction(R.id.mark_not_spam, target, false));
Paul Westbrook77eee622012-07-10 13:41:57 -0700959 break;
Paul Westbrook76b20622012-07-12 11:45:43 -0700960 case R.id.report_phishing:
mindyp84f7d322012-10-01 17:14:40 -0700961 delete(R.id.report_phishing, target,
962 getDeferredAction(R.id.report_phishing, target, false));
Paul Westbrook76b20622012-07-12 11:45:43 -0700963 break;
Mindy Pereiraf5acda42012-02-15 20:13:59 -0800964 case android.R.id.home:
965 onUpPressed();
966 break;
Mindy Pereira9b875682012-02-15 18:10:54 -0800967 case R.id.compose:
968 ComposeActivity.compose(mActivity.getActivityContext(), mAccount);
969 break;
Mindy Pereira28d5f722012-02-15 12:32:40 -0800970 case R.id.show_all_folders:
971 showFolderList();
972 break;
Mindy Pereira28e0c342012-02-17 15:05:13 -0800973 case R.id.refresh:
974 requestFolderRefresh();
975 break;
Mindy Pereira1f936682012-03-02 11:30:33 -0800976 case R.id.settings:
977 Utils.showSettings(mActivity.getActivityContext(), mAccount);
Paul Westbrook2861b6a2012-02-15 15:25:34 -0800978 break;
Paul Westbrooke5503552012-03-28 00:35:57 -0700979 case R.id.folder_options:
980 Utils.showFolderSettings(mActivity.getActivityContext(), mAccount, mFolder);
981 break;
Paul Westbrook94e440d2012-02-24 11:03:47 -0800982 case R.id.help_info_menu_item:
Paul Westbrook30745b62012-08-19 14:10:32 -0700983 Utils.showHelp(mActivity.getActivityContext(), mAccount, getHelpContext());
Paul Westbrook94e440d2012-02-24 11:03:47 -0800984 break;
Vikram Aggarwal54452ae2012-03-13 15:29:00 -0700985 case R.id.feedback_menu_item:
Paul Westbrook17beb0b2012-08-20 13:34:37 -0700986 Utils.sendFeedback(mActivity.getActivityContext(), mAccount, false);
Mindy Pereirafbe40192012-03-20 10:40:45 -0700987 break;
Paul Westbrook18babd22012-04-09 22:17:08 -0700988 case R.id.manage_folders_item:
989 Utils.showManageFolder(mActivity.getActivityContext(), mAccount);
990 break;
Vikram Aggarwald503df42012-05-11 10:13:35 -0700991 case R.id.change_folder:
Mark Wei8f98ac02012-10-01 17:05:08 -0700992 final FolderSelectionDialog dialog = FolderSelectionDialog.getInstance(
993 mActivity.getActivityContext(), mAccount, this,
994 Conversation.listOf(mCurrentConversation), false, mFolder);
Mark Wei9eb1c9a2012-10-01 12:54:50 -0700995 if (dialog != null) {
996 dialog.show();
mindypa7e15452012-09-18 14:22:11 -0700997 }
Vikram Aggarwald503df42012-05-11 10:13:35 -0700998 break;
Mindy Pereira9b875682012-02-15 18:10:54 -0800999 default:
1000 handled = false;
1001 break;
Mindy Pereira28d5f722012-02-15 12:32:40 -08001002 }
Mindy Pereira9b875682012-02-15 18:10:54 -08001003 return handled;
Vikram Aggarwala55b36c2012-01-13 11:45:02 -08001004 }
1005
Vikram Aggarwal531488e2012-05-29 16:36:52 -07001006 @Override
Mindy Pereira6c2663d2012-07-20 15:37:29 -07001007 public void updateConversation(Collection<Conversation> target, ContentValues values) {
1008 mConversationListCursor.updateValues(mContext, target, values);
1009 refreshConversationList();
1010 }
1011
1012 @Override
Vikram Aggarwal531488e2012-05-29 16:36:52 -07001013 public void updateConversation(Collection <Conversation> target, String columnName,
1014 boolean value) {
1015 mConversationListCursor.updateBoolean(mContext, target, columnName, value);
Vikram Aggarwal75daee52012-04-30 11:13:09 -07001016 refreshConversationList();
Vikram Aggarwal54452ae2012-03-13 15:29:00 -07001017 }
1018
Vikram Aggarwal531488e2012-05-29 16:36:52 -07001019 @Override
Mindy Pereira6c2663d2012-07-20 15:37:29 -07001020 public void updateConversation(Collection <Conversation> target, String columnName,
1021 int value) {
Vikram Aggarwal531488e2012-05-29 16:36:52 -07001022 mConversationListCursor.updateInt(mContext, target, columnName, value);
Vikram Aggarwal75daee52012-04-30 11:13:09 -07001023 refreshConversationList();
Mindy Pereirac9d59182012-03-22 16:06:46 -07001024 }
1025
Vikram Aggarwal531488e2012-05-29 16:36:52 -07001026 @Override
1027 public void updateConversation(Collection <Conversation> target, String columnName,
1028 String value) {
1029 mConversationListCursor.updateString(mContext, target, columnName, value);
Vikram Aggarwal75daee52012-04-30 11:13:09 -07001030 refreshConversationList();
Vikram Aggarwal54452ae2012-03-13 15:29:00 -07001031 }
1032
Andy Huang839ada22012-07-20 15:48:40 -07001033 @Override
Yu Ping Hu7c909c72013-01-18 11:58:01 -08001034 public void markConversationMessagesUnread(final Conversation conv,
1035 final Set<Uri> unreadMessageUris, final byte[] originalConversationInfo) {
Andy Huang8f6b0062012-07-31 15:36:31 -07001036 // The only caller of this method is the conversation view, from where marking unread should
1037 // *always* take you back to list mode.
1038 showConversation(null);
1039
Andy Huang839ada22012-07-20 15:48:40 -07001040 // locally mark conversation unread (the provider is supposed to propagate message unread
1041 // to conversation unread)
1042 conv.read = false;
Paul Westbrook1faf93d2012-10-16 08:58:07 -07001043 if (mConversationListCursor == null) {
Yu Ping Hu7c909c72013-01-18 11:58:01 -08001044 LogUtils.d(LOG_TAG, "deferring markConversationMessagesUnread for id=%d", conv.id);
Paul Westbrook1faf93d2012-10-16 08:58:07 -07001045
Yu Ping Hu7c909c72013-01-18 11:58:01 -08001046 mConversationListLoadFinishedCallbacks.add(new LoadFinishedCallback() {
1047 @Override
1048 public void onLoadFinished() {
1049 doMarkConversationMessagesUnread(conv, unreadMessageUris,
1050 originalConversationInfo);
1051 }
1052 });
1053 } else {
1054 doMarkConversationMessagesUnread(conv, unreadMessageUris, originalConversationInfo);
1055 }
1056 }
1057
1058 private void doMarkConversationMessagesUnread(Conversation conv, Set<Uri> unreadMessageUris,
1059 byte[] originalConversationInfo) {
1060 LogUtils.d(LOG_TAG, "performing markConversationMessagesUnread for id=%d", conv.id);
Andy Huang28e31e22012-07-26 16:33:15 -07001061 // only do a granular 'mark unread' if a subset of messages are unread
1062 final int unreadCount = (unreadMessageUris == null) ? 0 : unreadMessageUris.size();
Mindy Pereira0972e072012-08-01 17:43:06 -07001063 final int numMessages = conv.getNumMessages();
1064 final boolean subsetIsUnread = (numMessages > 1 && unreadCount > 0
1065 && unreadCount < numMessages);
Andy Huang28e31e22012-07-26 16:33:15 -07001066
1067 if (!subsetIsUnread) {
Vikram Aggarwal66bc2aa2012-08-02 10:47:03 -07001068 // Conversations are neither marked read, nor viewed, and we don't want to show
1069 // the next conversation.
1070 markConversationsRead(Collections.singletonList(conv), false, false, false);
Andy Huang839ada22012-07-20 15:48:40 -07001071 } else {
Andy Huangdaa06ab2012-07-24 10:46:44 -07001072 mConversationListCursor.setConversationColumn(conv.uri, ConversationColumns.READ, 0);
Andy Huang839ada22012-07-20 15:48:40 -07001073
Mindy Pereira7b6d03d2012-07-30 13:03:41 -07001074 // locally update conversation's conversationInfo to revert to original version
Andy Huang28e31e22012-07-26 16:33:15 -07001075 if (originalConversationInfo != null) {
1076 mConversationListCursor.setConversationColumn(conv.uri,
1077 ConversationColumns.CONVERSATION_INFO, originalConversationInfo);
1078 }
Andy Huang839ada22012-07-20 15:48:40 -07001079
1080 // applyBatch with each CPO as an UPDATE op on each affected message uri
1081 final ArrayList<ContentProviderOperation> ops = Lists.newArrayList();
1082 String authority = null;
1083 for (Uri messageUri : unreadMessageUris) {
1084 if (authority == null) {
1085 authority = messageUri.getAuthority();
1086 }
1087 ops.add(ContentProviderOperation.newUpdate(messageUri)
1088 .withValue(UIProvider.MessageColumns.READ, 0)
1089 .build());
1090 }
1091
1092 new ContentProviderTask() {
1093 @Override
1094 protected void onPostExecute(Result result) {
1095 // TODO: handle errors?
1096 }
1097 }.run(mResolver, authority, ops);
Andy Huang839ada22012-07-20 15:48:40 -07001098 }
Andy Huang839ada22012-07-20 15:48:40 -07001099 }
1100
1101 @Override
Yu Ping Hu7c909c72013-01-18 11:58:01 -08001102 public void markConversationsRead(final Collection<Conversation> targets, final boolean read,
1103 final boolean viewed) {
1104 if (mConversationListCursor == null) {
1105 LogUtils.d(LOG_TAG, "deferring markConversationsRead");
1106 mConversationListLoadFinishedCallbacks.add(new LoadFinishedCallback() {
1107 @Override
1108 public void onLoadFinished() {
1109 markConversationsRead(targets, read, viewed, true);
1110 }
1111 });
1112 } else {
1113 // We want to show the next conversation if we are marking unread.
1114 markConversationsRead(targets, read, viewed, true);
1115 }
Andy Huang8f6b0062012-07-31 15:36:31 -07001116 }
1117
Scott Kennedy0d0f8b02012-10-12 15:18:18 -07001118 private void markConversationsRead(final Collection<Conversation> targets, final boolean read,
1119 final boolean markViewed, final boolean showNext) {
Yu Ping Hu7c909c72013-01-18 11:58:01 -08001120 LogUtils.d(LOG_TAG, "performing markConversationsRead");
Vikram Aggarwalcc754b12012-08-30 14:04:21 -07001121 // Auto-advance if requested and the current conversation is being marked unread
Andy Huang8f6b0062012-07-31 15:36:31 -07001122 if (showNext && !read) {
Scott Kennedy0d0f8b02012-10-12 15:18:18 -07001123 final Runnable operation = new Runnable() {
1124 @Override
1125 public void run() {
1126 markConversationsRead(targets, read, markViewed, showNext);
1127 }
1128 };
1129
1130 if (!showNextConversation(targets, operation)) {
1131 // This method will be called again if the user selects an autoadvance option
1132 return;
1133 }
Andy Huang8f6b0062012-07-31 15:36:31 -07001134 }
Scott Kennedy0d0f8b02012-10-12 15:18:18 -07001135
Vikram Aggarwalcc754b12012-08-30 14:04:21 -07001136 final int size = targets.size();
1137 final List<ConversationOperation> opList = new ArrayList<ConversationOperation>(size);
1138 for (final Conversation target : targets) {
1139 final ContentValues value = new ContentValues();
1140 value.put(ConversationColumns.READ, read);
Paul Westbrook5109c512012-11-05 11:00:30 -08001141
1142 // The mark read/unread/viewed operations do not show an undo bar
1143 value.put(ConversationOperations.Parameters.SUPPRESS_UNDO, true);
Vikram Aggarwal66bc2aa2012-08-02 10:47:03 -07001144 if (markViewed) {
Vikram Aggarwalcc754b12012-08-30 14:04:21 -07001145 value.put(ConversationColumns.VIEWED, true);
Vikram Aggarwal66bc2aa2012-08-02 10:47:03 -07001146 }
Vikram Aggarwal192fac12012-07-25 16:44:55 -07001147 final ConversationInfo info = target.conversationInfo;
Andy Huang839ada22012-07-20 15:48:40 -07001148 if (info != null) {
mindyp7f55c682012-10-04 11:38:27 -07001149 boolean changed = info.markRead(read);
1150 if (changed) {
1151 value.put(ConversationColumns.CONVERSATION_INFO,
Andy Huang351ad4e2012-12-06 16:04:58 -08001152 info.toBlob());
mindyp7f55c682012-10-04 11:38:27 -07001153 }
Andy Huang839ada22012-07-20 15:48:40 -07001154 }
Vikram Aggarwalcc754b12012-08-30 14:04:21 -07001155 opList.add(mConversationListCursor.getOperationForConversation(
1156 target, ConversationOperation.UPDATE, value));
1157 // Update the local conversation objects so they immediately change state.
1158 target.read = read;
Andy Huangcd5c5ee2012-08-12 19:03:51 -07001159 if (markViewed) {
Vikram Aggarwalcc754b12012-08-30 14:04:21 -07001160 target.markViewed();
Andy Huangcd5c5ee2012-08-12 19:03:51 -07001161 }
Andy Huang839ada22012-07-20 15:48:40 -07001162 }
Vikram Aggarwalcc754b12012-08-30 14:04:21 -07001163 mConversationListCursor.updateBulkValues(mContext, opList);
Andy Huang839ada22012-07-20 15:48:40 -07001164 }
1165
Andy Huang8f6b0062012-07-31 15:36:31 -07001166 /**
1167 * Auto-advance to a different conversation if the currently visible conversation in
1168 * conversation mode is affected (deleted, marked unread, etc.).
1169 *
Scott Kennedy0d0f8b02012-10-12 15:18:18 -07001170 * <p>Does nothing if outside of conversation mode.</p>
Andy Huang8f6b0062012-07-31 15:36:31 -07001171 *
1172 * @param target the set of conversations being deleted/marked unread
1173 */
mindyp9365a822012-09-12 09:09:09 -07001174 @Override
Scott Kennedy0d0f8b02012-10-12 15:18:18 -07001175 public void showNextConversation(final Collection<Conversation> target) {
1176 showNextConversation(target, null);
1177 }
1178
1179 /**
1180 * Auto-advance to a different conversation if the currently visible conversation in
1181 * conversation mode is affected (deleted, marked unread, etc.).
1182 *
1183 * <p>Does nothing if outside of conversation mode.</p>
1184 *
1185 * @param target the set of conversations being deleted/marked unread
Yu Ping Hu7c909c72013-01-18 11:58:01 -08001186 * @param operation if auto-advance setting is unset, this operation is run after the user
1187 * is prompted to select a setting.
Scott Kennedy0d0f8b02012-10-12 15:18:18 -07001188 * @return <code>false</code> if we aborted because the user has not yet specified a default
1189 * action, <code>true</code> otherwise
1190 */
1191 private boolean showNextConversation(final Collection<Conversation> target,
1192 final Runnable operation) {
Scott Kennedy8fb8e262012-11-28 15:48:03 -08001193 final int viewMode = mViewMode.getMode();
1194 final boolean currentConversationInView = (viewMode == ViewMode.CONVERSATION
1195 || viewMode == ViewMode.SEARCH_RESULTS_CONVERSATION)
Andy Huang8f6b0062012-07-31 15:36:31 -07001196 && Conversation.contains(target, mCurrentConversation);
Scott Kennedy0d0f8b02012-10-12 15:18:18 -07001197
Andy Huang8f6b0062012-07-31 15:36:31 -07001198 if (currentConversationInView) {
Scott Kennedy0d0f8b02012-10-12 15:18:18 -07001199 final int autoAdvanceSetting = mAccount.settings.getAutoAdvanceSetting();
1200
Vikram Aggarwalbcb16b92013-01-28 18:05:03 -08001201 if (autoAdvanceSetting == AutoAdvance.UNSET && mIsTablet) {
Scott Kennedy0d0f8b02012-10-12 15:18:18 -07001202 displayAutoAdvanceDialogAndPerformAction(operation);
1203 return false;
1204 } else {
1205 // If we don't have one set, but we're here, just take the default
Vikram Aggarwal82d37502013-01-10 16:18:49 -08001206 final int autoAdvance = (autoAdvanceSetting == AutoAdvance.UNSET) ?
1207 AutoAdvance.DEFAULT : autoAdvanceSetting;
Scott Kennedy0d0f8b02012-10-12 15:18:18 -07001208
1209 final Conversation next = mTracker.getNextConversation(autoAdvance, target);
1210 LogUtils.d(LOG_TAG, "showNextConversation: showing %s next.", next);
1211 showConversation(next);
1212 return true;
1213 }
Andy Huang8f6b0062012-07-31 15:36:31 -07001214 }
Scott Kennedy0d0f8b02012-10-12 15:18:18 -07001215
1216 return true;
1217 }
1218
1219 /**
1220 * Displays a the auto-advance dialog, and when the user makes a selection, the preference is
1221 * stored, and the specified operation is run.
1222 */
1223 private void displayAutoAdvanceDialogAndPerformAction(final Runnable operation) {
1224 final String[] autoAdvanceDisplayOptions =
1225 mContext.getResources().getStringArray(R.array.prefEntries_autoAdvance);
1226 final String[] autoAdvanceOptionValues =
1227 mContext.getResources().getStringArray(R.array.prefValues_autoAdvance);
1228
1229 final String defaultValue = mContext.getString(R.string.prefDefault_autoAdvance);
1230 int initialIndex = 0;
1231 for (int i = 0; i < autoAdvanceOptionValues.length; i++) {
1232 if (defaultValue.equals(autoAdvanceOptionValues[i])) {
1233 initialIndex = i;
1234 break;
1235 }
1236 }
1237
1238 final DialogInterface.OnClickListener listClickListener =
1239 new DialogInterface.OnClickListener() {
1240 @Override
1241 public void onClick(DialogInterface dialog, int whichItem) {
1242 final String autoAdvanceValue = autoAdvanceOptionValues[whichItem];
1243 final int autoAdvanceValueInt =
1244 UIProvider.AutoAdvance.getAutoAdvanceInt(autoAdvanceValue);
1245 mAccount.settings.setAutoAdvanceSetting(autoAdvanceValueInt);
1246
1247 // Save the user's setting
1248 final ContentValues values = new ContentValues(1);
1249 values.put(AccountColumns.SettingsColumns.AUTO_ADVANCE, autoAdvanceValue);
1250
1251 final ContentResolver resolver = mContext.getContentResolver();
1252 resolver.update(mAccount.updateSettingsUri, values, null, null);
1253
1254 // Dismiss the dialog, as clicking the items in the list doesn't close the
1255 // dialog.
1256 dialog.dismiss();
1257 if (operation != null) {
1258 operation.run();
1259 }
1260 }
1261 };
1262
1263 new AlertDialog.Builder(mActivity.getActivityContext()).setTitle(
1264 R.string.auto_advance_help_title)
1265 .setSingleChoiceItems(autoAdvanceDisplayOptions, initialIndex, listClickListener)
1266 .setPositiveButton(null, null)
1267 .create()
1268 .show();
Andy Huang8f6b0062012-07-31 15:36:31 -07001269 }
1270
Andy Huang839ada22012-07-20 15:48:40 -07001271 @Override
1272 public void starMessage(ConversationMessage msg, boolean starred) {
1273 if (msg.starred == starred) {
1274 return;
1275 }
1276
1277 msg.starred = starred;
1278
1279 // locally propagate the change to the owning conversation
1280 // (figure the provider will properly propagate the change when it commits it)
1281 //
1282 // when unstarring, only propagate the change if this was the only message starred
1283 final boolean conversationStarred = starred || msg.isConversationStarred();
Andy Huangcd12e822012-11-08 19:50:57 -08001284 final Conversation conv = msg.getConversation();
1285 if (conversationStarred != conv.starred) {
1286 conv.starred = conversationStarred;
1287 mConversationListCursor.setConversationColumn(conv.uri,
Andy Huang839ada22012-07-20 15:48:40 -07001288 ConversationColumns.STARRED, conversationStarred);
1289 }
1290
1291 final ContentValues values = new ContentValues(1);
1292 values.put(UIProvider.MessageColumns.STARRED, starred ? 1 : 0);
1293
1294 new ContentProviderTask.UpdateTask() {
1295 @Override
1296 protected void onPostExecute(Result result) {
1297 // TODO: handle errors?
1298 }
1299 }.run(mResolver, msg.uri, values, null /* selection*/, null /* selectionArgs */);
1300 }
1301
Mindy Pereira28e0c342012-02-17 15:05:13 -08001302 private void requestFolderRefresh() {
1303 if (mFolder != null) {
Mindy Pereirab7b33e02012-02-21 15:32:19 -08001304 if (mAsyncRefreshTask != null) {
1305 mAsyncRefreshTask.cancel(true);
1306 }
Paul Westbrook7e2a2a12012-06-27 13:52:40 -07001307 mAsyncRefreshTask = new AsyncRefreshTask(mContext, mFolder.refreshUri);
Mindy Pereirab7b33e02012-02-21 15:32:19 -08001308 mAsyncRefreshTask.execute();
Mindy Pereira28e0c342012-02-17 15:05:13 -08001309 }
1310 }
1311
Mindy Pereirafbe40192012-03-20 10:40:45 -07001312 /**
1313 * Confirm (based on user's settings) and delete a conversation from the conversation list and
1314 * from the database.
Vikram Aggarwalb8c31712013-01-03 17:03:19 -08001315 * @param actionId the ID of the menu item that caused the delete: R.id.delete, R.id.archive...
Vikram Aggarwal3d7ca9d2012-05-11 14:40:36 -07001316 * @param target the conversations to act upon
1317 * @param showDialog true if a confirmation dialog is to be shown, false otherwise.
1318 * @param confirmResource the resource ID of the string that is shown in the confirmation dialog
Mindy Pereirafbe40192012-03-20 10:40:45 -07001319 */
Vikram Aggarwalb8c31712013-01-03 17:03:19 -08001320 private void confirmAndDelete(int actionId, final Collection<Conversation> target,
1321 boolean showDialog, int confirmResource) {
Mindy Pereirafbe40192012-03-20 10:40:45 -07001322 if (showDialog) {
Vikram Aggarwalb8c31712013-01-03 17:03:19 -08001323 makeDialogListener(actionId, false);
Vikram Aggarwal3d7ca9d2012-05-11 14:40:36 -07001324 final CharSequence message = Utils.formatPlural(mContext, confirmResource,
1325 target.size());
Vikram Aggarwalb8c31712013-01-03 17:03:19 -08001326 final ConfirmDialogFragment c = ConfirmDialogFragment.newInstance(message);
1327 c.displayDialog(mActivity.getFragmentManager());
Mindy Pereirafbe40192012-03-20 10:40:45 -07001328 } else {
Vikram Aggarwal669947b2013-01-10 17:05:56 -08001329 delete(0, target, getDeferredAction(actionId, target, false));
Mindy Pereirafbe40192012-03-20 10:40:45 -07001330 }
1331 }
1332
Vikram Aggarwal4f4782b2012-05-30 08:39:09 -07001333 @Override
Scott Kennedy0d0f8b02012-10-12 15:18:18 -07001334 public void delete(final int actionId, final Collection<Conversation> target,
Vikram Aggarwal669947b2013-01-10 17:05:56 -08001335 final DestructiveAction action) {
mindyp84f7d322012-10-01 17:14:40 -07001336 // Order of events is critical! The Conversation View Fragment must be
1337 // notified of the next conversation with showConversation(next) *before* the
1338 // conversation list
Vikram Aggarwal192fac12012-07-25 16:44:55 -07001339 // fragment has a chance to delete the conversation, animating it away.
1340
mindyp84f7d322012-10-01 17:14:40 -07001341 // Update the conversation fragment if the current conversation is
1342 // deleted.
Scott Kennedy0d0f8b02012-10-12 15:18:18 -07001343 final Runnable operation = new Runnable() {
1344 @Override
1345 public void run() {
Vikram Aggarwal669947b2013-01-10 17:05:56 -08001346 delete(actionId, target, action);
Scott Kennedy0d0f8b02012-10-12 15:18:18 -07001347 }
1348 };
1349
1350 if (!showNextConversation(target, operation)) {
1351 // This method will be called again if the user selects an autoadvance option
1352 return;
1353 }
1354
Vikram Aggarwal192fac12012-07-25 16:44:55 -07001355 // The conversation list deletes and performs the action if it exists.
1356 final ConversationListFragment convListFragment = getConversationListFragment();
1357 if (convListFragment != null) {
1358 LogUtils.d(LOG_TAG, "AAC.requestDelete: ListFragment is handling delete.");
Vikram Aggarwal669947b2013-01-10 17:05:56 -08001359 convListFragment.requestDelete(actionId, target, action);
Vikram Aggarwal192fac12012-07-25 16:44:55 -07001360 return;
1361 }
mindyp84f7d322012-10-01 17:14:40 -07001362 // No visible UI element handled it on our behalf. Perform the action
1363 // ourself.
Vikram Aggarwald503df42012-05-11 10:13:35 -07001364 action.performAction();
1365 }
1366
1367 /**
1368 * Requests that the action be performed and the UI state is updated to reflect the new change.
1369 * @param target
1370 * @param action
1371 */
Vikram Aggarwal4f4782b2012-05-30 08:39:09 -07001372 private void requestUpdate(final Collection<Conversation> target,
Vikram Aggarwald503df42012-05-11 10:13:35 -07001373 final DestructiveAction action) {
1374 action.performAction();
1375 refreshConversationList();
Vikram Aggarwal75daee52012-04-30 11:13:09 -07001376 }
Vikram Aggarwal6fbc87a2012-03-15 15:24:00 -07001377
1378 @Override
Vikram Aggarwala55b36c2012-01-13 11:45:02 -08001379 public void onPrepareDialog(int id, Dialog dialog, Bundle bundle) {
1380 // TODO(viki): Auto-generated method stub
Vikram Aggarwala55b36c2012-01-13 11:45:02 -08001381 }
1382
1383 @Override
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -08001384 public boolean onPrepareOptionsMenu(Menu menu) {
Andy Huangd736a382012-08-29 13:08:58 -07001385 return mActionBarView.onPrepareOptionsMenu(menu);
Vikram Aggarwala55b36c2012-01-13 11:45:02 -08001386 }
1387
Mindy Pereira68f2e222012-03-07 10:36:54 -08001388 @Override
1389 public void onPause() {
1390 isLoaderInitialized = false;
Paul Westbrook6ead20d2012-03-19 14:48:14 -07001391 enableNotifications();
Paul Westbrook94e440d2012-02-24 11:03:47 -08001392 }
1393
Vikram Aggarwala55b36c2012-01-13 11:45:02 -08001394 @Override
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -08001395 public void onResume() {
Paul Westbrook6ead20d2012-03-19 14:48:14 -07001396 // Register the receiver that will prevent the status receiver from
1397 // displaying its notification icon as long as we're running.
1398 // The SupressNotificationReceiver will block the broadcast if we're looking at the folder
1399 // that the notification was received for.
1400 disableNotifications();
Andy Huang1ee96b22012-08-24 20:19:53 -07001401
1402 mSafeToModifyFragments = true;
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -08001403 }
1404
1405 @Override
1406 public void onSaveInstanceState(Bundle outState) {
Vikram Aggarwal649b9ea2012-08-27 12:15:20 -07001407 mViewMode.handleSaveInstanceState(outState);
Vikram Aggarwal6c511582012-02-27 10:59:47 -08001408 if (mAccount != null) {
Vikram Aggarwal6c511582012-02-27 10:59:47 -08001409 outState.putParcelable(SAVED_ACCOUNT, mAccount);
1410 }
Mindy Pereira5e478d22012-03-26 18:04:58 -07001411 if (mFolder != null) {
1412 outState.putParcelable(SAVED_FOLDER, mFolder);
Vikram Aggarwal8b152632012-02-03 14:58:45 -08001413 }
Vikram Aggarwalf3341402012-08-07 10:09:38 -07001414 // If this is a search activity, let's store the search query term as well.
1415 if (ConversationListContext.isSearchResult(mConvListContext)) {
1416 outState.putString(SAVED_QUERY, mConvListContext.searchQuery);
1417 }
Vikram Aggarwal49e0e992012-09-21 13:53:15 -07001418 if (mCurrentConversation != null && mViewMode.isConversationMode()) {
Mindy Pereira26f23fc2012-03-27 10:26:04 -07001419 outState.putParcelable(SAVED_CONVERSATION, mCurrentConversation);
1420 }
Andy Huang4556a442012-03-30 16:42:05 -07001421 if (!mSelectedSet.isEmpty()) {
Vikram Aggarwalcabd3f22012-04-19 10:14:41 -07001422 outState.putParcelable(SAVED_SELECTED_SET, mSelectedSet);
Andy Huang4556a442012-03-30 16:42:05 -07001423 }
Mindy Pereirad33674992012-06-25 16:26:30 -07001424 if (mToastBar.getVisibility() == View.VISIBLE) {
1425 outState.putParcelable(SAVED_TOAST_BAR_OP, mToastBar.getOperation());
1426 }
Vikram Aggarwal649b9ea2012-08-27 12:15:20 -07001427 final ConversationListFragment convListFragment = getConversationListFragment();
Mindy Pereirad33674992012-06-25 16:26:30 -07001428 if (convListFragment != null) {
Vikram Aggarwal649b9ea2012-08-27 12:15:20 -07001429 convListFragment.getAnimatedAdapter().onSaveInstanceState(outState);
Mindy Pereirad33674992012-06-25 16:26:30 -07001430 }
Vikram Aggarwalb8c31712013-01-03 17:03:19 -08001431 // If there is a dialog being shown, save the state so we can create a listener for it.
Vikram Aggarwal6cadbfc2012-12-27 09:17:05 -08001432 if (mDialogAction != -1) {
1433 outState.putInt(SAVED_ACTION, mDialogAction);
Vikram Aggarwalb8c31712013-01-03 17:03:19 -08001434 outState.putBoolean(SAVED_ACTION_FROM_SELECTED, mDialogFromSelectedSet);
Vikram Aggarwal6cadbfc2012-12-27 09:17:05 -08001435 }
Vikram Aggarwala91d00b2013-01-18 12:00:37 -08001436 if (mDetachedConvUri != null) {
1437 outState.putParcelable(SAVED_DETACHED_CONV_URI, mDetachedConvUri);
1438 }
Andy Huang1ee96b22012-08-24 20:19:53 -07001439 mSafeToModifyFragments = false;
Vikram Aggarwal649b9ea2012-08-27 12:15:20 -07001440 outState.putString(SAVED_HIERARCHICAL_FOLDER,
1441 (mFolderListFolder != null) ? Folder.toString(mFolderListFolder) : null);
Andy Huang1ee96b22012-08-24 20:19:53 -07001442 }
1443
1444 /**
1445 * @see #mSafeToModifyFragments
1446 */
1447 protected boolean safeToModifyFragments() {
1448 return mSafeToModifyFragments;
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -08001449 }
1450
1451 @Override
Mindy Pereira68f2e222012-03-07 10:36:54 -08001452 public void onSearchRequested(String query) {
1453 Intent intent = new Intent();
1454 intent.setAction(Intent.ACTION_SEARCH);
1455 intent.putExtra(ConversationListContext.EXTRA_SEARCH_QUERY, query);
1456 intent.putExtra(Utils.EXTRA_ACCOUNT, mAccount);
1457 intent.setComponent(mActivity.getComponentName());
Vikram Aggarwalb17cbc02012-04-06 15:41:46 -07001458 mActionBarView.collapseSearch();
Mindy Pereira68f2e222012-03-07 10:36:54 -08001459 mActivity.startActivity(intent);
Vikram Aggarwala55b36c2012-01-13 11:45:02 -08001460 }
1461
1462 @Override
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -08001463 public void onStop() {
Mindy Pereira49e5dbe2012-07-12 11:47:54 -07001464 if (mEnableShareIntents != null) {
1465 mEnableShareIntents.cancel(true);
1466 }
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -08001467 }
1468
Andy Huang632721e2012-04-11 16:57:26 -07001469 @Override
1470 public void onDestroy() {
Andy Huangb2ef9c12012-12-18 12:58:41 -06001471 // stop listening to the cursor on e.g. configuration changes
1472 if (mConversationListCursor != null) {
1473 mConversationListCursor.removeListener(this);
1474 }
Andy Huang632721e2012-04-11 16:57:26 -07001475 // unregister the ViewPager's observer on the conversation cursor
1476 mPagerController.onDestroy();
Mindy Pereira641de652012-08-02 15:21:50 -07001477 mActionBarView.onDestroy();
Vikram Aggarwal7c401b72012-08-13 16:43:47 -07001478 mRecentFolderList.destroy();
Andy Huang4e0158f2012-08-07 21:06:01 -07001479 mDestroyed = true;
Andy Huang632721e2012-04-11 16:57:26 -07001480 }
1481
Vikram Aggarwalfa131a22012-02-02 13:56:22 -08001482 /**
Vikram Aggarwal93dc2022012-11-05 13:36:57 -08001483 * Set the Action Bar icon according to the mode. The Action Bar icon can contain a back button
1484 * or not. The individual controller is responsible for changing the icon based on the mode.
1485 */
1486 protected abstract void resetActionBarIcon();
1487
1488 /**
Mindy Pereira161f50d2012-02-28 15:47:19 -08001489 * {@inheritDoc} Subclasses must override this to listen to mode changes
1490 * from the ViewMode. Subclasses <b>must</b> call the parent's
1491 * onViewModeChanged since the parent will handle common state changes.
Vikram Aggarwalfa131a22012-02-02 13:56:22 -08001492 */
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -08001493 @Override
Vikram Aggarwalfa131a22012-02-02 13:56:22 -08001494 public void onViewModeChanged(int newMode) {
Vikram Aggarwal49e0e992012-09-21 13:53:15 -07001495 // When we step away from the conversation mode, we don't have a current conversation
1496 // anymore. Let's blank it out so clients calling getCurrentConversation are not misled.
1497 if (!ViewMode.isConversationMode(newMode)) {
1498 setCurrentConversation(null);
1499 }
Vikram Aggarwal93dc2022012-11-05 13:36:57 -08001500 // If the viewmode is not set, preserve existing icon.
1501 if (newMode != ViewMode.UNKNOWN) {
1502 resetActionBarIcon();
1503 }
Vikram Aggarwala55b36c2012-01-13 11:45:02 -08001504 }
1505
Andy Huang3825f3d2012-08-29 16:44:12 -07001506 public void disablePagerUpdates() {
1507 mPagerController.stopListening();
1508 }
1509
Andy Huang4e0158f2012-08-07 21:06:01 -07001510 public boolean isDestroyed() {
1511 return mDestroyed;
1512 }
1513
mindyp54f120f2012-08-28 13:10:33 -07001514 @Override
1515 public void commitDestructiveActions(boolean animate) {
mindypc6adce32012-08-22 18:46:42 -07001516 ConversationListFragment fragment = getConversationListFragment();
Mindy Pereira1e2573b2012-04-17 14:34:36 -07001517 if (fragment != null) {
mindypc6adce32012-08-22 18:46:42 -07001518 fragment.commitDestructiveActions(animate);
Mindy Pereira1e2573b2012-04-17 14:34:36 -07001519 }
1520 }
1521
Vikram Aggarwala55b36c2012-01-13 11:45:02 -08001522 @Override
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -08001523 public void onWindowFocusChanged(boolean hasFocus) {
Vikram Aggarwal69b5c302012-09-05 11:11:13 -07001524 final ConversationListFragment convList = getConversationListFragment();
Vikram Aggarwal6422b8d2013-01-14 10:47:41 -08001525 // hasFocus already ensures that the window is in focus, so we don't need to call
1526 // AAC.isFragmentVisible(convList) here.
Paul Westbrook9f119c72012-04-24 16:10:59 -07001527 if (hasFocus && convList != null && convList.isVisible()) {
1528 // The conversation list is visible.
Vikram Aggarwal69b5c302012-09-05 11:11:13 -07001529 informCursorVisiblity(true);
Paul Westbrook9f119c72012-04-24 16:10:59 -07001530 }
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -08001531 }
1532
Vikram Aggarwalca716f12012-08-20 11:11:48 -07001533 /**
1534 * Set the account, and carry out all the account-related changes that rely on this.
1535 * @param account
1536 */
Mindy Pereira75181e82012-04-18 08:17:13 -07001537 private void setAccount(Account account) {
Andy Huangb9ca9792012-05-18 15:31:49 -07001538 if (account == null) {
1539 LogUtils.w(LOG_TAG, new Error(),
1540 "AAC ignoring null (presumably invalid) account restoration");
1541 return;
1542 }
Andy Huangb1148412012-05-19 00:16:30 -07001543 LogUtils.d(LOG_TAG, "AbstractActivityController.setAccount(): account = %s", account.uri);
Vikram Aggarwal91e87372012-05-18 15:36:04 -07001544 mAccount = account;
Vikram Aggarwalca716f12012-08-20 11:11:48 -07001545 // Only change AAC state here. Do *not* modify any other object's state. The object
1546 // should listen on account changes.
1547 restartOptionalLoader(LOADER_RECENT_FOLDERS);
1548 mActivity.invalidateOptionsMenu();
1549 disableNotificationsOnAccountChange(mAccount);
1550 restartOptionalLoader(LOADER_ACCOUNT_UPDATE_CURSOR);
Vikram Aggarwal2fe343f2013-01-14 09:00:25 -08001551 // The Mail instance can be null during test runs.
1552 final MailAppProvider instance = MailAppProvider.getInstance();
1553 if (instance != null) {
1554 instance.setLastViewedAccount(mAccount.uri.toString());
1555 }
Vikram Aggarwal91e87372012-05-18 15:36:04 -07001556 if (account.settings == null) {
1557 LogUtils.w(LOG_TAG, new Error(), "AAC ignoring account with null settings.");
1558 return;
1559 }
Vikram Aggarwal7c401b72012-08-13 16:43:47 -07001560 mAccountObservers.notifyChanged();
Vikram Aggarwal34f7b232012-10-17 13:32:23 -07001561 perhapsEnterWaitMode();
Mindy Pereira75181e82012-04-18 08:17:13 -07001562 }
1563
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -08001564 /**
Mindy Pereira161f50d2012-02-28 15:47:19 -08001565 * Restore the state from the previous bundle. Subclasses should call this
1566 * method from the parent class, since it performs important UI
1567 * initialization.
1568 *
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -08001569 * @param savedState
1570 */
Andy Huang632721e2012-04-11 16:57:26 -07001571 @Override
1572 public void onRestoreInstanceState(Bundle savedState) {
Vikram Aggarwala91d00b2013-01-18 12:00:37 -08001573 mDetachedConvUri = savedState.getParcelable(SAVED_DETACHED_CONV_URI);
Andy Huang632721e2012-04-11 16:57:26 -07001574 if (savedState.containsKey(SAVED_CONVERSATION)) {
1575 // Open the conversation.
Andy Huang2bc8bc12012-11-12 17:24:25 -08001576 final Conversation conversation = savedState.getParcelable(SAVED_CONVERSATION);
Paul Westbrook534e4a22012-04-25 03:46:29 -07001577 if (conversation != null && conversation.position < 0) {
1578 // Set the position to 0 on this conversation, as we don't know where it is
1579 // in the list
1580 conversation.position = 0;
1581 }
Andy Huanged4fdf02012-07-26 17:12:50 -07001582 showConversation(conversation);
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -08001583 }
Mindy Pereira967ede62012-03-22 09:29:09 -07001584
Mindy Pereirad33674992012-06-25 16:26:30 -07001585 if (savedState.containsKey(SAVED_TOAST_BAR_OP)) {
Andy Huang2bc8bc12012-11-12 17:24:25 -08001586 ToastBarOperation op = savedState.getParcelable(SAVED_TOAST_BAR_OP);
Mindy Pereirad33674992012-06-25 16:26:30 -07001587 if (op != null) {
1588 if (op.getType() == ToastBarOperation.UNDO) {
1589 onUndoAvailable(op);
Andrew Sapperstein9d7519d2012-07-16 14:03:53 -07001590 } else if (op.getType() == ToastBarOperation.ERROR) {
1591 onError(mFolder, true);
Mindy Pereirad33674992012-06-25 16:26:30 -07001592 }
1593 }
1594 }
Vikram Aggarwal649b9ea2012-08-27 12:15:20 -07001595 final String folderString = savedState.getString(SAVED_HIERARCHICAL_FOLDER, null);
1596 if (!TextUtils.isEmpty(folderString)) {
1597 mFolderListFolder = Folder.fromString(folderString);
1598 }
1599 final ConversationListFragment convListFragment = getConversationListFragment();
Mindy Pereirad33674992012-06-25 16:26:30 -07001600 if (convListFragment != null) {
Vikram Aggarwal649b9ea2012-08-27 12:15:20 -07001601 convListFragment.getAnimatedAdapter().onRestoreInstanceState(savedState);
Mindy Pereirad33674992012-06-25 16:26:30 -07001602 }
Vikram Aggarwalb8c31712013-01-03 17:03:19 -08001603 /*
Mindy Pereira967ede62012-03-22 09:29:09 -07001604 * Restore the state of selected conversations. This needs to be done after the correct mode
1605 * is set and the action bar is fully initialized. If not, several key pieces of state
1606 * information will be missing, and the split views may not be initialized correctly.
Mindy Pereira967ede62012-03-22 09:29:09 -07001607 */
Andy Huang4556a442012-03-30 16:42:05 -07001608 restoreSelectedConversations(savedState);
Vikram Aggarwalb8c31712013-01-03 17:03:19 -08001609 // Order is important!!!
1610 // The dialog listener needs to happen *after* the selected set is restored.
1611
1612 // If there has been an orientation change, and we need to recreate the listener for the
1613 // confirm dialog fragment (delete/archive/...), then do it here.
1614 if (mDialogAction != -1) {
1615 makeDialogListener(mDialogAction, mDialogFromSelectedSet);
1616 }
Andy Huang632721e2012-04-11 16:57:26 -07001617 }
1618
Vikram Aggarwal649b9ea2012-08-27 12:15:20 -07001619 /**
1620 * Handle an intent to open the app. This method is called only when there is no saved state,
1621 * so we need to set state that wasn't set before. It is correct to change the viewmode here
1622 * since it has not been previously set.
1623 * @param intent
1624 */
Andy Huang632721e2012-04-11 16:57:26 -07001625 private void handleIntent(Intent intent) {
1626 boolean handled = false;
1627 if (Intent.ACTION_VIEW.equals(intent.getAction())) {
1628 if (intent.hasExtra(Utils.EXTRA_ACCOUNT)) {
Vikram Aggarwal649b9ea2012-08-27 12:15:20 -07001629 setAccount(Account.newinstance(intent.getStringExtra(Utils.EXTRA_ACCOUNT)));
Andy Huang632721e2012-04-11 16:57:26 -07001630 }
Andy Huangb9ca9792012-05-18 15:31:49 -07001631 if (mAccount == null) {
1632 return;
Andy Huang632721e2012-04-11 16:57:26 -07001633 }
Vikram Aggarwal1a249e02012-08-03 16:19:33 -07001634 final boolean isConversationMode = intent.hasExtra(Utils.EXTRA_CONVERSATION);
Vikram Aggarwal649b9ea2012-08-27 12:15:20 -07001635 if (isConversationMode && mViewMode.getMode() == ViewMode.UNKNOWN) {
Vikram Aggarwal1a249e02012-08-03 16:19:33 -07001636 mViewMode.enterConversationMode();
1637 } else {
1638 mViewMode.enterConversationListMode();
1639 }
Vikram Aggarwal1672ff82012-09-21 10:15:22 -07001640 final Folder folder = intent.hasExtra(Utils.EXTRA_FOLDER) ?
1641 Folder.fromString(intent.getStringExtra(Utils.EXTRA_FOLDER)) : null;
Andy Huang632721e2012-04-11 16:57:26 -07001642 if (folder != null) {
1643 onFolderChanged(folder);
1644 handled = true;
1645 }
1646
Vikram Aggarwal1a249e02012-08-03 16:19:33 -07001647 if (isConversationMode) {
Andy Huang632721e2012-04-11 16:57:26 -07001648 // Open the conversation.
1649 LogUtils.d(LOG_TAG, "SHOW THE CONVERSATION at %s",
1650 intent.getParcelableExtra(Utils.EXTRA_CONVERSATION));
Paul Westbrooka9161912012-04-24 10:10:04 -07001651 final Conversation conversation =
Andy Huang2bc8bc12012-11-12 17:24:25 -08001652 intent.getParcelableExtra(Utils.EXTRA_CONVERSATION);
Paul Westbrooka9161912012-04-24 10:10:04 -07001653 if (conversation != null && conversation.position < 0) {
1654 // Set the position to 0 on this conversation, as we don't know where it is
1655 // in the list
1656 conversation.position = 0;
1657 }
Andy Huang980aaea2012-07-26 17:22:19 -07001658 showConversation(conversation);
Andy Huang632721e2012-04-11 16:57:26 -07001659 handled = true;
1660 }
1661
1662 if (!handled) {
Vikram Aggarwal0c3c2052012-09-21 11:06:28 -07001663 // We have an account, but nothing else: load the default inbox.
Andy Huang632721e2012-04-11 16:57:26 -07001664 loadAccountInbox();
1665 }
1666 } else if (Intent.ACTION_SEARCH.equals(intent.getAction())) {
1667 if (intent.hasExtra(Utils.EXTRA_ACCOUNT)) {
Vikram Aggarwalf0ef2302012-11-07 14:53:34 -08001668 mHaveSearchResults = false;
Andy Huang632721e2012-04-11 16:57:26 -07001669 // Save this search query for future suggestions.
1670 final String query = intent.getStringExtra(SearchManager.QUERY);
1671 final String authority = mContext.getString(R.string.suggestions_authority);
Vikram Aggarwal2b703c62012-09-18 13:54:15 -07001672 final SearchRecentSuggestions suggestions = new SearchRecentSuggestions(
Andy Huang632721e2012-04-11 16:57:26 -07001673 mContext, authority, SuggestionsProvider.MODE);
1674 suggestions.saveRecentQuery(query, null);
Vikram Aggarwalf0ef2302012-11-07 14:53:34 -08001675 setAccount((Account) intent.getParcelableExtra(Utils.EXTRA_ACCOUNT));
1676 fetchSearchFolder(intent);
1677 if (shouldEnterSearchConvMode()) {
Mindy Pereiraac254822012-06-18 10:46:43 -07001678 mViewMode.enterSearchResultsConversationMode();
1679 } else {
1680 mViewMode.enterSearchResultsListMode();
1681 }
Andy Huang632721e2012-04-11 16:57:26 -07001682 } else {
1683 LogUtils.e(LOG_TAG, "Missing account extra from search intent. Finishing");
1684 mActivity.finish();
1685 }
1686 }
1687 if (mAccount != null) {
1688 restartOptionalLoader(LOADER_ACCOUNT_UPDATE_CURSOR);
1689 }
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -08001690 }
1691
Andy Huang4556a442012-03-30 16:42:05 -07001692 /**
Vikram Aggarwalf0ef2302012-11-07 14:53:34 -08001693 * Returns true if we should enter conversation mode with search.
1694 */
1695 protected final boolean shouldEnterSearchConvMode() {
1696 return mHaveSearchResults && Utils.showTwoPaneSearchResults(mActivity.getActivityContext());
1697 }
1698
1699 /**
Andy Huang4556a442012-03-30 16:42:05 -07001700 * Copy any selected conversations stored in the saved bundle into our selection set,
1701 * triggering {@link ConversationSetObserver} callbacks as our selection set changes.
1702 *
1703 */
Vikram Aggarwal4eb52712012-06-19 16:24:50 -07001704 private final void restoreSelectedConversations(Bundle savedState) {
Mindy Pereira967ede62012-03-22 09:29:09 -07001705 if (savedState == null) {
Andy Huang4556a442012-03-30 16:42:05 -07001706 mSelectedSet.clear();
Mindy Pereira967ede62012-03-22 09:29:09 -07001707 return;
1708 }
Vikram Aggarwalcabd3f22012-04-19 10:14:41 -07001709 final ConversationSelectionSet selectedSet = savedState.getParcelable(SAVED_SELECTED_SET);
Andy Huang4556a442012-03-30 16:42:05 -07001710 if (selectedSet == null || selectedSet.isEmpty()) {
1711 mSelectedSet.clear();
Mindy Pereira967ede62012-03-22 09:29:09 -07001712 return;
1713 }
Andy Huang632721e2012-04-11 16:57:26 -07001714
1715 // putAll will take care of calling our registered onSetPopulated method
Vikram Aggarwalcabd3f22012-04-19 10:14:41 -07001716 mSelectedSet.putAll(selectedSet);
Mindy Pereira967ede62012-03-22 09:29:09 -07001717 }
1718
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -08001719 @Override
Andy Huang5895f7b2012-06-01 17:07:20 -07001720 public SubjectDisplayChanger getSubjectDisplayChanger() {
1721 return mActionBarView;
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -08001722 }
1723
Vikram Aggarwal17f373e2012-09-17 15:49:32 -07001724 private final void showConversation(Conversation conversation) {
Andy Huang1ee96b22012-08-24 20:19:53 -07001725 showConversation(conversation, false /* inLoaderCallbacks */);
1726 }
1727
Vikram Aggarwalec5cbf72012-03-08 15:10:35 -08001728 /**
Vikram Aggarwal49e0e992012-09-21 13:53:15 -07001729 * Show the conversation provided in the arguments. It is safe to pass a null conversation
1730 * object, which is a signal to back out of conversation view mode.
1731 * Child classes must call super.showConversation() <b>before</b> their own implementations.
1732 * @param conversation
1733 * @param inLoaderCallbacks true if the method is called as a result of
1734 * {@link #onLoadFinished(Loader, Cursor)}
Vikram Aggarwalec5cbf72012-03-08 15:10:35 -08001735 */
Andy Huang1ee96b22012-08-24 20:19:53 -07001736 protected void showConversation(Conversation conversation, boolean inLoaderCallbacks) {
Vikram Aggarwalc67182d2012-04-03 14:35:06 -07001737 // Set the current conversation just in case it wasn't already set.
1738 setCurrentConversation(conversation);
Vikram Aggarwal0f142732012-08-24 09:39:34 -07001739 // Add the folder that we were viewing to the recent folders list.
1740 // TODO: this may need to be fine tuned. If this is the signal that is indicating that
1741 // the list is shown to the user, this could fire in one pane if the user goes directly
1742 // to a conversation
1743 updateRecentFolderList();
Vikram Aggarwalec5cbf72012-03-08 15:10:35 -08001744 }
1745
Vikram Aggarwale128fc22012-04-04 12:33:34 -07001746 /**
Paul Westbrook2d50bcd2012-04-10 11:53:47 -07001747 * Children can override this method, but they must call super.showWaitForInitialization().
1748 * {@inheritDoc}
1749 */
1750 @Override
1751 public void showWaitForInitialization() {
1752 mViewMode.enterWaitingForInitializationMode();
Vikram Aggarwala3f43d42012-10-25 16:21:30 -07001753 mWaitFragment = WaitFragment.newInstance(mAccount);
Paul Westbrook2d50bcd2012-04-10 11:53:47 -07001754 }
1755
Vikram Aggarwaldd6a7ce2012-10-22 15:45:57 -07001756 private void updateWaitMode() {
Paul Westbrook2d50bcd2012-04-10 11:53:47 -07001757 final FragmentManager manager = mActivity.getFragmentManager();
1758 final WaitFragment waitFragment =
Vikram Aggarwal6902dcf2012-04-11 08:57:42 -07001759 (WaitFragment)manager.findFragmentByTag(TAG_WAIT);
Paul Westbrook2d50bcd2012-04-10 11:53:47 -07001760 if (waitFragment != null) {
1761 waitFragment.updateAccount(mAccount);
1762 }
1763 }
1764
Vikram Aggarwala3f43d42012-10-25 16:21:30 -07001765 /**
1766 * Remove the "Waiting for Initialization" fragment. Child classes are free to override this
1767 * method, though they must call the parent implementation <b>after</b> they do anything.
1768 */
1769 protected void hideWaitForInitialization() {
1770 mWaitFragment = null;
1771 }
1772
1773 /**
1774 * Use the instance variable and the wait fragment's tag to get the wait fragment. This is
1775 * far superior to using the value of mWaitFragment, which might be invalid or might refer
1776 * to a fragment after it has been destroyed.
1777 * @return
1778 */
1779 protected final WaitFragment getWaitFragment() {
1780 final FragmentManager manager = mActivity.getFragmentManager();
1781 final WaitFragment waitFrag = (WaitFragment) manager.findFragmentByTag(TAG_WAIT);
1782 if (waitFrag != null) {
1783 // The Fragment Manager knows better, so use its instance.
1784 mWaitFragment = waitFrag;
1785 }
1786 return mWaitFragment;
1787 }
Vikram Aggarwaldd6a7ce2012-10-22 15:45:57 -07001788
Vikram Aggarwal48b2a6c2012-05-29 14:09:27 -07001789 /**
1790 * Returns true if we are waiting for the account to sync, and cannot show any folders or
1791 * conversation for the current account yet.
Vikram Aggarwal48b2a6c2012-05-29 14:09:27 -07001792 */
Vikram Aggarwaldd6a7ce2012-10-22 15:45:57 -07001793 private boolean inWaitMode() {
Vikram Aggarwala3f43d42012-10-25 16:21:30 -07001794 final WaitFragment waitFragment = getWaitFragment();
Paul Westbrook2d50bcd2012-04-10 11:53:47 -07001795 if (waitFragment != null) {
1796 final Account fragmentAccount = waitFragment.getAccount();
Paul Westbrook339004b2012-11-05 17:13:51 -08001797 return fragmentAccount != null && fragmentAccount.uri.equals(mAccount.uri) &&
Paul Westbrook2d50bcd2012-04-10 11:53:47 -07001798 mViewMode.getMode() == ViewMode.WAITING_FOR_ACCOUNT_INITIALIZATION;
1799 }
1800 return false;
1801 }
1802
1803 /**
Vikram Aggarwale128fc22012-04-04 12:33:34 -07001804 * Children can override this method, but they must call super.showConversationList().
1805 * {@inheritDoc}
1806 */
1807 @Override
1808 public void showConversationList(ConversationListContext listContext) {
1809 }
1810
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -08001811 @Override
Vikram Aggarwal17f373e2012-09-17 15:49:32 -07001812 public final void onConversationSelected(Conversation conversation, boolean inLoaderCallbacks) {
mindypaa55bc92012-08-24 09:49:56 -07001813 // Only animate destructive actions if we are going to be showing the
1814 // conversation list when we show the next conversation.
Vikram Aggarwalbcb16b92013-01-28 18:05:03 -08001815 commitDestructiveActions(mIsTablet);
Andy Huang1ee96b22012-08-24 20:19:53 -07001816 showConversation(conversation, inLoaderCallbacks);
1817 }
1818
1819 @Override
1820 public Conversation getCurrentConversation() {
1821 return mCurrentConversation;
Vikram Aggarwal7d602882012-02-07 15:01:20 -08001822 }
Mindy Pereira555140c2012-02-15 14:55:29 -08001823
Vikram Aggarwalc67182d2012-04-03 14:35:06 -07001824 /**
1825 * Set the current conversation. This is the conversation on which all actions are performed.
1826 * Do not modify mCurrentConversation except through this method, which makes it easy to
1827 * perform common actions associated with changing the current conversation.
1828 * @param conversation
1829 */
Andy Huang632721e2012-04-11 16:57:26 -07001830 @Override
1831 public void setCurrentConversation(Conversation conversation) {
Vikram Aggarwala91d00b2013-01-18 12:00:37 -08001832 // The controller should come out of detached mode if a new conversation is viewed, or if
1833 if (conversation == null || (mDetachedConvUri != null
1834 && !mDetachedConvUri.equals(conversation.uri))) {
1835 clearDetachedMode();
1836 }
1837
1838 // Must happen *before* setting mCurrentConversation because this sets
1839 // conversation.position if a cursor is available.
Andy Huang8883f222012-11-12 19:25:00 -08001840 mTracker.initialize(conversation);
Vikram Aggarwal17f373e2012-09-17 15:49:32 -07001841 mCurrentConversation = conversation;
Andy Huang7d646122012-09-05 19:41:44 -07001842
1843 if (mCurrentConversation != null) {
Yorke Leef807ba72012-09-20 17:18:05 -07001844 mActionBarView.setCurrentConversation(mCurrentConversation);
Vikram Aggarwalf98db952013-01-17 11:19:41 -08001845 mActionBarView.setSubject(mCurrentConversation.subject);
Yorke Leef807ba72012-09-20 17:18:05 -07001846 mActivity.invalidateOptionsMenu();
Andy Huang7d646122012-09-05 19:41:44 -07001847 }
Mindy Pereira5040f1a2012-03-20 10:14:06 -07001848 }
1849
Vikram Aggarwala9b93f32012-02-23 14:51:58 -08001850 /**
1851 * {@inheritDoc}
1852 */
Mindy Pereira555140c2012-02-15 14:55:29 -08001853 @Override
Vikram Aggarwal7dedb952012-02-16 16:10:23 -08001854 public Loader<Cursor> onCreateLoader(int id, Bundle args) {
Vikram Aggarwalfa4b47e2012-03-09 13:02:46 -08001855 switch (id) {
1856 case LOADER_ACCOUNT_CURSOR:
Paul Westbrookc2074c42012-03-22 15:26:58 -07001857 return new CursorLoader(mContext, MailAppProvider.getAccountsUri(),
Vikram Aggarwalfa4b47e2012-03-09 13:02:46 -08001858 UIProvider.ACCOUNTS_PROJECTION, null, null, null);
1859 case LOADER_FOLDER_CURSOR:
Paul Westbrookc7a070f2012-04-12 01:46:41 -07001860 final CursorLoader loader = new CursorLoader(mContext, mFolder.uri,
1861 UIProvider.FOLDERS_PROJECTION, null, null, null);
1862 loader.setUpdateThrottle(mFolderItemUpdateDelayMs);
1863 return loader;
Vikram Aggarwalfa4b47e2012-03-09 13:02:46 -08001864 case LOADER_RECENT_FOLDERS:
Paul Westbrook91d10502012-04-13 12:01:39 -07001865 if (mAccount != null && mAccount.recentFolderListUri != null) {
Paul Westbrookea4ee202012-03-12 14:12:33 -07001866 return new CursorLoader(mContext, mAccount.recentFolderListUri,
1867 UIProvider.FOLDERS_PROJECTION, null, null, null);
1868 }
1869 break;
Mindy Pereiraab486362012-03-21 18:18:53 -07001870 case LOADER_ACCOUNT_INBOX:
Vikram Aggarwal025eba82012-05-08 10:45:30 -07001871 final Uri defaultInbox = Settings.getDefaultInboxUri(mAccount.settings);
Vikram Aggarwal1e57e672012-05-07 14:48:24 -07001872 final Uri inboxUri = defaultInbox.equals(Uri.EMPTY) ?
1873 mAccount.folderListUri : defaultInbox;
Paul Westbrook7496e822012-04-24 09:50:54 -07001874 LogUtils.d(LOG_TAG, "Loading the default inbox: %s", inboxUri);
Paul Westbrook1220b6d2012-04-10 00:48:00 -07001875 if (inboxUri != null) {
1876 return new CursorLoader(mContext, inboxUri, UIProvider.FOLDERS_PROJECTION, null,
1877 null, null);
1878 }
1879 break;
Mindy Pereiraab486362012-03-21 18:18:53 -07001880 case LOADER_SEARCH:
1881 return Folder.forSearchResults(mAccount,
1882 args.getString(ConversationListContext.EXTRA_SEARCH_QUERY),
1883 mActivity.getActivityContext());
Paul Westbrook2d50bcd2012-04-10 11:53:47 -07001884 case LOADER_ACCOUNT_UPDATE_CURSOR:
1885 return new CursorLoader(mContext, mAccount.uri, UIProvider.ACCOUNTS_PROJECTION,
1886 null, null, null);
Vikram Aggarwalfa4b47e2012-03-09 13:02:46 -08001887 default:
Paul Westbrookad6a2752012-04-04 16:58:13 -07001888 LogUtils.wtf(LOG_TAG, "Loader returned unexpected id: %d", id);
Mindy Pereira5cb0c3e2012-02-22 15:22:47 -08001889 }
1890 return null;
Vikram Aggarwal7dedb952012-02-16 16:10:23 -08001891 }
1892
Paul Westbrookb1f573c2012-04-06 11:38:28 -07001893 @Override
1894 public void onLoaderReset(Loader<Cursor> loader) {
1895
1896 }
1897
Andy Huangf9a73482012-03-13 15:54:02 -07001898 /**
1899 * {@link LoaderManager} currently has a bug in
1900 * {@link LoaderManager#restartLoader(int, Bundle, android.app.LoaderManager.LoaderCallbacks)}
1901 * where, if a previous onCreateLoader returned a null loader, this method will NPE. Work around
1902 * this bug by destroying any loaders that may have been created as null (essentially because
1903 * they are optional loads, and may not apply to a particular account).
1904 * <p>
1905 * A simple null check before restarting a loader will not work, because that would not
1906 * give the controller a chance to invalidate UI corresponding the prior loader result.
1907 *
1908 * @param id loader ID to safely restart
Andy Huangf9a73482012-03-13 15:54:02 -07001909 */
Vikram Aggarwal94c94de2012-04-04 15:38:28 -07001910 private void restartOptionalLoader(int id) {
Andy Huangf9a73482012-03-13 15:54:02 -07001911 final LoaderManager lm = mActivity.getLoaderManager();
1912 lm.destroyLoader(id);
Vikram Aggarwal94c94de2012-04-04 15:38:28 -07001913 lm.restartLoader(id, Bundle.EMPTY, this);
1914 }
1915
Andy Huang632721e2012-04-11 16:57:26 -07001916 @Override
1917 public void registerConversationListObserver(DataSetObserver observer) {
1918 mConversationListObservable.registerObserver(observer);
1919 }
1920
1921 @Override
1922 public void unregisterConversationListObserver(DataSetObserver observer) {
1923 mConversationListObservable.unregisterObserver(observer);
1924 }
1925
Andy Huang090db1e2012-07-25 13:25:28 -07001926 @Override
1927 public void registerFolderObserver(DataSetObserver observer) {
1928 mFolderObservable.registerObserver(observer);
1929 }
1930
1931 @Override
1932 public void unregisterFolderObserver(DataSetObserver observer) {
1933 mFolderObservable.unregisterObserver(observer);
1934 }
1935
Andy Huang9d3fd922012-09-26 22:23:58 -07001936 @Override
1937 public void registerConversationLoadedObserver(DataSetObserver observer) {
1938 mPagerController.registerConversationLoadedObserver(observer);
1939 }
1940
1941 @Override
1942 public void unregisterConversationLoadedObserver(DataSetObserver observer) {
1943 mPagerController.unregisterConversationLoadedObserver(observer);
1944 }
1945
Vikram Aggarwal60069912012-07-24 14:26:09 -07001946 /**
1947 * Returns true if the number of accounts is different, or if the current account has been
1948 * removed from the device
1949 * @param accountCursor
1950 * @return
1951 */
Paul Westbrook23b74b92012-02-29 11:36:12 -08001952 private boolean accountsUpdated(Cursor accountCursor) {
1953 // Check to see if the current account hasn't been set, or the account cursor is empty
1954 if (mAccount == null || !accountCursor.moveToFirst()) {
Vikram Aggarwal6c511582012-02-27 10:59:47 -08001955 return true;
Paul Westbrook23b74b92012-02-29 11:36:12 -08001956 }
1957
1958 // Check to see if the number of accounts are different, from the number we saw on the last
1959 // updated
1960 if (mCurrentAccountUris.size() != accountCursor.getCount()) {
1961 return true;
1962 }
1963
1964 // Check to see if the account list is different or if the current account is not found in
1965 // the cursor.
1966 boolean foundCurrentAccount = false;
Vikram Aggarwal7dedb952012-02-16 16:10:23 -08001967 do {
Paul Westbrook23b74b92012-02-29 11:36:12 -08001968 final Uri accountUri =
1969 Uri.parse(accountCursor.getString(UIProvider.ACCOUNT_URI_COLUMN));
1970 if (!foundCurrentAccount && mAccount.uri.equals(accountUri)) {
1971 foundCurrentAccount = true;
1972 }
Vikram Aggarwal60069912012-07-24 14:26:09 -07001973 // Is there a new account that we do not know about?
Paul Westbrook23b74b92012-02-29 11:36:12 -08001974 if (!mCurrentAccountUris.contains(accountUri)) {
1975 return true;
1976 }
Vikram Aggarwal7dedb952012-02-16 16:10:23 -08001977 } while (accountCursor.moveToNext());
Paul Westbrook23b74b92012-02-29 11:36:12 -08001978
1979 // As long as we found the current account, the list hasn't been updated
1980 return !foundCurrentAccount;
Vikram Aggarwal7dedb952012-02-16 16:10:23 -08001981 }
1982
1983 /**
Vikram Aggarwal60069912012-07-24 14:26:09 -07001984 * Updates accounts for the app. If the current account is missing, the first
1985 * account in the list is set to the current account (we <em>have</em> to choose something).
Mindy Pereira161f50d2012-02-28 15:47:19 -08001986 *
Vikram Aggarwal6c511582012-02-27 10:59:47 -08001987 * @param accounts cursor into the AccountCache
Vikram Aggarwal7dedb952012-02-16 16:10:23 -08001988 * @return true if the update was successful, false otherwise
1989 */
Vikram Aggarwal60069912012-07-24 14:26:09 -07001990 private boolean updateAccounts(Cursor accounts) {
Vikram Aggarwal7dedb952012-02-16 16:10:23 -08001991 if (accounts == null || !accounts.moveToFirst()) {
1992 return false;
1993 }
Paul Westbrook23b74b92012-02-29 11:36:12 -08001994
Vikram Aggarwala9b93f32012-02-23 14:51:58 -08001995 final Account[] allAccounts = Account.getAllAccounts(accounts);
Vikram Aggarwal60069912012-07-24 14:26:09 -07001996 // A match for the current account's URI in the list of accounts.
1997 Account currentFromList = null;
Paul Westbrook23b74b92012-02-29 11:36:12 -08001998
Vikram Aggarwal636c1a12012-09-22 16:02:40 -07001999 // Save the uris for the accounts and find the current account in the updated cursor.
Paul Westbrook23b74b92012-02-29 11:36:12 -08002000 mCurrentAccountUris.clear();
Vikram Aggarwal636c1a12012-09-22 16:02:40 -07002001 for (final Account account : allAccounts) {
Vikram Aggarwal60069912012-07-24 14:26:09 -07002002 LogUtils.d(LOG_TAG, "updateAccounts(%s)", account);
Paul Westbrook23b74b92012-02-29 11:36:12 -08002003 mCurrentAccountUris.add(account.uri);
Vikram Aggarwal60069912012-07-24 14:26:09 -07002004 if (mAccount != null && account.uri.equals(mAccount.uri)) {
2005 currentFromList = account;
2006 }
Paul Westbrook23b74b92012-02-29 11:36:12 -08002007 }
2008
Vikram Aggarwal60069912012-07-24 14:26:09 -07002009 // 1. current account is already set and is in allAccounts:
2010 // 1a. It has changed -> load the updated account.
2011 // 2b. It is unchanged -> no-op
Andy Huang0d647352012-03-21 21:48:16 -07002012 // 2. current account is set and is not in allAccounts -> pick first (acct was deleted?)
Vikram Aggarwal60069912012-07-24 14:26:09 -07002013 // 3. saved preference has an account -> pick that one
Andy Huang0d647352012-03-21 21:48:16 -07002014 // 4. otherwise just pick first
2015
Vikram Aggarwal60069912012-07-24 14:26:09 -07002016 boolean accountChanged = false;
2017 /// Assume case 4, initialize to first account, and see if we can find anything better.
2018 Account newAccount = allAccounts[0];
2019 if (currentFromList != null) {
2020 // Case 1: Current account exists but has changed
2021 if (!currentFromList.equals(mAccount)) {
2022 newAccount = currentFromList;
2023 accountChanged = true;
Andy Huang0d647352012-03-21 21:48:16 -07002024 }
Vikram Aggarwal60069912012-07-24 14:26:09 -07002025 // Case 1b: else, current account is unchanged: nothing to do.
Paul Westbrook23b74b92012-02-29 11:36:12 -08002026 } else {
Vikram Aggarwal60069912012-07-24 14:26:09 -07002027 // Case 2: Current account is not in allAccounts, the account needs to change.
2028 accountChanged = true;
2029 if (mAccount == null) {
2030 // Case 3: Check for last viewed account, and check if it exists in the list.
2031 final String lastAccountUri = MailAppProvider.getInstance().getLastViewedAccount();
2032 if (lastAccountUri != null) {
2033 for (final Account account : allAccounts) {
2034 if (lastAccountUri.equals(account.uri.toString())) {
2035 newAccount = account;
2036 break;
2037 }
Andy Huang0d647352012-03-21 21:48:16 -07002038 }
2039 }
2040 }
Paul Westbrook23b74b92012-02-29 11:36:12 -08002041 }
Vikram Aggarwal60069912012-07-24 14:26:09 -07002042 if (accountChanged) {
2043 onAccountChanged(newAccount);
2044 }
2045 // Whether we have updated the current account or not, we need to update the list of
2046 // accounts in the ActionBar.
Vikram Aggarwala9b93f32012-02-23 14:51:58 -08002047 mActionBarView.setAccounts(allAccounts);
2048 return (allAccounts.length > 0);
Vikram Aggarwal7dedb952012-02-16 16:10:23 -08002049 }
2050
Paul Westbrook6ead20d2012-03-19 14:48:14 -07002051 private void disableNotifications() {
2052 mNewEmailReceiver.activate(mContext, this);
2053 }
2054
2055 private void enableNotifications() {
2056 mNewEmailReceiver.deactivate();
2057 }
2058
2059 private void disableNotificationsOnAccountChange(Account account) {
2060 // If the new mail suppression receiver is activated for a different account, we want to
2061 // activate it for the new account.
2062 if (mNewEmailReceiver.activated() &&
2063 !mNewEmailReceiver.notificationsDisabledForAccount(account)) {
2064 // Deactivate the current receiver, otherwise multiple receivers may be registered.
2065 mNewEmailReceiver.deactivate();
2066 mNewEmailReceiver.activate(mContext, this);
2067 }
2068 }
2069
Vikram Aggarwala9b93f32012-02-23 14:51:58 -08002070 /**
2071 * {@inheritDoc}
2072 */
Vikram Aggarwal7dedb952012-02-16 16:10:23 -08002073 @Override
2074 public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
Mindy Pereira5cb0c3e2012-02-22 15:22:47 -08002075 // We want to reinitialize only if we haven't ever been initialized, or
2076 // if the current account has vanished.
Paul Westbrooke3e84292012-03-05 16:19:30 -08002077 if (data == null) {
Vikram Aggarwalfa4b47e2012-03-09 13:02:46 -08002078 LogUtils.e(LOG_TAG, "Received null cursor from loader id: %d", loader.getId());
Paul Westbrooke3e84292012-03-05 16:19:30 -08002079 }
Vikram Aggarwalfa4b47e2012-03-09 13:02:46 -08002080 switch (loader.getId()) {
2081 case LOADER_ACCOUNT_CURSOR:
Vikram Aggarwal636c1a12012-09-22 16:02:40 -07002082 if (data == null) {
2083 // Nothing useful to do if we have no valid data.
2084 break;
2085 }
2086 if (data.getCount() == 0) {
Paul Westbrook2388c5d2012-03-25 12:29:11 -07002087 // If an empty cursor is returned, the MailAppProvider is indicating that
2088 // no accounts have been specified. We want to navigate to the "add account"
2089 // activity that will handle the intent returned by the MailAppProvider
2090
2091 // If the MailAppProvider believes that all accounts have been loaded, and the
2092 // account list is still empty, we want to prompt the user to add an account
2093 final Bundle extras = data.getExtras();
2094 final boolean accountsLoaded =
2095 extras.getInt(AccountCursorExtraKeys.ACCOUNTS_LOADED) != 0;
2096
2097 if (accountsLoaded) {
2098 final Intent noAccountIntent = MailAppProvider.getNoAccountIntent(mContext);
2099 if (noAccountIntent != null) {
2100 mActivity.startActivityForResult(noAccountIntent,
2101 ADD_ACCOUNT_REQUEST_CODE);
2102 }
2103 }
2104 } else {
2105 final boolean accountListUpdated = accountsUpdated(data);
2106 if (!isLoaderInitialized || accountListUpdated) {
Vikram Aggarwal60069912012-07-24 14:26:09 -07002107 isLoaderInitialized = updateAccounts(data);
Paul Westbrook2388c5d2012-03-25 12:29:11 -07002108 }
Vikram Aggarwalfa4b47e2012-03-09 13:02:46 -08002109 }
2110 break;
Paul Westbrook2d50bcd2012-04-10 11:53:47 -07002111 case LOADER_ACCOUNT_UPDATE_CURSOR:
2112 // We have gotten an update for current account.
2113
Vikram Aggarwal60069912012-07-24 14:26:09 -07002114 // Make sure that this is an update for the current account
Paul Westbrook2d50bcd2012-04-10 11:53:47 -07002115 if (data != null && data.moveToFirst()) {
2116 final Account updatedAccount = new Account(data);
2117
2118 if (updatedAccount.uri.equals(mAccount.uri)) {
Paul Westbrookca08fc12012-07-31 12:01:15 -07002119 // Keep a reference to the previous settings object
2120 final Settings previousSettings = mAccount.settings;
2121
Vikram Aggarwal7d816002012-04-17 17:06:41 -07002122 // Update the controller's reference to the current account
Paul Westbrook2d50bcd2012-04-10 11:53:47 -07002123 mAccount = updatedAccount;
Vikram Aggarwaledc137c2012-04-24 13:40:58 -07002124 LogUtils.d(LOG_TAG, "AbstractActivityController.onLoadFinished(): "
2125 + "mAccount = %s", mAccount.uri);
Paul Westbrookca08fc12012-07-31 12:01:15 -07002126
2127 // Only notify about a settings change if something differs
2128 if (!Objects.equal(mAccount.settings, previousSettings)) {
Vikram Aggarwal7c401b72012-08-13 16:43:47 -07002129 mAccountObservers.notifyChanged();
Paul Westbrookca08fc12012-07-31 12:01:15 -07002130 }
Vikram Aggarwal34f7b232012-10-17 13:32:23 -07002131 perhapsEnterWaitMode();
Paul Westbrook2d50bcd2012-04-10 11:53:47 -07002132 } else {
2133 LogUtils.e(LOG_TAG, "Got update for account: %s with current account: %s",
2134 updatedAccount.uri, mAccount.uri);
2135 // We need to restart the loader, so the correct account information will
2136 // be returned
2137 restartOptionalLoader(LOADER_ACCOUNT_UPDATE_CURSOR);
2138 }
2139 }
2140 break;
Vikram Aggarwalfa4b47e2012-03-09 13:02:46 -08002141 case LOADER_FOLDER_CURSOR:
Vikram Aggarwalfa4b47e2012-03-09 13:02:46 -08002142 // Check status of the cursor.
Marc Blankfd9d0b82012-04-23 16:01:51 -07002143 if (data != null && data.moveToFirst()) {
Andy Huang090db1e2012-07-25 13:25:28 -07002144 final Folder folder = new Folder(data);
Marc Blankfd9d0b82012-04-23 16:01:51 -07002145 LogUtils.d(LOG_TAG, "FOLDER STATUS = %d", folder.syncStatus);
Vikram Aggarwald00229d2012-09-20 12:31:44 -07002146 setHasFolderChanged(folder);
Andy Huang090db1e2012-07-25 13:25:28 -07002147 mFolder = folder;
2148 mFolderObservable.notifyChanged();
Paul Westbrookc808fac2012-02-22 16:42:18 -08002149 } else {
Marc Blankfd9d0b82012-04-23 16:01:51 -07002150 LogUtils.d(LOG_TAG, "Unable to get the folder %s",
2151 mFolder != null ? mAccount.name : "");
Mindy Pereira11dd5ef2012-03-10 15:10:18 -08002152 }
Vikram Aggarwalfa4b47e2012-03-09 13:02:46 -08002153 break;
Vikram Aggarwalfa4b47e2012-03-09 13:02:46 -08002154 case LOADER_RECENT_FOLDERS:
Vikram Aggarwalf9ba8712012-09-23 11:42:39 -07002155 // Few recent folders and we are running on a phone? Populate the default recents.
2156 // The number of default recent folders is at least 2: every provider has at
2157 // least two folders, and the recent folder count never decreases. Having a single
2158 // recent folder is an erroneous case, and we can gracefully recover by populating
2159 // default recents. The default recents will not stomp on the existing value: it
2160 // will be shown in addition to the default folders: the max number of recent
2161 // folders is more than 1+num(defaultRecents).
Vikram Aggarwalbcb16b92013-01-28 18:05:03 -08002162 if (data != null && data.getCount() <= 1 && !mIsTablet) {
Vikram Aggarwal27d89ad2012-06-12 13:38:40 -07002163 final class PopulateDefault extends AsyncTask<Uri, Void, Void> {
2164 @Override
2165 protected Void doInBackground(Uri... uri) {
2166 // Asking for an update on the URI and ignore the result.
2167 final ContentResolver resolver = mContext.getContentResolver();
2168 resolver.update(uri[0], null, null, null);
2169 return null;
2170 }
2171 }
2172 final Uri uri = mAccount.defaultRecentFolderListUri;
2173 LogUtils.v(LOG_TAG, "Default recents at %s", uri);
2174 new PopulateDefault().execute(uri);
2175 break;
2176 }
2177 LogUtils.v(LOG_TAG, "Reading recent folders from the cursor.");
mindyp6f54e1b2012-10-09 09:54:08 -07002178 loadRecentFolders(data);
Vikram Aggarwalfa4b47e2012-03-09 13:02:46 -08002179 break;
Mindy Pereiraab486362012-03-21 18:18:53 -07002180 case LOADER_ACCOUNT_INBOX:
Marc Blankfd9d0b82012-04-23 16:01:51 -07002181 if (data != null && !data.isClosed() && data.moveToFirst()) {
Mindy Pereira0e88e9f2012-03-25 13:47:41 -07002182 Folder inbox = new Folder(data);
2183 onFolderChanged(inbox);
Mindy Pereirab4a43282012-03-23 16:20:03 -07002184 // Just want to get the inbox, don't care about updates to it
2185 // as this will be tracked by the folder change listener.
2186 mActivity.getLoaderManager().destroyLoader(LOADER_ACCOUNT_INBOX);
Mindy Pereira5ba33802012-03-26 16:30:11 -07002187 } else {
2188 LogUtils.d(LOG_TAG, "Unable to get the account inbox for account %s",
2189 mAccount != null ? mAccount.name : "");
Mindy Pereirab4a43282012-03-23 16:20:03 -07002190 }
Mindy Pereiraab486362012-03-21 18:18:53 -07002191 break;
2192 case LOADER_SEARCH:
Paul Westbrookc4845c52012-08-29 21:48:43 -07002193 if (data != null && data.getCount() > 0) {
2194 data.moveToFirst();
Vikram Aggarwal2b703c62012-09-18 13:54:15 -07002195 final Folder search = new Folder(data);
Paul Westbrookc4845c52012-08-29 21:48:43 -07002196 updateFolder(search);
2197 mConvListContext = ConversationListContext.forSearchQuery(mAccount, mFolder,
2198 mActivity.getIntent()
2199 .getStringExtra(UIProvider.SearchQueryParameters.QUERY));
2200 showConversationList(mConvListContext);
2201 mActivity.invalidateOptionsMenu();
Vikram Aggarwalf0ef2302012-11-07 14:53:34 -08002202 mHaveSearchResults = search.totalCount > 0;
Paul Westbrookc4845c52012-08-29 21:48:43 -07002203 mActivity.getLoaderManager().destroyLoader(LOADER_SEARCH);
2204 } else {
2205 LogUtils.e(LOG_TAG, "Null or empty cursor returned by LOADER_SEARCH loader");
2206 }
Mindy Pereiraab486362012-03-21 18:18:53 -07002207 break;
Vikram Aggarwal7dedb952012-02-16 16:10:23 -08002208 }
2209 }
2210
mindyp6f54e1b2012-10-09 09:54:08 -07002211
Vikram Aggarwalc7694222012-04-23 13:37:01 -07002212 /**
2213 * Destructive actions on Conversations. This class should only be created by controllers, and
2214 * clients should only require {@link DestructiveAction}s, not specific implementations of the.
2215 * Only the controllers should know what kind of destructive actions are being created.
2216 */
Mindy Pereirade3e74a2012-07-24 09:43:10 -07002217 public class ConversationAction implements DestructiveAction {
Vikram Aggarwalacaa3c02012-04-24 12:45:27 -07002218 /**
2219 * The action to be performed. This is specified as the resource ID of the menu item
2220 * corresponding to this action: R.id.delete, R.id.report_spam, etc.
2221 */
Vikram Aggarwal7f602f72012-04-30 16:04:06 -07002222 private final int mAction;
Vikram Aggarwalbc67bb12012-04-30 14:05:35 -07002223 /** The action will act upon these conversations */
Paul Westbrook77eee622012-07-10 13:41:57 -07002224 private final Collection<Conversation> mTarget;
Vikram Aggarwal7f602f72012-04-30 16:04:06 -07002225 /** Whether this destructive action has already been performed */
2226 private boolean mCompleted;
Vikram Aggarwalc4113952012-05-11 14:14:56 -07002227 /** Whether this is an action on the currently selected set. */
2228 private final boolean mIsSelectedSet;
Vikram Aggarwale8a85322012-04-24 09:01:38 -07002229
Mindy Pereirafbe40192012-03-20 10:40:45 -07002230 /**
2231 * Create a listener object. action is one of four constants: R.id.y_button (archive),
2232 * R.id.delete , R.id.mute, and R.id.report_spam.
2233 * @param action
Vikram Aggarwalbc67bb12012-04-30 14:05:35 -07002234 * @param target Conversation that we want to apply the action to.
Vikram Aggarwalc4113952012-05-11 14:14:56 -07002235 * @param isBatch whether the conversations are in the currently selected batch set.
2236 */
2237 public ConversationAction(int action, Collection<Conversation> target, boolean isBatch) {
Mindy Pereirafbe40192012-03-20 10:40:45 -07002238 mAction = action;
Paul Westbrook77eee622012-07-10 13:41:57 -07002239 mTarget = ImmutableList.copyOf(target);
Vikram Aggarwalc4113952012-05-11 14:14:56 -07002240 mIsSelectedSet = isBatch;
Mindy Pereirafbe40192012-03-20 10:40:45 -07002241 }
2242
Vikram Aggarwalacaa3c02012-04-24 12:45:27 -07002243 /**
2244 * The action common to child classes. This performs the action specified in the constructor
2245 * on the conversations given here.
Vikram Aggarwalacaa3c02012-04-24 12:45:27 -07002246 */
Vikram Aggarwalbc67bb12012-04-30 14:05:35 -07002247 @Override
2248 public void performAction() {
Vikram Aggarwal7f602f72012-04-30 16:04:06 -07002249 if (isPerformed()) {
2250 return;
2251 }
Mindy Pereira3cb28b52012-05-24 15:26:39 -07002252 boolean undoEnabled = mAccount.supportsCapability(AccountCapabilities.UNDO);
Vikram Aggarwal7dd054e2012-05-21 14:43:10 -07002253
2254 // Are we destroying the currently shown conversation? Show the next one.
2255 if (LogUtils.isLoggable(LOG_TAG, LogUtils.DEBUG)){
Vikram Aggarwal8742a612012-08-13 10:22:50 -07002256 LogUtils.d(LOG_TAG, "ConversationAction.performAction():"
2257 + "\nmTarget=%s\nCurrent=%s",
Vikram Aggarwal7dd054e2012-05-21 14:43:10 -07002258 Conversation.toString(mTarget), mCurrentConversation);
2259 }
Vikram Aggarwal7dd054e2012-05-21 14:43:10 -07002260
Paul Westbrooke1221d22012-08-19 11:09:07 -07002261 if (mConversationListCursor == null) {
2262 LogUtils.e(LOG_TAG, "null ConversationCursor in ConversationAction.performAction():"
2263 + "\nmTarget=%s\nCurrent=%s",
2264 Conversation.toString(mTarget), mCurrentConversation);
2265 return;
2266 }
2267
Mindy Pereirafbe40192012-03-20 10:40:45 -07002268 switch (mAction) {
Mindy Pereira0692baf2012-03-23 17:34:31 -07002269 case R.id.archive:
Vikram Aggarwal7dd054e2012-05-21 14:43:10 -07002270 LogUtils.d(LOG_TAG, "Archiving");
Vikram Aggarwalbc67bb12012-04-30 14:05:35 -07002271 mConversationListCursor.archive(mContext, mTarget);
Mindy Pereirafbe40192012-03-20 10:40:45 -07002272 break;
2273 case R.id.delete:
Vikram Aggarwal7dd054e2012-05-21 14:43:10 -07002274 LogUtils.d(LOG_TAG, "Deleting");
Vikram Aggarwalbc67bb12012-04-30 14:05:35 -07002275 mConversationListCursor.delete(mContext, mTarget);
Mindy Pereira695d6962012-06-18 13:02:10 -07002276 if (mFolder.supportsCapability(FolderCapabilities.DELETE_ACTION_FINAL)) {
Marc Blank386243f2012-05-25 10:40:59 -07002277 undoEnabled = false;
2278 }
Mindy Pereirafbe40192012-03-20 10:40:45 -07002279 break;
2280 case R.id.mute:
Vikram Aggarwal7dd054e2012-05-21 14:43:10 -07002281 LogUtils.d(LOG_TAG, "Muting");
Vikram Aggarwalbc67bb12012-04-30 14:05:35 -07002282 if (mFolder.supportsCapability(FolderCapabilities.DESTRUCTIVE_MUTE)) {
Vikram Aggarwal7f602f72012-04-30 16:04:06 -07002283 for (Conversation c : mTarget) {
2284 c.localDeleteOnUpdate = true;
2285 }
Vikram Aggarwalbc67bb12012-04-30 14:05:35 -07002286 }
2287 mConversationListCursor.mute(mContext, mTarget);
Mindy Pereirafbe40192012-03-20 10:40:45 -07002288 break;
2289 case R.id.report_spam:
Vikram Aggarwal7dd054e2012-05-21 14:43:10 -07002290 LogUtils.d(LOG_TAG, "Reporting spam");
Vikram Aggarwalbc67bb12012-04-30 14:05:35 -07002291 mConversationListCursor.reportSpam(mContext, mTarget);
2292 break;
Paul Westbrook77eee622012-07-10 13:41:57 -07002293 case R.id.mark_not_spam:
2294 LogUtils.d(LOG_TAG, "Marking not spam");
2295 mConversationListCursor.reportNotSpam(mContext, mTarget);
2296 break;
Paul Westbrook76b20622012-07-12 11:45:43 -07002297 case R.id.report_phishing:
2298 LogUtils.d(LOG_TAG, "Reporting phishing");
2299 mConversationListCursor.reportPhishing(mContext, mTarget);
2300 break;
Vikram Aggarwalbc67bb12012-04-30 14:05:35 -07002301 case R.id.remove_star:
Vikram Aggarwal7dd054e2012-05-21 14:43:10 -07002302 LogUtils.d(LOG_TAG, "Removing star");
Vikram Aggarwalbc67bb12012-04-30 14:05:35 -07002303 // Star removal is destructive in the Starred folder.
2304 mConversationListCursor.updateBoolean(mContext, mTarget,
2305 ConversationColumns.STARRED, false);
2306 break;
2307 case R.id.mark_not_important:
Vikram Aggarwal7dd054e2012-05-21 14:43:10 -07002308 LogUtils.d(LOG_TAG, "Marking not-important");
Mindy Pereira445be212012-08-15 08:50:10 -07002309 // Marking not important is destructive in a mailbox
2310 // containing only important messages
Mindy Pereira0d03ef82012-08-15 09:05:48 -07002311 if (mFolder != null && mFolder.isImportantOnly()) {
Mindy Pereira445be212012-08-15 08:50:10 -07002312 for (Conversation conv : mTarget) {
2313 conv.localDeleteOnUpdate = true;
2314 }
2315 }
Vikram Aggarwalbc67bb12012-04-30 14:05:35 -07002316 mConversationListCursor.updateInt(mContext, mTarget,
2317 ConversationColumns.PRIORITY, UIProvider.ConversationPriority.LOW);
Mindy Pereirafbe40192012-03-20 10:40:45 -07002318 break;
Paul Westbrookef362542012-08-27 14:53:32 -07002319 case R.id.discard_drafts:
2320 LogUtils.d(LOG_TAG, "Discarding draft messages");
2321 // Discarding draft messages is destructive in a "draft" mailbox
2322 if (mFolder != null && mFolder.isDraft()) {
2323 for (Conversation conv : mTarget) {
2324 conv.localDeleteOnUpdate = true;
2325 }
2326 }
2327 mConversationListCursor.discardDrafts(mContext, mTarget);
2328 // We don't support undoing discarding drafts
2329 undoEnabled = false;
2330 break;
Vikram Aggarwal3d7ca9d2012-05-11 14:40:36 -07002331 }
2332 if (undoEnabled) {
mindypead50392012-08-23 11:03:53 -07002333 mHandler.postDelayed(new Runnable() {
2334 @Override
2335 public void run() {
2336 onUndoAvailable(new ToastBarOperation(mTarget.size(), mAction,
mindypa59283a2012-09-11 17:49:06 -07002337 ToastBarOperation.UNDO, mIsSelectedSet));
mindypead50392012-08-23 11:03:53 -07002338 }
2339 }, mShowUndoBarDelay);
Vikram Aggarwal3d7ca9d2012-05-11 14:40:36 -07002340 }
Vikram Aggarwalbc67bb12012-04-30 14:05:35 -07002341 refreshConversationList();
Vikram Aggarwalc4113952012-05-11 14:14:56 -07002342 if (mIsSelectedSet) {
Vikram Aggarwalc4113952012-05-11 14:14:56 -07002343 mSelectedSet.clear();
2344 }
Mindy Pereirafbe40192012-03-20 10:40:45 -07002345 }
Vikram Aggarwal7f602f72012-04-30 16:04:06 -07002346
2347 /**
2348 * Returns true if this action has been performed, false otherwise.
Andy Huang839ada22012-07-20 15:48:40 -07002349 *
Vikram Aggarwal7f602f72012-04-30 16:04:06 -07002350 */
2351 private synchronized boolean isPerformed() {
2352 if (mCompleted) {
2353 return true;
2354 }
2355 mCompleted = true;
2356 return false;
2357 }
Mindy Pereirafbe40192012-03-20 10:40:45 -07002358 }
Mindy Pereirae5f4dc02012-03-21 16:08:53 -07002359
Vikram Aggarwald503df42012-05-11 10:13:35 -07002360 // Called from the FolderSelectionDialog after a user is done selecting folders to assign the
2361 // conversations to.
Mindy Pereirae5f4dc02012-03-21 16:08:53 -07002362 @Override
Mindy Pereira8db7e402012-07-13 10:32:47 -07002363 public final void assignFolder(Collection<FolderOperation> folderOps,
2364 Collection<Conversation> target, boolean batch, boolean showUndo) {
2365 // Actions are destructive only when the current folder can be assigned
2366 // to (which is the same as being able to un-assign a conversation from the folder) and
2367 // when the list of folders contains the current folder.
2368 final boolean isDestructive = mFolder
2369 .supportsCapability(FolderCapabilities.CAN_ACCEPT_MOVED_MESSAGES)
2370 && FolderOperation.isDestructive(folderOps, mFolder);
Vikram Aggarwald503df42012-05-11 10:13:35 -07002371 LogUtils.d(LOG_TAG, "onFolderChangesCommit: isDestructive = %b", isDestructive);
2372 if (isDestructive) {
2373 for (final Conversation c : target) {
2374 c.localDeleteOnUpdate = true;
Mindy Pereira6778f462012-03-23 18:01:55 -07002375 }
Mindy Pereirae5f4dc02012-03-21 16:08:53 -07002376 }
mindypc84759c2012-08-29 09:51:53 -07002377 final DestructiveAction folderChange;
Vikram Aggarwald503df42012-05-11 10:13:35 -07002378 // Update the UI elements depending no their visibility and availability
Vikram Aggarwal3d7ca9d2012-05-11 14:40:36 -07002379 // TODO(viki): Consolidate this into a single method requestDelete.
Vikram Aggarwald503df42012-05-11 10:13:35 -07002380 if (isDestructive) {
mindypc84759c2012-08-29 09:51:53 -07002381 folderChange = getDeferredFolderChange(target, folderOps, isDestructive,
2382 batch, showUndo);
Vikram Aggarwala8e43182012-09-13 12:55:10 -07002383 delete(0, target, folderChange);
Vikram Aggarwal6902dcf2012-04-11 08:57:42 -07002384 } else {
mindypc84759c2012-08-29 09:51:53 -07002385 folderChange = getFolderChange(target, folderOps, isDestructive,
2386 batch, showUndo);
Vikram Aggarwald503df42012-05-11 10:13:35 -07002387 requestUpdate(target, folderChange);
Mindy Pereirae5f4dc02012-03-21 16:08:53 -07002388 }
2389 }
2390
Mindy Pereira967ede62012-03-22 09:29:09 -07002391 @Override
Vikram Aggarwal3d7ca9d2012-05-11 14:40:36 -07002392 public final void onRefreshRequired() {
mindyp5390fca2012-08-22 12:12:25 -07002393 if (isAnimating() || isDragging()) {
Mindy Pereira69e88dd2012-08-10 09:30:18 -07002394 LogUtils.d(LOG_TAG, "onRefreshRequired: delay until animating done");
2395 return;
2396 }
Marc Blankbf128eb2012-04-18 15:58:45 -07002397 // Refresh the query in the background
Paul Westbrookcff1aea2012-08-10 11:51:00 -07002398 if (mConversationListCursor.isRefreshRequired()) {
2399 mConversationListCursor.refresh();
2400 }
Marc Blankbf128eb2012-04-18 15:58:45 -07002401 }
2402
mindyp5390fca2012-08-22 12:12:25 -07002403 @Override
2404 public void startDragMode() {
2405 mIsDragHappening = true;
2406 }
2407
2408 @Override
2409 public void stopDragMode() {
2410 mIsDragHappening = false;
2411 if (mConversationListCursor.isRefreshReady()) {
2412 LogUtils.d(LOG_TAG, "Stopped animating: try sync");
2413 onRefreshReady();
2414 }
2415
2416 if (mConversationListCursor.isRefreshRequired()) {
2417 LogUtils.d(LOG_TAG, "Stopped animating: refresh");
2418 mConversationListCursor.refresh();
2419 }
2420 }
2421
2422 private boolean isDragging() {
2423 return mIsDragHappening;
2424 }
2425
mindyp6f54e1b2012-10-09 09:54:08 -07002426 @Override
2427 public boolean isAnimating() {
Mindy Pereira69e88dd2012-08-10 09:30:18 -07002428 boolean isAnimating = false;
2429 ConversationListFragment convListFragment = getConversationListFragment();
2430 if (convListFragment != null) {
2431 AnimatedAdapter adapter = convListFragment.getAnimatedAdapter();
2432 if (adapter != null) {
2433 isAnimating = adapter.isAnimating();
2434 }
2435 }
2436 return isAnimating;
2437 }
2438
Marc Blankbf128eb2012-04-18 15:58:45 -07002439 /**
2440 * Called when the {@link ConversationCursor} is changed or has new data in it.
2441 * <p>
2442 * {@inheritDoc}
2443 */
2444 @Override
Vikram Aggarwal3d7ca9d2012-05-11 14:40:36 -07002445 public final void onRefreshReady() {
mindyp5c1d8352012-11-05 10:12:44 -08002446 LogUtils.d(LOG_TAG, "Received refresh ready callback for folder %s",
2447 mFolder != null ? mFolder.id : "-1");
Andy Huangb2ef9c12012-12-18 12:58:41 -06002448
2449 if (mDestroyed) {
2450 LogUtils.i(LOG_TAG, "ignoring onRefreshReady on destroyed AAC");
2451 return;
2452 }
2453
Paul Westbrookcff1aea2012-08-10 11:51:00 -07002454 if (!isAnimating()) {
Marc Blankbf128eb2012-04-18 15:58:45 -07002455 // Swap cursors
2456 mConversationListCursor.sync();
Marc Blankbf128eb2012-04-18 15:58:45 -07002457 }
Paul Westbrook937c94f2012-08-16 13:01:18 -07002458 mTracker.onCursorUpdated();
Vikram Aggarwalf0ef2302012-11-07 14:53:34 -08002459 perhapsShowFirstSearchResult();
Marc Blankbf128eb2012-04-18 15:58:45 -07002460 }
2461
2462 @Override
Vikram Aggarwal3d7ca9d2012-05-11 14:40:36 -07002463 public final void onDataSetChanged() {
Paul Westbrook9f119c72012-04-24 16:10:59 -07002464 updateConversationListFragment();
Andy Huang632721e2012-04-11 16:57:26 -07002465 mConversationListObservable.notifyChanged();
Paul Westbrooka13b3742012-09-07 16:35:06 -07002466 mSelectedSet.validateAgainstCursor(mConversationListCursor);
Marc Blankbf128eb2012-04-18 15:58:45 -07002467 }
2468
Vikram Aggarwal3d7ca9d2012-05-11 14:40:36 -07002469 /**
2470 * If the Conversation List Fragment is visible, updates the fragment.
2471 */
2472 private final void updateConversationListFragment() {
Marc Blankbf128eb2012-04-18 15:58:45 -07002473 final ConversationListFragment convList = getConversationListFragment();
2474 if (convList != null) {
Vikram Aggarwal75daee52012-04-30 11:13:09 -07002475 refreshConversationList();
Vikram Aggarwal6422b8d2013-01-14 10:47:41 -08002476 if (isFragmentVisible(convList)) {
Vikram Aggarwal69b5c302012-09-05 11:11:13 -07002477 informCursorVisiblity(true);
Paul Westbrook9f119c72012-04-24 16:10:59 -07002478 }
Marc Blankbf128eb2012-04-18 15:58:45 -07002479 }
2480 }
2481
2482 /**
2483 * This class handles throttled refresh of the conversation list
2484 */
2485 static class RefreshTimerTask extends TimerTask {
2486 final Handler mHandler;
2487 final AbstractActivityController mController;
2488
2489 RefreshTimerTask(AbstractActivityController controller, Handler handler) {
2490 mHandler = handler;
2491 mController = controller;
2492 }
2493
2494 @Override
2495 public void run() {
2496 mHandler.post(new Runnable() {
2497 @Override
2498 public void run() {
2499 LogUtils.d(LOG_TAG, "Delay done... calling onRefreshRequired");
2500 mController.onRefreshRequired();
2501 }});
2502 }
2503 }
2504
2505 /**
2506 * Cancel the refresh task, if it's running
2507 */
2508 private void cancelRefreshTask () {
2509 if (mConversationListRefreshTask != null) {
2510 mConversationListRefreshTask.cancel();
2511 mConversationListRefreshTask = null;
2512 }
2513 }
2514
mindyp6f54e1b2012-10-09 09:54:08 -07002515 private void loadRecentFolders(Cursor data) {
2516 mRecentFolderList.loadFromUiProvider(data);
2517 if (isAnimating()) {
2518 mRecentsDataUpdated = true;
2519 } else {
2520 mRecentFolderObservers.notifyChanged();
2521 }
2522 }
2523
Marc Blankbf128eb2012-04-18 15:58:45 -07002524 @Override
Paul Westbrookcff1aea2012-08-10 11:51:00 -07002525 public void onAnimationEnd(AnimatedAdapter animatedAdapter) {
Paul Westbrook026139c2012-09-19 22:35:37 -07002526 if (mConversationListCursor == null) {
2527 LogUtils.e(LOG_TAG, "null ConversationCursor in onAnimationEnd");
2528 return;
2529 }
Paul Westbrookcff1aea2012-08-10 11:51:00 -07002530 if (mConversationListCursor.isRefreshReady()) {
mindyp52544862012-08-20 12:05:36 -07002531 LogUtils.d(LOG_TAG, "Stopped animating: try sync");
Paul Westbrookcff1aea2012-08-10 11:51:00 -07002532 onRefreshReady();
Marc Blankbf128eb2012-04-18 15:58:45 -07002533 }
Marc Blankbf128eb2012-04-18 15:58:45 -07002534
Paul Westbrookcff1aea2012-08-10 11:51:00 -07002535 if (mConversationListCursor.isRefreshRequired()) {
mindyp52544862012-08-20 12:05:36 -07002536 LogUtils.d(LOG_TAG, "Stopped animating: refresh");
Paul Westbrookcff1aea2012-08-10 11:51:00 -07002537 mConversationListCursor.refresh();
2538 }
mindyp6f54e1b2012-10-09 09:54:08 -07002539 if (mRecentsDataUpdated) {
2540 mRecentsDataUpdated = false;
2541 mRecentFolderObservers.notifyChanged();
2542 }
2543 FolderListFragment frag = this.getFolderListFragment();
2544 if (frag != null) {
2545 frag.onAnimationEnd();
2546 }
Marc Blankbf128eb2012-04-18 15:58:45 -07002547 }
2548
2549 @Override
Mindy Pereira967ede62012-03-22 09:29:09 -07002550 public void onSetEmpty() {
Vikram Aggarwal6cadbfc2012-12-27 09:17:05 -08002551 // There are no selected conversations. Ensure that the listener and its associated actions
2552 // are blanked out.
2553 setListener(null, -1);
Mindy Pereira967ede62012-03-22 09:29:09 -07002554 }
2555
2556 @Override
2557 public void onSetPopulated(ConversationSelectionSet set) {
Vikram Aggarwal7704d792013-01-11 15:48:24 -08002558 mCabActionMenu = new SelectedConversationsActionMenu(mActivity, set, mFolder);
Vikram Aggarwal5b9ed2b2013-01-28 18:08:37 -08002559 if (mViewMode.isListMode() || (mIsTablet && mViewMode.isConversationMode())) {
Vikram Aggarwal7704d792013-01-11 15:48:24 -08002560 enableCabMode();
Vikram Aggarwal6902dcf2012-04-11 08:57:42 -07002561 }
Mindy Pereira967ede62012-03-22 09:29:09 -07002562 }
2563
Mindy Pereira967ede62012-03-22 09:29:09 -07002564 @Override
2565 public void onSetChanged(ConversationSelectionSet set) {
2566 // Do nothing. We don't care about changes to the set.
2567 }
2568
2569 @Override
2570 public ConversationSelectionSet getSelectedSet() {
2571 return mSelectedSet;
2572 }
2573
Vikram Aggarwale128fc22012-04-04 12:33:34 -07002574 /**
2575 * Disable the Contextual Action Bar (CAB). The selected set is not changed.
2576 */
2577 protected void disableCabMode() {
Mindy Pereira8937bf12012-07-23 14:05:02 -07002578 // Commit any previous destructive actions when entering/ exiting CAB mode.
mindypc6adce32012-08-22 18:46:42 -07002579 commitDestructiveActions(true);
Vikram Aggarwale128fc22012-04-04 12:33:34 -07002580 if (mCabActionMenu != null) {
2581 mCabActionMenu.deactivate();
2582 }
2583 }
2584
2585 /**
2586 * Re-enable the CAB menu if required. The selection set is not changed.
2587 */
2588 protected void enableCabMode() {
2589 if (mCabActionMenu != null) {
2590 mCabActionMenu.activate();
2591 }
2592 }
2593
Vikram Aggarwal4eb52712012-06-19 16:24:50 -07002594 /**
2595 * Unselect conversations and exit CAB mode.
2596 */
2597 protected final void exitCabMode() {
2598 mSelectedSet.clear();
2599 }
2600
Mindy Pereira967ede62012-03-22 09:29:09 -07002601 @Override
Mindy Pereirafd0c2972012-03-27 13:50:39 -07002602 public void startSearch() {
Vikram Aggarwal35f19d72012-04-24 13:24:48 -07002603 if (mAccount == null) {
2604 // We cannot search if there is no account. Drop the request to the floor.
2605 LogUtils.d(LOG_TAG, "AbstractActivityController.startSearch(): null account");
2606 return;
2607 }
Mindy Pereirafd0c2972012-03-27 13:50:39 -07002608 if (mAccount.supportsCapability(UIProvider.AccountCapabilities.LOCAL_SEARCH)
2609 | mAccount.supportsCapability(UIProvider.AccountCapabilities.SERVER_SEARCH)) {
Vikram Aggarwal70f130e2012-04-03 12:32:14 -07002610 onSearchRequested(mActionBarView.getQuery());
Mindy Pereirafd0c2972012-03-27 13:50:39 -07002611 } else {
2612 Toast.makeText(mActivity.getActivityContext(), mActivity.getActivityContext()
Mindy Pereiraa46c2992012-03-27 14:12:39 -07002613 .getString(R.string.search_unsupported), Toast.LENGTH_SHORT).show();
Mindy Pereirafd0c2972012-03-27 13:50:39 -07002614 }
2615 }
Mindy Pereiraacf60392012-04-06 09:11:00 -07002616
Vikram Aggarwal0dda5732012-04-06 11:20:16 -07002617 @Override
2618 public void exitSearchMode() {
2619 if (mViewMode.getMode() == ViewMode.SEARCH_RESULTS_LIST) {
2620 mActivity.finish();
2621 }
2622 }
2623
Mindy Pereiraacf60392012-04-06 09:11:00 -07002624 /**
2625 * Supports dragging conversations to a folder.
2626 */
2627 @Override
2628 public boolean supportsDrag(DragEvent event, Folder folder) {
2629 return (folder != null
2630 && event != null
2631 && event.getClipDescription() != null
2632 && folder.supportsCapability
2633 (UIProvider.FolderCapabilities.CAN_ACCEPT_MOVED_MESSAGES)
2634 && folder.supportsCapability
2635 (UIProvider.FolderCapabilities.CAN_HOLD_MAIL)
2636 && !mFolder.uri.equals(folder.uri));
2637 }
2638
2639 /**
Mindy Pereira6c2663d2012-07-20 15:37:29 -07002640 * Handles dropping conversations to a folder.
Mindy Pereiraacf60392012-04-06 09:11:00 -07002641 */
2642 @Override
2643 public void handleDrop(DragEvent event, final Folder folder) {
Mindy Pereiraacf60392012-04-06 09:11:00 -07002644 if (!supportsDrag(event, folder)) {
2645 return;
2646 }
mindypae7e6a02012-11-29 13:28:10 -08002647 if (folder.type == UIProvider.FolderType.STARRED) {
2648 // Moving a conversation to the starred folder adds the star and
2649 // removes the current label
2650 handleDropInStarred(folder);
2651 return;
2652 }
2653 if (mFolder.type == UIProvider.FolderType.STARRED) {
2654 handleDragFromStarred(folder);
2655 return;
2656 }
mindypa8492632012-09-24 09:27:54 -07002657 final ArrayList<FolderOperation> dragDropOperations = new ArrayList<FolderOperation>();
mindypae7e6a02012-11-29 13:28:10 -08002658 final Collection<Conversation> conversations = mSelectedSet.values();
mindypa8492632012-09-24 09:27:54 -07002659 // Add the drop target folder.
2660 dragDropOperations.add(new FolderOperation(folder, true));
2661 // Remove the current folder unless the user is viewing "all".
2662 // That operation should just add the new folder.
2663 boolean isDestructive = !mFolder.isViewAll()
2664 && mFolder.supportsCapability
2665 (UIProvider.FolderCapabilities.CAN_ACCEPT_MOVED_MESSAGES);
2666 if (isDestructive) {
2667 dragDropOperations.add(new FolderOperation(mFolder, false));
2668 }
Mindy Pereira8db7e402012-07-13 10:32:47 -07002669 // Drag and drop is destructive: we remove conversations from the
2670 // current folder.
mindypa8492632012-09-24 09:27:54 -07002671 final DestructiveAction action = getFolderChange(conversations, dragDropOperations,
2672 isDestructive, true, true);
2673 if (isDestructive) {
2674 delete(0, conversations, action);
2675 } else {
2676 action.performAction();
2677 }
Mindy Pereiraacf60392012-04-06 09:11:00 -07002678 }
Mindy Pereira0963ef82012-04-10 11:43:01 -07002679
mindypae7e6a02012-11-29 13:28:10 -08002680 private void handleDragFromStarred(Folder folder) {
2681 final Collection<Conversation> conversations = mSelectedSet.values();
2682 // The conversation list deletes and performs the action if it exists.
2683 final ConversationListFragment convListFragment = getConversationListFragment();
2684 // There should always be a convlistfragment, or the user could not have
2685 // dragged/ dropped conversations.
2686 if (convListFragment != null) {
2687 LogUtils.d(LOG_TAG, "AAC.requestDelete: ListFragment is handling delete.");
2688 ArrayList<ConversationOperation> ops = new ArrayList<ConversationOperation>();
mindypcb0b30e2012-11-30 10:16:35 -08002689 ArrayList<Uri> folderUris;
2690 ArrayList<Boolean> adds;
mindypae7e6a02012-11-29 13:28:10 -08002691 for (Conversation target : conversations) {
mindypcb0b30e2012-11-30 10:16:35 -08002692 folderUris = new ArrayList<Uri>();
2693 adds = new ArrayList<Boolean>();
2694 folderUris.add(folder.uri);
2695 adds.add(Boolean.TRUE);
Paul Westbrook26746eb2012-12-06 14:44:01 -08002696 final HashMap<Uri, Folder> targetFolders =
2697 Folder.hashMapForFolders(target.getRawFolders());
mindypae7e6a02012-11-29 13:28:10 -08002698 targetFolders.put(folder.uri, folder);
Paul Westbrook26746eb2012-12-06 14:44:01 -08002699 ops.add(mConversationListCursor.getConversationFolderOperation(target,
2700 folderUris, adds, targetFolders.values()));
mindypae7e6a02012-11-29 13:28:10 -08002701 }
2702 if (mConversationListCursor != null) {
2703 mConversationListCursor.updateBulkValues(mContext, ops);
2704 }
2705 refreshConversationList();
2706 mSelectedSet.clear();
2707 return;
2708 }
2709 }
2710
2711 private void handleDropInStarred(Folder folder) {
2712 final Collection<Conversation> conversations = mSelectedSet.values();
2713 // The conversation list deletes and performs the action if it exists.
2714 final ConversationListFragment convListFragment = getConversationListFragment();
2715 // There should always be a convlistfragment, or the user could not have
2716 // dragged/ dropped conversations.
2717 if (convListFragment != null) {
2718 LogUtils.d(LOG_TAG, "AAC.requestDelete: ListFragment is handling delete.");
Vikram Aggarwal669947b2013-01-10 17:05:56 -08002719 convListFragment.requestDelete(R.id.change_folder, conversations,
mindyp5cc0ab22012-12-11 08:47:35 -08002720 new DroppedInStarredAction(conversations, mFolder, folder));
mindypae7e6a02012-11-29 13:28:10 -08002721 return;
2722 }
2723 }
2724
2725 // When dragging conversations to the starred folder, remove from the
2726 // original folder and add a star
2727 private class DroppedInStarredAction implements DestructiveAction {
2728 private Collection<Conversation> mConversations;
2729 private Folder mInitialFolder;
mindyp5cc0ab22012-12-11 08:47:35 -08002730 private Folder mStarred;
mindypae7e6a02012-11-29 13:28:10 -08002731
mindyp5cc0ab22012-12-11 08:47:35 -08002732 public DroppedInStarredAction(Collection<Conversation> conversations, Folder initialFolder,
2733 Folder starredFolder) {
mindypae7e6a02012-11-29 13:28:10 -08002734 mConversations = conversations;
mindyp5cc0ab22012-12-11 08:47:35 -08002735 mInitialFolder = initialFolder;
2736 mStarred = starredFolder;
mindypae7e6a02012-11-29 13:28:10 -08002737 }
2738
2739 @Override
2740 public void performAction() {
2741 ToastBarOperation undoOp = new ToastBarOperation(mConversations.size(),
2742 R.id.change_folder, ToastBarOperation.UNDO, true);
2743 onUndoAvailable(undoOp);
2744 ArrayList<ConversationOperation> ops = new ArrayList<ConversationOperation>();
2745 ContentValues values = new ContentValues();
mindypcb0b30e2012-11-30 10:16:35 -08002746 ArrayList<Uri> folderUris;
2747 ArrayList<Boolean> adds;
mindyp5cc0ab22012-12-11 08:47:35 -08002748 ConversationOperation operation;
mindypae7e6a02012-11-29 13:28:10 -08002749 for (Conversation target : mConversations) {
mindypcb0b30e2012-11-30 10:16:35 -08002750 folderUris = new ArrayList<Uri>();
2751 adds = new ArrayList<Boolean>();
mindyp5cc0ab22012-12-11 08:47:35 -08002752 folderUris.add(mStarred.uri);
2753 adds.add(Boolean.TRUE);
mindypcb0b30e2012-11-30 10:16:35 -08002754 folderUris.add(mInitialFolder.uri);
2755 adds.add(Boolean.FALSE);
mindyp5cc0ab22012-12-11 08:47:35 -08002756 final HashMap<Uri, Folder> targetFolders =
2757 Folder.hashMapForFolders(target.getRawFolders());
2758 targetFolders.put(mStarred.uri, mStarred);
mindypae7e6a02012-11-29 13:28:10 -08002759 targetFolders.remove(mInitialFolder.uri);
mindyp5cc0ab22012-12-11 08:47:35 -08002760 values.put(ConversationColumns.STARRED, true);
2761 operation = mConversationListCursor.getConversationFolderOperation(target,
2762 folderUris, adds, targetFolders.values(), values);
2763 ops.add(operation);
mindypae7e6a02012-11-29 13:28:10 -08002764 }
2765 if (mConversationListCursor != null) {
2766 mConversationListCursor.updateBulkValues(mContext, ops);
2767 }
2768 refreshConversationList();
2769 mSelectedSet.clear();
2770 }
2771 }
2772
Mindy Pereira0963ef82012-04-10 11:43:01 -07002773 @Override
Mindy Pereira0963ef82012-04-10 11:43:01 -07002774 public void onTouchEvent(MotionEvent event) {
2775 if (event.getAction() == MotionEvent.ACTION_DOWN) {
Andrew Sappersteinc2c9dc12012-07-02 18:17:32 -07002776 if (mToastBar != null && !mToastBar.isEventInToastBar(event)) {
Mark Weid243d452012-10-31 16:24:08 -07002777 hideOrRepositionToastBar(true);
Mindy Pereira0963ef82012-04-10 11:43:01 -07002778 }
2779 }
2780 }
Andy Huangb1c34dc2012-04-17 16:36:19 -07002781
Mark Weid243d452012-10-31 16:24:08 -07002782 protected abstract void hideOrRepositionToastBar(boolean animated);
2783
Andy Huang632721e2012-04-11 16:57:26 -07002784 @Override
2785 public void onConversationSeen(Conversation conv) {
2786 mPagerController.onConversationSeen(conv);
2787 }
2788
Andy Huang9d3fd922012-09-26 22:23:58 -07002789 @Override
2790 public boolean isInitialConversationLoading() {
2791 return mPagerController.isInitialConversationLoading();
2792 }
2793
Vikram Aggarwal6422b8d2013-01-14 10:47:41 -08002794 /**
2795 * Check if the fragment given here is visible. Checking {@link Fragment#isVisible()} is
2796 * insufficient because that doesn't check if the window is currently in focus or not.
2797 */
2798 private final boolean isFragmentVisible(Fragment in) {
2799 return in != null && in.isVisible() && mActivity.hasWindowFocus();
2800 }
2801
Andy Huangb1c34dc2012-04-17 16:36:19 -07002802 private class ConversationListLoaderCallbacks implements
2803 LoaderManager.LoaderCallbacks<ConversationCursor> {
2804
2805 @Override
2806 public Loader<ConversationCursor> onCreateLoader(int id, Bundle args) {
2807 Loader<ConversationCursor> result = new ConversationCursorLoader((Activity) mActivity,
Paul Westbrook9a70e912012-08-17 15:53:20 -07002808 mAccount, mFolder.conversationListUri, mFolder.name);
Andy Huangb1c34dc2012-04-17 16:36:19 -07002809 return result;
2810 }
2811
2812 @Override
2813 public void onLoadFinished(Loader<ConversationCursor> loader, ConversationCursor data) {
Andy Huang632721e2012-04-11 16:57:26 -07002814 LogUtils.d(LOG_TAG, "IN AAC.ConversationCursor.onLoadFinished, data=%s loader=%s",
2815 data, loader);
Vikram Aggarwale8a85322012-04-24 09:01:38 -07002816 // Clear our all pending destructive actions before swapping the conversation cursor
2817 destroyPending(null);
Andy Huangb1c34dc2012-04-17 16:36:19 -07002818 mConversationListCursor = data;
Paul Westbrookbf232c32012-04-18 03:17:41 -07002819 mConversationListCursor.addListener(AbstractActivityController.this);
Paul Westbrook937c94f2012-08-16 13:01:18 -07002820 mTracker.onCursorUpdated();
Andy Huange3df1ad2012-04-24 17:15:23 -07002821 mConversationListObservable.notifyChanged();
Yu Ping Hu7c909c72013-01-18 11:58:01 -08002822 // Handle actions that were deferred until after the conversation list was loaded.
2823 for (LoadFinishedCallback callback : mConversationListLoadFinishedCallbacks) {
2824 callback.onLoadFinished();
2825 }
2826 mConversationListLoadFinishedCallbacks.clear();
Paul Westbrook9f119c72012-04-24 16:10:59 -07002827
Vikram Aggarwal17f373e2012-09-17 15:49:32 -07002828 final ConversationListFragment convList = getConversationListFragment();
Vikram Aggarwal6422b8d2013-01-14 10:47:41 -08002829 if (isFragmentVisible(convList)) {
Vikram Aggarwal17f373e2012-09-17 15:49:32 -07002830 // The conversation list is already listening to list changes and gets notified
2831 // in the mConversationListObservable.notifyChanged() line above. We only need to
2832 // check and inform the cursor of the change in visibility here.
2833 informCursorVisiblity(true);
Andy Huangb1c34dc2012-04-17 16:36:19 -07002834 }
Vikram Aggarwalf0ef2302012-11-07 14:53:34 -08002835 perhapsShowFirstSearchResult();
Andy Huangb1c34dc2012-04-17 16:36:19 -07002836 }
2837
2838 @Override
2839 public void onLoaderReset(Loader<ConversationCursor> loader) {
Paul Westbrook9a70e912012-08-17 15:53:20 -07002840 LogUtils.d(LOG_TAG, "IN AAC.ConversationCursor.onLoaderReset, data=%s loader=%s",
2841 mConversationListCursor, loader);
2842
2843 if (mConversationListCursor != null) {
2844 // Unregister the listener
2845 mConversationListCursor.removeListener(AbstractActivityController.this);
2846 mConversationListCursor = null;
2847
2848 // Inform anyone who is interested about the change
2849 mTracker.onCursorUpdated();
2850 mConversationListObservable.notifyChanged();
Andy Huangb1c34dc2012-04-17 16:36:19 -07002851 }
Andy Huangb1c34dc2012-04-17 16:36:19 -07002852 }
Paul Westbrookbf232c32012-04-18 03:17:41 -07002853 }
Andy Huangb1c34dc2012-04-17 16:36:19 -07002854
Vikram Aggarwale8a85322012-04-24 09:01:38 -07002855 /**
Vikram Aggarwalf0ef2302012-11-07 14:53:34 -08002856 * Updates controller state based on search results and shows first conversation if required.
2857 */
2858 private final void perhapsShowFirstSearchResult() {
mindypd27009a2012-11-27 11:54:18 -08002859 if (mCurrentConversation == null) {
2860 // Shown for search results in two-pane mode only.
2861 mHaveSearchResults = Intent.ACTION_SEARCH.equals(mActivity.getIntent().getAction())
2862 && mConversationListCursor.getCount() > 0;
2863 if (!shouldShowFirstConversation()) {
2864 return;
2865 }
2866 mConversationListCursor.moveToPosition(0);
2867 final Conversation conv = new Conversation(mConversationListCursor);
2868 conv.position = 0;
2869 onConversationSelected(conv, true /* checkSafeToModifyFragments */);
Vikram Aggarwalf0ef2302012-11-07 14:53:34 -08002870 }
Vikram Aggarwalf0ef2302012-11-07 14:53:34 -08002871 }
2872
2873 /**
Vikram Aggarwale8a85322012-04-24 09:01:38 -07002874 * Destroy the pending {@link DestructiveAction} till now and assign the given action as the
2875 * next destructive action..
2876 * @param nextAction the next destructive action to be performed. This can be null.
2877 */
2878 private final void destroyPending(DestructiveAction nextAction) {
2879 // If there is a pending action, perform that first.
2880 if (mPendingDestruction != null) {
2881 mPendingDestruction.performAction();
2882 }
2883 mPendingDestruction = nextAction;
2884 }
2885
2886 /**
2887 * Register a destructive action with the controller. This performs the previous destructive
Vikram Aggarwalacaa3c02012-04-24 12:45:27 -07002888 * 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 -07002889 * embellish this method any more.
Vikram Aggarwale8a85322012-04-24 09:01:38 -07002890 * @param action
2891 */
Vikram Aggarwal3d7ca9d2012-05-11 14:40:36 -07002892 private final void registerDestructiveAction(DestructiveAction action) {
Vikram Aggarwale8a85322012-04-24 09:01:38 -07002893 // TODO(viki): This is not a good idea. The best solution is for clients to request a
2894 // destructive action from the controller and for the controller to own the action. This is
2895 // a half-way solution while refactoring DestructiveAction.
2896 destroyPending(action);
2897 return;
2898 }
2899
Vikram Aggarwal531488e2012-05-29 16:36:52 -07002900 @Override
2901 public final DestructiveAction getBatchAction(int action) {
Vikram Aggarwalc4113952012-05-11 14:14:56 -07002902 final DestructiveAction da = new ConversationAction(action, mSelectedSet.values(), true);
Vikram Aggarwale8a85322012-04-24 09:01:38 -07002903 registerDestructiveAction(da);
2904 return da;
2905 }
Vikram Aggarwal41e6e712012-04-24 11:22:57 -07002906
Mindy Pereirade3e74a2012-07-24 09:43:10 -07002907 @Override
2908 public final DestructiveAction getDeferredBatchAction(int action) {
mindypf0656a12012-10-01 08:30:57 -07002909 return getDeferredAction(action, mSelectedSet.values(), true);
2910 }
2911
2912 /**
2913 * Get a destructive action for a menu action. This is a temporary method,
2914 * to control the profusion of {@link DestructiveAction} classes that are
2915 * created. Please do not copy this paradigm.
2916 * @param action the resource ID of the menu action: R.id.delete, for
2917 * example
2918 * @param target the conversations to act upon.
2919 * @return a {@link DestructiveAction} that performs the specified action.
2920 */
Vikram Aggarwalb8c31712013-01-03 17:03:19 -08002921 private final DestructiveAction getDeferredAction(int action, Collection<Conversation> target,
mindypf0656a12012-10-01 08:30:57 -07002922 boolean batch) {
Vikram Aggarwalb8c31712013-01-03 17:03:19 -08002923 return new ConversationAction(action, target, batch);
Mindy Pereirade3e74a2012-07-24 09:43:10 -07002924 }
2925
Vikram Aggarwalbc67bb12012-04-30 14:05:35 -07002926 /**
2927 * Class to change the folders that are assigned to a set of conversations. This is destructive
2928 * because the user can remove the current folder from the conversation, in which case it has
2929 * to be animated away from the current folder.
2930 */
Vikram Aggarwal41e6e712012-04-24 11:22:57 -07002931 private class FolderDestruction implements DestructiveAction {
Paul Westbrook77eee622012-07-10 13:41:57 -07002932 private final Collection<Conversation> mTarget;
Mindy Pereira8db7e402012-07-13 10:32:47 -07002933 private final ArrayList<FolderOperation> mFolderOps = new ArrayList<FolderOperation>();
Vikram Aggarwal41e6e712012-04-24 11:22:57 -07002934 private final boolean mIsDestructive;
Vikram Aggarwal7f602f72012-04-30 16:04:06 -07002935 /** Whether this destructive action has already been performed */
2936 private boolean mCompleted;
Mindy Pereiraf3a45562012-05-24 16:30:19 -07002937 private boolean mIsSelectedSet;
Mindy Pereira06642fa2012-07-12 16:23:27 -07002938 private boolean mShowUndo;
Mindy Pereira01f30502012-08-14 10:30:51 -07002939 private int mAction;
Vikram Aggarwal41e6e712012-04-24 11:22:57 -07002940
2941 /**
2942 * Create a new folder destruction object to act on the given conversations.
2943 * @param target
2944 */
Vikram Aggarwal7f602f72012-04-30 16:04:06 -07002945 private FolderDestruction(final Collection<Conversation> target,
Mindy Pereira8db7e402012-07-13 10:32:47 -07002946 final Collection<FolderOperation> folders, boolean isDestructive, boolean isBatch,
Mindy Pereira01f30502012-08-14 10:30:51 -07002947 boolean showUndo, int action) {
Paul Westbrook77eee622012-07-10 13:41:57 -07002948 mTarget = ImmutableList.copyOf(target);
Mindy Pereira8db7e402012-07-13 10:32:47 -07002949 mFolderOps.addAll(folders);
Vikram Aggarwal41e6e712012-04-24 11:22:57 -07002950 mIsDestructive = isDestructive;
Mindy Pereiraf3a45562012-05-24 16:30:19 -07002951 mIsSelectedSet = isBatch;
Mindy Pereira06642fa2012-07-12 16:23:27 -07002952 mShowUndo = showUndo;
Mindy Pereira01f30502012-08-14 10:30:51 -07002953 mAction = action;
Vikram Aggarwal41e6e712012-04-24 11:22:57 -07002954 }
2955
Vikram Aggarwal41e6e712012-04-24 11:22:57 -07002956 @Override
2957 public void performAction() {
Vikram Aggarwal7f602f72012-04-30 16:04:06 -07002958 if (isPerformed()) {
2959 return;
2960 }
Mindy Pereira06642fa2012-07-12 16:23:27 -07002961 if (mIsDestructive && mShowUndo) {
mindypcb0b30e2012-11-30 10:16:35 -08002962 ToastBarOperation undoOp = new ToastBarOperation(mTarget.size(), mAction,
2963 ToastBarOperation.UNDO, mIsSelectedSet);
Vikram Aggarwal41e6e712012-04-24 11:22:57 -07002964 onUndoAvailable(undoOp);
2965 }
Mindy Pereira8db7e402012-07-13 10:32:47 -07002966 // For each conversation, for each operation, add/ remove the
2967 // appropriate folders.
mindypcb0b30e2012-11-30 10:16:35 -08002968 ArrayList<ConversationOperation> ops = new ArrayList<ConversationOperation>();
2969 ArrayList<Uri> folderUris;
2970 ArrayList<Boolean> adds;
Mindy Pereira8db7e402012-07-13 10:32:47 -07002971 for (Conversation target : mTarget) {
mindypcb0b30e2012-11-30 10:16:35 -08002972 HashMap<Uri, Folder> targetFolders = Folder.hashMapForFolders(target
2973 .getRawFolders());
2974 folderUris = new ArrayList<Uri>();
mindypcb0b30e2012-11-30 10:16:35 -08002975 adds = new ArrayList<Boolean>();
Mindy Pereira01f30502012-08-14 10:30:51 -07002976 if (mIsDestructive) {
2977 target.localDeleteOnUpdate = true;
2978 }
Mindy Pereira8db7e402012-07-13 10:32:47 -07002979 for (FolderOperation op : mFolderOps) {
mindypcb0b30e2012-11-30 10:16:35 -08002980 folderUris.add(op.mFolder.uri);
2981 adds.add(op.mAdd ? Boolean.TRUE : Boolean.FALSE);
Mindy Pereira8db7e402012-07-13 10:32:47 -07002982 if (op.mAdd) {
2983 targetFolders.put(op.mFolder.uri, op.mFolder);
2984 } else {
2985 targetFolders.remove(op.mFolder.uri);
2986 }
2987 }
Paul Westbrook26746eb2012-12-06 14:44:01 -08002988 ops.add(mConversationListCursor.getConversationFolderOperation(target,
2989 folderUris, adds, targetFolders.values()));
mindyp389f0b22012-08-29 11:12:54 -07002990 }
2991 if (mConversationListCursor != null) {
mindypcb0b30e2012-11-30 10:16:35 -08002992 mConversationListCursor.updateBulkValues(mContext, ops);
Mindy Pereira8db7e402012-07-13 10:32:47 -07002993 }
Vikram Aggarwal7f602f72012-04-30 16:04:06 -07002994 refreshConversationList();
Mindy Pereiraf3a45562012-05-24 16:30:19 -07002995 if (mIsSelectedSet) {
2996 mSelectedSet.clear();
2997 }
Vikram Aggarwal7f602f72012-04-30 16:04:06 -07002998 }
Mindy Pereira8db7e402012-07-13 10:32:47 -07002999
Vikram Aggarwal7f602f72012-04-30 16:04:06 -07003000 /**
3001 * Returns true if this action has been performed, false otherwise.
Andy Huang839ada22012-07-20 15:48:40 -07003002 *
Vikram Aggarwal7f602f72012-04-30 16:04:06 -07003003 */
3004 private synchronized boolean isPerformed() {
3005 if (mCompleted) {
3006 return true;
Vikram Aggarwal41e6e712012-04-24 11:22:57 -07003007 }
Vikram Aggarwal7f602f72012-04-30 16:04:06 -07003008 mCompleted = true;
3009 return false;
Vikram Aggarwal41e6e712012-04-24 11:22:57 -07003010 }
3011 }
Vikram Aggarwal7f602f72012-04-30 16:04:06 -07003012
mindypc84759c2012-08-29 09:51:53 -07003013 public final DestructiveAction getFolderChange(Collection<Conversation> target,
3014 Collection<FolderOperation> folders, boolean isDestructive, boolean isBatch,
3015 boolean showUndo) {
3016 final DestructiveAction da = getDeferredFolderChange(target, folders, isDestructive,
3017 isBatch, showUndo);
3018 registerDestructiveAction(da);
3019 return da;
3020 }
3021
3022 public final DestructiveAction getDeferredFolderChange(Collection<Conversation> target,
Mindy Pereira8db7e402012-07-13 10:32:47 -07003023 Collection<FolderOperation> folders, boolean isDestructive, boolean isBatch,
3024 boolean showUndo) {
Mindy Pereira06642fa2012-07-12 16:23:27 -07003025 final DestructiveAction da = new FolderDestruction(target, folders, isDestructive, isBatch,
Mindy Pereira01f30502012-08-14 10:30:51 -07003026 showUndo, R.id.change_folder);
Mindy Pereira01f30502012-08-14 10:30:51 -07003027 return da;
3028 }
3029
3030 @Override
3031 public final DestructiveAction getDeferredRemoveFolder(Collection<Conversation> target,
3032 Folder toRemove, boolean isDestructive, boolean isBatch,
3033 boolean showUndo) {
3034 Collection<FolderOperation> folderOps = new ArrayList<FolderOperation>();
3035 folderOps.add(new FolderOperation(toRemove, false));
3036 return new FolderDestruction(target, folderOps, isDestructive, isBatch,
3037 showUndo, R.id.remove_folder);
3038 }
3039
Vikram Aggarwal4f4782b2012-05-30 08:39:09 -07003040 @Override
3041 public final void refreshConversationList() {
Vikram Aggarwal75daee52012-04-30 11:13:09 -07003042 final ConversationListFragment convList = getConversationListFragment();
3043 if (convList == null) {
3044 return;
3045 }
3046 convList.requestListRefresh();
3047 }
Andrew Sappersteinc2c9dc12012-07-02 18:17:32 -07003048
3049 protected final ActionClickedListener getUndoClickedListener(
3050 final AnimatedAdapter listAdapter) {
3051 return new ActionClickedListener() {
3052 @Override
3053 public void onActionClicked() {
3054 if (mAccount.undoUri != null) {
3055 // NOTE: We might want undo to return the messages affected, in which case
3056 // the resulting cursor might be interesting...
3057 // TODO: Use UIProvider.SEQUENCE_QUERY_PARAMETER to indicate the set of
3058 // commands to undo
3059 if (mConversationListCursor != null) {
3060 mConversationListCursor.undo(
3061 mActivity.getActivityContext(), mAccount.undoUri);
3062 }
3063 if (listAdapter != null) {
3064 listAdapter.setUndo(true);
3065 }
3066 }
3067 }
3068 };
3069 }
3070
Vikram Aggarwal41b9e8f2012-09-25 10:15:04 -07003071 /**
3072 * Shows an error toast in the bottom when a folder was not fetched successfully.
3073 * @param folder the folder which could not be fetched.
3074 * @param replaceVisibleToast if true, this should replace any currently visible toast.
3075 */
Andrew Sapperstein9d7519d2012-07-16 14:03:53 -07003076 protected final void showErrorToast(final Folder folder, boolean replaceVisibleToast) {
Andrew Sappersteinc2c9dc12012-07-02 18:17:32 -07003077 mToastBar.setConversationMode(false);
Andrew Sapperstein00179f12012-08-09 15:15:40 -07003078
Vikram Aggarwal41b9e8f2012-09-25 10:15:04 -07003079 final ActionClickedListener listener;
3080 final int actionTextResourceId;
Andrew Sapperstein00179f12012-08-09 15:15:40 -07003081 final int lastSyncResult = folder.lastSyncResult;
Vikram Aggarwal41b9e8f2012-09-25 10:15:04 -07003082 switch (lastSyncResult & 0x0f) {
Andrew Sapperstein00179f12012-08-09 15:15:40 -07003083 case UIProvider.LastSyncResult.CONNECTION_ERROR:
Vikram Aggarwal41b9e8f2012-09-25 10:15:04 -07003084 // The sync request that caused this failure.
3085 final int syncRequest = lastSyncResult >> 4;
3086 // Show: User explicitly pressed the refresh button and there is no connection
3087 // Show: The first time the user enters the app and there is no connection
3088 // TODO(viki): Implement this.
3089 // Reference: http://b/7202801
3090 final boolean showToast = (syncRequest & UIProvider.SyncStatus.USER_REFRESH) != 0;
3091 // Don't show: Already in the app; user switches to a synced label
3092 // Don't show: In a live label and a background sync fails
3093 final boolean avoidToast = !showToast && (folder.syncWindow > 0
3094 || (syncRequest & UIProvider.SyncStatus.BACKGROUND_SYNC) != 0);
3095 if (avoidToast) {
3096 return;
3097 }
Andrew Sapperstein00179f12012-08-09 15:15:40 -07003098 listener = getRetryClickedListener(folder);
3099 actionTextResourceId = R.string.retry;
3100 break;
3101 case UIProvider.LastSyncResult.AUTH_ERROR:
3102 listener = getSignInClickedListener();
3103 actionTextResourceId = R.string.signin;
3104 break;
3105 case UIProvider.LastSyncResult.SECURITY_ERROR:
3106 return; // Currently we do nothing for security errors.
3107 case UIProvider.LastSyncResult.STORAGE_ERROR:
3108 listener = getStorageErrorClickedListener();
3109 actionTextResourceId = R.string.info;
3110 break;
3111 case UIProvider.LastSyncResult.INTERNAL_ERROR:
3112 listener = getInternalErrorClickedListener();
3113 actionTextResourceId = R.string.report;
3114 break;
3115 default:
3116 return;
3117 }
Vikram Aggarwal41b9e8f2012-09-25 10:15:04 -07003118 mToastBar.show(listener,
Andrew Sapperstein5d420962012-07-12 16:43:10 -07003119 R.drawable.ic_alert_white,
Vikram Aggarwal41b9e8f2012-09-25 10:15:04 -07003120 Utils.getSyncStatusText(mActivity.getActivityContext(), lastSyncResult),
Andrew Sappersteinc2c9dc12012-07-02 18:17:32 -07003121 false, /* showActionIcon */
Andrew Sapperstein00179f12012-08-09 15:15:40 -07003122 actionTextResourceId,
Andrew Sapperstein9d7519d2012-07-16 14:03:53 -07003123 replaceVisibleToast,
mindypa59283a2012-09-11 17:49:06 -07003124 new ToastBarOperation(1, 0, ToastBarOperation.ERROR, false));
Andrew Sappersteinc2c9dc12012-07-02 18:17:32 -07003125 }
3126
Andrew Sapperstein00179f12012-08-09 15:15:40 -07003127 private ActionClickedListener getRetryClickedListener(final Folder folder) {
Andrew Sappersteinc2c9dc12012-07-02 18:17:32 -07003128 return new ActionClickedListener() {
3129 @Override
3130 public void onActionClicked() {
3131 final Uri uri = folder.refreshUri;
3132
3133 if (uri != null) {
Paul Westbrook4969e0c2012-08-20 14:38:39 -07003134 startAsyncRefreshTask(uri);
Andrew Sappersteinc2c9dc12012-07-02 18:17:32 -07003135 }
3136 }
3137 };
3138 }
Andrew Sapperstein00179f12012-08-09 15:15:40 -07003139
3140 private ActionClickedListener getSignInClickedListener() {
3141 return new ActionClickedListener() {
3142 @Override
3143 public void onActionClicked() {
Paul Westbrook122f7c22012-08-20 17:50:31 -07003144 promptUserForAuthentication(mAccount);
Andrew Sapperstein00179f12012-08-09 15:15:40 -07003145 }
3146 };
3147 }
3148
3149 private ActionClickedListener getStorageErrorClickedListener() {
3150 return new ActionClickedListener() {
3151 @Override
3152 public void onActionClicked() {
Paul Westbrook4969e0c2012-08-20 14:38:39 -07003153 showStorageErrorDialog();
Andrew Sapperstein00179f12012-08-09 15:15:40 -07003154 }
3155 };
3156 }
3157
Paul Westbrook4969e0c2012-08-20 14:38:39 -07003158 private void showStorageErrorDialog() {
3159 DialogFragment fragment = (DialogFragment)
3160 mFragmentManager.findFragmentByTag(SYNC_ERROR_DIALOG_FRAGMENT_TAG);
3161 if (fragment == null) {
3162 fragment = SyncErrorDialogFragment.newInstance();
3163 }
3164 fragment.show(mFragmentManager, SYNC_ERROR_DIALOG_FRAGMENT_TAG);
3165 }
3166
Andrew Sapperstein00179f12012-08-09 15:15:40 -07003167 private ActionClickedListener getInternalErrorClickedListener() {
3168 return new ActionClickedListener() {
3169 @Override
3170 public void onActionClicked() {
Paul Westbrook17beb0b2012-08-20 13:34:37 -07003171 Utils.sendFeedback(
3172 mActivity.getActivityContext(), mAccount, true /* reportingProblem */);
Andrew Sapperstein00179f12012-08-09 15:15:40 -07003173 }
3174 };
3175 }
Paul Westbrook4969e0c2012-08-20 14:38:39 -07003176
3177 @Override
3178 public void onFooterViewErrorActionClick(Folder folder, int errorStatus) {
3179 Uri uri = null;
3180 switch (errorStatus) {
3181 case UIProvider.LastSyncResult.CONNECTION_ERROR:
3182 if (folder != null && folder.refreshUri != null) {
3183 uri = folder.refreshUri;
3184 }
3185 break;
3186 case UIProvider.LastSyncResult.AUTH_ERROR:
Paul Westbrook122f7c22012-08-20 17:50:31 -07003187 promptUserForAuthentication(mAccount);
Paul Westbrook4969e0c2012-08-20 14:38:39 -07003188 return;
3189 case UIProvider.LastSyncResult.SECURITY_ERROR:
3190 return; // Currently we do nothing for security errors.
3191 case UIProvider.LastSyncResult.STORAGE_ERROR:
3192 showStorageErrorDialog();
3193 return;
3194 case UIProvider.LastSyncResult.INTERNAL_ERROR:
3195 Utils.sendFeedback(
3196 mActivity.getActivityContext(), mAccount, true /* reportingProblem */);
3197 return;
3198 default:
3199 return;
3200 }
3201
3202 if (uri != null) {
3203 startAsyncRefreshTask(uri);
3204 }
3205 }
3206
3207 @Override
3208 public void onFooterViewLoadMoreClick(Folder folder) {
3209 if (folder != null && folder.loadMoreUri != null) {
3210 startAsyncRefreshTask(folder.loadMoreUri);
3211 }
3212 }
3213
3214 private void startAsyncRefreshTask(Uri uri) {
3215 if (mFolderSyncTask != null) {
3216 mFolderSyncTask.cancel(true);
3217 }
3218 mFolderSyncTask = new AsyncRefreshTask(mActivity.getActivityContext(), uri);
3219 mFolderSyncTask.execute();
3220 }
Paul Westbrook122f7c22012-08-20 17:50:31 -07003221
3222 private void promptUserForAuthentication(Account account) {
Paul Westbrook429399b2012-08-24 11:19:17 -07003223 if (account != null && !Utils.isEmpty(account.reauthenticationIntentUri)) {
Paul Westbrook122f7c22012-08-20 17:50:31 -07003224 final Intent authenticationIntent =
3225 new Intent(Intent.ACTION_VIEW, account.reauthenticationIntentUri);
3226 mActivity.startActivityForResult(authenticationIntent, REAUTHENTICATE_REQUEST_CODE);
3227 }
3228 }
mindypca87de42012-09-28 15:02:39 -07003229
3230 @Override
3231 public void onAccessibilityStateChanged() {
3232 // Clear the cache of objects.
3233 ConversationItemViewModel.onAccessibilityUpdated();
3234 // Re-render the list if it exists.
Vikram Aggarwala91d00b2013-01-18 12:00:37 -08003235 final ConversationListFragment frag = getConversationListFragment();
mindypca87de42012-09-28 15:02:39 -07003236 if (frag != null) {
3237 AnimatedAdapter adapter = frag.getAnimatedAdapter();
3238 if (adapter != null) {
3239 adapter.notifyDataSetInvalidated();
3240 }
3241 }
3242 }
Vikram Aggarwal6cadbfc2012-12-27 09:17:05 -08003243
3244 @Override
Vikram Aggarwalb8c31712013-01-03 17:03:19 -08003245 public void makeDialogListener (final int action, boolean isBatch) {
3246 final Collection<Conversation> target;
Vikram Aggarwalb8c31712013-01-03 17:03:19 -08003247 if (isBatch) {
3248 target = mSelectedSet.values();
Vikram Aggarwalb8c31712013-01-03 17:03:19 -08003249 } else {
3250 LogUtils.d(LOG_TAG, "Will act upon %s", mCurrentConversation);
3251 target = Conversation.listOf(mCurrentConversation);
Vikram Aggarwalb8c31712013-01-03 17:03:19 -08003252 }
3253 final DestructiveAction destructiveAction = getDeferredAction(action, target, isBatch);
Vikram Aggarwal6cadbfc2012-12-27 09:17:05 -08003254 mDialogAction = action;
Vikram Aggarwalb8c31712013-01-03 17:03:19 -08003255 mDialogFromSelectedSet = isBatch;
Vikram Aggarwal6cadbfc2012-12-27 09:17:05 -08003256 mDialogListener = new AlertDialog.OnClickListener() {
3257 @Override
3258 public void onClick(DialogInterface dialog, int which) {
Vikram Aggarwal669947b2013-01-10 17:05:56 -08003259 delete(action, target, destructiveAction);
Vikram Aggarwal6cadbfc2012-12-27 09:17:05 -08003260 // Afterwards, let's remove references to the listener and the action.
3261 setListener(null, -1);
3262 }
3263 };
3264 }
3265
3266 @Override
3267 public AlertDialog.OnClickListener getListener() {
3268 return mDialogListener;
3269 }
3270
3271 /**
3272 * Sets the listener for the positive action on a confirmation dialog. Since only a single
3273 * confirmation dialog can be shown, this overwrites the previous listener. It is safe to
3274 * unset the listener; in which case action should be set to -1.
3275 * @param listener
3276 * @param action
3277 */
3278 private void setListener(AlertDialog.OnClickListener listener, final int action){
3279 mDialogListener = listener;
3280 mDialogAction = action;
3281 }
Vikram Aggarwal69a6cdf2013-01-08 16:05:17 -08003282
3283 @Override
3284 public VeiledAddressMatcher getVeiledAddressMatcher() {
3285 return mVeiledMatcher;
Vikram Aggarwala91d00b2013-01-18 12:00:37 -08003286 }
3287
3288 @Override
3289 public void setDetachedMode() {
3290 // Tell the conversation list not to select anything.
3291 final ConversationListFragment frag = getConversationListFragment();
3292 if (frag != null) {
3293 frag.setChoiceNone();
3294 } else {
3295 // How did we ever land here? Detached mode, and no CLF???
3296 LogUtils.e(LOG_TAG, "AAC.setDetachedMode(): CLF = null!");
3297 }
3298 mDetachedConvUri = mCurrentConversation.uri;
3299 }
3300
3301 private void clearDetachedMode() {
3302 // Tell the conversation list to go back to its usual selection behavior.
3303 final ConversationListFragment frag = getConversationListFragment();
3304 if (frag != null) {
3305 frag.revertChoiceMode();
3306 } else {
3307 // How did we ever land here? Detached mode, and no CLF???
3308 LogUtils.e(LOG_TAG, "AAC.clearDetachedMode(): CLF = null!");
3309 }
3310 mDetachedConvUri = null;
3311 }
3312
Vikram Aggarwal4a5c5302012-01-12 15:07:13 -08003313}