blob: 390afc867047a61af38823e8ec0941ded195627e [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.widget.DrawerLayout;
Andrew Sapperstein52882ff2014-07-27 12:30:18 -070051import android.support.v7.app.ActionBar;
Andrew Sapperstein355dd902014-09-10 12:46:44 -070052import android.support.v7.app.ActionBarDrawerToggle;
Jin Cao41733e32014-09-15 12:02:34 -070053import android.text.TextUtils;
Andy Huang12b3ee42013-04-24 22:49:43 -070054import android.view.Gravity;
Vikram Aggarwala55b36c2012-01-13 11:45:02 -080055import android.view.KeyEvent;
56import android.view.Menu;
Mindy Pereira28d5f722012-02-15 12:32:40 -080057import android.view.MenuInflater;
Vikram Aggarwala55b36c2012-01-13 11:45:02 -080058import android.view.MenuItem;
59import android.view.MotionEvent;
Mindy Pereirad33674992012-06-25 16:26:30 -070060import android.view.View;
Andy Huang12b3ee42013-04-24 22:49:43 -070061import android.widget.ListView;
Mindy Pereirafd0c2972012-03-27 13:50:39 -070062import android.widget.Toast;
Vikram Aggarwala55b36c2012-01-13 11:45:02 -080063
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -080064import com.android.mail.ConversationListContext;
Vikram Aggarwal59f741f2013-03-01 15:55:40 -080065import com.android.mail.MailLogService;
Andy Huangf9a73482012-03-13 15:54:02 -070066import com.android.mail.R;
Andy Huang761522c2013-08-08 13:09:11 -070067import com.android.mail.analytics.Analytics;
Jin Cao779dd602014-04-22 16:16:28 -070068import com.android.mail.analytics.AnalyticsTimer;
Vikram Aggarwal6cadbfc2012-12-27 09:17:05 -080069import com.android.mail.browse.ConfirmDialogFragment;
Mindy Pereira967ede62012-03-22 09:29:09 -070070import com.android.mail.browse.ConversationCursor;
Yu Ping Hu7c909c72013-01-18 11:58:01 -080071import com.android.mail.browse.ConversationCursor.ConversationOperation;
mindypca87de42012-09-28 15:02:39 -070072import com.android.mail.browse.ConversationItemViewModel;
Andrew Sapperstein8812d3c2013-06-04 17:06:41 -070073import com.android.mail.browse.ConversationMessage;
Paul Westbrookbf232c32012-04-18 03:17:41 -070074import com.android.mail.browse.ConversationPagerController;
Marc Blank7c9f6ac2012-04-02 13:27:19 -070075import com.android.mail.browse.SelectedConversationsActionMenu;
Andy Huang991f4532012-08-14 13:32:55 -070076import com.android.mail.browse.SyncErrorDialogFragment;
Jin Cao30c881a2014-04-08 14:28:36 -070077import com.android.mail.browse.UndoCallback;
Mindy Pereira9b875682012-02-15 18:10:54 -080078import com.android.mail.compose.ComposeActivity;
Vikram Aggarwal177097f2013-03-08 11:19:53 -080079import com.android.mail.content.CursorCreator;
80import com.android.mail.content.ObjectCursor;
81import com.android.mail.content.ObjectCursorLoader;
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -080082import com.android.mail.providers.Account;
Mindy Pereira9b875682012-02-15 18:10:54 -080083import com.android.mail.providers.Conversation;
Andy Huang839ada22012-07-20 15:48:40 -070084import com.android.mail.providers.ConversationInfo;
Vikram Aggarwal4a5c5302012-01-12 15:07:13 -080085import com.android.mail.providers.Folder;
Vikram Aggarwal7a5d95a2012-07-27 16:24:54 -070086import com.android.mail.providers.FolderWatcher;
Paul Westbrookc2074c42012-03-22 15:26:58 -070087import com.android.mail.providers.MailAppProvider;
Mindy Pereiradac00542012-03-01 10:50:33 -080088import com.android.mail.providers.Settings;
Vikram Aggarwal80aeac52012-02-07 15:27:20 -080089import com.android.mail.providers.UIProvider;
Mindy Pereira3cb28b52012-05-24 15:26:39 -070090import com.android.mail.providers.UIProvider.AccountCapabilities;
Paul Westbrook2388c5d2012-03-25 12:29:11 -070091import com.android.mail.providers.UIProvider.AccountCursorExtraKeys;
Scott Kennedy0d0f8b02012-10-12 15:18:18 -070092import com.android.mail.providers.UIProvider.AutoAdvance;
Mindy Pereirac9d59182012-03-22 16:06:46 -070093import com.android.mail.providers.UIProvider.ConversationColumns;
Paul Westbrook5109c512012-11-05 11:00:30 -080094import com.android.mail.providers.UIProvider.ConversationOperations;
Vikram Aggarwal54452ae2012-03-13 15:29:00 -070095import com.android.mail.providers.UIProvider.FolderCapabilities;
Scott Kennedya158ac82013-09-04 13:48:13 -070096import com.android.mail.providers.UIProvider.FolderType;
Andrew Sappersteinc2c9dc12012-07-02 18:17:32 -070097import com.android.mail.ui.ActionableToastBar.ActionClickedListener;
Andy Huang839ada22012-07-20 15:48:40 -070098import com.android.mail.utils.ContentProviderTask;
Andy Huang144bfe72013-06-11 13:27:52 -070099import com.android.mail.utils.DrawIdler;
Paul Westbrookb334c902012-06-25 11:42:46 -0700100import com.android.mail.utils.LogTag;
Mindy Pereira5cb0c3e2012-02-22 15:22:47 -0800101import com.android.mail.utils.LogUtils;
Andy Huang61f26c22014-03-13 18:24:52 -0700102import com.android.mail.utils.MailObservable;
Scott Kennedycb85aea2013-02-25 13:08:32 -0800103import com.android.mail.utils.NotificationActionUtils;
Vikram Aggarwalfa131a22012-02-02 13:56:22 -0800104import com.android.mail.utils.Utils;
Vikram Aggarwal69a6cdf2013-01-08 16:05:17 -0800105import com.android.mail.utils.VeiledAddressMatcher;
Paul Westbrookca08fc12012-07-31 12:01:15 -0700106import com.google.common.base.Objects;
Paul Westbrook77eee622012-07-10 13:41:57 -0700107import com.google.common.collect.ImmutableList;
Mindy Pereiraacf60392012-04-06 09:11:00 -0700108import com.google.common.collect.Lists;
Marc Blank167faa82012-03-21 13:11:53 -0700109import com.google.common.collect.Sets;
Vikram Aggarwal4a5c5302012-01-12 15:07:13 -0800110
Marc Blank167faa82012-03-21 13:11:53 -0700111import java.util.ArrayList;
Andy Huang9e4ca792013-02-28 14:33:43 -0800112import java.util.Arrays;
Mindy Pereirafbe40192012-03-20 10:40:45 -0700113import java.util.Collection;
Andy Huang839ada22012-07-20 15:48:40 -0700114import java.util.Collections;
Mindy Pereira8db7e402012-07-13 10:32:47 -0700115import java.util.HashMap;
Vikram Aggarwalcc754b12012-08-30 14:04:21 -0700116import java.util.List;
Paul Westbrook23b74b92012-02-29 11:36:12 -0800117import java.util.Set;
Marc Blankbf128eb2012-04-18 15:58:45 -0700118import java.util.TimerTask;
Paul Westbrook23b74b92012-02-29 11:36:12 -0800119
120
Vikram Aggarwal4a5c5302012-01-12 15:07:13 -0800121/**
Mindy Pereira161f50d2012-02-28 15:47:19 -0800122 * This is an abstract implementation of the Activity Controller. This class
123 * knows how to respond to menu items, state changes, layout changes, etc. It
124 * weaves together the views and listeners, dispatching actions to the
125 * respective underlying classes.
Vikram Aggarwala55b36c2012-01-13 11:45:02 -0800126 * <p>
Mindy Pereira161f50d2012-02-28 15:47:19 -0800127 * Even though this class is abstract, it should provide default implementations
128 * for most, if not all the methods in the ActivityController interface. This
129 * makes the task of the subclasses easier: OnePaneActivityController and
130 * TwoPaneActivityController can be concise when the common functionality is in
131 * AbstractActivityController.
132 * </p>
133 * <p>
134 * In the Gmail codebase, this was called BaseActivityController
135 * </p>
Vikram Aggarwal4a5c5302012-01-12 15:07:13 -0800136 */
Andrew Sappersteined5b52d2013-04-30 13:40:18 -0700137public abstract class AbstractActivityController implements ActivityController,
James Lemieux5d79a912014-07-16 14:22:26 -0700138 EmptyFolderDialogFragment.EmptyFolderDialogFragmentListener, View.OnClickListener {
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -0800139 // Keys for serialization of various information in Bundles.
Vikram Aggarwalcabd3f22012-04-19 10:14:41 -0700140 /** Tag for {@link #mAccount} */
Vikram Aggarwal6c511582012-02-27 10:59:47 -0800141 private static final String SAVED_ACCOUNT = "saved-account";
Vikram Aggarwalcabd3f22012-04-19 10:14:41 -0700142 /** Tag for {@link #mFolder} */
Mindy Pereira5e478d22012-03-26 18:04:58 -0700143 private static final String SAVED_FOLDER = "saved-folder";
Vikram Aggarwalcabd3f22012-04-19 10:14:41 -0700144 /** Tag for {@link #mCurrentConversation} */
Mindy Pereira26f23fc2012-03-27 10:26:04 -0700145 private static final String SAVED_CONVERSATION = "saved-conversation";
Jin Caoec0fa482014-08-28 16:38:08 -0700146 /** Tag for {@link #mCheckedSet} */
Vikram Aggarwalcabd3f22012-04-19 10:14:41 -0700147 private static final String SAVED_SELECTED_SET = "saved-selected-set";
Vikram Aggarwal649b9ea2012-08-27 12:15:20 -0700148 /** Tag for {@link ActionableToastBar#getOperation()} */
Mindy Pereirad33674992012-06-25 16:26:30 -0700149 private static final String SAVED_TOAST_BAR_OP = "saved-toast-bar-op";
Vikram Aggarwal649b9ea2012-08-27 12:15:20 -0700150 /** Tag for {@link #mFolderListFolder} */
151 private static final String SAVED_HIERARCHICAL_FOLDER = "saved-hierarchical-folder";
Vikram Aggarwalf3341402012-08-07 10:09:38 -0700152 /** Tag for {@link ConversationListContext#searchQuery} */
153 private static final String SAVED_QUERY = "saved-query";
Vikram Aggarwal6cadbfc2012-12-27 09:17:05 -0800154 /** Tag for {@link #mDialogAction} */
155 private static final String SAVED_ACTION = "saved-action";
Vikram Aggarwalb8c31712013-01-03 17:03:19 -0800156 /** Tag for {@link #mDialogFromSelectedSet} */
157 private static final String SAVED_ACTION_FROM_SELECTED = "saved-action-from-selected";
Vikram Aggarwala91d00b2013-01-18 12:00:37 -0800158 /** Tag for {@link #mDetachedConvUri} */
159 private static final String SAVED_DETACHED_CONV_URI = "saved-detached-conv-uri";
Alice Yangebeef1b2013-09-04 06:41:10 +0000160 /** Key to store {@link #mInbox}. */
Scott Kennedyf77806e2013-08-30 11:38:15 -0700161 private static final String SAVED_INBOX_KEY = "m-inbox";
162 /** Key to store {@link #mConversationListScrollPositions} */
163 private static final String SAVED_CONVERSATION_LIST_SCROLL_POSITIONS =
164 "saved-conversation-list-scroll-positions";
Mindy Pereira5cb0c3e2012-02-22 15:22:47 -0800165
Greg Bullockede2e522014-05-30 14:11:35 +0200166 /** Tag used when loading a wait fragment */
Vikram Aggarwal6902dcf2012-04-11 08:57:42 -0700167 protected static final String TAG_WAIT = "wait-fragment";
168 /** Tag used when loading a conversation list fragment. */
Paul Westbrookbf232c32012-04-18 03:17:41 -0700169 public static final String TAG_CONVERSATION_LIST = "tag-conversation-list";
Scott Kennedy103319a2013-07-26 13:35:35 -0700170 /** Tag used when loading a custom fragment. */
171 protected static final String TAG_CUSTOM_FRAGMENT = "tag-custom-fragment";
Vikram Aggarwal6902dcf2012-04-11 08:57:42 -0700172
Vikram Aggarwala1b59dc2013-04-30 15:45:49 -0700173 /** Key to store an account in a bundle */
174 private final String BUNDLE_ACCOUNT_KEY = "account";
175 /** Key to store a folder in a bundle */
176 private final String BUNDLE_FOLDER_KEY = "folder";
Andrew Sapperstein5bb4d052014-03-31 16:22:31 -0700177 /**
178 * Key to set a flag for the ConversationCursorLoader to ignore any
179 * initial load limit that may be set by the Account. Instead,
180 * perform a full load instead of the full-stage load.
181 */
182 private final String BUNDLE_IGNORE_INITIAL_CONVERSATION_LIMIT_KEY =
183 "ignore-initial-conversation-limit";
Vikram Aggarwala1b59dc2013-04-30 15:45:49 -0700184
Vikram Aggarwald7a12cd2012-02-03 09:36:20 -0800185 protected Account mAccount;
Mindy Pereira12a4d802012-06-22 09:24:43 -0700186 protected Folder mFolder;
Alice Yangebeef1b2013-09-04 06:41:10 +0000187 protected Folder mInbox;
Vikram Aggarwal69b5c302012-09-05 11:11:13 -0700188 /** True when {@link #mFolder} is first shown to the user. */
189 private boolean mFolderChanged = false;
Andrew Sapperstein2d86d112014-07-24 20:27:37 -0700190 protected ActionBarController mActionBarController;
James Lemieux10fcd642014-03-03 13:01:04 -0800191 protected final MailActivity mActivity;
Vikram Aggarwala55b36c2012-01-13 11:45:02 -0800192 protected final Context mContext;
Vikram Aggarwal6902dcf2012-04-11 08:57:42 -0700193 private final FragmentManager mFragmentManager;
Vikram Aggarwalec5cbf72012-03-08 15:10:35 -0800194 protected final RecentFolderList mRecentFolderList;
Vikram Aggarwal80aeac52012-02-07 15:27:20 -0800195 protected ConversationListContext mConvListContext;
Mindy Pereira9b875682012-02-15 18:10:54 -0800196 protected Conversation mCurrentConversation;
Jin Caoc6801eb2014-08-12 18:16:57 -0700197 protected MaterialSearchViewController mSearchViewController;
Vikram Aggarwala91d00b2013-01-18 12:00:37 -0800198 /**
199 * The hash of {@link #mCurrentConversation} in detached mode. 0 if we are not in detached mode.
200 */
201 private Uri mDetachedConvUri;
Vikram Aggarwald7a12cd2012-02-03 09:36:20 -0800202
Scott Kennedyf77806e2013-08-30 11:38:15 -0700203 /** A map of {@link Folder} {@link Uri} to scroll position in the conversation list. */
204 private final Bundle mConversationListScrollPositions = new Bundle();
205
Paul Westbrook6ead20d2012-03-19 14:48:14 -0700206 /** A {@link android.content.BroadcastReceiver} that suppresses new e-mail notifications. */
207 private SuppressNotificationReceiver mNewEmailReceiver = null;
208
Vikram Aggarwal59f741f2013-03-01 15:55:40 -0800209 /** Handler for all our local runnables. */
Mindy Pereirafbe40192012-03-20 10:40:45 -0700210 protected Handler mHandler = new Handler();
Mindy Pereirafa995b42012-07-25 12:06:13 -0700211
Vikram Aggarwalfa131a22012-02-02 13:56:22 -0800212 /**
Mindy Pereira161f50d2012-02-28 15:47:19 -0800213 * The current mode of the application. All changes in mode are initiated by
214 * the activity controller. View mode changes are propagated to classes that
215 * attach themselves as listeners of view mode changes.
Vikram Aggarwalfa131a22012-02-02 13:56:22 -0800216 */
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -0800217 protected final ViewMode mViewMode;
Vikram Aggarwal80aeac52012-02-07 15:27:20 -0800218 protected ContentResolver mResolver;
Vikram Aggarwalc04cf7e2013-05-13 15:38:42 -0700219 protected boolean mHaveAccountList = false;
Mindy Pereirab7b33e02012-02-21 15:32:19 -0800220 private AsyncRefreshTask mAsyncRefreshTask;
Mindy Pereira5cb0c3e2012-02-22 15:22:47 -0800221
Andy Huang4e0158f2012-08-07 21:06:01 -0700222 private boolean mDestroyed;
223
Vikram Aggarwalbcb16b92013-01-28 18:05:03 -0800224 /** True if running on tablet */
225 private final boolean mIsTablet;
226
Andy Huang1ee96b22012-08-24 20:19:53 -0700227 /**
228 * Are we in a point in the Activity/Fragment lifecycle where it's safe to execute fragment
229 * transactions? (including back stack manipulation)
230 * <p>
231 * Per docs in {@link FragmentManager#beginTransaction()}, this flag starts out true, switches
232 * to false after {@link Activity#onSaveInstanceState}, and becomes true again in both onStart
233 * and onResume.
234 */
235 private boolean mSafeToModifyFragments = true;
236
Paul Westbrook23b74b92012-02-29 11:36:12 -0800237 private final Set<Uri> mCurrentAccountUris = Sets.newHashSet();
Mindy Pereira967ede62012-03-22 09:29:09 -0700238 protected ConversationCursor mConversationListCursor;
Andy Huang61f26c22014-03-13 18:24:52 -0700239 private final DataSetObservable mConversationListObservable = new MailObservable("List");
Marc Blankbf128eb2012-04-18 15:58:45 -0700240
Vikram Aggarwal59f741f2013-03-01 15:55:40 -0800241 /** Runnable that checks the logging level to enable/disable the logging service. */
242 private Runnable mLogServiceChecker = null;
Vikram Aggarwalde60c9d2013-04-10 12:58:56 -0700243 /** List of all accounts currently known to the controller. This is never null. */
244 private Account[] mAllAccounts = new Account[0];
Vikram Aggarwal59f741f2013-03-01 15:55:40 -0800245
Vikram Aggarwal77ee0ce2013-04-28 15:32:36 -0700246 private FolderWatcher mFolderWatcher;
247
Andrew Sapperstein5bb4d052014-03-31 16:22:31 -0700248 private boolean mIgnoreInitialConversationLimit;
249
Yu Ping Hu7c909c72013-01-18 11:58:01 -0800250 /**
251 * Interface for actions that are deferred until after a load completes. This is for handling
252 * user actions which affect cursors (e.g. marking messages read or unread) that happen before
253 * that cursor is loaded.
254 */
255 private interface LoadFinishedCallback {
256 void onLoadFinished();
257 }
258
259 /** The deferred actions to execute when mConversationListCursor load completes. */
260 private final ArrayList<LoadFinishedCallback> mConversationListLoadFinishedCallbacks =
261 new ArrayList<LoadFinishedCallback>();
262
Marc Blankbf128eb2012-04-18 15:58:45 -0700263 private RefreshTimerTask mConversationListRefreshTask;
Marc Blanke1d1b072012-04-13 17:29:16 -0700264
Vikram Aggarwal7c401b72012-08-13 16:43:47 -0700265 /** Listeners that are interested in changes to the current account. */
Andy Huang61f26c22014-03-13 18:24:52 -0700266 private final DataSetObservable mAccountObservers = new MailObservable("Account");
Vikram Aggarwal58cad2e2012-08-28 16:18:23 -0700267 /** Listeners that are interested in changes to the recent folders. */
Andy Huang61f26c22014-03-13 18:24:52 -0700268 private final DataSetObservable mRecentFolderObservers = new MailObservable("RecentFolder");
Vikram Aggarwal07dbaa62013-03-12 15:21:04 -0700269 /** Listeners that are interested in changes to the list of all accounts. */
Andy Huang61f26c22014-03-13 18:24:52 -0700270 private final DataSetObservable mAllAccountObservers = new MailObservable("AllAccounts");
Vikram Aggarwal50ff0e52013-03-14 13:58:02 -0700271 /** Listeners that are interested in changes to the current folder. */
Andy Huang61f26c22014-03-13 18:24:52 -0700272 private final DataSetObservable mFolderObservable = new MailObservable("CurrentFolder");
Tony Mantler54022ee2014-07-07 13:43:35 -0700273 /** Listeners that are interested in changes to the Folder or Account selection */
274 private final DataSetObservable mFolderOrAccountObservers =
275 new MailObservable("FolderOrAccount");
Vikram Aggarwal58cad2e2012-08-28 16:18:23 -0700276
Mindy Pereira967ede62012-03-22 09:29:09 -0700277 /**
278 * Selected conversations, if any.
279 */
Jin Caoec0fa482014-08-28 16:38:08 -0700280 private final ConversationCheckedSet mCheckedSet = new ConversationCheckedSet();
Mindy Pereiradac00542012-03-01 10:50:33 -0800281
Paul Westbrookc7a070f2012-04-12 01:46:41 -0700282 private final int mFolderItemUpdateDelayMs;
283
Vikram Aggarwal135fd022012-04-11 14:44:15 -0700284 /** Keeps track of selected and unselected conversations */
Paul Westbrook937c94f2012-08-16 13:01:18 -0700285 final protected ConversationPositionTracker mTracker;
Vikram Aggarwalaf1ee0c2012-04-12 17:13:13 -0700286
Vikram Aggarwale128fc22012-04-04 12:33:34 -0700287 /**
288 * Action menu associated with the selected set.
289 */
290 SelectedConversationsActionMenu mCabActionMenu;
James Lemieux1a1e9752014-07-18 13:50:40 -0700291
292 /** The compose button floating over the conversation/search lists */
293 protected View mFloatingComposeButton;
Andrew Sappersteinc2c9dc12012-07-02 18:17:32 -0700294 protected ActionableToastBar mToastBar;
Andy Huang632721e2012-04-11 16:57:26 -0700295 protected ConversationPagerController mPagerController;
Mindy Pereiraab486362012-03-21 18:18:53 -0700296
Vikram Aggarwald70fe492013-06-04 12:52:07 -0700297 // This is split out from the general loader dispatcher because its loader doesn't return a
Andy Huangb1c34dc2012-04-17 16:36:19 -0700298 // basic Cursor
Vikram Aggarwald70fe492013-06-04 12:52:07 -0700299 /** Handles loader callbacks to create a convesation cursor. */
Andy Huangb1c34dc2012-04-17 16:36:19 -0700300 private final ConversationListLoaderCallbacks mListCursorCallbacks =
301 new ConversationListLoaderCallbacks();
302
Vikram Aggarwal177097f2013-03-08 11:19:53 -0800303 /** Object that listens to all LoaderCallbacks that result in {@link Folder} creation. */
304 private final FolderLoads mFolderCallbacks = new FolderLoads();
305 /** Object that listens to all LoaderCallbacks that result in {@link Account} creation. */
306 private final AccountLoads mAccountCallbacks = new AccountLoads();
307
Vikram Aggarwal69a6cdf2013-01-08 16:05:17 -0800308 /**
309 * Matched addresses that must be shielded from users because they are temporary. Even though
310 * this is instantiated from settings, this matcher is valid for all accounts, and is expected
311 * to live past the life of an account.
312 */
313 private final VeiledAddressMatcher mVeiledMatcher;
314
Paul Westbrookb334c902012-06-25 11:42:46 -0700315 protected static final String LOG_TAG = LogTag.getLogTag();
Vikram Aggarwald70fe492013-06-04 12:52:07 -0700316
Vikram Aggarwalaa941d72013-06-04 15:34:28 -0700317 // Loader constants: Accounts
318 /**
319 * The list of accounts. This loader is started early in the application life-cycle since
320 * the list of accounts is central to all other data the application needs: unread counts for
321 * folders, critical UI settings like show/hide checkboxes, ...
322 * The loader is started when the application is created: both in
323 * {@link #onCreate(Bundle)} and in {@link #onActivityResult(int, int, Intent)}. It is never
324 * destroyed since the cursor is needed through the life of the application. When the list of
325 * accounts changes, we notify {@link #mAllAccountObservers}.
326 */
Vikram Aggarwalfa4b47e2012-03-09 13:02:46 -0800327 private static final int LOADER_ACCOUNT_CURSOR = 0;
Vikram Aggarwalaa941d72013-06-04 15:34:28 -0700328
329 /**
330 * The current account. This loader is started when we have an account. The mail application
331 * <b>needs</b> a valid account to function. As soon as we set {@link #mAccount},
332 * we start a loader to observe for changes on the current account.
333 * The loader is always restarted when an account is set in {@link #setAccount(Account)}.
334 * When the current account object changes, we notify {@link #mAccountObservers}.
335 * A possible performance improvement would be to listen purely on
336 * {@link #LOADER_ACCOUNT_CURSOR}. The current account is guaranteed to be in the list,
337 * and would avoid two updates when a single setting on the current account changes.
338 */
Régis Décamps004b46f2014-06-23 15:13:23 +0200339 private static final int LOADER_ACCOUNT_UPDATE_CURSOR = 1;
340
341 // Loader constants: Conversations
342
343 /** The conversation cursor over the current conversation list. This loader provides
344 * a cursor over conversation entries from a folder to display a conversation
345 * list.
346 * This loader is started when the user switches folders (in {@link #updateFolder(Folder)},
347 * or when the controller is told that a folder/account change is imminent
348 * (in {@link #preloadConvList(Account, Folder)}. The loader is maintained for the life of
349 * the current folder. When the user switches folders, the old loader is destroyed and a new
350 * one is created.
351 *
352 * When the conversation list changes, we notify {@link #mConversationListObservable}.
353 */
354 private static final int LOADER_CONVERSATION_LIST = 10;
355
356 // Loader constants: misc
357 /**
358 * The loader that determines whether the Warm welcome tour should be displayed for the user.
359 */
360 public static final int LOADER_WELCOME_TOUR = 20;
Vikram Aggarwald70fe492013-06-04 12:52:07 -0700361
Greg Bullock944ef3b2014-07-17 12:15:07 +0200362 /**
363 * The load which loads accounts for the welcome tour.
364 */
365 public static final int LOADER_WELCOME_TOUR_ACCOUNTS = 21;
366
Vikram Aggarwalaa941d72013-06-04 15:34:28 -0700367 // Loader constants: Folders
Régis Décamps004b46f2014-06-23 15:13:23 +0200368
Vikram Aggarwalaa941d72013-06-04 15:34:28 -0700369 /** The current folder. This loader watches for updates to the current folder in a manner
370 * analogous to the {@link #LOADER_ACCOUNT_UPDATE_CURSOR}. Updates to the current folder
371 * might be due to server-side changes (unread count), or local changes (sync window or sync
372 * status change).
373 * The change of current folder calls {@link #updateFolder(Folder)}.
374 * This is responsible for restarting a loader using the URI of the provided folder. When the
375 * loader returns, the current folder is updated and consumers, if any, are notified.
376 * When the current folder changes, we notify {@link #mFolderObservable}
377 */
Régis Décamps004b46f2014-06-23 15:13:23 +0200378 private static final int LOADER_FOLDER_CURSOR = 30;
379
Vikram Aggarwalaa941d72013-06-04 15:34:28 -0700380 /**
381 * The list of recent folders. Recent folders are shown in the DrawerFragment. The recent
382 * folders are tied to the current account being viewed. When the account is changed,
383 * we restart this loader to retrieve the recent accounts. Recents are pre-populated for
384 * phones historically, when they were displayed in the spinner. On the tablet,
385 * they showed in the {@link FolderListFragment} and were not-populated. The code to
386 * pre-populate the recents is somewhat convoluted: when the loader returns a short list of
387 * recent folders, it issues an update on the Recent Folder URI. The underlying provider then
388 * does the appropriate thing to populate recent folders, and notify of a change on the cursor.
389 * Recent folders are needed for the life of the current account.
390 * When the recent folders change, we notify {@link #mRecentFolderObservers}.
391 */
Régis Décamps004b46f2014-06-23 15:13:23 +0200392 private static final int LOADER_RECENT_FOLDERS = 31;
Vikram Aggarwalaa941d72013-06-04 15:34:28 -0700393 /**
394 * The primary inbox for the current account. The mechanism to load the default inbox for the
395 * current account is (sadly) different from loading other folders. The method
396 * {@link #loadAccountInbox()} is called, and it restarts this loader. When the loader returns
397 * a valid cursor, we create a folder, call {@link #onFolderChanged{Folder)} eventually
398 * calling {@link #updateFolder(Folder)} which starts a loader {@link #LOADER_FOLDER_CURSOR}
399 * over the current folder.
400 * When we have a valid cursor, we destroy this loader, This convoluted flow is historical.
401 */
Régis Décamps004b46f2014-06-23 15:13:23 +0200402 private static final int LOADER_ACCOUNT_INBOX = 32;
403
Vikram Aggarwalaa941d72013-06-04 15:34:28 -0700404 /**
405 * The fake folder of search results for a term. When we search for a term,
406 * a new activity is created with {@link Intent#ACTION_SEARCH}. For this new activity,
407 * we start a loader which returns conversations that match the user-provided query.
408 * We destroy the loader when we obtain a valid cursor since subsequent searches will create
409 * a new activity.
410 */
Régis Décamps004b46f2014-06-23 15:13:23 +0200411 private static final int LOADER_SEARCH = 33;
Vikram Aggarwalaa941d72013-06-04 15:34:28 -0700412 /**
413 * The initial folder at app start. When the application is launched from an intent that
414 * specifies the initial folder (notifications/widgets/shortcuts),
415 * then we extract the folder URI from the intent, but we cannot trust the folder object. Since
416 * shortcuts and widgets persist past application update, they might have incorrect
417 * information encoded in them. So, to obtain a {@link Folder} object from a {@link Uri},
418 * we need to start another loader. Upon obtaining a valid cursor, the loader is destroyed.
419 * An additional complication arises if we have to view a specific conversation within this
420 * folder. This is the case when launching the app from a single conversation notification
421 * or tapping on a specific conversation in the widget. In these cases, the conversation is
422 * saved in {@link #mConversationToShow} and is retrieved when the loader returns.
423 */
Régis Décamps004b46f2014-06-23 15:13:23 +0200424 public static final int LOADER_FIRST_FOLDER = 34;
Régis Décampsde21e892014-06-19 20:59:13 +0200425
426 /**
Vikram Aggarwal7a5d95a2012-07-27 16:24:54 -0700427 * Guaranteed to be the last loader ID used by the activity. Loaders are owned by Activity or
428 * fragments, and within an activity, loader IDs need to be unique. A hack to ensure that the
429 * {@link FolderWatcher} can create its folder loaders without clashing with the IDs of those
430 * of the {@link AbstractActivityController}. Currently, the {@link FolderWatcher} is the only
431 * other class that uses this activity's LoaderManager. If another class needs activity-level
432 * loaders, consider consolidating the loaders in a central location: a UI-less fragment
433 * perhaps.
434 */
Régis Décamps004b46f2014-06-23 15:13:23 +0200435 public static final int LAST_LOADER_ID = 35;
436
Scott Kennedy7c8325d2013-02-28 10:46:10 -0800437 /**
438 * Guaranteed to be the last loader ID used by the Fragment. Loaders are owned by Activity or
439 * fragments, and within an activity, loader IDs need to be unique. Currently,
Andrew Sapperstein2d86d112014-07-24 20:27:37 -0700440 * SectionedInboxTeaserView is the only class that uses the
Scott Kennedy7c8325d2013-02-28 10:46:10 -0800441 * {@link ConversationListFragment}'s LoaderManager.
442 */
443 public static final int LAST_FRAGMENT_LOADER_ID = 1000;
Vikram Aggarwala55b36c2012-01-13 11:45:02 -0800444
Vikram Aggarwal6aca6892013-06-04 13:53:27 -0700445 /** Code returned after an account has been added. */
Paul Westbrook2388c5d2012-03-25 12:29:11 -0700446 private static final int ADD_ACCOUNT_REQUEST_CODE = 1;
Vikram Aggarwal6aca6892013-06-04 13:53:27 -0700447 /** Code returned when the user has to enter the new password on an existing account. */
Paul Westbrook122f7c22012-08-20 17:50:31 -0700448 private static final int REAUTHENTICATE_REQUEST_CODE = 2;
Martin Hibdon371a71c2014-02-19 13:55:28 -0800449 /** Code returned when the previous activity needs to navigate to a different folder
450 * or account */
451 private static final int CHANGE_NAVIGATION_REQUEST_CODE = 3;
452
Greg Bullock406ae082014-08-27 17:03:16 +0200453 /** Code returned from voice search intent */
454 public static final int VOICE_SEARCH_REQUEST_CODE = 4;
455
Martin Hibdon371a71c2014-02-19 13:55:28 -0800456 public static final String EXTRA_FOLDER = "extra-folder";
457 public static final String EXTRA_ACCOUNT = "extra-account";
Paul Westbrook2388c5d2012-03-25 12:29:11 -0700458
Vikram Aggarwale8a85322012-04-24 09:01:38 -0700459 /** The pending destructive action to be carried out before swapping the conversation cursor.*/
460 private DestructiveAction mPendingDestruction;
Andrew Sappersteinc2c9dc12012-07-02 18:17:32 -0700461 protected AsyncRefreshTask mFolderSyncTask;
Mindy Pereirac975e842012-07-16 09:15:00 -0700462 private Folder mFolderListFolder;
Martin Hibdone78c40f2013-10-10 18:29:25 -0700463 private final int mShowUndoBarDelay;
mindyp6f54e1b2012-10-09 09:54:08 -0700464 private boolean mRecentsDataUpdated;
Vikram Aggarwala3f43d42012-10-25 16:21:30 -0700465 /** A wait fragment we added, if any. */
466 private WaitFragment mWaitFragment;
Vikram Aggarwalf0ef2302012-11-07 14:53:34 -0800467 /** True if we have results from a search query */
468 private boolean mHaveSearchResults = false;
Vikram Aggarwal6cadbfc2012-12-27 09:17:05 -0800469 /** If a confirmation dialog is being show, the listener for the positive action. */
470 private OnClickListener mDialogListener;
471 /**
472 * If a confirmation dialog is being show, the resource of the action: R.id.delete, etc. This
473 * is used to create a new {@link #mDialogListener} on orientation changes.
474 */
475 private int mDialogAction = -1;
Vikram Aggarwalb8c31712013-01-03 17:03:19 -0800476 /**
477 * If a confirmation dialog is being shown, this is true if the dialog acts on the selected set
478 * and false if it acts on the currently selected conversation
479 */
480 private boolean mDialogFromSelectedSet;
Vikram Aggarwal6cadbfc2012-12-27 09:17:05 -0800481
Vikram Aggarwal177097f2013-03-08 11:19:53 -0800482 /** Which conversation to show, if started from widget/notification. */
483 private Conversation mConversationToShow = null;
484
Andy Huangc94d07f2013-06-03 16:19:35 -0700485 /**
486 * A temporary reference to the pending destructive action that was deferred due to an
487 * auto-advance transition in progress.
488 * <p>
489 * In detail: when auto-advance triggers a mode change, we must wait until the transition
490 * completes before executing the destructive action to ensure a smooth mode change transition.
491 * This member variable houses the pending destructive action work to be run upon completion.
492 */
493 private Runnable mAutoAdvanceOp = null;
494
Andy Huang12b3ee42013-04-24 22:49:43 -0700495 protected DrawerLayout mDrawerContainer;
496 protected View mDrawerPullout;
497 protected ActionBarDrawerToggle mDrawerToggle;
Andy Huangf58e4c32014-07-09 16:58:18 -0700498
Andy Huang12b3ee42013-04-24 22:49:43 -0700499 protected ListView mListViewForAnimating;
500 protected boolean mHasNewAccountOrFolder;
Andrew Sappersteina3ce6782013-05-09 15:14:49 -0700501 private boolean mConversationListLoadFinishedIgnored;
Andy Huang61f26c22014-03-13 18:24:52 -0700502 private final MailDrawerListener mDrawerListener = new MailDrawerListener();
Andrew Sapperstein5747e152013-05-13 14:13:08 -0700503 private boolean mHideMenuItems;
Andy Huang12b3ee42013-04-24 22:49:43 -0700504
Andy Huang144bfe72013-06-11 13:27:52 -0700505 private final DrawIdler mDrawIdler = new DrawIdler();
506
Andrew Sapperstein00179f12012-08-09 15:15:40 -0700507 public static final String SYNC_ERROR_DIALOG_FRAGMENT_TAG = "SyncErrorDialogFragment";
Vikram Aggarwale8a85322012-04-24 09:01:38 -0700508
Scott Kennedycb85aea2013-02-25 13:08:32 -0800509 private final DataSetObserver mUndoNotificationObserver = new DataSetObserver() {
510 @Override
511 public void onChanged() {
512 super.onChanged();
513
514 if (mConversationListCursor != null) {
515 mConversationListCursor.handleNotificationActions();
516 }
517 }
518 };
519
Andrew Sapperstein53de4482014-07-29 02:39:39 +0000520 private final HomeButtonListener mHomeButtonListener = new HomeButtonListener();
521
Vikram Aggarwala55b36c2012-01-13 11:45:02 -0800522 public AbstractActivityController(MailActivity activity, ViewMode viewMode) {
523 mActivity = activity;
Vikram Aggarwal6902dcf2012-04-11 08:57:42 -0700524 mFragmentManager = mActivity.getFragmentManager();
Vikram Aggarwala55b36c2012-01-13 11:45:02 -0800525 mViewMode = viewMode;
526 mContext = activity.getApplicationContext();
Vikram Aggarwal025eba82012-05-08 10:45:30 -0700527 mRecentFolderList = new RecentFolderList(mContext);
Paul Westbrook937c94f2012-08-16 13:01:18 -0700528 mTracker = new ConversationPositionTracker(this);
Mindy Pereira967ede62012-03-22 09:29:09 -0700529 // Allow the fragment to observe changes to its own selection set. No other object is
530 // aware of the selected set.
Jin Caoec0fa482014-08-28 16:38:08 -0700531 mCheckedSet.addObserver(this);
Paul Westbrookc7a070f2012-04-12 01:46:41 -0700532
Vikram Aggarwalbcb16b92013-01-28 18:05:03 -0800533 final Resources r = mContext.getResources();
534 mFolderItemUpdateDelayMs = r.getInteger(R.integer.folder_item_refresh_delay_ms);
535 mShowUndoBarDelay = r.getInteger(R.integer.show_undo_bar_delay_ms);
Vikram Aggarwal69a6cdf2013-01-08 16:05:17 -0800536 mVeiledMatcher = VeiledAddressMatcher.newInstance(activity.getResources());
Vikram Aggarwalbcb16b92013-01-28 18:05:03 -0800537 mIsTablet = Utils.useTabletUI(r);
Andrew Sappersteina3ce6782013-05-09 15:14:49 -0700538 mConversationListLoadFinishedIgnored = false;
Vikram Aggarwala55b36c2012-01-13 11:45:02 -0800539 }
Vikram Aggarwal4a5c5302012-01-12 15:07:13 -0800540
Vikram Aggarwale9a81032012-02-22 13:15:35 -0800541 public Account getCurrentAccount() {
542 return mAccount;
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -0800543 }
544
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -0800545 public ConversationListContext getCurrentListContext() {
Vikram Aggarwale9a81032012-02-22 13:15:35 -0800546 return mConvListContext;
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -0800547 }
548
549 @Override
Vikram Aggarwalef7c9922012-04-23 12:35:04 -0700550 public final ConversationCursor getConversationListCursor() {
Mindy Pereira967ede62012-03-22 09:29:09 -0700551 return mConversationListCursor;
552 }
553
Vikram Aggarwal6902dcf2012-04-11 08:57:42 -0700554 /**
Vikram Aggarwal34a65012012-04-17 12:39:06 -0700555 * Check if the fragment is attached to an activity and has a root view.
Vikram Aggarwal715f83d2013-03-05 17:04:56 -0800556 * @param in fragment to be checked
Vikram Aggarwal34a65012012-04-17 12:39:06 -0700557 * @return true if the fragment is valid, false otherwise
558 */
Vikram Aggarwal715f83d2013-03-05 17:04:56 -0800559 private static boolean isValidFragment(Fragment in) {
560 return !(in == null || in.getActivity() == null || in.getView() == null);
Vikram Aggarwal34a65012012-04-17 12:39:06 -0700561 }
562
563 /**
Vikram Aggarwal49e0e992012-09-21 13:53:15 -0700564 * Get the conversation list fragment for this activity. If the conversation list fragment is
565 * not attached, this method returns null.
Andy Huang839ada22012-07-20 15:48:40 -0700566 *
Vikram Aggarwal49e0e992012-09-21 13:53:15 -0700567 * Caution! This method returns the {@link ConversationListFragment} after the fragment has been
568 * added, <b>and</b> after the {@link FragmentManager} has run through its queue to add the
569 * fragment. There is a non-trivial amount of time after the fragment is instantiated and before
570 * this call returns a non-null value, depending on the {@link FragmentManager}. If you
571 * need the fragment immediately after adding it, consider making the fragment an observer of
572 * the controller and perform the task immediately on {@link Fragment#onActivityCreated(Bundle)}
Vikram Aggarwal6902dcf2012-04-11 08:57:42 -0700573 */
574 protected ConversationListFragment getConversationListFragment() {
575 final Fragment fragment = mFragmentManager.findFragmentByTag(TAG_CONVERSATION_LIST);
Vikram Aggarwal34a65012012-04-17 12:39:06 -0700576 if (isValidFragment(fragment)) {
577 return (ConversationListFragment) fragment;
Andy Huang9585d782012-04-16 19:45:04 -0700578 }
Vikram Aggarwal34a65012012-04-17 12:39:06 -0700579 return null;
Vikram Aggarwal6902dcf2012-04-11 08:57:42 -0700580 }
581
582 /**
Vikram Aggarwal6902dcf2012-04-11 08:57:42 -0700583 * Returns the folder list fragment attached with this activity. If no such fragment is attached
584 * this method returns null.
Andy Huang839ada22012-07-20 15:48:40 -0700585 *
Vikram Aggarwal49e0e992012-09-21 13:53:15 -0700586 * Caution! This method returns the {@link FolderListFragment} after the fragment has been
587 * added, <b>and</b> after the {@link FragmentManager} has run through its queue to add the
588 * fragment. There is a non-trivial amount of time after the fragment is instantiated and before
589 * this call returns a non-null value, depending on the {@link FragmentManager}. If you
590 * need the fragment immediately after adding it, consider making the fragment an observer of
591 * the controller and perform the task immediately on {@link Fragment#onActivityCreated(Bundle)}
Vikram Aggarwal6902dcf2012-04-11 08:57:42 -0700592 */
593 protected FolderListFragment getFolderListFragment() {
James Lemieux10fcd642014-03-03 13:01:04 -0800594 final String drawerPulloutTag = mActivity.getString(R.string.drawer_pullout_tag);
595 final Fragment fragment = mFragmentManager.findFragmentByTag(drawerPulloutTag);
Vikram Aggarwal34a65012012-04-17 12:39:06 -0700596 if (isValidFragment(fragment)) {
597 return (FolderListFragment) fragment;
Andy Huang9585d782012-04-16 19:45:04 -0700598 }
Vikram Aggarwal34a65012012-04-17 12:39:06 -0700599 return null;
Vikram Aggarwal6902dcf2012-04-11 08:57:42 -0700600 }
601
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -0800602 /**
Mindy Pereira161f50d2012-02-28 15:47:19 -0800603 * Initialize the action bar. This is not visible to OnePaneController and
604 * TwoPaneController so they cannot override this behavior.
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -0800605 */
Vikram Aggarwalabd24d82012-04-26 13:23:14 -0700606 private void initializeActionBar() {
Andrew Sapperstein52882ff2014-07-27 12:30:18 -0700607 final ActionBar actionBar = mActivity.getSupportActionBar();
Andy Huang5895f7b2012-06-01 17:07:20 -0700608 if (actionBar == null) {
609 return;
Vikram Aggarwalabd24d82012-04-26 13:23:14 -0700610 }
Andy Huang5895f7b2012-06-01 17:07:20 -0700611
Jin Caoc6801eb2014-08-12 18:16:57 -0700612 mActionBarController = new ActionBarController(mContext);
Andrew Sapperstein2d86d112014-07-24 20:27:37 -0700613 mActionBarController.initialize(mActivity, this, actionBar);
Jin Caoc6801eb2014-08-12 18:16:57 -0700614 actionBar.setShowHideAnimationEnabled(false);
Rohan Shah1dd054f2013-04-01 11:23:44 -0700615
Andy Huang12b3ee42013-04-24 22:49:43 -0700616 // init the action bar to allow the 'up' affordance.
617 // any configurations that disallow 'up' should do that later.
Andrew Sapperstein2d86d112014-07-24 20:27:37 -0700618 mActionBarController.setBackButton();
Vikram Aggarwalabd24d82012-04-26 13:23:14 -0700619 }
620
621 /**
622 * Attach the action bar to the activity.
623 */
624 private void attachActionBar() {
Andrew Sapperstein52882ff2014-07-27 12:30:18 -0700625 final ActionBar actionBar = mActivity.getSupportActionBar();
Tony Mantlerbd091502013-09-16 13:59:47 -0700626 if (actionBar != null) {
Andrew Sapperstein2d86d112014-07-24 20:27:37 -0700627 // Show a title
628 final int mask = ActionBar.DISPLAY_SHOW_TITLE | ActionBar.DISPLAY_SHOW_HOME;
Vikram Aggarwalbc462ca2013-03-15 10:41:03 -0700629 actionBar.setDisplayOptions(mask, mask);
Andrew Sapperstein2d86d112014-07-24 20:27:37 -0700630 mActionBarController.setViewModeController(mViewMode);
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -0800631 }
632 }
633
634 /**
Mindy Pereira161f50d2012-02-28 15:47:19 -0800635 * Returns whether the conversation list fragment is visible or not.
636 * Different layouts will have their own notion on the visibility of
637 * fragments, so this method needs to be overriden.
638 *
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -0800639 */
640 protected abstract boolean isConversationListVisible();
641
Vikram Aggarwaleb7d3292012-04-20 17:07:20 -0700642 /**
Vikram Aggarwal34f7b232012-10-17 13:32:23 -0700643 * If required, starts wait mode for the current account.
Vikram Aggarwal34f7b232012-10-17 13:32:23 -0700644 */
645 final void perhapsEnterWaitMode() {
646 // If the account is not initialized, then show the wait fragment, since nothing can be
647 // shown.
648 if (mAccount.isAccountInitializationRequired()) {
649 showWaitForInitialization();
650 return;
651 }
652
653 final boolean inWaitingMode = inWaitMode();
654 final boolean isSyncRequired = mAccount.isAccountSyncRequired();
655 if (isSyncRequired) {
656 if (inWaitingMode) {
657 // Update the WaitFragment's account object
658 updateWaitMode();
659 } else {
660 // Transition to waiting mode
661 showWaitForInitialization();
662 }
663 } else if (inWaitingMode) {
664 // Dismiss waiting mode
665 hideWaitForInitialization();
666 }
667 }
668
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -0800669 @Override
Andrew Sapperstein4cbb0da2013-04-19 11:28:02 -0700670 public void switchToDefaultInboxOrChangeAccount(Account account) {
671 LogUtils.d(LOG_TAG, "AAC.switchToDefaultAccount(%s)", account);
Andy Huange764cfd2014-02-26 11:55:03 -0800672 if (mViewMode.isSearchMode()) {
Martin Hibdon371a71c2014-02-19 13:55:28 -0800673 // We are in an activity on top of the main navigation activity.
674 // We need to return to it with a result code that indicates it should navigate to
675 // a different folder.
676 final Intent intent = new Intent();
677 intent.putExtra(AbstractActivityController.EXTRA_ACCOUNT, account);
678 mActivity.setResult(Activity.RESULT_OK, intent);
679 mActivity.finish();
680 return;
681 }
Vikram Aggarwal66bc6ed2012-07-31 09:59:55 -0700682 final boolean firstLoad = mAccount == null;
Andrew Sapperstein252684a2013-04-17 14:43:26 -0700683 final boolean switchToDefaultInbox = !firstLoad && account.uri.equals(mAccount.uri);
Vikram Aggarwald70fe492013-06-04 12:52:07 -0700684 // If the active account has been clicked in the drawer, go to default inbox
Andrew Sapperstein252684a2013-04-17 14:43:26 -0700685 if (switchToDefaultInbox) {
686 loadAccountInbox();
687 return;
688 }
Andrew Sapperstein4cbb0da2013-04-19 11:28:02 -0700689 changeAccount(account);
690 }
691
Andrew Sapperstein4cbb0da2013-04-19 11:28:02 -0700692 public void changeAccount(Account account) {
693 LogUtils.d(LOG_TAG, "AAC.changeAccount(%s)", account);
694 // Is the account or account settings different from the existing account?
695 final boolean firstLoad = mAccount == null;
696 final boolean accountChanged = firstLoad || !account.uri.equals(mAccount.uri);
697
Vikram Aggarwal472e5362012-11-05 14:17:23 -0800698 // If nothing has changed, return early without wasting any more time.
699 if (!accountChanged && !account.settingsDiffer(mAccount)) {
700 return;
701 }
702 // We also don't want to do anything if the new account is null
703 if (account == null) {
Vikram Aggarwal5fd8afd2013-03-13 15:28:47 -0700704 LogUtils.e(LOG_TAG, "AAC.changeAccount(null) called.");
Vikram Aggarwal472e5362012-11-05 14:17:23 -0800705 return;
706 }
Tony Mantler79b11562013-10-09 15:31:50 -0700707 final String emailAddress = account.getEmailAddress();
Vikram Aggarwal472e5362012-11-05 14:17:23 -0800708 mHandler.post(new Runnable() {
709 @Override
710 public void run() {
Tony Mantler79b11562013-10-09 15:31:50 -0700711 MailActivity.setNfcMessage(emailAddress);
Mindy Pereirafa20c1a2012-07-23 13:00:02 -0700712 }
Vikram Aggarwal472e5362012-11-05 14:17:23 -0800713 });
714 if (accountChanged) {
715 commitDestructiveActions(false);
716 }
Régis Décamps2168cbc2014-08-22 15:00:37 +0200717
Vikram Aggarwal472e5362012-11-05 14:17:23 -0800718 // Change the account here
719 setAccount(account);
720 // And carry out associated actions.
721 cancelRefreshTask();
722 if (accountChanged) {
723 loadAccountInbox();
724 }
725 // Check if we need to force setting up an account before proceeding.
726 if (mAccount != null && !Uri.EMPTY.equals(mAccount.settings.setupIntentUri)) {
727 // Launch the intent!
728 final Intent intent = new Intent(Intent.ACTION_EDIT);
Tony Mantlerec6e2512014-05-22 13:31:10 -0700729
730 intent.setPackage(mContext.getPackageName());
Vikram Aggarwal472e5362012-11-05 14:17:23 -0800731 intent.setData(mAccount.settings.setupIntentUri);
Tony Mantlerec6e2512014-05-22 13:31:10 -0700732
Vikram Aggarwal472e5362012-11-05 14:17:23 -0800733 mActivity.startActivity(intent);
Mindy Pereira28d5f722012-02-15 12:32:40 -0800734 }
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -0800735 }
736
Vikram Aggarwale6340bc2012-03-26 15:57:09 -0700737 /**
Vikram Aggarwal7c401b72012-08-13 16:43:47 -0700738 * Adds a listener interested in change in the current account. If a class is storing a
739 * reference to the current account, it should listen on changes, so it can receive updates to
740 * settings. Must happen in the UI thread.
741 */
Mindy Pereiraefe3d252012-03-01 14:20:44 -0800742 @Override
Vikram Aggarwal7c401b72012-08-13 16:43:47 -0700743 public void registerAccountObserver(DataSetObserver obs) {
744 mAccountObservers.registerObserver(obs);
Mindy Pereiraefe3d252012-03-01 14:20:44 -0800745 }
746
Vikram Aggarwal7d816002012-04-17 17:06:41 -0700747 /**
Vikram Aggarwal7c401b72012-08-13 16:43:47 -0700748 * Removes a listener from receiving current account changes.
Vikram Aggarwal7d816002012-04-17 17:06:41 -0700749 * Must happen in the UI thread.
750 */
Vikram Aggarwal7c401b72012-08-13 16:43:47 -0700751 @Override
752 public void unregisterAccountObserver(DataSetObserver obs) {
753 mAccountObservers.unregisterObserver(obs);
Vikram Aggarwal7d816002012-04-17 17:06:41 -0700754 }
755
Vikram Aggarwal7c401b72012-08-13 16:43:47 -0700756 @Override
Vikram Aggarwal07dbaa62013-03-12 15:21:04 -0700757 public void registerAllAccountObserver(DataSetObserver observer) {
758 mAllAccountObservers.registerObserver(observer);
759 }
760
761 @Override
762 public void unregisterAllAccountObserver(DataSetObserver observer) {
763 mAllAccountObservers.unregisterObserver(observer);
764 }
765
766 @Override
767 public Account[] getAllAccounts() {
768 return mAllAccounts;
769 }
770
771 @Override
Vikram Aggarwal7c401b72012-08-13 16:43:47 -0700772 public Account getAccount() {
773 return mAccount;
Vikram Aggarwal7d816002012-04-17 17:06:41 -0700774 }
775
Rohan Shah0f73d902013-04-19 17:06:37 -0700776 @Override
Tony Mantler54022ee2014-07-07 13:43:35 -0700777 public void registerFolderOrAccountChangedObserver(final DataSetObserver observer) {
778 mFolderOrAccountObservers.registerObserver(observer);
Rohan Shah0f73d902013-04-19 17:06:37 -0700779 }
780
781 @Override
Tony Mantler54022ee2014-07-07 13:43:35 -0700782 public void unregisterFolderOrAccountChangedObserver(final DataSetObserver observer) {
783 mFolderOrAccountObservers.unregisterObserver(observer);
Rohan Shah0f73d902013-04-19 17:06:37 -0700784 }
785
786 /**
Andy Huang12b3ee42013-04-24 22:49:43 -0700787 * If the drawer is open, the function locks the drawer to the closed, thereby sliding in
788 * the drawer to the left edge, disabling events, and refreshing it once it's either closed
789 * or put in an idle state.
Rohan Shah0f73d902013-04-19 17:06:37 -0700790 */
791 @Override
Vikram Aggarwala1b59dc2013-04-30 15:45:49 -0700792 public void closeDrawer(final boolean hasNewFolderOrAccount, Account nextAccount,
793 Folder nextFolder) {
Andy Huang12b3ee42013-04-24 22:49:43 -0700794 if (!isDrawerEnabled()) {
Tony Mantler54022ee2014-07-07 13:43:35 -0700795 if (hasNewFolderOrAccount) {
796 mFolderOrAccountObservers.notifyChanged();
797 }
Andy Huang12b3ee42013-04-24 22:49:43 -0700798 return;
799 }
Andy Huang12b3ee42013-04-24 22:49:43 -0700800 // If there are no new folders or accounts to switch to, just close the drawer
801 if (!hasNewFolderOrAccount) {
802 mDrawerContainer.closeDrawers();
803 return;
804 }
Vikram Aggarwal2f9d3942013-05-03 12:31:39 -0700805 // Otherwise, start preloading the conversation list for the new folder.
806 if (nextFolder != null) {
807 preloadConvList(nextAccount, nextFolder);
808 }
809 // Remember if the conversation list view is animating
Andy Huang12b3ee42013-04-24 22:49:43 -0700810 final ConversationListFragment conversationList = getConversationListFragment();
811 if (conversationList != null) {
812 mListViewForAnimating = conversationList.getListView();
813 } else {
814 // There is no conversation list to animate, so just set it to null
815 mListViewForAnimating = null;
816 }
817
818 if (mDrawerContainer.isDrawerOpen(mDrawerPullout)) {
819 // Lets the drawer listener update the drawer contents and notify the FolderListFragment
820 mHasNewAccountOrFolder = true;
821 mDrawerContainer.setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED);
822 } else {
823 // Drawer is already closed, notify observers that is the case.
Tony Mantler54022ee2014-07-07 13:43:35 -0700824 if (hasNewFolderOrAccount) {
825 mFolderOrAccountObservers.notifyChanged();
826 }
Andy Huang12b3ee42013-04-24 22:49:43 -0700827 }
Rohan Shah0f73d902013-04-19 17:06:37 -0700828 }
829
Vikram Aggarwala1b59dc2013-04-30 15:45:49 -0700830 /**
Vikram Aggarwalaa941d72013-06-04 15:34:28 -0700831 * Load the conversation list early for the given folder. This happens when some UI element
832 * (usually the drawer) instructs the controller that an account change or folder change is
833 * imminent. While the UI element is animating, the controller can preload the conversation
834 * list for the default inbox of the account provided here or to the folder provided here.
835 *
836 * @param nextAccount The account which the app will switch to shortly, possibly null.
837 * @param nextFolder The folder which the app will switch to shortly, possibly null.
Vikram Aggarwala1b59dc2013-04-30 15:45:49 -0700838 */
839 protected void preloadConvList(Account nextAccount, Folder nextFolder) {
840 // Fire off the conversation list loader for this account already with a fake
841 // listener.
Paul Westbrooke2be97a2013-05-21 02:12:09 -0700842 final Bundle args = new Bundle(2);
Vikram Aggarwala1b59dc2013-04-30 15:45:49 -0700843 if (nextAccount != null) {
844 args.putParcelable(BUNDLE_ACCOUNT_KEY, nextAccount);
845 } else {
846 args.putParcelable(BUNDLE_ACCOUNT_KEY, mAccount);
847 }
848 if (nextFolder != null) {
849 args.putParcelable(BUNDLE_FOLDER_KEY, nextFolder);
Vikram Aggarwal2dc85042013-05-01 13:57:09 -0700850 } else {
851 LogUtils.e(LOG_TAG, new Error(), "AAC.preloadConvList(): Got an empty folder");
Vikram Aggarwala1b59dc2013-04-30 15:45:49 -0700852 }
853 mFolder = null;
854 final LoaderManager lm = mActivity.getLoaderManager();
855 lm.destroyLoader(LOADER_CONVERSATION_LIST);
856 lm.initLoader(LOADER_CONVERSATION_LIST, args, mListCursorCallbacks);
857 }
858
Vikram Aggarwalaa941d72013-06-04 15:34:28 -0700859 /**
860 * Initiates the async request to create a fake search folder, which returns conversations that
861 * match the query term provided by the user. Returns immediately.
862 * @param intent Intent that the app was started with. This intent contains the search query.
863 */
Mindy Pereirae0828392012-03-08 10:38:40 -0800864 private void fetchSearchFolder(Intent intent) {
Paul Westbrooke2be97a2013-05-21 02:12:09 -0700865 final Bundle args = new Bundle(1);
Mindy Pereiraab486362012-03-21 18:18:53 -0700866 args.putString(ConversationListContext.EXTRA_SEARCH_QUERY, intent
Mindy Pereirae0828392012-03-08 10:38:40 -0800867 .getStringExtra(ConversationListContext.EXTRA_SEARCH_QUERY));
Vikram Aggarwal177097f2013-03-08 11:19:53 -0800868 mActivity.getLoaderManager().restartLoader(LOADER_SEARCH, args, mFolderCallbacks);
Mindy Pereirae0828392012-03-08 10:38:40 -0800869 }
870
Jin Cao405a3442014-08-25 13:49:33 -0700871 protected void onFolderChanged(Folder folder, final boolean force) {
Andy Huangf58e4c32014-07-09 16:58:18 -0700872 if (isDrawerEnabled()) {
873 /** If the folder doesn't exist, or its parent URI is empty,
874 * this is not a child folder */
875 final boolean isTopLevel = Folder.isRoot(folder);
876 final int mode = mViewMode.getMode();
877 mDrawerToggle.setDrawerIndicatorEnabled(
878 getShouldShowDrawerIndicator(mode, isTopLevel));
879 mDrawerContainer.setDrawerLockMode(DrawerLayout.LOCK_MODE_UNLOCKED);
Vikram Aggarwaldfbc6982013-07-03 14:39:31 -0700880
Andy Huangf58e4c32014-07-09 16:58:18 -0700881 mDrawerContainer.closeDrawers();
882 }
Alice Yang7dd0e1c2013-09-04 06:43:16 +0000883
884 if (mFolder == null || !mFolder.equals(folder)) {
885 // We are actually changing the folder, so exit cab mode
886 exitCabMode();
887 }
888
Scott Kennedya158ac82013-09-04 13:48:13 -0700889 final String query;
890 if (folder != null && folder.isType(FolderType.SEARCH)) {
891 query = mConvListContext.searchQuery;
892 } else {
893 query = null;
894 }
895
896 changeFolder(folder, query, force);
Vikram Aggarwalf3341402012-08-07 10:09:38 -0700897 }
898
899 /**
Vikram Aggarwal203dc002012-08-23 13:59:04 -0700900 * Sets the folder state without changing view mode and without creating a list fragment, if
901 * possible.
Vikram Aggarwal715f83d2013-03-05 17:04:56 -0800902 * @param folder the folder whose list of conversations are to be shown
903 * @param query the query string for a list of conversations matching a search
Vikram Aggarwal203dc002012-08-23 13:59:04 -0700904 */
905 private void setListContext(Folder folder, String query) {
906 updateFolder(folder);
907 if (query != null) {
908 mConvListContext = ConversationListContext.forSearchQuery(mAccount, mFolder, query);
909 } else {
910 mConvListContext = ConversationListContext.forFolder(mAccount, mFolder);
911 }
Vikram Aggarwal203dc002012-08-23 13:59:04 -0700912 cancelRefreshTask();
913 }
914
915 /**
Vikram Aggarwalf3341402012-08-07 10:09:38 -0700916 * Changes the folder to the value provided here. This causes the view mode to change.
917 * @param folder the folder to change to
918 * @param query if non-null, this represents the search string that the folder represents.
Alice Yangebeef1b2013-09-04 06:41:10 +0000919 * @param force <code>true</code> to force a folder change, <code>false</code> to disallow
920 * changing to the current folder
Vikram Aggarwalf3341402012-08-07 10:09:38 -0700921 */
Alice Yangebeef1b2013-09-04 06:41:10 +0000922 private void changeFolder(Folder folder, String query, final boolean force) {
Vikram Aggarwal203dc002012-08-23 13:59:04 -0700923 if (!Objects.equal(mFolder, folder)) {
924 commitDestructiveActions(false);
925 }
Alice Yangebeef1b2013-09-04 06:41:10 +0000926 if (folder != null && (!folder.equals(mFolder) || force)
Mindy Pereiraa1f99982012-07-16 16:32:15 -0700927 || (mViewMode.getMode() != ViewMode.CONVERSATION_LIST)) {
Vikram Aggarwal203dc002012-08-23 13:59:04 -0700928 setListContext(folder, query);
Mindy Pereira28e0c342012-02-17 15:05:13 -0800929 showConversationList(mConvListContext);
Vikram Aggarwal58ccd692013-03-28 11:29:22 -0700930 // Touch the current folder: it is different, and it has been accessed.
931 mRecentFolderList.touchFolder(mFolder, mAccount);
Mindy Pereira28e0c342012-02-17 15:05:13 -0800932 }
Vikram Aggarwal93dc2022012-11-05 13:36:57 -0800933 resetActionBarIcon();
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -0800934 }
935
Mindy Pereira13c12a62012-05-31 15:41:08 -0700936 @Override
Mindy Pereira505df5f2012-06-19 17:57:17 -0700937 public void onFolderSelected(Folder folder) {
Alice Yangebeef1b2013-09-04 06:41:10 +0000938 onFolderChanged(folder, false /* force */);
Mindy Pereira13c12a62012-05-31 15:41:08 -0700939 }
940
Vikram Aggarwal792ccba2012-03-27 13:46:57 -0700941 /**
Vikram Aggarwal58cad2e2012-08-28 16:18:23 -0700942 * Adds a listener interested in change in the recent folders. If a class is storing a
943 * reference to the recent folders, it should listen on changes, so it can receive updates.
944 * Must happen in the UI thread.
945 */
946 @Override
947 public void registerRecentFolderObserver(DataSetObserver obs) {
948 mRecentFolderObservers.registerObserver(obs);
949 }
950
951 /**
952 * Removes a listener from receiving recent folder changes.
953 * Must happen in the UI thread.
954 */
955 @Override
956 public void unregisterRecentFolderObserver(DataSetObserver obs) {
957 mRecentFolderObservers.unregisterObserver(obs);
958 }
959
960 @Override
961 public RecentFolderList getRecentFolders() {
962 return mRecentFolderList;
963 }
964
Jin Cao405a3442014-08-25 13:49:33 -0700965 /**
966 * Load the default inbox associated with the current account.
967 */
968 protected void loadAccountInbox() {
Vikram Aggarwal77ee0ce2013-04-28 15:32:36 -0700969 boolean handled = false;
970 if (mFolderWatcher != null) {
971 final Folder inbox = mFolderWatcher.getDefaultInbox(mAccount);
972 if (inbox != null) {
Alice Yangebeef1b2013-09-04 06:41:10 +0000973 onFolderChanged(inbox, false /* force */);
Vikram Aggarwal77ee0ce2013-04-28 15:32:36 -0700974 handled = true;
975 }
976 }
977 if (!handled) {
Paul Westbrook1bf20e02014-02-26 12:48:54 -0800978 LogUtils.d(LOG_TAG, "Starting a LOADER_ACCOUNT_INBOX for %s", mAccount);
Vikram Aggarwal77ee0ce2013-04-28 15:32:36 -0700979 restartOptionalLoader(LOADER_ACCOUNT_INBOX, mFolderCallbacks, Bundle.EMPTY);
980 }
Vikram Aggarwal8cbf2812013-04-11 17:23:45 -0700981 final int mode = mViewMode.getMode();
982 if (mode == ViewMode.UNKNOWN || mode == ViewMode.WAITING_FOR_ACCOUNT_INITIALIZATION) {
Andy Huange6459422013-04-01 16:32:18 -0700983 mViewMode.enterConversationListMode();
984 }
Mindy Pereiraf6acdad2012-03-15 11:21:13 -0700985 }
986
Vikram Aggarwal77ee0ce2013-04-28 15:32:36 -0700987 @Override
988 public void setFolderWatcher(FolderWatcher watcher) {
989 mFolderWatcher = watcher;
990 }
991
Vikram Aggarwal69b5c302012-09-05 11:11:13 -0700992 /**
Vikram Aggarwald00229d2012-09-20 12:31:44 -0700993 * Marks the {@link #mFolderChanged} value if the newFolder is different from the existing
994 * {@link #mFolder}. This should be called immediately <b>before</b> assigning newFolder to
995 * mFolder.
Vikram Aggarwal715f83d2013-03-05 17:04:56 -0800996 * @param newFolder the new folder we are switching to.
Vikram Aggarwald00229d2012-09-20 12:31:44 -0700997 */
Vikram Aggarwal715f83d2013-03-05 17:04:56 -0800998 private void setHasFolderChanged(final Folder newFolder) {
Vikram Aggarwald00229d2012-09-20 12:31:44 -0700999 // We should never try to assign a null folder. But in the rare event that we do, we should
1000 // only set the bit when we have a valid folder, and null is not valid.
1001 if (newFolder == null) {
1002 return;
1003 }
1004 // If the previous folder was null, or if the two folders represent different data, then we
1005 // consider that the folder has changed.
Scott Kennedy259df5b2013-07-11 13:24:01 -07001006 if (mFolder == null || !newFolder.equals(mFolder)) {
Vikram Aggarwald00229d2012-09-20 12:31:44 -07001007 mFolderChanged = true;
1008 }
1009 }
1010
1011 /**
Vikram Aggarwal69b5c302012-09-05 11:11:13 -07001012 * Sets the current folder if it is different from the object provided here. This method does
1013 * NOT notify the folder observers that a change has happened. Observers are notified when we
1014 * get an updated folder from the loaders, which will happen as a consequence of this method
1015 * (since this method starts/restarts the loaders).
1016 * @param folder The folder to assign
1017 */
Mindy Pereira11e35962012-06-01 14:49:46 -07001018 private void updateFolder(Folder folder) {
Vikram Aggarwal1672ff82012-09-21 10:15:22 -07001019 if (folder == null || !folder.isInitialized()) {
1020 LogUtils.e(LOG_TAG, new Error(), "AAC.setFolder(%s): Bad input", folder);
1021 return;
1022 }
1023 if (folder.equals(mFolder)) {
1024 LogUtils.d(LOG_TAG, "AAC.setFolder(%s): Input matches mFolder", folder);
1025 return;
1026 }
1027 final boolean wasNull = mFolder == null;
1028 LogUtils.d(LOG_TAG, "AbstractActivityController.setFolder(%s)", folder.name);
1029 final LoaderManager lm = mActivity.getLoaderManager();
1030 // updateFolder is called from AAC.onLoadFinished() on folder changes. We need to
1031 // ensure that the folder is different from the previous folder before marking the
1032 // folder changed.
1033 setHasFolderChanged(folder);
1034 mFolder = folder;
Vikram Aggarwal69b5c302012-09-05 11:11:13 -07001035
Vikram Aggarwal1672ff82012-09-21 10:15:22 -07001036 // We do not need to notify folder observers yet. Instead we start the loaders and
1037 // when the load finishes, we will get an updated folder. Then, we notify the
1038 // folderObservers in onLoadFinished.
Andrew Sapperstein2d86d112014-07-24 20:27:37 -07001039 mActionBarController.setFolder(mFolder);
Andy Huangb1c34dc2012-04-17 16:36:19 -07001040
Vikram Aggarwal1672ff82012-09-21 10:15:22 -07001041 // Only when we switch from one folder to another do we want to restart the
1042 // folder and conversation list loaders (to trigger onCreateLoader).
1043 // The first time this runs when the activity is [re-]initialized, we want to re-use the
1044 // previous loader's instance and data upon configuration change (e.g. rotation).
1045 // If there was not already an instance of the loader, init it.
1046 if (lm.getLoader(LOADER_FOLDER_CURSOR) == null) {
Vikram Aggarwal177097f2013-03-08 11:19:53 -08001047 lm.initLoader(LOADER_FOLDER_CURSOR, Bundle.EMPTY, mFolderCallbacks);
Vikram Aggarwal1672ff82012-09-21 10:15:22 -07001048 } else {
Vikram Aggarwal177097f2013-03-08 11:19:53 -08001049 lm.restartLoader(LOADER_FOLDER_CURSOR, Bundle.EMPTY, mFolderCallbacks);
Vikram Aggarwal1672ff82012-09-21 10:15:22 -07001050 }
Vikram Aggarwala1b59dc2013-04-30 15:45:49 -07001051 if (!wasNull && lm.getLoader(LOADER_CONVERSATION_LIST) != null) {
1052 // If there was an existing folder AND we have changed
Vikram Aggarwal1672ff82012-09-21 10:15:22 -07001053 // folders, we want to restart the loader to get the information
1054 // for the newly selected folder
1055 lm.destroyLoader(LOADER_CONVERSATION_LIST);
Mindy Pereira5cb0c3e2012-02-22 15:22:47 -08001056 }
Paul Westbrooke2be97a2013-05-21 02:12:09 -07001057 final Bundle args = new Bundle(2);
Vikram Aggarwal2dc85042013-05-01 13:57:09 -07001058 args.putParcelable(BUNDLE_ACCOUNT_KEY, mAccount);
Vikram Aggarwala1b59dc2013-04-30 15:45:49 -07001059 args.putParcelable(BUNDLE_FOLDER_KEY, mFolder);
Andrew Sapperstein5bb4d052014-03-31 16:22:31 -07001060 args.putBoolean(BUNDLE_IGNORE_INITIAL_CONVERSATION_LIMIT_KEY,
1061 mIgnoreInitialConversationLimit);
1062 mIgnoreInitialConversationLimit = false;
Vikram Aggarwala1b59dc2013-04-30 15:45:49 -07001063 lm.initLoader(LOADER_CONVERSATION_LIST, args, mListCursorCallbacks);
Mindy Pereira5cb0c3e2012-02-22 15:22:47 -08001064 }
1065
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -08001066 @Override
Andy Huang090db1e2012-07-25 13:25:28 -07001067 public Folder getFolder() {
1068 return mFolder;
1069 }
1070
1071 @Override
Mindy Pereirac975e842012-07-16 09:15:00 -07001072 public Folder getHierarchyFolder() {
1073 return mFolderListFolder;
1074 }
1075
Jin Cao405a3442014-08-25 13:49:33 -07001076 /**
1077 * Set the folder currently selected in the folder selection hierarchy fragments.
1078 */
1079 protected void setHierarchyFolder(Folder folder) {
Mindy Pereirac975e842012-07-16 09:15:00 -07001080 mFolderListFolder = folder;
Mindy Pereira23aadfd2012-05-25 11:24:33 -07001081 }
1082
Vikram Aggarwal6aca6892013-06-04 13:53:27 -07001083 /**
1084 * The mail activity calls other activities for two specific reasons:
1085 * <ul>
1086 * <li>To add an account. And receives the result {@link #ADD_ACCOUNT_REQUEST_CODE}</li>
1087 * <li>To update the password on a current account. The result {@link
1088 * #REAUTHENTICATE_REQUEST_CODE} is received.</li>
1089 * </ul>
1090 * @param requestCode
1091 * @param resultCode
1092 * @param data
1093 */
Mindy Pereira23aadfd2012-05-25 11:24:33 -07001094 @Override
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -08001095 public void onActivityResult(int requestCode, int resultCode, Intent data) {
Paul Westbrook122f7c22012-08-20 17:50:31 -07001096 switch (requestCode) {
1097 case ADD_ACCOUNT_REQUEST_CODE:
1098 // We were waiting for the user to create an account
1099 if (resultCode == Activity.RESULT_OK) {
1100 // restart the loader to get the updated list of accounts
Vikram Aggarwal177097f2013-03-08 11:19:53 -08001101 mActivity.getLoaderManager().initLoader(LOADER_ACCOUNT_CURSOR, Bundle.EMPTY,
1102 mAccountCallbacks);
Paul Westbrook122f7c22012-08-20 17:50:31 -07001103 } else {
1104 // The user failed to create an account, just exit the app
1105 mActivity.finish();
1106 }
1107 break;
1108 case REAUTHENTICATE_REQUEST_CODE:
1109 if (resultCode == Activity.RESULT_OK) {
1110 // The user successfully authenticated, attempt to refresh the list
1111 final Uri refreshUri = mFolder != null ? mFolder.refreshUri : null;
1112 if (refreshUri != null) {
1113 startAsyncRefreshTask(refreshUri);
1114 }
1115 }
1116 break;
Martin Hibdon371a71c2014-02-19 13:55:28 -08001117 case CHANGE_NAVIGATION_REQUEST_CODE:
Jin Caoc6801eb2014-08-12 18:16:57 -07001118 if (ViewMode.isSearchMode(mViewMode.getMode())) {
1119 mActivity.setResult(resultCode, data);
1120 mActivity.finish();
1121 } else if (resultCode == Activity.RESULT_OK && data != null) {
Martin Hibdon371a71c2014-02-19 13:55:28 -08001122 // We have have received a result that indicates we need to navigate to a
1123 // different folder or account. This happens if someone navigates using the
1124 // drawer on the search results activity.
1125 final Folder folder = data.getParcelableExtra(EXTRA_FOLDER);
1126 final Account account = data.getParcelableExtra(EXTRA_ACCOUNT);
1127 if (folder != null) {
1128 onFolderSelected(folder);
1129 mViewMode.enterConversationListMode();
1130 } else if (account != null) {
1131 switchToDefaultInboxOrChangeAccount(account);
1132 mViewMode.enterConversationListMode();
1133 }
1134 }
1135 break;
Greg Bullock406ae082014-08-27 17:03:16 +02001136 case VOICE_SEARCH_REQUEST_CODE:
Jin Caoc6801eb2014-08-12 18:16:57 -07001137 if (resultCode == Activity.RESULT_OK) {
1138 final ArrayList<String> matches = data.getStringArrayListExtra(
1139 RecognizerIntent.EXTRA_RESULTS);
1140 if (!matches.isEmpty()) {
1141 // not sure how dependable the API is, but it's all we have.
1142 // take the top choice.
1143 mSearchViewController.onSearchPerformed(matches.get(0));
1144 }
1145 }
1146 break;
Paul Westbrook2388c5d2012-03-25 12:29:11 -07001147 }
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -08001148 }
1149
Vikram Aggarwal69b5c302012-09-05 11:11:13 -07001150 /**
1151 * Inform the conversation cursor that there has been a visibility change.
Vikram Aggarwal715f83d2013-03-05 17:04:56 -08001152 * @param visible true if the conversation list is visible, false otherwise.
Vikram Aggarwal69b5c302012-09-05 11:11:13 -07001153 */
1154 protected synchronized void informCursorVisiblity(boolean visible) {
1155 if (mConversationListCursor != null) {
1156 Utils.setConversationCursorVisibility(mConversationListCursor, visible, mFolderChanged);
1157 // We have informed the cursor. Subsequent visibility changes should not tell it that
1158 // the folder has changed.
1159 mFolderChanged = false;
1160 }
1161 }
1162
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -08001163 @Override
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -08001164 public void onConversationListVisibilityChanged(boolean visible) {
Jin Cao4a0456b2014-09-18 15:12:07 -07001165 mFloatingComposeButton.setVisibility(
1166 !ViewMode.isSearchMode(mViewMode.getMode()) && visible ? View.VISIBLE : View.GONE);
Andy Huang92ae7662014-09-11 17:00:47 -07001167
Vikram Aggarwal69b5c302012-09-05 11:11:13 -07001168 informCursorVisiblity(visible);
Andy Huangc94d07f2013-06-03 16:19:35 -07001169 commitAutoAdvanceOperation();
Scott Kennedy32ddb842013-08-28 17:38:22 -07001170
1171 // Notify special views
1172 final ConversationListFragment convListFragment = getConversationListFragment();
1173 if (convListFragment != null && convListFragment.getAnimatedAdapter() != null) {
1174 convListFragment.getAnimatedAdapter().onConversationListVisibilityChanged(visible);
1175 }
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -08001176 }
1177
Vikram Aggarwal4a5c5302012-01-12 15:07:13 -08001178 /**
Vikram Aggarwal7dd054e2012-05-21 14:43:10 -07001179 * Called when a conversation is visible. Child classes must call the super class implementation
1180 * before performing local computation.
Vikram Aggarwal4a5c5302012-01-12 15:07:13 -08001181 */
1182 @Override
1183 public void onConversationVisibilityChanged(boolean visible) {
Andy Huangc94d07f2013-06-03 16:19:35 -07001184 commitAutoAdvanceOperation();
1185 }
1186
1187 /**
1188 * Commits any pending destructive action that was earlier deferred by an auto-advance
1189 * mode-change transition.
1190 */
1191 private void commitAutoAdvanceOperation() {
1192 if (mAutoAdvanceOp != null) {
1193 mAutoAdvanceOp.run();
1194 mAutoAdvanceOp = null;
1195 }
Vikram Aggarwal4a5c5302012-01-12 15:07:13 -08001196 }
1197
Vikram Aggarwal59f741f2013-03-01 15:55:40 -08001198 /**
1199 * Initialize development time logging. This can potentially log a lot of PII, and we don't want
1200 * to turn it on for shipped versions.
1201 */
1202 private void initializeDevLoggingService() {
1203 if (!MailLogService.DEBUG_ENABLED) {
1204 return;
1205 }
1206 // Check every 5 minutes.
1207 final int WAIT_TIME = 5 * 60 * 1000;
1208 // Start a runnable that periodically checks the log level and starts/stops the service.
1209 mLogServiceChecker = new Runnable() {
1210 /** True if currently logging. */
1211 private boolean mCurrentlyLogging = false;
1212
1213 /**
1214 * If the logging level has been changed since the previous run, start or stop the
1215 * service.
1216 */
1217 private void startOrStopService() {
1218 // If the log level is already high, start the service.
1219 final Intent i = new Intent(mContext, MailLogService.class);
1220 final boolean loggingEnabled = MailLogService.isLoggingLevelHighEnough();
1221 if (mCurrentlyLogging == loggingEnabled) {
1222 // No change since previous run, just return;
1223 return;
1224 }
1225 if (loggingEnabled) {
1226 LogUtils.e(LOG_TAG, "Starting MailLogService");
1227 mContext.startService(i);
1228 } else {
1229 LogUtils.e(LOG_TAG, "Stopping MailLogService");
1230 mContext.stopService(i);
1231 }
1232 mCurrentlyLogging = loggingEnabled;
1233 }
1234
1235 @Override
1236 public void run() {
1237 startOrStopService();
1238 mHandler.postDelayed(this, WAIT_TIME);
1239 }
1240 };
1241 // Start the runnable right away.
1242 mHandler.post(mLogServiceChecker);
1243 }
1244
Vikram Aggarwal6aca6892013-06-04 13:53:27 -07001245 /**
1246 * The application can be started from the following entry points:
1247 * <ul>
1248 * <li>Launcher: you tap on the Gmail icon in the launcher. This is what most users think of
1249 * as “Starting the app”.</li>
1250 * <li>Shortcut: Users can make a shortcut to take them directly to a label.</li>
1251 * <li>Widget: Shows the contents of a synced label, and allows:
1252 * <ul>
1253 * <li>Viewing the list (tapping on the title)</li>
1254 * <li>Composing a new message (tapping on the new message icon in the title. This
1255 * launches the {@link ComposeActivity}.
1256 * </li>
1257 * <li>Viewing a single message (tapping on a list element)</li>
1258 * </ul>
1259 *
1260 * </li>
1261 * <li>Tapping on a notification:
1262 * <ul>
1263 * <li>Shows message list if more than one message</li>
1264 * <li>Shows the conversation if the notification is for a single message</li>
1265 * </ul>
1266 * </li>
1267 * <li>...and most importantly, the activity life cycle can tear down the application and
1268 * restart it:
1269 * <ul>
1270 * <li>Rotate the application: it is destroyed and recreated.</li>
1271 * <li>Navigate away, and return from recent applications.</li>
1272 * </ul>
1273 * </li>
1274 * <li>Add a new account: fires off an intent to add an account,
1275 * and returns in {@link #onActivityResult(int, int, android.content.Intent)} .</li>
1276 * <li>Re-authenticate your account: again returns in onActivityResult().</li>
1277 * <li>Composing can happen from many entry points: third party applications fire off an
1278 * intent to compose email, and launch directly into the {@link ComposeActivity}
1279 * .</li>
1280 * </ul>
1281 * {@inheritDoc}
1282 */
Vikram Aggarwal4a5c5302012-01-12 15:07:13 -08001283 @Override
Vikram Aggarwald7a12cd2012-02-03 09:36:20 -08001284 public boolean onCreate(Bundle savedState) {
Vikram Aggarwalabd24d82012-04-26 13:23:14 -07001285 initializeActionBar();
Vikram Aggarwal59f741f2013-03-01 15:55:40 -08001286 initializeDevLoggingService();
Vikram Aggarwala55b36c2012-01-13 11:45:02 -08001287 // Allow shortcut keys to function for the ActionBar and menus.
1288 mActivity.setDefaultKeyMode(Activity.DEFAULT_KEYS_SHORTCUT);
Vikram Aggarwal80aeac52012-02-07 15:27:20 -08001289 mResolver = mActivity.getContentResolver();
Paul Westbrook6ead20d2012-03-19 14:48:14 -07001290 mNewEmailReceiver = new SuppressNotificationReceiver();
Vikram Aggarwal7c401b72012-08-13 16:43:47 -07001291 mRecentFolderList.initialize(mActivity);
Vikram Aggarwal69a6cdf2013-01-08 16:05:17 -08001292 mVeiledMatcher.initialize(this);
Paul Westbrook6ead20d2012-03-19 14:48:14 -07001293
James Lemieux1a1e9752014-07-18 13:50:40 -07001294 mFloatingComposeButton = mActivity.findViewById(R.id.compose_button);
1295 mFloatingComposeButton.setOnClickListener(this);
James Lemieux5d79a912014-07-16 14:22:26 -07001296
Andy Huangf58e4c32014-07-09 16:58:18 -07001297 if (isDrawerEnabled()) {
Andrew Sapperstein355dd902014-09-10 12:46:44 -07001298 mDrawerToggle = new ActionBarDrawerToggle(mActivity, mDrawerContainer,
1299 R.string.drawer_open, R.string.drawer_close);
Andy Huangf58e4c32014-07-09 16:58:18 -07001300 mDrawerContainer.setDrawerListener(mDrawerListener);
1301 mDrawerContainer.setDrawerShadow(
1302 mContext.getResources().getDrawable(R.drawable.drawer_shadow), Gravity.START);
Andy Huang12b3ee42013-04-24 22:49:43 -07001303
Andy Huangf58e4c32014-07-09 16:58:18 -07001304 mDrawerToggle.setDrawerIndicatorEnabled(isDrawerEnabled());
1305 } else {
Andrew Sapperstein52882ff2014-07-27 12:30:18 -07001306 final ActionBar ab = mActivity.getSupportActionBar();
Andy Huangf58e4c32014-07-09 16:58:18 -07001307 ab.setHomeAsUpIndicator(R.drawable.ic_drawer);
1308 ab.setHomeActionContentDescription(R.string.drawer_open);
1309 ab.setDisplayHomeAsUpEnabled(true);
1310 }
Andy Huang12b3ee42013-04-24 22:49:43 -07001311
Mindy Pereira161f50d2012-02-28 15:47:19 -08001312 // All the individual UI components listen for ViewMode changes. This
Mindy Pereirab849dfb2012-03-07 18:13:15 -08001313 // simplifies the amount of logic in the AbstractActivityController, but increases the
1314 // possibility of timing-related bugs.
Vikram Aggarwala55b36c2012-01-13 11:45:02 -08001315 mViewMode.addListener(this);
Andy Huang632721e2012-04-11 16:57:26 -07001316 mPagerController = new ConversationPagerController(mActivity, this);
James Lemieux9a110112014-08-07 15:23:13 -07001317 mToastBar = findActionableToastBar(mActivity);
Vikram Aggarwalada51782012-04-26 14:18:31 -07001318 attachActionBar();
Andy Huang632721e2012-04-11 16:57:26 -07001319
Andy Huang144bfe72013-06-11 13:27:52 -07001320 mDrawIdler.setRootView(mActivity.getWindow().getDecorView());
1321
Andy Huang632721e2012-04-11 16:57:26 -07001322 final Intent intent = mActivity.getIntent();
Andy Huang4fe0af82013-08-20 17:24:51 -07001323
Jin Caoc6801eb2014-08-12 18:16:57 -07001324 mSearchViewController = new MaterialSearchViewController(mActivity, this, intent,
1325 savedState);
Jin Cao524ded52014-09-05 13:44:58 -07001326 addConversationListLayoutListener(mSearchViewController);
Jin Caoc6801eb2014-08-12 18:16:57 -07001327
Vikram Aggarwalabd24d82012-04-26 13:23:14 -07001328 // Immediately handle a clean launch with intent, and any state restoration
Andy Huang632721e2012-04-11 16:57:26 -07001329 // that does not rely on restored fragments or loader data
1330 // any state restoration that relies on those can be done later in
1331 // onRestoreInstanceState, once fragments are up and loader data is re-delivered
1332 if (savedState != null) {
1333 if (savedState.containsKey(SAVED_ACCOUNT)) {
1334 setAccount((Account) savedState.getParcelable(SAVED_ACCOUNT));
Andy Huang632721e2012-04-11 16:57:26 -07001335 }
1336 if (savedState.containsKey(SAVED_FOLDER)) {
Andy Huang2bc8bc12012-11-12 17:24:25 -08001337 final Folder folder = savedState.getParcelable(SAVED_FOLDER);
Vikram Aggarwal203dc002012-08-23 13:59:04 -07001338 final String query = savedState.getString(SAVED_QUERY, null);
Vikram Aggarwal649b9ea2012-08-27 12:15:20 -07001339 setListContext(folder, query);
Andy Huang632721e2012-04-11 16:57:26 -07001340 }
Vikram Aggarwal6cadbfc2012-12-27 09:17:05 -08001341 if (savedState.containsKey(SAVED_ACTION)) {
1342 mDialogAction = savedState.getInt(SAVED_ACTION);
1343 }
Vikram Aggarwala91d00b2013-01-18 12:00:37 -08001344 mDialogFromSelectedSet = savedState.getBoolean(SAVED_ACTION_FROM_SELECTED, false);
Vikram Aggarwal649b9ea2012-08-27 12:15:20 -07001345 mViewMode.handleRestore(savedState);
Andy Huang632721e2012-04-11 16:57:26 -07001346 } else if (intent != null) {
1347 handleIntent(intent);
1348 }
Andy Huang632721e2012-04-11 16:57:26 -07001349 // Create the accounts loader; this loads the account switch spinner.
Vikram Aggarwal177097f2013-03-08 11:19:53 -08001350 mActivity.getLoaderManager().initLoader(LOADER_ACCOUNT_CURSOR, Bundle.EMPTY,
1351 mAccountCallbacks);
Andy Huang632721e2012-04-11 16:57:26 -07001352 return true;
Andy Huangb1c34dc2012-04-17 16:36:19 -07001353 }
1354
James Lemieux9a110112014-08-07 15:23:13 -07001355 /**
1356 * @param activity the activity that has been inflated
1357 * @return the Actionable Toast Bar defined within the activity
1358 */
1359 protected ActionableToastBar findActionableToastBar(MailActivity activity) {
1360 return (ActionableToastBar) activity.findViewById(R.id.toast_bar);
1361 }
1362
Andy Huangb1c34dc2012-04-17 16:36:19 -07001363 @Override
Paul Westbrook57246a42013-04-21 09:40:22 -07001364 public void onPostCreate(Bundle savedState) {
Andy Huangf58e4c32014-07-09 16:58:18 -07001365 if (!isDrawerEnabled()) {
1366 return;
1367 }
Andy Huang12b3ee42013-04-24 22:49:43 -07001368 // Sync the toggle state after onRestoreInstanceState has occurred.
1369 mDrawerToggle.syncState();
Andrew Sapperstein5747e152013-05-13 14:13:08 -07001370
Vikram Aggarwald70fe492013-06-04 12:52:07 -07001371 mHideMenuItems = isDrawerEnabled() && mDrawerContainer.isDrawerOpen(mDrawerPullout);
Paul Westbrook57246a42013-04-21 09:40:22 -07001372 }
1373
1374 @Override
1375 public void onConfigurationChanged(Configuration newConfig) {
Andy Huangf58e4c32014-07-09 16:58:18 -07001376 if (isDrawerEnabled()) {
1377 mDrawerToggle.onConfigurationChanged(newConfig);
1378 }
Andy Huang12b3ee42013-04-24 22:49:43 -07001379 }
1380
1381 /**
James Lemieux5d79a912014-07-16 14:22:26 -07001382 * This controller listens for clicks on items in the floating action bar.
1383 *
1384 * @param view the item that was clicked in the floating action bar
1385 */
1386 @Override
1387 public void onClick(View view) {
Andrew Sapperstein53de4482014-07-29 02:39:39 +00001388 final int viewId = view.getId();
1389 if (viewId == R.id.compose_button) {
1390 ComposeActivity.compose(mActivity.getActivityContext(), getAccount());
1391 } else if (viewId == android.R.id.home) {
1392 // TODO: b/16627877
Jin Cao405a3442014-08-25 13:49:33 -07001393 handleUpPress();
Andrew Sapperstein53de4482014-07-29 02:39:39 +00001394 }
James Lemieux5d79a912014-07-16 14:22:26 -07001395 }
1396
1397 /**
Andy Huang12b3ee42013-04-24 22:49:43 -07001398 * If drawer is open/visible (even partially), close it.
1399 */
1400 protected void closeDrawerIfOpen() {
1401 if (!isDrawerEnabled()) {
1402 return;
1403 }
1404 if(mDrawerContainer.isDrawerOpen(mDrawerPullout)) {
1405 mDrawerContainer.closeDrawers();
1406 }
Paul Westbrook57246a42013-04-21 09:40:22 -07001407 }
1408
1409 @Override
Andy Huang1ee96b22012-08-24 20:19:53 -07001410 public void onStart() {
1411 mSafeToModifyFragments = true;
Scott Kennedycb85aea2013-02-25 13:08:32 -08001412
1413 NotificationActionUtils.registerUndoNotificationObserver(mUndoNotificationObserver);
Andy Huang761522c2013-08-08 13:09:11 -07001414
1415 if (mViewMode.getMode() != ViewMode.UNKNOWN) {
1416 Analytics.getInstance().sendView("MainActivity" + mViewMode.toString());
1417 }
Andy Huang1ee96b22012-08-24 20:19:53 -07001418 }
1419
1420 @Override
Andrew Sapperstein00179f12012-08-09 15:15:40 -07001421 public void onRestart() {
Vikram Aggarwal177097f2013-03-08 11:19:53 -08001422 final DialogFragment fragment = (DialogFragment)
Andrew Sapperstein00179f12012-08-09 15:15:40 -07001423 mFragmentManager.findFragmentByTag(SYNC_ERROR_DIALOG_FRAGMENT_TAG);
1424 if (fragment != null) {
1425 fragment.dismiss();
1426 }
mindypea04f932012-08-27 14:17:59 -07001427 // When the user places the app in the background by pressing "home",
1428 // dismiss the toast bar. However, since there is no way to determine if
1429 // home was pressed, just dismiss any existing toast bar when restarting
1430 // the app.
1431 if (mToastBar != null) {
Andrew Sappersteinf8ccdcf2013-08-08 19:30:38 -07001432 mToastBar.hide(false, false /* actionClicked */);
mindypea04f932012-08-27 14:17:59 -07001433 }
Andrew Sapperstein00179f12012-08-09 15:15:40 -07001434 }
1435
1436 @Override
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -08001437 public Dialog onCreateDialog(int id, Bundle bundle) {
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -08001438 return null;
Vikram Aggarwala55b36c2012-01-13 11:45:02 -08001439 }
1440
1441 @Override
Vikram Aggarwal4eb52712012-06-19 16:24:50 -07001442 public final boolean onCreateOptionsMenu(Menu menu) {
Andrew Sapperstein0fa9efd2013-08-07 14:09:31 -07001443 if (mViewMode.isAdMode()) {
1444 return false;
1445 }
Vikram Aggarwale5e917c2012-09-20 16:27:41 -07001446 final MenuInflater inflater = mActivity.getMenuInflater();
Andrew Sapperstein2d86d112014-07-24 20:27:37 -07001447 inflater.inflate(mActionBarController.getOptionsMenuId(), menu);
1448 mActionBarController.onCreateOptionsMenu(menu);
Mindy Pereira28d5f722012-02-15 12:32:40 -08001449 return true;
Vikram Aggarwala55b36c2012-01-13 11:45:02 -08001450 }
1451
1452 @Override
Vikram Aggarwal4eb52712012-06-19 16:24:50 -07001453 public final boolean onKeyDown(int keyCode, KeyEvent event) {
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -08001454 return false;
1455 }
1456
mindyp17a8e782012-11-29 14:56:17 -08001457 public abstract boolean doesActionChangeConversationListVisibility(int action);
1458
Jin Cao30c881a2014-04-08 14:28:36 -07001459 /**
1460 * Helper function that determines if we should associate an undo callback with
1461 * the current menu action item
1462 * @param actionId the id of the action
1463 * @return the appropriate callback handler, or null if not applicable
1464 */
1465 private UndoCallback getUndoCallbackForDestructiveActionsWithAutoAdvance(
1466 int actionId, final Conversation conv) {
1467 // We associated the undoCallback if the user is going to perform an action on the current
1468 // conversation, causing the current conversation to be removed from view and replacing it
1469 // with another (via Auto Advance). The undoCallback will bring the removed conversation
1470 // back into the view if the action is undone.
1471 final Collection<Conversation> convCol = Conversation.listOf(conv);
1472 final boolean isApplicableForReshow = mAccount != null &&
1473 mAccount.settings != null &&
1474 mTracker != null &&
1475 // ensure that we will show another conversation due to Auto Advance
1476 mTracker.getNextConversation(
1477 mAccount.settings.getAutoAdvanceSetting(), convCol) != null &&
1478 // ensure that we are performing the action from conversation view
1479 isCurrentConversationInView(convCol) &&
1480 // check for the appropriate destructive actions
1481 doesActionRemoveCurrentConversationFromView(actionId);
1482 return (isApplicableForReshow) ?
1483 new UndoCallback() {
1484 @Override
1485 public void performUndoCallback() {
1486 showConversation(conv);
1487 }
1488 } : null;
1489 }
1490
1491 /**
1492 * Check if the provided action will remove the active conversation from view
1493 * @param actionId the applied action
1494 * @return true if it will remove the conversation from view, false otherwise
1495 */
1496 private boolean doesActionRemoveCurrentConversationFromView(int actionId) {
1497 return actionId == R.id.archive ||
1498 actionId == R.id.delete ||
Jin Cao512821c2014-05-30 15:54:04 -07001499 actionId == R.id.discard_outbox ||
Jin Cao30c881a2014-04-08 14:28:36 -07001500 actionId == R.id.remove_folder ||
1501 actionId == R.id.report_spam ||
1502 actionId == R.id.report_phishing ||
1503 actionId == R.id.move_to;
1504 }
1505
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -08001506 @Override
Paul Westbrook57246a42013-04-21 09:40:22 -07001507 public boolean onOptionsItemSelected(MenuItem item) {
Andy Huang761522c2013-08-08 13:09:11 -07001508
Andy Huang12b3ee42013-04-24 22:49:43 -07001509 /*
1510 * The action bar home/up action should open or close the drawer.
1511 * mDrawerToggle will take care of this.
1512 */
Andy Huangf58e4c32014-07-09 16:58:18 -07001513 if (isDrawerEnabled() && mDrawerToggle.onOptionsItemSelected(item)) {
Andy Huang042a5302013-08-13 12:39:08 -07001514 Analytics.getInstance().sendEvent(Analytics.EVENT_CATEGORY_MENU_ITEM, "drawer_toggle",
1515 null, 0);
Andy Huang12b3ee42013-04-24 22:49:43 -07001516 return true;
1517 }
1518
Andy Huang2b555492013-08-14 21:06:21 -07001519 Analytics.getInstance().sendMenuItemEvent(Analytics.EVENT_CATEGORY_MENU_ITEM,
Andy Huangf8c59b02014-03-19 20:00:53 -07001520 item.getItemId(), "action_bar/" + mViewMode.getModeString(), 0);
Andy Huang042a5302013-08-13 12:39:08 -07001521
Vikram Aggarwal54452ae2012-03-13 15:29:00 -07001522 final int id = item.getItemId();
Vikram Aggarwal7dd054e2012-05-21 14:43:10 -07001523 LogUtils.d(LOG_TAG, "AbstractController.onOptionsItemSelected(%d) called.", id);
Mindy Pereira9b875682012-02-15 18:10:54 -08001524 boolean handled = true;
Vikram Aggarwal84fe9942013-04-17 11:20:58 -07001525 /** This is NOT a batch action. */
1526 final boolean isBatch = false;
Mindy Pereiraba68fda2012-05-24 15:53:06 -07001527 final Collection<Conversation> target = Conversation.listOf(mCurrentConversation);
Vikram Aggarwal112cd162012-06-18 10:51:13 -07001528 final Settings settings = (mAccount == null) ? null : mAccount.settings;
mindyp84f7d322012-10-01 17:14:40 -07001529 // The user is choosing a new action; commit whatever they had been
mindyp17a8e782012-11-29 14:56:17 -08001530 // doing before. Don't animate if we are launching a new screen.
1531 commitDestructiveActions(!doesActionChangeConversationListVisibility(id));
Jin Cao30c881a2014-04-08 14:28:36 -07001532 final UndoCallback undoCallback = getUndoCallbackForDestructiveActionsWithAutoAdvance(
1533 id, mCurrentConversation);
1534
Scott Kennedy2b9d80e2013-07-30 23:03:45 -07001535 if (id == R.id.archive) {
1536 final boolean showDialog = (settings != null && settings.confirmArchive);
Jin Cao30c881a2014-04-08 14:28:36 -07001537 confirmAndDelete(id, target, showDialog, R.plurals.confirm_archive_conversation, undoCallback);
Scott Kennedy2b9d80e2013-07-30 23:03:45 -07001538 } else if (id == R.id.remove_folder) {
1539 delete(R.id.remove_folder, target,
Jin Cao30c881a2014-04-08 14:28:36 -07001540 getDeferredRemoveFolder(target, mFolder, true, isBatch, true, undoCallback),
1541 isBatch);
Scott Kennedy2b9d80e2013-07-30 23:03:45 -07001542 } else if (id == R.id.delete) {
1543 final boolean showDialog = (settings != null && settings.confirmDelete);
Jin Cao30c881a2014-04-08 14:28:36 -07001544 confirmAndDelete(id, target, showDialog, R.plurals.confirm_delete_conversation, undoCallback);
Scott Kennedy2b9d80e2013-07-30 23:03:45 -07001545 } else if (id == R.id.discard_drafts) {
Andy Huang121c8b82013-08-05 11:52:55 -07001546 // drafts are lost forever, so always confirm
1547 confirmAndDelete(id, target, true /* showDialog */,
Jin Cao30c881a2014-04-08 14:28:36 -07001548 R.plurals.confirm_discard_drafts_conversation, undoCallback);
Jin Cao512821c2014-05-30 15:54:04 -07001549 } else if (id == R.id.discard_outbox) {
1550 // discard in outbox means we discard the failed message and save them in drafts
1551 delete(id, target, getDeferredAction(id, target, isBatch, undoCallback), isBatch);
Scott Kennedy2b9d80e2013-07-30 23:03:45 -07001552 } else if (id == R.id.mark_important) {
1553 updateConversation(Conversation.listOf(mCurrentConversation),
1554 ConversationColumns.PRIORITY, UIProvider.ConversationPriority.HIGH);
1555 } else if (id == R.id.mark_not_important) {
1556 if (mFolder != null && mFolder.isImportantOnly()) {
1557 delete(R.id.mark_not_important, target,
Jin Cao30c881a2014-04-08 14:28:36 -07001558 getDeferredAction(R.id.mark_not_important, target, isBatch, undoCallback),
1559 isBatch);
Scott Kennedy2b9d80e2013-07-30 23:03:45 -07001560 } else {
Vikram Aggarwal531488e2012-05-29 16:36:52 -07001561 updateConversation(Conversation.listOf(mCurrentConversation),
Scott Kennedy2b9d80e2013-07-30 23:03:45 -07001562 ConversationColumns.PRIORITY, UIProvider.ConversationPriority.LOW);
1563 }
1564 } else if (id == R.id.mute) {
Jin Cao30c881a2014-04-08 14:28:36 -07001565 delete(R.id.mute, target, getDeferredAction(R.id.mute, target, isBatch, undoCallback),
1566 isBatch);
Scott Kennedy2b9d80e2013-07-30 23:03:45 -07001567 } else if (id == R.id.report_spam) {
1568 delete(R.id.report_spam, target,
Jin Cao30c881a2014-04-08 14:28:36 -07001569 getDeferredAction(R.id.report_spam, target, isBatch, undoCallback), isBatch);
Scott Kennedy2b9d80e2013-07-30 23:03:45 -07001570 } else if (id == R.id.mark_not_spam) {
1571 // Currently, since spam messages are only shown in list with
1572 // other spam messages,
1573 // marking a message not as spam is a destructive action
1574 delete(R.id.mark_not_spam, target,
Jin Cao30c881a2014-04-08 14:28:36 -07001575 getDeferredAction(R.id.mark_not_spam, target, isBatch, undoCallback), isBatch);
Scott Kennedy2b9d80e2013-07-30 23:03:45 -07001576 } else if (id == R.id.report_phishing) {
1577 delete(R.id.report_phishing, target,
Jin Cao30c881a2014-04-08 14:28:36 -07001578 getDeferredAction(R.id.report_phishing, target, isBatch, undoCallback), isBatch);
Scott Kennedy2b9d80e2013-07-30 23:03:45 -07001579 } else if (id == android.R.id.home) {
Jin Cao405a3442014-08-25 13:49:33 -07001580 handleUpPress();
Scott Kennedy2b9d80e2013-07-30 23:03:45 -07001581 } else if (id == R.id.compose) {
1582 ComposeActivity.compose(mActivity.getActivityContext(), mAccount);
1583 } else if (id == R.id.refresh) {
1584 requestFolderRefresh();
1585 } else if (id == R.id.settings) {
1586 Utils.showSettings(mActivity.getActivityContext(), mAccount);
Scott Kennedy2b9d80e2013-07-30 23:03:45 -07001587 } else if (id == R.id.help_info_menu_item) {
Ray Chena57da3c2014-06-10 16:01:40 +02001588 mActivity.showHelp(mAccount, mViewMode.getMode());
Andrew Sapperstein6c570db2013-08-06 17:21:36 -07001589 } else if (id == R.id.move_to || id == R.id.change_folders) {
Tony Mantler2a4be242013-12-11 15:30:53 -08001590 final FolderSelectionDialog dialog = FolderSelectionDialog.getInstance(mAccount,
Scott Kennedy2b9d80e2013-07-30 23:03:45 -07001591 Conversation.listOf(mCurrentConversation), isBatch, mFolder,
1592 id == R.id.move_to);
1593 if (dialog != null) {
Tony Mantler2a4be242013-12-11 15:30:53 -08001594 dialog.show(mActivity.getFragmentManager(), null);
Scott Kennedy2b9d80e2013-07-30 23:03:45 -07001595 }
1596 } else if (id == R.id.move_to_inbox) {
1597 new AsyncTask<Void, Void, Folder>() {
1598 @Override
1599 protected Folder doInBackground(final Void... params) {
1600 // Get the "move to" inbox
1601 return Utils.getFolder(mContext, mAccount.settings.moveToInbox,
1602 true /* allowHidden */);
Mindy Pereira0d03ef82012-08-15 09:05:48 -07001603 }
Scott Kennedydd2ec682013-06-03 19:16:13 -07001604
Scott Kennedy2b9d80e2013-07-30 23:03:45 -07001605 @Override
1606 protected void onPostExecute(final Folder moveToInbox) {
1607 final List<FolderOperation> ops = Lists.newArrayListWithCapacity(1);
1608 // Add inbox
1609 ops.add(new FolderOperation(moveToInbox, true));
1610 assignFolder(ops, Conversation.listOf(mCurrentConversation), true,
1611 true /* showUndo */, false /* isMoveTo */);
1612 }
1613 }.execute((Void[]) null);
1614 } else if (id == R.id.empty_trash) {
1615 showEmptyDialog();
1616 } else if (id == R.id.empty_spam) {
1617 showEmptyDialog();
Jin Caod0334732014-09-01 22:33:14 -07001618 } else if (id == R.id.search) {
1619 mSearchViewController.showSearchActionBar(
1620 MaterialSearchViewController.SEARCH_VIEW_STATE_VISIBLE);
Scott Kennedy2b9d80e2013-07-30 23:03:45 -07001621 } else {
1622 handled = false;
Mindy Pereira28d5f722012-02-15 12:32:40 -08001623 }
Mindy Pereira9b875682012-02-15 18:10:54 -08001624 return handled;
Vikram Aggarwala55b36c2012-01-13 11:45:02 -08001625 }
1626
Andrew Sappersteined5b52d2013-04-30 13:40:18 -07001627 /**
1628 * Opens an {@link EmptyFolderDialogFragment} for the current folder.
1629 */
1630 private void showEmptyDialog() {
1631 if (mFolder != null) {
1632 final EmptyFolderDialogFragment fragment =
1633 EmptyFolderDialogFragment.newInstance(mFolder.totalCount, mFolder.type);
1634 fragment.setListener(this);
1635 fragment.show(mActivity.getFragmentManager(), EmptyFolderDialogFragment.FRAGMENT_TAG);
1636 }
1637 }
1638
1639 @Override
1640 public void onFolderEmptied() {
1641 emptyFolder();
1642 }
1643
1644 /**
1645 * Performs the work of emptying the currently visible folder.
1646 */
Scott Kennedy7ee089e2013-03-25 17:05:44 -04001647 private void emptyFolder() {
Yorke Lee1d0f1f82013-04-26 14:10:27 -07001648 if (mConversationListCursor != null) {
1649 mConversationListCursor.emptyFolder();
1650 }
Scott Kennedy7ee089e2013-03-25 17:05:44 -04001651 }
1652
Andrew Sappersteined5b52d2013-04-30 13:40:18 -07001653 private void attachEmptyFolderDialogFragmentListener() {
1654 final EmptyFolderDialogFragment fragment =
1655 (EmptyFolderDialogFragment) mActivity.getFragmentManager()
1656 .findFragmentByTag(EmptyFolderDialogFragment.FRAGMENT_TAG);
1657
1658 if (fragment != null) {
1659 fragment.setListener(this);
1660 }
1661 }
1662
Rohan Shah8e65c6d2013-03-07 16:47:25 -08001663 /**
Andy Huang12b3ee42013-04-24 22:49:43 -07001664 * Toggles the drawer pullout. If it was open (Fully extended), the
1665 * drawer will be closed. Otherwise, the drawer will be opened. This should
1666 * only be called when used with a toggle item. Other cases should be handled
1667 * explicitly with just closeDrawers() or openDrawer(View drawerView);
Rohan Shah8e65c6d2013-03-07 16:47:25 -08001668 */
Tony Mantlerd9eaca82013-08-05 11:42:29 -07001669 protected void toggleDrawerState() {
Andy Huang12b3ee42013-04-24 22:49:43 -07001670 if (!isDrawerEnabled()) {
1671 return;
1672 }
1673 if(mDrawerContainer.isDrawerOpen(mDrawerPullout)) {
1674 mDrawerContainer.closeDrawers();
1675 } else {
1676 mDrawerContainer.openDrawer(mDrawerPullout);
1677 }
Rohan Shah8e65c6d2013-03-07 16:47:25 -08001678 }
1679
Vikram Aggarwal531488e2012-05-29 16:36:52 -07001680 @Override
Andy Huangc1fb9a92013-02-11 13:09:12 -08001681 public final boolean onBackPressed() {
Andy Huang12b3ee42013-04-24 22:49:43 -07001682 if (isDrawerEnabled() && mDrawerContainer.isDrawerVisible(mDrawerPullout)) {
1683 mDrawerContainer.closeDrawers();
1684 return true;
Jin Caoc6801eb2014-08-12 18:16:57 -07001685 } else if (mSearchViewController.handleBackPress()) {
1686 return true;
Andrew Sapperstein4c928742014-08-29 15:34:23 -07001687 // If we're in CAB mode, let the activity handle onBackPressed.
1688 // It will handle closing CAB mode for us.
1689 } else if (mCabActionMenu != null && mCabActionMenu.isActivated()) {
1690 return false;
Andy Huang12b3ee42013-04-24 22:49:43 -07001691 }
1692
Andy Huangc1fb9a92013-02-11 13:09:12 -08001693 return handleBackPress();
1694 }
1695
1696 protected abstract boolean handleBackPress();
Greg Bullockede2e522014-05-30 14:11:35 +02001697
Andy Huangc1fb9a92013-02-11 13:09:12 -08001698 protected abstract boolean handleUpPress();
1699
1700 @Override
Mindy Pereira6c2663d2012-07-20 15:37:29 -07001701 public void updateConversation(Collection<Conversation> target, ContentValues values) {
Scott Kennedy9e2d4072013-03-21 21:46:01 -07001702 mConversationListCursor.updateValues(target, values);
Mindy Pereira6c2663d2012-07-20 15:37:29 -07001703 refreshConversationList();
1704 }
1705
1706 @Override
Vikram Aggarwal531488e2012-05-29 16:36:52 -07001707 public void updateConversation(Collection <Conversation> target, String columnName,
1708 boolean value) {
Scott Kennedy9e2d4072013-03-21 21:46:01 -07001709 mConversationListCursor.updateBoolean(target, columnName, value);
Vikram Aggarwal75daee52012-04-30 11:13:09 -07001710 refreshConversationList();
Vikram Aggarwal54452ae2012-03-13 15:29:00 -07001711 }
1712
Vikram Aggarwal531488e2012-05-29 16:36:52 -07001713 @Override
Mindy Pereira6c2663d2012-07-20 15:37:29 -07001714 public void updateConversation(Collection <Conversation> target, String columnName,
1715 int value) {
Scott Kennedy9e2d4072013-03-21 21:46:01 -07001716 mConversationListCursor.updateInt(target, columnName, value);
Vikram Aggarwal75daee52012-04-30 11:13:09 -07001717 refreshConversationList();
Mindy Pereirac9d59182012-03-22 16:06:46 -07001718 }
1719
Vikram Aggarwal531488e2012-05-29 16:36:52 -07001720 @Override
1721 public void updateConversation(Collection <Conversation> target, String columnName,
1722 String value) {
Scott Kennedy9e2d4072013-03-21 21:46:01 -07001723 mConversationListCursor.updateString(target, columnName, value);
Vikram Aggarwal75daee52012-04-30 11:13:09 -07001724 refreshConversationList();
Vikram Aggarwal54452ae2012-03-13 15:29:00 -07001725 }
1726
Andy Huang839ada22012-07-20 15:48:40 -07001727 @Override
Yu Ping Hu7c909c72013-01-18 11:58:01 -08001728 public void markConversationMessagesUnread(final Conversation conv,
1729 final Set<Uri> unreadMessageUris, final byte[] originalConversationInfo) {
Andy Huang8f6b0062012-07-31 15:36:31 -07001730 // The only caller of this method is the conversation view, from where marking unread should
1731 // *always* take you back to list mode.
1732 showConversation(null);
1733
Andy Huang839ada22012-07-20 15:48:40 -07001734 // locally mark conversation unread (the provider is supposed to propagate message unread
1735 // to conversation unread)
1736 conv.read = false;
Paul Westbrook1faf93d2012-10-16 08:58:07 -07001737 if (mConversationListCursor == null) {
Vikram Aggarwal81230ee2013-02-26 15:26:44 -08001738 LogUtils.d(LOG_TAG, "markConversationMessagesUnread(id=%d), deferring", conv.id);
Paul Westbrook1faf93d2012-10-16 08:58:07 -07001739
Yu Ping Hu7c909c72013-01-18 11:58:01 -08001740 mConversationListLoadFinishedCallbacks.add(new LoadFinishedCallback() {
1741 @Override
1742 public void onLoadFinished() {
1743 doMarkConversationMessagesUnread(conv, unreadMessageUris,
1744 originalConversationInfo);
1745 }
1746 });
1747 } else {
Vikram Aggarwal81230ee2013-02-26 15:26:44 -08001748 LogUtils.d(LOG_TAG, "markConversationMessagesUnread(id=%d), performing", conv.id);
Yu Ping Hu7c909c72013-01-18 11:58:01 -08001749 doMarkConversationMessagesUnread(conv, unreadMessageUris, originalConversationInfo);
1750 }
1751 }
1752
1753 private void doMarkConversationMessagesUnread(Conversation conv, Set<Uri> unreadMessageUris,
1754 byte[] originalConversationInfo) {
Vikram Aggarwal81230ee2013-02-26 15:26:44 -08001755 // Only do a granular 'mark unread' if a subset of messages are unread
Andy Huang28e31e22012-07-26 16:33:15 -07001756 final int unreadCount = (unreadMessageUris == null) ? 0 : unreadMessageUris.size();
Mindy Pereira0972e072012-08-01 17:43:06 -07001757 final int numMessages = conv.getNumMessages();
1758 final boolean subsetIsUnread = (numMessages > 1 && unreadCount > 0
1759 && unreadCount < numMessages);
Andy Huang28e31e22012-07-26 16:33:15 -07001760
Andy Huang9e4ca792013-02-28 14:33:43 -08001761 LogUtils.d(LOG_TAG, "markConversationMessagesUnread(conv=%s)"
Vikram Aggarwal81230ee2013-02-26 15:26:44 -08001762 + ", numMessages=%d, unreadCount=%d, subsetIsUnread=%b",
Andy Huang9e4ca792013-02-28 14:33:43 -08001763 conv, numMessages, unreadCount, subsetIsUnread);
Andy Huang28e31e22012-07-26 16:33:15 -07001764 if (!subsetIsUnread) {
Vikram Aggarwal66bc2aa2012-08-02 10:47:03 -07001765 // Conversations are neither marked read, nor viewed, and we don't want to show
1766 // the next conversation.
Vikram Aggarwal81230ee2013-02-26 15:26:44 -08001767 LogUtils.d(LOG_TAG, ". . doing full mark unread");
Scott Kennedycaaeed32013-06-12 13:39:16 -07001768 markConversationsRead(Collections.singletonList(conv), false, false, false);
Andy Huang839ada22012-07-20 15:48:40 -07001769 } else {
Vikram Aggarwal81230ee2013-02-26 15:26:44 -08001770 if (LogUtils.isLoggable(LOG_TAG, LogUtils.DEBUG)) {
1771 final ConversationInfo info = ConversationInfo.fromBlob(originalConversationInfo);
1772 LogUtils.d(LOG_TAG, ". . doing subset mark unread, originalConversationInfo = %s",
1773 info);
1774 }
Andy Huangdaa06ab2012-07-24 10:46:44 -07001775 mConversationListCursor.setConversationColumn(conv.uri, ConversationColumns.READ, 0);
Andy Huang839ada22012-07-20 15:48:40 -07001776
Vikram Aggarwal81230ee2013-02-26 15:26:44 -08001777 // Locally update conversation's conversationInfo to revert to original version
Andy Huang28e31e22012-07-26 16:33:15 -07001778 if (originalConversationInfo != null) {
1779 mConversationListCursor.setConversationColumn(conv.uri,
1780 ConversationColumns.CONVERSATION_INFO, originalConversationInfo);
1781 }
Andy Huang839ada22012-07-20 15:48:40 -07001782
1783 // applyBatch with each CPO as an UPDATE op on each affected message uri
1784 final ArrayList<ContentProviderOperation> ops = Lists.newArrayList();
1785 String authority = null;
1786 for (Uri messageUri : unreadMessageUris) {
1787 if (authority == null) {
1788 authority = messageUri.getAuthority();
1789 }
1790 ops.add(ContentProviderOperation.newUpdate(messageUri)
1791 .withValue(UIProvider.MessageColumns.READ, 0)
1792 .build());
Vikram Aggarwal81230ee2013-02-26 15:26:44 -08001793 LogUtils.d(LOG_TAG, ". . Adding op: read=0, uri=%s", messageUri);
Andy Huang839ada22012-07-20 15:48:40 -07001794 }
Vikram Aggarwal81230ee2013-02-26 15:26:44 -08001795 LogUtils.d(LOG_TAG, ". . operations = %s", ops);
Andy Huang839ada22012-07-20 15:48:40 -07001796 new ContentProviderTask() {
1797 @Override
1798 protected void onPostExecute(Result result) {
Vikram Aggarwald9895b62013-02-27 16:52:27 -08001799 if (result.exception != null) {
1800 LogUtils.e(LOG_TAG, result.exception, "ContentProviderTask() ERROR.");
1801 } else {
Andy Huang9e4ca792013-02-28 14:33:43 -08001802 LogUtils.d(LOG_TAG, "ContentProviderTask(): success %s",
1803 Arrays.toString(result.results));
Vikram Aggarwald9895b62013-02-27 16:52:27 -08001804 }
Andy Huang839ada22012-07-20 15:48:40 -07001805 }
1806 }.run(mResolver, authority, ops);
Andy Huang839ada22012-07-20 15:48:40 -07001807 }
Andy Huang839ada22012-07-20 15:48:40 -07001808 }
1809
1810 @Override
Yu Ping Hu7c909c72013-01-18 11:58:01 -08001811 public void markConversationsRead(final Collection<Conversation> targets, final boolean read,
1812 final boolean viewed) {
Scott Kennedy919d01a2013-05-07 16:13:29 -07001813 LogUtils.d(LOG_TAG, "markConversationsRead(targets=%s)", targets.toArray());
1814
Yu Ping Hu7c909c72013-01-18 11:58:01 -08001815 if (mConversationListCursor == null) {
Vikram Aggarwal81230ee2013-02-26 15:26:44 -08001816 if (LogUtils.isLoggable(LOG_TAG, LogUtils.DEBUG)) {
1817 LogUtils.d(LOG_TAG, "markConversationsRead(targets=%s), deferring",
1818 targets.toArray());
1819 }
Yu Ping Hu7c909c72013-01-18 11:58:01 -08001820 mConversationListLoadFinishedCallbacks.add(new LoadFinishedCallback() {
1821 @Override
1822 public void onLoadFinished() {
Scott Kennedycaaeed32013-06-12 13:39:16 -07001823 markConversationsRead(targets, read, viewed, true);
Yu Ping Hu7c909c72013-01-18 11:58:01 -08001824 }
1825 });
1826 } else {
1827 // We want to show the next conversation if we are marking unread.
Scott Kennedycaaeed32013-06-12 13:39:16 -07001828 markConversationsRead(targets, read, viewed, true);
Yu Ping Hu7c909c72013-01-18 11:58:01 -08001829 }
Andy Huang8f6b0062012-07-31 15:36:31 -07001830 }
1831
Scott Kennedy0d0f8b02012-10-12 15:18:18 -07001832 private void markConversationsRead(final Collection<Conversation> targets, final boolean read,
Scott Kennedycaaeed32013-06-12 13:39:16 -07001833 final boolean markViewed, final boolean showNext) {
Yu Ping Hu7c909c72013-01-18 11:58:01 -08001834 LogUtils.d(LOG_TAG, "performing markConversationsRead");
Vikram Aggarwalcc754b12012-08-30 14:04:21 -07001835 // Auto-advance if requested and the current conversation is being marked unread
Andy Huang8f6b0062012-07-31 15:36:31 -07001836 if (showNext && !read) {
Scott Kennedy0d0f8b02012-10-12 15:18:18 -07001837 final Runnable operation = new Runnable() {
1838 @Override
1839 public void run() {
Scott Kennedycaaeed32013-06-12 13:39:16 -07001840 markConversationsRead(targets, read, markViewed, showNext);
Scott Kennedy0d0f8b02012-10-12 15:18:18 -07001841 }
1842 };
1843
Scott Kennedycaaeed32013-06-12 13:39:16 -07001844 if (!showNextConversation(targets, operation)) {
Scott Kennedy0d0f8b02012-10-12 15:18:18 -07001845 // This method will be called again if the user selects an autoadvance option
1846 return;
1847 }
Andy Huang8f6b0062012-07-31 15:36:31 -07001848 }
Scott Kennedy0d0f8b02012-10-12 15:18:18 -07001849
Vikram Aggarwalcc754b12012-08-30 14:04:21 -07001850 final int size = targets.size();
1851 final List<ConversationOperation> opList = new ArrayList<ConversationOperation>(size);
1852 for (final Conversation target : targets) {
Paul Westbrooke2be97a2013-05-21 02:12:09 -07001853 final ContentValues value = new ContentValues(4);
Vikram Aggarwalcc754b12012-08-30 14:04:21 -07001854 value.put(ConversationColumns.READ, read);
Paul Westbrook5109c512012-11-05 11:00:30 -08001855
Scott Kennedyd5edd2d2012-12-05 11:11:32 -08001856 // We never want to mark unseen here, but we do want to mark it seen
1857 if (read || markViewed) {
1858 value.put(ConversationColumns.SEEN, Boolean.TRUE);
1859 }
1860
Paul Westbrook5109c512012-11-05 11:00:30 -08001861 // The mark read/unread/viewed operations do not show an undo bar
1862 value.put(ConversationOperations.Parameters.SUPPRESS_UNDO, true);
Vikram Aggarwal66bc2aa2012-08-02 10:47:03 -07001863 if (markViewed) {
Vikram Aggarwalcc754b12012-08-30 14:04:21 -07001864 value.put(ConversationColumns.VIEWED, true);
Vikram Aggarwal66bc2aa2012-08-02 10:47:03 -07001865 }
Vikram Aggarwal192fac12012-07-25 16:44:55 -07001866 final ConversationInfo info = target.conversationInfo;
Tony Mantleredd6c1a2013-10-08 14:47:43 -07001867 final boolean changed = info.markRead(read);
1868 if (changed) {
1869 value.put(ConversationColumns.CONVERSATION_INFO, info.toBlob());
Andy Huang839ada22012-07-20 15:48:40 -07001870 }
Vikram Aggarwalcc754b12012-08-30 14:04:21 -07001871 opList.add(mConversationListCursor.getOperationForConversation(
1872 target, ConversationOperation.UPDATE, value));
1873 // Update the local conversation objects so they immediately change state.
1874 target.read = read;
Andy Huangcd5c5ee2012-08-12 19:03:51 -07001875 if (markViewed) {
Vikram Aggarwalcc754b12012-08-30 14:04:21 -07001876 target.markViewed();
Andy Huangcd5c5ee2012-08-12 19:03:51 -07001877 }
Andy Huang839ada22012-07-20 15:48:40 -07001878 }
Scott Kennedy9e2d4072013-03-21 21:46:01 -07001879 mConversationListCursor.updateBulkValues(opList);
Andy Huang839ada22012-07-20 15:48:40 -07001880 }
1881
Andy Huang8f6b0062012-07-31 15:36:31 -07001882 /**
1883 * Auto-advance to a different conversation if the currently visible conversation in
1884 * conversation mode is affected (deleted, marked unread, etc.).
1885 *
Scott Kennedy0d0f8b02012-10-12 15:18:18 -07001886 * <p>Does nothing if outside of conversation mode.</p>
Andy Huang8f6b0062012-07-31 15:36:31 -07001887 *
1888 * @param target the set of conversations being deleted/marked unread
1889 */
mindyp9365a822012-09-12 09:09:09 -07001890 @Override
Scott Kennedy0d0f8b02012-10-12 15:18:18 -07001891 public void showNextConversation(final Collection<Conversation> target) {
Scott Kennedycaaeed32013-06-12 13:39:16 -07001892 showNextConversation(target, null);
Scott Kennedy0d0f8b02012-10-12 15:18:18 -07001893 }
1894
1895 /**
Jin Cao30c881a2014-04-08 14:28:36 -07001896 * Helper function to determine if the provided set of conversations is in view
1897 * @param target set of conversations that we are interested in
1898 * @return true if they are in view, false otherwise
1899 */
1900 private boolean isCurrentConversationInView(final Collection<Conversation> target) {
1901 final int viewMode = mViewMode.getMode();
1902 return (viewMode == ViewMode.CONVERSATION
1903 || viewMode == ViewMode.SEARCH_RESULTS_CONVERSATION)
1904 && Conversation.contains(target, mCurrentConversation);
1905 }
1906
1907 /**
Scott Kennedy0d0f8b02012-10-12 15:18:18 -07001908 * Auto-advance to a different conversation if the currently visible conversation in
1909 * conversation mode is affected (deleted, marked unread, etc.).
1910 *
1911 * <p>Does nothing if outside of conversation mode.</p>
Andy Huangc94d07f2013-06-03 16:19:35 -07001912 * <p>
1913 * Clients may pass an operation to execute on the target that this method will run after
1914 * auto-advance is complete. The operation, if provided, may run immediately, or it may run
1915 * later, or not at all. Reasons it may run later include:
1916 * <ul>
1917 * <li>the auto-advance setting is uninitialized and we need to wait for the user to set it</li>
1918 * <li>auto-advance in this configuration requires a mode change, and we need to wait for the
1919 * mode change transition to finish</li>
1920 * </ul>
1921 * <p>If the current conversation is not in the target collection, this method will do nothing,
1922 * and will not execute the operation.
Scott Kennedy0d0f8b02012-10-12 15:18:18 -07001923 *
1924 * @param target the set of conversations being deleted/marked unread
Andy Huangc94d07f2013-06-03 16:19:35 -07001925 * @param operation (optional) the operation to execute after advancing
1926 * @return <code>false</code> if this method handled or will execute the operation,
1927 * <code>true</code> otherwise.
Scott Kennedy0d0f8b02012-10-12 15:18:18 -07001928 */
1929 private boolean showNextConversation(final Collection<Conversation> target,
Scott Kennedycaaeed32013-06-12 13:39:16 -07001930 final Runnable operation) {
Jin Cao30c881a2014-04-08 14:28:36 -07001931 if (isCurrentConversationInView(target)) {
Scott Kennedy0d0f8b02012-10-12 15:18:18 -07001932 final int autoAdvanceSetting = mAccount.settings.getAutoAdvanceSetting();
1933
Tony Mantler93e64572014-07-11 14:35:56 -07001934 // If we don't have one set, but we're here, just take the default
1935 final int autoAdvance = (autoAdvanceSetting == AutoAdvance.UNSET) ?
1936 AutoAdvance.DEFAULT : autoAdvanceSetting;
Scott Kennedy0d0f8b02012-10-12 15:18:18 -07001937
Tony Mantler93e64572014-07-11 14:35:56 -07001938 final Conversation next = mTracker.getNextConversation(autoAdvance, target);
1939 LogUtils.d(LOG_TAG, "showNextConversation: showing %s next.", next);
1940 // Set mAutoAdvanceOp *before* showConversation() to ensure that it runs when the
1941 // transition doesn't run (i.e. it "completes" immediately).
1942 mAutoAdvanceOp = operation;
1943 showConversation(next);
1944 return (mAutoAdvanceOp == null);
Andy Huang8f6b0062012-07-31 15:36:31 -07001945 }
Scott Kennedy0d0f8b02012-10-12 15:18:18 -07001946
1947 return true;
1948 }
1949
Andy Huang839ada22012-07-20 15:48:40 -07001950 @Override
1951 public void starMessage(ConversationMessage msg, boolean starred) {
1952 if (msg.starred == starred) {
1953 return;
1954 }
1955
1956 msg.starred = starred;
1957
1958 // locally propagate the change to the owning conversation
1959 // (figure the provider will properly propagate the change when it commits it)
1960 //
1961 // when unstarring, only propagate the change if this was the only message starred
1962 final boolean conversationStarred = starred || msg.isConversationStarred();
Andy Huangcd12e822012-11-08 19:50:57 -08001963 final Conversation conv = msg.getConversation();
1964 if (conversationStarred != conv.starred) {
1965 conv.starred = conversationStarred;
1966 mConversationListCursor.setConversationColumn(conv.uri,
Andy Huang839ada22012-07-20 15:48:40 -07001967 ConversationColumns.STARRED, conversationStarred);
1968 }
1969
1970 final ContentValues values = new ContentValues(1);
1971 values.put(UIProvider.MessageColumns.STARRED, starred ? 1 : 0);
1972
1973 new ContentProviderTask.UpdateTask() {
1974 @Override
1975 protected void onPostExecute(Result result) {
1976 // TODO: handle errors?
1977 }
1978 }.run(mResolver, msg.uri, values, null /* selection*/, null /* selectionArgs */);
1979 }
1980
Andy Huang12b3ee42013-04-24 22:49:43 -07001981 @Override
Alice Yang486e63e2013-04-05 13:01:50 -07001982 public void requestFolderRefresh() {
Alice Yang37dda442013-03-26 22:48:53 -07001983 if (mFolder == null) {
1984 return;
Mindy Pereira28e0c342012-02-17 15:05:13 -08001985 }
Alice Yang37dda442013-03-26 22:48:53 -07001986 final ConversationListFragment convList = getConversationListFragment();
1987 if (convList == null) {
1988 // This could happen if this account is in initial sync (user
1989 // is seeing the "your mail will appear shortly" message)
1990 return;
1991 }
Jin Cao41733e32014-09-15 12:02:34 -07001992
1993 // TODO: remove me after experiment b/17508768
1994 final String id = android.provider.Settings.Secure.getString(
1995 mActivity.getContentResolver(), android.provider.Settings.Secure.ANDROID_ID);
1996 if (!TextUtils.isEmpty(id)) {
1997 final long longId = Long.parseLong(
1998 Character.toString(id.charAt(id.length() - 1)), 16 /* hex */);
1999 Analytics.getInstance().sendEvent("battery_experiment", "refresh",
2000 longId % 2 == 0 ? "even" : "odd", 1);
2001 }
2002
Alice Yang37dda442013-03-26 22:48:53 -07002003 convList.showSyncStatusBar();
2004
2005 if (mAsyncRefreshTask != null) {
2006 mAsyncRefreshTask.cancel(true);
2007 }
2008 mAsyncRefreshTask = new AsyncRefreshTask(mContext, mFolder.refreshUri);
2009 mAsyncRefreshTask.execute();
Mindy Pereira28e0c342012-02-17 15:05:13 -08002010 }
2011
Mindy Pereirafbe40192012-03-20 10:40:45 -07002012 /**
2013 * Confirm (based on user's settings) and delete a conversation from the conversation list and
2014 * from the database.
Vikram Aggarwalb8c31712013-01-03 17:03:19 -08002015 * @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 -07002016 * @param target the conversations to act upon
2017 * @param showDialog true if a confirmation dialog is to be shown, false otherwise.
2018 * @param confirmResource the resource ID of the string that is shown in the confirmation dialog
Mindy Pereirafbe40192012-03-20 10:40:45 -07002019 */
Vikram Aggarwalb8c31712013-01-03 17:03:19 -08002020 private void confirmAndDelete(int actionId, final Collection<Conversation> target,
Jin Cao30c881a2014-04-08 14:28:36 -07002021 boolean showDialog, int confirmResource, UndoCallback undoCallback) {
Vikram Aggarwal84fe9942013-04-17 11:20:58 -07002022 final boolean isBatch = false;
Mindy Pereirafbe40192012-03-20 10:40:45 -07002023 if (showDialog) {
Jin Cao30c881a2014-04-08 14:28:36 -07002024 makeDialogListener(actionId, isBatch, undoCallback);
Vikram Aggarwal3d7ca9d2012-05-11 14:40:36 -07002025 final CharSequence message = Utils.formatPlural(mContext, confirmResource,
2026 target.size());
Vikram Aggarwalb8c31712013-01-03 17:03:19 -08002027 final ConfirmDialogFragment c = ConfirmDialogFragment.newInstance(message);
2028 c.displayDialog(mActivity.getFragmentManager());
Mindy Pereirafbe40192012-03-20 10:40:45 -07002029 } else {
Jin Cao30c881a2014-04-08 14:28:36 -07002030 delete(0, target, getDeferredAction(actionId, target, isBatch, undoCallback), isBatch);
Mindy Pereirafbe40192012-03-20 10:40:45 -07002031 }
2032 }
2033
Vikram Aggarwal4f4782b2012-05-30 08:39:09 -07002034 @Override
Scott Kennedy0d0f8b02012-10-12 15:18:18 -07002035 public void delete(final int actionId, final Collection<Conversation> target,
Scott Kennedycaaeed32013-06-12 13:39:16 -07002036 final DestructiveAction action, final boolean isBatch) {
mindyp84f7d322012-10-01 17:14:40 -07002037 // Order of events is critical! The Conversation View Fragment must be
2038 // notified of the next conversation with showConversation(next) *before* the
2039 // conversation list
Vikram Aggarwal192fac12012-07-25 16:44:55 -07002040 // fragment has a chance to delete the conversation, animating it away.
2041
mindyp84f7d322012-10-01 17:14:40 -07002042 // Update the conversation fragment if the current conversation is
2043 // deleted.
Scott Kennedy0d0f8b02012-10-12 15:18:18 -07002044 final Runnable operation = new Runnable() {
2045 @Override
2046 public void run() {
Scott Kennedycaaeed32013-06-12 13:39:16 -07002047 delete(actionId, target, action, isBatch);
Scott Kennedy0d0f8b02012-10-12 15:18:18 -07002048 }
2049 };
2050
Rohan Shah52bab6f2014-08-26 18:52:14 -07002051 showNextConversation(target, operation);
2052
Vikram Aggarwalab1b5b62013-04-16 15:45:50 -07002053 // If the conversation is in the selected set, remove it from the set.
Vikram Aggarwal84fe9942013-04-17 11:20:58 -07002054 // Batch selections are cleared in the end of the action, so not done for batch actions.
2055 if (!isBatch) {
2056 for (final Conversation conv : target) {
Jin Caoec0fa482014-08-28 16:38:08 -07002057 if (mCheckedSet.contains(conv)) {
2058 mCheckedSet.toggle(conv);
Vikram Aggarwal84fe9942013-04-17 11:20:58 -07002059 }
Vikram Aggarwalab1b5b62013-04-16 15:45:50 -07002060 }
2061 }
Vikram Aggarwal192fac12012-07-25 16:44:55 -07002062 // The conversation list deletes and performs the action if it exists.
2063 final ConversationListFragment convListFragment = getConversationListFragment();
2064 if (convListFragment != null) {
Alice Yang193e05a2013-05-05 14:12:08 -07002065 LogUtils.i(LOG_TAG, "AAC.requestDelete: ListFragment is handling delete.");
Vikram Aggarwal669947b2013-01-10 17:05:56 -08002066 convListFragment.requestDelete(actionId, target, action);
Vikram Aggarwal192fac12012-07-25 16:44:55 -07002067 return;
2068 }
mindyp84f7d322012-10-01 17:14:40 -07002069 // No visible UI element handled it on our behalf. Perform the action
2070 // ourself.
Alice Yang193e05a2013-05-05 14:12:08 -07002071 LogUtils.i(LOG_TAG, "ACC.requestDelete: performing remove action ourselves");
Vikram Aggarwald503df42012-05-11 10:13:35 -07002072 action.performAction();
2073 }
2074
2075 /**
2076 * Requests that the action be performed and the UI state is updated to reflect the new change.
Vikram Aggarwal715f83d2013-03-05 17:04:56 -08002077 * @param action the action to be performed, specified as a menu id: R.id.archive, ...
Vikram Aggarwald503df42012-05-11 10:13:35 -07002078 */
Vikram Aggarwal715f83d2013-03-05 17:04:56 -08002079 private void requestUpdate(final DestructiveAction action) {
Vikram Aggarwald503df42012-05-11 10:13:35 -07002080 action.performAction();
2081 refreshConversationList();
Vikram Aggarwal75daee52012-04-30 11:13:09 -07002082 }
Vikram Aggarwal6fbc87a2012-03-15 15:24:00 -07002083
2084 @Override
Vikram Aggarwala55b36c2012-01-13 11:45:02 -08002085 public void onPrepareDialog(int id, Dialog dialog, Bundle bundle) {
2086 // TODO(viki): Auto-generated method stub
Vikram Aggarwala55b36c2012-01-13 11:45:02 -08002087 }
2088
2089 @Override
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -08002090 public boolean onPrepareOptionsMenu(Menu menu) {
Andrew Sapperstein2d86d112014-07-24 20:27:37 -07002091 return mActionBarController.onPrepareOptionsMenu(menu);
Vikram Aggarwala55b36c2012-01-13 11:45:02 -08002092 }
2093
Mindy Pereira68f2e222012-03-07 10:36:54 -08002094 @Override
2095 public void onPause() {
Vikram Aggarwalc04cf7e2013-05-13 15:38:42 -07002096 mHaveAccountList = false;
Paul Westbrook6ead20d2012-03-19 14:48:14 -07002097 enableNotifications();
Paul Westbrook94e440d2012-02-24 11:03:47 -08002098 }
2099
Vikram Aggarwala55b36c2012-01-13 11:45:02 -08002100 @Override
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -08002101 public void onResume() {
Paul Westbrook6ead20d2012-03-19 14:48:14 -07002102 // Register the receiver that will prevent the status receiver from
2103 // displaying its notification icon as long as we're running.
2104 // The SupressNotificationReceiver will block the broadcast if we're looking at the folder
2105 // that the notification was received for.
2106 disableNotifications();
Andy Huang1ee96b22012-08-24 20:19:53 -07002107
2108 mSafeToModifyFragments = true;
Andrew Sappersteined5b52d2013-04-30 13:40:18 -07002109
2110 attachEmptyFolderDialogFragmentListener();
Andrew Sapperstein23e33132013-05-07 18:26:49 -07002111
2112 // Invalidating the options menu so that when we make changes in settings,
2113 // the changes will always be updated in the action bar/options menu/
2114 mActivity.invalidateOptionsMenu();
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -08002115 }
2116
2117 @Override
2118 public void onSaveInstanceState(Bundle outState) {
Vikram Aggarwal649b9ea2012-08-27 12:15:20 -07002119 mViewMode.handleSaveInstanceState(outState);
Vikram Aggarwal6c511582012-02-27 10:59:47 -08002120 if (mAccount != null) {
Vikram Aggarwal6c511582012-02-27 10:59:47 -08002121 outState.putParcelable(SAVED_ACCOUNT, mAccount);
2122 }
Mindy Pereira5e478d22012-03-26 18:04:58 -07002123 if (mFolder != null) {
2124 outState.putParcelable(SAVED_FOLDER, mFolder);
Vikram Aggarwal8b152632012-02-03 14:58:45 -08002125 }
Vikram Aggarwalf3341402012-08-07 10:09:38 -07002126 // If this is a search activity, let's store the search query term as well.
2127 if (ConversationListContext.isSearchResult(mConvListContext)) {
2128 outState.putString(SAVED_QUERY, mConvListContext.searchQuery);
2129 }
Vikram Aggarwal49e0e992012-09-21 13:53:15 -07002130 if (mCurrentConversation != null && mViewMode.isConversationMode()) {
Mindy Pereira26f23fc2012-03-27 10:26:04 -07002131 outState.putParcelable(SAVED_CONVERSATION, mCurrentConversation);
2132 }
Jin Caoec0fa482014-08-28 16:38:08 -07002133 if (!mCheckedSet.isEmpty()) {
2134 outState.putParcelable(SAVED_SELECTED_SET, mCheckedSet);
Andy Huang4556a442012-03-30 16:42:05 -07002135 }
Mindy Pereirad33674992012-06-25 16:26:30 -07002136 if (mToastBar.getVisibility() == View.VISIBLE) {
2137 outState.putParcelable(SAVED_TOAST_BAR_OP, mToastBar.getOperation());
2138 }
Vikram Aggarwal649b9ea2012-08-27 12:15:20 -07002139 final ConversationListFragment convListFragment = getConversationListFragment();
Mindy Pereirad33674992012-06-25 16:26:30 -07002140 if (convListFragment != null) {
Vikram Aggarwal649b9ea2012-08-27 12:15:20 -07002141 convListFragment.getAnimatedAdapter().onSaveInstanceState(outState);
Mindy Pereirad33674992012-06-25 16:26:30 -07002142 }
Vikram Aggarwalb8c31712013-01-03 17:03:19 -08002143 // 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 -08002144 if (mDialogAction != -1) {
2145 outState.putInt(SAVED_ACTION, mDialogAction);
Vikram Aggarwalb8c31712013-01-03 17:03:19 -08002146 outState.putBoolean(SAVED_ACTION_FROM_SELECTED, mDialogFromSelectedSet);
Vikram Aggarwal6cadbfc2012-12-27 09:17:05 -08002147 }
Vikram Aggarwala91d00b2013-01-18 12:00:37 -08002148 if (mDetachedConvUri != null) {
2149 outState.putParcelable(SAVED_DETACHED_CONV_URI, mDetachedConvUri);
2150 }
Andrew Sapperstein5747e152013-05-13 14:13:08 -07002151
Scott Kennedyb10212e2013-02-22 16:27:00 -08002152 outState.putParcelable(SAVED_HIERARCHICAL_FOLDER, mFolderListFolder);
Andrew Sapperstein5747e152013-05-13 14:13:08 -07002153 mSafeToModifyFragments = false;
Alice Yangebeef1b2013-09-04 06:41:10 +00002154
2155 outState.putParcelable(SAVED_INBOX_KEY, mInbox);
Scott Kennedyf77806e2013-08-30 11:38:15 -07002156
2157 outState.putBundle(SAVED_CONVERSATION_LIST_SCROLL_POSITIONS,
2158 mConversationListScrollPositions);
Jin Caoc6801eb2014-08-12 18:16:57 -07002159
2160 mSearchViewController.saveState(outState);
Andy Huang1ee96b22012-08-24 20:19:53 -07002161 }
2162
2163 /**
2164 * @see #mSafeToModifyFragments
2165 */
2166 protected boolean safeToModifyFragments() {
2167 return mSafeToModifyFragments;
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -08002168 }
2169
2170 @Override
Andy Huang313ac132013-03-04 23:40:58 -08002171 public void executeSearch(String query) {
Jin Cao779dd602014-04-22 16:16:28 -07002172 AnalyticsTimer.getInstance().trackStart(AnalyticsTimer.SEARCH_TO_LIST);
Mindy Pereira68f2e222012-03-07 10:36:54 -08002173 Intent intent = new Intent();
2174 intent.setAction(Intent.ACTION_SEARCH);
2175 intent.putExtra(ConversationListContext.EXTRA_SEARCH_QUERY, query);
2176 intent.putExtra(Utils.EXTRA_ACCOUNT, mAccount);
2177 intent.setComponent(mActivity.getComponentName());
Jin Caoc6801eb2014-08-12 18:16:57 -07002178 mSearchViewController.showSearchActionBar(
2179 MaterialSearchViewController.SEARCH_VIEW_STATE_GONE);
Martin Hibdon371a71c2014-02-19 13:55:28 -08002180 // Call startActivityForResult here so we can tell if we have navigated to a different folder
2181 // or account from search results.
2182 mActivity.startActivityForResult(intent, CHANGE_NAVIGATION_REQUEST_CODE);
Vikram Aggarwala55b36c2012-01-13 11:45:02 -08002183 }
2184
2185 @Override
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -08002186 public void onStop() {
Scott Kennedycb85aea2013-02-25 13:08:32 -08002187 NotificationActionUtils.unregisterUndoNotificationObserver(mUndoNotificationObserver);
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -08002188 }
2189
Andy Huang632721e2012-04-11 16:57:26 -07002190 @Override
2191 public void onDestroy() {
Andy Huangb2ef9c12012-12-18 12:58:41 -06002192 // stop listening to the cursor on e.g. configuration changes
2193 if (mConversationListCursor != null) {
2194 mConversationListCursor.removeListener(this);
2195 }
Andy Huang144bfe72013-06-11 13:27:52 -07002196 mDrawIdler.setListener(null);
2197 mDrawIdler.setRootView(null);
Andy Huang632721e2012-04-11 16:57:26 -07002198 // unregister the ViewPager's observer on the conversation cursor
2199 mPagerController.onDestroy();
Andrew Sapperstein2d86d112014-07-24 20:27:37 -07002200 mActionBarController.onDestroy();
Vikram Aggarwal7c401b72012-08-13 16:43:47 -07002201 mRecentFolderList.destroy();
Andy Huang4e0158f2012-08-07 21:06:01 -07002202 mDestroyed = true;
Vikram Aggarwal59f741f2013-03-01 15:55:40 -08002203 mHandler.removeCallbacks(mLogServiceChecker);
2204 mLogServiceChecker = null;
Jin Caoc6801eb2014-08-12 18:16:57 -07002205 mSearchViewController.onDestroy();
Andy Huang632721e2012-04-11 16:57:26 -07002206 }
2207
Vikram Aggarwalfa131a22012-02-02 13:56:22 -08002208 /**
Vikram Aggarwal93dc2022012-11-05 13:36:57 -08002209 * Set the Action Bar icon according to the mode. The Action Bar icon can contain a back button
2210 * or not. The individual controller is responsible for changing the icon based on the mode.
2211 */
2212 protected abstract void resetActionBarIcon();
2213
2214 /**
Mindy Pereira161f50d2012-02-28 15:47:19 -08002215 * {@inheritDoc} Subclasses must override this to listen to mode changes
2216 * from the ViewMode. Subclasses <b>must</b> call the parent's
2217 * onViewModeChanged since the parent will handle common state changes.
Vikram Aggarwalfa131a22012-02-02 13:56:22 -08002218 */
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -08002219 @Override
Vikram Aggarwalfa131a22012-02-02 13:56:22 -08002220 public void onViewModeChanged(int newMode) {
Vikram Aggarwal49e0e992012-09-21 13:53:15 -07002221 // When we step away from the conversation mode, we don't have a current conversation
2222 // anymore. Let's blank it out so clients calling getCurrentConversation are not misled.
2223 if (!ViewMode.isConversationMode(newMode)) {
2224 setCurrentConversation(null);
2225 }
Andrew Sapperstein6c570db2013-08-06 17:21:36 -07002226
Vikram Aggarwal93dc2022012-11-05 13:36:57 -08002227 // If the viewmode is not set, preserve existing icon.
2228 if (newMode != ViewMode.UNKNOWN) {
2229 resetActionBarIcon();
2230 }
Andy Huang12b3ee42013-04-24 22:49:43 -07002231
2232 if (isDrawerEnabled()) {
Vikram Aggarwaldfbc6982013-07-03 14:39:31 -07002233 /** If the folder doesn't exist, or its parent URI is empty,
2234 * this is not a child folder */
Jin Cao9695e002014-05-29 11:56:44 -07002235 final boolean isTopLevel = Folder.isRoot(mFolder);
Vikram Aggarwaldfbc6982013-07-03 14:39:31 -07002236 mDrawerToggle.setDrawerIndicatorEnabled(
2237 getShouldShowDrawerIndicator(newMode, isTopLevel));
Martin Hibdon371a71c2014-02-19 13:55:28 -08002238 mDrawerContainer.setDrawerLockMode(DrawerLayout.LOCK_MODE_UNLOCKED);
Andy Huang12b3ee42013-04-24 22:49:43 -07002239 closeDrawerIfOpen();
2240 }
Vikram Aggarwala55b36c2012-01-13 11:45:02 -08002241 }
2242
Vikram Aggarwaldfbc6982013-07-03 14:39:31 -07002243 /**
2244 * Returns true if the drawer icon is shown
2245 * @param viewMode the current view mode
2246 * @param isTopLevel true if the current folder is not a child
2247 * @return whether the drawer indicator is shown
2248 */
Scott Kennedy81b5fa02013-07-11 14:39:18 -07002249 private boolean getShouldShowDrawerIndicator(final int viewMode,
Vikram Aggarwaldfbc6982013-07-03 14:39:31 -07002250 final boolean isTopLevel) {
2251 // If search list/conv mode: disable indicator
2252 // Indicator is enabled either in conversation list or folder list mode.
Scott Kennedy81b5fa02013-07-11 14:39:18 -07002253 return isDrawerEnabled() && !ViewMode.isSearchMode(viewMode)
Scott Kennedyaded5782013-07-16 14:21:53 -07002254 && (viewMode == ViewMode.CONVERSATION_LIST && isTopLevel);
Scott Kennedy8a72b852013-05-02 14:18:50 -07002255 }
2256
Andy Huang3825f3d2012-08-29 16:44:12 -07002257 public void disablePagerUpdates() {
2258 mPagerController.stopListening();
2259 }
2260
Andy Huang4e0158f2012-08-07 21:06:01 -07002261 public boolean isDestroyed() {
2262 return mDestroyed;
2263 }
2264
mindyp54f120f2012-08-28 13:10:33 -07002265 @Override
2266 public void commitDestructiveActions(boolean animate) {
mindypc6adce32012-08-22 18:46:42 -07002267 ConversationListFragment fragment = getConversationListFragment();
Mindy Pereira1e2573b2012-04-17 14:34:36 -07002268 if (fragment != null) {
mindypc6adce32012-08-22 18:46:42 -07002269 fragment.commitDestructiveActions(animate);
Mindy Pereira1e2573b2012-04-17 14:34:36 -07002270 }
2271 }
2272
Vikram Aggarwala55b36c2012-01-13 11:45:02 -08002273 @Override
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -08002274 public void onWindowFocusChanged(boolean hasFocus) {
Vikram Aggarwal69b5c302012-09-05 11:11:13 -07002275 final ConversationListFragment convList = getConversationListFragment();
Vikram Aggarwal6422b8d2013-01-14 10:47:41 -08002276 // hasFocus already ensures that the window is in focus, so we don't need to call
2277 // AAC.isFragmentVisible(convList) here.
Paul Westbrook9f119c72012-04-24 16:10:59 -07002278 if (hasFocus && convList != null && convList.isVisible()) {
2279 // The conversation list is visible.
Vikram Aggarwal69b5c302012-09-05 11:11:13 -07002280 informCursorVisiblity(true);
Paul Westbrook9f119c72012-04-24 16:10:59 -07002281 }
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -08002282 }
2283
Vikram Aggarwalca716f12012-08-20 11:11:48 -07002284 /**
2285 * Set the account, and carry out all the account-related changes that rely on this.
Vikram Aggarwal715f83d2013-03-05 17:04:56 -08002286 * @param account new account to set to.
Vikram Aggarwalca716f12012-08-20 11:11:48 -07002287 */
Mindy Pereira75181e82012-04-18 08:17:13 -07002288 private void setAccount(Account account) {
Andy Huangb9ca9792012-05-18 15:31:49 -07002289 if (account == null) {
2290 LogUtils.w(LOG_TAG, new Error(),
2291 "AAC ignoring null (presumably invalid) account restoration");
2292 return;
2293 }
Andy Huangb1148412012-05-19 00:16:30 -07002294 LogUtils.d(LOG_TAG, "AbstractActivityController.setAccount(): account = %s", account.uri);
Vikram Aggarwal91e87372012-05-18 15:36:04 -07002295 mAccount = account;
Régis Décamps2168cbc2014-08-22 15:00:37 +02002296
2297 Analytics.getInstance().setEmail(account.getEmailAddress(), account.getType());
2298
Vikram Aggarwalca716f12012-08-20 11:11:48 -07002299 // Only change AAC state here. Do *not* modify any other object's state. The object
2300 // should listen on account changes.
Vikram Aggarwal177097f2013-03-08 11:19:53 -08002301 restartOptionalLoader(LOADER_RECENT_FOLDERS, mFolderCallbacks, Bundle.EMPTY);
Vikram Aggarwalca716f12012-08-20 11:11:48 -07002302 mActivity.invalidateOptionsMenu();
2303 disableNotificationsOnAccountChange(mAccount);
Vikram Aggarwal177097f2013-03-08 11:19:53 -08002304 restartOptionalLoader(LOADER_ACCOUNT_UPDATE_CURSOR, mAccountCallbacks, Bundle.EMPTY);
Vikram Aggarwal2fe343f2013-01-14 09:00:25 -08002305 // The Mail instance can be null during test runs.
2306 final MailAppProvider instance = MailAppProvider.getInstance();
2307 if (instance != null) {
2308 instance.setLastViewedAccount(mAccount.uri.toString());
2309 }
Vikram Aggarwal91e87372012-05-18 15:36:04 -07002310 if (account.settings == null) {
2311 LogUtils.w(LOG_TAG, new Error(), "AAC ignoring account with null settings.");
2312 return;
2313 }
Vikram Aggarwal7c401b72012-08-13 16:43:47 -07002314 mAccountObservers.notifyChanged();
Vikram Aggarwal34f7b232012-10-17 13:32:23 -07002315 perhapsEnterWaitMode();
Mindy Pereira75181e82012-04-18 08:17:13 -07002316 }
2317
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -08002318 /**
Mindy Pereira161f50d2012-02-28 15:47:19 -08002319 * Restore the state from the previous bundle. Subclasses should call this
2320 * method from the parent class, since it performs important UI
2321 * initialization.
2322 *
Vikram Aggarwal715f83d2013-03-05 17:04:56 -08002323 * @param savedState previous state
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -08002324 */
Andy Huang632721e2012-04-11 16:57:26 -07002325 @Override
2326 public void onRestoreInstanceState(Bundle savedState) {
Vikram Aggarwala91d00b2013-01-18 12:00:37 -08002327 mDetachedConvUri = savedState.getParcelable(SAVED_DETACHED_CONV_URI);
Andy Huang632721e2012-04-11 16:57:26 -07002328 if (savedState.containsKey(SAVED_CONVERSATION)) {
2329 // Open the conversation.
Andy Huang2bc8bc12012-11-12 17:24:25 -08002330 final Conversation conversation = savedState.getParcelable(SAVED_CONVERSATION);
Paul Westbrook534e4a22012-04-25 03:46:29 -07002331 if (conversation != null && conversation.position < 0) {
2332 // Set the position to 0 on this conversation, as we don't know where it is
2333 // in the list
2334 conversation.position = 0;
2335 }
Andy Huanged4fdf02012-07-26 17:12:50 -07002336 showConversation(conversation);
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -08002337 }
Mindy Pereira967ede62012-03-22 09:29:09 -07002338
Mindy Pereirad33674992012-06-25 16:26:30 -07002339 if (savedState.containsKey(SAVED_TOAST_BAR_OP)) {
Andy Huang2bc8bc12012-11-12 17:24:25 -08002340 ToastBarOperation op = savedState.getParcelable(SAVED_TOAST_BAR_OP);
Mindy Pereirad33674992012-06-25 16:26:30 -07002341 if (op != null) {
2342 if (op.getType() == ToastBarOperation.UNDO) {
2343 onUndoAvailable(op);
Andrew Sapperstein9d7519d2012-07-16 14:03:53 -07002344 } else if (op.getType() == ToastBarOperation.ERROR) {
2345 onError(mFolder, true);
Mindy Pereirad33674992012-06-25 16:26:30 -07002346 }
2347 }
2348 }
Scott Kennedyb10212e2013-02-22 16:27:00 -08002349 mFolderListFolder = savedState.getParcelable(SAVED_HIERARCHICAL_FOLDER);
Vikram Aggarwal649b9ea2012-08-27 12:15:20 -07002350 final ConversationListFragment convListFragment = getConversationListFragment();
Mindy Pereirad33674992012-06-25 16:26:30 -07002351 if (convListFragment != null) {
Vikram Aggarwal649b9ea2012-08-27 12:15:20 -07002352 convListFragment.getAnimatedAdapter().onRestoreInstanceState(savedState);
Mindy Pereirad33674992012-06-25 16:26:30 -07002353 }
Vikram Aggarwalb8c31712013-01-03 17:03:19 -08002354 /*
Mindy Pereira967ede62012-03-22 09:29:09 -07002355 * Restore the state of selected conversations. This needs to be done after the correct mode
2356 * is set and the action bar is fully initialized. If not, several key pieces of state
2357 * information will be missing, and the split views may not be initialized correctly.
Mindy Pereira967ede62012-03-22 09:29:09 -07002358 */
Andy Huang4556a442012-03-30 16:42:05 -07002359 restoreSelectedConversations(savedState);
Vikram Aggarwalb8c31712013-01-03 17:03:19 -08002360 // Order is important!!!
2361 // The dialog listener needs to happen *after* the selected set is restored.
2362
2363 // If there has been an orientation change, and we need to recreate the listener for the
2364 // confirm dialog fragment (delete/archive/...), then do it here.
2365 if (mDialogAction != -1) {
Jin Cao30c881a2014-04-08 14:28:36 -07002366 makeDialogListener(mDialogAction, mDialogFromSelectedSet,
2367 getUndoCallbackForDestructiveActionsWithAutoAdvance(
2368 mDialogAction, mCurrentConversation));
Vikram Aggarwalb8c31712013-01-03 17:03:19 -08002369 }
Alice Yangebeef1b2013-09-04 06:41:10 +00002370
2371 mInbox = savedState.getParcelable(SAVED_INBOX_KEY);
Scott Kennedyf77806e2013-08-30 11:38:15 -07002372
2373 mConversationListScrollPositions.clear();
2374 mConversationListScrollPositions.putAll(
2375 savedState.getBundle(SAVED_CONVERSATION_LIST_SCROLL_POSITIONS));
Andy Huang632721e2012-04-11 16:57:26 -07002376 }
2377
Vikram Aggarwal649b9ea2012-08-27 12:15:20 -07002378 /**
2379 * Handle an intent to open the app. This method is called only when there is no saved state,
2380 * so we need to set state that wasn't set before. It is correct to change the viewmode here
2381 * since it has not been previously set.
Vikram Aggarwal6aca6892013-06-04 13:53:27 -07002382 *
2383 * This method is called for a subset of the reasons mentioned in
2384 * {@link #onCreate(android.os.Bundle)}. Notably, this is called when launching the app from
2385 * notifications, widgets, and shortcuts.
Vikram Aggarwal715f83d2013-03-05 17:04:56 -08002386 * @param intent intent passed to the activity.
Vikram Aggarwal649b9ea2012-08-27 12:15:20 -07002387 */
Andy Huang632721e2012-04-11 16:57:26 -07002388 private void handleIntent(Intent intent) {
Andy Huange6459422013-04-01 16:32:18 -07002389 LogUtils.d(LOG_TAG, "IN AAC.handleIntent. action=%s", intent.getAction());
Andy Huang632721e2012-04-11 16:57:26 -07002390 if (Intent.ACTION_VIEW.equals(intent.getAction())) {
2391 if (intent.hasExtra(Utils.EXTRA_ACCOUNT)) {
Tony Mantler26a20752014-02-28 16:44:24 -08002392 setAccount(Account.newInstance(intent.getStringExtra(Utils.EXTRA_ACCOUNT)));
Andy Huang632721e2012-04-11 16:57:26 -07002393 }
Andy Huangb9ca9792012-05-18 15:31:49 -07002394 if (mAccount == null) {
2395 return;
Andy Huang632721e2012-04-11 16:57:26 -07002396 }
Vikram Aggarwal1a249e02012-08-03 16:19:33 -07002397 final boolean isConversationMode = intent.hasExtra(Utils.EXTRA_CONVERSATION);
Andy Huang4fe0af82013-08-20 17:24:51 -07002398
2399 if (intent.getBooleanExtra(Utils.EXTRA_FROM_NOTIFICATION, false)) {
Régis Décamps2168cbc2014-08-22 15:00:37 +02002400 Analytics.getInstance().setEmail(mAccount.getEmailAddress(), mAccount.getType());
Andy Huang4fe0af82013-08-20 17:24:51 -07002401 Analytics.getInstance().sendEvent("notification_click",
2402 isConversationMode ? "conversation" : "conversation_list", null, 0);
2403 }
2404
Vikram Aggarwal649b9ea2012-08-27 12:15:20 -07002405 if (isConversationMode && mViewMode.getMode() == ViewMode.UNKNOWN) {
Vikram Aggarwal1a249e02012-08-03 16:19:33 -07002406 mViewMode.enterConversationMode();
2407 } else {
2408 mViewMode.enterConversationListMode();
2409 }
Vikram Aggarwal177097f2013-03-08 11:19:53 -08002410 // Put the folder and conversation, and ask the loader to create this folder.
2411 final Bundle args = new Bundle();
Scott Kennedy48cfe462013-04-10 11:32:02 -07002412
2413 final Uri folderUri;
2414 if (intent.hasExtra(Utils.EXTRA_FOLDER_URI)) {
Andrew Sapperstein5bb4d052014-03-31 16:22:31 -07002415 folderUri = intent.getParcelableExtra(Utils.EXTRA_FOLDER_URI);
Scott Kennedy48cfe462013-04-10 11:32:02 -07002416 } else if (intent.hasExtra(Utils.EXTRA_FOLDER)) {
2417 final Folder folder =
2418 Folder.fromString(intent.getStringExtra(Utils.EXTRA_FOLDER));
Scott Kennedy259df5b2013-07-11 13:24:01 -07002419 folderUri = folder.folderUri.fullUri;
Scott Kennedy48cfe462013-04-10 11:32:02 -07002420 } else {
Scott Kennedy72727ef2013-05-01 18:10:55 -07002421 final Bundle extras = intent.getExtras();
2422 LogUtils.d(LOG_TAG, "Couldn't find a folder URI in the extras: %s",
2423 extras == null ? "null" : extras.toString());
Scott Kennedy48cfe462013-04-10 11:32:02 -07002424 folderUri = mAccount.settings.defaultInbox;
2425 }
2426
Andrew Sapperstein5bb4d052014-03-31 16:22:31 -07002427 // Check if we should load all conversations instead of using
2428 // the default behavior which loads an initial subset.
2429 mIgnoreInitialConversationLimit =
2430 intent.getBooleanExtra(Utils.EXTRA_IGNORE_INITIAL_CONVERSATION_LIMIT, false);
2431
Scott Kennedy60593352013-03-13 13:45:30 -07002432 args.putParcelable(Utils.EXTRA_FOLDER_URI, folderUri);
Vikram Aggarwal177097f2013-03-08 11:19:53 -08002433 args.putParcelable(Utils.EXTRA_CONVERSATION,
2434 intent.getParcelableExtra(Utils.EXTRA_CONVERSATION));
2435 restartOptionalLoader(LOADER_FIRST_FOLDER, mFolderCallbacks, args);
Andy Huang632721e2012-04-11 16:57:26 -07002436 } else if (Intent.ACTION_SEARCH.equals(intent.getAction())) {
2437 if (intent.hasExtra(Utils.EXTRA_ACCOUNT)) {
Vikram Aggarwalf0ef2302012-11-07 14:53:34 -08002438 mHaveSearchResults = false;
Jin Caoc6801eb2014-08-12 18:16:57 -07002439 // Save this search query for future suggestions
Andy Huang632721e2012-04-11 16:57:26 -07002440 final String query = intent.getStringExtra(SearchManager.QUERY);
Jin Caoc6801eb2014-08-12 18:16:57 -07002441 mSearchViewController.saveRecentQuery(query);
Vikram Aggarwalf0ef2302012-11-07 14:53:34 -08002442 setAccount((Account) intent.getParcelableExtra(Utils.EXTRA_ACCOUNT));
2443 fetchSearchFolder(intent);
2444 if (shouldEnterSearchConvMode()) {
Mindy Pereiraac254822012-06-18 10:46:43 -07002445 mViewMode.enterSearchResultsConversationMode();
2446 } else {
2447 mViewMode.enterSearchResultsListMode();
2448 }
Andy Huang632721e2012-04-11 16:57:26 -07002449 } else {
2450 LogUtils.e(LOG_TAG, "Missing account extra from search intent. Finishing");
2451 mActivity.finish();
2452 }
2453 }
2454 if (mAccount != null) {
Vikram Aggarwal177097f2013-03-08 11:19:53 -08002455 restartOptionalLoader(LOADER_ACCOUNT_UPDATE_CURSOR, mAccountCallbacks, Bundle.EMPTY);
Scott Kennedyb39aaf52013-03-06 19:17:22 -08002456 }
2457 }
2458
Andy Huang4556a442012-03-30 16:42:05 -07002459 /**
Vikram Aggarwalf0ef2302012-11-07 14:53:34 -08002460 * Returns true if we should enter conversation mode with search.
2461 */
2462 protected final boolean shouldEnterSearchConvMode() {
2463 return mHaveSearchResults && Utils.showTwoPaneSearchResults(mActivity.getActivityContext());
2464 }
2465
2466 /**
Andy Huang4556a442012-03-30 16:42:05 -07002467 * Copy any selected conversations stored in the saved bundle into our selection set,
2468 * triggering {@link ConversationSetObserver} callbacks as our selection set changes.
2469 *
2470 */
Vikram Aggarwal715f83d2013-03-05 17:04:56 -08002471 private void restoreSelectedConversations(Bundle savedState) {
Mindy Pereira967ede62012-03-22 09:29:09 -07002472 if (savedState == null) {
Jin Caoec0fa482014-08-28 16:38:08 -07002473 mCheckedSet.clear();
Mindy Pereira967ede62012-03-22 09:29:09 -07002474 return;
2475 }
Jin Caoec0fa482014-08-28 16:38:08 -07002476 final ConversationCheckedSet selectedSet = savedState.getParcelable(SAVED_SELECTED_SET);
Andy Huang4556a442012-03-30 16:42:05 -07002477 if (selectedSet == null || selectedSet.isEmpty()) {
Jin Caoec0fa482014-08-28 16:38:08 -07002478 mCheckedSet.clear();
Mindy Pereira967ede62012-03-22 09:29:09 -07002479 return;
2480 }
Andy Huang632721e2012-04-11 16:57:26 -07002481
2482 // putAll will take care of calling our registered onSetPopulated method
Jin Caoec0fa482014-08-28 16:38:08 -07002483 mCheckedSet.putAll(selectedSet);
Mindy Pereira967ede62012-03-22 09:29:09 -07002484 }
2485
Vikram Aggarwalec5cbf72012-03-08 15:10:35 -08002486 /**
Vikram Aggarwal49e0e992012-09-21 13:53:15 -07002487 * Show the conversation provided in the arguments. It is safe to pass a null conversation
2488 * object, which is a signal to back out of conversation view mode.
2489 * Child classes must call super.showConversation() <b>before</b> their own implementations.
Vikram Aggarwal59f741f2013-03-01 15:55:40 -08002490 * @param conversation the conversation to be shown, or null if we want to back out to list
2491 * mode.
Vikram Aggarwal177097f2013-03-08 11:19:53 -08002492 * onLoadFinished(Loader, Cursor) on any callback.
Vikram Aggarwalec5cbf72012-03-08 15:10:35 -08002493 */
Jin Cao30c881a2014-04-08 14:28:36 -07002494 protected void showConversation(Conversation conversation) {
Jin Caof8471262014-08-14 18:51:52 -07002495 showConversation(conversation, false /* peek */);
Jin Caod23f6d12014-08-08 15:23:20 -07002496 }
2497
Jin Caof8471262014-08-14 18:51:52 -07002498 protected void showConversation(Conversation conversation, boolean peek) {
Andy Huang243c2362013-03-01 17:50:35 -08002499 if (conversation != null) {
2500 Utils.sConvLoadTimer.start();
2501 }
2502
Andy Huang54e925e2013-03-14 13:24:18 -07002503 MailLogService.log("AbstractActivityController", "showConversation(%s)", conversation);
Vikram Aggarwalc67182d2012-04-03 14:35:06 -07002504 // Set the current conversation just in case it wasn't already set.
2505 setCurrentConversation(conversation);
Vikram Aggarwalec5cbf72012-03-08 15:10:35 -08002506 }
2507
Vikram Aggarwale128fc22012-04-04 12:33:34 -07002508 /**
Jin Cao405a3442014-08-25 13:49:33 -07002509 * Show the wait for account initialization mode.
Paul Westbrook2d50bcd2012-04-10 11:53:47 -07002510 * Children can override this method, but they must call super.showWaitForInitialization().
Paul Westbrook2d50bcd2012-04-10 11:53:47 -07002511 */
Jin Cao405a3442014-08-25 13:49:33 -07002512 protected void showWaitForInitialization() {
Paul Westbrook2d50bcd2012-04-10 11:53:47 -07002513 mViewMode.enterWaitingForInitializationMode();
Andy Huangc96efcc2014-04-09 15:30:42 -07002514 mWaitFragment = WaitFragment.newInstance(mAccount, true /* expectingMessages */);
Paul Westbrook2d50bcd2012-04-10 11:53:47 -07002515 }
2516
Vikram Aggarwaldd6a7ce2012-10-22 15:45:57 -07002517 private void updateWaitMode() {
Paul Westbrook2d50bcd2012-04-10 11:53:47 -07002518 final FragmentManager manager = mActivity.getFragmentManager();
2519 final WaitFragment waitFragment =
Vikram Aggarwal6902dcf2012-04-11 08:57:42 -07002520 (WaitFragment)manager.findFragmentByTag(TAG_WAIT);
Paul Westbrook2d50bcd2012-04-10 11:53:47 -07002521 if (waitFragment != null) {
2522 waitFragment.updateAccount(mAccount);
2523 }
2524 }
2525
Vikram Aggarwala3f43d42012-10-25 16:21:30 -07002526 /**
2527 * Remove the "Waiting for Initialization" fragment. Child classes are free to override this
2528 * method, though they must call the parent implementation <b>after</b> they do anything.
2529 */
2530 protected void hideWaitForInitialization() {
2531 mWaitFragment = null;
2532 }
2533
2534 /**
2535 * Use the instance variable and the wait fragment's tag to get the wait fragment. This is
2536 * far superior to using the value of mWaitFragment, which might be invalid or might refer
2537 * to a fragment after it has been destroyed.
Vikram Aggarwal715f83d2013-03-05 17:04:56 -08002538 * @return a wait fragment that is already attached to the activity, if one exists
Vikram Aggarwala3f43d42012-10-25 16:21:30 -07002539 */
2540 protected final WaitFragment getWaitFragment() {
2541 final FragmentManager manager = mActivity.getFragmentManager();
2542 final WaitFragment waitFrag = (WaitFragment) manager.findFragmentByTag(TAG_WAIT);
2543 if (waitFrag != null) {
2544 // The Fragment Manager knows better, so use its instance.
2545 mWaitFragment = waitFrag;
2546 }
2547 return mWaitFragment;
2548 }
Vikram Aggarwaldd6a7ce2012-10-22 15:45:57 -07002549
Vikram Aggarwal48b2a6c2012-05-29 14:09:27 -07002550 /**
2551 * Returns true if we are waiting for the account to sync, and cannot show any folders or
2552 * conversation for the current account yet.
Vikram Aggarwal48b2a6c2012-05-29 14:09:27 -07002553 */
Vikram Aggarwaldd6a7ce2012-10-22 15:45:57 -07002554 private boolean inWaitMode() {
Vikram Aggarwala3f43d42012-10-25 16:21:30 -07002555 final WaitFragment waitFragment = getWaitFragment();
Paul Westbrook2d50bcd2012-04-10 11:53:47 -07002556 if (waitFragment != null) {
2557 final Account fragmentAccount = waitFragment.getAccount();
Paul Westbrook339004b2012-11-05 17:13:51 -08002558 return fragmentAccount != null && fragmentAccount.uri.equals(mAccount.uri) &&
Paul Westbrook2d50bcd2012-04-10 11:53:47 -07002559 mViewMode.getMode() == ViewMode.WAITING_FOR_ACCOUNT_INITIALIZATION;
2560 }
2561 return false;
2562 }
2563
2564 /**
Jin Cao405a3442014-08-25 13:49:33 -07002565 * Show the conversation List with the list context provided here. On certain layouts, this
2566 * might show more than just the conversation list. For instance, on tablets this might show
2567 * the conversations along with the conversation list.
2568 * @param listContext context providing information on what conversation list to display.
Vikram Aggarwale128fc22012-04-04 12:33:34 -07002569 */
Jin Cao405a3442014-08-25 13:49:33 -07002570 protected abstract void showConversationList(ConversationListContext listContext);
Vikram Aggarwale128fc22012-04-04 12:33:34 -07002571
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -08002572 @Override
Jin Cao0b693382014-08-11 10:46:12 -07002573 public void onConversationSelected(Conversation conversation, boolean inLoaderCallbacks) {
Alice Yangfe8e0812013-04-22 13:37:26 -07002574 final ConversationListFragment convListFragment = getConversationListFragment();
2575 if (convListFragment != null && convListFragment.getAnimatedAdapter() != null) {
2576 convListFragment.getAnimatedAdapter().onConversationSelected();
2577 }
mindypaa55bc92012-08-24 09:49:56 -07002578 // Only animate destructive actions if we are going to be showing the
2579 // conversation list when we show the next conversation.
Vikram Aggarwalbcb16b92013-01-28 18:05:03 -08002580 commitDestructiveActions(mIsTablet);
Jin Cao30c881a2014-04-08 14:28:36 -07002581 showConversation(conversation);
Andy Huang1ee96b22012-08-24 20:19:53 -07002582 }
2583
2584 @Override
Andrew Sapperstein2f542872013-06-11 10:48:30 -07002585 public final void onCabModeEntered() {
2586 final ConversationListFragment convListFragment = getConversationListFragment();
2587 if (convListFragment != null && convListFragment.getAnimatedAdapter() != null) {
2588 convListFragment.getAnimatedAdapter().onCabModeEntered();
2589 }
2590 }
2591
2592 @Override
Scott Kennedycc139832013-08-19 18:03:54 -07002593 public final void onCabModeExited() {
2594 final ConversationListFragment convListFragment = getConversationListFragment();
2595 if (convListFragment != null && convListFragment.getAnimatedAdapter() != null) {
2596 convListFragment.getAnimatedAdapter().onCabModeExited();
2597 }
2598 }
2599
2600 @Override
Andy Huang1ee96b22012-08-24 20:19:53 -07002601 public Conversation getCurrentConversation() {
2602 return mCurrentConversation;
Vikram Aggarwal7d602882012-02-07 15:01:20 -08002603 }
Mindy Pereira555140c2012-02-15 14:55:29 -08002604
Vikram Aggarwalc67182d2012-04-03 14:35:06 -07002605 /**
2606 * Set the current conversation. This is the conversation on which all actions are performed.
2607 * Do not modify mCurrentConversation except through this method, which makes it easy to
2608 * perform common actions associated with changing the current conversation.
Vikram Aggarwal715f83d2013-03-05 17:04:56 -08002609 * @param conversation new conversation to view. Passing null indicates that we are backing
2610 * out to conversation list mode.
Vikram Aggarwalc67182d2012-04-03 14:35:06 -07002611 */
Andy Huang632721e2012-04-11 16:57:26 -07002612 @Override
2613 public void setCurrentConversation(Conversation conversation) {
Vikram Aggarwala91d00b2013-01-18 12:00:37 -08002614 // The controller should come out of detached mode if a new conversation is viewed, or if
Vikram Aggarwal81230ee2013-02-26 15:26:44 -08002615 // we are going back to conversation list mode.
2616 if (mDetachedConvUri != null && (conversation == null
2617 || !mDetachedConvUri.equals(conversation.uri))) {
Vikram Aggarwala91d00b2013-01-18 12:00:37 -08002618 clearDetachedMode();
2619 }
2620
2621 // Must happen *before* setting mCurrentConversation because this sets
2622 // conversation.position if a cursor is available.
Andy Huang8883f222012-11-12 19:25:00 -08002623 mTracker.initialize(conversation);
Vikram Aggarwal17f373e2012-09-17 15:49:32 -07002624 mCurrentConversation = conversation;
Andy Huang7d646122012-09-05 19:41:44 -07002625
2626 if (mCurrentConversation != null) {
Andrew Sapperstein2d86d112014-07-24 20:27:37 -07002627 mActionBarController.setCurrentConversation(mCurrentConversation);
Yorke Leef807ba72012-09-20 17:18:05 -07002628 mActivity.invalidateOptionsMenu();
Andy Huang7d646122012-09-05 19:41:44 -07002629 }
Mindy Pereira5040f1a2012-03-20 10:14:06 -07002630 }
2631
Vikram Aggarwala9b93f32012-02-23 14:51:58 -08002632 /**
Andy Huangf9a73482012-03-13 15:54:02 -07002633 * {@link LoaderManager} currently has a bug in
2634 * {@link LoaderManager#restartLoader(int, Bundle, android.app.LoaderManager.LoaderCallbacks)}
2635 * where, if a previous onCreateLoader returned a null loader, this method will NPE. Work around
2636 * this bug by destroying any loaders that may have been created as null (essentially because
2637 * they are optional loads, and may not apply to a particular account).
2638 * <p>
2639 * A simple null check before restarting a loader will not work, because that would not
2640 * give the controller a chance to invalidate UI corresponding the prior loader result.
2641 *
2642 * @param id loader ID to safely restart
Vikram Aggarwal177097f2013-03-08 11:19:53 -08002643 * @param handler the LoaderCallback which will handle this loader ID.
2644 * @param args arguments, if any, to be passed to the loader. Use {@link Bundle#EMPTY} if no
2645 * arguments need to be specified.
Andy Huangf9a73482012-03-13 15:54:02 -07002646 */
Vikram Aggarwal177097f2013-03-08 11:19:53 -08002647 private void restartOptionalLoader(int id, LoaderManager.LoaderCallbacks handler, Bundle args) {
Andy Huangf9a73482012-03-13 15:54:02 -07002648 final LoaderManager lm = mActivity.getLoaderManager();
2649 lm.destroyLoader(id);
Vikram Aggarwal177097f2013-03-08 11:19:53 -08002650 lm.restartLoader(id, args, handler);
Vikram Aggarwal94c94de2012-04-04 15:38:28 -07002651 }
2652
Andy Huang632721e2012-04-11 16:57:26 -07002653 @Override
2654 public void registerConversationListObserver(DataSetObserver observer) {
2655 mConversationListObservable.registerObserver(observer);
2656 }
2657
2658 @Override
2659 public void unregisterConversationListObserver(DataSetObserver observer) {
Alice Yang647aefb2013-03-04 19:13:11 -08002660 try {
2661 mConversationListObservable.unregisterObserver(observer);
2662 } catch (IllegalStateException e) {
2663 // Log instead of crash
2664 LogUtils.e(LOG_TAG, e, "unregisterConversationListObserver called for an observer that "
2665 + "hasn't been registered");
2666 }
Andy Huang632721e2012-04-11 16:57:26 -07002667 }
2668
Andy Huang090db1e2012-07-25 13:25:28 -07002669 @Override
2670 public void registerFolderObserver(DataSetObserver observer) {
2671 mFolderObservable.registerObserver(observer);
2672 }
2673
2674 @Override
2675 public void unregisterFolderObserver(DataSetObserver observer) {
Alice Yang647aefb2013-03-04 19:13:11 -08002676 try {
2677 mFolderObservable.unregisterObserver(observer);
2678 } catch (IllegalStateException e) {
2679 // Log instead of crash
2680 LogUtils.e(LOG_TAG, e, "unregisterFolderObserver called for an observer that "
2681 + "hasn't been registered");
2682 }
Andy Huang090db1e2012-07-25 13:25:28 -07002683 }
2684
Andy Huang9d3fd922012-09-26 22:23:58 -07002685 @Override
2686 public void registerConversationLoadedObserver(DataSetObserver observer) {
2687 mPagerController.registerConversationLoadedObserver(observer);
2688 }
2689
2690 @Override
2691 public void unregisterConversationLoadedObserver(DataSetObserver observer) {
Alice Yang647aefb2013-03-04 19:13:11 -08002692 try {
2693 mPagerController.unregisterConversationLoadedObserver(observer);
2694 } catch (IllegalStateException e) {
2695 // Log instead of crash
2696 LogUtils.e(LOG_TAG, e, "unregisterConversationLoadedObserver called for an observer "
2697 + "that hasn't been registered");
2698 }
Andy Huang9d3fd922012-09-26 22:23:58 -07002699 }
2700
Vikram Aggarwal60069912012-07-24 14:26:09 -07002701 /**
Vikram Aggarwalc04cf7e2013-05-13 15:38:42 -07002702 * Returns true if the number of accounts is different, or if the current account has
2703 * changed. This method is meant to filter frequent changes to the list of
2704 * accounts, and only return true if the new list is substantially different from the existing
2705 * list. Returning true is safe here, it leads to more work in creating the
2706 * same account list again.
Vikram Aggarwal715f83d2013-03-05 17:04:56 -08002707 * @param accountCursor the cursor which points to all the accounts.
2708 * @return true if the number of accounts is changed or current account missing from the list.
Vikram Aggarwal60069912012-07-24 14:26:09 -07002709 */
Vikram Aggarwal177097f2013-03-08 11:19:53 -08002710 private boolean accountsUpdated(ObjectCursor<Account> accountCursor) {
Paul Westbrook23b74b92012-02-29 11:36:12 -08002711 // Check to see if the current account hasn't been set, or the account cursor is empty
2712 if (mAccount == null || !accountCursor.moveToFirst()) {
Vikram Aggarwal6c511582012-02-27 10:59:47 -08002713 return true;
Paul Westbrook23b74b92012-02-29 11:36:12 -08002714 }
2715
2716 // Check to see if the number of accounts are different, from the number we saw on the last
2717 // updated
2718 if (mCurrentAccountUris.size() != accountCursor.getCount()) {
2719 return true;
2720 }
2721
2722 // Check to see if the account list is different or if the current account is not found in
2723 // the cursor.
2724 boolean foundCurrentAccount = false;
Vikram Aggarwal7dedb952012-02-16 16:10:23 -08002725 do {
Vikram Aggarwalc04cf7e2013-05-13 15:38:42 -07002726 final Account account = accountCursor.getModel();
2727 if (!foundCurrentAccount && mAccount.uri.equals(account.uri)) {
2728 if (mAccount.settingsDiffer(account)) {
2729 // Settings changed, and we don't need to look any further.
2730 return true;
2731 }
Paul Westbrook23b74b92012-02-29 11:36:12 -08002732 foundCurrentAccount = true;
2733 }
Vikram Aggarwal60069912012-07-24 14:26:09 -07002734 // Is there a new account that we do not know about?
Vikram Aggarwalc04cf7e2013-05-13 15:38:42 -07002735 if (!mCurrentAccountUris.contains(account.uri)) {
Paul Westbrook23b74b92012-02-29 11:36:12 -08002736 return true;
2737 }
Vikram Aggarwal7dedb952012-02-16 16:10:23 -08002738 } while (accountCursor.moveToNext());
Paul Westbrook23b74b92012-02-29 11:36:12 -08002739
2740 // As long as we found the current account, the list hasn't been updated
2741 return !foundCurrentAccount;
Vikram Aggarwal7dedb952012-02-16 16:10:23 -08002742 }
2743
2744 /**
Vikram Aggarwal60069912012-07-24 14:26:09 -07002745 * Updates accounts for the app. If the current account is missing, the first
2746 * account in the list is set to the current account (we <em>have</em> to choose something).
Mindy Pereira161f50d2012-02-28 15:47:19 -08002747 *
Vikram Aggarwal6c511582012-02-27 10:59:47 -08002748 * @param accounts cursor into the AccountCache
Vikram Aggarwal7dedb952012-02-16 16:10:23 -08002749 * @return true if the update was successful, false otherwise
2750 */
Vikram Aggarwal177097f2013-03-08 11:19:53 -08002751 private boolean updateAccounts(ObjectCursor<Account> accounts) {
Vikram Aggarwal7dedb952012-02-16 16:10:23 -08002752 if (accounts == null || !accounts.moveToFirst()) {
2753 return false;
2754 }
Paul Westbrook23b74b92012-02-29 11:36:12 -08002755
Vikram Aggarwala9b93f32012-02-23 14:51:58 -08002756 final Account[] allAccounts = Account.getAllAccounts(accounts);
Vikram Aggarwal60069912012-07-24 14:26:09 -07002757 // A match for the current account's URI in the list of accounts.
2758 Account currentFromList = null;
Paul Westbrook23b74b92012-02-29 11:36:12 -08002759
Vikram Aggarwal636c1a12012-09-22 16:02:40 -07002760 // Save the uris for the accounts and find the current account in the updated cursor.
Paul Westbrook23b74b92012-02-29 11:36:12 -08002761 mCurrentAccountUris.clear();
Vikram Aggarwal636c1a12012-09-22 16:02:40 -07002762 for (final Account account : allAccounts) {
Vikram Aggarwal60069912012-07-24 14:26:09 -07002763 LogUtils.d(LOG_TAG, "updateAccounts(%s)", account);
Paul Westbrook23b74b92012-02-29 11:36:12 -08002764 mCurrentAccountUris.add(account.uri);
Vikram Aggarwal60069912012-07-24 14:26:09 -07002765 if (mAccount != null && account.uri.equals(mAccount.uri)) {
2766 currentFromList = account;
2767 }
Paul Westbrook23b74b92012-02-29 11:36:12 -08002768 }
2769
Vikram Aggarwal60069912012-07-24 14:26:09 -07002770 // 1. current account is already set and is in allAccounts:
2771 // 1a. It has changed -> load the updated account.
Greg Bullock406ae082014-08-27 17:03:16 +02002772 // 1b. It is unchanged -> no-op
Andy Huang0d647352012-03-21 21:48:16 -07002773 // 2. current account is set and is not in allAccounts -> pick first (acct was deleted?)
Vikram Aggarwal60069912012-07-24 14:26:09 -07002774 // 3. saved preference has an account -> pick that one
Andy Huang0d647352012-03-21 21:48:16 -07002775 // 4. otherwise just pick first
2776
Vikram Aggarwal60069912012-07-24 14:26:09 -07002777 boolean accountChanged = false;
2778 /// Assume case 4, initialize to first account, and see if we can find anything better.
2779 Account newAccount = allAccounts[0];
2780 if (currentFromList != null) {
2781 // Case 1: Current account exists but has changed
2782 if (!currentFromList.equals(mAccount)) {
2783 newAccount = currentFromList;
2784 accountChanged = true;
Andy Huang0d647352012-03-21 21:48:16 -07002785 }
Vikram Aggarwal60069912012-07-24 14:26:09 -07002786 // Case 1b: else, current account is unchanged: nothing to do.
Paul Westbrook23b74b92012-02-29 11:36:12 -08002787 } else {
Vikram Aggarwal60069912012-07-24 14:26:09 -07002788 // Case 2: Current account is not in allAccounts, the account needs to change.
2789 accountChanged = true;
2790 if (mAccount == null) {
2791 // Case 3: Check for last viewed account, and check if it exists in the list.
2792 final String lastAccountUri = MailAppProvider.getInstance().getLastViewedAccount();
2793 if (lastAccountUri != null) {
2794 for (final Account account : allAccounts) {
2795 if (lastAccountUri.equals(account.uri.toString())) {
2796 newAccount = account;
2797 break;
2798 }
Andy Huang0d647352012-03-21 21:48:16 -07002799 }
2800 }
2801 }
Paul Westbrook23b74b92012-02-29 11:36:12 -08002802 }
Vikram Aggarwal60069912012-07-24 14:26:09 -07002803 if (accountChanged) {
Vikram Aggarwal5fd8afd2013-03-13 15:28:47 -07002804 changeAccount(newAccount);
Vikram Aggarwal60069912012-07-24 14:26:09 -07002805 }
Vikram Aggarwal07dbaa62013-03-12 15:21:04 -07002806
Vikram Aggarwal60069912012-07-24 14:26:09 -07002807 // Whether we have updated the current account or not, we need to update the list of
2808 // accounts in the ActionBar.
Rohan Shahb905f0e2013-04-26 09:17:37 -07002809 mAllAccounts = allAccounts;
Vikram Aggarwal07dbaa62013-03-12 15:21:04 -07002810 mAllAccountObservers.notifyChanged();
Vikram Aggarwala9b93f32012-02-23 14:51:58 -08002811 return (allAccounts.length > 0);
Vikram Aggarwal7dedb952012-02-16 16:10:23 -08002812 }
2813
Paul Westbrook6ead20d2012-03-19 14:48:14 -07002814 private void disableNotifications() {
2815 mNewEmailReceiver.activate(mContext, this);
2816 }
2817
2818 private void enableNotifications() {
2819 mNewEmailReceiver.deactivate();
2820 }
2821
2822 private void disableNotificationsOnAccountChange(Account account) {
2823 // If the new mail suppression receiver is activated for a different account, we want to
2824 // activate it for the new account.
2825 if (mNewEmailReceiver.activated() &&
2826 !mNewEmailReceiver.notificationsDisabledForAccount(account)) {
2827 // Deactivate the current receiver, otherwise multiple receivers may be registered.
2828 mNewEmailReceiver.deactivate();
2829 mNewEmailReceiver.activate(mContext, this);
2830 }
2831 }
2832
Vikram Aggarwala9b93f32012-02-23 14:51:58 -08002833 /**
Vikram Aggarwalc7694222012-04-23 13:37:01 -07002834 * Destructive actions on Conversations. This class should only be created by controllers, and
2835 * clients should only require {@link DestructiveAction}s, not specific implementations of the.
2836 * Only the controllers should know what kind of destructive actions are being created.
2837 */
Mindy Pereirade3e74a2012-07-24 09:43:10 -07002838 public class ConversationAction implements DestructiveAction {
Vikram Aggarwalacaa3c02012-04-24 12:45:27 -07002839 /**
2840 * The action to be performed. This is specified as the resource ID of the menu item
2841 * corresponding to this action: R.id.delete, R.id.report_spam, etc.
2842 */
Vikram Aggarwal7f602f72012-04-30 16:04:06 -07002843 private final int mAction;
Vikram Aggarwalbc67bb12012-04-30 14:05:35 -07002844 /** The action will act upon these conversations */
Paul Westbrook77eee622012-07-10 13:41:57 -07002845 private final Collection<Conversation> mTarget;
Vikram Aggarwal7f602f72012-04-30 16:04:06 -07002846 /** Whether this destructive action has already been performed */
2847 private boolean mCompleted;
Vikram Aggarwalc4113952012-05-11 14:14:56 -07002848 /** Whether this is an action on the currently selected set. */
2849 private final boolean mIsSelectedSet;
Vikram Aggarwale8a85322012-04-24 09:01:38 -07002850
Jin Cao30c881a2014-04-08 14:28:36 -07002851 private UndoCallback mCallback;
2852
Mindy Pereirafbe40192012-03-20 10:40:45 -07002853 /**
Vikram Aggarwal715f83d2013-03-05 17:04:56 -08002854 * Create a listener object.
2855 * @param action action is one of four constants: R.id.y_button (archive),
Mindy Pereirafbe40192012-03-20 10:40:45 -07002856 * R.id.delete , R.id.mute, and R.id.report_spam.
Vikram Aggarwalbc67bb12012-04-30 14:05:35 -07002857 * @param target Conversation that we want to apply the action to.
Vikram Aggarwalc4113952012-05-11 14:14:56 -07002858 * @param isBatch whether the conversations are in the currently selected batch set.
2859 */
2860 public ConversationAction(int action, Collection<Conversation> target, boolean isBatch) {
Mindy Pereirafbe40192012-03-20 10:40:45 -07002861 mAction = action;
Paul Westbrook77eee622012-07-10 13:41:57 -07002862 mTarget = ImmutableList.copyOf(target);
Vikram Aggarwalc4113952012-05-11 14:14:56 -07002863 mIsSelectedSet = isBatch;
Mindy Pereirafbe40192012-03-20 10:40:45 -07002864 }
2865
Jin Cao30c881a2014-04-08 14:28:36 -07002866 @Override
2867 public void setUndoCallback(UndoCallback undoCallback) {
2868 mCallback = undoCallback;
2869 }
2870
Vikram Aggarwalacaa3c02012-04-24 12:45:27 -07002871 /**
2872 * The action common to child classes. This performs the action specified in the constructor
2873 * on the conversations given here.
Vikram Aggarwalacaa3c02012-04-24 12:45:27 -07002874 */
Vikram Aggarwalbc67bb12012-04-30 14:05:35 -07002875 @Override
2876 public void performAction() {
Vikram Aggarwal7f602f72012-04-30 16:04:06 -07002877 if (isPerformed()) {
2878 return;
2879 }
Mindy Pereira3cb28b52012-05-24 15:26:39 -07002880 boolean undoEnabled = mAccount.supportsCapability(AccountCapabilities.UNDO);
Vikram Aggarwal7dd054e2012-05-21 14:43:10 -07002881
2882 // Are we destroying the currently shown conversation? Show the next one.
2883 if (LogUtils.isLoggable(LOG_TAG, LogUtils.DEBUG)){
Vikram Aggarwal8742a612012-08-13 10:22:50 -07002884 LogUtils.d(LOG_TAG, "ConversationAction.performAction():"
2885 + "\nmTarget=%s\nCurrent=%s",
Vikram Aggarwal7dd054e2012-05-21 14:43:10 -07002886 Conversation.toString(mTarget), mCurrentConversation);
2887 }
Vikram Aggarwal7dd054e2012-05-21 14:43:10 -07002888
Paul Westbrooke1221d22012-08-19 11:09:07 -07002889 if (mConversationListCursor == null) {
2890 LogUtils.e(LOG_TAG, "null ConversationCursor in ConversationAction.performAction():"
2891 + "\nmTarget=%s\nCurrent=%s",
2892 Conversation.toString(mTarget), mCurrentConversation);
2893 return;
2894 }
2895
Scott Kennedy2b9d80e2013-07-30 23:03:45 -07002896 if (mAction == R.id.archive) {
2897 LogUtils.d(LOG_TAG, "Archiving");
Jin Cao30c881a2014-04-08 14:28:36 -07002898 mConversationListCursor.archive(mTarget, mCallback);
Scott Kennedy2b9d80e2013-07-30 23:03:45 -07002899 } else if (mAction == R.id.delete) {
2900 LogUtils.d(LOG_TAG, "Deleting");
Jin Cao30c881a2014-04-08 14:28:36 -07002901 mConversationListCursor.delete(mTarget, mCallback);
Scott Kennedy2b9d80e2013-07-30 23:03:45 -07002902 if (mFolder.supportsCapability(FolderCapabilities.DELETE_ACTION_FINAL)) {
Paul Westbrookef362542012-08-27 14:53:32 -07002903 undoEnabled = false;
Scott Kennedy2b9d80e2013-07-30 23:03:45 -07002904 }
2905 } else if (mAction == R.id.mute) {
2906 LogUtils.d(LOG_TAG, "Muting");
2907 if (mFolder.supportsCapability(FolderCapabilities.DESTRUCTIVE_MUTE)) {
2908 for (Conversation c : mTarget) {
2909 c.localDeleteOnUpdate = true;
2910 }
2911 }
Jin Cao30c881a2014-04-08 14:28:36 -07002912 mConversationListCursor.mute(mTarget, mCallback);
Scott Kennedy2b9d80e2013-07-30 23:03:45 -07002913 } else if (mAction == R.id.report_spam) {
2914 LogUtils.d(LOG_TAG, "Reporting spam");
Jin Cao30c881a2014-04-08 14:28:36 -07002915 mConversationListCursor.reportSpam(mTarget, mCallback);
Scott Kennedy2b9d80e2013-07-30 23:03:45 -07002916 } else if (mAction == R.id.mark_not_spam) {
2917 LogUtils.d(LOG_TAG, "Marking not spam");
Jin Cao30c881a2014-04-08 14:28:36 -07002918 mConversationListCursor.reportNotSpam(mTarget, mCallback);
Scott Kennedy2b9d80e2013-07-30 23:03:45 -07002919 } else if (mAction == R.id.report_phishing) {
2920 LogUtils.d(LOG_TAG, "Reporting phishing");
Jin Cao30c881a2014-04-08 14:28:36 -07002921 mConversationListCursor.reportPhishing(mTarget, mCallback);
Scott Kennedy2b9d80e2013-07-30 23:03:45 -07002922 } else if (mAction == R.id.remove_star) {
2923 LogUtils.d(LOG_TAG, "Removing star");
2924 // Star removal is destructive in the Starred folder.
2925 mConversationListCursor.updateBoolean(mTarget, ConversationColumns.STARRED,
2926 false);
2927 } else if (mAction == R.id.mark_not_important) {
2928 LogUtils.d(LOG_TAG, "Marking not-important");
2929 // Marking not important is destructive in a mailbox
2930 // containing only important messages
2931 if (mFolder != null && mFolder.isImportantOnly()) {
2932 for (Conversation conv : mTarget) {
2933 conv.localDeleteOnUpdate = true;
2934 }
2935 }
2936 mConversationListCursor.updateInt(mTarget, ConversationColumns.PRIORITY,
2937 UIProvider.ConversationPriority.LOW);
2938 } else if (mAction == R.id.discard_drafts) {
2939 LogUtils.d(LOG_TAG, "Discarding draft messages");
2940 // Discarding draft messages is destructive in a "draft" mailbox
2941 if (mFolder != null && mFolder.isDraft()) {
2942 for (Conversation conv : mTarget) {
2943 conv.localDeleteOnUpdate = true;
2944 }
2945 }
2946 mConversationListCursor.discardDrafts(mTarget);
2947 // We don't support undoing discarding drafts
2948 undoEnabled = false;
Jin Cao512821c2014-05-30 15:54:04 -07002949 } else if (mAction == R.id.discard_outbox) {
2950 LogUtils.d(LOG_TAG, "Discarding failed messages in Outbox");
2951 mConversationListCursor.moveFailedIntoDrafts(mTarget);
2952 undoEnabled = false;
Vikram Aggarwal3d7ca9d2012-05-11 14:40:36 -07002953 }
Jin Cao2cb6c1c2014-09-18 14:18:17 -07002954 if (undoEnabled && mTarget.size() > 0) {
mindypead50392012-08-23 11:03:53 -07002955 mHandler.postDelayed(new Runnable() {
2956 @Override
2957 public void run() {
2958 onUndoAvailable(new ToastBarOperation(mTarget.size(), mAction,
Scott Kennedy6a3d5ce2013-03-15 17:33:14 -07002959 ToastBarOperation.UNDO, mIsSelectedSet, mFolder));
mindypead50392012-08-23 11:03:53 -07002960 }
2961 }, mShowUndoBarDelay);
Vikram Aggarwal3d7ca9d2012-05-11 14:40:36 -07002962 }
Vikram Aggarwalbc67bb12012-04-30 14:05:35 -07002963 refreshConversationList();
Vikram Aggarwalc4113952012-05-11 14:14:56 -07002964 if (mIsSelectedSet) {
Jin Caoec0fa482014-08-28 16:38:08 -07002965 mCheckedSet.clear();
Vikram Aggarwalc4113952012-05-11 14:14:56 -07002966 }
Mindy Pereirafbe40192012-03-20 10:40:45 -07002967 }
Vikram Aggarwal7f602f72012-04-30 16:04:06 -07002968
2969 /**
2970 * Returns true if this action has been performed, false otherwise.
Andy Huang839ada22012-07-20 15:48:40 -07002971 *
Vikram Aggarwal7f602f72012-04-30 16:04:06 -07002972 */
2973 private synchronized boolean isPerformed() {
2974 if (mCompleted) {
2975 return true;
2976 }
2977 mCompleted = true;
2978 return false;
2979 }
Mindy Pereirafbe40192012-03-20 10:40:45 -07002980 }
Mindy Pereirae5f4dc02012-03-21 16:08:53 -07002981
Vikram Aggarwald503df42012-05-11 10:13:35 -07002982 // Called from the FolderSelectionDialog after a user is done selecting folders to assign the
2983 // conversations to.
Mindy Pereirae5f4dc02012-03-21 16:08:53 -07002984 @Override
Mindy Pereira8db7e402012-07-13 10:32:47 -07002985 public final void assignFolder(Collection<FolderOperation> folderOps,
Scott Kennedy6a3d5ce2013-03-15 17:33:14 -07002986 Collection<Conversation> target, boolean batch, boolean showUndo,
2987 final boolean isMoveTo) {
Jin Cao479753f2014-07-31 14:31:01 -07002988 // Actions are destructive only when the current folder can be un-assigned from and
Mindy Pereira8db7e402012-07-13 10:32:47 -07002989 // when the list of folders contains the current folder.
2990 final boolean isDestructive = mFolder
Jin Cao479753f2014-07-31 14:31:01 -07002991 .supportsCapability(FolderCapabilities.ALLOWS_REMOVE_CONVERSATION)
Mindy Pereira8db7e402012-07-13 10:32:47 -07002992 && FolderOperation.isDestructive(folderOps, mFolder);
Vikram Aggarwald503df42012-05-11 10:13:35 -07002993 LogUtils.d(LOG_TAG, "onFolderChangesCommit: isDestructive = %b", isDestructive);
2994 if (isDestructive) {
2995 for (final Conversation c : target) {
2996 c.localDeleteOnUpdate = true;
Mindy Pereira6778f462012-03-23 18:01:55 -07002997 }
Mindy Pereirae5f4dc02012-03-21 16:08:53 -07002998 }
mindypc84759c2012-08-29 09:51:53 -07002999 final DestructiveAction folderChange;
Jin Cao30c881a2014-04-08 14:28:36 -07003000 final UndoCallback undoCallback = isMoveTo ?
3001 getUndoCallbackForDestructiveActionsWithAutoAdvance(R.id.move_to,
3002 mCurrentConversation)
3003 : null;
Vikram Aggarwald503df42012-05-11 10:13:35 -07003004 // Update the UI elements depending no their visibility and availability
Vikram Aggarwal3d7ca9d2012-05-11 14:40:36 -07003005 // TODO(viki): Consolidate this into a single method requestDelete.
Vikram Aggarwald503df42012-05-11 10:13:35 -07003006 if (isDestructive) {
Scott Kennedy6a3d5ce2013-03-15 17:33:14 -07003007 /*
3008 * If this is a MOVE operation, we want the action folder to be the destination folder.
3009 * Otherwise, we want it to be the current folder.
3010 *
3011 * A set of folder operations is a move if there are exactly two operations: an add and
3012 * a remove.
3013 */
3014 final Folder actionFolder;
3015 if (folderOps.size() != 2) {
3016 actionFolder = mFolder;
3017 } else {
3018 Folder addedFolder = null;
3019 boolean hasRemove = false;
3020 for (final FolderOperation folderOperation : folderOps) {
3021 if (folderOperation.mAdd) {
3022 addedFolder = folderOperation.mFolder;
3023 } else {
3024 hasRemove = true;
3025 }
3026 }
3027
3028 if (hasRemove && addedFolder != null) {
3029 actionFolder = addedFolder;
3030 } else {
3031 actionFolder = mFolder;
3032 }
3033 }
3034
mindypc84759c2012-08-29 09:51:53 -07003035 folderChange = getDeferredFolderChange(target, folderOps, isDestructive,
Jin Cao30c881a2014-04-08 14:28:36 -07003036 batch, showUndo, isMoveTo, actionFolder, undoCallback);
Scott Kennedycaaeed32013-06-12 13:39:16 -07003037 delete(0, target, folderChange, batch);
Vikram Aggarwal6902dcf2012-04-11 08:57:42 -07003038 } else {
mindypc84759c2012-08-29 09:51:53 -07003039 folderChange = getFolderChange(target, folderOps, isDestructive,
Jin Cao30c881a2014-04-08 14:28:36 -07003040 batch, showUndo, false /* isMoveTo */, mFolder, undoCallback);
Vikram Aggarwal715f83d2013-03-05 17:04:56 -08003041 requestUpdate(folderChange);
Mindy Pereirae5f4dc02012-03-21 16:08:53 -07003042 }
3043 }
3044
Mindy Pereira967ede62012-03-22 09:29:09 -07003045 @Override
Vikram Aggarwal3d7ca9d2012-05-11 14:40:36 -07003046 public final void onRefreshRequired() {
Andy Huang12a05d22014-08-28 21:36:18 -07003047 if (isAnimating()) {
Andy Huangf21787a2014-05-02 14:22:01 +02003048 final ConversationListFragment f = getConversationListFragment();
3049 LogUtils.w(ConversationCursor.LOG_TAG,
3050 "onRefreshRequired: delay until animating done. cursor=%s adapter=%s",
3051 mConversationListCursor, (f != null) ? f.getAnimatedAdapter() : null);
Mindy Pereira69e88dd2012-08-10 09:30:18 -07003052 return;
3053 }
Marc Blankbf128eb2012-04-18 15:58:45 -07003054 // Refresh the query in the background
Paul Westbrookcff1aea2012-08-10 11:51:00 -07003055 if (mConversationListCursor.isRefreshRequired()) {
3056 mConversationListCursor.refresh();
3057 }
Marc Blankbf128eb2012-04-18 15:58:45 -07003058 }
3059
mindyp5390fca2012-08-22 12:12:25 -07003060 @Override
mindyp6f54e1b2012-10-09 09:54:08 -07003061 public boolean isAnimating() {
Mindy Pereira69e88dd2012-08-10 09:30:18 -07003062 boolean isAnimating = false;
3063 ConversationListFragment convListFragment = getConversationListFragment();
3064 if (convListFragment != null) {
Andy Huang48ccbc52013-06-05 20:30:47 -07003065 isAnimating = convListFragment.isAnimating();
Mindy Pereira69e88dd2012-08-10 09:30:18 -07003066 }
3067 return isAnimating;
3068 }
3069
Marc Blankbf128eb2012-04-18 15:58:45 -07003070 /**
3071 * Called when the {@link ConversationCursor} is changed or has new data in it.
3072 * <p>
3073 * {@inheritDoc}
3074 */
3075 @Override
Vikram Aggarwal3d7ca9d2012-05-11 14:40:36 -07003076 public final void onRefreshReady() {
mindyp5c1d8352012-11-05 10:12:44 -08003077 LogUtils.d(LOG_TAG, "Received refresh ready callback for folder %s",
3078 mFolder != null ? mFolder.id : "-1");
Andy Huangb2ef9c12012-12-18 12:58:41 -06003079
3080 if (mDestroyed) {
3081 LogUtils.i(LOG_TAG, "ignoring onRefreshReady on destroyed AAC");
3082 return;
3083 }
3084
Paul Westbrookcff1aea2012-08-10 11:51:00 -07003085 if (!isAnimating()) {
Marc Blankbf128eb2012-04-18 15:58:45 -07003086 // Swap cursors
3087 mConversationListCursor.sync();
Andy Huangf21787a2014-05-02 14:22:01 +02003088 } else {
3089 // (CLF guaranteed to be non-null due to check in isAnimating)
3090 LogUtils.w(LOG_TAG,
3091 "AAC.onRefreshReady suppressing sync() due to animation. cursor=%s aa=%s",
3092 mConversationListCursor, getConversationListFragment().getAnimatedAdapter());
Marc Blankbf128eb2012-04-18 15:58:45 -07003093 }
Paul Westbrook937c94f2012-08-16 13:01:18 -07003094 mTracker.onCursorUpdated();
Vikram Aggarwalf0ef2302012-11-07 14:53:34 -08003095 perhapsShowFirstSearchResult();
Marc Blankbf128eb2012-04-18 15:58:45 -07003096 }
3097
3098 @Override
Vikram Aggarwal3d7ca9d2012-05-11 14:40:36 -07003099 public final void onDataSetChanged() {
Paul Westbrook9f119c72012-04-24 16:10:59 -07003100 updateConversationListFragment();
Andy Huang632721e2012-04-11 16:57:26 -07003101 mConversationListObservable.notifyChanged();
Jin Caoec0fa482014-08-28 16:38:08 -07003102 mCheckedSet.validateAgainstCursor(mConversationListCursor);
Marc Blankbf128eb2012-04-18 15:58:45 -07003103 }
3104
Vikram Aggarwal3d7ca9d2012-05-11 14:40:36 -07003105 /**
3106 * If the Conversation List Fragment is visible, updates the fragment.
3107 */
Vikram Aggarwal715f83d2013-03-05 17:04:56 -08003108 private void updateConversationListFragment() {
Marc Blankbf128eb2012-04-18 15:58:45 -07003109 final ConversationListFragment convList = getConversationListFragment();
3110 if (convList != null) {
Vikram Aggarwal75daee52012-04-30 11:13:09 -07003111 refreshConversationList();
Vikram Aggarwal6422b8d2013-01-14 10:47:41 -08003112 if (isFragmentVisible(convList)) {
Vikram Aggarwal69b5c302012-09-05 11:11:13 -07003113 informCursorVisiblity(true);
Paul Westbrook9f119c72012-04-24 16:10:59 -07003114 }
Marc Blankbf128eb2012-04-18 15:58:45 -07003115 }
3116 }
3117
3118 /**
3119 * This class handles throttled refresh of the conversation list
3120 */
3121 static class RefreshTimerTask extends TimerTask {
3122 final Handler mHandler;
3123 final AbstractActivityController mController;
3124
3125 RefreshTimerTask(AbstractActivityController controller, Handler handler) {
3126 mHandler = handler;
3127 mController = controller;
3128 }
3129
3130 @Override
3131 public void run() {
3132 mHandler.post(new Runnable() {
3133 @Override
3134 public void run() {
3135 LogUtils.d(LOG_TAG, "Delay done... calling onRefreshRequired");
3136 mController.onRefreshRequired();
3137 }});
3138 }
3139 }
3140
3141 /**
3142 * Cancel the refresh task, if it's running
3143 */
3144 private void cancelRefreshTask () {
3145 if (mConversationListRefreshTask != null) {
3146 mConversationListRefreshTask.cancel();
3147 mConversationListRefreshTask = null;
3148 }
3149 }
3150
3151 @Override
Paul Westbrookcff1aea2012-08-10 11:51:00 -07003152 public void onAnimationEnd(AnimatedAdapter animatedAdapter) {
Andy Huangf21787a2014-05-02 14:22:01 +02003153 if (animatedAdapter != null) {
3154 LogUtils.i(LOG_TAG, "AAC.onAnimationEnd. cursor=%s adapter=%s", mConversationListCursor,
3155 animatedAdapter);
3156 }
Paul Westbrook026139c2012-09-19 22:35:37 -07003157 if (mConversationListCursor == null) {
3158 LogUtils.e(LOG_TAG, "null ConversationCursor in onAnimationEnd");
3159 return;
3160 }
Paul Westbrookcff1aea2012-08-10 11:51:00 -07003161 if (mConversationListCursor.isRefreshReady()) {
Andy Huangc1922a92013-05-13 14:33:05 -07003162 LogUtils.i(ConversationCursor.LOG_TAG, "Stopped animating: try sync");
Paul Westbrookcff1aea2012-08-10 11:51:00 -07003163 onRefreshReady();
Marc Blankbf128eb2012-04-18 15:58:45 -07003164 }
Marc Blankbf128eb2012-04-18 15:58:45 -07003165
Paul Westbrookcff1aea2012-08-10 11:51:00 -07003166 if (mConversationListCursor.isRefreshRequired()) {
Andy Huangc1922a92013-05-13 14:33:05 -07003167 LogUtils.i(ConversationCursor.LOG_TAG, "Stopped animating: refresh");
Paul Westbrookcff1aea2012-08-10 11:51:00 -07003168 mConversationListCursor.refresh();
3169 }
mindyp6f54e1b2012-10-09 09:54:08 -07003170 if (mRecentsDataUpdated) {
3171 mRecentsDataUpdated = false;
3172 mRecentFolderObservers.notifyChanged();
3173 }
Marc Blankbf128eb2012-04-18 15:58:45 -07003174 }
3175
3176 @Override
Mindy Pereira967ede62012-03-22 09:29:09 -07003177 public void onSetEmpty() {
Vikram Aggarwal6cadbfc2012-12-27 09:17:05 -08003178 // There are no selected conversations. Ensure that the listener and its associated actions
3179 // are blanked out.
3180 setListener(null, -1);
Mindy Pereira967ede62012-03-22 09:29:09 -07003181 }
3182
3183 @Override
Jin Caoec0fa482014-08-28 16:38:08 -07003184 public void onSetPopulated(ConversationCheckedSet set) {
Vikram Aggarwal7704d792013-01-11 15:48:24 -08003185 mCabActionMenu = new SelectedConversationsActionMenu(mActivity, set, mFolder);
Vikram Aggarwal5b9ed2b2013-01-28 18:08:37 -08003186 if (mViewMode.isListMode() || (mIsTablet && mViewMode.isConversationMode())) {
Vikram Aggarwal7704d792013-01-11 15:48:24 -08003187 enableCabMode();
Vikram Aggarwal6902dcf2012-04-11 08:57:42 -07003188 }
Mindy Pereira967ede62012-03-22 09:29:09 -07003189 }
3190
Mindy Pereira967ede62012-03-22 09:29:09 -07003191 @Override
Jin Caoec0fa482014-08-28 16:38:08 -07003192 public void onSetChanged(ConversationCheckedSet set) {
Mindy Pereira967ede62012-03-22 09:29:09 -07003193 // Do nothing. We don't care about changes to the set.
3194 }
3195
3196 @Override
Jin Caoec0fa482014-08-28 16:38:08 -07003197 public ConversationCheckedSet getCheckedSet() {
3198 return mCheckedSet;
Mindy Pereira967ede62012-03-22 09:29:09 -07003199 }
3200
Vikram Aggarwale128fc22012-04-04 12:33:34 -07003201 /**
3202 * Disable the Contextual Action Bar (CAB). The selected set is not changed.
3203 */
3204 protected void disableCabMode() {
Mindy Pereira8937bf12012-07-23 14:05:02 -07003205 // Commit any previous destructive actions when entering/ exiting CAB mode.
mindypc6adce32012-08-22 18:46:42 -07003206 commitDestructiveActions(true);
Vikram Aggarwale128fc22012-04-04 12:33:34 -07003207 if (mCabActionMenu != null) {
3208 mCabActionMenu.deactivate();
3209 }
3210 }
3211
3212 /**
3213 * Re-enable the CAB menu if required. The selection set is not changed.
3214 */
3215 protected void enableCabMode() {
Tony Mantler3d6751a2013-08-02 11:30:59 -07003216 if (mCabActionMenu != null &&
3217 !(isDrawerEnabled() && mDrawerContainer.isDrawerOpen(mDrawerPullout))) {
Vikram Aggarwale128fc22012-04-04 12:33:34 -07003218 mCabActionMenu.activate();
3219 }
3220 }
3221
Vikram Aggarwal4eb52712012-06-19 16:24:50 -07003222 /**
Tony Mantler43fab322013-07-26 16:38:35 -07003223 * Re-enable CAB mode only if we have an active selection
3224 */
3225 protected void maybeEnableCabMode() {
Jin Caoec0fa482014-08-28 16:38:08 -07003226 if (!mCheckedSet.isEmpty()) {
Tony Mantlera703d8a2013-07-31 12:00:46 -07003227 if (mCabActionMenu != null) {
3228 mCabActionMenu.activate();
3229 }
Tony Mantler43fab322013-07-26 16:38:35 -07003230 }
3231 }
3232
3233 /**
Vikram Aggarwal4eb52712012-06-19 16:24:50 -07003234 * Unselect conversations and exit CAB mode.
3235 */
3236 protected final void exitCabMode() {
Jin Caoec0fa482014-08-28 16:38:08 -07003237 mCheckedSet.clear();
Vikram Aggarwal4eb52712012-06-19 16:24:50 -07003238 }
3239
Mindy Pereira967ede62012-03-22 09:29:09 -07003240 @Override
Mindy Pereirafd0c2972012-03-27 13:50:39 -07003241 public void startSearch() {
Vikram Aggarwal35f19d72012-04-24 13:24:48 -07003242 if (mAccount == null) {
3243 // We cannot search if there is no account. Drop the request to the floor.
3244 LogUtils.d(LOG_TAG, "AbstractActivityController.startSearch(): null account");
3245 return;
3246 }
James Lemieux3531d7e2014-01-28 11:10:05 -08003247 if (mAccount.supportsSearch()) {
Jin Caoc6801eb2014-08-12 18:16:57 -07003248 mSearchViewController.showSearchActionBar(
3249 MaterialSearchViewController.SEARCH_VIEW_STATE_VISIBLE);
Mindy Pereirafd0c2972012-03-27 13:50:39 -07003250 } else {
3251 Toast.makeText(mActivity.getActivityContext(), mActivity.getActivityContext()
Mindy Pereiraa46c2992012-03-27 14:12:39 -07003252 .getString(R.string.search_unsupported), Toast.LENGTH_SHORT).show();
Mindy Pereirafd0c2972012-03-27 13:50:39 -07003253 }
3254 }
Mindy Pereiraacf60392012-04-06 09:11:00 -07003255
Mindy Pereira0963ef82012-04-10 11:43:01 -07003256 @Override
Mindy Pereira0963ef82012-04-10 11:43:01 -07003257 public void onTouchEvent(MotionEvent event) {
3258 if (event.getAction() == MotionEvent.ACTION_DOWN) {
Andrew Sappersteinc2c9dc12012-07-02 18:17:32 -07003259 if (mToastBar != null && !mToastBar.isEventInToastBar(event)) {
James Lemieux9a110112014-08-07 15:23:13 -07003260 // if the toast bar is still animating, ignore this attempt to hide it
3261 if (mToastBar.isAnimating()) {
3262 return;
3263 }
3264
3265 // if the toast bar has not been seen long enough, ignore this attempt to hide it
3266 if (mToastBar.cannotBeHidden()) {
3267 return;
3268 }
3269
3270 // hide the toast bar
3271 mToastBar.hide(true /* animated */, false /* actionClicked */);
Mindy Pereira0963ef82012-04-10 11:43:01 -07003272 }
3273 }
3274 }
Andy Huangb1c34dc2012-04-17 16:36:19 -07003275
Andy Huang632721e2012-04-11 16:57:26 -07003276 @Override
Scott Kennedy3b965d72013-06-25 14:36:55 -07003277 public void onConversationSeen() {
3278 mPagerController.onConversationSeen();
Andy Huang632721e2012-04-11 16:57:26 -07003279 }
3280
Andy Huang9d3fd922012-09-26 22:23:58 -07003281 @Override
3282 public boolean isInitialConversationLoading() {
3283 return mPagerController.isInitialConversationLoading();
3284 }
3285
Vikram Aggarwal6422b8d2013-01-14 10:47:41 -08003286 /**
3287 * Check if the fragment given here is visible. Checking {@link Fragment#isVisible()} is
3288 * insufficient because that doesn't check if the window is currently in focus or not.
3289 */
Vikram Aggarwal715f83d2013-03-05 17:04:56 -08003290 private boolean isFragmentVisible(Fragment in) {
Vikram Aggarwal6422b8d2013-01-14 10:47:41 -08003291 return in != null && in.isVisible() && mActivity.hasWindowFocus();
3292 }
3293
Vikram Aggarwald70fe492013-06-04 12:52:07 -07003294 /**
3295 * This class handles callbacks that create a {@link ConversationCursor}.
3296 */
Andy Huangb1c34dc2012-04-17 16:36:19 -07003297 private class ConversationListLoaderCallbacks implements
3298 LoaderManager.LoaderCallbacks<ConversationCursor> {
3299
3300 @Override
3301 public Loader<ConversationCursor> onCreateLoader(int id, Bundle args) {
Vikram Aggarwala1b59dc2013-04-30 15:45:49 -07003302 final Account account = args.getParcelable(BUNDLE_ACCOUNT_KEY);
3303 final Folder folder = args.getParcelable(BUNDLE_FOLDER_KEY);
Andrew Sapperstein5bb4d052014-03-31 16:22:31 -07003304 final boolean ignoreInitialConversationLimit =
3305 args.getBoolean(BUNDLE_IGNORE_INITIAL_CONVERSATION_LIMIT_KEY, false);
Vikram Aggarwala1b59dc2013-04-30 15:45:49 -07003306 if (account == null || folder == null) {
3307 return null;
3308 }
Andrew Sapperstein5bb4d052014-03-31 16:22:31 -07003309 return new ConversationCursorLoader(mActivity, account,
Andy Huangf21787a2014-05-02 14:22:01 +02003310 folder.conversationListUri, folder.getTypeDescription(),
3311 ignoreInitialConversationLimit);
Andy Huangb1c34dc2012-04-17 16:36:19 -07003312 }
3313
3314 @Override
3315 public void onLoadFinished(Loader<ConversationCursor> loader, ConversationCursor data) {
Andy Huang144bfe72013-06-11 13:27:52 -07003316 LogUtils.d(LOG_TAG,
3317 "IN AAC.ConversationCursor.onLoadFinished, data=%s loader=%s this=%s",
3318 data, loader, this);
Tony Mantler0c532ef2014-09-24 10:47:53 -07003319 if (isDestroyed()) {
3320 return;
3321 }
Andrew Sappersteina3ce6782013-05-09 15:14:49 -07003322 if (isDrawerEnabled() && mDrawerListener.getDrawerState() != DrawerLayout.STATE_IDLE) {
Vikram Aggarwala1b59dc2013-04-30 15:45:49 -07003323 LogUtils.d(LOG_TAG, "ConversationListLoaderCallbacks.onLoadFinished: ignoring.");
Andrew Sappersteina3ce6782013-05-09 15:14:49 -07003324 mConversationListLoadFinishedIgnored = true;
Vikram Aggarwala1b59dc2013-04-30 15:45:49 -07003325 return;
3326 }
Vikram Aggarwale8a85322012-04-24 09:01:38 -07003327 // Clear our all pending destructive actions before swapping the conversation cursor
3328 destroyPending(null);
Andy Huangb1c34dc2012-04-17 16:36:19 -07003329 mConversationListCursor = data;
Paul Westbrookbf232c32012-04-18 03:17:41 -07003330 mConversationListCursor.addListener(AbstractActivityController.this);
Andy Huang144bfe72013-06-11 13:27:52 -07003331 mDrawIdler.setListener(mConversationListCursor);
Paul Westbrook937c94f2012-08-16 13:01:18 -07003332 mTracker.onCursorUpdated();
Andy Huange3df1ad2012-04-24 17:15:23 -07003333 mConversationListObservable.notifyChanged();
Yu Ping Hu7c909c72013-01-18 11:58:01 -08003334 // Handle actions that were deferred until after the conversation list was loaded.
3335 for (LoadFinishedCallback callback : mConversationListLoadFinishedCallbacks) {
3336 callback.onLoadFinished();
3337 }
3338 mConversationListLoadFinishedCallbacks.clear();
Paul Westbrook9f119c72012-04-24 16:10:59 -07003339
Vikram Aggarwal17f373e2012-09-17 15:49:32 -07003340 final ConversationListFragment convList = getConversationListFragment();
Vikram Aggarwal6422b8d2013-01-14 10:47:41 -08003341 if (isFragmentVisible(convList)) {
Vikram Aggarwal17f373e2012-09-17 15:49:32 -07003342 // The conversation list is already listening to list changes and gets notified
3343 // in the mConversationListObservable.notifyChanged() line above. We only need to
3344 // check and inform the cursor of the change in visibility here.
3345 informCursorVisiblity(true);
Andy Huangb1c34dc2012-04-17 16:36:19 -07003346 }
Vikram Aggarwalf0ef2302012-11-07 14:53:34 -08003347 perhapsShowFirstSearchResult();
Andy Huangb1c34dc2012-04-17 16:36:19 -07003348 }
3349
3350 @Override
3351 public void onLoaderReset(Loader<ConversationCursor> loader) {
Andy Huang144bfe72013-06-11 13:27:52 -07003352 LogUtils.d(LOG_TAG,
3353 "IN AAC.ConversationCursor.onLoaderReset, data=%s loader=%s this=%s",
3354 mConversationListCursor, loader, this);
Paul Westbrook9a70e912012-08-17 15:53:20 -07003355
3356 if (mConversationListCursor != null) {
3357 // Unregister the listener
3358 mConversationListCursor.removeListener(AbstractActivityController.this);
Andy Huang144bfe72013-06-11 13:27:52 -07003359 mDrawIdler.setListener(null);
Paul Westbrook9a70e912012-08-17 15:53:20 -07003360 mConversationListCursor = null;
3361
3362 // Inform anyone who is interested about the change
3363 mTracker.onCursorUpdated();
3364 mConversationListObservable.notifyChanged();
Andy Huangb1c34dc2012-04-17 16:36:19 -07003365 }
Andy Huangb1c34dc2012-04-17 16:36:19 -07003366 }
Paul Westbrookbf232c32012-04-18 03:17:41 -07003367 }
Andy Huangb1c34dc2012-04-17 16:36:19 -07003368
Vikram Aggarwale8a85322012-04-24 09:01:38 -07003369 /**
Vikram Aggarwal177097f2013-03-08 11:19:53 -08003370 * Class to perform {@link LoaderManager.LoaderCallbacks} for creating {@link Folder} objects.
3371 */
3372 private class FolderLoads implements LoaderManager.LoaderCallbacks<ObjectCursor<Folder>> {
3373 @Override
3374 public Loader<ObjectCursor<Folder>> onCreateLoader(int id, Bundle args) {
3375 final String[] everything = UIProvider.FOLDERS_PROJECTION;
3376 switch (id) {
3377 case LOADER_FOLDER_CURSOR:
3378 LogUtils.d(LOG_TAG, "LOADER_FOLDER_CURSOR created");
3379 final ObjectCursorLoader<Folder> loader = new
3380 ObjectCursorLoader<Folder>(
Scott Kennedy259df5b2013-07-11 13:24:01 -07003381 mContext, mFolder.folderUri.fullUri, everything, Folder.FACTORY);
Vikram Aggarwal177097f2013-03-08 11:19:53 -08003382 loader.setUpdateThrottle(mFolderItemUpdateDelayMs);
3383 return loader;
3384 case LOADER_RECENT_FOLDERS:
3385 LogUtils.d(LOG_TAG, "LOADER_RECENT_FOLDERS created");
Yu Ping Huf2632b92013-03-15 13:56:27 -07003386 if (mAccount != null && mAccount.recentFolderListUri != null
3387 && !mAccount.recentFolderListUri.equals(Uri.EMPTY)) {
Vikram Aggarwal177097f2013-03-08 11:19:53 -08003388 return new ObjectCursorLoader<Folder>(mContext,
3389 mAccount.recentFolderListUri, everything, Folder.FACTORY);
3390 }
3391 break;
3392 case LOADER_ACCOUNT_INBOX:
3393 LogUtils.d(LOG_TAG, "LOADER_ACCOUNT_INBOX created");
3394 final Uri defaultInbox = Settings.getDefaultInboxUri(mAccount.settings);
3395 final Uri inboxUri = defaultInbox.equals(Uri.EMPTY) ?
3396 mAccount.folderListUri : defaultInbox;
3397 LogUtils.d(LOG_TAG, "Loading the default inbox: %s", inboxUri);
3398 if (inboxUri != null) {
3399 return new ObjectCursorLoader<Folder>(mContext, inboxUri,
3400 everything, Folder.FACTORY);
3401 }
3402 break;
3403 case LOADER_SEARCH:
3404 LogUtils.d(LOG_TAG, "LOADER_SEARCH created");
3405 return Folder.forSearchResults(mAccount,
3406 args.getString(ConversationListContext.EXTRA_SEARCH_QUERY),
Jin Cao1a864cc2014-05-21 11:16:39 -07003407 // We can just use current time as a unique identifier for this search
3408 Long.toString(SystemClock.uptimeMillis()),
Vikram Aggarwal177097f2013-03-08 11:19:53 -08003409 mActivity.getActivityContext());
3410 case LOADER_FIRST_FOLDER:
3411 LogUtils.d(LOG_TAG, "LOADER_FIRST_FOLDER created");
3412 final Uri folderUri = args.getParcelable(Utils.EXTRA_FOLDER_URI);
3413 mConversationToShow = args.getParcelable(Utils.EXTRA_CONVERSATION);
3414 if (mConversationToShow != null && mConversationToShow.position < 0){
3415 mConversationToShow.position = 0;
3416 }
3417 return new ObjectCursorLoader<Folder>(mContext, folderUri,
3418 everything, Folder.FACTORY);
3419 default:
3420 LogUtils.wtf(LOG_TAG, "FolderLoads.onCreateLoader(%d) for invalid id", id);
3421 return null;
3422 }
3423 return null;
3424 }
3425
3426 @Override
3427 public void onLoadFinished(Loader<ObjectCursor<Folder>> loader, ObjectCursor<Folder> data) {
3428 if (data == null) {
3429 LogUtils.e(LOG_TAG, "Received null cursor from loader id: %d", loader.getId());
3430 }
Tony Mantler0c532ef2014-09-24 10:47:53 -07003431 if (isDestroyed()) {
3432 return;
3433 }
Vikram Aggarwal177097f2013-03-08 11:19:53 -08003434 switch (loader.getId()) {
3435 case LOADER_FOLDER_CURSOR:
3436 if (data != null && data.moveToFirst()) {
3437 final Folder folder = data.getModel();
3438 setHasFolderChanged(folder);
3439 mFolder = folder;
3440 mFolderObservable.notifyChanged();
3441 } else {
3442 LogUtils.d(LOG_TAG, "Unable to get the folder %s",
Tony Mantler26a20752014-02-28 16:44:24 -08003443 mFolder != null ? mFolder.name : "");
Vikram Aggarwal177097f2013-03-08 11:19:53 -08003444 }
3445 break;
3446 case LOADER_RECENT_FOLDERS:
3447 // Few recent folders and we are running on a phone? Populate the default
3448 // recents. The number of default recent folders is at least 2: every provider
3449 // has at least two folders, and the recent folder count never decreases.
3450 // Having a single recent folder is an erroneous case, and we can gracefully
3451 // recover by populating default recents. The default recents will not stomp on
3452 // the existing value: it will be shown in addition to the default folders:
3453 // the max number of recent folders is more than 1+num(defaultRecents).
3454 if (data != null && data.getCount() <= 1 && !mIsTablet) {
3455 final class PopulateDefault extends AsyncTask<Uri, Void, Void> {
3456 @Override
3457 protected Void doInBackground(Uri... uri) {
3458 // Asking for an update on the URI and ignore the result.
3459 final ContentResolver resolver = mContext.getContentResolver();
3460 resolver.update(uri[0], null, null, null);
3461 return null;
3462 }
3463 }
3464 final Uri uri = mAccount.defaultRecentFolderListUri;
3465 LogUtils.v(LOG_TAG, "Default recents at %s", uri);
3466 new PopulateDefault().execute(uri);
3467 break;
3468 }
3469 LogUtils.v(LOG_TAG, "Reading recent folders from the cursor.");
3470 mRecentFolderList.loadFromUiProvider(data);
3471 if (isAnimating()) {
3472 mRecentsDataUpdated = true;
3473 } else {
3474 mRecentFolderObservers.notifyChanged();
3475 }
3476 break;
3477 case LOADER_ACCOUNT_INBOX:
3478 if (data != null && !data.isClosed() && data.moveToFirst()) {
3479 final Folder inbox = data.getModel();
Alice Yangebeef1b2013-09-04 06:41:10 +00003480 onFolderChanged(inbox, false /* force */);
Vikram Aggarwal177097f2013-03-08 11:19:53 -08003481 // Just want to get the inbox, don't care about updates to it
3482 // as this will be tracked by the folder change listener.
3483 mActivity.getLoaderManager().destroyLoader(LOADER_ACCOUNT_INBOX);
3484 } else {
3485 LogUtils.d(LOG_TAG, "Unable to get the account inbox for account %s",
Tony Mantler26a20752014-02-28 16:44:24 -08003486 mAccount != null ? mAccount.getEmailAddress() : "");
Vikram Aggarwal177097f2013-03-08 11:19:53 -08003487 }
3488 break;
3489 case LOADER_SEARCH:
3490 if (data != null && data.getCount() > 0) {
3491 data.moveToFirst();
3492 final Folder search = data.getModel();
3493 updateFolder(search);
3494 mConvListContext = ConversationListContext.forSearchQuery(mAccount, mFolder,
3495 mActivity.getIntent()
3496 .getStringExtra(UIProvider.SearchQueryParameters.QUERY));
3497 showConversationList(mConvListContext);
3498 mActivity.invalidateOptionsMenu();
3499 mHaveSearchResults = search.totalCount > 0;
3500 mActivity.getLoaderManager().destroyLoader(LOADER_SEARCH);
3501 } else {
3502 LogUtils.e(LOG_TAG, "Null/empty cursor returned by LOADER_SEARCH loader");
3503 }
3504 break;
3505 case LOADER_FIRST_FOLDER:
3506 if (data == null || data.getCount() <=0 || !data.moveToFirst()) {
3507 return;
3508 }
3509 final Folder folder = data.getModel();
3510 boolean handled = false;
3511 if (folder != null) {
Alice Yangebeef1b2013-09-04 06:41:10 +00003512 onFolderChanged(folder, false /* force */);
Vikram Aggarwal177097f2013-03-08 11:19:53 -08003513 handled = true;
3514 }
3515 if (mConversationToShow != null) {
3516 // Open the conversation.
3517 showConversation(mConversationToShow);
3518 handled = true;
3519 }
3520 if (!handled) {
3521 // We have an account, but nothing else: load the default inbox.
3522 loadAccountInbox();
3523 }
3524 mConversationToShow = null;
3525 // And don't run this anymore.
3526 mActivity.getLoaderManager().destroyLoader(LOADER_FIRST_FOLDER);
3527 break;
3528 }
3529 }
3530
3531 @Override
3532 public void onLoaderReset(Loader<ObjectCursor<Folder>> loader) {
3533 }
3534 }
3535
3536 /**
3537 * Class to perform {@link LoaderManager.LoaderCallbacks} for creating {@link Account} objects.
3538 */
3539 private class AccountLoads implements LoaderManager.LoaderCallbacks<ObjectCursor<Account>> {
3540 final String[] mProjection = UIProvider.ACCOUNTS_PROJECTION;
3541 final CursorCreator<Account> mFactory = Account.FACTORY;
3542
3543 @Override
3544 public Loader<ObjectCursor<Account>> onCreateLoader(int id, Bundle args) {
3545 switch (id) {
3546 case LOADER_ACCOUNT_CURSOR:
3547 LogUtils.d(LOG_TAG, "LOADER_ACCOUNT_CURSOR created");
3548 return new ObjectCursorLoader<Account>(mContext,
3549 MailAppProvider.getAccountsUri(), mProjection, mFactory);
3550 case LOADER_ACCOUNT_UPDATE_CURSOR:
3551 LogUtils.d(LOG_TAG, "LOADER_ACCOUNT_UPDATE_CURSOR created");
3552 return new ObjectCursorLoader<Account>(mContext, mAccount.uri, mProjection,
3553 mFactory);
3554 default:
3555 LogUtils.wtf(LOG_TAG, "Got an id (%d) that I cannot create!", id);
3556 break;
3557 }
3558 return null;
3559 }
3560
3561 @Override
3562 public void onLoadFinished(Loader<ObjectCursor<Account>> loader,
3563 ObjectCursor<Account> data) {
3564 if (data == null) {
3565 LogUtils.e(LOG_TAG, "Received null cursor from loader id: %d", loader.getId());
3566 }
Tony Mantler0c532ef2014-09-24 10:47:53 -07003567 if (isDestroyed()) {
3568 return;
3569 }
Vikram Aggarwal177097f2013-03-08 11:19:53 -08003570 switch (loader.getId()) {
3571 case LOADER_ACCOUNT_CURSOR:
Vikram Aggarwald70fe492013-06-04 12:52:07 -07003572 // We have received an update on the list of accounts.
Vikram Aggarwal177097f2013-03-08 11:19:53 -08003573 if (data == null) {
3574 // Nothing useful to do if we have no valid data.
3575 break;
3576 }
Andy Huang761522c2013-08-08 13:09:11 -07003577 final long count = data.getCount();
3578 if (count == 0) {
Vikram Aggarwal177097f2013-03-08 11:19:53 -08003579 // If an empty cursor is returned, the MailAppProvider is indicating that
3580 // no accounts have been specified. We want to navigate to the
3581 // "add account" activity that will handle the intent returned by the
3582 // MailAppProvider
3583
3584 // If the MailAppProvider believes that all accounts have been loaded,
3585 // and the account list is still empty, we want to prompt the user to add
3586 // an account.
3587 final Bundle extras = data.getExtras();
3588 final boolean accountsLoaded =
3589 extras.getInt(AccountCursorExtraKeys.ACCOUNTS_LOADED) != 0;
3590
3591 if (accountsLoaded) {
3592 final Intent noAccountIntent = MailAppProvider.getNoAccountIntent
3593 (mContext);
3594 if (noAccountIntent != null) {
3595 mActivity.startActivityForResult(noAccountIntent,
3596 ADD_ACCOUNT_REQUEST_CODE);
3597 }
3598 }
3599 } else {
3600 final boolean accountListUpdated = accountsUpdated(data);
Vikram Aggarwalc04cf7e2013-05-13 15:38:42 -07003601 if (!mHaveAccountList || accountListUpdated) {
3602 mHaveAccountList = updateAccounts(data);
Vikram Aggarwal177097f2013-03-08 11:19:53 -08003603 }
Andy Huang761522c2013-08-08 13:09:11 -07003604 Analytics.getInstance().setCustomDimension(Analytics.CD_INDEX_ACCOUNT_COUNT,
3605 Long.toString(count));
Vikram Aggarwal177097f2013-03-08 11:19:53 -08003606 }
3607 break;
3608 case LOADER_ACCOUNT_UPDATE_CURSOR:
3609 // We have received an update for current account.
Vikram Aggarwal177097f2013-03-08 11:19:53 -08003610 if (data != null && data.moveToFirst()) {
3611 final Account updatedAccount = data.getModel();
Vikram Aggarwald70fe492013-06-04 12:52:07 -07003612 // Make sure that this is an update for the current account
Vikram Aggarwal177097f2013-03-08 11:19:53 -08003613 if (updatedAccount.uri.equals(mAccount.uri)) {
Vikram Aggarwal177097f2013-03-08 11:19:53 -08003614 final Settings previousSettings = mAccount.settings;
3615
3616 // Update the controller's reference to the current account
3617 mAccount = updatedAccount;
3618 LogUtils.d(LOG_TAG, "AbstractActivityController.onLoadFinished(): "
3619 + "mAccount = %s", mAccount.uri);
3620
3621 // Only notify about a settings change if something differs
3622 if (!Objects.equal(mAccount.settings, previousSettings)) {
3623 mAccountObservers.notifyChanged();
3624 }
3625 perhapsEnterWaitMode();
3626 } else {
3627 LogUtils.e(LOG_TAG, "Got update for account: %s with current account:"
3628 + " %s", updatedAccount.uri, mAccount.uri);
3629 // We need to restart the loader, so the correct account information
3630 // will be returned.
3631 restartOptionalLoader(LOADER_ACCOUNT_UPDATE_CURSOR, this, Bundle.EMPTY);
3632 }
3633 }
3634 break;
3635 }
3636 }
3637
3638 @Override
3639 public void onLoaderReset(Loader<ObjectCursor<Account>> loader) {
Vikram Aggarwald70fe492013-06-04 12:52:07 -07003640 // Do nothing. In onLoadFinished() we copy the relevant data from the cursor.
Vikram Aggarwal177097f2013-03-08 11:19:53 -08003641 }
3642 }
3643
3644 /**
Vikram Aggarwalf0ef2302012-11-07 14:53:34 -08003645 * Updates controller state based on search results and shows first conversation if required.
3646 */
Vikram Aggarwal715f83d2013-03-05 17:04:56 -08003647 private void perhapsShowFirstSearchResult() {
mindypd27009a2012-11-27 11:54:18 -08003648 if (mCurrentConversation == null) {
3649 // Shown for search results in two-pane mode only.
3650 mHaveSearchResults = Intent.ACTION_SEARCH.equals(mActivity.getIntent().getAction())
3651 && mConversationListCursor.getCount() > 0;
3652 if (!shouldShowFirstConversation()) {
3653 return;
3654 }
3655 mConversationListCursor.moveToPosition(0);
3656 final Conversation conv = new Conversation(mConversationListCursor);
3657 conv.position = 0;
3658 onConversationSelected(conv, true /* checkSafeToModifyFragments */);
Vikram Aggarwalf0ef2302012-11-07 14:53:34 -08003659 }
Vikram Aggarwalf0ef2302012-11-07 14:53:34 -08003660 }
3661
3662 /**
Vikram Aggarwale8a85322012-04-24 09:01:38 -07003663 * Destroy the pending {@link DestructiveAction} till now and assign the given action as the
3664 * next destructive action..
3665 * @param nextAction the next destructive action to be performed. This can be null.
3666 */
Vikram Aggarwal715f83d2013-03-05 17:04:56 -08003667 private void destroyPending(DestructiveAction nextAction) {
Vikram Aggarwale8a85322012-04-24 09:01:38 -07003668 // If there is a pending action, perform that first.
3669 if (mPendingDestruction != null) {
3670 mPendingDestruction.performAction();
3671 }
3672 mPendingDestruction = nextAction;
3673 }
3674
3675 /**
3676 * Register a destructive action with the controller. This performs the previous destructive
Vikram Aggarwalacaa3c02012-04-24 12:45:27 -07003677 * 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 -07003678 * embellish this method any more.
Vikram Aggarwal715f83d2013-03-05 17:04:56 -08003679 * @param action the action to register.
Vikram Aggarwale8a85322012-04-24 09:01:38 -07003680 */
Vikram Aggarwal715f83d2013-03-05 17:04:56 -08003681 private void registerDestructiveAction(DestructiveAction action) {
Vikram Aggarwale8a85322012-04-24 09:01:38 -07003682 // TODO(viki): This is not a good idea. The best solution is for clients to request a
3683 // destructive action from the controller and for the controller to own the action. This is
3684 // a half-way solution while refactoring DestructiveAction.
3685 destroyPending(action);
Vikram Aggarwale8a85322012-04-24 09:01:38 -07003686 }
3687
Vikram Aggarwal531488e2012-05-29 16:36:52 -07003688 @Override
Jin Cao30c881a2014-04-08 14:28:36 -07003689 public final DestructiveAction getBatchAction(int action, UndoCallback undoCallback) {
Jin Caoec0fa482014-08-28 16:38:08 -07003690 final DestructiveAction da = new ConversationAction(action, mCheckedSet.values(), true);
Jin Cao30c881a2014-04-08 14:28:36 -07003691 da.setUndoCallback(undoCallback);
Vikram Aggarwale8a85322012-04-24 09:01:38 -07003692 registerDestructiveAction(da);
3693 return da;
3694 }
Vikram Aggarwal41e6e712012-04-24 11:22:57 -07003695
Mindy Pereirade3e74a2012-07-24 09:43:10 -07003696 @Override
Jin Cao30c881a2014-04-08 14:28:36 -07003697 public final DestructiveAction getDeferredBatchAction(int action, UndoCallback undoCallback) {
Jin Caoec0fa482014-08-28 16:38:08 -07003698 return getDeferredAction(action, mCheckedSet.values(), true, undoCallback);
mindypf0656a12012-10-01 08:30:57 -07003699 }
3700
3701 /**
3702 * Get a destructive action for a menu action. This is a temporary method,
3703 * to control the profusion of {@link DestructiveAction} classes that are
3704 * created. Please do not copy this paradigm.
3705 * @param action the resource ID of the menu action: R.id.delete, for
3706 * example
3707 * @param target the conversations to act upon.
3708 * @return a {@link DestructiveAction} that performs the specified action.
3709 */
Vikram Aggarwal715f83d2013-03-05 17:04:56 -08003710 private DestructiveAction getDeferredAction(int action, Collection<Conversation> target,
Jin Cao30c881a2014-04-08 14:28:36 -07003711 boolean batch, UndoCallback callback) {
3712 ConversationAction cAction = new ConversationAction(action, target, batch);
3713 cAction.setUndoCallback(callback);
3714 return cAction;
Mindy Pereirade3e74a2012-07-24 09:43:10 -07003715 }
3716
Vikram Aggarwalbc67bb12012-04-30 14:05:35 -07003717 /**
3718 * Class to change the folders that are assigned to a set of conversations. This is destructive
3719 * because the user can remove the current folder from the conversation, in which case it has
3720 * to be animated away from the current folder.
3721 */
Vikram Aggarwal41e6e712012-04-24 11:22:57 -07003722 private class FolderDestruction implements DestructiveAction {
Paul Westbrook77eee622012-07-10 13:41:57 -07003723 private final Collection<Conversation> mTarget;
Mindy Pereira8db7e402012-07-13 10:32:47 -07003724 private final ArrayList<FolderOperation> mFolderOps = new ArrayList<FolderOperation>();
Vikram Aggarwal41e6e712012-04-24 11:22:57 -07003725 private final boolean mIsDestructive;
Vikram Aggarwal7f602f72012-04-30 16:04:06 -07003726 /** Whether this destructive action has already been performed */
3727 private boolean mCompleted;
Martin Hibdone78c40f2013-10-10 18:29:25 -07003728 private final boolean mIsSelectedSet;
3729 private final boolean mShowUndo;
3730 private final int mAction;
Scott Kennedy6a3d5ce2013-03-15 17:33:14 -07003731 private final Folder mActionFolder;
Vikram Aggarwal41e6e712012-04-24 11:22:57 -07003732
Jin Cao30c881a2014-04-08 14:28:36 -07003733 private UndoCallback mUndoCallback;
3734
Vikram Aggarwal41e6e712012-04-24 11:22:57 -07003735 /**
3736 * Create a new folder destruction object to act on the given conversations.
Vikram Aggarwal715f83d2013-03-05 17:04:56 -08003737 * @param target conversations to act upon.
Scott Kennedy6a3d5ce2013-03-15 17:33:14 -07003738 * @param actionFolder the {@link Folder} being acted upon, used for displaying the undo bar
Vikram Aggarwal41e6e712012-04-24 11:22:57 -07003739 */
Vikram Aggarwal7f602f72012-04-30 16:04:06 -07003740 private FolderDestruction(final Collection<Conversation> target,
Mindy Pereira8db7e402012-07-13 10:32:47 -07003741 final Collection<FolderOperation> folders, boolean isDestructive, boolean isBatch,
Scott Kennedy6a3d5ce2013-03-15 17:33:14 -07003742 boolean showUndo, int action, final Folder actionFolder) {
Paul Westbrook77eee622012-07-10 13:41:57 -07003743 mTarget = ImmutableList.copyOf(target);
Mindy Pereira8db7e402012-07-13 10:32:47 -07003744 mFolderOps.addAll(folders);
Vikram Aggarwal41e6e712012-04-24 11:22:57 -07003745 mIsDestructive = isDestructive;
Mindy Pereiraf3a45562012-05-24 16:30:19 -07003746 mIsSelectedSet = isBatch;
Mindy Pereira06642fa2012-07-12 16:23:27 -07003747 mShowUndo = showUndo;
Mindy Pereira01f30502012-08-14 10:30:51 -07003748 mAction = action;
Scott Kennedy6a3d5ce2013-03-15 17:33:14 -07003749 mActionFolder = actionFolder;
Vikram Aggarwal41e6e712012-04-24 11:22:57 -07003750 }
3751
Vikram Aggarwal41e6e712012-04-24 11:22:57 -07003752 @Override
Jin Cao30c881a2014-04-08 14:28:36 -07003753 public void setUndoCallback(UndoCallback undoCallback) {
3754 mUndoCallback = undoCallback;
3755 }
3756
3757 @Override
Vikram Aggarwal41e6e712012-04-24 11:22:57 -07003758 public void performAction() {
Vikram Aggarwal7f602f72012-04-30 16:04:06 -07003759 if (isPerformed()) {
3760 return;
3761 }
Jin Cao2cb6c1c2014-09-18 14:18:17 -07003762 if (mIsDestructive && mShowUndo && mTarget.size() > 0) {
mindypcb0b30e2012-11-30 10:16:35 -08003763 ToastBarOperation undoOp = new ToastBarOperation(mTarget.size(), mAction,
Scott Kennedy6a3d5ce2013-03-15 17:33:14 -07003764 ToastBarOperation.UNDO, mIsSelectedSet, mActionFolder);
Vikram Aggarwal41e6e712012-04-24 11:22:57 -07003765 onUndoAvailable(undoOp);
3766 }
Mindy Pereira8db7e402012-07-13 10:32:47 -07003767 // For each conversation, for each operation, add/ remove the
3768 // appropriate folders.
mindypcb0b30e2012-11-30 10:16:35 -08003769 ArrayList<ConversationOperation> ops = new ArrayList<ConversationOperation>();
3770 ArrayList<Uri> folderUris;
3771 ArrayList<Boolean> adds;
Mindy Pereira8db7e402012-07-13 10:32:47 -07003772 for (Conversation target : mTarget) {
mindypcb0b30e2012-11-30 10:16:35 -08003773 HashMap<Uri, Folder> targetFolders = Folder.hashMapForFolders(target
3774 .getRawFolders());
3775 folderUris = new ArrayList<Uri>();
mindypcb0b30e2012-11-30 10:16:35 -08003776 adds = new ArrayList<Boolean>();
Mindy Pereira01f30502012-08-14 10:30:51 -07003777 if (mIsDestructive) {
3778 target.localDeleteOnUpdate = true;
3779 }
Mindy Pereira8db7e402012-07-13 10:32:47 -07003780 for (FolderOperation op : mFolderOps) {
Scott Kennedy259df5b2013-07-11 13:24:01 -07003781 folderUris.add(op.mFolder.folderUri.fullUri);
mindypcb0b30e2012-11-30 10:16:35 -08003782 adds.add(op.mAdd ? Boolean.TRUE : Boolean.FALSE);
Mindy Pereira8db7e402012-07-13 10:32:47 -07003783 if (op.mAdd) {
Scott Kennedy259df5b2013-07-11 13:24:01 -07003784 targetFolders.put(op.mFolder.folderUri.fullUri, op.mFolder);
Mindy Pereira8db7e402012-07-13 10:32:47 -07003785 } else {
Scott Kennedy259df5b2013-07-11 13:24:01 -07003786 targetFolders.remove(op.mFolder.folderUri.fullUri);
Mindy Pereira8db7e402012-07-13 10:32:47 -07003787 }
3788 }
Paul Westbrook26746eb2012-12-06 14:44:01 -08003789 ops.add(mConversationListCursor.getConversationFolderOperation(target,
Jin Cao30c881a2014-04-08 14:28:36 -07003790 folderUris, adds, targetFolders.values(), mUndoCallback));
mindyp389f0b22012-08-29 11:12:54 -07003791 }
3792 if (mConversationListCursor != null) {
Scott Kennedy9e2d4072013-03-21 21:46:01 -07003793 mConversationListCursor.updateBulkValues(ops);
Mindy Pereira8db7e402012-07-13 10:32:47 -07003794 }
Vikram Aggarwal7f602f72012-04-30 16:04:06 -07003795 refreshConversationList();
Mindy Pereiraf3a45562012-05-24 16:30:19 -07003796 if (mIsSelectedSet) {
Jin Caoec0fa482014-08-28 16:38:08 -07003797 mCheckedSet.clear();
Mindy Pereiraf3a45562012-05-24 16:30:19 -07003798 }
Vikram Aggarwal7f602f72012-04-30 16:04:06 -07003799 }
Mindy Pereira8db7e402012-07-13 10:32:47 -07003800
Vikram Aggarwal7f602f72012-04-30 16:04:06 -07003801 /**
3802 * Returns true if this action has been performed, false otherwise.
Andy Huang839ada22012-07-20 15:48:40 -07003803 *
Vikram Aggarwal7f602f72012-04-30 16:04:06 -07003804 */
3805 private synchronized boolean isPerformed() {
3806 if (mCompleted) {
3807 return true;
Vikram Aggarwal41e6e712012-04-24 11:22:57 -07003808 }
Vikram Aggarwal7f602f72012-04-30 16:04:06 -07003809 mCompleted = true;
3810 return false;
Vikram Aggarwal41e6e712012-04-24 11:22:57 -07003811 }
3812 }
Vikram Aggarwal7f602f72012-04-30 16:04:06 -07003813
mindypc84759c2012-08-29 09:51:53 -07003814 public final DestructiveAction getFolderChange(Collection<Conversation> target,
3815 Collection<FolderOperation> folders, boolean isDestructive, boolean isBatch,
Jin Cao30c881a2014-04-08 14:28:36 -07003816 boolean showUndo, final boolean isMoveTo, final Folder actionFolder,
3817 UndoCallback undoCallback) {
mindypc84759c2012-08-29 09:51:53 -07003818 final DestructiveAction da = getDeferredFolderChange(target, folders, isDestructive,
Jin Cao30c881a2014-04-08 14:28:36 -07003819 isBatch, showUndo, isMoveTo, actionFolder, undoCallback);
mindypc84759c2012-08-29 09:51:53 -07003820 registerDestructiveAction(da);
3821 return da;
3822 }
3823
3824 public final DestructiveAction getDeferredFolderChange(Collection<Conversation> target,
Mindy Pereira8db7e402012-07-13 10:32:47 -07003825 Collection<FolderOperation> folders, boolean isDestructive, boolean isBatch,
Jin Cao30c881a2014-04-08 14:28:36 -07003826 boolean showUndo, final boolean isMoveTo, final Folder actionFolder,
3827 UndoCallback undoCallback) {
3828 final DestructiveAction fd = new FolderDestruction(target, folders, isDestructive, isBatch,
3829 showUndo, isMoveTo ? R.id.move_folder : R.id.change_folders, actionFolder);
3830 fd.setUndoCallback(undoCallback);
3831 return fd;
Mindy Pereira01f30502012-08-14 10:30:51 -07003832 }
3833
3834 @Override
3835 public final DestructiveAction getDeferredRemoveFolder(Collection<Conversation> target,
3836 Folder toRemove, boolean isDestructive, boolean isBatch,
Jin Cao30c881a2014-04-08 14:28:36 -07003837 boolean showUndo, UndoCallback undoCallback) {
Mindy Pereira01f30502012-08-14 10:30:51 -07003838 Collection<FolderOperation> folderOps = new ArrayList<FolderOperation>();
3839 folderOps.add(new FolderOperation(toRemove, false));
Jin Cao30c881a2014-04-08 14:28:36 -07003840 final DestructiveAction da = new FolderDestruction(target, folderOps, isDestructive, isBatch,
Scott Kennedy6a3d5ce2013-03-15 17:33:14 -07003841 showUndo, R.id.remove_folder, mFolder);
Jin Cao30c881a2014-04-08 14:28:36 -07003842 da.setUndoCallback(undoCallback);
3843 return da;
Mindy Pereira01f30502012-08-14 10:30:51 -07003844 }
3845
Vikram Aggarwal4f4782b2012-05-30 08:39:09 -07003846 @Override
3847 public final void refreshConversationList() {
Vikram Aggarwal75daee52012-04-30 11:13:09 -07003848 final ConversationListFragment convList = getConversationListFragment();
3849 if (convList == null) {
3850 return;
3851 }
3852 convList.requestListRefresh();
3853 }
Andrew Sappersteinc2c9dc12012-07-02 18:17:32 -07003854
3855 protected final ActionClickedListener getUndoClickedListener(
3856 final AnimatedAdapter listAdapter) {
3857 return new ActionClickedListener() {
3858 @Override
Andrew Sappersteinf8ccdcf2013-08-08 19:30:38 -07003859 public void onActionClicked(Context context) {
Andrew Sappersteinc2c9dc12012-07-02 18:17:32 -07003860 if (mAccount.undoUri != null) {
3861 // NOTE: We might want undo to return the messages affected, in which case
3862 // the resulting cursor might be interesting...
3863 // TODO: Use UIProvider.SEQUENCE_QUERY_PARAMETER to indicate the set of
3864 // commands to undo
3865 if (mConversationListCursor != null) {
3866 mConversationListCursor.undo(
3867 mActivity.getActivityContext(), mAccount.undoUri);
3868 }
3869 if (listAdapter != null) {
3870 listAdapter.setUndo(true);
3871 }
3872 }
3873 }
3874 };
3875 }
3876
Vikram Aggarwal41b9e8f2012-09-25 10:15:04 -07003877 /**
3878 * Shows an error toast in the bottom when a folder was not fetched successfully.
3879 * @param folder the folder which could not be fetched.
3880 * @param replaceVisibleToast if true, this should replace any currently visible toast.
3881 */
Andrew Sapperstein9d7519d2012-07-16 14:03:53 -07003882 protected final void showErrorToast(final Folder folder, boolean replaceVisibleToast) {
Andrew Sapperstein00179f12012-08-09 15:15:40 -07003883
Vikram Aggarwal41b9e8f2012-09-25 10:15:04 -07003884 final ActionClickedListener listener;
3885 final int actionTextResourceId;
Andrew Sapperstein00179f12012-08-09 15:15:40 -07003886 final int lastSyncResult = folder.lastSyncResult;
Vikram Aggarwal41b9e8f2012-09-25 10:15:04 -07003887 switch (lastSyncResult & 0x0f) {
Andrew Sapperstein00179f12012-08-09 15:15:40 -07003888 case UIProvider.LastSyncResult.CONNECTION_ERROR:
Vikram Aggarwal41b9e8f2012-09-25 10:15:04 -07003889 // The sync request that caused this failure.
3890 final int syncRequest = lastSyncResult >> 4;
3891 // Show: User explicitly pressed the refresh button and there is no connection
3892 // Show: The first time the user enters the app and there is no connection
3893 // TODO(viki): Implement this.
3894 // Reference: http://b/7202801
3895 final boolean showToast = (syncRequest & UIProvider.SyncStatus.USER_REFRESH) != 0;
3896 // Don't show: Already in the app; user switches to a synced label
3897 // Don't show: In a live label and a background sync fails
3898 final boolean avoidToast = !showToast && (folder.syncWindow > 0
3899 || (syncRequest & UIProvider.SyncStatus.BACKGROUND_SYNC) != 0);
3900 if (avoidToast) {
3901 return;
3902 }
Andrew Sapperstein00179f12012-08-09 15:15:40 -07003903 listener = getRetryClickedListener(folder);
3904 actionTextResourceId = R.string.retry;
3905 break;
3906 case UIProvider.LastSyncResult.AUTH_ERROR:
3907 listener = getSignInClickedListener();
3908 actionTextResourceId = R.string.signin;
3909 break;
3910 case UIProvider.LastSyncResult.SECURITY_ERROR:
3911 return; // Currently we do nothing for security errors.
3912 case UIProvider.LastSyncResult.STORAGE_ERROR:
3913 listener = getStorageErrorClickedListener();
3914 actionTextResourceId = R.string.info;
3915 break;
3916 case UIProvider.LastSyncResult.INTERNAL_ERROR:
3917 listener = getInternalErrorClickedListener();
3918 actionTextResourceId = R.string.report;
3919 break;
3920 default:
3921 return;
3922 }
Vikram Aggarwal41b9e8f2012-09-25 10:15:04 -07003923 mToastBar.show(listener,
Vikram Aggarwal41b9e8f2012-09-25 10:15:04 -07003924 Utils.getSyncStatusText(mActivity.getActivityContext(), lastSyncResult),
Andrew Sapperstein00179f12012-08-09 15:15:40 -07003925 actionTextResourceId,
Andrew Sapperstein9d7519d2012-07-16 14:03:53 -07003926 replaceVisibleToast,
James Lemieux0ec03e82014-09-03 14:01:53 -07003927 true /* autohide */,
Scott Kennedy6a3d5ce2013-03-15 17:33:14 -07003928 new ToastBarOperation(1, 0, ToastBarOperation.ERROR, false, folder));
Andrew Sappersteinc2c9dc12012-07-02 18:17:32 -07003929 }
3930
Andrew Sapperstein00179f12012-08-09 15:15:40 -07003931 private ActionClickedListener getRetryClickedListener(final Folder folder) {
Andrew Sappersteinc2c9dc12012-07-02 18:17:32 -07003932 return new ActionClickedListener() {
3933 @Override
Andrew Sappersteinf8ccdcf2013-08-08 19:30:38 -07003934 public void onActionClicked(Context context) {
Andrew Sappersteinc2c9dc12012-07-02 18:17:32 -07003935 final Uri uri = folder.refreshUri;
3936
3937 if (uri != null) {
Paul Westbrook4969e0c2012-08-20 14:38:39 -07003938 startAsyncRefreshTask(uri);
Andrew Sappersteinc2c9dc12012-07-02 18:17:32 -07003939 }
3940 }
3941 };
3942 }
Andrew Sapperstein00179f12012-08-09 15:15:40 -07003943
3944 private ActionClickedListener getSignInClickedListener() {
3945 return new ActionClickedListener() {
3946 @Override
Andrew Sappersteinf8ccdcf2013-08-08 19:30:38 -07003947 public void onActionClicked(Context context) {
Paul Westbrook122f7c22012-08-20 17:50:31 -07003948 promptUserForAuthentication(mAccount);
Andrew Sapperstein00179f12012-08-09 15:15:40 -07003949 }
3950 };
3951 }
3952
3953 private ActionClickedListener getStorageErrorClickedListener() {
3954 return new ActionClickedListener() {
3955 @Override
Andrew Sappersteinf8ccdcf2013-08-08 19:30:38 -07003956 public void onActionClicked(Context context) {
Paul Westbrook4969e0c2012-08-20 14:38:39 -07003957 showStorageErrorDialog();
Andrew Sapperstein00179f12012-08-09 15:15:40 -07003958 }
3959 };
3960 }
3961
Paul Westbrook4969e0c2012-08-20 14:38:39 -07003962 private void showStorageErrorDialog() {
3963 DialogFragment fragment = (DialogFragment)
3964 mFragmentManager.findFragmentByTag(SYNC_ERROR_DIALOG_FRAGMENT_TAG);
3965 if (fragment == null) {
3966 fragment = SyncErrorDialogFragment.newInstance();
3967 }
3968 fragment.show(mFragmentManager, SYNC_ERROR_DIALOG_FRAGMENT_TAG);
3969 }
3970
Andrew Sapperstein00179f12012-08-09 15:15:40 -07003971 private ActionClickedListener getInternalErrorClickedListener() {
3972 return new ActionClickedListener() {
3973 @Override
Andrew Sappersteinf8ccdcf2013-08-08 19:30:38 -07003974 public void onActionClicked(Context context) {
Paul Westbrook83e6b572013-02-05 16:22:42 -08003975 Utils.sendFeedback(mActivity, mAccount, true /* reportingProblem */);
Andrew Sapperstein00179f12012-08-09 15:15:40 -07003976 }
3977 };
3978 }
Paul Westbrook4969e0c2012-08-20 14:38:39 -07003979
3980 @Override
3981 public void onFooterViewErrorActionClick(Folder folder, int errorStatus) {
3982 Uri uri = null;
3983 switch (errorStatus) {
3984 case UIProvider.LastSyncResult.CONNECTION_ERROR:
3985 if (folder != null && folder.refreshUri != null) {
3986 uri = folder.refreshUri;
3987 }
3988 break;
3989 case UIProvider.LastSyncResult.AUTH_ERROR:
Paul Westbrook122f7c22012-08-20 17:50:31 -07003990 promptUserForAuthentication(mAccount);
Paul Westbrook4969e0c2012-08-20 14:38:39 -07003991 return;
3992 case UIProvider.LastSyncResult.SECURITY_ERROR:
3993 return; // Currently we do nothing for security errors.
3994 case UIProvider.LastSyncResult.STORAGE_ERROR:
3995 showStorageErrorDialog();
3996 return;
3997 case UIProvider.LastSyncResult.INTERNAL_ERROR:
Paul Westbrook83e6b572013-02-05 16:22:42 -08003998 Utils.sendFeedback(mActivity, mAccount, true /* reportingProblem */);
Paul Westbrook4969e0c2012-08-20 14:38:39 -07003999 return;
4000 default:
4001 return;
4002 }
4003
4004 if (uri != null) {
4005 startAsyncRefreshTask(uri);
4006 }
4007 }
4008
4009 @Override
4010 public void onFooterViewLoadMoreClick(Folder folder) {
4011 if (folder != null && folder.loadMoreUri != null) {
4012 startAsyncRefreshTask(folder.loadMoreUri);
4013 }
4014 }
4015
4016 private void startAsyncRefreshTask(Uri uri) {
4017 if (mFolderSyncTask != null) {
4018 mFolderSyncTask.cancel(true);
4019 }
4020 mFolderSyncTask = new AsyncRefreshTask(mActivity.getActivityContext(), uri);
4021 mFolderSyncTask.execute();
4022 }
Paul Westbrook122f7c22012-08-20 17:50:31 -07004023
4024 private void promptUserForAuthentication(Account account) {
Paul Westbrook429399b2012-08-24 11:19:17 -07004025 if (account != null && !Utils.isEmpty(account.reauthenticationIntentUri)) {
Paul Westbrook122f7c22012-08-20 17:50:31 -07004026 final Intent authenticationIntent =
4027 new Intent(Intent.ACTION_VIEW, account.reauthenticationIntentUri);
4028 mActivity.startActivityForResult(authenticationIntent, REAUTHENTICATE_REQUEST_CODE);
4029 }
4030 }
mindypca87de42012-09-28 15:02:39 -07004031
4032 @Override
4033 public void onAccessibilityStateChanged() {
4034 // Clear the cache of objects.
4035 ConversationItemViewModel.onAccessibilityUpdated();
4036 // Re-render the list if it exists.
Vikram Aggarwala91d00b2013-01-18 12:00:37 -08004037 final ConversationListFragment frag = getConversationListFragment();
mindypca87de42012-09-28 15:02:39 -07004038 if (frag != null) {
4039 AnimatedAdapter adapter = frag.getAnimatedAdapter();
4040 if (adapter != null) {
4041 adapter.notifyDataSetInvalidated();
4042 }
4043 }
4044 }
Vikram Aggarwal6cadbfc2012-12-27 09:17:05 -08004045
4046 @Override
Jin Cao30c881a2014-04-08 14:28:36 -07004047 public void makeDialogListener (final int action, final boolean isBatch,
4048 UndoCallback undoCallback) {
Vikram Aggarwalb8c31712013-01-03 17:03:19 -08004049 final Collection<Conversation> target;
Vikram Aggarwalb8c31712013-01-03 17:03:19 -08004050 if (isBatch) {
Jin Caoec0fa482014-08-28 16:38:08 -07004051 target = mCheckedSet.values();
Vikram Aggarwalb8c31712013-01-03 17:03:19 -08004052 } else {
4053 LogUtils.d(LOG_TAG, "Will act upon %s", mCurrentConversation);
4054 target = Conversation.listOf(mCurrentConversation);
Vikram Aggarwalb8c31712013-01-03 17:03:19 -08004055 }
Jin Cao30c881a2014-04-08 14:28:36 -07004056 final DestructiveAction destructiveAction = getDeferredAction(action, target, isBatch,
4057 undoCallback);
Vikram Aggarwal6cadbfc2012-12-27 09:17:05 -08004058 mDialogAction = action;
Vikram Aggarwalb8c31712013-01-03 17:03:19 -08004059 mDialogFromSelectedSet = isBatch;
Vikram Aggarwal6cadbfc2012-12-27 09:17:05 -08004060 mDialogListener = new AlertDialog.OnClickListener() {
4061 @Override
4062 public void onClick(DialogInterface dialog, int which) {
Scott Kennedycaaeed32013-06-12 13:39:16 -07004063 delete(action, target, destructiveAction, isBatch);
Vikram Aggarwal6cadbfc2012-12-27 09:17:05 -08004064 // Afterwards, let's remove references to the listener and the action.
4065 setListener(null, -1);
4066 }
4067 };
4068 }
4069
4070 @Override
4071 public AlertDialog.OnClickListener getListener() {
4072 return mDialogListener;
4073 }
4074
4075 /**
4076 * Sets the listener for the positive action on a confirmation dialog. Since only a single
4077 * confirmation dialog can be shown, this overwrites the previous listener. It is safe to
4078 * unset the listener; in which case action should be set to -1.
Vikram Aggarwal715f83d2013-03-05 17:04:56 -08004079 * @param listener the listener that will perform the task for this dialog's positive action.
4080 * @param action the action that created this dialog.
Vikram Aggarwal6cadbfc2012-12-27 09:17:05 -08004081 */
4082 private void setListener(AlertDialog.OnClickListener listener, final int action){
4083 mDialogListener = listener;
4084 mDialogAction = action;
4085 }
Vikram Aggarwal69a6cdf2013-01-08 16:05:17 -08004086
4087 @Override
4088 public VeiledAddressMatcher getVeiledAddressMatcher() {
4089 return mVeiledMatcher;
Vikram Aggarwala91d00b2013-01-18 12:00:37 -08004090 }
4091
4092 @Override
4093 public void setDetachedMode() {
4094 // Tell the conversation list not to select anything.
4095 final ConversationListFragment frag = getConversationListFragment();
4096 if (frag != null) {
4097 frag.setChoiceNone();
Vikram Aggarwal81230ee2013-02-26 15:26:44 -08004098 } else if (mIsTablet) {
4099 // How did we ever land here? Detached mode, and no CLF on tablet???
Vikram Aggarwala91d00b2013-01-18 12:00:37 -08004100 LogUtils.e(LOG_TAG, "AAC.setDetachedMode(): CLF = null!");
4101 }
4102 mDetachedConvUri = mCurrentConversation.uri;
4103 }
4104
4105 private void clearDetachedMode() {
4106 // Tell the conversation list to go back to its usual selection behavior.
4107 final ConversationListFragment frag = getConversationListFragment();
4108 if (frag != null) {
4109 frag.revertChoiceMode();
Vikram Aggarwal81230ee2013-02-26 15:26:44 -08004110 } else if (mIsTablet) {
4111 // How did we ever land here? Detached mode, and no CLF on tablet???
4112 LogUtils.e(LOG_TAG, "AAC.clearDetachedMode(): CLF = null on tablet!");
Vikram Aggarwala91d00b2013-01-18 12:00:37 -08004113 }
4114 mDetachedConvUri = null;
4115 }
Andy Huang12b3ee42013-04-24 22:49:43 -07004116
Andy Huang61f26c22014-03-13 18:24:52 -07004117 @Override
Andy Huang8712f412014-08-21 23:10:41 -07004118 public boolean shouldPreventListSwipesEntirely() {
4119 return false;
4120 }
4121
4122 @Override
Andy Huang61f26c22014-03-13 18:24:52 -07004123 public DrawerController getDrawerController() {
4124 return mDrawerListener;
4125 }
4126
4127 private class MailDrawerListener extends Observable<DrawerLayout.DrawerListener>
4128 implements DrawerLayout.DrawerListener, DrawerController {
Andrew Sappersteina3ce6782013-05-09 15:14:49 -07004129 private int mDrawerState;
Andrew Sappersteinfc5be1c2013-05-15 17:48:10 -07004130 private float mOldSlideOffset;
Andrew Sappersteina3ce6782013-05-09 15:14:49 -07004131
4132 public MailDrawerListener() {
4133 mDrawerState = DrawerLayout.STATE_IDLE;
Andrew Sappersteinfc5be1c2013-05-15 17:48:10 -07004134 mOldSlideOffset = 0.f;
Andrew Sappersteina3ce6782013-05-09 15:14:49 -07004135 }
4136
Andy Huang12b3ee42013-04-24 22:49:43 -07004137 @Override
Andy Huang3e95fe92014-07-25 17:46:33 -07004138 public boolean isDrawerEnabled() {
4139 return AbstractActivityController.this.isDrawerEnabled();
4140 }
4141
4142 @Override
Andy Huang61f26c22014-03-13 18:24:52 -07004143 public void registerDrawerListener(DrawerLayout.DrawerListener l) {
4144 registerObserver(l);
4145 }
4146
4147 @Override
4148 public void unregisterDrawerListener(DrawerLayout.DrawerListener l) {
4149 unregisterObserver(l);
4150 }
4151
4152 @Override
4153 public boolean isDrawerOpen() {
4154 return isDrawerEnabled() && mDrawerContainer.isDrawerOpen(mDrawerPullout);
4155 }
4156
4157 @Override
4158 public boolean isDrawerVisible() {
4159 return isDrawerEnabled() && mDrawerContainer.isDrawerVisible(mDrawerPullout);
4160 }
4161
4162 @Override
Andy Huangf58e4c32014-07-09 16:58:18 -07004163 public void toggleDrawerState() {
4164 AbstractActivityController.this.toggleDrawerState();
4165 }
4166
4167 @Override
Andy Huang12b3ee42013-04-24 22:49:43 -07004168 public void onDrawerOpened(View drawerView) {
4169 mDrawerToggle.onDrawerOpened(drawerView);
Andy Huang61f26c22014-03-13 18:24:52 -07004170
4171 for (DrawerLayout.DrawerListener l : mObservers) {
4172 l.onDrawerOpened(drawerView);
4173 }
Andy Huang12b3ee42013-04-24 22:49:43 -07004174 }
4175
4176 @Override
4177 public void onDrawerClosed(View drawerView) {
4178 mDrawerToggle.onDrawerClosed(drawerView);
4179 if (mHasNewAccountOrFolder) {
4180 refreshDrawer();
4181 }
Scott Kennedy10c99ef2013-08-29 13:44:17 -07004182
4183 // When closed, we want to use either the burger, or up, based on where we are
4184 final int mode = mViewMode.getMode();
Jin Cao9695e002014-05-29 11:56:44 -07004185 final boolean isTopLevel = Folder.isRoot(mFolder);
Scott Kennedy10c99ef2013-08-29 13:44:17 -07004186 mDrawerToggle.setDrawerIndicatorEnabled(getShouldShowDrawerIndicator(mode, isTopLevel));
Andy Huang61f26c22014-03-13 18:24:52 -07004187
4188 for (DrawerLayout.DrawerListener l : mObservers) {
4189 l.onDrawerClosed(drawerView);
4190 }
Andy Huang12b3ee42013-04-24 22:49:43 -07004191 }
4192
4193 /**
4194 * As part of the overriden function, it will animate the alpha of the conversation list
4195 * view along with the drawer sliding when we're in the process of switching accounts or
4196 * folders. Note, this is the same amount of work done as {@link ValueAnimator#ofFloat}.
4197 */
4198 @Override
4199 public void onDrawerSlide(View drawerView, float slideOffset) {
4200 mDrawerToggle.onDrawerSlide(drawerView, slideOffset);
4201 if (mHasNewAccountOrFolder && mListViewForAnimating != null) {
4202 mListViewForAnimating.setAlpha(slideOffset);
4203 }
Andrew Sappersteinfc5be1c2013-05-15 17:48:10 -07004204
4205 // This code handles when to change the visibility of action items
4206 // based on drawer state. The basic logic is that right when we
4207 // open the drawer, we hide the action items. We show the action items
4208 // when the drawer closes. However, due to the animation of the drawer closing,
4209 // to make the reshowing of the action items feel right, we make the items visible
4210 // slightly sooner.
4211 //
4212 // However, to make the animating behavior work properly, we have to know whether
4213 // we're animating open or closed. Only if we're animating closed do we want to
4214 // show the action items early. We save the last slide offset so that we can compare
4215 // the current slide offset to it to determine if we're opening or closing.
4216 if (mDrawerState == DrawerLayout.STATE_SETTLING) {
4217 if (mHideMenuItems && slideOffset < 0.15f && mOldSlideOffset > slideOffset) {
4218 mHideMenuItems = false;
Andrew Sapperstein52882ff2014-07-27 12:30:18 -07004219 mActivity.supportInvalidateOptionsMenu();
Tony Mantler43fab322013-07-26 16:38:35 -07004220 maybeEnableCabMode();
Andrew Sappersteinfc5be1c2013-05-15 17:48:10 -07004221 } else if (!mHideMenuItems && slideOffset > 0.f && mOldSlideOffset < slideOffset) {
4222 mHideMenuItems = true;
Andrew Sapperstein52882ff2014-07-27 12:30:18 -07004223 mActivity.supportInvalidateOptionsMenu();
Tony Mantler43fab322013-07-26 16:38:35 -07004224 disableCabMode();
Andrew Sappersteinfc5be1c2013-05-15 17:48:10 -07004225 }
4226 } else {
4227 if (mHideMenuItems && Float.compare(slideOffset, 0.f) == 0) {
4228 mHideMenuItems = false;
Andrew Sapperstein52882ff2014-07-27 12:30:18 -07004229 mActivity.supportInvalidateOptionsMenu();
Tony Mantler43fab322013-07-26 16:38:35 -07004230 maybeEnableCabMode();
Andrew Sappersteinfc5be1c2013-05-15 17:48:10 -07004231 } else if (!mHideMenuItems && slideOffset > 0.f) {
4232 mHideMenuItems = true;
Andrew Sapperstein52882ff2014-07-27 12:30:18 -07004233 mActivity.supportInvalidateOptionsMenu();
Tony Mantler43fab322013-07-26 16:38:35 -07004234 disableCabMode();
Andrew Sappersteinfc5be1c2013-05-15 17:48:10 -07004235 }
4236 }
4237
4238 mOldSlideOffset = slideOffset;
Scott Kennedy10c99ef2013-08-29 13:44:17 -07004239
4240 // If we're sliding, we always want to show the burger
4241 mDrawerToggle.setDrawerIndicatorEnabled(true /* enable */);
Andy Huang61f26c22014-03-13 18:24:52 -07004242
4243 for (DrawerLayout.DrawerListener l : mObservers) {
4244 l.onDrawerSlide(drawerView, slideOffset);
4245 }
Andy Huang12b3ee42013-04-24 22:49:43 -07004246 }
4247
4248 /**
4249 * This condition here should only be called when the drawer is stuck in a weird state
4250 * and doesn't register the onDrawerClosed, but shows up as idle. Make sure to refresh
4251 * and, more importantly, unlock the drawer when this is the case.
4252 */
4253 @Override
4254 public void onDrawerStateChanged(int newState) {
Martin Hibdon371a71c2014-02-19 13:55:28 -08004255 LogUtils.d(LOG_TAG, "AAC onDrawerStateChanged %d", newState);
Andrew Sappersteina3ce6782013-05-09 15:14:49 -07004256 mDrawerState = newState;
4257 mDrawerToggle.onDrawerStateChanged(mDrawerState);
Andy Huang61f26c22014-03-13 18:24:52 -07004258
4259 for (DrawerLayout.DrawerListener l : mObservers) {
4260 l.onDrawerStateChanged(newState);
4261 }
4262
Andy Huange764cfd2014-02-26 11:55:03 -08004263 if (mViewMode.isSearchMode()) {
Martin Hibdon371a71c2014-02-19 13:55:28 -08004264 return;
4265 }
Andrew Sappersteina3ce6782013-05-09 15:14:49 -07004266 if (mDrawerState == DrawerLayout.STATE_IDLE) {
4267 if (mHasNewAccountOrFolder) {
4268 refreshDrawer();
4269 }
4270 if (mConversationListLoadFinishedIgnored) {
4271 mConversationListLoadFinishedIgnored = false;
4272 final Bundle args = new Bundle();
4273 args.putParcelable(BUNDLE_ACCOUNT_KEY, mAccount);
4274 args.putParcelable(BUNDLE_FOLDER_KEY, mFolder);
4275 mActivity.getLoaderManager().initLoader(
4276 LOADER_CONVERSATION_LIST, args, mListCursorCallbacks);
4277 }
Andy Huang12b3ee42013-04-24 22:49:43 -07004278 }
4279 }
4280
4281 /**
4282 * If we've reached a stable drawer state, unlock the drawer for usage, clear the
4283 * conversation list, and finish end actions. Also, make
4284 * {@link #mHasNewAccountOrFolder} false to reflect we're done changing.
4285 */
4286 public void refreshDrawer() {
4287 mHasNewAccountOrFolder = false;
4288 mDrawerContainer.setDrawerLockMode(DrawerLayout.LOCK_MODE_UNLOCKED);
4289 ConversationListFragment conversationList = getConversationListFragment();
4290 if (conversationList != null) {
4291 conversationList.clear();
4292 }
Tony Mantler54022ee2014-07-07 13:43:35 -07004293 mFolderOrAccountObservers.notifyChanged();
Andy Huang12b3ee42013-04-24 22:49:43 -07004294 }
Andrew Sappersteina3ce6782013-05-09 15:14:49 -07004295
4296 /**
4297 * Returns the most recent update of the {@link DrawerLayout}'s state provided
4298 * by {@link #onDrawerStateChanged(int)}.
4299 * @return The {@link DrawerLayout}'s current state. One of
4300 * {@link DrawerLayout#STATE_DRAGGING}, {@link DrawerLayout#STATE_IDLE},
4301 * or {@link DrawerLayout#STATE_SETTLING}.
4302 */
4303 public int getDrawerState() {
4304 return mDrawerState;
4305 }
Andy Huang12b3ee42013-04-24 22:49:43 -07004306 }
4307
Scott Kennedy8a72b852013-05-02 14:18:50 -07004308 @Override
4309 public boolean isDrawerPullEnabled() {
Martin Hibdon371a71c2014-02-19 13:55:28 -08004310 return true;
Scott Kennedy8a72b852013-05-02 14:18:50 -07004311 }
Andrew Sapperstein5747e152013-05-13 14:13:08 -07004312
4313 @Override
4314 public boolean shouldHideMenuItems() {
4315 return mHideMenuItems;
4316 }
Alice Yangebeef1b2013-09-04 06:41:10 +00004317
4318 protected void navigateUpFolderHierarchy() {
4319 new AsyncTask<Void, Void, Folder>() {
4320 @Override
4321 protected Folder doInBackground(final Void... params) {
4322 if (mInbox == null) {
4323 // We don't have an inbox, but we need it
4324 final Cursor cursor = mContext.getContentResolver().query(
4325 mAccount.settings.defaultInbox, UIProvider.FOLDERS_PROJECTION, null,
4326 null, null);
4327
4328 if (cursor != null) {
4329 try {
4330 if (cursor.moveToFirst()) {
4331 mInbox = new Folder(cursor);
4332 }
4333 } finally {
4334 cursor.close();
4335 }
4336 }
4337 }
4338
4339 // Now try to load our parent
4340 final Folder folder;
4341
Alice Yang5ac10032013-09-04 06:41:43 +00004342 if (mFolder != null) {
Martin Hibdone78c40f2013-10-10 18:29:25 -07004343 Cursor cursor = null;
4344 try {
4345 cursor = mContext.getContentResolver().query(mFolder.parent,
4346 UIProvider.FOLDERS_PROJECTION, null, null, null);
Alice Yangebeef1b2013-09-04 06:41:10 +00004347
Martin Hibdone78c40f2013-10-10 18:29:25 -07004348 if (cursor == null || !cursor.moveToFirst()) {
4349 // We couldn't load the parent, so use the inbox
4350 folder = mInbox;
4351 } else {
Alice Yang5ac10032013-09-04 06:41:43 +00004352 folder = new Folder(cursor);
Martin Hibdone78c40f2013-10-10 18:29:25 -07004353 }
4354 } finally {
4355 if (cursor != null) {
Alice Yang5ac10032013-09-04 06:41:43 +00004356 cursor.close();
4357 }
Alice Yangebeef1b2013-09-04 06:41:10 +00004358 }
Alice Yang5ac10032013-09-04 06:41:43 +00004359 } else {
4360 folder = mInbox;
Alice Yangebeef1b2013-09-04 06:41:10 +00004361 }
4362
4363 return folder;
4364 }
4365
4366 @Override
4367 protected void onPostExecute(final Folder result) {
4368 onFolderSelected(result);
4369 }
4370 }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, (Void[]) null);
4371 }
Scott Kennedyf77806e2013-08-30 11:38:15 -07004372
4373 @Override
4374 public Parcelable getConversationListScrollPosition(final String folderUri) {
4375 return mConversationListScrollPositions.getParcelable(folderUri);
4376 }
4377
4378 @Override
4379 public void setConversationListScrollPosition(final String folderUri,
4380 final Parcelable savedPosition) {
4381 mConversationListScrollPositions.putParcelable(folderUri, savedPosition);
4382 }
Andrew Sapperstein53de4482014-07-29 02:39:39 +00004383
4384 @Override
4385 public View.OnClickListener getNavigationViewClickListener() {
4386 return mHomeButtonListener;
4387 }
4388
4389 // TODO: Fold this into the outer class when b/16627877 is fixed
4390 private class HomeButtonListener implements View.OnClickListener {
Andrew Sapperstein53de4482014-07-29 02:39:39 +00004391 @Override
4392 public void onClick(View v) {
Jin Cao405a3442014-08-25 13:49:33 -07004393 handleUpPress();
Andrew Sapperstein53de4482014-07-29 02:39:39 +00004394 }
4395 }
Vikram Aggarwal4a5c5302012-01-12 15:07:13 -08004396}