blob: b0012e97c8db6add0a8ea0abe9917c7155752ded [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 Aggarwal80aeac52012-02-07 15:27:20 -080039import android.database.Cursor;
Andy Huang632721e2012-04-11 16:57:26 -070040import android.database.DataSetObservable;
41import android.database.DataSetObserver;
Paul Westbrook23b74b92012-02-29 11:36:12 -080042import android.net.Uri;
Vikram Aggarwal27d89ad2012-06-12 13:38:40 -070043import android.os.AsyncTask;
Vikram Aggarwala55b36c2012-01-13 11:45:02 -080044import android.os.Bundle;
Mindy Pereira21ab4902012-03-19 18:48:03 -070045import android.os.Handler;
Vikram Aggarwale620a7a2012-03-28 13:16:14 -070046import android.provider.SearchRecentSuggestions;
Vikram Aggarwal649b9ea2012-08-27 12:15:20 -070047import android.text.TextUtils;
Mindy Pereiraacf60392012-04-06 09:11:00 -070048import android.view.DragEvent;
Vikram Aggarwala55b36c2012-01-13 11:45:02 -080049import android.view.KeyEvent;
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -080050import android.view.LayoutInflater;
Vikram Aggarwala55b36c2012-01-13 11:45:02 -080051import android.view.Menu;
Mindy Pereira28d5f722012-02-15 12:32:40 -080052import android.view.MenuInflater;
Vikram Aggarwala55b36c2012-01-13 11:45:02 -080053import android.view.MenuItem;
54import android.view.MotionEvent;
Mindy Pereirad33674992012-06-25 16:26:30 -070055import android.view.View;
Mindy Pereirafd0c2972012-03-27 13:50:39 -070056import android.widget.Toast;
Vikram Aggarwala55b36c2012-01-13 11:45:02 -080057
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -080058import com.android.mail.ConversationListContext;
Andy Huangf9a73482012-03-13 15:54:02 -070059import com.android.mail.R;
Vikram Aggarwal6cadbfc2012-12-27 09:17:05 -080060import com.android.mail.browse.ConfirmDialogFragment;
Mindy Pereira967ede62012-03-22 09:29:09 -070061import com.android.mail.browse.ConversationCursor;
mindyp84f7d322012-10-01 17:14:40 -070062import com.android.mail.browse.ConversationItemView;
mindypca87de42012-09-28 15:02:39 -070063import com.android.mail.browse.ConversationItemViewModel;
Paul Westbrookbf232c32012-04-18 03:17:41 -070064import com.android.mail.browse.ConversationPagerController;
Vikram Aggarwalcc754b12012-08-30 14:04:21 -070065import com.android.mail.browse.ConversationCursor.ConversationOperation;
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;
Scott Kennedy0d0f8b02012-10-12 15:18:18 -070091
Paul Westbrookca08fc12012-07-31 12:01:15 -070092import com.google.common.base.Objects;
Paul Westbrook77eee622012-07-10 13:41:57 -070093import com.google.common.collect.ImmutableList;
Mindy Pereiraacf60392012-04-06 09:11:00 -070094import com.google.common.collect.Lists;
Marc Blank167faa82012-03-21 13:11:53 -070095import com.google.common.collect.Sets;
Vikram Aggarwal4a5c5302012-01-12 15:07:13 -080096
Marc Blank167faa82012-03-21 13:11:53 -070097import java.util.ArrayList;
Mindy Pereirafbe40192012-03-20 10:40:45 -070098import java.util.Collection;
Andy Huang839ada22012-07-20 15:48:40 -070099import java.util.Collections;
Mindy Pereira8db7e402012-07-13 10:32:47 -0700100import java.util.HashMap;
Vikram Aggarwalcc754b12012-08-30 14:04:21 -0700101import java.util.List;
Paul Westbrook23b74b92012-02-29 11:36:12 -0800102import java.util.Set;
Marc Blankbf128eb2012-04-18 15:58:45 -0700103import java.util.TimerTask;
Paul Westbrook23b74b92012-02-29 11:36:12 -0800104
105
Vikram Aggarwal4a5c5302012-01-12 15:07:13 -0800106/**
Mindy Pereira161f50d2012-02-28 15:47:19 -0800107 * This is an abstract implementation of the Activity Controller. This class
108 * knows how to respond to menu items, state changes, layout changes, etc. It
109 * weaves together the views and listeners, dispatching actions to the
110 * respective underlying classes.
Vikram Aggarwala55b36c2012-01-13 11:45:02 -0800111 * <p>
Mindy Pereira161f50d2012-02-28 15:47:19 -0800112 * Even though this class is abstract, it should provide default implementations
113 * for most, if not all the methods in the ActivityController interface. This
114 * makes the task of the subclasses easier: OnePaneActivityController and
115 * TwoPaneActivityController can be concise when the common functionality is in
116 * AbstractActivityController.
117 * </p>
118 * <p>
119 * In the Gmail codebase, this was called BaseActivityController
120 * </p>
Vikram Aggarwal4a5c5302012-01-12 15:07:13 -0800121 */
Vikram Aggarwal531488e2012-05-29 16:36:52 -0700122public abstract class AbstractActivityController implements ActivityController {
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -0800123 // Keys for serialization of various information in Bundles.
Vikram Aggarwalcabd3f22012-04-19 10:14:41 -0700124 /** Tag for {@link #mAccount} */
Vikram Aggarwal6c511582012-02-27 10:59:47 -0800125 private static final String SAVED_ACCOUNT = "saved-account";
Vikram Aggarwalcabd3f22012-04-19 10:14:41 -0700126 /** Tag for {@link #mFolder} */
Mindy Pereira5e478d22012-03-26 18:04:58 -0700127 private static final String SAVED_FOLDER = "saved-folder";
Vikram Aggarwalcabd3f22012-04-19 10:14:41 -0700128 /** Tag for {@link #mCurrentConversation} */
Mindy Pereira26f23fc2012-03-27 10:26:04 -0700129 private static final String SAVED_CONVERSATION = "saved-conversation";
Vikram Aggarwalcabd3f22012-04-19 10:14:41 -0700130 /** Tag for {@link #mSelectedSet} */
131 private static final String SAVED_SELECTED_SET = "saved-selected-set";
Vikram Aggarwal649b9ea2012-08-27 12:15:20 -0700132 /** Tag for {@link ActionableToastBar#getOperation()} */
Mindy Pereirad33674992012-06-25 16:26:30 -0700133 private static final String SAVED_TOAST_BAR_OP = "saved-toast-bar-op";
Vikram Aggarwal649b9ea2012-08-27 12:15:20 -0700134 /** Tag for {@link #mFolderListFolder} */
135 private static final String SAVED_HIERARCHICAL_FOLDER = "saved-hierarchical-folder";
Vikram Aggarwalf3341402012-08-07 10:09:38 -0700136 /** Tag for {@link ConversationListContext#searchQuery} */
137 private static final String SAVED_QUERY = "saved-query";
Vikram Aggarwal6cadbfc2012-12-27 09:17:05 -0800138 /** Tag for {@link #mDialogAction} */
139 private static final String SAVED_ACTION = "saved-action";
Mindy Pereira5cb0c3e2012-02-22 15:22:47 -0800140
Vikram Aggarwal6902dcf2012-04-11 08:57:42 -0700141 /** Tag used when loading a wait fragment */
142 protected static final String TAG_WAIT = "wait-fragment";
143 /** Tag used when loading a conversation list fragment. */
Paul Westbrookbf232c32012-04-18 03:17:41 -0700144 public static final String TAG_CONVERSATION_LIST = "tag-conversation-list";
Vikram Aggarwal6902dcf2012-04-11 08:57:42 -0700145 /** Tag used when loading a folder list fragment. */
146 protected static final String TAG_FOLDER_LIST = "tag-folder-list";
147
Vikram Aggarwald7a12cd2012-02-03 09:36:20 -0800148 protected Account mAccount;
Mindy Pereira12a4d802012-06-22 09:24:43 -0700149 protected Folder mFolder;
Vikram Aggarwal69b5c302012-09-05 11:11:13 -0700150 /** True when {@link #mFolder} is first shown to the user. */
151 private boolean mFolderChanged = false;
Andy Huang6681e542012-06-14 14:36:45 -0700152 protected MailActionBarView mActionBarView;
Vikram Aggarwal7c401b72012-08-13 16:43:47 -0700153 protected final ControllableActivity mActivity;
Vikram Aggarwala55b36c2012-01-13 11:45:02 -0800154 protected final Context mContext;
Vikram Aggarwal6902dcf2012-04-11 08:57:42 -0700155 private final FragmentManager mFragmentManager;
Vikram Aggarwalec5cbf72012-03-08 15:10:35 -0800156 protected final RecentFolderList mRecentFolderList;
Vikram Aggarwal80aeac52012-02-07 15:27:20 -0800157 protected ConversationListContext mConvListContext;
Mindy Pereira9b875682012-02-15 18:10:54 -0800158 protected Conversation mCurrentConversation;
Vikram Aggarwald7a12cd2012-02-03 09:36:20 -0800159
Paul Westbrook6ead20d2012-03-19 14:48:14 -0700160 /** A {@link android.content.BroadcastReceiver} that suppresses new e-mail notifications. */
161 private SuppressNotificationReceiver mNewEmailReceiver = null;
162
Mindy Pereirafbe40192012-03-20 10:40:45 -0700163 protected Handler mHandler = new Handler();
Mindy Pereirafa995b42012-07-25 12:06:13 -0700164
Vikram Aggarwalfa131a22012-02-02 13:56:22 -0800165 /**
Mindy Pereira161f50d2012-02-28 15:47:19 -0800166 * The current mode of the application. All changes in mode are initiated by
167 * the activity controller. View mode changes are propagated to classes that
168 * attach themselves as listeners of view mode changes.
Vikram Aggarwalfa131a22012-02-02 13:56:22 -0800169 */
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -0800170 protected final ViewMode mViewMode;
Vikram Aggarwal80aeac52012-02-07 15:27:20 -0800171 protected ContentResolver mResolver;
Vikram Aggarwal7dedb952012-02-16 16:10:23 -0800172 protected boolean isLoaderInitialized = false;
Mindy Pereirab7b33e02012-02-21 15:32:19 -0800173 private AsyncRefreshTask mAsyncRefreshTask;
Mindy Pereira5cb0c3e2012-02-22 15:22:47 -0800174
Andy Huang4e0158f2012-08-07 21:06:01 -0700175 private boolean mDestroyed;
176
Andy Huang1ee96b22012-08-24 20:19:53 -0700177 /**
178 * Are we in a point in the Activity/Fragment lifecycle where it's safe to execute fragment
179 * transactions? (including back stack manipulation)
180 * <p>
181 * Per docs in {@link FragmentManager#beginTransaction()}, this flag starts out true, switches
182 * to false after {@link Activity#onSaveInstanceState}, and becomes true again in both onStart
183 * and onResume.
184 */
185 private boolean mSafeToModifyFragments = true;
186
Paul Westbrook23b74b92012-02-29 11:36:12 -0800187 private final Set<Uri> mCurrentAccountUris = Sets.newHashSet();
Mindy Pereira967ede62012-03-22 09:29:09 -0700188 protected ConversationCursor mConversationListCursor;
Andy Huang632721e2012-04-11 16:57:26 -0700189 private final DataSetObservable mConversationListObservable = new DataSetObservable() {
190 @Override
191 public void registerObserver(DataSetObserver observer) {
192 final int count = mObservers.size();
193 super.registerObserver(observer);
Vikram Aggarwal7c401b72012-08-13 16:43:47 -0700194 LogUtils.d(LOG_TAG, "IN AAC.register(List)Observer: %s before=%d after=%d", observer,
Andy Huang632721e2012-04-11 16:57:26 -0700195 count, mObservers.size());
196 }
197 @Override
198 public void unregisterObserver(DataSetObserver observer) {
199 final int count = mObservers.size();
200 super.unregisterObserver(observer);
Vikram Aggarwal7c401b72012-08-13 16:43:47 -0700201 LogUtils.d(LOG_TAG, "IN AAC.unregister(List)Observer: %s before=%d after=%d", observer,
Andy Huang632721e2012-04-11 16:57:26 -0700202 count, mObservers.size());
203 }
204 };
Marc Blankbf128eb2012-04-18 15:58:45 -0700205
Marc Blankbf128eb2012-04-18 15:58:45 -0700206 private RefreshTimerTask mConversationListRefreshTask;
Marc Blanke1d1b072012-04-13 17:29:16 -0700207
Vikram Aggarwal7c401b72012-08-13 16:43:47 -0700208 /** Listeners that are interested in changes to the current account. */
209 private final DataSetObservable mAccountObservers = new DataSetObservable() {
210 @Override
211 public void registerObserver(DataSetObserver observer) {
212 final int count = mObservers.size();
213 super.registerObserver(observer);
214 LogUtils.d(LOG_TAG, "IN AAC.register(Account)Observer: %s before=%d after=%d",
215 observer, count, mObservers.size());
216 }
217 @Override
218 public void unregisterObserver(DataSetObserver observer) {
219 final int count = mObservers.size();
220 super.unregisterObserver(observer);
221 LogUtils.d(LOG_TAG, "IN AAC.unregister(Account)Observer: %s before=%d after=%d",
222 observer, count, mObservers.size());
223 }
224 };
Vikram Aggarwal7d816002012-04-17 17:06:41 -0700225
Vikram Aggarwal58cad2e2012-08-28 16:18:23 -0700226 /** Listeners that are interested in changes to the recent folders. */
227 private final DataSetObservable mRecentFolderObservers = new DataSetObservable() {
228 @Override
229 public void registerObserver(DataSetObserver observer) {
230 final int count = mObservers.size();
231 super.registerObserver(observer);
232 LogUtils.d(LOG_TAG, "IN AAC.register(RecentFolder)Observer: %s before=%d after=%d",
233 observer, count, mObservers.size());
234 }
235 @Override
236 public void unregisterObserver(DataSetObserver observer) {
237 final int count = mObservers.size();
238 super.unregisterObserver(observer);
239 LogUtils.d(LOG_TAG, "IN AAC.unregister(RecentFolder)Observer: %s before=%d after=%d",
240 observer, count, mObservers.size());
241 }
242 };
243
Mindy Pereira967ede62012-03-22 09:29:09 -0700244 /**
245 * Selected conversations, if any.
246 */
Andy Huang4556a442012-03-30 16:42:05 -0700247 private final ConversationSelectionSet mSelectedSet = new ConversationSelectionSet();
Mindy Pereiradac00542012-03-01 10:50:33 -0800248
Paul Westbrookc7a070f2012-04-12 01:46:41 -0700249 private final int mFolderItemUpdateDelayMs;
250
Vikram Aggarwal135fd022012-04-11 14:44:15 -0700251 /** Keeps track of selected and unselected conversations */
Paul Westbrook937c94f2012-08-16 13:01:18 -0700252 final protected ConversationPositionTracker mTracker;
Vikram Aggarwalaf1ee0c2012-04-12 17:13:13 -0700253
Vikram Aggarwale128fc22012-04-04 12:33:34 -0700254 /**
255 * Action menu associated with the selected set.
256 */
257 SelectedConversationsActionMenu mCabActionMenu;
Andrew Sappersteinc2c9dc12012-07-02 18:17:32 -0700258 protected ActionableToastBar mToastBar;
Andy Huang632721e2012-04-11 16:57:26 -0700259 protected ConversationPagerController mPagerController;
Mindy Pereiraab486362012-03-21 18:18:53 -0700260
Andy Huangb1c34dc2012-04-17 16:36:19 -0700261 // this is split out from the general loader dispatcher because its loader doesn't return a
262 // basic Cursor
263 private final ConversationListLoaderCallbacks mListCursorCallbacks =
264 new ConversationListLoaderCallbacks();
265
Andy Huang090db1e2012-07-25 13:25:28 -0700266 private final DataSetObservable mFolderObservable = new DataSetObservable();
267
Paul Westbrookb334c902012-06-25 11:42:46 -0700268 protected static final String LOG_TAG = LogTag.getLogTag();
Vikram Aggarwalfa4b47e2012-03-09 13:02:46 -0800269 /** Constants used to differentiate between the types of loaders. */
270 private static final int LOADER_ACCOUNT_CURSOR = 0;
Vikram Aggarwalfa4b47e2012-03-09 13:02:46 -0800271 private static final int LOADER_FOLDER_CURSOR = 2;
272 private static final int LOADER_RECENT_FOLDERS = 3;
Mindy Pereira967ede62012-03-22 09:29:09 -0700273 private static final int LOADER_CONVERSATION_LIST = 4;
Mindy Pereiraab486362012-03-21 18:18:53 -0700274 private static final int LOADER_ACCOUNT_INBOX = 5;
275 private static final int LOADER_SEARCH = 6;
Paul Westbrook2d50bcd2012-04-10 11:53:47 -0700276 private static final int LOADER_ACCOUNT_UPDATE_CURSOR = 7;
Vikram Aggarwal7a5d95a2012-07-27 16:24:54 -0700277 /**
278 * Guaranteed to be the last loader ID used by the activity. Loaders are owned by Activity or
279 * fragments, and within an activity, loader IDs need to be unique. A hack to ensure that the
280 * {@link FolderWatcher} can create its folder loaders without clashing with the IDs of those
281 * of the {@link AbstractActivityController}. Currently, the {@link FolderWatcher} is the only
282 * other class that uses this activity's LoaderManager. If another class needs activity-level
283 * loaders, consider consolidating the loaders in a central location: a UI-less fragment
284 * perhaps.
285 */
286 public static final int LAST_LOADER_ID = 100;
Vikram Aggarwala55b36c2012-01-13 11:45:02 -0800287
Paul Westbrook2388c5d2012-03-25 12:29:11 -0700288 private static final int ADD_ACCOUNT_REQUEST_CODE = 1;
Paul Westbrook122f7c22012-08-20 17:50:31 -0700289 private static final int REAUTHENTICATE_REQUEST_CODE = 2;
Paul Westbrook2388c5d2012-03-25 12:29:11 -0700290
Vikram Aggarwale8a85322012-04-24 09:01:38 -0700291 /** The pending destructive action to be carried out before swapping the conversation cursor.*/
292 private DestructiveAction mPendingDestruction;
Andrew Sappersteinc2c9dc12012-07-02 18:17:32 -0700293 protected AsyncRefreshTask mFolderSyncTask;
Mindy Pereira49e5dbe2012-07-12 11:47:54 -0700294 // Task for setting any share intents for the account to enabled.
295 // This gets cancelled if the user kills the app before it finishes, and
296 // will just run the next time the user opens the app.
297 private AsyncTask<String, Void, Void> mEnableShareIntents;
Mindy Pereirac975e842012-07-16 09:15:00 -0700298 private Folder mFolderListFolder;
mindyp5390fca2012-08-22 12:12:25 -0700299 private boolean mIsDragHappening;
mindypead50392012-08-23 11:03:53 -0700300 private int mShowUndoBarDelay;
mindyp6f54e1b2012-10-09 09:54:08 -0700301 private boolean mRecentsDataUpdated;
Vikram Aggarwala3f43d42012-10-25 16:21:30 -0700302 /** A wait fragment we added, if any. */
303 private WaitFragment mWaitFragment;
Vikram Aggarwalf0ef2302012-11-07 14:53:34 -0800304 /** True if we have results from a search query */
305 private boolean mHaveSearchResults = false;
Vikram Aggarwal6cadbfc2012-12-27 09:17:05 -0800306 /** If a confirmation dialog is being show, the listener for the positive action. */
307 private OnClickListener mDialogListener;
308 /**
309 * If a confirmation dialog is being show, the resource of the action: R.id.delete, etc. This
310 * is used to create a new {@link #mDialogListener} on orientation changes.
311 */
312 private int mDialogAction = -1;
313
Andrew Sapperstein00179f12012-08-09 15:15:40 -0700314 public static final String SYNC_ERROR_DIALOG_FRAGMENT_TAG = "SyncErrorDialogFragment";
Vikram Aggarwale8a85322012-04-24 09:01:38 -0700315
Vikram Aggarwala55b36c2012-01-13 11:45:02 -0800316 public AbstractActivityController(MailActivity activity, ViewMode viewMode) {
317 mActivity = activity;
Vikram Aggarwal6902dcf2012-04-11 08:57:42 -0700318 mFragmentManager = mActivity.getFragmentManager();
Vikram Aggarwala55b36c2012-01-13 11:45:02 -0800319 mViewMode = viewMode;
320 mContext = activity.getApplicationContext();
Vikram Aggarwal025eba82012-05-08 10:45:30 -0700321 mRecentFolderList = new RecentFolderList(mContext);
Paul Westbrook937c94f2012-08-16 13:01:18 -0700322 mTracker = new ConversationPositionTracker(this);
Mindy Pereira967ede62012-03-22 09:29:09 -0700323 // Allow the fragment to observe changes to its own selection set. No other object is
324 // aware of the selected set.
325 mSelectedSet.addObserver(this);
Paul Westbrookc7a070f2012-04-12 01:46:41 -0700326
327 mFolderItemUpdateDelayMs =
328 mContext.getResources().getInteger(R.integer.folder_item_refresh_delay_ms);
mindypead50392012-08-23 11:03:53 -0700329 mShowUndoBarDelay =
330 mContext.getResources().getInteger(R.integer.show_undo_bar_delay_ms);
Vikram Aggarwala55b36c2012-01-13 11:45:02 -0800331 }
Vikram Aggarwal4a5c5302012-01-12 15:07:13 -0800332
333 @Override
Vikram Aggarwale9a81032012-02-22 13:15:35 -0800334 public Account getCurrentAccount() {
335 return mAccount;
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -0800336 }
337
338 @Override
339 public ConversationListContext getCurrentListContext() {
Vikram Aggarwale9a81032012-02-22 13:15:35 -0800340 return mConvListContext;
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -0800341 }
342
343 @Override
Vikram Aggarwal4a5c5302012-01-12 15:07:13 -0800344 public String getHelpContext() {
Paul Westbrook30745b62012-08-19 14:10:32 -0700345 final int mode = mViewMode.getMode();
346 final int helpContextResId;
347 switch (mode) {
348 case ViewMode.WAITING_FOR_ACCOUNT_INITIALIZATION:
349 helpContextResId = R.string.wait_help_context;
350 break;
351 default:
352 helpContextResId = R.string.main_help_context;
353 }
354 return mContext.getString(helpContextResId);
Vikram Aggarwal4a5c5302012-01-12 15:07:13 -0800355 }
356
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -0800357 @Override
Vikram Aggarwalef7c9922012-04-23 12:35:04 -0700358 public final ConversationCursor getConversationListCursor() {
Mindy Pereira967ede62012-03-22 09:29:09 -0700359 return mConversationListCursor;
360 }
361
Vikram Aggarwal6902dcf2012-04-11 08:57:42 -0700362 /**
Vikram Aggarwal34a65012012-04-17 12:39:06 -0700363 * Check if the fragment is attached to an activity and has a root view.
364 * @param in
365 * @return true if the fragment is valid, false otherwise
366 */
367 private static final boolean isValidFragment(Fragment in) {
368 if (in == null || in.getActivity() == null || in.getView() == null) {
369 return false;
370 }
371 return true;
372 }
373
374 /**
Vikram Aggarwal49e0e992012-09-21 13:53:15 -0700375 * Get the conversation list fragment for this activity. If the conversation list fragment is
376 * not attached, this method returns null.
Andy Huang839ada22012-07-20 15:48:40 -0700377 *
Vikram Aggarwal49e0e992012-09-21 13:53:15 -0700378 * Caution! This method returns the {@link ConversationListFragment} after the fragment has been
379 * added, <b>and</b> after the {@link FragmentManager} has run through its queue to add the
380 * fragment. There is a non-trivial amount of time after the fragment is instantiated and before
381 * this call returns a non-null value, depending on the {@link FragmentManager}. If you
382 * need the fragment immediately after adding it, consider making the fragment an observer of
383 * the controller and perform the task immediately on {@link Fragment#onActivityCreated(Bundle)}
Vikram Aggarwal6902dcf2012-04-11 08:57:42 -0700384 */
385 protected ConversationListFragment getConversationListFragment() {
386 final Fragment fragment = mFragmentManager.findFragmentByTag(TAG_CONVERSATION_LIST);
Vikram Aggarwal34a65012012-04-17 12:39:06 -0700387 if (isValidFragment(fragment)) {
388 return (ConversationListFragment) fragment;
Andy Huang9585d782012-04-16 19:45:04 -0700389 }
Vikram Aggarwal34a65012012-04-17 12:39:06 -0700390 return null;
Vikram Aggarwal6902dcf2012-04-11 08:57:42 -0700391 }
392
393 /**
Vikram Aggarwal6902dcf2012-04-11 08:57:42 -0700394 * Returns the folder list fragment attached with this activity. If no such fragment is attached
395 * this method returns null.
Andy Huang839ada22012-07-20 15:48:40 -0700396 *
Vikram Aggarwal49e0e992012-09-21 13:53:15 -0700397 * Caution! This method returns the {@link FolderListFragment} after the fragment has been
398 * added, <b>and</b> after the {@link FragmentManager} has run through its queue to add the
399 * fragment. There is a non-trivial amount of time after the fragment is instantiated and before
400 * this call returns a non-null value, depending on the {@link FragmentManager}. If you
401 * need the fragment immediately after adding it, consider making the fragment an observer of
402 * the controller and perform the task immediately on {@link Fragment#onActivityCreated(Bundle)}
Vikram Aggarwal6902dcf2012-04-11 08:57:42 -0700403 */
404 protected FolderListFragment getFolderListFragment() {
405 final Fragment fragment = mFragmentManager.findFragmentByTag(TAG_FOLDER_LIST);
Vikram Aggarwal34a65012012-04-17 12:39:06 -0700406 if (isValidFragment(fragment)) {
407 return (FolderListFragment) fragment;
Andy Huang9585d782012-04-16 19:45:04 -0700408 }
Vikram Aggarwal34a65012012-04-17 12:39:06 -0700409 return null;
Vikram Aggarwal6902dcf2012-04-11 08:57:42 -0700410 }
411
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -0800412 /**
Mindy Pereira161f50d2012-02-28 15:47:19 -0800413 * Initialize the action bar. This is not visible to OnePaneController and
414 * TwoPaneController so they cannot override this behavior.
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -0800415 */
Vikram Aggarwalabd24d82012-04-26 13:23:14 -0700416 private void initializeActionBar() {
417 final ActionBar actionBar = mActivity.getActionBar();
Andy Huang5895f7b2012-06-01 17:07:20 -0700418 if (actionBar == null) {
419 return;
Vikram Aggarwalabd24d82012-04-26 13:23:14 -0700420 }
Andy Huang5895f7b2012-06-01 17:07:20 -0700421
422 // be sure to inherit from the ActionBar theme when inflating
423 final LayoutInflater inflater = LayoutInflater.from(actionBar.getThemedContext());
Mindy Pereira82faec72012-06-14 17:21:50 -0700424 final boolean isSearch = mActivity.getIntent() != null
425 && Intent.ACTION_SEARCH.equals(mActivity.getIntent().getAction());
426 mActionBarView = (MailActionBarView) inflater.inflate(
427 isSearch ? R.layout.search_actionbar_view : R.layout.actionbar_view, null);
Andy Huang5895f7b2012-06-01 17:07:20 -0700428 mActionBarView.initialize(mActivity, this, mViewMode, actionBar, mRecentFolderList);
Vikram Aggarwalabd24d82012-04-26 13:23:14 -0700429 }
430
431 /**
432 * Attach the action bar to the activity.
433 */
434 private void attachActionBar() {
435 final ActionBar actionBar = mActivity.getActionBar();
436 if (actionBar != null && mActionBarView != null) {
Vikram Aggarwalec5cbf72012-03-08 15:10:35 -0800437 actionBar.setCustomView(mActionBarView, new ActionBar.LayoutParams(
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -0800438 LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT));
Vikram Aggarwal649b9ea2012-08-27 12:15:20 -0700439 // Show a custom view and home icon, but remove the title
440 final int mask = ActionBar.DISPLAY_SHOW_CUSTOM | ActionBar.DISPLAY_SHOW_TITLE
441 | ActionBar.DISPLAY_SHOW_HOME;
442 final int enabled = ActionBar.DISPLAY_SHOW_CUSTOM | ActionBar.DISPLAY_SHOW_HOME;
443 actionBar.setDisplayOptions(enabled, mask);
Vikram Aggarwalabd24d82012-04-26 13:23:14 -0700444 mActionBarView.attach();
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -0800445 }
Vikram Aggarwalabd24d82012-04-26 13:23:14 -0700446 mViewMode.addListener(mActionBarView);
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -0800447 }
448
449 /**
Mindy Pereira161f50d2012-02-28 15:47:19 -0800450 * Returns whether the conversation list fragment is visible or not.
451 * Different layouts will have their own notion on the visibility of
452 * fragments, so this method needs to be overriden.
453 *
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -0800454 */
455 protected abstract boolean isConversationListVisible();
456
Vikram Aggarwaleb7d3292012-04-20 17:07:20 -0700457 /**
Vikram Aggarwal34f7b232012-10-17 13:32:23 -0700458 * If required, starts wait mode for the current account.
Vikram Aggarwal34f7b232012-10-17 13:32:23 -0700459 */
460 final void perhapsEnterWaitMode() {
461 // If the account is not initialized, then show the wait fragment, since nothing can be
462 // shown.
463 if (mAccount.isAccountInitializationRequired()) {
464 showWaitForInitialization();
465 return;
466 }
467
468 final boolean inWaitingMode = inWaitMode();
469 final boolean isSyncRequired = mAccount.isAccountSyncRequired();
470 if (isSyncRequired) {
471 if (inWaitingMode) {
472 // Update the WaitFragment's account object
473 updateWaitMode();
474 } else {
475 // Transition to waiting mode
476 showWaitForInitialization();
477 }
478 } else if (inWaitingMode) {
479 // Dismiss waiting mode
480 hideWaitForInitialization();
481 }
482 }
483
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -0800484 @Override
Mindy Pereira28e0c342012-02-17 15:05:13 -0800485 public void onAccountChanged(Account account) {
Vikram Aggarwal60069912012-07-24 14:26:09 -0700486 // Is the account or account settings different from the existing account?
Vikram Aggarwal66bc6ed2012-07-31 09:59:55 -0700487 final boolean firstLoad = mAccount == null;
488 final boolean accountChanged = firstLoad || !account.uri.equals(mAccount.uri);
Vikram Aggarwal472e5362012-11-05 14:17:23 -0800489 // If nothing has changed, return early without wasting any more time.
490 if (!accountChanged && !account.settingsDiffer(mAccount)) {
491 return;
492 }
493 // We also don't want to do anything if the new account is null
494 if (account == null) {
495 LogUtils.e(LOG_TAG, "AAC.onAccountChanged(null) called.");
496 return;
497 }
498 final String accountName = account.name;
499 mHandler.post(new Runnable() {
500 @Override
501 public void run() {
502 MailActivity.setForegroundNdef(MailActivity.getMailtoNdef(accountName));
Mindy Pereirafa20c1a2012-07-23 13:00:02 -0700503 }
Vikram Aggarwal472e5362012-11-05 14:17:23 -0800504 });
505 if (accountChanged) {
506 commitDestructiveActions(false);
507 }
508 // Change the account here
509 setAccount(account);
510 // And carry out associated actions.
511 cancelRefreshTask();
512 if (accountChanged) {
513 loadAccountInbox();
514 }
515 // Check if we need to force setting up an account before proceeding.
516 if (mAccount != null && !Uri.EMPTY.equals(mAccount.settings.setupIntentUri)) {
517 // Launch the intent!
518 final Intent intent = new Intent(Intent.ACTION_EDIT);
519 intent.setData(mAccount.settings.setupIntentUri);
520 mActivity.startActivity(intent);
Mindy Pereira28d5f722012-02-15 12:32:40 -0800521 }
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -0800522 }
523
Vikram Aggarwale6340bc2012-03-26 15:57:09 -0700524 /**
Vikram Aggarwal7c401b72012-08-13 16:43:47 -0700525 * Adds a listener interested in change in the current account. If a class is storing a
526 * reference to the current account, it should listen on changes, so it can receive updates to
527 * settings. Must happen in the UI thread.
528 */
Mindy Pereiraefe3d252012-03-01 14:20:44 -0800529 @Override
Vikram Aggarwal7c401b72012-08-13 16:43:47 -0700530 public void registerAccountObserver(DataSetObserver obs) {
531 mAccountObservers.registerObserver(obs);
Mindy Pereiraefe3d252012-03-01 14:20:44 -0800532 }
533
Vikram Aggarwal7d816002012-04-17 17:06:41 -0700534 /**
Vikram Aggarwal7c401b72012-08-13 16:43:47 -0700535 * Removes a listener from receiving current account changes.
Vikram Aggarwal7d816002012-04-17 17:06:41 -0700536 * Must happen in the UI thread.
537 */
Vikram Aggarwal7c401b72012-08-13 16:43:47 -0700538 @Override
539 public void unregisterAccountObserver(DataSetObserver obs) {
540 mAccountObservers.unregisterObserver(obs);
Vikram Aggarwal7d816002012-04-17 17:06:41 -0700541 }
542
Vikram Aggarwal7c401b72012-08-13 16:43:47 -0700543 @Override
544 public Account getAccount() {
545 return mAccount;
Vikram Aggarwal7d816002012-04-17 17:06:41 -0700546 }
547
Mindy Pereirae0828392012-03-08 10:38:40 -0800548 private void fetchSearchFolder(Intent intent) {
Vikram Aggarwal2b703c62012-09-18 13:54:15 -0700549 final Bundle args = new Bundle();
Mindy Pereiraab486362012-03-21 18:18:53 -0700550 args.putString(ConversationListContext.EXTRA_SEARCH_QUERY, intent
Mindy Pereirae0828392012-03-08 10:38:40 -0800551 .getStringExtra(ConversationListContext.EXTRA_SEARCH_QUERY));
Mindy Pereiraab486362012-03-21 18:18:53 -0700552 mActivity.getLoaderManager().restartLoader(LOADER_SEARCH, args, this);
Mindy Pereirae0828392012-03-08 10:38:40 -0800553 }
554
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -0800555 @Override
Mindy Pereira28e0c342012-02-17 15:05:13 -0800556 public void onFolderChanged(Folder folder) {
Vikram Aggarwalf3341402012-08-07 10:09:38 -0700557 changeFolder(folder, null);
558 }
559
560 /**
Vikram Aggarwal203dc002012-08-23 13:59:04 -0700561 * Sets the folder state without changing view mode and without creating a list fragment, if
562 * possible.
563 * @param folder
564 */
565 private void setListContext(Folder folder, String query) {
566 updateFolder(folder);
567 if (query != null) {
568 mConvListContext = ConversationListContext.forSearchQuery(mAccount, mFolder, query);
569 } else {
570 mConvListContext = ConversationListContext.forFolder(mAccount, mFolder);
571 }
Vikram Aggarwal203dc002012-08-23 13:59:04 -0700572 cancelRefreshTask();
573 }
574
575 /**
Vikram Aggarwalf3341402012-08-07 10:09:38 -0700576 * Changes the folder to the value provided here. This causes the view mode to change.
577 * @param folder the folder to change to
578 * @param query if non-null, this represents the search string that the folder represents.
579 */
Vikram Aggarwal93dc2022012-11-05 13:36:57 -0800580 private final void changeFolder(Folder folder, String query) {
Vikram Aggarwal203dc002012-08-23 13:59:04 -0700581 if (!Objects.equal(mFolder, folder)) {
582 commitDestructiveActions(false);
583 }
Mindy Pereiraa1f99982012-07-16 16:32:15 -0700584 if (folder != null && !folder.equals(mFolder)
585 || (mViewMode.getMode() != ViewMode.CONVERSATION_LIST)) {
Vikram Aggarwal203dc002012-08-23 13:59:04 -0700586 setListContext(folder, query);
Mindy Pereira28e0c342012-02-17 15:05:13 -0800587 showConversationList(mConvListContext);
Mindy Pereira28e0c342012-02-17 15:05:13 -0800588 }
Vikram Aggarwal93dc2022012-11-05 13:36:57 -0800589 resetActionBarIcon();
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -0800590 }
591
Mindy Pereira13c12a62012-05-31 15:41:08 -0700592 @Override
Mindy Pereira505df5f2012-06-19 17:57:17 -0700593 public void onFolderSelected(Folder folder) {
Mindy Pereira13c12a62012-05-31 15:41:08 -0700594 onFolderChanged(folder);
595 }
596
Vikram Aggarwal792ccba2012-03-27 13:46:57 -0700597 /**
598 * Update the recent folders. This only needs to be done once when accessing a new folder.
599 */
Paul Westbrook7ebdfd02012-03-21 15:55:30 -0700600 private void updateRecentFolderList() {
Mindy Pereiraab486362012-03-21 18:18:53 -0700601 if (mFolder != null) {
Marc Blank2675dbc2012-04-03 10:17:13 -0700602 mRecentFolderList.touchFolder(mFolder, mAccount);
Mindy Pereiraab486362012-03-21 18:18:53 -0700603 }
Paul Westbrook7ebdfd02012-03-21 15:55:30 -0700604 }
605
Vikram Aggarwal58cad2e2012-08-28 16:18:23 -0700606 /**
607 * Adds a listener interested in change in the recent folders. If a class is storing a
608 * reference to the recent folders, it should listen on changes, so it can receive updates.
609 * Must happen in the UI thread.
610 */
611 @Override
612 public void registerRecentFolderObserver(DataSetObserver obs) {
613 mRecentFolderObservers.registerObserver(obs);
614 }
615
616 /**
617 * Removes a listener from receiving recent folder changes.
618 * Must happen in the UI thread.
619 */
620 @Override
621 public void unregisterRecentFolderObserver(DataSetObserver obs) {
622 mRecentFolderObservers.unregisterObserver(obs);
623 }
624
625 @Override
626 public RecentFolderList getRecentFolders() {
627 return mRecentFolderList;
628 }
629
Mindy Pereiraab486362012-03-21 18:18:53 -0700630 // TODO(mindyp): set this up to store a copy of the folder as a transient
631 // field in the account.
Vikram Aggarwaleb7d3292012-04-20 17:07:20 -0700632 @Override
633 public void loadAccountInbox() {
Vikram Aggarwal94c94de2012-04-04 15:38:28 -0700634 restartOptionalLoader(LOADER_ACCOUNT_INBOX);
Mindy Pereiraf6acdad2012-03-15 11:21:13 -0700635 }
636
Vikram Aggarwal69b5c302012-09-05 11:11:13 -0700637 /**
Vikram Aggarwald00229d2012-09-20 12:31:44 -0700638 * Marks the {@link #mFolderChanged} value if the newFolder is different from the existing
639 * {@link #mFolder}. This should be called immediately <b>before</b> assigning newFolder to
640 * mFolder.
641 * @param newFolder
642 */
643 private final void setHasFolderChanged(final Folder newFolder) {
644 // We should never try to assign a null folder. But in the rare event that we do, we should
645 // only set the bit when we have a valid folder, and null is not valid.
646 if (newFolder == null) {
647 return;
648 }
649 // If the previous folder was null, or if the two folders represent different data, then we
650 // consider that the folder has changed.
651 if (mFolder == null || !newFolder.uri.equals(mFolder.uri)) {
652 mFolderChanged = true;
653 }
654 }
655
656 /**
Vikram Aggarwal69b5c302012-09-05 11:11:13 -0700657 * Sets the current folder if it is different from the object provided here. This method does
658 * NOT notify the folder observers that a change has happened. Observers are notified when we
659 * get an updated folder from the loaders, which will happen as a consequence of this method
660 * (since this method starts/restarts the loaders).
661 * @param folder The folder to assign
662 */
Mindy Pereira11e35962012-06-01 14:49:46 -0700663 private void updateFolder(Folder folder) {
Vikram Aggarwal1672ff82012-09-21 10:15:22 -0700664 if (folder == null || !folder.isInitialized()) {
665 LogUtils.e(LOG_TAG, new Error(), "AAC.setFolder(%s): Bad input", folder);
666 return;
667 }
668 if (folder.equals(mFolder)) {
669 LogUtils.d(LOG_TAG, "AAC.setFolder(%s): Input matches mFolder", folder);
670 return;
671 }
672 final boolean wasNull = mFolder == null;
673 LogUtils.d(LOG_TAG, "AbstractActivityController.setFolder(%s)", folder.name);
674 final LoaderManager lm = mActivity.getLoaderManager();
675 // updateFolder is called from AAC.onLoadFinished() on folder changes. We need to
676 // ensure that the folder is different from the previous folder before marking the
677 // folder changed.
678 setHasFolderChanged(folder);
679 mFolder = folder;
Vikram Aggarwal69b5c302012-09-05 11:11:13 -0700680
Vikram Aggarwal1672ff82012-09-21 10:15:22 -0700681 // We do not need to notify folder observers yet. Instead we start the loaders and
682 // when the load finishes, we will get an updated folder. Then, we notify the
683 // folderObservers in onLoadFinished.
684 mActionBarView.setFolder(mFolder);
Andy Huangb1c34dc2012-04-17 16:36:19 -0700685
Vikram Aggarwal1672ff82012-09-21 10:15:22 -0700686 // Only when we switch from one folder to another do we want to restart the
687 // folder and conversation list loaders (to trigger onCreateLoader).
688 // The first time this runs when the activity is [re-]initialized, we want to re-use the
689 // previous loader's instance and data upon configuration change (e.g. rotation).
690 // If there was not already an instance of the loader, init it.
691 if (lm.getLoader(LOADER_FOLDER_CURSOR) == null) {
692 lm.initLoader(LOADER_FOLDER_CURSOR, null, this);
693 } else {
694 lm.restartLoader(LOADER_FOLDER_CURSOR, null, this);
695 }
696 // In this case, we are starting from no folder, which would occur
697 // the first time the app was launched or on orientation changes.
698 // We want to attach to an existing loader, if available.
699 if (wasNull || lm.getLoader(LOADER_CONVERSATION_LIST) == null) {
700 lm.initLoader(LOADER_CONVERSATION_LIST, null, mListCursorCallbacks);
701 } else {
702 // However, if there was an existing folder AND we have changed
703 // folders, we want to restart the loader to get the information
704 // for the newly selected folder
705 lm.destroyLoader(LOADER_CONVERSATION_LIST);
706 lm.initLoader(LOADER_CONVERSATION_LIST, null, mListCursorCallbacks);
Mindy Pereira5cb0c3e2012-02-22 15:22:47 -0800707 }
708 }
709
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -0800710 @Override
Andy Huang090db1e2012-07-25 13:25:28 -0700711 public Folder getFolder() {
712 return mFolder;
713 }
714
715 @Override
Mindy Pereirac975e842012-07-16 09:15:00 -0700716 public Folder getHierarchyFolder() {
717 return mFolderListFolder;
718 }
719
720 @Override
721 public void setHierarchyFolder(Folder folder) {
722 mFolderListFolder = folder;
Mindy Pereira23aadfd2012-05-25 11:24:33 -0700723 }
724
725 @Override
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -0800726 public void onActivityResult(int requestCode, int resultCode, Intent data) {
Paul Westbrook122f7c22012-08-20 17:50:31 -0700727 switch (requestCode) {
728 case ADD_ACCOUNT_REQUEST_CODE:
729 // We were waiting for the user to create an account
730 if (resultCode == Activity.RESULT_OK) {
731 // restart the loader to get the updated list of accounts
732 mActivity.getLoaderManager().initLoader(
733 LOADER_ACCOUNT_CURSOR, null, this);
734 } else {
735 // The user failed to create an account, just exit the app
736 mActivity.finish();
737 }
738 break;
739 case REAUTHENTICATE_REQUEST_CODE:
740 if (resultCode == Activity.RESULT_OK) {
741 // The user successfully authenticated, attempt to refresh the list
742 final Uri refreshUri = mFolder != null ? mFolder.refreshUri : null;
743 if (refreshUri != null) {
744 startAsyncRefreshTask(refreshUri);
745 }
746 }
747 break;
Paul Westbrook2388c5d2012-03-25 12:29:11 -0700748 }
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -0800749 }
750
Vikram Aggarwal69b5c302012-09-05 11:11:13 -0700751 /**
752 * Inform the conversation cursor that there has been a visibility change.
753 * @param visible
754 */
755 protected synchronized void informCursorVisiblity(boolean visible) {
756 if (mConversationListCursor != null) {
757 Utils.setConversationCursorVisibility(mConversationListCursor, visible, mFolderChanged);
758 // We have informed the cursor. Subsequent visibility changes should not tell it that
759 // the folder has changed.
760 mFolderChanged = false;
761 }
762 }
763
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -0800764 @Override
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -0800765 public void onConversationListVisibilityChanged(boolean visible) {
Vikram Aggarwal69b5c302012-09-05 11:11:13 -0700766 informCursorVisiblity(visible);
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -0800767 }
768
Vikram Aggarwal4a5c5302012-01-12 15:07:13 -0800769 /**
Vikram Aggarwal7dd054e2012-05-21 14:43:10 -0700770 * Called when a conversation is visible. Child classes must call the super class implementation
771 * before performing local computation.
Vikram Aggarwal4a5c5302012-01-12 15:07:13 -0800772 */
773 @Override
774 public void onConversationVisibilityChanged(boolean visible) {
Vikram Aggarwal4a5c5302012-01-12 15:07:13 -0800775 }
776
777 @Override
Vikram Aggarwald7a12cd2012-02-03 09:36:20 -0800778 public boolean onCreate(Bundle savedState) {
Vikram Aggarwalabd24d82012-04-26 13:23:14 -0700779 initializeActionBar();
Vikram Aggarwala55b36c2012-01-13 11:45:02 -0800780 // Allow shortcut keys to function for the ActionBar and menus.
781 mActivity.setDefaultKeyMode(Activity.DEFAULT_KEYS_SHORTCUT);
Vikram Aggarwal80aeac52012-02-07 15:27:20 -0800782 mResolver = mActivity.getContentResolver();
Paul Westbrook6ead20d2012-03-19 14:48:14 -0700783 mNewEmailReceiver = new SuppressNotificationReceiver();
Vikram Aggarwal7c401b72012-08-13 16:43:47 -0700784 mRecentFolderList.initialize(mActivity);
Paul Westbrook6ead20d2012-03-19 14:48:14 -0700785
Mindy Pereira161f50d2012-02-28 15:47:19 -0800786 // All the individual UI components listen for ViewMode changes. This
Mindy Pereirab849dfb2012-03-07 18:13:15 -0800787 // simplifies the amount of logic in the AbstractActivityController, but increases the
788 // possibility of timing-related bugs.
Vikram Aggarwala55b36c2012-01-13 11:45:02 -0800789 mViewMode.addListener(this);
Andy Huang632721e2012-04-11 16:57:26 -0700790 mPagerController = new ConversationPagerController(mActivity, this);
Andrew Sappersteinc2c9dc12012-07-02 18:17:32 -0700791 mToastBar = (ActionableToastBar) mActivity.findViewById(R.id.toast_bar);
Vikram Aggarwalada51782012-04-26 14:18:31 -0700792 attachActionBar();
Mark Wei9eb1c9a2012-10-01 12:54:50 -0700793 FolderSelectionDialog.setDialogDismissed();
Andy Huang632721e2012-04-11 16:57:26 -0700794
795 final Intent intent = mActivity.getIntent();
Vikram Aggarwalabd24d82012-04-26 13:23:14 -0700796 // Immediately handle a clean launch with intent, and any state restoration
Andy Huang632721e2012-04-11 16:57:26 -0700797 // that does not rely on restored fragments or loader data
798 // any state restoration that relies on those can be done later in
799 // onRestoreInstanceState, once fragments are up and loader data is re-delivered
800 if (savedState != null) {
801 if (savedState.containsKey(SAVED_ACCOUNT)) {
802 setAccount((Account) savedState.getParcelable(SAVED_ACCOUNT));
Andy Huang632721e2012-04-11 16:57:26 -0700803 }
804 if (savedState.containsKey(SAVED_FOLDER)) {
Andy Huang2bc8bc12012-11-12 17:24:25 -0800805 final Folder folder = savedState.getParcelable(SAVED_FOLDER);
Vikram Aggarwal203dc002012-08-23 13:59:04 -0700806 final String query = savedState.getString(SAVED_QUERY, null);
Vikram Aggarwal649b9ea2012-08-27 12:15:20 -0700807 setListContext(folder, query);
Andy Huang632721e2012-04-11 16:57:26 -0700808 }
Vikram Aggarwal6cadbfc2012-12-27 09:17:05 -0800809 if (savedState.containsKey(SAVED_ACTION)) {
810 mDialogAction = savedState.getInt(SAVED_ACTION);
811 }
Vikram Aggarwal649b9ea2012-08-27 12:15:20 -0700812 mViewMode.handleRestore(savedState);
Andy Huang632721e2012-04-11 16:57:26 -0700813 } else if (intent != null) {
814 handleIntent(intent);
815 }
Andy Huang632721e2012-04-11 16:57:26 -0700816 // Create the accounts loader; this loads the account switch spinner.
817 mActivity.getLoaderManager().initLoader(LOADER_ACCOUNT_CURSOR, null, this);
Andy Huang632721e2012-04-11 16:57:26 -0700818 return true;
Andy Huangb1c34dc2012-04-17 16:36:19 -0700819 }
820
821 @Override
Andy Huang1ee96b22012-08-24 20:19:53 -0700822 public void onStart() {
823 mSafeToModifyFragments = true;
824 }
825
826 @Override
Andrew Sapperstein00179f12012-08-09 15:15:40 -0700827 public void onRestart() {
828 DialogFragment fragment = (DialogFragment)
829 mFragmentManager.findFragmentByTag(SYNC_ERROR_DIALOG_FRAGMENT_TAG);
830 if (fragment != null) {
831 fragment.dismiss();
832 }
mindypea04f932012-08-27 14:17:59 -0700833 // When the user places the app in the background by pressing "home",
834 // dismiss the toast bar. However, since there is no way to determine if
835 // home was pressed, just dismiss any existing toast bar when restarting
836 // the app.
837 if (mToastBar != null) {
838 mToastBar.hide(false);
839 }
Andrew Sapperstein00179f12012-08-09 15:15:40 -0700840 }
841
842 @Override
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -0800843 public Dialog onCreateDialog(int id, Bundle bundle) {
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -0800844 return null;
Vikram Aggarwala55b36c2012-01-13 11:45:02 -0800845 }
846
847 @Override
Vikram Aggarwal4eb52712012-06-19 16:24:50 -0700848 public final boolean onCreateOptionsMenu(Menu menu) {
Vikram Aggarwale5e917c2012-09-20 16:27:41 -0700849 final MenuInflater inflater = mActivity.getMenuInflater();
Mindy Pereiraf5acda42012-02-15 20:13:59 -0800850 inflater.inflate(mActionBarView.getOptionsMenuId(), menu);
Mindy Pereira68f2e222012-03-07 10:36:54 -0800851 mActionBarView.onCreateOptionsMenu(menu);
Mindy Pereira28d5f722012-02-15 12:32:40 -0800852 return true;
Vikram Aggarwala55b36c2012-01-13 11:45:02 -0800853 }
854
855 @Override
Vikram Aggarwal4eb52712012-06-19 16:24:50 -0700856 public final boolean onKeyDown(int keyCode, KeyEvent event) {
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -0800857 // TODO(viki): Auto-generated method stub
858 return false;
859 }
860
mindyp17a8e782012-11-29 14:56:17 -0800861 public abstract boolean doesActionChangeConversationListVisibility(int action);
862
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -0800863 @Override
Vikram Aggarwal4eb52712012-06-19 16:24:50 -0700864 public final boolean onOptionsItemSelected(MenuItem item) {
Vikram Aggarwal54452ae2012-03-13 15:29:00 -0700865 final int id = item.getItemId();
Vikram Aggarwal7dd054e2012-05-21 14:43:10 -0700866 LogUtils.d(LOG_TAG, "AbstractController.onOptionsItemSelected(%d) called.", id);
Mindy Pereira9b875682012-02-15 18:10:54 -0800867 boolean handled = true;
Mindy Pereiraba68fda2012-05-24 15:53:06 -0700868 final Collection<Conversation> target = Conversation.listOf(mCurrentConversation);
Vikram Aggarwal112cd162012-06-18 10:51:13 -0700869 final Settings settings = (mAccount == null) ? null : mAccount.settings;
mindyp84f7d322012-10-01 17:14:40 -0700870 // The user is choosing a new action; commit whatever they had been
mindyp17a8e782012-11-29 14:56:17 -0800871 // doing before. Don't animate if we are launching a new screen.
872 commitDestructiveActions(!doesActionChangeConversationListVisibility(id));
Mindy Pereira28d5f722012-02-15 12:32:40 -0800873 switch (id) {
Mindy Pereiraba68fda2012-05-24 15:53:06 -0700874 case R.id.archive: {
875 final boolean showDialog = (settings != null && settings.confirmArchive);
876 confirmAndDelete(target, showDialog, R.plurals.confirm_archive_conversation,
mindypf0656a12012-10-01 08:30:57 -0700877 getDeferredAction(R.id.archive, target, false));
Mindy Pereiraba68fda2012-05-24 15:53:06 -0700878 break;
879 }
Mindy Pereira01f30502012-08-14 10:30:51 -0700880 case R.id.remove_folder:
Vikram Aggarwala8e43182012-09-13 12:55:10 -0700881 delete(R.id.remove_folder, target,
mindyp84f7d322012-10-01 17:14:40 -0700882 getDeferredRemoveFolder(target, mFolder, true, false, true));
Mindy Pereira01f30502012-08-14 10:30:51 -0700883 break;
Mindy Pereiraba68fda2012-05-24 15:53:06 -0700884 case R.id.delete: {
885 final boolean showDialog = (settings != null && settings.confirmDelete);
886 confirmAndDelete(target, showDialog, R.plurals.confirm_delete_conversation,
mindypf0656a12012-10-01 08:30:57 -0700887 getDeferredAction(R.id.delete, target, false));
Mindy Pereiraba68fda2012-05-24 15:53:06 -0700888 break;
889 }
Vikram Aggarwala8e43182012-09-13 12:55:10 -0700890 case R.id.discard_drafts: {
Paul Westbrookef362542012-08-27 14:53:32 -0700891 final boolean showDialog = (settings != null && settings.confirmDelete);
892 confirmAndDelete(target, showDialog, R.plurals.confirm_discard_drafts_conversation,
mindypf0656a12012-10-01 08:30:57 -0700893 getDeferredAction(R.id.discard_drafts, target, false));
Paul Westbrookef362542012-08-27 14:53:32 -0700894 break;
Vikram Aggarwala8e43182012-09-13 12:55:10 -0700895 }
Mindy Pereiraba68fda2012-05-24 15:53:06 -0700896 case R.id.mark_important:
Vikram Aggarwal531488e2012-05-29 16:36:52 -0700897 updateConversation(Conversation.listOf(mCurrentConversation),
898 ConversationColumns.PRIORITY, UIProvider.ConversationPriority.HIGH);
Mindy Pereiraba68fda2012-05-24 15:53:06 -0700899 break;
900 case R.id.mark_not_important:
Mindy Pereira0d03ef82012-08-15 09:05:48 -0700901 if (mFolder != null && mFolder.isImportantOnly()) {
Vikram Aggarwala8e43182012-09-13 12:55:10 -0700902 delete(R.id.mark_not_important, target,
mindyp84f7d322012-10-01 17:14:40 -0700903 getDeferredAction(R.id.mark_not_important, target, false));
Mindy Pereira0d03ef82012-08-15 09:05:48 -0700904 } else {
905 updateConversation(Conversation.listOf(mCurrentConversation),
906 ConversationColumns.PRIORITY, UIProvider.ConversationPriority.LOW);
907 }
Mindy Pereiraba68fda2012-05-24 15:53:06 -0700908 break;
909 case R.id.mute:
mindyp84f7d322012-10-01 17:14:40 -0700910 delete(R.id.mute, target, getDeferredAction(R.id.mute, target, false));
Mindy Pereiraba68fda2012-05-24 15:53:06 -0700911 break;
912 case R.id.report_spam:
mindyp84f7d322012-10-01 17:14:40 -0700913 delete(R.id.report_spam, target,
914 getDeferredAction(R.id.report_spam, target, false));
Mindy Pereiraba68fda2012-05-24 15:53:06 -0700915 break;
Paul Westbrook77eee622012-07-10 13:41:57 -0700916 case R.id.mark_not_spam:
mindyp84f7d322012-10-01 17:14:40 -0700917 // Currently, since spam messages are only shown in list with
918 // other spam messages,
Paul Westbrook77eee622012-07-10 13:41:57 -0700919 // marking a message not as spam is a destructive action
mindyp84f7d322012-10-01 17:14:40 -0700920 delete(R.id.mark_not_spam, target,
921 getDeferredAction(R.id.mark_not_spam, target, false));
Paul Westbrook77eee622012-07-10 13:41:57 -0700922 break;
Paul Westbrook76b20622012-07-12 11:45:43 -0700923 case R.id.report_phishing:
mindyp84f7d322012-10-01 17:14:40 -0700924 delete(R.id.report_phishing, target,
925 getDeferredAction(R.id.report_phishing, target, false));
Paul Westbrook76b20622012-07-12 11:45:43 -0700926 break;
Mindy Pereiraf5acda42012-02-15 20:13:59 -0800927 case android.R.id.home:
928 onUpPressed();
929 break;
Mindy Pereira9b875682012-02-15 18:10:54 -0800930 case R.id.compose:
931 ComposeActivity.compose(mActivity.getActivityContext(), mAccount);
932 break;
Mindy Pereira28d5f722012-02-15 12:32:40 -0800933 case R.id.show_all_folders:
934 showFolderList();
935 break;
Mindy Pereira28e0c342012-02-17 15:05:13 -0800936 case R.id.refresh:
937 requestFolderRefresh();
938 break;
Mindy Pereira1f936682012-03-02 11:30:33 -0800939 case R.id.settings:
940 Utils.showSettings(mActivity.getActivityContext(), mAccount);
Paul Westbrook2861b6a2012-02-15 15:25:34 -0800941 break;
Paul Westbrooke5503552012-03-28 00:35:57 -0700942 case R.id.folder_options:
943 Utils.showFolderSettings(mActivity.getActivityContext(), mAccount, mFolder);
944 break;
Paul Westbrook94e440d2012-02-24 11:03:47 -0800945 case R.id.help_info_menu_item:
Paul Westbrook30745b62012-08-19 14:10:32 -0700946 Utils.showHelp(mActivity.getActivityContext(), mAccount, getHelpContext());
Paul Westbrook94e440d2012-02-24 11:03:47 -0800947 break;
Vikram Aggarwal54452ae2012-03-13 15:29:00 -0700948 case R.id.feedback_menu_item:
Paul Westbrook17beb0b2012-08-20 13:34:37 -0700949 Utils.sendFeedback(mActivity.getActivityContext(), mAccount, false);
Mindy Pereirafbe40192012-03-20 10:40:45 -0700950 break;
Paul Westbrook18babd22012-04-09 22:17:08 -0700951 case R.id.manage_folders_item:
952 Utils.showManageFolder(mActivity.getActivityContext(), mAccount);
953 break;
Vikram Aggarwald503df42012-05-11 10:13:35 -0700954 case R.id.change_folder:
Mark Wei8f98ac02012-10-01 17:05:08 -0700955 final FolderSelectionDialog dialog = FolderSelectionDialog.getInstance(
956 mActivity.getActivityContext(), mAccount, this,
957 Conversation.listOf(mCurrentConversation), false, mFolder);
Mark Wei9eb1c9a2012-10-01 12:54:50 -0700958 if (dialog != null) {
959 dialog.show();
mindypa7e15452012-09-18 14:22:11 -0700960 }
Vikram Aggarwald503df42012-05-11 10:13:35 -0700961 break;
Mindy Pereira9b875682012-02-15 18:10:54 -0800962 default:
963 handled = false;
964 break;
Mindy Pereira28d5f722012-02-15 12:32:40 -0800965 }
Mindy Pereira9b875682012-02-15 18:10:54 -0800966 return handled;
Vikram Aggarwala55b36c2012-01-13 11:45:02 -0800967 }
968
Vikram Aggarwal531488e2012-05-29 16:36:52 -0700969 @Override
Mindy Pereira6c2663d2012-07-20 15:37:29 -0700970 public void updateConversation(Collection<Conversation> target, ContentValues values) {
971 mConversationListCursor.updateValues(mContext, target, values);
972 refreshConversationList();
973 }
974
975 @Override
Vikram Aggarwal531488e2012-05-29 16:36:52 -0700976 public void updateConversation(Collection <Conversation> target, String columnName,
977 boolean value) {
978 mConversationListCursor.updateBoolean(mContext, target, columnName, value);
Vikram Aggarwal75daee52012-04-30 11:13:09 -0700979 refreshConversationList();
Vikram Aggarwal54452ae2012-03-13 15:29:00 -0700980 }
981
Vikram Aggarwal531488e2012-05-29 16:36:52 -0700982 @Override
Mindy Pereira6c2663d2012-07-20 15:37:29 -0700983 public void updateConversation(Collection <Conversation> target, String columnName,
984 int value) {
Vikram Aggarwal531488e2012-05-29 16:36:52 -0700985 mConversationListCursor.updateInt(mContext, target, columnName, value);
Vikram Aggarwal75daee52012-04-30 11:13:09 -0700986 refreshConversationList();
Mindy Pereirac9d59182012-03-22 16:06:46 -0700987 }
988
Vikram Aggarwal531488e2012-05-29 16:36:52 -0700989 @Override
990 public void updateConversation(Collection <Conversation> target, String columnName,
991 String value) {
992 mConversationListCursor.updateString(mContext, target, columnName, value);
Vikram Aggarwal75daee52012-04-30 11:13:09 -0700993 refreshConversationList();
Vikram Aggarwal54452ae2012-03-13 15:29:00 -0700994 }
995
Andy Huang839ada22012-07-20 15:48:40 -0700996 @Override
997 public void markConversationMessagesUnread(Conversation conv, Set<Uri> unreadMessageUris,
Andy Huang351ad4e2012-12-06 16:04:58 -0800998 byte[] originalConversationInfo) {
Andy Huang8f6b0062012-07-31 15:36:31 -0700999 // The only caller of this method is the conversation view, from where marking unread should
1000 // *always* take you back to list mode.
1001 showConversation(null);
1002
Andy Huang839ada22012-07-20 15:48:40 -07001003 // locally mark conversation unread (the provider is supposed to propagate message unread
1004 // to conversation unread)
1005 conv.read = false;
1006
Paul Westbrook1faf93d2012-10-16 08:58:07 -07001007 if (mConversationListCursor == null) {
1008 LogUtils.e(LOG_TAG, "null ConversationCursor in markConversationMessagesUnread");
1009 return;
1010 }
1011
Andy Huang28e31e22012-07-26 16:33:15 -07001012 // only do a granular 'mark unread' if a subset of messages are unread
1013 final int unreadCount = (unreadMessageUris == null) ? 0 : unreadMessageUris.size();
Mindy Pereira0972e072012-08-01 17:43:06 -07001014 final int numMessages = conv.getNumMessages();
1015 final boolean subsetIsUnread = (numMessages > 1 && unreadCount > 0
1016 && unreadCount < numMessages);
Andy Huang28e31e22012-07-26 16:33:15 -07001017
1018 if (!subsetIsUnread) {
Vikram Aggarwal66bc2aa2012-08-02 10:47:03 -07001019 // Conversations are neither marked read, nor viewed, and we don't want to show
1020 // the next conversation.
1021 markConversationsRead(Collections.singletonList(conv), false, false, false);
Andy Huang839ada22012-07-20 15:48:40 -07001022 } else {
Andy Huangdaa06ab2012-07-24 10:46:44 -07001023 mConversationListCursor.setConversationColumn(conv.uri, ConversationColumns.READ, 0);
Andy Huang839ada22012-07-20 15:48:40 -07001024
Mindy Pereira7b6d03d2012-07-30 13:03:41 -07001025 // locally update conversation's conversationInfo to revert to original version
Andy Huang28e31e22012-07-26 16:33:15 -07001026 if (originalConversationInfo != null) {
1027 mConversationListCursor.setConversationColumn(conv.uri,
1028 ConversationColumns.CONVERSATION_INFO, originalConversationInfo);
1029 }
Andy Huang839ada22012-07-20 15:48:40 -07001030
1031 // applyBatch with each CPO as an UPDATE op on each affected message uri
1032 final ArrayList<ContentProviderOperation> ops = Lists.newArrayList();
1033 String authority = null;
1034 for (Uri messageUri : unreadMessageUris) {
1035 if (authority == null) {
1036 authority = messageUri.getAuthority();
1037 }
1038 ops.add(ContentProviderOperation.newUpdate(messageUri)
1039 .withValue(UIProvider.MessageColumns.READ, 0)
1040 .build());
1041 }
1042
1043 new ContentProviderTask() {
1044 @Override
1045 protected void onPostExecute(Result result) {
1046 // TODO: handle errors?
1047 }
1048 }.run(mResolver, authority, ops);
Andy Huang839ada22012-07-20 15:48:40 -07001049 }
Andy Huang839ada22012-07-20 15:48:40 -07001050 }
1051
1052 @Override
Vikram Aggarwal66bc2aa2012-08-02 10:47:03 -07001053 public void markConversationsRead(Collection<Conversation> targets, boolean read,
1054 boolean viewed) {
1055 // We want to show the next conversation if we are marking unread.
1056 markConversationsRead(targets, read, viewed, true);
Andy Huang8f6b0062012-07-31 15:36:31 -07001057 }
1058
Scott Kennedy0d0f8b02012-10-12 15:18:18 -07001059 private void markConversationsRead(final Collection<Conversation> targets, final boolean read,
1060 final boolean markViewed, final boolean showNext) {
Vikram Aggarwalcc754b12012-08-30 14:04:21 -07001061 // Auto-advance if requested and the current conversation is being marked unread
Andy Huang8f6b0062012-07-31 15:36:31 -07001062 if (showNext && !read) {
Scott Kennedy0d0f8b02012-10-12 15:18:18 -07001063 final Runnable operation = new Runnable() {
1064 @Override
1065 public void run() {
1066 markConversationsRead(targets, read, markViewed, showNext);
1067 }
1068 };
1069
1070 if (!showNextConversation(targets, operation)) {
1071 // This method will be called again if the user selects an autoadvance option
1072 return;
1073 }
Andy Huang8f6b0062012-07-31 15:36:31 -07001074 }
Scott Kennedy0d0f8b02012-10-12 15:18:18 -07001075
Vikram Aggarwalcc754b12012-08-30 14:04:21 -07001076 final int size = targets.size();
1077 final List<ConversationOperation> opList = new ArrayList<ConversationOperation>(size);
1078 for (final Conversation target : targets) {
1079 final ContentValues value = new ContentValues();
1080 value.put(ConversationColumns.READ, read);
Paul Westbrook5109c512012-11-05 11:00:30 -08001081
1082 // The mark read/unread/viewed operations do not show an undo bar
1083 value.put(ConversationOperations.Parameters.SUPPRESS_UNDO, true);
Vikram Aggarwal66bc2aa2012-08-02 10:47:03 -07001084 if (markViewed) {
Vikram Aggarwalcc754b12012-08-30 14:04:21 -07001085 value.put(ConversationColumns.VIEWED, true);
Vikram Aggarwal66bc2aa2012-08-02 10:47:03 -07001086 }
Vikram Aggarwal192fac12012-07-25 16:44:55 -07001087 final ConversationInfo info = target.conversationInfo;
Andy Huang839ada22012-07-20 15:48:40 -07001088 if (info != null) {
mindyp7f55c682012-10-04 11:38:27 -07001089 boolean changed = info.markRead(read);
1090 if (changed) {
1091 value.put(ConversationColumns.CONVERSATION_INFO,
Andy Huang351ad4e2012-12-06 16:04:58 -08001092 info.toBlob());
mindyp7f55c682012-10-04 11:38:27 -07001093 }
Andy Huang839ada22012-07-20 15:48:40 -07001094 }
Vikram Aggarwalcc754b12012-08-30 14:04:21 -07001095 opList.add(mConversationListCursor.getOperationForConversation(
1096 target, ConversationOperation.UPDATE, value));
1097 // Update the local conversation objects so they immediately change state.
1098 target.read = read;
Andy Huangcd5c5ee2012-08-12 19:03:51 -07001099 if (markViewed) {
Vikram Aggarwalcc754b12012-08-30 14:04:21 -07001100 target.markViewed();
Andy Huangcd5c5ee2012-08-12 19:03:51 -07001101 }
Andy Huang839ada22012-07-20 15:48:40 -07001102 }
Vikram Aggarwalcc754b12012-08-30 14:04:21 -07001103 mConversationListCursor.updateBulkValues(mContext, opList);
Andy Huang839ada22012-07-20 15:48:40 -07001104 }
1105
Andy Huang8f6b0062012-07-31 15:36:31 -07001106 /**
1107 * Auto-advance to a different conversation if the currently visible conversation in
1108 * conversation mode is affected (deleted, marked unread, etc.).
1109 *
Scott Kennedy0d0f8b02012-10-12 15:18:18 -07001110 * <p>Does nothing if outside of conversation mode.</p>
Andy Huang8f6b0062012-07-31 15:36:31 -07001111 *
1112 * @param target the set of conversations being deleted/marked unread
1113 */
mindyp9365a822012-09-12 09:09:09 -07001114 @Override
Scott Kennedy0d0f8b02012-10-12 15:18:18 -07001115 public void showNextConversation(final Collection<Conversation> target) {
1116 showNextConversation(target, null);
1117 }
1118
1119 /**
1120 * Auto-advance to a different conversation if the currently visible conversation in
1121 * conversation mode is affected (deleted, marked unread, etc.).
1122 *
1123 * <p>Does nothing if outside of conversation mode.</p>
1124 *
1125 * @param target the set of conversations being deleted/marked unread
1126 * @return <code>false</code> if we aborted because the user has not yet specified a default
1127 * action, <code>true</code> otherwise
1128 */
1129 private boolean showNextConversation(final Collection<Conversation> target,
1130 final Runnable operation) {
Scott Kennedy8fb8e262012-11-28 15:48:03 -08001131 final int viewMode = mViewMode.getMode();
1132 final boolean currentConversationInView = (viewMode == ViewMode.CONVERSATION
1133 || viewMode == ViewMode.SEARCH_RESULTS_CONVERSATION)
Andy Huang8f6b0062012-07-31 15:36:31 -07001134 && Conversation.contains(target, mCurrentConversation);
Scott Kennedy0d0f8b02012-10-12 15:18:18 -07001135
Andy Huang8f6b0062012-07-31 15:36:31 -07001136 if (currentConversationInView) {
Scott Kennedy0d0f8b02012-10-12 15:18:18 -07001137 final int autoAdvanceSetting = mAccount.settings.getAutoAdvanceSetting();
1138
1139 if (autoAdvanceSetting == AutoAdvance.UNSET && Utils.useTabletUI(mContext)) {
1140 displayAutoAdvanceDialogAndPerformAction(operation);
1141 return false;
1142 } else {
1143 // If we don't have one set, but we're here, just take the default
1144 final int autoAdvance = (autoAdvanceSetting == AutoAdvance.UNSET) ? Settings
1145 .getAutoAdvanceSetting(null)
1146 : autoAdvanceSetting;
1147
1148 final Conversation next = mTracker.getNextConversation(autoAdvance, target);
1149 LogUtils.d(LOG_TAG, "showNextConversation: showing %s next.", next);
1150 showConversation(next);
1151 return true;
1152 }
Andy Huang8f6b0062012-07-31 15:36:31 -07001153 }
Scott Kennedy0d0f8b02012-10-12 15:18:18 -07001154
1155 return true;
1156 }
1157
1158 /**
1159 * Displays a the auto-advance dialog, and when the user makes a selection, the preference is
1160 * stored, and the specified operation is run.
1161 */
1162 private void displayAutoAdvanceDialogAndPerformAction(final Runnable operation) {
1163 final String[] autoAdvanceDisplayOptions =
1164 mContext.getResources().getStringArray(R.array.prefEntries_autoAdvance);
1165 final String[] autoAdvanceOptionValues =
1166 mContext.getResources().getStringArray(R.array.prefValues_autoAdvance);
1167
1168 final String defaultValue = mContext.getString(R.string.prefDefault_autoAdvance);
1169 int initialIndex = 0;
1170 for (int i = 0; i < autoAdvanceOptionValues.length; i++) {
1171 if (defaultValue.equals(autoAdvanceOptionValues[i])) {
1172 initialIndex = i;
1173 break;
1174 }
1175 }
1176
1177 final DialogInterface.OnClickListener listClickListener =
1178 new DialogInterface.OnClickListener() {
1179 @Override
1180 public void onClick(DialogInterface dialog, int whichItem) {
1181 final String autoAdvanceValue = autoAdvanceOptionValues[whichItem];
1182 final int autoAdvanceValueInt =
1183 UIProvider.AutoAdvance.getAutoAdvanceInt(autoAdvanceValue);
1184 mAccount.settings.setAutoAdvanceSetting(autoAdvanceValueInt);
1185
1186 // Save the user's setting
1187 final ContentValues values = new ContentValues(1);
1188 values.put(AccountColumns.SettingsColumns.AUTO_ADVANCE, autoAdvanceValue);
1189
1190 final ContentResolver resolver = mContext.getContentResolver();
1191 resolver.update(mAccount.updateSettingsUri, values, null, null);
1192
1193 // Dismiss the dialog, as clicking the items in the list doesn't close the
1194 // dialog.
1195 dialog.dismiss();
1196 if (operation != null) {
1197 operation.run();
1198 }
1199 }
1200 };
1201
1202 new AlertDialog.Builder(mActivity.getActivityContext()).setTitle(
1203 R.string.auto_advance_help_title)
1204 .setSingleChoiceItems(autoAdvanceDisplayOptions, initialIndex, listClickListener)
1205 .setPositiveButton(null, null)
1206 .create()
1207 .show();
Andy Huang8f6b0062012-07-31 15:36:31 -07001208 }
1209
Andy Huang839ada22012-07-20 15:48:40 -07001210 @Override
1211 public void starMessage(ConversationMessage msg, boolean starred) {
1212 if (msg.starred == starred) {
1213 return;
1214 }
1215
1216 msg.starred = starred;
1217
1218 // locally propagate the change to the owning conversation
1219 // (figure the provider will properly propagate the change when it commits it)
1220 //
1221 // when unstarring, only propagate the change if this was the only message starred
1222 final boolean conversationStarred = starred || msg.isConversationStarred();
Andy Huangcd12e822012-11-08 19:50:57 -08001223 final Conversation conv = msg.getConversation();
1224 if (conversationStarred != conv.starred) {
1225 conv.starred = conversationStarred;
1226 mConversationListCursor.setConversationColumn(conv.uri,
Andy Huang839ada22012-07-20 15:48:40 -07001227 ConversationColumns.STARRED, conversationStarred);
1228 }
1229
1230 final ContentValues values = new ContentValues(1);
1231 values.put(UIProvider.MessageColumns.STARRED, starred ? 1 : 0);
1232
1233 new ContentProviderTask.UpdateTask() {
1234 @Override
1235 protected void onPostExecute(Result result) {
1236 // TODO: handle errors?
1237 }
1238 }.run(mResolver, msg.uri, values, null /* selection*/, null /* selectionArgs */);
1239 }
1240
Mindy Pereira28e0c342012-02-17 15:05:13 -08001241 private void requestFolderRefresh() {
1242 if (mFolder != null) {
Mindy Pereirab7b33e02012-02-21 15:32:19 -08001243 if (mAsyncRefreshTask != null) {
1244 mAsyncRefreshTask.cancel(true);
1245 }
Paul Westbrook7e2a2a12012-06-27 13:52:40 -07001246 mAsyncRefreshTask = new AsyncRefreshTask(mContext, mFolder.refreshUri);
Mindy Pereirab7b33e02012-02-21 15:32:19 -08001247 mAsyncRefreshTask.execute();
Mindy Pereira28e0c342012-02-17 15:05:13 -08001248 }
1249 }
1250
Mindy Pereirafbe40192012-03-20 10:40:45 -07001251 /**
1252 * Confirm (based on user's settings) and delete a conversation from the conversation list and
1253 * from the database.
Vikram Aggarwal3d7ca9d2012-05-11 14:40:36 -07001254 * @param target the conversations to act upon
1255 * @param showDialog true if a confirmation dialog is to be shown, false otherwise.
1256 * @param confirmResource the resource ID of the string that is shown in the confirmation dialog
1257 * @param action the action to perform after animating the deletion of the conversations.
Mindy Pereirafbe40192012-03-20 10:40:45 -07001258 */
Vikram Aggarwal7f602f72012-04-30 16:04:06 -07001259 protected void confirmAndDelete(final Collection<Conversation> target, boolean showDialog,
1260 int confirmResource, final DestructiveAction action) {
Mindy Pereirafbe40192012-03-20 10:40:45 -07001261 if (showDialog) {
1262 final AlertDialog.OnClickListener onClick = new AlertDialog.OnClickListener() {
1263 @Override
1264 public void onClick(DialogInterface dialog, int which) {
mindypf0656a12012-10-01 08:30:57 -07001265 if (which == DialogInterface.BUTTON_POSITIVE) {
1266 delete(0, target, action);
1267 }
Mindy Pereirafbe40192012-03-20 10:40:45 -07001268 }
1269 };
Vikram Aggarwal3d7ca9d2012-05-11 14:40:36 -07001270 final CharSequence message = Utils.formatPlural(mContext, confirmResource,
1271 target.size());
Mindy Pereirafbe40192012-03-20 10:40:45 -07001272 new AlertDialog.Builder(mActivity.getActivityContext()).setMessage(message)
1273 .setPositiveButton(R.string.ok, onClick)
1274 .setNegativeButton(R.string.cancel, null)
1275 .create().show();
1276 } else {
Vikram Aggarwala8e43182012-09-13 12:55:10 -07001277 delete(0, target, action);
Mindy Pereirafbe40192012-03-20 10:40:45 -07001278 }
1279 }
1280
Vikram Aggarwal4f4782b2012-05-30 08:39:09 -07001281 @Override
Scott Kennedy0d0f8b02012-10-12 15:18:18 -07001282 public void delete(final int actionId, final Collection<Conversation> target,
mindyp84f7d322012-10-01 17:14:40 -07001283 final Collection<ConversationItemView> targetViews, final DestructiveAction action) {
1284 // Order of events is critical! The Conversation View Fragment must be
1285 // notified of the next conversation with showConversation(next) *before* the
1286 // conversation list
Vikram Aggarwal192fac12012-07-25 16:44:55 -07001287 // fragment has a chance to delete the conversation, animating it away.
1288
mindyp84f7d322012-10-01 17:14:40 -07001289 // Update the conversation fragment if the current conversation is
1290 // deleted.
Scott Kennedy0d0f8b02012-10-12 15:18:18 -07001291 final Runnable operation = new Runnable() {
1292 @Override
1293 public void run() {
1294 delete(actionId, target, targetViews, action);
1295 }
1296 };
1297
1298 if (!showNextConversation(target, operation)) {
1299 // This method will be called again if the user selects an autoadvance option
1300 return;
1301 }
1302
Vikram Aggarwal192fac12012-07-25 16:44:55 -07001303 // The conversation list deletes and performs the action if it exists.
1304 final ConversationListFragment convListFragment = getConversationListFragment();
1305 if (convListFragment != null) {
1306 LogUtils.d(LOG_TAG, "AAC.requestDelete: ListFragment is handling delete.");
mindyp84f7d322012-10-01 17:14:40 -07001307 convListFragment.requestDelete(actionId, target, targetViews, action);
Vikram Aggarwal192fac12012-07-25 16:44:55 -07001308 return;
1309 }
mindyp84f7d322012-10-01 17:14:40 -07001310 // No visible UI element handled it on our behalf. Perform the action
1311 // ourself.
Vikram Aggarwald503df42012-05-11 10:13:35 -07001312 action.performAction();
1313 }
1314
mindyp84f7d322012-10-01 17:14:40 -07001315 @Override
1316 public void delete(int actionId, final Collection<Conversation> target,
1317 final DestructiveAction action) {
1318 delete(actionId, target, null, action);
1319 }
1320
Vikram Aggarwald503df42012-05-11 10:13:35 -07001321 /**
1322 * Requests that the action be performed and the UI state is updated to reflect the new change.
1323 * @param target
1324 * @param action
1325 */
Vikram Aggarwal4f4782b2012-05-30 08:39:09 -07001326 private void requestUpdate(final Collection<Conversation> target,
Vikram Aggarwald503df42012-05-11 10:13:35 -07001327 final DestructiveAction action) {
1328 action.performAction();
1329 refreshConversationList();
Vikram Aggarwal75daee52012-04-30 11:13:09 -07001330 }
Vikram Aggarwal6fbc87a2012-03-15 15:24:00 -07001331
1332 @Override
Vikram Aggarwala55b36c2012-01-13 11:45:02 -08001333 public void onPrepareDialog(int id, Dialog dialog, Bundle bundle) {
1334 // TODO(viki): Auto-generated method stub
Vikram Aggarwala55b36c2012-01-13 11:45:02 -08001335 }
1336
1337 @Override
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -08001338 public boolean onPrepareOptionsMenu(Menu menu) {
Andy Huangd736a382012-08-29 13:08:58 -07001339 return mActionBarView.onPrepareOptionsMenu(menu);
Vikram Aggarwala55b36c2012-01-13 11:45:02 -08001340 }
1341
Mindy Pereira68f2e222012-03-07 10:36:54 -08001342 @Override
1343 public void onPause() {
1344 isLoaderInitialized = false;
Paul Westbrook6ead20d2012-03-19 14:48:14 -07001345 enableNotifications();
Paul Westbrook94e440d2012-02-24 11:03:47 -08001346 }
1347
Vikram Aggarwala55b36c2012-01-13 11:45:02 -08001348 @Override
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -08001349 public void onResume() {
Paul Westbrook6ead20d2012-03-19 14:48:14 -07001350 // Register the receiver that will prevent the status receiver from
1351 // displaying its notification icon as long as we're running.
1352 // The SupressNotificationReceiver will block the broadcast if we're looking at the folder
1353 // that the notification was received for.
1354 disableNotifications();
Andy Huang1ee96b22012-08-24 20:19:53 -07001355
1356 mSafeToModifyFragments = true;
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -08001357 }
1358
1359 @Override
1360 public void onSaveInstanceState(Bundle outState) {
Vikram Aggarwal649b9ea2012-08-27 12:15:20 -07001361 mViewMode.handleSaveInstanceState(outState);
Vikram Aggarwal6c511582012-02-27 10:59:47 -08001362 if (mAccount != null) {
Vikram Aggarwal6c511582012-02-27 10:59:47 -08001363 outState.putParcelable(SAVED_ACCOUNT, mAccount);
1364 }
Mindy Pereira5e478d22012-03-26 18:04:58 -07001365 if (mFolder != null) {
1366 outState.putParcelable(SAVED_FOLDER, mFolder);
Vikram Aggarwal8b152632012-02-03 14:58:45 -08001367 }
Vikram Aggarwalf3341402012-08-07 10:09:38 -07001368 // If this is a search activity, let's store the search query term as well.
1369 if (ConversationListContext.isSearchResult(mConvListContext)) {
1370 outState.putString(SAVED_QUERY, mConvListContext.searchQuery);
1371 }
Vikram Aggarwal49e0e992012-09-21 13:53:15 -07001372 if (mCurrentConversation != null && mViewMode.isConversationMode()) {
Mindy Pereira26f23fc2012-03-27 10:26:04 -07001373 outState.putParcelable(SAVED_CONVERSATION, mCurrentConversation);
1374 }
Andy Huang4556a442012-03-30 16:42:05 -07001375 if (!mSelectedSet.isEmpty()) {
Vikram Aggarwalcabd3f22012-04-19 10:14:41 -07001376 outState.putParcelable(SAVED_SELECTED_SET, mSelectedSet);
Andy Huang4556a442012-03-30 16:42:05 -07001377 }
Mindy Pereirad33674992012-06-25 16:26:30 -07001378 if (mToastBar.getVisibility() == View.VISIBLE) {
1379 outState.putParcelable(SAVED_TOAST_BAR_OP, mToastBar.getOperation());
1380 }
Vikram Aggarwal649b9ea2012-08-27 12:15:20 -07001381 final ConversationListFragment convListFragment = getConversationListFragment();
Mindy Pereirad33674992012-06-25 16:26:30 -07001382 if (convListFragment != null) {
Vikram Aggarwal649b9ea2012-08-27 12:15:20 -07001383 convListFragment.getAnimatedAdapter().onSaveInstanceState(outState);
Mindy Pereirad33674992012-06-25 16:26:30 -07001384 }
Vikram Aggarwal6cadbfc2012-12-27 09:17:05 -08001385 if (mDialogAction != -1) {
1386 outState.putInt(SAVED_ACTION, mDialogAction);
1387 }
Andy Huang1ee96b22012-08-24 20:19:53 -07001388 mSafeToModifyFragments = false;
Vikram Aggarwal649b9ea2012-08-27 12:15:20 -07001389 outState.putString(SAVED_HIERARCHICAL_FOLDER,
1390 (mFolderListFolder != null) ? Folder.toString(mFolderListFolder) : null);
Andy Huang1ee96b22012-08-24 20:19:53 -07001391 }
1392
1393 /**
1394 * @see #mSafeToModifyFragments
1395 */
1396 protected boolean safeToModifyFragments() {
1397 return mSafeToModifyFragments;
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -08001398 }
1399
1400 @Override
Mindy Pereira68f2e222012-03-07 10:36:54 -08001401 public void onSearchRequested(String query) {
1402 Intent intent = new Intent();
1403 intent.setAction(Intent.ACTION_SEARCH);
1404 intent.putExtra(ConversationListContext.EXTRA_SEARCH_QUERY, query);
1405 intent.putExtra(Utils.EXTRA_ACCOUNT, mAccount);
1406 intent.setComponent(mActivity.getComponentName());
Vikram Aggarwalb17cbc02012-04-06 15:41:46 -07001407 mActionBarView.collapseSearch();
Mindy Pereira68f2e222012-03-07 10:36:54 -08001408 mActivity.startActivity(intent);
Vikram Aggarwala55b36c2012-01-13 11:45:02 -08001409 }
1410
1411 @Override
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -08001412 public void onStop() {
Mindy Pereira49e5dbe2012-07-12 11:47:54 -07001413 if (mEnableShareIntents != null) {
1414 mEnableShareIntents.cancel(true);
1415 }
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -08001416 }
1417
Andy Huang632721e2012-04-11 16:57:26 -07001418 @Override
1419 public void onDestroy() {
Andy Huangb2ef9c12012-12-18 12:58:41 -06001420 // stop listening to the cursor on e.g. configuration changes
1421 if (mConversationListCursor != null) {
1422 mConversationListCursor.removeListener(this);
1423 }
Andy Huang632721e2012-04-11 16:57:26 -07001424 // unregister the ViewPager's observer on the conversation cursor
1425 mPagerController.onDestroy();
Mindy Pereira641de652012-08-02 15:21:50 -07001426 mActionBarView.onDestroy();
Vikram Aggarwal7c401b72012-08-13 16:43:47 -07001427 mRecentFolderList.destroy();
Andy Huang4e0158f2012-08-07 21:06:01 -07001428 mDestroyed = true;
Andy Huang632721e2012-04-11 16:57:26 -07001429 }
1430
Vikram Aggarwalfa131a22012-02-02 13:56:22 -08001431 /**
Vikram Aggarwal93dc2022012-11-05 13:36:57 -08001432 * Set the Action Bar icon according to the mode. The Action Bar icon can contain a back button
1433 * or not. The individual controller is responsible for changing the icon based on the mode.
1434 */
1435 protected abstract void resetActionBarIcon();
1436
1437 /**
Mindy Pereira161f50d2012-02-28 15:47:19 -08001438 * {@inheritDoc} Subclasses must override this to listen to mode changes
1439 * from the ViewMode. Subclasses <b>must</b> call the parent's
1440 * onViewModeChanged since the parent will handle common state changes.
Vikram Aggarwalfa131a22012-02-02 13:56:22 -08001441 */
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -08001442 @Override
Vikram Aggarwalfa131a22012-02-02 13:56:22 -08001443 public void onViewModeChanged(int newMode) {
Vikram Aggarwal49e0e992012-09-21 13:53:15 -07001444 // When we step away from the conversation mode, we don't have a current conversation
1445 // anymore. Let's blank it out so clients calling getCurrentConversation are not misled.
1446 if (!ViewMode.isConversationMode(newMode)) {
1447 setCurrentConversation(null);
1448 }
Vikram Aggarwal93dc2022012-11-05 13:36:57 -08001449 // If the viewmode is not set, preserve existing icon.
1450 if (newMode != ViewMode.UNKNOWN) {
1451 resetActionBarIcon();
1452 }
Vikram Aggarwala55b36c2012-01-13 11:45:02 -08001453 }
1454
Andy Huang3825f3d2012-08-29 16:44:12 -07001455 public void disablePagerUpdates() {
1456 mPagerController.stopListening();
1457 }
1458
Andy Huang4e0158f2012-08-07 21:06:01 -07001459 public boolean isDestroyed() {
1460 return mDestroyed;
1461 }
1462
mindyp54f120f2012-08-28 13:10:33 -07001463 @Override
1464 public void commitDestructiveActions(boolean animate) {
mindypc6adce32012-08-22 18:46:42 -07001465 ConversationListFragment fragment = getConversationListFragment();
Mindy Pereira1e2573b2012-04-17 14:34:36 -07001466 if (fragment != null) {
mindypc6adce32012-08-22 18:46:42 -07001467 fragment.commitDestructiveActions(animate);
Mindy Pereira1e2573b2012-04-17 14:34:36 -07001468 }
1469 }
1470
Vikram Aggarwala55b36c2012-01-13 11:45:02 -08001471 @Override
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -08001472 public void onWindowFocusChanged(boolean hasFocus) {
Vikram Aggarwal69b5c302012-09-05 11:11:13 -07001473 final ConversationListFragment convList = getConversationListFragment();
Paul Westbrook9f119c72012-04-24 16:10:59 -07001474 if (hasFocus && convList != null && convList.isVisible()) {
1475 // The conversation list is visible.
Vikram Aggarwal69b5c302012-09-05 11:11:13 -07001476 informCursorVisiblity(true);
Paul Westbrook9f119c72012-04-24 16:10:59 -07001477 }
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -08001478 }
1479
Vikram Aggarwalca716f12012-08-20 11:11:48 -07001480 /**
1481 * Set the account, and carry out all the account-related changes that rely on this.
1482 * @param account
1483 */
Mindy Pereira75181e82012-04-18 08:17:13 -07001484 private void setAccount(Account account) {
Andy Huangb9ca9792012-05-18 15:31:49 -07001485 if (account == null) {
1486 LogUtils.w(LOG_TAG, new Error(),
1487 "AAC ignoring null (presumably invalid) account restoration");
1488 return;
1489 }
Andy Huangb1148412012-05-19 00:16:30 -07001490 LogUtils.d(LOG_TAG, "AbstractActivityController.setAccount(): account = %s", account.uri);
Vikram Aggarwal91e87372012-05-18 15:36:04 -07001491 mAccount = account;
Vikram Aggarwalca716f12012-08-20 11:11:48 -07001492 // Only change AAC state here. Do *not* modify any other object's state. The object
1493 // should listen on account changes.
1494 restartOptionalLoader(LOADER_RECENT_FOLDERS);
1495 mActivity.invalidateOptionsMenu();
1496 disableNotificationsOnAccountChange(mAccount);
1497 restartOptionalLoader(LOADER_ACCOUNT_UPDATE_CURSOR);
1498 MailAppProvider.getInstance().setLastViewedAccount(mAccount.uri.toString());
1499
Vikram Aggarwal91e87372012-05-18 15:36:04 -07001500 if (account.settings == null) {
1501 LogUtils.w(LOG_TAG, new Error(), "AAC ignoring account with null settings.");
1502 return;
1503 }
Vikram Aggarwal7c401b72012-08-13 16:43:47 -07001504 mAccountObservers.notifyChanged();
Vikram Aggarwal34f7b232012-10-17 13:32:23 -07001505 perhapsEnterWaitMode();
Mindy Pereira75181e82012-04-18 08:17:13 -07001506 }
1507
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -08001508 /**
Mindy Pereira161f50d2012-02-28 15:47:19 -08001509 * Restore the state from the previous bundle. Subclasses should call this
1510 * method from the parent class, since it performs important UI
1511 * initialization.
1512 *
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -08001513 * @param savedState
1514 */
Andy Huang632721e2012-04-11 16:57:26 -07001515 @Override
1516 public void onRestoreInstanceState(Bundle savedState) {
1517 LogUtils.d(LOG_TAG, "IN AAC.onRestoreInstanceState");
1518 if (savedState.containsKey(SAVED_CONVERSATION)) {
1519 // Open the conversation.
Andy Huang2bc8bc12012-11-12 17:24:25 -08001520 final Conversation conversation = savedState.getParcelable(SAVED_CONVERSATION);
Paul Westbrook534e4a22012-04-25 03:46:29 -07001521 if (conversation != null && conversation.position < 0) {
1522 // Set the position to 0 on this conversation, as we don't know where it is
1523 // in the list
1524 conversation.position = 0;
1525 }
Andy Huanged4fdf02012-07-26 17:12:50 -07001526 showConversation(conversation);
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -08001527 }
Mindy Pereira967ede62012-03-22 09:29:09 -07001528
Mindy Pereirad33674992012-06-25 16:26:30 -07001529 if (savedState.containsKey(SAVED_TOAST_BAR_OP)) {
Andy Huang2bc8bc12012-11-12 17:24:25 -08001530 ToastBarOperation op = savedState.getParcelable(SAVED_TOAST_BAR_OP);
Mindy Pereirad33674992012-06-25 16:26:30 -07001531 if (op != null) {
1532 if (op.getType() == ToastBarOperation.UNDO) {
1533 onUndoAvailable(op);
Andrew Sapperstein9d7519d2012-07-16 14:03:53 -07001534 } else if (op.getType() == ToastBarOperation.ERROR) {
1535 onError(mFolder, true);
Mindy Pereirad33674992012-06-25 16:26:30 -07001536 }
1537 }
1538 }
Vikram Aggarwal649b9ea2012-08-27 12:15:20 -07001539 final String folderString = savedState.getString(SAVED_HIERARCHICAL_FOLDER, null);
1540 if (!TextUtils.isEmpty(folderString)) {
1541 mFolderListFolder = Folder.fromString(folderString);
1542 }
1543 final ConversationListFragment convListFragment = getConversationListFragment();
Mindy Pereirad33674992012-06-25 16:26:30 -07001544 if (convListFragment != null) {
Vikram Aggarwal649b9ea2012-08-27 12:15:20 -07001545 convListFragment.getAnimatedAdapter().onRestoreInstanceState(savedState);
Mindy Pereirad33674992012-06-25 16:26:30 -07001546 }
Mindy Pereira967ede62012-03-22 09:29:09 -07001547 /**
1548 * Restore the state of selected conversations. This needs to be done after the correct mode
1549 * is set and the action bar is fully initialized. If not, several key pieces of state
1550 * information will be missing, and the split views may not be initialized correctly.
1551 * @param savedState
1552 */
Andy Huang4556a442012-03-30 16:42:05 -07001553 restoreSelectedConversations(savedState);
Andy Huang632721e2012-04-11 16:57:26 -07001554 }
1555
Vikram Aggarwal649b9ea2012-08-27 12:15:20 -07001556 /**
1557 * Handle an intent to open the app. This method is called only when there is no saved state,
1558 * so we need to set state that wasn't set before. It is correct to change the viewmode here
1559 * since it has not been previously set.
1560 * @param intent
1561 */
Andy Huang632721e2012-04-11 16:57:26 -07001562 private void handleIntent(Intent intent) {
1563 boolean handled = false;
1564 if (Intent.ACTION_VIEW.equals(intent.getAction())) {
1565 if (intent.hasExtra(Utils.EXTRA_ACCOUNT)) {
Vikram Aggarwal649b9ea2012-08-27 12:15:20 -07001566 setAccount(Account.newinstance(intent.getStringExtra(Utils.EXTRA_ACCOUNT)));
Andy Huang632721e2012-04-11 16:57:26 -07001567 }
Andy Huangb9ca9792012-05-18 15:31:49 -07001568 if (mAccount == null) {
1569 return;
Andy Huang632721e2012-04-11 16:57:26 -07001570 }
Vikram Aggarwal1a249e02012-08-03 16:19:33 -07001571 final boolean isConversationMode = intent.hasExtra(Utils.EXTRA_CONVERSATION);
Vikram Aggarwal649b9ea2012-08-27 12:15:20 -07001572 if (isConversationMode && mViewMode.getMode() == ViewMode.UNKNOWN) {
Vikram Aggarwal1a249e02012-08-03 16:19:33 -07001573 mViewMode.enterConversationMode();
1574 } else {
1575 mViewMode.enterConversationListMode();
1576 }
Vikram Aggarwal1672ff82012-09-21 10:15:22 -07001577 final Folder folder = intent.hasExtra(Utils.EXTRA_FOLDER) ?
1578 Folder.fromString(intent.getStringExtra(Utils.EXTRA_FOLDER)) : null;
Andy Huang632721e2012-04-11 16:57:26 -07001579 if (folder != null) {
1580 onFolderChanged(folder);
1581 handled = true;
1582 }
1583
Vikram Aggarwal1a249e02012-08-03 16:19:33 -07001584 if (isConversationMode) {
Andy Huang632721e2012-04-11 16:57:26 -07001585 // Open the conversation.
1586 LogUtils.d(LOG_TAG, "SHOW THE CONVERSATION at %s",
1587 intent.getParcelableExtra(Utils.EXTRA_CONVERSATION));
Paul Westbrooka9161912012-04-24 10:10:04 -07001588 final Conversation conversation =
Andy Huang2bc8bc12012-11-12 17:24:25 -08001589 intent.getParcelableExtra(Utils.EXTRA_CONVERSATION);
Paul Westbrooka9161912012-04-24 10:10:04 -07001590 if (conversation != null && conversation.position < 0) {
1591 // Set the position to 0 on this conversation, as we don't know where it is
1592 // in the list
1593 conversation.position = 0;
1594 }
Andy Huang980aaea2012-07-26 17:22:19 -07001595 showConversation(conversation);
Andy Huang632721e2012-04-11 16:57:26 -07001596 handled = true;
1597 }
1598
1599 if (!handled) {
Vikram Aggarwal0c3c2052012-09-21 11:06:28 -07001600 // We have an account, but nothing else: load the default inbox.
Andy Huang632721e2012-04-11 16:57:26 -07001601 loadAccountInbox();
1602 }
1603 } else if (Intent.ACTION_SEARCH.equals(intent.getAction())) {
1604 if (intent.hasExtra(Utils.EXTRA_ACCOUNT)) {
Vikram Aggarwalf0ef2302012-11-07 14:53:34 -08001605 mHaveSearchResults = false;
Andy Huang632721e2012-04-11 16:57:26 -07001606 // Save this search query for future suggestions.
1607 final String query = intent.getStringExtra(SearchManager.QUERY);
1608 final String authority = mContext.getString(R.string.suggestions_authority);
Vikram Aggarwal2b703c62012-09-18 13:54:15 -07001609 final SearchRecentSuggestions suggestions = new SearchRecentSuggestions(
Andy Huang632721e2012-04-11 16:57:26 -07001610 mContext, authority, SuggestionsProvider.MODE);
1611 suggestions.saveRecentQuery(query, null);
Vikram Aggarwalf0ef2302012-11-07 14:53:34 -08001612 setAccount((Account) intent.getParcelableExtra(Utils.EXTRA_ACCOUNT));
1613 fetchSearchFolder(intent);
1614 if (shouldEnterSearchConvMode()) {
Mindy Pereiraac254822012-06-18 10:46:43 -07001615 mViewMode.enterSearchResultsConversationMode();
1616 } else {
1617 mViewMode.enterSearchResultsListMode();
1618 }
Andy Huang632721e2012-04-11 16:57:26 -07001619 } else {
1620 LogUtils.e(LOG_TAG, "Missing account extra from search intent. Finishing");
1621 mActivity.finish();
1622 }
1623 }
1624 if (mAccount != null) {
1625 restartOptionalLoader(LOADER_ACCOUNT_UPDATE_CURSOR);
1626 }
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -08001627 }
1628
Andy Huang4556a442012-03-30 16:42:05 -07001629 /**
Vikram Aggarwalf0ef2302012-11-07 14:53:34 -08001630 * Returns true if we should enter conversation mode with search.
1631 */
1632 protected final boolean shouldEnterSearchConvMode() {
1633 return mHaveSearchResults && Utils.showTwoPaneSearchResults(mActivity.getActivityContext());
1634 }
1635
1636 /**
Andy Huang4556a442012-03-30 16:42:05 -07001637 * Copy any selected conversations stored in the saved bundle into our selection set,
1638 * triggering {@link ConversationSetObserver} callbacks as our selection set changes.
1639 *
1640 */
Vikram Aggarwal4eb52712012-06-19 16:24:50 -07001641 private final void restoreSelectedConversations(Bundle savedState) {
Mindy Pereira967ede62012-03-22 09:29:09 -07001642 if (savedState == null) {
Andy Huang4556a442012-03-30 16:42:05 -07001643 mSelectedSet.clear();
Mindy Pereira967ede62012-03-22 09:29:09 -07001644 return;
1645 }
Vikram Aggarwalcabd3f22012-04-19 10:14:41 -07001646 final ConversationSelectionSet selectedSet = savedState.getParcelable(SAVED_SELECTED_SET);
Andy Huang4556a442012-03-30 16:42:05 -07001647 if (selectedSet == null || selectedSet.isEmpty()) {
1648 mSelectedSet.clear();
Mindy Pereira967ede62012-03-22 09:29:09 -07001649 return;
1650 }
Andy Huang632721e2012-04-11 16:57:26 -07001651
1652 // putAll will take care of calling our registered onSetPopulated method
Vikram Aggarwalcabd3f22012-04-19 10:14:41 -07001653 mSelectedSet.putAll(selectedSet);
Mindy Pereira967ede62012-03-22 09:29:09 -07001654 }
1655
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -08001656 @Override
Andy Huang5895f7b2012-06-01 17:07:20 -07001657 public SubjectDisplayChanger getSubjectDisplayChanger() {
1658 return mActionBarView;
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -08001659 }
1660
Vikram Aggarwal17f373e2012-09-17 15:49:32 -07001661 private final void showConversation(Conversation conversation) {
Andy Huang1ee96b22012-08-24 20:19:53 -07001662 showConversation(conversation, false /* inLoaderCallbacks */);
1663 }
1664
Vikram Aggarwalec5cbf72012-03-08 15:10:35 -08001665 /**
Vikram Aggarwal49e0e992012-09-21 13:53:15 -07001666 * Show the conversation provided in the arguments. It is safe to pass a null conversation
1667 * object, which is a signal to back out of conversation view mode.
1668 * Child classes must call super.showConversation() <b>before</b> their own implementations.
1669 * @param conversation
1670 * @param inLoaderCallbacks true if the method is called as a result of
1671 * {@link #onLoadFinished(Loader, Cursor)}
Vikram Aggarwalec5cbf72012-03-08 15:10:35 -08001672 */
Andy Huang1ee96b22012-08-24 20:19:53 -07001673 protected void showConversation(Conversation conversation, boolean inLoaderCallbacks) {
Vikram Aggarwalc67182d2012-04-03 14:35:06 -07001674 // Set the current conversation just in case it wasn't already set.
1675 setCurrentConversation(conversation);
Vikram Aggarwal0f142732012-08-24 09:39:34 -07001676 // Add the folder that we were viewing to the recent folders list.
1677 // TODO: this may need to be fine tuned. If this is the signal that is indicating that
1678 // the list is shown to the user, this could fire in one pane if the user goes directly
1679 // to a conversation
1680 updateRecentFolderList();
Vikram Aggarwalec5cbf72012-03-08 15:10:35 -08001681 }
1682
Vikram Aggarwale128fc22012-04-04 12:33:34 -07001683 /**
Paul Westbrook2d50bcd2012-04-10 11:53:47 -07001684 * Children can override this method, but they must call super.showWaitForInitialization().
1685 * {@inheritDoc}
1686 */
1687 @Override
1688 public void showWaitForInitialization() {
1689 mViewMode.enterWaitingForInitializationMode();
Vikram Aggarwala3f43d42012-10-25 16:21:30 -07001690 mWaitFragment = WaitFragment.newInstance(mAccount);
Paul Westbrook2d50bcd2012-04-10 11:53:47 -07001691 }
1692
Vikram Aggarwaldd6a7ce2012-10-22 15:45:57 -07001693 private void updateWaitMode() {
Paul Westbrook2d50bcd2012-04-10 11:53:47 -07001694 final FragmentManager manager = mActivity.getFragmentManager();
1695 final WaitFragment waitFragment =
Vikram Aggarwal6902dcf2012-04-11 08:57:42 -07001696 (WaitFragment)manager.findFragmentByTag(TAG_WAIT);
Paul Westbrook2d50bcd2012-04-10 11:53:47 -07001697 if (waitFragment != null) {
1698 waitFragment.updateAccount(mAccount);
1699 }
1700 }
1701
Vikram Aggarwala3f43d42012-10-25 16:21:30 -07001702 /**
1703 * Remove the "Waiting for Initialization" fragment. Child classes are free to override this
1704 * method, though they must call the parent implementation <b>after</b> they do anything.
1705 */
1706 protected void hideWaitForInitialization() {
1707 mWaitFragment = null;
1708 }
1709
1710 /**
1711 * Use the instance variable and the wait fragment's tag to get the wait fragment. This is
1712 * far superior to using the value of mWaitFragment, which might be invalid or might refer
1713 * to a fragment after it has been destroyed.
1714 * @return
1715 */
1716 protected final WaitFragment getWaitFragment() {
1717 final FragmentManager manager = mActivity.getFragmentManager();
1718 final WaitFragment waitFrag = (WaitFragment) manager.findFragmentByTag(TAG_WAIT);
1719 if (waitFrag != null) {
1720 // The Fragment Manager knows better, so use its instance.
1721 mWaitFragment = waitFrag;
1722 }
1723 return mWaitFragment;
1724 }
Vikram Aggarwaldd6a7ce2012-10-22 15:45:57 -07001725
Vikram Aggarwal48b2a6c2012-05-29 14:09:27 -07001726 /**
1727 * Returns true if we are waiting for the account to sync, and cannot show any folders or
1728 * conversation for the current account yet.
Vikram Aggarwal48b2a6c2012-05-29 14:09:27 -07001729 */
Vikram Aggarwaldd6a7ce2012-10-22 15:45:57 -07001730 private boolean inWaitMode() {
Vikram Aggarwala3f43d42012-10-25 16:21:30 -07001731 final WaitFragment waitFragment = getWaitFragment();
Paul Westbrook2d50bcd2012-04-10 11:53:47 -07001732 if (waitFragment != null) {
1733 final Account fragmentAccount = waitFragment.getAccount();
Paul Westbrook339004b2012-11-05 17:13:51 -08001734 return fragmentAccount != null && fragmentAccount.uri.equals(mAccount.uri) &&
Paul Westbrook2d50bcd2012-04-10 11:53:47 -07001735 mViewMode.getMode() == ViewMode.WAITING_FOR_ACCOUNT_INITIALIZATION;
1736 }
1737 return false;
1738 }
1739
1740 /**
Vikram Aggarwale128fc22012-04-04 12:33:34 -07001741 * Children can override this method, but they must call super.showConversationList().
1742 * {@inheritDoc}
1743 */
1744 @Override
1745 public void showConversationList(ConversationListContext listContext) {
1746 }
1747
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -08001748 @Override
Vikram Aggarwal17f373e2012-09-17 15:49:32 -07001749 public final void onConversationSelected(Conversation conversation, boolean inLoaderCallbacks) {
mindypaa55bc92012-08-24 09:49:56 -07001750 // Only animate destructive actions if we are going to be showing the
1751 // conversation list when we show the next conversation.
1752 commitDestructiveActions(Utils.useTabletUI(mContext));
Andy Huang1ee96b22012-08-24 20:19:53 -07001753 showConversation(conversation, inLoaderCallbacks);
1754 }
1755
1756 @Override
1757 public Conversation getCurrentConversation() {
1758 return mCurrentConversation;
Vikram Aggarwal7d602882012-02-07 15:01:20 -08001759 }
Mindy Pereira555140c2012-02-15 14:55:29 -08001760
Vikram Aggarwalc67182d2012-04-03 14:35:06 -07001761 /**
1762 * Set the current conversation. This is the conversation on which all actions are performed.
1763 * Do not modify mCurrentConversation except through this method, which makes it easy to
1764 * perform common actions associated with changing the current conversation.
1765 * @param conversation
1766 */
Andy Huang632721e2012-04-11 16:57:26 -07001767 @Override
1768 public void setCurrentConversation(Conversation conversation) {
Vikram Aggarwal17f373e2012-09-17 15:49:32 -07001769 // Must be the first call because this sets conversation.position if a cursor is available.
Andy Huang8883f222012-11-12 19:25:00 -08001770 mTracker.initialize(conversation);
Vikram Aggarwal17f373e2012-09-17 15:49:32 -07001771 mCurrentConversation = conversation;
Andy Huang7d646122012-09-05 19:41:44 -07001772
1773 if (mCurrentConversation != null) {
Yorke Leef807ba72012-09-20 17:18:05 -07001774 mActionBarView.setCurrentConversation(mCurrentConversation);
Andy Huang7d646122012-09-05 19:41:44 -07001775 getSubjectDisplayChanger().setSubject(mCurrentConversation.subject);
Yorke Leef807ba72012-09-20 17:18:05 -07001776 mActivity.invalidateOptionsMenu();
Andy Huang7d646122012-09-05 19:41:44 -07001777 }
Mindy Pereira5040f1a2012-03-20 10:14:06 -07001778 }
1779
Vikram Aggarwala9b93f32012-02-23 14:51:58 -08001780 /**
1781 * {@inheritDoc}
1782 */
Mindy Pereira555140c2012-02-15 14:55:29 -08001783 @Override
Vikram Aggarwal7dedb952012-02-16 16:10:23 -08001784 public Loader<Cursor> onCreateLoader(int id, Bundle args) {
Vikram Aggarwalfa4b47e2012-03-09 13:02:46 -08001785 switch (id) {
1786 case LOADER_ACCOUNT_CURSOR:
Paul Westbrookc2074c42012-03-22 15:26:58 -07001787 return new CursorLoader(mContext, MailAppProvider.getAccountsUri(),
Vikram Aggarwalfa4b47e2012-03-09 13:02:46 -08001788 UIProvider.ACCOUNTS_PROJECTION, null, null, null);
1789 case LOADER_FOLDER_CURSOR:
Paul Westbrookc7a070f2012-04-12 01:46:41 -07001790 final CursorLoader loader = new CursorLoader(mContext, mFolder.uri,
1791 UIProvider.FOLDERS_PROJECTION, null, null, null);
1792 loader.setUpdateThrottle(mFolderItemUpdateDelayMs);
1793 return loader;
Vikram Aggarwalfa4b47e2012-03-09 13:02:46 -08001794 case LOADER_RECENT_FOLDERS:
Paul Westbrook91d10502012-04-13 12:01:39 -07001795 if (mAccount != null && mAccount.recentFolderListUri != null) {
Paul Westbrookea4ee202012-03-12 14:12:33 -07001796 return new CursorLoader(mContext, mAccount.recentFolderListUri,
1797 UIProvider.FOLDERS_PROJECTION, null, null, null);
1798 }
1799 break;
Mindy Pereiraab486362012-03-21 18:18:53 -07001800 case LOADER_ACCOUNT_INBOX:
Vikram Aggarwal025eba82012-05-08 10:45:30 -07001801 final Uri defaultInbox = Settings.getDefaultInboxUri(mAccount.settings);
Vikram Aggarwal1e57e672012-05-07 14:48:24 -07001802 final Uri inboxUri = defaultInbox.equals(Uri.EMPTY) ?
1803 mAccount.folderListUri : defaultInbox;
Paul Westbrook7496e822012-04-24 09:50:54 -07001804 LogUtils.d(LOG_TAG, "Loading the default inbox: %s", inboxUri);
Paul Westbrook1220b6d2012-04-10 00:48:00 -07001805 if (inboxUri != null) {
1806 return new CursorLoader(mContext, inboxUri, UIProvider.FOLDERS_PROJECTION, null,
1807 null, null);
1808 }
1809 break;
Mindy Pereiraab486362012-03-21 18:18:53 -07001810 case LOADER_SEARCH:
1811 return Folder.forSearchResults(mAccount,
1812 args.getString(ConversationListContext.EXTRA_SEARCH_QUERY),
1813 mActivity.getActivityContext());
Paul Westbrook2d50bcd2012-04-10 11:53:47 -07001814 case LOADER_ACCOUNT_UPDATE_CURSOR:
1815 return new CursorLoader(mContext, mAccount.uri, UIProvider.ACCOUNTS_PROJECTION,
1816 null, null, null);
Vikram Aggarwalfa4b47e2012-03-09 13:02:46 -08001817 default:
Paul Westbrookad6a2752012-04-04 16:58:13 -07001818 LogUtils.wtf(LOG_TAG, "Loader returned unexpected id: %d", id);
Mindy Pereira5cb0c3e2012-02-22 15:22:47 -08001819 }
1820 return null;
Vikram Aggarwal7dedb952012-02-16 16:10:23 -08001821 }
1822
Paul Westbrookb1f573c2012-04-06 11:38:28 -07001823 @Override
1824 public void onLoaderReset(Loader<Cursor> loader) {
1825
1826 }
1827
Andy Huangf9a73482012-03-13 15:54:02 -07001828 /**
1829 * {@link LoaderManager} currently has a bug in
1830 * {@link LoaderManager#restartLoader(int, Bundle, android.app.LoaderManager.LoaderCallbacks)}
1831 * where, if a previous onCreateLoader returned a null loader, this method will NPE. Work around
1832 * this bug by destroying any loaders that may have been created as null (essentially because
1833 * they are optional loads, and may not apply to a particular account).
1834 * <p>
1835 * A simple null check before restarting a loader will not work, because that would not
1836 * give the controller a chance to invalidate UI corresponding the prior loader result.
1837 *
1838 * @param id loader ID to safely restart
Andy Huangf9a73482012-03-13 15:54:02 -07001839 */
Vikram Aggarwal94c94de2012-04-04 15:38:28 -07001840 private void restartOptionalLoader(int id) {
Andy Huangf9a73482012-03-13 15:54:02 -07001841 final LoaderManager lm = mActivity.getLoaderManager();
1842 lm.destroyLoader(id);
Vikram Aggarwal94c94de2012-04-04 15:38:28 -07001843 lm.restartLoader(id, Bundle.EMPTY, this);
1844 }
1845
Andy Huang632721e2012-04-11 16:57:26 -07001846 @Override
1847 public void registerConversationListObserver(DataSetObserver observer) {
1848 mConversationListObservable.registerObserver(observer);
1849 }
1850
1851 @Override
1852 public void unregisterConversationListObserver(DataSetObserver observer) {
1853 mConversationListObservable.unregisterObserver(observer);
1854 }
1855
Andy Huang090db1e2012-07-25 13:25:28 -07001856 @Override
1857 public void registerFolderObserver(DataSetObserver observer) {
1858 mFolderObservable.registerObserver(observer);
1859 }
1860
1861 @Override
1862 public void unregisterFolderObserver(DataSetObserver observer) {
1863 mFolderObservable.unregisterObserver(observer);
1864 }
1865
Andy Huang9d3fd922012-09-26 22:23:58 -07001866 @Override
1867 public void registerConversationLoadedObserver(DataSetObserver observer) {
1868 mPagerController.registerConversationLoadedObserver(observer);
1869 }
1870
1871 @Override
1872 public void unregisterConversationLoadedObserver(DataSetObserver observer) {
1873 mPagerController.unregisterConversationLoadedObserver(observer);
1874 }
1875
Vikram Aggarwal60069912012-07-24 14:26:09 -07001876 /**
1877 * Returns true if the number of accounts is different, or if the current account has been
1878 * removed from the device
1879 * @param accountCursor
1880 * @return
1881 */
Paul Westbrook23b74b92012-02-29 11:36:12 -08001882 private boolean accountsUpdated(Cursor accountCursor) {
1883 // Check to see if the current account hasn't been set, or the account cursor is empty
1884 if (mAccount == null || !accountCursor.moveToFirst()) {
Vikram Aggarwal6c511582012-02-27 10:59:47 -08001885 return true;
Paul Westbrook23b74b92012-02-29 11:36:12 -08001886 }
1887
1888 // Check to see if the number of accounts are different, from the number we saw on the last
1889 // updated
1890 if (mCurrentAccountUris.size() != accountCursor.getCount()) {
1891 return true;
1892 }
1893
1894 // Check to see if the account list is different or if the current account is not found in
1895 // the cursor.
1896 boolean foundCurrentAccount = false;
Vikram Aggarwal7dedb952012-02-16 16:10:23 -08001897 do {
Paul Westbrook23b74b92012-02-29 11:36:12 -08001898 final Uri accountUri =
1899 Uri.parse(accountCursor.getString(UIProvider.ACCOUNT_URI_COLUMN));
1900 if (!foundCurrentAccount && mAccount.uri.equals(accountUri)) {
1901 foundCurrentAccount = true;
1902 }
Vikram Aggarwal60069912012-07-24 14:26:09 -07001903 // Is there a new account that we do not know about?
Paul Westbrook23b74b92012-02-29 11:36:12 -08001904 if (!mCurrentAccountUris.contains(accountUri)) {
1905 return true;
1906 }
Vikram Aggarwal7dedb952012-02-16 16:10:23 -08001907 } while (accountCursor.moveToNext());
Paul Westbrook23b74b92012-02-29 11:36:12 -08001908
1909 // As long as we found the current account, the list hasn't been updated
1910 return !foundCurrentAccount;
Vikram Aggarwal7dedb952012-02-16 16:10:23 -08001911 }
1912
1913 /**
Vikram Aggarwal60069912012-07-24 14:26:09 -07001914 * Updates accounts for the app. If the current account is missing, the first
1915 * account in the list is set to the current account (we <em>have</em> to choose something).
Mindy Pereira161f50d2012-02-28 15:47:19 -08001916 *
Vikram Aggarwal6c511582012-02-27 10:59:47 -08001917 * @param accounts cursor into the AccountCache
Vikram Aggarwal7dedb952012-02-16 16:10:23 -08001918 * @return true if the update was successful, false otherwise
1919 */
Vikram Aggarwal60069912012-07-24 14:26:09 -07001920 private boolean updateAccounts(Cursor accounts) {
Vikram Aggarwal7dedb952012-02-16 16:10:23 -08001921 if (accounts == null || !accounts.moveToFirst()) {
1922 return false;
1923 }
Paul Westbrook23b74b92012-02-29 11:36:12 -08001924
Vikram Aggarwala9b93f32012-02-23 14:51:58 -08001925 final Account[] allAccounts = Account.getAllAccounts(accounts);
Vikram Aggarwal60069912012-07-24 14:26:09 -07001926 // A match for the current account's URI in the list of accounts.
1927 Account currentFromList = null;
Paul Westbrook23b74b92012-02-29 11:36:12 -08001928
Vikram Aggarwal636c1a12012-09-22 16:02:40 -07001929 // Save the uris for the accounts and find the current account in the updated cursor.
Paul Westbrook23b74b92012-02-29 11:36:12 -08001930 mCurrentAccountUris.clear();
Vikram Aggarwal636c1a12012-09-22 16:02:40 -07001931 for (final Account account : allAccounts) {
Vikram Aggarwal60069912012-07-24 14:26:09 -07001932 LogUtils.d(LOG_TAG, "updateAccounts(%s)", account);
Paul Westbrook23b74b92012-02-29 11:36:12 -08001933 mCurrentAccountUris.add(account.uri);
Vikram Aggarwal60069912012-07-24 14:26:09 -07001934 if (mAccount != null && account.uri.equals(mAccount.uri)) {
1935 currentFromList = account;
1936 }
Paul Westbrook23b74b92012-02-29 11:36:12 -08001937 }
1938
Vikram Aggarwal60069912012-07-24 14:26:09 -07001939 // 1. current account is already set and is in allAccounts:
1940 // 1a. It has changed -> load the updated account.
1941 // 2b. It is unchanged -> no-op
Andy Huang0d647352012-03-21 21:48:16 -07001942 // 2. current account is set and is not in allAccounts -> pick first (acct was deleted?)
Vikram Aggarwal60069912012-07-24 14:26:09 -07001943 // 3. saved preference has an account -> pick that one
Andy Huang0d647352012-03-21 21:48:16 -07001944 // 4. otherwise just pick first
1945
Vikram Aggarwal60069912012-07-24 14:26:09 -07001946 boolean accountChanged = false;
1947 /// Assume case 4, initialize to first account, and see if we can find anything better.
1948 Account newAccount = allAccounts[0];
1949 if (currentFromList != null) {
1950 // Case 1: Current account exists but has changed
1951 if (!currentFromList.equals(mAccount)) {
1952 newAccount = currentFromList;
1953 accountChanged = true;
Andy Huang0d647352012-03-21 21:48:16 -07001954 }
Vikram Aggarwal60069912012-07-24 14:26:09 -07001955 // Case 1b: else, current account is unchanged: nothing to do.
Paul Westbrook23b74b92012-02-29 11:36:12 -08001956 } else {
Vikram Aggarwal60069912012-07-24 14:26:09 -07001957 // Case 2: Current account is not in allAccounts, the account needs to change.
1958 accountChanged = true;
1959 if (mAccount == null) {
1960 // Case 3: Check for last viewed account, and check if it exists in the list.
1961 final String lastAccountUri = MailAppProvider.getInstance().getLastViewedAccount();
1962 if (lastAccountUri != null) {
1963 for (final Account account : allAccounts) {
1964 if (lastAccountUri.equals(account.uri.toString())) {
1965 newAccount = account;
1966 break;
1967 }
Andy Huang0d647352012-03-21 21:48:16 -07001968 }
1969 }
1970 }
Paul Westbrook23b74b92012-02-29 11:36:12 -08001971 }
Vikram Aggarwal60069912012-07-24 14:26:09 -07001972 if (accountChanged) {
1973 onAccountChanged(newAccount);
1974 }
1975 // Whether we have updated the current account or not, we need to update the list of
1976 // accounts in the ActionBar.
Vikram Aggarwala9b93f32012-02-23 14:51:58 -08001977 mActionBarView.setAccounts(allAccounts);
1978 return (allAccounts.length > 0);
Vikram Aggarwal7dedb952012-02-16 16:10:23 -08001979 }
1980
Paul Westbrook6ead20d2012-03-19 14:48:14 -07001981 private void disableNotifications() {
1982 mNewEmailReceiver.activate(mContext, this);
1983 }
1984
1985 private void enableNotifications() {
1986 mNewEmailReceiver.deactivate();
1987 }
1988
1989 private void disableNotificationsOnAccountChange(Account account) {
1990 // If the new mail suppression receiver is activated for a different account, we want to
1991 // activate it for the new account.
1992 if (mNewEmailReceiver.activated() &&
1993 !mNewEmailReceiver.notificationsDisabledForAccount(account)) {
1994 // Deactivate the current receiver, otherwise multiple receivers may be registered.
1995 mNewEmailReceiver.deactivate();
1996 mNewEmailReceiver.activate(mContext, this);
1997 }
1998 }
1999
Vikram Aggarwala9b93f32012-02-23 14:51:58 -08002000 /**
2001 * {@inheritDoc}
2002 */
Vikram Aggarwal7dedb952012-02-16 16:10:23 -08002003 @Override
2004 public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
Mindy Pereira5cb0c3e2012-02-22 15:22:47 -08002005 // We want to reinitialize only if we haven't ever been initialized, or
2006 // if the current account has vanished.
Paul Westbrooke3e84292012-03-05 16:19:30 -08002007 if (data == null) {
Vikram Aggarwalfa4b47e2012-03-09 13:02:46 -08002008 LogUtils.e(LOG_TAG, "Received null cursor from loader id: %d", loader.getId());
Paul Westbrooke3e84292012-03-05 16:19:30 -08002009 }
Vikram Aggarwalfa4b47e2012-03-09 13:02:46 -08002010 switch (loader.getId()) {
2011 case LOADER_ACCOUNT_CURSOR:
Vikram Aggarwal636c1a12012-09-22 16:02:40 -07002012 if (data == null) {
2013 // Nothing useful to do if we have no valid data.
2014 break;
2015 }
2016 if (data.getCount() == 0) {
Paul Westbrook2388c5d2012-03-25 12:29:11 -07002017 // If an empty cursor is returned, the MailAppProvider is indicating that
2018 // no accounts have been specified. We want to navigate to the "add account"
2019 // activity that will handle the intent returned by the MailAppProvider
2020
2021 // If the MailAppProvider believes that all accounts have been loaded, and the
2022 // account list is still empty, we want to prompt the user to add an account
2023 final Bundle extras = data.getExtras();
2024 final boolean accountsLoaded =
2025 extras.getInt(AccountCursorExtraKeys.ACCOUNTS_LOADED) != 0;
2026
2027 if (accountsLoaded) {
2028 final Intent noAccountIntent = MailAppProvider.getNoAccountIntent(mContext);
2029 if (noAccountIntent != null) {
2030 mActivity.startActivityForResult(noAccountIntent,
2031 ADD_ACCOUNT_REQUEST_CODE);
2032 }
2033 }
2034 } else {
2035 final boolean accountListUpdated = accountsUpdated(data);
2036 if (!isLoaderInitialized || accountListUpdated) {
Vikram Aggarwal60069912012-07-24 14:26:09 -07002037 isLoaderInitialized = updateAccounts(data);
Paul Westbrook2388c5d2012-03-25 12:29:11 -07002038 }
Vikram Aggarwalfa4b47e2012-03-09 13:02:46 -08002039 }
2040 break;
Paul Westbrook2d50bcd2012-04-10 11:53:47 -07002041 case LOADER_ACCOUNT_UPDATE_CURSOR:
2042 // We have gotten an update for current account.
2043
Vikram Aggarwal60069912012-07-24 14:26:09 -07002044 // Make sure that this is an update for the current account
Paul Westbrook2d50bcd2012-04-10 11:53:47 -07002045 if (data != null && data.moveToFirst()) {
2046 final Account updatedAccount = new Account(data);
2047
2048 if (updatedAccount.uri.equals(mAccount.uri)) {
Paul Westbrookca08fc12012-07-31 12:01:15 -07002049 // Keep a reference to the previous settings object
2050 final Settings previousSettings = mAccount.settings;
2051
Vikram Aggarwal7d816002012-04-17 17:06:41 -07002052 // Update the controller's reference to the current account
Paul Westbrook2d50bcd2012-04-10 11:53:47 -07002053 mAccount = updatedAccount;
Vikram Aggarwaledc137c2012-04-24 13:40:58 -07002054 LogUtils.d(LOG_TAG, "AbstractActivityController.onLoadFinished(): "
2055 + "mAccount = %s", mAccount.uri);
Paul Westbrookca08fc12012-07-31 12:01:15 -07002056
2057 // Only notify about a settings change if something differs
2058 if (!Objects.equal(mAccount.settings, previousSettings)) {
Vikram Aggarwal7c401b72012-08-13 16:43:47 -07002059 mAccountObservers.notifyChanged();
Paul Westbrookca08fc12012-07-31 12:01:15 -07002060 }
Vikram Aggarwal34f7b232012-10-17 13:32:23 -07002061 perhapsEnterWaitMode();
Paul Westbrook2d50bcd2012-04-10 11:53:47 -07002062 } else {
2063 LogUtils.e(LOG_TAG, "Got update for account: %s with current account: %s",
2064 updatedAccount.uri, mAccount.uri);
2065 // We need to restart the loader, so the correct account information will
2066 // be returned
2067 restartOptionalLoader(LOADER_ACCOUNT_UPDATE_CURSOR);
2068 }
2069 }
2070 break;
Vikram Aggarwalfa4b47e2012-03-09 13:02:46 -08002071 case LOADER_FOLDER_CURSOR:
Vikram Aggarwalfa4b47e2012-03-09 13:02:46 -08002072 // Check status of the cursor.
Marc Blankfd9d0b82012-04-23 16:01:51 -07002073 if (data != null && data.moveToFirst()) {
Andy Huang090db1e2012-07-25 13:25:28 -07002074 final Folder folder = new Folder(data);
Marc Blankfd9d0b82012-04-23 16:01:51 -07002075 LogUtils.d(LOG_TAG, "FOLDER STATUS = %d", folder.syncStatus);
Vikram Aggarwald00229d2012-09-20 12:31:44 -07002076 setHasFolderChanged(folder);
Andy Huang090db1e2012-07-25 13:25:28 -07002077 mFolder = folder;
2078 mFolderObservable.notifyChanged();
Paul Westbrookc808fac2012-02-22 16:42:18 -08002079 } else {
Marc Blankfd9d0b82012-04-23 16:01:51 -07002080 LogUtils.d(LOG_TAG, "Unable to get the folder %s",
2081 mFolder != null ? mAccount.name : "");
Mindy Pereira11dd5ef2012-03-10 15:10:18 -08002082 }
Vikram Aggarwalfa4b47e2012-03-09 13:02:46 -08002083 break;
Vikram Aggarwalfa4b47e2012-03-09 13:02:46 -08002084 case LOADER_RECENT_FOLDERS:
Vikram Aggarwalf9ba8712012-09-23 11:42:39 -07002085 // Few recent folders and we are running on a phone? Populate the default recents.
2086 // The number of default recent folders is at least 2: every provider has at
2087 // least two folders, and the recent folder count never decreases. Having a single
2088 // recent folder is an erroneous case, and we can gracefully recover by populating
2089 // default recents. The default recents will not stomp on the existing value: it
2090 // will be shown in addition to the default folders: the max number of recent
2091 // folders is more than 1+num(defaultRecents).
2092 if (data != null && data.getCount() <= 1 && !Utils.useTabletUI(mContext)) {
Vikram Aggarwal27d89ad2012-06-12 13:38:40 -07002093 final class PopulateDefault extends AsyncTask<Uri, Void, Void> {
2094 @Override
2095 protected Void doInBackground(Uri... uri) {
2096 // Asking for an update on the URI and ignore the result.
2097 final ContentResolver resolver = mContext.getContentResolver();
2098 resolver.update(uri[0], null, null, null);
2099 return null;
2100 }
2101 }
2102 final Uri uri = mAccount.defaultRecentFolderListUri;
2103 LogUtils.v(LOG_TAG, "Default recents at %s", uri);
2104 new PopulateDefault().execute(uri);
2105 break;
2106 }
2107 LogUtils.v(LOG_TAG, "Reading recent folders from the cursor.");
mindyp6f54e1b2012-10-09 09:54:08 -07002108 loadRecentFolders(data);
Vikram Aggarwalfa4b47e2012-03-09 13:02:46 -08002109 break;
Mindy Pereiraab486362012-03-21 18:18:53 -07002110 case LOADER_ACCOUNT_INBOX:
Marc Blankfd9d0b82012-04-23 16:01:51 -07002111 if (data != null && !data.isClosed() && data.moveToFirst()) {
Mindy Pereira0e88e9f2012-03-25 13:47:41 -07002112 Folder inbox = new Folder(data);
2113 onFolderChanged(inbox);
Mindy Pereirab4a43282012-03-23 16:20:03 -07002114 // Just want to get the inbox, don't care about updates to it
2115 // as this will be tracked by the folder change listener.
2116 mActivity.getLoaderManager().destroyLoader(LOADER_ACCOUNT_INBOX);
Mindy Pereira5ba33802012-03-26 16:30:11 -07002117 } else {
2118 LogUtils.d(LOG_TAG, "Unable to get the account inbox for account %s",
2119 mAccount != null ? mAccount.name : "");
Mindy Pereirab4a43282012-03-23 16:20:03 -07002120 }
Mindy Pereiraab486362012-03-21 18:18:53 -07002121 break;
2122 case LOADER_SEARCH:
Paul Westbrookc4845c52012-08-29 21:48:43 -07002123 if (data != null && data.getCount() > 0) {
2124 data.moveToFirst();
Vikram Aggarwal2b703c62012-09-18 13:54:15 -07002125 final Folder search = new Folder(data);
Paul Westbrookc4845c52012-08-29 21:48:43 -07002126 updateFolder(search);
2127 mConvListContext = ConversationListContext.forSearchQuery(mAccount, mFolder,
2128 mActivity.getIntent()
2129 .getStringExtra(UIProvider.SearchQueryParameters.QUERY));
2130 showConversationList(mConvListContext);
2131 mActivity.invalidateOptionsMenu();
Vikram Aggarwalf0ef2302012-11-07 14:53:34 -08002132 mHaveSearchResults = search.totalCount > 0;
Paul Westbrookc4845c52012-08-29 21:48:43 -07002133 mActivity.getLoaderManager().destroyLoader(LOADER_SEARCH);
2134 } else {
2135 LogUtils.e(LOG_TAG, "Null or empty cursor returned by LOADER_SEARCH loader");
2136 }
Mindy Pereiraab486362012-03-21 18:18:53 -07002137 break;
Vikram Aggarwal7dedb952012-02-16 16:10:23 -08002138 }
2139 }
2140
mindyp6f54e1b2012-10-09 09:54:08 -07002141
Vikram Aggarwalc7694222012-04-23 13:37:01 -07002142 /**
2143 * Destructive actions on Conversations. This class should only be created by controllers, and
2144 * clients should only require {@link DestructiveAction}s, not specific implementations of the.
2145 * Only the controllers should know what kind of destructive actions are being created.
2146 */
Mindy Pereirade3e74a2012-07-24 09:43:10 -07002147 public class ConversationAction implements DestructiveAction {
Vikram Aggarwalacaa3c02012-04-24 12:45:27 -07002148 /**
2149 * The action to be performed. This is specified as the resource ID of the menu item
2150 * corresponding to this action: R.id.delete, R.id.report_spam, etc.
2151 */
Vikram Aggarwal7f602f72012-04-30 16:04:06 -07002152 private final int mAction;
Vikram Aggarwalbc67bb12012-04-30 14:05:35 -07002153 /** The action will act upon these conversations */
Paul Westbrook77eee622012-07-10 13:41:57 -07002154 private final Collection<Conversation> mTarget;
Vikram Aggarwal7f602f72012-04-30 16:04:06 -07002155 /** Whether this destructive action has already been performed */
2156 private boolean mCompleted;
Vikram Aggarwalc4113952012-05-11 14:14:56 -07002157 /** Whether this is an action on the currently selected set. */
2158 private final boolean mIsSelectedSet;
Vikram Aggarwale8a85322012-04-24 09:01:38 -07002159
Mindy Pereirafbe40192012-03-20 10:40:45 -07002160 /**
2161 * Create a listener object. action is one of four constants: R.id.y_button (archive),
2162 * R.id.delete , R.id.mute, and R.id.report_spam.
2163 * @param action
Vikram Aggarwalbc67bb12012-04-30 14:05:35 -07002164 * @param target Conversation that we want to apply the action to.
Vikram Aggarwalc4113952012-05-11 14:14:56 -07002165 * @param isBatch whether the conversations are in the currently selected batch set.
2166 */
2167 public ConversationAction(int action, Collection<Conversation> target, boolean isBatch) {
Mindy Pereirafbe40192012-03-20 10:40:45 -07002168 mAction = action;
Paul Westbrook77eee622012-07-10 13:41:57 -07002169 mTarget = ImmutableList.copyOf(target);
Vikram Aggarwalc4113952012-05-11 14:14:56 -07002170 mIsSelectedSet = isBatch;
Mindy Pereirafbe40192012-03-20 10:40:45 -07002171 }
2172
Vikram Aggarwalacaa3c02012-04-24 12:45:27 -07002173 /**
2174 * The action common to child classes. This performs the action specified in the constructor
2175 * on the conversations given here.
Vikram Aggarwalacaa3c02012-04-24 12:45:27 -07002176 */
Vikram Aggarwalbc67bb12012-04-30 14:05:35 -07002177 @Override
2178 public void performAction() {
Vikram Aggarwal7f602f72012-04-30 16:04:06 -07002179 if (isPerformed()) {
2180 return;
2181 }
Mindy Pereira3cb28b52012-05-24 15:26:39 -07002182 boolean undoEnabled = mAccount.supportsCapability(AccountCapabilities.UNDO);
Vikram Aggarwal7dd054e2012-05-21 14:43:10 -07002183
2184 // Are we destroying the currently shown conversation? Show the next one.
2185 if (LogUtils.isLoggable(LOG_TAG, LogUtils.DEBUG)){
Vikram Aggarwal8742a612012-08-13 10:22:50 -07002186 LogUtils.d(LOG_TAG, "ConversationAction.performAction():"
2187 + "\nmTarget=%s\nCurrent=%s",
Vikram Aggarwal7dd054e2012-05-21 14:43:10 -07002188 Conversation.toString(mTarget), mCurrentConversation);
2189 }
Vikram Aggarwal7dd054e2012-05-21 14:43:10 -07002190
Paul Westbrooke1221d22012-08-19 11:09:07 -07002191 if (mConversationListCursor == null) {
2192 LogUtils.e(LOG_TAG, "null ConversationCursor in ConversationAction.performAction():"
2193 + "\nmTarget=%s\nCurrent=%s",
2194 Conversation.toString(mTarget), mCurrentConversation);
2195 return;
2196 }
2197
Mindy Pereirafbe40192012-03-20 10:40:45 -07002198 switch (mAction) {
Mindy Pereira0692baf2012-03-23 17:34:31 -07002199 case R.id.archive:
Vikram Aggarwal7dd054e2012-05-21 14:43:10 -07002200 LogUtils.d(LOG_TAG, "Archiving");
Vikram Aggarwalbc67bb12012-04-30 14:05:35 -07002201 mConversationListCursor.archive(mContext, mTarget);
Mindy Pereirafbe40192012-03-20 10:40:45 -07002202 break;
2203 case R.id.delete:
Vikram Aggarwal7dd054e2012-05-21 14:43:10 -07002204 LogUtils.d(LOG_TAG, "Deleting");
Vikram Aggarwalbc67bb12012-04-30 14:05:35 -07002205 mConversationListCursor.delete(mContext, mTarget);
Mindy Pereira695d6962012-06-18 13:02:10 -07002206 if (mFolder.supportsCapability(FolderCapabilities.DELETE_ACTION_FINAL)) {
Marc Blank386243f2012-05-25 10:40:59 -07002207 undoEnabled = false;
2208 }
Mindy Pereirafbe40192012-03-20 10:40:45 -07002209 break;
2210 case R.id.mute:
Vikram Aggarwal7dd054e2012-05-21 14:43:10 -07002211 LogUtils.d(LOG_TAG, "Muting");
Vikram Aggarwalbc67bb12012-04-30 14:05:35 -07002212 if (mFolder.supportsCapability(FolderCapabilities.DESTRUCTIVE_MUTE)) {
Vikram Aggarwal7f602f72012-04-30 16:04:06 -07002213 for (Conversation c : mTarget) {
2214 c.localDeleteOnUpdate = true;
2215 }
Vikram Aggarwalbc67bb12012-04-30 14:05:35 -07002216 }
2217 mConversationListCursor.mute(mContext, mTarget);
Mindy Pereirafbe40192012-03-20 10:40:45 -07002218 break;
2219 case R.id.report_spam:
Vikram Aggarwal7dd054e2012-05-21 14:43:10 -07002220 LogUtils.d(LOG_TAG, "Reporting spam");
Vikram Aggarwalbc67bb12012-04-30 14:05:35 -07002221 mConversationListCursor.reportSpam(mContext, mTarget);
2222 break;
Paul Westbrook77eee622012-07-10 13:41:57 -07002223 case R.id.mark_not_spam:
2224 LogUtils.d(LOG_TAG, "Marking not spam");
2225 mConversationListCursor.reportNotSpam(mContext, mTarget);
2226 break;
Paul Westbrook76b20622012-07-12 11:45:43 -07002227 case R.id.report_phishing:
2228 LogUtils.d(LOG_TAG, "Reporting phishing");
2229 mConversationListCursor.reportPhishing(mContext, mTarget);
2230 break;
Vikram Aggarwalbc67bb12012-04-30 14:05:35 -07002231 case R.id.remove_star:
Vikram Aggarwal7dd054e2012-05-21 14:43:10 -07002232 LogUtils.d(LOG_TAG, "Removing star");
Vikram Aggarwalbc67bb12012-04-30 14:05:35 -07002233 // Star removal is destructive in the Starred folder.
2234 mConversationListCursor.updateBoolean(mContext, mTarget,
2235 ConversationColumns.STARRED, false);
2236 break;
2237 case R.id.mark_not_important:
Vikram Aggarwal7dd054e2012-05-21 14:43:10 -07002238 LogUtils.d(LOG_TAG, "Marking not-important");
Mindy Pereira445be212012-08-15 08:50:10 -07002239 // Marking not important is destructive in a mailbox
2240 // containing only important messages
Mindy Pereira0d03ef82012-08-15 09:05:48 -07002241 if (mFolder != null && mFolder.isImportantOnly()) {
Mindy Pereira445be212012-08-15 08:50:10 -07002242 for (Conversation conv : mTarget) {
2243 conv.localDeleteOnUpdate = true;
2244 }
2245 }
Vikram Aggarwalbc67bb12012-04-30 14:05:35 -07002246 mConversationListCursor.updateInt(mContext, mTarget,
2247 ConversationColumns.PRIORITY, UIProvider.ConversationPriority.LOW);
Mindy Pereirafbe40192012-03-20 10:40:45 -07002248 break;
Paul Westbrookef362542012-08-27 14:53:32 -07002249 case R.id.discard_drafts:
2250 LogUtils.d(LOG_TAG, "Discarding draft messages");
2251 // Discarding draft messages is destructive in a "draft" mailbox
2252 if (mFolder != null && mFolder.isDraft()) {
2253 for (Conversation conv : mTarget) {
2254 conv.localDeleteOnUpdate = true;
2255 }
2256 }
2257 mConversationListCursor.discardDrafts(mContext, mTarget);
2258 // We don't support undoing discarding drafts
2259 undoEnabled = false;
2260 break;
Vikram Aggarwal3d7ca9d2012-05-11 14:40:36 -07002261 }
2262 if (undoEnabled) {
mindypead50392012-08-23 11:03:53 -07002263 mHandler.postDelayed(new Runnable() {
2264 @Override
2265 public void run() {
2266 onUndoAvailable(new ToastBarOperation(mTarget.size(), mAction,
mindypa59283a2012-09-11 17:49:06 -07002267 ToastBarOperation.UNDO, mIsSelectedSet));
mindypead50392012-08-23 11:03:53 -07002268 }
2269 }, mShowUndoBarDelay);
Vikram Aggarwal3d7ca9d2012-05-11 14:40:36 -07002270 }
Vikram Aggarwalbc67bb12012-04-30 14:05:35 -07002271 refreshConversationList();
Vikram Aggarwalc4113952012-05-11 14:14:56 -07002272 if (mIsSelectedSet) {
Vikram Aggarwalc4113952012-05-11 14:14:56 -07002273 mSelectedSet.clear();
2274 }
Mindy Pereirafbe40192012-03-20 10:40:45 -07002275 }
Vikram Aggarwal7f602f72012-04-30 16:04:06 -07002276
2277 /**
2278 * Returns true if this action has been performed, false otherwise.
Andy Huang839ada22012-07-20 15:48:40 -07002279 *
Vikram Aggarwal7f602f72012-04-30 16:04:06 -07002280 */
2281 private synchronized boolean isPerformed() {
2282 if (mCompleted) {
2283 return true;
2284 }
2285 mCompleted = true;
2286 return false;
2287 }
Mindy Pereirafbe40192012-03-20 10:40:45 -07002288 }
Mindy Pereirae5f4dc02012-03-21 16:08:53 -07002289
Vikram Aggarwald503df42012-05-11 10:13:35 -07002290 // Called from the FolderSelectionDialog after a user is done selecting folders to assign the
2291 // conversations to.
Mindy Pereirae5f4dc02012-03-21 16:08:53 -07002292 @Override
Mindy Pereira8db7e402012-07-13 10:32:47 -07002293 public final void assignFolder(Collection<FolderOperation> folderOps,
2294 Collection<Conversation> target, boolean batch, boolean showUndo) {
2295 // Actions are destructive only when the current folder can be assigned
2296 // to (which is the same as being able to un-assign a conversation from the folder) and
2297 // when the list of folders contains the current folder.
2298 final boolean isDestructive = mFolder
2299 .supportsCapability(FolderCapabilities.CAN_ACCEPT_MOVED_MESSAGES)
2300 && FolderOperation.isDestructive(folderOps, mFolder);
Vikram Aggarwald503df42012-05-11 10:13:35 -07002301 LogUtils.d(LOG_TAG, "onFolderChangesCommit: isDestructive = %b", isDestructive);
2302 if (isDestructive) {
2303 for (final Conversation c : target) {
2304 c.localDeleteOnUpdate = true;
Mindy Pereira6778f462012-03-23 18:01:55 -07002305 }
Mindy Pereirae5f4dc02012-03-21 16:08:53 -07002306 }
mindypc84759c2012-08-29 09:51:53 -07002307 final DestructiveAction folderChange;
Vikram Aggarwald503df42012-05-11 10:13:35 -07002308 // Update the UI elements depending no their visibility and availability
Vikram Aggarwal3d7ca9d2012-05-11 14:40:36 -07002309 // TODO(viki): Consolidate this into a single method requestDelete.
Vikram Aggarwald503df42012-05-11 10:13:35 -07002310 if (isDestructive) {
mindypc84759c2012-08-29 09:51:53 -07002311 folderChange = getDeferredFolderChange(target, folderOps, isDestructive,
2312 batch, showUndo);
Vikram Aggarwala8e43182012-09-13 12:55:10 -07002313 delete(0, target, folderChange);
Vikram Aggarwal6902dcf2012-04-11 08:57:42 -07002314 } else {
mindypc84759c2012-08-29 09:51:53 -07002315 folderChange = getFolderChange(target, folderOps, isDestructive,
2316 batch, showUndo);
Vikram Aggarwald503df42012-05-11 10:13:35 -07002317 requestUpdate(target, folderChange);
Mindy Pereirae5f4dc02012-03-21 16:08:53 -07002318 }
2319 }
2320
Mindy Pereira967ede62012-03-22 09:29:09 -07002321 @Override
Vikram Aggarwal3d7ca9d2012-05-11 14:40:36 -07002322 public final void onRefreshRequired() {
mindyp5390fca2012-08-22 12:12:25 -07002323 if (isAnimating() || isDragging()) {
Mindy Pereira69e88dd2012-08-10 09:30:18 -07002324 LogUtils.d(LOG_TAG, "onRefreshRequired: delay until animating done");
2325 return;
2326 }
Marc Blankbf128eb2012-04-18 15:58:45 -07002327 // Refresh the query in the background
Paul Westbrookcff1aea2012-08-10 11:51:00 -07002328 if (mConversationListCursor.isRefreshRequired()) {
2329 mConversationListCursor.refresh();
2330 }
Marc Blankbf128eb2012-04-18 15:58:45 -07002331 }
2332
mindyp5390fca2012-08-22 12:12:25 -07002333 @Override
2334 public void startDragMode() {
2335 mIsDragHappening = true;
2336 }
2337
2338 @Override
2339 public void stopDragMode() {
2340 mIsDragHappening = false;
2341 if (mConversationListCursor.isRefreshReady()) {
2342 LogUtils.d(LOG_TAG, "Stopped animating: try sync");
2343 onRefreshReady();
2344 }
2345
2346 if (mConversationListCursor.isRefreshRequired()) {
2347 LogUtils.d(LOG_TAG, "Stopped animating: refresh");
2348 mConversationListCursor.refresh();
2349 }
2350 }
2351
2352 private boolean isDragging() {
2353 return mIsDragHappening;
2354 }
2355
mindyp6f54e1b2012-10-09 09:54:08 -07002356 @Override
2357 public boolean isAnimating() {
Mindy Pereira69e88dd2012-08-10 09:30:18 -07002358 boolean isAnimating = false;
2359 ConversationListFragment convListFragment = getConversationListFragment();
2360 if (convListFragment != null) {
2361 AnimatedAdapter adapter = convListFragment.getAnimatedAdapter();
2362 if (adapter != null) {
2363 isAnimating = adapter.isAnimating();
2364 }
2365 }
2366 return isAnimating;
2367 }
2368
Marc Blankbf128eb2012-04-18 15:58:45 -07002369 /**
2370 * Called when the {@link ConversationCursor} is changed or has new data in it.
2371 * <p>
2372 * {@inheritDoc}
2373 */
2374 @Override
Vikram Aggarwal3d7ca9d2012-05-11 14:40:36 -07002375 public final void onRefreshReady() {
mindyp5c1d8352012-11-05 10:12:44 -08002376 LogUtils.d(LOG_TAG, "Received refresh ready callback for folder %s",
2377 mFolder != null ? mFolder.id : "-1");
Andy Huangb2ef9c12012-12-18 12:58:41 -06002378
2379 if (mDestroyed) {
2380 LogUtils.i(LOG_TAG, "ignoring onRefreshReady on destroyed AAC");
2381 return;
2382 }
2383
Paul Westbrookcff1aea2012-08-10 11:51:00 -07002384 if (!isAnimating()) {
Marc Blankbf128eb2012-04-18 15:58:45 -07002385 // Swap cursors
2386 mConversationListCursor.sync();
Marc Blankbf128eb2012-04-18 15:58:45 -07002387 }
Paul Westbrook937c94f2012-08-16 13:01:18 -07002388 mTracker.onCursorUpdated();
Vikram Aggarwalf0ef2302012-11-07 14:53:34 -08002389 perhapsShowFirstSearchResult();
Marc Blankbf128eb2012-04-18 15:58:45 -07002390 }
2391
2392 @Override
Vikram Aggarwal3d7ca9d2012-05-11 14:40:36 -07002393 public final void onDataSetChanged() {
Paul Westbrook9f119c72012-04-24 16:10:59 -07002394 updateConversationListFragment();
Andy Huang632721e2012-04-11 16:57:26 -07002395 mConversationListObservable.notifyChanged();
Paul Westbrooka13b3742012-09-07 16:35:06 -07002396 mSelectedSet.validateAgainstCursor(mConversationListCursor);
Marc Blankbf128eb2012-04-18 15:58:45 -07002397 }
2398
Vikram Aggarwal3d7ca9d2012-05-11 14:40:36 -07002399 /**
2400 * If the Conversation List Fragment is visible, updates the fragment.
2401 */
2402 private final void updateConversationListFragment() {
Marc Blankbf128eb2012-04-18 15:58:45 -07002403 final ConversationListFragment convList = getConversationListFragment();
2404 if (convList != null) {
Vikram Aggarwal75daee52012-04-30 11:13:09 -07002405 refreshConversationList();
Paul Westbrook9f119c72012-04-24 16:10:59 -07002406 if (convList.isVisible()) {
Vikram Aggarwal69b5c302012-09-05 11:11:13 -07002407 informCursorVisiblity(true);
Paul Westbrook9f119c72012-04-24 16:10:59 -07002408 }
Marc Blankbf128eb2012-04-18 15:58:45 -07002409 }
2410 }
2411
2412 /**
2413 * This class handles throttled refresh of the conversation list
2414 */
2415 static class RefreshTimerTask extends TimerTask {
2416 final Handler mHandler;
2417 final AbstractActivityController mController;
2418
2419 RefreshTimerTask(AbstractActivityController controller, Handler handler) {
2420 mHandler = handler;
2421 mController = controller;
2422 }
2423
2424 @Override
2425 public void run() {
2426 mHandler.post(new Runnable() {
2427 @Override
2428 public void run() {
2429 LogUtils.d(LOG_TAG, "Delay done... calling onRefreshRequired");
2430 mController.onRefreshRequired();
2431 }});
2432 }
2433 }
2434
2435 /**
2436 * Cancel the refresh task, if it's running
2437 */
2438 private void cancelRefreshTask () {
2439 if (mConversationListRefreshTask != null) {
2440 mConversationListRefreshTask.cancel();
2441 mConversationListRefreshTask = null;
2442 }
2443 }
2444
mindyp6f54e1b2012-10-09 09:54:08 -07002445 private void loadRecentFolders(Cursor data) {
2446 mRecentFolderList.loadFromUiProvider(data);
2447 if (isAnimating()) {
2448 mRecentsDataUpdated = true;
2449 } else {
2450 mRecentFolderObservers.notifyChanged();
2451 }
2452 }
2453
Marc Blankbf128eb2012-04-18 15:58:45 -07002454 @Override
Paul Westbrookcff1aea2012-08-10 11:51:00 -07002455 public void onAnimationEnd(AnimatedAdapter animatedAdapter) {
Paul Westbrook026139c2012-09-19 22:35:37 -07002456 if (mConversationListCursor == null) {
2457 LogUtils.e(LOG_TAG, "null ConversationCursor in onAnimationEnd");
2458 return;
2459 }
Paul Westbrookcff1aea2012-08-10 11:51:00 -07002460 if (mConversationListCursor.isRefreshReady()) {
mindyp52544862012-08-20 12:05:36 -07002461 LogUtils.d(LOG_TAG, "Stopped animating: try sync");
Paul Westbrookcff1aea2012-08-10 11:51:00 -07002462 onRefreshReady();
Marc Blankbf128eb2012-04-18 15:58:45 -07002463 }
Marc Blankbf128eb2012-04-18 15:58:45 -07002464
Paul Westbrookcff1aea2012-08-10 11:51:00 -07002465 if (mConversationListCursor.isRefreshRequired()) {
mindyp52544862012-08-20 12:05:36 -07002466 LogUtils.d(LOG_TAG, "Stopped animating: refresh");
Paul Westbrookcff1aea2012-08-10 11:51:00 -07002467 mConversationListCursor.refresh();
2468 }
mindyp6f54e1b2012-10-09 09:54:08 -07002469 if (mRecentsDataUpdated) {
2470 mRecentsDataUpdated = false;
2471 mRecentFolderObservers.notifyChanged();
2472 }
2473 FolderListFragment frag = this.getFolderListFragment();
2474 if (frag != null) {
2475 frag.onAnimationEnd();
2476 }
Marc Blankbf128eb2012-04-18 15:58:45 -07002477 }
2478
2479 @Override
Mindy Pereira967ede62012-03-22 09:29:09 -07002480 public void onSetEmpty() {
Vikram Aggarwal6cadbfc2012-12-27 09:17:05 -08002481 // There are no selected conversations. Ensure that the listener and its associated actions
2482 // are blanked out.
2483 setListener(null, -1);
Mindy Pereira967ede62012-03-22 09:29:09 -07002484 }
2485
2486 @Override
2487 public void onSetPopulated(ConversationSelectionSet set) {
Vikram Aggarwal6902dcf2012-04-11 08:57:42 -07002488 final ConversationListFragment convList = getConversationListFragment();
2489 if (convList == null) {
2490 return;
2491 }
Vikram Aggarwal7c401b72012-08-13 16:43:47 -07002492 mCabActionMenu = new SelectedConversationsActionMenu(mActivity, set, mFolder,
Vikram Aggarwal531488e2012-05-29 16:36:52 -07002493 (SwipeableListView) convList.getListView());
Vikram Aggarwal6cadbfc2012-12-27 09:17:05 -08002494 // If there has been an orientation change, and we need to recreate the listener for the
2495 // confirm dialog fragment (delete/archive/...), then do it here.
2496 if (mDialogAction != -1) {
2497 makeDialogListener(mDialogAction);
2498 }
Vikram Aggarwale128fc22012-04-04 12:33:34 -07002499 enableCabMode();
Mindy Pereira967ede62012-03-22 09:29:09 -07002500 }
2501
Mindy Pereira967ede62012-03-22 09:29:09 -07002502 @Override
2503 public void onSetChanged(ConversationSelectionSet set) {
2504 // Do nothing. We don't care about changes to the set.
2505 }
2506
2507 @Override
2508 public ConversationSelectionSet getSelectedSet() {
2509 return mSelectedSet;
2510 }
2511
Vikram Aggarwale128fc22012-04-04 12:33:34 -07002512 /**
2513 * Disable the Contextual Action Bar (CAB). The selected set is not changed.
2514 */
2515 protected void disableCabMode() {
Mindy Pereira8937bf12012-07-23 14:05:02 -07002516 // Commit any previous destructive actions when entering/ exiting CAB mode.
mindypc6adce32012-08-22 18:46:42 -07002517 commitDestructiveActions(true);
Vikram Aggarwale128fc22012-04-04 12:33:34 -07002518 if (mCabActionMenu != null) {
2519 mCabActionMenu.deactivate();
2520 }
2521 }
2522
2523 /**
2524 * Re-enable the CAB menu if required. The selection set is not changed.
2525 */
2526 protected void enableCabMode() {
2527 if (mCabActionMenu != null) {
2528 mCabActionMenu.activate();
2529 }
2530 }
2531
Vikram Aggarwal4eb52712012-06-19 16:24:50 -07002532 /**
2533 * Unselect conversations and exit CAB mode.
2534 */
2535 protected final void exitCabMode() {
2536 mSelectedSet.clear();
2537 }
2538
Mindy Pereira967ede62012-03-22 09:29:09 -07002539 @Override
Mindy Pereirafd0c2972012-03-27 13:50:39 -07002540 public void startSearch() {
Vikram Aggarwal35f19d72012-04-24 13:24:48 -07002541 if (mAccount == null) {
2542 // We cannot search if there is no account. Drop the request to the floor.
2543 LogUtils.d(LOG_TAG, "AbstractActivityController.startSearch(): null account");
2544 return;
2545 }
Mindy Pereirafd0c2972012-03-27 13:50:39 -07002546 if (mAccount.supportsCapability(UIProvider.AccountCapabilities.LOCAL_SEARCH)
2547 | mAccount.supportsCapability(UIProvider.AccountCapabilities.SERVER_SEARCH)) {
Vikram Aggarwal70f130e2012-04-03 12:32:14 -07002548 onSearchRequested(mActionBarView.getQuery());
Mindy Pereirafd0c2972012-03-27 13:50:39 -07002549 } else {
2550 Toast.makeText(mActivity.getActivityContext(), mActivity.getActivityContext()
Mindy Pereiraa46c2992012-03-27 14:12:39 -07002551 .getString(R.string.search_unsupported), Toast.LENGTH_SHORT).show();
Mindy Pereirafd0c2972012-03-27 13:50:39 -07002552 }
2553 }
Mindy Pereiraacf60392012-04-06 09:11:00 -07002554
Vikram Aggarwal0dda5732012-04-06 11:20:16 -07002555 @Override
2556 public void exitSearchMode() {
2557 if (mViewMode.getMode() == ViewMode.SEARCH_RESULTS_LIST) {
2558 mActivity.finish();
2559 }
2560 }
2561
Mindy Pereiraacf60392012-04-06 09:11:00 -07002562 /**
2563 * Supports dragging conversations to a folder.
2564 */
2565 @Override
2566 public boolean supportsDrag(DragEvent event, Folder folder) {
2567 return (folder != null
2568 && event != null
2569 && event.getClipDescription() != null
2570 && folder.supportsCapability
2571 (UIProvider.FolderCapabilities.CAN_ACCEPT_MOVED_MESSAGES)
2572 && folder.supportsCapability
2573 (UIProvider.FolderCapabilities.CAN_HOLD_MAIL)
2574 && !mFolder.uri.equals(folder.uri));
2575 }
2576
2577 /**
Mindy Pereira6c2663d2012-07-20 15:37:29 -07002578 * Handles dropping conversations to a folder.
Mindy Pereiraacf60392012-04-06 09:11:00 -07002579 */
2580 @Override
2581 public void handleDrop(DragEvent event, final Folder folder) {
Mindy Pereiraacf60392012-04-06 09:11:00 -07002582 if (!supportsDrag(event, folder)) {
2583 return;
2584 }
mindypae7e6a02012-11-29 13:28:10 -08002585 if (folder.type == UIProvider.FolderType.STARRED) {
2586 // Moving a conversation to the starred folder adds the star and
2587 // removes the current label
2588 handleDropInStarred(folder);
2589 return;
2590 }
2591 if (mFolder.type == UIProvider.FolderType.STARRED) {
2592 handleDragFromStarred(folder);
2593 return;
2594 }
mindypa8492632012-09-24 09:27:54 -07002595 final ArrayList<FolderOperation> dragDropOperations = new ArrayList<FolderOperation>();
mindypae7e6a02012-11-29 13:28:10 -08002596 final Collection<Conversation> conversations = mSelectedSet.values();
mindypa8492632012-09-24 09:27:54 -07002597 // Add the drop target folder.
2598 dragDropOperations.add(new FolderOperation(folder, true));
2599 // Remove the current folder unless the user is viewing "all".
2600 // That operation should just add the new folder.
2601 boolean isDestructive = !mFolder.isViewAll()
2602 && mFolder.supportsCapability
2603 (UIProvider.FolderCapabilities.CAN_ACCEPT_MOVED_MESSAGES);
2604 if (isDestructive) {
2605 dragDropOperations.add(new FolderOperation(mFolder, false));
2606 }
Mindy Pereira8db7e402012-07-13 10:32:47 -07002607 // Drag and drop is destructive: we remove conversations from the
2608 // current folder.
mindypa8492632012-09-24 09:27:54 -07002609 final DestructiveAction action = getFolderChange(conversations, dragDropOperations,
2610 isDestructive, true, true);
2611 if (isDestructive) {
2612 delete(0, conversations, action);
2613 } else {
2614 action.performAction();
2615 }
Mindy Pereiraacf60392012-04-06 09:11:00 -07002616 }
Mindy Pereira0963ef82012-04-10 11:43:01 -07002617
mindypae7e6a02012-11-29 13:28:10 -08002618 private void handleDragFromStarred(Folder folder) {
2619 final Collection<Conversation> conversations = mSelectedSet.values();
2620 // The conversation list deletes and performs the action if it exists.
2621 final ConversationListFragment convListFragment = getConversationListFragment();
2622 // There should always be a convlistfragment, or the user could not have
2623 // dragged/ dropped conversations.
2624 if (convListFragment != null) {
2625 LogUtils.d(LOG_TAG, "AAC.requestDelete: ListFragment is handling delete.");
2626 ArrayList<ConversationOperation> ops = new ArrayList<ConversationOperation>();
mindypcb0b30e2012-11-30 10:16:35 -08002627 ArrayList<Uri> folderUris;
2628 ArrayList<Boolean> adds;
mindypae7e6a02012-11-29 13:28:10 -08002629 for (Conversation target : conversations) {
mindypcb0b30e2012-11-30 10:16:35 -08002630 folderUris = new ArrayList<Uri>();
2631 adds = new ArrayList<Boolean>();
2632 folderUris.add(folder.uri);
2633 adds.add(Boolean.TRUE);
Paul Westbrook26746eb2012-12-06 14:44:01 -08002634 final HashMap<Uri, Folder> targetFolders =
2635 Folder.hashMapForFolders(target.getRawFolders());
mindypae7e6a02012-11-29 13:28:10 -08002636 targetFolders.put(folder.uri, folder);
Paul Westbrook26746eb2012-12-06 14:44:01 -08002637 ops.add(mConversationListCursor.getConversationFolderOperation(target,
2638 folderUris, adds, targetFolders.values()));
mindypae7e6a02012-11-29 13:28:10 -08002639 }
2640 if (mConversationListCursor != null) {
2641 mConversationListCursor.updateBulkValues(mContext, ops);
2642 }
2643 refreshConversationList();
2644 mSelectedSet.clear();
2645 return;
2646 }
2647 }
2648
2649 private void handleDropInStarred(Folder folder) {
2650 final Collection<Conversation> conversations = mSelectedSet.values();
2651 // The conversation list deletes and performs the action if it exists.
2652 final ConversationListFragment convListFragment = getConversationListFragment();
2653 // There should always be a convlistfragment, or the user could not have
2654 // dragged/ dropped conversations.
2655 if (convListFragment != null) {
2656 LogUtils.d(LOG_TAG, "AAC.requestDelete: ListFragment is handling delete.");
2657 convListFragment.requestDelete(R.id.change_folder, conversations, mSelectedSet.views(),
mindyp5cc0ab22012-12-11 08:47:35 -08002658 new DroppedInStarredAction(conversations, mFolder, folder));
mindypae7e6a02012-11-29 13:28:10 -08002659 return;
2660 }
2661 }
2662
2663 // When dragging conversations to the starred folder, remove from the
2664 // original folder and add a star
2665 private class DroppedInStarredAction implements DestructiveAction {
2666 private Collection<Conversation> mConversations;
2667 private Folder mInitialFolder;
mindyp5cc0ab22012-12-11 08:47:35 -08002668 private Folder mStarred;
mindypae7e6a02012-11-29 13:28:10 -08002669
mindyp5cc0ab22012-12-11 08:47:35 -08002670 public DroppedInStarredAction(Collection<Conversation> conversations, Folder initialFolder,
2671 Folder starredFolder) {
mindypae7e6a02012-11-29 13:28:10 -08002672 mConversations = conversations;
mindyp5cc0ab22012-12-11 08:47:35 -08002673 mInitialFolder = initialFolder;
2674 mStarred = starredFolder;
mindypae7e6a02012-11-29 13:28:10 -08002675 }
2676
2677 @Override
2678 public void performAction() {
2679 ToastBarOperation undoOp = new ToastBarOperation(mConversations.size(),
2680 R.id.change_folder, ToastBarOperation.UNDO, true);
2681 onUndoAvailable(undoOp);
2682 ArrayList<ConversationOperation> ops = new ArrayList<ConversationOperation>();
2683 ContentValues values = new ContentValues();
mindypcb0b30e2012-11-30 10:16:35 -08002684 ArrayList<Uri> folderUris;
2685 ArrayList<Boolean> adds;
mindyp5cc0ab22012-12-11 08:47:35 -08002686 ConversationOperation operation;
mindypae7e6a02012-11-29 13:28:10 -08002687 for (Conversation target : mConversations) {
mindypcb0b30e2012-11-30 10:16:35 -08002688 folderUris = new ArrayList<Uri>();
2689 adds = new ArrayList<Boolean>();
mindyp5cc0ab22012-12-11 08:47:35 -08002690 folderUris.add(mStarred.uri);
2691 adds.add(Boolean.TRUE);
mindypcb0b30e2012-11-30 10:16:35 -08002692 folderUris.add(mInitialFolder.uri);
2693 adds.add(Boolean.FALSE);
mindyp5cc0ab22012-12-11 08:47:35 -08002694 final HashMap<Uri, Folder> targetFolders =
2695 Folder.hashMapForFolders(target.getRawFolders());
2696 targetFolders.put(mStarred.uri, mStarred);
mindypae7e6a02012-11-29 13:28:10 -08002697 targetFolders.remove(mInitialFolder.uri);
mindyp5cc0ab22012-12-11 08:47:35 -08002698 values.put(ConversationColumns.STARRED, true);
2699 operation = mConversationListCursor.getConversationFolderOperation(target,
2700 folderUris, adds, targetFolders.values(), values);
2701 ops.add(operation);
mindypae7e6a02012-11-29 13:28:10 -08002702 }
2703 if (mConversationListCursor != null) {
2704 mConversationListCursor.updateBulkValues(mContext, ops);
2705 }
2706 refreshConversationList();
2707 mSelectedSet.clear();
2708 }
2709 }
2710
Mindy Pereira0963ef82012-04-10 11:43:01 -07002711 @Override
Mindy Pereira0963ef82012-04-10 11:43:01 -07002712 public void onTouchEvent(MotionEvent event) {
2713 if (event.getAction() == MotionEvent.ACTION_DOWN) {
Andrew Sappersteinc2c9dc12012-07-02 18:17:32 -07002714 if (mToastBar != null && !mToastBar.isEventInToastBar(event)) {
Mark Weid243d452012-10-31 16:24:08 -07002715 hideOrRepositionToastBar(true);
Mindy Pereira0963ef82012-04-10 11:43:01 -07002716 }
2717 }
2718 }
Andy Huangb1c34dc2012-04-17 16:36:19 -07002719
Mark Weid243d452012-10-31 16:24:08 -07002720 protected abstract void hideOrRepositionToastBar(boolean animated);
2721
Andy Huang632721e2012-04-11 16:57:26 -07002722 @Override
2723 public void onConversationSeen(Conversation conv) {
2724 mPagerController.onConversationSeen(conv);
2725 }
2726
Andy Huang9d3fd922012-09-26 22:23:58 -07002727 @Override
2728 public boolean isInitialConversationLoading() {
2729 return mPagerController.isInitialConversationLoading();
2730 }
2731
Andy Huangb1c34dc2012-04-17 16:36:19 -07002732 private class ConversationListLoaderCallbacks implements
2733 LoaderManager.LoaderCallbacks<ConversationCursor> {
2734
2735 @Override
2736 public Loader<ConversationCursor> onCreateLoader(int id, Bundle args) {
2737 Loader<ConversationCursor> result = new ConversationCursorLoader((Activity) mActivity,
Paul Westbrook9a70e912012-08-17 15:53:20 -07002738 mAccount, mFolder.conversationListUri, mFolder.name);
Andy Huangb1c34dc2012-04-17 16:36:19 -07002739 return result;
2740 }
2741
2742 @Override
2743 public void onLoadFinished(Loader<ConversationCursor> loader, ConversationCursor data) {
Andy Huang632721e2012-04-11 16:57:26 -07002744 LogUtils.d(LOG_TAG, "IN AAC.ConversationCursor.onLoadFinished, data=%s loader=%s",
2745 data, loader);
Vikram Aggarwale8a85322012-04-24 09:01:38 -07002746 // Clear our all pending destructive actions before swapping the conversation cursor
2747 destroyPending(null);
Andy Huangb1c34dc2012-04-17 16:36:19 -07002748 mConversationListCursor = data;
Paul Westbrookbf232c32012-04-18 03:17:41 -07002749 mConversationListCursor.addListener(AbstractActivityController.this);
Andy Huangb1c34dc2012-04-17 16:36:19 -07002750
Paul Westbrook937c94f2012-08-16 13:01:18 -07002751 mTracker.onCursorUpdated();
Andy Huange3df1ad2012-04-24 17:15:23 -07002752 mConversationListObservable.notifyChanged();
Paul Westbrook9f119c72012-04-24 16:10:59 -07002753
Vikram Aggarwal17f373e2012-09-17 15:49:32 -07002754 final ConversationListFragment convList = getConversationListFragment();
2755 if (convList != null && convList.isVisible()) {
2756 // The conversation list is already listening to list changes and gets notified
2757 // in the mConversationListObservable.notifyChanged() line above. We only need to
2758 // check and inform the cursor of the change in visibility here.
2759 informCursorVisiblity(true);
Andy Huangb1c34dc2012-04-17 16:36:19 -07002760 }
Vikram Aggarwalf0ef2302012-11-07 14:53:34 -08002761 perhapsShowFirstSearchResult();
Andy Huangb1c34dc2012-04-17 16:36:19 -07002762 }
2763
2764 @Override
2765 public void onLoaderReset(Loader<ConversationCursor> loader) {
Paul Westbrook9a70e912012-08-17 15:53:20 -07002766 LogUtils.d(LOG_TAG, "IN AAC.ConversationCursor.onLoaderReset, data=%s loader=%s",
2767 mConversationListCursor, loader);
2768
2769 if (mConversationListCursor != null) {
2770 // Unregister the listener
2771 mConversationListCursor.removeListener(AbstractActivityController.this);
2772 mConversationListCursor = null;
2773
2774 // Inform anyone who is interested about the change
2775 mTracker.onCursorUpdated();
2776 mConversationListObservable.notifyChanged();
Andy Huangb1c34dc2012-04-17 16:36:19 -07002777 }
Andy Huangb1c34dc2012-04-17 16:36:19 -07002778 }
Paul Westbrookbf232c32012-04-18 03:17:41 -07002779 }
Andy Huangb1c34dc2012-04-17 16:36:19 -07002780
Vikram Aggarwale8a85322012-04-24 09:01:38 -07002781 /**
Vikram Aggarwalf0ef2302012-11-07 14:53:34 -08002782 * Updates controller state based on search results and shows first conversation if required.
2783 */
2784 private final void perhapsShowFirstSearchResult() {
mindypd27009a2012-11-27 11:54:18 -08002785 if (mCurrentConversation == null) {
2786 // Shown for search results in two-pane mode only.
2787 mHaveSearchResults = Intent.ACTION_SEARCH.equals(mActivity.getIntent().getAction())
2788 && mConversationListCursor.getCount() > 0;
2789 if (!shouldShowFirstConversation()) {
2790 return;
2791 }
2792 mConversationListCursor.moveToPosition(0);
2793 final Conversation conv = new Conversation(mConversationListCursor);
2794 conv.position = 0;
2795 onConversationSelected(conv, true /* checkSafeToModifyFragments */);
Vikram Aggarwalf0ef2302012-11-07 14:53:34 -08002796 }
Vikram Aggarwalf0ef2302012-11-07 14:53:34 -08002797 }
2798
2799 /**
Vikram Aggarwale8a85322012-04-24 09:01:38 -07002800 * Destroy the pending {@link DestructiveAction} till now and assign the given action as the
2801 * next destructive action..
2802 * @param nextAction the next destructive action to be performed. This can be null.
2803 */
2804 private final void destroyPending(DestructiveAction nextAction) {
2805 // If there is a pending action, perform that first.
2806 if (mPendingDestruction != null) {
2807 mPendingDestruction.performAction();
2808 }
2809 mPendingDestruction = nextAction;
2810 }
2811
2812 /**
2813 * Register a destructive action with the controller. This performs the previous destructive
Vikram Aggarwalacaa3c02012-04-24 12:45:27 -07002814 * 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 -07002815 * embellish this method any more.
Vikram Aggarwale8a85322012-04-24 09:01:38 -07002816 * @param action
2817 */
Vikram Aggarwal3d7ca9d2012-05-11 14:40:36 -07002818 private final void registerDestructiveAction(DestructiveAction action) {
Vikram Aggarwale8a85322012-04-24 09:01:38 -07002819 // TODO(viki): This is not a good idea. The best solution is for clients to request a
2820 // destructive action from the controller and for the controller to own the action. This is
2821 // a half-way solution while refactoring DestructiveAction.
2822 destroyPending(action);
2823 return;
2824 }
2825
Vikram Aggarwal531488e2012-05-29 16:36:52 -07002826 @Override
2827 public final DestructiveAction getBatchAction(int action) {
Vikram Aggarwalc4113952012-05-11 14:14:56 -07002828 final DestructiveAction da = new ConversationAction(action, mSelectedSet.values(), true);
Vikram Aggarwale8a85322012-04-24 09:01:38 -07002829 registerDestructiveAction(da);
2830 return da;
2831 }
Vikram Aggarwal41e6e712012-04-24 11:22:57 -07002832
Mindy Pereirade3e74a2012-07-24 09:43:10 -07002833 @Override
2834 public final DestructiveAction getDeferredBatchAction(int action) {
mindypf0656a12012-10-01 08:30:57 -07002835 return getDeferredAction(action, mSelectedSet.values(), true);
2836 }
2837
2838 /**
2839 * Get a destructive action for a menu action. This is a temporary method,
2840 * to control the profusion of {@link DestructiveAction} classes that are
2841 * created. Please do not copy this paradigm.
2842 * @param action the resource ID of the menu action: R.id.delete, for
2843 * example
2844 * @param target the conversations to act upon.
2845 * @return a {@link DestructiveAction} that performs the specified action.
2846 */
2847 @Override
2848 public DestructiveAction getDeferredAction(int action, Collection<Conversation> target,
2849 boolean batch) {
2850 final DestructiveAction da = new ConversationAction(action, target, batch);
Mindy Pereirade3e74a2012-07-24 09:43:10 -07002851 return da;
2852 }
2853
Vikram Aggarwalbc67bb12012-04-30 14:05:35 -07002854 /**
2855 * Class to change the folders that are assigned to a set of conversations. This is destructive
2856 * because the user can remove the current folder from the conversation, in which case it has
2857 * to be animated away from the current folder.
2858 */
Vikram Aggarwal41e6e712012-04-24 11:22:57 -07002859 private class FolderDestruction implements DestructiveAction {
Paul Westbrook77eee622012-07-10 13:41:57 -07002860 private final Collection<Conversation> mTarget;
Mindy Pereira8db7e402012-07-13 10:32:47 -07002861 private final ArrayList<FolderOperation> mFolderOps = new ArrayList<FolderOperation>();
Vikram Aggarwal41e6e712012-04-24 11:22:57 -07002862 private final boolean mIsDestructive;
Vikram Aggarwal7f602f72012-04-30 16:04:06 -07002863 /** Whether this destructive action has already been performed */
2864 private boolean mCompleted;
Mindy Pereiraf3a45562012-05-24 16:30:19 -07002865 private boolean mIsSelectedSet;
Mindy Pereira06642fa2012-07-12 16:23:27 -07002866 private boolean mShowUndo;
Mindy Pereira01f30502012-08-14 10:30:51 -07002867 private int mAction;
Vikram Aggarwal41e6e712012-04-24 11:22:57 -07002868
2869 /**
2870 * Create a new folder destruction object to act on the given conversations.
2871 * @param target
2872 */
Vikram Aggarwal7f602f72012-04-30 16:04:06 -07002873 private FolderDestruction(final Collection<Conversation> target,
Mindy Pereira8db7e402012-07-13 10:32:47 -07002874 final Collection<FolderOperation> folders, boolean isDestructive, boolean isBatch,
Mindy Pereira01f30502012-08-14 10:30:51 -07002875 boolean showUndo, int action) {
Paul Westbrook77eee622012-07-10 13:41:57 -07002876 mTarget = ImmutableList.copyOf(target);
Mindy Pereira8db7e402012-07-13 10:32:47 -07002877 mFolderOps.addAll(folders);
Vikram Aggarwal41e6e712012-04-24 11:22:57 -07002878 mIsDestructive = isDestructive;
Mindy Pereiraf3a45562012-05-24 16:30:19 -07002879 mIsSelectedSet = isBatch;
Mindy Pereira06642fa2012-07-12 16:23:27 -07002880 mShowUndo = showUndo;
Mindy Pereira01f30502012-08-14 10:30:51 -07002881 mAction = action;
Vikram Aggarwal41e6e712012-04-24 11:22:57 -07002882 }
2883
Vikram Aggarwal41e6e712012-04-24 11:22:57 -07002884 @Override
2885 public void performAction() {
Vikram Aggarwal7f602f72012-04-30 16:04:06 -07002886 if (isPerformed()) {
2887 return;
2888 }
Mindy Pereira06642fa2012-07-12 16:23:27 -07002889 if (mIsDestructive && mShowUndo) {
mindypcb0b30e2012-11-30 10:16:35 -08002890 ToastBarOperation undoOp = new ToastBarOperation(mTarget.size(), mAction,
2891 ToastBarOperation.UNDO, mIsSelectedSet);
Vikram Aggarwal41e6e712012-04-24 11:22:57 -07002892 onUndoAvailable(undoOp);
2893 }
Mindy Pereira8db7e402012-07-13 10:32:47 -07002894 // For each conversation, for each operation, add/ remove the
2895 // appropriate folders.
mindypcb0b30e2012-11-30 10:16:35 -08002896 ArrayList<ConversationOperation> ops = new ArrayList<ConversationOperation>();
2897 ArrayList<Uri> folderUris;
2898 ArrayList<Boolean> adds;
Mindy Pereira8db7e402012-07-13 10:32:47 -07002899 for (Conversation target : mTarget) {
mindypcb0b30e2012-11-30 10:16:35 -08002900 HashMap<Uri, Folder> targetFolders = Folder.hashMapForFolders(target
2901 .getRawFolders());
2902 folderUris = new ArrayList<Uri>();
mindypcb0b30e2012-11-30 10:16:35 -08002903 adds = new ArrayList<Boolean>();
Mindy Pereira01f30502012-08-14 10:30:51 -07002904 if (mIsDestructive) {
2905 target.localDeleteOnUpdate = true;
2906 }
Mindy Pereira8db7e402012-07-13 10:32:47 -07002907 for (FolderOperation op : mFolderOps) {
mindypcb0b30e2012-11-30 10:16:35 -08002908 folderUris.add(op.mFolder.uri);
2909 adds.add(op.mAdd ? Boolean.TRUE : Boolean.FALSE);
Mindy Pereira8db7e402012-07-13 10:32:47 -07002910 if (op.mAdd) {
2911 targetFolders.put(op.mFolder.uri, op.mFolder);
2912 } else {
2913 targetFolders.remove(op.mFolder.uri);
2914 }
2915 }
Paul Westbrook26746eb2012-12-06 14:44:01 -08002916 ops.add(mConversationListCursor.getConversationFolderOperation(target,
2917 folderUris, adds, targetFolders.values()));
mindyp389f0b22012-08-29 11:12:54 -07002918 }
2919 if (mConversationListCursor != null) {
mindypcb0b30e2012-11-30 10:16:35 -08002920 mConversationListCursor.updateBulkValues(mContext, ops);
Mindy Pereira8db7e402012-07-13 10:32:47 -07002921 }
Vikram Aggarwal7f602f72012-04-30 16:04:06 -07002922 refreshConversationList();
Mindy Pereiraf3a45562012-05-24 16:30:19 -07002923 if (mIsSelectedSet) {
2924 mSelectedSet.clear();
2925 }
Vikram Aggarwal7f602f72012-04-30 16:04:06 -07002926 }
Mindy Pereira8db7e402012-07-13 10:32:47 -07002927
Vikram Aggarwal7f602f72012-04-30 16:04:06 -07002928 /**
2929 * Returns true if this action has been performed, false otherwise.
Andy Huang839ada22012-07-20 15:48:40 -07002930 *
Vikram Aggarwal7f602f72012-04-30 16:04:06 -07002931 */
2932 private synchronized boolean isPerformed() {
2933 if (mCompleted) {
2934 return true;
Vikram Aggarwal41e6e712012-04-24 11:22:57 -07002935 }
Vikram Aggarwal7f602f72012-04-30 16:04:06 -07002936 mCompleted = true;
2937 return false;
Vikram Aggarwal41e6e712012-04-24 11:22:57 -07002938 }
2939 }
Vikram Aggarwal7f602f72012-04-30 16:04:06 -07002940
mindypc84759c2012-08-29 09:51:53 -07002941 public final DestructiveAction getFolderChange(Collection<Conversation> target,
2942 Collection<FolderOperation> folders, boolean isDestructive, boolean isBatch,
2943 boolean showUndo) {
2944 final DestructiveAction da = getDeferredFolderChange(target, folders, isDestructive,
2945 isBatch, showUndo);
2946 registerDestructiveAction(da);
2947 return da;
2948 }
2949
2950 public final DestructiveAction getDeferredFolderChange(Collection<Conversation> target,
Mindy Pereira8db7e402012-07-13 10:32:47 -07002951 Collection<FolderOperation> folders, boolean isDestructive, boolean isBatch,
2952 boolean showUndo) {
Mindy Pereira06642fa2012-07-12 16:23:27 -07002953 final DestructiveAction da = new FolderDestruction(target, folders, isDestructive, isBatch,
Mindy Pereira01f30502012-08-14 10:30:51 -07002954 showUndo, R.id.change_folder);
Mindy Pereira01f30502012-08-14 10:30:51 -07002955 return da;
2956 }
2957
2958 @Override
2959 public final DestructiveAction getDeferredRemoveFolder(Collection<Conversation> target,
2960 Folder toRemove, boolean isDestructive, boolean isBatch,
2961 boolean showUndo) {
2962 Collection<FolderOperation> folderOps = new ArrayList<FolderOperation>();
2963 folderOps.add(new FolderOperation(toRemove, false));
2964 return new FolderDestruction(target, folderOps, isDestructive, isBatch,
2965 showUndo, R.id.remove_folder);
2966 }
2967
Vikram Aggarwal4f4782b2012-05-30 08:39:09 -07002968 @Override
2969 public final void refreshConversationList() {
Vikram Aggarwal75daee52012-04-30 11:13:09 -07002970 final ConversationListFragment convList = getConversationListFragment();
2971 if (convList == null) {
2972 return;
2973 }
2974 convList.requestListRefresh();
2975 }
Andrew Sappersteinc2c9dc12012-07-02 18:17:32 -07002976
2977 protected final ActionClickedListener getUndoClickedListener(
2978 final AnimatedAdapter listAdapter) {
2979 return new ActionClickedListener() {
2980 @Override
2981 public void onActionClicked() {
2982 if (mAccount.undoUri != null) {
2983 // NOTE: We might want undo to return the messages affected, in which case
2984 // the resulting cursor might be interesting...
2985 // TODO: Use UIProvider.SEQUENCE_QUERY_PARAMETER to indicate the set of
2986 // commands to undo
2987 if (mConversationListCursor != null) {
2988 mConversationListCursor.undo(
2989 mActivity.getActivityContext(), mAccount.undoUri);
2990 }
2991 if (listAdapter != null) {
2992 listAdapter.setUndo(true);
2993 }
2994 }
2995 }
2996 };
2997 }
2998
Vikram Aggarwal41b9e8f2012-09-25 10:15:04 -07002999 /**
3000 * Shows an error toast in the bottom when a folder was not fetched successfully.
3001 * @param folder the folder which could not be fetched.
3002 * @param replaceVisibleToast if true, this should replace any currently visible toast.
3003 */
Andrew Sapperstein9d7519d2012-07-16 14:03:53 -07003004 protected final void showErrorToast(final Folder folder, boolean replaceVisibleToast) {
Andrew Sappersteinc2c9dc12012-07-02 18:17:32 -07003005 mToastBar.setConversationMode(false);
Andrew Sapperstein00179f12012-08-09 15:15:40 -07003006
Vikram Aggarwal41b9e8f2012-09-25 10:15:04 -07003007 final ActionClickedListener listener;
3008 final int actionTextResourceId;
Andrew Sapperstein00179f12012-08-09 15:15:40 -07003009 final int lastSyncResult = folder.lastSyncResult;
Vikram Aggarwal41b9e8f2012-09-25 10:15:04 -07003010 switch (lastSyncResult & 0x0f) {
Andrew Sapperstein00179f12012-08-09 15:15:40 -07003011 case UIProvider.LastSyncResult.CONNECTION_ERROR:
Vikram Aggarwal41b9e8f2012-09-25 10:15:04 -07003012 // The sync request that caused this failure.
3013 final int syncRequest = lastSyncResult >> 4;
3014 // Show: User explicitly pressed the refresh button and there is no connection
3015 // Show: The first time the user enters the app and there is no connection
3016 // TODO(viki): Implement this.
3017 // Reference: http://b/7202801
3018 final boolean showToast = (syncRequest & UIProvider.SyncStatus.USER_REFRESH) != 0;
3019 // Don't show: Already in the app; user switches to a synced label
3020 // Don't show: In a live label and a background sync fails
3021 final boolean avoidToast = !showToast && (folder.syncWindow > 0
3022 || (syncRequest & UIProvider.SyncStatus.BACKGROUND_SYNC) != 0);
3023 if (avoidToast) {
3024 return;
3025 }
Andrew Sapperstein00179f12012-08-09 15:15:40 -07003026 listener = getRetryClickedListener(folder);
3027 actionTextResourceId = R.string.retry;
3028 break;
3029 case UIProvider.LastSyncResult.AUTH_ERROR:
3030 listener = getSignInClickedListener();
3031 actionTextResourceId = R.string.signin;
3032 break;
3033 case UIProvider.LastSyncResult.SECURITY_ERROR:
3034 return; // Currently we do nothing for security errors.
3035 case UIProvider.LastSyncResult.STORAGE_ERROR:
3036 listener = getStorageErrorClickedListener();
3037 actionTextResourceId = R.string.info;
3038 break;
3039 case UIProvider.LastSyncResult.INTERNAL_ERROR:
3040 listener = getInternalErrorClickedListener();
3041 actionTextResourceId = R.string.report;
3042 break;
3043 default:
3044 return;
3045 }
Vikram Aggarwal41b9e8f2012-09-25 10:15:04 -07003046 mToastBar.show(listener,
Andrew Sapperstein5d420962012-07-12 16:43:10 -07003047 R.drawable.ic_alert_white,
Vikram Aggarwal41b9e8f2012-09-25 10:15:04 -07003048 Utils.getSyncStatusText(mActivity.getActivityContext(), lastSyncResult),
Andrew Sappersteinc2c9dc12012-07-02 18:17:32 -07003049 false, /* showActionIcon */
Andrew Sapperstein00179f12012-08-09 15:15:40 -07003050 actionTextResourceId,
Andrew Sapperstein9d7519d2012-07-16 14:03:53 -07003051 replaceVisibleToast,
mindypa59283a2012-09-11 17:49:06 -07003052 new ToastBarOperation(1, 0, ToastBarOperation.ERROR, false));
Andrew Sappersteinc2c9dc12012-07-02 18:17:32 -07003053 }
3054
Andrew Sapperstein00179f12012-08-09 15:15:40 -07003055 private ActionClickedListener getRetryClickedListener(final Folder folder) {
Andrew Sappersteinc2c9dc12012-07-02 18:17:32 -07003056 return new ActionClickedListener() {
3057 @Override
3058 public void onActionClicked() {
3059 final Uri uri = folder.refreshUri;
3060
3061 if (uri != null) {
Paul Westbrook4969e0c2012-08-20 14:38:39 -07003062 startAsyncRefreshTask(uri);
Andrew Sappersteinc2c9dc12012-07-02 18:17:32 -07003063 }
3064 }
3065 };
3066 }
Andrew Sapperstein00179f12012-08-09 15:15:40 -07003067
3068 private ActionClickedListener getSignInClickedListener() {
3069 return new ActionClickedListener() {
3070 @Override
3071 public void onActionClicked() {
Paul Westbrook122f7c22012-08-20 17:50:31 -07003072 promptUserForAuthentication(mAccount);
Andrew Sapperstein00179f12012-08-09 15:15:40 -07003073 }
3074 };
3075 }
3076
3077 private ActionClickedListener getStorageErrorClickedListener() {
3078 return new ActionClickedListener() {
3079 @Override
3080 public void onActionClicked() {
Paul Westbrook4969e0c2012-08-20 14:38:39 -07003081 showStorageErrorDialog();
Andrew Sapperstein00179f12012-08-09 15:15:40 -07003082 }
3083 };
3084 }
3085
Paul Westbrook4969e0c2012-08-20 14:38:39 -07003086 private void showStorageErrorDialog() {
3087 DialogFragment fragment = (DialogFragment)
3088 mFragmentManager.findFragmentByTag(SYNC_ERROR_DIALOG_FRAGMENT_TAG);
3089 if (fragment == null) {
3090 fragment = SyncErrorDialogFragment.newInstance();
3091 }
3092 fragment.show(mFragmentManager, SYNC_ERROR_DIALOG_FRAGMENT_TAG);
3093 }
3094
Andrew Sapperstein00179f12012-08-09 15:15:40 -07003095 private ActionClickedListener getInternalErrorClickedListener() {
3096 return new ActionClickedListener() {
3097 @Override
3098 public void onActionClicked() {
Paul Westbrook17beb0b2012-08-20 13:34:37 -07003099 Utils.sendFeedback(
3100 mActivity.getActivityContext(), mAccount, true /* reportingProblem */);
Andrew Sapperstein00179f12012-08-09 15:15:40 -07003101 }
3102 };
3103 }
Paul Westbrook4969e0c2012-08-20 14:38:39 -07003104
3105 @Override
3106 public void onFooterViewErrorActionClick(Folder folder, int errorStatus) {
3107 Uri uri = null;
3108 switch (errorStatus) {
3109 case UIProvider.LastSyncResult.CONNECTION_ERROR:
3110 if (folder != null && folder.refreshUri != null) {
3111 uri = folder.refreshUri;
3112 }
3113 break;
3114 case UIProvider.LastSyncResult.AUTH_ERROR:
Paul Westbrook122f7c22012-08-20 17:50:31 -07003115 promptUserForAuthentication(mAccount);
Paul Westbrook4969e0c2012-08-20 14:38:39 -07003116 return;
3117 case UIProvider.LastSyncResult.SECURITY_ERROR:
3118 return; // Currently we do nothing for security errors.
3119 case UIProvider.LastSyncResult.STORAGE_ERROR:
3120 showStorageErrorDialog();
3121 return;
3122 case UIProvider.LastSyncResult.INTERNAL_ERROR:
3123 Utils.sendFeedback(
3124 mActivity.getActivityContext(), mAccount, true /* reportingProblem */);
3125 return;
3126 default:
3127 return;
3128 }
3129
3130 if (uri != null) {
3131 startAsyncRefreshTask(uri);
3132 }
3133 }
3134
3135 @Override
3136 public void onFooterViewLoadMoreClick(Folder folder) {
3137 if (folder != null && folder.loadMoreUri != null) {
3138 startAsyncRefreshTask(folder.loadMoreUri);
3139 }
3140 }
3141
3142 private void startAsyncRefreshTask(Uri uri) {
3143 if (mFolderSyncTask != null) {
3144 mFolderSyncTask.cancel(true);
3145 }
3146 mFolderSyncTask = new AsyncRefreshTask(mActivity.getActivityContext(), uri);
3147 mFolderSyncTask.execute();
3148 }
Paul Westbrook122f7c22012-08-20 17:50:31 -07003149
3150 private void promptUserForAuthentication(Account account) {
Paul Westbrook429399b2012-08-24 11:19:17 -07003151 if (account != null && !Utils.isEmpty(account.reauthenticationIntentUri)) {
Paul Westbrook122f7c22012-08-20 17:50:31 -07003152 final Intent authenticationIntent =
3153 new Intent(Intent.ACTION_VIEW, account.reauthenticationIntentUri);
3154 mActivity.startActivityForResult(authenticationIntent, REAUTHENTICATE_REQUEST_CODE);
3155 }
3156 }
mindypca87de42012-09-28 15:02:39 -07003157
3158 @Override
3159 public void onAccessibilityStateChanged() {
3160 // Clear the cache of objects.
3161 ConversationItemViewModel.onAccessibilityUpdated();
3162 // Re-render the list if it exists.
3163 ConversationListFragment frag = getConversationListFragment();
3164 if (frag != null) {
3165 AnimatedAdapter adapter = frag.getAnimatedAdapter();
3166 if (adapter != null) {
3167 adapter.notifyDataSetInvalidated();
3168 }
3169 }
3170 }
Vikram Aggarwal6cadbfc2012-12-27 09:17:05 -08003171
3172 @Override
3173 public void makeDialogListener (final int action) {
3174 final DestructiveAction destructiveAction = getDeferredBatchAction(action);
3175 final Collection<Conversation> conversations = mSelectedSet.values();
3176 final Collection<ConversationItemView> views = mSelectedSet.views();
3177 mDialogAction = action;
3178 mDialogListener = new AlertDialog.OnClickListener() {
3179 @Override
3180 public void onClick(DialogInterface dialog, int which) {
3181 delete(action, conversations, views, destructiveAction);
3182 // Afterwards, let's remove references to the listener and the action.
3183 setListener(null, -1);
3184 }
3185 };
3186 }
3187
3188 @Override
3189 public AlertDialog.OnClickListener getListener() {
3190 return mDialogListener;
3191 }
3192
3193 /**
3194 * Sets the listener for the positive action on a confirmation dialog. Since only a single
3195 * confirmation dialog can be shown, this overwrites the previous listener. It is safe to
3196 * unset the listener; in which case action should be set to -1.
3197 * @param listener
3198 * @param action
3199 */
3200 private void setListener(AlertDialog.OnClickListener listener, final int action){
3201 mDialogListener = listener;
3202 mDialogAction = action;
3203 }
Vikram Aggarwal4a5c5302012-01-12 15:07:13 -08003204}