blob: b1498988618ea9ccd1536095ff579af9e1853b03 [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 Aggarwala55b36c2012-01-13 11:45:02 -080021import android.app.Activity;
Vikram Aggarwal54452ae2012-03-13 15:29:00 -070022import android.app.AlertDialog;
Vikram Aggarwala55b36c2012-01-13 11:45:02 -080023import android.app.Dialog;
Andrew Sapperstein00179f12012-08-09 15:15:40 -070024import android.app.DialogFragment;
Vikram Aggarwal6902dcf2012-04-11 08:57:42 -070025import android.app.Fragment;
Paul Westbrook2d50bcd2012-04-10 11:53:47 -070026import android.app.FragmentManager;
Andy Huangf9a73482012-03-13 15:54:02 -070027import android.app.LoaderManager;
Vikram Aggarwale620a7a2012-03-28 13:16:14 -070028import android.app.SearchManager;
Andy Huang839ada22012-07-20 15:48:40 -070029import android.content.ContentProviderOperation;
Vikram Aggarwal80aeac52012-02-07 15:27:20 -080030import android.content.ContentResolver;
Mindy Pereira6c2663d2012-07-20 15:37:29 -070031import android.content.ContentValues;
Vikram Aggarwala55b36c2012-01-13 11:45:02 -080032import android.content.Context;
Vikram Aggarwal54452ae2012-03-13 15:29:00 -070033import android.content.DialogInterface;
Vikram Aggarwal6cadbfc2012-12-27 09:17:05 -080034import android.content.DialogInterface.OnClickListener;
Vikram Aggarwala55b36c2012-01-13 11:45:02 -080035import android.content.Intent;
Vikram Aggarwal7dedb952012-02-16 16:10:23 -080036import android.content.Loader;
Paul Westbrook57246a42013-04-21 09:40:22 -070037import android.content.res.Configuration;
Vikram Aggarwalbcb16b92013-01-28 18:05:03 -080038import android.content.res.Resources;
Alice Yangebeef1b2013-09-04 06:41:10 +000039import android.database.Cursor;
Andy Huang632721e2012-04-11 16:57:26 -070040import android.database.DataSetObservable;
41import android.database.DataSetObserver;
Andy Huang61f26c22014-03-13 18:24:52 -070042import android.database.Observable;
Paul Westbrook23b74b92012-02-29 11:36:12 -080043import android.net.Uri;
Vikram Aggarwal27d89ad2012-06-12 13:38:40 -070044import android.os.AsyncTask;
Vikram Aggarwala55b36c2012-01-13 11:45:02 -080045import android.os.Bundle;
Mindy Pereira21ab4902012-03-19 18:48:03 -070046import android.os.Handler;
Scott Kennedyf77806e2013-08-30 11:38:15 -070047import android.os.Parcelable;
Jin Cao1a864cc2014-05-21 11:16:39 -070048import android.os.SystemClock;
Jin Caoc6801eb2014-08-12 18:16:57 -070049import android.speech.RecognizerIntent;
Andy Huang12b3ee42013-04-24 22:49:43 -070050import android.support.v4.app.ActionBarDrawerToggle;
51import android.support.v4.widget.DrawerLayout;
Andrew Sapperstein52882ff2014-07-27 12:30:18 -070052import android.support.v7.app.ActionBar;
Andy Huang12b3ee42013-04-24 22:49:43 -070053import android.view.Gravity;
Vikram Aggarwala55b36c2012-01-13 11:45:02 -080054import android.view.KeyEvent;
55import android.view.Menu;
Mindy Pereira28d5f722012-02-15 12:32:40 -080056import android.view.MenuInflater;
Vikram Aggarwala55b36c2012-01-13 11:45:02 -080057import android.view.MenuItem;
58import android.view.MotionEvent;
Mindy Pereirad33674992012-06-25 16:26:30 -070059import android.view.View;
Andy Huang12b3ee42013-04-24 22:49:43 -070060import android.widget.ListView;
Mindy Pereirafd0c2972012-03-27 13:50:39 -070061import android.widget.Toast;
Vikram Aggarwala55b36c2012-01-13 11:45:02 -080062
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -080063import com.android.mail.ConversationListContext;
Vikram Aggarwal59f741f2013-03-01 15:55:40 -080064import com.android.mail.MailLogService;
Andy Huangf9a73482012-03-13 15:54:02 -070065import com.android.mail.R;
Andy Huang761522c2013-08-08 13:09:11 -070066import com.android.mail.analytics.Analytics;
Jin Cao779dd602014-04-22 16:16:28 -070067import com.android.mail.analytics.AnalyticsTimer;
Vikram Aggarwal6cadbfc2012-12-27 09:17:05 -080068import com.android.mail.browse.ConfirmDialogFragment;
Mindy Pereira967ede62012-03-22 09:29:09 -070069import com.android.mail.browse.ConversationCursor;
Yu Ping Hu7c909c72013-01-18 11:58:01 -080070import com.android.mail.browse.ConversationCursor.ConversationOperation;
mindypca87de42012-09-28 15:02:39 -070071import com.android.mail.browse.ConversationItemViewModel;
Andrew Sapperstein8812d3c2013-06-04 17:06:41 -070072import com.android.mail.browse.ConversationMessage;
Paul Westbrookbf232c32012-04-18 03:17:41 -070073import com.android.mail.browse.ConversationPagerController;
Marc Blank7c9f6ac2012-04-02 13:27:19 -070074import com.android.mail.browse.SelectedConversationsActionMenu;
Andy Huang991f4532012-08-14 13:32:55 -070075import com.android.mail.browse.SyncErrorDialogFragment;
Jin Cao30c881a2014-04-08 14:28:36 -070076import com.android.mail.browse.UndoCallback;
Mindy Pereira9b875682012-02-15 18:10:54 -080077import com.android.mail.compose.ComposeActivity;
Vikram Aggarwal177097f2013-03-08 11:19:53 -080078import com.android.mail.content.CursorCreator;
79import com.android.mail.content.ObjectCursor;
80import com.android.mail.content.ObjectCursorLoader;
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -080081import com.android.mail.providers.Account;
Mindy Pereira9b875682012-02-15 18:10:54 -080082import com.android.mail.providers.Conversation;
Andy Huang839ada22012-07-20 15:48:40 -070083import com.android.mail.providers.ConversationInfo;
Vikram Aggarwal4a5c5302012-01-12 15:07:13 -080084import com.android.mail.providers.Folder;
Vikram Aggarwal7a5d95a2012-07-27 16:24:54 -070085import com.android.mail.providers.FolderWatcher;
Paul Westbrookc2074c42012-03-22 15:26:58 -070086import com.android.mail.providers.MailAppProvider;
Mindy Pereiradac00542012-03-01 10:50:33 -080087import com.android.mail.providers.Settings;
Vikram Aggarwal80aeac52012-02-07 15:27:20 -080088import com.android.mail.providers.UIProvider;
Mindy Pereira3cb28b52012-05-24 15:26:39 -070089import com.android.mail.providers.UIProvider.AccountCapabilities;
Paul Westbrook2388c5d2012-03-25 12:29:11 -070090import com.android.mail.providers.UIProvider.AccountCursorExtraKeys;
Scott Kennedy0d0f8b02012-10-12 15:18:18 -070091import com.android.mail.providers.UIProvider.AutoAdvance;
Mindy Pereirac9d59182012-03-22 16:06:46 -070092import com.android.mail.providers.UIProvider.ConversationColumns;
Paul Westbrook5109c512012-11-05 11:00:30 -080093import com.android.mail.providers.UIProvider.ConversationOperations;
Vikram Aggarwal54452ae2012-03-13 15:29:00 -070094import com.android.mail.providers.UIProvider.FolderCapabilities;
Scott Kennedya158ac82013-09-04 13:48:13 -070095import com.android.mail.providers.UIProvider.FolderType;
Andrew Sappersteinc2c9dc12012-07-02 18:17:32 -070096import com.android.mail.ui.ActionableToastBar.ActionClickedListener;
Andy Huang839ada22012-07-20 15:48:40 -070097import com.android.mail.utils.ContentProviderTask;
Andy Huang144bfe72013-06-11 13:27:52 -070098import com.android.mail.utils.DrawIdler;
Paul Westbrookb334c902012-06-25 11:42:46 -070099import com.android.mail.utils.LogTag;
Mindy Pereira5cb0c3e2012-02-22 15:22:47 -0800100import com.android.mail.utils.LogUtils;
Andy Huang61f26c22014-03-13 18:24:52 -0700101import com.android.mail.utils.MailObservable;
Scott Kennedycb85aea2013-02-25 13:08:32 -0800102import com.android.mail.utils.NotificationActionUtils;
Vikram Aggarwalfa131a22012-02-02 13:56:22 -0800103import com.android.mail.utils.Utils;
Vikram Aggarwal69a6cdf2013-01-08 16:05:17 -0800104import com.android.mail.utils.VeiledAddressMatcher;
Paul Westbrookca08fc12012-07-31 12:01:15 -0700105import com.google.common.base.Objects;
Paul Westbrook77eee622012-07-10 13:41:57 -0700106import com.google.common.collect.ImmutableList;
Mindy Pereiraacf60392012-04-06 09:11:00 -0700107import com.google.common.collect.Lists;
Marc Blank167faa82012-03-21 13:11:53 -0700108import com.google.common.collect.Sets;
Vikram Aggarwal4a5c5302012-01-12 15:07:13 -0800109
Marc Blank167faa82012-03-21 13:11:53 -0700110import java.util.ArrayList;
Andy Huang9e4ca792013-02-28 14:33:43 -0800111import java.util.Arrays;
Mindy Pereirafbe40192012-03-20 10:40:45 -0700112import java.util.Collection;
Andy Huang839ada22012-07-20 15:48:40 -0700113import java.util.Collections;
Mindy Pereira8db7e402012-07-13 10:32:47 -0700114import java.util.HashMap;
Vikram Aggarwalcc754b12012-08-30 14:04:21 -0700115import java.util.List;
Paul Westbrook23b74b92012-02-29 11:36:12 -0800116import java.util.Set;
Marc Blankbf128eb2012-04-18 15:58:45 -0700117import java.util.TimerTask;
Paul Westbrook23b74b92012-02-29 11:36:12 -0800118
119
Vikram Aggarwal4a5c5302012-01-12 15:07:13 -0800120/**
Mindy Pereira161f50d2012-02-28 15:47:19 -0800121 * This is an abstract implementation of the Activity Controller. This class
122 * knows how to respond to menu items, state changes, layout changes, etc. It
123 * weaves together the views and listeners, dispatching actions to the
124 * respective underlying classes.
Vikram Aggarwala55b36c2012-01-13 11:45:02 -0800125 * <p>
Mindy Pereira161f50d2012-02-28 15:47:19 -0800126 * Even though this class is abstract, it should provide default implementations
127 * for most, if not all the methods in the ActivityController interface. This
128 * makes the task of the subclasses easier: OnePaneActivityController and
129 * TwoPaneActivityController can be concise when the common functionality is in
130 * AbstractActivityController.
131 * </p>
132 * <p>
133 * In the Gmail codebase, this was called BaseActivityController
134 * </p>
Vikram Aggarwal4a5c5302012-01-12 15:07:13 -0800135 */
Andrew Sappersteined5b52d2013-04-30 13:40:18 -0700136public abstract class AbstractActivityController implements ActivityController,
James Lemieux5d79a912014-07-16 14:22:26 -0700137 EmptyFolderDialogFragment.EmptyFolderDialogFragmentListener, View.OnClickListener {
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -0800138 // Keys for serialization of various information in Bundles.
Vikram Aggarwalcabd3f22012-04-19 10:14:41 -0700139 /** Tag for {@link #mAccount} */
Vikram Aggarwal6c511582012-02-27 10:59:47 -0800140 private static final String SAVED_ACCOUNT = "saved-account";
Vikram Aggarwalcabd3f22012-04-19 10:14:41 -0700141 /** Tag for {@link #mFolder} */
Mindy Pereira5e478d22012-03-26 18:04:58 -0700142 private static final String SAVED_FOLDER = "saved-folder";
Vikram Aggarwalcabd3f22012-04-19 10:14:41 -0700143 /** Tag for {@link #mCurrentConversation} */
Mindy Pereira26f23fc2012-03-27 10:26:04 -0700144 private static final String SAVED_CONVERSATION = "saved-conversation";
Jin Caoec0fa482014-08-28 16:38:08 -0700145 /** Tag for {@link #mCheckedSet} */
Vikram Aggarwalcabd3f22012-04-19 10:14:41 -0700146 private static final String SAVED_SELECTED_SET = "saved-selected-set";
Vikram Aggarwal649b9ea2012-08-27 12:15:20 -0700147 /** Tag for {@link ActionableToastBar#getOperation()} */
Mindy Pereirad33674992012-06-25 16:26:30 -0700148 private static final String SAVED_TOAST_BAR_OP = "saved-toast-bar-op";
Vikram Aggarwal649b9ea2012-08-27 12:15:20 -0700149 /** Tag for {@link #mFolderListFolder} */
150 private static final String SAVED_HIERARCHICAL_FOLDER = "saved-hierarchical-folder";
Vikram Aggarwalf3341402012-08-07 10:09:38 -0700151 /** Tag for {@link ConversationListContext#searchQuery} */
152 private static final String SAVED_QUERY = "saved-query";
Vikram Aggarwal6cadbfc2012-12-27 09:17:05 -0800153 /** Tag for {@link #mDialogAction} */
154 private static final String SAVED_ACTION = "saved-action";
Vikram Aggarwalb8c31712013-01-03 17:03:19 -0800155 /** Tag for {@link #mDialogFromSelectedSet} */
156 private static final String SAVED_ACTION_FROM_SELECTED = "saved-action-from-selected";
Vikram Aggarwala91d00b2013-01-18 12:00:37 -0800157 /** Tag for {@link #mDetachedConvUri} */
158 private static final String SAVED_DETACHED_CONV_URI = "saved-detached-conv-uri";
Alice Yangebeef1b2013-09-04 06:41:10 +0000159 /** Key to store {@link #mInbox}. */
Scott Kennedyf77806e2013-08-30 11:38:15 -0700160 private static final String SAVED_INBOX_KEY = "m-inbox";
161 /** Key to store {@link #mConversationListScrollPositions} */
162 private static final String SAVED_CONVERSATION_LIST_SCROLL_POSITIONS =
163 "saved-conversation-list-scroll-positions";
Mindy Pereira5cb0c3e2012-02-22 15:22:47 -0800164
Greg Bullockede2e522014-05-30 14:11:35 +0200165 /** Tag used when loading a wait fragment */
Vikram Aggarwal6902dcf2012-04-11 08:57:42 -0700166 protected static final String TAG_WAIT = "wait-fragment";
167 /** Tag used when loading a conversation list fragment. */
Paul Westbrookbf232c32012-04-18 03:17:41 -0700168 public static final String TAG_CONVERSATION_LIST = "tag-conversation-list";
Scott Kennedy103319a2013-07-26 13:35:35 -0700169 /** Tag used when loading a custom fragment. */
170 protected static final String TAG_CUSTOM_FRAGMENT = "tag-custom-fragment";
Vikram Aggarwal6902dcf2012-04-11 08:57:42 -0700171
Vikram Aggarwala1b59dc2013-04-30 15:45:49 -0700172 /** Key to store an account in a bundle */
173 private final String BUNDLE_ACCOUNT_KEY = "account";
174 /** Key to store a folder in a bundle */
175 private final String BUNDLE_FOLDER_KEY = "folder";
Andrew Sapperstein5bb4d052014-03-31 16:22:31 -0700176 /**
177 * Key to set a flag for the ConversationCursorLoader to ignore any
178 * initial load limit that may be set by the Account. Instead,
179 * perform a full load instead of the full-stage load.
180 */
181 private final String BUNDLE_IGNORE_INITIAL_CONVERSATION_LIMIT_KEY =
182 "ignore-initial-conversation-limit";
Vikram Aggarwala1b59dc2013-04-30 15:45:49 -0700183
Vikram Aggarwald7a12cd2012-02-03 09:36:20 -0800184 protected Account mAccount;
Mindy Pereira12a4d802012-06-22 09:24:43 -0700185 protected Folder mFolder;
Alice Yangebeef1b2013-09-04 06:41:10 +0000186 protected Folder mInbox;
Vikram Aggarwal69b5c302012-09-05 11:11:13 -0700187 /** True when {@link #mFolder} is first shown to the user. */
188 private boolean mFolderChanged = false;
Andrew Sapperstein2d86d112014-07-24 20:27:37 -0700189 protected ActionBarController mActionBarController;
James Lemieux10fcd642014-03-03 13:01:04 -0800190 protected final MailActivity mActivity;
Vikram Aggarwala55b36c2012-01-13 11:45:02 -0800191 protected final Context mContext;
Vikram Aggarwal6902dcf2012-04-11 08:57:42 -0700192 private final FragmentManager mFragmentManager;
Vikram Aggarwalec5cbf72012-03-08 15:10:35 -0800193 protected final RecentFolderList mRecentFolderList;
Vikram Aggarwal80aeac52012-02-07 15:27:20 -0800194 protected ConversationListContext mConvListContext;
Mindy Pereira9b875682012-02-15 18:10:54 -0800195 protected Conversation mCurrentConversation;
Jin Caoc6801eb2014-08-12 18:16:57 -0700196 protected MaterialSearchViewController mSearchViewController;
Vikram Aggarwala91d00b2013-01-18 12:00:37 -0800197 /**
198 * The hash of {@link #mCurrentConversation} in detached mode. 0 if we are not in detached mode.
199 */
200 private Uri mDetachedConvUri;
Vikram Aggarwald7a12cd2012-02-03 09:36:20 -0800201
Scott Kennedyf77806e2013-08-30 11:38:15 -0700202 /** A map of {@link Folder} {@link Uri} to scroll position in the conversation list. */
203 private final Bundle mConversationListScrollPositions = new Bundle();
204
Paul Westbrook6ead20d2012-03-19 14:48:14 -0700205 /** A {@link android.content.BroadcastReceiver} that suppresses new e-mail notifications. */
206 private SuppressNotificationReceiver mNewEmailReceiver = null;
207
Vikram Aggarwal59f741f2013-03-01 15:55:40 -0800208 /** Handler for all our local runnables. */
Mindy Pereirafbe40192012-03-20 10:40:45 -0700209 protected Handler mHandler = new Handler();
Mindy Pereirafa995b42012-07-25 12:06:13 -0700210
Vikram Aggarwalfa131a22012-02-02 13:56:22 -0800211 /**
Mindy Pereira161f50d2012-02-28 15:47:19 -0800212 * The current mode of the application. All changes in mode are initiated by
213 * the activity controller. View mode changes are propagated to classes that
214 * attach themselves as listeners of view mode changes.
Vikram Aggarwalfa131a22012-02-02 13:56:22 -0800215 */
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -0800216 protected final ViewMode mViewMode;
Vikram Aggarwal80aeac52012-02-07 15:27:20 -0800217 protected ContentResolver mResolver;
Vikram Aggarwalc04cf7e2013-05-13 15:38:42 -0700218 protected boolean mHaveAccountList = false;
Mindy Pereirab7b33e02012-02-21 15:32:19 -0800219 private AsyncRefreshTask mAsyncRefreshTask;
Mindy Pereira5cb0c3e2012-02-22 15:22:47 -0800220
Andy Huang4e0158f2012-08-07 21:06:01 -0700221 private boolean mDestroyed;
222
Vikram Aggarwalbcb16b92013-01-28 18:05:03 -0800223 /** True if running on tablet */
224 private final boolean mIsTablet;
225
Andy Huang1ee96b22012-08-24 20:19:53 -0700226 /**
227 * Are we in a point in the Activity/Fragment lifecycle where it's safe to execute fragment
228 * transactions? (including back stack manipulation)
229 * <p>
230 * Per docs in {@link FragmentManager#beginTransaction()}, this flag starts out true, switches
231 * to false after {@link Activity#onSaveInstanceState}, and becomes true again in both onStart
232 * and onResume.
233 */
234 private boolean mSafeToModifyFragments = true;
235
Paul Westbrook23b74b92012-02-29 11:36:12 -0800236 private final Set<Uri> mCurrentAccountUris = Sets.newHashSet();
Mindy Pereira967ede62012-03-22 09:29:09 -0700237 protected ConversationCursor mConversationListCursor;
Andy Huang61f26c22014-03-13 18:24:52 -0700238 private final DataSetObservable mConversationListObservable = new MailObservable("List");
Marc Blankbf128eb2012-04-18 15:58:45 -0700239
Vikram Aggarwal59f741f2013-03-01 15:55:40 -0800240 /** Runnable that checks the logging level to enable/disable the logging service. */
241 private Runnable mLogServiceChecker = null;
Vikram Aggarwalde60c9d2013-04-10 12:58:56 -0700242 /** List of all accounts currently known to the controller. This is never null. */
243 private Account[] mAllAccounts = new Account[0];
Vikram Aggarwal59f741f2013-03-01 15:55:40 -0800244
Vikram Aggarwal77ee0ce2013-04-28 15:32:36 -0700245 private FolderWatcher mFolderWatcher;
246
Andrew Sapperstein5bb4d052014-03-31 16:22:31 -0700247 private boolean mIgnoreInitialConversationLimit;
248
Yu Ping Hu7c909c72013-01-18 11:58:01 -0800249 /**
250 * Interface for actions that are deferred until after a load completes. This is for handling
251 * user actions which affect cursors (e.g. marking messages read or unread) that happen before
252 * that cursor is loaded.
253 */
254 private interface LoadFinishedCallback {
255 void onLoadFinished();
256 }
257
258 /** The deferred actions to execute when mConversationListCursor load completes. */
259 private final ArrayList<LoadFinishedCallback> mConversationListLoadFinishedCallbacks =
260 new ArrayList<LoadFinishedCallback>();
261
Marc Blankbf128eb2012-04-18 15:58:45 -0700262 private RefreshTimerTask mConversationListRefreshTask;
Marc Blanke1d1b072012-04-13 17:29:16 -0700263
Vikram Aggarwal7c401b72012-08-13 16:43:47 -0700264 /** Listeners that are interested in changes to the current account. */
Andy Huang61f26c22014-03-13 18:24:52 -0700265 private final DataSetObservable mAccountObservers = new MailObservable("Account");
Vikram Aggarwal58cad2e2012-08-28 16:18:23 -0700266 /** Listeners that are interested in changes to the recent folders. */
Andy Huang61f26c22014-03-13 18:24:52 -0700267 private final DataSetObservable mRecentFolderObservers = new MailObservable("RecentFolder");
Vikram Aggarwal07dbaa62013-03-12 15:21:04 -0700268 /** Listeners that are interested in changes to the list of all accounts. */
Andy Huang61f26c22014-03-13 18:24:52 -0700269 private final DataSetObservable mAllAccountObservers = new MailObservable("AllAccounts");
Vikram Aggarwal50ff0e52013-03-14 13:58:02 -0700270 /** Listeners that are interested in changes to the current folder. */
Andy Huang61f26c22014-03-13 18:24:52 -0700271 private final DataSetObservable mFolderObservable = new MailObservable("CurrentFolder");
Tony Mantler54022ee2014-07-07 13:43:35 -0700272 /** Listeners that are interested in changes to the Folder or Account selection */
273 private final DataSetObservable mFolderOrAccountObservers =
274 new MailObservable("FolderOrAccount");
Vikram Aggarwal58cad2e2012-08-28 16:18:23 -0700275
Mindy Pereira967ede62012-03-22 09:29:09 -0700276 /**
277 * Selected conversations, if any.
278 */
Jin Caoec0fa482014-08-28 16:38:08 -0700279 private final ConversationCheckedSet mCheckedSet = new ConversationCheckedSet();
Mindy Pereiradac00542012-03-01 10:50:33 -0800280
Paul Westbrookc7a070f2012-04-12 01:46:41 -0700281 private final int mFolderItemUpdateDelayMs;
282
Vikram Aggarwal135fd022012-04-11 14:44:15 -0700283 /** Keeps track of selected and unselected conversations */
Paul Westbrook937c94f2012-08-16 13:01:18 -0700284 final protected ConversationPositionTracker mTracker;
Vikram Aggarwalaf1ee0c2012-04-12 17:13:13 -0700285
Vikram Aggarwale128fc22012-04-04 12:33:34 -0700286 /**
287 * Action menu associated with the selected set.
288 */
289 SelectedConversationsActionMenu mCabActionMenu;
James Lemieux1a1e9752014-07-18 13:50:40 -0700290
291 /** The compose button floating over the conversation/search lists */
292 protected View mFloatingComposeButton;
Andrew Sappersteinc2c9dc12012-07-02 18:17:32 -0700293 protected ActionableToastBar mToastBar;
Andy Huang632721e2012-04-11 16:57:26 -0700294 protected ConversationPagerController mPagerController;
Mindy Pereiraab486362012-03-21 18:18:53 -0700295
Vikram Aggarwald70fe492013-06-04 12:52:07 -0700296 // This is split out from the general loader dispatcher because its loader doesn't return a
Andy Huangb1c34dc2012-04-17 16:36:19 -0700297 // basic Cursor
Vikram Aggarwald70fe492013-06-04 12:52:07 -0700298 /** Handles loader callbacks to create a convesation cursor. */
Andy Huangb1c34dc2012-04-17 16:36:19 -0700299 private final ConversationListLoaderCallbacks mListCursorCallbacks =
300 new ConversationListLoaderCallbacks();
301
Vikram Aggarwal177097f2013-03-08 11:19:53 -0800302 /** Object that listens to all LoaderCallbacks that result in {@link Folder} creation. */
303 private final FolderLoads mFolderCallbacks = new FolderLoads();
304 /** Object that listens to all LoaderCallbacks that result in {@link Account} creation. */
305 private final AccountLoads mAccountCallbacks = new AccountLoads();
306
Vikram Aggarwal69a6cdf2013-01-08 16:05:17 -0800307 /**
308 * Matched addresses that must be shielded from users because they are temporary. Even though
309 * this is instantiated from settings, this matcher is valid for all accounts, and is expected
310 * to live past the life of an account.
311 */
312 private final VeiledAddressMatcher mVeiledMatcher;
313
Paul Westbrookb334c902012-06-25 11:42:46 -0700314 protected static final String LOG_TAG = LogTag.getLogTag();
Vikram Aggarwald70fe492013-06-04 12:52:07 -0700315
Vikram Aggarwalaa941d72013-06-04 15:34:28 -0700316 // Loader constants: Accounts
317 /**
318 * The list of accounts. This loader is started early in the application life-cycle since
319 * the list of accounts is central to all other data the application needs: unread counts for
320 * folders, critical UI settings like show/hide checkboxes, ...
321 * The loader is started when the application is created: both in
322 * {@link #onCreate(Bundle)} and in {@link #onActivityResult(int, int, Intent)}. It is never
323 * destroyed since the cursor is needed through the life of the application. When the list of
324 * accounts changes, we notify {@link #mAllAccountObservers}.
325 */
Vikram Aggarwalfa4b47e2012-03-09 13:02:46 -0800326 private static final int LOADER_ACCOUNT_CURSOR = 0;
Vikram Aggarwalaa941d72013-06-04 15:34:28 -0700327
328 /**
329 * The current account. This loader is started when we have an account. The mail application
330 * <b>needs</b> a valid account to function. As soon as we set {@link #mAccount},
331 * we start a loader to observe for changes on the current account.
332 * The loader is always restarted when an account is set in {@link #setAccount(Account)}.
333 * When the current account object changes, we notify {@link #mAccountObservers}.
334 * A possible performance improvement would be to listen purely on
335 * {@link #LOADER_ACCOUNT_CURSOR}. The current account is guaranteed to be in the list,
336 * and would avoid two updates when a single setting on the current account changes.
337 */
Régis Décamps004b46f2014-06-23 15:13:23 +0200338 private static final int LOADER_ACCOUNT_UPDATE_CURSOR = 1;
339
340 // Loader constants: Conversations
341
342 /** The conversation cursor over the current conversation list. This loader provides
343 * a cursor over conversation entries from a folder to display a conversation
344 * list.
345 * This loader is started when the user switches folders (in {@link #updateFolder(Folder)},
346 * or when the controller is told that a folder/account change is imminent
347 * (in {@link #preloadConvList(Account, Folder)}. The loader is maintained for the life of
348 * the current folder. When the user switches folders, the old loader is destroyed and a new
349 * one is created.
350 *
351 * When the conversation list changes, we notify {@link #mConversationListObservable}.
352 */
353 private static final int LOADER_CONVERSATION_LIST = 10;
354
355 // Loader constants: misc
356 /**
357 * The loader that determines whether the Warm welcome tour should be displayed for the user.
358 */
359 public static final int LOADER_WELCOME_TOUR = 20;
Vikram Aggarwald70fe492013-06-04 12:52:07 -0700360
Greg Bullock944ef3b2014-07-17 12:15:07 +0200361 /**
362 * The load which loads accounts for the welcome tour.
363 */
364 public static final int LOADER_WELCOME_TOUR_ACCOUNTS = 21;
365
Vikram Aggarwalaa941d72013-06-04 15:34:28 -0700366 // Loader constants: Folders
Régis Décamps004b46f2014-06-23 15:13:23 +0200367
Vikram Aggarwalaa941d72013-06-04 15:34:28 -0700368 /** The current folder. This loader watches for updates to the current folder in a manner
369 * analogous to the {@link #LOADER_ACCOUNT_UPDATE_CURSOR}. Updates to the current folder
370 * might be due to server-side changes (unread count), or local changes (sync window or sync
371 * status change).
372 * The change of current folder calls {@link #updateFolder(Folder)}.
373 * This is responsible for restarting a loader using the URI of the provided folder. When the
374 * loader returns, the current folder is updated and consumers, if any, are notified.
375 * When the current folder changes, we notify {@link #mFolderObservable}
376 */
Régis Décamps004b46f2014-06-23 15:13:23 +0200377 private static final int LOADER_FOLDER_CURSOR = 30;
378
Vikram Aggarwalaa941d72013-06-04 15:34:28 -0700379 /**
380 * The list of recent folders. Recent folders are shown in the DrawerFragment. The recent
381 * folders are tied to the current account being viewed. When the account is changed,
382 * we restart this loader to retrieve the recent accounts. Recents are pre-populated for
383 * phones historically, when they were displayed in the spinner. On the tablet,
384 * they showed in the {@link FolderListFragment} and were not-populated. The code to
385 * pre-populate the recents is somewhat convoluted: when the loader returns a short list of
386 * recent folders, it issues an update on the Recent Folder URI. The underlying provider then
387 * does the appropriate thing to populate recent folders, and notify of a change on the cursor.
388 * Recent folders are needed for the life of the current account.
389 * When the recent folders change, we notify {@link #mRecentFolderObservers}.
390 */
Régis Décamps004b46f2014-06-23 15:13:23 +0200391 private static final int LOADER_RECENT_FOLDERS = 31;
Vikram Aggarwalaa941d72013-06-04 15:34:28 -0700392 /**
393 * The primary inbox for the current account. The mechanism to load the default inbox for the
394 * current account is (sadly) different from loading other folders. The method
395 * {@link #loadAccountInbox()} is called, and it restarts this loader. When the loader returns
396 * a valid cursor, we create a folder, call {@link #onFolderChanged{Folder)} eventually
397 * calling {@link #updateFolder(Folder)} which starts a loader {@link #LOADER_FOLDER_CURSOR}
398 * over the current folder.
399 * When we have a valid cursor, we destroy this loader, This convoluted flow is historical.
400 */
Régis Décamps004b46f2014-06-23 15:13:23 +0200401 private static final int LOADER_ACCOUNT_INBOX = 32;
402
Vikram Aggarwalaa941d72013-06-04 15:34:28 -0700403 /**
404 * The fake folder of search results for a term. When we search for a term,
405 * a new activity is created with {@link Intent#ACTION_SEARCH}. For this new activity,
406 * we start a loader which returns conversations that match the user-provided query.
407 * We destroy the loader when we obtain a valid cursor since subsequent searches will create
408 * a new activity.
409 */
Régis Décamps004b46f2014-06-23 15:13:23 +0200410 private static final int LOADER_SEARCH = 33;
Vikram Aggarwalaa941d72013-06-04 15:34:28 -0700411 /**
412 * The initial folder at app start. When the application is launched from an intent that
413 * specifies the initial folder (notifications/widgets/shortcuts),
414 * then we extract the folder URI from the intent, but we cannot trust the folder object. Since
415 * shortcuts and widgets persist past application update, they might have incorrect
416 * information encoded in them. So, to obtain a {@link Folder} object from a {@link Uri},
417 * we need to start another loader. Upon obtaining a valid cursor, the loader is destroyed.
418 * An additional complication arises if we have to view a specific conversation within this
419 * folder. This is the case when launching the app from a single conversation notification
420 * or tapping on a specific conversation in the widget. In these cases, the conversation is
421 * saved in {@link #mConversationToShow} and is retrieved when the loader returns.
422 */
Régis Décamps004b46f2014-06-23 15:13:23 +0200423 public static final int LOADER_FIRST_FOLDER = 34;
Régis Décampsde21e892014-06-19 20:59:13 +0200424
425 /**
Vikram Aggarwal7a5d95a2012-07-27 16:24:54 -0700426 * Guaranteed to be the last loader ID used by the activity. Loaders are owned by Activity or
427 * fragments, and within an activity, loader IDs need to be unique. A hack to ensure that the
428 * {@link FolderWatcher} can create its folder loaders without clashing with the IDs of those
429 * of the {@link AbstractActivityController}. Currently, the {@link FolderWatcher} is the only
430 * other class that uses this activity's LoaderManager. If another class needs activity-level
431 * loaders, consider consolidating the loaders in a central location: a UI-less fragment
432 * perhaps.
433 */
Régis Décamps004b46f2014-06-23 15:13:23 +0200434 public static final int LAST_LOADER_ID = 35;
435
Scott Kennedy7c8325d2013-02-28 10:46:10 -0800436 /**
437 * Guaranteed to be the last loader ID used by the Fragment. Loaders are owned by Activity or
438 * fragments, and within an activity, loader IDs need to be unique. Currently,
Andrew Sapperstein2d86d112014-07-24 20:27:37 -0700439 * SectionedInboxTeaserView is the only class that uses the
Scott Kennedy7c8325d2013-02-28 10:46:10 -0800440 * {@link ConversationListFragment}'s LoaderManager.
441 */
442 public static final int LAST_FRAGMENT_LOADER_ID = 1000;
Vikram Aggarwala55b36c2012-01-13 11:45:02 -0800443
Vikram Aggarwal6aca6892013-06-04 13:53:27 -0700444 /** Code returned after an account has been added. */
Paul Westbrook2388c5d2012-03-25 12:29:11 -0700445 private static final int ADD_ACCOUNT_REQUEST_CODE = 1;
Vikram Aggarwal6aca6892013-06-04 13:53:27 -0700446 /** Code returned when the user has to enter the new password on an existing account. */
Paul Westbrook122f7c22012-08-20 17:50:31 -0700447 private static final int REAUTHENTICATE_REQUEST_CODE = 2;
Martin Hibdon371a71c2014-02-19 13:55:28 -0800448 /** Code returned when the previous activity needs to navigate to a different folder
449 * or account */
450 private static final int CHANGE_NAVIGATION_REQUEST_CODE = 3;
451
452 public static final String EXTRA_FOLDER = "extra-folder";
453 public static final String EXTRA_ACCOUNT = "extra-account";
Paul Westbrook2388c5d2012-03-25 12:29:11 -0700454
Vikram Aggarwale8a85322012-04-24 09:01:38 -0700455 /** The pending destructive action to be carried out before swapping the conversation cursor.*/
456 private DestructiveAction mPendingDestruction;
Andrew Sappersteinc2c9dc12012-07-02 18:17:32 -0700457 protected AsyncRefreshTask mFolderSyncTask;
Mindy Pereirac975e842012-07-16 09:15:00 -0700458 private Folder mFolderListFolder;
Martin Hibdone78c40f2013-10-10 18:29:25 -0700459 private final int mShowUndoBarDelay;
mindyp6f54e1b2012-10-09 09:54:08 -0700460 private boolean mRecentsDataUpdated;
Vikram Aggarwala3f43d42012-10-25 16:21:30 -0700461 /** A wait fragment we added, if any. */
462 private WaitFragment mWaitFragment;
Vikram Aggarwalf0ef2302012-11-07 14:53:34 -0800463 /** True if we have results from a search query */
464 private boolean mHaveSearchResults = false;
Vikram Aggarwal6cadbfc2012-12-27 09:17:05 -0800465 /** If a confirmation dialog is being show, the listener for the positive action. */
466 private OnClickListener mDialogListener;
467 /**
468 * If a confirmation dialog is being show, the resource of the action: R.id.delete, etc. This
469 * is used to create a new {@link #mDialogListener} on orientation changes.
470 */
471 private int mDialogAction = -1;
Vikram Aggarwalb8c31712013-01-03 17:03:19 -0800472 /**
473 * If a confirmation dialog is being shown, this is true if the dialog acts on the selected set
474 * and false if it acts on the currently selected conversation
475 */
476 private boolean mDialogFromSelectedSet;
Vikram Aggarwal6cadbfc2012-12-27 09:17:05 -0800477
Vikram Aggarwal177097f2013-03-08 11:19:53 -0800478 /** Which conversation to show, if started from widget/notification. */
479 private Conversation mConversationToShow = null;
480
Andy Huangc94d07f2013-06-03 16:19:35 -0700481 /**
482 * A temporary reference to the pending destructive action that was deferred due to an
483 * auto-advance transition in progress.
484 * <p>
485 * In detail: when auto-advance triggers a mode change, we must wait until the transition
486 * completes before executing the destructive action to ensure a smooth mode change transition.
487 * This member variable houses the pending destructive action work to be run upon completion.
488 */
489 private Runnable mAutoAdvanceOp = null;
490
Andy Huang12b3ee42013-04-24 22:49:43 -0700491 protected DrawerLayout mDrawerContainer;
492 protected View mDrawerPullout;
493 protected ActionBarDrawerToggle mDrawerToggle;
Andy Huangf58e4c32014-07-09 16:58:18 -0700494
Andy Huang12b3ee42013-04-24 22:49:43 -0700495 protected ListView mListViewForAnimating;
496 protected boolean mHasNewAccountOrFolder;
Andrew Sappersteina3ce6782013-05-09 15:14:49 -0700497 private boolean mConversationListLoadFinishedIgnored;
Andy Huang61f26c22014-03-13 18:24:52 -0700498 private final MailDrawerListener mDrawerListener = new MailDrawerListener();
Andrew Sapperstein5747e152013-05-13 14:13:08 -0700499 private boolean mHideMenuItems;
Andy Huang12b3ee42013-04-24 22:49:43 -0700500
Andy Huang144bfe72013-06-11 13:27:52 -0700501 private final DrawIdler mDrawIdler = new DrawIdler();
502
Andrew Sapperstein00179f12012-08-09 15:15:40 -0700503 public static final String SYNC_ERROR_DIALOG_FRAGMENT_TAG = "SyncErrorDialogFragment";
Vikram Aggarwale8a85322012-04-24 09:01:38 -0700504
Scott Kennedycb85aea2013-02-25 13:08:32 -0800505 private final DataSetObserver mUndoNotificationObserver = new DataSetObserver() {
506 @Override
507 public void onChanged() {
508 super.onChanged();
509
510 if (mConversationListCursor != null) {
511 mConversationListCursor.handleNotificationActions();
512 }
513 }
514 };
515
Andrew Sapperstein53de4482014-07-29 02:39:39 +0000516 private final HomeButtonListener mHomeButtonListener = new HomeButtonListener();
517
Vikram Aggarwala55b36c2012-01-13 11:45:02 -0800518 public AbstractActivityController(MailActivity activity, ViewMode viewMode) {
519 mActivity = activity;
Vikram Aggarwal6902dcf2012-04-11 08:57:42 -0700520 mFragmentManager = mActivity.getFragmentManager();
Vikram Aggarwala55b36c2012-01-13 11:45:02 -0800521 mViewMode = viewMode;
522 mContext = activity.getApplicationContext();
Vikram Aggarwal025eba82012-05-08 10:45:30 -0700523 mRecentFolderList = new RecentFolderList(mContext);
Paul Westbrook937c94f2012-08-16 13:01:18 -0700524 mTracker = new ConversationPositionTracker(this);
Mindy Pereira967ede62012-03-22 09:29:09 -0700525 // Allow the fragment to observe changes to its own selection set. No other object is
526 // aware of the selected set.
Jin Caoec0fa482014-08-28 16:38:08 -0700527 mCheckedSet.addObserver(this);
Paul Westbrookc7a070f2012-04-12 01:46:41 -0700528
Vikram Aggarwalbcb16b92013-01-28 18:05:03 -0800529 final Resources r = mContext.getResources();
530 mFolderItemUpdateDelayMs = r.getInteger(R.integer.folder_item_refresh_delay_ms);
531 mShowUndoBarDelay = r.getInteger(R.integer.show_undo_bar_delay_ms);
Vikram Aggarwal69a6cdf2013-01-08 16:05:17 -0800532 mVeiledMatcher = VeiledAddressMatcher.newInstance(activity.getResources());
Vikram Aggarwalbcb16b92013-01-28 18:05:03 -0800533 mIsTablet = Utils.useTabletUI(r);
Andrew Sappersteina3ce6782013-05-09 15:14:49 -0700534 mConversationListLoadFinishedIgnored = false;
Vikram Aggarwala55b36c2012-01-13 11:45:02 -0800535 }
Vikram Aggarwal4a5c5302012-01-12 15:07:13 -0800536
Vikram Aggarwale9a81032012-02-22 13:15:35 -0800537 public Account getCurrentAccount() {
538 return mAccount;
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -0800539 }
540
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -0800541 public ConversationListContext getCurrentListContext() {
Vikram Aggarwale9a81032012-02-22 13:15:35 -0800542 return mConvListContext;
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -0800543 }
544
545 @Override
Vikram Aggarwalef7c9922012-04-23 12:35:04 -0700546 public final ConversationCursor getConversationListCursor() {
Mindy Pereira967ede62012-03-22 09:29:09 -0700547 return mConversationListCursor;
548 }
549
Vikram Aggarwal6902dcf2012-04-11 08:57:42 -0700550 /**
Vikram Aggarwal34a65012012-04-17 12:39:06 -0700551 * Check if the fragment is attached to an activity and has a root view.
Vikram Aggarwal715f83d2013-03-05 17:04:56 -0800552 * @param in fragment to be checked
Vikram Aggarwal34a65012012-04-17 12:39:06 -0700553 * @return true if the fragment is valid, false otherwise
554 */
Vikram Aggarwal715f83d2013-03-05 17:04:56 -0800555 private static boolean isValidFragment(Fragment in) {
556 return !(in == null || in.getActivity() == null || in.getView() == null);
Vikram Aggarwal34a65012012-04-17 12:39:06 -0700557 }
558
559 /**
Vikram Aggarwal49e0e992012-09-21 13:53:15 -0700560 * Get the conversation list fragment for this activity. If the conversation list fragment is
561 * not attached, this method returns null.
Andy Huang839ada22012-07-20 15:48:40 -0700562 *
Vikram Aggarwal49e0e992012-09-21 13:53:15 -0700563 * Caution! This method returns the {@link ConversationListFragment} after the fragment has been
564 * added, <b>and</b> after the {@link FragmentManager} has run through its queue to add the
565 * fragment. There is a non-trivial amount of time after the fragment is instantiated and before
566 * this call returns a non-null value, depending on the {@link FragmentManager}. If you
567 * need the fragment immediately after adding it, consider making the fragment an observer of
568 * the controller and perform the task immediately on {@link Fragment#onActivityCreated(Bundle)}
Vikram Aggarwal6902dcf2012-04-11 08:57:42 -0700569 */
570 protected ConversationListFragment getConversationListFragment() {
571 final Fragment fragment = mFragmentManager.findFragmentByTag(TAG_CONVERSATION_LIST);
Vikram Aggarwal34a65012012-04-17 12:39:06 -0700572 if (isValidFragment(fragment)) {
573 return (ConversationListFragment) fragment;
Andy Huang9585d782012-04-16 19:45:04 -0700574 }
Vikram Aggarwal34a65012012-04-17 12:39:06 -0700575 return null;
Vikram Aggarwal6902dcf2012-04-11 08:57:42 -0700576 }
577
578 /**
Vikram Aggarwal6902dcf2012-04-11 08:57:42 -0700579 * Returns the folder list fragment attached with this activity. If no such fragment is attached
580 * this method returns null.
Andy Huang839ada22012-07-20 15:48:40 -0700581 *
Vikram Aggarwal49e0e992012-09-21 13:53:15 -0700582 * Caution! This method returns the {@link FolderListFragment} after the fragment has been
583 * added, <b>and</b> after the {@link FragmentManager} has run through its queue to add the
584 * fragment. There is a non-trivial amount of time after the fragment is instantiated and before
585 * this call returns a non-null value, depending on the {@link FragmentManager}. If you
586 * need the fragment immediately after adding it, consider making the fragment an observer of
587 * the controller and perform the task immediately on {@link Fragment#onActivityCreated(Bundle)}
Vikram Aggarwal6902dcf2012-04-11 08:57:42 -0700588 */
589 protected FolderListFragment getFolderListFragment() {
James Lemieux10fcd642014-03-03 13:01:04 -0800590 final String drawerPulloutTag = mActivity.getString(R.string.drawer_pullout_tag);
591 final Fragment fragment = mFragmentManager.findFragmentByTag(drawerPulloutTag);
Vikram Aggarwal34a65012012-04-17 12:39:06 -0700592 if (isValidFragment(fragment)) {
593 return (FolderListFragment) fragment;
Andy Huang9585d782012-04-16 19:45:04 -0700594 }
Vikram Aggarwal34a65012012-04-17 12:39:06 -0700595 return null;
Vikram Aggarwal6902dcf2012-04-11 08:57:42 -0700596 }
597
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -0800598 /**
Mindy Pereira161f50d2012-02-28 15:47:19 -0800599 * Initialize the action bar. This is not visible to OnePaneController and
600 * TwoPaneController so they cannot override this behavior.
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -0800601 */
Vikram Aggarwalabd24d82012-04-26 13:23:14 -0700602 private void initializeActionBar() {
Andrew Sapperstein52882ff2014-07-27 12:30:18 -0700603 final ActionBar actionBar = mActivity.getSupportActionBar();
Andy Huang5895f7b2012-06-01 17:07:20 -0700604 if (actionBar == null) {
605 return;
Vikram Aggarwalabd24d82012-04-26 13:23:14 -0700606 }
Andy Huang5895f7b2012-06-01 17:07:20 -0700607
Jin Caoc6801eb2014-08-12 18:16:57 -0700608 mActionBarController = new ActionBarController(mContext);
Andrew Sapperstein2d86d112014-07-24 20:27:37 -0700609 mActionBarController.initialize(mActivity, this, actionBar);
Jin Caoc6801eb2014-08-12 18:16:57 -0700610 actionBar.setShowHideAnimationEnabled(false);
Rohan Shah1dd054f2013-04-01 11:23:44 -0700611
Andy Huang12b3ee42013-04-24 22:49:43 -0700612 // init the action bar to allow the 'up' affordance.
613 // any configurations that disallow 'up' should do that later.
Andrew Sapperstein2d86d112014-07-24 20:27:37 -0700614 mActionBarController.setBackButton();
Vikram Aggarwalabd24d82012-04-26 13:23:14 -0700615 }
616
617 /**
618 * Attach the action bar to the activity.
619 */
620 private void attachActionBar() {
Andrew Sapperstein52882ff2014-07-27 12:30:18 -0700621 final ActionBar actionBar = mActivity.getSupportActionBar();
Tony Mantlerbd091502013-09-16 13:59:47 -0700622 if (actionBar != null) {
Andrew Sapperstein2d86d112014-07-24 20:27:37 -0700623 // Show a title
624 final int mask = ActionBar.DISPLAY_SHOW_TITLE | ActionBar.DISPLAY_SHOW_HOME;
Vikram Aggarwalbc462ca2013-03-15 10:41:03 -0700625 actionBar.setDisplayOptions(mask, mask);
Andrew Sapperstein2d86d112014-07-24 20:27:37 -0700626 mActionBarController.setViewModeController(mViewMode);
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -0800627 }
628 }
629
630 /**
Mindy Pereira161f50d2012-02-28 15:47:19 -0800631 * Returns whether the conversation list fragment is visible or not.
632 * Different layouts will have their own notion on the visibility of
633 * fragments, so this method needs to be overriden.
634 *
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -0800635 */
636 protected abstract boolean isConversationListVisible();
637
Vikram Aggarwaleb7d3292012-04-20 17:07:20 -0700638 /**
Vikram Aggarwal34f7b232012-10-17 13:32:23 -0700639 * If required, starts wait mode for the current account.
Vikram Aggarwal34f7b232012-10-17 13:32:23 -0700640 */
641 final void perhapsEnterWaitMode() {
642 // If the account is not initialized, then show the wait fragment, since nothing can be
643 // shown.
644 if (mAccount.isAccountInitializationRequired()) {
645 showWaitForInitialization();
646 return;
647 }
648
649 final boolean inWaitingMode = inWaitMode();
650 final boolean isSyncRequired = mAccount.isAccountSyncRequired();
651 if (isSyncRequired) {
652 if (inWaitingMode) {
653 // Update the WaitFragment's account object
654 updateWaitMode();
655 } else {
656 // Transition to waiting mode
657 showWaitForInitialization();
658 }
659 } else if (inWaitingMode) {
660 // Dismiss waiting mode
661 hideWaitForInitialization();
662 }
663 }
664
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -0800665 @Override
Andrew Sapperstein4cbb0da2013-04-19 11:28:02 -0700666 public void switchToDefaultInboxOrChangeAccount(Account account) {
667 LogUtils.d(LOG_TAG, "AAC.switchToDefaultAccount(%s)", account);
Andy Huange764cfd2014-02-26 11:55:03 -0800668 if (mViewMode.isSearchMode()) {
Martin Hibdon371a71c2014-02-19 13:55:28 -0800669 // We are in an activity on top of the main navigation activity.
670 // We need to return to it with a result code that indicates it should navigate to
671 // a different folder.
672 final Intent intent = new Intent();
673 intent.putExtra(AbstractActivityController.EXTRA_ACCOUNT, account);
674 mActivity.setResult(Activity.RESULT_OK, intent);
675 mActivity.finish();
676 return;
677 }
Vikram Aggarwal66bc6ed2012-07-31 09:59:55 -0700678 final boolean firstLoad = mAccount == null;
Andrew Sapperstein252684a2013-04-17 14:43:26 -0700679 final boolean switchToDefaultInbox = !firstLoad && account.uri.equals(mAccount.uri);
Vikram Aggarwald70fe492013-06-04 12:52:07 -0700680 // If the active account has been clicked in the drawer, go to default inbox
Andrew Sapperstein252684a2013-04-17 14:43:26 -0700681 if (switchToDefaultInbox) {
682 loadAccountInbox();
683 return;
684 }
Andrew Sapperstein4cbb0da2013-04-19 11:28:02 -0700685 changeAccount(account);
686 }
687
Andrew Sapperstein4cbb0da2013-04-19 11:28:02 -0700688 public void changeAccount(Account account) {
689 LogUtils.d(LOG_TAG, "AAC.changeAccount(%s)", account);
690 // Is the account or account settings different from the existing account?
691 final boolean firstLoad = mAccount == null;
692 final boolean accountChanged = firstLoad || !account.uri.equals(mAccount.uri);
693
Vikram Aggarwal472e5362012-11-05 14:17:23 -0800694 // If nothing has changed, return early without wasting any more time.
695 if (!accountChanged && !account.settingsDiffer(mAccount)) {
696 return;
697 }
698 // We also don't want to do anything if the new account is null
699 if (account == null) {
Vikram Aggarwal5fd8afd2013-03-13 15:28:47 -0700700 LogUtils.e(LOG_TAG, "AAC.changeAccount(null) called.");
Vikram Aggarwal472e5362012-11-05 14:17:23 -0800701 return;
702 }
Tony Mantler79b11562013-10-09 15:31:50 -0700703 final String emailAddress = account.getEmailAddress();
Vikram Aggarwal472e5362012-11-05 14:17:23 -0800704 mHandler.post(new Runnable() {
705 @Override
706 public void run() {
Tony Mantler79b11562013-10-09 15:31:50 -0700707 MailActivity.setNfcMessage(emailAddress);
Mindy Pereirafa20c1a2012-07-23 13:00:02 -0700708 }
Vikram Aggarwal472e5362012-11-05 14:17:23 -0800709 });
710 if (accountChanged) {
711 commitDestructiveActions(false);
712 }
Régis Décamps2168cbc2014-08-22 15:00:37 +0200713
Vikram Aggarwal472e5362012-11-05 14:17:23 -0800714 // Change the account here
715 setAccount(account);
716 // And carry out associated actions.
717 cancelRefreshTask();
718 if (accountChanged) {
719 loadAccountInbox();
720 }
721 // Check if we need to force setting up an account before proceeding.
722 if (mAccount != null && !Uri.EMPTY.equals(mAccount.settings.setupIntentUri)) {
723 // Launch the intent!
724 final Intent intent = new Intent(Intent.ACTION_EDIT);
Tony Mantlerec6e2512014-05-22 13:31:10 -0700725
726 intent.setPackage(mContext.getPackageName());
Vikram Aggarwal472e5362012-11-05 14:17:23 -0800727 intent.setData(mAccount.settings.setupIntentUri);
Tony Mantlerec6e2512014-05-22 13:31:10 -0700728
Vikram Aggarwal472e5362012-11-05 14:17:23 -0800729 mActivity.startActivity(intent);
Mindy Pereira28d5f722012-02-15 12:32:40 -0800730 }
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -0800731 }
732
Vikram Aggarwale6340bc2012-03-26 15:57:09 -0700733 /**
Vikram Aggarwal7c401b72012-08-13 16:43:47 -0700734 * Adds a listener interested in change in the current account. If a class is storing a
735 * reference to the current account, it should listen on changes, so it can receive updates to
736 * settings. Must happen in the UI thread.
737 */
Mindy Pereiraefe3d252012-03-01 14:20:44 -0800738 @Override
Vikram Aggarwal7c401b72012-08-13 16:43:47 -0700739 public void registerAccountObserver(DataSetObserver obs) {
740 mAccountObservers.registerObserver(obs);
Mindy Pereiraefe3d252012-03-01 14:20:44 -0800741 }
742
Vikram Aggarwal7d816002012-04-17 17:06:41 -0700743 /**
Vikram Aggarwal7c401b72012-08-13 16:43:47 -0700744 * Removes a listener from receiving current account changes.
Vikram Aggarwal7d816002012-04-17 17:06:41 -0700745 * Must happen in the UI thread.
746 */
Vikram Aggarwal7c401b72012-08-13 16:43:47 -0700747 @Override
748 public void unregisterAccountObserver(DataSetObserver obs) {
749 mAccountObservers.unregisterObserver(obs);
Vikram Aggarwal7d816002012-04-17 17:06:41 -0700750 }
751
Vikram Aggarwal7c401b72012-08-13 16:43:47 -0700752 @Override
Vikram Aggarwal07dbaa62013-03-12 15:21:04 -0700753 public void registerAllAccountObserver(DataSetObserver observer) {
754 mAllAccountObservers.registerObserver(observer);
755 }
756
757 @Override
758 public void unregisterAllAccountObserver(DataSetObserver observer) {
759 mAllAccountObservers.unregisterObserver(observer);
760 }
761
762 @Override
763 public Account[] getAllAccounts() {
764 return mAllAccounts;
765 }
766
767 @Override
Vikram Aggarwal7c401b72012-08-13 16:43:47 -0700768 public Account getAccount() {
769 return mAccount;
Vikram Aggarwal7d816002012-04-17 17:06:41 -0700770 }
771
Rohan Shah0f73d902013-04-19 17:06:37 -0700772 @Override
Tony Mantler54022ee2014-07-07 13:43:35 -0700773 public void registerFolderOrAccountChangedObserver(final DataSetObserver observer) {
774 mFolderOrAccountObservers.registerObserver(observer);
Rohan Shah0f73d902013-04-19 17:06:37 -0700775 }
776
777 @Override
Tony Mantler54022ee2014-07-07 13:43:35 -0700778 public void unregisterFolderOrAccountChangedObserver(final DataSetObserver observer) {
779 mFolderOrAccountObservers.unregisterObserver(observer);
Rohan Shah0f73d902013-04-19 17:06:37 -0700780 }
781
782 /**
Andy Huang12b3ee42013-04-24 22:49:43 -0700783 * If the drawer is open, the function locks the drawer to the closed, thereby sliding in
784 * the drawer to the left edge, disabling events, and refreshing it once it's either closed
785 * or put in an idle state.
Rohan Shah0f73d902013-04-19 17:06:37 -0700786 */
787 @Override
Vikram Aggarwala1b59dc2013-04-30 15:45:49 -0700788 public void closeDrawer(final boolean hasNewFolderOrAccount, Account nextAccount,
789 Folder nextFolder) {
Andy Huang12b3ee42013-04-24 22:49:43 -0700790 if (!isDrawerEnabled()) {
Tony Mantler54022ee2014-07-07 13:43:35 -0700791 if (hasNewFolderOrAccount) {
792 mFolderOrAccountObservers.notifyChanged();
793 }
Andy Huang12b3ee42013-04-24 22:49:43 -0700794 return;
795 }
Andy Huang12b3ee42013-04-24 22:49:43 -0700796 // If there are no new folders or accounts to switch to, just close the drawer
797 if (!hasNewFolderOrAccount) {
798 mDrawerContainer.closeDrawers();
799 return;
800 }
Vikram Aggarwal2f9d3942013-05-03 12:31:39 -0700801 // Otherwise, start preloading the conversation list for the new folder.
802 if (nextFolder != null) {
803 preloadConvList(nextAccount, nextFolder);
804 }
805 // Remember if the conversation list view is animating
Andy Huang12b3ee42013-04-24 22:49:43 -0700806 final ConversationListFragment conversationList = getConversationListFragment();
807 if (conversationList != null) {
808 mListViewForAnimating = conversationList.getListView();
809 } else {
810 // There is no conversation list to animate, so just set it to null
811 mListViewForAnimating = null;
812 }
813
814 if (mDrawerContainer.isDrawerOpen(mDrawerPullout)) {
815 // Lets the drawer listener update the drawer contents and notify the FolderListFragment
816 mHasNewAccountOrFolder = true;
817 mDrawerContainer.setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED);
818 } else {
819 // Drawer is already closed, notify observers that is the case.
Tony Mantler54022ee2014-07-07 13:43:35 -0700820 if (hasNewFolderOrAccount) {
821 mFolderOrAccountObservers.notifyChanged();
822 }
Andy Huang12b3ee42013-04-24 22:49:43 -0700823 }
Rohan Shah0f73d902013-04-19 17:06:37 -0700824 }
825
Vikram Aggarwala1b59dc2013-04-30 15:45:49 -0700826 /**
Vikram Aggarwalaa941d72013-06-04 15:34:28 -0700827 * Load the conversation list early for the given folder. This happens when some UI element
828 * (usually the drawer) instructs the controller that an account change or folder change is
829 * imminent. While the UI element is animating, the controller can preload the conversation
830 * list for the default inbox of the account provided here or to the folder provided here.
831 *
832 * @param nextAccount The account which the app will switch to shortly, possibly null.
833 * @param nextFolder The folder which the app will switch to shortly, possibly null.
Vikram Aggarwala1b59dc2013-04-30 15:45:49 -0700834 */
835 protected void preloadConvList(Account nextAccount, Folder nextFolder) {
836 // Fire off the conversation list loader for this account already with a fake
837 // listener.
Paul Westbrooke2be97a2013-05-21 02:12:09 -0700838 final Bundle args = new Bundle(2);
Vikram Aggarwala1b59dc2013-04-30 15:45:49 -0700839 if (nextAccount != null) {
840 args.putParcelable(BUNDLE_ACCOUNT_KEY, nextAccount);
841 } else {
842 args.putParcelable(BUNDLE_ACCOUNT_KEY, mAccount);
843 }
844 if (nextFolder != null) {
845 args.putParcelable(BUNDLE_FOLDER_KEY, nextFolder);
Vikram Aggarwal2dc85042013-05-01 13:57:09 -0700846 } else {
847 LogUtils.e(LOG_TAG, new Error(), "AAC.preloadConvList(): Got an empty folder");
Vikram Aggarwala1b59dc2013-04-30 15:45:49 -0700848 }
849 mFolder = null;
850 final LoaderManager lm = mActivity.getLoaderManager();
851 lm.destroyLoader(LOADER_CONVERSATION_LIST);
852 lm.initLoader(LOADER_CONVERSATION_LIST, args, mListCursorCallbacks);
853 }
854
Vikram Aggarwalaa941d72013-06-04 15:34:28 -0700855 /**
856 * Initiates the async request to create a fake search folder, which returns conversations that
857 * match the query term provided by the user. Returns immediately.
858 * @param intent Intent that the app was started with. This intent contains the search query.
859 */
Mindy Pereirae0828392012-03-08 10:38:40 -0800860 private void fetchSearchFolder(Intent intent) {
Paul Westbrooke2be97a2013-05-21 02:12:09 -0700861 final Bundle args = new Bundle(1);
Mindy Pereiraab486362012-03-21 18:18:53 -0700862 args.putString(ConversationListContext.EXTRA_SEARCH_QUERY, intent
Mindy Pereirae0828392012-03-08 10:38:40 -0800863 .getStringExtra(ConversationListContext.EXTRA_SEARCH_QUERY));
Vikram Aggarwal177097f2013-03-08 11:19:53 -0800864 mActivity.getLoaderManager().restartLoader(LOADER_SEARCH, args, mFolderCallbacks);
Mindy Pereirae0828392012-03-08 10:38:40 -0800865 }
866
Jin Cao405a3442014-08-25 13:49:33 -0700867 protected void onFolderChanged(Folder folder, final boolean force) {
Andy Huangf58e4c32014-07-09 16:58:18 -0700868 if (isDrawerEnabled()) {
869 /** If the folder doesn't exist, or its parent URI is empty,
870 * this is not a child folder */
871 final boolean isTopLevel = Folder.isRoot(folder);
872 final int mode = mViewMode.getMode();
873 mDrawerToggle.setDrawerIndicatorEnabled(
874 getShouldShowDrawerIndicator(mode, isTopLevel));
875 mDrawerContainer.setDrawerLockMode(DrawerLayout.LOCK_MODE_UNLOCKED);
Vikram Aggarwaldfbc6982013-07-03 14:39:31 -0700876
Andy Huangf58e4c32014-07-09 16:58:18 -0700877 mDrawerContainer.closeDrawers();
878 }
Alice Yang7dd0e1c2013-09-04 06:43:16 +0000879
880 if (mFolder == null || !mFolder.equals(folder)) {
881 // We are actually changing the folder, so exit cab mode
882 exitCabMode();
883 }
884
Scott Kennedya158ac82013-09-04 13:48:13 -0700885 final String query;
886 if (folder != null && folder.isType(FolderType.SEARCH)) {
887 query = mConvListContext.searchQuery;
888 } else {
889 query = null;
890 }
891
892 changeFolder(folder, query, force);
Vikram Aggarwalf3341402012-08-07 10:09:38 -0700893 }
894
895 /**
Vikram Aggarwal203dc002012-08-23 13:59:04 -0700896 * Sets the folder state without changing view mode and without creating a list fragment, if
897 * possible.
Vikram Aggarwal715f83d2013-03-05 17:04:56 -0800898 * @param folder the folder whose list of conversations are to be shown
899 * @param query the query string for a list of conversations matching a search
Vikram Aggarwal203dc002012-08-23 13:59:04 -0700900 */
901 private void setListContext(Folder folder, String query) {
902 updateFolder(folder);
903 if (query != null) {
904 mConvListContext = ConversationListContext.forSearchQuery(mAccount, mFolder, query);
905 } else {
906 mConvListContext = ConversationListContext.forFolder(mAccount, mFolder);
907 }
Vikram Aggarwal203dc002012-08-23 13:59:04 -0700908 cancelRefreshTask();
909 }
910
911 /**
Vikram Aggarwalf3341402012-08-07 10:09:38 -0700912 * Changes the folder to the value provided here. This causes the view mode to change.
913 * @param folder the folder to change to
914 * @param query if non-null, this represents the search string that the folder represents.
Alice Yangebeef1b2013-09-04 06:41:10 +0000915 * @param force <code>true</code> to force a folder change, <code>false</code> to disallow
916 * changing to the current folder
Vikram Aggarwalf3341402012-08-07 10:09:38 -0700917 */
Alice Yangebeef1b2013-09-04 06:41:10 +0000918 private void changeFolder(Folder folder, String query, final boolean force) {
Vikram Aggarwal203dc002012-08-23 13:59:04 -0700919 if (!Objects.equal(mFolder, folder)) {
920 commitDestructiveActions(false);
921 }
Alice Yangebeef1b2013-09-04 06:41:10 +0000922 if (folder != null && (!folder.equals(mFolder) || force)
Mindy Pereiraa1f99982012-07-16 16:32:15 -0700923 || (mViewMode.getMode() != ViewMode.CONVERSATION_LIST)) {
Vikram Aggarwal203dc002012-08-23 13:59:04 -0700924 setListContext(folder, query);
Mindy Pereira28e0c342012-02-17 15:05:13 -0800925 showConversationList(mConvListContext);
Vikram Aggarwal58ccd692013-03-28 11:29:22 -0700926 // Touch the current folder: it is different, and it has been accessed.
927 mRecentFolderList.touchFolder(mFolder, mAccount);
Mindy Pereira28e0c342012-02-17 15:05:13 -0800928 }
Vikram Aggarwal93dc2022012-11-05 13:36:57 -0800929 resetActionBarIcon();
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -0800930 }
931
Mindy Pereira13c12a62012-05-31 15:41:08 -0700932 @Override
Mindy Pereira505df5f2012-06-19 17:57:17 -0700933 public void onFolderSelected(Folder folder) {
Alice Yangebeef1b2013-09-04 06:41:10 +0000934 onFolderChanged(folder, false /* force */);
Mindy Pereira13c12a62012-05-31 15:41:08 -0700935 }
936
Vikram Aggarwal792ccba2012-03-27 13:46:57 -0700937 /**
Vikram Aggarwal58cad2e2012-08-28 16:18:23 -0700938 * Adds a listener interested in change in the recent folders. If a class is storing a
939 * reference to the recent folders, it should listen on changes, so it can receive updates.
940 * Must happen in the UI thread.
941 */
942 @Override
943 public void registerRecentFolderObserver(DataSetObserver obs) {
944 mRecentFolderObservers.registerObserver(obs);
945 }
946
947 /**
948 * Removes a listener from receiving recent folder changes.
949 * Must happen in the UI thread.
950 */
951 @Override
952 public void unregisterRecentFolderObserver(DataSetObserver obs) {
953 mRecentFolderObservers.unregisterObserver(obs);
954 }
955
956 @Override
957 public RecentFolderList getRecentFolders() {
958 return mRecentFolderList;
959 }
960
Jin Cao405a3442014-08-25 13:49:33 -0700961 /**
962 * Load the default inbox associated with the current account.
963 */
964 protected 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.
Andrew Sapperstein2d86d112014-07-24 20:27:37 -07001035 mActionBarController.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
Jin Cao405a3442014-08-25 13:49:33 -07001072 /**
1073 * Set the folder currently selected in the folder selection hierarchy fragments.
1074 */
1075 protected void setHierarchyFolder(Folder folder) {
Mindy Pereirac975e842012-07-16 09:15:00 -07001076 mFolderListFolder = folder;
Mindy Pereira23aadfd2012-05-25 11:24:33 -07001077 }
1078
Vikram Aggarwal6aca6892013-06-04 13:53:27 -07001079 /**
1080 * The mail activity calls other activities for two specific reasons:
1081 * <ul>
1082 * <li>To add an account. And receives the result {@link #ADD_ACCOUNT_REQUEST_CODE}</li>
1083 * <li>To update the password on a current account. The result {@link
1084 * #REAUTHENTICATE_REQUEST_CODE} is received.</li>
1085 * </ul>
1086 * @param requestCode
1087 * @param resultCode
1088 * @param data
1089 */
Mindy Pereira23aadfd2012-05-25 11:24:33 -07001090 @Override
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -08001091 public void onActivityResult(int requestCode, int resultCode, Intent data) {
Paul Westbrook122f7c22012-08-20 17:50:31 -07001092 switch (requestCode) {
1093 case ADD_ACCOUNT_REQUEST_CODE:
1094 // We were waiting for the user to create an account
1095 if (resultCode == Activity.RESULT_OK) {
1096 // restart the loader to get the updated list of accounts
Vikram Aggarwal177097f2013-03-08 11:19:53 -08001097 mActivity.getLoaderManager().initLoader(LOADER_ACCOUNT_CURSOR, Bundle.EMPTY,
1098 mAccountCallbacks);
Paul Westbrook122f7c22012-08-20 17:50:31 -07001099 } else {
1100 // The user failed to create an account, just exit the app
1101 mActivity.finish();
1102 }
1103 break;
1104 case REAUTHENTICATE_REQUEST_CODE:
1105 if (resultCode == Activity.RESULT_OK) {
1106 // The user successfully authenticated, attempt to refresh the list
1107 final Uri refreshUri = mFolder != null ? mFolder.refreshUri : null;
1108 if (refreshUri != null) {
1109 startAsyncRefreshTask(refreshUri);
1110 }
1111 }
1112 break;
Martin Hibdon371a71c2014-02-19 13:55:28 -08001113 case CHANGE_NAVIGATION_REQUEST_CODE:
Jin Caoc6801eb2014-08-12 18:16:57 -07001114 if (ViewMode.isSearchMode(mViewMode.getMode())) {
1115 mActivity.setResult(resultCode, data);
1116 mActivity.finish();
1117 } else if (resultCode == Activity.RESULT_OK && data != null) {
Martin Hibdon371a71c2014-02-19 13:55:28 -08001118 // We have have received a result that indicates we need to navigate to a
1119 // different folder or account. This happens if someone navigates using the
1120 // drawer on the search results activity.
1121 final Folder folder = data.getParcelableExtra(EXTRA_FOLDER);
1122 final Account account = data.getParcelableExtra(EXTRA_ACCOUNT);
1123 if (folder != null) {
1124 onFolderSelected(folder);
1125 mViewMode.enterConversationListMode();
1126 } else if (account != null) {
1127 switchToDefaultInboxOrChangeAccount(account);
1128 mViewMode.enterConversationListMode();
1129 }
1130 }
1131 break;
Jin Caoc6801eb2014-08-12 18:16:57 -07001132 case MaterialSearchViewController.VOICE_SEARCH_REQUEST_CODE:
1133 if (resultCode == Activity.RESULT_OK) {
1134 final ArrayList<String> matches = data.getStringArrayListExtra(
1135 RecognizerIntent.EXTRA_RESULTS);
1136 if (!matches.isEmpty()) {
1137 // not sure how dependable the API is, but it's all we have.
1138 // take the top choice.
1139 mSearchViewController.onSearchPerformed(matches.get(0));
1140 }
1141 }
1142 break;
Paul Westbrook2388c5d2012-03-25 12:29:11 -07001143 }
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -08001144 }
1145
Vikram Aggarwal69b5c302012-09-05 11:11:13 -07001146 /**
1147 * Inform the conversation cursor that there has been a visibility change.
Vikram Aggarwal715f83d2013-03-05 17:04:56 -08001148 * @param visible true if the conversation list is visible, false otherwise.
Vikram Aggarwal69b5c302012-09-05 11:11:13 -07001149 */
1150 protected synchronized void informCursorVisiblity(boolean visible) {
1151 if (mConversationListCursor != null) {
1152 Utils.setConversationCursorVisibility(mConversationListCursor, visible, mFolderChanged);
1153 // We have informed the cursor. Subsequent visibility changes should not tell it that
1154 // the folder has changed.
1155 mFolderChanged = false;
1156 }
1157 }
1158
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -08001159 @Override
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -08001160 public void onConversationListVisibilityChanged(boolean visible) {
Vikram Aggarwal69b5c302012-09-05 11:11:13 -07001161 informCursorVisiblity(visible);
Andy Huangc94d07f2013-06-03 16:19:35 -07001162 commitAutoAdvanceOperation();
Scott Kennedy32ddb842013-08-28 17:38:22 -07001163
1164 // Notify special views
1165 final ConversationListFragment convListFragment = getConversationListFragment();
1166 if (convListFragment != null && convListFragment.getAnimatedAdapter() != null) {
1167 convListFragment.getAnimatedAdapter().onConversationListVisibilityChanged(visible);
1168 }
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -08001169 }
1170
Vikram Aggarwal4a5c5302012-01-12 15:07:13 -08001171 /**
Vikram Aggarwal7dd054e2012-05-21 14:43:10 -07001172 * Called when a conversation is visible. Child classes must call the super class implementation
1173 * before performing local computation.
Vikram Aggarwal4a5c5302012-01-12 15:07:13 -08001174 */
1175 @Override
1176 public void onConversationVisibilityChanged(boolean visible) {
Andy Huangc94d07f2013-06-03 16:19:35 -07001177 commitAutoAdvanceOperation();
1178 }
1179
1180 /**
1181 * Commits any pending destructive action that was earlier deferred by an auto-advance
1182 * mode-change transition.
1183 */
1184 private void commitAutoAdvanceOperation() {
1185 if (mAutoAdvanceOp != null) {
1186 mAutoAdvanceOp.run();
1187 mAutoAdvanceOp = null;
1188 }
Vikram Aggarwal4a5c5302012-01-12 15:07:13 -08001189 }
1190
Vikram Aggarwal59f741f2013-03-01 15:55:40 -08001191 /**
1192 * Initialize development time logging. This can potentially log a lot of PII, and we don't want
1193 * to turn it on for shipped versions.
1194 */
1195 private void initializeDevLoggingService() {
1196 if (!MailLogService.DEBUG_ENABLED) {
1197 return;
1198 }
1199 // Check every 5 minutes.
1200 final int WAIT_TIME = 5 * 60 * 1000;
1201 // Start a runnable that periodically checks the log level and starts/stops the service.
1202 mLogServiceChecker = new Runnable() {
1203 /** True if currently logging. */
1204 private boolean mCurrentlyLogging = false;
1205
1206 /**
1207 * If the logging level has been changed since the previous run, start or stop the
1208 * service.
1209 */
1210 private void startOrStopService() {
1211 // If the log level is already high, start the service.
1212 final Intent i = new Intent(mContext, MailLogService.class);
1213 final boolean loggingEnabled = MailLogService.isLoggingLevelHighEnough();
1214 if (mCurrentlyLogging == loggingEnabled) {
1215 // No change since previous run, just return;
1216 return;
1217 }
1218 if (loggingEnabled) {
1219 LogUtils.e(LOG_TAG, "Starting MailLogService");
1220 mContext.startService(i);
1221 } else {
1222 LogUtils.e(LOG_TAG, "Stopping MailLogService");
1223 mContext.stopService(i);
1224 }
1225 mCurrentlyLogging = loggingEnabled;
1226 }
1227
1228 @Override
1229 public void run() {
1230 startOrStopService();
1231 mHandler.postDelayed(this, WAIT_TIME);
1232 }
1233 };
1234 // Start the runnable right away.
1235 mHandler.post(mLogServiceChecker);
1236 }
1237
Vikram Aggarwal6aca6892013-06-04 13:53:27 -07001238 /**
1239 * The application can be started from the following entry points:
1240 * <ul>
1241 * <li>Launcher: you tap on the Gmail icon in the launcher. This is what most users think of
1242 * as “Starting the app”.</li>
1243 * <li>Shortcut: Users can make a shortcut to take them directly to a label.</li>
1244 * <li>Widget: Shows the contents of a synced label, and allows:
1245 * <ul>
1246 * <li>Viewing the list (tapping on the title)</li>
1247 * <li>Composing a new message (tapping on the new message icon in the title. This
1248 * launches the {@link ComposeActivity}.
1249 * </li>
1250 * <li>Viewing a single message (tapping on a list element)</li>
1251 * </ul>
1252 *
1253 * </li>
1254 * <li>Tapping on a notification:
1255 * <ul>
1256 * <li>Shows message list if more than one message</li>
1257 * <li>Shows the conversation if the notification is for a single message</li>
1258 * </ul>
1259 * </li>
1260 * <li>...and most importantly, the activity life cycle can tear down the application and
1261 * restart it:
1262 * <ul>
1263 * <li>Rotate the application: it is destroyed and recreated.</li>
1264 * <li>Navigate away, and return from recent applications.</li>
1265 * </ul>
1266 * </li>
1267 * <li>Add a new account: fires off an intent to add an account,
1268 * and returns in {@link #onActivityResult(int, int, android.content.Intent)} .</li>
1269 * <li>Re-authenticate your account: again returns in onActivityResult().</li>
1270 * <li>Composing can happen from many entry points: third party applications fire off an
1271 * intent to compose email, and launch directly into the {@link ComposeActivity}
1272 * .</li>
1273 * </ul>
1274 * {@inheritDoc}
1275 */
Vikram Aggarwal4a5c5302012-01-12 15:07:13 -08001276 @Override
Vikram Aggarwald7a12cd2012-02-03 09:36:20 -08001277 public boolean onCreate(Bundle savedState) {
Vikram Aggarwalabd24d82012-04-26 13:23:14 -07001278 initializeActionBar();
Vikram Aggarwal59f741f2013-03-01 15:55:40 -08001279 initializeDevLoggingService();
Vikram Aggarwala55b36c2012-01-13 11:45:02 -08001280 // Allow shortcut keys to function for the ActionBar and menus.
1281 mActivity.setDefaultKeyMode(Activity.DEFAULT_KEYS_SHORTCUT);
Vikram Aggarwal80aeac52012-02-07 15:27:20 -08001282 mResolver = mActivity.getContentResolver();
Paul Westbrook6ead20d2012-03-19 14:48:14 -07001283 mNewEmailReceiver = new SuppressNotificationReceiver();
Vikram Aggarwal7c401b72012-08-13 16:43:47 -07001284 mRecentFolderList.initialize(mActivity);
Vikram Aggarwal69a6cdf2013-01-08 16:05:17 -08001285 mVeiledMatcher.initialize(this);
Paul Westbrook6ead20d2012-03-19 14:48:14 -07001286
James Lemieux1a1e9752014-07-18 13:50:40 -07001287 mFloatingComposeButton = mActivity.findViewById(R.id.compose_button);
1288 mFloatingComposeButton.setOnClickListener(this);
James Lemieux5d79a912014-07-16 14:22:26 -07001289
Andy Huangf58e4c32014-07-09 16:58:18 -07001290 if (isDrawerEnabled()) {
1291 mDrawerToggle = new ActionBarDrawerToggle(mActivity, mDrawerContainer, false,
1292 R.drawable.ic_drawer, R.string.drawer_open, R.string.drawer_close);
1293 mDrawerContainer.setDrawerListener(mDrawerListener);
1294 mDrawerContainer.setDrawerShadow(
1295 mContext.getResources().getDrawable(R.drawable.drawer_shadow), Gravity.START);
Andy Huang12b3ee42013-04-24 22:49:43 -07001296
Andy Huangf58e4c32014-07-09 16:58:18 -07001297 mDrawerToggle.setDrawerIndicatorEnabled(isDrawerEnabled());
1298 } else {
Andrew Sapperstein52882ff2014-07-27 12:30:18 -07001299 final ActionBar ab = mActivity.getSupportActionBar();
Andy Huangf58e4c32014-07-09 16:58:18 -07001300 ab.setHomeAsUpIndicator(R.drawable.ic_drawer);
1301 ab.setHomeActionContentDescription(R.string.drawer_open);
1302 ab.setDisplayHomeAsUpEnabled(true);
1303 }
Andy Huang12b3ee42013-04-24 22:49:43 -07001304
Mindy Pereira161f50d2012-02-28 15:47:19 -08001305 // All the individual UI components listen for ViewMode changes. This
Mindy Pereirab849dfb2012-03-07 18:13:15 -08001306 // simplifies the amount of logic in the AbstractActivityController, but increases the
1307 // possibility of timing-related bugs.
Vikram Aggarwala55b36c2012-01-13 11:45:02 -08001308 mViewMode.addListener(this);
Andy Huang632721e2012-04-11 16:57:26 -07001309 mPagerController = new ConversationPagerController(mActivity, this);
James Lemieux9a110112014-08-07 15:23:13 -07001310 mToastBar = findActionableToastBar(mActivity);
Vikram Aggarwalada51782012-04-26 14:18:31 -07001311 attachActionBar();
Andy Huang632721e2012-04-11 16:57:26 -07001312
Andy Huang144bfe72013-06-11 13:27:52 -07001313 mDrawIdler.setRootView(mActivity.getWindow().getDecorView());
1314
Andy Huang632721e2012-04-11 16:57:26 -07001315 final Intent intent = mActivity.getIntent();
Andy Huang4fe0af82013-08-20 17:24:51 -07001316
Jin Caoc6801eb2014-08-12 18:16:57 -07001317 mSearchViewController = new MaterialSearchViewController(mActivity, this, intent,
1318 savedState);
1319
Vikram Aggarwalabd24d82012-04-26 13:23:14 -07001320 // Immediately handle a clean launch with intent, and any state restoration
Andy Huang632721e2012-04-11 16:57:26 -07001321 // that does not rely on restored fragments or loader data
1322 // any state restoration that relies on those can be done later in
1323 // onRestoreInstanceState, once fragments are up and loader data is re-delivered
1324 if (savedState != null) {
1325 if (savedState.containsKey(SAVED_ACCOUNT)) {
1326 setAccount((Account) savedState.getParcelable(SAVED_ACCOUNT));
Andy Huang632721e2012-04-11 16:57:26 -07001327 }
1328 if (savedState.containsKey(SAVED_FOLDER)) {
Andy Huang2bc8bc12012-11-12 17:24:25 -08001329 final Folder folder = savedState.getParcelable(SAVED_FOLDER);
Vikram Aggarwal203dc002012-08-23 13:59:04 -07001330 final String query = savedState.getString(SAVED_QUERY, null);
Vikram Aggarwal649b9ea2012-08-27 12:15:20 -07001331 setListContext(folder, query);
Andy Huang632721e2012-04-11 16:57:26 -07001332 }
Vikram Aggarwal6cadbfc2012-12-27 09:17:05 -08001333 if (savedState.containsKey(SAVED_ACTION)) {
1334 mDialogAction = savedState.getInt(SAVED_ACTION);
1335 }
Vikram Aggarwala91d00b2013-01-18 12:00:37 -08001336 mDialogFromSelectedSet = savedState.getBoolean(SAVED_ACTION_FROM_SELECTED, false);
Vikram Aggarwal649b9ea2012-08-27 12:15:20 -07001337 mViewMode.handleRestore(savedState);
Andy Huang632721e2012-04-11 16:57:26 -07001338 } else if (intent != null) {
1339 handleIntent(intent);
1340 }
Andy Huang632721e2012-04-11 16:57:26 -07001341 // Create the accounts loader; this loads the account switch spinner.
Vikram Aggarwal177097f2013-03-08 11:19:53 -08001342 mActivity.getLoaderManager().initLoader(LOADER_ACCOUNT_CURSOR, Bundle.EMPTY,
1343 mAccountCallbacks);
Andy Huang632721e2012-04-11 16:57:26 -07001344 return true;
Andy Huangb1c34dc2012-04-17 16:36:19 -07001345 }
1346
James Lemieux9a110112014-08-07 15:23:13 -07001347 /**
1348 * @param activity the activity that has been inflated
1349 * @return the Actionable Toast Bar defined within the activity
1350 */
1351 protected ActionableToastBar findActionableToastBar(MailActivity activity) {
1352 return (ActionableToastBar) activity.findViewById(R.id.toast_bar);
1353 }
1354
Andy Huangb1c34dc2012-04-17 16:36:19 -07001355 @Override
Paul Westbrook57246a42013-04-21 09:40:22 -07001356 public void onPostCreate(Bundle savedState) {
Andy Huangf58e4c32014-07-09 16:58:18 -07001357 if (!isDrawerEnabled()) {
1358 return;
1359 }
Andy Huang12b3ee42013-04-24 22:49:43 -07001360 // Sync the toggle state after onRestoreInstanceState has occurred.
1361 mDrawerToggle.syncState();
Andrew Sapperstein5747e152013-05-13 14:13:08 -07001362
Vikram Aggarwald70fe492013-06-04 12:52:07 -07001363 mHideMenuItems = isDrawerEnabled() && mDrawerContainer.isDrawerOpen(mDrawerPullout);
Paul Westbrook57246a42013-04-21 09:40:22 -07001364 }
1365
1366 @Override
1367 public void onConfigurationChanged(Configuration newConfig) {
Andy Huangf58e4c32014-07-09 16:58:18 -07001368 if (isDrawerEnabled()) {
1369 mDrawerToggle.onConfigurationChanged(newConfig);
1370 }
Andy Huang12b3ee42013-04-24 22:49:43 -07001371 }
1372
1373 /**
James Lemieux5d79a912014-07-16 14:22:26 -07001374 * This controller listens for clicks on items in the floating action bar.
1375 *
1376 * @param view the item that was clicked in the floating action bar
1377 */
1378 @Override
1379 public void onClick(View view) {
Andrew Sapperstein53de4482014-07-29 02:39:39 +00001380 final int viewId = view.getId();
1381 if (viewId == R.id.compose_button) {
1382 ComposeActivity.compose(mActivity.getActivityContext(), getAccount());
1383 } else if (viewId == android.R.id.home) {
1384 // TODO: b/16627877
Jin Cao405a3442014-08-25 13:49:33 -07001385 handleUpPress();
Andrew Sapperstein53de4482014-07-29 02:39:39 +00001386 }
James Lemieux5d79a912014-07-16 14:22:26 -07001387 }
1388
1389 /**
Andy Huang12b3ee42013-04-24 22:49:43 -07001390 * If drawer is open/visible (even partially), close it.
1391 */
1392 protected void closeDrawerIfOpen() {
1393 if (!isDrawerEnabled()) {
1394 return;
1395 }
1396 if(mDrawerContainer.isDrawerOpen(mDrawerPullout)) {
1397 mDrawerContainer.closeDrawers();
1398 }
Paul Westbrook57246a42013-04-21 09:40:22 -07001399 }
1400
1401 @Override
Andy Huang1ee96b22012-08-24 20:19:53 -07001402 public void onStart() {
1403 mSafeToModifyFragments = true;
Scott Kennedycb85aea2013-02-25 13:08:32 -08001404
1405 NotificationActionUtils.registerUndoNotificationObserver(mUndoNotificationObserver);
Andy Huang761522c2013-08-08 13:09:11 -07001406
1407 if (mViewMode.getMode() != ViewMode.UNKNOWN) {
1408 Analytics.getInstance().sendView("MainActivity" + mViewMode.toString());
1409 }
Andy Huang1ee96b22012-08-24 20:19:53 -07001410 }
1411
1412 @Override
Andrew Sapperstein00179f12012-08-09 15:15:40 -07001413 public void onRestart() {
Vikram Aggarwal177097f2013-03-08 11:19:53 -08001414 final DialogFragment fragment = (DialogFragment)
Andrew Sapperstein00179f12012-08-09 15:15:40 -07001415 mFragmentManager.findFragmentByTag(SYNC_ERROR_DIALOG_FRAGMENT_TAG);
1416 if (fragment != null) {
1417 fragment.dismiss();
1418 }
mindypea04f932012-08-27 14:17:59 -07001419 // When the user places the app in the background by pressing "home",
1420 // dismiss the toast bar. However, since there is no way to determine if
1421 // home was pressed, just dismiss any existing toast bar when restarting
1422 // the app.
1423 if (mToastBar != null) {
Andrew Sappersteinf8ccdcf2013-08-08 19:30:38 -07001424 mToastBar.hide(false, false /* actionClicked */);
mindypea04f932012-08-27 14:17:59 -07001425 }
Andrew Sapperstein00179f12012-08-09 15:15:40 -07001426 }
1427
1428 @Override
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -08001429 public Dialog onCreateDialog(int id, Bundle bundle) {
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -08001430 return null;
Vikram Aggarwala55b36c2012-01-13 11:45:02 -08001431 }
1432
1433 @Override
Vikram Aggarwal4eb52712012-06-19 16:24:50 -07001434 public final boolean onCreateOptionsMenu(Menu menu) {
Andrew Sapperstein0fa9efd2013-08-07 14:09:31 -07001435 if (mViewMode.isAdMode()) {
1436 return false;
1437 }
Vikram Aggarwale5e917c2012-09-20 16:27:41 -07001438 final MenuInflater inflater = mActivity.getMenuInflater();
Andrew Sapperstein2d86d112014-07-24 20:27:37 -07001439 inflater.inflate(mActionBarController.getOptionsMenuId(), menu);
1440 mActionBarController.onCreateOptionsMenu(menu);
Mindy Pereira28d5f722012-02-15 12:32:40 -08001441 return true;
Vikram Aggarwala55b36c2012-01-13 11:45:02 -08001442 }
1443
1444 @Override
Vikram Aggarwal4eb52712012-06-19 16:24:50 -07001445 public final boolean onKeyDown(int keyCode, KeyEvent event) {
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -08001446 return false;
1447 }
1448
mindyp17a8e782012-11-29 14:56:17 -08001449 public abstract boolean doesActionChangeConversationListVisibility(int action);
1450
Jin Cao30c881a2014-04-08 14:28:36 -07001451 /**
1452 * Helper function that determines if we should associate an undo callback with
1453 * the current menu action item
1454 * @param actionId the id of the action
1455 * @return the appropriate callback handler, or null if not applicable
1456 */
1457 private UndoCallback getUndoCallbackForDestructiveActionsWithAutoAdvance(
1458 int actionId, final Conversation conv) {
1459 // We associated the undoCallback if the user is going to perform an action on the current
1460 // conversation, causing the current conversation to be removed from view and replacing it
1461 // with another (via Auto Advance). The undoCallback will bring the removed conversation
1462 // back into the view if the action is undone.
1463 final Collection<Conversation> convCol = Conversation.listOf(conv);
1464 final boolean isApplicableForReshow = mAccount != null &&
1465 mAccount.settings != null &&
1466 mTracker != null &&
1467 // ensure that we will show another conversation due to Auto Advance
1468 mTracker.getNextConversation(
1469 mAccount.settings.getAutoAdvanceSetting(), convCol) != null &&
1470 // ensure that we are performing the action from conversation view
1471 isCurrentConversationInView(convCol) &&
1472 // check for the appropriate destructive actions
1473 doesActionRemoveCurrentConversationFromView(actionId);
1474 return (isApplicableForReshow) ?
1475 new UndoCallback() {
1476 @Override
1477 public void performUndoCallback() {
1478 showConversation(conv);
1479 }
1480 } : null;
1481 }
1482
1483 /**
1484 * Check if the provided action will remove the active conversation from view
1485 * @param actionId the applied action
1486 * @return true if it will remove the conversation from view, false otherwise
1487 */
1488 private boolean doesActionRemoveCurrentConversationFromView(int actionId) {
1489 return actionId == R.id.archive ||
1490 actionId == R.id.delete ||
Jin Cao512821c2014-05-30 15:54:04 -07001491 actionId == R.id.discard_outbox ||
Jin Cao30c881a2014-04-08 14:28:36 -07001492 actionId == R.id.remove_folder ||
1493 actionId == R.id.report_spam ||
1494 actionId == R.id.report_phishing ||
1495 actionId == R.id.move_to;
1496 }
1497
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -08001498 @Override
Paul Westbrook57246a42013-04-21 09:40:22 -07001499 public boolean onOptionsItemSelected(MenuItem item) {
Andy Huang761522c2013-08-08 13:09:11 -07001500
Andy Huang12b3ee42013-04-24 22:49:43 -07001501 /*
1502 * The action bar home/up action should open or close the drawer.
1503 * mDrawerToggle will take care of this.
1504 */
Andy Huangf58e4c32014-07-09 16:58:18 -07001505 if (isDrawerEnabled() && mDrawerToggle.onOptionsItemSelected(item)) {
Andy Huang042a5302013-08-13 12:39:08 -07001506 Analytics.getInstance().sendEvent(Analytics.EVENT_CATEGORY_MENU_ITEM, "drawer_toggle",
1507 null, 0);
Andy Huang12b3ee42013-04-24 22:49:43 -07001508 return true;
1509 }
1510
Andy Huang2b555492013-08-14 21:06:21 -07001511 Analytics.getInstance().sendMenuItemEvent(Analytics.EVENT_CATEGORY_MENU_ITEM,
Andy Huangf8c59b02014-03-19 20:00:53 -07001512 item.getItemId(), "action_bar/" + mViewMode.getModeString(), 0);
Andy Huang042a5302013-08-13 12:39:08 -07001513
Vikram Aggarwal54452ae2012-03-13 15:29:00 -07001514 final int id = item.getItemId();
Vikram Aggarwal7dd054e2012-05-21 14:43:10 -07001515 LogUtils.d(LOG_TAG, "AbstractController.onOptionsItemSelected(%d) called.", id);
Mindy Pereira9b875682012-02-15 18:10:54 -08001516 boolean handled = true;
Vikram Aggarwal84fe9942013-04-17 11:20:58 -07001517 /** This is NOT a batch action. */
1518 final boolean isBatch = false;
Mindy Pereiraba68fda2012-05-24 15:53:06 -07001519 final Collection<Conversation> target = Conversation.listOf(mCurrentConversation);
Vikram Aggarwal112cd162012-06-18 10:51:13 -07001520 final Settings settings = (mAccount == null) ? null : mAccount.settings;
mindyp84f7d322012-10-01 17:14:40 -07001521 // The user is choosing a new action; commit whatever they had been
mindyp17a8e782012-11-29 14:56:17 -08001522 // doing before. Don't animate if we are launching a new screen.
1523 commitDestructiveActions(!doesActionChangeConversationListVisibility(id));
Jin Cao30c881a2014-04-08 14:28:36 -07001524 final UndoCallback undoCallback = getUndoCallbackForDestructiveActionsWithAutoAdvance(
1525 id, mCurrentConversation);
1526
Scott Kennedy2b9d80e2013-07-30 23:03:45 -07001527 if (id == R.id.archive) {
1528 final boolean showDialog = (settings != null && settings.confirmArchive);
Jin Cao30c881a2014-04-08 14:28:36 -07001529 confirmAndDelete(id, target, showDialog, R.plurals.confirm_archive_conversation, undoCallback);
Scott Kennedy2b9d80e2013-07-30 23:03:45 -07001530 } else if (id == R.id.remove_folder) {
1531 delete(R.id.remove_folder, target,
Jin Cao30c881a2014-04-08 14:28:36 -07001532 getDeferredRemoveFolder(target, mFolder, true, isBatch, true, undoCallback),
1533 isBatch);
Scott Kennedy2b9d80e2013-07-30 23:03:45 -07001534 } else if (id == R.id.delete) {
1535 final boolean showDialog = (settings != null && settings.confirmDelete);
Jin Cao30c881a2014-04-08 14:28:36 -07001536 confirmAndDelete(id, target, showDialog, R.plurals.confirm_delete_conversation, undoCallback);
Scott Kennedy2b9d80e2013-07-30 23:03:45 -07001537 } else if (id == R.id.discard_drafts) {
Andy Huang121c8b82013-08-05 11:52:55 -07001538 // drafts are lost forever, so always confirm
1539 confirmAndDelete(id, target, true /* showDialog */,
Jin Cao30c881a2014-04-08 14:28:36 -07001540 R.plurals.confirm_discard_drafts_conversation, undoCallback);
Jin Cao512821c2014-05-30 15:54:04 -07001541 } else if (id == R.id.discard_outbox) {
1542 // discard in outbox means we discard the failed message and save them in drafts
1543 delete(id, target, getDeferredAction(id, target, isBatch, undoCallback), isBatch);
Scott Kennedy2b9d80e2013-07-30 23:03:45 -07001544 } else if (id == R.id.mark_important) {
1545 updateConversation(Conversation.listOf(mCurrentConversation),
1546 ConversationColumns.PRIORITY, UIProvider.ConversationPriority.HIGH);
1547 } else if (id == R.id.mark_not_important) {
1548 if (mFolder != null && mFolder.isImportantOnly()) {
1549 delete(R.id.mark_not_important, target,
Jin Cao30c881a2014-04-08 14:28:36 -07001550 getDeferredAction(R.id.mark_not_important, target, isBatch, undoCallback),
1551 isBatch);
Scott Kennedy2b9d80e2013-07-30 23:03:45 -07001552 } else {
Vikram Aggarwal531488e2012-05-29 16:36:52 -07001553 updateConversation(Conversation.listOf(mCurrentConversation),
Scott Kennedy2b9d80e2013-07-30 23:03:45 -07001554 ConversationColumns.PRIORITY, UIProvider.ConversationPriority.LOW);
1555 }
1556 } else if (id == R.id.mute) {
Jin Cao30c881a2014-04-08 14:28:36 -07001557 delete(R.id.mute, target, getDeferredAction(R.id.mute, target, isBatch, undoCallback),
1558 isBatch);
Scott Kennedy2b9d80e2013-07-30 23:03:45 -07001559 } else if (id == R.id.report_spam) {
1560 delete(R.id.report_spam, target,
Jin Cao30c881a2014-04-08 14:28:36 -07001561 getDeferredAction(R.id.report_spam, target, isBatch, undoCallback), isBatch);
Scott Kennedy2b9d80e2013-07-30 23:03:45 -07001562 } else if (id == R.id.mark_not_spam) {
1563 // Currently, since spam messages are only shown in list with
1564 // other spam messages,
1565 // marking a message not as spam is a destructive action
1566 delete(R.id.mark_not_spam, target,
Jin Cao30c881a2014-04-08 14:28:36 -07001567 getDeferredAction(R.id.mark_not_spam, target, isBatch, undoCallback), isBatch);
Scott Kennedy2b9d80e2013-07-30 23:03:45 -07001568 } else if (id == R.id.report_phishing) {
1569 delete(R.id.report_phishing, target,
Jin Cao30c881a2014-04-08 14:28:36 -07001570 getDeferredAction(R.id.report_phishing, target, isBatch, undoCallback), isBatch);
Scott Kennedy2b9d80e2013-07-30 23:03:45 -07001571 } else if (id == android.R.id.home) {
Jin Cao405a3442014-08-25 13:49:33 -07001572 handleUpPress();
Scott Kennedy2b9d80e2013-07-30 23:03:45 -07001573 } else if (id == R.id.compose) {
1574 ComposeActivity.compose(mActivity.getActivityContext(), mAccount);
1575 } else if (id == R.id.refresh) {
1576 requestFolderRefresh();
1577 } else if (id == R.id.settings) {
1578 Utils.showSettings(mActivity.getActivityContext(), mAccount);
Scott Kennedy2b9d80e2013-07-30 23:03:45 -07001579 } else if (id == R.id.help_info_menu_item) {
Ray Chena57da3c2014-06-10 16:01:40 +02001580 mActivity.showHelp(mAccount, mViewMode.getMode());
Andrew Sapperstein6c570db2013-08-06 17:21:36 -07001581 } else if (id == R.id.move_to || id == R.id.change_folders) {
Tony Mantler2a4be242013-12-11 15:30:53 -08001582 final FolderSelectionDialog dialog = FolderSelectionDialog.getInstance(mAccount,
Scott Kennedy2b9d80e2013-07-30 23:03:45 -07001583 Conversation.listOf(mCurrentConversation), isBatch, mFolder,
1584 id == R.id.move_to);
1585 if (dialog != null) {
Tony Mantler2a4be242013-12-11 15:30:53 -08001586 dialog.show(mActivity.getFragmentManager(), null);
Scott Kennedy2b9d80e2013-07-30 23:03:45 -07001587 }
1588 } else if (id == R.id.move_to_inbox) {
1589 new AsyncTask<Void, Void, Folder>() {
1590 @Override
1591 protected Folder doInBackground(final Void... params) {
1592 // Get the "move to" inbox
1593 return Utils.getFolder(mContext, mAccount.settings.moveToInbox,
1594 true /* allowHidden */);
Mindy Pereira0d03ef82012-08-15 09:05:48 -07001595 }
Scott Kennedydd2ec682013-06-03 19:16:13 -07001596
Scott Kennedy2b9d80e2013-07-30 23:03:45 -07001597 @Override
1598 protected void onPostExecute(final Folder moveToInbox) {
1599 final List<FolderOperation> ops = Lists.newArrayListWithCapacity(1);
1600 // Add inbox
1601 ops.add(new FolderOperation(moveToInbox, true));
1602 assignFolder(ops, Conversation.listOf(mCurrentConversation), true,
1603 true /* showUndo */, false /* isMoveTo */);
1604 }
1605 }.execute((Void[]) null);
1606 } else if (id == R.id.empty_trash) {
1607 showEmptyDialog();
1608 } else if (id == R.id.empty_spam) {
1609 showEmptyDialog();
Jin Caod0334732014-09-01 22:33:14 -07001610 } else if (id == R.id.search) {
1611 mSearchViewController.showSearchActionBar(
1612 MaterialSearchViewController.SEARCH_VIEW_STATE_VISIBLE);
Scott Kennedy2b9d80e2013-07-30 23:03:45 -07001613 } else {
1614 handled = false;
Mindy Pereira28d5f722012-02-15 12:32:40 -08001615 }
Mindy Pereira9b875682012-02-15 18:10:54 -08001616 return handled;
Vikram Aggarwala55b36c2012-01-13 11:45:02 -08001617 }
1618
Andrew Sappersteined5b52d2013-04-30 13:40:18 -07001619 /**
1620 * Opens an {@link EmptyFolderDialogFragment} for the current folder.
1621 */
1622 private void showEmptyDialog() {
1623 if (mFolder != null) {
1624 final EmptyFolderDialogFragment fragment =
1625 EmptyFolderDialogFragment.newInstance(mFolder.totalCount, mFolder.type);
1626 fragment.setListener(this);
1627 fragment.show(mActivity.getFragmentManager(), EmptyFolderDialogFragment.FRAGMENT_TAG);
1628 }
1629 }
1630
1631 @Override
1632 public void onFolderEmptied() {
1633 emptyFolder();
1634 }
1635
1636 /**
1637 * Performs the work of emptying the currently visible folder.
1638 */
Scott Kennedy7ee089e2013-03-25 17:05:44 -04001639 private void emptyFolder() {
Yorke Lee1d0f1f82013-04-26 14:10:27 -07001640 if (mConversationListCursor != null) {
1641 mConversationListCursor.emptyFolder();
1642 }
Scott Kennedy7ee089e2013-03-25 17:05:44 -04001643 }
1644
Andrew Sappersteined5b52d2013-04-30 13:40:18 -07001645 private void attachEmptyFolderDialogFragmentListener() {
1646 final EmptyFolderDialogFragment fragment =
1647 (EmptyFolderDialogFragment) mActivity.getFragmentManager()
1648 .findFragmentByTag(EmptyFolderDialogFragment.FRAGMENT_TAG);
1649
1650 if (fragment != null) {
1651 fragment.setListener(this);
1652 }
1653 }
1654
Rohan Shah8e65c6d2013-03-07 16:47:25 -08001655 /**
Andy Huang12b3ee42013-04-24 22:49:43 -07001656 * Toggles the drawer pullout. If it was open (Fully extended), the
1657 * drawer will be closed. Otherwise, the drawer will be opened. This should
1658 * only be called when used with a toggle item. Other cases should be handled
1659 * explicitly with just closeDrawers() or openDrawer(View drawerView);
Rohan Shah8e65c6d2013-03-07 16:47:25 -08001660 */
Tony Mantlerd9eaca82013-08-05 11:42:29 -07001661 protected void toggleDrawerState() {
Andy Huang12b3ee42013-04-24 22:49:43 -07001662 if (!isDrawerEnabled()) {
1663 return;
1664 }
1665 if(mDrawerContainer.isDrawerOpen(mDrawerPullout)) {
1666 mDrawerContainer.closeDrawers();
1667 } else {
1668 mDrawerContainer.openDrawer(mDrawerPullout);
1669 }
Rohan Shah8e65c6d2013-03-07 16:47:25 -08001670 }
1671
Vikram Aggarwal531488e2012-05-29 16:36:52 -07001672 @Override
Andy Huangc1fb9a92013-02-11 13:09:12 -08001673 public final boolean onBackPressed() {
Andy Huang12b3ee42013-04-24 22:49:43 -07001674 if (isDrawerEnabled() && mDrawerContainer.isDrawerVisible(mDrawerPullout)) {
1675 mDrawerContainer.closeDrawers();
1676 return true;
Jin Caoc6801eb2014-08-12 18:16:57 -07001677 } else if (mSearchViewController.handleBackPress()) {
1678 return true;
Andrew Sapperstein4c928742014-08-29 15:34:23 -07001679 // If we're in CAB mode, let the activity handle onBackPressed.
1680 // It will handle closing CAB mode for us.
1681 } else if (mCabActionMenu != null && mCabActionMenu.isActivated()) {
1682 return false;
Andy Huang12b3ee42013-04-24 22:49:43 -07001683 }
1684
Andy Huangc1fb9a92013-02-11 13:09:12 -08001685 return handleBackPress();
1686 }
1687
1688 protected abstract boolean handleBackPress();
Greg Bullockede2e522014-05-30 14:11:35 +02001689
Andy Huangc1fb9a92013-02-11 13:09:12 -08001690 protected abstract boolean handleUpPress();
1691
1692 @Override
Mindy Pereira6c2663d2012-07-20 15:37:29 -07001693 public void updateConversation(Collection<Conversation> target, ContentValues values) {
Scott Kennedy9e2d4072013-03-21 21:46:01 -07001694 mConversationListCursor.updateValues(target, values);
Mindy Pereira6c2663d2012-07-20 15:37:29 -07001695 refreshConversationList();
1696 }
1697
1698 @Override
Vikram Aggarwal531488e2012-05-29 16:36:52 -07001699 public void updateConversation(Collection <Conversation> target, String columnName,
1700 boolean value) {
Scott Kennedy9e2d4072013-03-21 21:46:01 -07001701 mConversationListCursor.updateBoolean(target, columnName, value);
Vikram Aggarwal75daee52012-04-30 11:13:09 -07001702 refreshConversationList();
Vikram Aggarwal54452ae2012-03-13 15:29:00 -07001703 }
1704
Vikram Aggarwal531488e2012-05-29 16:36:52 -07001705 @Override
Mindy Pereira6c2663d2012-07-20 15:37:29 -07001706 public void updateConversation(Collection <Conversation> target, String columnName,
1707 int value) {
Scott Kennedy9e2d4072013-03-21 21:46:01 -07001708 mConversationListCursor.updateInt(target, columnName, value);
Vikram Aggarwal75daee52012-04-30 11:13:09 -07001709 refreshConversationList();
Mindy Pereirac9d59182012-03-22 16:06:46 -07001710 }
1711
Vikram Aggarwal531488e2012-05-29 16:36:52 -07001712 @Override
1713 public void updateConversation(Collection <Conversation> target, String columnName,
1714 String value) {
Scott Kennedy9e2d4072013-03-21 21:46:01 -07001715 mConversationListCursor.updateString(target, columnName, value);
Vikram Aggarwal75daee52012-04-30 11:13:09 -07001716 refreshConversationList();
Vikram Aggarwal54452ae2012-03-13 15:29:00 -07001717 }
1718
Andy Huang839ada22012-07-20 15:48:40 -07001719 @Override
Yu Ping Hu7c909c72013-01-18 11:58:01 -08001720 public void markConversationMessagesUnread(final Conversation conv,
1721 final Set<Uri> unreadMessageUris, final byte[] originalConversationInfo) {
Andy Huang8f6b0062012-07-31 15:36:31 -07001722 // The only caller of this method is the conversation view, from where marking unread should
1723 // *always* take you back to list mode.
1724 showConversation(null);
1725
Andy Huang839ada22012-07-20 15:48:40 -07001726 // locally mark conversation unread (the provider is supposed to propagate message unread
1727 // to conversation unread)
1728 conv.read = false;
Paul Westbrook1faf93d2012-10-16 08:58:07 -07001729 if (mConversationListCursor == null) {
Vikram Aggarwal81230ee2013-02-26 15:26:44 -08001730 LogUtils.d(LOG_TAG, "markConversationMessagesUnread(id=%d), deferring", conv.id);
Paul Westbrook1faf93d2012-10-16 08:58:07 -07001731
Yu Ping Hu7c909c72013-01-18 11:58:01 -08001732 mConversationListLoadFinishedCallbacks.add(new LoadFinishedCallback() {
1733 @Override
1734 public void onLoadFinished() {
1735 doMarkConversationMessagesUnread(conv, unreadMessageUris,
1736 originalConversationInfo);
1737 }
1738 });
1739 } else {
Vikram Aggarwal81230ee2013-02-26 15:26:44 -08001740 LogUtils.d(LOG_TAG, "markConversationMessagesUnread(id=%d), performing", conv.id);
Yu Ping Hu7c909c72013-01-18 11:58:01 -08001741 doMarkConversationMessagesUnread(conv, unreadMessageUris, originalConversationInfo);
1742 }
1743 }
1744
1745 private void doMarkConversationMessagesUnread(Conversation conv, Set<Uri> unreadMessageUris,
1746 byte[] originalConversationInfo) {
Vikram Aggarwal81230ee2013-02-26 15:26:44 -08001747 // Only do a granular 'mark unread' if a subset of messages are unread
Andy Huang28e31e22012-07-26 16:33:15 -07001748 final int unreadCount = (unreadMessageUris == null) ? 0 : unreadMessageUris.size();
Mindy Pereira0972e072012-08-01 17:43:06 -07001749 final int numMessages = conv.getNumMessages();
1750 final boolean subsetIsUnread = (numMessages > 1 && unreadCount > 0
1751 && unreadCount < numMessages);
Andy Huang28e31e22012-07-26 16:33:15 -07001752
Andy Huang9e4ca792013-02-28 14:33:43 -08001753 LogUtils.d(LOG_TAG, "markConversationMessagesUnread(conv=%s)"
Vikram Aggarwal81230ee2013-02-26 15:26:44 -08001754 + ", numMessages=%d, unreadCount=%d, subsetIsUnread=%b",
Andy Huang9e4ca792013-02-28 14:33:43 -08001755 conv, numMessages, unreadCount, subsetIsUnread);
Andy Huang28e31e22012-07-26 16:33:15 -07001756 if (!subsetIsUnread) {
Vikram Aggarwal66bc2aa2012-08-02 10:47:03 -07001757 // Conversations are neither marked read, nor viewed, and we don't want to show
1758 // the next conversation.
Vikram Aggarwal81230ee2013-02-26 15:26:44 -08001759 LogUtils.d(LOG_TAG, ". . doing full mark unread");
Scott Kennedycaaeed32013-06-12 13:39:16 -07001760 markConversationsRead(Collections.singletonList(conv), false, false, false);
Andy Huang839ada22012-07-20 15:48:40 -07001761 } else {
Vikram Aggarwal81230ee2013-02-26 15:26:44 -08001762 if (LogUtils.isLoggable(LOG_TAG, LogUtils.DEBUG)) {
1763 final ConversationInfo info = ConversationInfo.fromBlob(originalConversationInfo);
1764 LogUtils.d(LOG_TAG, ". . doing subset mark unread, originalConversationInfo = %s",
1765 info);
1766 }
Andy Huangdaa06ab2012-07-24 10:46:44 -07001767 mConversationListCursor.setConversationColumn(conv.uri, ConversationColumns.READ, 0);
Andy Huang839ada22012-07-20 15:48:40 -07001768
Vikram Aggarwal81230ee2013-02-26 15:26:44 -08001769 // Locally update conversation's conversationInfo to revert to original version
Andy Huang28e31e22012-07-26 16:33:15 -07001770 if (originalConversationInfo != null) {
1771 mConversationListCursor.setConversationColumn(conv.uri,
1772 ConversationColumns.CONVERSATION_INFO, originalConversationInfo);
1773 }
Andy Huang839ada22012-07-20 15:48:40 -07001774
1775 // applyBatch with each CPO as an UPDATE op on each affected message uri
1776 final ArrayList<ContentProviderOperation> ops = Lists.newArrayList();
1777 String authority = null;
1778 for (Uri messageUri : unreadMessageUris) {
1779 if (authority == null) {
1780 authority = messageUri.getAuthority();
1781 }
1782 ops.add(ContentProviderOperation.newUpdate(messageUri)
1783 .withValue(UIProvider.MessageColumns.READ, 0)
1784 .build());
Vikram Aggarwal81230ee2013-02-26 15:26:44 -08001785 LogUtils.d(LOG_TAG, ". . Adding op: read=0, uri=%s", messageUri);
Andy Huang839ada22012-07-20 15:48:40 -07001786 }
Vikram Aggarwal81230ee2013-02-26 15:26:44 -08001787 LogUtils.d(LOG_TAG, ". . operations = %s", ops);
Andy Huang839ada22012-07-20 15:48:40 -07001788 new ContentProviderTask() {
1789 @Override
1790 protected void onPostExecute(Result result) {
Vikram Aggarwald9895b62013-02-27 16:52:27 -08001791 if (result.exception != null) {
1792 LogUtils.e(LOG_TAG, result.exception, "ContentProviderTask() ERROR.");
1793 } else {
Andy Huang9e4ca792013-02-28 14:33:43 -08001794 LogUtils.d(LOG_TAG, "ContentProviderTask(): success %s",
1795 Arrays.toString(result.results));
Vikram Aggarwald9895b62013-02-27 16:52:27 -08001796 }
Andy Huang839ada22012-07-20 15:48:40 -07001797 }
1798 }.run(mResolver, authority, ops);
Andy Huang839ada22012-07-20 15:48:40 -07001799 }
Andy Huang839ada22012-07-20 15:48:40 -07001800 }
1801
1802 @Override
Yu Ping Hu7c909c72013-01-18 11:58:01 -08001803 public void markConversationsRead(final Collection<Conversation> targets, final boolean read,
1804 final boolean viewed) {
Scott Kennedy919d01a2013-05-07 16:13:29 -07001805 LogUtils.d(LOG_TAG, "markConversationsRead(targets=%s)", targets.toArray());
1806
Yu Ping Hu7c909c72013-01-18 11:58:01 -08001807 if (mConversationListCursor == null) {
Vikram Aggarwal81230ee2013-02-26 15:26:44 -08001808 if (LogUtils.isLoggable(LOG_TAG, LogUtils.DEBUG)) {
1809 LogUtils.d(LOG_TAG, "markConversationsRead(targets=%s), deferring",
1810 targets.toArray());
1811 }
Yu Ping Hu7c909c72013-01-18 11:58:01 -08001812 mConversationListLoadFinishedCallbacks.add(new LoadFinishedCallback() {
1813 @Override
1814 public void onLoadFinished() {
Scott Kennedycaaeed32013-06-12 13:39:16 -07001815 markConversationsRead(targets, read, viewed, true);
Yu Ping Hu7c909c72013-01-18 11:58:01 -08001816 }
1817 });
1818 } else {
1819 // We want to show the next conversation if we are marking unread.
Scott Kennedycaaeed32013-06-12 13:39:16 -07001820 markConversationsRead(targets, read, viewed, true);
Yu Ping Hu7c909c72013-01-18 11:58:01 -08001821 }
Andy Huang8f6b0062012-07-31 15:36:31 -07001822 }
1823
Scott Kennedy0d0f8b02012-10-12 15:18:18 -07001824 private void markConversationsRead(final Collection<Conversation> targets, final boolean read,
Scott Kennedycaaeed32013-06-12 13:39:16 -07001825 final boolean markViewed, final boolean showNext) {
Yu Ping Hu7c909c72013-01-18 11:58:01 -08001826 LogUtils.d(LOG_TAG, "performing markConversationsRead");
Vikram Aggarwalcc754b12012-08-30 14:04:21 -07001827 // Auto-advance if requested and the current conversation is being marked unread
Andy Huang8f6b0062012-07-31 15:36:31 -07001828 if (showNext && !read) {
Scott Kennedy0d0f8b02012-10-12 15:18:18 -07001829 final Runnable operation = new Runnable() {
1830 @Override
1831 public void run() {
Scott Kennedycaaeed32013-06-12 13:39:16 -07001832 markConversationsRead(targets, read, markViewed, showNext);
Scott Kennedy0d0f8b02012-10-12 15:18:18 -07001833 }
1834 };
1835
Scott Kennedycaaeed32013-06-12 13:39:16 -07001836 if (!showNextConversation(targets, operation)) {
Scott Kennedy0d0f8b02012-10-12 15:18:18 -07001837 // This method will be called again if the user selects an autoadvance option
1838 return;
1839 }
Andy Huang8f6b0062012-07-31 15:36:31 -07001840 }
Scott Kennedy0d0f8b02012-10-12 15:18:18 -07001841
Vikram Aggarwalcc754b12012-08-30 14:04:21 -07001842 final int size = targets.size();
1843 final List<ConversationOperation> opList = new ArrayList<ConversationOperation>(size);
1844 for (final Conversation target : targets) {
Paul Westbrooke2be97a2013-05-21 02:12:09 -07001845 final ContentValues value = new ContentValues(4);
Vikram Aggarwalcc754b12012-08-30 14:04:21 -07001846 value.put(ConversationColumns.READ, read);
Paul Westbrook5109c512012-11-05 11:00:30 -08001847
Scott Kennedyd5edd2d2012-12-05 11:11:32 -08001848 // We never want to mark unseen here, but we do want to mark it seen
1849 if (read || markViewed) {
1850 value.put(ConversationColumns.SEEN, Boolean.TRUE);
1851 }
1852
Paul Westbrook5109c512012-11-05 11:00:30 -08001853 // The mark read/unread/viewed operations do not show an undo bar
1854 value.put(ConversationOperations.Parameters.SUPPRESS_UNDO, true);
Vikram Aggarwal66bc2aa2012-08-02 10:47:03 -07001855 if (markViewed) {
Vikram Aggarwalcc754b12012-08-30 14:04:21 -07001856 value.put(ConversationColumns.VIEWED, true);
Vikram Aggarwal66bc2aa2012-08-02 10:47:03 -07001857 }
Vikram Aggarwal192fac12012-07-25 16:44:55 -07001858 final ConversationInfo info = target.conversationInfo;
Tony Mantleredd6c1a2013-10-08 14:47:43 -07001859 final boolean changed = info.markRead(read);
1860 if (changed) {
1861 value.put(ConversationColumns.CONVERSATION_INFO, info.toBlob());
Andy Huang839ada22012-07-20 15:48:40 -07001862 }
Vikram Aggarwalcc754b12012-08-30 14:04:21 -07001863 opList.add(mConversationListCursor.getOperationForConversation(
1864 target, ConversationOperation.UPDATE, value));
1865 // Update the local conversation objects so they immediately change state.
1866 target.read = read;
Andy Huangcd5c5ee2012-08-12 19:03:51 -07001867 if (markViewed) {
Vikram Aggarwalcc754b12012-08-30 14:04:21 -07001868 target.markViewed();
Andy Huangcd5c5ee2012-08-12 19:03:51 -07001869 }
Andy Huang839ada22012-07-20 15:48:40 -07001870 }
Scott Kennedy9e2d4072013-03-21 21:46:01 -07001871 mConversationListCursor.updateBulkValues(opList);
Andy Huang839ada22012-07-20 15:48:40 -07001872 }
1873
Andy Huang8f6b0062012-07-31 15:36:31 -07001874 /**
1875 * Auto-advance to a different conversation if the currently visible conversation in
1876 * conversation mode is affected (deleted, marked unread, etc.).
1877 *
Scott Kennedy0d0f8b02012-10-12 15:18:18 -07001878 * <p>Does nothing if outside of conversation mode.</p>
Andy Huang8f6b0062012-07-31 15:36:31 -07001879 *
1880 * @param target the set of conversations being deleted/marked unread
1881 */
mindyp9365a822012-09-12 09:09:09 -07001882 @Override
Scott Kennedy0d0f8b02012-10-12 15:18:18 -07001883 public void showNextConversation(final Collection<Conversation> target) {
Scott Kennedycaaeed32013-06-12 13:39:16 -07001884 showNextConversation(target, null);
Scott Kennedy0d0f8b02012-10-12 15:18:18 -07001885 }
1886
1887 /**
Jin Cao30c881a2014-04-08 14:28:36 -07001888 * Helper function to determine if the provided set of conversations is in view
1889 * @param target set of conversations that we are interested in
1890 * @return true if they are in view, false otherwise
1891 */
1892 private boolean isCurrentConversationInView(final Collection<Conversation> target) {
1893 final int viewMode = mViewMode.getMode();
1894 return (viewMode == ViewMode.CONVERSATION
1895 || viewMode == ViewMode.SEARCH_RESULTS_CONVERSATION)
1896 && Conversation.contains(target, mCurrentConversation);
1897 }
1898
1899 /**
Scott Kennedy0d0f8b02012-10-12 15:18:18 -07001900 * Auto-advance to a different conversation if the currently visible conversation in
1901 * conversation mode is affected (deleted, marked unread, etc.).
1902 *
1903 * <p>Does nothing if outside of conversation mode.</p>
Andy Huangc94d07f2013-06-03 16:19:35 -07001904 * <p>
1905 * Clients may pass an operation to execute on the target that this method will run after
1906 * auto-advance is complete. The operation, if provided, may run immediately, or it may run
1907 * later, or not at all. Reasons it may run later include:
1908 * <ul>
1909 * <li>the auto-advance setting is uninitialized and we need to wait for the user to set it</li>
1910 * <li>auto-advance in this configuration requires a mode change, and we need to wait for the
1911 * mode change transition to finish</li>
1912 * </ul>
1913 * <p>If the current conversation is not in the target collection, this method will do nothing,
1914 * and will not execute the operation.
Scott Kennedy0d0f8b02012-10-12 15:18:18 -07001915 *
1916 * @param target the set of conversations being deleted/marked unread
Andy Huangc94d07f2013-06-03 16:19:35 -07001917 * @param operation (optional) the operation to execute after advancing
1918 * @return <code>false</code> if this method handled or will execute the operation,
1919 * <code>true</code> otherwise.
Scott Kennedy0d0f8b02012-10-12 15:18:18 -07001920 */
1921 private boolean showNextConversation(final Collection<Conversation> target,
Scott Kennedycaaeed32013-06-12 13:39:16 -07001922 final Runnable operation) {
Jin Cao30c881a2014-04-08 14:28:36 -07001923 if (isCurrentConversationInView(target)) {
Scott Kennedy0d0f8b02012-10-12 15:18:18 -07001924 final int autoAdvanceSetting = mAccount.settings.getAutoAdvanceSetting();
1925
Tony Mantler93e64572014-07-11 14:35:56 -07001926 // If we don't have one set, but we're here, just take the default
1927 final int autoAdvance = (autoAdvanceSetting == AutoAdvance.UNSET) ?
1928 AutoAdvance.DEFAULT : autoAdvanceSetting;
Scott Kennedy0d0f8b02012-10-12 15:18:18 -07001929
Tony Mantler93e64572014-07-11 14:35:56 -07001930 final Conversation next = mTracker.getNextConversation(autoAdvance, target);
1931 LogUtils.d(LOG_TAG, "showNextConversation: showing %s next.", next);
1932 // Set mAutoAdvanceOp *before* showConversation() to ensure that it runs when the
1933 // transition doesn't run (i.e. it "completes" immediately).
1934 mAutoAdvanceOp = operation;
1935 showConversation(next);
1936 return (mAutoAdvanceOp == null);
Andy Huang8f6b0062012-07-31 15:36:31 -07001937 }
Scott Kennedy0d0f8b02012-10-12 15:18:18 -07001938
1939 return true;
1940 }
1941
Andy Huang839ada22012-07-20 15:48:40 -07001942 @Override
1943 public void starMessage(ConversationMessage msg, boolean starred) {
1944 if (msg.starred == starred) {
1945 return;
1946 }
1947
1948 msg.starred = starred;
1949
1950 // locally propagate the change to the owning conversation
1951 // (figure the provider will properly propagate the change when it commits it)
1952 //
1953 // when unstarring, only propagate the change if this was the only message starred
1954 final boolean conversationStarred = starred || msg.isConversationStarred();
Andy Huangcd12e822012-11-08 19:50:57 -08001955 final Conversation conv = msg.getConversation();
1956 if (conversationStarred != conv.starred) {
1957 conv.starred = conversationStarred;
1958 mConversationListCursor.setConversationColumn(conv.uri,
Andy Huang839ada22012-07-20 15:48:40 -07001959 ConversationColumns.STARRED, conversationStarred);
1960 }
1961
1962 final ContentValues values = new ContentValues(1);
1963 values.put(UIProvider.MessageColumns.STARRED, starred ? 1 : 0);
1964
1965 new ContentProviderTask.UpdateTask() {
1966 @Override
1967 protected void onPostExecute(Result result) {
1968 // TODO: handle errors?
1969 }
1970 }.run(mResolver, msg.uri, values, null /* selection*/, null /* selectionArgs */);
1971 }
1972
Andy Huang12b3ee42013-04-24 22:49:43 -07001973 @Override
Alice Yang486e63e2013-04-05 13:01:50 -07001974 public void requestFolderRefresh() {
Alice Yang37dda442013-03-26 22:48:53 -07001975 if (mFolder == null) {
1976 return;
Mindy Pereira28e0c342012-02-17 15:05:13 -08001977 }
Alice Yang37dda442013-03-26 22:48:53 -07001978 final ConversationListFragment convList = getConversationListFragment();
1979 if (convList == null) {
1980 // This could happen if this account is in initial sync (user
1981 // is seeing the "your mail will appear shortly" message)
1982 return;
1983 }
1984 convList.showSyncStatusBar();
1985
1986 if (mAsyncRefreshTask != null) {
1987 mAsyncRefreshTask.cancel(true);
1988 }
1989 mAsyncRefreshTask = new AsyncRefreshTask(mContext, mFolder.refreshUri);
1990 mAsyncRefreshTask.execute();
Mindy Pereira28e0c342012-02-17 15:05:13 -08001991 }
1992
Mindy Pereirafbe40192012-03-20 10:40:45 -07001993 /**
1994 * Confirm (based on user's settings) and delete a conversation from the conversation list and
1995 * from the database.
Vikram Aggarwalb8c31712013-01-03 17:03:19 -08001996 * @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 -07001997 * @param target the conversations to act upon
1998 * @param showDialog true if a confirmation dialog is to be shown, false otherwise.
1999 * @param confirmResource the resource ID of the string that is shown in the confirmation dialog
Mindy Pereirafbe40192012-03-20 10:40:45 -07002000 */
Vikram Aggarwalb8c31712013-01-03 17:03:19 -08002001 private void confirmAndDelete(int actionId, final Collection<Conversation> target,
Jin Cao30c881a2014-04-08 14:28:36 -07002002 boolean showDialog, int confirmResource, UndoCallback undoCallback) {
Vikram Aggarwal84fe9942013-04-17 11:20:58 -07002003 final boolean isBatch = false;
Mindy Pereirafbe40192012-03-20 10:40:45 -07002004 if (showDialog) {
Jin Cao30c881a2014-04-08 14:28:36 -07002005 makeDialogListener(actionId, isBatch, undoCallback);
Vikram Aggarwal3d7ca9d2012-05-11 14:40:36 -07002006 final CharSequence message = Utils.formatPlural(mContext, confirmResource,
2007 target.size());
Vikram Aggarwalb8c31712013-01-03 17:03:19 -08002008 final ConfirmDialogFragment c = ConfirmDialogFragment.newInstance(message);
2009 c.displayDialog(mActivity.getFragmentManager());
Mindy Pereirafbe40192012-03-20 10:40:45 -07002010 } else {
Jin Cao30c881a2014-04-08 14:28:36 -07002011 delete(0, target, getDeferredAction(actionId, target, isBatch, undoCallback), isBatch);
Mindy Pereirafbe40192012-03-20 10:40:45 -07002012 }
2013 }
2014
Vikram Aggarwal4f4782b2012-05-30 08:39:09 -07002015 @Override
Scott Kennedy0d0f8b02012-10-12 15:18:18 -07002016 public void delete(final int actionId, final Collection<Conversation> target,
Scott Kennedycaaeed32013-06-12 13:39:16 -07002017 final DestructiveAction action, final boolean isBatch) {
mindyp84f7d322012-10-01 17:14:40 -07002018 // Order of events is critical! The Conversation View Fragment must be
2019 // notified of the next conversation with showConversation(next) *before* the
2020 // conversation list
Vikram Aggarwal192fac12012-07-25 16:44:55 -07002021 // fragment has a chance to delete the conversation, animating it away.
2022
mindyp84f7d322012-10-01 17:14:40 -07002023 // Update the conversation fragment if the current conversation is
2024 // deleted.
Scott Kennedy0d0f8b02012-10-12 15:18:18 -07002025 final Runnable operation = new Runnable() {
2026 @Override
2027 public void run() {
Scott Kennedycaaeed32013-06-12 13:39:16 -07002028 delete(actionId, target, action, isBatch);
Scott Kennedy0d0f8b02012-10-12 15:18:18 -07002029 }
2030 };
2031
Rohan Shah52bab6f2014-08-26 18:52:14 -07002032 showNextConversation(target, operation);
2033
Vikram Aggarwalab1b5b62013-04-16 15:45:50 -07002034 // If the conversation is in the selected set, remove it from the set.
Vikram Aggarwal84fe9942013-04-17 11:20:58 -07002035 // Batch selections are cleared in the end of the action, so not done for batch actions.
2036 if (!isBatch) {
2037 for (final Conversation conv : target) {
Jin Caoec0fa482014-08-28 16:38:08 -07002038 if (mCheckedSet.contains(conv)) {
2039 mCheckedSet.toggle(conv);
Vikram Aggarwal84fe9942013-04-17 11:20:58 -07002040 }
Vikram Aggarwalab1b5b62013-04-16 15:45:50 -07002041 }
2042 }
Vikram Aggarwal192fac12012-07-25 16:44:55 -07002043 // The conversation list deletes and performs the action if it exists.
2044 final ConversationListFragment convListFragment = getConversationListFragment();
2045 if (convListFragment != null) {
Alice Yang193e05a2013-05-05 14:12:08 -07002046 LogUtils.i(LOG_TAG, "AAC.requestDelete: ListFragment is handling delete.");
Vikram Aggarwal669947b2013-01-10 17:05:56 -08002047 convListFragment.requestDelete(actionId, target, action);
Vikram Aggarwal192fac12012-07-25 16:44:55 -07002048 return;
2049 }
mindyp84f7d322012-10-01 17:14:40 -07002050 // No visible UI element handled it on our behalf. Perform the action
2051 // ourself.
Alice Yang193e05a2013-05-05 14:12:08 -07002052 LogUtils.i(LOG_TAG, "ACC.requestDelete: performing remove action ourselves");
Vikram Aggarwald503df42012-05-11 10:13:35 -07002053 action.performAction();
2054 }
2055
2056 /**
2057 * Requests that the action be performed and the UI state is updated to reflect the new change.
Vikram Aggarwal715f83d2013-03-05 17:04:56 -08002058 * @param action the action to be performed, specified as a menu id: R.id.archive, ...
Vikram Aggarwald503df42012-05-11 10:13:35 -07002059 */
Vikram Aggarwal715f83d2013-03-05 17:04:56 -08002060 private void requestUpdate(final DestructiveAction action) {
Vikram Aggarwald503df42012-05-11 10:13:35 -07002061 action.performAction();
2062 refreshConversationList();
Vikram Aggarwal75daee52012-04-30 11:13:09 -07002063 }
Vikram Aggarwal6fbc87a2012-03-15 15:24:00 -07002064
2065 @Override
Vikram Aggarwala55b36c2012-01-13 11:45:02 -08002066 public void onPrepareDialog(int id, Dialog dialog, Bundle bundle) {
2067 // TODO(viki): Auto-generated method stub
Vikram Aggarwala55b36c2012-01-13 11:45:02 -08002068 }
2069
2070 @Override
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -08002071 public boolean onPrepareOptionsMenu(Menu menu) {
Andrew Sapperstein2d86d112014-07-24 20:27:37 -07002072 return mActionBarController.onPrepareOptionsMenu(menu);
Vikram Aggarwala55b36c2012-01-13 11:45:02 -08002073 }
2074
Mindy Pereira68f2e222012-03-07 10:36:54 -08002075 @Override
2076 public void onPause() {
Vikram Aggarwalc04cf7e2013-05-13 15:38:42 -07002077 mHaveAccountList = false;
Paul Westbrook6ead20d2012-03-19 14:48:14 -07002078 enableNotifications();
Paul Westbrook94e440d2012-02-24 11:03:47 -08002079 }
2080
Vikram Aggarwala55b36c2012-01-13 11:45:02 -08002081 @Override
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -08002082 public void onResume() {
Paul Westbrook6ead20d2012-03-19 14:48:14 -07002083 // Register the receiver that will prevent the status receiver from
2084 // displaying its notification icon as long as we're running.
2085 // The SupressNotificationReceiver will block the broadcast if we're looking at the folder
2086 // that the notification was received for.
2087 disableNotifications();
Andy Huang1ee96b22012-08-24 20:19:53 -07002088
2089 mSafeToModifyFragments = true;
Andrew Sappersteined5b52d2013-04-30 13:40:18 -07002090
2091 attachEmptyFolderDialogFragmentListener();
Andrew Sapperstein23e33132013-05-07 18:26:49 -07002092
2093 // Invalidating the options menu so that when we make changes in settings,
2094 // the changes will always be updated in the action bar/options menu/
2095 mActivity.invalidateOptionsMenu();
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -08002096 }
2097
2098 @Override
2099 public void onSaveInstanceState(Bundle outState) {
Vikram Aggarwal649b9ea2012-08-27 12:15:20 -07002100 mViewMode.handleSaveInstanceState(outState);
Vikram Aggarwal6c511582012-02-27 10:59:47 -08002101 if (mAccount != null) {
Vikram Aggarwal6c511582012-02-27 10:59:47 -08002102 outState.putParcelable(SAVED_ACCOUNT, mAccount);
2103 }
Mindy Pereira5e478d22012-03-26 18:04:58 -07002104 if (mFolder != null) {
2105 outState.putParcelable(SAVED_FOLDER, mFolder);
Vikram Aggarwal8b152632012-02-03 14:58:45 -08002106 }
Vikram Aggarwalf3341402012-08-07 10:09:38 -07002107 // If this is a search activity, let's store the search query term as well.
2108 if (ConversationListContext.isSearchResult(mConvListContext)) {
2109 outState.putString(SAVED_QUERY, mConvListContext.searchQuery);
2110 }
Vikram Aggarwal49e0e992012-09-21 13:53:15 -07002111 if (mCurrentConversation != null && mViewMode.isConversationMode()) {
Mindy Pereira26f23fc2012-03-27 10:26:04 -07002112 outState.putParcelable(SAVED_CONVERSATION, mCurrentConversation);
2113 }
Jin Caoec0fa482014-08-28 16:38:08 -07002114 if (!mCheckedSet.isEmpty()) {
2115 outState.putParcelable(SAVED_SELECTED_SET, mCheckedSet);
Andy Huang4556a442012-03-30 16:42:05 -07002116 }
Mindy Pereirad33674992012-06-25 16:26:30 -07002117 if (mToastBar.getVisibility() == View.VISIBLE) {
2118 outState.putParcelable(SAVED_TOAST_BAR_OP, mToastBar.getOperation());
2119 }
Vikram Aggarwal649b9ea2012-08-27 12:15:20 -07002120 final ConversationListFragment convListFragment = getConversationListFragment();
Mindy Pereirad33674992012-06-25 16:26:30 -07002121 if (convListFragment != null) {
Vikram Aggarwal649b9ea2012-08-27 12:15:20 -07002122 convListFragment.getAnimatedAdapter().onSaveInstanceState(outState);
Mindy Pereirad33674992012-06-25 16:26:30 -07002123 }
Vikram Aggarwalb8c31712013-01-03 17:03:19 -08002124 // 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 -08002125 if (mDialogAction != -1) {
2126 outState.putInt(SAVED_ACTION, mDialogAction);
Vikram Aggarwalb8c31712013-01-03 17:03:19 -08002127 outState.putBoolean(SAVED_ACTION_FROM_SELECTED, mDialogFromSelectedSet);
Vikram Aggarwal6cadbfc2012-12-27 09:17:05 -08002128 }
Vikram Aggarwala91d00b2013-01-18 12:00:37 -08002129 if (mDetachedConvUri != null) {
2130 outState.putParcelable(SAVED_DETACHED_CONV_URI, mDetachedConvUri);
2131 }
Andrew Sapperstein5747e152013-05-13 14:13:08 -07002132
Scott Kennedyb10212e2013-02-22 16:27:00 -08002133 outState.putParcelable(SAVED_HIERARCHICAL_FOLDER, mFolderListFolder);
Andrew Sapperstein5747e152013-05-13 14:13:08 -07002134 mSafeToModifyFragments = false;
Alice Yangebeef1b2013-09-04 06:41:10 +00002135
2136 outState.putParcelable(SAVED_INBOX_KEY, mInbox);
Scott Kennedyf77806e2013-08-30 11:38:15 -07002137
2138 outState.putBundle(SAVED_CONVERSATION_LIST_SCROLL_POSITIONS,
2139 mConversationListScrollPositions);
Jin Caoc6801eb2014-08-12 18:16:57 -07002140
2141 mSearchViewController.saveState(outState);
Andy Huang1ee96b22012-08-24 20:19:53 -07002142 }
2143
2144 /**
2145 * @see #mSafeToModifyFragments
2146 */
2147 protected boolean safeToModifyFragments() {
2148 return mSafeToModifyFragments;
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -08002149 }
2150
2151 @Override
Andy Huang313ac132013-03-04 23:40:58 -08002152 public void executeSearch(String query) {
Jin Cao779dd602014-04-22 16:16:28 -07002153 AnalyticsTimer.getInstance().trackStart(AnalyticsTimer.SEARCH_TO_LIST);
Mindy Pereira68f2e222012-03-07 10:36:54 -08002154 Intent intent = new Intent();
2155 intent.setAction(Intent.ACTION_SEARCH);
2156 intent.putExtra(ConversationListContext.EXTRA_SEARCH_QUERY, query);
2157 intent.putExtra(Utils.EXTRA_ACCOUNT, mAccount);
2158 intent.setComponent(mActivity.getComponentName());
Jin Caoc6801eb2014-08-12 18:16:57 -07002159 mSearchViewController.showSearchActionBar(
2160 MaterialSearchViewController.SEARCH_VIEW_STATE_GONE);
Martin Hibdon371a71c2014-02-19 13:55:28 -08002161 // Call startActivityForResult here so we can tell if we have navigated to a different folder
2162 // or account from search results.
2163 mActivity.startActivityForResult(intent, CHANGE_NAVIGATION_REQUEST_CODE);
Vikram Aggarwala55b36c2012-01-13 11:45:02 -08002164 }
2165
2166 @Override
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -08002167 public void onStop() {
Scott Kennedycb85aea2013-02-25 13:08:32 -08002168 NotificationActionUtils.unregisterUndoNotificationObserver(mUndoNotificationObserver);
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -08002169 }
2170
Andy Huang632721e2012-04-11 16:57:26 -07002171 @Override
2172 public void onDestroy() {
Andy Huangb2ef9c12012-12-18 12:58:41 -06002173 // stop listening to the cursor on e.g. configuration changes
2174 if (mConversationListCursor != null) {
2175 mConversationListCursor.removeListener(this);
2176 }
Andy Huang144bfe72013-06-11 13:27:52 -07002177 mDrawIdler.setListener(null);
2178 mDrawIdler.setRootView(null);
Andy Huang632721e2012-04-11 16:57:26 -07002179 // unregister the ViewPager's observer on the conversation cursor
2180 mPagerController.onDestroy();
Andrew Sapperstein2d86d112014-07-24 20:27:37 -07002181 mActionBarController.onDestroy();
Vikram Aggarwal7c401b72012-08-13 16:43:47 -07002182 mRecentFolderList.destroy();
Andy Huang4e0158f2012-08-07 21:06:01 -07002183 mDestroyed = true;
Vikram Aggarwal59f741f2013-03-01 15:55:40 -08002184 mHandler.removeCallbacks(mLogServiceChecker);
2185 mLogServiceChecker = null;
Jin Caoc6801eb2014-08-12 18:16:57 -07002186 mSearchViewController.onDestroy();
Andy Huang632721e2012-04-11 16:57:26 -07002187 }
2188
Vikram Aggarwalfa131a22012-02-02 13:56:22 -08002189 /**
Vikram Aggarwal93dc2022012-11-05 13:36:57 -08002190 * Set the Action Bar icon according to the mode. The Action Bar icon can contain a back button
2191 * or not. The individual controller is responsible for changing the icon based on the mode.
2192 */
2193 protected abstract void resetActionBarIcon();
2194
2195 /**
Jin Caof84fe2e2014-08-26 16:29:36 -07002196 * When should the compose button be visible
2197 */
2198 protected boolean isComposeVisible(int mode) {
2199 return mode == ViewMode.CONVERSATION_LIST;
2200 }
2201
2202 /**
Mindy Pereira161f50d2012-02-28 15:47:19 -08002203 * {@inheritDoc} Subclasses must override this to listen to mode changes
2204 * from the ViewMode. Subclasses <b>must</b> call the parent's
2205 * onViewModeChanged since the parent will handle common state changes.
Vikram Aggarwalfa131a22012-02-02 13:56:22 -08002206 */
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -08002207 @Override
Vikram Aggarwalfa131a22012-02-02 13:56:22 -08002208 public void onViewModeChanged(int newMode) {
James Lemieux1a1e9752014-07-18 13:50:40 -07002209 // The floating action compose button is only visible in the conversation/search lists
Jin Caof84fe2e2014-08-26 16:29:36 -07002210 final int composeVisible = isComposeVisible(newMode) ? View.VISIBLE : View.GONE;
James Lemieux1a1e9752014-07-18 13:50:40 -07002211 mFloatingComposeButton.setVisibility(composeVisible);
2212
Vikram Aggarwal49e0e992012-09-21 13:53:15 -07002213 // When we step away from the conversation mode, we don't have a current conversation
2214 // anymore. Let's blank it out so clients calling getCurrentConversation are not misled.
2215 if (!ViewMode.isConversationMode(newMode)) {
2216 setCurrentConversation(null);
2217 }
Andrew Sapperstein6c570db2013-08-06 17:21:36 -07002218
Vikram Aggarwal93dc2022012-11-05 13:36:57 -08002219 // If the viewmode is not set, preserve existing icon.
2220 if (newMode != ViewMode.UNKNOWN) {
2221 resetActionBarIcon();
2222 }
Andy Huang12b3ee42013-04-24 22:49:43 -07002223
2224 if (isDrawerEnabled()) {
Vikram Aggarwaldfbc6982013-07-03 14:39:31 -07002225 /** If the folder doesn't exist, or its parent URI is empty,
2226 * this is not a child folder */
Jin Cao9695e002014-05-29 11:56:44 -07002227 final boolean isTopLevel = Folder.isRoot(mFolder);
Vikram Aggarwaldfbc6982013-07-03 14:39:31 -07002228 mDrawerToggle.setDrawerIndicatorEnabled(
2229 getShouldShowDrawerIndicator(newMode, isTopLevel));
Martin Hibdon371a71c2014-02-19 13:55:28 -08002230 mDrawerContainer.setDrawerLockMode(DrawerLayout.LOCK_MODE_UNLOCKED);
Andy Huang12b3ee42013-04-24 22:49:43 -07002231 closeDrawerIfOpen();
2232 }
Vikram Aggarwala55b36c2012-01-13 11:45:02 -08002233 }
2234
Vikram Aggarwaldfbc6982013-07-03 14:39:31 -07002235 /**
2236 * Returns true if the drawer icon is shown
2237 * @param viewMode the current view mode
2238 * @param isTopLevel true if the current folder is not a child
2239 * @return whether the drawer indicator is shown
2240 */
Scott Kennedy81b5fa02013-07-11 14:39:18 -07002241 private boolean getShouldShowDrawerIndicator(final int viewMode,
Vikram Aggarwaldfbc6982013-07-03 14:39:31 -07002242 final boolean isTopLevel) {
2243 // If search list/conv mode: disable indicator
2244 // Indicator is enabled either in conversation list or folder list mode.
Scott Kennedy81b5fa02013-07-11 14:39:18 -07002245 return isDrawerEnabled() && !ViewMode.isSearchMode(viewMode)
Scott Kennedyaded5782013-07-16 14:21:53 -07002246 && (viewMode == ViewMode.CONVERSATION_LIST && isTopLevel);
Scott Kennedy8a72b852013-05-02 14:18:50 -07002247 }
2248
Andy Huang3825f3d2012-08-29 16:44:12 -07002249 public void disablePagerUpdates() {
2250 mPagerController.stopListening();
2251 }
2252
Andy Huang4e0158f2012-08-07 21:06:01 -07002253 public boolean isDestroyed() {
2254 return mDestroyed;
2255 }
2256
mindyp54f120f2012-08-28 13:10:33 -07002257 @Override
2258 public void commitDestructiveActions(boolean animate) {
mindypc6adce32012-08-22 18:46:42 -07002259 ConversationListFragment fragment = getConversationListFragment();
Mindy Pereira1e2573b2012-04-17 14:34:36 -07002260 if (fragment != null) {
mindypc6adce32012-08-22 18:46:42 -07002261 fragment.commitDestructiveActions(animate);
Mindy Pereira1e2573b2012-04-17 14:34:36 -07002262 }
2263 }
2264
Vikram Aggarwala55b36c2012-01-13 11:45:02 -08002265 @Override
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -08002266 public void onWindowFocusChanged(boolean hasFocus) {
Vikram Aggarwal69b5c302012-09-05 11:11:13 -07002267 final ConversationListFragment convList = getConversationListFragment();
Vikram Aggarwal6422b8d2013-01-14 10:47:41 -08002268 // hasFocus already ensures that the window is in focus, so we don't need to call
2269 // AAC.isFragmentVisible(convList) here.
Paul Westbrook9f119c72012-04-24 16:10:59 -07002270 if (hasFocus && convList != null && convList.isVisible()) {
2271 // The conversation list is visible.
Vikram Aggarwal69b5c302012-09-05 11:11:13 -07002272 informCursorVisiblity(true);
Paul Westbrook9f119c72012-04-24 16:10:59 -07002273 }
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -08002274 }
2275
Vikram Aggarwalca716f12012-08-20 11:11:48 -07002276 /**
2277 * Set the account, and carry out all the account-related changes that rely on this.
Vikram Aggarwal715f83d2013-03-05 17:04:56 -08002278 * @param account new account to set to.
Vikram Aggarwalca716f12012-08-20 11:11:48 -07002279 */
Mindy Pereira75181e82012-04-18 08:17:13 -07002280 private void setAccount(Account account) {
Andy Huangb9ca9792012-05-18 15:31:49 -07002281 if (account == null) {
2282 LogUtils.w(LOG_TAG, new Error(),
2283 "AAC ignoring null (presumably invalid) account restoration");
2284 return;
2285 }
Andy Huangb1148412012-05-19 00:16:30 -07002286 LogUtils.d(LOG_TAG, "AbstractActivityController.setAccount(): account = %s", account.uri);
Vikram Aggarwal91e87372012-05-18 15:36:04 -07002287 mAccount = account;
Régis Décamps2168cbc2014-08-22 15:00:37 +02002288
2289 Analytics.getInstance().setEmail(account.getEmailAddress(), account.getType());
2290
Vikram Aggarwalca716f12012-08-20 11:11:48 -07002291 // Only change AAC state here. Do *not* modify any other object's state. The object
2292 // should listen on account changes.
Vikram Aggarwal177097f2013-03-08 11:19:53 -08002293 restartOptionalLoader(LOADER_RECENT_FOLDERS, mFolderCallbacks, Bundle.EMPTY);
Vikram Aggarwalca716f12012-08-20 11:11:48 -07002294 mActivity.invalidateOptionsMenu();
2295 disableNotificationsOnAccountChange(mAccount);
Vikram Aggarwal177097f2013-03-08 11:19:53 -08002296 restartOptionalLoader(LOADER_ACCOUNT_UPDATE_CURSOR, mAccountCallbacks, Bundle.EMPTY);
Vikram Aggarwal2fe343f2013-01-14 09:00:25 -08002297 // The Mail instance can be null during test runs.
2298 final MailAppProvider instance = MailAppProvider.getInstance();
2299 if (instance != null) {
2300 instance.setLastViewedAccount(mAccount.uri.toString());
2301 }
Vikram Aggarwal91e87372012-05-18 15:36:04 -07002302 if (account.settings == null) {
2303 LogUtils.w(LOG_TAG, new Error(), "AAC ignoring account with null settings.");
2304 return;
2305 }
Vikram Aggarwal7c401b72012-08-13 16:43:47 -07002306 mAccountObservers.notifyChanged();
Vikram Aggarwal34f7b232012-10-17 13:32:23 -07002307 perhapsEnterWaitMode();
Mindy Pereira75181e82012-04-18 08:17:13 -07002308 }
2309
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -08002310 /**
Mindy Pereira161f50d2012-02-28 15:47:19 -08002311 * Restore the state from the previous bundle. Subclasses should call this
2312 * method from the parent class, since it performs important UI
2313 * initialization.
2314 *
Vikram Aggarwal715f83d2013-03-05 17:04:56 -08002315 * @param savedState previous state
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -08002316 */
Andy Huang632721e2012-04-11 16:57:26 -07002317 @Override
2318 public void onRestoreInstanceState(Bundle savedState) {
Vikram Aggarwala91d00b2013-01-18 12:00:37 -08002319 mDetachedConvUri = savedState.getParcelable(SAVED_DETACHED_CONV_URI);
Andy Huang632721e2012-04-11 16:57:26 -07002320 if (savedState.containsKey(SAVED_CONVERSATION)) {
2321 // Open the conversation.
Andy Huang2bc8bc12012-11-12 17:24:25 -08002322 final Conversation conversation = savedState.getParcelable(SAVED_CONVERSATION);
Paul Westbrook534e4a22012-04-25 03:46:29 -07002323 if (conversation != null && conversation.position < 0) {
2324 // Set the position to 0 on this conversation, as we don't know where it is
2325 // in the list
2326 conversation.position = 0;
2327 }
Andy Huanged4fdf02012-07-26 17:12:50 -07002328 showConversation(conversation);
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -08002329 }
Mindy Pereira967ede62012-03-22 09:29:09 -07002330
Mindy Pereirad33674992012-06-25 16:26:30 -07002331 if (savedState.containsKey(SAVED_TOAST_BAR_OP)) {
Andy Huang2bc8bc12012-11-12 17:24:25 -08002332 ToastBarOperation op = savedState.getParcelable(SAVED_TOAST_BAR_OP);
Mindy Pereirad33674992012-06-25 16:26:30 -07002333 if (op != null) {
2334 if (op.getType() == ToastBarOperation.UNDO) {
2335 onUndoAvailable(op);
Andrew Sapperstein9d7519d2012-07-16 14:03:53 -07002336 } else if (op.getType() == ToastBarOperation.ERROR) {
2337 onError(mFolder, true);
Mindy Pereirad33674992012-06-25 16:26:30 -07002338 }
2339 }
2340 }
Scott Kennedyb10212e2013-02-22 16:27:00 -08002341 mFolderListFolder = savedState.getParcelable(SAVED_HIERARCHICAL_FOLDER);
Vikram Aggarwal649b9ea2012-08-27 12:15:20 -07002342 final ConversationListFragment convListFragment = getConversationListFragment();
Mindy Pereirad33674992012-06-25 16:26:30 -07002343 if (convListFragment != null) {
Vikram Aggarwal649b9ea2012-08-27 12:15:20 -07002344 convListFragment.getAnimatedAdapter().onRestoreInstanceState(savedState);
Mindy Pereirad33674992012-06-25 16:26:30 -07002345 }
Vikram Aggarwalb8c31712013-01-03 17:03:19 -08002346 /*
Mindy Pereira967ede62012-03-22 09:29:09 -07002347 * Restore the state of selected conversations. This needs to be done after the correct mode
2348 * is set and the action bar is fully initialized. If not, several key pieces of state
2349 * information will be missing, and the split views may not be initialized correctly.
Mindy Pereira967ede62012-03-22 09:29:09 -07002350 */
Andy Huang4556a442012-03-30 16:42:05 -07002351 restoreSelectedConversations(savedState);
Vikram Aggarwalb8c31712013-01-03 17:03:19 -08002352 // Order is important!!!
2353 // The dialog listener needs to happen *after* the selected set is restored.
2354
2355 // If there has been an orientation change, and we need to recreate the listener for the
2356 // confirm dialog fragment (delete/archive/...), then do it here.
2357 if (mDialogAction != -1) {
Jin Cao30c881a2014-04-08 14:28:36 -07002358 makeDialogListener(mDialogAction, mDialogFromSelectedSet,
2359 getUndoCallbackForDestructiveActionsWithAutoAdvance(
2360 mDialogAction, mCurrentConversation));
Vikram Aggarwalb8c31712013-01-03 17:03:19 -08002361 }
Alice Yangebeef1b2013-09-04 06:41:10 +00002362
2363 mInbox = savedState.getParcelable(SAVED_INBOX_KEY);
Scott Kennedyf77806e2013-08-30 11:38:15 -07002364
2365 mConversationListScrollPositions.clear();
2366 mConversationListScrollPositions.putAll(
2367 savedState.getBundle(SAVED_CONVERSATION_LIST_SCROLL_POSITIONS));
Andy Huang632721e2012-04-11 16:57:26 -07002368 }
2369
Vikram Aggarwal649b9ea2012-08-27 12:15:20 -07002370 /**
2371 * Handle an intent to open the app. This method is called only when there is no saved state,
2372 * so we need to set state that wasn't set before. It is correct to change the viewmode here
2373 * since it has not been previously set.
Vikram Aggarwal6aca6892013-06-04 13:53:27 -07002374 *
2375 * This method is called for a subset of the reasons mentioned in
2376 * {@link #onCreate(android.os.Bundle)}. Notably, this is called when launching the app from
2377 * notifications, widgets, and shortcuts.
Vikram Aggarwal715f83d2013-03-05 17:04:56 -08002378 * @param intent intent passed to the activity.
Vikram Aggarwal649b9ea2012-08-27 12:15:20 -07002379 */
Andy Huang632721e2012-04-11 16:57:26 -07002380 private void handleIntent(Intent intent) {
Andy Huange6459422013-04-01 16:32:18 -07002381 LogUtils.d(LOG_TAG, "IN AAC.handleIntent. action=%s", intent.getAction());
Andy Huang632721e2012-04-11 16:57:26 -07002382 if (Intent.ACTION_VIEW.equals(intent.getAction())) {
2383 if (intent.hasExtra(Utils.EXTRA_ACCOUNT)) {
Tony Mantler26a20752014-02-28 16:44:24 -08002384 setAccount(Account.newInstance(intent.getStringExtra(Utils.EXTRA_ACCOUNT)));
Andy Huang632721e2012-04-11 16:57:26 -07002385 }
Andy Huangb9ca9792012-05-18 15:31:49 -07002386 if (mAccount == null) {
2387 return;
Andy Huang632721e2012-04-11 16:57:26 -07002388 }
Vikram Aggarwal1a249e02012-08-03 16:19:33 -07002389 final boolean isConversationMode = intent.hasExtra(Utils.EXTRA_CONVERSATION);
Andy Huang4fe0af82013-08-20 17:24:51 -07002390
2391 if (intent.getBooleanExtra(Utils.EXTRA_FROM_NOTIFICATION, false)) {
Régis Décamps2168cbc2014-08-22 15:00:37 +02002392 Analytics.getInstance().setEmail(mAccount.getEmailAddress(), mAccount.getType());
Andy Huang4fe0af82013-08-20 17:24:51 -07002393 Analytics.getInstance().sendEvent("notification_click",
2394 isConversationMode ? "conversation" : "conversation_list", null, 0);
2395 }
2396
Vikram Aggarwal649b9ea2012-08-27 12:15:20 -07002397 if (isConversationMode && mViewMode.getMode() == ViewMode.UNKNOWN) {
Vikram Aggarwal1a249e02012-08-03 16:19:33 -07002398 mViewMode.enterConversationMode();
2399 } else {
2400 mViewMode.enterConversationListMode();
2401 }
Vikram Aggarwal177097f2013-03-08 11:19:53 -08002402 // Put the folder and conversation, and ask the loader to create this folder.
2403 final Bundle args = new Bundle();
Scott Kennedy48cfe462013-04-10 11:32:02 -07002404
2405 final Uri folderUri;
2406 if (intent.hasExtra(Utils.EXTRA_FOLDER_URI)) {
Andrew Sapperstein5bb4d052014-03-31 16:22:31 -07002407 folderUri = intent.getParcelableExtra(Utils.EXTRA_FOLDER_URI);
Scott Kennedy48cfe462013-04-10 11:32:02 -07002408 } else if (intent.hasExtra(Utils.EXTRA_FOLDER)) {
2409 final Folder folder =
2410 Folder.fromString(intent.getStringExtra(Utils.EXTRA_FOLDER));
Scott Kennedy259df5b2013-07-11 13:24:01 -07002411 folderUri = folder.folderUri.fullUri;
Scott Kennedy48cfe462013-04-10 11:32:02 -07002412 } else {
Scott Kennedy72727ef2013-05-01 18:10:55 -07002413 final Bundle extras = intent.getExtras();
2414 LogUtils.d(LOG_TAG, "Couldn't find a folder URI in the extras: %s",
2415 extras == null ? "null" : extras.toString());
Scott Kennedy48cfe462013-04-10 11:32:02 -07002416 folderUri = mAccount.settings.defaultInbox;
2417 }
2418
Andrew Sapperstein5bb4d052014-03-31 16:22:31 -07002419 // Check if we should load all conversations instead of using
2420 // the default behavior which loads an initial subset.
2421 mIgnoreInitialConversationLimit =
2422 intent.getBooleanExtra(Utils.EXTRA_IGNORE_INITIAL_CONVERSATION_LIMIT, false);
2423
Scott Kennedy60593352013-03-13 13:45:30 -07002424 args.putParcelable(Utils.EXTRA_FOLDER_URI, folderUri);
Vikram Aggarwal177097f2013-03-08 11:19:53 -08002425 args.putParcelable(Utils.EXTRA_CONVERSATION,
2426 intent.getParcelableExtra(Utils.EXTRA_CONVERSATION));
2427 restartOptionalLoader(LOADER_FIRST_FOLDER, mFolderCallbacks, args);
Andy Huang632721e2012-04-11 16:57:26 -07002428 } else if (Intent.ACTION_SEARCH.equals(intent.getAction())) {
2429 if (intent.hasExtra(Utils.EXTRA_ACCOUNT)) {
Vikram Aggarwalf0ef2302012-11-07 14:53:34 -08002430 mHaveSearchResults = false;
Jin Caoc6801eb2014-08-12 18:16:57 -07002431 // Save this search query for future suggestions
Andy Huang632721e2012-04-11 16:57:26 -07002432 final String query = intent.getStringExtra(SearchManager.QUERY);
Jin Caoc6801eb2014-08-12 18:16:57 -07002433 mSearchViewController.saveRecentQuery(query);
Vikram Aggarwalf0ef2302012-11-07 14:53:34 -08002434 setAccount((Account) intent.getParcelableExtra(Utils.EXTRA_ACCOUNT));
2435 fetchSearchFolder(intent);
2436 if (shouldEnterSearchConvMode()) {
Mindy Pereiraac254822012-06-18 10:46:43 -07002437 mViewMode.enterSearchResultsConversationMode();
2438 } else {
2439 mViewMode.enterSearchResultsListMode();
2440 }
Andy Huang632721e2012-04-11 16:57:26 -07002441 } else {
2442 LogUtils.e(LOG_TAG, "Missing account extra from search intent. Finishing");
2443 mActivity.finish();
2444 }
2445 }
2446 if (mAccount != null) {
Vikram Aggarwal177097f2013-03-08 11:19:53 -08002447 restartOptionalLoader(LOADER_ACCOUNT_UPDATE_CURSOR, mAccountCallbacks, Bundle.EMPTY);
Scott Kennedyb39aaf52013-03-06 19:17:22 -08002448 }
2449 }
2450
Andy Huang4556a442012-03-30 16:42:05 -07002451 /**
Vikram Aggarwalf0ef2302012-11-07 14:53:34 -08002452 * Returns true if we should enter conversation mode with search.
2453 */
2454 protected final boolean shouldEnterSearchConvMode() {
2455 return mHaveSearchResults && Utils.showTwoPaneSearchResults(mActivity.getActivityContext());
2456 }
2457
2458 /**
Andy Huang4556a442012-03-30 16:42:05 -07002459 * Copy any selected conversations stored in the saved bundle into our selection set,
2460 * triggering {@link ConversationSetObserver} callbacks as our selection set changes.
2461 *
2462 */
Vikram Aggarwal715f83d2013-03-05 17:04:56 -08002463 private void restoreSelectedConversations(Bundle savedState) {
Mindy Pereira967ede62012-03-22 09:29:09 -07002464 if (savedState == null) {
Jin Caoec0fa482014-08-28 16:38:08 -07002465 mCheckedSet.clear();
Mindy Pereira967ede62012-03-22 09:29:09 -07002466 return;
2467 }
Jin Caoec0fa482014-08-28 16:38:08 -07002468 final ConversationCheckedSet selectedSet = savedState.getParcelable(SAVED_SELECTED_SET);
Andy Huang4556a442012-03-30 16:42:05 -07002469 if (selectedSet == null || selectedSet.isEmpty()) {
Jin Caoec0fa482014-08-28 16:38:08 -07002470 mCheckedSet.clear();
Mindy Pereira967ede62012-03-22 09:29:09 -07002471 return;
2472 }
Andy Huang632721e2012-04-11 16:57:26 -07002473
2474 // putAll will take care of calling our registered onSetPopulated method
Jin Caoec0fa482014-08-28 16:38:08 -07002475 mCheckedSet.putAll(selectedSet);
Mindy Pereira967ede62012-03-22 09:29:09 -07002476 }
2477
Vikram Aggarwalec5cbf72012-03-08 15:10:35 -08002478 /**
Vikram Aggarwal49e0e992012-09-21 13:53:15 -07002479 * Show the conversation provided in the arguments. It is safe to pass a null conversation
2480 * object, which is a signal to back out of conversation view mode.
2481 * Child classes must call super.showConversation() <b>before</b> their own implementations.
Vikram Aggarwal59f741f2013-03-01 15:55:40 -08002482 * @param conversation the conversation to be shown, or null if we want to back out to list
2483 * mode.
Vikram Aggarwal177097f2013-03-08 11:19:53 -08002484 * onLoadFinished(Loader, Cursor) on any callback.
Vikram Aggarwalec5cbf72012-03-08 15:10:35 -08002485 */
Jin Cao30c881a2014-04-08 14:28:36 -07002486 protected void showConversation(Conversation conversation) {
Jin Caof8471262014-08-14 18:51:52 -07002487 showConversation(conversation, false /* peek */);
Jin Caod23f6d12014-08-08 15:23:20 -07002488 }
2489
Jin Caof8471262014-08-14 18:51:52 -07002490 protected void showConversation(Conversation conversation, boolean peek) {
Andy Huang243c2362013-03-01 17:50:35 -08002491 if (conversation != null) {
2492 Utils.sConvLoadTimer.start();
2493 }
2494
Andy Huang54e925e2013-03-14 13:24:18 -07002495 MailLogService.log("AbstractActivityController", "showConversation(%s)", conversation);
Vikram Aggarwalc67182d2012-04-03 14:35:06 -07002496 // Set the current conversation just in case it wasn't already set.
2497 setCurrentConversation(conversation);
Vikram Aggarwalec5cbf72012-03-08 15:10:35 -08002498 }
2499
Vikram Aggarwale128fc22012-04-04 12:33:34 -07002500 /**
Jin Cao405a3442014-08-25 13:49:33 -07002501 * Show the wait for account initialization mode.
Paul Westbrook2d50bcd2012-04-10 11:53:47 -07002502 * Children can override this method, but they must call super.showWaitForInitialization().
Paul Westbrook2d50bcd2012-04-10 11:53:47 -07002503 */
Jin Cao405a3442014-08-25 13:49:33 -07002504 protected void showWaitForInitialization() {
Paul Westbrook2d50bcd2012-04-10 11:53:47 -07002505 mViewMode.enterWaitingForInitializationMode();
Andy Huangc96efcc2014-04-09 15:30:42 -07002506 mWaitFragment = WaitFragment.newInstance(mAccount, true /* expectingMessages */);
Paul Westbrook2d50bcd2012-04-10 11:53:47 -07002507 }
2508
Vikram Aggarwaldd6a7ce2012-10-22 15:45:57 -07002509 private void updateWaitMode() {
Paul Westbrook2d50bcd2012-04-10 11:53:47 -07002510 final FragmentManager manager = mActivity.getFragmentManager();
2511 final WaitFragment waitFragment =
Vikram Aggarwal6902dcf2012-04-11 08:57:42 -07002512 (WaitFragment)manager.findFragmentByTag(TAG_WAIT);
Paul Westbrook2d50bcd2012-04-10 11:53:47 -07002513 if (waitFragment != null) {
2514 waitFragment.updateAccount(mAccount);
2515 }
2516 }
2517
Vikram Aggarwala3f43d42012-10-25 16:21:30 -07002518 /**
2519 * Remove the "Waiting for Initialization" fragment. Child classes are free to override this
2520 * method, though they must call the parent implementation <b>after</b> they do anything.
2521 */
2522 protected void hideWaitForInitialization() {
2523 mWaitFragment = null;
2524 }
2525
2526 /**
2527 * Use the instance variable and the wait fragment's tag to get the wait fragment. This is
2528 * far superior to using the value of mWaitFragment, which might be invalid or might refer
2529 * to a fragment after it has been destroyed.
Vikram Aggarwal715f83d2013-03-05 17:04:56 -08002530 * @return a wait fragment that is already attached to the activity, if one exists
Vikram Aggarwala3f43d42012-10-25 16:21:30 -07002531 */
2532 protected final WaitFragment getWaitFragment() {
2533 final FragmentManager manager = mActivity.getFragmentManager();
2534 final WaitFragment waitFrag = (WaitFragment) manager.findFragmentByTag(TAG_WAIT);
2535 if (waitFrag != null) {
2536 // The Fragment Manager knows better, so use its instance.
2537 mWaitFragment = waitFrag;
2538 }
2539 return mWaitFragment;
2540 }
Vikram Aggarwaldd6a7ce2012-10-22 15:45:57 -07002541
Vikram Aggarwal48b2a6c2012-05-29 14:09:27 -07002542 /**
2543 * Returns true if we are waiting for the account to sync, and cannot show any folders or
2544 * conversation for the current account yet.
Vikram Aggarwal48b2a6c2012-05-29 14:09:27 -07002545 */
Vikram Aggarwaldd6a7ce2012-10-22 15:45:57 -07002546 private boolean inWaitMode() {
Vikram Aggarwala3f43d42012-10-25 16:21:30 -07002547 final WaitFragment waitFragment = getWaitFragment();
Paul Westbrook2d50bcd2012-04-10 11:53:47 -07002548 if (waitFragment != null) {
2549 final Account fragmentAccount = waitFragment.getAccount();
Paul Westbrook339004b2012-11-05 17:13:51 -08002550 return fragmentAccount != null && fragmentAccount.uri.equals(mAccount.uri) &&
Paul Westbrook2d50bcd2012-04-10 11:53:47 -07002551 mViewMode.getMode() == ViewMode.WAITING_FOR_ACCOUNT_INITIALIZATION;
2552 }
2553 return false;
2554 }
2555
2556 /**
Jin Cao405a3442014-08-25 13:49:33 -07002557 * Show the conversation List with the list context provided here. On certain layouts, this
2558 * might show more than just the conversation list. For instance, on tablets this might show
2559 * the conversations along with the conversation list.
2560 * @param listContext context providing information on what conversation list to display.
Vikram Aggarwale128fc22012-04-04 12:33:34 -07002561 */
Jin Cao405a3442014-08-25 13:49:33 -07002562 protected abstract void showConversationList(ConversationListContext listContext);
Vikram Aggarwale128fc22012-04-04 12:33:34 -07002563
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -08002564 @Override
Jin Cao0b693382014-08-11 10:46:12 -07002565 public void onConversationSelected(Conversation conversation, boolean inLoaderCallbacks) {
Alice Yangfe8e0812013-04-22 13:37:26 -07002566 final ConversationListFragment convListFragment = getConversationListFragment();
2567 if (convListFragment != null && convListFragment.getAnimatedAdapter() != null) {
2568 convListFragment.getAnimatedAdapter().onConversationSelected();
2569 }
mindypaa55bc92012-08-24 09:49:56 -07002570 // Only animate destructive actions if we are going to be showing the
2571 // conversation list when we show the next conversation.
Vikram Aggarwalbcb16b92013-01-28 18:05:03 -08002572 commitDestructiveActions(mIsTablet);
Jin Cao30c881a2014-04-08 14:28:36 -07002573 showConversation(conversation);
Andy Huang1ee96b22012-08-24 20:19:53 -07002574 }
2575
2576 @Override
Andrew Sapperstein2f542872013-06-11 10:48:30 -07002577 public final void onCabModeEntered() {
2578 final ConversationListFragment convListFragment = getConversationListFragment();
2579 if (convListFragment != null && convListFragment.getAnimatedAdapter() != null) {
2580 convListFragment.getAnimatedAdapter().onCabModeEntered();
2581 }
2582 }
2583
2584 @Override
Scott Kennedycc139832013-08-19 18:03:54 -07002585 public final void onCabModeExited() {
2586 final ConversationListFragment convListFragment = getConversationListFragment();
2587 if (convListFragment != null && convListFragment.getAnimatedAdapter() != null) {
2588 convListFragment.getAnimatedAdapter().onCabModeExited();
2589 }
2590 }
2591
2592 @Override
Andy Huang1ee96b22012-08-24 20:19:53 -07002593 public Conversation getCurrentConversation() {
2594 return mCurrentConversation;
Vikram Aggarwal7d602882012-02-07 15:01:20 -08002595 }
Mindy Pereira555140c2012-02-15 14:55:29 -08002596
Vikram Aggarwalc67182d2012-04-03 14:35:06 -07002597 /**
2598 * Set the current conversation. This is the conversation on which all actions are performed.
2599 * Do not modify mCurrentConversation except through this method, which makes it easy to
2600 * perform common actions associated with changing the current conversation.
Vikram Aggarwal715f83d2013-03-05 17:04:56 -08002601 * @param conversation new conversation to view. Passing null indicates that we are backing
2602 * out to conversation list mode.
Vikram Aggarwalc67182d2012-04-03 14:35:06 -07002603 */
Andy Huang632721e2012-04-11 16:57:26 -07002604 @Override
2605 public void setCurrentConversation(Conversation conversation) {
Vikram Aggarwala91d00b2013-01-18 12:00:37 -08002606 // The controller should come out of detached mode if a new conversation is viewed, or if
Vikram Aggarwal81230ee2013-02-26 15:26:44 -08002607 // we are going back to conversation list mode.
2608 if (mDetachedConvUri != null && (conversation == null
2609 || !mDetachedConvUri.equals(conversation.uri))) {
Vikram Aggarwala91d00b2013-01-18 12:00:37 -08002610 clearDetachedMode();
2611 }
2612
2613 // Must happen *before* setting mCurrentConversation because this sets
2614 // conversation.position if a cursor is available.
Andy Huang8883f222012-11-12 19:25:00 -08002615 mTracker.initialize(conversation);
Vikram Aggarwal17f373e2012-09-17 15:49:32 -07002616 mCurrentConversation = conversation;
Andy Huang7d646122012-09-05 19:41:44 -07002617
2618 if (mCurrentConversation != null) {
Andrew Sapperstein2d86d112014-07-24 20:27:37 -07002619 mActionBarController.setCurrentConversation(mCurrentConversation);
Yorke Leef807ba72012-09-20 17:18:05 -07002620 mActivity.invalidateOptionsMenu();
Andy Huang7d646122012-09-05 19:41:44 -07002621 }
Mindy Pereira5040f1a2012-03-20 10:14:06 -07002622 }
2623
Vikram Aggarwala9b93f32012-02-23 14:51:58 -08002624 /**
Andy Huangf9a73482012-03-13 15:54:02 -07002625 * {@link LoaderManager} currently has a bug in
2626 * {@link LoaderManager#restartLoader(int, Bundle, android.app.LoaderManager.LoaderCallbacks)}
2627 * where, if a previous onCreateLoader returned a null loader, this method will NPE. Work around
2628 * this bug by destroying any loaders that may have been created as null (essentially because
2629 * they are optional loads, and may not apply to a particular account).
2630 * <p>
2631 * A simple null check before restarting a loader will not work, because that would not
2632 * give the controller a chance to invalidate UI corresponding the prior loader result.
2633 *
2634 * @param id loader ID to safely restart
Vikram Aggarwal177097f2013-03-08 11:19:53 -08002635 * @param handler the LoaderCallback which will handle this loader ID.
2636 * @param args arguments, if any, to be passed to the loader. Use {@link Bundle#EMPTY} if no
2637 * arguments need to be specified.
Andy Huangf9a73482012-03-13 15:54:02 -07002638 */
Vikram Aggarwal177097f2013-03-08 11:19:53 -08002639 private void restartOptionalLoader(int id, LoaderManager.LoaderCallbacks handler, Bundle args) {
Andy Huangf9a73482012-03-13 15:54:02 -07002640 final LoaderManager lm = mActivity.getLoaderManager();
2641 lm.destroyLoader(id);
Vikram Aggarwal177097f2013-03-08 11:19:53 -08002642 lm.restartLoader(id, args, handler);
Vikram Aggarwal94c94de2012-04-04 15:38:28 -07002643 }
2644
Andy Huang632721e2012-04-11 16:57:26 -07002645 @Override
2646 public void registerConversationListObserver(DataSetObserver observer) {
2647 mConversationListObservable.registerObserver(observer);
2648 }
2649
2650 @Override
2651 public void unregisterConversationListObserver(DataSetObserver observer) {
Alice Yang647aefb2013-03-04 19:13:11 -08002652 try {
2653 mConversationListObservable.unregisterObserver(observer);
2654 } catch (IllegalStateException e) {
2655 // Log instead of crash
2656 LogUtils.e(LOG_TAG, e, "unregisterConversationListObserver called for an observer that "
2657 + "hasn't been registered");
2658 }
Andy Huang632721e2012-04-11 16:57:26 -07002659 }
2660
Andy Huang090db1e2012-07-25 13:25:28 -07002661 @Override
2662 public void registerFolderObserver(DataSetObserver observer) {
2663 mFolderObservable.registerObserver(observer);
2664 }
2665
2666 @Override
2667 public void unregisterFolderObserver(DataSetObserver observer) {
Alice Yang647aefb2013-03-04 19:13:11 -08002668 try {
2669 mFolderObservable.unregisterObserver(observer);
2670 } catch (IllegalStateException e) {
2671 // Log instead of crash
2672 LogUtils.e(LOG_TAG, e, "unregisterFolderObserver called for an observer that "
2673 + "hasn't been registered");
2674 }
Andy Huang090db1e2012-07-25 13:25:28 -07002675 }
2676
Andy Huang9d3fd922012-09-26 22:23:58 -07002677 @Override
2678 public void registerConversationLoadedObserver(DataSetObserver observer) {
2679 mPagerController.registerConversationLoadedObserver(observer);
2680 }
2681
2682 @Override
2683 public void unregisterConversationLoadedObserver(DataSetObserver observer) {
Alice Yang647aefb2013-03-04 19:13:11 -08002684 try {
2685 mPagerController.unregisterConversationLoadedObserver(observer);
2686 } catch (IllegalStateException e) {
2687 // Log instead of crash
2688 LogUtils.e(LOG_TAG, e, "unregisterConversationLoadedObserver called for an observer "
2689 + "that hasn't been registered");
2690 }
Andy Huang9d3fd922012-09-26 22:23:58 -07002691 }
2692
Vikram Aggarwal60069912012-07-24 14:26:09 -07002693 /**
Vikram Aggarwalc04cf7e2013-05-13 15:38:42 -07002694 * Returns true if the number of accounts is different, or if the current account has
2695 * changed. This method is meant to filter frequent changes to the list of
2696 * accounts, and only return true if the new list is substantially different from the existing
2697 * list. Returning true is safe here, it leads to more work in creating the
2698 * same account list again.
Vikram Aggarwal715f83d2013-03-05 17:04:56 -08002699 * @param accountCursor the cursor which points to all the accounts.
2700 * @return true if the number of accounts is changed or current account missing from the list.
Vikram Aggarwal60069912012-07-24 14:26:09 -07002701 */
Vikram Aggarwal177097f2013-03-08 11:19:53 -08002702 private boolean accountsUpdated(ObjectCursor<Account> accountCursor) {
Paul Westbrook23b74b92012-02-29 11:36:12 -08002703 // Check to see if the current account hasn't been set, or the account cursor is empty
2704 if (mAccount == null || !accountCursor.moveToFirst()) {
Vikram Aggarwal6c511582012-02-27 10:59:47 -08002705 return true;
Paul Westbrook23b74b92012-02-29 11:36:12 -08002706 }
2707
2708 // Check to see if the number of accounts are different, from the number we saw on the last
2709 // updated
2710 if (mCurrentAccountUris.size() != accountCursor.getCount()) {
2711 return true;
2712 }
2713
2714 // Check to see if the account list is different or if the current account is not found in
2715 // the cursor.
2716 boolean foundCurrentAccount = false;
Vikram Aggarwal7dedb952012-02-16 16:10:23 -08002717 do {
Vikram Aggarwalc04cf7e2013-05-13 15:38:42 -07002718 final Account account = accountCursor.getModel();
2719 if (!foundCurrentAccount && mAccount.uri.equals(account.uri)) {
2720 if (mAccount.settingsDiffer(account)) {
2721 // Settings changed, and we don't need to look any further.
2722 return true;
2723 }
Paul Westbrook23b74b92012-02-29 11:36:12 -08002724 foundCurrentAccount = true;
2725 }
Vikram Aggarwal60069912012-07-24 14:26:09 -07002726 // Is there a new account that we do not know about?
Vikram Aggarwalc04cf7e2013-05-13 15:38:42 -07002727 if (!mCurrentAccountUris.contains(account.uri)) {
Paul Westbrook23b74b92012-02-29 11:36:12 -08002728 return true;
2729 }
Vikram Aggarwal7dedb952012-02-16 16:10:23 -08002730 } while (accountCursor.moveToNext());
Paul Westbrook23b74b92012-02-29 11:36:12 -08002731
2732 // As long as we found the current account, the list hasn't been updated
2733 return !foundCurrentAccount;
Vikram Aggarwal7dedb952012-02-16 16:10:23 -08002734 }
2735
2736 /**
Vikram Aggarwal60069912012-07-24 14:26:09 -07002737 * Updates accounts for the app. If the current account is missing, the first
2738 * account in the list is set to the current account (we <em>have</em> to choose something).
Mindy Pereira161f50d2012-02-28 15:47:19 -08002739 *
Vikram Aggarwal6c511582012-02-27 10:59:47 -08002740 * @param accounts cursor into the AccountCache
Vikram Aggarwal7dedb952012-02-16 16:10:23 -08002741 * @return true if the update was successful, false otherwise
2742 */
Vikram Aggarwal177097f2013-03-08 11:19:53 -08002743 private boolean updateAccounts(ObjectCursor<Account> accounts) {
Vikram Aggarwal7dedb952012-02-16 16:10:23 -08002744 if (accounts == null || !accounts.moveToFirst()) {
2745 return false;
2746 }
Paul Westbrook23b74b92012-02-29 11:36:12 -08002747
Vikram Aggarwala9b93f32012-02-23 14:51:58 -08002748 final Account[] allAccounts = Account.getAllAccounts(accounts);
Vikram Aggarwal60069912012-07-24 14:26:09 -07002749 // A match for the current account's URI in the list of accounts.
2750 Account currentFromList = null;
Paul Westbrook23b74b92012-02-29 11:36:12 -08002751
Vikram Aggarwal636c1a12012-09-22 16:02:40 -07002752 // Save the uris for the accounts and find the current account in the updated cursor.
Paul Westbrook23b74b92012-02-29 11:36:12 -08002753 mCurrentAccountUris.clear();
Vikram Aggarwal636c1a12012-09-22 16:02:40 -07002754 for (final Account account : allAccounts) {
Vikram Aggarwal60069912012-07-24 14:26:09 -07002755 LogUtils.d(LOG_TAG, "updateAccounts(%s)", account);
Paul Westbrook23b74b92012-02-29 11:36:12 -08002756 mCurrentAccountUris.add(account.uri);
Vikram Aggarwal60069912012-07-24 14:26:09 -07002757 if (mAccount != null && account.uri.equals(mAccount.uri)) {
2758 currentFromList = account;
2759 }
Paul Westbrook23b74b92012-02-29 11:36:12 -08002760 }
2761
Vikram Aggarwal60069912012-07-24 14:26:09 -07002762 // 1. current account is already set and is in allAccounts:
2763 // 1a. It has changed -> load the updated account.
2764 // 2b. It is unchanged -> no-op
Andy Huang0d647352012-03-21 21:48:16 -07002765 // 2. current account is set and is not in allAccounts -> pick first (acct was deleted?)
Vikram Aggarwal60069912012-07-24 14:26:09 -07002766 // 3. saved preference has an account -> pick that one
Andy Huang0d647352012-03-21 21:48:16 -07002767 // 4. otherwise just pick first
2768
Vikram Aggarwal60069912012-07-24 14:26:09 -07002769 boolean accountChanged = false;
2770 /// Assume case 4, initialize to first account, and see if we can find anything better.
2771 Account newAccount = allAccounts[0];
2772 if (currentFromList != null) {
2773 // Case 1: Current account exists but has changed
2774 if (!currentFromList.equals(mAccount)) {
2775 newAccount = currentFromList;
2776 accountChanged = true;
Andy Huang0d647352012-03-21 21:48:16 -07002777 }
Vikram Aggarwal60069912012-07-24 14:26:09 -07002778 // Case 1b: else, current account is unchanged: nothing to do.
Paul Westbrook23b74b92012-02-29 11:36:12 -08002779 } else {
Vikram Aggarwal60069912012-07-24 14:26:09 -07002780 // Case 2: Current account is not in allAccounts, the account needs to change.
2781 accountChanged = true;
2782 if (mAccount == null) {
2783 // Case 3: Check for last viewed account, and check if it exists in the list.
2784 final String lastAccountUri = MailAppProvider.getInstance().getLastViewedAccount();
2785 if (lastAccountUri != null) {
2786 for (final Account account : allAccounts) {
2787 if (lastAccountUri.equals(account.uri.toString())) {
2788 newAccount = account;
2789 break;
2790 }
Andy Huang0d647352012-03-21 21:48:16 -07002791 }
2792 }
2793 }
Paul Westbrook23b74b92012-02-29 11:36:12 -08002794 }
Vikram Aggarwal60069912012-07-24 14:26:09 -07002795 if (accountChanged) {
Vikram Aggarwal5fd8afd2013-03-13 15:28:47 -07002796 changeAccount(newAccount);
Vikram Aggarwal60069912012-07-24 14:26:09 -07002797 }
Vikram Aggarwal07dbaa62013-03-12 15:21:04 -07002798
Vikram Aggarwal60069912012-07-24 14:26:09 -07002799 // Whether we have updated the current account or not, we need to update the list of
2800 // accounts in the ActionBar.
Rohan Shahb905f0e2013-04-26 09:17:37 -07002801 mAllAccounts = allAccounts;
Vikram Aggarwal07dbaa62013-03-12 15:21:04 -07002802 mAllAccountObservers.notifyChanged();
Vikram Aggarwala9b93f32012-02-23 14:51:58 -08002803 return (allAccounts.length > 0);
Vikram Aggarwal7dedb952012-02-16 16:10:23 -08002804 }
2805
Paul Westbrook6ead20d2012-03-19 14:48:14 -07002806 private void disableNotifications() {
2807 mNewEmailReceiver.activate(mContext, this);
2808 }
2809
2810 private void enableNotifications() {
2811 mNewEmailReceiver.deactivate();
2812 }
2813
2814 private void disableNotificationsOnAccountChange(Account account) {
2815 // If the new mail suppression receiver is activated for a different account, we want to
2816 // activate it for the new account.
2817 if (mNewEmailReceiver.activated() &&
2818 !mNewEmailReceiver.notificationsDisabledForAccount(account)) {
2819 // Deactivate the current receiver, otherwise multiple receivers may be registered.
2820 mNewEmailReceiver.deactivate();
2821 mNewEmailReceiver.activate(mContext, this);
2822 }
2823 }
2824
Vikram Aggarwala9b93f32012-02-23 14:51:58 -08002825 /**
Vikram Aggarwalc7694222012-04-23 13:37:01 -07002826 * Destructive actions on Conversations. This class should only be created by controllers, and
2827 * clients should only require {@link DestructiveAction}s, not specific implementations of the.
2828 * Only the controllers should know what kind of destructive actions are being created.
2829 */
Mindy Pereirade3e74a2012-07-24 09:43:10 -07002830 public class ConversationAction implements DestructiveAction {
Vikram Aggarwalacaa3c02012-04-24 12:45:27 -07002831 /**
2832 * The action to be performed. This is specified as the resource ID of the menu item
2833 * corresponding to this action: R.id.delete, R.id.report_spam, etc.
2834 */
Vikram Aggarwal7f602f72012-04-30 16:04:06 -07002835 private final int mAction;
Vikram Aggarwalbc67bb12012-04-30 14:05:35 -07002836 /** The action will act upon these conversations */
Paul Westbrook77eee622012-07-10 13:41:57 -07002837 private final Collection<Conversation> mTarget;
Vikram Aggarwal7f602f72012-04-30 16:04:06 -07002838 /** Whether this destructive action has already been performed */
2839 private boolean mCompleted;
Vikram Aggarwalc4113952012-05-11 14:14:56 -07002840 /** Whether this is an action on the currently selected set. */
2841 private final boolean mIsSelectedSet;
Vikram Aggarwale8a85322012-04-24 09:01:38 -07002842
Jin Cao30c881a2014-04-08 14:28:36 -07002843 private UndoCallback mCallback;
2844
Mindy Pereirafbe40192012-03-20 10:40:45 -07002845 /**
Vikram Aggarwal715f83d2013-03-05 17:04:56 -08002846 * Create a listener object.
2847 * @param action action is one of four constants: R.id.y_button (archive),
Mindy Pereirafbe40192012-03-20 10:40:45 -07002848 * R.id.delete , R.id.mute, and R.id.report_spam.
Vikram Aggarwalbc67bb12012-04-30 14:05:35 -07002849 * @param target Conversation that we want to apply the action to.
Vikram Aggarwalc4113952012-05-11 14:14:56 -07002850 * @param isBatch whether the conversations are in the currently selected batch set.
2851 */
2852 public ConversationAction(int action, Collection<Conversation> target, boolean isBatch) {
Mindy Pereirafbe40192012-03-20 10:40:45 -07002853 mAction = action;
Paul Westbrook77eee622012-07-10 13:41:57 -07002854 mTarget = ImmutableList.copyOf(target);
Vikram Aggarwalc4113952012-05-11 14:14:56 -07002855 mIsSelectedSet = isBatch;
Mindy Pereirafbe40192012-03-20 10:40:45 -07002856 }
2857
Jin Cao30c881a2014-04-08 14:28:36 -07002858 @Override
2859 public void setUndoCallback(UndoCallback undoCallback) {
2860 mCallback = undoCallback;
2861 }
2862
Vikram Aggarwalacaa3c02012-04-24 12:45:27 -07002863 /**
2864 * The action common to child classes. This performs the action specified in the constructor
2865 * on the conversations given here.
Vikram Aggarwalacaa3c02012-04-24 12:45:27 -07002866 */
Vikram Aggarwalbc67bb12012-04-30 14:05:35 -07002867 @Override
2868 public void performAction() {
Vikram Aggarwal7f602f72012-04-30 16:04:06 -07002869 if (isPerformed()) {
2870 return;
2871 }
Mindy Pereira3cb28b52012-05-24 15:26:39 -07002872 boolean undoEnabled = mAccount.supportsCapability(AccountCapabilities.UNDO);
Vikram Aggarwal7dd054e2012-05-21 14:43:10 -07002873
2874 // Are we destroying the currently shown conversation? Show the next one.
2875 if (LogUtils.isLoggable(LOG_TAG, LogUtils.DEBUG)){
Vikram Aggarwal8742a612012-08-13 10:22:50 -07002876 LogUtils.d(LOG_TAG, "ConversationAction.performAction():"
2877 + "\nmTarget=%s\nCurrent=%s",
Vikram Aggarwal7dd054e2012-05-21 14:43:10 -07002878 Conversation.toString(mTarget), mCurrentConversation);
2879 }
Vikram Aggarwal7dd054e2012-05-21 14:43:10 -07002880
Paul Westbrooke1221d22012-08-19 11:09:07 -07002881 if (mConversationListCursor == null) {
2882 LogUtils.e(LOG_TAG, "null ConversationCursor in ConversationAction.performAction():"
2883 + "\nmTarget=%s\nCurrent=%s",
2884 Conversation.toString(mTarget), mCurrentConversation);
2885 return;
2886 }
2887
Scott Kennedy2b9d80e2013-07-30 23:03:45 -07002888 if (mAction == R.id.archive) {
2889 LogUtils.d(LOG_TAG, "Archiving");
Jin Cao30c881a2014-04-08 14:28:36 -07002890 mConversationListCursor.archive(mTarget, mCallback);
Scott Kennedy2b9d80e2013-07-30 23:03:45 -07002891 } else if (mAction == R.id.delete) {
2892 LogUtils.d(LOG_TAG, "Deleting");
Jin Cao30c881a2014-04-08 14:28:36 -07002893 mConversationListCursor.delete(mTarget, mCallback);
Scott Kennedy2b9d80e2013-07-30 23:03:45 -07002894 if (mFolder.supportsCapability(FolderCapabilities.DELETE_ACTION_FINAL)) {
Paul Westbrookef362542012-08-27 14:53:32 -07002895 undoEnabled = false;
Scott Kennedy2b9d80e2013-07-30 23:03:45 -07002896 }
2897 } else if (mAction == R.id.mute) {
2898 LogUtils.d(LOG_TAG, "Muting");
2899 if (mFolder.supportsCapability(FolderCapabilities.DESTRUCTIVE_MUTE)) {
2900 for (Conversation c : mTarget) {
2901 c.localDeleteOnUpdate = true;
2902 }
2903 }
Jin Cao30c881a2014-04-08 14:28:36 -07002904 mConversationListCursor.mute(mTarget, mCallback);
Scott Kennedy2b9d80e2013-07-30 23:03:45 -07002905 } else if (mAction == R.id.report_spam) {
2906 LogUtils.d(LOG_TAG, "Reporting spam");
Jin Cao30c881a2014-04-08 14:28:36 -07002907 mConversationListCursor.reportSpam(mTarget, mCallback);
Scott Kennedy2b9d80e2013-07-30 23:03:45 -07002908 } else if (mAction == R.id.mark_not_spam) {
2909 LogUtils.d(LOG_TAG, "Marking not spam");
Jin Cao30c881a2014-04-08 14:28:36 -07002910 mConversationListCursor.reportNotSpam(mTarget, mCallback);
Scott Kennedy2b9d80e2013-07-30 23:03:45 -07002911 } else if (mAction == R.id.report_phishing) {
2912 LogUtils.d(LOG_TAG, "Reporting phishing");
Jin Cao30c881a2014-04-08 14:28:36 -07002913 mConversationListCursor.reportPhishing(mTarget, mCallback);
Scott Kennedy2b9d80e2013-07-30 23:03:45 -07002914 } else if (mAction == R.id.remove_star) {
2915 LogUtils.d(LOG_TAG, "Removing star");
2916 // Star removal is destructive in the Starred folder.
2917 mConversationListCursor.updateBoolean(mTarget, ConversationColumns.STARRED,
2918 false);
2919 } else if (mAction == R.id.mark_not_important) {
2920 LogUtils.d(LOG_TAG, "Marking not-important");
2921 // Marking not important is destructive in a mailbox
2922 // containing only important messages
2923 if (mFolder != null && mFolder.isImportantOnly()) {
2924 for (Conversation conv : mTarget) {
2925 conv.localDeleteOnUpdate = true;
2926 }
2927 }
2928 mConversationListCursor.updateInt(mTarget, ConversationColumns.PRIORITY,
2929 UIProvider.ConversationPriority.LOW);
2930 } else if (mAction == R.id.discard_drafts) {
2931 LogUtils.d(LOG_TAG, "Discarding draft messages");
2932 // Discarding draft messages is destructive in a "draft" mailbox
2933 if (mFolder != null && mFolder.isDraft()) {
2934 for (Conversation conv : mTarget) {
2935 conv.localDeleteOnUpdate = true;
2936 }
2937 }
2938 mConversationListCursor.discardDrafts(mTarget);
2939 // We don't support undoing discarding drafts
2940 undoEnabled = false;
Jin Cao512821c2014-05-30 15:54:04 -07002941 } else if (mAction == R.id.discard_outbox) {
2942 LogUtils.d(LOG_TAG, "Discarding failed messages in Outbox");
2943 mConversationListCursor.moveFailedIntoDrafts(mTarget);
2944 undoEnabled = false;
Vikram Aggarwal3d7ca9d2012-05-11 14:40:36 -07002945 }
2946 if (undoEnabled) {
mindypead50392012-08-23 11:03:53 -07002947 mHandler.postDelayed(new Runnable() {
2948 @Override
2949 public void run() {
2950 onUndoAvailable(new ToastBarOperation(mTarget.size(), mAction,
Scott Kennedy6a3d5ce2013-03-15 17:33:14 -07002951 ToastBarOperation.UNDO, mIsSelectedSet, mFolder));
mindypead50392012-08-23 11:03:53 -07002952 }
2953 }, mShowUndoBarDelay);
Vikram Aggarwal3d7ca9d2012-05-11 14:40:36 -07002954 }
Vikram Aggarwalbc67bb12012-04-30 14:05:35 -07002955 refreshConversationList();
Vikram Aggarwalc4113952012-05-11 14:14:56 -07002956 if (mIsSelectedSet) {
Jin Caoec0fa482014-08-28 16:38:08 -07002957 mCheckedSet.clear();
Vikram Aggarwalc4113952012-05-11 14:14:56 -07002958 }
Mindy Pereirafbe40192012-03-20 10:40:45 -07002959 }
Vikram Aggarwal7f602f72012-04-30 16:04:06 -07002960
2961 /**
2962 * Returns true if this action has been performed, false otherwise.
Andy Huang839ada22012-07-20 15:48:40 -07002963 *
Vikram Aggarwal7f602f72012-04-30 16:04:06 -07002964 */
2965 private synchronized boolean isPerformed() {
2966 if (mCompleted) {
2967 return true;
2968 }
2969 mCompleted = true;
2970 return false;
2971 }
Mindy Pereirafbe40192012-03-20 10:40:45 -07002972 }
Mindy Pereirae5f4dc02012-03-21 16:08:53 -07002973
Vikram Aggarwald503df42012-05-11 10:13:35 -07002974 // Called from the FolderSelectionDialog after a user is done selecting folders to assign the
2975 // conversations to.
Mindy Pereirae5f4dc02012-03-21 16:08:53 -07002976 @Override
Mindy Pereira8db7e402012-07-13 10:32:47 -07002977 public final void assignFolder(Collection<FolderOperation> folderOps,
Scott Kennedy6a3d5ce2013-03-15 17:33:14 -07002978 Collection<Conversation> target, boolean batch, boolean showUndo,
2979 final boolean isMoveTo) {
Jin Cao479753f2014-07-31 14:31:01 -07002980 // Actions are destructive only when the current folder can be un-assigned from and
Mindy Pereira8db7e402012-07-13 10:32:47 -07002981 // when the list of folders contains the current folder.
2982 final boolean isDestructive = mFolder
Jin Cao479753f2014-07-31 14:31:01 -07002983 .supportsCapability(FolderCapabilities.ALLOWS_REMOVE_CONVERSATION)
Mindy Pereira8db7e402012-07-13 10:32:47 -07002984 && FolderOperation.isDestructive(folderOps, mFolder);
Vikram Aggarwald503df42012-05-11 10:13:35 -07002985 LogUtils.d(LOG_TAG, "onFolderChangesCommit: isDestructive = %b", isDestructive);
2986 if (isDestructive) {
2987 for (final Conversation c : target) {
2988 c.localDeleteOnUpdate = true;
Mindy Pereira6778f462012-03-23 18:01:55 -07002989 }
Mindy Pereirae5f4dc02012-03-21 16:08:53 -07002990 }
mindypc84759c2012-08-29 09:51:53 -07002991 final DestructiveAction folderChange;
Jin Cao30c881a2014-04-08 14:28:36 -07002992 final UndoCallback undoCallback = isMoveTo ?
2993 getUndoCallbackForDestructiveActionsWithAutoAdvance(R.id.move_to,
2994 mCurrentConversation)
2995 : null;
Vikram Aggarwald503df42012-05-11 10:13:35 -07002996 // Update the UI elements depending no their visibility and availability
Vikram Aggarwal3d7ca9d2012-05-11 14:40:36 -07002997 // TODO(viki): Consolidate this into a single method requestDelete.
Vikram Aggarwald503df42012-05-11 10:13:35 -07002998 if (isDestructive) {
Scott Kennedy6a3d5ce2013-03-15 17:33:14 -07002999 /*
3000 * If this is a MOVE operation, we want the action folder to be the destination folder.
3001 * Otherwise, we want it to be the current folder.
3002 *
3003 * A set of folder operations is a move if there are exactly two operations: an add and
3004 * a remove.
3005 */
3006 final Folder actionFolder;
3007 if (folderOps.size() != 2) {
3008 actionFolder = mFolder;
3009 } else {
3010 Folder addedFolder = null;
3011 boolean hasRemove = false;
3012 for (final FolderOperation folderOperation : folderOps) {
3013 if (folderOperation.mAdd) {
3014 addedFolder = folderOperation.mFolder;
3015 } else {
3016 hasRemove = true;
3017 }
3018 }
3019
3020 if (hasRemove && addedFolder != null) {
3021 actionFolder = addedFolder;
3022 } else {
3023 actionFolder = mFolder;
3024 }
3025 }
3026
mindypc84759c2012-08-29 09:51:53 -07003027 folderChange = getDeferredFolderChange(target, folderOps, isDestructive,
Jin Cao30c881a2014-04-08 14:28:36 -07003028 batch, showUndo, isMoveTo, actionFolder, undoCallback);
Scott Kennedycaaeed32013-06-12 13:39:16 -07003029 delete(0, target, folderChange, batch);
Vikram Aggarwal6902dcf2012-04-11 08:57:42 -07003030 } else {
mindypc84759c2012-08-29 09:51:53 -07003031 folderChange = getFolderChange(target, folderOps, isDestructive,
Jin Cao30c881a2014-04-08 14:28:36 -07003032 batch, showUndo, false /* isMoveTo */, mFolder, undoCallback);
Vikram Aggarwal715f83d2013-03-05 17:04:56 -08003033 requestUpdate(folderChange);
Mindy Pereirae5f4dc02012-03-21 16:08:53 -07003034 }
3035 }
3036
Mindy Pereira967ede62012-03-22 09:29:09 -07003037 @Override
Vikram Aggarwal3d7ca9d2012-05-11 14:40:36 -07003038 public final void onRefreshRequired() {
Andy Huang12a05d22014-08-28 21:36:18 -07003039 if (isAnimating()) {
Andy Huangf21787a2014-05-02 14:22:01 +02003040 final ConversationListFragment f = getConversationListFragment();
3041 LogUtils.w(ConversationCursor.LOG_TAG,
3042 "onRefreshRequired: delay until animating done. cursor=%s adapter=%s",
3043 mConversationListCursor, (f != null) ? f.getAnimatedAdapter() : null);
Mindy Pereira69e88dd2012-08-10 09:30:18 -07003044 return;
3045 }
Marc Blankbf128eb2012-04-18 15:58:45 -07003046 // Refresh the query in the background
Paul Westbrookcff1aea2012-08-10 11:51:00 -07003047 if (mConversationListCursor.isRefreshRequired()) {
3048 mConversationListCursor.refresh();
3049 }
Marc Blankbf128eb2012-04-18 15:58:45 -07003050 }
3051
mindyp5390fca2012-08-22 12:12:25 -07003052 @Override
mindyp6f54e1b2012-10-09 09:54:08 -07003053 public boolean isAnimating() {
Mindy Pereira69e88dd2012-08-10 09:30:18 -07003054 boolean isAnimating = false;
3055 ConversationListFragment convListFragment = getConversationListFragment();
3056 if (convListFragment != null) {
Andy Huang48ccbc52013-06-05 20:30:47 -07003057 isAnimating = convListFragment.isAnimating();
Mindy Pereira69e88dd2012-08-10 09:30:18 -07003058 }
3059 return isAnimating;
3060 }
3061
Marc Blankbf128eb2012-04-18 15:58:45 -07003062 /**
3063 * Called when the {@link ConversationCursor} is changed or has new data in it.
3064 * <p>
3065 * {@inheritDoc}
3066 */
3067 @Override
Vikram Aggarwal3d7ca9d2012-05-11 14:40:36 -07003068 public final void onRefreshReady() {
mindyp5c1d8352012-11-05 10:12:44 -08003069 LogUtils.d(LOG_TAG, "Received refresh ready callback for folder %s",
3070 mFolder != null ? mFolder.id : "-1");
Andy Huangb2ef9c12012-12-18 12:58:41 -06003071
3072 if (mDestroyed) {
3073 LogUtils.i(LOG_TAG, "ignoring onRefreshReady on destroyed AAC");
3074 return;
3075 }
3076
Paul Westbrookcff1aea2012-08-10 11:51:00 -07003077 if (!isAnimating()) {
Marc Blankbf128eb2012-04-18 15:58:45 -07003078 // Swap cursors
3079 mConversationListCursor.sync();
Andy Huangf21787a2014-05-02 14:22:01 +02003080 } else {
3081 // (CLF guaranteed to be non-null due to check in isAnimating)
3082 LogUtils.w(LOG_TAG,
3083 "AAC.onRefreshReady suppressing sync() due to animation. cursor=%s aa=%s",
3084 mConversationListCursor, getConversationListFragment().getAnimatedAdapter());
Marc Blankbf128eb2012-04-18 15:58:45 -07003085 }
Paul Westbrook937c94f2012-08-16 13:01:18 -07003086 mTracker.onCursorUpdated();
Vikram Aggarwalf0ef2302012-11-07 14:53:34 -08003087 perhapsShowFirstSearchResult();
Marc Blankbf128eb2012-04-18 15:58:45 -07003088 }
3089
3090 @Override
Vikram Aggarwal3d7ca9d2012-05-11 14:40:36 -07003091 public final void onDataSetChanged() {
Paul Westbrook9f119c72012-04-24 16:10:59 -07003092 updateConversationListFragment();
Andy Huang632721e2012-04-11 16:57:26 -07003093 mConversationListObservable.notifyChanged();
Jin Caoec0fa482014-08-28 16:38:08 -07003094 mCheckedSet.validateAgainstCursor(mConversationListCursor);
Marc Blankbf128eb2012-04-18 15:58:45 -07003095 }
3096
Vikram Aggarwal3d7ca9d2012-05-11 14:40:36 -07003097 /**
3098 * If the Conversation List Fragment is visible, updates the fragment.
3099 */
Vikram Aggarwal715f83d2013-03-05 17:04:56 -08003100 private void updateConversationListFragment() {
Marc Blankbf128eb2012-04-18 15:58:45 -07003101 final ConversationListFragment convList = getConversationListFragment();
3102 if (convList != null) {
Vikram Aggarwal75daee52012-04-30 11:13:09 -07003103 refreshConversationList();
Vikram Aggarwal6422b8d2013-01-14 10:47:41 -08003104 if (isFragmentVisible(convList)) {
Vikram Aggarwal69b5c302012-09-05 11:11:13 -07003105 informCursorVisiblity(true);
Paul Westbrook9f119c72012-04-24 16:10:59 -07003106 }
Marc Blankbf128eb2012-04-18 15:58:45 -07003107 }
3108 }
3109
3110 /**
3111 * This class handles throttled refresh of the conversation list
3112 */
3113 static class RefreshTimerTask extends TimerTask {
3114 final Handler mHandler;
3115 final AbstractActivityController mController;
3116
3117 RefreshTimerTask(AbstractActivityController controller, Handler handler) {
3118 mHandler = handler;
3119 mController = controller;
3120 }
3121
3122 @Override
3123 public void run() {
3124 mHandler.post(new Runnable() {
3125 @Override
3126 public void run() {
3127 LogUtils.d(LOG_TAG, "Delay done... calling onRefreshRequired");
3128 mController.onRefreshRequired();
3129 }});
3130 }
3131 }
3132
3133 /**
3134 * Cancel the refresh task, if it's running
3135 */
3136 private void cancelRefreshTask () {
3137 if (mConversationListRefreshTask != null) {
3138 mConversationListRefreshTask.cancel();
3139 mConversationListRefreshTask = null;
3140 }
3141 }
3142
3143 @Override
Paul Westbrookcff1aea2012-08-10 11:51:00 -07003144 public void onAnimationEnd(AnimatedAdapter animatedAdapter) {
Andy Huangf21787a2014-05-02 14:22:01 +02003145 if (animatedAdapter != null) {
3146 LogUtils.i(LOG_TAG, "AAC.onAnimationEnd. cursor=%s adapter=%s", mConversationListCursor,
3147 animatedAdapter);
3148 }
Paul Westbrook026139c2012-09-19 22:35:37 -07003149 if (mConversationListCursor == null) {
3150 LogUtils.e(LOG_TAG, "null ConversationCursor in onAnimationEnd");
3151 return;
3152 }
Paul Westbrookcff1aea2012-08-10 11:51:00 -07003153 if (mConversationListCursor.isRefreshReady()) {
Andy Huangc1922a92013-05-13 14:33:05 -07003154 LogUtils.i(ConversationCursor.LOG_TAG, "Stopped animating: try sync");
Paul Westbrookcff1aea2012-08-10 11:51:00 -07003155 onRefreshReady();
Marc Blankbf128eb2012-04-18 15:58:45 -07003156 }
Marc Blankbf128eb2012-04-18 15:58:45 -07003157
Paul Westbrookcff1aea2012-08-10 11:51:00 -07003158 if (mConversationListCursor.isRefreshRequired()) {
Andy Huangc1922a92013-05-13 14:33:05 -07003159 LogUtils.i(ConversationCursor.LOG_TAG, "Stopped animating: refresh");
Paul Westbrookcff1aea2012-08-10 11:51:00 -07003160 mConversationListCursor.refresh();
3161 }
mindyp6f54e1b2012-10-09 09:54:08 -07003162 if (mRecentsDataUpdated) {
3163 mRecentsDataUpdated = false;
3164 mRecentFolderObservers.notifyChanged();
3165 }
Marc Blankbf128eb2012-04-18 15:58:45 -07003166 }
3167
3168 @Override
Mindy Pereira967ede62012-03-22 09:29:09 -07003169 public void onSetEmpty() {
Vikram Aggarwal6cadbfc2012-12-27 09:17:05 -08003170 // There are no selected conversations. Ensure that the listener and its associated actions
3171 // are blanked out.
3172 setListener(null, -1);
Mindy Pereira967ede62012-03-22 09:29:09 -07003173 }
3174
3175 @Override
Jin Caoec0fa482014-08-28 16:38:08 -07003176 public void onSetPopulated(ConversationCheckedSet set) {
Vikram Aggarwal7704d792013-01-11 15:48:24 -08003177 mCabActionMenu = new SelectedConversationsActionMenu(mActivity, set, mFolder);
Vikram Aggarwal5b9ed2b2013-01-28 18:08:37 -08003178 if (mViewMode.isListMode() || (mIsTablet && mViewMode.isConversationMode())) {
Vikram Aggarwal7704d792013-01-11 15:48:24 -08003179 enableCabMode();
Vikram Aggarwal6902dcf2012-04-11 08:57:42 -07003180 }
Mindy Pereira967ede62012-03-22 09:29:09 -07003181 }
3182
Mindy Pereira967ede62012-03-22 09:29:09 -07003183 @Override
Jin Caoec0fa482014-08-28 16:38:08 -07003184 public void onSetChanged(ConversationCheckedSet set) {
Mindy Pereira967ede62012-03-22 09:29:09 -07003185 // Do nothing. We don't care about changes to the set.
3186 }
3187
3188 @Override
Jin Caoec0fa482014-08-28 16:38:08 -07003189 public ConversationCheckedSet getCheckedSet() {
3190 return mCheckedSet;
Mindy Pereira967ede62012-03-22 09:29:09 -07003191 }
3192
Vikram Aggarwale128fc22012-04-04 12:33:34 -07003193 /**
3194 * Disable the Contextual Action Bar (CAB). The selected set is not changed.
3195 */
3196 protected void disableCabMode() {
Mindy Pereira8937bf12012-07-23 14:05:02 -07003197 // Commit any previous destructive actions when entering/ exiting CAB mode.
mindypc6adce32012-08-22 18:46:42 -07003198 commitDestructiveActions(true);
Vikram Aggarwale128fc22012-04-04 12:33:34 -07003199 if (mCabActionMenu != null) {
3200 mCabActionMenu.deactivate();
3201 }
3202 }
3203
3204 /**
3205 * Re-enable the CAB menu if required. The selection set is not changed.
3206 */
3207 protected void enableCabMode() {
Tony Mantler3d6751a2013-08-02 11:30:59 -07003208 if (mCabActionMenu != null &&
3209 !(isDrawerEnabled() && mDrawerContainer.isDrawerOpen(mDrawerPullout))) {
Vikram Aggarwale128fc22012-04-04 12:33:34 -07003210 mCabActionMenu.activate();
3211 }
3212 }
3213
Vikram Aggarwal4eb52712012-06-19 16:24:50 -07003214 /**
Tony Mantler43fab322013-07-26 16:38:35 -07003215 * Re-enable CAB mode only if we have an active selection
3216 */
3217 protected void maybeEnableCabMode() {
Jin Caoec0fa482014-08-28 16:38:08 -07003218 if (!mCheckedSet.isEmpty()) {
Tony Mantlera703d8a2013-07-31 12:00:46 -07003219 if (mCabActionMenu != null) {
3220 mCabActionMenu.activate();
3221 }
Tony Mantler43fab322013-07-26 16:38:35 -07003222 }
3223 }
3224
3225 /**
Vikram Aggarwal4eb52712012-06-19 16:24:50 -07003226 * Unselect conversations and exit CAB mode.
3227 */
3228 protected final void exitCabMode() {
Jin Caoec0fa482014-08-28 16:38:08 -07003229 mCheckedSet.clear();
Vikram Aggarwal4eb52712012-06-19 16:24:50 -07003230 }
3231
Mindy Pereira967ede62012-03-22 09:29:09 -07003232 @Override
Mindy Pereirafd0c2972012-03-27 13:50:39 -07003233 public void startSearch() {
Vikram Aggarwal35f19d72012-04-24 13:24:48 -07003234 if (mAccount == null) {
3235 // We cannot search if there is no account. Drop the request to the floor.
3236 LogUtils.d(LOG_TAG, "AbstractActivityController.startSearch(): null account");
3237 return;
3238 }
James Lemieux3531d7e2014-01-28 11:10:05 -08003239 if (mAccount.supportsSearch()) {
Jin Caoc6801eb2014-08-12 18:16:57 -07003240 mSearchViewController.showSearchActionBar(
3241 MaterialSearchViewController.SEARCH_VIEW_STATE_VISIBLE);
Mindy Pereirafd0c2972012-03-27 13:50:39 -07003242 } else {
3243 Toast.makeText(mActivity.getActivityContext(), mActivity.getActivityContext()
Mindy Pereiraa46c2992012-03-27 14:12:39 -07003244 .getString(R.string.search_unsupported), Toast.LENGTH_SHORT).show();
Mindy Pereirafd0c2972012-03-27 13:50:39 -07003245 }
3246 }
Mindy Pereiraacf60392012-04-06 09:11:00 -07003247
Mindy Pereira0963ef82012-04-10 11:43:01 -07003248 @Override
Mindy Pereira0963ef82012-04-10 11:43:01 -07003249 public void onTouchEvent(MotionEvent event) {
3250 if (event.getAction() == MotionEvent.ACTION_DOWN) {
Andrew Sappersteinc2c9dc12012-07-02 18:17:32 -07003251 if (mToastBar != null && !mToastBar.isEventInToastBar(event)) {
James Lemieux9a110112014-08-07 15:23:13 -07003252 // if the toast bar is still animating, ignore this attempt to hide it
3253 if (mToastBar.isAnimating()) {
3254 return;
3255 }
3256
3257 // if the toast bar has not been seen long enough, ignore this attempt to hide it
3258 if (mToastBar.cannotBeHidden()) {
3259 return;
3260 }
3261
3262 // hide the toast bar
3263 mToastBar.hide(true /* animated */, false /* actionClicked */);
Mindy Pereira0963ef82012-04-10 11:43:01 -07003264 }
3265 }
3266 }
Andy Huangb1c34dc2012-04-17 16:36:19 -07003267
Andy Huang632721e2012-04-11 16:57:26 -07003268 @Override
Scott Kennedy3b965d72013-06-25 14:36:55 -07003269 public void onConversationSeen() {
3270 mPagerController.onConversationSeen();
Andy Huang632721e2012-04-11 16:57:26 -07003271 }
3272
Andy Huang9d3fd922012-09-26 22:23:58 -07003273 @Override
3274 public boolean isInitialConversationLoading() {
3275 return mPagerController.isInitialConversationLoading();
3276 }
3277
Vikram Aggarwal6422b8d2013-01-14 10:47:41 -08003278 /**
3279 * Check if the fragment given here is visible. Checking {@link Fragment#isVisible()} is
3280 * insufficient because that doesn't check if the window is currently in focus or not.
3281 */
Vikram Aggarwal715f83d2013-03-05 17:04:56 -08003282 private boolean isFragmentVisible(Fragment in) {
Vikram Aggarwal6422b8d2013-01-14 10:47:41 -08003283 return in != null && in.isVisible() && mActivity.hasWindowFocus();
3284 }
3285
Vikram Aggarwald70fe492013-06-04 12:52:07 -07003286 /**
3287 * This class handles callbacks that create a {@link ConversationCursor}.
3288 */
Andy Huangb1c34dc2012-04-17 16:36:19 -07003289 private class ConversationListLoaderCallbacks implements
3290 LoaderManager.LoaderCallbacks<ConversationCursor> {
3291
3292 @Override
3293 public Loader<ConversationCursor> onCreateLoader(int id, Bundle args) {
Vikram Aggarwala1b59dc2013-04-30 15:45:49 -07003294 final Account account = args.getParcelable(BUNDLE_ACCOUNT_KEY);
3295 final Folder folder = args.getParcelable(BUNDLE_FOLDER_KEY);
Andrew Sapperstein5bb4d052014-03-31 16:22:31 -07003296 final boolean ignoreInitialConversationLimit =
3297 args.getBoolean(BUNDLE_IGNORE_INITIAL_CONVERSATION_LIMIT_KEY, false);
Vikram Aggarwala1b59dc2013-04-30 15:45:49 -07003298 if (account == null || folder == null) {
3299 return null;
3300 }
Andrew Sapperstein5bb4d052014-03-31 16:22:31 -07003301 return new ConversationCursorLoader(mActivity, account,
Andy Huangf21787a2014-05-02 14:22:01 +02003302 folder.conversationListUri, folder.getTypeDescription(),
3303 ignoreInitialConversationLimit);
Andy Huangb1c34dc2012-04-17 16:36:19 -07003304 }
3305
3306 @Override
3307 public void onLoadFinished(Loader<ConversationCursor> loader, ConversationCursor data) {
Andy Huang144bfe72013-06-11 13:27:52 -07003308 LogUtils.d(LOG_TAG,
3309 "IN AAC.ConversationCursor.onLoadFinished, data=%s loader=%s this=%s",
3310 data, loader, this);
Andrew Sappersteina3ce6782013-05-09 15:14:49 -07003311 if (isDrawerEnabled() && mDrawerListener.getDrawerState() != DrawerLayout.STATE_IDLE) {
Vikram Aggarwala1b59dc2013-04-30 15:45:49 -07003312 LogUtils.d(LOG_TAG, "ConversationListLoaderCallbacks.onLoadFinished: ignoring.");
Andrew Sappersteina3ce6782013-05-09 15:14:49 -07003313 mConversationListLoadFinishedIgnored = true;
Vikram Aggarwala1b59dc2013-04-30 15:45:49 -07003314 return;
3315 }
Vikram Aggarwale8a85322012-04-24 09:01:38 -07003316 // Clear our all pending destructive actions before swapping the conversation cursor
3317 destroyPending(null);
Andy Huangb1c34dc2012-04-17 16:36:19 -07003318 mConversationListCursor = data;
Paul Westbrookbf232c32012-04-18 03:17:41 -07003319 mConversationListCursor.addListener(AbstractActivityController.this);
Andy Huang144bfe72013-06-11 13:27:52 -07003320 mDrawIdler.setListener(mConversationListCursor);
Paul Westbrook937c94f2012-08-16 13:01:18 -07003321 mTracker.onCursorUpdated();
Andy Huange3df1ad2012-04-24 17:15:23 -07003322 mConversationListObservable.notifyChanged();
Yu Ping Hu7c909c72013-01-18 11:58:01 -08003323 // Handle actions that were deferred until after the conversation list was loaded.
3324 for (LoadFinishedCallback callback : mConversationListLoadFinishedCallbacks) {
3325 callback.onLoadFinished();
3326 }
3327 mConversationListLoadFinishedCallbacks.clear();
Paul Westbrook9f119c72012-04-24 16:10:59 -07003328
Vikram Aggarwal17f373e2012-09-17 15:49:32 -07003329 final ConversationListFragment convList = getConversationListFragment();
Vikram Aggarwal6422b8d2013-01-14 10:47:41 -08003330 if (isFragmentVisible(convList)) {
Vikram Aggarwal17f373e2012-09-17 15:49:32 -07003331 // The conversation list is already listening to list changes and gets notified
3332 // in the mConversationListObservable.notifyChanged() line above. We only need to
3333 // check and inform the cursor of the change in visibility here.
3334 informCursorVisiblity(true);
Andy Huangb1c34dc2012-04-17 16:36:19 -07003335 }
Vikram Aggarwalf0ef2302012-11-07 14:53:34 -08003336 perhapsShowFirstSearchResult();
Andy Huangb1c34dc2012-04-17 16:36:19 -07003337 }
3338
3339 @Override
3340 public void onLoaderReset(Loader<ConversationCursor> loader) {
Andy Huang144bfe72013-06-11 13:27:52 -07003341 LogUtils.d(LOG_TAG,
3342 "IN AAC.ConversationCursor.onLoaderReset, data=%s loader=%s this=%s",
3343 mConversationListCursor, loader, this);
Paul Westbrook9a70e912012-08-17 15:53:20 -07003344
3345 if (mConversationListCursor != null) {
3346 // Unregister the listener
3347 mConversationListCursor.removeListener(AbstractActivityController.this);
Andy Huang144bfe72013-06-11 13:27:52 -07003348 mDrawIdler.setListener(null);
Paul Westbrook9a70e912012-08-17 15:53:20 -07003349 mConversationListCursor = null;
3350
3351 // Inform anyone who is interested about the change
3352 mTracker.onCursorUpdated();
3353 mConversationListObservable.notifyChanged();
Andy Huangb1c34dc2012-04-17 16:36:19 -07003354 }
Andy Huangb1c34dc2012-04-17 16:36:19 -07003355 }
Paul Westbrookbf232c32012-04-18 03:17:41 -07003356 }
Andy Huangb1c34dc2012-04-17 16:36:19 -07003357
Vikram Aggarwale8a85322012-04-24 09:01:38 -07003358 /**
Vikram Aggarwal177097f2013-03-08 11:19:53 -08003359 * Class to perform {@link LoaderManager.LoaderCallbacks} for creating {@link Folder} objects.
3360 */
3361 private class FolderLoads implements LoaderManager.LoaderCallbacks<ObjectCursor<Folder>> {
3362 @Override
3363 public Loader<ObjectCursor<Folder>> onCreateLoader(int id, Bundle args) {
3364 final String[] everything = UIProvider.FOLDERS_PROJECTION;
3365 switch (id) {
3366 case LOADER_FOLDER_CURSOR:
3367 LogUtils.d(LOG_TAG, "LOADER_FOLDER_CURSOR created");
3368 final ObjectCursorLoader<Folder> loader = new
3369 ObjectCursorLoader<Folder>(
Scott Kennedy259df5b2013-07-11 13:24:01 -07003370 mContext, mFolder.folderUri.fullUri, everything, Folder.FACTORY);
Vikram Aggarwal177097f2013-03-08 11:19:53 -08003371 loader.setUpdateThrottle(mFolderItemUpdateDelayMs);
3372 return loader;
3373 case LOADER_RECENT_FOLDERS:
3374 LogUtils.d(LOG_TAG, "LOADER_RECENT_FOLDERS created");
Yu Ping Huf2632b92013-03-15 13:56:27 -07003375 if (mAccount != null && mAccount.recentFolderListUri != null
3376 && !mAccount.recentFolderListUri.equals(Uri.EMPTY)) {
Vikram Aggarwal177097f2013-03-08 11:19:53 -08003377 return new ObjectCursorLoader<Folder>(mContext,
3378 mAccount.recentFolderListUri, everything, Folder.FACTORY);
3379 }
3380 break;
3381 case LOADER_ACCOUNT_INBOX:
3382 LogUtils.d(LOG_TAG, "LOADER_ACCOUNT_INBOX created");
3383 final Uri defaultInbox = Settings.getDefaultInboxUri(mAccount.settings);
3384 final Uri inboxUri = defaultInbox.equals(Uri.EMPTY) ?
3385 mAccount.folderListUri : defaultInbox;
3386 LogUtils.d(LOG_TAG, "Loading the default inbox: %s", inboxUri);
3387 if (inboxUri != null) {
3388 return new ObjectCursorLoader<Folder>(mContext, inboxUri,
3389 everything, Folder.FACTORY);
3390 }
3391 break;
3392 case LOADER_SEARCH:
3393 LogUtils.d(LOG_TAG, "LOADER_SEARCH created");
3394 return Folder.forSearchResults(mAccount,
3395 args.getString(ConversationListContext.EXTRA_SEARCH_QUERY),
Jin Cao1a864cc2014-05-21 11:16:39 -07003396 // We can just use current time as a unique identifier for this search
3397 Long.toString(SystemClock.uptimeMillis()),
Vikram Aggarwal177097f2013-03-08 11:19:53 -08003398 mActivity.getActivityContext());
3399 case LOADER_FIRST_FOLDER:
3400 LogUtils.d(LOG_TAG, "LOADER_FIRST_FOLDER created");
3401 final Uri folderUri = args.getParcelable(Utils.EXTRA_FOLDER_URI);
3402 mConversationToShow = args.getParcelable(Utils.EXTRA_CONVERSATION);
3403 if (mConversationToShow != null && mConversationToShow.position < 0){
3404 mConversationToShow.position = 0;
3405 }
3406 return new ObjectCursorLoader<Folder>(mContext, folderUri,
3407 everything, Folder.FACTORY);
3408 default:
3409 LogUtils.wtf(LOG_TAG, "FolderLoads.onCreateLoader(%d) for invalid id", id);
3410 return null;
3411 }
3412 return null;
3413 }
3414
3415 @Override
3416 public void onLoadFinished(Loader<ObjectCursor<Folder>> loader, ObjectCursor<Folder> data) {
3417 if (data == null) {
3418 LogUtils.e(LOG_TAG, "Received null cursor from loader id: %d", loader.getId());
3419 }
3420 switch (loader.getId()) {
3421 case LOADER_FOLDER_CURSOR:
3422 if (data != null && data.moveToFirst()) {
3423 final Folder folder = data.getModel();
3424 setHasFolderChanged(folder);
3425 mFolder = folder;
3426 mFolderObservable.notifyChanged();
3427 } else {
3428 LogUtils.d(LOG_TAG, "Unable to get the folder %s",
Tony Mantler26a20752014-02-28 16:44:24 -08003429 mFolder != null ? mFolder.name : "");
Vikram Aggarwal177097f2013-03-08 11:19:53 -08003430 }
3431 break;
3432 case LOADER_RECENT_FOLDERS:
3433 // Few recent folders and we are running on a phone? Populate the default
3434 // recents. The number of default recent folders is at least 2: every provider
3435 // has at least two folders, and the recent folder count never decreases.
3436 // Having a single recent folder is an erroneous case, and we can gracefully
3437 // recover by populating default recents. The default recents will not stomp on
3438 // the existing value: it will be shown in addition to the default folders:
3439 // the max number of recent folders is more than 1+num(defaultRecents).
3440 if (data != null && data.getCount() <= 1 && !mIsTablet) {
3441 final class PopulateDefault extends AsyncTask<Uri, Void, Void> {
3442 @Override
3443 protected Void doInBackground(Uri... uri) {
3444 // Asking for an update on the URI and ignore the result.
3445 final ContentResolver resolver = mContext.getContentResolver();
3446 resolver.update(uri[0], null, null, null);
3447 return null;
3448 }
3449 }
3450 final Uri uri = mAccount.defaultRecentFolderListUri;
3451 LogUtils.v(LOG_TAG, "Default recents at %s", uri);
3452 new PopulateDefault().execute(uri);
3453 break;
3454 }
3455 LogUtils.v(LOG_TAG, "Reading recent folders from the cursor.");
3456 mRecentFolderList.loadFromUiProvider(data);
3457 if (isAnimating()) {
3458 mRecentsDataUpdated = true;
3459 } else {
3460 mRecentFolderObservers.notifyChanged();
3461 }
3462 break;
3463 case LOADER_ACCOUNT_INBOX:
3464 if (data != null && !data.isClosed() && data.moveToFirst()) {
3465 final Folder inbox = data.getModel();
Alice Yangebeef1b2013-09-04 06:41:10 +00003466 onFolderChanged(inbox, false /* force */);
Vikram Aggarwal177097f2013-03-08 11:19:53 -08003467 // Just want to get the inbox, don't care about updates to it
3468 // as this will be tracked by the folder change listener.
3469 mActivity.getLoaderManager().destroyLoader(LOADER_ACCOUNT_INBOX);
3470 } else {
3471 LogUtils.d(LOG_TAG, "Unable to get the account inbox for account %s",
Tony Mantler26a20752014-02-28 16:44:24 -08003472 mAccount != null ? mAccount.getEmailAddress() : "");
Vikram Aggarwal177097f2013-03-08 11:19:53 -08003473 }
3474 break;
3475 case LOADER_SEARCH:
3476 if (data != null && data.getCount() > 0) {
3477 data.moveToFirst();
3478 final Folder search = data.getModel();
3479 updateFolder(search);
3480 mConvListContext = ConversationListContext.forSearchQuery(mAccount, mFolder,
3481 mActivity.getIntent()
3482 .getStringExtra(UIProvider.SearchQueryParameters.QUERY));
3483 showConversationList(mConvListContext);
3484 mActivity.invalidateOptionsMenu();
3485 mHaveSearchResults = search.totalCount > 0;
3486 mActivity.getLoaderManager().destroyLoader(LOADER_SEARCH);
3487 } else {
3488 LogUtils.e(LOG_TAG, "Null/empty cursor returned by LOADER_SEARCH loader");
3489 }
3490 break;
3491 case LOADER_FIRST_FOLDER:
3492 if (data == null || data.getCount() <=0 || !data.moveToFirst()) {
3493 return;
3494 }
3495 final Folder folder = data.getModel();
3496 boolean handled = false;
3497 if (folder != null) {
Alice Yangebeef1b2013-09-04 06:41:10 +00003498 onFolderChanged(folder, false /* force */);
Vikram Aggarwal177097f2013-03-08 11:19:53 -08003499 handled = true;
3500 }
3501 if (mConversationToShow != null) {
3502 // Open the conversation.
3503 showConversation(mConversationToShow);
3504 handled = true;
3505 }
3506 if (!handled) {
3507 // We have an account, but nothing else: load the default inbox.
3508 loadAccountInbox();
3509 }
3510 mConversationToShow = null;
3511 // And don't run this anymore.
3512 mActivity.getLoaderManager().destroyLoader(LOADER_FIRST_FOLDER);
3513 break;
3514 }
3515 }
3516
3517 @Override
3518 public void onLoaderReset(Loader<ObjectCursor<Folder>> loader) {
3519 }
3520 }
3521
3522 /**
3523 * Class to perform {@link LoaderManager.LoaderCallbacks} for creating {@link Account} objects.
3524 */
3525 private class AccountLoads implements LoaderManager.LoaderCallbacks<ObjectCursor<Account>> {
3526 final String[] mProjection = UIProvider.ACCOUNTS_PROJECTION;
3527 final CursorCreator<Account> mFactory = Account.FACTORY;
3528
3529 @Override
3530 public Loader<ObjectCursor<Account>> onCreateLoader(int id, Bundle args) {
3531 switch (id) {
3532 case LOADER_ACCOUNT_CURSOR:
3533 LogUtils.d(LOG_TAG, "LOADER_ACCOUNT_CURSOR created");
3534 return new ObjectCursorLoader<Account>(mContext,
3535 MailAppProvider.getAccountsUri(), mProjection, mFactory);
3536 case LOADER_ACCOUNT_UPDATE_CURSOR:
3537 LogUtils.d(LOG_TAG, "LOADER_ACCOUNT_UPDATE_CURSOR created");
3538 return new ObjectCursorLoader<Account>(mContext, mAccount.uri, mProjection,
3539 mFactory);
3540 default:
3541 LogUtils.wtf(LOG_TAG, "Got an id (%d) that I cannot create!", id);
3542 break;
3543 }
3544 return null;
3545 }
3546
3547 @Override
3548 public void onLoadFinished(Loader<ObjectCursor<Account>> loader,
3549 ObjectCursor<Account> data) {
3550 if (data == null) {
3551 LogUtils.e(LOG_TAG, "Received null cursor from loader id: %d", loader.getId());
3552 }
3553 switch (loader.getId()) {
3554 case LOADER_ACCOUNT_CURSOR:
Vikram Aggarwald70fe492013-06-04 12:52:07 -07003555 // We have received an update on the list of accounts.
Vikram Aggarwal177097f2013-03-08 11:19:53 -08003556 if (data == null) {
3557 // Nothing useful to do if we have no valid data.
3558 break;
3559 }
Andy Huang761522c2013-08-08 13:09:11 -07003560 final long count = data.getCount();
3561 if (count == 0) {
Vikram Aggarwal177097f2013-03-08 11:19:53 -08003562 // If an empty cursor is returned, the MailAppProvider is indicating that
3563 // no accounts have been specified. We want to navigate to the
3564 // "add account" activity that will handle the intent returned by the
3565 // MailAppProvider
3566
3567 // If the MailAppProvider believes that all accounts have been loaded,
3568 // and the account list is still empty, we want to prompt the user to add
3569 // an account.
3570 final Bundle extras = data.getExtras();
3571 final boolean accountsLoaded =
3572 extras.getInt(AccountCursorExtraKeys.ACCOUNTS_LOADED) != 0;
3573
3574 if (accountsLoaded) {
3575 final Intent noAccountIntent = MailAppProvider.getNoAccountIntent
3576 (mContext);
3577 if (noAccountIntent != null) {
3578 mActivity.startActivityForResult(noAccountIntent,
3579 ADD_ACCOUNT_REQUEST_CODE);
3580 }
3581 }
3582 } else {
3583 final boolean accountListUpdated = accountsUpdated(data);
Vikram Aggarwalc04cf7e2013-05-13 15:38:42 -07003584 if (!mHaveAccountList || accountListUpdated) {
3585 mHaveAccountList = updateAccounts(data);
Vikram Aggarwal177097f2013-03-08 11:19:53 -08003586 }
Andy Huang761522c2013-08-08 13:09:11 -07003587 Analytics.getInstance().setCustomDimension(Analytics.CD_INDEX_ACCOUNT_COUNT,
3588 Long.toString(count));
Vikram Aggarwal177097f2013-03-08 11:19:53 -08003589 }
3590 break;
3591 case LOADER_ACCOUNT_UPDATE_CURSOR:
3592 // We have received an update for current account.
Vikram Aggarwal177097f2013-03-08 11:19:53 -08003593 if (data != null && data.moveToFirst()) {
3594 final Account updatedAccount = data.getModel();
Vikram Aggarwald70fe492013-06-04 12:52:07 -07003595 // Make sure that this is an update for the current account
Vikram Aggarwal177097f2013-03-08 11:19:53 -08003596 if (updatedAccount.uri.equals(mAccount.uri)) {
Vikram Aggarwal177097f2013-03-08 11:19:53 -08003597 final Settings previousSettings = mAccount.settings;
3598
3599 // Update the controller's reference to the current account
3600 mAccount = updatedAccount;
3601 LogUtils.d(LOG_TAG, "AbstractActivityController.onLoadFinished(): "
3602 + "mAccount = %s", mAccount.uri);
3603
3604 // Only notify about a settings change if something differs
3605 if (!Objects.equal(mAccount.settings, previousSettings)) {
3606 mAccountObservers.notifyChanged();
3607 }
3608 perhapsEnterWaitMode();
Régis Décamps62e14962014-06-23 11:04:38 +02003609 perhapsStartWelcomeTour();
Vikram Aggarwal177097f2013-03-08 11:19:53 -08003610 } else {
3611 LogUtils.e(LOG_TAG, "Got update for account: %s with current account:"
3612 + " %s", updatedAccount.uri, mAccount.uri);
3613 // We need to restart the loader, so the correct account information
3614 // will be returned.
3615 restartOptionalLoader(LOADER_ACCOUNT_UPDATE_CURSOR, this, Bundle.EMPTY);
3616 }
3617 }
3618 break;
3619 }
3620 }
3621
3622 @Override
3623 public void onLoaderReset(Loader<ObjectCursor<Account>> loader) {
Vikram Aggarwald70fe492013-06-04 12:52:07 -07003624 // Do nothing. In onLoadFinished() we copy the relevant data from the cursor.
Vikram Aggarwal177097f2013-03-08 11:19:53 -08003625 }
3626 }
3627
3628 /**
Régis Décamps62e14962014-06-23 11:04:38 +02003629 * Loads the preference that tells whether the welcome tour should be displayed,
3630 * and calls the callback with this value.
3631 * For this to function, the account must have been synced.
3632 */
3633 private void perhapsStartWelcomeTour() {
Paul Westbrook550f15f2014-07-31 09:14:02 -07003634 new AsyncTask<Void, Void, Boolean>() {
3635 @Override
3636 protected Boolean doInBackground(Void... params) {
3637 if (mActivity.wasLatestWelcomeTourShownOnDeviceForAllAccounts()) {
3638 // No need to go through the WelcomeStateLoader machinery.
3639 return false;
3640 }
3641 return true;
Régis Décamps62e14962014-06-23 11:04:38 +02003642 }
Paul Westbrook550f15f2014-07-31 09:14:02 -07003643
3644 @Override
3645 protected void onPostExecute(Boolean result) {
3646 if (result) {
3647 if (mAccount != null && mAccount.isAccountReady()) {
3648 LoaderManager.LoaderCallbacks<?> welcomeLoaderCallbacks =
3649 mActivity.getWelcomeCallbacks();
3650 if (welcomeLoaderCallbacks != null) {
3651 // The callback is responsible for showing the tour when appropriate.
3652 mActivity.getLoaderManager().initLoader(LOADER_WELCOME_TOUR_ACCOUNTS,
3653 Bundle.EMPTY, welcomeLoaderCallbacks);
3654 }
3655 }
3656 }
3657 }
3658 }.execute();
Régis Décamps62e14962014-06-23 11:04:38 +02003659 }
3660
3661 /**
Vikram Aggarwalf0ef2302012-11-07 14:53:34 -08003662 * Updates controller state based on search results and shows first conversation if required.
3663 */
Vikram Aggarwal715f83d2013-03-05 17:04:56 -08003664 private void perhapsShowFirstSearchResult() {
mindypd27009a2012-11-27 11:54:18 -08003665 if (mCurrentConversation == null) {
3666 // Shown for search results in two-pane mode only.
3667 mHaveSearchResults = Intent.ACTION_SEARCH.equals(mActivity.getIntent().getAction())
3668 && mConversationListCursor.getCount() > 0;
3669 if (!shouldShowFirstConversation()) {
3670 return;
3671 }
3672 mConversationListCursor.moveToPosition(0);
3673 final Conversation conv = new Conversation(mConversationListCursor);
3674 conv.position = 0;
3675 onConversationSelected(conv, true /* checkSafeToModifyFragments */);
Vikram Aggarwalf0ef2302012-11-07 14:53:34 -08003676 }
Vikram Aggarwalf0ef2302012-11-07 14:53:34 -08003677 }
3678
3679 /**
Vikram Aggarwale8a85322012-04-24 09:01:38 -07003680 * Destroy the pending {@link DestructiveAction} till now and assign the given action as the
3681 * next destructive action..
3682 * @param nextAction the next destructive action to be performed. This can be null.
3683 */
Vikram Aggarwal715f83d2013-03-05 17:04:56 -08003684 private void destroyPending(DestructiveAction nextAction) {
Vikram Aggarwale8a85322012-04-24 09:01:38 -07003685 // If there is a pending action, perform that first.
3686 if (mPendingDestruction != null) {
3687 mPendingDestruction.performAction();
3688 }
3689 mPendingDestruction = nextAction;
3690 }
3691
3692 /**
3693 * Register a destructive action with the controller. This performs the previous destructive
Vikram Aggarwalacaa3c02012-04-24 12:45:27 -07003694 * 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 -07003695 * embellish this method any more.
Vikram Aggarwal715f83d2013-03-05 17:04:56 -08003696 * @param action the action to register.
Vikram Aggarwale8a85322012-04-24 09:01:38 -07003697 */
Vikram Aggarwal715f83d2013-03-05 17:04:56 -08003698 private void registerDestructiveAction(DestructiveAction action) {
Vikram Aggarwale8a85322012-04-24 09:01:38 -07003699 // TODO(viki): This is not a good idea. The best solution is for clients to request a
3700 // destructive action from the controller and for the controller to own the action. This is
3701 // a half-way solution while refactoring DestructiveAction.
3702 destroyPending(action);
Vikram Aggarwale8a85322012-04-24 09:01:38 -07003703 }
3704
Vikram Aggarwal531488e2012-05-29 16:36:52 -07003705 @Override
Jin Cao30c881a2014-04-08 14:28:36 -07003706 public final DestructiveAction getBatchAction(int action, UndoCallback undoCallback) {
Jin Caoec0fa482014-08-28 16:38:08 -07003707 final DestructiveAction da = new ConversationAction(action, mCheckedSet.values(), true);
Jin Cao30c881a2014-04-08 14:28:36 -07003708 da.setUndoCallback(undoCallback);
Vikram Aggarwale8a85322012-04-24 09:01:38 -07003709 registerDestructiveAction(da);
3710 return da;
3711 }
Vikram Aggarwal41e6e712012-04-24 11:22:57 -07003712
Mindy Pereirade3e74a2012-07-24 09:43:10 -07003713 @Override
Jin Cao30c881a2014-04-08 14:28:36 -07003714 public final DestructiveAction getDeferredBatchAction(int action, UndoCallback undoCallback) {
Jin Caoec0fa482014-08-28 16:38:08 -07003715 return getDeferredAction(action, mCheckedSet.values(), true, undoCallback);
mindypf0656a12012-10-01 08:30:57 -07003716 }
3717
3718 /**
3719 * Get a destructive action for a menu action. This is a temporary method,
3720 * to control the profusion of {@link DestructiveAction} classes that are
3721 * created. Please do not copy this paradigm.
3722 * @param action the resource ID of the menu action: R.id.delete, for
3723 * example
3724 * @param target the conversations to act upon.
3725 * @return a {@link DestructiveAction} that performs the specified action.
3726 */
Vikram Aggarwal715f83d2013-03-05 17:04:56 -08003727 private DestructiveAction getDeferredAction(int action, Collection<Conversation> target,
Jin Cao30c881a2014-04-08 14:28:36 -07003728 boolean batch, UndoCallback callback) {
3729 ConversationAction cAction = new ConversationAction(action, target, batch);
3730 cAction.setUndoCallback(callback);
3731 return cAction;
Mindy Pereirade3e74a2012-07-24 09:43:10 -07003732 }
3733
Vikram Aggarwalbc67bb12012-04-30 14:05:35 -07003734 /**
3735 * Class to change the folders that are assigned to a set of conversations. This is destructive
3736 * because the user can remove the current folder from the conversation, in which case it has
3737 * to be animated away from the current folder.
3738 */
Vikram Aggarwal41e6e712012-04-24 11:22:57 -07003739 private class FolderDestruction implements DestructiveAction {
Paul Westbrook77eee622012-07-10 13:41:57 -07003740 private final Collection<Conversation> mTarget;
Mindy Pereira8db7e402012-07-13 10:32:47 -07003741 private final ArrayList<FolderOperation> mFolderOps = new ArrayList<FolderOperation>();
Vikram Aggarwal41e6e712012-04-24 11:22:57 -07003742 private final boolean mIsDestructive;
Vikram Aggarwal7f602f72012-04-30 16:04:06 -07003743 /** Whether this destructive action has already been performed */
3744 private boolean mCompleted;
Martin Hibdone78c40f2013-10-10 18:29:25 -07003745 private final boolean mIsSelectedSet;
3746 private final boolean mShowUndo;
3747 private final int mAction;
Scott Kennedy6a3d5ce2013-03-15 17:33:14 -07003748 private final Folder mActionFolder;
Vikram Aggarwal41e6e712012-04-24 11:22:57 -07003749
Jin Cao30c881a2014-04-08 14:28:36 -07003750 private UndoCallback mUndoCallback;
3751
Vikram Aggarwal41e6e712012-04-24 11:22:57 -07003752 /**
3753 * Create a new folder destruction object to act on the given conversations.
Vikram Aggarwal715f83d2013-03-05 17:04:56 -08003754 * @param target conversations to act upon.
Scott Kennedy6a3d5ce2013-03-15 17:33:14 -07003755 * @param actionFolder the {@link Folder} being acted upon, used for displaying the undo bar
Vikram Aggarwal41e6e712012-04-24 11:22:57 -07003756 */
Vikram Aggarwal7f602f72012-04-30 16:04:06 -07003757 private FolderDestruction(final Collection<Conversation> target,
Mindy Pereira8db7e402012-07-13 10:32:47 -07003758 final Collection<FolderOperation> folders, boolean isDestructive, boolean isBatch,
Scott Kennedy6a3d5ce2013-03-15 17:33:14 -07003759 boolean showUndo, int action, final Folder actionFolder) {
Paul Westbrook77eee622012-07-10 13:41:57 -07003760 mTarget = ImmutableList.copyOf(target);
Mindy Pereira8db7e402012-07-13 10:32:47 -07003761 mFolderOps.addAll(folders);
Vikram Aggarwal41e6e712012-04-24 11:22:57 -07003762 mIsDestructive = isDestructive;
Mindy Pereiraf3a45562012-05-24 16:30:19 -07003763 mIsSelectedSet = isBatch;
Mindy Pereira06642fa2012-07-12 16:23:27 -07003764 mShowUndo = showUndo;
Mindy Pereira01f30502012-08-14 10:30:51 -07003765 mAction = action;
Scott Kennedy6a3d5ce2013-03-15 17:33:14 -07003766 mActionFolder = actionFolder;
Vikram Aggarwal41e6e712012-04-24 11:22:57 -07003767 }
3768
Vikram Aggarwal41e6e712012-04-24 11:22:57 -07003769 @Override
Jin Cao30c881a2014-04-08 14:28:36 -07003770 public void setUndoCallback(UndoCallback undoCallback) {
3771 mUndoCallback = undoCallback;
3772 }
3773
3774 @Override
Vikram Aggarwal41e6e712012-04-24 11:22:57 -07003775 public void performAction() {
Vikram Aggarwal7f602f72012-04-30 16:04:06 -07003776 if (isPerformed()) {
3777 return;
3778 }
Mindy Pereira06642fa2012-07-12 16:23:27 -07003779 if (mIsDestructive && mShowUndo) {
mindypcb0b30e2012-11-30 10:16:35 -08003780 ToastBarOperation undoOp = new ToastBarOperation(mTarget.size(), mAction,
Scott Kennedy6a3d5ce2013-03-15 17:33:14 -07003781 ToastBarOperation.UNDO, mIsSelectedSet, mActionFolder);
Vikram Aggarwal41e6e712012-04-24 11:22:57 -07003782 onUndoAvailable(undoOp);
3783 }
Mindy Pereira8db7e402012-07-13 10:32:47 -07003784 // For each conversation, for each operation, add/ remove the
3785 // appropriate folders.
mindypcb0b30e2012-11-30 10:16:35 -08003786 ArrayList<ConversationOperation> ops = new ArrayList<ConversationOperation>();
3787 ArrayList<Uri> folderUris;
3788 ArrayList<Boolean> adds;
Mindy Pereira8db7e402012-07-13 10:32:47 -07003789 for (Conversation target : mTarget) {
mindypcb0b30e2012-11-30 10:16:35 -08003790 HashMap<Uri, Folder> targetFolders = Folder.hashMapForFolders(target
3791 .getRawFolders());
3792 folderUris = new ArrayList<Uri>();
mindypcb0b30e2012-11-30 10:16:35 -08003793 adds = new ArrayList<Boolean>();
Mindy Pereira01f30502012-08-14 10:30:51 -07003794 if (mIsDestructive) {
3795 target.localDeleteOnUpdate = true;
3796 }
Mindy Pereira8db7e402012-07-13 10:32:47 -07003797 for (FolderOperation op : mFolderOps) {
Scott Kennedy259df5b2013-07-11 13:24:01 -07003798 folderUris.add(op.mFolder.folderUri.fullUri);
mindypcb0b30e2012-11-30 10:16:35 -08003799 adds.add(op.mAdd ? Boolean.TRUE : Boolean.FALSE);
Mindy Pereira8db7e402012-07-13 10:32:47 -07003800 if (op.mAdd) {
Scott Kennedy259df5b2013-07-11 13:24:01 -07003801 targetFolders.put(op.mFolder.folderUri.fullUri, op.mFolder);
Mindy Pereira8db7e402012-07-13 10:32:47 -07003802 } else {
Scott Kennedy259df5b2013-07-11 13:24:01 -07003803 targetFolders.remove(op.mFolder.folderUri.fullUri);
Mindy Pereira8db7e402012-07-13 10:32:47 -07003804 }
3805 }
Paul Westbrook26746eb2012-12-06 14:44:01 -08003806 ops.add(mConversationListCursor.getConversationFolderOperation(target,
Jin Cao30c881a2014-04-08 14:28:36 -07003807 folderUris, adds, targetFolders.values(), mUndoCallback));
mindyp389f0b22012-08-29 11:12:54 -07003808 }
3809 if (mConversationListCursor != null) {
Scott Kennedy9e2d4072013-03-21 21:46:01 -07003810 mConversationListCursor.updateBulkValues(ops);
Mindy Pereira8db7e402012-07-13 10:32:47 -07003811 }
Vikram Aggarwal7f602f72012-04-30 16:04:06 -07003812 refreshConversationList();
Mindy Pereiraf3a45562012-05-24 16:30:19 -07003813 if (mIsSelectedSet) {
Jin Caoec0fa482014-08-28 16:38:08 -07003814 mCheckedSet.clear();
Mindy Pereiraf3a45562012-05-24 16:30:19 -07003815 }
Vikram Aggarwal7f602f72012-04-30 16:04:06 -07003816 }
Mindy Pereira8db7e402012-07-13 10:32:47 -07003817
Vikram Aggarwal7f602f72012-04-30 16:04:06 -07003818 /**
3819 * Returns true if this action has been performed, false otherwise.
Andy Huang839ada22012-07-20 15:48:40 -07003820 *
Vikram Aggarwal7f602f72012-04-30 16:04:06 -07003821 */
3822 private synchronized boolean isPerformed() {
3823 if (mCompleted) {
3824 return true;
Vikram Aggarwal41e6e712012-04-24 11:22:57 -07003825 }
Vikram Aggarwal7f602f72012-04-30 16:04:06 -07003826 mCompleted = true;
3827 return false;
Vikram Aggarwal41e6e712012-04-24 11:22:57 -07003828 }
3829 }
Vikram Aggarwal7f602f72012-04-30 16:04:06 -07003830
mindypc84759c2012-08-29 09:51:53 -07003831 public final DestructiveAction getFolderChange(Collection<Conversation> target,
3832 Collection<FolderOperation> folders, boolean isDestructive, boolean isBatch,
Jin Cao30c881a2014-04-08 14:28:36 -07003833 boolean showUndo, final boolean isMoveTo, final Folder actionFolder,
3834 UndoCallback undoCallback) {
mindypc84759c2012-08-29 09:51:53 -07003835 final DestructiveAction da = getDeferredFolderChange(target, folders, isDestructive,
Jin Cao30c881a2014-04-08 14:28:36 -07003836 isBatch, showUndo, isMoveTo, actionFolder, undoCallback);
mindypc84759c2012-08-29 09:51:53 -07003837 registerDestructiveAction(da);
3838 return da;
3839 }
3840
3841 public final DestructiveAction getDeferredFolderChange(Collection<Conversation> target,
Mindy Pereira8db7e402012-07-13 10:32:47 -07003842 Collection<FolderOperation> folders, boolean isDestructive, boolean isBatch,
Jin Cao30c881a2014-04-08 14:28:36 -07003843 boolean showUndo, final boolean isMoveTo, final Folder actionFolder,
3844 UndoCallback undoCallback) {
3845 final DestructiveAction fd = new FolderDestruction(target, folders, isDestructive, isBatch,
3846 showUndo, isMoveTo ? R.id.move_folder : R.id.change_folders, actionFolder);
3847 fd.setUndoCallback(undoCallback);
3848 return fd;
Mindy Pereira01f30502012-08-14 10:30:51 -07003849 }
3850
3851 @Override
3852 public final DestructiveAction getDeferredRemoveFolder(Collection<Conversation> target,
3853 Folder toRemove, boolean isDestructive, boolean isBatch,
Jin Cao30c881a2014-04-08 14:28:36 -07003854 boolean showUndo, UndoCallback undoCallback) {
Mindy Pereira01f30502012-08-14 10:30:51 -07003855 Collection<FolderOperation> folderOps = new ArrayList<FolderOperation>();
3856 folderOps.add(new FolderOperation(toRemove, false));
Jin Cao30c881a2014-04-08 14:28:36 -07003857 final DestructiveAction da = new FolderDestruction(target, folderOps, isDestructive, isBatch,
Scott Kennedy6a3d5ce2013-03-15 17:33:14 -07003858 showUndo, R.id.remove_folder, mFolder);
Jin Cao30c881a2014-04-08 14:28:36 -07003859 da.setUndoCallback(undoCallback);
3860 return da;
Mindy Pereira01f30502012-08-14 10:30:51 -07003861 }
3862
Vikram Aggarwal4f4782b2012-05-30 08:39:09 -07003863 @Override
3864 public final void refreshConversationList() {
Vikram Aggarwal75daee52012-04-30 11:13:09 -07003865 final ConversationListFragment convList = getConversationListFragment();
3866 if (convList == null) {
3867 return;
3868 }
3869 convList.requestListRefresh();
3870 }
Andrew Sappersteinc2c9dc12012-07-02 18:17:32 -07003871
3872 protected final ActionClickedListener getUndoClickedListener(
3873 final AnimatedAdapter listAdapter) {
3874 return new ActionClickedListener() {
3875 @Override
Andrew Sappersteinf8ccdcf2013-08-08 19:30:38 -07003876 public void onActionClicked(Context context) {
Andrew Sappersteinc2c9dc12012-07-02 18:17:32 -07003877 if (mAccount.undoUri != null) {
3878 // NOTE: We might want undo to return the messages affected, in which case
3879 // the resulting cursor might be interesting...
3880 // TODO: Use UIProvider.SEQUENCE_QUERY_PARAMETER to indicate the set of
3881 // commands to undo
3882 if (mConversationListCursor != null) {
3883 mConversationListCursor.undo(
3884 mActivity.getActivityContext(), mAccount.undoUri);
3885 }
3886 if (listAdapter != null) {
3887 listAdapter.setUndo(true);
3888 }
3889 }
3890 }
3891 };
3892 }
3893
Vikram Aggarwal41b9e8f2012-09-25 10:15:04 -07003894 /**
3895 * Shows an error toast in the bottom when a folder was not fetched successfully.
3896 * @param folder the folder which could not be fetched.
3897 * @param replaceVisibleToast if true, this should replace any currently visible toast.
3898 */
Andrew Sapperstein9d7519d2012-07-16 14:03:53 -07003899 protected final void showErrorToast(final Folder folder, boolean replaceVisibleToast) {
Andrew Sapperstein00179f12012-08-09 15:15:40 -07003900
Vikram Aggarwal41b9e8f2012-09-25 10:15:04 -07003901 final ActionClickedListener listener;
3902 final int actionTextResourceId;
Andrew Sapperstein00179f12012-08-09 15:15:40 -07003903 final int lastSyncResult = folder.lastSyncResult;
Vikram Aggarwal41b9e8f2012-09-25 10:15:04 -07003904 switch (lastSyncResult & 0x0f) {
Andrew Sapperstein00179f12012-08-09 15:15:40 -07003905 case UIProvider.LastSyncResult.CONNECTION_ERROR:
Vikram Aggarwal41b9e8f2012-09-25 10:15:04 -07003906 // The sync request that caused this failure.
3907 final int syncRequest = lastSyncResult >> 4;
3908 // Show: User explicitly pressed the refresh button and there is no connection
3909 // Show: The first time the user enters the app and there is no connection
3910 // TODO(viki): Implement this.
3911 // Reference: http://b/7202801
3912 final boolean showToast = (syncRequest & UIProvider.SyncStatus.USER_REFRESH) != 0;
3913 // Don't show: Already in the app; user switches to a synced label
3914 // Don't show: In a live label and a background sync fails
3915 final boolean avoidToast = !showToast && (folder.syncWindow > 0
3916 || (syncRequest & UIProvider.SyncStatus.BACKGROUND_SYNC) != 0);
3917 if (avoidToast) {
3918 return;
3919 }
Andrew Sapperstein00179f12012-08-09 15:15:40 -07003920 listener = getRetryClickedListener(folder);
3921 actionTextResourceId = R.string.retry;
3922 break;
3923 case UIProvider.LastSyncResult.AUTH_ERROR:
3924 listener = getSignInClickedListener();
3925 actionTextResourceId = R.string.signin;
3926 break;
3927 case UIProvider.LastSyncResult.SECURITY_ERROR:
3928 return; // Currently we do nothing for security errors.
3929 case UIProvider.LastSyncResult.STORAGE_ERROR:
3930 listener = getStorageErrorClickedListener();
3931 actionTextResourceId = R.string.info;
3932 break;
3933 case UIProvider.LastSyncResult.INTERNAL_ERROR:
3934 listener = getInternalErrorClickedListener();
3935 actionTextResourceId = R.string.report;
3936 break;
3937 default:
3938 return;
3939 }
Vikram Aggarwal41b9e8f2012-09-25 10:15:04 -07003940 mToastBar.show(listener,
Vikram Aggarwal41b9e8f2012-09-25 10:15:04 -07003941 Utils.getSyncStatusText(mActivity.getActivityContext(), lastSyncResult),
Andrew Sapperstein00179f12012-08-09 15:15:40 -07003942 actionTextResourceId,
Andrew Sapperstein9d7519d2012-07-16 14:03:53 -07003943 replaceVisibleToast,
James Lemieux0ec03e82014-09-03 14:01:53 -07003944 true /* autohide */,
Scott Kennedy6a3d5ce2013-03-15 17:33:14 -07003945 new ToastBarOperation(1, 0, ToastBarOperation.ERROR, false, folder));
Andrew Sappersteinc2c9dc12012-07-02 18:17:32 -07003946 }
3947
Andrew Sapperstein00179f12012-08-09 15:15:40 -07003948 private ActionClickedListener getRetryClickedListener(final Folder folder) {
Andrew Sappersteinc2c9dc12012-07-02 18:17:32 -07003949 return new ActionClickedListener() {
3950 @Override
Andrew Sappersteinf8ccdcf2013-08-08 19:30:38 -07003951 public void onActionClicked(Context context) {
Andrew Sappersteinc2c9dc12012-07-02 18:17:32 -07003952 final Uri uri = folder.refreshUri;
3953
3954 if (uri != null) {
Paul Westbrook4969e0c2012-08-20 14:38:39 -07003955 startAsyncRefreshTask(uri);
Andrew Sappersteinc2c9dc12012-07-02 18:17:32 -07003956 }
3957 }
3958 };
3959 }
Andrew Sapperstein00179f12012-08-09 15:15:40 -07003960
3961 private ActionClickedListener getSignInClickedListener() {
3962 return new ActionClickedListener() {
3963 @Override
Andrew Sappersteinf8ccdcf2013-08-08 19:30:38 -07003964 public void onActionClicked(Context context) {
Paul Westbrook122f7c22012-08-20 17:50:31 -07003965 promptUserForAuthentication(mAccount);
Andrew Sapperstein00179f12012-08-09 15:15:40 -07003966 }
3967 };
3968 }
3969
3970 private ActionClickedListener getStorageErrorClickedListener() {
3971 return new ActionClickedListener() {
3972 @Override
Andrew Sappersteinf8ccdcf2013-08-08 19:30:38 -07003973 public void onActionClicked(Context context) {
Paul Westbrook4969e0c2012-08-20 14:38:39 -07003974 showStorageErrorDialog();
Andrew Sapperstein00179f12012-08-09 15:15:40 -07003975 }
3976 };
3977 }
3978
Paul Westbrook4969e0c2012-08-20 14:38:39 -07003979 private void showStorageErrorDialog() {
3980 DialogFragment fragment = (DialogFragment)
3981 mFragmentManager.findFragmentByTag(SYNC_ERROR_DIALOG_FRAGMENT_TAG);
3982 if (fragment == null) {
3983 fragment = SyncErrorDialogFragment.newInstance();
3984 }
3985 fragment.show(mFragmentManager, SYNC_ERROR_DIALOG_FRAGMENT_TAG);
3986 }
3987
Andrew Sapperstein00179f12012-08-09 15:15:40 -07003988 private ActionClickedListener getInternalErrorClickedListener() {
3989 return new ActionClickedListener() {
3990 @Override
Andrew Sappersteinf8ccdcf2013-08-08 19:30:38 -07003991 public void onActionClicked(Context context) {
Paul Westbrook83e6b572013-02-05 16:22:42 -08003992 Utils.sendFeedback(mActivity, mAccount, true /* reportingProblem */);
Andrew Sapperstein00179f12012-08-09 15:15:40 -07003993 }
3994 };
3995 }
Paul Westbrook4969e0c2012-08-20 14:38:39 -07003996
3997 @Override
3998 public void onFooterViewErrorActionClick(Folder folder, int errorStatus) {
3999 Uri uri = null;
4000 switch (errorStatus) {
4001 case UIProvider.LastSyncResult.CONNECTION_ERROR:
4002 if (folder != null && folder.refreshUri != null) {
4003 uri = folder.refreshUri;
4004 }
4005 break;
4006 case UIProvider.LastSyncResult.AUTH_ERROR:
Paul Westbrook122f7c22012-08-20 17:50:31 -07004007 promptUserForAuthentication(mAccount);
Paul Westbrook4969e0c2012-08-20 14:38:39 -07004008 return;
4009 case UIProvider.LastSyncResult.SECURITY_ERROR:
4010 return; // Currently we do nothing for security errors.
4011 case UIProvider.LastSyncResult.STORAGE_ERROR:
4012 showStorageErrorDialog();
4013 return;
4014 case UIProvider.LastSyncResult.INTERNAL_ERROR:
Paul Westbrook83e6b572013-02-05 16:22:42 -08004015 Utils.sendFeedback(mActivity, mAccount, true /* reportingProblem */);
Paul Westbrook4969e0c2012-08-20 14:38:39 -07004016 return;
4017 default:
4018 return;
4019 }
4020
4021 if (uri != null) {
4022 startAsyncRefreshTask(uri);
4023 }
4024 }
4025
4026 @Override
4027 public void onFooterViewLoadMoreClick(Folder folder) {
4028 if (folder != null && folder.loadMoreUri != null) {
4029 startAsyncRefreshTask(folder.loadMoreUri);
4030 }
4031 }
4032
4033 private void startAsyncRefreshTask(Uri uri) {
4034 if (mFolderSyncTask != null) {
4035 mFolderSyncTask.cancel(true);
4036 }
4037 mFolderSyncTask = new AsyncRefreshTask(mActivity.getActivityContext(), uri);
4038 mFolderSyncTask.execute();
4039 }
Paul Westbrook122f7c22012-08-20 17:50:31 -07004040
4041 private void promptUserForAuthentication(Account account) {
Paul Westbrook429399b2012-08-24 11:19:17 -07004042 if (account != null && !Utils.isEmpty(account.reauthenticationIntentUri)) {
Paul Westbrook122f7c22012-08-20 17:50:31 -07004043 final Intent authenticationIntent =
4044 new Intent(Intent.ACTION_VIEW, account.reauthenticationIntentUri);
4045 mActivity.startActivityForResult(authenticationIntent, REAUTHENTICATE_REQUEST_CODE);
4046 }
4047 }
mindypca87de42012-09-28 15:02:39 -07004048
4049 @Override
4050 public void onAccessibilityStateChanged() {
4051 // Clear the cache of objects.
4052 ConversationItemViewModel.onAccessibilityUpdated();
4053 // Re-render the list if it exists.
Vikram Aggarwala91d00b2013-01-18 12:00:37 -08004054 final ConversationListFragment frag = getConversationListFragment();
mindypca87de42012-09-28 15:02:39 -07004055 if (frag != null) {
4056 AnimatedAdapter adapter = frag.getAnimatedAdapter();
4057 if (adapter != null) {
4058 adapter.notifyDataSetInvalidated();
4059 }
4060 }
4061 }
Vikram Aggarwal6cadbfc2012-12-27 09:17:05 -08004062
4063 @Override
Jin Cao30c881a2014-04-08 14:28:36 -07004064 public void makeDialogListener (final int action, final boolean isBatch,
4065 UndoCallback undoCallback) {
Vikram Aggarwalb8c31712013-01-03 17:03:19 -08004066 final Collection<Conversation> target;
Vikram Aggarwalb8c31712013-01-03 17:03:19 -08004067 if (isBatch) {
Jin Caoec0fa482014-08-28 16:38:08 -07004068 target = mCheckedSet.values();
Vikram Aggarwalb8c31712013-01-03 17:03:19 -08004069 } else {
4070 LogUtils.d(LOG_TAG, "Will act upon %s", mCurrentConversation);
4071 target = Conversation.listOf(mCurrentConversation);
Vikram Aggarwalb8c31712013-01-03 17:03:19 -08004072 }
Jin Cao30c881a2014-04-08 14:28:36 -07004073 final DestructiveAction destructiveAction = getDeferredAction(action, target, isBatch,
4074 undoCallback);
Vikram Aggarwal6cadbfc2012-12-27 09:17:05 -08004075 mDialogAction = action;
Vikram Aggarwalb8c31712013-01-03 17:03:19 -08004076 mDialogFromSelectedSet = isBatch;
Vikram Aggarwal6cadbfc2012-12-27 09:17:05 -08004077 mDialogListener = new AlertDialog.OnClickListener() {
4078 @Override
4079 public void onClick(DialogInterface dialog, int which) {
Scott Kennedycaaeed32013-06-12 13:39:16 -07004080 delete(action, target, destructiveAction, isBatch);
Vikram Aggarwal6cadbfc2012-12-27 09:17:05 -08004081 // Afterwards, let's remove references to the listener and the action.
4082 setListener(null, -1);
4083 }
4084 };
4085 }
4086
4087 @Override
4088 public AlertDialog.OnClickListener getListener() {
4089 return mDialogListener;
4090 }
4091
4092 /**
4093 * Sets the listener for the positive action on a confirmation dialog. Since only a single
4094 * confirmation dialog can be shown, this overwrites the previous listener. It is safe to
4095 * unset the listener; in which case action should be set to -1.
Vikram Aggarwal715f83d2013-03-05 17:04:56 -08004096 * @param listener the listener that will perform the task for this dialog's positive action.
4097 * @param action the action that created this dialog.
Vikram Aggarwal6cadbfc2012-12-27 09:17:05 -08004098 */
4099 private void setListener(AlertDialog.OnClickListener listener, final int action){
4100 mDialogListener = listener;
4101 mDialogAction = action;
4102 }
Vikram Aggarwal69a6cdf2013-01-08 16:05:17 -08004103
4104 @Override
4105 public VeiledAddressMatcher getVeiledAddressMatcher() {
4106 return mVeiledMatcher;
Vikram Aggarwala91d00b2013-01-18 12:00:37 -08004107 }
4108
4109 @Override
4110 public void setDetachedMode() {
4111 // Tell the conversation list not to select anything.
4112 final ConversationListFragment frag = getConversationListFragment();
4113 if (frag != null) {
4114 frag.setChoiceNone();
Vikram Aggarwal81230ee2013-02-26 15:26:44 -08004115 } else if (mIsTablet) {
4116 // How did we ever land here? Detached mode, and no CLF on tablet???
Vikram Aggarwala91d00b2013-01-18 12:00:37 -08004117 LogUtils.e(LOG_TAG, "AAC.setDetachedMode(): CLF = null!");
4118 }
4119 mDetachedConvUri = mCurrentConversation.uri;
4120 }
4121
4122 private void clearDetachedMode() {
4123 // Tell the conversation list to go back to its usual selection behavior.
4124 final ConversationListFragment frag = getConversationListFragment();
4125 if (frag != null) {
4126 frag.revertChoiceMode();
Vikram Aggarwal81230ee2013-02-26 15:26:44 -08004127 } else if (mIsTablet) {
4128 // How did we ever land here? Detached mode, and no CLF on tablet???
4129 LogUtils.e(LOG_TAG, "AAC.clearDetachedMode(): CLF = null on tablet!");
Vikram Aggarwala91d00b2013-01-18 12:00:37 -08004130 }
4131 mDetachedConvUri = null;
4132 }
Andy Huang12b3ee42013-04-24 22:49:43 -07004133
Andy Huang61f26c22014-03-13 18:24:52 -07004134 @Override
Andy Huang8712f412014-08-21 23:10:41 -07004135 public boolean shouldPreventListSwipesEntirely() {
4136 return false;
4137 }
4138
4139 @Override
Andy Huang61f26c22014-03-13 18:24:52 -07004140 public DrawerController getDrawerController() {
4141 return mDrawerListener;
4142 }
4143
4144 private class MailDrawerListener extends Observable<DrawerLayout.DrawerListener>
4145 implements DrawerLayout.DrawerListener, DrawerController {
Andrew Sappersteina3ce6782013-05-09 15:14:49 -07004146 private int mDrawerState;
Andrew Sappersteinfc5be1c2013-05-15 17:48:10 -07004147 private float mOldSlideOffset;
Andrew Sappersteina3ce6782013-05-09 15:14:49 -07004148
4149 public MailDrawerListener() {
4150 mDrawerState = DrawerLayout.STATE_IDLE;
Andrew Sappersteinfc5be1c2013-05-15 17:48:10 -07004151 mOldSlideOffset = 0.f;
Andrew Sappersteina3ce6782013-05-09 15:14:49 -07004152 }
4153
Andy Huang12b3ee42013-04-24 22:49:43 -07004154 @Override
Andy Huang3e95fe92014-07-25 17:46:33 -07004155 public boolean isDrawerEnabled() {
4156 return AbstractActivityController.this.isDrawerEnabled();
4157 }
4158
4159 @Override
Andy Huang61f26c22014-03-13 18:24:52 -07004160 public void registerDrawerListener(DrawerLayout.DrawerListener l) {
4161 registerObserver(l);
4162 }
4163
4164 @Override
4165 public void unregisterDrawerListener(DrawerLayout.DrawerListener l) {
4166 unregisterObserver(l);
4167 }
4168
4169 @Override
4170 public boolean isDrawerOpen() {
4171 return isDrawerEnabled() && mDrawerContainer.isDrawerOpen(mDrawerPullout);
4172 }
4173
4174 @Override
4175 public boolean isDrawerVisible() {
4176 return isDrawerEnabled() && mDrawerContainer.isDrawerVisible(mDrawerPullout);
4177 }
4178
4179 @Override
Andy Huangf58e4c32014-07-09 16:58:18 -07004180 public void toggleDrawerState() {
4181 AbstractActivityController.this.toggleDrawerState();
4182 }
4183
4184 @Override
Andy Huang12b3ee42013-04-24 22:49:43 -07004185 public void onDrawerOpened(View drawerView) {
4186 mDrawerToggle.onDrawerOpened(drawerView);
Andy Huang61f26c22014-03-13 18:24:52 -07004187
4188 for (DrawerLayout.DrawerListener l : mObservers) {
4189 l.onDrawerOpened(drawerView);
4190 }
Andy Huang12b3ee42013-04-24 22:49:43 -07004191 }
4192
4193 @Override
4194 public void onDrawerClosed(View drawerView) {
4195 mDrawerToggle.onDrawerClosed(drawerView);
4196 if (mHasNewAccountOrFolder) {
4197 refreshDrawer();
4198 }
Scott Kennedy10c99ef2013-08-29 13:44:17 -07004199
4200 // When closed, we want to use either the burger, or up, based on where we are
4201 final int mode = mViewMode.getMode();
Jin Cao9695e002014-05-29 11:56:44 -07004202 final boolean isTopLevel = Folder.isRoot(mFolder);
Scott Kennedy10c99ef2013-08-29 13:44:17 -07004203 mDrawerToggle.setDrawerIndicatorEnabled(getShouldShowDrawerIndicator(mode, isTopLevel));
Andy Huang61f26c22014-03-13 18:24:52 -07004204
4205 for (DrawerLayout.DrawerListener l : mObservers) {
4206 l.onDrawerClosed(drawerView);
4207 }
Andy Huang12b3ee42013-04-24 22:49:43 -07004208 }
4209
4210 /**
4211 * As part of the overriden function, it will animate the alpha of the conversation list
4212 * view along with the drawer sliding when we're in the process of switching accounts or
4213 * folders. Note, this is the same amount of work done as {@link ValueAnimator#ofFloat}.
4214 */
4215 @Override
4216 public void onDrawerSlide(View drawerView, float slideOffset) {
4217 mDrawerToggle.onDrawerSlide(drawerView, slideOffset);
4218 if (mHasNewAccountOrFolder && mListViewForAnimating != null) {
4219 mListViewForAnimating.setAlpha(slideOffset);
4220 }
Andrew Sappersteinfc5be1c2013-05-15 17:48:10 -07004221
4222 // This code handles when to change the visibility of action items
4223 // based on drawer state. The basic logic is that right when we
4224 // open the drawer, we hide the action items. We show the action items
4225 // when the drawer closes. However, due to the animation of the drawer closing,
4226 // to make the reshowing of the action items feel right, we make the items visible
4227 // slightly sooner.
4228 //
4229 // However, to make the animating behavior work properly, we have to know whether
4230 // we're animating open or closed. Only if we're animating closed do we want to
4231 // show the action items early. We save the last slide offset so that we can compare
4232 // the current slide offset to it to determine if we're opening or closing.
4233 if (mDrawerState == DrawerLayout.STATE_SETTLING) {
4234 if (mHideMenuItems && slideOffset < 0.15f && mOldSlideOffset > slideOffset) {
4235 mHideMenuItems = false;
Andrew Sapperstein52882ff2014-07-27 12:30:18 -07004236 mActivity.supportInvalidateOptionsMenu();
Tony Mantler43fab322013-07-26 16:38:35 -07004237 maybeEnableCabMode();
Andrew Sappersteinfc5be1c2013-05-15 17:48:10 -07004238 } else if (!mHideMenuItems && slideOffset > 0.f && mOldSlideOffset < slideOffset) {
4239 mHideMenuItems = true;
Andrew Sapperstein52882ff2014-07-27 12:30:18 -07004240 mActivity.supportInvalidateOptionsMenu();
Tony Mantler43fab322013-07-26 16:38:35 -07004241 disableCabMode();
Andrew Sappersteinfc5be1c2013-05-15 17:48:10 -07004242 }
4243 } else {
4244 if (mHideMenuItems && Float.compare(slideOffset, 0.f) == 0) {
4245 mHideMenuItems = false;
Andrew Sapperstein52882ff2014-07-27 12:30:18 -07004246 mActivity.supportInvalidateOptionsMenu();
Tony Mantler43fab322013-07-26 16:38:35 -07004247 maybeEnableCabMode();
Andrew Sappersteinfc5be1c2013-05-15 17:48:10 -07004248 } else if (!mHideMenuItems && slideOffset > 0.f) {
4249 mHideMenuItems = true;
Andrew Sapperstein52882ff2014-07-27 12:30:18 -07004250 mActivity.supportInvalidateOptionsMenu();
Tony Mantler43fab322013-07-26 16:38:35 -07004251 disableCabMode();
Andrew Sappersteinfc5be1c2013-05-15 17:48:10 -07004252 }
4253 }
4254
4255 mOldSlideOffset = slideOffset;
Scott Kennedy10c99ef2013-08-29 13:44:17 -07004256
4257 // If we're sliding, we always want to show the burger
4258 mDrawerToggle.setDrawerIndicatorEnabled(true /* enable */);
Andy Huang61f26c22014-03-13 18:24:52 -07004259
4260 for (DrawerLayout.DrawerListener l : mObservers) {
4261 l.onDrawerSlide(drawerView, slideOffset);
4262 }
Andy Huang12b3ee42013-04-24 22:49:43 -07004263 }
4264
4265 /**
4266 * This condition here should only be called when the drawer is stuck in a weird state
4267 * and doesn't register the onDrawerClosed, but shows up as idle. Make sure to refresh
4268 * and, more importantly, unlock the drawer when this is the case.
4269 */
4270 @Override
4271 public void onDrawerStateChanged(int newState) {
Martin Hibdon371a71c2014-02-19 13:55:28 -08004272 LogUtils.d(LOG_TAG, "AAC onDrawerStateChanged %d", newState);
Andrew Sappersteina3ce6782013-05-09 15:14:49 -07004273 mDrawerState = newState;
4274 mDrawerToggle.onDrawerStateChanged(mDrawerState);
Andy Huang61f26c22014-03-13 18:24:52 -07004275
4276 for (DrawerLayout.DrawerListener l : mObservers) {
4277 l.onDrawerStateChanged(newState);
4278 }
4279
Andy Huange764cfd2014-02-26 11:55:03 -08004280 if (mViewMode.isSearchMode()) {
Martin Hibdon371a71c2014-02-19 13:55:28 -08004281 return;
4282 }
Andrew Sappersteina3ce6782013-05-09 15:14:49 -07004283 if (mDrawerState == DrawerLayout.STATE_IDLE) {
4284 if (mHasNewAccountOrFolder) {
4285 refreshDrawer();
4286 }
4287 if (mConversationListLoadFinishedIgnored) {
4288 mConversationListLoadFinishedIgnored = false;
4289 final Bundle args = new Bundle();
4290 args.putParcelable(BUNDLE_ACCOUNT_KEY, mAccount);
4291 args.putParcelable(BUNDLE_FOLDER_KEY, mFolder);
4292 mActivity.getLoaderManager().initLoader(
4293 LOADER_CONVERSATION_LIST, args, mListCursorCallbacks);
4294 }
Andy Huang12b3ee42013-04-24 22:49:43 -07004295 }
4296 }
4297
4298 /**
4299 * If we've reached a stable drawer state, unlock the drawer for usage, clear the
4300 * conversation list, and finish end actions. Also, make
4301 * {@link #mHasNewAccountOrFolder} false to reflect we're done changing.
4302 */
4303 public void refreshDrawer() {
4304 mHasNewAccountOrFolder = false;
4305 mDrawerContainer.setDrawerLockMode(DrawerLayout.LOCK_MODE_UNLOCKED);
4306 ConversationListFragment conversationList = getConversationListFragment();
4307 if (conversationList != null) {
4308 conversationList.clear();
4309 }
Tony Mantler54022ee2014-07-07 13:43:35 -07004310 mFolderOrAccountObservers.notifyChanged();
Andy Huang12b3ee42013-04-24 22:49:43 -07004311 }
Andrew Sappersteina3ce6782013-05-09 15:14:49 -07004312
4313 /**
4314 * Returns the most recent update of the {@link DrawerLayout}'s state provided
4315 * by {@link #onDrawerStateChanged(int)}.
4316 * @return The {@link DrawerLayout}'s current state. One of
4317 * {@link DrawerLayout#STATE_DRAGGING}, {@link DrawerLayout#STATE_IDLE},
4318 * or {@link DrawerLayout#STATE_SETTLING}.
4319 */
4320 public int getDrawerState() {
4321 return mDrawerState;
4322 }
Andy Huang12b3ee42013-04-24 22:49:43 -07004323 }
4324
Scott Kennedy8a72b852013-05-02 14:18:50 -07004325 @Override
4326 public boolean isDrawerPullEnabled() {
Martin Hibdon371a71c2014-02-19 13:55:28 -08004327 return true;
Scott Kennedy8a72b852013-05-02 14:18:50 -07004328 }
Andrew Sapperstein5747e152013-05-13 14:13:08 -07004329
4330 @Override
4331 public boolean shouldHideMenuItems() {
4332 return mHideMenuItems;
4333 }
Alice Yangebeef1b2013-09-04 06:41:10 +00004334
4335 protected void navigateUpFolderHierarchy() {
4336 new AsyncTask<Void, Void, Folder>() {
4337 @Override
4338 protected Folder doInBackground(final Void... params) {
4339 if (mInbox == null) {
4340 // We don't have an inbox, but we need it
4341 final Cursor cursor = mContext.getContentResolver().query(
4342 mAccount.settings.defaultInbox, UIProvider.FOLDERS_PROJECTION, null,
4343 null, null);
4344
4345 if (cursor != null) {
4346 try {
4347 if (cursor.moveToFirst()) {
4348 mInbox = new Folder(cursor);
4349 }
4350 } finally {
4351 cursor.close();
4352 }
4353 }
4354 }
4355
4356 // Now try to load our parent
4357 final Folder folder;
4358
Alice Yang5ac10032013-09-04 06:41:43 +00004359 if (mFolder != null) {
Martin Hibdone78c40f2013-10-10 18:29:25 -07004360 Cursor cursor = null;
4361 try {
4362 cursor = mContext.getContentResolver().query(mFolder.parent,
4363 UIProvider.FOLDERS_PROJECTION, null, null, null);
Alice Yangebeef1b2013-09-04 06:41:10 +00004364
Martin Hibdone78c40f2013-10-10 18:29:25 -07004365 if (cursor == null || !cursor.moveToFirst()) {
4366 // We couldn't load the parent, so use the inbox
4367 folder = mInbox;
4368 } else {
Alice Yang5ac10032013-09-04 06:41:43 +00004369 folder = new Folder(cursor);
Martin Hibdone78c40f2013-10-10 18:29:25 -07004370 }
4371 } finally {
4372 if (cursor != null) {
Alice Yang5ac10032013-09-04 06:41:43 +00004373 cursor.close();
4374 }
Alice Yangebeef1b2013-09-04 06:41:10 +00004375 }
Alice Yang5ac10032013-09-04 06:41:43 +00004376 } else {
4377 folder = mInbox;
Alice Yangebeef1b2013-09-04 06:41:10 +00004378 }
4379
4380 return folder;
4381 }
4382
4383 @Override
4384 protected void onPostExecute(final Folder result) {
4385 onFolderSelected(result);
4386 }
4387 }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, (Void[]) null);
4388 }
Scott Kennedyf77806e2013-08-30 11:38:15 -07004389
4390 @Override
4391 public Parcelable getConversationListScrollPosition(final String folderUri) {
4392 return mConversationListScrollPositions.getParcelable(folderUri);
4393 }
4394
4395 @Override
4396 public void setConversationListScrollPosition(final String folderUri,
4397 final Parcelable savedPosition) {
4398 mConversationListScrollPositions.putParcelable(folderUri, savedPosition);
4399 }
Andrew Sapperstein53de4482014-07-29 02:39:39 +00004400
4401 @Override
4402 public View.OnClickListener getNavigationViewClickListener() {
4403 return mHomeButtonListener;
4404 }
4405
4406 // TODO: Fold this into the outer class when b/16627877 is fixed
4407 private class HomeButtonListener implements View.OnClickListener {
Andrew Sapperstein53de4482014-07-29 02:39:39 +00004408 @Override
4409 public void onClick(View v) {
Jin Cao405a3442014-08-25 13:49:33 -07004410 handleUpPress();
Andrew Sapperstein53de4482014-07-29 02:39:39 +00004411 }
4412 }
Vikram Aggarwal4a5c5302012-01-12 15:07:13 -08004413}