blob: 198de263f2e9c4dd0230b31b10b721e1af1e8e5b [file] [log] [blame]
Vikram Aggarwal4a5c5302012-01-12 15:07:13 -08001/*******************************************************************************
2 * Copyright (C) 2012 Google Inc.
3 * Licensed to The Android Open Source Project.
4 *
5 * Licensed under the Apache License, Version 2.0 (the "License");
6 * you may not use this file except in compliance with the License.
7 * You may obtain a copy of the License at
8 *
9 * http://www.apache.org/licenses/LICENSE-2.0
10 *
11 * Unless required by applicable law or agreed to in writing, software
12 * distributed under the License is distributed on an "AS IS" BASIS,
13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 * See the License for the specific language governing permissions and
15 * limitations under the License.
16 *******************************************************************************/
17
18package com.android.mail.ui;
19
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -080020import android.app.ActionBar;
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -080021import android.app.ActionBar.LayoutParams;
Vikram Aggarwala55b36c2012-01-13 11:45:02 -080022import android.app.Activity;
Vikram Aggarwal54452ae2012-03-13 15:29:00 -070023import android.app.AlertDialog;
Vikram Aggarwala55b36c2012-01-13 11:45:02 -080024import android.app.Dialog;
Andrew Sapperstein00179f12012-08-09 15:15:40 -070025import android.app.DialogFragment;
Vikram Aggarwal6902dcf2012-04-11 08:57:42 -070026import android.app.Fragment;
Paul Westbrook2d50bcd2012-04-10 11:53:47 -070027import android.app.FragmentManager;
Andy Huangf9a73482012-03-13 15:54:02 -070028import android.app.LoaderManager;
Vikram Aggarwale620a7a2012-03-28 13:16:14 -070029import android.app.SearchManager;
Andy Huang839ada22012-07-20 15:48:40 -070030import android.content.ContentProviderOperation;
Vikram Aggarwal80aeac52012-02-07 15:27:20 -080031import android.content.ContentResolver;
Mindy Pereira6c2663d2012-07-20 15:37:29 -070032import android.content.ContentValues;
Vikram Aggarwala55b36c2012-01-13 11:45:02 -080033import android.content.Context;
Vikram Aggarwal7dedb952012-02-16 16:10:23 -080034import android.content.CursorLoader;
Vikram Aggarwal54452ae2012-03-13 15:29:00 -070035import android.content.DialogInterface;
Vikram Aggarwala55b36c2012-01-13 11:45:02 -080036import android.content.Intent;
Vikram Aggarwal7dedb952012-02-16 16:10:23 -080037import android.content.Loader;
Vikram Aggarwal80aeac52012-02-07 15:27:20 -080038import android.database.Cursor;
Andy Huang632721e2012-04-11 16:57:26 -070039import android.database.DataSetObservable;
40import android.database.DataSetObserver;
Paul Westbrook23b74b92012-02-29 11:36:12 -080041import android.net.Uri;
Vikram Aggarwal27d89ad2012-06-12 13:38:40 -070042import android.os.AsyncTask;
Vikram Aggarwala55b36c2012-01-13 11:45:02 -080043import android.os.Bundle;
Mindy Pereira21ab4902012-03-19 18:48:03 -070044import android.os.Handler;
Vikram Aggarwale620a7a2012-03-28 13:16:14 -070045import android.provider.SearchRecentSuggestions;
Mindy Pereiraacf60392012-04-06 09:11:00 -070046import android.view.DragEvent;
Vikram Aggarwala55b36c2012-01-13 11:45:02 -080047import android.view.KeyEvent;
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -080048import android.view.LayoutInflater;
Vikram Aggarwala55b36c2012-01-13 11:45:02 -080049import android.view.Menu;
Mindy Pereira28d5f722012-02-15 12:32:40 -080050import android.view.MenuInflater;
Vikram Aggarwala55b36c2012-01-13 11:45:02 -080051import android.view.MenuItem;
52import android.view.MotionEvent;
Mindy Pereirad33674992012-06-25 16:26:30 -070053import android.view.View;
Mindy Pereirafd0c2972012-03-27 13:50:39 -070054import android.widget.Toast;
Vikram Aggarwala55b36c2012-01-13 11:45:02 -080055
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -080056import com.android.mail.ConversationListContext;
Andy Huangf9a73482012-03-13 15:54:02 -070057import com.android.mail.R;
Mindy Pereira967ede62012-03-22 09:29:09 -070058import com.android.mail.browse.ConversationCursor;
Paul Westbrookbf232c32012-04-18 03:17:41 -070059import com.android.mail.browse.ConversationPagerController;
Andy Huang839ada22012-07-20 15:48:40 -070060import com.android.mail.browse.MessageCursor.ConversationMessage;
Marc Blank7c9f6ac2012-04-02 13:27:19 -070061import com.android.mail.browse.SelectedConversationsActionMenu;
Andy Huang991f4532012-08-14 13:32:55 -070062import com.android.mail.browse.SyncErrorDialogFragment;
Mindy Pereira9b875682012-02-15 18:10:54 -080063import com.android.mail.compose.ComposeActivity;
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -080064import com.android.mail.providers.Account;
Mindy Pereira9b875682012-02-15 18:10:54 -080065import com.android.mail.providers.Conversation;
Andy Huang839ada22012-07-20 15:48:40 -070066import com.android.mail.providers.ConversationInfo;
Vikram Aggarwal4a5c5302012-01-12 15:07:13 -080067import com.android.mail.providers.Folder;
Vikram Aggarwal7a5d95a2012-07-27 16:24:54 -070068import com.android.mail.providers.FolderWatcher;
Paul Westbrookc2074c42012-03-22 15:26:58 -070069import com.android.mail.providers.MailAppProvider;
Mindy Pereiradac00542012-03-01 10:50:33 -080070import com.android.mail.providers.Settings;
Vikram Aggarwale620a7a2012-03-28 13:16:14 -070071import com.android.mail.providers.SuggestionsProvider;
Vikram Aggarwal80aeac52012-02-07 15:27:20 -080072import com.android.mail.providers.UIProvider;
Mindy Pereira3cb28b52012-05-24 15:26:39 -070073import com.android.mail.providers.UIProvider.AccountCapabilities;
Paul Westbrook2388c5d2012-03-25 12:29:11 -070074import com.android.mail.providers.UIProvider.AccountCursorExtraKeys;
Mindy Pereirac9d59182012-03-22 16:06:46 -070075import com.android.mail.providers.UIProvider.ConversationColumns;
Vikram Aggarwal54452ae2012-03-13 15:29:00 -070076import com.android.mail.providers.UIProvider.FolderCapabilities;
Andrew Sappersteinc2c9dc12012-07-02 18:17:32 -070077import com.android.mail.ui.ActionableToastBar.ActionClickedListener;
Andy Huang839ada22012-07-20 15:48:40 -070078import com.android.mail.utils.ContentProviderTask;
Paul Westbrookb334c902012-06-25 11:42:46 -070079import com.android.mail.utils.LogTag;
Mindy Pereira5cb0c3e2012-02-22 15:22:47 -080080import com.android.mail.utils.LogUtils;
Vikram Aggarwalfa131a22012-02-02 13:56:22 -080081import com.android.mail.utils.Utils;
Paul Westbrookca08fc12012-07-31 12:01:15 -070082import com.google.common.base.Objects;
Paul Westbrook77eee622012-07-10 13:41:57 -070083import com.google.common.collect.ImmutableList;
Mindy Pereiraacf60392012-04-06 09:11:00 -070084import com.google.common.collect.Lists;
Marc Blank167faa82012-03-21 13:11:53 -070085import com.google.common.collect.Sets;
Vikram Aggarwal4a5c5302012-01-12 15:07:13 -080086
Marc Blank167faa82012-03-21 13:11:53 -070087import java.util.ArrayList;
Mindy Pereirafbe40192012-03-20 10:40:45 -070088import java.util.Collection;
Andy Huang839ada22012-07-20 15:48:40 -070089import java.util.Collections;
Mindy Pereira8db7e402012-07-13 10:32:47 -070090import java.util.HashMap;
Paul Westbrook23b74b92012-02-29 11:36:12 -080091import java.util.Set;
Marc Blankbf128eb2012-04-18 15:58:45 -070092import java.util.TimerTask;
Paul Westbrook23b74b92012-02-29 11:36:12 -080093
94
Vikram Aggarwal4a5c5302012-01-12 15:07:13 -080095/**
Mindy Pereira161f50d2012-02-28 15:47:19 -080096 * This is an abstract implementation of the Activity Controller. This class
97 * knows how to respond to menu items, state changes, layout changes, etc. It
98 * weaves together the views and listeners, dispatching actions to the
99 * respective underlying classes.
Vikram Aggarwala55b36c2012-01-13 11:45:02 -0800100 * <p>
Mindy Pereira161f50d2012-02-28 15:47:19 -0800101 * Even though this class is abstract, it should provide default implementations
102 * for most, if not all the methods in the ActivityController interface. This
103 * makes the task of the subclasses easier: OnePaneActivityController and
104 * TwoPaneActivityController can be concise when the common functionality is in
105 * AbstractActivityController.
106 * </p>
107 * <p>
108 * In the Gmail codebase, this was called BaseActivityController
109 * </p>
Vikram Aggarwal4a5c5302012-01-12 15:07:13 -0800110 */
Vikram Aggarwal531488e2012-05-29 16:36:52 -0700111public abstract class AbstractActivityController implements ActivityController {
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -0800112 // Keys for serialization of various information in Bundles.
Vikram Aggarwalcabd3f22012-04-19 10:14:41 -0700113 /** Tag for {@link #mAccount} */
Vikram Aggarwal6c511582012-02-27 10:59:47 -0800114 private static final String SAVED_ACCOUNT = "saved-account";
Vikram Aggarwalcabd3f22012-04-19 10:14:41 -0700115 /** Tag for {@link #mFolder} */
Mindy Pereira5e478d22012-03-26 18:04:58 -0700116 private static final String SAVED_FOLDER = "saved-folder";
Vikram Aggarwalcabd3f22012-04-19 10:14:41 -0700117 /** Tag for {@link #mCurrentConversation} */
Mindy Pereira26f23fc2012-03-27 10:26:04 -0700118 private static final String SAVED_CONVERSATION = "saved-conversation";
Vikram Aggarwalcabd3f22012-04-19 10:14:41 -0700119 /** Tag for {@link #mSelectedSet} */
120 private static final String SAVED_SELECTED_SET = "saved-selected-set";
Mindy Pereirad33674992012-06-25 16:26:30 -0700121 private static final String SAVED_TOAST_BAR_OP = "saved-toast-bar-op";
Mindy Pereira148b1912012-07-17 13:39:56 -0700122 protected static final String SAVED_HIERARCHICAL_FOLDER = "saved-hierarchical-folder";
Vikram Aggarwalf3341402012-08-07 10:09:38 -0700123 /** Tag for {@link ConversationListContext#searchQuery} */
124 private static final String SAVED_QUERY = "saved-query";
Mindy Pereira5cb0c3e2012-02-22 15:22:47 -0800125
Vikram Aggarwal6902dcf2012-04-11 08:57:42 -0700126 /** Tag used when loading a wait fragment */
127 protected static final String TAG_WAIT = "wait-fragment";
128 /** Tag used when loading a conversation list fragment. */
Paul Westbrookbf232c32012-04-18 03:17:41 -0700129 public static final String TAG_CONVERSATION_LIST = "tag-conversation-list";
Vikram Aggarwal6902dcf2012-04-11 08:57:42 -0700130 /** Tag used when loading a folder list fragment. */
131 protected static final String TAG_FOLDER_LIST = "tag-folder-list";
132
Vikram Aggarwald7a12cd2012-02-03 09:36:20 -0800133 protected Account mAccount;
Mindy Pereira12a4d802012-06-22 09:24:43 -0700134 protected Folder mFolder;
Andy Huang6681e542012-06-14 14:36:45 -0700135 protected MailActionBarView mActionBarView;
Vikram Aggarwal7c401b72012-08-13 16:43:47 -0700136 protected final ControllableActivity mActivity;
Vikram Aggarwala55b36c2012-01-13 11:45:02 -0800137 protected final Context mContext;
Vikram Aggarwal6902dcf2012-04-11 08:57:42 -0700138 private final FragmentManager mFragmentManager;
Vikram Aggarwalec5cbf72012-03-08 15:10:35 -0800139 protected final RecentFolderList mRecentFolderList;
Vikram Aggarwal80aeac52012-02-07 15:27:20 -0800140 protected ConversationListContext mConvListContext;
Mindy Pereira9b875682012-02-15 18:10:54 -0800141 protected Conversation mCurrentConversation;
Vikram Aggarwald7a12cd2012-02-03 09:36:20 -0800142
Paul Westbrook6ead20d2012-03-19 14:48:14 -0700143 /** A {@link android.content.BroadcastReceiver} that suppresses new e-mail notifications. */
144 private SuppressNotificationReceiver mNewEmailReceiver = null;
145
Mindy Pereirafbe40192012-03-20 10:40:45 -0700146 protected Handler mHandler = new Handler();
Mindy Pereirafa995b42012-07-25 12:06:13 -0700147
Vikram Aggarwalfa131a22012-02-02 13:56:22 -0800148 /**
Mindy Pereira161f50d2012-02-28 15:47:19 -0800149 * The current mode of the application. All changes in mode are initiated by
150 * the activity controller. View mode changes are propagated to classes that
151 * attach themselves as listeners of view mode changes.
Vikram Aggarwalfa131a22012-02-02 13:56:22 -0800152 */
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -0800153 protected final ViewMode mViewMode;
Vikram Aggarwal80aeac52012-02-07 15:27:20 -0800154 protected ContentResolver mResolver;
Vikram Aggarwal7dedb952012-02-16 16:10:23 -0800155 protected boolean isLoaderInitialized = false;
Mindy Pereirab7b33e02012-02-21 15:32:19 -0800156 private AsyncRefreshTask mAsyncRefreshTask;
Mindy Pereira5cb0c3e2012-02-22 15:22:47 -0800157
Andy Huang4e0158f2012-08-07 21:06:01 -0700158 private boolean mDestroyed;
159
Andy Huang1ee96b22012-08-24 20:19:53 -0700160 /**
161 * Are we in a point in the Activity/Fragment lifecycle where it's safe to execute fragment
162 * transactions? (including back stack manipulation)
163 * <p>
164 * Per docs in {@link FragmentManager#beginTransaction()}, this flag starts out true, switches
165 * to false after {@link Activity#onSaveInstanceState}, and becomes true again in both onStart
166 * and onResume.
167 */
168 private boolean mSafeToModifyFragments = true;
169
Paul Westbrook23b74b92012-02-29 11:36:12 -0800170 private final Set<Uri> mCurrentAccountUris = Sets.newHashSet();
Mindy Pereira967ede62012-03-22 09:29:09 -0700171 protected ConversationCursor mConversationListCursor;
Andy Huang632721e2012-04-11 16:57:26 -0700172 private final DataSetObservable mConversationListObservable = new DataSetObservable() {
173 @Override
174 public void registerObserver(DataSetObserver observer) {
175 final int count = mObservers.size();
176 super.registerObserver(observer);
Vikram Aggarwal7c401b72012-08-13 16:43:47 -0700177 LogUtils.d(LOG_TAG, "IN AAC.register(List)Observer: %s before=%d after=%d", observer,
Andy Huang632721e2012-04-11 16:57:26 -0700178 count, mObservers.size());
179 }
180 @Override
181 public void unregisterObserver(DataSetObserver observer) {
182 final int count = mObservers.size();
183 super.unregisterObserver(observer);
Vikram Aggarwal7c401b72012-08-13 16:43:47 -0700184 LogUtils.d(LOG_TAG, "IN AAC.unregister(List)Observer: %s before=%d after=%d", observer,
Andy Huang632721e2012-04-11 16:57:26 -0700185 count, mObservers.size());
186 }
187 };
Marc Blankbf128eb2012-04-18 15:58:45 -0700188
Marc Blankbf128eb2012-04-18 15:58:45 -0700189 private RefreshTimerTask mConversationListRefreshTask;
Marc Blanke1d1b072012-04-13 17:29:16 -0700190
Vikram Aggarwal7c401b72012-08-13 16:43:47 -0700191 /** Listeners that are interested in changes to the current account. */
192 private final DataSetObservable mAccountObservers = new DataSetObservable() {
193 @Override
194 public void registerObserver(DataSetObserver observer) {
195 final int count = mObservers.size();
196 super.registerObserver(observer);
197 LogUtils.d(LOG_TAG, "IN AAC.register(Account)Observer: %s before=%d after=%d",
198 observer, count, mObservers.size());
199 }
200 @Override
201 public void unregisterObserver(DataSetObserver observer) {
202 final int count = mObservers.size();
203 super.unregisterObserver(observer);
204 LogUtils.d(LOG_TAG, "IN AAC.unregister(Account)Observer: %s before=%d after=%d",
205 observer, count, mObservers.size());
206 }
207 };
Vikram Aggarwal7d816002012-04-17 17:06:41 -0700208
Mindy Pereira967ede62012-03-22 09:29:09 -0700209 /**
210 * Selected conversations, if any.
211 */
Andy Huang4556a442012-03-30 16:42:05 -0700212 private final ConversationSelectionSet mSelectedSet = new ConversationSelectionSet();
Mindy Pereiradac00542012-03-01 10:50:33 -0800213
Paul Westbrookc7a070f2012-04-12 01:46:41 -0700214 private final int mFolderItemUpdateDelayMs;
215
Vikram Aggarwal135fd022012-04-11 14:44:15 -0700216 /** Keeps track of selected and unselected conversations */
Paul Westbrook937c94f2012-08-16 13:01:18 -0700217 final protected ConversationPositionTracker mTracker;
Vikram Aggarwalaf1ee0c2012-04-12 17:13:13 -0700218
Vikram Aggarwale128fc22012-04-04 12:33:34 -0700219 /**
220 * Action menu associated with the selected set.
221 */
222 SelectedConversationsActionMenu mCabActionMenu;
Andrew Sappersteinc2c9dc12012-07-02 18:17:32 -0700223 protected ActionableToastBar mToastBar;
Andy Huang632721e2012-04-11 16:57:26 -0700224 protected ConversationPagerController mPagerController;
Mindy Pereiraab486362012-03-21 18:18:53 -0700225
Andy Huangb1c34dc2012-04-17 16:36:19 -0700226 // this is split out from the general loader dispatcher because its loader doesn't return a
227 // basic Cursor
228 private final ConversationListLoaderCallbacks mListCursorCallbacks =
229 new ConversationListLoaderCallbacks();
230
Andy Huang090db1e2012-07-25 13:25:28 -0700231 private final DataSetObservable mFolderObservable = new DataSetObservable();
232
Paul Westbrookb334c902012-06-25 11:42:46 -0700233 protected static final String LOG_TAG = LogTag.getLogTag();
Vikram Aggarwalfa4b47e2012-03-09 13:02:46 -0800234 /** Constants used to differentiate between the types of loaders. */
235 private static final int LOADER_ACCOUNT_CURSOR = 0;
Vikram Aggarwalfa4b47e2012-03-09 13:02:46 -0800236 private static final int LOADER_FOLDER_CURSOR = 2;
237 private static final int LOADER_RECENT_FOLDERS = 3;
Mindy Pereira967ede62012-03-22 09:29:09 -0700238 private static final int LOADER_CONVERSATION_LIST = 4;
Mindy Pereiraab486362012-03-21 18:18:53 -0700239 private static final int LOADER_ACCOUNT_INBOX = 5;
240 private static final int LOADER_SEARCH = 6;
Paul Westbrook2d50bcd2012-04-10 11:53:47 -0700241 private static final int LOADER_ACCOUNT_UPDATE_CURSOR = 7;
Vikram Aggarwal7a5d95a2012-07-27 16:24:54 -0700242 /**
243 * Guaranteed to be the last loader ID used by the activity. Loaders are owned by Activity or
244 * fragments, and within an activity, loader IDs need to be unique. A hack to ensure that the
245 * {@link FolderWatcher} can create its folder loaders without clashing with the IDs of those
246 * of the {@link AbstractActivityController}. Currently, the {@link FolderWatcher} is the only
247 * other class that uses this activity's LoaderManager. If another class needs activity-level
248 * loaders, consider consolidating the loaders in a central location: a UI-less fragment
249 * perhaps.
250 */
251 public static final int LAST_LOADER_ID = 100;
Vikram Aggarwala55b36c2012-01-13 11:45:02 -0800252
Paul Westbrook2388c5d2012-03-25 12:29:11 -0700253 private static final int ADD_ACCOUNT_REQUEST_CODE = 1;
Paul Westbrook122f7c22012-08-20 17:50:31 -0700254 private static final int REAUTHENTICATE_REQUEST_CODE = 2;
Paul Westbrook2388c5d2012-03-25 12:29:11 -0700255
Vikram Aggarwale8a85322012-04-24 09:01:38 -0700256 /** The pending destructive action to be carried out before swapping the conversation cursor.*/
257 private DestructiveAction mPendingDestruction;
Andrew Sappersteinc2c9dc12012-07-02 18:17:32 -0700258 protected AsyncRefreshTask mFolderSyncTask;
Mindy Pereira49e5dbe2012-07-12 11:47:54 -0700259 // Task for setting any share intents for the account to enabled.
260 // This gets cancelled if the user kills the app before it finishes, and
261 // will just run the next time the user opens the app.
262 private AsyncTask<String, Void, Void> mEnableShareIntents;
Mindy Pereirac975e842012-07-16 09:15:00 -0700263 private Folder mFolderListFolder;
mindyp5390fca2012-08-22 12:12:25 -0700264 private boolean mIsDragHappening;
mindypead50392012-08-23 11:03:53 -0700265 private int mShowUndoBarDelay;
Andrew Sapperstein00179f12012-08-09 15:15:40 -0700266 public static final String SYNC_ERROR_DIALOG_FRAGMENT_TAG = "SyncErrorDialogFragment";
Vikram Aggarwale8a85322012-04-24 09:01:38 -0700267
Vikram Aggarwala55b36c2012-01-13 11:45:02 -0800268 public AbstractActivityController(MailActivity activity, ViewMode viewMode) {
269 mActivity = activity;
Vikram Aggarwal6902dcf2012-04-11 08:57:42 -0700270 mFragmentManager = mActivity.getFragmentManager();
Vikram Aggarwala55b36c2012-01-13 11:45:02 -0800271 mViewMode = viewMode;
272 mContext = activity.getApplicationContext();
Vikram Aggarwal025eba82012-05-08 10:45:30 -0700273 mRecentFolderList = new RecentFolderList(mContext);
Paul Westbrook937c94f2012-08-16 13:01:18 -0700274 mTracker = new ConversationPositionTracker(this);
Mindy Pereira967ede62012-03-22 09:29:09 -0700275 // Allow the fragment to observe changes to its own selection set. No other object is
276 // aware of the selected set.
277 mSelectedSet.addObserver(this);
Paul Westbrookc7a070f2012-04-12 01:46:41 -0700278
279 mFolderItemUpdateDelayMs =
280 mContext.getResources().getInteger(R.integer.folder_item_refresh_delay_ms);
mindypead50392012-08-23 11:03:53 -0700281 mShowUndoBarDelay =
282 mContext.getResources().getInteger(R.integer.show_undo_bar_delay_ms);
Vikram Aggarwala55b36c2012-01-13 11:45:02 -0800283 }
Vikram Aggarwal4a5c5302012-01-12 15:07:13 -0800284
285 @Override
Vikram Aggarwale9a81032012-02-22 13:15:35 -0800286 public Account getCurrentAccount() {
287 return mAccount;
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -0800288 }
289
290 @Override
291 public ConversationListContext getCurrentListContext() {
Vikram Aggarwale9a81032012-02-22 13:15:35 -0800292 return mConvListContext;
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -0800293 }
294
295 @Override
Vikram Aggarwal4a5c5302012-01-12 15:07:13 -0800296 public String getHelpContext() {
Paul Westbrook30745b62012-08-19 14:10:32 -0700297 final int mode = mViewMode.getMode();
298 final int helpContextResId;
299 switch (mode) {
300 case ViewMode.WAITING_FOR_ACCOUNT_INITIALIZATION:
301 helpContextResId = R.string.wait_help_context;
302 break;
303 default:
304 helpContextResId = R.string.main_help_context;
305 }
306 return mContext.getString(helpContextResId);
Vikram Aggarwal4a5c5302012-01-12 15:07:13 -0800307 }
308
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -0800309 @Override
Vikram Aggarwalef7c9922012-04-23 12:35:04 -0700310 public final ConversationCursor getConversationListCursor() {
Mindy Pereira967ede62012-03-22 09:29:09 -0700311 return mConversationListCursor;
312 }
313
Vikram Aggarwal6902dcf2012-04-11 08:57:42 -0700314 /**
Vikram Aggarwal34a65012012-04-17 12:39:06 -0700315 * Check if the fragment is attached to an activity and has a root view.
316 * @param in
317 * @return true if the fragment is valid, false otherwise
318 */
319 private static final boolean isValidFragment(Fragment in) {
320 if (in == null || in.getActivity() == null || in.getView() == null) {
321 return false;
322 }
323 return true;
324 }
325
326 /**
Vikram Aggarwal6902dcf2012-04-11 08:57:42 -0700327 * Get the conversation list fragment for this activity. If the conversation list fragment
328 * is not attached, this method returns null
Andy Huang839ada22012-07-20 15:48:40 -0700329 *
Vikram Aggarwal6902dcf2012-04-11 08:57:42 -0700330 */
331 protected ConversationListFragment getConversationListFragment() {
332 final Fragment fragment = mFragmentManager.findFragmentByTag(TAG_CONVERSATION_LIST);
Vikram Aggarwal34a65012012-04-17 12:39:06 -0700333 if (isValidFragment(fragment)) {
334 return (ConversationListFragment) fragment;
Andy Huang9585d782012-04-16 19:45:04 -0700335 }
Vikram Aggarwal34a65012012-04-17 12:39:06 -0700336 return null;
Vikram Aggarwal6902dcf2012-04-11 08:57:42 -0700337 }
338
339 /**
Vikram Aggarwal6902dcf2012-04-11 08:57:42 -0700340 * Returns the folder list fragment attached with this activity. If no such fragment is attached
341 * this method returns null.
Andy Huang839ada22012-07-20 15:48:40 -0700342 *
Vikram Aggarwal6902dcf2012-04-11 08:57:42 -0700343 */
344 protected FolderListFragment getFolderListFragment() {
345 final Fragment fragment = mFragmentManager.findFragmentByTag(TAG_FOLDER_LIST);
Vikram Aggarwal34a65012012-04-17 12:39:06 -0700346 if (isValidFragment(fragment)) {
347 return (FolderListFragment) fragment;
Andy Huang9585d782012-04-16 19:45:04 -0700348 }
Vikram Aggarwal34a65012012-04-17 12:39:06 -0700349 return null;
Vikram Aggarwal6902dcf2012-04-11 08:57:42 -0700350 }
351
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -0800352 /**
Mindy Pereira161f50d2012-02-28 15:47:19 -0800353 * Initialize the action bar. This is not visible to OnePaneController and
354 * TwoPaneController so they cannot override this behavior.
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -0800355 */
Vikram Aggarwalabd24d82012-04-26 13:23:14 -0700356 private void initializeActionBar() {
357 final ActionBar actionBar = mActivity.getActionBar();
Andy Huang5895f7b2012-06-01 17:07:20 -0700358 if (actionBar == null) {
359 return;
Vikram Aggarwalabd24d82012-04-26 13:23:14 -0700360 }
Andy Huang5895f7b2012-06-01 17:07:20 -0700361
362 // be sure to inherit from the ActionBar theme when inflating
363 final LayoutInflater inflater = LayoutInflater.from(actionBar.getThemedContext());
Mindy Pereira82faec72012-06-14 17:21:50 -0700364 final boolean isSearch = mActivity.getIntent() != null
365 && Intent.ACTION_SEARCH.equals(mActivity.getIntent().getAction());
366 mActionBarView = (MailActionBarView) inflater.inflate(
367 isSearch ? R.layout.search_actionbar_view : R.layout.actionbar_view, null);
Andy Huang5895f7b2012-06-01 17:07:20 -0700368 mActionBarView.initialize(mActivity, this, mViewMode, actionBar, mRecentFolderList);
Vikram Aggarwalabd24d82012-04-26 13:23:14 -0700369 }
370
371 /**
372 * Attach the action bar to the activity.
373 */
374 private void attachActionBar() {
375 final ActionBar actionBar = mActivity.getActionBar();
376 if (actionBar != null && mActionBarView != null) {
Vikram Aggarwalec5cbf72012-03-08 15:10:35 -0800377 actionBar.setCustomView(mActionBarView, new ActionBar.LayoutParams(
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -0800378 LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT));
379 actionBar.setDisplayOptions(ActionBar.DISPLAY_SHOW_CUSTOM,
Vikram Aggarwal2a25d0c2012-02-21 16:43:10 -0800380 ActionBar.DISPLAY_SHOW_CUSTOM | ActionBar.DISPLAY_SHOW_TITLE);
Vikram Aggarwalabd24d82012-04-26 13:23:14 -0700381 mActionBarView.attach();
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -0800382 }
Vikram Aggarwalabd24d82012-04-26 13:23:14 -0700383 mViewMode.addListener(mActionBarView);
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -0800384 }
385
386 /**
Mindy Pereira161f50d2012-02-28 15:47:19 -0800387 * Returns whether the conversation list fragment is visible or not.
388 * Different layouts will have their own notion on the visibility of
389 * fragments, so this method needs to be overriden.
390 *
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -0800391 */
392 protected abstract boolean isConversationListVisible();
393
Vikram Aggarwaleb7d3292012-04-20 17:07:20 -0700394 /**
395 * Switch the current account to the one provided as an argument to the method.
Vikram Aggarwal66bc6ed2012-07-31 09:59:55 -0700396 * @param account new account
397 * @param shouldReloadInbox whether the default inbox should be reloaded.
Vikram Aggarwaleb7d3292012-04-20 17:07:20 -0700398 */
Vikram Aggarwal66bc6ed2012-07-31 09:59:55 -0700399 private void switchAccount(Account account, boolean shouldReloadInbox){
Vikram Aggarwaleb7d3292012-04-20 17:07:20 -0700400 // Current account is different from the new account, restart loaders and show
401 // the account Inbox.
402 mAccount = account;
Vikram Aggarwaledc137c2012-04-24 13:40:58 -0700403 LogUtils.d(LOG_TAG, "AbstractActivityController.switchAccount(): mAccount = %s",
404 mAccount.uri);
Vikram Aggarwaleb7d3292012-04-20 17:07:20 -0700405 cancelRefreshTask();
Vikram Aggarwal2e0a2362012-08-22 09:05:49 -0700406 // If we are currently listening to changes in conversation, the account switch is going to
407 // make us do a lot of un-necessary work. Let's stop listening to changes.
408 if (mPagerController != null) {
409 mPagerController.stopListening();
410 }
Vikram Aggarwal60069912012-07-24 14:26:09 -0700411 updateSettings();
Vikram Aggarwal66bc6ed2012-07-31 09:59:55 -0700412 if (shouldReloadInbox) {
413 loadAccountInbox();
414 }
Vikram Aggarwaleb7d3292012-04-20 17:07:20 -0700415 restartOptionalLoader(LOADER_RECENT_FOLDERS);
416 mActivity.invalidateOptionsMenu();
417 disableNotificationsOnAccountChange(mAccount);
418 restartOptionalLoader(LOADER_ACCOUNT_UPDATE_CURSOR);
419 MailAppProvider.getInstance().setLastViewedAccount(mAccount.uri.toString());
420 }
421
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -0800422 @Override
Mindy Pereira28e0c342012-02-17 15:05:13 -0800423 public void onAccountChanged(Account account) {
Vikram Aggarwal60069912012-07-24 14:26:09 -0700424 LogUtils.d(LOG_TAG, "onAccountChanged (%s) called.", account);
425 // Is the account or account settings different from the existing account?
Vikram Aggarwal66bc6ed2012-07-31 09:59:55 -0700426 final boolean firstLoad = mAccount == null;
427 final boolean accountChanged = firstLoad || !account.uri.equals(mAccount.uri);
428 final boolean settingsChanged = firstLoad || !account.settings.equals(mAccount.settings);
429 if (accountChanged || settingsChanged) {
Mindy Pereirafa20c1a2012-07-23 13:00:02 -0700430 if (account != null) {
Mindy Pereirafa995b42012-07-25 12:06:13 -0700431 final String accountName = account.name;
432 mHandler.post(new Runnable() {
433 @Override
434 public void run() {
435 MailActivity.setForegroundNdef(MailActivity.getMailtoNdef(accountName));
436 }
437 });
Mindy Pereirafa20c1a2012-07-23 13:00:02 -0700438 }
mindypc6adce32012-08-22 18:46:42 -0700439 if (accountChanged) {
440 commitDestructiveActions(false);
441 }
Vikram Aggarwal66bc6ed2012-07-31 09:59:55 -0700442 switchAccount(account, accountChanged);
Mindy Pereira28d5f722012-02-15 12:32:40 -0800443 }
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -0800444 }
445
Vikram Aggarwale6340bc2012-03-26 15:57:09 -0700446 /**
Vikram Aggarwaleb7d3292012-04-20 17:07:20 -0700447 * Changes the settings for the current account. The new settings are provided as a parameter.
448 * @param settings
449 */
Vikram Aggarwal60069912012-07-24 14:26:09 -0700450 public void updateSettings() {
Vikram Aggarwal7c401b72012-08-13 16:43:47 -0700451 mAccountObservers.notifyChanged();
Mindy Pereira12a676a2012-03-23 13:00:22 -0700452 resetActionBarIcon();
Vikram Aggarwaleb7d3292012-04-20 17:07:20 -0700453 mActivity.invalidateOptionsMenu();
454 // If the user was viewing the default Inbox here, and the new setting contains a different
Vikram Aggarwal60069912012-07-24 14:26:09 -0700455 // default Inbox, we don't want to load a different folder here. So do not change the
456 // current folder.
Mindy Pereiradac00542012-03-01 10:50:33 -0800457 }
458
Vikram Aggarwal7c401b72012-08-13 16:43:47 -0700459 /**
460 * Adds a listener interested in change in the current account. If a class is storing a
461 * reference to the current account, it should listen on changes, so it can receive updates to
462 * settings. Must happen in the UI thread.
463 */
Mindy Pereiraefe3d252012-03-01 14:20:44 -0800464 @Override
Vikram Aggarwal7c401b72012-08-13 16:43:47 -0700465 public void registerAccountObserver(DataSetObserver obs) {
466 mAccountObservers.registerObserver(obs);
Mindy Pereiraefe3d252012-03-01 14:20:44 -0800467 }
468
Vikram Aggarwal7d816002012-04-17 17:06:41 -0700469 /**
Vikram Aggarwal7c401b72012-08-13 16:43:47 -0700470 * Removes a listener from receiving current account changes.
Vikram Aggarwal7d816002012-04-17 17:06:41 -0700471 * Must happen in the UI thread.
472 */
Vikram Aggarwal7c401b72012-08-13 16:43:47 -0700473 @Override
474 public void unregisterAccountObserver(DataSetObserver obs) {
475 mAccountObservers.unregisterObserver(obs);
Vikram Aggarwal7d816002012-04-17 17:06:41 -0700476 }
477
Vikram Aggarwal7c401b72012-08-13 16:43:47 -0700478 @Override
479 public Account getAccount() {
480 return mAccount;
Vikram Aggarwal7d816002012-04-17 17:06:41 -0700481 }
482
Mindy Pereirae0828392012-03-08 10:38:40 -0800483 private void fetchSearchFolder(Intent intent) {
Mindy Pereiraab486362012-03-21 18:18:53 -0700484 Bundle args = new Bundle();
485 args.putString(ConversationListContext.EXTRA_SEARCH_QUERY, intent
Mindy Pereirae0828392012-03-08 10:38:40 -0800486 .getStringExtra(ConversationListContext.EXTRA_SEARCH_QUERY));
Mindy Pereiraab486362012-03-21 18:18:53 -0700487 mActivity.getLoaderManager().restartLoader(LOADER_SEARCH, args, this);
Mindy Pereirae0828392012-03-08 10:38:40 -0800488 }
489
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -0800490 @Override
Mindy Pereira28e0c342012-02-17 15:05:13 -0800491 public void onFolderChanged(Folder folder) {
Vikram Aggarwalf3341402012-08-07 10:09:38 -0700492 changeFolder(folder, null);
493 }
494
495 /**
Vikram Aggarwal203dc002012-08-23 13:59:04 -0700496 * Sets the folder state without changing view mode and without creating a list fragment, if
497 * possible.
498 * @param folder
499 */
500 private void setListContext(Folder folder, String query) {
501 updateFolder(folder);
502 if (query != null) {
503 mConvListContext = ConversationListContext.forSearchQuery(mAccount, mFolder, query);
504 } else {
505 mConvListContext = ConversationListContext.forFolder(mAccount, mFolder);
506 }
Vikram Aggarwal203dc002012-08-23 13:59:04 -0700507 cancelRefreshTask();
508 }
509
510 /**
Vikram Aggarwalf3341402012-08-07 10:09:38 -0700511 * Changes the folder to the value provided here. This causes the view mode to change.
512 * @param folder the folder to change to
513 * @param query if non-null, this represents the search string that the folder represents.
514 */
515 private void changeFolder(Folder folder, String query) {
Vikram Aggarwal203dc002012-08-23 13:59:04 -0700516 if (!Objects.equal(mFolder, folder)) {
517 commitDestructiveActions(false);
518 }
Mindy Pereiraa1f99982012-07-16 16:32:15 -0700519 if (folder != null && !folder.equals(mFolder)
520 || (mViewMode.getMode() != ViewMode.CONVERSATION_LIST)) {
Vikram Aggarwal203dc002012-08-23 13:59:04 -0700521 setListContext(folder, query);
Mindy Pereira28e0c342012-02-17 15:05:13 -0800522 showConversationList(mConvListContext);
Mindy Pereira28e0c342012-02-17 15:05:13 -0800523 }
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -0800524 }
525
Vikram Aggarwal203dc002012-08-23 13:59:04 -0700526 /**
527 * Update the conversation list to {@link #mConvListContext} without creating a new frament if
528 * possible.
529 */
530 protected abstract void updateConversationList();
531
532 private void setFirstFolder(Folder folder, String query) {
533 setListContext(folder, query);
534 updateConversationList();
535 }
536
Mindy Pereira13c12a62012-05-31 15:41:08 -0700537 @Override
Mindy Pereira505df5f2012-06-19 17:57:17 -0700538 public void onFolderSelected(Folder folder) {
Mindy Pereira13c12a62012-05-31 15:41:08 -0700539 onFolderChanged(folder);
540 }
541
Vikram Aggarwal792ccba2012-03-27 13:46:57 -0700542 /**
543 * Update the recent folders. This only needs to be done once when accessing a new folder.
544 */
Paul Westbrook7ebdfd02012-03-21 15:55:30 -0700545 private void updateRecentFolderList() {
Mindy Pereiraab486362012-03-21 18:18:53 -0700546 if (mFolder != null) {
Marc Blank2675dbc2012-04-03 10:17:13 -0700547 mRecentFolderList.touchFolder(mFolder, mAccount);
Mindy Pereiraab486362012-03-21 18:18:53 -0700548 }
Paul Westbrook7ebdfd02012-03-21 15:55:30 -0700549 }
550
Mindy Pereiraab486362012-03-21 18:18:53 -0700551 // TODO(mindyp): set this up to store a copy of the folder as a transient
552 // field in the account.
Vikram Aggarwaleb7d3292012-04-20 17:07:20 -0700553 @Override
554 public void loadAccountInbox() {
Vikram Aggarwal94c94de2012-04-04 15:38:28 -0700555 restartOptionalLoader(LOADER_ACCOUNT_INBOX);
Mindy Pereiraf6acdad2012-03-15 11:21:13 -0700556 }
557
Vikram Aggarwalec5cbf72012-03-08 15:10:35 -0800558 /** Set the current folder */
Mindy Pereira11e35962012-06-01 14:49:46 -0700559 private void updateFolder(Folder folder) {
Mindy Pereira5cb0c3e2012-02-22 15:22:47 -0800560 // Start watching folder for sync status.
Mindy Pereirac975e842012-07-16 09:15:00 -0700561 boolean wasNull = mFolder == null;
Paul Westbrook4bfce9a2012-08-07 22:54:42 -0700562 if (folder != null && !folder.equals(mFolder) && folder.isInitialized()) {
Vikram Aggarwaleb7d3292012-04-20 17:07:20 -0700563 LogUtils.d(LOG_TAG, "AbstractActivityController.setFolder(%s)", folder.name);
Andy Huangb1c34dc2012-04-17 16:36:19 -0700564 final LoaderManager lm = mActivity.getLoaderManager();
Vikram Aggarwal7a5d95a2012-07-27 16:24:54 -0700565 mFolder = folder;
Mindy Pereiraf9323cd2012-02-29 13:47:09 -0800566 mActionBarView.setFolder(mFolder);
Andy Huangb1c34dc2012-04-17 16:36:19 -0700567
Vikram Aggarwal2e0a2362012-08-22 09:05:49 -0700568 // Stop listening to changes to the previous folder.
569 if (mPagerController != null) {
570 mPagerController.stopListening();
571 }
572
Andy Huangb1c34dc2012-04-17 16:36:19 -0700573 // Only when we switch from one folder to another do we want to restart the
574 // folder and conversation list loaders (to trigger onCreateLoader).
575 // The first time this runs when the activity is [re-]initialized, we want to re-use the
576 // previous loader's instance and data upon configuration change (e.g. rotation).
Mindy Pereira11e35962012-06-01 14:49:46 -0700577 // If there was not already an instance of the loader, init it.
578 if (lm.getLoader(LOADER_FOLDER_CURSOR) == null) {
Andy Huangb1c34dc2012-04-17 16:36:19 -0700579 lm.initLoader(LOADER_FOLDER_CURSOR, null, this);
Andy Huangb1c34dc2012-04-17 16:36:19 -0700580 } else {
581 lm.restartLoader(LOADER_FOLDER_CURSOR, null, this);
Mindy Pereirac975e842012-07-16 09:15:00 -0700582 }
583 // In this case, we are starting from no folder, which would occur
584 // the first time the app was launched or on orientation changes.
585 // We want to attach to an existing loader, if available.
586 if (wasNull || lm.getLoader(LOADER_CONVERSATION_LIST) == null) {
587 lm.initLoader(LOADER_CONVERSATION_LIST, null, mListCursorCallbacks);
588 } else {
589 // However, if there was an existing folder AND we have changed
590 // folders, we want to restart the loader to get the information
591 // for the newly selected folder
mindypfaa87d52012-08-20 09:35:03 -0700592 lm.destroyLoader(LOADER_CONVERSATION_LIST);
593 lm.initLoader(LOADER_CONVERSATION_LIST, null, mListCursorCallbacks);
Andy Huangb1c34dc2012-04-17 16:36:19 -0700594 }
Paul Westbrook4bfce9a2012-08-07 22:54:42 -0700595 } else if (!folder.isInitialized()) {
Paul Westbrook3c0f4192012-08-09 12:15:42 -0700596 LogUtils.e(LOG_TAG, new Error(), "Uninitialized Folder %s in setFolder.", folder);
Mindy Pereira5cb0c3e2012-02-22 15:22:47 -0800597 }
598 }
599
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -0800600 @Override
Andy Huang090db1e2012-07-25 13:25:28 -0700601 public Folder getFolder() {
602 return mFolder;
603 }
604
605 @Override
Mindy Pereirac975e842012-07-16 09:15:00 -0700606 public Folder getHierarchyFolder() {
607 return mFolderListFolder;
608 }
609
610 @Override
611 public void setHierarchyFolder(Folder folder) {
612 mFolderListFolder = folder;
Mindy Pereira23aadfd2012-05-25 11:24:33 -0700613 }
614
615 @Override
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -0800616 public void onActivityResult(int requestCode, int resultCode, Intent data) {
Paul Westbrook122f7c22012-08-20 17:50:31 -0700617 switch (requestCode) {
618 case ADD_ACCOUNT_REQUEST_CODE:
619 // We were waiting for the user to create an account
620 if (resultCode == Activity.RESULT_OK) {
621 // restart the loader to get the updated list of accounts
622 mActivity.getLoaderManager().initLoader(
623 LOADER_ACCOUNT_CURSOR, null, this);
624 } else {
625 // The user failed to create an account, just exit the app
626 mActivity.finish();
627 }
628 break;
629 case REAUTHENTICATE_REQUEST_CODE:
630 if (resultCode == Activity.RESULT_OK) {
631 // The user successfully authenticated, attempt to refresh the list
632 final Uri refreshUri = mFolder != null ? mFolder.refreshUri : null;
633 if (refreshUri != null) {
634 startAsyncRefreshTask(refreshUri);
635 }
636 }
637 break;
Paul Westbrook2388c5d2012-03-25 12:29:11 -0700638 }
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -0800639 }
640
641 @Override
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -0800642 public void onConversationListVisibilityChanged(boolean visible) {
Paul Westbrook9f119c72012-04-24 16:10:59 -0700643 if (mConversationListCursor != null) {
644 // The conversation list is visible.
645 Utils.setConversationCursorVisibility(mConversationListCursor, visible);
646 }
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -0800647 }
648
Vikram Aggarwal4a5c5302012-01-12 15:07:13 -0800649 /**
Vikram Aggarwal7dd054e2012-05-21 14:43:10 -0700650 * Called when a conversation is visible. Child classes must call the super class implementation
651 * before performing local computation.
Vikram Aggarwal4a5c5302012-01-12 15:07:13 -0800652 */
653 @Override
654 public void onConversationVisibilityChanged(boolean visible) {
Vikram Aggarwal4a5c5302012-01-12 15:07:13 -0800655 return;
656 }
657
658 @Override
Vikram Aggarwald7a12cd2012-02-03 09:36:20 -0800659 public boolean onCreate(Bundle savedState) {
Vikram Aggarwalabd24d82012-04-26 13:23:14 -0700660 initializeActionBar();
Vikram Aggarwala55b36c2012-01-13 11:45:02 -0800661 // Allow shortcut keys to function for the ActionBar and menus.
662 mActivity.setDefaultKeyMode(Activity.DEFAULT_KEYS_SHORTCUT);
Vikram Aggarwal80aeac52012-02-07 15:27:20 -0800663 mResolver = mActivity.getContentResolver();
Paul Westbrook6ead20d2012-03-19 14:48:14 -0700664 mNewEmailReceiver = new SuppressNotificationReceiver();
Vikram Aggarwal7c401b72012-08-13 16:43:47 -0700665 mRecentFolderList.initialize(mActivity);
Paul Westbrook6ead20d2012-03-19 14:48:14 -0700666
Mindy Pereira161f50d2012-02-28 15:47:19 -0800667 // All the individual UI components listen for ViewMode changes. This
Mindy Pereirab849dfb2012-03-07 18:13:15 -0800668 // simplifies the amount of logic in the AbstractActivityController, but increases the
669 // possibility of timing-related bugs.
Vikram Aggarwala55b36c2012-01-13 11:45:02 -0800670 mViewMode.addListener(this);
Andy Huang632721e2012-04-11 16:57:26 -0700671 mPagerController = new ConversationPagerController(mActivity, this);
Andrew Sappersteinc2c9dc12012-07-02 18:17:32 -0700672 mToastBar = (ActionableToastBar) mActivity.findViewById(R.id.toast_bar);
Vikram Aggarwalada51782012-04-26 14:18:31 -0700673 attachActionBar();
Andy Huang632721e2012-04-11 16:57:26 -0700674
675 final Intent intent = mActivity.getIntent();
Vikram Aggarwalabd24d82012-04-26 13:23:14 -0700676 // Immediately handle a clean launch with intent, and any state restoration
Andy Huang632721e2012-04-11 16:57:26 -0700677 // that does not rely on restored fragments or loader data
678 // any state restoration that relies on those can be done later in
679 // onRestoreInstanceState, once fragments are up and loader data is re-delivered
680 if (savedState != null) {
681 if (savedState.containsKey(SAVED_ACCOUNT)) {
682 setAccount((Account) savedState.getParcelable(SAVED_ACCOUNT));
683 mActivity.invalidateOptionsMenu();
684 }
685 if (savedState.containsKey(SAVED_FOLDER)) {
Vikram Aggarwalf3341402012-08-07 10:09:38 -0700686 final Folder folder = (Folder) savedState.getParcelable(SAVED_FOLDER);
Vikram Aggarwal203dc002012-08-23 13:59:04 -0700687 final String query = savedState.getString(SAVED_QUERY, null);
688 setFirstFolder(folder, query);
Andy Huang632721e2012-04-11 16:57:26 -0700689 }
690 } else if (intent != null) {
691 handleIntent(intent);
692 }
Andy Huang632721e2012-04-11 16:57:26 -0700693 // Create the accounts loader; this loads the account switch spinner.
694 mActivity.getLoaderManager().initLoader(LOADER_ACCOUNT_CURSOR, null, this);
Andy Huang632721e2012-04-11 16:57:26 -0700695 return true;
Andy Huangb1c34dc2012-04-17 16:36:19 -0700696 }
697
698 @Override
Andy Huang1ee96b22012-08-24 20:19:53 -0700699 public void onStart() {
700 mSafeToModifyFragments = true;
701 }
702
703 @Override
Andrew Sapperstein00179f12012-08-09 15:15:40 -0700704 public void onRestart() {
705 DialogFragment fragment = (DialogFragment)
706 mFragmentManager.findFragmentByTag(SYNC_ERROR_DIALOG_FRAGMENT_TAG);
707 if (fragment != null) {
708 fragment.dismiss();
709 }
mindypea04f932012-08-27 14:17:59 -0700710 // When the user places the app in the background by pressing "home",
711 // dismiss the toast bar. However, since there is no way to determine if
712 // home was pressed, just dismiss any existing toast bar when restarting
713 // the app.
714 if (mToastBar != null) {
715 mToastBar.hide(false);
716 }
Andrew Sapperstein00179f12012-08-09 15:15:40 -0700717 }
718
719 @Override
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -0800720 public Dialog onCreateDialog(int id, Bundle bundle) {
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -0800721 return null;
Vikram Aggarwala55b36c2012-01-13 11:45:02 -0800722 }
723
724 @Override
Vikram Aggarwal4eb52712012-06-19 16:24:50 -0700725 public final boolean onCreateOptionsMenu(Menu menu) {
Mindy Pereira28d5f722012-02-15 12:32:40 -0800726 MenuInflater inflater = mActivity.getMenuInflater();
Mindy Pereiraf5acda42012-02-15 20:13:59 -0800727 inflater.inflate(mActionBarView.getOptionsMenuId(), menu);
Mindy Pereira68f2e222012-03-07 10:36:54 -0800728 mActionBarView.onCreateOptionsMenu(menu);
Mindy Pereira28d5f722012-02-15 12:32:40 -0800729 return true;
Vikram Aggarwala55b36c2012-01-13 11:45:02 -0800730 }
731
732 @Override
Vikram Aggarwal4eb52712012-06-19 16:24:50 -0700733 public final boolean onKeyDown(int keyCode, KeyEvent event) {
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -0800734 // TODO(viki): Auto-generated method stub
735 return false;
736 }
737
738 @Override
Vikram Aggarwal4eb52712012-06-19 16:24:50 -0700739 public final boolean onOptionsItemSelected(MenuItem item) {
Vikram Aggarwal54452ae2012-03-13 15:29:00 -0700740 final int id = item.getItemId();
Vikram Aggarwal7dd054e2012-05-21 14:43:10 -0700741 LogUtils.d(LOG_TAG, "AbstractController.onOptionsItemSelected(%d) called.", id);
Mindy Pereira9b875682012-02-15 18:10:54 -0800742 boolean handled = true;
Mindy Pereiraba68fda2012-05-24 15:53:06 -0700743 final Collection<Conversation> target = Conversation.listOf(mCurrentConversation);
Vikram Aggarwal112cd162012-06-18 10:51:13 -0700744 final Settings settings = (mAccount == null) ? null : mAccount.settings;
Mindy Pereira8937bf12012-07-23 14:05:02 -0700745 // The user is choosing a new action; commit whatever they had been doing before.
mindypc6adce32012-08-22 18:46:42 -0700746 commitDestructiveActions(true);
Mindy Pereira28d5f722012-02-15 12:32:40 -0800747 switch (id) {
Mindy Pereiraba68fda2012-05-24 15:53:06 -0700748 case R.id.archive: {
749 final boolean showDialog = (settings != null && settings.confirmArchive);
750 confirmAndDelete(target, showDialog, R.plurals.confirm_archive_conversation,
751 getAction(R.id.archive, target));
752 break;
753 }
Mindy Pereira01f30502012-08-14 10:30:51 -0700754 case R.id.remove_folder:
755 delete(target, getRemoveFolder(target, mFolder, true, false, true));
756 break;
Mindy Pereiraba68fda2012-05-24 15:53:06 -0700757 case R.id.delete: {
758 final boolean showDialog = (settings != null && settings.confirmDelete);
759 confirmAndDelete(target, showDialog, R.plurals.confirm_delete_conversation,
760 getAction(R.id.delete, target));
761 break;
762 }
Paul Westbrookef362542012-08-27 14:53:32 -0700763 case R.id.discard_drafts:
764 final boolean showDialog = (settings != null && settings.confirmDelete);
765 confirmAndDelete(target, showDialog, R.plurals.confirm_discard_drafts_conversation,
766 getAction(R.id.discard_drafts, target));
767 break;
Mindy Pereiraba68fda2012-05-24 15:53:06 -0700768 case R.id.mark_important:
Vikram Aggarwal531488e2012-05-29 16:36:52 -0700769 updateConversation(Conversation.listOf(mCurrentConversation),
770 ConversationColumns.PRIORITY, UIProvider.ConversationPriority.HIGH);
Mindy Pereiraba68fda2012-05-24 15:53:06 -0700771 break;
772 case R.id.mark_not_important:
Mindy Pereira0d03ef82012-08-15 09:05:48 -0700773 if (mFolder != null && mFolder.isImportantOnly()) {
774 delete(target, getAction(R.id.mark_not_important, target));
775 } else {
776 updateConversation(Conversation.listOf(mCurrentConversation),
777 ConversationColumns.PRIORITY, UIProvider.ConversationPriority.LOW);
778 }
Mindy Pereiraba68fda2012-05-24 15:53:06 -0700779 break;
780 case R.id.mute:
Vikram Aggarwal4f4782b2012-05-30 08:39:09 -0700781 delete(target, getAction(R.id.mute, target));
Mindy Pereiraba68fda2012-05-24 15:53:06 -0700782 break;
783 case R.id.report_spam:
Vikram Aggarwal4f4782b2012-05-30 08:39:09 -0700784 delete(target, getAction(R.id.report_spam, target));
Mindy Pereiraba68fda2012-05-24 15:53:06 -0700785 break;
Paul Westbrook77eee622012-07-10 13:41:57 -0700786 case R.id.mark_not_spam:
787 // Currently, since spam messages are only shown in list with other spam messages,
788 // marking a message not as spam is a destructive action
789 delete(target, getAction(R.id.mark_not_spam, target));
790 break;
Paul Westbrook76b20622012-07-12 11:45:43 -0700791 case R.id.report_phishing:
792 delete(target, getAction(R.id.report_phishing, target));
793 break;
Mindy Pereiraf5acda42012-02-15 20:13:59 -0800794 case android.R.id.home:
795 onUpPressed();
796 break;
Mindy Pereira9b875682012-02-15 18:10:54 -0800797 case R.id.compose:
798 ComposeActivity.compose(mActivity.getActivityContext(), mAccount);
799 break;
Mindy Pereira28d5f722012-02-15 12:32:40 -0800800 case R.id.show_all_folders:
801 showFolderList();
802 break;
Mindy Pereira28e0c342012-02-17 15:05:13 -0800803 case R.id.refresh:
804 requestFolderRefresh();
805 break;
Mindy Pereira1f936682012-03-02 11:30:33 -0800806 case R.id.settings:
807 Utils.showSettings(mActivity.getActivityContext(), mAccount);
Paul Westbrook2861b6a2012-02-15 15:25:34 -0800808 break;
Paul Westbrooke5503552012-03-28 00:35:57 -0700809 case R.id.folder_options:
810 Utils.showFolderSettings(mActivity.getActivityContext(), mAccount, mFolder);
811 break;
Paul Westbrook94e440d2012-02-24 11:03:47 -0800812 case R.id.help_info_menu_item:
Paul Westbrook30745b62012-08-19 14:10:32 -0700813 Utils.showHelp(mActivity.getActivityContext(), mAccount, getHelpContext());
Paul Westbrook94e440d2012-02-24 11:03:47 -0800814 break;
Vikram Aggarwal54452ae2012-03-13 15:29:00 -0700815 case R.id.feedback_menu_item:
Paul Westbrook17beb0b2012-08-20 13:34:37 -0700816 Utils.sendFeedback(mActivity.getActivityContext(), mAccount, false);
Mindy Pereirafbe40192012-03-20 10:40:45 -0700817 break;
Paul Westbrook18babd22012-04-09 22:17:08 -0700818 case R.id.manage_folders_item:
819 Utils.showManageFolder(mActivity.getActivityContext(), mAccount);
820 break;
Vikram Aggarwald503df42012-05-11 10:13:35 -0700821 case R.id.change_folder:
822 new FoldersSelectionDialog(mActivity.getActivityContext(), mAccount, this,
Mindy Pereiraa832d3d2012-07-27 11:19:50 -0700823 Conversation.listOf(mCurrentConversation), false, mFolder).show();
Vikram Aggarwald503df42012-05-11 10:13:35 -0700824 break;
Mindy Pereira9b875682012-02-15 18:10:54 -0800825 default:
826 handled = false;
827 break;
Mindy Pereira28d5f722012-02-15 12:32:40 -0800828 }
Mindy Pereira9b875682012-02-15 18:10:54 -0800829 return handled;
Vikram Aggarwala55b36c2012-01-13 11:45:02 -0800830 }
831
Vikram Aggarwal531488e2012-05-29 16:36:52 -0700832 @Override
Mindy Pereira6c2663d2012-07-20 15:37:29 -0700833 public void updateConversation(Collection<Conversation> target, ContentValues values) {
834 mConversationListCursor.updateValues(mContext, target, values);
835 refreshConversationList();
836 }
837
838 @Override
Vikram Aggarwal531488e2012-05-29 16:36:52 -0700839 public void updateConversation(Collection <Conversation> target, String columnName,
840 boolean value) {
841 mConversationListCursor.updateBoolean(mContext, target, columnName, value);
Vikram Aggarwal75daee52012-04-30 11:13:09 -0700842 refreshConversationList();
Vikram Aggarwal54452ae2012-03-13 15:29:00 -0700843 }
844
Vikram Aggarwal531488e2012-05-29 16:36:52 -0700845 @Override
Mindy Pereira6c2663d2012-07-20 15:37:29 -0700846 public void updateConversation(Collection <Conversation> target, String columnName,
847 int value) {
Vikram Aggarwal531488e2012-05-29 16:36:52 -0700848 mConversationListCursor.updateInt(mContext, target, columnName, value);
Vikram Aggarwal75daee52012-04-30 11:13:09 -0700849 refreshConversationList();
Mindy Pereirac9d59182012-03-22 16:06:46 -0700850 }
851
Vikram Aggarwal531488e2012-05-29 16:36:52 -0700852 @Override
853 public void updateConversation(Collection <Conversation> target, String columnName,
854 String value) {
855 mConversationListCursor.updateString(mContext, target, columnName, value);
Vikram Aggarwal75daee52012-04-30 11:13:09 -0700856 refreshConversationList();
Vikram Aggarwal54452ae2012-03-13 15:29:00 -0700857 }
858
Andy Huang839ada22012-07-20 15:48:40 -0700859 @Override
860 public void markConversationMessagesUnread(Conversation conv, Set<Uri> unreadMessageUris,
Vikram Aggarwal4a878b62012-07-31 15:09:25 -0700861 String originalConversationInfo) {
Andy Huang8f6b0062012-07-31 15:36:31 -0700862 // The only caller of this method is the conversation view, from where marking unread should
863 // *always* take you back to list mode.
864 showConversation(null);
865
Andy Huang839ada22012-07-20 15:48:40 -0700866 // locally mark conversation unread (the provider is supposed to propagate message unread
867 // to conversation unread)
868 conv.read = false;
869
Andy Huang28e31e22012-07-26 16:33:15 -0700870 // only do a granular 'mark unread' if a subset of messages are unread
871 final int unreadCount = (unreadMessageUris == null) ? 0 : unreadMessageUris.size();
Mindy Pereira0972e072012-08-01 17:43:06 -0700872 final int numMessages = conv.getNumMessages();
873 final boolean subsetIsUnread = (numMessages > 1 && unreadCount > 0
874 && unreadCount < numMessages);
Andy Huang28e31e22012-07-26 16:33:15 -0700875
876 if (!subsetIsUnread) {
Vikram Aggarwal66bc2aa2012-08-02 10:47:03 -0700877 // Conversations are neither marked read, nor viewed, and we don't want to show
878 // the next conversation.
879 markConversationsRead(Collections.singletonList(conv), false, false, false);
Andy Huang839ada22012-07-20 15:48:40 -0700880 } else {
Andy Huangdaa06ab2012-07-24 10:46:44 -0700881 mConversationListCursor.setConversationColumn(conv.uri, ConversationColumns.READ, 0);
Andy Huang839ada22012-07-20 15:48:40 -0700882
Mindy Pereira7b6d03d2012-07-30 13:03:41 -0700883 // locally update conversation's conversationInfo to revert to original version
Andy Huang28e31e22012-07-26 16:33:15 -0700884 if (originalConversationInfo != null) {
885 mConversationListCursor.setConversationColumn(conv.uri,
886 ConversationColumns.CONVERSATION_INFO, originalConversationInfo);
887 }
Andy Huang839ada22012-07-20 15:48:40 -0700888
889 // applyBatch with each CPO as an UPDATE op on each affected message uri
890 final ArrayList<ContentProviderOperation> ops = Lists.newArrayList();
891 String authority = null;
892 for (Uri messageUri : unreadMessageUris) {
893 if (authority == null) {
894 authority = messageUri.getAuthority();
895 }
896 ops.add(ContentProviderOperation.newUpdate(messageUri)
897 .withValue(UIProvider.MessageColumns.READ, 0)
898 .build());
899 }
900
901 new ContentProviderTask() {
902 @Override
903 protected void onPostExecute(Result result) {
904 // TODO: handle errors?
905 }
906 }.run(mResolver, authority, ops);
Andy Huang839ada22012-07-20 15:48:40 -0700907 }
Andy Huang839ada22012-07-20 15:48:40 -0700908 }
909
910 @Override
Vikram Aggarwal66bc2aa2012-08-02 10:47:03 -0700911 public void markConversationsRead(Collection<Conversation> targets, boolean read,
912 boolean viewed) {
913 // We want to show the next conversation if we are marking unread.
914 markConversationsRead(targets, read, viewed, true);
Andy Huang8f6b0062012-07-31 15:36:31 -0700915 }
916
917 private void markConversationsRead(Collection<Conversation> targets, boolean read,
Vikram Aggarwal66bc2aa2012-08-02 10:47:03 -0700918 boolean markViewed, boolean showNext) {
Andy Huang8f6b0062012-07-31 15:36:31 -0700919 // auto-advance if requested and the current conversation is being marked unread
920 if (showNext && !read) {
921 showNextConversation(targets);
922 }
923
Andy Huang839ada22012-07-20 15:48:40 -0700924 for (Conversation target : targets) {
Vikram Aggarwal192fac12012-07-25 16:44:55 -0700925 final ContentValues values = new ContentValues();
Andy Huang839ada22012-07-20 15:48:40 -0700926 values.put(ConversationColumns.READ, read);
Vikram Aggarwal66bc2aa2012-08-02 10:47:03 -0700927 if (markViewed) {
928 values.put(ConversationColumns.VIEWED, true);
929 }
Vikram Aggarwal192fac12012-07-25 16:44:55 -0700930 final ConversationInfo info = target.conversationInfo;
Andy Huang839ada22012-07-20 15:48:40 -0700931 if (info != null) {
Mindy Pereira5f424372012-07-30 11:49:55 -0700932 info.markRead(read);
933 values.put(ConversationColumns.CONVERSATION_INFO, ConversationInfo.toString(info));
Andy Huang839ada22012-07-20 15:48:40 -0700934 }
935 updateConversation(Conversation.listOf(target), values);
936 }
937 // Update the conversations in the selection too.
938 for (final Conversation c : targets) {
939 c.read = read;
Andy Huangcd5c5ee2012-08-12 19:03:51 -0700940 if (markViewed) {
941 c.markViewed();
942 }
Andy Huang839ada22012-07-20 15:48:40 -0700943 }
944 }
945
Andy Huang8f6b0062012-07-31 15:36:31 -0700946 /**
947 * Auto-advance to a different conversation if the currently visible conversation in
948 * conversation mode is affected (deleted, marked unread, etc.).
949 *
950 * <p>Does nothing if outside of conversation mode.
951 *
952 * @param target the set of conversations being deleted/marked unread
953 */
954 private void showNextConversation(Collection<Conversation> target) {
955 final boolean currentConversationInView = (mViewMode.getMode() == ViewMode.CONVERSATION)
956 && Conversation.contains(target, mCurrentConversation);
957 if (currentConversationInView) {
958 final Conversation next = mTracker.getNextConversation(
959 Settings.getAutoAdvanceSetting(mAccount.settings), target);
960 LogUtils.d(LOG_TAG, "showNextConversation: showing %s next.", next);
961 showConversation(next);
962 }
963 }
964
Andy Huang839ada22012-07-20 15:48:40 -0700965 @Override
966 public void starMessage(ConversationMessage msg, boolean starred) {
967 if (msg.starred == starred) {
968 return;
969 }
970
971 msg.starred = starred;
972
973 // locally propagate the change to the owning conversation
974 // (figure the provider will properly propagate the change when it commits it)
975 //
976 // when unstarring, only propagate the change if this was the only message starred
977 final boolean conversationStarred = starred || msg.isConversationStarred();
978 if (conversationStarred != msg.conversation.starred) {
979 msg.conversation.starred = conversationStarred;
Andy Huangdaa06ab2012-07-24 10:46:44 -0700980 mConversationListCursor.setConversationColumn(msg.conversation.uri,
Andy Huang839ada22012-07-20 15:48:40 -0700981 ConversationColumns.STARRED, conversationStarred);
982 }
983
984 final ContentValues values = new ContentValues(1);
985 values.put(UIProvider.MessageColumns.STARRED, starred ? 1 : 0);
986
987 new ContentProviderTask.UpdateTask() {
988 @Override
989 protected void onPostExecute(Result result) {
990 // TODO: handle errors?
991 }
992 }.run(mResolver, msg.uri, values, null /* selection*/, null /* selectionArgs */);
993 }
994
Mindy Pereira28e0c342012-02-17 15:05:13 -0800995 private void requestFolderRefresh() {
996 if (mFolder != null) {
Mindy Pereirab7b33e02012-02-21 15:32:19 -0800997 if (mAsyncRefreshTask != null) {
998 mAsyncRefreshTask.cancel(true);
999 }
Paul Westbrook7e2a2a12012-06-27 13:52:40 -07001000 mAsyncRefreshTask = new AsyncRefreshTask(mContext, mFolder.refreshUri);
Mindy Pereirab7b33e02012-02-21 15:32:19 -08001001 mAsyncRefreshTask.execute();
Mindy Pereira28e0c342012-02-17 15:05:13 -08001002 }
1003 }
1004
Mindy Pereirafbe40192012-03-20 10:40:45 -07001005 /**
1006 * Confirm (based on user's settings) and delete a conversation from the conversation list and
1007 * from the database.
Vikram Aggarwal3d7ca9d2012-05-11 14:40:36 -07001008 * @param target the conversations to act upon
1009 * @param showDialog true if a confirmation dialog is to be shown, false otherwise.
1010 * @param confirmResource the resource ID of the string that is shown in the confirmation dialog
1011 * @param action the action to perform after animating the deletion of the conversations.
Mindy Pereirafbe40192012-03-20 10:40:45 -07001012 */
Vikram Aggarwal7f602f72012-04-30 16:04:06 -07001013 protected void confirmAndDelete(final Collection<Conversation> target, boolean showDialog,
1014 int confirmResource, final DestructiveAction action) {
Mindy Pereirafbe40192012-03-20 10:40:45 -07001015 if (showDialog) {
1016 final AlertDialog.OnClickListener onClick = new AlertDialog.OnClickListener() {
1017 @Override
1018 public void onClick(DialogInterface dialog, int which) {
Vikram Aggarwal4f4782b2012-05-30 08:39:09 -07001019 delete(target, action);
Mindy Pereirafbe40192012-03-20 10:40:45 -07001020 }
1021 };
Vikram Aggarwal3d7ca9d2012-05-11 14:40:36 -07001022 final CharSequence message = Utils.formatPlural(mContext, confirmResource,
1023 target.size());
Mindy Pereirafbe40192012-03-20 10:40:45 -07001024 new AlertDialog.Builder(mActivity.getActivityContext()).setMessage(message)
1025 .setPositiveButton(R.string.ok, onClick)
1026 .setNegativeButton(R.string.cancel, null)
1027 .create().show();
1028 } else {
Vikram Aggarwal4f4782b2012-05-30 08:39:09 -07001029 delete(target, action);
Mindy Pereirafbe40192012-03-20 10:40:45 -07001030 }
1031 }
1032
Vikram Aggarwal4f4782b2012-05-30 08:39:09 -07001033 @Override
1034 public void delete(final Collection<Conversation> target, final DestructiveAction action) {
Vikram Aggarwal192fac12012-07-25 16:44:55 -07001035 // Order of events is critical! The Conversation View Fragment must be notified
1036 // of the next conversation with showConversation(next) *before* the conversation list
1037 // fragment has a chance to delete the conversation, animating it away.
1038
Vikram Aggarwald503df42012-05-11 10:13:35 -07001039 // Update the conversation fragment if the current conversation is deleted.
Andy Huang8f6b0062012-07-31 15:36:31 -07001040 showNextConversation(target);
Vikram Aggarwal192fac12012-07-25 16:44:55 -07001041 // The conversation list deletes and performs the action if it exists.
1042 final ConversationListFragment convListFragment = getConversationListFragment();
1043 if (convListFragment != null) {
1044 LogUtils.d(LOG_TAG, "AAC.requestDelete: ListFragment is handling delete.");
1045 convListFragment.requestDelete(target, action);
1046 return;
1047 }
Vikram Aggarwald503df42012-05-11 10:13:35 -07001048 // No visible UI element handled it on our behalf. Perform the action ourself.
1049 action.performAction();
1050 }
1051
1052 /**
1053 * Requests that the action be performed and the UI state is updated to reflect the new change.
1054 * @param target
1055 * @param action
1056 */
Vikram Aggarwal4f4782b2012-05-30 08:39:09 -07001057 private void requestUpdate(final Collection<Conversation> target,
Vikram Aggarwald503df42012-05-11 10:13:35 -07001058 final DestructiveAction action) {
1059 action.performAction();
1060 refreshConversationList();
Vikram Aggarwal75daee52012-04-30 11:13:09 -07001061 }
Vikram Aggarwal6fbc87a2012-03-15 15:24:00 -07001062
1063 @Override
Vikram Aggarwala55b36c2012-01-13 11:45:02 -08001064 public void onPrepareDialog(int id, Dialog dialog, Bundle bundle) {
1065 // TODO(viki): Auto-generated method stub
Vikram Aggarwala55b36c2012-01-13 11:45:02 -08001066 }
1067
1068 @Override
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -08001069 public boolean onPrepareOptionsMenu(Menu menu) {
Mindy Pereirab849dfb2012-03-07 18:13:15 -08001070 mActionBarView.onPrepareOptionsMenu(menu);
Mindy Pereira363451a2012-02-22 14:14:46 -08001071 return true;
Vikram Aggarwala55b36c2012-01-13 11:45:02 -08001072 }
1073
Mindy Pereira68f2e222012-03-07 10:36:54 -08001074 @Override
1075 public void onPause() {
1076 isLoaderInitialized = false;
Paul Westbrook6ead20d2012-03-19 14:48:14 -07001077 enableNotifications();
Paul Westbrook94e440d2012-02-24 11:03:47 -08001078 }
1079
Vikram Aggarwala55b36c2012-01-13 11:45:02 -08001080 @Override
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -08001081 public void onResume() {
Paul Westbrook6ead20d2012-03-19 14:48:14 -07001082 // Register the receiver that will prevent the status receiver from
1083 // displaying its notification icon as long as we're running.
1084 // The SupressNotificationReceiver will block the broadcast if we're looking at the folder
1085 // that the notification was received for.
1086 disableNotifications();
Andy Huang1ee96b22012-08-24 20:19:53 -07001087
1088 mSafeToModifyFragments = true;
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -08001089 }
1090
1091 @Override
1092 public void onSaveInstanceState(Bundle outState) {
Vikram Aggarwal6c511582012-02-27 10:59:47 -08001093 if (mAccount != null) {
1094 LogUtils.d(LOG_TAG, "Saving the account now");
1095 outState.putParcelable(SAVED_ACCOUNT, mAccount);
1096 }
Mindy Pereira5e478d22012-03-26 18:04:58 -07001097 if (mFolder != null) {
1098 outState.putParcelable(SAVED_FOLDER, mFolder);
Vikram Aggarwal8b152632012-02-03 14:58:45 -08001099 }
Vikram Aggarwalf3341402012-08-07 10:09:38 -07001100 // If this is a search activity, let's store the search query term as well.
1101 if (ConversationListContext.isSearchResult(mConvListContext)) {
1102 outState.putString(SAVED_QUERY, mConvListContext.searchQuery);
1103 }
Mindy Pereira0f7ae7a2012-07-17 13:39:56 -07001104 int mode = mViewMode.getMode();
Mindy Pereira9fa43ca2012-05-17 14:18:01 -07001105 if (mCurrentConversation != null
Mindy Pereira0f7ae7a2012-07-17 13:39:56 -07001106 && (mode == ViewMode.CONVERSATION ||
Mindy Pereira9fa43ca2012-05-17 14:18:01 -07001107 mViewMode.getMode() == ViewMode.SEARCH_RESULTS_CONVERSATION)) {
Mindy Pereira26f23fc2012-03-27 10:26:04 -07001108 outState.putParcelable(SAVED_CONVERSATION, mCurrentConversation);
1109 }
Andy Huang4556a442012-03-30 16:42:05 -07001110 if (!mSelectedSet.isEmpty()) {
Vikram Aggarwalcabd3f22012-04-19 10:14:41 -07001111 outState.putParcelable(SAVED_SELECTED_SET, mSelectedSet);
Andy Huang4556a442012-03-30 16:42:05 -07001112 }
Mindy Pereirad33674992012-06-25 16:26:30 -07001113 if (mToastBar.getVisibility() == View.VISIBLE) {
1114 outState.putParcelable(SAVED_TOAST_BAR_OP, mToastBar.getOperation());
1115 }
1116 ConversationListFragment convListFragment = getConversationListFragment();
1117 if (convListFragment != null) {
Andy Huang839ada22012-07-20 15:48:40 -07001118 convListFragment.getAnimatedAdapter()
Mindy Pereirad33674992012-06-25 16:26:30 -07001119 .onSaveInstanceState(outState);
1120 }
Andy Huang1ee96b22012-08-24 20:19:53 -07001121
1122 mSafeToModifyFragments = false;
1123 }
1124
1125 /**
1126 * @see #mSafeToModifyFragments
1127 */
1128 protected boolean safeToModifyFragments() {
1129 return mSafeToModifyFragments;
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -08001130 }
1131
1132 @Override
Mindy Pereira68f2e222012-03-07 10:36:54 -08001133 public void onSearchRequested(String query) {
1134 Intent intent = new Intent();
1135 intent.setAction(Intent.ACTION_SEARCH);
1136 intent.putExtra(ConversationListContext.EXTRA_SEARCH_QUERY, query);
1137 intent.putExtra(Utils.EXTRA_ACCOUNT, mAccount);
1138 intent.setComponent(mActivity.getComponentName());
Vikram Aggarwalb17cbc02012-04-06 15:41:46 -07001139 mActionBarView.collapseSearch();
Mindy Pereira68f2e222012-03-07 10:36:54 -08001140 mActivity.startActivity(intent);
Vikram Aggarwala55b36c2012-01-13 11:45:02 -08001141 }
1142
1143 @Override
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -08001144 public void onStop() {
Mindy Pereira49e5dbe2012-07-12 11:47:54 -07001145 if (mEnableShareIntents != null) {
1146 mEnableShareIntents.cancel(true);
1147 }
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -08001148 }
1149
Andy Huang632721e2012-04-11 16:57:26 -07001150 @Override
1151 public void onDestroy() {
1152 // unregister the ViewPager's observer on the conversation cursor
1153 mPagerController.onDestroy();
Mindy Pereira641de652012-08-02 15:21:50 -07001154 mActionBarView.onDestroy();
Vikram Aggarwal7c401b72012-08-13 16:43:47 -07001155 mRecentFolderList.destroy();
Andy Huang4e0158f2012-08-07 21:06:01 -07001156 mDestroyed = true;
Andy Huang632721e2012-04-11 16:57:26 -07001157 }
1158
Vikram Aggarwalfa131a22012-02-02 13:56:22 -08001159 /**
Mindy Pereira161f50d2012-02-28 15:47:19 -08001160 * {@inheritDoc} Subclasses must override this to listen to mode changes
1161 * from the ViewMode. Subclasses <b>must</b> call the parent's
1162 * onViewModeChanged since the parent will handle common state changes.
Vikram Aggarwalfa131a22012-02-02 13:56:22 -08001163 */
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -08001164 @Override
Vikram Aggarwalfa131a22012-02-02 13:56:22 -08001165 public void onViewModeChanged(int newMode) {
1166 // Perform any mode specific work here.
Mindy Pereira161f50d2012-02-28 15:47:19 -08001167 // reset the action bar icon based on the mode. Why don't the individual
1168 // controllers do
Vikram Aggarwalfa131a22012-02-02 13:56:22 -08001169 // this themselves?
Vikram Aggarwala55b36c2012-01-13 11:45:02 -08001170
Mindy Pereira8937bf12012-07-23 14:05:02 -07001171 // Commit any destructive undoable actions the user may have performed.
mindypc6adce32012-08-22 18:46:42 -07001172 commitDestructiveActions(true);
Mindy Pereira8937bf12012-07-23 14:05:02 -07001173
Mindy Pereira161f50d2012-02-28 15:47:19 -08001174 // We don't want to invalidate the options menu when switching to
1175 // conversation
Vikram Aggarwalfa131a22012-02-02 13:56:22 -08001176 // mode, as it will happen when the conversation finishes loading.
1177 if (newMode != ViewMode.CONVERSATION) {
1178 mActivity.invalidateOptionsMenu();
1179 }
Vikram Aggarwala55b36c2012-01-13 11:45:02 -08001180 }
1181
Andy Huang4e0158f2012-08-07 21:06:01 -07001182 public boolean isDestroyed() {
1183 return mDestroyed;
1184 }
1185
mindypc6adce32012-08-22 18:46:42 -07001186 private void commitDestructiveActions(boolean animate) {
1187 ConversationListFragment fragment = getConversationListFragment();
Mindy Pereira1e2573b2012-04-17 14:34:36 -07001188 if (fragment != null) {
mindypc6adce32012-08-22 18:46:42 -07001189 fragment.commitDestructiveActions(animate);
Mindy Pereira1e2573b2012-04-17 14:34:36 -07001190 }
1191 }
1192
Vikram Aggarwala55b36c2012-01-13 11:45:02 -08001193 @Override
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -08001194 public void onWindowFocusChanged(boolean hasFocus) {
Paul Westbrook9f119c72012-04-24 16:10:59 -07001195 ConversationListFragment convList = getConversationListFragment();
1196 if (hasFocus && convList != null && convList.isVisible()) {
1197 // The conversation list is visible.
1198 Utils.setConversationCursorVisibility(mConversationListCursor, true);
1199 }
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -08001200 }
1201
Vikram Aggarwalca716f12012-08-20 11:11:48 -07001202 /**
1203 * Set the account, and carry out all the account-related changes that rely on this.
1204 * @param account
1205 */
1206 // TODO(viki): Two different methods do the same thing. Resolve
1207 // {@link #setAccount(Account)} and {@link #switchAccount(Account, boolean)}
Mindy Pereira75181e82012-04-18 08:17:13 -07001208 private void setAccount(Account account) {
Andy Huangb9ca9792012-05-18 15:31:49 -07001209 if (account == null) {
1210 LogUtils.w(LOG_TAG, new Error(),
1211 "AAC ignoring null (presumably invalid) account restoration");
1212 return;
1213 }
Andy Huangb1148412012-05-19 00:16:30 -07001214 LogUtils.d(LOG_TAG, "AbstractActivityController.setAccount(): account = %s", account.uri);
Vikram Aggarwal91e87372012-05-18 15:36:04 -07001215 mAccount = account;
Vikram Aggarwalca716f12012-08-20 11:11:48 -07001216 // Only change AAC state here. Do *not* modify any other object's state. The object
1217 // should listen on account changes.
1218 restartOptionalLoader(LOADER_RECENT_FOLDERS);
1219 mActivity.invalidateOptionsMenu();
1220 disableNotificationsOnAccountChange(mAccount);
1221 restartOptionalLoader(LOADER_ACCOUNT_UPDATE_CURSOR);
1222 MailAppProvider.getInstance().setLastViewedAccount(mAccount.uri.toString());
1223
Vikram Aggarwal91e87372012-05-18 15:36:04 -07001224 if (account.settings == null) {
1225 LogUtils.w(LOG_TAG, new Error(), "AAC ignoring account with null settings.");
1226 return;
1227 }
Vikram Aggarwal7c401b72012-08-13 16:43:47 -07001228 mAccountObservers.notifyChanged();
Mindy Pereira75181e82012-04-18 08:17:13 -07001229 }
1230
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -08001231 /**
Mindy Pereira161f50d2012-02-28 15:47:19 -08001232 * Restore the state from the previous bundle. Subclasses should call this
1233 * method from the parent class, since it performs important UI
1234 * initialization.
1235 *
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -08001236 * @param savedState
1237 */
Andy Huang632721e2012-04-11 16:57:26 -07001238 @Override
1239 public void onRestoreInstanceState(Bundle savedState) {
1240 LogUtils.d(LOG_TAG, "IN AAC.onRestoreInstanceState");
1241 if (savedState.containsKey(SAVED_CONVERSATION)) {
1242 // Open the conversation.
Paul Westbrook534e4a22012-04-25 03:46:29 -07001243 final Conversation conversation =
1244 (Conversation)savedState.getParcelable(SAVED_CONVERSATION);
1245 if (conversation != null && conversation.position < 0) {
1246 // Set the position to 0 on this conversation, as we don't know where it is
1247 // in the list
1248 conversation.position = 0;
1249 }
Andy Huanged4fdf02012-07-26 17:12:50 -07001250 showConversation(conversation);
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -08001251 }
Mindy Pereira967ede62012-03-22 09:29:09 -07001252
Mindy Pereirad33674992012-06-25 16:26:30 -07001253 if (savedState.containsKey(SAVED_TOAST_BAR_OP)) {
1254 ToastBarOperation op = ((ToastBarOperation) savedState
1255 .getParcelable(SAVED_TOAST_BAR_OP));
1256 if (op != null) {
1257 if (op.getType() == ToastBarOperation.UNDO) {
1258 onUndoAvailable(op);
Andrew Sapperstein9d7519d2012-07-16 14:03:53 -07001259 } else if (op.getType() == ToastBarOperation.ERROR) {
1260 onError(mFolder, true);
Mindy Pereirad33674992012-06-25 16:26:30 -07001261 }
1262 }
1263 }
Mindy Pereira0f7ae7a2012-07-17 13:39:56 -07001264
Mindy Pereirad33674992012-06-25 16:26:30 -07001265 ConversationListFragment convListFragment = getConversationListFragment();
1266 if (convListFragment != null) {
Andy Huang839ada22012-07-20 15:48:40 -07001267 convListFragment.getAnimatedAdapter()
Mindy Pereirad33674992012-06-25 16:26:30 -07001268 .onRestoreInstanceState(savedState);
1269 }
Mindy Pereira967ede62012-03-22 09:29:09 -07001270 /**
1271 * Restore the state of selected conversations. This needs to be done after the correct mode
1272 * is set and the action bar is fully initialized. If not, several key pieces of state
1273 * information will be missing, and the split views may not be initialized correctly.
1274 * @param savedState
1275 */
Andy Huang4556a442012-03-30 16:42:05 -07001276 restoreSelectedConversations(savedState);
Andy Huang632721e2012-04-11 16:57:26 -07001277 }
1278
1279 private void handleIntent(Intent intent) {
1280 boolean handled = false;
1281 if (Intent.ACTION_VIEW.equals(intent.getAction())) {
1282 if (intent.hasExtra(Utils.EXTRA_ACCOUNT)) {
Andy Huang632721e2012-04-11 16:57:26 -07001283 setAccount(Account.newinstance(intent
Mindy Pereira5ad02912012-07-09 09:57:18 -07001284 .getStringExtra(Utils.EXTRA_ACCOUNT)));
Andy Huang632721e2012-04-11 16:57:26 -07001285 }
Andy Huangb9ca9792012-05-18 15:31:49 -07001286 if (mAccount == null) {
1287 return;
Andy Huang632721e2012-04-11 16:57:26 -07001288 }
Andy Huangb9ca9792012-05-18 15:31:49 -07001289 mActivity.invalidateOptionsMenu();
Vikram Aggarwal1a249e02012-08-03 16:19:33 -07001290 final boolean isConversationMode = intent.hasExtra(Utils.EXTRA_CONVERSATION);
1291 // TODO(viki): Allow the controller to set the mode instead of a mode transition.
1292 if (isConversationMode) {
1293 mViewMode.enterConversationMode();
1294 } else {
1295 mViewMode.enterConversationListMode();
1296 }
Andy Huang632721e2012-04-11 16:57:26 -07001297
1298 Folder folder = null;
1299 if (intent.hasExtra(Utils.EXTRA_FOLDER)) {
1300 // Open the folder.
Mindy Pereira7b6d03d2012-07-30 13:03:41 -07001301 folder = Folder.fromString(intent.getStringExtra(Utils.EXTRA_FOLDER));
Andy Huang632721e2012-04-11 16:57:26 -07001302 }
1303 if (folder != null) {
1304 onFolderChanged(folder);
1305 handled = true;
1306 }
1307
Vikram Aggarwal1a249e02012-08-03 16:19:33 -07001308 if (isConversationMode) {
Andy Huang632721e2012-04-11 16:57:26 -07001309 // Open the conversation.
1310 LogUtils.d(LOG_TAG, "SHOW THE CONVERSATION at %s",
1311 intent.getParcelableExtra(Utils.EXTRA_CONVERSATION));
Paul Westbrooka9161912012-04-24 10:10:04 -07001312 final Conversation conversation =
1313 (Conversation)intent.getParcelableExtra(Utils.EXTRA_CONVERSATION);
1314 if (conversation != null && conversation.position < 0) {
1315 // Set the position to 0 on this conversation, as we don't know where it is
1316 // in the list
1317 conversation.position = 0;
1318 }
Andy Huang980aaea2012-07-26 17:22:19 -07001319 showConversation(conversation);
Andy Huang632721e2012-04-11 16:57:26 -07001320 handled = true;
1321 }
1322
1323 if (!handled) {
1324 // Nothing was saved; just load the account inbox.
1325 loadAccountInbox();
1326 }
1327 } else if (Intent.ACTION_SEARCH.equals(intent.getAction())) {
1328 if (intent.hasExtra(Utils.EXTRA_ACCOUNT)) {
1329 // Save this search query for future suggestions.
1330 final String query = intent.getStringExtra(SearchManager.QUERY);
1331 final String authority = mContext.getString(R.string.suggestions_authority);
1332 SearchRecentSuggestions suggestions = new SearchRecentSuggestions(
1333 mContext, authority, SuggestionsProvider.MODE);
1334 suggestions.saveRecentQuery(query, null);
Mindy Pereiraac254822012-06-18 10:46:43 -07001335 if (Utils.showTwoPaneSearchResults(mActivity.getActivityContext())) {
1336 mViewMode.enterSearchResultsConversationMode();
1337 } else {
1338 mViewMode.enterSearchResultsListMode();
1339 }
Andy Huang632721e2012-04-11 16:57:26 -07001340 setAccount((Account) intent.getParcelableExtra(Utils.EXTRA_ACCOUNT));
1341 mActivity.invalidateOptionsMenu();
1342 restartOptionalLoader(LOADER_RECENT_FOLDERS);
Andy Huang632721e2012-04-11 16:57:26 -07001343 fetchSearchFolder(intent);
1344 } else {
1345 LogUtils.e(LOG_TAG, "Missing account extra from search intent. Finishing");
1346 mActivity.finish();
1347 }
1348 }
1349 if (mAccount != null) {
1350 restartOptionalLoader(LOADER_ACCOUNT_UPDATE_CURSOR);
1351 }
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -08001352 }
1353
Andy Huang4556a442012-03-30 16:42:05 -07001354 /**
1355 * Copy any selected conversations stored in the saved bundle into our selection set,
1356 * triggering {@link ConversationSetObserver} callbacks as our selection set changes.
1357 *
1358 */
Vikram Aggarwal4eb52712012-06-19 16:24:50 -07001359 private final void restoreSelectedConversations(Bundle savedState) {
Mindy Pereira967ede62012-03-22 09:29:09 -07001360 if (savedState == null) {
Andy Huang4556a442012-03-30 16:42:05 -07001361 mSelectedSet.clear();
Mindy Pereira967ede62012-03-22 09:29:09 -07001362 return;
1363 }
Vikram Aggarwalcabd3f22012-04-19 10:14:41 -07001364 final ConversationSelectionSet selectedSet = savedState.getParcelable(SAVED_SELECTED_SET);
Andy Huang4556a442012-03-30 16:42:05 -07001365 if (selectedSet == null || selectedSet.isEmpty()) {
1366 mSelectedSet.clear();
Mindy Pereira967ede62012-03-22 09:29:09 -07001367 return;
1368 }
Andy Huang632721e2012-04-11 16:57:26 -07001369
1370 // putAll will take care of calling our registered onSetPopulated method
Vikram Aggarwalcabd3f22012-04-19 10:14:41 -07001371 mSelectedSet.putAll(selectedSet);
Mindy Pereira967ede62012-03-22 09:29:09 -07001372 }
1373
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -08001374 @Override
Andy Huang5895f7b2012-06-01 17:07:20 -07001375 public SubjectDisplayChanger getSubjectDisplayChanger() {
1376 return mActionBarView;
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -08001377 }
1378
Andy Huang1ee96b22012-08-24 20:19:53 -07001379 private void showConversation(Conversation conversation) {
1380 showConversation(conversation, false /* inLoaderCallbacks */);
1381 }
1382
Vikram Aggarwalec5cbf72012-03-08 15:10:35 -08001383 /**
1384 * Children can override this method, but they must call super.showConversation().
Andy Huang1ee96b22012-08-24 20:19:53 -07001385 *
Vikram Aggarwalec5cbf72012-03-08 15:10:35 -08001386 */
Andy Huang1ee96b22012-08-24 20:19:53 -07001387 protected void showConversation(Conversation conversation, boolean inLoaderCallbacks) {
Vikram Aggarwalc67182d2012-04-03 14:35:06 -07001388 // Set the current conversation just in case it wasn't already set.
1389 setCurrentConversation(conversation);
Vikram Aggarwal0f142732012-08-24 09:39:34 -07001390 // Add the folder that we were viewing to the recent folders list.
1391 // TODO: this may need to be fine tuned. If this is the signal that is indicating that
1392 // the list is shown to the user, this could fire in one pane if the user goes directly
1393 // to a conversation
1394 updateRecentFolderList();
Vikram Aggarwalec5cbf72012-03-08 15:10:35 -08001395 }
1396
Vikram Aggarwale128fc22012-04-04 12:33:34 -07001397 /**
Paul Westbrook2d50bcd2012-04-10 11:53:47 -07001398 * Children can override this method, but they must call super.showWaitForInitialization().
1399 * {@inheritDoc}
1400 */
1401 @Override
1402 public void showWaitForInitialization() {
1403 mViewMode.enterWaitingForInitializationMode();
1404 }
1405
1406 @Override
1407 public void hideWaitForInitialization() {
1408 }
1409
1410 @Override
1411 public void updateWaitMode() {
1412 final FragmentManager manager = mActivity.getFragmentManager();
1413 final WaitFragment waitFragment =
Vikram Aggarwal6902dcf2012-04-11 08:57:42 -07001414 (WaitFragment)manager.findFragmentByTag(TAG_WAIT);
Paul Westbrook2d50bcd2012-04-10 11:53:47 -07001415 if (waitFragment != null) {
1416 waitFragment.updateAccount(mAccount);
1417 }
1418 }
1419
Vikram Aggarwal48b2a6c2012-05-29 14:09:27 -07001420 /**
1421 * Returns true if we are waiting for the account to sync, and cannot show any folders or
1422 * conversation for the current account yet.
Andy Huang839ada22012-07-20 15:48:40 -07001423 *
Vikram Aggarwal48b2a6c2012-05-29 14:09:27 -07001424 */
Paul Westbrook2d50bcd2012-04-10 11:53:47 -07001425 public boolean inWaitMode() {
1426 final FragmentManager manager = mActivity.getFragmentManager();
1427 final WaitFragment waitFragment =
Vikram Aggarwal6902dcf2012-04-11 08:57:42 -07001428 (WaitFragment)manager.findFragmentByTag(TAG_WAIT);
Paul Westbrook2d50bcd2012-04-10 11:53:47 -07001429 if (waitFragment != null) {
1430 final Account fragmentAccount = waitFragment.getAccount();
1431 return fragmentAccount.uri.equals(mAccount.uri) &&
1432 mViewMode.getMode() == ViewMode.WAITING_FOR_ACCOUNT_INITIALIZATION;
1433 }
1434 return false;
1435 }
1436
1437 /**
Vikram Aggarwale128fc22012-04-04 12:33:34 -07001438 * Children can override this method, but they must call super.showConversationList().
1439 * {@inheritDoc}
1440 */
1441 @Override
1442 public void showConversationList(ConversationListContext listContext) {
1443 }
1444
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -08001445 @Override
Andy Huang1ee96b22012-08-24 20:19:53 -07001446 public void onConversationSelected(Conversation conversation, boolean inLoaderCallbacks) {
mindypaa55bc92012-08-24 09:49:56 -07001447 // Only animate destructive actions if we are going to be showing the
1448 // conversation list when we show the next conversation.
1449 commitDestructiveActions(Utils.useTabletUI(mContext));
Andy Huang1ee96b22012-08-24 20:19:53 -07001450 showConversation(conversation, inLoaderCallbacks);
1451 }
1452
1453 @Override
1454 public Conversation getCurrentConversation() {
1455 return mCurrentConversation;
Vikram Aggarwal7d602882012-02-07 15:01:20 -08001456 }
Mindy Pereira555140c2012-02-15 14:55:29 -08001457
Vikram Aggarwalc67182d2012-04-03 14:35:06 -07001458 /**
1459 * Set the current conversation. This is the conversation on which all actions are performed.
1460 * Do not modify mCurrentConversation except through this method, which makes it easy to
1461 * perform common actions associated with changing the current conversation.
1462 * @param conversation
1463 */
Andy Huang632721e2012-04-11 16:57:26 -07001464 @Override
1465 public void setCurrentConversation(Conversation conversation) {
Mindy Pereira5040f1a2012-03-20 10:14:06 -07001466 mCurrentConversation = conversation;
Vikram Aggarwal135fd022012-04-11 14:44:15 -07001467 mTracker.initialize(mCurrentConversation);
Mindy Pereira5040f1a2012-03-20 10:14:06 -07001468 }
1469
Vikram Aggarwala9b93f32012-02-23 14:51:58 -08001470 /**
1471 * {@inheritDoc}
1472 */
Mindy Pereira555140c2012-02-15 14:55:29 -08001473 @Override
Vikram Aggarwal7dedb952012-02-16 16:10:23 -08001474 public Loader<Cursor> onCreateLoader(int id, Bundle args) {
1475 // Create a loader to listen in on account changes.
Vikram Aggarwalfa4b47e2012-03-09 13:02:46 -08001476 switch (id) {
1477 case LOADER_ACCOUNT_CURSOR:
Paul Westbrookc2074c42012-03-22 15:26:58 -07001478 return new CursorLoader(mContext, MailAppProvider.getAccountsUri(),
Vikram Aggarwalfa4b47e2012-03-09 13:02:46 -08001479 UIProvider.ACCOUNTS_PROJECTION, null, null, null);
1480 case LOADER_FOLDER_CURSOR:
Paul Westbrookc7a070f2012-04-12 01:46:41 -07001481 final CursorLoader loader = new CursorLoader(mContext, mFolder.uri,
1482 UIProvider.FOLDERS_PROJECTION, null, null, null);
1483 loader.setUpdateThrottle(mFolderItemUpdateDelayMs);
1484 return loader;
Vikram Aggarwalfa4b47e2012-03-09 13:02:46 -08001485 case LOADER_RECENT_FOLDERS:
Paul Westbrook91d10502012-04-13 12:01:39 -07001486 if (mAccount != null && mAccount.recentFolderListUri != null) {
Paul Westbrookea4ee202012-03-12 14:12:33 -07001487 return new CursorLoader(mContext, mAccount.recentFolderListUri,
1488 UIProvider.FOLDERS_PROJECTION, null, null, null);
1489 }
1490 break;
Mindy Pereiraab486362012-03-21 18:18:53 -07001491 case LOADER_ACCOUNT_INBOX:
Vikram Aggarwal025eba82012-05-08 10:45:30 -07001492 final Uri defaultInbox = Settings.getDefaultInboxUri(mAccount.settings);
Vikram Aggarwal1e57e672012-05-07 14:48:24 -07001493 final Uri inboxUri = defaultInbox.equals(Uri.EMPTY) ?
1494 mAccount.folderListUri : defaultInbox;
Paul Westbrook7496e822012-04-24 09:50:54 -07001495 LogUtils.d(LOG_TAG, "Loading the default inbox: %s", inboxUri);
Paul Westbrook1220b6d2012-04-10 00:48:00 -07001496 if (inboxUri != null) {
1497 return new CursorLoader(mContext, inboxUri, UIProvider.FOLDERS_PROJECTION, null,
1498 null, null);
1499 }
1500 break;
Mindy Pereiraab486362012-03-21 18:18:53 -07001501 case LOADER_SEARCH:
1502 return Folder.forSearchResults(mAccount,
1503 args.getString(ConversationListContext.EXTRA_SEARCH_QUERY),
1504 mActivity.getActivityContext());
Paul Westbrook2d50bcd2012-04-10 11:53:47 -07001505 case LOADER_ACCOUNT_UPDATE_CURSOR:
1506 return new CursorLoader(mContext, mAccount.uri, UIProvider.ACCOUNTS_PROJECTION,
1507 null, null, null);
Vikram Aggarwalfa4b47e2012-03-09 13:02:46 -08001508 default:
Paul Westbrookad6a2752012-04-04 16:58:13 -07001509 LogUtils.wtf(LOG_TAG, "Loader returned unexpected id: %d", id);
Mindy Pereira5cb0c3e2012-02-22 15:22:47 -08001510 }
1511 return null;
Vikram Aggarwal7dedb952012-02-16 16:10:23 -08001512 }
1513
Paul Westbrookb1f573c2012-04-06 11:38:28 -07001514 @Override
1515 public void onLoaderReset(Loader<Cursor> loader) {
1516
1517 }
1518
Andy Huangf9a73482012-03-13 15:54:02 -07001519 /**
1520 * {@link LoaderManager} currently has a bug in
1521 * {@link LoaderManager#restartLoader(int, Bundle, android.app.LoaderManager.LoaderCallbacks)}
1522 * where, if a previous onCreateLoader returned a null loader, this method will NPE. Work around
1523 * this bug by destroying any loaders that may have been created as null (essentially because
1524 * they are optional loads, and may not apply to a particular account).
1525 * <p>
1526 * A simple null check before restarting a loader will not work, because that would not
1527 * give the controller a chance to invalidate UI corresponding the prior loader result.
1528 *
1529 * @param id loader ID to safely restart
Andy Huangf9a73482012-03-13 15:54:02 -07001530 */
Vikram Aggarwal94c94de2012-04-04 15:38:28 -07001531 private void restartOptionalLoader(int id) {
Andy Huangf9a73482012-03-13 15:54:02 -07001532 final LoaderManager lm = mActivity.getLoaderManager();
1533 lm.destroyLoader(id);
Vikram Aggarwal94c94de2012-04-04 15:38:28 -07001534 lm.restartLoader(id, Bundle.EMPTY, this);
1535 }
1536
Andy Huang632721e2012-04-11 16:57:26 -07001537 @Override
1538 public void registerConversationListObserver(DataSetObserver observer) {
1539 mConversationListObservable.registerObserver(observer);
1540 }
1541
1542 @Override
1543 public void unregisterConversationListObserver(DataSetObserver observer) {
1544 mConversationListObservable.unregisterObserver(observer);
1545 }
1546
Andy Huang090db1e2012-07-25 13:25:28 -07001547 @Override
1548 public void registerFolderObserver(DataSetObserver observer) {
1549 mFolderObservable.registerObserver(observer);
1550 }
1551
1552 @Override
1553 public void unregisterFolderObserver(DataSetObserver observer) {
1554 mFolderObservable.unregisterObserver(observer);
1555 }
1556
Vikram Aggarwal60069912012-07-24 14:26:09 -07001557 /**
1558 * Returns true if the number of accounts is different, or if the current account has been
1559 * removed from the device
1560 * @param accountCursor
1561 * @return
1562 */
Paul Westbrook23b74b92012-02-29 11:36:12 -08001563 private boolean accountsUpdated(Cursor accountCursor) {
1564 // Check to see if the current account hasn't been set, or the account cursor is empty
1565 if (mAccount == null || !accountCursor.moveToFirst()) {
Vikram Aggarwal6c511582012-02-27 10:59:47 -08001566 return true;
Paul Westbrook23b74b92012-02-29 11:36:12 -08001567 }
1568
1569 // Check to see if the number of accounts are different, from the number we saw on the last
1570 // updated
1571 if (mCurrentAccountUris.size() != accountCursor.getCount()) {
1572 return true;
1573 }
1574
1575 // Check to see if the account list is different or if the current account is not found in
1576 // the cursor.
1577 boolean foundCurrentAccount = false;
Vikram Aggarwal7dedb952012-02-16 16:10:23 -08001578 do {
Paul Westbrook23b74b92012-02-29 11:36:12 -08001579 final Uri accountUri =
1580 Uri.parse(accountCursor.getString(UIProvider.ACCOUNT_URI_COLUMN));
1581 if (!foundCurrentAccount && mAccount.uri.equals(accountUri)) {
1582 foundCurrentAccount = true;
1583 }
Vikram Aggarwal60069912012-07-24 14:26:09 -07001584 // Is there a new account that we do not know about?
Paul Westbrook23b74b92012-02-29 11:36:12 -08001585 if (!mCurrentAccountUris.contains(accountUri)) {
1586 return true;
1587 }
Vikram Aggarwal7dedb952012-02-16 16:10:23 -08001588 } while (accountCursor.moveToNext());
Paul Westbrook23b74b92012-02-29 11:36:12 -08001589
1590 // As long as we found the current account, the list hasn't been updated
1591 return !foundCurrentAccount;
Vikram Aggarwal7dedb952012-02-16 16:10:23 -08001592 }
1593
1594 /**
Vikram Aggarwal60069912012-07-24 14:26:09 -07001595 * Updates accounts for the app. If the current account is missing, the first
1596 * account in the list is set to the current account (we <em>have</em> to choose something).
Mindy Pereira161f50d2012-02-28 15:47:19 -08001597 *
Vikram Aggarwal6c511582012-02-27 10:59:47 -08001598 * @param accounts cursor into the AccountCache
Vikram Aggarwal7dedb952012-02-16 16:10:23 -08001599 * @return true if the update was successful, false otherwise
1600 */
Vikram Aggarwal60069912012-07-24 14:26:09 -07001601 private boolean updateAccounts(Cursor accounts) {
Vikram Aggarwal7dedb952012-02-16 16:10:23 -08001602 if (accounts == null || !accounts.moveToFirst()) {
1603 return false;
1604 }
Paul Westbrook23b74b92012-02-29 11:36:12 -08001605
Vikram Aggarwala9b93f32012-02-23 14:51:58 -08001606 final Account[] allAccounts = Account.getAllAccounts(accounts);
Vikram Aggarwal60069912012-07-24 14:26:09 -07001607 // A match for the current account's URI in the list of accounts.
1608 Account currentFromList = null;
Paul Westbrook23b74b92012-02-29 11:36:12 -08001609
1610 // Save the uris for the accounts
1611 mCurrentAccountUris.clear();
1612 for (Account account : allAccounts) {
Vikram Aggarwal60069912012-07-24 14:26:09 -07001613 LogUtils.d(LOG_TAG, "updateAccounts(%s)", account);
Paul Westbrook23b74b92012-02-29 11:36:12 -08001614 mCurrentAccountUris.add(account.uri);
Vikram Aggarwal60069912012-07-24 14:26:09 -07001615 if (mAccount != null && account.uri.equals(mAccount.uri)) {
1616 currentFromList = account;
1617 }
Paul Westbrook23b74b92012-02-29 11:36:12 -08001618 }
1619
Vikram Aggarwal60069912012-07-24 14:26:09 -07001620 // 1. current account is already set and is in allAccounts:
1621 // 1a. It has changed -> load the updated account.
1622 // 2b. It is unchanged -> no-op
Andy Huang0d647352012-03-21 21:48:16 -07001623 // 2. current account is set and is not in allAccounts -> pick first (acct was deleted?)
Vikram Aggarwal60069912012-07-24 14:26:09 -07001624 // 3. saved preference has an account -> pick that one
Andy Huang0d647352012-03-21 21:48:16 -07001625 // 4. otherwise just pick first
1626
Vikram Aggarwal60069912012-07-24 14:26:09 -07001627 boolean accountChanged = false;
1628 /// Assume case 4, initialize to first account, and see if we can find anything better.
1629 Account newAccount = allAccounts[0];
1630 if (currentFromList != null) {
1631 // Case 1: Current account exists but has changed
1632 if (!currentFromList.equals(mAccount)) {
1633 newAccount = currentFromList;
1634 accountChanged = true;
Andy Huang0d647352012-03-21 21:48:16 -07001635 }
Vikram Aggarwal60069912012-07-24 14:26:09 -07001636 // Case 1b: else, current account is unchanged: nothing to do.
Paul Westbrook23b74b92012-02-29 11:36:12 -08001637 } else {
Vikram Aggarwal60069912012-07-24 14:26:09 -07001638 // Case 2: Current account is not in allAccounts, the account needs to change.
1639 accountChanged = true;
1640 if (mAccount == null) {
1641 // Case 3: Check for last viewed account, and check if it exists in the list.
1642 final String lastAccountUri = MailAppProvider.getInstance().getLastViewedAccount();
1643 if (lastAccountUri != null) {
1644 for (final Account account : allAccounts) {
1645 if (lastAccountUri.equals(account.uri.toString())) {
1646 newAccount = account;
1647 break;
1648 }
Andy Huang0d647352012-03-21 21:48:16 -07001649 }
1650 }
1651 }
Paul Westbrook23b74b92012-02-29 11:36:12 -08001652 }
Vikram Aggarwal60069912012-07-24 14:26:09 -07001653 if (accountChanged) {
1654 onAccountChanged(newAccount);
1655 }
1656 // Whether we have updated the current account or not, we need to update the list of
1657 // accounts in the ActionBar.
Vikram Aggarwala9b93f32012-02-23 14:51:58 -08001658 mActionBarView.setAccounts(allAccounts);
1659 return (allAccounts.length > 0);
Vikram Aggarwal7dedb952012-02-16 16:10:23 -08001660 }
1661
Paul Westbrook6ead20d2012-03-19 14:48:14 -07001662 private void disableNotifications() {
1663 mNewEmailReceiver.activate(mContext, this);
1664 }
1665
1666 private void enableNotifications() {
1667 mNewEmailReceiver.deactivate();
1668 }
1669
1670 private void disableNotificationsOnAccountChange(Account account) {
1671 // If the new mail suppression receiver is activated for a different account, we want to
1672 // activate it for the new account.
1673 if (mNewEmailReceiver.activated() &&
1674 !mNewEmailReceiver.notificationsDisabledForAccount(account)) {
1675 // Deactivate the current receiver, otherwise multiple receivers may be registered.
1676 mNewEmailReceiver.deactivate();
1677 mNewEmailReceiver.activate(mContext, this);
1678 }
1679 }
1680
Vikram Aggarwala9b93f32012-02-23 14:51:58 -08001681 /**
1682 * {@inheritDoc}
1683 */
Vikram Aggarwal7dedb952012-02-16 16:10:23 -08001684 @Override
1685 public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
Mindy Pereira5cb0c3e2012-02-22 15:22:47 -08001686 // We want to reinitialize only if we haven't ever been initialized, or
1687 // if the current account has vanished.
Paul Westbrooke3e84292012-03-05 16:19:30 -08001688 if (data == null) {
Vikram Aggarwalfa4b47e2012-03-09 13:02:46 -08001689 LogUtils.e(LOG_TAG, "Received null cursor from loader id: %d", loader.getId());
Paul Westbrooke3e84292012-03-05 16:19:30 -08001690 }
Vikram Aggarwalfa4b47e2012-03-09 13:02:46 -08001691 switch (loader.getId()) {
1692 case LOADER_ACCOUNT_CURSOR:
Vikram Aggarwal27d89ad2012-06-12 13:38:40 -07001693 // If the account list is not null, and the account list cursor is empty,
Paul Westbrook2388c5d2012-03-25 12:29:11 -07001694 // we need to start the specified activity.
1695 if (data != null && data.getCount() == 0) {
1696 // If an empty cursor is returned, the MailAppProvider is indicating that
1697 // no accounts have been specified. We want to navigate to the "add account"
1698 // activity that will handle the intent returned by the MailAppProvider
1699
1700 // If the MailAppProvider believes that all accounts have been loaded, and the
1701 // account list is still empty, we want to prompt the user to add an account
1702 final Bundle extras = data.getExtras();
1703 final boolean accountsLoaded =
1704 extras.getInt(AccountCursorExtraKeys.ACCOUNTS_LOADED) != 0;
1705
1706 if (accountsLoaded) {
1707 final Intent noAccountIntent = MailAppProvider.getNoAccountIntent(mContext);
1708 if (noAccountIntent != null) {
1709 mActivity.startActivityForResult(noAccountIntent,
1710 ADD_ACCOUNT_REQUEST_CODE);
1711 }
1712 }
1713 } else {
1714 final boolean accountListUpdated = accountsUpdated(data);
1715 if (!isLoaderInitialized || accountListUpdated) {
Vikram Aggarwal60069912012-07-24 14:26:09 -07001716 isLoaderInitialized = updateAccounts(data);
Paul Westbrook2388c5d2012-03-25 12:29:11 -07001717 }
Vikram Aggarwalfa4b47e2012-03-09 13:02:46 -08001718 }
1719 break;
Paul Westbrook2d50bcd2012-04-10 11:53:47 -07001720 case LOADER_ACCOUNT_UPDATE_CURSOR:
1721 // We have gotten an update for current account.
1722
Vikram Aggarwal60069912012-07-24 14:26:09 -07001723 // Make sure that this is an update for the current account
Paul Westbrook2d50bcd2012-04-10 11:53:47 -07001724 if (data != null && data.moveToFirst()) {
1725 final Account updatedAccount = new Account(data);
1726
1727 if (updatedAccount.uri.equals(mAccount.uri)) {
Paul Westbrookca08fc12012-07-31 12:01:15 -07001728 // Keep a reference to the previous settings object
1729 final Settings previousSettings = mAccount.settings;
1730
Vikram Aggarwal7d816002012-04-17 17:06:41 -07001731 // Update the controller's reference to the current account
Paul Westbrook2d50bcd2012-04-10 11:53:47 -07001732 mAccount = updatedAccount;
Vikram Aggarwaledc137c2012-04-24 13:40:58 -07001733 LogUtils.d(LOG_TAG, "AbstractActivityController.onLoadFinished(): "
1734 + "mAccount = %s", mAccount.uri);
Paul Westbrookca08fc12012-07-31 12:01:15 -07001735
1736 // Only notify about a settings change if something differs
1737 if (!Objects.equal(mAccount.settings, previousSettings)) {
Vikram Aggarwal7c401b72012-08-13 16:43:47 -07001738 mAccountObservers.notifyChanged();
Paul Westbrookca08fc12012-07-31 12:01:15 -07001739 }
Paul Westbrook2d50bcd2012-04-10 11:53:47 -07001740
1741 // Got an update for the current account
1742 final boolean inWaitingMode = inWaitMode();
1743 if (!updatedAccount.isAccountIntialized() && !inWaitingMode) {
1744 // Transition to waiting mode
1745 showWaitForInitialization();
Mindy Pereira830bdaf2012-07-11 12:59:55 -07001746 } else if (updatedAccount.isAccountIntialized()) {
1747 if (inWaitingMode) {
1748 // Dismiss waiting mode
1749 hideWaitForInitialization();
1750 }
Paul Westbrook2d50bcd2012-04-10 11:53:47 -07001751 } else if (!updatedAccount.isAccountIntialized() && inWaitingMode) {
1752 // Update the WaitFragment's account object
1753 updateWaitMode();
1754 }
1755 } else {
1756 LogUtils.e(LOG_TAG, "Got update for account: %s with current account: %s",
1757 updatedAccount.uri, mAccount.uri);
1758 // We need to restart the loader, so the correct account information will
1759 // be returned
1760 restartOptionalLoader(LOADER_ACCOUNT_UPDATE_CURSOR);
1761 }
1762 }
1763 break;
Vikram Aggarwalfa4b47e2012-03-09 13:02:46 -08001764 case LOADER_FOLDER_CURSOR:
Vikram Aggarwalfa4b47e2012-03-09 13:02:46 -08001765 // Check status of the cursor.
Marc Blankfd9d0b82012-04-23 16:01:51 -07001766 if (data != null && data.moveToFirst()) {
Andy Huang090db1e2012-07-25 13:25:28 -07001767 final Folder folder = new Folder(data);
Marc Blankfd9d0b82012-04-23 16:01:51 -07001768 LogUtils.d(LOG_TAG, "FOLDER STATUS = %d", folder.syncStatus);
Andy Huang090db1e2012-07-25 13:25:28 -07001769
1770 mFolder = folder;
1771 mFolderObservable.notifyChanged();
1772
Paul Westbrookc808fac2012-02-22 16:42:18 -08001773 } else {
Marc Blankfd9d0b82012-04-23 16:01:51 -07001774 LogUtils.d(LOG_TAG, "Unable to get the folder %s",
1775 mFolder != null ? mAccount.name : "");
Mindy Pereira11dd5ef2012-03-10 15:10:18 -08001776 }
Vikram Aggarwalfa4b47e2012-03-09 13:02:46 -08001777 break;
Vikram Aggarwalfa4b47e2012-03-09 13:02:46 -08001778 case LOADER_RECENT_FOLDERS:
Vikram Aggarwal27d89ad2012-06-12 13:38:40 -07001779 // No recent folders and we are running on a phone? Populate the default recents.
1780 if (data != null && data.getCount() == 0 && !Utils.useTabletUI(mContext)) {
1781 final class PopulateDefault extends AsyncTask<Uri, Void, Void> {
1782 @Override
1783 protected Void doInBackground(Uri... uri) {
1784 // Asking for an update on the URI and ignore the result.
1785 final ContentResolver resolver = mContext.getContentResolver();
1786 resolver.update(uri[0], null, null, null);
1787 return null;
1788 }
1789 }
1790 final Uri uri = mAccount.defaultRecentFolderListUri;
1791 LogUtils.v(LOG_TAG, "Default recents at %s", uri);
1792 new PopulateDefault().execute(uri);
1793 break;
1794 }
1795 LogUtils.v(LOG_TAG, "Reading recent folders from the cursor.");
Vikram Aggarwalfa4b47e2012-03-09 13:02:46 -08001796 mRecentFolderList.loadFromUiProvider(data);
Vikram Aggarwal37263972012-04-17 15:51:14 -07001797 mActionBarView.requestRecentFoldersAndRedraw();
Vikram Aggarwalfa4b47e2012-03-09 13:02:46 -08001798 break;
Mindy Pereiraab486362012-03-21 18:18:53 -07001799 case LOADER_ACCOUNT_INBOX:
Marc Blankfd9d0b82012-04-23 16:01:51 -07001800 if (data != null && !data.isClosed() && data.moveToFirst()) {
Mindy Pereira0e88e9f2012-03-25 13:47:41 -07001801 Folder inbox = new Folder(data);
1802 onFolderChanged(inbox);
Mindy Pereirab4a43282012-03-23 16:20:03 -07001803 // Just want to get the inbox, don't care about updates to it
1804 // as this will be tracked by the folder change listener.
1805 mActivity.getLoaderManager().destroyLoader(LOADER_ACCOUNT_INBOX);
Mindy Pereira5ba33802012-03-26 16:30:11 -07001806 } else {
1807 LogUtils.d(LOG_TAG, "Unable to get the account inbox for account %s",
1808 mAccount != null ? mAccount.name : "");
Mindy Pereirab4a43282012-03-23 16:20:03 -07001809 }
Mindy Pereiraab486362012-03-21 18:18:53 -07001810 break;
1811 case LOADER_SEARCH:
1812 data.moveToFirst();
1813 Folder search = new Folder(data);
Mindy Pereira11e35962012-06-01 14:49:46 -07001814 updateFolder(search);
Mindy Pereiraab486362012-03-21 18:18:53 -07001815 mConvListContext = ConversationListContext.forSearchQuery(mAccount, mFolder,
Mindy Pereirad660d252012-03-26 11:48:43 -07001816 mActivity.getIntent()
1817 .getStringExtra(UIProvider.SearchQueryParameters.QUERY));
Mindy Pereiraab486362012-03-21 18:18:53 -07001818 showConversationList(mConvListContext);
1819 mActivity.invalidateOptionsMenu();
Mindy Pereira3b399222012-03-28 15:19:47 -07001820 mActivity.getLoaderManager().destroyLoader(LOADER_SEARCH);
Mindy Pereiraab486362012-03-21 18:18:53 -07001821 break;
Vikram Aggarwal7dedb952012-02-16 16:10:23 -08001822 }
1823 }
1824
Vikram Aggarwalc7694222012-04-23 13:37:01 -07001825 /**
1826 * Destructive actions on Conversations. This class should only be created by controllers, and
1827 * clients should only require {@link DestructiveAction}s, not specific implementations of the.
1828 * Only the controllers should know what kind of destructive actions are being created.
1829 */
Mindy Pereirade3e74a2012-07-24 09:43:10 -07001830 public class ConversationAction implements DestructiveAction {
Vikram Aggarwalacaa3c02012-04-24 12:45:27 -07001831 /**
1832 * The action to be performed. This is specified as the resource ID of the menu item
1833 * corresponding to this action: R.id.delete, R.id.report_spam, etc.
1834 */
Vikram Aggarwal7f602f72012-04-30 16:04:06 -07001835 private final int mAction;
Vikram Aggarwalbc67bb12012-04-30 14:05:35 -07001836 /** The action will act upon these conversations */
Paul Westbrook77eee622012-07-10 13:41:57 -07001837 private final Collection<Conversation> mTarget;
Vikram Aggarwal7f602f72012-04-30 16:04:06 -07001838 /** Whether this destructive action has already been performed */
1839 private boolean mCompleted;
Vikram Aggarwalc4113952012-05-11 14:14:56 -07001840 /** Whether this is an action on the currently selected set. */
1841 private final boolean mIsSelectedSet;
Vikram Aggarwale8a85322012-04-24 09:01:38 -07001842
Mindy Pereirafbe40192012-03-20 10:40:45 -07001843 /**
1844 * Create a listener object. action is one of four constants: R.id.y_button (archive),
1845 * R.id.delete , R.id.mute, and R.id.report_spam.
1846 * @param action
Vikram Aggarwalbc67bb12012-04-30 14:05:35 -07001847 * @param target Conversation that we want to apply the action to.
Vikram Aggarwalc4113952012-05-11 14:14:56 -07001848 * @param isBatch whether the conversations are in the currently selected batch set.
1849 */
1850 public ConversationAction(int action, Collection<Conversation> target, boolean isBatch) {
Mindy Pereirafbe40192012-03-20 10:40:45 -07001851 mAction = action;
Paul Westbrook77eee622012-07-10 13:41:57 -07001852 mTarget = ImmutableList.copyOf(target);
Vikram Aggarwalc4113952012-05-11 14:14:56 -07001853 mIsSelectedSet = isBatch;
Mindy Pereirafbe40192012-03-20 10:40:45 -07001854 }
1855
Vikram Aggarwalacaa3c02012-04-24 12:45:27 -07001856 /**
1857 * The action common to child classes. This performs the action specified in the constructor
1858 * on the conversations given here.
Vikram Aggarwalacaa3c02012-04-24 12:45:27 -07001859 */
Vikram Aggarwalbc67bb12012-04-30 14:05:35 -07001860 @Override
1861 public void performAction() {
Vikram Aggarwal7f602f72012-04-30 16:04:06 -07001862 if (isPerformed()) {
1863 return;
1864 }
Mindy Pereira3cb28b52012-05-24 15:26:39 -07001865 boolean undoEnabled = mAccount.supportsCapability(AccountCapabilities.UNDO);
Vikram Aggarwal7dd054e2012-05-21 14:43:10 -07001866
1867 // Are we destroying the currently shown conversation? Show the next one.
1868 if (LogUtils.isLoggable(LOG_TAG, LogUtils.DEBUG)){
Vikram Aggarwal8742a612012-08-13 10:22:50 -07001869 LogUtils.d(LOG_TAG, "ConversationAction.performAction():"
1870 + "\nmTarget=%s\nCurrent=%s",
Vikram Aggarwal7dd054e2012-05-21 14:43:10 -07001871 Conversation.toString(mTarget), mCurrentConversation);
1872 }
Vikram Aggarwal7dd054e2012-05-21 14:43:10 -07001873
Paul Westbrooke1221d22012-08-19 11:09:07 -07001874 if (mConversationListCursor == null) {
1875 LogUtils.e(LOG_TAG, "null ConversationCursor in ConversationAction.performAction():"
1876 + "\nmTarget=%s\nCurrent=%s",
1877 Conversation.toString(mTarget), mCurrentConversation);
1878 return;
1879 }
1880
Mindy Pereirafbe40192012-03-20 10:40:45 -07001881 switch (mAction) {
Mindy Pereira0692baf2012-03-23 17:34:31 -07001882 case R.id.archive:
Vikram Aggarwal7dd054e2012-05-21 14:43:10 -07001883 LogUtils.d(LOG_TAG, "Archiving");
Vikram Aggarwalbc67bb12012-04-30 14:05:35 -07001884 mConversationListCursor.archive(mContext, mTarget);
Mindy Pereirafbe40192012-03-20 10:40:45 -07001885 break;
1886 case R.id.delete:
Vikram Aggarwal7dd054e2012-05-21 14:43:10 -07001887 LogUtils.d(LOG_TAG, "Deleting");
Vikram Aggarwalbc67bb12012-04-30 14:05:35 -07001888 mConversationListCursor.delete(mContext, mTarget);
Mindy Pereira695d6962012-06-18 13:02:10 -07001889 if (mFolder.supportsCapability(FolderCapabilities.DELETE_ACTION_FINAL)) {
Marc Blank386243f2012-05-25 10:40:59 -07001890 undoEnabled = false;
1891 }
Mindy Pereirafbe40192012-03-20 10:40:45 -07001892 break;
1893 case R.id.mute:
Vikram Aggarwal7dd054e2012-05-21 14:43:10 -07001894 LogUtils.d(LOG_TAG, "Muting");
Vikram Aggarwalbc67bb12012-04-30 14:05:35 -07001895 if (mFolder.supportsCapability(FolderCapabilities.DESTRUCTIVE_MUTE)) {
Vikram Aggarwal7f602f72012-04-30 16:04:06 -07001896 for (Conversation c : mTarget) {
1897 c.localDeleteOnUpdate = true;
1898 }
Vikram Aggarwalbc67bb12012-04-30 14:05:35 -07001899 }
1900 mConversationListCursor.mute(mContext, mTarget);
Mindy Pereirafbe40192012-03-20 10:40:45 -07001901 break;
1902 case R.id.report_spam:
Vikram Aggarwal7dd054e2012-05-21 14:43:10 -07001903 LogUtils.d(LOG_TAG, "Reporting spam");
Vikram Aggarwalbc67bb12012-04-30 14:05:35 -07001904 mConversationListCursor.reportSpam(mContext, mTarget);
1905 break;
Paul Westbrook77eee622012-07-10 13:41:57 -07001906 case R.id.mark_not_spam:
1907 LogUtils.d(LOG_TAG, "Marking not spam");
1908 mConversationListCursor.reportNotSpam(mContext, mTarget);
1909 break;
Paul Westbrook76b20622012-07-12 11:45:43 -07001910 case R.id.report_phishing:
1911 LogUtils.d(LOG_TAG, "Reporting phishing");
1912 mConversationListCursor.reportPhishing(mContext, mTarget);
1913 break;
Vikram Aggarwalbc67bb12012-04-30 14:05:35 -07001914 case R.id.remove_star:
Vikram Aggarwal7dd054e2012-05-21 14:43:10 -07001915 LogUtils.d(LOG_TAG, "Removing star");
Vikram Aggarwalbc67bb12012-04-30 14:05:35 -07001916 // Star removal is destructive in the Starred folder.
1917 mConversationListCursor.updateBoolean(mContext, mTarget,
1918 ConversationColumns.STARRED, false);
1919 break;
1920 case R.id.mark_not_important:
Vikram Aggarwal7dd054e2012-05-21 14:43:10 -07001921 LogUtils.d(LOG_TAG, "Marking not-important");
Mindy Pereira445be212012-08-15 08:50:10 -07001922 // Marking not important is destructive in a mailbox
1923 // containing only important messages
Mindy Pereira0d03ef82012-08-15 09:05:48 -07001924 if (mFolder != null && mFolder.isImportantOnly()) {
Mindy Pereira445be212012-08-15 08:50:10 -07001925 for (Conversation conv : mTarget) {
1926 conv.localDeleteOnUpdate = true;
1927 }
1928 }
Vikram Aggarwalbc67bb12012-04-30 14:05:35 -07001929 mConversationListCursor.updateInt(mContext, mTarget,
1930 ConversationColumns.PRIORITY, UIProvider.ConversationPriority.LOW);
Mindy Pereirafbe40192012-03-20 10:40:45 -07001931 break;
Paul Westbrookef362542012-08-27 14:53:32 -07001932 case R.id.discard_drafts:
1933 LogUtils.d(LOG_TAG, "Discarding draft messages");
1934 // Discarding draft messages is destructive in a "draft" mailbox
1935 if (mFolder != null && mFolder.isDraft()) {
1936 for (Conversation conv : mTarget) {
1937 conv.localDeleteOnUpdate = true;
1938 }
1939 }
1940 mConversationListCursor.discardDrafts(mContext, mTarget);
1941 // We don't support undoing discarding drafts
1942 undoEnabled = false;
1943 break;
Vikram Aggarwal3d7ca9d2012-05-11 14:40:36 -07001944 }
1945 if (undoEnabled) {
mindypead50392012-08-23 11:03:53 -07001946 mHandler.postDelayed(new Runnable() {
1947 @Override
1948 public void run() {
1949 onUndoAvailable(new ToastBarOperation(mTarget.size(), mAction,
1950 ToastBarOperation.UNDO));
1951 }
1952 }, mShowUndoBarDelay);
Vikram Aggarwal3d7ca9d2012-05-11 14:40:36 -07001953 }
Vikram Aggarwalbc67bb12012-04-30 14:05:35 -07001954 refreshConversationList();
Vikram Aggarwalc4113952012-05-11 14:14:56 -07001955 if (mIsSelectedSet) {
Vikram Aggarwalc4113952012-05-11 14:14:56 -07001956 mSelectedSet.clear();
1957 }
Mindy Pereirafbe40192012-03-20 10:40:45 -07001958 }
Vikram Aggarwal7f602f72012-04-30 16:04:06 -07001959
1960 /**
1961 * Returns true if this action has been performed, false otherwise.
Andy Huang839ada22012-07-20 15:48:40 -07001962 *
Vikram Aggarwal7f602f72012-04-30 16:04:06 -07001963 */
1964 private synchronized boolean isPerformed() {
1965 if (mCompleted) {
1966 return true;
1967 }
1968 mCompleted = true;
1969 return false;
1970 }
Mindy Pereirafbe40192012-03-20 10:40:45 -07001971 }
Mindy Pereirae5f4dc02012-03-21 16:08:53 -07001972
Vikram Aggarwal3d7ca9d2012-05-11 14:40:36 -07001973 /**
1974 * Get a destructive action for a menu action.
1975 * This is a temporary method, to control the profusion of {@link DestructiveAction} classes
1976 * that are created. Please do not copy this paradigm.
1977 * @param action the resource ID of the menu action: R.id.delete, for example
1978 * @param target the conversations to act upon.
1979 * @return a {@link DestructiveAction} that performs the specified action.
1980 */
Vikram Aggarwal531488e2012-05-29 16:36:52 -07001981 private final DestructiveAction getAction(int action, Collection<Conversation> target) {
Vikram Aggarwal3d7ca9d2012-05-11 14:40:36 -07001982 final DestructiveAction da = new ConversationAction(action, target, false);
1983 registerDestructiveAction(da);
1984 return da;
1985 }
1986
Vikram Aggarwald503df42012-05-11 10:13:35 -07001987 // Called from the FolderSelectionDialog after a user is done selecting folders to assign the
1988 // conversations to.
Mindy Pereirae5f4dc02012-03-21 16:08:53 -07001989 @Override
Mindy Pereira8db7e402012-07-13 10:32:47 -07001990 public final void assignFolder(Collection<FolderOperation> folderOps,
1991 Collection<Conversation> target, boolean batch, boolean showUndo) {
1992 // Actions are destructive only when the current folder can be assigned
1993 // to (which is the same as being able to un-assign a conversation from the folder) and
1994 // when the list of folders contains the current folder.
1995 final boolean isDestructive = mFolder
1996 .supportsCapability(FolderCapabilities.CAN_ACCEPT_MOVED_MESSAGES)
1997 && FolderOperation.isDestructive(folderOps, mFolder);
Vikram Aggarwald503df42012-05-11 10:13:35 -07001998 LogUtils.d(LOG_TAG, "onFolderChangesCommit: isDestructive = %b", isDestructive);
1999 if (isDestructive) {
2000 for (final Conversation c : target) {
2001 c.localDeleteOnUpdate = true;
Mindy Pereira6778f462012-03-23 18:01:55 -07002002 }
Mindy Pereirae5f4dc02012-03-21 16:08:53 -07002003 }
Mindy Pereira8db7e402012-07-13 10:32:47 -07002004 final DestructiveAction folderChange = getFolderChange(target, folderOps, isDestructive,
Mindy Pereira06642fa2012-07-12 16:23:27 -07002005 batch, showUndo);
Vikram Aggarwald503df42012-05-11 10:13:35 -07002006 // Update the UI elements depending no their visibility and availability
Vikram Aggarwal3d7ca9d2012-05-11 14:40:36 -07002007 // TODO(viki): Consolidate this into a single method requestDelete.
Vikram Aggarwald503df42012-05-11 10:13:35 -07002008 if (isDestructive) {
Vikram Aggarwal4f4782b2012-05-30 08:39:09 -07002009 delete(target, folderChange);
Vikram Aggarwal6902dcf2012-04-11 08:57:42 -07002010 } else {
Vikram Aggarwald503df42012-05-11 10:13:35 -07002011 requestUpdate(target, folderChange);
Mindy Pereirae5f4dc02012-03-21 16:08:53 -07002012 }
2013 }
2014
Mindy Pereira967ede62012-03-22 09:29:09 -07002015 @Override
Vikram Aggarwal3d7ca9d2012-05-11 14:40:36 -07002016 public final void onRefreshRequired() {
mindyp5390fca2012-08-22 12:12:25 -07002017 if (isAnimating() || isDragging()) {
Mindy Pereira69e88dd2012-08-10 09:30:18 -07002018 LogUtils.d(LOG_TAG, "onRefreshRequired: delay until animating done");
2019 return;
2020 }
Marc Blankbf128eb2012-04-18 15:58:45 -07002021 // Refresh the query in the background
Paul Westbrookcff1aea2012-08-10 11:51:00 -07002022 if (mConversationListCursor.isRefreshRequired()) {
2023 mConversationListCursor.refresh();
2024 }
Marc Blankbf128eb2012-04-18 15:58:45 -07002025 }
2026
mindyp5390fca2012-08-22 12:12:25 -07002027 @Override
2028 public void startDragMode() {
2029 mIsDragHappening = true;
2030 }
2031
2032 @Override
2033 public void stopDragMode() {
2034 mIsDragHappening = false;
2035 if (mConversationListCursor.isRefreshReady()) {
2036 LogUtils.d(LOG_TAG, "Stopped animating: try sync");
2037 onRefreshReady();
2038 }
2039
2040 if (mConversationListCursor.isRefreshRequired()) {
2041 LogUtils.d(LOG_TAG, "Stopped animating: refresh");
2042 mConversationListCursor.refresh();
2043 }
2044 }
2045
2046 private boolean isDragging() {
2047 return mIsDragHappening;
2048 }
2049
Mindy Pereira69e88dd2012-08-10 09:30:18 -07002050 private boolean isAnimating() {
2051 boolean isAnimating = false;
2052 ConversationListFragment convListFragment = getConversationListFragment();
2053 if (convListFragment != null) {
2054 AnimatedAdapter adapter = convListFragment.getAnimatedAdapter();
2055 if (adapter != null) {
2056 isAnimating = adapter.isAnimating();
2057 }
2058 }
2059 return isAnimating;
2060 }
2061
Marc Blankbf128eb2012-04-18 15:58:45 -07002062 /**
2063 * Called when the {@link ConversationCursor} is changed or has new data in it.
2064 * <p>
2065 * {@inheritDoc}
2066 */
2067 @Override
Vikram Aggarwal3d7ca9d2012-05-11 14:40:36 -07002068 public final void onRefreshReady() {
Paul Westbrookcff1aea2012-08-10 11:51:00 -07002069 if (!isAnimating()) {
Marc Blankbf128eb2012-04-18 15:58:45 -07002070 // Swap cursors
2071 mConversationListCursor.sync();
Marc Blankbf128eb2012-04-18 15:58:45 -07002072 }
Paul Westbrook937c94f2012-08-16 13:01:18 -07002073 mTracker.onCursorUpdated();
Marc Blankbf128eb2012-04-18 15:58:45 -07002074 }
2075
2076 @Override
Vikram Aggarwal3d7ca9d2012-05-11 14:40:36 -07002077 public final void onDataSetChanged() {
Paul Westbrook9f119c72012-04-24 16:10:59 -07002078 updateConversationListFragment();
Andy Huang632721e2012-04-11 16:57:26 -07002079 mConversationListObservable.notifyChanged();
Marc Blankbf128eb2012-04-18 15:58:45 -07002080 }
2081
Vikram Aggarwal3d7ca9d2012-05-11 14:40:36 -07002082 /**
2083 * If the Conversation List Fragment is visible, updates the fragment.
2084 */
2085 private final void updateConversationListFragment() {
Marc Blankbf128eb2012-04-18 15:58:45 -07002086 final ConversationListFragment convList = getConversationListFragment();
2087 if (convList != null) {
Vikram Aggarwal75daee52012-04-30 11:13:09 -07002088 refreshConversationList();
Paul Westbrook9f119c72012-04-24 16:10:59 -07002089 if (convList.isVisible()) {
Paul Westbrook9f119c72012-04-24 16:10:59 -07002090 Utils.setConversationCursorVisibility(mConversationListCursor, true);
2091 }
Marc Blankbf128eb2012-04-18 15:58:45 -07002092 }
2093 }
2094
2095 /**
2096 * This class handles throttled refresh of the conversation list
2097 */
2098 static class RefreshTimerTask extends TimerTask {
2099 final Handler mHandler;
2100 final AbstractActivityController mController;
2101
2102 RefreshTimerTask(AbstractActivityController controller, Handler handler) {
2103 mHandler = handler;
2104 mController = controller;
2105 }
2106
2107 @Override
2108 public void run() {
2109 mHandler.post(new Runnable() {
2110 @Override
2111 public void run() {
2112 LogUtils.d(LOG_TAG, "Delay done... calling onRefreshRequired");
2113 mController.onRefreshRequired();
2114 }});
2115 }
2116 }
2117
2118 /**
2119 * Cancel the refresh task, if it's running
2120 */
2121 private void cancelRefreshTask () {
2122 if (mConversationListRefreshTask != null) {
2123 mConversationListRefreshTask.cancel();
2124 mConversationListRefreshTask = null;
2125 }
2126 }
2127
2128 @Override
Paul Westbrookcff1aea2012-08-10 11:51:00 -07002129 public void onAnimationEnd(AnimatedAdapter animatedAdapter) {
2130 if (mConversationListCursor.isRefreshReady()) {
mindyp52544862012-08-20 12:05:36 -07002131 LogUtils.d(LOG_TAG, "Stopped animating: try sync");
Paul Westbrookcff1aea2012-08-10 11:51:00 -07002132 onRefreshReady();
Marc Blankbf128eb2012-04-18 15:58:45 -07002133 }
Marc Blankbf128eb2012-04-18 15:58:45 -07002134
Paul Westbrookcff1aea2012-08-10 11:51:00 -07002135 if (mConversationListCursor.isRefreshRequired()) {
mindyp52544862012-08-20 12:05:36 -07002136 LogUtils.d(LOG_TAG, "Stopped animating: refresh");
Paul Westbrookcff1aea2012-08-10 11:51:00 -07002137 mConversationListCursor.refresh();
2138 }
Marc Blankbf128eb2012-04-18 15:58:45 -07002139 }
2140
2141 @Override
Mindy Pereira967ede62012-03-22 09:29:09 -07002142 public void onSetEmpty() {
Mindy Pereira967ede62012-03-22 09:29:09 -07002143 }
2144
2145 @Override
2146 public void onSetPopulated(ConversationSelectionSet set) {
Vikram Aggarwal6902dcf2012-04-11 08:57:42 -07002147 final ConversationListFragment convList = getConversationListFragment();
2148 if (convList == null) {
2149 return;
2150 }
Vikram Aggarwal7c401b72012-08-13 16:43:47 -07002151 mCabActionMenu = new SelectedConversationsActionMenu(mActivity, set, mFolder,
Vikram Aggarwal531488e2012-05-29 16:36:52 -07002152 (SwipeableListView) convList.getListView());
Vikram Aggarwale128fc22012-04-04 12:33:34 -07002153 enableCabMode();
Mindy Pereira967ede62012-03-22 09:29:09 -07002154 }
2155
Mindy Pereira967ede62012-03-22 09:29:09 -07002156 @Override
2157 public void onSetChanged(ConversationSelectionSet set) {
2158 // Do nothing. We don't care about changes to the set.
2159 }
2160
2161 @Override
2162 public ConversationSelectionSet getSelectedSet() {
2163 return mSelectedSet;
2164 }
2165
Vikram Aggarwale128fc22012-04-04 12:33:34 -07002166 /**
2167 * Disable the Contextual Action Bar (CAB). The selected set is not changed.
2168 */
2169 protected void disableCabMode() {
Mindy Pereira8937bf12012-07-23 14:05:02 -07002170 // Commit any previous destructive actions when entering/ exiting CAB mode.
mindypc6adce32012-08-22 18:46:42 -07002171 commitDestructiveActions(true);
Vikram Aggarwale128fc22012-04-04 12:33:34 -07002172 if (mCabActionMenu != null) {
2173 mCabActionMenu.deactivate();
2174 }
2175 }
2176
2177 /**
2178 * Re-enable the CAB menu if required. The selection set is not changed.
2179 */
2180 protected void enableCabMode() {
Mindy Pereira8937bf12012-07-23 14:05:02 -07002181 // Commit any previous destructive actions when entering/ exiting CAB mode.
mindypc6adce32012-08-22 18:46:42 -07002182 commitDestructiveActions(true);
Vikram Aggarwale128fc22012-04-04 12:33:34 -07002183 if (mCabActionMenu != null) {
2184 mCabActionMenu.activate();
2185 }
2186 }
2187
Vikram Aggarwal4eb52712012-06-19 16:24:50 -07002188 /**
2189 * Unselect conversations and exit CAB mode.
2190 */
2191 protected final void exitCabMode() {
2192 mSelectedSet.clear();
2193 }
2194
Mindy Pereira967ede62012-03-22 09:29:09 -07002195 @Override
Mindy Pereirafd0c2972012-03-27 13:50:39 -07002196 public void startSearch() {
Vikram Aggarwal35f19d72012-04-24 13:24:48 -07002197 if (mAccount == null) {
2198 // We cannot search if there is no account. Drop the request to the floor.
2199 LogUtils.d(LOG_TAG, "AbstractActivityController.startSearch(): null account");
2200 return;
2201 }
Mindy Pereirafd0c2972012-03-27 13:50:39 -07002202 if (mAccount.supportsCapability(UIProvider.AccountCapabilities.LOCAL_SEARCH)
2203 | mAccount.supportsCapability(UIProvider.AccountCapabilities.SERVER_SEARCH)) {
Vikram Aggarwal70f130e2012-04-03 12:32:14 -07002204 onSearchRequested(mActionBarView.getQuery());
Mindy Pereirafd0c2972012-03-27 13:50:39 -07002205 } else {
2206 Toast.makeText(mActivity.getActivityContext(), mActivity.getActivityContext()
Mindy Pereiraa46c2992012-03-27 14:12:39 -07002207 .getString(R.string.search_unsupported), Toast.LENGTH_SHORT).show();
Mindy Pereirafd0c2972012-03-27 13:50:39 -07002208 }
2209 }
Mindy Pereiraacf60392012-04-06 09:11:00 -07002210
Vikram Aggarwal0dda5732012-04-06 11:20:16 -07002211 @Override
2212 public void exitSearchMode() {
2213 if (mViewMode.getMode() == ViewMode.SEARCH_RESULTS_LIST) {
2214 mActivity.finish();
2215 }
2216 }
2217
Mindy Pereiraacf60392012-04-06 09:11:00 -07002218 /**
2219 * Supports dragging conversations to a folder.
2220 */
2221 @Override
2222 public boolean supportsDrag(DragEvent event, Folder folder) {
2223 return (folder != null
2224 && event != null
2225 && event.getClipDescription() != null
2226 && folder.supportsCapability
2227 (UIProvider.FolderCapabilities.CAN_ACCEPT_MOVED_MESSAGES)
2228 && folder.supportsCapability
2229 (UIProvider.FolderCapabilities.CAN_HOLD_MAIL)
2230 && !mFolder.uri.equals(folder.uri));
2231 }
2232
2233 /**
Mindy Pereira6c2663d2012-07-20 15:37:29 -07002234 * Handles dropping conversations to a folder.
Mindy Pereiraacf60392012-04-06 09:11:00 -07002235 */
2236 @Override
2237 public void handleDrop(DragEvent event, final Folder folder) {
Mindy Pereiraacf60392012-04-06 09:11:00 -07002238 if (!supportsDrag(event, folder)) {
2239 return;
2240 }
Mindy Pereiraacf60392012-04-06 09:11:00 -07002241 final Collection<Conversation> conversations = mSelectedSet.values();
Mindy Pereira8db7e402012-07-13 10:32:47 -07002242 final Collection<FolderOperation> dropTarget = FolderOperation.listOf(new FolderOperation(
2243 folder, true));
2244 // Drag and drop is destructive: we remove conversations from the
2245 // current folder.
Mindy Pereira06642fa2012-07-12 16:23:27 -07002246 final DestructiveAction action = getFolderChange(conversations, dropTarget, true, true,
2247 true);
Vikram Aggarwal4f4782b2012-05-30 08:39:09 -07002248 delete(conversations, action);
Mindy Pereiraacf60392012-04-06 09:11:00 -07002249 }
Mindy Pereira0963ef82012-04-10 11:43:01 -07002250
2251 @Override
Mindy Pereira0963ef82012-04-10 11:43:01 -07002252 public void onTouchEvent(MotionEvent event) {
2253 if (event.getAction() == MotionEvent.ACTION_DOWN) {
Andrew Sappersteinc2c9dc12012-07-02 18:17:32 -07002254 if (mToastBar != null && !mToastBar.isEventInToastBar(event)) {
2255 mToastBar.hide(true);
Mindy Pereira0963ef82012-04-10 11:43:01 -07002256 }
2257 }
2258 }
Andy Huangb1c34dc2012-04-17 16:36:19 -07002259
Andy Huang632721e2012-04-11 16:57:26 -07002260 @Override
2261 public void onConversationSeen(Conversation conv) {
2262 mPagerController.onConversationSeen(conv);
2263 }
2264
Andy Huangb1c34dc2012-04-17 16:36:19 -07002265 private class ConversationListLoaderCallbacks implements
2266 LoaderManager.LoaderCallbacks<ConversationCursor> {
2267
2268 @Override
2269 public Loader<ConversationCursor> onCreateLoader(int id, Bundle args) {
2270 Loader<ConversationCursor> result = new ConversationCursorLoader((Activity) mActivity,
Paul Westbrook9a70e912012-08-17 15:53:20 -07002271 mAccount, mFolder.conversationListUri, mFolder.name);
Andy Huangb1c34dc2012-04-17 16:36:19 -07002272 return result;
2273 }
2274
2275 @Override
2276 public void onLoadFinished(Loader<ConversationCursor> loader, ConversationCursor data) {
Andy Huang632721e2012-04-11 16:57:26 -07002277 LogUtils.d(LOG_TAG, "IN AAC.ConversationCursor.onLoadFinished, data=%s loader=%s",
2278 data, loader);
Vikram Aggarwale8a85322012-04-24 09:01:38 -07002279 // Clear our all pending destructive actions before swapping the conversation cursor
2280 destroyPending(null);
Andy Huangb1c34dc2012-04-17 16:36:19 -07002281 mConversationListCursor = data;
Paul Westbrookbf232c32012-04-18 03:17:41 -07002282 mConversationListCursor.addListener(AbstractActivityController.this);
Andy Huangb1c34dc2012-04-17 16:36:19 -07002283
Paul Westbrook937c94f2012-08-16 13:01:18 -07002284 mTracker.onCursorUpdated();
2285
Andy Huange3df1ad2012-04-24 17:15:23 -07002286 mConversationListObservable.notifyChanged();
Andy Huangb1c34dc2012-04-17 16:36:19 -07002287 // Register the AbstractActivityController as a listener to changes in
2288 // data in the cursor.
2289 final ConversationListFragment convList = getConversationListFragment();
2290 if (convList != null) {
2291 convList.onCursorUpdated();
Paul Westbrook9f119c72012-04-24 16:10:59 -07002292
2293 if (convList.isVisible()) {
2294 // The conversation list is visible.
2295 Utils.setConversationCursorVisibility(mConversationListCursor, true);
2296 }
Andy Huangb1c34dc2012-04-17 16:36:19 -07002297 }
2298 // Shown for search results in two-pane mode only.
2299 if (shouldShowFirstConversation()) {
2300 if (mConversationListCursor.getCount() > 0) {
2301 mConversationListCursor.moveToPosition(0);
Andy Huangb1c34dc2012-04-17 16:36:19 -07002302 final Conversation conv = new Conversation(mConversationListCursor);
2303 conv.position = 0;
Andy Huang1ee96b22012-08-24 20:19:53 -07002304 onConversationSelected(conv, true /* checkSafeToModifyFragments */);
Andy Huangb1c34dc2012-04-17 16:36:19 -07002305 }
2306 }
2307 }
2308
2309 @Override
2310 public void onLoaderReset(Loader<ConversationCursor> loader) {
Paul Westbrook9a70e912012-08-17 15:53:20 -07002311 LogUtils.d(LOG_TAG, "IN AAC.ConversationCursor.onLoaderReset, data=%s loader=%s",
2312 mConversationListCursor, loader);
2313
2314 if (mConversationListCursor != null) {
2315 // Unregister the listener
2316 mConversationListCursor.removeListener(AbstractActivityController.this);
2317 mConversationListCursor = null;
2318
2319 // Inform anyone who is interested about the change
2320 mTracker.onCursorUpdated();
2321 mConversationListObservable.notifyChanged();
2322
2323 final ConversationListFragment convList = getConversationListFragment();
2324 if (convList != null) {
2325 convList.onCursorUpdated();
2326 }
Andy Huangb1c34dc2012-04-17 16:36:19 -07002327 }
Andy Huangb1c34dc2012-04-17 16:36:19 -07002328 }
Paul Westbrookbf232c32012-04-18 03:17:41 -07002329 }
Andy Huangb1c34dc2012-04-17 16:36:19 -07002330
Vikram Aggarwale8a85322012-04-24 09:01:38 -07002331 /**
2332 * Destroy the pending {@link DestructiveAction} till now and assign the given action as the
2333 * next destructive action..
2334 * @param nextAction the next destructive action to be performed. This can be null.
2335 */
2336 private final void destroyPending(DestructiveAction nextAction) {
2337 // If there is a pending action, perform that first.
2338 if (mPendingDestruction != null) {
2339 mPendingDestruction.performAction();
2340 }
2341 mPendingDestruction = nextAction;
2342 }
2343
2344 /**
2345 * Register a destructive action with the controller. This performs the previous destructive
Vikram Aggarwalacaa3c02012-04-24 12:45:27 -07002346 * 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 -07002347 * embellish this method any more.
Vikram Aggarwale8a85322012-04-24 09:01:38 -07002348 * @param action
2349 */
Vikram Aggarwal3d7ca9d2012-05-11 14:40:36 -07002350 private final void registerDestructiveAction(DestructiveAction action) {
Vikram Aggarwale8a85322012-04-24 09:01:38 -07002351 // TODO(viki): This is not a good idea. The best solution is for clients to request a
2352 // destructive action from the controller and for the controller to own the action. This is
2353 // a half-way solution while refactoring DestructiveAction.
2354 destroyPending(action);
2355 return;
2356 }
2357
Vikram Aggarwal531488e2012-05-29 16:36:52 -07002358 @Override
2359 public final DestructiveAction getBatchAction(int action) {
Vikram Aggarwalc4113952012-05-11 14:14:56 -07002360 final DestructiveAction da = new ConversationAction(action, mSelectedSet.values(), true);
Vikram Aggarwale8a85322012-04-24 09:01:38 -07002361 registerDestructiveAction(da);
2362 return da;
2363 }
Vikram Aggarwal41e6e712012-04-24 11:22:57 -07002364
Mindy Pereirade3e74a2012-07-24 09:43:10 -07002365 @Override
2366 public final DestructiveAction getDeferredBatchAction(int action) {
2367 final DestructiveAction da = new ConversationAction(action, mSelectedSet.values(), true);
2368 return da;
2369 }
2370
Vikram Aggarwalbc67bb12012-04-30 14:05:35 -07002371 /**
2372 * Class to change the folders that are assigned to a set of conversations. This is destructive
2373 * because the user can remove the current folder from the conversation, in which case it has
2374 * to be animated away from the current folder.
2375 */
Vikram Aggarwal41e6e712012-04-24 11:22:57 -07002376 private class FolderDestruction implements DestructiveAction {
Paul Westbrook77eee622012-07-10 13:41:57 -07002377 private final Collection<Conversation> mTarget;
Mindy Pereira8db7e402012-07-13 10:32:47 -07002378 private final ArrayList<FolderOperation> mFolderOps = new ArrayList<FolderOperation>();
Vikram Aggarwal41e6e712012-04-24 11:22:57 -07002379 private final boolean mIsDestructive;
Vikram Aggarwal7f602f72012-04-30 16:04:06 -07002380 /** Whether this destructive action has already been performed */
2381 private boolean mCompleted;
Mindy Pereiraf3a45562012-05-24 16:30:19 -07002382 private boolean mIsSelectedSet;
Mindy Pereira06642fa2012-07-12 16:23:27 -07002383 private boolean mShowUndo;
Mindy Pereira01f30502012-08-14 10:30:51 -07002384 private int mAction;
Vikram Aggarwal41e6e712012-04-24 11:22:57 -07002385
2386 /**
2387 * Create a new folder destruction object to act on the given conversations.
2388 * @param target
2389 */
Vikram Aggarwal7f602f72012-04-30 16:04:06 -07002390 private FolderDestruction(final Collection<Conversation> target,
Mindy Pereira8db7e402012-07-13 10:32:47 -07002391 final Collection<FolderOperation> folders, boolean isDestructive, boolean isBatch,
Mindy Pereira01f30502012-08-14 10:30:51 -07002392 boolean showUndo, int action) {
Paul Westbrook77eee622012-07-10 13:41:57 -07002393 mTarget = ImmutableList.copyOf(target);
Mindy Pereira8db7e402012-07-13 10:32:47 -07002394 mFolderOps.addAll(folders);
Vikram Aggarwal41e6e712012-04-24 11:22:57 -07002395 mIsDestructive = isDestructive;
Mindy Pereiraf3a45562012-05-24 16:30:19 -07002396 mIsSelectedSet = isBatch;
Mindy Pereira06642fa2012-07-12 16:23:27 -07002397 mShowUndo = showUndo;
Mindy Pereira01f30502012-08-14 10:30:51 -07002398 mAction = action;
Vikram Aggarwal41e6e712012-04-24 11:22:57 -07002399 }
2400
Vikram Aggarwal41e6e712012-04-24 11:22:57 -07002401 @Override
2402 public void performAction() {
Vikram Aggarwal7f602f72012-04-30 16:04:06 -07002403 if (isPerformed()) {
2404 return;
2405 }
Mindy Pereira06642fa2012-07-12 16:23:27 -07002406 if (mIsDestructive && mShowUndo) {
Mindy Pereirad33674992012-06-25 16:26:30 -07002407 ToastBarOperation undoOp = new ToastBarOperation(mTarget.size(),
Mindy Pereira01f30502012-08-14 10:30:51 -07002408 mAction, ToastBarOperation.UNDO);
Vikram Aggarwal41e6e712012-04-24 11:22:57 -07002409 onUndoAvailable(undoOp);
2410 }
Mindy Pereira8db7e402012-07-13 10:32:47 -07002411 // For each conversation, for each operation, add/ remove the
2412 // appropriate folders.
2413 for (Conversation target : mTarget) {
2414 HashMap<Uri, Folder> targetFolders = Folder
Mindy Pereira68f83842012-07-27 09:43:31 -07002415 .hashMapForFolders(target.getRawFolders());
Mindy Pereira01f30502012-08-14 10:30:51 -07002416 if (mIsDestructive) {
2417 target.localDeleteOnUpdate = true;
2418 }
Mindy Pereira8db7e402012-07-13 10:32:47 -07002419 for (FolderOperation op : mFolderOps) {
2420 if (op.mAdd) {
2421 targetFolders.put(op.mFolder.uri, op.mFolder);
2422 } else {
2423 targetFolders.remove(op.mFolder.uri);
2424 }
2425 }
Mindy Pereira85c4a772012-07-30 10:47:26 -07002426 target.setRawFolders(Folder.getSerializedFolderString(targetFolders.values()));
Mindy Pereira00ffece2012-07-27 08:49:56 -07002427 mConversationListCursor.updateString(mContext, Conversation.listOf(target),
Mindy Pereira85c4a772012-07-30 10:47:26 -07002428 Conversation.UPDATE_FOLDER_COLUMN, target.getRawFoldersString());
Mindy Pereira8db7e402012-07-13 10:32:47 -07002429 }
Vikram Aggarwal7f602f72012-04-30 16:04:06 -07002430 refreshConversationList();
Mindy Pereiraf3a45562012-05-24 16:30:19 -07002431 if (mIsSelectedSet) {
2432 mSelectedSet.clear();
2433 }
Vikram Aggarwal7f602f72012-04-30 16:04:06 -07002434 }
Mindy Pereira8db7e402012-07-13 10:32:47 -07002435
Vikram Aggarwal7f602f72012-04-30 16:04:06 -07002436 /**
2437 * Returns true if this action has been performed, false otherwise.
Andy Huang839ada22012-07-20 15:48:40 -07002438 *
Vikram Aggarwal7f602f72012-04-30 16:04:06 -07002439 */
2440 private synchronized boolean isPerformed() {
2441 if (mCompleted) {
2442 return true;
Vikram Aggarwal41e6e712012-04-24 11:22:57 -07002443 }
Vikram Aggarwal7f602f72012-04-30 16:04:06 -07002444 mCompleted = true;
2445 return false;
Vikram Aggarwal41e6e712012-04-24 11:22:57 -07002446 }
2447 }
Vikram Aggarwal7f602f72012-04-30 16:04:06 -07002448
Vikram Aggarwald503df42012-05-11 10:13:35 -07002449 private final DestructiveAction getFolderChange(Collection<Conversation> target,
Mindy Pereira8db7e402012-07-13 10:32:47 -07002450 Collection<FolderOperation> folders, boolean isDestructive, boolean isBatch,
2451 boolean showUndo) {
Mindy Pereira06642fa2012-07-12 16:23:27 -07002452 final DestructiveAction da = new FolderDestruction(target, folders, isDestructive, isBatch,
Mindy Pereira01f30502012-08-14 10:30:51 -07002453 showUndo, R.id.change_folder);
2454 registerDestructiveAction(da);
2455 return da;
2456 }
2457
2458 @Override
2459 public final DestructiveAction getDeferredRemoveFolder(Collection<Conversation> target,
2460 Folder toRemove, boolean isDestructive, boolean isBatch,
2461 boolean showUndo) {
2462 Collection<FolderOperation> folderOps = new ArrayList<FolderOperation>();
2463 folderOps.add(new FolderOperation(toRemove, false));
2464 return new FolderDestruction(target, folderOps, isDestructive, isBatch,
2465 showUndo, R.id.remove_folder);
2466 }
2467
2468 private final DestructiveAction getRemoveFolder(Collection<Conversation> target,
2469 Folder toRemove, boolean isDestructive, boolean isBatch,
2470 boolean showUndo) {
2471 Collection<FolderOperation> folderOps = new ArrayList<FolderOperation>();
2472 folderOps.add(new FolderOperation(toRemove, false));
2473 DestructiveAction da = new FolderDestruction(target, folderOps, isDestructive, isBatch,
2474 showUndo, R.id.remove_folder);
Vikram Aggarwal41e6e712012-04-24 11:22:57 -07002475 registerDestructiveAction(da);
2476 return da;
2477 }
Vikram Aggarwal75daee52012-04-30 11:13:09 -07002478
Vikram Aggarwal4f4782b2012-05-30 08:39:09 -07002479 @Override
2480 public final void refreshConversationList() {
Vikram Aggarwal75daee52012-04-30 11:13:09 -07002481 final ConversationListFragment convList = getConversationListFragment();
2482 if (convList == null) {
2483 return;
2484 }
2485 convList.requestListRefresh();
2486 }
Andrew Sappersteinc2c9dc12012-07-02 18:17:32 -07002487
2488 protected final ActionClickedListener getUndoClickedListener(
2489 final AnimatedAdapter listAdapter) {
2490 return new ActionClickedListener() {
2491 @Override
2492 public void onActionClicked() {
2493 if (mAccount.undoUri != null) {
2494 // NOTE: We might want undo to return the messages affected, in which case
2495 // the resulting cursor might be interesting...
2496 // TODO: Use UIProvider.SEQUENCE_QUERY_PARAMETER to indicate the set of
2497 // commands to undo
2498 if (mConversationListCursor != null) {
2499 mConversationListCursor.undo(
2500 mActivity.getActivityContext(), mAccount.undoUri);
2501 }
2502 if (listAdapter != null) {
2503 listAdapter.setUndo(true);
2504 }
2505 }
2506 }
2507 };
2508 }
2509
Andrew Sapperstein9d7519d2012-07-16 14:03:53 -07002510 protected final void showErrorToast(final Folder folder, boolean replaceVisibleToast) {
Andrew Sappersteinc2c9dc12012-07-02 18:17:32 -07002511 mToastBar.setConversationMode(false);
Andrew Sapperstein00179f12012-08-09 15:15:40 -07002512
2513 ActionClickedListener listener = null;
2514 int actionTextResourceId;
2515 final int lastSyncResult = folder.lastSyncResult;
2516 switch (lastSyncResult) {
2517 case UIProvider.LastSyncResult.CONNECTION_ERROR:
2518 listener = getRetryClickedListener(folder);
2519 actionTextResourceId = R.string.retry;
2520 break;
2521 case UIProvider.LastSyncResult.AUTH_ERROR:
2522 listener = getSignInClickedListener();
2523 actionTextResourceId = R.string.signin;
2524 break;
2525 case UIProvider.LastSyncResult.SECURITY_ERROR:
2526 return; // Currently we do nothing for security errors.
2527 case UIProvider.LastSyncResult.STORAGE_ERROR:
2528 listener = getStorageErrorClickedListener();
2529 actionTextResourceId = R.string.info;
2530 break;
2531 case UIProvider.LastSyncResult.INTERNAL_ERROR:
2532 listener = getInternalErrorClickedListener();
2533 actionTextResourceId = R.string.report;
2534 break;
2535 default:
2536 return;
2537 }
Andrew Sappersteinc2c9dc12012-07-02 18:17:32 -07002538 mToastBar.show(
Andrew Sapperstein00179f12012-08-09 15:15:40 -07002539 listener,
Andrew Sapperstein5d420962012-07-12 16:43:10 -07002540 R.drawable.ic_alert_white,
Andrew Sappersteinc2c9dc12012-07-02 18:17:32 -07002541 Utils.getSyncStatusText(mActivity.getActivityContext(),
Andrew Sapperstein00179f12012-08-09 15:15:40 -07002542 lastSyncResult),
Andrew Sappersteinc2c9dc12012-07-02 18:17:32 -07002543 false, /* showActionIcon */
Andrew Sapperstein00179f12012-08-09 15:15:40 -07002544 actionTextResourceId,
Andrew Sapperstein9d7519d2012-07-16 14:03:53 -07002545 replaceVisibleToast,
2546 new ToastBarOperation(1, 0, ToastBarOperation.ERROR));
Andrew Sappersteinc2c9dc12012-07-02 18:17:32 -07002547 }
2548
Andrew Sapperstein00179f12012-08-09 15:15:40 -07002549 private ActionClickedListener getRetryClickedListener(final Folder folder) {
Andrew Sappersteinc2c9dc12012-07-02 18:17:32 -07002550 return new ActionClickedListener() {
2551 @Override
2552 public void onActionClicked() {
2553 final Uri uri = folder.refreshUri;
2554
2555 if (uri != null) {
Paul Westbrook4969e0c2012-08-20 14:38:39 -07002556 startAsyncRefreshTask(uri);
Andrew Sappersteinc2c9dc12012-07-02 18:17:32 -07002557 }
2558 }
2559 };
2560 }
Andrew Sapperstein00179f12012-08-09 15:15:40 -07002561
2562 private ActionClickedListener getSignInClickedListener() {
2563 return new ActionClickedListener() {
2564 @Override
2565 public void onActionClicked() {
Paul Westbrook122f7c22012-08-20 17:50:31 -07002566 promptUserForAuthentication(mAccount);
Andrew Sapperstein00179f12012-08-09 15:15:40 -07002567 }
2568 };
2569 }
2570
2571 private ActionClickedListener getStorageErrorClickedListener() {
2572 return new ActionClickedListener() {
2573 @Override
2574 public void onActionClicked() {
Paul Westbrook4969e0c2012-08-20 14:38:39 -07002575 showStorageErrorDialog();
Andrew Sapperstein00179f12012-08-09 15:15:40 -07002576 }
2577 };
2578 }
2579
Paul Westbrook4969e0c2012-08-20 14:38:39 -07002580 private void showStorageErrorDialog() {
2581 DialogFragment fragment = (DialogFragment)
2582 mFragmentManager.findFragmentByTag(SYNC_ERROR_DIALOG_FRAGMENT_TAG);
2583 if (fragment == null) {
2584 fragment = SyncErrorDialogFragment.newInstance();
2585 }
2586 fragment.show(mFragmentManager, SYNC_ERROR_DIALOG_FRAGMENT_TAG);
2587 }
2588
Andrew Sapperstein00179f12012-08-09 15:15:40 -07002589 private ActionClickedListener getInternalErrorClickedListener() {
2590 return new ActionClickedListener() {
2591 @Override
2592 public void onActionClicked() {
Paul Westbrook17beb0b2012-08-20 13:34:37 -07002593 Utils.sendFeedback(
2594 mActivity.getActivityContext(), mAccount, true /* reportingProblem */);
Andrew Sapperstein00179f12012-08-09 15:15:40 -07002595 }
2596 };
2597 }
Paul Westbrook4969e0c2012-08-20 14:38:39 -07002598
2599 @Override
2600 public void onFooterViewErrorActionClick(Folder folder, int errorStatus) {
2601 Uri uri = null;
2602 switch (errorStatus) {
2603 case UIProvider.LastSyncResult.CONNECTION_ERROR:
2604 if (folder != null && folder.refreshUri != null) {
2605 uri = folder.refreshUri;
2606 }
2607 break;
2608 case UIProvider.LastSyncResult.AUTH_ERROR:
Paul Westbrook122f7c22012-08-20 17:50:31 -07002609 promptUserForAuthentication(mAccount);
Paul Westbrook4969e0c2012-08-20 14:38:39 -07002610 return;
2611 case UIProvider.LastSyncResult.SECURITY_ERROR:
2612 return; // Currently we do nothing for security errors.
2613 case UIProvider.LastSyncResult.STORAGE_ERROR:
2614 showStorageErrorDialog();
2615 return;
2616 case UIProvider.LastSyncResult.INTERNAL_ERROR:
2617 Utils.sendFeedback(
2618 mActivity.getActivityContext(), mAccount, true /* reportingProblem */);
2619 return;
2620 default:
2621 return;
2622 }
2623
2624 if (uri != null) {
2625 startAsyncRefreshTask(uri);
2626 }
2627 }
2628
2629 @Override
2630 public void onFooterViewLoadMoreClick(Folder folder) {
2631 if (folder != null && folder.loadMoreUri != null) {
2632 startAsyncRefreshTask(folder.loadMoreUri);
2633 }
2634 }
2635
2636 private void startAsyncRefreshTask(Uri uri) {
2637 if (mFolderSyncTask != null) {
2638 mFolderSyncTask.cancel(true);
2639 }
2640 mFolderSyncTask = new AsyncRefreshTask(mActivity.getActivityContext(), uri);
2641 mFolderSyncTask.execute();
2642 }
Paul Westbrook122f7c22012-08-20 17:50:31 -07002643
2644 private void promptUserForAuthentication(Account account) {
Paul Westbrook429399b2012-08-24 11:19:17 -07002645 if (account != null && !Utils.isEmpty(account.reauthenticationIntentUri)) {
Paul Westbrook122f7c22012-08-20 17:50:31 -07002646 final Intent authenticationIntent =
2647 new Intent(Intent.ACTION_VIEW, account.reauthenticationIntentUri);
2648 mActivity.startActivityForResult(authenticationIntent, REAUTHENTICATE_REQUEST_CODE);
2649 }
2650 }
Vikram Aggarwal4a5c5302012-01-12 15:07:13 -08002651}