blob: 94df3a87193c5daf046c4c18fc0fff5ae5789e30 [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
Andy Huang12b3ee42013-04-24 22:49:43 -070020import android.animation.ValueAnimator;
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -080021import android.app.ActionBar;
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -080022import android.app.ActionBar.LayoutParams;
Vikram Aggarwala55b36c2012-01-13 11:45:02 -080023import android.app.Activity;
Vikram Aggarwal54452ae2012-03-13 15:29:00 -070024import android.app.AlertDialog;
Vikram Aggarwala55b36c2012-01-13 11:45:02 -080025import android.app.Dialog;
Andrew Sapperstein00179f12012-08-09 15:15:40 -070026import android.app.DialogFragment;
Vikram Aggarwal6902dcf2012-04-11 08:57:42 -070027import android.app.Fragment;
Paul Westbrook2d50bcd2012-04-10 11:53:47 -070028import android.app.FragmentManager;
Andy Huangf9a73482012-03-13 15:54:02 -070029import android.app.LoaderManager;
Vikram Aggarwale620a7a2012-03-28 13:16:14 -070030import android.app.SearchManager;
Andy Huang839ada22012-07-20 15:48:40 -070031import android.content.ContentProviderOperation;
Vikram Aggarwal80aeac52012-02-07 15:27:20 -080032import android.content.ContentResolver;
Mindy Pereira6c2663d2012-07-20 15:37:29 -070033import android.content.ContentValues;
Vikram Aggarwala55b36c2012-01-13 11:45:02 -080034import android.content.Context;
Vikram Aggarwal54452ae2012-03-13 15:29:00 -070035import android.content.DialogInterface;
Vikram Aggarwal6cadbfc2012-12-27 09:17:05 -080036import android.content.DialogInterface.OnClickListener;
Vikram Aggarwala55b36c2012-01-13 11:45:02 -080037import android.content.Intent;
Vikram Aggarwal7dedb952012-02-16 16:10:23 -080038import android.content.Loader;
Paul Westbrook57246a42013-04-21 09:40:22 -070039import android.content.res.Configuration;
Vikram Aggarwalbcb16b92013-01-28 18:05:03 -080040import android.content.res.Resources;
Alice Yangebeef1b2013-09-04 06:41:10 +000041import android.database.Cursor;
Andy Huang632721e2012-04-11 16:57:26 -070042import android.database.DataSetObservable;
43import android.database.DataSetObserver;
Andy Huang61f26c22014-03-13 18:24:52 -070044import android.database.Observable;
Paul Westbrook23b74b92012-02-29 11:36:12 -080045import android.net.Uri;
Vikram Aggarwal27d89ad2012-06-12 13:38:40 -070046import android.os.AsyncTask;
Vikram Aggarwala55b36c2012-01-13 11:45:02 -080047import android.os.Bundle;
Mindy Pereira21ab4902012-03-19 18:48:03 -070048import android.os.Handler;
Scott Kennedyf77806e2013-08-30 11:38:15 -070049import android.os.Parcelable;
Vikram Aggarwale620a7a2012-03-28 13:16:14 -070050import android.provider.SearchRecentSuggestions;
Andy Huang12b3ee42013-04-24 22:49:43 -070051import android.support.v4.app.ActionBarDrawerToggle;
52import android.support.v4.widget.DrawerLayout;
Mindy Pereiraacf60392012-04-06 09:11:00 -070053import android.view.DragEvent;
Andy Huang12b3ee42013-04-24 22:49:43 -070054import android.view.Gravity;
Vikram Aggarwala55b36c2012-01-13 11:45:02 -080055import android.view.KeyEvent;
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -080056import android.view.LayoutInflater;
Vikram Aggarwala55b36c2012-01-13 11:45:02 -080057import android.view.Menu;
Mindy Pereira28d5f722012-02-15 12:32:40 -080058import android.view.MenuInflater;
Vikram Aggarwala55b36c2012-01-13 11:45:02 -080059import android.view.MenuItem;
60import android.view.MotionEvent;
Mindy Pereirad33674992012-06-25 16:26:30 -070061import android.view.View;
Andy Huang12b3ee42013-04-24 22:49:43 -070062import android.widget.ListView;
Mindy Pereirafd0c2972012-03-27 13:50:39 -070063import android.widget.Toast;
Vikram Aggarwala55b36c2012-01-13 11:45:02 -080064
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -080065import com.android.mail.ConversationListContext;
Vikram Aggarwal59f741f2013-03-01 15:55:40 -080066import com.android.mail.MailLogService;
Andy Huangf9a73482012-03-13 15:54:02 -070067import com.android.mail.R;
Andy Huang761522c2013-08-08 13:09:11 -070068import com.android.mail.analytics.Analytics;
Jin Cao779dd602014-04-22 16:16:28 -070069import com.android.mail.analytics.AnalyticsTimer;
Andy Huang761522c2013-08-08 13:09:11 -070070import com.android.mail.analytics.AnalyticsUtils;
Vikram Aggarwal6cadbfc2012-12-27 09:17:05 -080071import com.android.mail.browse.ConfirmDialogFragment;
Mindy Pereira967ede62012-03-22 09:29:09 -070072import com.android.mail.browse.ConversationCursor;
Yu Ping Hu7c909c72013-01-18 11:58:01 -080073import com.android.mail.browse.ConversationCursor.ConversationOperation;
mindypca87de42012-09-28 15:02:39 -070074import com.android.mail.browse.ConversationItemViewModel;
Andrew Sapperstein8812d3c2013-06-04 17:06:41 -070075import com.android.mail.browse.ConversationMessage;
Paul Westbrookbf232c32012-04-18 03:17:41 -070076import com.android.mail.browse.ConversationPagerController;
Marc Blank7c9f6ac2012-04-02 13:27:19 -070077import com.android.mail.browse.SelectedConversationsActionMenu;
Andy Huang991f4532012-08-14 13:32:55 -070078import com.android.mail.browse.SyncErrorDialogFragment;
Jin Cao30c881a2014-04-08 14:28:36 -070079import com.android.mail.browse.UndoCallback;
Mindy Pereira9b875682012-02-15 18:10:54 -080080import com.android.mail.compose.ComposeActivity;
Vikram Aggarwal177097f2013-03-08 11:19:53 -080081import com.android.mail.content.CursorCreator;
82import com.android.mail.content.ObjectCursor;
83import com.android.mail.content.ObjectCursorLoader;
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -080084import com.android.mail.providers.Account;
Mindy Pereira9b875682012-02-15 18:10:54 -080085import com.android.mail.providers.Conversation;
Andy Huang839ada22012-07-20 15:48:40 -070086import com.android.mail.providers.ConversationInfo;
Vikram Aggarwal4a5c5302012-01-12 15:07:13 -080087import com.android.mail.providers.Folder;
Vikram Aggarwal7a5d95a2012-07-27 16:24:54 -070088import com.android.mail.providers.FolderWatcher;
Paul Westbrookc2074c42012-03-22 15:26:58 -070089import com.android.mail.providers.MailAppProvider;
Mindy Pereiradac00542012-03-01 10:50:33 -080090import com.android.mail.providers.Settings;
Vikram Aggarwale620a7a2012-03-28 13:16:14 -070091import com.android.mail.providers.SuggestionsProvider;
Vikram Aggarwal80aeac52012-02-07 15:27:20 -080092import com.android.mail.providers.UIProvider;
Mindy Pereira3cb28b52012-05-24 15:26:39 -070093import com.android.mail.providers.UIProvider.AccountCapabilities;
Scott Kennedy0d0f8b02012-10-12 15:18:18 -070094import com.android.mail.providers.UIProvider.AccountColumns;
Paul Westbrook2388c5d2012-03-25 12:29:11 -070095import com.android.mail.providers.UIProvider.AccountCursorExtraKeys;
Scott Kennedy0d0f8b02012-10-12 15:18:18 -070096import com.android.mail.providers.UIProvider.AutoAdvance;
Mindy Pereirac9d59182012-03-22 16:06:46 -070097import com.android.mail.providers.UIProvider.ConversationColumns;
Paul Westbrook5109c512012-11-05 11:00:30 -080098import com.android.mail.providers.UIProvider.ConversationOperations;
Vikram Aggarwal54452ae2012-03-13 15:29:00 -070099import com.android.mail.providers.UIProvider.FolderCapabilities;
Scott Kennedya158ac82013-09-04 13:48:13 -0700100import com.android.mail.providers.UIProvider.FolderType;
Andrew Sappersteinc2c9dc12012-07-02 18:17:32 -0700101import com.android.mail.ui.ActionableToastBar.ActionClickedListener;
Andy Huang839ada22012-07-20 15:48:40 -0700102import com.android.mail.utils.ContentProviderTask;
Andy Huang144bfe72013-06-11 13:27:52 -0700103import com.android.mail.utils.DrawIdler;
Paul Westbrookb334c902012-06-25 11:42:46 -0700104import com.android.mail.utils.LogTag;
Mindy Pereira5cb0c3e2012-02-22 15:22:47 -0800105import com.android.mail.utils.LogUtils;
Andy Huang61f26c22014-03-13 18:24:52 -0700106import com.android.mail.utils.MailObservable;
Scott Kennedycb85aea2013-02-25 13:08:32 -0800107import com.android.mail.utils.NotificationActionUtils;
Vikram Aggarwalfa131a22012-02-02 13:56:22 -0800108import com.android.mail.utils.Utils;
Vikram Aggarwal69a6cdf2013-01-08 16:05:17 -0800109import com.android.mail.utils.VeiledAddressMatcher;
Paul Westbrookca08fc12012-07-31 12:01:15 -0700110import com.google.common.base.Objects;
Paul Westbrook77eee622012-07-10 13:41:57 -0700111import com.google.common.collect.ImmutableList;
Mindy Pereiraacf60392012-04-06 09:11:00 -0700112import com.google.common.collect.Lists;
Marc Blank167faa82012-03-21 13:11:53 -0700113import com.google.common.collect.Sets;
Vikram Aggarwal4a5c5302012-01-12 15:07:13 -0800114
Marc Blank167faa82012-03-21 13:11:53 -0700115import java.util.ArrayList;
Andy Huang9e4ca792013-02-28 14:33:43 -0800116import java.util.Arrays;
Mindy Pereirafbe40192012-03-20 10:40:45 -0700117import java.util.Collection;
Andy Huang839ada22012-07-20 15:48:40 -0700118import java.util.Collections;
Andy Huangc1fb9a92013-02-11 13:09:12 -0800119import java.util.Deque;
Mindy Pereira8db7e402012-07-13 10:32:47 -0700120import java.util.HashMap;
Vikram Aggarwalcc754b12012-08-30 14:04:21 -0700121import java.util.List;
Paul Westbrook23b74b92012-02-29 11:36:12 -0800122import java.util.Set;
Marc Blankbf128eb2012-04-18 15:58:45 -0700123import java.util.TimerTask;
Paul Westbrook23b74b92012-02-29 11:36:12 -0800124
125
Vikram Aggarwal4a5c5302012-01-12 15:07:13 -0800126/**
Mindy Pereira161f50d2012-02-28 15:47:19 -0800127 * This is an abstract implementation of the Activity Controller. This class
128 * knows how to respond to menu items, state changes, layout changes, etc. It
129 * weaves together the views and listeners, dispatching actions to the
130 * respective underlying classes.
Vikram Aggarwala55b36c2012-01-13 11:45:02 -0800131 * <p>
Mindy Pereira161f50d2012-02-28 15:47:19 -0800132 * Even though this class is abstract, it should provide default implementations
133 * for most, if not all the methods in the ActivityController interface. This
134 * makes the task of the subclasses easier: OnePaneActivityController and
135 * TwoPaneActivityController can be concise when the common functionality is in
136 * AbstractActivityController.
137 * </p>
138 * <p>
139 * In the Gmail codebase, this was called BaseActivityController
140 * </p>
Vikram Aggarwal4a5c5302012-01-12 15:07:13 -0800141 */
Andrew Sappersteined5b52d2013-04-30 13:40:18 -0700142public abstract class AbstractActivityController implements ActivityController,
143 EmptyFolderDialogFragment.EmptyFolderDialogFragmentListener {
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -0800144 // Keys for serialization of various information in Bundles.
Vikram Aggarwalcabd3f22012-04-19 10:14:41 -0700145 /** Tag for {@link #mAccount} */
Vikram Aggarwal6c511582012-02-27 10:59:47 -0800146 private static final String SAVED_ACCOUNT = "saved-account";
Vikram Aggarwalcabd3f22012-04-19 10:14:41 -0700147 /** Tag for {@link #mFolder} */
Mindy Pereira5e478d22012-03-26 18:04:58 -0700148 private static final String SAVED_FOLDER = "saved-folder";
Vikram Aggarwalcabd3f22012-04-19 10:14:41 -0700149 /** Tag for {@link #mCurrentConversation} */
Mindy Pereira26f23fc2012-03-27 10:26:04 -0700150 private static final String SAVED_CONVERSATION = "saved-conversation";
Vikram Aggarwalcabd3f22012-04-19 10:14:41 -0700151 /** Tag for {@link #mSelectedSet} */
152 private static final String SAVED_SELECTED_SET = "saved-selected-set";
Vikram Aggarwal649b9ea2012-08-27 12:15:20 -0700153 /** Tag for {@link ActionableToastBar#getOperation()} */
Mindy Pereirad33674992012-06-25 16:26:30 -0700154 private static final String SAVED_TOAST_BAR_OP = "saved-toast-bar-op";
Vikram Aggarwal649b9ea2012-08-27 12:15:20 -0700155 /** Tag for {@link #mFolderListFolder} */
156 private static final String SAVED_HIERARCHICAL_FOLDER = "saved-hierarchical-folder";
Vikram Aggarwalf3341402012-08-07 10:09:38 -0700157 /** Tag for {@link ConversationListContext#searchQuery} */
158 private static final String SAVED_QUERY = "saved-query";
Vikram Aggarwal6cadbfc2012-12-27 09:17:05 -0800159 /** Tag for {@link #mDialogAction} */
160 private static final String SAVED_ACTION = "saved-action";
Vikram Aggarwalb8c31712013-01-03 17:03:19 -0800161 /** Tag for {@link #mDialogFromSelectedSet} */
162 private static final String SAVED_ACTION_FROM_SELECTED = "saved-action-from-selected";
Vikram Aggarwala91d00b2013-01-18 12:00:37 -0800163 /** Tag for {@link #mDetachedConvUri} */
164 private static final String SAVED_DETACHED_CONV_URI = "saved-detached-conv-uri";
Alice Yangebeef1b2013-09-04 06:41:10 +0000165 /** Key to store {@link #mInbox}. */
Scott Kennedyf77806e2013-08-30 11:38:15 -0700166 private static final String SAVED_INBOX_KEY = "m-inbox";
167 /** Key to store {@link #mConversationListScrollPositions} */
168 private static final String SAVED_CONVERSATION_LIST_SCROLL_POSITIONS =
169 "saved-conversation-list-scroll-positions";
Mindy Pereira5cb0c3e2012-02-22 15:22:47 -0800170
Vikram Aggarwal6902dcf2012-04-11 08:57:42 -0700171 /** Tag used when loading a wait fragment */
172 protected static final String TAG_WAIT = "wait-fragment";
173 /** Tag used when loading a conversation list fragment. */
Paul Westbrookbf232c32012-04-18 03:17:41 -0700174 public static final String TAG_CONVERSATION_LIST = "tag-conversation-list";
Scott Kennedy103319a2013-07-26 13:35:35 -0700175 /** Tag used when loading a custom fragment. */
176 protected static final String TAG_CUSTOM_FRAGMENT = "tag-custom-fragment";
Vikram Aggarwal6902dcf2012-04-11 08:57:42 -0700177
Vikram Aggarwala1b59dc2013-04-30 15:45:49 -0700178 /** Key to store an account in a bundle */
179 private final String BUNDLE_ACCOUNT_KEY = "account";
180 /** Key to store a folder in a bundle */
181 private final String BUNDLE_FOLDER_KEY = "folder";
Andrew Sapperstein5bb4d052014-03-31 16:22:31 -0700182 /**
183 * Key to set a flag for the ConversationCursorLoader to ignore any
184 * initial load limit that may be set by the Account. Instead,
185 * perform a full load instead of the full-stage load.
186 */
187 private final String BUNDLE_IGNORE_INITIAL_CONVERSATION_LIMIT_KEY =
188 "ignore-initial-conversation-limit";
Vikram Aggarwala1b59dc2013-04-30 15:45:49 -0700189
Vikram Aggarwald7a12cd2012-02-03 09:36:20 -0800190 protected Account mAccount;
Mindy Pereira12a4d802012-06-22 09:24:43 -0700191 protected Folder mFolder;
Alice Yangebeef1b2013-09-04 06:41:10 +0000192 protected Folder mInbox;
Vikram Aggarwal69b5c302012-09-05 11:11:13 -0700193 /** True when {@link #mFolder} is first shown to the user. */
194 private boolean mFolderChanged = false;
Andy Huang6681e542012-06-14 14:36:45 -0700195 protected MailActionBarView mActionBarView;
James Lemieux10fcd642014-03-03 13:01:04 -0800196 protected final MailActivity mActivity;
Vikram Aggarwala55b36c2012-01-13 11:45:02 -0800197 protected final Context mContext;
Vikram Aggarwal6902dcf2012-04-11 08:57:42 -0700198 private final FragmentManager mFragmentManager;
Vikram Aggarwalec5cbf72012-03-08 15:10:35 -0800199 protected final RecentFolderList mRecentFolderList;
Vikram Aggarwal80aeac52012-02-07 15:27:20 -0800200 protected ConversationListContext mConvListContext;
Mindy Pereira9b875682012-02-15 18:10:54 -0800201 protected Conversation mCurrentConversation;
Vikram Aggarwala91d00b2013-01-18 12:00:37 -0800202 /**
203 * The hash of {@link #mCurrentConversation} in detached mode. 0 if we are not in detached mode.
204 */
205 private Uri mDetachedConvUri;
Vikram Aggarwald7a12cd2012-02-03 09:36:20 -0800206
Scott Kennedyf77806e2013-08-30 11:38:15 -0700207 /** A map of {@link Folder} {@link Uri} to scroll position in the conversation list. */
208 private final Bundle mConversationListScrollPositions = new Bundle();
209
Paul Westbrook6ead20d2012-03-19 14:48:14 -0700210 /** A {@link android.content.BroadcastReceiver} that suppresses new e-mail notifications. */
211 private SuppressNotificationReceiver mNewEmailReceiver = null;
212
Vikram Aggarwal59f741f2013-03-01 15:55:40 -0800213 /** Handler for all our local runnables. */
Mindy Pereirafbe40192012-03-20 10:40:45 -0700214 protected Handler mHandler = new Handler();
Mindy Pereirafa995b42012-07-25 12:06:13 -0700215
Vikram Aggarwalfa131a22012-02-02 13:56:22 -0800216 /**
Mindy Pereira161f50d2012-02-28 15:47:19 -0800217 * The current mode of the application. All changes in mode are initiated by
218 * the activity controller. View mode changes are propagated to classes that
219 * attach themselves as listeners of view mode changes.
Vikram Aggarwalfa131a22012-02-02 13:56:22 -0800220 */
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -0800221 protected final ViewMode mViewMode;
Vikram Aggarwal80aeac52012-02-07 15:27:20 -0800222 protected ContentResolver mResolver;
Vikram Aggarwalc04cf7e2013-05-13 15:38:42 -0700223 protected boolean mHaveAccountList = false;
Mindy Pereirab7b33e02012-02-21 15:32:19 -0800224 private AsyncRefreshTask mAsyncRefreshTask;
Mindy Pereira5cb0c3e2012-02-22 15:22:47 -0800225
Andy Huang4e0158f2012-08-07 21:06:01 -0700226 private boolean mDestroyed;
227
Vikram Aggarwalbcb16b92013-01-28 18:05:03 -0800228 /** True if running on tablet */
229 private final boolean mIsTablet;
230
Andy Huang1ee96b22012-08-24 20:19:53 -0700231 /**
232 * Are we in a point in the Activity/Fragment lifecycle where it's safe to execute fragment
233 * transactions? (including back stack manipulation)
234 * <p>
235 * Per docs in {@link FragmentManager#beginTransaction()}, this flag starts out true, switches
236 * to false after {@link Activity#onSaveInstanceState}, and becomes true again in both onStart
237 * and onResume.
238 */
239 private boolean mSafeToModifyFragments = true;
240
Paul Westbrook23b74b92012-02-29 11:36:12 -0800241 private final Set<Uri> mCurrentAccountUris = Sets.newHashSet();
Mindy Pereira967ede62012-03-22 09:29:09 -0700242 protected ConversationCursor mConversationListCursor;
Andy Huang61f26c22014-03-13 18:24:52 -0700243 private final DataSetObservable mConversationListObservable = new MailObservable("List");
Marc Blankbf128eb2012-04-18 15:58:45 -0700244
Vikram Aggarwal59f741f2013-03-01 15:55:40 -0800245 /** Runnable that checks the logging level to enable/disable the logging service. */
246 private Runnable mLogServiceChecker = null;
Vikram Aggarwalde60c9d2013-04-10 12:58:56 -0700247 /** List of all accounts currently known to the controller. This is never null. */
248 private Account[] mAllAccounts = new Account[0];
Vikram Aggarwal59f741f2013-03-01 15:55:40 -0800249
Vikram Aggarwal77ee0ce2013-04-28 15:32:36 -0700250 private FolderWatcher mFolderWatcher;
251
Andrew Sapperstein5bb4d052014-03-31 16:22:31 -0700252 private boolean mIgnoreInitialConversationLimit;
253
Yu Ping Hu7c909c72013-01-18 11:58:01 -0800254 /**
255 * Interface for actions that are deferred until after a load completes. This is for handling
256 * user actions which affect cursors (e.g. marking messages read or unread) that happen before
257 * that cursor is loaded.
258 */
259 private interface LoadFinishedCallback {
260 void onLoadFinished();
261 }
262
263 /** The deferred actions to execute when mConversationListCursor load completes. */
264 private final ArrayList<LoadFinishedCallback> mConversationListLoadFinishedCallbacks =
265 new ArrayList<LoadFinishedCallback>();
266
Marc Blankbf128eb2012-04-18 15:58:45 -0700267 private RefreshTimerTask mConversationListRefreshTask;
Marc Blanke1d1b072012-04-13 17:29:16 -0700268
Vikram Aggarwal7c401b72012-08-13 16:43:47 -0700269 /** Listeners that are interested in changes to the current account. */
Andy Huang61f26c22014-03-13 18:24:52 -0700270 private final DataSetObservable mAccountObservers = new MailObservable("Account");
Vikram Aggarwal58cad2e2012-08-28 16:18:23 -0700271 /** Listeners that are interested in changes to the recent folders. */
Andy Huang61f26c22014-03-13 18:24:52 -0700272 private final DataSetObservable mRecentFolderObservers = new MailObservable("RecentFolder");
Vikram Aggarwal07dbaa62013-03-12 15:21:04 -0700273 /** Listeners that are interested in changes to the list of all accounts. */
Andy Huang61f26c22014-03-13 18:24:52 -0700274 private final DataSetObservable mAllAccountObservers = new MailObservable("AllAccounts");
Vikram Aggarwal50ff0e52013-03-14 13:58:02 -0700275 /** Listeners that are interested in changes to the current folder. */
Andy Huang61f26c22014-03-13 18:24:52 -0700276 private final DataSetObservable mFolderObservable = new MailObservable("CurrentFolder");
Rohan Shah0f73d902013-04-19 17:06:37 -0700277 /** Listeners that are interested in changes to the drawer state. */
Andy Huang61f26c22014-03-13 18:24:52 -0700278 private final DataSetObservable mDrawerObservers = new MailObservable("Drawer");
Vikram Aggarwal58cad2e2012-08-28 16:18:23 -0700279
Mindy Pereira967ede62012-03-22 09:29:09 -0700280 /**
281 * Selected conversations, if any.
282 */
Andy Huang4556a442012-03-30 16:42:05 -0700283 private final ConversationSelectionSet mSelectedSet = new ConversationSelectionSet();
Mindy Pereiradac00542012-03-01 10:50:33 -0800284
Paul Westbrookc7a070f2012-04-12 01:46:41 -0700285 private final int mFolderItemUpdateDelayMs;
286
Vikram Aggarwal135fd022012-04-11 14:44:15 -0700287 /** Keeps track of selected and unselected conversations */
Paul Westbrook937c94f2012-08-16 13:01:18 -0700288 final protected ConversationPositionTracker mTracker;
Vikram Aggarwalaf1ee0c2012-04-12 17:13:13 -0700289
Vikram Aggarwale128fc22012-04-04 12:33:34 -0700290 /**
291 * Action menu associated with the selected set.
292 */
293 SelectedConversationsActionMenu mCabActionMenu;
Andrew Sappersteinc2c9dc12012-07-02 18:17:32 -0700294 protected ActionableToastBar mToastBar;
Andy Huang632721e2012-04-11 16:57:26 -0700295 protected ConversationPagerController mPagerController;
Mindy Pereiraab486362012-03-21 18:18:53 -0700296
Vikram Aggarwald70fe492013-06-04 12:52:07 -0700297 // This is split out from the general loader dispatcher because its loader doesn't return a
Andy Huangb1c34dc2012-04-17 16:36:19 -0700298 // basic Cursor
Vikram Aggarwald70fe492013-06-04 12:52:07 -0700299 /** Handles loader callbacks to create a convesation cursor. */
Andy Huangb1c34dc2012-04-17 16:36:19 -0700300 private final ConversationListLoaderCallbacks mListCursorCallbacks =
301 new ConversationListLoaderCallbacks();
302
Vikram Aggarwal177097f2013-03-08 11:19:53 -0800303 /** Object that listens to all LoaderCallbacks that result in {@link Folder} creation. */
304 private final FolderLoads mFolderCallbacks = new FolderLoads();
305 /** Object that listens to all LoaderCallbacks that result in {@link Account} creation. */
306 private final AccountLoads mAccountCallbacks = new AccountLoads();
307
Vikram Aggarwal69a6cdf2013-01-08 16:05:17 -0800308 /**
309 * Matched addresses that must be shielded from users because they are temporary. Even though
310 * this is instantiated from settings, this matcher is valid for all accounts, and is expected
311 * to live past the life of an account.
312 */
313 private final VeiledAddressMatcher mVeiledMatcher;
314
Paul Westbrookb334c902012-06-25 11:42:46 -0700315 protected static final String LOG_TAG = LogTag.getLogTag();
Vikram Aggarwald70fe492013-06-04 12:52:07 -0700316
Vikram Aggarwalaa941d72013-06-04 15:34:28 -0700317 // Loader constants: Accounts
318 /**
319 * The list of accounts. This loader is started early in the application life-cycle since
320 * the list of accounts is central to all other data the application needs: unread counts for
321 * folders, critical UI settings like show/hide checkboxes, ...
322 * The loader is started when the application is created: both in
323 * {@link #onCreate(Bundle)} and in {@link #onActivityResult(int, int, Intent)}. It is never
324 * destroyed since the cursor is needed through the life of the application. When the list of
325 * accounts changes, we notify {@link #mAllAccountObservers}.
326 */
Vikram Aggarwalfa4b47e2012-03-09 13:02:46 -0800327 private static final int LOADER_ACCOUNT_CURSOR = 0;
Vikram Aggarwalaa941d72013-06-04 15:34:28 -0700328
329 /**
330 * The current account. This loader is started when we have an account. The mail application
331 * <b>needs</b> a valid account to function. As soon as we set {@link #mAccount},
332 * we start a loader to observe for changes on the current account.
333 * The loader is always restarted when an account is set in {@link #setAccount(Account)}.
334 * When the current account object changes, we notify {@link #mAccountObservers}.
335 * A possible performance improvement would be to listen purely on
336 * {@link #LOADER_ACCOUNT_CURSOR}. The current account is guaranteed to be in the list,
337 * and would avoid two updates when a single setting on the current account changes.
338 */
Paul Westbrook2d50bcd2012-04-10 11:53:47 -0700339 private static final int LOADER_ACCOUNT_UPDATE_CURSOR = 7;
Vikram Aggarwald70fe492013-06-04 12:52:07 -0700340
Vikram Aggarwalaa941d72013-06-04 15:34:28 -0700341 // Loader constants: Folders
342 /** The current folder. This loader watches for updates to the current folder in a manner
343 * analogous to the {@link #LOADER_ACCOUNT_UPDATE_CURSOR}. Updates to the current folder
344 * might be due to server-side changes (unread count), or local changes (sync window or sync
345 * status change).
346 * The change of current folder calls {@link #updateFolder(Folder)}.
347 * This is responsible for restarting a loader using the URI of the provided folder. When the
348 * loader returns, the current folder is updated and consumers, if any, are notified.
349 * When the current folder changes, we notify {@link #mFolderObservable}
350 */
Vikram Aggarwald70fe492013-06-04 12:52:07 -0700351 private static final int LOADER_FOLDER_CURSOR = 2;
Vikram Aggarwalaa941d72013-06-04 15:34:28 -0700352 /**
353 * The list of recent folders. Recent folders are shown in the DrawerFragment. The recent
354 * folders are tied to the current account being viewed. When the account is changed,
355 * we restart this loader to retrieve the recent accounts. Recents are pre-populated for
356 * phones historically, when they were displayed in the spinner. On the tablet,
357 * they showed in the {@link FolderListFragment} and were not-populated. The code to
358 * pre-populate the recents is somewhat convoluted: when the loader returns a short list of
359 * recent folders, it issues an update on the Recent Folder URI. The underlying provider then
360 * does the appropriate thing to populate recent folders, and notify of a change on the cursor.
361 * Recent folders are needed for the life of the current account.
362 * When the recent folders change, we notify {@link #mRecentFolderObservers}.
363 */
Vikram Aggarwald70fe492013-06-04 12:52:07 -0700364 private static final int LOADER_RECENT_FOLDERS = 3;
Vikram Aggarwalaa941d72013-06-04 15:34:28 -0700365 /**
366 * The primary inbox for the current account. The mechanism to load the default inbox for the
367 * current account is (sadly) different from loading other folders. The method
368 * {@link #loadAccountInbox()} is called, and it restarts this loader. When the loader returns
369 * a valid cursor, we create a folder, call {@link #onFolderChanged{Folder)} eventually
370 * calling {@link #updateFolder(Folder)} which starts a loader {@link #LOADER_FOLDER_CURSOR}
371 * over the current folder.
372 * When we have a valid cursor, we destroy this loader, This convoluted flow is historical.
373 */
Vikram Aggarwald70fe492013-06-04 12:52:07 -0700374 private static final int LOADER_ACCOUNT_INBOX = 5;
Vikram Aggarwalaa941d72013-06-04 15:34:28 -0700375 /**
376 * The fake folder of search results for a term. When we search for a term,
377 * a new activity is created with {@link Intent#ACTION_SEARCH}. For this new activity,
378 * we start a loader which returns conversations that match the user-provided query.
379 * We destroy the loader when we obtain a valid cursor since subsequent searches will create
380 * a new activity.
381 */
Vikram Aggarwald70fe492013-06-04 12:52:07 -0700382 private static final int LOADER_SEARCH = 6;
Vikram Aggarwalaa941d72013-06-04 15:34:28 -0700383 /**
384 * The initial folder at app start. When the application is launched from an intent that
385 * specifies the initial folder (notifications/widgets/shortcuts),
386 * then we extract the folder URI from the intent, but we cannot trust the folder object. Since
387 * shortcuts and widgets persist past application update, they might have incorrect
388 * information encoded in them. So, to obtain a {@link Folder} object from a {@link Uri},
389 * we need to start another loader. Upon obtaining a valid cursor, the loader is destroyed.
390 * An additional complication arises if we have to view a specific conversation within this
391 * folder. This is the case when launching the app from a single conversation notification
392 * or tapping on a specific conversation in the widget. In these cases, the conversation is
393 * saved in {@link #mConversationToShow} and is retrieved when the loader returns.
394 */
Vikram Aggarwal177097f2013-03-08 11:19:53 -0800395 public static final int LOADER_FIRST_FOLDER = 8;
396
Vikram Aggarwalaa941d72013-06-04 15:34:28 -0700397 // Loader constants: Conversations
398 /** The conversation cursor over the current conversation list. This loader provides
399 * a cursor over conversation entries from a folder to display a conversation
400 * list.
401 * This loader is started when the user switches folders (in {@link #updateFolder(Folder)},
402 * or when the controller is told that a folder/account change is imminent
403 * (in {@link #preloadConvList(Account, Folder)}. The loader is maintained for the life of
404 * the current folder. When the user switches folders, the old loader is destroyed and a new
405 * one is created.
406 *
407 * When the conversation list changes, we notify {@link #mConversationListObservable}.
408 */
Vikram Aggarwald70fe492013-06-04 12:52:07 -0700409 private static final int LOADER_CONVERSATION_LIST = 4;
410
Vikram Aggarwal7a5d95a2012-07-27 16:24:54 -0700411 /**
412 * Guaranteed to be the last loader ID used by the activity. Loaders are owned by Activity or
413 * fragments, and within an activity, loader IDs need to be unique. A hack to ensure that the
414 * {@link FolderWatcher} can create its folder loaders without clashing with the IDs of those
415 * of the {@link AbstractActivityController}. Currently, the {@link FolderWatcher} is the only
416 * other class that uses this activity's LoaderManager. If another class needs activity-level
417 * loaders, consider consolidating the loaders in a central location: a UI-less fragment
418 * perhaps.
419 */
420 public static final int LAST_LOADER_ID = 100;
Scott Kennedy7c8325d2013-02-28 10:46:10 -0800421 /**
422 * Guaranteed to be the last loader ID used by the Fragment. Loaders are owned by Activity or
423 * fragments, and within an activity, loader IDs need to be unique. Currently,
424 * {@link SectionedInboxTeaserView} is the only class that uses the
425 * {@link ConversationListFragment}'s LoaderManager.
426 */
427 public static final int LAST_FRAGMENT_LOADER_ID = 1000;
Vikram Aggarwala55b36c2012-01-13 11:45:02 -0800428
Vikram Aggarwal6aca6892013-06-04 13:53:27 -0700429 /** Code returned after an account has been added. */
Paul Westbrook2388c5d2012-03-25 12:29:11 -0700430 private static final int ADD_ACCOUNT_REQUEST_CODE = 1;
Vikram Aggarwal6aca6892013-06-04 13:53:27 -0700431 /** Code returned when the user has to enter the new password on an existing account. */
Paul Westbrook122f7c22012-08-20 17:50:31 -0700432 private static final int REAUTHENTICATE_REQUEST_CODE = 2;
Martin Hibdon371a71c2014-02-19 13:55:28 -0800433 /** Code returned when the previous activity needs to navigate to a different folder
434 * or account */
435 private static final int CHANGE_NAVIGATION_REQUEST_CODE = 3;
436
437 public static final String EXTRA_FOLDER = "extra-folder";
438 public static final String EXTRA_ACCOUNT = "extra-account";
Paul Westbrook2388c5d2012-03-25 12:29:11 -0700439
Vikram Aggarwale8a85322012-04-24 09:01:38 -0700440 /** The pending destructive action to be carried out before swapping the conversation cursor.*/
441 private DestructiveAction mPendingDestruction;
Andrew Sappersteinc2c9dc12012-07-02 18:17:32 -0700442 protected AsyncRefreshTask mFolderSyncTask;
Mindy Pereirac975e842012-07-16 09:15:00 -0700443 private Folder mFolderListFolder;
mindyp5390fca2012-08-22 12:12:25 -0700444 private boolean mIsDragHappening;
Martin Hibdone78c40f2013-10-10 18:29:25 -0700445 private final int mShowUndoBarDelay;
mindyp6f54e1b2012-10-09 09:54:08 -0700446 private boolean mRecentsDataUpdated;
Vikram Aggarwala3f43d42012-10-25 16:21:30 -0700447 /** A wait fragment we added, if any. */
448 private WaitFragment mWaitFragment;
Vikram Aggarwalf0ef2302012-11-07 14:53:34 -0800449 /** True if we have results from a search query */
450 private boolean mHaveSearchResults = false;
Vikram Aggarwal6cadbfc2012-12-27 09:17:05 -0800451 /** If a confirmation dialog is being show, the listener for the positive action. */
452 private OnClickListener mDialogListener;
453 /**
454 * If a confirmation dialog is being show, the resource of the action: R.id.delete, etc. This
455 * is used to create a new {@link #mDialogListener} on orientation changes.
456 */
457 private int mDialogAction = -1;
Vikram Aggarwalb8c31712013-01-03 17:03:19 -0800458 /**
459 * If a confirmation dialog is being shown, this is true if the dialog acts on the selected set
460 * and false if it acts on the currently selected conversation
461 */
462 private boolean mDialogFromSelectedSet;
Vikram Aggarwal6cadbfc2012-12-27 09:17:05 -0800463
Vikram Aggarwal177097f2013-03-08 11:19:53 -0800464 /** Which conversation to show, if started from widget/notification. */
465 private Conversation mConversationToShow = null;
466
Andy Huangc94d07f2013-06-03 16:19:35 -0700467 /**
468 * A temporary reference to the pending destructive action that was deferred due to an
469 * auto-advance transition in progress.
470 * <p>
471 * In detail: when auto-advance triggers a mode change, we must wait until the transition
472 * completes before executing the destructive action to ensure a smooth mode change transition.
473 * This member variable houses the pending destructive action work to be run upon completion.
474 */
475 private Runnable mAutoAdvanceOp = null;
476
Andy Huangc1fb9a92013-02-11 13:09:12 -0800477 private final Deque<UpOrBackHandler> mUpOrBackHandlers = Lists.newLinkedList();
478
Andy Huang12b3ee42013-04-24 22:49:43 -0700479 protected DrawerLayout mDrawerContainer;
480 protected View mDrawerPullout;
481 protected ActionBarDrawerToggle mDrawerToggle;
482 protected ListView mListViewForAnimating;
483 protected boolean mHasNewAccountOrFolder;
Andrew Sappersteina3ce6782013-05-09 15:14:49 -0700484 private boolean mConversationListLoadFinishedIgnored;
Andy Huang61f26c22014-03-13 18:24:52 -0700485 private final MailDrawerListener mDrawerListener = new MailDrawerListener();
Andrew Sapperstein5747e152013-05-13 14:13:08 -0700486 private boolean mHideMenuItems;
Andy Huang12b3ee42013-04-24 22:49:43 -0700487
Andy Huang144bfe72013-06-11 13:27:52 -0700488 private final DrawIdler mDrawIdler = new DrawIdler();
489
Andrew Sapperstein00179f12012-08-09 15:15:40 -0700490 public static final String SYNC_ERROR_DIALOG_FRAGMENT_TAG = "SyncErrorDialogFragment";
Vikram Aggarwale8a85322012-04-24 09:01:38 -0700491
Scott Kennedycb85aea2013-02-25 13:08:32 -0800492 private final DataSetObserver mUndoNotificationObserver = new DataSetObserver() {
493 @Override
494 public void onChanged() {
495 super.onChanged();
496
497 if (mConversationListCursor != null) {
498 mConversationListCursor.handleNotificationActions();
499 }
500 }
501 };
502
Vikram Aggarwala55b36c2012-01-13 11:45:02 -0800503 public AbstractActivityController(MailActivity activity, ViewMode viewMode) {
504 mActivity = activity;
Vikram Aggarwal6902dcf2012-04-11 08:57:42 -0700505 mFragmentManager = mActivity.getFragmentManager();
Vikram Aggarwala55b36c2012-01-13 11:45:02 -0800506 mViewMode = viewMode;
507 mContext = activity.getApplicationContext();
Vikram Aggarwal025eba82012-05-08 10:45:30 -0700508 mRecentFolderList = new RecentFolderList(mContext);
Paul Westbrook937c94f2012-08-16 13:01:18 -0700509 mTracker = new ConversationPositionTracker(this);
Mindy Pereira967ede62012-03-22 09:29:09 -0700510 // Allow the fragment to observe changes to its own selection set. No other object is
511 // aware of the selected set.
512 mSelectedSet.addObserver(this);
Paul Westbrookc7a070f2012-04-12 01:46:41 -0700513
Vikram Aggarwalbcb16b92013-01-28 18:05:03 -0800514 final Resources r = mContext.getResources();
515 mFolderItemUpdateDelayMs = r.getInteger(R.integer.folder_item_refresh_delay_ms);
516 mShowUndoBarDelay = r.getInteger(R.integer.show_undo_bar_delay_ms);
Vikram Aggarwal69a6cdf2013-01-08 16:05:17 -0800517 mVeiledMatcher = VeiledAddressMatcher.newInstance(activity.getResources());
Vikram Aggarwalbcb16b92013-01-28 18:05:03 -0800518 mIsTablet = Utils.useTabletUI(r);
Andrew Sappersteina3ce6782013-05-09 15:14:49 -0700519 mConversationListLoadFinishedIgnored = false;
Vikram Aggarwala55b36c2012-01-13 11:45:02 -0800520 }
Vikram Aggarwal4a5c5302012-01-12 15:07:13 -0800521
522 @Override
Vikram Aggarwale9a81032012-02-22 13:15:35 -0800523 public Account getCurrentAccount() {
524 return mAccount;
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -0800525 }
526
527 @Override
528 public ConversationListContext getCurrentListContext() {
Vikram Aggarwale9a81032012-02-22 13:15:35 -0800529 return mConvListContext;
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -0800530 }
531
532 @Override
Vikram Aggarwal4a5c5302012-01-12 15:07:13 -0800533 public String getHelpContext() {
Paul Westbrook30745b62012-08-19 14:10:32 -0700534 final int mode = mViewMode.getMode();
535 final int helpContextResId;
536 switch (mode) {
537 case ViewMode.WAITING_FOR_ACCOUNT_INITIALIZATION:
538 helpContextResId = R.string.wait_help_context;
539 break;
540 default:
541 helpContextResId = R.string.main_help_context;
542 }
543 return mContext.getString(helpContextResId);
Vikram Aggarwal4a5c5302012-01-12 15:07:13 -0800544 }
545
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -0800546 @Override
Vikram Aggarwalef7c9922012-04-23 12:35:04 -0700547 public final ConversationCursor getConversationListCursor() {
Mindy Pereira967ede62012-03-22 09:29:09 -0700548 return mConversationListCursor;
549 }
550
Vikram Aggarwal6902dcf2012-04-11 08:57:42 -0700551 /**
Vikram Aggarwal34a65012012-04-17 12:39:06 -0700552 * Check if the fragment is attached to an activity and has a root view.
Vikram Aggarwal715f83d2013-03-05 17:04:56 -0800553 * @param in fragment to be checked
Vikram Aggarwal34a65012012-04-17 12:39:06 -0700554 * @return true if the fragment is valid, false otherwise
555 */
Vikram Aggarwal715f83d2013-03-05 17:04:56 -0800556 private static boolean isValidFragment(Fragment in) {
557 return !(in == null || in.getActivity() == null || in.getView() == null);
Vikram Aggarwal34a65012012-04-17 12:39:06 -0700558 }
559
560 /**
Vikram Aggarwal49e0e992012-09-21 13:53:15 -0700561 * Get the conversation list fragment for this activity. If the conversation list fragment is
562 * not attached, this method returns null.
Andy Huang839ada22012-07-20 15:48:40 -0700563 *
Vikram Aggarwal49e0e992012-09-21 13:53:15 -0700564 * Caution! This method returns the {@link ConversationListFragment} after the fragment has been
565 * added, <b>and</b> after the {@link FragmentManager} has run through its queue to add the
566 * fragment. There is a non-trivial amount of time after the fragment is instantiated and before
567 * this call returns a non-null value, depending on the {@link FragmentManager}. If you
568 * need the fragment immediately after adding it, consider making the fragment an observer of
569 * the controller and perform the task immediately on {@link Fragment#onActivityCreated(Bundle)}
Vikram Aggarwal6902dcf2012-04-11 08:57:42 -0700570 */
571 protected ConversationListFragment getConversationListFragment() {
572 final Fragment fragment = mFragmentManager.findFragmentByTag(TAG_CONVERSATION_LIST);
Vikram Aggarwal34a65012012-04-17 12:39:06 -0700573 if (isValidFragment(fragment)) {
574 return (ConversationListFragment) fragment;
Andy Huang9585d782012-04-16 19:45:04 -0700575 }
Vikram Aggarwal34a65012012-04-17 12:39:06 -0700576 return null;
Vikram Aggarwal6902dcf2012-04-11 08:57:42 -0700577 }
578
579 /**
Vikram Aggarwal6902dcf2012-04-11 08:57:42 -0700580 * Returns the folder list fragment attached with this activity. If no such fragment is attached
581 * this method returns null.
Andy Huang839ada22012-07-20 15:48:40 -0700582 *
Vikram Aggarwal49e0e992012-09-21 13:53:15 -0700583 * Caution! This method returns the {@link FolderListFragment} after the fragment has been
584 * added, <b>and</b> after the {@link FragmentManager} has run through its queue to add the
585 * fragment. There is a non-trivial amount of time after the fragment is instantiated and before
586 * this call returns a non-null value, depending on the {@link FragmentManager}. If you
587 * need the fragment immediately after adding it, consider making the fragment an observer of
588 * the controller and perform the task immediately on {@link Fragment#onActivityCreated(Bundle)}
Vikram Aggarwal6902dcf2012-04-11 08:57:42 -0700589 */
590 protected FolderListFragment getFolderListFragment() {
James Lemieux10fcd642014-03-03 13:01:04 -0800591 final String drawerPulloutTag = mActivity.getString(R.string.drawer_pullout_tag);
592 final Fragment fragment = mFragmentManager.findFragmentByTag(drawerPulloutTag);
Vikram Aggarwal34a65012012-04-17 12:39:06 -0700593 if (isValidFragment(fragment)) {
594 return (FolderListFragment) fragment;
Andy Huang9585d782012-04-16 19:45:04 -0700595 }
Vikram Aggarwal34a65012012-04-17 12:39:06 -0700596 return null;
Vikram Aggarwal6902dcf2012-04-11 08:57:42 -0700597 }
598
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -0800599 /**
Mindy Pereira161f50d2012-02-28 15:47:19 -0800600 * Initialize the action bar. This is not visible to OnePaneController and
601 * TwoPaneController so they cannot override this behavior.
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -0800602 */
Vikram Aggarwalabd24d82012-04-26 13:23:14 -0700603 private void initializeActionBar() {
604 final ActionBar actionBar = mActivity.getActionBar();
Andy Huang5895f7b2012-06-01 17:07:20 -0700605 if (actionBar == null) {
606 return;
Vikram Aggarwalabd24d82012-04-26 13:23:14 -0700607 }
Andy Huang5895f7b2012-06-01 17:07:20 -0700608
609 // be sure to inherit from the ActionBar theme when inflating
610 final LayoutInflater inflater = LayoutInflater.from(actionBar.getThemedContext());
Mindy Pereira82faec72012-06-14 17:21:50 -0700611 final boolean isSearch = mActivity.getIntent() != null
612 && Intent.ACTION_SEARCH.equals(mActivity.getIntent().getAction());
613 mActionBarView = (MailActionBarView) inflater.inflate(
614 isSearch ? R.layout.search_actionbar_view : R.layout.actionbar_view, null);
Vikram Aggarwaldac89fa2013-03-05 16:43:09 -0800615 mActionBarView.initialize(mActivity, this, actionBar);
Rohan Shah1dd054f2013-04-01 11:23:44 -0700616
Andy Huang12b3ee42013-04-24 22:49:43 -0700617 // init the action bar to allow the 'up' affordance.
618 // any configurations that disallow 'up' should do that later.
619 mActionBarView.setBackButton();
Vikram Aggarwalabd24d82012-04-26 13:23:14 -0700620 }
621
622 /**
623 * Attach the action bar to the activity.
624 */
625 private void attachActionBar() {
626 final ActionBar actionBar = mActivity.getActionBar();
Tony Mantlerbd091502013-09-16 13:59:47 -0700627 if (actionBar != null) {
Vikram Aggarwalec5cbf72012-03-08 15:10:35 -0800628 actionBar.setCustomView(mActionBarView, new ActionBar.LayoutParams(
Vikram Aggarwal26b0bfd2013-03-29 13:05:08 -0700629 LayoutParams.WRAP_CONTENT, LayoutParams.MATCH_PARENT));
Vikram Aggarwalbc462ca2013-03-15 10:41:03 -0700630 // Show a custom view and home icon, keep the title and subttitle
Vikram Aggarwal649b9ea2012-08-27 12:15:20 -0700631 final int mask = ActionBar.DISPLAY_SHOW_CUSTOM | ActionBar.DISPLAY_SHOW_TITLE
632 | ActionBar.DISPLAY_SHOW_HOME;
Vikram Aggarwalbc462ca2013-03-15 10:41:03 -0700633 actionBar.setDisplayOptions(mask, mask);
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -0800634 }
Tony Mantlerbd091502013-09-16 13:59:47 -0700635 mActionBarView.setViewModeController(mViewMode);
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -0800636 }
637
638 /**
Mindy Pereira161f50d2012-02-28 15:47:19 -0800639 * Returns whether the conversation list fragment is visible or not.
640 * Different layouts will have their own notion on the visibility of
641 * fragments, so this method needs to be overriden.
642 *
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -0800643 */
644 protected abstract boolean isConversationListVisible();
645
Vikram Aggarwaleb7d3292012-04-20 17:07:20 -0700646 /**
Vikram Aggarwal34f7b232012-10-17 13:32:23 -0700647 * If required, starts wait mode for the current account.
Vikram Aggarwal34f7b232012-10-17 13:32:23 -0700648 */
649 final void perhapsEnterWaitMode() {
650 // If the account is not initialized, then show the wait fragment, since nothing can be
651 // shown.
652 if (mAccount.isAccountInitializationRequired()) {
653 showWaitForInitialization();
654 return;
655 }
656
657 final boolean inWaitingMode = inWaitMode();
658 final boolean isSyncRequired = mAccount.isAccountSyncRequired();
659 if (isSyncRequired) {
660 if (inWaitingMode) {
661 // Update the WaitFragment's account object
662 updateWaitMode();
663 } else {
664 // Transition to waiting mode
665 showWaitForInitialization();
666 }
667 } else if (inWaitingMode) {
668 // Dismiss waiting mode
669 hideWaitForInitialization();
670 }
671 }
672
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -0800673 @Override
Andrew Sapperstein4cbb0da2013-04-19 11:28:02 -0700674 public void switchToDefaultInboxOrChangeAccount(Account account) {
675 LogUtils.d(LOG_TAG, "AAC.switchToDefaultAccount(%s)", account);
Andy Huange764cfd2014-02-26 11:55:03 -0800676 if (mViewMode.isSearchMode()) {
Martin Hibdon371a71c2014-02-19 13:55:28 -0800677 // We are in an activity on top of the main navigation activity.
678 // We need to return to it with a result code that indicates it should navigate to
679 // a different folder.
680 final Intent intent = new Intent();
681 intent.putExtra(AbstractActivityController.EXTRA_ACCOUNT, account);
682 mActivity.setResult(Activity.RESULT_OK, intent);
683 mActivity.finish();
684 return;
685 }
Vikram Aggarwal66bc6ed2012-07-31 09:59:55 -0700686 final boolean firstLoad = mAccount == null;
Andrew Sapperstein252684a2013-04-17 14:43:26 -0700687 final boolean switchToDefaultInbox = !firstLoad && account.uri.equals(mAccount.uri);
Vikram Aggarwald70fe492013-06-04 12:52:07 -0700688 // If the active account has been clicked in the drawer, go to default inbox
Andrew Sapperstein252684a2013-04-17 14:43:26 -0700689 if (switchToDefaultInbox) {
690 loadAccountInbox();
691 return;
692 }
Andrew Sapperstein4cbb0da2013-04-19 11:28:02 -0700693 changeAccount(account);
694 }
695
696 @Override
697 public void changeAccount(Account account) {
698 LogUtils.d(LOG_TAG, "AAC.changeAccount(%s)", account);
699 // Is the account or account settings different from the existing account?
700 final boolean firstLoad = mAccount == null;
701 final boolean accountChanged = firstLoad || !account.uri.equals(mAccount.uri);
702
Vikram Aggarwal472e5362012-11-05 14:17:23 -0800703 // If nothing has changed, return early without wasting any more time.
704 if (!accountChanged && !account.settingsDiffer(mAccount)) {
705 return;
706 }
707 // We also don't want to do anything if the new account is null
708 if (account == null) {
Vikram Aggarwal5fd8afd2013-03-13 15:28:47 -0700709 LogUtils.e(LOG_TAG, "AAC.changeAccount(null) called.");
Vikram Aggarwal472e5362012-11-05 14:17:23 -0800710 return;
711 }
Tony Mantler79b11562013-10-09 15:31:50 -0700712 final String emailAddress = account.getEmailAddress();
Vikram Aggarwal472e5362012-11-05 14:17:23 -0800713 mHandler.post(new Runnable() {
714 @Override
715 public void run() {
Tony Mantler79b11562013-10-09 15:31:50 -0700716 MailActivity.setNfcMessage(emailAddress);
Mindy Pereirafa20c1a2012-07-23 13:00:02 -0700717 }
Vikram Aggarwal472e5362012-11-05 14:17:23 -0800718 });
719 if (accountChanged) {
720 commitDestructiveActions(false);
721 }
Andy Huang761522c2013-08-08 13:09:11 -0700722 Analytics.getInstance().setCustomDimension(Analytics.CD_INDEX_ACCOUNT_TYPE,
Tony Mantler79b11562013-10-09 15:31:50 -0700723 AnalyticsUtils.getAccountTypeForAccount(emailAddress));
Vikram Aggarwal472e5362012-11-05 14:17:23 -0800724 // Change the account here
725 setAccount(account);
726 // And carry out associated actions.
727 cancelRefreshTask();
728 if (accountChanged) {
729 loadAccountInbox();
730 }
731 // Check if we need to force setting up an account before proceeding.
732 if (mAccount != null && !Uri.EMPTY.equals(mAccount.settings.setupIntentUri)) {
733 // Launch the intent!
734 final Intent intent = new Intent(Intent.ACTION_EDIT);
735 intent.setData(mAccount.settings.setupIntentUri);
736 mActivity.startActivity(intent);
Mindy Pereira28d5f722012-02-15 12:32:40 -0800737 }
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -0800738 }
739
Vikram Aggarwale6340bc2012-03-26 15:57:09 -0700740 /**
Vikram Aggarwal7c401b72012-08-13 16:43:47 -0700741 * Adds a listener interested in change in the current account. If a class is storing a
742 * reference to the current account, it should listen on changes, so it can receive updates to
743 * settings. Must happen in the UI thread.
744 */
Mindy Pereiraefe3d252012-03-01 14:20:44 -0800745 @Override
Vikram Aggarwal7c401b72012-08-13 16:43:47 -0700746 public void registerAccountObserver(DataSetObserver obs) {
747 mAccountObservers.registerObserver(obs);
Mindy Pereiraefe3d252012-03-01 14:20:44 -0800748 }
749
Vikram Aggarwal7d816002012-04-17 17:06:41 -0700750 /**
Vikram Aggarwal7c401b72012-08-13 16:43:47 -0700751 * Removes a listener from receiving current account changes.
Vikram Aggarwal7d816002012-04-17 17:06:41 -0700752 * Must happen in the UI thread.
753 */
Vikram Aggarwal7c401b72012-08-13 16:43:47 -0700754 @Override
755 public void unregisterAccountObserver(DataSetObserver obs) {
756 mAccountObservers.unregisterObserver(obs);
Vikram Aggarwal7d816002012-04-17 17:06:41 -0700757 }
758
Vikram Aggarwal7c401b72012-08-13 16:43:47 -0700759 @Override
Vikram Aggarwal07dbaa62013-03-12 15:21:04 -0700760 public void registerAllAccountObserver(DataSetObserver observer) {
761 mAllAccountObservers.registerObserver(observer);
762 }
763
764 @Override
765 public void unregisterAllAccountObserver(DataSetObserver observer) {
766 mAllAccountObservers.unregisterObserver(observer);
767 }
768
769 @Override
770 public Account[] getAllAccounts() {
771 return mAllAccounts;
772 }
773
774 @Override
Vikram Aggarwal7c401b72012-08-13 16:43:47 -0700775 public Account getAccount() {
776 return mAccount;
Vikram Aggarwal7d816002012-04-17 17:06:41 -0700777 }
778
Rohan Shah0f73d902013-04-19 17:06:37 -0700779 @Override
780 public void registerDrawerClosedObserver(final DataSetObserver observer) {
781 mDrawerObservers.registerObserver(observer);
782 }
783
784 @Override
785 public void unregisterDrawerClosedObserver(final DataSetObserver observer) {
786 mDrawerObservers.unregisterObserver(observer);
787 }
788
789 /**
Andy Huang12b3ee42013-04-24 22:49:43 -0700790 * If the drawer is open, the function locks the drawer to the closed, thereby sliding in
791 * the drawer to the left edge, disabling events, and refreshing it once it's either closed
792 * or put in an idle state.
Rohan Shah0f73d902013-04-19 17:06:37 -0700793 */
794 @Override
Vikram Aggarwala1b59dc2013-04-30 15:45:49 -0700795 public void closeDrawer(final boolean hasNewFolderOrAccount, Account nextAccount,
796 Folder nextFolder) {
Andy Huang12b3ee42013-04-24 22:49:43 -0700797 if (!isDrawerEnabled()) {
798 mDrawerObservers.notifyChanged();
799 return;
800 }
Andy Huang12b3ee42013-04-24 22:49:43 -0700801 // If there are no new folders or accounts to switch to, just close the drawer
802 if (!hasNewFolderOrAccount) {
803 mDrawerContainer.closeDrawers();
804 return;
805 }
Vikram Aggarwal2f9d3942013-05-03 12:31:39 -0700806 // Otherwise, start preloading the conversation list for the new folder.
807 if (nextFolder != null) {
808 preloadConvList(nextAccount, nextFolder);
809 }
810 // Remember if the conversation list view is animating
Andy Huang12b3ee42013-04-24 22:49:43 -0700811 final ConversationListFragment conversationList = getConversationListFragment();
812 if (conversationList != null) {
813 mListViewForAnimating = conversationList.getListView();
814 } else {
815 // There is no conversation list to animate, so just set it to null
816 mListViewForAnimating = null;
817 }
818
819 if (mDrawerContainer.isDrawerOpen(mDrawerPullout)) {
820 // Lets the drawer listener update the drawer contents and notify the FolderListFragment
821 mHasNewAccountOrFolder = true;
822 mDrawerContainer.setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED);
823 } else {
824 // Drawer is already closed, notify observers that is the case.
825 mDrawerObservers.notifyChanged();
826 }
Rohan Shah0f73d902013-04-19 17:06:37 -0700827 }
828
Vikram Aggarwala1b59dc2013-04-30 15:45:49 -0700829 /**
Vikram Aggarwalaa941d72013-06-04 15:34:28 -0700830 * Load the conversation list early for the given folder. This happens when some UI element
831 * (usually the drawer) instructs the controller that an account change or folder change is
832 * imminent. While the UI element is animating, the controller can preload the conversation
833 * list for the default inbox of the account provided here or to the folder provided here.
834 *
835 * @param nextAccount The account which the app will switch to shortly, possibly null.
836 * @param nextFolder The folder which the app will switch to shortly, possibly null.
Vikram Aggarwala1b59dc2013-04-30 15:45:49 -0700837 */
838 protected void preloadConvList(Account nextAccount, Folder nextFolder) {
839 // Fire off the conversation list loader for this account already with a fake
840 // listener.
Paul Westbrooke2be97a2013-05-21 02:12:09 -0700841 final Bundle args = new Bundle(2);
Vikram Aggarwala1b59dc2013-04-30 15:45:49 -0700842 if (nextAccount != null) {
843 args.putParcelable(BUNDLE_ACCOUNT_KEY, nextAccount);
844 } else {
845 args.putParcelable(BUNDLE_ACCOUNT_KEY, mAccount);
846 }
847 if (nextFolder != null) {
848 args.putParcelable(BUNDLE_FOLDER_KEY, nextFolder);
Vikram Aggarwal2dc85042013-05-01 13:57:09 -0700849 } else {
850 LogUtils.e(LOG_TAG, new Error(), "AAC.preloadConvList(): Got an empty folder");
Vikram Aggarwala1b59dc2013-04-30 15:45:49 -0700851 }
852 mFolder = null;
853 final LoaderManager lm = mActivity.getLoaderManager();
854 lm.destroyLoader(LOADER_CONVERSATION_LIST);
855 lm.initLoader(LOADER_CONVERSATION_LIST, args, mListCursorCallbacks);
856 }
857
Vikram Aggarwalaa941d72013-06-04 15:34:28 -0700858 /**
859 * Initiates the async request to create a fake search folder, which returns conversations that
860 * match the query term provided by the user. Returns immediately.
861 * @param intent Intent that the app was started with. This intent contains the search query.
862 */
Mindy Pereirae0828392012-03-08 10:38:40 -0800863 private void fetchSearchFolder(Intent intent) {
Paul Westbrooke2be97a2013-05-21 02:12:09 -0700864 final Bundle args = new Bundle(1);
Mindy Pereiraab486362012-03-21 18:18:53 -0700865 args.putString(ConversationListContext.EXTRA_SEARCH_QUERY, intent
Mindy Pereirae0828392012-03-08 10:38:40 -0800866 .getStringExtra(ConversationListContext.EXTRA_SEARCH_QUERY));
Vikram Aggarwal177097f2013-03-08 11:19:53 -0800867 mActivity.getLoaderManager().restartLoader(LOADER_SEARCH, args, mFolderCallbacks);
Mindy Pereirae0828392012-03-08 10:38:40 -0800868 }
869
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -0800870 @Override
Alice Yangebeef1b2013-09-04 06:41:10 +0000871 public void onFolderChanged(Folder folder, final boolean force) {
Vikram Aggarwaldfbc6982013-07-03 14:39:31 -0700872 /** If the folder doesn't exist, or its parent URI is empty,
873 * this is not a child folder */
874 final boolean isTopLevel = (folder == null) || (folder.parent == Uri.EMPTY);
875 final int mode = mViewMode.getMode();
876 mDrawerToggle.setDrawerIndicatorEnabled(
877 getShouldShowDrawerIndicator(mode, isTopLevel));
Martin Hibdon371a71c2014-02-19 13:55:28 -0800878 mDrawerContainer.setDrawerLockMode(DrawerLayout.LOCK_MODE_UNLOCKED);
Vikram Aggarwaldfbc6982013-07-03 14:39:31 -0700879
Andy Huang12b3ee42013-04-24 22:49:43 -0700880 mDrawerContainer.closeDrawers();
Alice Yang7dd0e1c2013-09-04 06:43:16 +0000881
882 if (mFolder == null || !mFolder.equals(folder)) {
883 // We are actually changing the folder, so exit cab mode
884 exitCabMode();
885 }
886
Scott Kennedya158ac82013-09-04 13:48:13 -0700887 final String query;
888 if (folder != null && folder.isType(FolderType.SEARCH)) {
889 query = mConvListContext.searchQuery;
890 } else {
891 query = null;
892 }
893
894 changeFolder(folder, query, force);
Vikram Aggarwalf3341402012-08-07 10:09:38 -0700895 }
896
897 /**
Vikram Aggarwal203dc002012-08-23 13:59:04 -0700898 * Sets the folder state without changing view mode and without creating a list fragment, if
899 * possible.
Vikram Aggarwal715f83d2013-03-05 17:04:56 -0800900 * @param folder the folder whose list of conversations are to be shown
901 * @param query the query string for a list of conversations matching a search
Vikram Aggarwal203dc002012-08-23 13:59:04 -0700902 */
903 private void setListContext(Folder folder, String query) {
904 updateFolder(folder);
905 if (query != null) {
906 mConvListContext = ConversationListContext.forSearchQuery(mAccount, mFolder, query);
907 } else {
908 mConvListContext = ConversationListContext.forFolder(mAccount, mFolder);
909 }
Vikram Aggarwal203dc002012-08-23 13:59:04 -0700910 cancelRefreshTask();
911 }
912
913 /**
Vikram Aggarwalf3341402012-08-07 10:09:38 -0700914 * Changes the folder to the value provided here. This causes the view mode to change.
915 * @param folder the folder to change to
916 * @param query if non-null, this represents the search string that the folder represents.
Alice Yangebeef1b2013-09-04 06:41:10 +0000917 * @param force <code>true</code> to force a folder change, <code>false</code> to disallow
918 * changing to the current folder
Vikram Aggarwalf3341402012-08-07 10:09:38 -0700919 */
Alice Yangebeef1b2013-09-04 06:41:10 +0000920 private void changeFolder(Folder folder, String query, final boolean force) {
Vikram Aggarwal203dc002012-08-23 13:59:04 -0700921 if (!Objects.equal(mFolder, folder)) {
922 commitDestructiveActions(false);
923 }
Alice Yangebeef1b2013-09-04 06:41:10 +0000924 if (folder != null && (!folder.equals(mFolder) || force)
Mindy Pereiraa1f99982012-07-16 16:32:15 -0700925 || (mViewMode.getMode() != ViewMode.CONVERSATION_LIST)) {
Vikram Aggarwal203dc002012-08-23 13:59:04 -0700926 setListContext(folder, query);
Mindy Pereira28e0c342012-02-17 15:05:13 -0800927 showConversationList(mConvListContext);
Vikram Aggarwal58ccd692013-03-28 11:29:22 -0700928 // Touch the current folder: it is different, and it has been accessed.
929 mRecentFolderList.touchFolder(mFolder, mAccount);
Mindy Pereira28e0c342012-02-17 15:05:13 -0800930 }
Vikram Aggarwal93dc2022012-11-05 13:36:57 -0800931 resetActionBarIcon();
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -0800932 }
933
Mindy Pereira13c12a62012-05-31 15:41:08 -0700934 @Override
Mindy Pereira505df5f2012-06-19 17:57:17 -0700935 public void onFolderSelected(Folder folder) {
Alice Yangebeef1b2013-09-04 06:41:10 +0000936 onFolderChanged(folder, false /* force */);
Mindy Pereira13c12a62012-05-31 15:41:08 -0700937 }
938
Vikram Aggarwal792ccba2012-03-27 13:46:57 -0700939 /**
Vikram Aggarwal58cad2e2012-08-28 16:18:23 -0700940 * Adds a listener interested in change in the recent folders. If a class is storing a
941 * reference to the recent folders, it should listen on changes, so it can receive updates.
942 * Must happen in the UI thread.
943 */
944 @Override
945 public void registerRecentFolderObserver(DataSetObserver obs) {
946 mRecentFolderObservers.registerObserver(obs);
947 }
948
949 /**
950 * Removes a listener from receiving recent folder changes.
951 * Must happen in the UI thread.
952 */
953 @Override
954 public void unregisterRecentFolderObserver(DataSetObserver obs) {
955 mRecentFolderObservers.unregisterObserver(obs);
956 }
957
958 @Override
959 public RecentFolderList getRecentFolders() {
960 return mRecentFolderList;
961 }
962
Vikram Aggarwaleb7d3292012-04-20 17:07:20 -0700963 @Override
964 public void loadAccountInbox() {
Vikram Aggarwal77ee0ce2013-04-28 15:32:36 -0700965 boolean handled = false;
966 if (mFolderWatcher != null) {
967 final Folder inbox = mFolderWatcher.getDefaultInbox(mAccount);
968 if (inbox != null) {
Alice Yangebeef1b2013-09-04 06:41:10 +0000969 onFolderChanged(inbox, false /* force */);
Vikram Aggarwal77ee0ce2013-04-28 15:32:36 -0700970 handled = true;
971 }
972 }
973 if (!handled) {
Paul Westbrook1bf20e02014-02-26 12:48:54 -0800974 LogUtils.d(LOG_TAG, "Starting a LOADER_ACCOUNT_INBOX for %s", mAccount);
Vikram Aggarwal77ee0ce2013-04-28 15:32:36 -0700975 restartOptionalLoader(LOADER_ACCOUNT_INBOX, mFolderCallbacks, Bundle.EMPTY);
976 }
Vikram Aggarwal8cbf2812013-04-11 17:23:45 -0700977 final int mode = mViewMode.getMode();
978 if (mode == ViewMode.UNKNOWN || mode == ViewMode.WAITING_FOR_ACCOUNT_INITIALIZATION) {
Andy Huange6459422013-04-01 16:32:18 -0700979 mViewMode.enterConversationListMode();
980 }
Mindy Pereiraf6acdad2012-03-15 11:21:13 -0700981 }
982
Vikram Aggarwal77ee0ce2013-04-28 15:32:36 -0700983 @Override
984 public void setFolderWatcher(FolderWatcher watcher) {
985 mFolderWatcher = watcher;
986 }
987
Vikram Aggarwal69b5c302012-09-05 11:11:13 -0700988 /**
Vikram Aggarwald00229d2012-09-20 12:31:44 -0700989 * Marks the {@link #mFolderChanged} value if the newFolder is different from the existing
990 * {@link #mFolder}. This should be called immediately <b>before</b> assigning newFolder to
991 * mFolder.
Vikram Aggarwal715f83d2013-03-05 17:04:56 -0800992 * @param newFolder the new folder we are switching to.
Vikram Aggarwald00229d2012-09-20 12:31:44 -0700993 */
Vikram Aggarwal715f83d2013-03-05 17:04:56 -0800994 private void setHasFolderChanged(final Folder newFolder) {
Vikram Aggarwald00229d2012-09-20 12:31:44 -0700995 // We should never try to assign a null folder. But in the rare event that we do, we should
996 // only set the bit when we have a valid folder, and null is not valid.
997 if (newFolder == null) {
998 return;
999 }
1000 // If the previous folder was null, or if the two folders represent different data, then we
1001 // consider that the folder has changed.
Scott Kennedy259df5b2013-07-11 13:24:01 -07001002 if (mFolder == null || !newFolder.equals(mFolder)) {
Vikram Aggarwald00229d2012-09-20 12:31:44 -07001003 mFolderChanged = true;
1004 }
1005 }
1006
1007 /**
Vikram Aggarwal69b5c302012-09-05 11:11:13 -07001008 * Sets the current folder if it is different from the object provided here. This method does
1009 * NOT notify the folder observers that a change has happened. Observers are notified when we
1010 * get an updated folder from the loaders, which will happen as a consequence of this method
1011 * (since this method starts/restarts the loaders).
1012 * @param folder The folder to assign
1013 */
Mindy Pereira11e35962012-06-01 14:49:46 -07001014 private void updateFolder(Folder folder) {
Vikram Aggarwal1672ff82012-09-21 10:15:22 -07001015 if (folder == null || !folder.isInitialized()) {
1016 LogUtils.e(LOG_TAG, new Error(), "AAC.setFolder(%s): Bad input", folder);
1017 return;
1018 }
1019 if (folder.equals(mFolder)) {
1020 LogUtils.d(LOG_TAG, "AAC.setFolder(%s): Input matches mFolder", folder);
1021 return;
1022 }
1023 final boolean wasNull = mFolder == null;
1024 LogUtils.d(LOG_TAG, "AbstractActivityController.setFolder(%s)", folder.name);
1025 final LoaderManager lm = mActivity.getLoaderManager();
1026 // updateFolder is called from AAC.onLoadFinished() on folder changes. We need to
1027 // ensure that the folder is different from the previous folder before marking the
1028 // folder changed.
1029 setHasFolderChanged(folder);
1030 mFolder = folder;
Vikram Aggarwal69b5c302012-09-05 11:11:13 -07001031
Vikram Aggarwal1672ff82012-09-21 10:15:22 -07001032 // We do not need to notify folder observers yet. Instead we start the loaders and
1033 // when the load finishes, we will get an updated folder. Then, we notify the
1034 // folderObservers in onLoadFinished.
1035 mActionBarView.setFolder(mFolder);
Andy Huangb1c34dc2012-04-17 16:36:19 -07001036
Vikram Aggarwal1672ff82012-09-21 10:15:22 -07001037 // Only when we switch from one folder to another do we want to restart the
1038 // folder and conversation list loaders (to trigger onCreateLoader).
1039 // The first time this runs when the activity is [re-]initialized, we want to re-use the
1040 // previous loader's instance and data upon configuration change (e.g. rotation).
1041 // If there was not already an instance of the loader, init it.
1042 if (lm.getLoader(LOADER_FOLDER_CURSOR) == null) {
Vikram Aggarwal177097f2013-03-08 11:19:53 -08001043 lm.initLoader(LOADER_FOLDER_CURSOR, Bundle.EMPTY, mFolderCallbacks);
Vikram Aggarwal1672ff82012-09-21 10:15:22 -07001044 } else {
Vikram Aggarwal177097f2013-03-08 11:19:53 -08001045 lm.restartLoader(LOADER_FOLDER_CURSOR, Bundle.EMPTY, mFolderCallbacks);
Vikram Aggarwal1672ff82012-09-21 10:15:22 -07001046 }
Vikram Aggarwala1b59dc2013-04-30 15:45:49 -07001047 if (!wasNull && lm.getLoader(LOADER_CONVERSATION_LIST) != null) {
1048 // If there was an existing folder AND we have changed
Vikram Aggarwal1672ff82012-09-21 10:15:22 -07001049 // folders, we want to restart the loader to get the information
1050 // for the newly selected folder
1051 lm.destroyLoader(LOADER_CONVERSATION_LIST);
Mindy Pereira5cb0c3e2012-02-22 15:22:47 -08001052 }
Paul Westbrooke2be97a2013-05-21 02:12:09 -07001053 final Bundle args = new Bundle(2);
Vikram Aggarwal2dc85042013-05-01 13:57:09 -07001054 args.putParcelable(BUNDLE_ACCOUNT_KEY, mAccount);
Vikram Aggarwala1b59dc2013-04-30 15:45:49 -07001055 args.putParcelable(BUNDLE_FOLDER_KEY, mFolder);
Andrew Sapperstein5bb4d052014-03-31 16:22:31 -07001056 args.putBoolean(BUNDLE_IGNORE_INITIAL_CONVERSATION_LIMIT_KEY,
1057 mIgnoreInitialConversationLimit);
1058 mIgnoreInitialConversationLimit = false;
Vikram Aggarwala1b59dc2013-04-30 15:45:49 -07001059 lm.initLoader(LOADER_CONVERSATION_LIST, args, mListCursorCallbacks);
Mindy Pereira5cb0c3e2012-02-22 15:22:47 -08001060 }
1061
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -08001062 @Override
Andy Huang090db1e2012-07-25 13:25:28 -07001063 public Folder getFolder() {
1064 return mFolder;
1065 }
1066
1067 @Override
Mindy Pereirac975e842012-07-16 09:15:00 -07001068 public Folder getHierarchyFolder() {
1069 return mFolderListFolder;
1070 }
1071
1072 @Override
1073 public void setHierarchyFolder(Folder folder) {
1074 mFolderListFolder = folder;
Mindy Pereira23aadfd2012-05-25 11:24:33 -07001075 }
1076
Vikram Aggarwal6aca6892013-06-04 13:53:27 -07001077 /**
1078 * The mail activity calls other activities for two specific reasons:
1079 * <ul>
1080 * <li>To add an account. And receives the result {@link #ADD_ACCOUNT_REQUEST_CODE}</li>
1081 * <li>To update the password on a current account. The result {@link
1082 * #REAUTHENTICATE_REQUEST_CODE} is received.</li>
1083 * </ul>
1084 * @param requestCode
1085 * @param resultCode
1086 * @param data
1087 */
Mindy Pereira23aadfd2012-05-25 11:24:33 -07001088 @Override
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -08001089 public void onActivityResult(int requestCode, int resultCode, Intent data) {
Paul Westbrook122f7c22012-08-20 17:50:31 -07001090 switch (requestCode) {
1091 case ADD_ACCOUNT_REQUEST_CODE:
1092 // We were waiting for the user to create an account
1093 if (resultCode == Activity.RESULT_OK) {
1094 // restart the loader to get the updated list of accounts
Vikram Aggarwal177097f2013-03-08 11:19:53 -08001095 mActivity.getLoaderManager().initLoader(LOADER_ACCOUNT_CURSOR, Bundle.EMPTY,
1096 mAccountCallbacks);
Paul Westbrook122f7c22012-08-20 17:50:31 -07001097 } else {
1098 // The user failed to create an account, just exit the app
1099 mActivity.finish();
1100 }
1101 break;
1102 case REAUTHENTICATE_REQUEST_CODE:
1103 if (resultCode == Activity.RESULT_OK) {
1104 // The user successfully authenticated, attempt to refresh the list
1105 final Uri refreshUri = mFolder != null ? mFolder.refreshUri : null;
1106 if (refreshUri != null) {
1107 startAsyncRefreshTask(refreshUri);
1108 }
1109 }
1110 break;
Martin Hibdon371a71c2014-02-19 13:55:28 -08001111 case CHANGE_NAVIGATION_REQUEST_CODE:
1112 if (resultCode == Activity.RESULT_OK && data != null) {
1113 // We have have received a result that indicates we need to navigate to a
1114 // different folder or account. This happens if someone navigates using the
1115 // drawer on the search results activity.
1116 final Folder folder = data.getParcelableExtra(EXTRA_FOLDER);
1117 final Account account = data.getParcelableExtra(EXTRA_ACCOUNT);
1118 if (folder != null) {
1119 onFolderSelected(folder);
1120 mViewMode.enterConversationListMode();
1121 } else if (account != null) {
1122 switchToDefaultInboxOrChangeAccount(account);
1123 mViewMode.enterConversationListMode();
1124 }
1125 }
1126 break;
Paul Westbrook2388c5d2012-03-25 12:29:11 -07001127 }
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -08001128 }
1129
Vikram Aggarwal69b5c302012-09-05 11:11:13 -07001130 /**
1131 * Inform the conversation cursor that there has been a visibility change.
Vikram Aggarwal715f83d2013-03-05 17:04:56 -08001132 * @param visible true if the conversation list is visible, false otherwise.
Vikram Aggarwal69b5c302012-09-05 11:11:13 -07001133 */
1134 protected synchronized void informCursorVisiblity(boolean visible) {
1135 if (mConversationListCursor != null) {
1136 Utils.setConversationCursorVisibility(mConversationListCursor, visible, mFolderChanged);
1137 // We have informed the cursor. Subsequent visibility changes should not tell it that
1138 // the folder has changed.
1139 mFolderChanged = false;
1140 }
1141 }
1142
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -08001143 @Override
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -08001144 public void onConversationListVisibilityChanged(boolean visible) {
Vikram Aggarwal69b5c302012-09-05 11:11:13 -07001145 informCursorVisiblity(visible);
Andy Huangc94d07f2013-06-03 16:19:35 -07001146 commitAutoAdvanceOperation();
Scott Kennedy32ddb842013-08-28 17:38:22 -07001147
1148 // Notify special views
1149 final ConversationListFragment convListFragment = getConversationListFragment();
1150 if (convListFragment != null && convListFragment.getAnimatedAdapter() != null) {
1151 convListFragment.getAnimatedAdapter().onConversationListVisibilityChanged(visible);
1152 }
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -08001153 }
1154
Vikram Aggarwal4a5c5302012-01-12 15:07:13 -08001155 /**
Vikram Aggarwal7dd054e2012-05-21 14:43:10 -07001156 * Called when a conversation is visible. Child classes must call the super class implementation
1157 * before performing local computation.
Vikram Aggarwal4a5c5302012-01-12 15:07:13 -08001158 */
1159 @Override
1160 public void onConversationVisibilityChanged(boolean visible) {
Andy Huangc94d07f2013-06-03 16:19:35 -07001161 commitAutoAdvanceOperation();
1162 }
1163
1164 /**
1165 * Commits any pending destructive action that was earlier deferred by an auto-advance
1166 * mode-change transition.
1167 */
1168 private void commitAutoAdvanceOperation() {
1169 if (mAutoAdvanceOp != null) {
1170 mAutoAdvanceOp.run();
1171 mAutoAdvanceOp = null;
1172 }
Vikram Aggarwal4a5c5302012-01-12 15:07:13 -08001173 }
1174
Vikram Aggarwal59f741f2013-03-01 15:55:40 -08001175 /**
1176 * Initialize development time logging. This can potentially log a lot of PII, and we don't want
1177 * to turn it on for shipped versions.
1178 */
1179 private void initializeDevLoggingService() {
1180 if (!MailLogService.DEBUG_ENABLED) {
1181 return;
1182 }
1183 // Check every 5 minutes.
1184 final int WAIT_TIME = 5 * 60 * 1000;
1185 // Start a runnable that periodically checks the log level and starts/stops the service.
1186 mLogServiceChecker = new Runnable() {
1187 /** True if currently logging. */
1188 private boolean mCurrentlyLogging = false;
1189
1190 /**
1191 * If the logging level has been changed since the previous run, start or stop the
1192 * service.
1193 */
1194 private void startOrStopService() {
1195 // If the log level is already high, start the service.
1196 final Intent i = new Intent(mContext, MailLogService.class);
1197 final boolean loggingEnabled = MailLogService.isLoggingLevelHighEnough();
1198 if (mCurrentlyLogging == loggingEnabled) {
1199 // No change since previous run, just return;
1200 return;
1201 }
1202 if (loggingEnabled) {
1203 LogUtils.e(LOG_TAG, "Starting MailLogService");
1204 mContext.startService(i);
1205 } else {
1206 LogUtils.e(LOG_TAG, "Stopping MailLogService");
1207 mContext.stopService(i);
1208 }
1209 mCurrentlyLogging = loggingEnabled;
1210 }
1211
1212 @Override
1213 public void run() {
1214 startOrStopService();
1215 mHandler.postDelayed(this, WAIT_TIME);
1216 }
1217 };
1218 // Start the runnable right away.
1219 mHandler.post(mLogServiceChecker);
1220 }
1221
Vikram Aggarwal6aca6892013-06-04 13:53:27 -07001222 /**
1223 * The application can be started from the following entry points:
1224 * <ul>
1225 * <li>Launcher: you tap on the Gmail icon in the launcher. This is what most users think of
1226 * as “Starting the app”.</li>
1227 * <li>Shortcut: Users can make a shortcut to take them directly to a label.</li>
1228 * <li>Widget: Shows the contents of a synced label, and allows:
1229 * <ul>
1230 * <li>Viewing the list (tapping on the title)</li>
1231 * <li>Composing a new message (tapping on the new message icon in the title. This
1232 * launches the {@link ComposeActivity}.
1233 * </li>
1234 * <li>Viewing a single message (tapping on a list element)</li>
1235 * </ul>
1236 *
1237 * </li>
1238 * <li>Tapping on a notification:
1239 * <ul>
1240 * <li>Shows message list if more than one message</li>
1241 * <li>Shows the conversation if the notification is for a single message</li>
1242 * </ul>
1243 * </li>
1244 * <li>...and most importantly, the activity life cycle can tear down the application and
1245 * restart it:
1246 * <ul>
1247 * <li>Rotate the application: it is destroyed and recreated.</li>
1248 * <li>Navigate away, and return from recent applications.</li>
1249 * </ul>
1250 * </li>
1251 * <li>Add a new account: fires off an intent to add an account,
1252 * and returns in {@link #onActivityResult(int, int, android.content.Intent)} .</li>
1253 * <li>Re-authenticate your account: again returns in onActivityResult().</li>
1254 * <li>Composing can happen from many entry points: third party applications fire off an
1255 * intent to compose email, and launch directly into the {@link ComposeActivity}
1256 * .</li>
1257 * </ul>
1258 * {@inheritDoc}
1259 */
Vikram Aggarwal4a5c5302012-01-12 15:07:13 -08001260 @Override
Vikram Aggarwald7a12cd2012-02-03 09:36:20 -08001261 public boolean onCreate(Bundle savedState) {
Vikram Aggarwalabd24d82012-04-26 13:23:14 -07001262 initializeActionBar();
Vikram Aggarwal59f741f2013-03-01 15:55:40 -08001263 initializeDevLoggingService();
Vikram Aggarwala55b36c2012-01-13 11:45:02 -08001264 // Allow shortcut keys to function for the ActionBar and menus.
1265 mActivity.setDefaultKeyMode(Activity.DEFAULT_KEYS_SHORTCUT);
Vikram Aggarwal80aeac52012-02-07 15:27:20 -08001266 mResolver = mActivity.getContentResolver();
Paul Westbrook6ead20d2012-03-19 14:48:14 -07001267 mNewEmailReceiver = new SuppressNotificationReceiver();
Vikram Aggarwal7c401b72012-08-13 16:43:47 -07001268 mRecentFolderList.initialize(mActivity);
Vikram Aggarwal69a6cdf2013-01-08 16:05:17 -08001269 mVeiledMatcher.initialize(this);
Paul Westbrook6ead20d2012-03-19 14:48:14 -07001270
Andy Huang12b3ee42013-04-24 22:49:43 -07001271 mDrawerToggle = new ActionBarDrawerToggle((Activity) mActivity, mDrawerContainer,
Tony Mantlera9a9b9f2014-01-13 19:29:07 +00001272 R.drawable.ic_drawer, R.string.drawer_open, R.string.drawer_close);
Andrew Sappersteina3ce6782013-05-09 15:14:49 -07001273 mDrawerContainer.setDrawerListener(mDrawerListener);
Andy Huang12b3ee42013-04-24 22:49:43 -07001274 mDrawerContainer.setDrawerShadow(
1275 mContext.getResources().getDrawable(R.drawable.drawer_shadow), Gravity.START);
1276
1277 mDrawerToggle.setDrawerIndicatorEnabled(isDrawerEnabled());
1278
Mindy Pereira161f50d2012-02-28 15:47:19 -08001279 // All the individual UI components listen for ViewMode changes. This
Mindy Pereirab849dfb2012-03-07 18:13:15 -08001280 // simplifies the amount of logic in the AbstractActivityController, but increases the
1281 // possibility of timing-related bugs.
Vikram Aggarwala55b36c2012-01-13 11:45:02 -08001282 mViewMode.addListener(this);
Andy Huang632721e2012-04-11 16:57:26 -07001283 mPagerController = new ConversationPagerController(mActivity, this);
Andrew Sappersteinc2c9dc12012-07-02 18:17:32 -07001284 mToastBar = (ActionableToastBar) mActivity.findViewById(R.id.toast_bar);
Vikram Aggarwalada51782012-04-26 14:18:31 -07001285 attachActionBar();
Andy Huang632721e2012-04-11 16:57:26 -07001286
Andy Huang144bfe72013-06-11 13:27:52 -07001287 mDrawIdler.setRootView(mActivity.getWindow().getDecorView());
1288
Andy Huang632721e2012-04-11 16:57:26 -07001289 final Intent intent = mActivity.getIntent();
Andy Huang4fe0af82013-08-20 17:24:51 -07001290
Vikram Aggarwalabd24d82012-04-26 13:23:14 -07001291 // Immediately handle a clean launch with intent, and any state restoration
Andy Huang632721e2012-04-11 16:57:26 -07001292 // that does not rely on restored fragments or loader data
1293 // any state restoration that relies on those can be done later in
1294 // onRestoreInstanceState, once fragments are up and loader data is re-delivered
1295 if (savedState != null) {
1296 if (savedState.containsKey(SAVED_ACCOUNT)) {
1297 setAccount((Account) savedState.getParcelable(SAVED_ACCOUNT));
Andy Huang632721e2012-04-11 16:57:26 -07001298 }
1299 if (savedState.containsKey(SAVED_FOLDER)) {
Andy Huang2bc8bc12012-11-12 17:24:25 -08001300 final Folder folder = savedState.getParcelable(SAVED_FOLDER);
Vikram Aggarwal203dc002012-08-23 13:59:04 -07001301 final String query = savedState.getString(SAVED_QUERY, null);
Vikram Aggarwal649b9ea2012-08-27 12:15:20 -07001302 setListContext(folder, query);
Andy Huang632721e2012-04-11 16:57:26 -07001303 }
Vikram Aggarwal6cadbfc2012-12-27 09:17:05 -08001304 if (savedState.containsKey(SAVED_ACTION)) {
1305 mDialogAction = savedState.getInt(SAVED_ACTION);
1306 }
Vikram Aggarwala91d00b2013-01-18 12:00:37 -08001307 mDialogFromSelectedSet = savedState.getBoolean(SAVED_ACTION_FROM_SELECTED, false);
Vikram Aggarwal649b9ea2012-08-27 12:15:20 -07001308 mViewMode.handleRestore(savedState);
Andy Huang632721e2012-04-11 16:57:26 -07001309 } else if (intent != null) {
1310 handleIntent(intent);
1311 }
Andy Huang632721e2012-04-11 16:57:26 -07001312 // Create the accounts loader; this loads the account switch spinner.
Vikram Aggarwal177097f2013-03-08 11:19:53 -08001313 mActivity.getLoaderManager().initLoader(LOADER_ACCOUNT_CURSOR, Bundle.EMPTY,
1314 mAccountCallbacks);
Andy Huang632721e2012-04-11 16:57:26 -07001315 return true;
Andy Huangb1c34dc2012-04-17 16:36:19 -07001316 }
1317
1318 @Override
Paul Westbrook57246a42013-04-21 09:40:22 -07001319 public void onPostCreate(Bundle savedState) {
Andy Huang12b3ee42013-04-24 22:49:43 -07001320 // Sync the toggle state after onRestoreInstanceState has occurred.
1321 mDrawerToggle.syncState();
Andrew Sapperstein5747e152013-05-13 14:13:08 -07001322
Vikram Aggarwald70fe492013-06-04 12:52:07 -07001323 mHideMenuItems = isDrawerEnabled() && mDrawerContainer.isDrawerOpen(mDrawerPullout);
Paul Westbrook57246a42013-04-21 09:40:22 -07001324 }
1325
1326 @Override
1327 public void onConfigurationChanged(Configuration newConfig) {
Andy Huang12b3ee42013-04-24 22:49:43 -07001328 mDrawerToggle.onConfigurationChanged(newConfig);
1329 }
1330
1331 /**
1332 * If drawer is open/visible (even partially), close it.
1333 */
1334 protected void closeDrawerIfOpen() {
1335 if (!isDrawerEnabled()) {
1336 return;
1337 }
1338 if(mDrawerContainer.isDrawerOpen(mDrawerPullout)) {
1339 mDrawerContainer.closeDrawers();
1340 }
Paul Westbrook57246a42013-04-21 09:40:22 -07001341 }
1342
1343 @Override
Andy Huang1ee96b22012-08-24 20:19:53 -07001344 public void onStart() {
1345 mSafeToModifyFragments = true;
Scott Kennedycb85aea2013-02-25 13:08:32 -08001346
1347 NotificationActionUtils.registerUndoNotificationObserver(mUndoNotificationObserver);
Andy Huang761522c2013-08-08 13:09:11 -07001348
1349 if (mViewMode.getMode() != ViewMode.UNKNOWN) {
1350 Analytics.getInstance().sendView("MainActivity" + mViewMode.toString());
1351 }
Andy Huang1ee96b22012-08-24 20:19:53 -07001352 }
1353
1354 @Override
Andrew Sapperstein00179f12012-08-09 15:15:40 -07001355 public void onRestart() {
Vikram Aggarwal177097f2013-03-08 11:19:53 -08001356 final DialogFragment fragment = (DialogFragment)
Andrew Sapperstein00179f12012-08-09 15:15:40 -07001357 mFragmentManager.findFragmentByTag(SYNC_ERROR_DIALOG_FRAGMENT_TAG);
1358 if (fragment != null) {
1359 fragment.dismiss();
1360 }
mindypea04f932012-08-27 14:17:59 -07001361 // When the user places the app in the background by pressing "home",
1362 // dismiss the toast bar. However, since there is no way to determine if
1363 // home was pressed, just dismiss any existing toast bar when restarting
1364 // the app.
1365 if (mToastBar != null) {
Andrew Sappersteinf8ccdcf2013-08-08 19:30:38 -07001366 mToastBar.hide(false, false /* actionClicked */);
mindypea04f932012-08-27 14:17:59 -07001367 }
Andrew Sapperstein00179f12012-08-09 15:15:40 -07001368 }
1369
1370 @Override
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -08001371 public Dialog onCreateDialog(int id, Bundle bundle) {
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -08001372 return null;
Vikram Aggarwala55b36c2012-01-13 11:45:02 -08001373 }
1374
1375 @Override
Vikram Aggarwal4eb52712012-06-19 16:24:50 -07001376 public final boolean onCreateOptionsMenu(Menu menu) {
Andrew Sapperstein0fa9efd2013-08-07 14:09:31 -07001377 if (mViewMode.isAdMode()) {
1378 return false;
1379 }
Vikram Aggarwale5e917c2012-09-20 16:27:41 -07001380 final MenuInflater inflater = mActivity.getMenuInflater();
Mindy Pereiraf5acda42012-02-15 20:13:59 -08001381 inflater.inflate(mActionBarView.getOptionsMenuId(), menu);
Mindy Pereira68f2e222012-03-07 10:36:54 -08001382 mActionBarView.onCreateOptionsMenu(menu);
Mindy Pereira28d5f722012-02-15 12:32:40 -08001383 return true;
Vikram Aggarwala55b36c2012-01-13 11:45:02 -08001384 }
1385
1386 @Override
Vikram Aggarwal4eb52712012-06-19 16:24:50 -07001387 public final boolean onKeyDown(int keyCode, KeyEvent event) {
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -08001388 return false;
1389 }
1390
mindyp17a8e782012-11-29 14:56:17 -08001391 public abstract boolean doesActionChangeConversationListVisibility(int action);
1392
Jin Cao30c881a2014-04-08 14:28:36 -07001393 /**
1394 * Helper function that determines if we should associate an undo callback with
1395 * the current menu action item
1396 * @param actionId the id of the action
1397 * @return the appropriate callback handler, or null if not applicable
1398 */
1399 private UndoCallback getUndoCallbackForDestructiveActionsWithAutoAdvance(
1400 int actionId, final Conversation conv) {
1401 // We associated the undoCallback if the user is going to perform an action on the current
1402 // conversation, causing the current conversation to be removed from view and replacing it
1403 // with another (via Auto Advance). The undoCallback will bring the removed conversation
1404 // back into the view if the action is undone.
1405 final Collection<Conversation> convCol = Conversation.listOf(conv);
1406 final boolean isApplicableForReshow = mAccount != null &&
1407 mAccount.settings != null &&
1408 mTracker != null &&
1409 // ensure that we will show another conversation due to Auto Advance
1410 mTracker.getNextConversation(
1411 mAccount.settings.getAutoAdvanceSetting(), convCol) != null &&
1412 // ensure that we are performing the action from conversation view
1413 isCurrentConversationInView(convCol) &&
1414 // check for the appropriate destructive actions
1415 doesActionRemoveCurrentConversationFromView(actionId);
1416 return (isApplicableForReshow) ?
1417 new UndoCallback() {
1418 @Override
1419 public void performUndoCallback() {
1420 showConversation(conv);
1421 }
1422 } : null;
1423 }
1424
1425 /**
1426 * Check if the provided action will remove the active conversation from view
1427 * @param actionId the applied action
1428 * @return true if it will remove the conversation from view, false otherwise
1429 */
1430 private boolean doesActionRemoveCurrentConversationFromView(int actionId) {
1431 return actionId == R.id.archive ||
1432 actionId == R.id.delete ||
1433 actionId == R.id.remove_folder ||
1434 actionId == R.id.report_spam ||
1435 actionId == R.id.report_phishing ||
1436 actionId == R.id.move_to;
1437 }
1438
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -08001439 @Override
Paul Westbrook57246a42013-04-21 09:40:22 -07001440 public boolean onOptionsItemSelected(MenuItem item) {
Andy Huang761522c2013-08-08 13:09:11 -07001441
Andy Huang12b3ee42013-04-24 22:49:43 -07001442 /*
1443 * The action bar home/up action should open or close the drawer.
1444 * mDrawerToggle will take care of this.
1445 */
1446 if (mDrawerToggle.onOptionsItemSelected(item)) {
Andy Huang042a5302013-08-13 12:39:08 -07001447 Analytics.getInstance().sendEvent(Analytics.EVENT_CATEGORY_MENU_ITEM, "drawer_toggle",
1448 null, 0);
Andy Huang12b3ee42013-04-24 22:49:43 -07001449 return true;
1450 }
1451
Andy Huang2b555492013-08-14 21:06:21 -07001452 Analytics.getInstance().sendMenuItemEvent(Analytics.EVENT_CATEGORY_MENU_ITEM,
Andy Huangf8c59b02014-03-19 20:00:53 -07001453 item.getItemId(), "action_bar/" + mViewMode.getModeString(), 0);
Andy Huang042a5302013-08-13 12:39:08 -07001454
Vikram Aggarwal54452ae2012-03-13 15:29:00 -07001455 final int id = item.getItemId();
Vikram Aggarwal7dd054e2012-05-21 14:43:10 -07001456 LogUtils.d(LOG_TAG, "AbstractController.onOptionsItemSelected(%d) called.", id);
Mindy Pereira9b875682012-02-15 18:10:54 -08001457 boolean handled = true;
Vikram Aggarwal84fe9942013-04-17 11:20:58 -07001458 /** This is NOT a batch action. */
1459 final boolean isBatch = false;
Mindy Pereiraba68fda2012-05-24 15:53:06 -07001460 final Collection<Conversation> target = Conversation.listOf(mCurrentConversation);
Vikram Aggarwal112cd162012-06-18 10:51:13 -07001461 final Settings settings = (mAccount == null) ? null : mAccount.settings;
mindyp84f7d322012-10-01 17:14:40 -07001462 // The user is choosing a new action; commit whatever they had been
mindyp17a8e782012-11-29 14:56:17 -08001463 // doing before. Don't animate if we are launching a new screen.
1464 commitDestructiveActions(!doesActionChangeConversationListVisibility(id));
Jin Cao30c881a2014-04-08 14:28:36 -07001465 final UndoCallback undoCallback = getUndoCallbackForDestructiveActionsWithAutoAdvance(
1466 id, mCurrentConversation);
1467
Scott Kennedy2b9d80e2013-07-30 23:03:45 -07001468 if (id == R.id.archive) {
1469 final boolean showDialog = (settings != null && settings.confirmArchive);
Jin Cao30c881a2014-04-08 14:28:36 -07001470 confirmAndDelete(id, target, showDialog, R.plurals.confirm_archive_conversation, undoCallback);
Scott Kennedy2b9d80e2013-07-30 23:03:45 -07001471 } else if (id == R.id.remove_folder) {
1472 delete(R.id.remove_folder, target,
Jin Cao30c881a2014-04-08 14:28:36 -07001473 getDeferredRemoveFolder(target, mFolder, true, isBatch, true, undoCallback),
1474 isBatch);
Scott Kennedy2b9d80e2013-07-30 23:03:45 -07001475 } else if (id == R.id.delete) {
1476 final boolean showDialog = (settings != null && settings.confirmDelete);
Jin Cao30c881a2014-04-08 14:28:36 -07001477 confirmAndDelete(id, target, showDialog, R.plurals.confirm_delete_conversation, undoCallback);
Scott Kennedy2b9d80e2013-07-30 23:03:45 -07001478 } else if (id == R.id.discard_drafts) {
Andy Huang121c8b82013-08-05 11:52:55 -07001479 // drafts are lost forever, so always confirm
1480 confirmAndDelete(id, target, true /* showDialog */,
Jin Cao30c881a2014-04-08 14:28:36 -07001481 R.plurals.confirm_discard_drafts_conversation, undoCallback);
Scott Kennedy2b9d80e2013-07-30 23:03:45 -07001482 } else if (id == R.id.mark_important) {
1483 updateConversation(Conversation.listOf(mCurrentConversation),
1484 ConversationColumns.PRIORITY, UIProvider.ConversationPriority.HIGH);
1485 } else if (id == R.id.mark_not_important) {
1486 if (mFolder != null && mFolder.isImportantOnly()) {
1487 delete(R.id.mark_not_important, target,
Jin Cao30c881a2014-04-08 14:28:36 -07001488 getDeferredAction(R.id.mark_not_important, target, isBatch, undoCallback),
1489 isBatch);
Scott Kennedy2b9d80e2013-07-30 23:03:45 -07001490 } else {
Vikram Aggarwal531488e2012-05-29 16:36:52 -07001491 updateConversation(Conversation.listOf(mCurrentConversation),
Scott Kennedy2b9d80e2013-07-30 23:03:45 -07001492 ConversationColumns.PRIORITY, UIProvider.ConversationPriority.LOW);
1493 }
1494 } else if (id == R.id.mute) {
Jin Cao30c881a2014-04-08 14:28:36 -07001495 delete(R.id.mute, target, getDeferredAction(R.id.mute, target, isBatch, undoCallback),
1496 isBatch);
Scott Kennedy2b9d80e2013-07-30 23:03:45 -07001497 } else if (id == R.id.report_spam) {
1498 delete(R.id.report_spam, target,
Jin Cao30c881a2014-04-08 14:28:36 -07001499 getDeferredAction(R.id.report_spam, target, isBatch, undoCallback), isBatch);
Scott Kennedy2b9d80e2013-07-30 23:03:45 -07001500 } else if (id == R.id.mark_not_spam) {
1501 // Currently, since spam messages are only shown in list with
1502 // other spam messages,
1503 // marking a message not as spam is a destructive action
1504 delete(R.id.mark_not_spam, target,
Jin Cao30c881a2014-04-08 14:28:36 -07001505 getDeferredAction(R.id.mark_not_spam, target, isBatch, undoCallback), isBatch);
Scott Kennedy2b9d80e2013-07-30 23:03:45 -07001506 } else if (id == R.id.report_phishing) {
1507 delete(R.id.report_phishing, target,
Jin Cao30c881a2014-04-08 14:28:36 -07001508 getDeferredAction(R.id.report_phishing, target, isBatch, undoCallback), isBatch);
Scott Kennedy2b9d80e2013-07-30 23:03:45 -07001509 } else if (id == android.R.id.home) {
1510 onUpPressed();
1511 } else if (id == R.id.compose) {
1512 ComposeActivity.compose(mActivity.getActivityContext(), mAccount);
1513 } else if (id == R.id.refresh) {
1514 requestFolderRefresh();
1515 } else if (id == R.id.settings) {
1516 Utils.showSettings(mActivity.getActivityContext(), mAccount);
1517 } else if (id == R.id.folder_options) {
1518 Utils.showFolderSettings(mActivity.getActivityContext(), mAccount, mFolder);
1519 } else if (id == R.id.help_info_menu_item) {
1520 Utils.showHelp(mActivity.getActivityContext(), mAccount, getHelpContext());
1521 } else if (id == R.id.feedback_menu_item) {
1522 Utils.sendFeedback(mActivity, mAccount, false);
1523 } else if (id == R.id.manage_folders_item) {
1524 Utils.showManageFolder(mActivity.getActivityContext(), mAccount);
Andrew Sapperstein6c570db2013-08-06 17:21:36 -07001525 } else if (id == R.id.move_to || id == R.id.change_folders) {
Tony Mantler2a4be242013-12-11 15:30:53 -08001526 final FolderSelectionDialog dialog = FolderSelectionDialog.getInstance(mAccount,
Scott Kennedy2b9d80e2013-07-30 23:03:45 -07001527 Conversation.listOf(mCurrentConversation), isBatch, mFolder,
1528 id == R.id.move_to);
1529 if (dialog != null) {
Tony Mantler2a4be242013-12-11 15:30:53 -08001530 dialog.show(mActivity.getFragmentManager(), null);
Scott Kennedy2b9d80e2013-07-30 23:03:45 -07001531 }
1532 } else if (id == R.id.move_to_inbox) {
1533 new AsyncTask<Void, Void, Folder>() {
1534 @Override
1535 protected Folder doInBackground(final Void... params) {
1536 // Get the "move to" inbox
1537 return Utils.getFolder(mContext, mAccount.settings.moveToInbox,
1538 true /* allowHidden */);
Mindy Pereira0d03ef82012-08-15 09:05:48 -07001539 }
Scott Kennedydd2ec682013-06-03 19:16:13 -07001540
Scott Kennedy2b9d80e2013-07-30 23:03:45 -07001541 @Override
1542 protected void onPostExecute(final Folder moveToInbox) {
1543 final List<FolderOperation> ops = Lists.newArrayListWithCapacity(1);
1544 // Add inbox
1545 ops.add(new FolderOperation(moveToInbox, true));
1546 assignFolder(ops, Conversation.listOf(mCurrentConversation), true,
1547 true /* showUndo */, false /* isMoveTo */);
1548 }
1549 }.execute((Void[]) null);
1550 } else if (id == R.id.empty_trash) {
1551 showEmptyDialog();
1552 } else if (id == R.id.empty_spam) {
1553 showEmptyDialog();
1554 } else {
1555 handled = false;
Mindy Pereira28d5f722012-02-15 12:32:40 -08001556 }
Mindy Pereira9b875682012-02-15 18:10:54 -08001557 return handled;
Vikram Aggarwala55b36c2012-01-13 11:45:02 -08001558 }
1559
Andrew Sappersteined5b52d2013-04-30 13:40:18 -07001560 /**
1561 * Opens an {@link EmptyFolderDialogFragment} for the current folder.
1562 */
1563 private void showEmptyDialog() {
1564 if (mFolder != null) {
1565 final EmptyFolderDialogFragment fragment =
1566 EmptyFolderDialogFragment.newInstance(mFolder.totalCount, mFolder.type);
1567 fragment.setListener(this);
1568 fragment.show(mActivity.getFragmentManager(), EmptyFolderDialogFragment.FRAGMENT_TAG);
1569 }
1570 }
1571
1572 @Override
1573 public void onFolderEmptied() {
1574 emptyFolder();
1575 }
1576
1577 /**
1578 * Performs the work of emptying the currently visible folder.
1579 */
Scott Kennedy7ee089e2013-03-25 17:05:44 -04001580 private void emptyFolder() {
Yorke Lee1d0f1f82013-04-26 14:10:27 -07001581 if (mConversationListCursor != null) {
1582 mConversationListCursor.emptyFolder();
1583 }
Scott Kennedy7ee089e2013-03-25 17:05:44 -04001584 }
1585
Andrew Sappersteined5b52d2013-04-30 13:40:18 -07001586 private void attachEmptyFolderDialogFragmentListener() {
1587 final EmptyFolderDialogFragment fragment =
1588 (EmptyFolderDialogFragment) mActivity.getFragmentManager()
1589 .findFragmentByTag(EmptyFolderDialogFragment.FRAGMENT_TAG);
1590
1591 if (fragment != null) {
1592 fragment.setListener(this);
1593 }
1594 }
1595
Rohan Shah8e65c6d2013-03-07 16:47:25 -08001596 /**
Andy Huang12b3ee42013-04-24 22:49:43 -07001597 * Toggles the drawer pullout. If it was open (Fully extended), the
1598 * drawer will be closed. Otherwise, the drawer will be opened. This should
1599 * only be called when used with a toggle item. Other cases should be handled
1600 * explicitly with just closeDrawers() or openDrawer(View drawerView);
Rohan Shah8e65c6d2013-03-07 16:47:25 -08001601 */
Tony Mantlerd9eaca82013-08-05 11:42:29 -07001602 protected void toggleDrawerState() {
Andy Huang12b3ee42013-04-24 22:49:43 -07001603 if (!isDrawerEnabled()) {
1604 return;
1605 }
1606 if(mDrawerContainer.isDrawerOpen(mDrawerPullout)) {
1607 mDrawerContainer.closeDrawers();
1608 } else {
1609 mDrawerContainer.openDrawer(mDrawerPullout);
1610 }
Rohan Shah8e65c6d2013-03-07 16:47:25 -08001611 }
1612
Vikram Aggarwal531488e2012-05-29 16:36:52 -07001613 @Override
Andy Huangc1fb9a92013-02-11 13:09:12 -08001614 public final boolean onUpPressed() {
1615 for (UpOrBackHandler h : mUpOrBackHandlers) {
1616 if (h.onUpPressed()) {
1617 return true;
1618 }
1619 }
1620 return handleUpPress();
1621 }
1622
1623 @Override
1624 public final boolean onBackPressed() {
1625 for (UpOrBackHandler h : mUpOrBackHandlers) {
1626 if (h.onBackPressed()) {
1627 return true;
1628 }
1629 }
Andy Huang12b3ee42013-04-24 22:49:43 -07001630
1631 if (isDrawerEnabled() && mDrawerContainer.isDrawerVisible(mDrawerPullout)) {
1632 mDrawerContainer.closeDrawers();
1633 return true;
1634 }
1635
Andy Huangc1fb9a92013-02-11 13:09:12 -08001636 return handleBackPress();
1637 }
1638
1639 protected abstract boolean handleBackPress();
1640 protected abstract boolean handleUpPress();
1641
1642 @Override
1643 public void addUpOrBackHandler(UpOrBackHandler handler) {
1644 if (mUpOrBackHandlers.contains(handler)) {
1645 return;
1646 }
1647 mUpOrBackHandlers.addFirst(handler);
1648 }
1649
1650 @Override
1651 public void removeUpOrBackHandler(UpOrBackHandler handler) {
1652 mUpOrBackHandlers.remove(handler);
1653 }
1654
1655 @Override
Mindy Pereira6c2663d2012-07-20 15:37:29 -07001656 public void updateConversation(Collection<Conversation> target, ContentValues values) {
Scott Kennedy9e2d4072013-03-21 21:46:01 -07001657 mConversationListCursor.updateValues(target, values);
Mindy Pereira6c2663d2012-07-20 15:37:29 -07001658 refreshConversationList();
1659 }
1660
1661 @Override
Vikram Aggarwal531488e2012-05-29 16:36:52 -07001662 public void updateConversation(Collection <Conversation> target, String columnName,
1663 boolean value) {
Scott Kennedy9e2d4072013-03-21 21:46:01 -07001664 mConversationListCursor.updateBoolean(target, columnName, value);
Vikram Aggarwal75daee52012-04-30 11:13:09 -07001665 refreshConversationList();
Vikram Aggarwal54452ae2012-03-13 15:29:00 -07001666 }
1667
Vikram Aggarwal531488e2012-05-29 16:36:52 -07001668 @Override
Mindy Pereira6c2663d2012-07-20 15:37:29 -07001669 public void updateConversation(Collection <Conversation> target, String columnName,
1670 int value) {
Scott Kennedy9e2d4072013-03-21 21:46:01 -07001671 mConversationListCursor.updateInt(target, columnName, value);
Vikram Aggarwal75daee52012-04-30 11:13:09 -07001672 refreshConversationList();
Mindy Pereirac9d59182012-03-22 16:06:46 -07001673 }
1674
Vikram Aggarwal531488e2012-05-29 16:36:52 -07001675 @Override
1676 public void updateConversation(Collection <Conversation> target, String columnName,
1677 String value) {
Scott Kennedy9e2d4072013-03-21 21:46:01 -07001678 mConversationListCursor.updateString(target, columnName, value);
Vikram Aggarwal75daee52012-04-30 11:13:09 -07001679 refreshConversationList();
Vikram Aggarwal54452ae2012-03-13 15:29:00 -07001680 }
1681
Andy Huang839ada22012-07-20 15:48:40 -07001682 @Override
Yu Ping Hu7c909c72013-01-18 11:58:01 -08001683 public void markConversationMessagesUnread(final Conversation conv,
1684 final Set<Uri> unreadMessageUris, final byte[] originalConversationInfo) {
Andy Huang8f6b0062012-07-31 15:36:31 -07001685 // The only caller of this method is the conversation view, from where marking unread should
1686 // *always* take you back to list mode.
1687 showConversation(null);
1688
Andy Huang839ada22012-07-20 15:48:40 -07001689 // locally mark conversation unread (the provider is supposed to propagate message unread
1690 // to conversation unread)
1691 conv.read = false;
Paul Westbrook1faf93d2012-10-16 08:58:07 -07001692 if (mConversationListCursor == null) {
Vikram Aggarwal81230ee2013-02-26 15:26:44 -08001693 LogUtils.d(LOG_TAG, "markConversationMessagesUnread(id=%d), deferring", conv.id);
Paul Westbrook1faf93d2012-10-16 08:58:07 -07001694
Yu Ping Hu7c909c72013-01-18 11:58:01 -08001695 mConversationListLoadFinishedCallbacks.add(new LoadFinishedCallback() {
1696 @Override
1697 public void onLoadFinished() {
1698 doMarkConversationMessagesUnread(conv, unreadMessageUris,
1699 originalConversationInfo);
1700 }
1701 });
1702 } else {
Vikram Aggarwal81230ee2013-02-26 15:26:44 -08001703 LogUtils.d(LOG_TAG, "markConversationMessagesUnread(id=%d), performing", conv.id);
Yu Ping Hu7c909c72013-01-18 11:58:01 -08001704 doMarkConversationMessagesUnread(conv, unreadMessageUris, originalConversationInfo);
1705 }
1706 }
1707
1708 private void doMarkConversationMessagesUnread(Conversation conv, Set<Uri> unreadMessageUris,
1709 byte[] originalConversationInfo) {
Vikram Aggarwal81230ee2013-02-26 15:26:44 -08001710 // Only do a granular 'mark unread' if a subset of messages are unread
Andy Huang28e31e22012-07-26 16:33:15 -07001711 final int unreadCount = (unreadMessageUris == null) ? 0 : unreadMessageUris.size();
Mindy Pereira0972e072012-08-01 17:43:06 -07001712 final int numMessages = conv.getNumMessages();
1713 final boolean subsetIsUnread = (numMessages > 1 && unreadCount > 0
1714 && unreadCount < numMessages);
Andy Huang28e31e22012-07-26 16:33:15 -07001715
Andy Huang9e4ca792013-02-28 14:33:43 -08001716 LogUtils.d(LOG_TAG, "markConversationMessagesUnread(conv=%s)"
Vikram Aggarwal81230ee2013-02-26 15:26:44 -08001717 + ", numMessages=%d, unreadCount=%d, subsetIsUnread=%b",
Andy Huang9e4ca792013-02-28 14:33:43 -08001718 conv, numMessages, unreadCount, subsetIsUnread);
Andy Huang28e31e22012-07-26 16:33:15 -07001719 if (!subsetIsUnread) {
Vikram Aggarwal66bc2aa2012-08-02 10:47:03 -07001720 // Conversations are neither marked read, nor viewed, and we don't want to show
1721 // the next conversation.
Vikram Aggarwal81230ee2013-02-26 15:26:44 -08001722 LogUtils.d(LOG_TAG, ". . doing full mark unread");
Scott Kennedycaaeed32013-06-12 13:39:16 -07001723 markConversationsRead(Collections.singletonList(conv), false, false, false);
Andy Huang839ada22012-07-20 15:48:40 -07001724 } else {
Vikram Aggarwal81230ee2013-02-26 15:26:44 -08001725 if (LogUtils.isLoggable(LOG_TAG, LogUtils.DEBUG)) {
1726 final ConversationInfo info = ConversationInfo.fromBlob(originalConversationInfo);
1727 LogUtils.d(LOG_TAG, ". . doing subset mark unread, originalConversationInfo = %s",
1728 info);
1729 }
Andy Huangdaa06ab2012-07-24 10:46:44 -07001730 mConversationListCursor.setConversationColumn(conv.uri, ConversationColumns.READ, 0);
Andy Huang839ada22012-07-20 15:48:40 -07001731
Vikram Aggarwal81230ee2013-02-26 15:26:44 -08001732 // Locally update conversation's conversationInfo to revert to original version
Andy Huang28e31e22012-07-26 16:33:15 -07001733 if (originalConversationInfo != null) {
1734 mConversationListCursor.setConversationColumn(conv.uri,
1735 ConversationColumns.CONVERSATION_INFO, originalConversationInfo);
1736 }
Andy Huang839ada22012-07-20 15:48:40 -07001737
1738 // applyBatch with each CPO as an UPDATE op on each affected message uri
1739 final ArrayList<ContentProviderOperation> ops = Lists.newArrayList();
1740 String authority = null;
1741 for (Uri messageUri : unreadMessageUris) {
1742 if (authority == null) {
1743 authority = messageUri.getAuthority();
1744 }
1745 ops.add(ContentProviderOperation.newUpdate(messageUri)
1746 .withValue(UIProvider.MessageColumns.READ, 0)
1747 .build());
Vikram Aggarwal81230ee2013-02-26 15:26:44 -08001748 LogUtils.d(LOG_TAG, ". . Adding op: read=0, uri=%s", messageUri);
Andy Huang839ada22012-07-20 15:48:40 -07001749 }
Vikram Aggarwal81230ee2013-02-26 15:26:44 -08001750 LogUtils.d(LOG_TAG, ". . operations = %s", ops);
Andy Huang839ada22012-07-20 15:48:40 -07001751 new ContentProviderTask() {
1752 @Override
1753 protected void onPostExecute(Result result) {
Vikram Aggarwald9895b62013-02-27 16:52:27 -08001754 if (result.exception != null) {
1755 LogUtils.e(LOG_TAG, result.exception, "ContentProviderTask() ERROR.");
1756 } else {
Andy Huang9e4ca792013-02-28 14:33:43 -08001757 LogUtils.d(LOG_TAG, "ContentProviderTask(): success %s",
1758 Arrays.toString(result.results));
Vikram Aggarwald9895b62013-02-27 16:52:27 -08001759 }
Andy Huang839ada22012-07-20 15:48:40 -07001760 }
1761 }.run(mResolver, authority, ops);
Andy Huang839ada22012-07-20 15:48:40 -07001762 }
Andy Huang839ada22012-07-20 15:48:40 -07001763 }
1764
1765 @Override
Yu Ping Hu7c909c72013-01-18 11:58:01 -08001766 public void markConversationsRead(final Collection<Conversation> targets, final boolean read,
1767 final boolean viewed) {
Scott Kennedy919d01a2013-05-07 16:13:29 -07001768 LogUtils.d(LOG_TAG, "markConversationsRead(targets=%s)", targets.toArray());
1769
Yu Ping Hu7c909c72013-01-18 11:58:01 -08001770 if (mConversationListCursor == null) {
Vikram Aggarwal81230ee2013-02-26 15:26:44 -08001771 if (LogUtils.isLoggable(LOG_TAG, LogUtils.DEBUG)) {
1772 LogUtils.d(LOG_TAG, "markConversationsRead(targets=%s), deferring",
1773 targets.toArray());
1774 }
Yu Ping Hu7c909c72013-01-18 11:58:01 -08001775 mConversationListLoadFinishedCallbacks.add(new LoadFinishedCallback() {
1776 @Override
1777 public void onLoadFinished() {
Scott Kennedycaaeed32013-06-12 13:39:16 -07001778 markConversationsRead(targets, read, viewed, true);
Yu Ping Hu7c909c72013-01-18 11:58:01 -08001779 }
1780 });
1781 } else {
1782 // We want to show the next conversation if we are marking unread.
Scott Kennedycaaeed32013-06-12 13:39:16 -07001783 markConversationsRead(targets, read, viewed, true);
Yu Ping Hu7c909c72013-01-18 11:58:01 -08001784 }
Andy Huang8f6b0062012-07-31 15:36:31 -07001785 }
1786
Scott Kennedy0d0f8b02012-10-12 15:18:18 -07001787 private void markConversationsRead(final Collection<Conversation> targets, final boolean read,
Scott Kennedycaaeed32013-06-12 13:39:16 -07001788 final boolean markViewed, final boolean showNext) {
Yu Ping Hu7c909c72013-01-18 11:58:01 -08001789 LogUtils.d(LOG_TAG, "performing markConversationsRead");
Vikram Aggarwalcc754b12012-08-30 14:04:21 -07001790 // Auto-advance if requested and the current conversation is being marked unread
Andy Huang8f6b0062012-07-31 15:36:31 -07001791 if (showNext && !read) {
Scott Kennedy0d0f8b02012-10-12 15:18:18 -07001792 final Runnable operation = new Runnable() {
1793 @Override
1794 public void run() {
Scott Kennedycaaeed32013-06-12 13:39:16 -07001795 markConversationsRead(targets, read, markViewed, showNext);
Scott Kennedy0d0f8b02012-10-12 15:18:18 -07001796 }
1797 };
1798
Scott Kennedycaaeed32013-06-12 13:39:16 -07001799 if (!showNextConversation(targets, operation)) {
Scott Kennedy0d0f8b02012-10-12 15:18:18 -07001800 // This method will be called again if the user selects an autoadvance option
1801 return;
1802 }
Andy Huang8f6b0062012-07-31 15:36:31 -07001803 }
Scott Kennedy0d0f8b02012-10-12 15:18:18 -07001804
Vikram Aggarwalcc754b12012-08-30 14:04:21 -07001805 final int size = targets.size();
1806 final List<ConversationOperation> opList = new ArrayList<ConversationOperation>(size);
1807 for (final Conversation target : targets) {
Paul Westbrooke2be97a2013-05-21 02:12:09 -07001808 final ContentValues value = new ContentValues(4);
Vikram Aggarwalcc754b12012-08-30 14:04:21 -07001809 value.put(ConversationColumns.READ, read);
Paul Westbrook5109c512012-11-05 11:00:30 -08001810
Scott Kennedyd5edd2d2012-12-05 11:11:32 -08001811 // We never want to mark unseen here, but we do want to mark it seen
1812 if (read || markViewed) {
1813 value.put(ConversationColumns.SEEN, Boolean.TRUE);
1814 }
1815
Paul Westbrook5109c512012-11-05 11:00:30 -08001816 // The mark read/unread/viewed operations do not show an undo bar
1817 value.put(ConversationOperations.Parameters.SUPPRESS_UNDO, true);
Vikram Aggarwal66bc2aa2012-08-02 10:47:03 -07001818 if (markViewed) {
Vikram Aggarwalcc754b12012-08-30 14:04:21 -07001819 value.put(ConversationColumns.VIEWED, true);
Vikram Aggarwal66bc2aa2012-08-02 10:47:03 -07001820 }
Vikram Aggarwal192fac12012-07-25 16:44:55 -07001821 final ConversationInfo info = target.conversationInfo;
Tony Mantleredd6c1a2013-10-08 14:47:43 -07001822 final boolean changed = info.markRead(read);
1823 if (changed) {
1824 value.put(ConversationColumns.CONVERSATION_INFO, info.toBlob());
Andy Huang839ada22012-07-20 15:48:40 -07001825 }
Vikram Aggarwalcc754b12012-08-30 14:04:21 -07001826 opList.add(mConversationListCursor.getOperationForConversation(
1827 target, ConversationOperation.UPDATE, value));
1828 // Update the local conversation objects so they immediately change state.
1829 target.read = read;
Andy Huangcd5c5ee2012-08-12 19:03:51 -07001830 if (markViewed) {
Vikram Aggarwalcc754b12012-08-30 14:04:21 -07001831 target.markViewed();
Andy Huangcd5c5ee2012-08-12 19:03:51 -07001832 }
Andy Huang839ada22012-07-20 15:48:40 -07001833 }
Scott Kennedy9e2d4072013-03-21 21:46:01 -07001834 mConversationListCursor.updateBulkValues(opList);
Andy Huang839ada22012-07-20 15:48:40 -07001835 }
1836
Andy Huang8f6b0062012-07-31 15:36:31 -07001837 /**
1838 * Auto-advance to a different conversation if the currently visible conversation in
1839 * conversation mode is affected (deleted, marked unread, etc.).
1840 *
Scott Kennedy0d0f8b02012-10-12 15:18:18 -07001841 * <p>Does nothing if outside of conversation mode.</p>
Andy Huang8f6b0062012-07-31 15:36:31 -07001842 *
1843 * @param target the set of conversations being deleted/marked unread
1844 */
mindyp9365a822012-09-12 09:09:09 -07001845 @Override
Scott Kennedy0d0f8b02012-10-12 15:18:18 -07001846 public void showNextConversation(final Collection<Conversation> target) {
Scott Kennedycaaeed32013-06-12 13:39:16 -07001847 showNextConversation(target, null);
Scott Kennedy0d0f8b02012-10-12 15:18:18 -07001848 }
1849
1850 /**
Jin Cao30c881a2014-04-08 14:28:36 -07001851 * Helper function to determine if the provided set of conversations is in view
1852 * @param target set of conversations that we are interested in
1853 * @return true if they are in view, false otherwise
1854 */
1855 private boolean isCurrentConversationInView(final Collection<Conversation> target) {
1856 final int viewMode = mViewMode.getMode();
1857 return (viewMode == ViewMode.CONVERSATION
1858 || viewMode == ViewMode.SEARCH_RESULTS_CONVERSATION)
1859 && Conversation.contains(target, mCurrentConversation);
1860 }
1861
1862 /**
Scott Kennedy0d0f8b02012-10-12 15:18:18 -07001863 * Auto-advance to a different conversation if the currently visible conversation in
1864 * conversation mode is affected (deleted, marked unread, etc.).
1865 *
1866 * <p>Does nothing if outside of conversation mode.</p>
Andy Huangc94d07f2013-06-03 16:19:35 -07001867 * <p>
1868 * Clients may pass an operation to execute on the target that this method will run after
1869 * auto-advance is complete. The operation, if provided, may run immediately, or it may run
1870 * later, or not at all. Reasons it may run later include:
1871 * <ul>
1872 * <li>the auto-advance setting is uninitialized and we need to wait for the user to set it</li>
1873 * <li>auto-advance in this configuration requires a mode change, and we need to wait for the
1874 * mode change transition to finish</li>
1875 * </ul>
1876 * <p>If the current conversation is not in the target collection, this method will do nothing,
1877 * and will not execute the operation.
Scott Kennedy0d0f8b02012-10-12 15:18:18 -07001878 *
1879 * @param target the set of conversations being deleted/marked unread
Andy Huangc94d07f2013-06-03 16:19:35 -07001880 * @param operation (optional) the operation to execute after advancing
1881 * @return <code>false</code> if this method handled or will execute the operation,
1882 * <code>true</code> otherwise.
Scott Kennedy0d0f8b02012-10-12 15:18:18 -07001883 */
1884 private boolean showNextConversation(final Collection<Conversation> target,
Scott Kennedycaaeed32013-06-12 13:39:16 -07001885 final Runnable operation) {
Jin Cao30c881a2014-04-08 14:28:36 -07001886 if (isCurrentConversationInView(target)) {
Scott Kennedy0d0f8b02012-10-12 15:18:18 -07001887 final int autoAdvanceSetting = mAccount.settings.getAutoAdvanceSetting();
1888
Scott Kennedycaaeed32013-06-12 13:39:16 -07001889 if (autoAdvanceSetting == AutoAdvance.UNSET && mIsTablet) {
Scott Kennedy0d0f8b02012-10-12 15:18:18 -07001890 displayAutoAdvanceDialogAndPerformAction(operation);
1891 return false;
1892 } else {
1893 // If we don't have one set, but we're here, just take the default
Vikram Aggarwal82d37502013-01-10 16:18:49 -08001894 final int autoAdvance = (autoAdvanceSetting == AutoAdvance.UNSET) ?
1895 AutoAdvance.DEFAULT : autoAdvanceSetting;
Scott Kennedy0d0f8b02012-10-12 15:18:18 -07001896
1897 final Conversation next = mTracker.getNextConversation(autoAdvance, target);
1898 LogUtils.d(LOG_TAG, "showNextConversation: showing %s next.", next);
Andy Huangc94d07f2013-06-03 16:19:35 -07001899 // Set mAutoAdvanceOp *before* showConversation() to ensure that it runs when the
1900 // transition doesn't run (i.e. it "completes" immediately).
1901 mAutoAdvanceOp = operation;
Scott Kennedy0d0f8b02012-10-12 15:18:18 -07001902 showConversation(next);
Andy Huangc94d07f2013-06-03 16:19:35 -07001903 return (mAutoAdvanceOp == null);
Scott Kennedy0d0f8b02012-10-12 15:18:18 -07001904 }
Andy Huang8f6b0062012-07-31 15:36:31 -07001905 }
Scott Kennedy0d0f8b02012-10-12 15:18:18 -07001906
1907 return true;
1908 }
1909
1910 /**
1911 * Displays a the auto-advance dialog, and when the user makes a selection, the preference is
1912 * stored, and the specified operation is run.
1913 */
1914 private void displayAutoAdvanceDialogAndPerformAction(final Runnable operation) {
1915 final String[] autoAdvanceDisplayOptions =
1916 mContext.getResources().getStringArray(R.array.prefEntries_autoAdvance);
1917 final String[] autoAdvanceOptionValues =
1918 mContext.getResources().getStringArray(R.array.prefValues_autoAdvance);
1919
1920 final String defaultValue = mContext.getString(R.string.prefDefault_autoAdvance);
1921 int initialIndex = 0;
1922 for (int i = 0; i < autoAdvanceOptionValues.length; i++) {
1923 if (defaultValue.equals(autoAdvanceOptionValues[i])) {
1924 initialIndex = i;
1925 break;
1926 }
1927 }
1928
1929 final DialogInterface.OnClickListener listClickListener =
1930 new DialogInterface.OnClickListener() {
1931 @Override
1932 public void onClick(DialogInterface dialog, int whichItem) {
1933 final String autoAdvanceValue = autoAdvanceOptionValues[whichItem];
1934 final int autoAdvanceValueInt =
1935 UIProvider.AutoAdvance.getAutoAdvanceInt(autoAdvanceValue);
1936 mAccount.settings.setAutoAdvanceSetting(autoAdvanceValueInt);
1937
1938 // Save the user's setting
1939 final ContentValues values = new ContentValues(1);
1940 values.put(AccountColumns.SettingsColumns.AUTO_ADVANCE, autoAdvanceValue);
1941
1942 final ContentResolver resolver = mContext.getContentResolver();
1943 resolver.update(mAccount.updateSettingsUri, values, null, null);
1944
1945 // Dismiss the dialog, as clicking the items in the list doesn't close the
1946 // dialog.
1947 dialog.dismiss();
1948 if (operation != null) {
1949 operation.run();
1950 }
1951 }
1952 };
1953
1954 new AlertDialog.Builder(mActivity.getActivityContext()).setTitle(
1955 R.string.auto_advance_help_title)
1956 .setSingleChoiceItems(autoAdvanceDisplayOptions, initialIndex, listClickListener)
1957 .setPositiveButton(null, null)
1958 .create()
1959 .show();
Andy Huang8f6b0062012-07-31 15:36:31 -07001960 }
1961
Andy Huang839ada22012-07-20 15:48:40 -07001962 @Override
1963 public void starMessage(ConversationMessage msg, boolean starred) {
1964 if (msg.starred == starred) {
1965 return;
1966 }
1967
1968 msg.starred = starred;
1969
1970 // locally propagate the change to the owning conversation
1971 // (figure the provider will properly propagate the change when it commits it)
1972 //
1973 // when unstarring, only propagate the change if this was the only message starred
1974 final boolean conversationStarred = starred || msg.isConversationStarred();
Andy Huangcd12e822012-11-08 19:50:57 -08001975 final Conversation conv = msg.getConversation();
1976 if (conversationStarred != conv.starred) {
1977 conv.starred = conversationStarred;
1978 mConversationListCursor.setConversationColumn(conv.uri,
Andy Huang839ada22012-07-20 15:48:40 -07001979 ConversationColumns.STARRED, conversationStarred);
1980 }
1981
1982 final ContentValues values = new ContentValues(1);
1983 values.put(UIProvider.MessageColumns.STARRED, starred ? 1 : 0);
1984
1985 new ContentProviderTask.UpdateTask() {
1986 @Override
1987 protected void onPostExecute(Result result) {
1988 // TODO: handle errors?
1989 }
1990 }.run(mResolver, msg.uri, values, null /* selection*/, null /* selectionArgs */);
1991 }
1992
Andy Huang12b3ee42013-04-24 22:49:43 -07001993 @Override
Alice Yang486e63e2013-04-05 13:01:50 -07001994 public void requestFolderRefresh() {
Alice Yang37dda442013-03-26 22:48:53 -07001995 if (mFolder == null) {
1996 return;
Mindy Pereira28e0c342012-02-17 15:05:13 -08001997 }
Alice Yang37dda442013-03-26 22:48:53 -07001998 final ConversationListFragment convList = getConversationListFragment();
1999 if (convList == null) {
2000 // This could happen if this account is in initial sync (user
2001 // is seeing the "your mail will appear shortly" message)
2002 return;
2003 }
2004 convList.showSyncStatusBar();
2005
2006 if (mAsyncRefreshTask != null) {
2007 mAsyncRefreshTask.cancel(true);
2008 }
2009 mAsyncRefreshTask = new AsyncRefreshTask(mContext, mFolder.refreshUri);
2010 mAsyncRefreshTask.execute();
Mindy Pereira28e0c342012-02-17 15:05:13 -08002011 }
2012
Mindy Pereirafbe40192012-03-20 10:40:45 -07002013 /**
2014 * Confirm (based on user's settings) and delete a conversation from the conversation list and
2015 * from the database.
Vikram Aggarwalb8c31712013-01-03 17:03:19 -08002016 * @param actionId the ID of the menu item that caused the delete: R.id.delete, R.id.archive...
Vikram Aggarwal3d7ca9d2012-05-11 14:40:36 -07002017 * @param target the conversations to act upon
2018 * @param showDialog true if a confirmation dialog is to be shown, false otherwise.
2019 * @param confirmResource the resource ID of the string that is shown in the confirmation dialog
Mindy Pereirafbe40192012-03-20 10:40:45 -07002020 */
Vikram Aggarwalb8c31712013-01-03 17:03:19 -08002021 private void confirmAndDelete(int actionId, final Collection<Conversation> target,
Jin Cao30c881a2014-04-08 14:28:36 -07002022 boolean showDialog, int confirmResource, UndoCallback undoCallback) {
Vikram Aggarwal84fe9942013-04-17 11:20:58 -07002023 final boolean isBatch = false;
Mindy Pereirafbe40192012-03-20 10:40:45 -07002024 if (showDialog) {
Jin Cao30c881a2014-04-08 14:28:36 -07002025 makeDialogListener(actionId, isBatch, undoCallback);
Vikram Aggarwal3d7ca9d2012-05-11 14:40:36 -07002026 final CharSequence message = Utils.formatPlural(mContext, confirmResource,
2027 target.size());
Vikram Aggarwalb8c31712013-01-03 17:03:19 -08002028 final ConfirmDialogFragment c = ConfirmDialogFragment.newInstance(message);
2029 c.displayDialog(mActivity.getFragmentManager());
Mindy Pereirafbe40192012-03-20 10:40:45 -07002030 } else {
Jin Cao30c881a2014-04-08 14:28:36 -07002031 delete(0, target, getDeferredAction(actionId, target, isBatch, undoCallback), isBatch);
Mindy Pereirafbe40192012-03-20 10:40:45 -07002032 }
2033 }
2034
Vikram Aggarwal4f4782b2012-05-30 08:39:09 -07002035 @Override
Scott Kennedy0d0f8b02012-10-12 15:18:18 -07002036 public void delete(final int actionId, final Collection<Conversation> target,
Scott Kennedycaaeed32013-06-12 13:39:16 -07002037 final DestructiveAction action, final boolean isBatch) {
mindyp84f7d322012-10-01 17:14:40 -07002038 // Order of events is critical! The Conversation View Fragment must be
2039 // notified of the next conversation with showConversation(next) *before* the
2040 // conversation list
Vikram Aggarwal192fac12012-07-25 16:44:55 -07002041 // fragment has a chance to delete the conversation, animating it away.
2042
mindyp84f7d322012-10-01 17:14:40 -07002043 // Update the conversation fragment if the current conversation is
2044 // deleted.
Scott Kennedy0d0f8b02012-10-12 15:18:18 -07002045 final Runnable operation = new Runnable() {
2046 @Override
2047 public void run() {
Scott Kennedycaaeed32013-06-12 13:39:16 -07002048 delete(actionId, target, action, isBatch);
Scott Kennedy0d0f8b02012-10-12 15:18:18 -07002049 }
2050 };
2051
Scott Kennedycaaeed32013-06-12 13:39:16 -07002052 if (!showNextConversation(target, operation)) {
Scott Kennedy0d0f8b02012-10-12 15:18:18 -07002053 // This method will be called again if the user selects an autoadvance option
Andrew Sapperstein1165d292014-03-25 16:00:43 -07002054
2055 // HACKFIX around b/9904716. We were not properly performing the last
2056 // DestructiveAction. A proper fix probably involves rewriting
2057 // the logic for animating changes in the list as well as undo
2058 // and probably batch DestructiveActions. The change is limited to
2059 // tablets where the issue occurs.
2060 final ConversationListFragment convListFragment = getConversationListFragment();
2061 if (mIsTablet && convListFragment != null) {
2062 convListFragment.getAnimatedAdapter().setNextAction(
2063 new SwipeableListView.ListItemsRemovedListener() {
2064 @Override
2065 public void onListItemsRemoved() {
2066 action.performAction();
2067 }
2068 }
2069 );
2070 }
Scott Kennedy0d0f8b02012-10-12 15:18:18 -07002071 return;
2072 }
Vikram Aggarwalab1b5b62013-04-16 15:45:50 -07002073 // If the conversation is in the selected set, remove it from the set.
Vikram Aggarwal84fe9942013-04-17 11:20:58 -07002074 // Batch selections are cleared in the end of the action, so not done for batch actions.
2075 if (!isBatch) {
2076 for (final Conversation conv : target) {
2077 if (mSelectedSet.contains(conv)) {
Vikram Aggarwal97ae7842013-04-22 16:29:12 -07002078 mSelectedSet.toggle(conv);
Vikram Aggarwal84fe9942013-04-17 11:20:58 -07002079 }
Vikram Aggarwalab1b5b62013-04-16 15:45:50 -07002080 }
2081 }
Vikram Aggarwal192fac12012-07-25 16:44:55 -07002082 // The conversation list deletes and performs the action if it exists.
2083 final ConversationListFragment convListFragment = getConversationListFragment();
2084 if (convListFragment != null) {
Alice Yang193e05a2013-05-05 14:12:08 -07002085 LogUtils.i(LOG_TAG, "AAC.requestDelete: ListFragment is handling delete.");
Vikram Aggarwal669947b2013-01-10 17:05:56 -08002086 convListFragment.requestDelete(actionId, target, action);
Vikram Aggarwal192fac12012-07-25 16:44:55 -07002087 return;
2088 }
mindyp84f7d322012-10-01 17:14:40 -07002089 // No visible UI element handled it on our behalf. Perform the action
2090 // ourself.
Alice Yang193e05a2013-05-05 14:12:08 -07002091 LogUtils.i(LOG_TAG, "ACC.requestDelete: performing remove action ourselves");
Vikram Aggarwald503df42012-05-11 10:13:35 -07002092 action.performAction();
2093 }
2094
2095 /**
2096 * Requests that the action be performed and the UI state is updated to reflect the new change.
Vikram Aggarwal715f83d2013-03-05 17:04:56 -08002097 * @param action the action to be performed, specified as a menu id: R.id.archive, ...
Vikram Aggarwald503df42012-05-11 10:13:35 -07002098 */
Vikram Aggarwal715f83d2013-03-05 17:04:56 -08002099 private void requestUpdate(final DestructiveAction action) {
Vikram Aggarwald503df42012-05-11 10:13:35 -07002100 action.performAction();
2101 refreshConversationList();
Vikram Aggarwal75daee52012-04-30 11:13:09 -07002102 }
Vikram Aggarwal6fbc87a2012-03-15 15:24:00 -07002103
2104 @Override
Vikram Aggarwala55b36c2012-01-13 11:45:02 -08002105 public void onPrepareDialog(int id, Dialog dialog, Bundle bundle) {
2106 // TODO(viki): Auto-generated method stub
Vikram Aggarwala55b36c2012-01-13 11:45:02 -08002107 }
2108
2109 @Override
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -08002110 public boolean onPrepareOptionsMenu(Menu menu) {
Andy Huangd736a382012-08-29 13:08:58 -07002111 return mActionBarView.onPrepareOptionsMenu(menu);
Vikram Aggarwala55b36c2012-01-13 11:45:02 -08002112 }
2113
Mindy Pereira68f2e222012-03-07 10:36:54 -08002114 @Override
2115 public void onPause() {
Vikram Aggarwalc04cf7e2013-05-13 15:38:42 -07002116 mHaveAccountList = false;
Paul Westbrook6ead20d2012-03-19 14:48:14 -07002117 enableNotifications();
Paul Westbrook94e440d2012-02-24 11:03:47 -08002118 }
2119
Vikram Aggarwala55b36c2012-01-13 11:45:02 -08002120 @Override
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -08002121 public void onResume() {
Paul Westbrook6ead20d2012-03-19 14:48:14 -07002122 // Register the receiver that will prevent the status receiver from
2123 // displaying its notification icon as long as we're running.
2124 // The SupressNotificationReceiver will block the broadcast if we're looking at the folder
2125 // that the notification was received for.
2126 disableNotifications();
Andy Huang1ee96b22012-08-24 20:19:53 -07002127
2128 mSafeToModifyFragments = true;
Andrew Sappersteined5b52d2013-04-30 13:40:18 -07002129
2130 attachEmptyFolderDialogFragmentListener();
Andrew Sapperstein23e33132013-05-07 18:26:49 -07002131
2132 // Invalidating the options menu so that when we make changes in settings,
2133 // the changes will always be updated in the action bar/options menu/
2134 mActivity.invalidateOptionsMenu();
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -08002135 }
2136
2137 @Override
2138 public void onSaveInstanceState(Bundle outState) {
Vikram Aggarwal649b9ea2012-08-27 12:15:20 -07002139 mViewMode.handleSaveInstanceState(outState);
Vikram Aggarwal6c511582012-02-27 10:59:47 -08002140 if (mAccount != null) {
Vikram Aggarwal6c511582012-02-27 10:59:47 -08002141 outState.putParcelable(SAVED_ACCOUNT, mAccount);
2142 }
Mindy Pereira5e478d22012-03-26 18:04:58 -07002143 if (mFolder != null) {
2144 outState.putParcelable(SAVED_FOLDER, mFolder);
Vikram Aggarwal8b152632012-02-03 14:58:45 -08002145 }
Vikram Aggarwalf3341402012-08-07 10:09:38 -07002146 // If this is a search activity, let's store the search query term as well.
2147 if (ConversationListContext.isSearchResult(mConvListContext)) {
2148 outState.putString(SAVED_QUERY, mConvListContext.searchQuery);
2149 }
Vikram Aggarwal49e0e992012-09-21 13:53:15 -07002150 if (mCurrentConversation != null && mViewMode.isConversationMode()) {
Mindy Pereira26f23fc2012-03-27 10:26:04 -07002151 outState.putParcelable(SAVED_CONVERSATION, mCurrentConversation);
2152 }
Andy Huang4556a442012-03-30 16:42:05 -07002153 if (!mSelectedSet.isEmpty()) {
Vikram Aggarwalcabd3f22012-04-19 10:14:41 -07002154 outState.putParcelable(SAVED_SELECTED_SET, mSelectedSet);
Andy Huang4556a442012-03-30 16:42:05 -07002155 }
Mindy Pereirad33674992012-06-25 16:26:30 -07002156 if (mToastBar.getVisibility() == View.VISIBLE) {
2157 outState.putParcelable(SAVED_TOAST_BAR_OP, mToastBar.getOperation());
2158 }
Vikram Aggarwal649b9ea2012-08-27 12:15:20 -07002159 final ConversationListFragment convListFragment = getConversationListFragment();
Mindy Pereirad33674992012-06-25 16:26:30 -07002160 if (convListFragment != null) {
Vikram Aggarwal649b9ea2012-08-27 12:15:20 -07002161 convListFragment.getAnimatedAdapter().onSaveInstanceState(outState);
Mindy Pereirad33674992012-06-25 16:26:30 -07002162 }
Vikram Aggarwalb8c31712013-01-03 17:03:19 -08002163 // If there is a dialog being shown, save the state so we can create a listener for it.
Vikram Aggarwal6cadbfc2012-12-27 09:17:05 -08002164 if (mDialogAction != -1) {
2165 outState.putInt(SAVED_ACTION, mDialogAction);
Vikram Aggarwalb8c31712013-01-03 17:03:19 -08002166 outState.putBoolean(SAVED_ACTION_FROM_SELECTED, mDialogFromSelectedSet);
Vikram Aggarwal6cadbfc2012-12-27 09:17:05 -08002167 }
Vikram Aggarwala91d00b2013-01-18 12:00:37 -08002168 if (mDetachedConvUri != null) {
2169 outState.putParcelable(SAVED_DETACHED_CONV_URI, mDetachedConvUri);
2170 }
Andrew Sapperstein5747e152013-05-13 14:13:08 -07002171
Scott Kennedyb10212e2013-02-22 16:27:00 -08002172 outState.putParcelable(SAVED_HIERARCHICAL_FOLDER, mFolderListFolder);
Andrew Sapperstein5747e152013-05-13 14:13:08 -07002173 mSafeToModifyFragments = false;
Alice Yangebeef1b2013-09-04 06:41:10 +00002174
2175 outState.putParcelable(SAVED_INBOX_KEY, mInbox);
Scott Kennedyf77806e2013-08-30 11:38:15 -07002176
2177 outState.putBundle(SAVED_CONVERSATION_LIST_SCROLL_POSITIONS,
2178 mConversationListScrollPositions);
Andy Huang1ee96b22012-08-24 20:19:53 -07002179 }
2180
2181 /**
2182 * @see #mSafeToModifyFragments
2183 */
2184 protected boolean safeToModifyFragments() {
2185 return mSafeToModifyFragments;
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -08002186 }
2187
2188 @Override
Andy Huang313ac132013-03-04 23:40:58 -08002189 public void executeSearch(String query) {
Jin Cao779dd602014-04-22 16:16:28 -07002190 AnalyticsTimer.getInstance().trackStart(AnalyticsTimer.SEARCH_TO_LIST);
Mindy Pereira68f2e222012-03-07 10:36:54 -08002191 Intent intent = new Intent();
2192 intent.setAction(Intent.ACTION_SEARCH);
2193 intent.putExtra(ConversationListContext.EXTRA_SEARCH_QUERY, query);
2194 intent.putExtra(Utils.EXTRA_ACCOUNT, mAccount);
2195 intent.setComponent(mActivity.getComponentName());
Vikram Aggarwalb17cbc02012-04-06 15:41:46 -07002196 mActionBarView.collapseSearch();
Martin Hibdon371a71c2014-02-19 13:55:28 -08002197 // Call startActivityForResult here so we can tell if we have navigated to a different folder
2198 // or account from search results.
2199 mActivity.startActivityForResult(intent, CHANGE_NAVIGATION_REQUEST_CODE);
Vikram Aggarwala55b36c2012-01-13 11:45:02 -08002200 }
2201
2202 @Override
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -08002203 public void onStop() {
Scott Kennedycb85aea2013-02-25 13:08:32 -08002204 NotificationActionUtils.unregisterUndoNotificationObserver(mUndoNotificationObserver);
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -08002205 }
2206
Andy Huang632721e2012-04-11 16:57:26 -07002207 @Override
2208 public void onDestroy() {
Andy Huangb2ef9c12012-12-18 12:58:41 -06002209 // stop listening to the cursor on e.g. configuration changes
2210 if (mConversationListCursor != null) {
2211 mConversationListCursor.removeListener(this);
2212 }
Andy Huang144bfe72013-06-11 13:27:52 -07002213 mDrawIdler.setListener(null);
2214 mDrawIdler.setRootView(null);
Andy Huang632721e2012-04-11 16:57:26 -07002215 // unregister the ViewPager's observer on the conversation cursor
2216 mPagerController.onDestroy();
Mindy Pereira641de652012-08-02 15:21:50 -07002217 mActionBarView.onDestroy();
Vikram Aggarwal7c401b72012-08-13 16:43:47 -07002218 mRecentFolderList.destroy();
Andy Huang4e0158f2012-08-07 21:06:01 -07002219 mDestroyed = true;
Vikram Aggarwal59f741f2013-03-01 15:55:40 -08002220 mHandler.removeCallbacks(mLogServiceChecker);
2221 mLogServiceChecker = null;
Andy Huang632721e2012-04-11 16:57:26 -07002222 }
2223
Vikram Aggarwalfa131a22012-02-02 13:56:22 -08002224 /**
Vikram Aggarwal93dc2022012-11-05 13:36:57 -08002225 * Set the Action Bar icon according to the mode. The Action Bar icon can contain a back button
2226 * or not. The individual controller is responsible for changing the icon based on the mode.
2227 */
2228 protected abstract void resetActionBarIcon();
2229
2230 /**
Mindy Pereira161f50d2012-02-28 15:47:19 -08002231 * {@inheritDoc} Subclasses must override this to listen to mode changes
2232 * from the ViewMode. Subclasses <b>must</b> call the parent's
2233 * onViewModeChanged since the parent will handle common state changes.
Vikram Aggarwalfa131a22012-02-02 13:56:22 -08002234 */
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -08002235 @Override
Vikram Aggarwalfa131a22012-02-02 13:56:22 -08002236 public void onViewModeChanged(int newMode) {
Vikram Aggarwal49e0e992012-09-21 13:53:15 -07002237 // When we step away from the conversation mode, we don't have a current conversation
2238 // anymore. Let's blank it out so clients calling getCurrentConversation are not misled.
2239 if (!ViewMode.isConversationMode(newMode)) {
2240 setCurrentConversation(null);
2241 }
Andrew Sapperstein6c570db2013-08-06 17:21:36 -07002242
Vikram Aggarwal93dc2022012-11-05 13:36:57 -08002243 // If the viewmode is not set, preserve existing icon.
2244 if (newMode != ViewMode.UNKNOWN) {
2245 resetActionBarIcon();
2246 }
Andy Huang12b3ee42013-04-24 22:49:43 -07002247
2248 if (isDrawerEnabled()) {
Vikram Aggarwaldfbc6982013-07-03 14:39:31 -07002249 /** If the folder doesn't exist, or its parent URI is empty,
2250 * this is not a child folder */
2251 final boolean isTopLevel = (mFolder == null) || (mFolder.parent == Uri.EMPTY);
2252 mDrawerToggle.setDrawerIndicatorEnabled(
2253 getShouldShowDrawerIndicator(newMode, isTopLevel));
Martin Hibdon371a71c2014-02-19 13:55:28 -08002254 mDrawerContainer.setDrawerLockMode(DrawerLayout.LOCK_MODE_UNLOCKED);
Andy Huang12b3ee42013-04-24 22:49:43 -07002255 closeDrawerIfOpen();
2256 }
Vikram Aggarwala55b36c2012-01-13 11:45:02 -08002257 }
2258
Vikram Aggarwaldfbc6982013-07-03 14:39:31 -07002259 /**
2260 * Returns true if the drawer icon is shown
2261 * @param viewMode the current view mode
2262 * @param isTopLevel true if the current folder is not a child
2263 * @return whether the drawer indicator is shown
2264 */
Scott Kennedy81b5fa02013-07-11 14:39:18 -07002265 private boolean getShouldShowDrawerIndicator(final int viewMode,
Vikram Aggarwaldfbc6982013-07-03 14:39:31 -07002266 final boolean isTopLevel) {
2267 // If search list/conv mode: disable indicator
2268 // Indicator is enabled either in conversation list or folder list mode.
Scott Kennedy81b5fa02013-07-11 14:39:18 -07002269 return isDrawerEnabled() && !ViewMode.isSearchMode(viewMode)
Scott Kennedyaded5782013-07-16 14:21:53 -07002270 && (viewMode == ViewMode.CONVERSATION_LIST && isTopLevel);
Scott Kennedy8a72b852013-05-02 14:18:50 -07002271 }
2272
Andy Huang3825f3d2012-08-29 16:44:12 -07002273 public void disablePagerUpdates() {
2274 mPagerController.stopListening();
2275 }
2276
Andy Huang4e0158f2012-08-07 21:06:01 -07002277 public boolean isDestroyed() {
2278 return mDestroyed;
2279 }
2280
mindyp54f120f2012-08-28 13:10:33 -07002281 @Override
2282 public void commitDestructiveActions(boolean animate) {
mindypc6adce32012-08-22 18:46:42 -07002283 ConversationListFragment fragment = getConversationListFragment();
Mindy Pereira1e2573b2012-04-17 14:34:36 -07002284 if (fragment != null) {
mindypc6adce32012-08-22 18:46:42 -07002285 fragment.commitDestructiveActions(animate);
Mindy Pereira1e2573b2012-04-17 14:34:36 -07002286 }
2287 }
2288
Vikram Aggarwala55b36c2012-01-13 11:45:02 -08002289 @Override
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -08002290 public void onWindowFocusChanged(boolean hasFocus) {
Vikram Aggarwal69b5c302012-09-05 11:11:13 -07002291 final ConversationListFragment convList = getConversationListFragment();
Vikram Aggarwal6422b8d2013-01-14 10:47:41 -08002292 // hasFocus already ensures that the window is in focus, so we don't need to call
2293 // AAC.isFragmentVisible(convList) here.
Paul Westbrook9f119c72012-04-24 16:10:59 -07002294 if (hasFocus && convList != null && convList.isVisible()) {
2295 // The conversation list is visible.
Vikram Aggarwal69b5c302012-09-05 11:11:13 -07002296 informCursorVisiblity(true);
Paul Westbrook9f119c72012-04-24 16:10:59 -07002297 }
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -08002298 }
2299
Vikram Aggarwalca716f12012-08-20 11:11:48 -07002300 /**
2301 * Set the account, and carry out all the account-related changes that rely on this.
Vikram Aggarwal715f83d2013-03-05 17:04:56 -08002302 * @param account new account to set to.
Vikram Aggarwalca716f12012-08-20 11:11:48 -07002303 */
Mindy Pereira75181e82012-04-18 08:17:13 -07002304 private void setAccount(Account account) {
Andy Huangb9ca9792012-05-18 15:31:49 -07002305 if (account == null) {
2306 LogUtils.w(LOG_TAG, new Error(),
2307 "AAC ignoring null (presumably invalid) account restoration");
2308 return;
2309 }
Andy Huangb1148412012-05-19 00:16:30 -07002310 LogUtils.d(LOG_TAG, "AbstractActivityController.setAccount(): account = %s", account.uri);
Vikram Aggarwal91e87372012-05-18 15:36:04 -07002311 mAccount = account;
Vikram Aggarwalca716f12012-08-20 11:11:48 -07002312 // Only change AAC state here. Do *not* modify any other object's state. The object
2313 // should listen on account changes.
Vikram Aggarwal177097f2013-03-08 11:19:53 -08002314 restartOptionalLoader(LOADER_RECENT_FOLDERS, mFolderCallbacks, Bundle.EMPTY);
Vikram Aggarwalca716f12012-08-20 11:11:48 -07002315 mActivity.invalidateOptionsMenu();
2316 disableNotificationsOnAccountChange(mAccount);
Vikram Aggarwal177097f2013-03-08 11:19:53 -08002317 restartOptionalLoader(LOADER_ACCOUNT_UPDATE_CURSOR, mAccountCallbacks, Bundle.EMPTY);
Vikram Aggarwal2fe343f2013-01-14 09:00:25 -08002318 // The Mail instance can be null during test runs.
2319 final MailAppProvider instance = MailAppProvider.getInstance();
2320 if (instance != null) {
2321 instance.setLastViewedAccount(mAccount.uri.toString());
2322 }
Vikram Aggarwal91e87372012-05-18 15:36:04 -07002323 if (account.settings == null) {
2324 LogUtils.w(LOG_TAG, new Error(), "AAC ignoring account with null settings.");
2325 return;
2326 }
Vikram Aggarwal7c401b72012-08-13 16:43:47 -07002327 mAccountObservers.notifyChanged();
Vikram Aggarwal34f7b232012-10-17 13:32:23 -07002328 perhapsEnterWaitMode();
Mindy Pereira75181e82012-04-18 08:17:13 -07002329 }
2330
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -08002331 /**
Mindy Pereira161f50d2012-02-28 15:47:19 -08002332 * Restore the state from the previous bundle. Subclasses should call this
2333 * method from the parent class, since it performs important UI
2334 * initialization.
2335 *
Vikram Aggarwal715f83d2013-03-05 17:04:56 -08002336 * @param savedState previous state
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -08002337 */
Andy Huang632721e2012-04-11 16:57:26 -07002338 @Override
2339 public void onRestoreInstanceState(Bundle savedState) {
Vikram Aggarwala91d00b2013-01-18 12:00:37 -08002340 mDetachedConvUri = savedState.getParcelable(SAVED_DETACHED_CONV_URI);
Andy Huang632721e2012-04-11 16:57:26 -07002341 if (savedState.containsKey(SAVED_CONVERSATION)) {
2342 // Open the conversation.
Andy Huang2bc8bc12012-11-12 17:24:25 -08002343 final Conversation conversation = savedState.getParcelable(SAVED_CONVERSATION);
Paul Westbrook534e4a22012-04-25 03:46:29 -07002344 if (conversation != null && conversation.position < 0) {
2345 // Set the position to 0 on this conversation, as we don't know where it is
2346 // in the list
2347 conversation.position = 0;
2348 }
Andy Huanged4fdf02012-07-26 17:12:50 -07002349 showConversation(conversation);
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -08002350 }
Mindy Pereira967ede62012-03-22 09:29:09 -07002351
Mindy Pereirad33674992012-06-25 16:26:30 -07002352 if (savedState.containsKey(SAVED_TOAST_BAR_OP)) {
Andy Huang2bc8bc12012-11-12 17:24:25 -08002353 ToastBarOperation op = savedState.getParcelable(SAVED_TOAST_BAR_OP);
Mindy Pereirad33674992012-06-25 16:26:30 -07002354 if (op != null) {
2355 if (op.getType() == ToastBarOperation.UNDO) {
2356 onUndoAvailable(op);
Andrew Sapperstein9d7519d2012-07-16 14:03:53 -07002357 } else if (op.getType() == ToastBarOperation.ERROR) {
2358 onError(mFolder, true);
Mindy Pereirad33674992012-06-25 16:26:30 -07002359 }
2360 }
2361 }
Scott Kennedyb10212e2013-02-22 16:27:00 -08002362 mFolderListFolder = savedState.getParcelable(SAVED_HIERARCHICAL_FOLDER);
Vikram Aggarwal649b9ea2012-08-27 12:15:20 -07002363 final ConversationListFragment convListFragment = getConversationListFragment();
Mindy Pereirad33674992012-06-25 16:26:30 -07002364 if (convListFragment != null) {
Vikram Aggarwal649b9ea2012-08-27 12:15:20 -07002365 convListFragment.getAnimatedAdapter().onRestoreInstanceState(savedState);
Mindy Pereirad33674992012-06-25 16:26:30 -07002366 }
Vikram Aggarwalb8c31712013-01-03 17:03:19 -08002367 /*
Mindy Pereira967ede62012-03-22 09:29:09 -07002368 * Restore the state of selected conversations. This needs to be done after the correct mode
2369 * is set and the action bar is fully initialized. If not, several key pieces of state
2370 * information will be missing, and the split views may not be initialized correctly.
Mindy Pereira967ede62012-03-22 09:29:09 -07002371 */
Andy Huang4556a442012-03-30 16:42:05 -07002372 restoreSelectedConversations(savedState);
Vikram Aggarwalb8c31712013-01-03 17:03:19 -08002373 // Order is important!!!
2374 // The dialog listener needs to happen *after* the selected set is restored.
2375
2376 // If there has been an orientation change, and we need to recreate the listener for the
2377 // confirm dialog fragment (delete/archive/...), then do it here.
2378 if (mDialogAction != -1) {
Jin Cao30c881a2014-04-08 14:28:36 -07002379 makeDialogListener(mDialogAction, mDialogFromSelectedSet,
2380 getUndoCallbackForDestructiveActionsWithAutoAdvance(
2381 mDialogAction, mCurrentConversation));
Vikram Aggarwalb8c31712013-01-03 17:03:19 -08002382 }
Alice Yangebeef1b2013-09-04 06:41:10 +00002383
2384 mInbox = savedState.getParcelable(SAVED_INBOX_KEY);
Scott Kennedyf77806e2013-08-30 11:38:15 -07002385
2386 mConversationListScrollPositions.clear();
2387 mConversationListScrollPositions.putAll(
2388 savedState.getBundle(SAVED_CONVERSATION_LIST_SCROLL_POSITIONS));
Andy Huang632721e2012-04-11 16:57:26 -07002389 }
2390
Vikram Aggarwal649b9ea2012-08-27 12:15:20 -07002391 /**
2392 * Handle an intent to open the app. This method is called only when there is no saved state,
2393 * so we need to set state that wasn't set before. It is correct to change the viewmode here
2394 * since it has not been previously set.
Vikram Aggarwal6aca6892013-06-04 13:53:27 -07002395 *
2396 * This method is called for a subset of the reasons mentioned in
2397 * {@link #onCreate(android.os.Bundle)}. Notably, this is called when launching the app from
2398 * notifications, widgets, and shortcuts.
Vikram Aggarwal715f83d2013-03-05 17:04:56 -08002399 * @param intent intent passed to the activity.
Vikram Aggarwal649b9ea2012-08-27 12:15:20 -07002400 */
Andy Huang632721e2012-04-11 16:57:26 -07002401 private void handleIntent(Intent intent) {
Andy Huange6459422013-04-01 16:32:18 -07002402 LogUtils.d(LOG_TAG, "IN AAC.handleIntent. action=%s", intent.getAction());
Andy Huang632721e2012-04-11 16:57:26 -07002403 if (Intent.ACTION_VIEW.equals(intent.getAction())) {
2404 if (intent.hasExtra(Utils.EXTRA_ACCOUNT)) {
Tony Mantler26a20752014-02-28 16:44:24 -08002405 setAccount(Account.newInstance(intent.getStringExtra(Utils.EXTRA_ACCOUNT)));
Andy Huang632721e2012-04-11 16:57:26 -07002406 }
Andy Huangb9ca9792012-05-18 15:31:49 -07002407 if (mAccount == null) {
2408 return;
Andy Huang632721e2012-04-11 16:57:26 -07002409 }
Vikram Aggarwal1a249e02012-08-03 16:19:33 -07002410 final boolean isConversationMode = intent.hasExtra(Utils.EXTRA_CONVERSATION);
Andy Huang4fe0af82013-08-20 17:24:51 -07002411
2412 if (intent.getBooleanExtra(Utils.EXTRA_FROM_NOTIFICATION, false)) {
2413 Analytics.getInstance().setCustomDimension(Analytics.CD_INDEX_ACCOUNT_TYPE,
Tony Mantler79b11562013-10-09 15:31:50 -07002414 AnalyticsUtils.getAccountTypeForAccount(mAccount.getEmailAddress()));
Andy Huang4fe0af82013-08-20 17:24:51 -07002415 Analytics.getInstance().sendEvent("notification_click",
2416 isConversationMode ? "conversation" : "conversation_list", null, 0);
2417 }
2418
Vikram Aggarwal649b9ea2012-08-27 12:15:20 -07002419 if (isConversationMode && mViewMode.getMode() == ViewMode.UNKNOWN) {
Vikram Aggarwal1a249e02012-08-03 16:19:33 -07002420 mViewMode.enterConversationMode();
2421 } else {
2422 mViewMode.enterConversationListMode();
2423 }
Vikram Aggarwal177097f2013-03-08 11:19:53 -08002424 // Put the folder and conversation, and ask the loader to create this folder.
2425 final Bundle args = new Bundle();
Scott Kennedy48cfe462013-04-10 11:32:02 -07002426
2427 final Uri folderUri;
2428 if (intent.hasExtra(Utils.EXTRA_FOLDER_URI)) {
Andrew Sapperstein5bb4d052014-03-31 16:22:31 -07002429 folderUri = intent.getParcelableExtra(Utils.EXTRA_FOLDER_URI);
Scott Kennedy48cfe462013-04-10 11:32:02 -07002430 } else if (intent.hasExtra(Utils.EXTRA_FOLDER)) {
2431 final Folder folder =
2432 Folder.fromString(intent.getStringExtra(Utils.EXTRA_FOLDER));
Scott Kennedy259df5b2013-07-11 13:24:01 -07002433 folderUri = folder.folderUri.fullUri;
Scott Kennedy48cfe462013-04-10 11:32:02 -07002434 } else {
Scott Kennedy72727ef2013-05-01 18:10:55 -07002435 final Bundle extras = intent.getExtras();
2436 LogUtils.d(LOG_TAG, "Couldn't find a folder URI in the extras: %s",
2437 extras == null ? "null" : extras.toString());
Scott Kennedy48cfe462013-04-10 11:32:02 -07002438 folderUri = mAccount.settings.defaultInbox;
2439 }
2440
Andrew Sapperstein5bb4d052014-03-31 16:22:31 -07002441 // Check if we should load all conversations instead of using
2442 // the default behavior which loads an initial subset.
2443 mIgnoreInitialConversationLimit =
2444 intent.getBooleanExtra(Utils.EXTRA_IGNORE_INITIAL_CONVERSATION_LIMIT, false);
2445
Scott Kennedy60593352013-03-13 13:45:30 -07002446 args.putParcelable(Utils.EXTRA_FOLDER_URI, folderUri);
Vikram Aggarwal177097f2013-03-08 11:19:53 -08002447 args.putParcelable(Utils.EXTRA_CONVERSATION,
2448 intent.getParcelableExtra(Utils.EXTRA_CONVERSATION));
2449 restartOptionalLoader(LOADER_FIRST_FOLDER, mFolderCallbacks, args);
Andy Huang632721e2012-04-11 16:57:26 -07002450 } else if (Intent.ACTION_SEARCH.equals(intent.getAction())) {
2451 if (intent.hasExtra(Utils.EXTRA_ACCOUNT)) {
Vikram Aggarwalf0ef2302012-11-07 14:53:34 -08002452 mHaveSearchResults = false;
Andy Huang632721e2012-04-11 16:57:26 -07002453 // Save this search query for future suggestions.
2454 final String query = intent.getStringExtra(SearchManager.QUERY);
2455 final String authority = mContext.getString(R.string.suggestions_authority);
Vikram Aggarwal2b703c62012-09-18 13:54:15 -07002456 final SearchRecentSuggestions suggestions = new SearchRecentSuggestions(
Andy Huang632721e2012-04-11 16:57:26 -07002457 mContext, authority, SuggestionsProvider.MODE);
2458 suggestions.saveRecentQuery(query, null);
Vikram Aggarwalf0ef2302012-11-07 14:53:34 -08002459 setAccount((Account) intent.getParcelableExtra(Utils.EXTRA_ACCOUNT));
2460 fetchSearchFolder(intent);
2461 if (shouldEnterSearchConvMode()) {
Mindy Pereiraac254822012-06-18 10:46:43 -07002462 mViewMode.enterSearchResultsConversationMode();
2463 } else {
2464 mViewMode.enterSearchResultsListMode();
2465 }
Andy Huang632721e2012-04-11 16:57:26 -07002466 } else {
2467 LogUtils.e(LOG_TAG, "Missing account extra from search intent. Finishing");
2468 mActivity.finish();
2469 }
2470 }
2471 if (mAccount != null) {
Vikram Aggarwal177097f2013-03-08 11:19:53 -08002472 restartOptionalLoader(LOADER_ACCOUNT_UPDATE_CURSOR, mAccountCallbacks, Bundle.EMPTY);
Scott Kennedyb39aaf52013-03-06 19:17:22 -08002473 }
2474 }
2475
Andy Huang4556a442012-03-30 16:42:05 -07002476 /**
Vikram Aggarwalf0ef2302012-11-07 14:53:34 -08002477 * Returns true if we should enter conversation mode with search.
2478 */
2479 protected final boolean shouldEnterSearchConvMode() {
2480 return mHaveSearchResults && Utils.showTwoPaneSearchResults(mActivity.getActivityContext());
2481 }
2482
2483 /**
Andy Huang4556a442012-03-30 16:42:05 -07002484 * Copy any selected conversations stored in the saved bundle into our selection set,
2485 * triggering {@link ConversationSetObserver} callbacks as our selection set changes.
2486 *
2487 */
Vikram Aggarwal715f83d2013-03-05 17:04:56 -08002488 private void restoreSelectedConversations(Bundle savedState) {
Mindy Pereira967ede62012-03-22 09:29:09 -07002489 if (savedState == null) {
Andy Huang4556a442012-03-30 16:42:05 -07002490 mSelectedSet.clear();
Mindy Pereira967ede62012-03-22 09:29:09 -07002491 return;
2492 }
Vikram Aggarwalcabd3f22012-04-19 10:14:41 -07002493 final ConversationSelectionSet selectedSet = savedState.getParcelable(SAVED_SELECTED_SET);
Andy Huang4556a442012-03-30 16:42:05 -07002494 if (selectedSet == null || selectedSet.isEmpty()) {
2495 mSelectedSet.clear();
Mindy Pereira967ede62012-03-22 09:29:09 -07002496 return;
2497 }
Andy Huang632721e2012-04-11 16:57:26 -07002498
2499 // putAll will take care of calling our registered onSetPopulated method
Vikram Aggarwalcabd3f22012-04-19 10:14:41 -07002500 mSelectedSet.putAll(selectedSet);
Mindy Pereira967ede62012-03-22 09:29:09 -07002501 }
2502
Vikram Aggarwalec5cbf72012-03-08 15:10:35 -08002503 /**
Vikram Aggarwal49e0e992012-09-21 13:53:15 -07002504 * Show the conversation provided in the arguments. It is safe to pass a null conversation
2505 * object, which is a signal to back out of conversation view mode.
2506 * Child classes must call super.showConversation() <b>before</b> their own implementations.
Vikram Aggarwal59f741f2013-03-01 15:55:40 -08002507 * @param conversation the conversation to be shown, or null if we want to back out to list
2508 * mode.
Vikram Aggarwal177097f2013-03-08 11:19:53 -08002509 * onLoadFinished(Loader, Cursor) on any callback.
Vikram Aggarwalec5cbf72012-03-08 15:10:35 -08002510 */
Jin Cao30c881a2014-04-08 14:28:36 -07002511 protected void showConversation(Conversation conversation) {
Andy Huang243c2362013-03-01 17:50:35 -08002512 if (conversation != null) {
2513 Utils.sConvLoadTimer.start();
2514 }
2515
Andy Huang54e925e2013-03-14 13:24:18 -07002516 MailLogService.log("AbstractActivityController", "showConversation(%s)", conversation);
Vikram Aggarwalc67182d2012-04-03 14:35:06 -07002517 // Set the current conversation just in case it wasn't already set.
2518 setCurrentConversation(conversation);
Vikram Aggarwalec5cbf72012-03-08 15:10:35 -08002519 }
2520
Vikram Aggarwale128fc22012-04-04 12:33:34 -07002521 /**
Paul Westbrook2d50bcd2012-04-10 11:53:47 -07002522 * Children can override this method, but they must call super.showWaitForInitialization().
2523 * {@inheritDoc}
2524 */
2525 @Override
2526 public void showWaitForInitialization() {
2527 mViewMode.enterWaitingForInitializationMode();
Andy Huangc96efcc2014-04-09 15:30:42 -07002528 mWaitFragment = WaitFragment.newInstance(mAccount, true /* expectingMessages */);
Paul Westbrook2d50bcd2012-04-10 11:53:47 -07002529 }
2530
Vikram Aggarwaldd6a7ce2012-10-22 15:45:57 -07002531 private void updateWaitMode() {
Paul Westbrook2d50bcd2012-04-10 11:53:47 -07002532 final FragmentManager manager = mActivity.getFragmentManager();
2533 final WaitFragment waitFragment =
Vikram Aggarwal6902dcf2012-04-11 08:57:42 -07002534 (WaitFragment)manager.findFragmentByTag(TAG_WAIT);
Paul Westbrook2d50bcd2012-04-10 11:53:47 -07002535 if (waitFragment != null) {
2536 waitFragment.updateAccount(mAccount);
2537 }
2538 }
2539
Vikram Aggarwala3f43d42012-10-25 16:21:30 -07002540 /**
2541 * Remove the "Waiting for Initialization" fragment. Child classes are free to override this
2542 * method, though they must call the parent implementation <b>after</b> they do anything.
2543 */
2544 protected void hideWaitForInitialization() {
2545 mWaitFragment = null;
2546 }
2547
2548 /**
2549 * Use the instance variable and the wait fragment's tag to get the wait fragment. This is
2550 * far superior to using the value of mWaitFragment, which might be invalid or might refer
2551 * to a fragment after it has been destroyed.
Vikram Aggarwal715f83d2013-03-05 17:04:56 -08002552 * @return a wait fragment that is already attached to the activity, if one exists
Vikram Aggarwala3f43d42012-10-25 16:21:30 -07002553 */
2554 protected final WaitFragment getWaitFragment() {
2555 final FragmentManager manager = mActivity.getFragmentManager();
2556 final WaitFragment waitFrag = (WaitFragment) manager.findFragmentByTag(TAG_WAIT);
2557 if (waitFrag != null) {
2558 // The Fragment Manager knows better, so use its instance.
2559 mWaitFragment = waitFrag;
2560 }
2561 return mWaitFragment;
2562 }
Vikram Aggarwaldd6a7ce2012-10-22 15:45:57 -07002563
Vikram Aggarwal48b2a6c2012-05-29 14:09:27 -07002564 /**
2565 * Returns true if we are waiting for the account to sync, and cannot show any folders or
2566 * conversation for the current account yet.
Vikram Aggarwal48b2a6c2012-05-29 14:09:27 -07002567 */
Vikram Aggarwaldd6a7ce2012-10-22 15:45:57 -07002568 private boolean inWaitMode() {
Vikram Aggarwala3f43d42012-10-25 16:21:30 -07002569 final WaitFragment waitFragment = getWaitFragment();
Paul Westbrook2d50bcd2012-04-10 11:53:47 -07002570 if (waitFragment != null) {
2571 final Account fragmentAccount = waitFragment.getAccount();
Paul Westbrook339004b2012-11-05 17:13:51 -08002572 return fragmentAccount != null && fragmentAccount.uri.equals(mAccount.uri) &&
Paul Westbrook2d50bcd2012-04-10 11:53:47 -07002573 mViewMode.getMode() == ViewMode.WAITING_FOR_ACCOUNT_INITIALIZATION;
2574 }
2575 return false;
2576 }
2577
2578 /**
Vikram Aggarwale128fc22012-04-04 12:33:34 -07002579 * Children can override this method, but they must call super.showConversationList().
2580 * {@inheritDoc}
2581 */
2582 @Override
2583 public void showConversationList(ConversationListContext listContext) {
2584 }
2585
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -08002586 @Override
Vikram Aggarwal17f373e2012-09-17 15:49:32 -07002587 public final void onConversationSelected(Conversation conversation, boolean inLoaderCallbacks) {
Alice Yangfe8e0812013-04-22 13:37:26 -07002588 final ConversationListFragment convListFragment = getConversationListFragment();
2589 if (convListFragment != null && convListFragment.getAnimatedAdapter() != null) {
2590 convListFragment.getAnimatedAdapter().onConversationSelected();
2591 }
mindypaa55bc92012-08-24 09:49:56 -07002592 // Only animate destructive actions if we are going to be showing the
2593 // conversation list when we show the next conversation.
Vikram Aggarwalbcb16b92013-01-28 18:05:03 -08002594 commitDestructiveActions(mIsTablet);
Jin Cao30c881a2014-04-08 14:28:36 -07002595 showConversation(conversation);
Andy Huang1ee96b22012-08-24 20:19:53 -07002596 }
2597
2598 @Override
Andrew Sapperstein2f542872013-06-11 10:48:30 -07002599 public final void onCabModeEntered() {
2600 final ConversationListFragment convListFragment = getConversationListFragment();
2601 if (convListFragment != null && convListFragment.getAnimatedAdapter() != null) {
2602 convListFragment.getAnimatedAdapter().onCabModeEntered();
2603 }
2604 }
2605
2606 @Override
Scott Kennedycc139832013-08-19 18:03:54 -07002607 public final void onCabModeExited() {
2608 final ConversationListFragment convListFragment = getConversationListFragment();
2609 if (convListFragment != null && convListFragment.getAnimatedAdapter() != null) {
2610 convListFragment.getAnimatedAdapter().onCabModeExited();
2611 }
2612 }
2613
2614 @Override
Andy Huang1ee96b22012-08-24 20:19:53 -07002615 public Conversation getCurrentConversation() {
2616 return mCurrentConversation;
Vikram Aggarwal7d602882012-02-07 15:01:20 -08002617 }
Mindy Pereira555140c2012-02-15 14:55:29 -08002618
Vikram Aggarwalc67182d2012-04-03 14:35:06 -07002619 /**
2620 * Set the current conversation. This is the conversation on which all actions are performed.
2621 * Do not modify mCurrentConversation except through this method, which makes it easy to
2622 * perform common actions associated with changing the current conversation.
Vikram Aggarwal715f83d2013-03-05 17:04:56 -08002623 * @param conversation new conversation to view. Passing null indicates that we are backing
2624 * out to conversation list mode.
Vikram Aggarwalc67182d2012-04-03 14:35:06 -07002625 */
Andy Huang632721e2012-04-11 16:57:26 -07002626 @Override
2627 public void setCurrentConversation(Conversation conversation) {
Vikram Aggarwala91d00b2013-01-18 12:00:37 -08002628 // The controller should come out of detached mode if a new conversation is viewed, or if
Vikram Aggarwal81230ee2013-02-26 15:26:44 -08002629 // we are going back to conversation list mode.
2630 if (mDetachedConvUri != null && (conversation == null
2631 || !mDetachedConvUri.equals(conversation.uri))) {
Vikram Aggarwala91d00b2013-01-18 12:00:37 -08002632 clearDetachedMode();
2633 }
2634
2635 // Must happen *before* setting mCurrentConversation because this sets
2636 // conversation.position if a cursor is available.
Andy Huang8883f222012-11-12 19:25:00 -08002637 mTracker.initialize(conversation);
Vikram Aggarwal17f373e2012-09-17 15:49:32 -07002638 mCurrentConversation = conversation;
Andy Huang7d646122012-09-05 19:41:44 -07002639
2640 if (mCurrentConversation != null) {
Yorke Leef807ba72012-09-20 17:18:05 -07002641 mActionBarView.setCurrentConversation(mCurrentConversation);
Yorke Leef807ba72012-09-20 17:18:05 -07002642 mActivity.invalidateOptionsMenu();
Andy Huang7d646122012-09-05 19:41:44 -07002643 }
Mindy Pereira5040f1a2012-03-20 10:14:06 -07002644 }
2645
Vikram Aggarwala9b93f32012-02-23 14:51:58 -08002646 /**
Andy Huangf9a73482012-03-13 15:54:02 -07002647 * {@link LoaderManager} currently has a bug in
2648 * {@link LoaderManager#restartLoader(int, Bundle, android.app.LoaderManager.LoaderCallbacks)}
2649 * where, if a previous onCreateLoader returned a null loader, this method will NPE. Work around
2650 * this bug by destroying any loaders that may have been created as null (essentially because
2651 * they are optional loads, and may not apply to a particular account).
2652 * <p>
2653 * A simple null check before restarting a loader will not work, because that would not
2654 * give the controller a chance to invalidate UI corresponding the prior loader result.
2655 *
2656 * @param id loader ID to safely restart
Vikram Aggarwal177097f2013-03-08 11:19:53 -08002657 * @param handler the LoaderCallback which will handle this loader ID.
2658 * @param args arguments, if any, to be passed to the loader. Use {@link Bundle#EMPTY} if no
2659 * arguments need to be specified.
Andy Huangf9a73482012-03-13 15:54:02 -07002660 */
Vikram Aggarwal177097f2013-03-08 11:19:53 -08002661 private void restartOptionalLoader(int id, LoaderManager.LoaderCallbacks handler, Bundle args) {
Andy Huangf9a73482012-03-13 15:54:02 -07002662 final LoaderManager lm = mActivity.getLoaderManager();
2663 lm.destroyLoader(id);
Vikram Aggarwal177097f2013-03-08 11:19:53 -08002664 lm.restartLoader(id, args, handler);
Vikram Aggarwal94c94de2012-04-04 15:38:28 -07002665 }
2666
Andy Huang632721e2012-04-11 16:57:26 -07002667 @Override
2668 public void registerConversationListObserver(DataSetObserver observer) {
2669 mConversationListObservable.registerObserver(observer);
2670 }
2671
2672 @Override
2673 public void unregisterConversationListObserver(DataSetObserver observer) {
Alice Yang647aefb2013-03-04 19:13:11 -08002674 try {
2675 mConversationListObservable.unregisterObserver(observer);
2676 } catch (IllegalStateException e) {
2677 // Log instead of crash
2678 LogUtils.e(LOG_TAG, e, "unregisterConversationListObserver called for an observer that "
2679 + "hasn't been registered");
2680 }
Andy Huang632721e2012-04-11 16:57:26 -07002681 }
2682
Andy Huang090db1e2012-07-25 13:25:28 -07002683 @Override
2684 public void registerFolderObserver(DataSetObserver observer) {
2685 mFolderObservable.registerObserver(observer);
2686 }
2687
2688 @Override
2689 public void unregisterFolderObserver(DataSetObserver observer) {
Alice Yang647aefb2013-03-04 19:13:11 -08002690 try {
2691 mFolderObservable.unregisterObserver(observer);
2692 } catch (IllegalStateException e) {
2693 // Log instead of crash
2694 LogUtils.e(LOG_TAG, e, "unregisterFolderObserver called for an observer that "
2695 + "hasn't been registered");
2696 }
Andy Huang090db1e2012-07-25 13:25:28 -07002697 }
2698
Andy Huang9d3fd922012-09-26 22:23:58 -07002699 @Override
2700 public void registerConversationLoadedObserver(DataSetObserver observer) {
2701 mPagerController.registerConversationLoadedObserver(observer);
2702 }
2703
2704 @Override
2705 public void unregisterConversationLoadedObserver(DataSetObserver observer) {
Alice Yang647aefb2013-03-04 19:13:11 -08002706 try {
2707 mPagerController.unregisterConversationLoadedObserver(observer);
2708 } catch (IllegalStateException e) {
2709 // Log instead of crash
2710 LogUtils.e(LOG_TAG, e, "unregisterConversationLoadedObserver called for an observer "
2711 + "that hasn't been registered");
2712 }
Andy Huang9d3fd922012-09-26 22:23:58 -07002713 }
2714
Vikram Aggarwal60069912012-07-24 14:26:09 -07002715 /**
Vikram Aggarwalc04cf7e2013-05-13 15:38:42 -07002716 * Returns true if the number of accounts is different, or if the current account has
2717 * changed. This method is meant to filter frequent changes to the list of
2718 * accounts, and only return true if the new list is substantially different from the existing
2719 * list. Returning true is safe here, it leads to more work in creating the
2720 * same account list again.
Vikram Aggarwal715f83d2013-03-05 17:04:56 -08002721 * @param accountCursor the cursor which points to all the accounts.
2722 * @return true if the number of accounts is changed or current account missing from the list.
Vikram Aggarwal60069912012-07-24 14:26:09 -07002723 */
Vikram Aggarwal177097f2013-03-08 11:19:53 -08002724 private boolean accountsUpdated(ObjectCursor<Account> accountCursor) {
Paul Westbrook23b74b92012-02-29 11:36:12 -08002725 // Check to see if the current account hasn't been set, or the account cursor is empty
2726 if (mAccount == null || !accountCursor.moveToFirst()) {
Vikram Aggarwal6c511582012-02-27 10:59:47 -08002727 return true;
Paul Westbrook23b74b92012-02-29 11:36:12 -08002728 }
2729
2730 // Check to see if the number of accounts are different, from the number we saw on the last
2731 // updated
2732 if (mCurrentAccountUris.size() != accountCursor.getCount()) {
2733 return true;
2734 }
2735
2736 // Check to see if the account list is different or if the current account is not found in
2737 // the cursor.
2738 boolean foundCurrentAccount = false;
Vikram Aggarwal7dedb952012-02-16 16:10:23 -08002739 do {
Vikram Aggarwalc04cf7e2013-05-13 15:38:42 -07002740 final Account account = accountCursor.getModel();
2741 if (!foundCurrentAccount && mAccount.uri.equals(account.uri)) {
2742 if (mAccount.settingsDiffer(account)) {
2743 // Settings changed, and we don't need to look any further.
2744 return true;
2745 }
Paul Westbrook23b74b92012-02-29 11:36:12 -08002746 foundCurrentAccount = true;
2747 }
Vikram Aggarwal60069912012-07-24 14:26:09 -07002748 // Is there a new account that we do not know about?
Vikram Aggarwalc04cf7e2013-05-13 15:38:42 -07002749 if (!mCurrentAccountUris.contains(account.uri)) {
Paul Westbrook23b74b92012-02-29 11:36:12 -08002750 return true;
2751 }
Vikram Aggarwal7dedb952012-02-16 16:10:23 -08002752 } while (accountCursor.moveToNext());
Paul Westbrook23b74b92012-02-29 11:36:12 -08002753
2754 // As long as we found the current account, the list hasn't been updated
2755 return !foundCurrentAccount;
Vikram Aggarwal7dedb952012-02-16 16:10:23 -08002756 }
2757
2758 /**
Vikram Aggarwal60069912012-07-24 14:26:09 -07002759 * Updates accounts for the app. If the current account is missing, the first
2760 * account in the list is set to the current account (we <em>have</em> to choose something).
Mindy Pereira161f50d2012-02-28 15:47:19 -08002761 *
Vikram Aggarwal6c511582012-02-27 10:59:47 -08002762 * @param accounts cursor into the AccountCache
Vikram Aggarwal7dedb952012-02-16 16:10:23 -08002763 * @return true if the update was successful, false otherwise
2764 */
Vikram Aggarwal177097f2013-03-08 11:19:53 -08002765 private boolean updateAccounts(ObjectCursor<Account> accounts) {
Vikram Aggarwal7dedb952012-02-16 16:10:23 -08002766 if (accounts == null || !accounts.moveToFirst()) {
2767 return false;
2768 }
Paul Westbrook23b74b92012-02-29 11:36:12 -08002769
Vikram Aggarwala9b93f32012-02-23 14:51:58 -08002770 final Account[] allAccounts = Account.getAllAccounts(accounts);
Vikram Aggarwal60069912012-07-24 14:26:09 -07002771 // A match for the current account's URI in the list of accounts.
2772 Account currentFromList = null;
Paul Westbrook23b74b92012-02-29 11:36:12 -08002773
Vikram Aggarwal636c1a12012-09-22 16:02:40 -07002774 // Save the uris for the accounts and find the current account in the updated cursor.
Paul Westbrook23b74b92012-02-29 11:36:12 -08002775 mCurrentAccountUris.clear();
Vikram Aggarwal636c1a12012-09-22 16:02:40 -07002776 for (final Account account : allAccounts) {
Vikram Aggarwal60069912012-07-24 14:26:09 -07002777 LogUtils.d(LOG_TAG, "updateAccounts(%s)", account);
Paul Westbrook23b74b92012-02-29 11:36:12 -08002778 mCurrentAccountUris.add(account.uri);
Vikram Aggarwal60069912012-07-24 14:26:09 -07002779 if (mAccount != null && account.uri.equals(mAccount.uri)) {
2780 currentFromList = account;
2781 }
Paul Westbrook23b74b92012-02-29 11:36:12 -08002782 }
2783
Vikram Aggarwal60069912012-07-24 14:26:09 -07002784 // 1. current account is already set and is in allAccounts:
2785 // 1a. It has changed -> load the updated account.
2786 // 2b. It is unchanged -> no-op
Andy Huang0d647352012-03-21 21:48:16 -07002787 // 2. current account is set and is not in allAccounts -> pick first (acct was deleted?)
Vikram Aggarwal60069912012-07-24 14:26:09 -07002788 // 3. saved preference has an account -> pick that one
Andy Huang0d647352012-03-21 21:48:16 -07002789 // 4. otherwise just pick first
2790
Vikram Aggarwal60069912012-07-24 14:26:09 -07002791 boolean accountChanged = false;
2792 /// Assume case 4, initialize to first account, and see if we can find anything better.
2793 Account newAccount = allAccounts[0];
2794 if (currentFromList != null) {
2795 // Case 1: Current account exists but has changed
2796 if (!currentFromList.equals(mAccount)) {
2797 newAccount = currentFromList;
2798 accountChanged = true;
Andy Huang0d647352012-03-21 21:48:16 -07002799 }
Vikram Aggarwal60069912012-07-24 14:26:09 -07002800 // Case 1b: else, current account is unchanged: nothing to do.
Paul Westbrook23b74b92012-02-29 11:36:12 -08002801 } else {
Vikram Aggarwal60069912012-07-24 14:26:09 -07002802 // Case 2: Current account is not in allAccounts, the account needs to change.
2803 accountChanged = true;
2804 if (mAccount == null) {
2805 // Case 3: Check for last viewed account, and check if it exists in the list.
2806 final String lastAccountUri = MailAppProvider.getInstance().getLastViewedAccount();
2807 if (lastAccountUri != null) {
2808 for (final Account account : allAccounts) {
2809 if (lastAccountUri.equals(account.uri.toString())) {
2810 newAccount = account;
2811 break;
2812 }
Andy Huang0d647352012-03-21 21:48:16 -07002813 }
2814 }
2815 }
Paul Westbrook23b74b92012-02-29 11:36:12 -08002816 }
Vikram Aggarwal60069912012-07-24 14:26:09 -07002817 if (accountChanged) {
Vikram Aggarwal5fd8afd2013-03-13 15:28:47 -07002818 changeAccount(newAccount);
Vikram Aggarwal60069912012-07-24 14:26:09 -07002819 }
Vikram Aggarwal07dbaa62013-03-12 15:21:04 -07002820
Vikram Aggarwal60069912012-07-24 14:26:09 -07002821 // Whether we have updated the current account or not, we need to update the list of
2822 // accounts in the ActionBar.
Rohan Shahb905f0e2013-04-26 09:17:37 -07002823 mAllAccounts = allAccounts;
Vikram Aggarwal07dbaa62013-03-12 15:21:04 -07002824 mAllAccountObservers.notifyChanged();
Vikram Aggarwala9b93f32012-02-23 14:51:58 -08002825 return (allAccounts.length > 0);
Vikram Aggarwal7dedb952012-02-16 16:10:23 -08002826 }
2827
Paul Westbrook6ead20d2012-03-19 14:48:14 -07002828 private void disableNotifications() {
2829 mNewEmailReceiver.activate(mContext, this);
2830 }
2831
2832 private void enableNotifications() {
2833 mNewEmailReceiver.deactivate();
2834 }
2835
2836 private void disableNotificationsOnAccountChange(Account account) {
2837 // If the new mail suppression receiver is activated for a different account, we want to
2838 // activate it for the new account.
2839 if (mNewEmailReceiver.activated() &&
2840 !mNewEmailReceiver.notificationsDisabledForAccount(account)) {
2841 // Deactivate the current receiver, otherwise multiple receivers may be registered.
2842 mNewEmailReceiver.deactivate();
2843 mNewEmailReceiver.activate(mContext, this);
2844 }
2845 }
2846
Vikram Aggarwala9b93f32012-02-23 14:51:58 -08002847 /**
Vikram Aggarwalc7694222012-04-23 13:37:01 -07002848 * Destructive actions on Conversations. This class should only be created by controllers, and
2849 * clients should only require {@link DestructiveAction}s, not specific implementations of the.
2850 * Only the controllers should know what kind of destructive actions are being created.
2851 */
Mindy Pereirade3e74a2012-07-24 09:43:10 -07002852 public class ConversationAction implements DestructiveAction {
Vikram Aggarwalacaa3c02012-04-24 12:45:27 -07002853 /**
2854 * The action to be performed. This is specified as the resource ID of the menu item
2855 * corresponding to this action: R.id.delete, R.id.report_spam, etc.
2856 */
Vikram Aggarwal7f602f72012-04-30 16:04:06 -07002857 private final int mAction;
Vikram Aggarwalbc67bb12012-04-30 14:05:35 -07002858 /** The action will act upon these conversations */
Paul Westbrook77eee622012-07-10 13:41:57 -07002859 private final Collection<Conversation> mTarget;
Vikram Aggarwal7f602f72012-04-30 16:04:06 -07002860 /** Whether this destructive action has already been performed */
2861 private boolean mCompleted;
Vikram Aggarwalc4113952012-05-11 14:14:56 -07002862 /** Whether this is an action on the currently selected set. */
2863 private final boolean mIsSelectedSet;
Vikram Aggarwale8a85322012-04-24 09:01:38 -07002864
Jin Cao30c881a2014-04-08 14:28:36 -07002865 private UndoCallback mCallback;
2866
Mindy Pereirafbe40192012-03-20 10:40:45 -07002867 /**
Vikram Aggarwal715f83d2013-03-05 17:04:56 -08002868 * Create a listener object.
2869 * @param action action is one of four constants: R.id.y_button (archive),
Mindy Pereirafbe40192012-03-20 10:40:45 -07002870 * R.id.delete , R.id.mute, and R.id.report_spam.
Vikram Aggarwalbc67bb12012-04-30 14:05:35 -07002871 * @param target Conversation that we want to apply the action to.
Vikram Aggarwalc4113952012-05-11 14:14:56 -07002872 * @param isBatch whether the conversations are in the currently selected batch set.
2873 */
2874 public ConversationAction(int action, Collection<Conversation> target, boolean isBatch) {
Mindy Pereirafbe40192012-03-20 10:40:45 -07002875 mAction = action;
Paul Westbrook77eee622012-07-10 13:41:57 -07002876 mTarget = ImmutableList.copyOf(target);
Vikram Aggarwalc4113952012-05-11 14:14:56 -07002877 mIsSelectedSet = isBatch;
Mindy Pereirafbe40192012-03-20 10:40:45 -07002878 }
2879
Jin Cao30c881a2014-04-08 14:28:36 -07002880 @Override
2881 public void setUndoCallback(UndoCallback undoCallback) {
2882 mCallback = undoCallback;
2883 }
2884
Vikram Aggarwalacaa3c02012-04-24 12:45:27 -07002885 /**
2886 * The action common to child classes. This performs the action specified in the constructor
2887 * on the conversations given here.
Vikram Aggarwalacaa3c02012-04-24 12:45:27 -07002888 */
Vikram Aggarwalbc67bb12012-04-30 14:05:35 -07002889 @Override
2890 public void performAction() {
Vikram Aggarwal7f602f72012-04-30 16:04:06 -07002891 if (isPerformed()) {
2892 return;
2893 }
Mindy Pereira3cb28b52012-05-24 15:26:39 -07002894 boolean undoEnabled = mAccount.supportsCapability(AccountCapabilities.UNDO);
Vikram Aggarwal7dd054e2012-05-21 14:43:10 -07002895
2896 // Are we destroying the currently shown conversation? Show the next one.
2897 if (LogUtils.isLoggable(LOG_TAG, LogUtils.DEBUG)){
Vikram Aggarwal8742a612012-08-13 10:22:50 -07002898 LogUtils.d(LOG_TAG, "ConversationAction.performAction():"
2899 + "\nmTarget=%s\nCurrent=%s",
Vikram Aggarwal7dd054e2012-05-21 14:43:10 -07002900 Conversation.toString(mTarget), mCurrentConversation);
2901 }
Vikram Aggarwal7dd054e2012-05-21 14:43:10 -07002902
Paul Westbrooke1221d22012-08-19 11:09:07 -07002903 if (mConversationListCursor == null) {
2904 LogUtils.e(LOG_TAG, "null ConversationCursor in ConversationAction.performAction():"
2905 + "\nmTarget=%s\nCurrent=%s",
2906 Conversation.toString(mTarget), mCurrentConversation);
2907 return;
2908 }
2909
Scott Kennedy2b9d80e2013-07-30 23:03:45 -07002910 if (mAction == R.id.archive) {
2911 LogUtils.d(LOG_TAG, "Archiving");
Jin Cao30c881a2014-04-08 14:28:36 -07002912 mConversationListCursor.archive(mTarget, mCallback);
Scott Kennedy2b9d80e2013-07-30 23:03:45 -07002913 } else if (mAction == R.id.delete) {
2914 LogUtils.d(LOG_TAG, "Deleting");
Jin Cao30c881a2014-04-08 14:28:36 -07002915 mConversationListCursor.delete(mTarget, mCallback);
Scott Kennedy2b9d80e2013-07-30 23:03:45 -07002916 if (mFolder.supportsCapability(FolderCapabilities.DELETE_ACTION_FINAL)) {
Paul Westbrookef362542012-08-27 14:53:32 -07002917 undoEnabled = false;
Scott Kennedy2b9d80e2013-07-30 23:03:45 -07002918 }
2919 } else if (mAction == R.id.mute) {
2920 LogUtils.d(LOG_TAG, "Muting");
2921 if (mFolder.supportsCapability(FolderCapabilities.DESTRUCTIVE_MUTE)) {
2922 for (Conversation c : mTarget) {
2923 c.localDeleteOnUpdate = true;
2924 }
2925 }
Jin Cao30c881a2014-04-08 14:28:36 -07002926 mConversationListCursor.mute(mTarget, mCallback);
Scott Kennedy2b9d80e2013-07-30 23:03:45 -07002927 } else if (mAction == R.id.report_spam) {
2928 LogUtils.d(LOG_TAG, "Reporting spam");
Jin Cao30c881a2014-04-08 14:28:36 -07002929 mConversationListCursor.reportSpam(mTarget, mCallback);
Scott Kennedy2b9d80e2013-07-30 23:03:45 -07002930 } else if (mAction == R.id.mark_not_spam) {
2931 LogUtils.d(LOG_TAG, "Marking not spam");
Jin Cao30c881a2014-04-08 14:28:36 -07002932 mConversationListCursor.reportNotSpam(mTarget, mCallback);
Scott Kennedy2b9d80e2013-07-30 23:03:45 -07002933 } else if (mAction == R.id.report_phishing) {
2934 LogUtils.d(LOG_TAG, "Reporting phishing");
Jin Cao30c881a2014-04-08 14:28:36 -07002935 mConversationListCursor.reportPhishing(mTarget, mCallback);
Scott Kennedy2b9d80e2013-07-30 23:03:45 -07002936 } else if (mAction == R.id.remove_star) {
2937 LogUtils.d(LOG_TAG, "Removing star");
2938 // Star removal is destructive in the Starred folder.
2939 mConversationListCursor.updateBoolean(mTarget, ConversationColumns.STARRED,
2940 false);
2941 } else if (mAction == R.id.mark_not_important) {
2942 LogUtils.d(LOG_TAG, "Marking not-important");
2943 // Marking not important is destructive in a mailbox
2944 // containing only important messages
2945 if (mFolder != null && mFolder.isImportantOnly()) {
2946 for (Conversation conv : mTarget) {
2947 conv.localDeleteOnUpdate = true;
2948 }
2949 }
2950 mConversationListCursor.updateInt(mTarget, ConversationColumns.PRIORITY,
2951 UIProvider.ConversationPriority.LOW);
2952 } else if (mAction == R.id.discard_drafts) {
2953 LogUtils.d(LOG_TAG, "Discarding draft messages");
2954 // Discarding draft messages is destructive in a "draft" mailbox
2955 if (mFolder != null && mFolder.isDraft()) {
2956 for (Conversation conv : mTarget) {
2957 conv.localDeleteOnUpdate = true;
2958 }
2959 }
2960 mConversationListCursor.discardDrafts(mTarget);
2961 // We don't support undoing discarding drafts
2962 undoEnabled = false;
Vikram Aggarwal3d7ca9d2012-05-11 14:40:36 -07002963 }
2964 if (undoEnabled) {
mindypead50392012-08-23 11:03:53 -07002965 mHandler.postDelayed(new Runnable() {
2966 @Override
2967 public void run() {
2968 onUndoAvailable(new ToastBarOperation(mTarget.size(), mAction,
Scott Kennedy6a3d5ce2013-03-15 17:33:14 -07002969 ToastBarOperation.UNDO, mIsSelectedSet, mFolder));
mindypead50392012-08-23 11:03:53 -07002970 }
2971 }, mShowUndoBarDelay);
Vikram Aggarwal3d7ca9d2012-05-11 14:40:36 -07002972 }
Vikram Aggarwalbc67bb12012-04-30 14:05:35 -07002973 refreshConversationList();
Vikram Aggarwalc4113952012-05-11 14:14:56 -07002974 if (mIsSelectedSet) {
Vikram Aggarwalc4113952012-05-11 14:14:56 -07002975 mSelectedSet.clear();
2976 }
Mindy Pereirafbe40192012-03-20 10:40:45 -07002977 }
Vikram Aggarwal7f602f72012-04-30 16:04:06 -07002978
2979 /**
2980 * Returns true if this action has been performed, false otherwise.
Andy Huang839ada22012-07-20 15:48:40 -07002981 *
Vikram Aggarwal7f602f72012-04-30 16:04:06 -07002982 */
2983 private synchronized boolean isPerformed() {
2984 if (mCompleted) {
2985 return true;
2986 }
2987 mCompleted = true;
2988 return false;
2989 }
Mindy Pereirafbe40192012-03-20 10:40:45 -07002990 }
Mindy Pereirae5f4dc02012-03-21 16:08:53 -07002991
Vikram Aggarwald503df42012-05-11 10:13:35 -07002992 // Called from the FolderSelectionDialog after a user is done selecting folders to assign the
2993 // conversations to.
Mindy Pereirae5f4dc02012-03-21 16:08:53 -07002994 @Override
Mindy Pereira8db7e402012-07-13 10:32:47 -07002995 public final void assignFolder(Collection<FolderOperation> folderOps,
Scott Kennedy6a3d5ce2013-03-15 17:33:14 -07002996 Collection<Conversation> target, boolean batch, boolean showUndo,
2997 final boolean isMoveTo) {
Mindy Pereira8db7e402012-07-13 10:32:47 -07002998 // Actions are destructive only when the current folder can be assigned
2999 // to (which is the same as being able to un-assign a conversation from the folder) and
3000 // when the list of folders contains the current folder.
3001 final boolean isDestructive = mFolder
3002 .supportsCapability(FolderCapabilities.CAN_ACCEPT_MOVED_MESSAGES)
3003 && FolderOperation.isDestructive(folderOps, mFolder);
Vikram Aggarwald503df42012-05-11 10:13:35 -07003004 LogUtils.d(LOG_TAG, "onFolderChangesCommit: isDestructive = %b", isDestructive);
3005 if (isDestructive) {
3006 for (final Conversation c : target) {
3007 c.localDeleteOnUpdate = true;
Mindy Pereira6778f462012-03-23 18:01:55 -07003008 }
Mindy Pereirae5f4dc02012-03-21 16:08:53 -07003009 }
mindypc84759c2012-08-29 09:51:53 -07003010 final DestructiveAction folderChange;
Jin Cao30c881a2014-04-08 14:28:36 -07003011 final UndoCallback undoCallback = isMoveTo ?
3012 getUndoCallbackForDestructiveActionsWithAutoAdvance(R.id.move_to,
3013 mCurrentConversation)
3014 : null;
Vikram Aggarwald503df42012-05-11 10:13:35 -07003015 // Update the UI elements depending no their visibility and availability
Vikram Aggarwal3d7ca9d2012-05-11 14:40:36 -07003016 // TODO(viki): Consolidate this into a single method requestDelete.
Vikram Aggarwald503df42012-05-11 10:13:35 -07003017 if (isDestructive) {
Scott Kennedy6a3d5ce2013-03-15 17:33:14 -07003018 /*
3019 * If this is a MOVE operation, we want the action folder to be the destination folder.
3020 * Otherwise, we want it to be the current folder.
3021 *
3022 * A set of folder operations is a move if there are exactly two operations: an add and
3023 * a remove.
3024 */
3025 final Folder actionFolder;
3026 if (folderOps.size() != 2) {
3027 actionFolder = mFolder;
3028 } else {
3029 Folder addedFolder = null;
3030 boolean hasRemove = false;
3031 for (final FolderOperation folderOperation : folderOps) {
3032 if (folderOperation.mAdd) {
3033 addedFolder = folderOperation.mFolder;
3034 } else {
3035 hasRemove = true;
3036 }
3037 }
3038
3039 if (hasRemove && addedFolder != null) {
3040 actionFolder = addedFolder;
3041 } else {
3042 actionFolder = mFolder;
3043 }
3044 }
3045
mindypc84759c2012-08-29 09:51:53 -07003046 folderChange = getDeferredFolderChange(target, folderOps, isDestructive,
Jin Cao30c881a2014-04-08 14:28:36 -07003047 batch, showUndo, isMoveTo, actionFolder, undoCallback);
Scott Kennedycaaeed32013-06-12 13:39:16 -07003048 delete(0, target, folderChange, batch);
Vikram Aggarwal6902dcf2012-04-11 08:57:42 -07003049 } else {
mindypc84759c2012-08-29 09:51:53 -07003050 folderChange = getFolderChange(target, folderOps, isDestructive,
Jin Cao30c881a2014-04-08 14:28:36 -07003051 batch, showUndo, false /* isMoveTo */, mFolder, undoCallback);
Vikram Aggarwal715f83d2013-03-05 17:04:56 -08003052 requestUpdate(folderChange);
Mindy Pereirae5f4dc02012-03-21 16:08:53 -07003053 }
3054 }
3055
Mindy Pereira967ede62012-03-22 09:29:09 -07003056 @Override
Vikram Aggarwal3d7ca9d2012-05-11 14:40:36 -07003057 public final void onRefreshRequired() {
mindyp5390fca2012-08-22 12:12:25 -07003058 if (isAnimating() || isDragging()) {
Andy Huangc1922a92013-05-13 14:33:05 -07003059 LogUtils.i(ConversationCursor.LOG_TAG, "onRefreshRequired: delay until animating done");
Mindy Pereira69e88dd2012-08-10 09:30:18 -07003060 return;
3061 }
Marc Blankbf128eb2012-04-18 15:58:45 -07003062 // Refresh the query in the background
Paul Westbrookcff1aea2012-08-10 11:51:00 -07003063 if (mConversationListCursor.isRefreshRequired()) {
3064 mConversationListCursor.refresh();
3065 }
Marc Blankbf128eb2012-04-18 15:58:45 -07003066 }
3067
mindyp5390fca2012-08-22 12:12:25 -07003068 @Override
3069 public void startDragMode() {
3070 mIsDragHappening = true;
3071 }
3072
3073 @Override
3074 public void stopDragMode() {
3075 mIsDragHappening = false;
3076 if (mConversationListCursor.isRefreshReady()) {
Andy Huangc1922a92013-05-13 14:33:05 -07003077 LogUtils.i(ConversationCursor.LOG_TAG, "Stopped dragging: try sync");
mindyp5390fca2012-08-22 12:12:25 -07003078 onRefreshReady();
3079 }
3080
3081 if (mConversationListCursor.isRefreshRequired()) {
Andy Huangc1922a92013-05-13 14:33:05 -07003082 LogUtils.i(ConversationCursor.LOG_TAG, "Stopped dragging: refresh");
mindyp5390fca2012-08-22 12:12:25 -07003083 mConversationListCursor.refresh();
3084 }
3085 }
3086
3087 private boolean isDragging() {
3088 return mIsDragHappening;
3089 }
3090
mindyp6f54e1b2012-10-09 09:54:08 -07003091 @Override
3092 public boolean isAnimating() {
Mindy Pereira69e88dd2012-08-10 09:30:18 -07003093 boolean isAnimating = false;
3094 ConversationListFragment convListFragment = getConversationListFragment();
3095 if (convListFragment != null) {
Andy Huang48ccbc52013-06-05 20:30:47 -07003096 isAnimating = convListFragment.isAnimating();
Mindy Pereira69e88dd2012-08-10 09:30:18 -07003097 }
3098 return isAnimating;
3099 }
3100
Marc Blankbf128eb2012-04-18 15:58:45 -07003101 /**
3102 * Called when the {@link ConversationCursor} is changed or has new data in it.
3103 * <p>
3104 * {@inheritDoc}
3105 */
3106 @Override
Vikram Aggarwal3d7ca9d2012-05-11 14:40:36 -07003107 public final void onRefreshReady() {
mindyp5c1d8352012-11-05 10:12:44 -08003108 LogUtils.d(LOG_TAG, "Received refresh ready callback for folder %s",
3109 mFolder != null ? mFolder.id : "-1");
Andy Huangb2ef9c12012-12-18 12:58:41 -06003110
3111 if (mDestroyed) {
3112 LogUtils.i(LOG_TAG, "ignoring onRefreshReady on destroyed AAC");
3113 return;
3114 }
3115
Paul Westbrookcff1aea2012-08-10 11:51:00 -07003116 if (!isAnimating()) {
Marc Blankbf128eb2012-04-18 15:58:45 -07003117 // Swap cursors
3118 mConversationListCursor.sync();
Marc Blankbf128eb2012-04-18 15:58:45 -07003119 }
Paul Westbrook937c94f2012-08-16 13:01:18 -07003120 mTracker.onCursorUpdated();
Vikram Aggarwalf0ef2302012-11-07 14:53:34 -08003121 perhapsShowFirstSearchResult();
Marc Blankbf128eb2012-04-18 15:58:45 -07003122 }
3123
3124 @Override
Vikram Aggarwal3d7ca9d2012-05-11 14:40:36 -07003125 public final void onDataSetChanged() {
Paul Westbrook9f119c72012-04-24 16:10:59 -07003126 updateConversationListFragment();
Andy Huang632721e2012-04-11 16:57:26 -07003127 mConversationListObservable.notifyChanged();
Paul Westbrooka13b3742012-09-07 16:35:06 -07003128 mSelectedSet.validateAgainstCursor(mConversationListCursor);
Marc Blankbf128eb2012-04-18 15:58:45 -07003129 }
3130
Vikram Aggarwal3d7ca9d2012-05-11 14:40:36 -07003131 /**
3132 * If the Conversation List Fragment is visible, updates the fragment.
3133 */
Vikram Aggarwal715f83d2013-03-05 17:04:56 -08003134 private void updateConversationListFragment() {
Marc Blankbf128eb2012-04-18 15:58:45 -07003135 final ConversationListFragment convList = getConversationListFragment();
3136 if (convList != null) {
Vikram Aggarwal75daee52012-04-30 11:13:09 -07003137 refreshConversationList();
Vikram Aggarwal6422b8d2013-01-14 10:47:41 -08003138 if (isFragmentVisible(convList)) {
Vikram Aggarwal69b5c302012-09-05 11:11:13 -07003139 informCursorVisiblity(true);
Paul Westbrook9f119c72012-04-24 16:10:59 -07003140 }
Marc Blankbf128eb2012-04-18 15:58:45 -07003141 }
3142 }
3143
3144 /**
3145 * This class handles throttled refresh of the conversation list
3146 */
3147 static class RefreshTimerTask extends TimerTask {
3148 final Handler mHandler;
3149 final AbstractActivityController mController;
3150
3151 RefreshTimerTask(AbstractActivityController controller, Handler handler) {
3152 mHandler = handler;
3153 mController = controller;
3154 }
3155
3156 @Override
3157 public void run() {
3158 mHandler.post(new Runnable() {
3159 @Override
3160 public void run() {
3161 LogUtils.d(LOG_TAG, "Delay done... calling onRefreshRequired");
3162 mController.onRefreshRequired();
3163 }});
3164 }
3165 }
3166
3167 /**
3168 * Cancel the refresh task, if it's running
3169 */
3170 private void cancelRefreshTask () {
3171 if (mConversationListRefreshTask != null) {
3172 mConversationListRefreshTask.cancel();
3173 mConversationListRefreshTask = null;
3174 }
3175 }
3176
3177 @Override
Paul Westbrookcff1aea2012-08-10 11:51:00 -07003178 public void onAnimationEnd(AnimatedAdapter animatedAdapter) {
Paul Westbrook026139c2012-09-19 22:35:37 -07003179 if (mConversationListCursor == null) {
3180 LogUtils.e(LOG_TAG, "null ConversationCursor in onAnimationEnd");
3181 return;
3182 }
Paul Westbrookcff1aea2012-08-10 11:51:00 -07003183 if (mConversationListCursor.isRefreshReady()) {
Andy Huangc1922a92013-05-13 14:33:05 -07003184 LogUtils.i(ConversationCursor.LOG_TAG, "Stopped animating: try sync");
Paul Westbrookcff1aea2012-08-10 11:51:00 -07003185 onRefreshReady();
Marc Blankbf128eb2012-04-18 15:58:45 -07003186 }
Marc Blankbf128eb2012-04-18 15:58:45 -07003187
Paul Westbrookcff1aea2012-08-10 11:51:00 -07003188 if (mConversationListCursor.isRefreshRequired()) {
Andy Huangc1922a92013-05-13 14:33:05 -07003189 LogUtils.i(ConversationCursor.LOG_TAG, "Stopped animating: refresh");
Paul Westbrookcff1aea2012-08-10 11:51:00 -07003190 mConversationListCursor.refresh();
3191 }
mindyp6f54e1b2012-10-09 09:54:08 -07003192 if (mRecentsDataUpdated) {
3193 mRecentsDataUpdated = false;
3194 mRecentFolderObservers.notifyChanged();
3195 }
Marc Blankbf128eb2012-04-18 15:58:45 -07003196 }
3197
3198 @Override
Mindy Pereira967ede62012-03-22 09:29:09 -07003199 public void onSetEmpty() {
Vikram Aggarwal6cadbfc2012-12-27 09:17:05 -08003200 // There are no selected conversations. Ensure that the listener and its associated actions
3201 // are blanked out.
3202 setListener(null, -1);
Mindy Pereira967ede62012-03-22 09:29:09 -07003203 }
3204
3205 @Override
3206 public void onSetPopulated(ConversationSelectionSet set) {
Vikram Aggarwal7704d792013-01-11 15:48:24 -08003207 mCabActionMenu = new SelectedConversationsActionMenu(mActivity, set, mFolder);
Vikram Aggarwal5b9ed2b2013-01-28 18:08:37 -08003208 if (mViewMode.isListMode() || (mIsTablet && mViewMode.isConversationMode())) {
Vikram Aggarwal7704d792013-01-11 15:48:24 -08003209 enableCabMode();
Vikram Aggarwal6902dcf2012-04-11 08:57:42 -07003210 }
Mindy Pereira967ede62012-03-22 09:29:09 -07003211 }
3212
Mindy Pereira967ede62012-03-22 09:29:09 -07003213 @Override
3214 public void onSetChanged(ConversationSelectionSet set) {
3215 // Do nothing. We don't care about changes to the set.
3216 }
3217
3218 @Override
3219 public ConversationSelectionSet getSelectedSet() {
3220 return mSelectedSet;
3221 }
3222
Vikram Aggarwale128fc22012-04-04 12:33:34 -07003223 /**
3224 * Disable the Contextual Action Bar (CAB). The selected set is not changed.
3225 */
3226 protected void disableCabMode() {
Mindy Pereira8937bf12012-07-23 14:05:02 -07003227 // Commit any previous destructive actions when entering/ exiting CAB mode.
mindypc6adce32012-08-22 18:46:42 -07003228 commitDestructiveActions(true);
Vikram Aggarwale128fc22012-04-04 12:33:34 -07003229 if (mCabActionMenu != null) {
3230 mCabActionMenu.deactivate();
3231 }
3232 }
3233
3234 /**
3235 * Re-enable the CAB menu if required. The selection set is not changed.
3236 */
3237 protected void enableCabMode() {
Tony Mantler3d6751a2013-08-02 11:30:59 -07003238 if (mCabActionMenu != null &&
3239 !(isDrawerEnabled() && mDrawerContainer.isDrawerOpen(mDrawerPullout))) {
Vikram Aggarwale128fc22012-04-04 12:33:34 -07003240 mCabActionMenu.activate();
3241 }
3242 }
3243
Vikram Aggarwal4eb52712012-06-19 16:24:50 -07003244 /**
Tony Mantler43fab322013-07-26 16:38:35 -07003245 * Re-enable CAB mode only if we have an active selection
3246 */
3247 protected void maybeEnableCabMode() {
3248 if (!mSelectedSet.isEmpty()) {
Tony Mantlera703d8a2013-07-31 12:00:46 -07003249 if (mCabActionMenu != null) {
3250 mCabActionMenu.activate();
3251 }
Tony Mantler43fab322013-07-26 16:38:35 -07003252 }
3253 }
3254
3255 /**
Vikram Aggarwal4eb52712012-06-19 16:24:50 -07003256 * Unselect conversations and exit CAB mode.
3257 */
3258 protected final void exitCabMode() {
3259 mSelectedSet.clear();
3260 }
3261
Mindy Pereira967ede62012-03-22 09:29:09 -07003262 @Override
Mindy Pereirafd0c2972012-03-27 13:50:39 -07003263 public void startSearch() {
Vikram Aggarwal35f19d72012-04-24 13:24:48 -07003264 if (mAccount == null) {
3265 // We cannot search if there is no account. Drop the request to the floor.
3266 LogUtils.d(LOG_TAG, "AbstractActivityController.startSearch(): null account");
3267 return;
3268 }
James Lemieux3531d7e2014-01-28 11:10:05 -08003269 if (mAccount.supportsSearch()) {
Andy Huang313ac132013-03-04 23:40:58 -08003270 mActionBarView.expandSearch();
Mindy Pereirafd0c2972012-03-27 13:50:39 -07003271 } else {
3272 Toast.makeText(mActivity.getActivityContext(), mActivity.getActivityContext()
Mindy Pereiraa46c2992012-03-27 14:12:39 -07003273 .getString(R.string.search_unsupported), Toast.LENGTH_SHORT).show();
Mindy Pereirafd0c2972012-03-27 13:50:39 -07003274 }
3275 }
Mindy Pereiraacf60392012-04-06 09:11:00 -07003276
Vikram Aggarwal0dda5732012-04-06 11:20:16 -07003277 @Override
3278 public void exitSearchMode() {
3279 if (mViewMode.getMode() == ViewMode.SEARCH_RESULTS_LIST) {
3280 mActivity.finish();
3281 }
3282 }
3283
Mindy Pereiraacf60392012-04-06 09:11:00 -07003284 /**
3285 * Supports dragging conversations to a folder.
3286 */
3287 @Override
3288 public boolean supportsDrag(DragEvent event, Folder folder) {
3289 return (folder != null
3290 && event != null
3291 && event.getClipDescription() != null
3292 && folder.supportsCapability
3293 (UIProvider.FolderCapabilities.CAN_ACCEPT_MOVED_MESSAGES)
Scott Kennedy259df5b2013-07-11 13:24:01 -07003294 && !mFolder.equals(folder));
Mindy Pereiraacf60392012-04-06 09:11:00 -07003295 }
3296
3297 /**
Mindy Pereira6c2663d2012-07-20 15:37:29 -07003298 * Handles dropping conversations to a folder.
Mindy Pereiraacf60392012-04-06 09:11:00 -07003299 */
3300 @Override
3301 public void handleDrop(DragEvent event, final Folder folder) {
Mindy Pereiraacf60392012-04-06 09:11:00 -07003302 if (!supportsDrag(event, folder)) {
3303 return;
3304 }
Scott Kennedy8c1058e2013-03-20 13:40:20 -07003305 if (folder.isType(UIProvider.FolderType.STARRED)) {
mindypae7e6a02012-11-29 13:28:10 -08003306 // Moving a conversation to the starred folder adds the star and
3307 // removes the current label
3308 handleDropInStarred(folder);
3309 return;
3310 }
Scott Kennedy8c1058e2013-03-20 13:40:20 -07003311 if (mFolder.isType(UIProvider.FolderType.STARRED)) {
mindypae7e6a02012-11-29 13:28:10 -08003312 handleDragFromStarred(folder);
3313 return;
3314 }
mindypa8492632012-09-24 09:27:54 -07003315 final ArrayList<FolderOperation> dragDropOperations = new ArrayList<FolderOperation>();
mindypae7e6a02012-11-29 13:28:10 -08003316 final Collection<Conversation> conversations = mSelectedSet.values();
mindypa8492632012-09-24 09:27:54 -07003317 // Add the drop target folder.
3318 dragDropOperations.add(new FolderOperation(folder, true));
3319 // Remove the current folder unless the user is viewing "all".
3320 // That operation should just add the new folder.
3321 boolean isDestructive = !mFolder.isViewAll()
3322 && mFolder.supportsCapability
3323 (UIProvider.FolderCapabilities.CAN_ACCEPT_MOVED_MESSAGES);
3324 if (isDestructive) {
3325 dragDropOperations.add(new FolderOperation(mFolder, false));
3326 }
Mindy Pereira8db7e402012-07-13 10:32:47 -07003327 // Drag and drop is destructive: we remove conversations from the
3328 // current folder.
Scott Kennedy6a3d5ce2013-03-15 17:33:14 -07003329 final DestructiveAction action =
3330 getFolderChange(conversations, dragDropOperations, isDestructive,
Jin Cao30c881a2014-04-08 14:28:36 -07003331 true /* isBatch */, true /* showUndo */, true /* isMoveTo */, folder,
3332 null /* undoCallback */);
mindypa8492632012-09-24 09:27:54 -07003333 if (isDestructive) {
Scott Kennedycaaeed32013-06-12 13:39:16 -07003334 delete(0, conversations, action, true);
mindypa8492632012-09-24 09:27:54 -07003335 } else {
3336 action.performAction();
3337 }
Mindy Pereiraacf60392012-04-06 09:11:00 -07003338 }
Mindy Pereira0963ef82012-04-10 11:43:01 -07003339
mindypae7e6a02012-11-29 13:28:10 -08003340 private void handleDragFromStarred(Folder folder) {
3341 final Collection<Conversation> conversations = mSelectedSet.values();
3342 // The conversation list deletes and performs the action if it exists.
3343 final ConversationListFragment convListFragment = getConversationListFragment();
3344 // There should always be a convlistfragment, or the user could not have
3345 // dragged/ dropped conversations.
3346 if (convListFragment != null) {
3347 LogUtils.d(LOG_TAG, "AAC.requestDelete: ListFragment is handling delete.");
3348 ArrayList<ConversationOperation> ops = new ArrayList<ConversationOperation>();
mindypcb0b30e2012-11-30 10:16:35 -08003349 ArrayList<Uri> folderUris;
3350 ArrayList<Boolean> adds;
mindypae7e6a02012-11-29 13:28:10 -08003351 for (Conversation target : conversations) {
mindypcb0b30e2012-11-30 10:16:35 -08003352 folderUris = new ArrayList<Uri>();
3353 adds = new ArrayList<Boolean>();
Scott Kennedy259df5b2013-07-11 13:24:01 -07003354 folderUris.add(folder.folderUri.fullUri);
mindypcb0b30e2012-11-30 10:16:35 -08003355 adds.add(Boolean.TRUE);
Paul Westbrook26746eb2012-12-06 14:44:01 -08003356 final HashMap<Uri, Folder> targetFolders =
3357 Folder.hashMapForFolders(target.getRawFolders());
Scott Kennedy259df5b2013-07-11 13:24:01 -07003358 targetFolders.put(folder.folderUri.fullUri, folder);
Paul Westbrook26746eb2012-12-06 14:44:01 -08003359 ops.add(mConversationListCursor.getConversationFolderOperation(target,
3360 folderUris, adds, targetFolders.values()));
mindypae7e6a02012-11-29 13:28:10 -08003361 }
3362 if (mConversationListCursor != null) {
Scott Kennedy9e2d4072013-03-21 21:46:01 -07003363 mConversationListCursor.updateBulkValues(ops);
mindypae7e6a02012-11-29 13:28:10 -08003364 }
3365 refreshConversationList();
3366 mSelectedSet.clear();
mindypae7e6a02012-11-29 13:28:10 -08003367 }
3368 }
3369
3370 private void handleDropInStarred(Folder folder) {
3371 final Collection<Conversation> conversations = mSelectedSet.values();
3372 // The conversation list deletes and performs the action if it exists.
3373 final ConversationListFragment convListFragment = getConversationListFragment();
3374 // There should always be a convlistfragment, or the user could not have
3375 // dragged/ dropped conversations.
3376 if (convListFragment != null) {
3377 LogUtils.d(LOG_TAG, "AAC.requestDelete: ListFragment is handling delete.");
Andrew Sapperstein6c570db2013-08-06 17:21:36 -07003378 convListFragment.requestDelete(R.id.change_folders, conversations,
mindyp5cc0ab22012-12-11 08:47:35 -08003379 new DroppedInStarredAction(conversations, mFolder, folder));
mindypae7e6a02012-11-29 13:28:10 -08003380 }
3381 }
3382
3383 // When dragging conversations to the starred folder, remove from the
3384 // original folder and add a star
3385 private class DroppedInStarredAction implements DestructiveAction {
Martin Hibdone78c40f2013-10-10 18:29:25 -07003386 private final Collection<Conversation> mConversations;
3387 private final Folder mInitialFolder;
3388 private final Folder mStarred;
mindypae7e6a02012-11-29 13:28:10 -08003389
mindyp5cc0ab22012-12-11 08:47:35 -08003390 public DroppedInStarredAction(Collection<Conversation> conversations, Folder initialFolder,
3391 Folder starredFolder) {
mindypae7e6a02012-11-29 13:28:10 -08003392 mConversations = conversations;
mindyp5cc0ab22012-12-11 08:47:35 -08003393 mInitialFolder = initialFolder;
3394 mStarred = starredFolder;
mindypae7e6a02012-11-29 13:28:10 -08003395 }
3396
3397 @Override
Jin Cao30c881a2014-04-08 14:28:36 -07003398 public void setUndoCallback(UndoCallback undoCallback) {
3399 return; // currently not applicable
3400 }
3401
3402 @Override
mindypae7e6a02012-11-29 13:28:10 -08003403 public void performAction() {
3404 ToastBarOperation undoOp = new ToastBarOperation(mConversations.size(),
Andrew Sapperstein6c570db2013-08-06 17:21:36 -07003405 R.id.change_folders, ToastBarOperation.UNDO, true /* batch */, mInitialFolder);
mindypae7e6a02012-11-29 13:28:10 -08003406 onUndoAvailable(undoOp);
3407 ArrayList<ConversationOperation> ops = new ArrayList<ConversationOperation>();
3408 ContentValues values = new ContentValues();
mindypcb0b30e2012-11-30 10:16:35 -08003409 ArrayList<Uri> folderUris;
3410 ArrayList<Boolean> adds;
mindyp5cc0ab22012-12-11 08:47:35 -08003411 ConversationOperation operation;
mindypae7e6a02012-11-29 13:28:10 -08003412 for (Conversation target : mConversations) {
mindypcb0b30e2012-11-30 10:16:35 -08003413 folderUris = new ArrayList<Uri>();
3414 adds = new ArrayList<Boolean>();
Scott Kennedy259df5b2013-07-11 13:24:01 -07003415 folderUris.add(mStarred.folderUri.fullUri);
mindyp5cc0ab22012-12-11 08:47:35 -08003416 adds.add(Boolean.TRUE);
Scott Kennedy259df5b2013-07-11 13:24:01 -07003417 folderUris.add(mInitialFolder.folderUri.fullUri);
mindypcb0b30e2012-11-30 10:16:35 -08003418 adds.add(Boolean.FALSE);
mindyp5cc0ab22012-12-11 08:47:35 -08003419 final HashMap<Uri, Folder> targetFolders =
3420 Folder.hashMapForFolders(target.getRawFolders());
Scott Kennedy259df5b2013-07-11 13:24:01 -07003421 targetFolders.put(mStarred.folderUri.fullUri, mStarred);
3422 targetFolders.remove(mInitialFolder.folderUri.fullUri);
mindyp5cc0ab22012-12-11 08:47:35 -08003423 values.put(ConversationColumns.STARRED, true);
3424 operation = mConversationListCursor.getConversationFolderOperation(target,
3425 folderUris, adds, targetFolders.values(), values);
3426 ops.add(operation);
mindypae7e6a02012-11-29 13:28:10 -08003427 }
3428 if (mConversationListCursor != null) {
Scott Kennedy9e2d4072013-03-21 21:46:01 -07003429 mConversationListCursor.updateBulkValues(ops);
mindypae7e6a02012-11-29 13:28:10 -08003430 }
3431 refreshConversationList();
3432 mSelectedSet.clear();
3433 }
3434 }
3435
Mindy Pereira0963ef82012-04-10 11:43:01 -07003436 @Override
Mindy Pereira0963ef82012-04-10 11:43:01 -07003437 public void onTouchEvent(MotionEvent event) {
3438 if (event.getAction() == MotionEvent.ACTION_DOWN) {
Andrew Sappersteinc2c9dc12012-07-02 18:17:32 -07003439 if (mToastBar != null && !mToastBar.isEventInToastBar(event)) {
Mark Weid243d452012-10-31 16:24:08 -07003440 hideOrRepositionToastBar(true);
Mindy Pereira0963ef82012-04-10 11:43:01 -07003441 }
3442 }
3443 }
Andy Huangb1c34dc2012-04-17 16:36:19 -07003444
Mark Weid243d452012-10-31 16:24:08 -07003445 protected abstract void hideOrRepositionToastBar(boolean animated);
3446
Andy Huang632721e2012-04-11 16:57:26 -07003447 @Override
Scott Kennedy3b965d72013-06-25 14:36:55 -07003448 public void onConversationSeen() {
3449 mPagerController.onConversationSeen();
Andy Huang632721e2012-04-11 16:57:26 -07003450 }
3451
Andy Huang9d3fd922012-09-26 22:23:58 -07003452 @Override
3453 public boolean isInitialConversationLoading() {
3454 return mPagerController.isInitialConversationLoading();
3455 }
3456
Vikram Aggarwal6422b8d2013-01-14 10:47:41 -08003457 /**
3458 * Check if the fragment given here is visible. Checking {@link Fragment#isVisible()} is
3459 * insufficient because that doesn't check if the window is currently in focus or not.
3460 */
Vikram Aggarwal715f83d2013-03-05 17:04:56 -08003461 private boolean isFragmentVisible(Fragment in) {
Vikram Aggarwal6422b8d2013-01-14 10:47:41 -08003462 return in != null && in.isVisible() && mActivity.hasWindowFocus();
3463 }
3464
Vikram Aggarwald70fe492013-06-04 12:52:07 -07003465 /**
3466 * This class handles callbacks that create a {@link ConversationCursor}.
3467 */
Andy Huangb1c34dc2012-04-17 16:36:19 -07003468 private class ConversationListLoaderCallbacks implements
3469 LoaderManager.LoaderCallbacks<ConversationCursor> {
3470
3471 @Override
3472 public Loader<ConversationCursor> onCreateLoader(int id, Bundle args) {
Vikram Aggarwala1b59dc2013-04-30 15:45:49 -07003473 final Account account = args.getParcelable(BUNDLE_ACCOUNT_KEY);
3474 final Folder folder = args.getParcelable(BUNDLE_FOLDER_KEY);
Andrew Sapperstein5bb4d052014-03-31 16:22:31 -07003475 final boolean ignoreInitialConversationLimit =
3476 args.getBoolean(BUNDLE_IGNORE_INITIAL_CONVERSATION_LIMIT_KEY, false);
Vikram Aggarwala1b59dc2013-04-30 15:45:49 -07003477 if (account == null || folder == null) {
3478 return null;
3479 }
Andrew Sapperstein5bb4d052014-03-31 16:22:31 -07003480 return new ConversationCursorLoader(mActivity, account,
3481 folder.conversationListUri, folder.name, ignoreInitialConversationLimit);
Andy Huangb1c34dc2012-04-17 16:36:19 -07003482 }
3483
3484 @Override
3485 public void onLoadFinished(Loader<ConversationCursor> loader, ConversationCursor data) {
Andy Huang144bfe72013-06-11 13:27:52 -07003486 LogUtils.d(LOG_TAG,
3487 "IN AAC.ConversationCursor.onLoadFinished, data=%s loader=%s this=%s",
3488 data, loader, this);
Andrew Sappersteina3ce6782013-05-09 15:14:49 -07003489 if (isDrawerEnabled() && mDrawerListener.getDrawerState() != DrawerLayout.STATE_IDLE) {
Vikram Aggarwala1b59dc2013-04-30 15:45:49 -07003490 LogUtils.d(LOG_TAG, "ConversationListLoaderCallbacks.onLoadFinished: ignoring.");
Andrew Sappersteina3ce6782013-05-09 15:14:49 -07003491 mConversationListLoadFinishedIgnored = true;
Vikram Aggarwala1b59dc2013-04-30 15:45:49 -07003492 return;
3493 }
Vikram Aggarwale8a85322012-04-24 09:01:38 -07003494 // Clear our all pending destructive actions before swapping the conversation cursor
3495 destroyPending(null);
Andy Huangb1c34dc2012-04-17 16:36:19 -07003496 mConversationListCursor = data;
Paul Westbrookbf232c32012-04-18 03:17:41 -07003497 mConversationListCursor.addListener(AbstractActivityController.this);
Andy Huang144bfe72013-06-11 13:27:52 -07003498 mDrawIdler.setListener(mConversationListCursor);
Paul Westbrook937c94f2012-08-16 13:01:18 -07003499 mTracker.onCursorUpdated();
Andy Huange3df1ad2012-04-24 17:15:23 -07003500 mConversationListObservable.notifyChanged();
Yu Ping Hu7c909c72013-01-18 11:58:01 -08003501 // Handle actions that were deferred until after the conversation list was loaded.
3502 for (LoadFinishedCallback callback : mConversationListLoadFinishedCallbacks) {
3503 callback.onLoadFinished();
3504 }
3505 mConversationListLoadFinishedCallbacks.clear();
Paul Westbrook9f119c72012-04-24 16:10:59 -07003506
Vikram Aggarwal17f373e2012-09-17 15:49:32 -07003507 final ConversationListFragment convList = getConversationListFragment();
Vikram Aggarwal6422b8d2013-01-14 10:47:41 -08003508 if (isFragmentVisible(convList)) {
Vikram Aggarwal17f373e2012-09-17 15:49:32 -07003509 // The conversation list is already listening to list changes and gets notified
3510 // in the mConversationListObservable.notifyChanged() line above. We only need to
3511 // check and inform the cursor of the change in visibility here.
3512 informCursorVisiblity(true);
Andy Huangb1c34dc2012-04-17 16:36:19 -07003513 }
Vikram Aggarwalf0ef2302012-11-07 14:53:34 -08003514 perhapsShowFirstSearchResult();
Andy Huangb1c34dc2012-04-17 16:36:19 -07003515 }
3516
3517 @Override
3518 public void onLoaderReset(Loader<ConversationCursor> loader) {
Andy Huang144bfe72013-06-11 13:27:52 -07003519 LogUtils.d(LOG_TAG,
3520 "IN AAC.ConversationCursor.onLoaderReset, data=%s loader=%s this=%s",
3521 mConversationListCursor, loader, this);
Paul Westbrook9a70e912012-08-17 15:53:20 -07003522
3523 if (mConversationListCursor != null) {
3524 // Unregister the listener
3525 mConversationListCursor.removeListener(AbstractActivityController.this);
Andy Huang144bfe72013-06-11 13:27:52 -07003526 mDrawIdler.setListener(null);
Paul Westbrook9a70e912012-08-17 15:53:20 -07003527 mConversationListCursor = null;
3528
3529 // Inform anyone who is interested about the change
3530 mTracker.onCursorUpdated();
3531 mConversationListObservable.notifyChanged();
Andy Huangb1c34dc2012-04-17 16:36:19 -07003532 }
Andy Huangb1c34dc2012-04-17 16:36:19 -07003533 }
Paul Westbrookbf232c32012-04-18 03:17:41 -07003534 }
Andy Huangb1c34dc2012-04-17 16:36:19 -07003535
Vikram Aggarwale8a85322012-04-24 09:01:38 -07003536 /**
Vikram Aggarwal177097f2013-03-08 11:19:53 -08003537 * Class to perform {@link LoaderManager.LoaderCallbacks} for creating {@link Folder} objects.
3538 */
3539 private class FolderLoads implements LoaderManager.LoaderCallbacks<ObjectCursor<Folder>> {
3540 @Override
3541 public Loader<ObjectCursor<Folder>> onCreateLoader(int id, Bundle args) {
3542 final String[] everything = UIProvider.FOLDERS_PROJECTION;
3543 switch (id) {
3544 case LOADER_FOLDER_CURSOR:
3545 LogUtils.d(LOG_TAG, "LOADER_FOLDER_CURSOR created");
3546 final ObjectCursorLoader<Folder> loader = new
3547 ObjectCursorLoader<Folder>(
Scott Kennedy259df5b2013-07-11 13:24:01 -07003548 mContext, mFolder.folderUri.fullUri, everything, Folder.FACTORY);
Vikram Aggarwal177097f2013-03-08 11:19:53 -08003549 loader.setUpdateThrottle(mFolderItemUpdateDelayMs);
3550 return loader;
3551 case LOADER_RECENT_FOLDERS:
3552 LogUtils.d(LOG_TAG, "LOADER_RECENT_FOLDERS created");
Yu Ping Huf2632b92013-03-15 13:56:27 -07003553 if (mAccount != null && mAccount.recentFolderListUri != null
3554 && !mAccount.recentFolderListUri.equals(Uri.EMPTY)) {
Vikram Aggarwal177097f2013-03-08 11:19:53 -08003555 return new ObjectCursorLoader<Folder>(mContext,
3556 mAccount.recentFolderListUri, everything, Folder.FACTORY);
3557 }
3558 break;
3559 case LOADER_ACCOUNT_INBOX:
3560 LogUtils.d(LOG_TAG, "LOADER_ACCOUNT_INBOX created");
3561 final Uri defaultInbox = Settings.getDefaultInboxUri(mAccount.settings);
3562 final Uri inboxUri = defaultInbox.equals(Uri.EMPTY) ?
3563 mAccount.folderListUri : defaultInbox;
3564 LogUtils.d(LOG_TAG, "Loading the default inbox: %s", inboxUri);
3565 if (inboxUri != null) {
3566 return new ObjectCursorLoader<Folder>(mContext, inboxUri,
3567 everything, Folder.FACTORY);
3568 }
3569 break;
3570 case LOADER_SEARCH:
3571 LogUtils.d(LOG_TAG, "LOADER_SEARCH created");
3572 return Folder.forSearchResults(mAccount,
3573 args.getString(ConversationListContext.EXTRA_SEARCH_QUERY),
3574 mActivity.getActivityContext());
3575 case LOADER_FIRST_FOLDER:
3576 LogUtils.d(LOG_TAG, "LOADER_FIRST_FOLDER created");
3577 final Uri folderUri = args.getParcelable(Utils.EXTRA_FOLDER_URI);
3578 mConversationToShow = args.getParcelable(Utils.EXTRA_CONVERSATION);
3579 if (mConversationToShow != null && mConversationToShow.position < 0){
3580 mConversationToShow.position = 0;
3581 }
3582 return new ObjectCursorLoader<Folder>(mContext, folderUri,
3583 everything, Folder.FACTORY);
3584 default:
3585 LogUtils.wtf(LOG_TAG, "FolderLoads.onCreateLoader(%d) for invalid id", id);
3586 return null;
3587 }
3588 return null;
3589 }
3590
3591 @Override
3592 public void onLoadFinished(Loader<ObjectCursor<Folder>> loader, ObjectCursor<Folder> data) {
3593 if (data == null) {
3594 LogUtils.e(LOG_TAG, "Received null cursor from loader id: %d", loader.getId());
3595 }
3596 switch (loader.getId()) {
3597 case LOADER_FOLDER_CURSOR:
3598 if (data != null && data.moveToFirst()) {
3599 final Folder folder = data.getModel();
3600 setHasFolderChanged(folder);
3601 mFolder = folder;
3602 mFolderObservable.notifyChanged();
3603 } else {
3604 LogUtils.d(LOG_TAG, "Unable to get the folder %s",
Tony Mantler26a20752014-02-28 16:44:24 -08003605 mFolder != null ? mFolder.name : "");
Vikram Aggarwal177097f2013-03-08 11:19:53 -08003606 }
3607 break;
3608 case LOADER_RECENT_FOLDERS:
3609 // Few recent folders and we are running on a phone? Populate the default
3610 // recents. The number of default recent folders is at least 2: every provider
3611 // has at least two folders, and the recent folder count never decreases.
3612 // Having a single recent folder is an erroneous case, and we can gracefully
3613 // recover by populating default recents. The default recents will not stomp on
3614 // the existing value: it will be shown in addition to the default folders:
3615 // the max number of recent folders is more than 1+num(defaultRecents).
3616 if (data != null && data.getCount() <= 1 && !mIsTablet) {
3617 final class PopulateDefault extends AsyncTask<Uri, Void, Void> {
3618 @Override
3619 protected Void doInBackground(Uri... uri) {
3620 // Asking for an update on the URI and ignore the result.
3621 final ContentResolver resolver = mContext.getContentResolver();
3622 resolver.update(uri[0], null, null, null);
3623 return null;
3624 }
3625 }
3626 final Uri uri = mAccount.defaultRecentFolderListUri;
3627 LogUtils.v(LOG_TAG, "Default recents at %s", uri);
3628 new PopulateDefault().execute(uri);
3629 break;
3630 }
3631 LogUtils.v(LOG_TAG, "Reading recent folders from the cursor.");
3632 mRecentFolderList.loadFromUiProvider(data);
3633 if (isAnimating()) {
3634 mRecentsDataUpdated = true;
3635 } else {
3636 mRecentFolderObservers.notifyChanged();
3637 }
3638 break;
3639 case LOADER_ACCOUNT_INBOX:
3640 if (data != null && !data.isClosed() && data.moveToFirst()) {
3641 final Folder inbox = data.getModel();
Alice Yangebeef1b2013-09-04 06:41:10 +00003642 onFolderChanged(inbox, false /* force */);
Vikram Aggarwal177097f2013-03-08 11:19:53 -08003643 // Just want to get the inbox, don't care about updates to it
3644 // as this will be tracked by the folder change listener.
3645 mActivity.getLoaderManager().destroyLoader(LOADER_ACCOUNT_INBOX);
3646 } else {
3647 LogUtils.d(LOG_TAG, "Unable to get the account inbox for account %s",
Tony Mantler26a20752014-02-28 16:44:24 -08003648 mAccount != null ? mAccount.getEmailAddress() : "");
Vikram Aggarwal177097f2013-03-08 11:19:53 -08003649 }
3650 break;
3651 case LOADER_SEARCH:
3652 if (data != null && data.getCount() > 0) {
3653 data.moveToFirst();
3654 final Folder search = data.getModel();
3655 updateFolder(search);
3656 mConvListContext = ConversationListContext.forSearchQuery(mAccount, mFolder,
3657 mActivity.getIntent()
3658 .getStringExtra(UIProvider.SearchQueryParameters.QUERY));
3659 showConversationList(mConvListContext);
3660 mActivity.invalidateOptionsMenu();
3661 mHaveSearchResults = search.totalCount > 0;
3662 mActivity.getLoaderManager().destroyLoader(LOADER_SEARCH);
3663 } else {
3664 LogUtils.e(LOG_TAG, "Null/empty cursor returned by LOADER_SEARCH loader");
3665 }
3666 break;
3667 case LOADER_FIRST_FOLDER:
3668 if (data == null || data.getCount() <=0 || !data.moveToFirst()) {
3669 return;
3670 }
3671 final Folder folder = data.getModel();
3672 boolean handled = false;
3673 if (folder != null) {
Alice Yangebeef1b2013-09-04 06:41:10 +00003674 onFolderChanged(folder, false /* force */);
Vikram Aggarwal177097f2013-03-08 11:19:53 -08003675 handled = true;
3676 }
3677 if (mConversationToShow != null) {
3678 // Open the conversation.
3679 showConversation(mConversationToShow);
3680 handled = true;
3681 }
3682 if (!handled) {
3683 // We have an account, but nothing else: load the default inbox.
3684 loadAccountInbox();
3685 }
3686 mConversationToShow = null;
3687 // And don't run this anymore.
3688 mActivity.getLoaderManager().destroyLoader(LOADER_FIRST_FOLDER);
3689 break;
3690 }
3691 }
3692
3693 @Override
3694 public void onLoaderReset(Loader<ObjectCursor<Folder>> loader) {
3695 }
3696 }
3697
3698 /**
3699 * Class to perform {@link LoaderManager.LoaderCallbacks} for creating {@link Account} objects.
3700 */
3701 private class AccountLoads implements LoaderManager.LoaderCallbacks<ObjectCursor<Account>> {
3702 final String[] mProjection = UIProvider.ACCOUNTS_PROJECTION;
3703 final CursorCreator<Account> mFactory = Account.FACTORY;
3704
3705 @Override
3706 public Loader<ObjectCursor<Account>> onCreateLoader(int id, Bundle args) {
3707 switch (id) {
3708 case LOADER_ACCOUNT_CURSOR:
3709 LogUtils.d(LOG_TAG, "LOADER_ACCOUNT_CURSOR created");
3710 return new ObjectCursorLoader<Account>(mContext,
3711 MailAppProvider.getAccountsUri(), mProjection, mFactory);
3712 case LOADER_ACCOUNT_UPDATE_CURSOR:
3713 LogUtils.d(LOG_TAG, "LOADER_ACCOUNT_UPDATE_CURSOR created");
3714 return new ObjectCursorLoader<Account>(mContext, mAccount.uri, mProjection,
3715 mFactory);
3716 default:
3717 LogUtils.wtf(LOG_TAG, "Got an id (%d) that I cannot create!", id);
3718 break;
3719 }
3720 return null;
3721 }
3722
3723 @Override
3724 public void onLoadFinished(Loader<ObjectCursor<Account>> loader,
3725 ObjectCursor<Account> data) {
3726 if (data == null) {
3727 LogUtils.e(LOG_TAG, "Received null cursor from loader id: %d", loader.getId());
3728 }
3729 switch (loader.getId()) {
3730 case LOADER_ACCOUNT_CURSOR:
Vikram Aggarwald70fe492013-06-04 12:52:07 -07003731 // We have received an update on the list of accounts.
Vikram Aggarwal177097f2013-03-08 11:19:53 -08003732 if (data == null) {
3733 // Nothing useful to do if we have no valid data.
3734 break;
3735 }
Andy Huang761522c2013-08-08 13:09:11 -07003736 final long count = data.getCount();
3737 if (count == 0) {
Vikram Aggarwal177097f2013-03-08 11:19:53 -08003738 // If an empty cursor is returned, the MailAppProvider is indicating that
3739 // no accounts have been specified. We want to navigate to the
3740 // "add account" activity that will handle the intent returned by the
3741 // MailAppProvider
3742
3743 // If the MailAppProvider believes that all accounts have been loaded,
3744 // and the account list is still empty, we want to prompt the user to add
3745 // an account.
3746 final Bundle extras = data.getExtras();
3747 final boolean accountsLoaded =
3748 extras.getInt(AccountCursorExtraKeys.ACCOUNTS_LOADED) != 0;
3749
3750 if (accountsLoaded) {
3751 final Intent noAccountIntent = MailAppProvider.getNoAccountIntent
3752 (mContext);
3753 if (noAccountIntent != null) {
3754 mActivity.startActivityForResult(noAccountIntent,
3755 ADD_ACCOUNT_REQUEST_CODE);
3756 }
3757 }
3758 } else {
3759 final boolean accountListUpdated = accountsUpdated(data);
Vikram Aggarwalc04cf7e2013-05-13 15:38:42 -07003760 if (!mHaveAccountList || accountListUpdated) {
3761 mHaveAccountList = updateAccounts(data);
Vikram Aggarwal177097f2013-03-08 11:19:53 -08003762 }
Andy Huang761522c2013-08-08 13:09:11 -07003763 Analytics.getInstance().setCustomDimension(Analytics.CD_INDEX_ACCOUNT_COUNT,
3764 Long.toString(count));
Vikram Aggarwal177097f2013-03-08 11:19:53 -08003765 }
3766 break;
3767 case LOADER_ACCOUNT_UPDATE_CURSOR:
3768 // We have received an update for current account.
Vikram Aggarwal177097f2013-03-08 11:19:53 -08003769 if (data != null && data.moveToFirst()) {
3770 final Account updatedAccount = data.getModel();
Vikram Aggarwald70fe492013-06-04 12:52:07 -07003771 // Make sure that this is an update for the current account
Vikram Aggarwal177097f2013-03-08 11:19:53 -08003772 if (updatedAccount.uri.equals(mAccount.uri)) {
Vikram Aggarwal177097f2013-03-08 11:19:53 -08003773 final Settings previousSettings = mAccount.settings;
3774
3775 // Update the controller's reference to the current account
3776 mAccount = updatedAccount;
3777 LogUtils.d(LOG_TAG, "AbstractActivityController.onLoadFinished(): "
3778 + "mAccount = %s", mAccount.uri);
3779
3780 // Only notify about a settings change if something differs
3781 if (!Objects.equal(mAccount.settings, previousSettings)) {
3782 mAccountObservers.notifyChanged();
3783 }
3784 perhapsEnterWaitMode();
3785 } else {
3786 LogUtils.e(LOG_TAG, "Got update for account: %s with current account:"
3787 + " %s", updatedAccount.uri, mAccount.uri);
3788 // We need to restart the loader, so the correct account information
3789 // will be returned.
3790 restartOptionalLoader(LOADER_ACCOUNT_UPDATE_CURSOR, this, Bundle.EMPTY);
3791 }
3792 }
3793 break;
3794 }
3795 }
3796
3797 @Override
3798 public void onLoaderReset(Loader<ObjectCursor<Account>> loader) {
Vikram Aggarwald70fe492013-06-04 12:52:07 -07003799 // Do nothing. In onLoadFinished() we copy the relevant data from the cursor.
Vikram Aggarwal177097f2013-03-08 11:19:53 -08003800 }
3801 }
3802
3803 /**
Vikram Aggarwalf0ef2302012-11-07 14:53:34 -08003804 * Updates controller state based on search results and shows first conversation if required.
3805 */
Vikram Aggarwal715f83d2013-03-05 17:04:56 -08003806 private void perhapsShowFirstSearchResult() {
mindypd27009a2012-11-27 11:54:18 -08003807 if (mCurrentConversation == null) {
3808 // Shown for search results in two-pane mode only.
3809 mHaveSearchResults = Intent.ACTION_SEARCH.equals(mActivity.getIntent().getAction())
3810 && mConversationListCursor.getCount() > 0;
3811 if (!shouldShowFirstConversation()) {
3812 return;
3813 }
3814 mConversationListCursor.moveToPosition(0);
3815 final Conversation conv = new Conversation(mConversationListCursor);
3816 conv.position = 0;
3817 onConversationSelected(conv, true /* checkSafeToModifyFragments */);
Vikram Aggarwalf0ef2302012-11-07 14:53:34 -08003818 }
Vikram Aggarwalf0ef2302012-11-07 14:53:34 -08003819 }
3820
3821 /**
Vikram Aggarwale8a85322012-04-24 09:01:38 -07003822 * Destroy the pending {@link DestructiveAction} till now and assign the given action as the
3823 * next destructive action..
3824 * @param nextAction the next destructive action to be performed. This can be null.
3825 */
Vikram Aggarwal715f83d2013-03-05 17:04:56 -08003826 private void destroyPending(DestructiveAction nextAction) {
Vikram Aggarwale8a85322012-04-24 09:01:38 -07003827 // If there is a pending action, perform that first.
3828 if (mPendingDestruction != null) {
3829 mPendingDestruction.performAction();
3830 }
3831 mPendingDestruction = nextAction;
3832 }
3833
3834 /**
3835 * Register a destructive action with the controller. This performs the previous destructive
Vikram Aggarwalacaa3c02012-04-24 12:45:27 -07003836 * 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 -07003837 * embellish this method any more.
Vikram Aggarwal715f83d2013-03-05 17:04:56 -08003838 * @param action the action to register.
Vikram Aggarwale8a85322012-04-24 09:01:38 -07003839 */
Vikram Aggarwal715f83d2013-03-05 17:04:56 -08003840 private void registerDestructiveAction(DestructiveAction action) {
Vikram Aggarwale8a85322012-04-24 09:01:38 -07003841 // TODO(viki): This is not a good idea. The best solution is for clients to request a
3842 // destructive action from the controller and for the controller to own the action. This is
3843 // a half-way solution while refactoring DestructiveAction.
3844 destroyPending(action);
Vikram Aggarwale8a85322012-04-24 09:01:38 -07003845 }
3846
Vikram Aggarwal531488e2012-05-29 16:36:52 -07003847 @Override
Jin Cao30c881a2014-04-08 14:28:36 -07003848 public final DestructiveAction getBatchAction(int action, UndoCallback undoCallback) {
Vikram Aggarwalc4113952012-05-11 14:14:56 -07003849 final DestructiveAction da = new ConversationAction(action, mSelectedSet.values(), true);
Jin Cao30c881a2014-04-08 14:28:36 -07003850 da.setUndoCallback(undoCallback);
Vikram Aggarwale8a85322012-04-24 09:01:38 -07003851 registerDestructiveAction(da);
3852 return da;
3853 }
Vikram Aggarwal41e6e712012-04-24 11:22:57 -07003854
Mindy Pereirade3e74a2012-07-24 09:43:10 -07003855 @Override
Jin Cao30c881a2014-04-08 14:28:36 -07003856 public final DestructiveAction getDeferredBatchAction(int action, UndoCallback undoCallback) {
3857 return getDeferredAction(action, mSelectedSet.values(), true, undoCallback);
mindypf0656a12012-10-01 08:30:57 -07003858 }
3859
3860 /**
3861 * Get a destructive action for a menu action. This is a temporary method,
3862 * to control the profusion of {@link DestructiveAction} classes that are
3863 * created. Please do not copy this paradigm.
3864 * @param action the resource ID of the menu action: R.id.delete, for
3865 * example
3866 * @param target the conversations to act upon.
3867 * @return a {@link DestructiveAction} that performs the specified action.
3868 */
Vikram Aggarwal715f83d2013-03-05 17:04:56 -08003869 private DestructiveAction getDeferredAction(int action, Collection<Conversation> target,
Jin Cao30c881a2014-04-08 14:28:36 -07003870 boolean batch, UndoCallback callback) {
3871 ConversationAction cAction = new ConversationAction(action, target, batch);
3872 cAction.setUndoCallback(callback);
3873 return cAction;
Mindy Pereirade3e74a2012-07-24 09:43:10 -07003874 }
3875
Vikram Aggarwalbc67bb12012-04-30 14:05:35 -07003876 /**
3877 * Class to change the folders that are assigned to a set of conversations. This is destructive
3878 * because the user can remove the current folder from the conversation, in which case it has
3879 * to be animated away from the current folder.
3880 */
Vikram Aggarwal41e6e712012-04-24 11:22:57 -07003881 private class FolderDestruction implements DestructiveAction {
Paul Westbrook77eee622012-07-10 13:41:57 -07003882 private final Collection<Conversation> mTarget;
Mindy Pereira8db7e402012-07-13 10:32:47 -07003883 private final ArrayList<FolderOperation> mFolderOps = new ArrayList<FolderOperation>();
Vikram Aggarwal41e6e712012-04-24 11:22:57 -07003884 private final boolean mIsDestructive;
Vikram Aggarwal7f602f72012-04-30 16:04:06 -07003885 /** Whether this destructive action has already been performed */
3886 private boolean mCompleted;
Martin Hibdone78c40f2013-10-10 18:29:25 -07003887 private final boolean mIsSelectedSet;
3888 private final boolean mShowUndo;
3889 private final int mAction;
Scott Kennedy6a3d5ce2013-03-15 17:33:14 -07003890 private final Folder mActionFolder;
Vikram Aggarwal41e6e712012-04-24 11:22:57 -07003891
Jin Cao30c881a2014-04-08 14:28:36 -07003892 private UndoCallback mUndoCallback;
3893
Vikram Aggarwal41e6e712012-04-24 11:22:57 -07003894 /**
3895 * Create a new folder destruction object to act on the given conversations.
Vikram Aggarwal715f83d2013-03-05 17:04:56 -08003896 * @param target conversations to act upon.
Scott Kennedy6a3d5ce2013-03-15 17:33:14 -07003897 * @param actionFolder the {@link Folder} being acted upon, used for displaying the undo bar
Vikram Aggarwal41e6e712012-04-24 11:22:57 -07003898 */
Vikram Aggarwal7f602f72012-04-30 16:04:06 -07003899 private FolderDestruction(final Collection<Conversation> target,
Mindy Pereira8db7e402012-07-13 10:32:47 -07003900 final Collection<FolderOperation> folders, boolean isDestructive, boolean isBatch,
Scott Kennedy6a3d5ce2013-03-15 17:33:14 -07003901 boolean showUndo, int action, final Folder actionFolder) {
Paul Westbrook77eee622012-07-10 13:41:57 -07003902 mTarget = ImmutableList.copyOf(target);
Mindy Pereira8db7e402012-07-13 10:32:47 -07003903 mFolderOps.addAll(folders);
Vikram Aggarwal41e6e712012-04-24 11:22:57 -07003904 mIsDestructive = isDestructive;
Mindy Pereiraf3a45562012-05-24 16:30:19 -07003905 mIsSelectedSet = isBatch;
Mindy Pereira06642fa2012-07-12 16:23:27 -07003906 mShowUndo = showUndo;
Mindy Pereira01f30502012-08-14 10:30:51 -07003907 mAction = action;
Scott Kennedy6a3d5ce2013-03-15 17:33:14 -07003908 mActionFolder = actionFolder;
Vikram Aggarwal41e6e712012-04-24 11:22:57 -07003909 }
3910
Vikram Aggarwal41e6e712012-04-24 11:22:57 -07003911 @Override
Jin Cao30c881a2014-04-08 14:28:36 -07003912 public void setUndoCallback(UndoCallback undoCallback) {
3913 mUndoCallback = undoCallback;
3914 }
3915
3916 @Override
Vikram Aggarwal41e6e712012-04-24 11:22:57 -07003917 public void performAction() {
Vikram Aggarwal7f602f72012-04-30 16:04:06 -07003918 if (isPerformed()) {
3919 return;
3920 }
Mindy Pereira06642fa2012-07-12 16:23:27 -07003921 if (mIsDestructive && mShowUndo) {
mindypcb0b30e2012-11-30 10:16:35 -08003922 ToastBarOperation undoOp = new ToastBarOperation(mTarget.size(), mAction,
Scott Kennedy6a3d5ce2013-03-15 17:33:14 -07003923 ToastBarOperation.UNDO, mIsSelectedSet, mActionFolder);
Vikram Aggarwal41e6e712012-04-24 11:22:57 -07003924 onUndoAvailable(undoOp);
3925 }
Mindy Pereira8db7e402012-07-13 10:32:47 -07003926 // For each conversation, for each operation, add/ remove the
3927 // appropriate folders.
mindypcb0b30e2012-11-30 10:16:35 -08003928 ArrayList<ConversationOperation> ops = new ArrayList<ConversationOperation>();
3929 ArrayList<Uri> folderUris;
3930 ArrayList<Boolean> adds;
Mindy Pereira8db7e402012-07-13 10:32:47 -07003931 for (Conversation target : mTarget) {
mindypcb0b30e2012-11-30 10:16:35 -08003932 HashMap<Uri, Folder> targetFolders = Folder.hashMapForFolders(target
3933 .getRawFolders());
3934 folderUris = new ArrayList<Uri>();
mindypcb0b30e2012-11-30 10:16:35 -08003935 adds = new ArrayList<Boolean>();
Mindy Pereira01f30502012-08-14 10:30:51 -07003936 if (mIsDestructive) {
3937 target.localDeleteOnUpdate = true;
3938 }
Mindy Pereira8db7e402012-07-13 10:32:47 -07003939 for (FolderOperation op : mFolderOps) {
Scott Kennedy259df5b2013-07-11 13:24:01 -07003940 folderUris.add(op.mFolder.folderUri.fullUri);
mindypcb0b30e2012-11-30 10:16:35 -08003941 adds.add(op.mAdd ? Boolean.TRUE : Boolean.FALSE);
Mindy Pereira8db7e402012-07-13 10:32:47 -07003942 if (op.mAdd) {
Scott Kennedy259df5b2013-07-11 13:24:01 -07003943 targetFolders.put(op.mFolder.folderUri.fullUri, op.mFolder);
Mindy Pereira8db7e402012-07-13 10:32:47 -07003944 } else {
Scott Kennedy259df5b2013-07-11 13:24:01 -07003945 targetFolders.remove(op.mFolder.folderUri.fullUri);
Mindy Pereira8db7e402012-07-13 10:32:47 -07003946 }
3947 }
Paul Westbrook26746eb2012-12-06 14:44:01 -08003948 ops.add(mConversationListCursor.getConversationFolderOperation(target,
Jin Cao30c881a2014-04-08 14:28:36 -07003949 folderUris, adds, targetFolders.values(), mUndoCallback));
mindyp389f0b22012-08-29 11:12:54 -07003950 }
3951 if (mConversationListCursor != null) {
Scott Kennedy9e2d4072013-03-21 21:46:01 -07003952 mConversationListCursor.updateBulkValues(ops);
Mindy Pereira8db7e402012-07-13 10:32:47 -07003953 }
Vikram Aggarwal7f602f72012-04-30 16:04:06 -07003954 refreshConversationList();
Mindy Pereiraf3a45562012-05-24 16:30:19 -07003955 if (mIsSelectedSet) {
3956 mSelectedSet.clear();
3957 }
Vikram Aggarwal7f602f72012-04-30 16:04:06 -07003958 }
Mindy Pereira8db7e402012-07-13 10:32:47 -07003959
Vikram Aggarwal7f602f72012-04-30 16:04:06 -07003960 /**
3961 * Returns true if this action has been performed, false otherwise.
Andy Huang839ada22012-07-20 15:48:40 -07003962 *
Vikram Aggarwal7f602f72012-04-30 16:04:06 -07003963 */
3964 private synchronized boolean isPerformed() {
3965 if (mCompleted) {
3966 return true;
Vikram Aggarwal41e6e712012-04-24 11:22:57 -07003967 }
Vikram Aggarwal7f602f72012-04-30 16:04:06 -07003968 mCompleted = true;
3969 return false;
Vikram Aggarwal41e6e712012-04-24 11:22:57 -07003970 }
3971 }
Vikram Aggarwal7f602f72012-04-30 16:04:06 -07003972
mindypc84759c2012-08-29 09:51:53 -07003973 public final DestructiveAction getFolderChange(Collection<Conversation> target,
3974 Collection<FolderOperation> folders, boolean isDestructive, boolean isBatch,
Jin Cao30c881a2014-04-08 14:28:36 -07003975 boolean showUndo, final boolean isMoveTo, final Folder actionFolder,
3976 UndoCallback undoCallback) {
mindypc84759c2012-08-29 09:51:53 -07003977 final DestructiveAction da = getDeferredFolderChange(target, folders, isDestructive,
Jin Cao30c881a2014-04-08 14:28:36 -07003978 isBatch, showUndo, isMoveTo, actionFolder, undoCallback);
mindypc84759c2012-08-29 09:51:53 -07003979 registerDestructiveAction(da);
3980 return da;
3981 }
3982
3983 public final DestructiveAction getDeferredFolderChange(Collection<Conversation> target,
Mindy Pereira8db7e402012-07-13 10:32:47 -07003984 Collection<FolderOperation> folders, boolean isDestructive, boolean isBatch,
Jin Cao30c881a2014-04-08 14:28:36 -07003985 boolean showUndo, final boolean isMoveTo, final Folder actionFolder,
3986 UndoCallback undoCallback) {
3987 final DestructiveAction fd = new FolderDestruction(target, folders, isDestructive, isBatch,
3988 showUndo, isMoveTo ? R.id.move_folder : R.id.change_folders, actionFolder);
3989 fd.setUndoCallback(undoCallback);
3990 return fd;
Mindy Pereira01f30502012-08-14 10:30:51 -07003991 }
3992
3993 @Override
3994 public final DestructiveAction getDeferredRemoveFolder(Collection<Conversation> target,
3995 Folder toRemove, boolean isDestructive, boolean isBatch,
Jin Cao30c881a2014-04-08 14:28:36 -07003996 boolean showUndo, UndoCallback undoCallback) {
Mindy Pereira01f30502012-08-14 10:30:51 -07003997 Collection<FolderOperation> folderOps = new ArrayList<FolderOperation>();
3998 folderOps.add(new FolderOperation(toRemove, false));
Jin Cao30c881a2014-04-08 14:28:36 -07003999 final DestructiveAction da = new FolderDestruction(target, folderOps, isDestructive, isBatch,
Scott Kennedy6a3d5ce2013-03-15 17:33:14 -07004000 showUndo, R.id.remove_folder, mFolder);
Jin Cao30c881a2014-04-08 14:28:36 -07004001 da.setUndoCallback(undoCallback);
4002 return da;
Mindy Pereira01f30502012-08-14 10:30:51 -07004003 }
4004
Vikram Aggarwal4f4782b2012-05-30 08:39:09 -07004005 @Override
4006 public final void refreshConversationList() {
Vikram Aggarwal75daee52012-04-30 11:13:09 -07004007 final ConversationListFragment convList = getConversationListFragment();
4008 if (convList == null) {
4009 return;
4010 }
4011 convList.requestListRefresh();
4012 }
Andrew Sappersteinc2c9dc12012-07-02 18:17:32 -07004013
4014 protected final ActionClickedListener getUndoClickedListener(
4015 final AnimatedAdapter listAdapter) {
4016 return new ActionClickedListener() {
4017 @Override
Andrew Sappersteinf8ccdcf2013-08-08 19:30:38 -07004018 public void onActionClicked(Context context) {
Andrew Sappersteinc2c9dc12012-07-02 18:17:32 -07004019 if (mAccount.undoUri != null) {
4020 // NOTE: We might want undo to return the messages affected, in which case
4021 // the resulting cursor might be interesting...
4022 // TODO: Use UIProvider.SEQUENCE_QUERY_PARAMETER to indicate the set of
4023 // commands to undo
4024 if (mConversationListCursor != null) {
4025 mConversationListCursor.undo(
4026 mActivity.getActivityContext(), mAccount.undoUri);
4027 }
4028 if (listAdapter != null) {
4029 listAdapter.setUndo(true);
4030 }
4031 }
4032 }
4033 };
4034 }
4035
Vikram Aggarwal41b9e8f2012-09-25 10:15:04 -07004036 /**
4037 * Shows an error toast in the bottom when a folder was not fetched successfully.
4038 * @param folder the folder which could not be fetched.
4039 * @param replaceVisibleToast if true, this should replace any currently visible toast.
4040 */
Andrew Sapperstein9d7519d2012-07-16 14:03:53 -07004041 protected final void showErrorToast(final Folder folder, boolean replaceVisibleToast) {
Andrew Sapperstein00179f12012-08-09 15:15:40 -07004042
Vikram Aggarwal41b9e8f2012-09-25 10:15:04 -07004043 final ActionClickedListener listener;
4044 final int actionTextResourceId;
Andrew Sapperstein00179f12012-08-09 15:15:40 -07004045 final int lastSyncResult = folder.lastSyncResult;
Vikram Aggarwal41b9e8f2012-09-25 10:15:04 -07004046 switch (lastSyncResult & 0x0f) {
Andrew Sapperstein00179f12012-08-09 15:15:40 -07004047 case UIProvider.LastSyncResult.CONNECTION_ERROR:
Vikram Aggarwal41b9e8f2012-09-25 10:15:04 -07004048 // The sync request that caused this failure.
4049 final int syncRequest = lastSyncResult >> 4;
4050 // Show: User explicitly pressed the refresh button and there is no connection
4051 // Show: The first time the user enters the app and there is no connection
4052 // TODO(viki): Implement this.
4053 // Reference: http://b/7202801
4054 final boolean showToast = (syncRequest & UIProvider.SyncStatus.USER_REFRESH) != 0;
4055 // Don't show: Already in the app; user switches to a synced label
4056 // Don't show: In a live label and a background sync fails
4057 final boolean avoidToast = !showToast && (folder.syncWindow > 0
4058 || (syncRequest & UIProvider.SyncStatus.BACKGROUND_SYNC) != 0);
4059 if (avoidToast) {
4060 return;
4061 }
Andrew Sapperstein00179f12012-08-09 15:15:40 -07004062 listener = getRetryClickedListener(folder);
4063 actionTextResourceId = R.string.retry;
4064 break;
4065 case UIProvider.LastSyncResult.AUTH_ERROR:
4066 listener = getSignInClickedListener();
4067 actionTextResourceId = R.string.signin;
4068 break;
4069 case UIProvider.LastSyncResult.SECURITY_ERROR:
4070 return; // Currently we do nothing for security errors.
4071 case UIProvider.LastSyncResult.STORAGE_ERROR:
4072 listener = getStorageErrorClickedListener();
4073 actionTextResourceId = R.string.info;
4074 break;
4075 case UIProvider.LastSyncResult.INTERNAL_ERROR:
4076 listener = getInternalErrorClickedListener();
4077 actionTextResourceId = R.string.report;
4078 break;
4079 default:
4080 return;
4081 }
Vikram Aggarwal41b9e8f2012-09-25 10:15:04 -07004082 mToastBar.show(listener,
Andrew Sapperstein5d420962012-07-12 16:43:10 -07004083 R.drawable.ic_alert_white,
Vikram Aggarwal41b9e8f2012-09-25 10:15:04 -07004084 Utils.getSyncStatusText(mActivity.getActivityContext(), lastSyncResult),
Andrew Sappersteinc2c9dc12012-07-02 18:17:32 -07004085 false, /* showActionIcon */
Andrew Sapperstein00179f12012-08-09 15:15:40 -07004086 actionTextResourceId,
Andrew Sapperstein9d7519d2012-07-16 14:03:53 -07004087 replaceVisibleToast,
Scott Kennedy6a3d5ce2013-03-15 17:33:14 -07004088 new ToastBarOperation(1, 0, ToastBarOperation.ERROR, false, folder));
Andrew Sappersteinc2c9dc12012-07-02 18:17:32 -07004089 }
4090
Andrew Sapperstein00179f12012-08-09 15:15:40 -07004091 private ActionClickedListener getRetryClickedListener(final Folder folder) {
Andrew Sappersteinc2c9dc12012-07-02 18:17:32 -07004092 return new ActionClickedListener() {
4093 @Override
Andrew Sappersteinf8ccdcf2013-08-08 19:30:38 -07004094 public void onActionClicked(Context context) {
Andrew Sappersteinc2c9dc12012-07-02 18:17:32 -07004095 final Uri uri = folder.refreshUri;
4096
4097 if (uri != null) {
Paul Westbrook4969e0c2012-08-20 14:38:39 -07004098 startAsyncRefreshTask(uri);
Andrew Sappersteinc2c9dc12012-07-02 18:17:32 -07004099 }
4100 }
4101 };
4102 }
Andrew Sapperstein00179f12012-08-09 15:15:40 -07004103
4104 private ActionClickedListener getSignInClickedListener() {
4105 return new ActionClickedListener() {
4106 @Override
Andrew Sappersteinf8ccdcf2013-08-08 19:30:38 -07004107 public void onActionClicked(Context context) {
Paul Westbrook122f7c22012-08-20 17:50:31 -07004108 promptUserForAuthentication(mAccount);
Andrew Sapperstein00179f12012-08-09 15:15:40 -07004109 }
4110 };
4111 }
4112
4113 private ActionClickedListener getStorageErrorClickedListener() {
4114 return new ActionClickedListener() {
4115 @Override
Andrew Sappersteinf8ccdcf2013-08-08 19:30:38 -07004116 public void onActionClicked(Context context) {
Paul Westbrook4969e0c2012-08-20 14:38:39 -07004117 showStorageErrorDialog();
Andrew Sapperstein00179f12012-08-09 15:15:40 -07004118 }
4119 };
4120 }
4121
Paul Westbrook4969e0c2012-08-20 14:38:39 -07004122 private void showStorageErrorDialog() {
4123 DialogFragment fragment = (DialogFragment)
4124 mFragmentManager.findFragmentByTag(SYNC_ERROR_DIALOG_FRAGMENT_TAG);
4125 if (fragment == null) {
4126 fragment = SyncErrorDialogFragment.newInstance();
4127 }
4128 fragment.show(mFragmentManager, SYNC_ERROR_DIALOG_FRAGMENT_TAG);
4129 }
4130
Andrew Sapperstein00179f12012-08-09 15:15:40 -07004131 private ActionClickedListener getInternalErrorClickedListener() {
4132 return new ActionClickedListener() {
4133 @Override
Andrew Sappersteinf8ccdcf2013-08-08 19:30:38 -07004134 public void onActionClicked(Context context) {
Paul Westbrook83e6b572013-02-05 16:22:42 -08004135 Utils.sendFeedback(mActivity, mAccount, true /* reportingProblem */);
Andrew Sapperstein00179f12012-08-09 15:15:40 -07004136 }
4137 };
4138 }
Paul Westbrook4969e0c2012-08-20 14:38:39 -07004139
4140 @Override
4141 public void onFooterViewErrorActionClick(Folder folder, int errorStatus) {
4142 Uri uri = null;
4143 switch (errorStatus) {
4144 case UIProvider.LastSyncResult.CONNECTION_ERROR:
4145 if (folder != null && folder.refreshUri != null) {
4146 uri = folder.refreshUri;
4147 }
4148 break;
4149 case UIProvider.LastSyncResult.AUTH_ERROR:
Paul Westbrook122f7c22012-08-20 17:50:31 -07004150 promptUserForAuthentication(mAccount);
Paul Westbrook4969e0c2012-08-20 14:38:39 -07004151 return;
4152 case UIProvider.LastSyncResult.SECURITY_ERROR:
4153 return; // Currently we do nothing for security errors.
4154 case UIProvider.LastSyncResult.STORAGE_ERROR:
4155 showStorageErrorDialog();
4156 return;
4157 case UIProvider.LastSyncResult.INTERNAL_ERROR:
Paul Westbrook83e6b572013-02-05 16:22:42 -08004158 Utils.sendFeedback(mActivity, mAccount, true /* reportingProblem */);
Paul Westbrook4969e0c2012-08-20 14:38:39 -07004159 return;
4160 default:
4161 return;
4162 }
4163
4164 if (uri != null) {
4165 startAsyncRefreshTask(uri);
4166 }
4167 }
4168
4169 @Override
4170 public void onFooterViewLoadMoreClick(Folder folder) {
4171 if (folder != null && folder.loadMoreUri != null) {
4172 startAsyncRefreshTask(folder.loadMoreUri);
4173 }
4174 }
4175
4176 private void startAsyncRefreshTask(Uri uri) {
4177 if (mFolderSyncTask != null) {
4178 mFolderSyncTask.cancel(true);
4179 }
4180 mFolderSyncTask = new AsyncRefreshTask(mActivity.getActivityContext(), uri);
4181 mFolderSyncTask.execute();
4182 }
Paul Westbrook122f7c22012-08-20 17:50:31 -07004183
4184 private void promptUserForAuthentication(Account account) {
Paul Westbrook429399b2012-08-24 11:19:17 -07004185 if (account != null && !Utils.isEmpty(account.reauthenticationIntentUri)) {
Paul Westbrook122f7c22012-08-20 17:50:31 -07004186 final Intent authenticationIntent =
4187 new Intent(Intent.ACTION_VIEW, account.reauthenticationIntentUri);
4188 mActivity.startActivityForResult(authenticationIntent, REAUTHENTICATE_REQUEST_CODE);
4189 }
4190 }
mindypca87de42012-09-28 15:02:39 -07004191
4192 @Override
4193 public void onAccessibilityStateChanged() {
4194 // Clear the cache of objects.
4195 ConversationItemViewModel.onAccessibilityUpdated();
4196 // Re-render the list if it exists.
Vikram Aggarwala91d00b2013-01-18 12:00:37 -08004197 final ConversationListFragment frag = getConversationListFragment();
mindypca87de42012-09-28 15:02:39 -07004198 if (frag != null) {
4199 AnimatedAdapter adapter = frag.getAnimatedAdapter();
4200 if (adapter != null) {
4201 adapter.notifyDataSetInvalidated();
4202 }
4203 }
4204 }
Vikram Aggarwal6cadbfc2012-12-27 09:17:05 -08004205
4206 @Override
Jin Cao30c881a2014-04-08 14:28:36 -07004207 public void makeDialogListener (final int action, final boolean isBatch,
4208 UndoCallback undoCallback) {
Vikram Aggarwalb8c31712013-01-03 17:03:19 -08004209 final Collection<Conversation> target;
Vikram Aggarwalb8c31712013-01-03 17:03:19 -08004210 if (isBatch) {
4211 target = mSelectedSet.values();
Vikram Aggarwalb8c31712013-01-03 17:03:19 -08004212 } else {
4213 LogUtils.d(LOG_TAG, "Will act upon %s", mCurrentConversation);
4214 target = Conversation.listOf(mCurrentConversation);
Vikram Aggarwalb8c31712013-01-03 17:03:19 -08004215 }
Jin Cao30c881a2014-04-08 14:28:36 -07004216 final DestructiveAction destructiveAction = getDeferredAction(action, target, isBatch,
4217 undoCallback);
Vikram Aggarwal6cadbfc2012-12-27 09:17:05 -08004218 mDialogAction = action;
Vikram Aggarwalb8c31712013-01-03 17:03:19 -08004219 mDialogFromSelectedSet = isBatch;
Vikram Aggarwal6cadbfc2012-12-27 09:17:05 -08004220 mDialogListener = new AlertDialog.OnClickListener() {
4221 @Override
4222 public void onClick(DialogInterface dialog, int which) {
Scott Kennedycaaeed32013-06-12 13:39:16 -07004223 delete(action, target, destructiveAction, isBatch);
Vikram Aggarwal6cadbfc2012-12-27 09:17:05 -08004224 // Afterwards, let's remove references to the listener and the action.
4225 setListener(null, -1);
4226 }
4227 };
4228 }
4229
4230 @Override
4231 public AlertDialog.OnClickListener getListener() {
4232 return mDialogListener;
4233 }
4234
4235 /**
4236 * Sets the listener for the positive action on a confirmation dialog. Since only a single
4237 * confirmation dialog can be shown, this overwrites the previous listener. It is safe to
4238 * unset the listener; in which case action should be set to -1.
Vikram Aggarwal715f83d2013-03-05 17:04:56 -08004239 * @param listener the listener that will perform the task for this dialog's positive action.
4240 * @param action the action that created this dialog.
Vikram Aggarwal6cadbfc2012-12-27 09:17:05 -08004241 */
4242 private void setListener(AlertDialog.OnClickListener listener, final int action){
4243 mDialogListener = listener;
4244 mDialogAction = action;
4245 }
Vikram Aggarwal69a6cdf2013-01-08 16:05:17 -08004246
4247 @Override
4248 public VeiledAddressMatcher getVeiledAddressMatcher() {
4249 return mVeiledMatcher;
Vikram Aggarwala91d00b2013-01-18 12:00:37 -08004250 }
4251
4252 @Override
4253 public void setDetachedMode() {
4254 // Tell the conversation list not to select anything.
4255 final ConversationListFragment frag = getConversationListFragment();
4256 if (frag != null) {
4257 frag.setChoiceNone();
Vikram Aggarwal81230ee2013-02-26 15:26:44 -08004258 } else if (mIsTablet) {
4259 // How did we ever land here? Detached mode, and no CLF on tablet???
Vikram Aggarwala91d00b2013-01-18 12:00:37 -08004260 LogUtils.e(LOG_TAG, "AAC.setDetachedMode(): CLF = null!");
4261 }
4262 mDetachedConvUri = mCurrentConversation.uri;
4263 }
4264
4265 private void clearDetachedMode() {
4266 // Tell the conversation list to go back to its usual selection behavior.
4267 final ConversationListFragment frag = getConversationListFragment();
4268 if (frag != null) {
4269 frag.revertChoiceMode();
Vikram Aggarwal81230ee2013-02-26 15:26:44 -08004270 } else if (mIsTablet) {
4271 // How did we ever land here? Detached mode, and no CLF on tablet???
4272 LogUtils.e(LOG_TAG, "AAC.clearDetachedMode(): CLF = null on tablet!");
Vikram Aggarwala91d00b2013-01-18 12:00:37 -08004273 }
4274 mDetachedConvUri = null;
4275 }
Andy Huang12b3ee42013-04-24 22:49:43 -07004276
Andy Huang61f26c22014-03-13 18:24:52 -07004277 @Override
4278 public DrawerController getDrawerController() {
4279 return mDrawerListener;
4280 }
4281
4282 private class MailDrawerListener extends Observable<DrawerLayout.DrawerListener>
4283 implements DrawerLayout.DrawerListener, DrawerController {
Andrew Sappersteina3ce6782013-05-09 15:14:49 -07004284 private int mDrawerState;
Andrew Sappersteinfc5be1c2013-05-15 17:48:10 -07004285 private float mOldSlideOffset;
Andrew Sappersteina3ce6782013-05-09 15:14:49 -07004286
4287 public MailDrawerListener() {
4288 mDrawerState = DrawerLayout.STATE_IDLE;
Andrew Sappersteinfc5be1c2013-05-15 17:48:10 -07004289 mOldSlideOffset = 0.f;
Andrew Sappersteina3ce6782013-05-09 15:14:49 -07004290 }
4291
Andy Huang12b3ee42013-04-24 22:49:43 -07004292 @Override
Andy Huang61f26c22014-03-13 18:24:52 -07004293 public void registerDrawerListener(DrawerLayout.DrawerListener l) {
4294 registerObserver(l);
4295 }
4296
4297 @Override
4298 public void unregisterDrawerListener(DrawerLayout.DrawerListener l) {
4299 unregisterObserver(l);
4300 }
4301
4302 @Override
4303 public boolean isDrawerOpen() {
4304 return isDrawerEnabled() && mDrawerContainer.isDrawerOpen(mDrawerPullout);
4305 }
4306
4307 @Override
4308 public boolean isDrawerVisible() {
4309 return isDrawerEnabled() && mDrawerContainer.isDrawerVisible(mDrawerPullout);
4310 }
4311
4312 @Override
Andy Huang12b3ee42013-04-24 22:49:43 -07004313 public void onDrawerOpened(View drawerView) {
4314 mDrawerToggle.onDrawerOpened(drawerView);
Andy Huang61f26c22014-03-13 18:24:52 -07004315
4316 for (DrawerLayout.DrawerListener l : mObservers) {
4317 l.onDrawerOpened(drawerView);
4318 }
Andy Huang12b3ee42013-04-24 22:49:43 -07004319 }
4320
4321 @Override
4322 public void onDrawerClosed(View drawerView) {
4323 mDrawerToggle.onDrawerClosed(drawerView);
4324 if (mHasNewAccountOrFolder) {
4325 refreshDrawer();
4326 }
Scott Kennedy10c99ef2013-08-29 13:44:17 -07004327
4328 // When closed, we want to use either the burger, or up, based on where we are
4329 final int mode = mViewMode.getMode();
4330 final boolean isTopLevel = (mFolder == null) || (mFolder.parent == Uri.EMPTY);
4331 mDrawerToggle.setDrawerIndicatorEnabled(getShouldShowDrawerIndicator(mode, isTopLevel));
Andy Huang61f26c22014-03-13 18:24:52 -07004332
4333 for (DrawerLayout.DrawerListener l : mObservers) {
4334 l.onDrawerClosed(drawerView);
4335 }
Andy Huang12b3ee42013-04-24 22:49:43 -07004336 }
4337
4338 /**
4339 * As part of the overriden function, it will animate the alpha of the conversation list
4340 * view along with the drawer sliding when we're in the process of switching accounts or
4341 * folders. Note, this is the same amount of work done as {@link ValueAnimator#ofFloat}.
4342 */
4343 @Override
4344 public void onDrawerSlide(View drawerView, float slideOffset) {
4345 mDrawerToggle.onDrawerSlide(drawerView, slideOffset);
4346 if (mHasNewAccountOrFolder && mListViewForAnimating != null) {
4347 mListViewForAnimating.setAlpha(slideOffset);
4348 }
Andrew Sappersteinfc5be1c2013-05-15 17:48:10 -07004349
4350 // This code handles when to change the visibility of action items
4351 // based on drawer state. The basic logic is that right when we
4352 // open the drawer, we hide the action items. We show the action items
4353 // when the drawer closes. However, due to the animation of the drawer closing,
4354 // to make the reshowing of the action items feel right, we make the items visible
4355 // slightly sooner.
4356 //
4357 // However, to make the animating behavior work properly, we have to know whether
4358 // we're animating open or closed. Only if we're animating closed do we want to
4359 // show the action items early. We save the last slide offset so that we can compare
4360 // the current slide offset to it to determine if we're opening or closing.
4361 if (mDrawerState == DrawerLayout.STATE_SETTLING) {
4362 if (mHideMenuItems && slideOffset < 0.15f && mOldSlideOffset > slideOffset) {
4363 mHideMenuItems = false;
4364 mActivity.invalidateOptionsMenu();
Tony Mantler43fab322013-07-26 16:38:35 -07004365 maybeEnableCabMode();
Andrew Sappersteinfc5be1c2013-05-15 17:48:10 -07004366 } else if (!mHideMenuItems && slideOffset > 0.f && mOldSlideOffset < slideOffset) {
4367 mHideMenuItems = true;
4368 mActivity.invalidateOptionsMenu();
Tony Mantler43fab322013-07-26 16:38:35 -07004369 disableCabMode();
Andrew Sappersteinfc5be1c2013-05-15 17:48:10 -07004370 }
4371 } else {
4372 if (mHideMenuItems && Float.compare(slideOffset, 0.f) == 0) {
4373 mHideMenuItems = false;
4374 mActivity.invalidateOptionsMenu();
Tony Mantler43fab322013-07-26 16:38:35 -07004375 maybeEnableCabMode();
Andrew Sappersteinfc5be1c2013-05-15 17:48:10 -07004376 } else if (!mHideMenuItems && slideOffset > 0.f) {
4377 mHideMenuItems = true;
4378 mActivity.invalidateOptionsMenu();
Tony Mantler43fab322013-07-26 16:38:35 -07004379 disableCabMode();
Andrew Sappersteinfc5be1c2013-05-15 17:48:10 -07004380 }
4381 }
4382
4383 mOldSlideOffset = slideOffset;
Scott Kennedy10c99ef2013-08-29 13:44:17 -07004384
4385 // If we're sliding, we always want to show the burger
4386 mDrawerToggle.setDrawerIndicatorEnabled(true /* enable */);
Andy Huang61f26c22014-03-13 18:24:52 -07004387
4388 for (DrawerLayout.DrawerListener l : mObservers) {
4389 l.onDrawerSlide(drawerView, slideOffset);
4390 }
Andy Huang12b3ee42013-04-24 22:49:43 -07004391 }
4392
4393 /**
4394 * This condition here should only be called when the drawer is stuck in a weird state
4395 * and doesn't register the onDrawerClosed, but shows up as idle. Make sure to refresh
4396 * and, more importantly, unlock the drawer when this is the case.
4397 */
4398 @Override
4399 public void onDrawerStateChanged(int newState) {
Martin Hibdon371a71c2014-02-19 13:55:28 -08004400 LogUtils.d(LOG_TAG, "AAC onDrawerStateChanged %d", newState);
Andrew Sappersteina3ce6782013-05-09 15:14:49 -07004401 mDrawerState = newState;
4402 mDrawerToggle.onDrawerStateChanged(mDrawerState);
Andy Huang61f26c22014-03-13 18:24:52 -07004403
4404 for (DrawerLayout.DrawerListener l : mObservers) {
4405 l.onDrawerStateChanged(newState);
4406 }
4407
Andy Huange764cfd2014-02-26 11:55:03 -08004408 if (mViewMode.isSearchMode()) {
Martin Hibdon371a71c2014-02-19 13:55:28 -08004409 return;
4410 }
Andrew Sappersteina3ce6782013-05-09 15:14:49 -07004411 if (mDrawerState == DrawerLayout.STATE_IDLE) {
4412 if (mHasNewAccountOrFolder) {
4413 refreshDrawer();
4414 }
4415 if (mConversationListLoadFinishedIgnored) {
4416 mConversationListLoadFinishedIgnored = false;
4417 final Bundle args = new Bundle();
4418 args.putParcelable(BUNDLE_ACCOUNT_KEY, mAccount);
4419 args.putParcelable(BUNDLE_FOLDER_KEY, mFolder);
4420 mActivity.getLoaderManager().initLoader(
4421 LOADER_CONVERSATION_LIST, args, mListCursorCallbacks);
4422 }
Andy Huang12b3ee42013-04-24 22:49:43 -07004423 }
4424 }
4425
4426 /**
4427 * If we've reached a stable drawer state, unlock the drawer for usage, clear the
4428 * conversation list, and finish end actions. Also, make
4429 * {@link #mHasNewAccountOrFolder} false to reflect we're done changing.
4430 */
4431 public void refreshDrawer() {
4432 mHasNewAccountOrFolder = false;
4433 mDrawerContainer.setDrawerLockMode(DrawerLayout.LOCK_MODE_UNLOCKED);
4434 ConversationListFragment conversationList = getConversationListFragment();
4435 if (conversationList != null) {
4436 conversationList.clear();
4437 }
4438 mDrawerObservers.notifyChanged();
4439 }
Andrew Sappersteina3ce6782013-05-09 15:14:49 -07004440
4441 /**
4442 * Returns the most recent update of the {@link DrawerLayout}'s state provided
4443 * by {@link #onDrawerStateChanged(int)}.
4444 * @return The {@link DrawerLayout}'s current state. One of
4445 * {@link DrawerLayout#STATE_DRAGGING}, {@link DrawerLayout#STATE_IDLE},
4446 * or {@link DrawerLayout#STATE_SETTLING}.
4447 */
4448 public int getDrawerState() {
4449 return mDrawerState;
4450 }
Andy Huang12b3ee42013-04-24 22:49:43 -07004451 }
4452
Scott Kennedy8a72b852013-05-02 14:18:50 -07004453 @Override
4454 public boolean isDrawerPullEnabled() {
Martin Hibdon371a71c2014-02-19 13:55:28 -08004455 return true;
Scott Kennedy8a72b852013-05-02 14:18:50 -07004456 }
Andrew Sapperstein5747e152013-05-13 14:13:08 -07004457
4458 @Override
4459 public boolean shouldHideMenuItems() {
4460 return mHideMenuItems;
4461 }
Alice Yangebeef1b2013-09-04 06:41:10 +00004462
4463 protected void navigateUpFolderHierarchy() {
4464 new AsyncTask<Void, Void, Folder>() {
4465 @Override
4466 protected Folder doInBackground(final Void... params) {
4467 if (mInbox == null) {
4468 // We don't have an inbox, but we need it
4469 final Cursor cursor = mContext.getContentResolver().query(
4470 mAccount.settings.defaultInbox, UIProvider.FOLDERS_PROJECTION, null,
4471 null, null);
4472
4473 if (cursor != null) {
4474 try {
4475 if (cursor.moveToFirst()) {
4476 mInbox = new Folder(cursor);
4477 }
4478 } finally {
4479 cursor.close();
4480 }
4481 }
4482 }
4483
4484 // Now try to load our parent
4485 final Folder folder;
4486
Alice Yang5ac10032013-09-04 06:41:43 +00004487 if (mFolder != null) {
Martin Hibdone78c40f2013-10-10 18:29:25 -07004488 Cursor cursor = null;
4489 try {
4490 cursor = mContext.getContentResolver().query(mFolder.parent,
4491 UIProvider.FOLDERS_PROJECTION, null, null, null);
Alice Yangebeef1b2013-09-04 06:41:10 +00004492
Martin Hibdone78c40f2013-10-10 18:29:25 -07004493 if (cursor == null || !cursor.moveToFirst()) {
4494 // We couldn't load the parent, so use the inbox
4495 folder = mInbox;
4496 } else {
Alice Yang5ac10032013-09-04 06:41:43 +00004497 folder = new Folder(cursor);
Martin Hibdone78c40f2013-10-10 18:29:25 -07004498 }
4499 } finally {
4500 if (cursor != null) {
Alice Yang5ac10032013-09-04 06:41:43 +00004501 cursor.close();
4502 }
Alice Yangebeef1b2013-09-04 06:41:10 +00004503 }
Alice Yang5ac10032013-09-04 06:41:43 +00004504 } else {
4505 folder = mInbox;
Alice Yangebeef1b2013-09-04 06:41:10 +00004506 }
4507
4508 return folder;
4509 }
4510
4511 @Override
4512 protected void onPostExecute(final Folder result) {
4513 onFolderSelected(result);
4514 }
4515 }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, (Void[]) null);
4516 }
Scott Kennedyf77806e2013-08-30 11:38:15 -07004517
4518 @Override
4519 public Parcelable getConversationListScrollPosition(final String folderUri) {
4520 return mConversationListScrollPositions.getParcelable(folderUri);
4521 }
4522
4523 @Override
4524 public void setConversationListScrollPosition(final String folderUri,
4525 final Parcelable savedPosition) {
4526 mConversationListScrollPositions.putParcelable(folderUri, savedPosition);
4527 }
Vikram Aggarwal4a5c5302012-01-12 15:07:13 -08004528}